@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.
- package/package.json +2 -2
- package/src/global.js +70 -10
- package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
- package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
- package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
- package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
- package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
- package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
- package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
- package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
- package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
- package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
- package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
- package/tools/skills/vsaf-build/SKILL.md +32 -2
- package/tools/skills/vsaf-ship/SKILL.md +41 -10
- package/tools/skills/vsaf-test/SKILL.md +8 -0
- package/tools/vds-scripts/.mcp.json +11 -0
- package/tools/vds-scripts/.secrets.baseline +133 -0
- package/tools/vds-scripts/AGENTS.md +152 -0
- package/tools/vds-scripts/CLAUDE.md +101 -0
- package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
- package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
- package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
- package/tools/vds-scripts/README.md +97 -0
- package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
- package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
- package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
- package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
- package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
- package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
- package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
- package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
- package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
- package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
- package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
- package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
- package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
- package/tools/vds-scripts/git_orchestrator/README.md +280 -0
- package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
- package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
- package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
- package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
- package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
- package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
- package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
- package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
- package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
- package/tools/vds-scripts/mcp_server/README.md +140 -0
- package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
- package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
- package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
- package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
- package/tools/vds-scripts/mypy.ini +5 -0
- package/tools/vds-scripts/pyproject.toml +29 -0
- package/tools/vds-scripts/repo-manifest.yaml +273 -0
- package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
- package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
- package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
- package/tools/vds-scripts/scripts/README.md +162 -0
- package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
- package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
- package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
- package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
- package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
- package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
- package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
- package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
- package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
- package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
- package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
- package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
- package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
- package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
- package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
- package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
- package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
- package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
- package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
- package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
- package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
- package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
- package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
- package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
- package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
- package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
- package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
- package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
- package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
- package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
- package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
- package/tools/vds-scripts/uv.lock +8 -0
- package/tools/vds-scripts/vds_cli/README.md +126 -0
- package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
- package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
- package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
- package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
- package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
- package/tools/vds-scripts/vds_cli/uv.lock +524 -0
- package/tools/vds-scripts/vds_cli_common/README.md +190 -0
- package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
- package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
|
@@ -0,0 +1,2532 @@
|
|
|
1
|
+
"""Typer CLI wrapper for Confluence operations during the native Python implementations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import structlog
|
|
15
|
+
import typer
|
|
16
|
+
|
|
17
|
+
from .config import ConfluenceSettings, load_settings
|
|
18
|
+
from .content import ContentClient
|
|
19
|
+
from .content_v2 import ContentClientV2
|
|
20
|
+
from .errors import ConfluenceClientError
|
|
21
|
+
from .eventing import Poller
|
|
22
|
+
from .http import ConfluenceClient
|
|
23
|
+
from .orchestration import BatchRunner, WebhookClient
|
|
24
|
+
from .reporting import RunSummary, write_reports
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Typer setup & logging
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
app = typer.Typer(add_completion=False, help="Confluence operations orchestrator")
|
|
31
|
+
content_app = typer.Typer(help="Native Confluence API commands")
|
|
32
|
+
batch_app = typer.Typer(help="Batch polling utilities (REST v1)")
|
|
33
|
+
webhook_app = typer.Typer(help="Webhook management (REST v1)")
|
|
34
|
+
app.add_typer(content_app, name="content")
|
|
35
|
+
app.add_typer(batch_app, name="batch-native")
|
|
36
|
+
app.add_typer(webhook_app, name="webhook-native")
|
|
37
|
+
|
|
38
|
+
# Logger will be configured in main() callback
|
|
39
|
+
logger = logging.getLogger("confluence_cli")
|
|
40
|
+
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
# Paths and legacy script mapping
|
|
43
|
+
# ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parents[3]
|
|
46
|
+
_DEFAULT_REPORT_DIR = (_SCRIPTS_DIR / "reports" / "confluence_runs").resolve()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(slots=True)
|
|
50
|
+
class CLIContext:
|
|
51
|
+
report_dir: Path
|
|
52
|
+
markdown: bool
|
|
53
|
+
api_version: str
|
|
54
|
+
reports: bool
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# Helper functions
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _normalise_server(server: str | None, settings: ConfluenceSettings) -> str:
|
|
63
|
+
return server.lower() if server else settings.default_server.lower()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_env(server: str, settings: ConfluenceSettings) -> dict:
|
|
67
|
+
token = settings.token_for(server)
|
|
68
|
+
if not token:
|
|
69
|
+
raise ConfluenceClientError(
|
|
70
|
+
f"Token for server '{server}' is missing. Run 'uv run --project vds_cli vds-cli env load'.",
|
|
71
|
+
context={"server": server},
|
|
72
|
+
)
|
|
73
|
+
env = os.environ.copy()
|
|
74
|
+
env.update(
|
|
75
|
+
{
|
|
76
|
+
"CONFLUENCE_SERVER": server,
|
|
77
|
+
"CONFLUENCE_TOKEN": token,
|
|
78
|
+
"CONFLUENCE_URL": str(settings.url_for(server)),
|
|
79
|
+
"INTERNAL_CONFLUENCE_TOKEN": settings.internal_token or "",
|
|
80
|
+
"EXTERNAL_CONFLUENCE_TOKEN": settings.external_token or "",
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
return env
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _build_http_client(settings: ConfluenceSettings, server: str) -> ConfluenceClient:
|
|
87
|
+
return ConfluenceClient(settings, server=server)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _get_cli_ctx(ctx: typer.Context) -> CLIContext:
|
|
91
|
+
cli_ctx = ctx.obj
|
|
92
|
+
if not isinstance(cli_ctx, CLIContext):
|
|
93
|
+
raise RuntimeError("CLI context not initialised")
|
|
94
|
+
return cli_ctx
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _ensure_v1_api(ctx: typer.Context, command: str) -> None:
|
|
98
|
+
"""Legacy command guard - redirects to native implementations."""
|
|
99
|
+
settings = load_settings(strict=False)
|
|
100
|
+
if settings is None:
|
|
101
|
+
typer.echo("Error: configuration not found", err=True)
|
|
102
|
+
raise typer.Exit(code=1) from None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _build_content_client(
|
|
106
|
+
settings: ConfluenceSettings,
|
|
107
|
+
server: str,
|
|
108
|
+
api_version: str,
|
|
109
|
+
):
|
|
110
|
+
http_client = _build_http_client(settings, server)
|
|
111
|
+
if not http_client.supports_api_version(api_version):
|
|
112
|
+
raise ConfluenceClientError(
|
|
113
|
+
f"API version '{api_version}' is not supported for server '{server}'.",
|
|
114
|
+
context={"server": server, "api_version": api_version},
|
|
115
|
+
)
|
|
116
|
+
if api_version == "v1":
|
|
117
|
+
return ContentClient(http_client)
|
|
118
|
+
if api_version == "v2":
|
|
119
|
+
return ContentClientV2(http_client)
|
|
120
|
+
raise ConfluenceClientError(f"Unsupported API version '{api_version}'")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _record_summary(
|
|
124
|
+
ctx: typer.Context,
|
|
125
|
+
*,
|
|
126
|
+
command: str,
|
|
127
|
+
args: list[str],
|
|
128
|
+
server: str,
|
|
129
|
+
exit_code: int,
|
|
130
|
+
started_at: datetime,
|
|
131
|
+
finished_at: datetime,
|
|
132
|
+
) -> None:
|
|
133
|
+
cli_ctx = _get_cli_ctx(ctx)
|
|
134
|
+
summary = RunSummary(
|
|
135
|
+
command=command,
|
|
136
|
+
args=args,
|
|
137
|
+
server=server,
|
|
138
|
+
exit_code=exit_code,
|
|
139
|
+
duration_ms=int((finished_at - started_at).total_seconds() * 1000),
|
|
140
|
+
started_at=started_at,
|
|
141
|
+
finished_at=finished_at,
|
|
142
|
+
)
|
|
143
|
+
if cli_ctx.reports:
|
|
144
|
+
write_reports(summary, base_dir=cli_ctx.report_dir, include_markdown=cli_ctx.markdown)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _emit_json(payload: object) -> None:
|
|
148
|
+
text = json.dumps(payload, indent=2, sort_keys=True)
|
|
149
|
+
if sys.stdout.isatty():
|
|
150
|
+
typer.echo(text)
|
|
151
|
+
else:
|
|
152
|
+
sys.stdout.write(text + "\n")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _expand_option(value: str | None) -> list[str] | None:
|
|
156
|
+
if not value:
|
|
157
|
+
return None
|
|
158
|
+
return [part.strip() for part in value.split(",") if part.strip()]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _content_summary_args(
|
|
162
|
+
*, cql: str | None = None, limit: int | None = None, start: int | None = None, space: str | None = None
|
|
163
|
+
) -> list[str]:
|
|
164
|
+
parts: list[str] = []
|
|
165
|
+
if cql is not None:
|
|
166
|
+
parts.append(cql)
|
|
167
|
+
if limit is not None:
|
|
168
|
+
parts.append(f"limit={limit}")
|
|
169
|
+
if start is not None:
|
|
170
|
+
parts.append(f"start={start}")
|
|
171
|
+
if space is not None:
|
|
172
|
+
parts.append(f"space={space}")
|
|
173
|
+
return parts
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _read_text_file(path: Path) -> str:
|
|
177
|
+
if not path.exists():
|
|
178
|
+
raise typer.BadParameter(f"File not found: {path}")
|
|
179
|
+
if path.is_dir():
|
|
180
|
+
raise typer.BadParameter(f"Expected file but found directory: {path}")
|
|
181
|
+
try:
|
|
182
|
+
return path.read_text(encoding="utf-8")
|
|
183
|
+
except OSError as exc:
|
|
184
|
+
raise typer.BadParameter(f"Unable to read file {path}: {exc}") from exc
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _parse_json_option(value: str | None, option_name: str) -> dict[str, Any] | None:
|
|
188
|
+
if value is None:
|
|
189
|
+
return None
|
|
190
|
+
try:
|
|
191
|
+
parsed = json.loads(value)
|
|
192
|
+
except json.JSONDecodeError as exc:
|
|
193
|
+
raise typer.BadParameter(f"Invalid JSON for {option_name}: {exc}") from exc
|
|
194
|
+
if not isinstance(parsed, dict):
|
|
195
|
+
raise typer.BadParameter(f"{option_name} must decode to a JSON object")
|
|
196
|
+
return parsed
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _parent_context(ctx: typer.Context) -> typer.Context:
|
|
200
|
+
return ctx.parent or ctx
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ---------------------------------------------------------------------------
|
|
204
|
+
# Typer callbacks & native commands
|
|
205
|
+
# ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@app.callback()
|
|
209
|
+
def main(
|
|
210
|
+
ctx: typer.Context,
|
|
211
|
+
report_dir: Path | None = typer.Option(None, help=f"Directory for run reports (default: {_DEFAULT_REPORT_DIR})"),
|
|
212
|
+
markdown: bool = typer.Option(True, help="Emit Markdown summaries alongside JSON reports"),
|
|
213
|
+
reports: bool = typer.Option(True, "--reports/--no-reports", help="Persist run reports to disk"),
|
|
214
|
+
json_only: bool = typer.Option(
|
|
215
|
+
False,
|
|
216
|
+
"--json-only",
|
|
217
|
+
help="Emit JSON to stdout only (disables reports and markdown)",
|
|
218
|
+
),
|
|
219
|
+
api_version: str = typer.Option("v1", "--api-version", "-A", help="Select REST API version (v1 or v2)"),
|
|
220
|
+
structured_logs: bool = typer.Option(
|
|
221
|
+
False, "--structured-logs/--no-structured-logs", help="Emit structured JSON logs to stderr"
|
|
222
|
+
),
|
|
223
|
+
) -> None:
|
|
224
|
+
resolved = report_dir.resolve() if report_dir else _DEFAULT_REPORT_DIR
|
|
225
|
+
version = api_version.lower()
|
|
226
|
+
if version not in {"v1", "v2"}:
|
|
227
|
+
raise typer.BadParameter("api-version must be one of: v1, v2")
|
|
228
|
+
if json_only:
|
|
229
|
+
markdown = False
|
|
230
|
+
reports = False
|
|
231
|
+
|
|
232
|
+
# Configure logging/structlog
|
|
233
|
+
if structured_logs:
|
|
234
|
+
structlog.configure(
|
|
235
|
+
processors=[
|
|
236
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
237
|
+
structlog.stdlib.add_log_level,
|
|
238
|
+
structlog.processors.StackInfoRenderer(),
|
|
239
|
+
structlog.processors.format_exc_info,
|
|
240
|
+
structlog.processors.JSONRenderer(),
|
|
241
|
+
],
|
|
242
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
243
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
244
|
+
cache_logger_on_first_use=True,
|
|
245
|
+
)
|
|
246
|
+
global logger # noqa: PLW0603
|
|
247
|
+
logger = structlog.get_logger("confluence_cli") # type: ignore[assignment]
|
|
248
|
+
else:
|
|
249
|
+
if json_only:
|
|
250
|
+
logging.basicConfig(level=logging.CRITICAL, stream=sys.stderr)
|
|
251
|
+
structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL))
|
|
252
|
+
else:
|
|
253
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format="%(levelname)s %(name)s: %(message)s")
|
|
254
|
+
|
|
255
|
+
ctx.obj = CLIContext(report_dir=resolved, markdown=markdown, api_version=version, reports=reports)
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
@app.command("templates")
|
|
259
|
+
def cmd_templates(
|
|
260
|
+
ctx: typer.Context,
|
|
261
|
+
args: list[str] | None = typer.Argument(
|
|
262
|
+
None, help="Additional arguments for template operations", show_default=False, metavar="ARGS"
|
|
263
|
+
),
|
|
264
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
265
|
+
limit: int = typer.Option(20, "--limit", "-l", help="Number of templates to list"),
|
|
266
|
+
space_key: str | None = typer.Option(None, "--space", "-k", help="Space key filter"),
|
|
267
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
268
|
+
) -> None:
|
|
269
|
+
"""Native Python template management"""
|
|
270
|
+
parent_ctx = _parent_context(ctx)
|
|
271
|
+
started_at = datetime.now(UTC)
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
settings = load_settings(strict=True)
|
|
275
|
+
server_key = _normalise_server(server, settings)
|
|
276
|
+
client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
|
|
277
|
+
|
|
278
|
+
# Use native Python implementation
|
|
279
|
+
if hasattr(client, "list_templates"):
|
|
280
|
+
result = client.list_templates(space_key=space_key)
|
|
281
|
+
else:
|
|
282
|
+
result = {"templates": [], "message": "Templates API not available for this server"}
|
|
283
|
+
|
|
284
|
+
if json_output:
|
|
285
|
+
_emit_json(result)
|
|
286
|
+
else:
|
|
287
|
+
templates_payload = []
|
|
288
|
+
if isinstance(result, dict):
|
|
289
|
+
templates_payload = (
|
|
290
|
+
result.get("blueprints")
|
|
291
|
+
or result.get("templates")
|
|
292
|
+
or result.get("results")
|
|
293
|
+
or result.get("contentTemplates")
|
|
294
|
+
or []
|
|
295
|
+
)
|
|
296
|
+
elif isinstance(result, list):
|
|
297
|
+
templates_payload = result
|
|
298
|
+
|
|
299
|
+
count = len(templates_payload)
|
|
300
|
+
typer.echo(f"Found {count} templates:")
|
|
301
|
+
for template in templates_payload[:limit]:
|
|
302
|
+
if isinstance(template, dict):
|
|
303
|
+
name = template.get("name", "Unknown")
|
|
304
|
+
template_id = template.get("templateId") or template.get("id") or template.get("uuid") or "Unknown"
|
|
305
|
+
else:
|
|
306
|
+
name = str(template)
|
|
307
|
+
template_id = "Unknown"
|
|
308
|
+
typer.echo(f" - {name} (ID: {template_id})")
|
|
309
|
+
|
|
310
|
+
exit_code = 0
|
|
311
|
+
finished_at = datetime.now(UTC)
|
|
312
|
+
_record_summary(
|
|
313
|
+
parent_ctx,
|
|
314
|
+
command="templates",
|
|
315
|
+
args=args or [],
|
|
316
|
+
server=server_key,
|
|
317
|
+
exit_code=exit_code,
|
|
318
|
+
started_at=started_at,
|
|
319
|
+
finished_at=finished_at,
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
if exit_code != 0:
|
|
323
|
+
raise typer.Exit(code=exit_code) from None
|
|
324
|
+
|
|
325
|
+
except Exception as exc:
|
|
326
|
+
logger.error("templates_command_failed: %s", exc)
|
|
327
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
328
|
+
raise typer.Exit(code=1) from None
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@app.command("batch")
|
|
332
|
+
def cmd_batch(
|
|
333
|
+
ctx: typer.Context,
|
|
334
|
+
args: list[str] | None = typer.Argument(
|
|
335
|
+
None, help="Use 'confluence content batch --help' for available subcommands", show_default=False, metavar="ARGS"
|
|
336
|
+
),
|
|
337
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
338
|
+
) -> None:
|
|
339
|
+
"""Legacy batch command - use 'confluence content batch' subcommands instead."""
|
|
340
|
+
typer.echo("Error: The 'batch' command is deprecated. Use 'confluence content batch' subcommands:", err=True)
|
|
341
|
+
typer.echo(" confluence content batch scan --cql '<query>' [options]", err=True)
|
|
342
|
+
typer.echo(" confluence content batch snapshot --cql '<query>' [options]", err=True)
|
|
343
|
+
raise typer.Exit(code=1) from None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
@app.command("webhooks")
|
|
347
|
+
def cmd_webhooks(
|
|
348
|
+
ctx: typer.Context,
|
|
349
|
+
args: list[str] | None = typer.Argument(
|
|
350
|
+
None, help="Additional arguments for webhook operations", show_default=False, metavar="ARGS"
|
|
351
|
+
),
|
|
352
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
353
|
+
list_webhooks: bool = typer.Option(False, "--list", "-l", help="List existing webhooks"),
|
|
354
|
+
create_name: str | None = typer.Option(None, "--name", "-n", help="Webhook name"),
|
|
355
|
+
create_url: str | None = typer.Option(None, "--url", "-u", help="Webhook URL"),
|
|
356
|
+
create_event: str | None = typer.Option(None, "--event", "-e", help="Event type"),
|
|
357
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
358
|
+
) -> None:
|
|
359
|
+
"""Native Python webhook management"""
|
|
360
|
+
parent_ctx = _parent_context(ctx)
|
|
361
|
+
started_at = datetime.now(UTC)
|
|
362
|
+
|
|
363
|
+
try:
|
|
364
|
+
settings = load_settings(strict=True)
|
|
365
|
+
server_key = _normalise_server(server, settings)
|
|
366
|
+
webhook_client = WebhookClient(_build_http_client(settings, server_key))
|
|
367
|
+
|
|
368
|
+
if list_webhooks or (not create_name and not create_url and not create_event):
|
|
369
|
+
# List webhooks
|
|
370
|
+
result = webhook_client.list()
|
|
371
|
+
if json_output or not sys.stdout.isatty():
|
|
372
|
+
_emit_json(result)
|
|
373
|
+
else:
|
|
374
|
+
webhooks = result.get("webhooks", [])
|
|
375
|
+
count = len(webhooks)
|
|
376
|
+
typer.echo(f"Found {count} webhooks:")
|
|
377
|
+
for webhook in webhooks:
|
|
378
|
+
name = webhook.get("name", "Unknown")
|
|
379
|
+
url = webhook.get("url", "Unknown")
|
|
380
|
+
events = webhook.get("events", [])
|
|
381
|
+
typer.echo(f" - {name}: {url} (events: {', '.join(events)})")
|
|
382
|
+
|
|
383
|
+
elif create_name and create_url and create_event:
|
|
384
|
+
# Create webhook
|
|
385
|
+
result = webhook_client.create(name=create_name, url=create_url, events=[create_event])
|
|
386
|
+
if json_output or not sys.stdout.isatty():
|
|
387
|
+
_emit_json(result)
|
|
388
|
+
else:
|
|
389
|
+
webhook_id = result.get("id", "Unknown")
|
|
390
|
+
typer.echo(f"Created webhook '{create_name}' with ID: {webhook_id}")
|
|
391
|
+
|
|
392
|
+
else:
|
|
393
|
+
typer.echo(
|
|
394
|
+
"Error: Either use --list to list webhooks or provide --name, --url, and --event to create", err=True
|
|
395
|
+
)
|
|
396
|
+
raise typer.Exit(code=1) from None
|
|
397
|
+
|
|
398
|
+
exit_code = 0
|
|
399
|
+
finished_at = datetime.now(UTC)
|
|
400
|
+
_record_summary(
|
|
401
|
+
parent_ctx,
|
|
402
|
+
command="webhooks",
|
|
403
|
+
args=args or [],
|
|
404
|
+
server=server_key,
|
|
405
|
+
exit_code=exit_code,
|
|
406
|
+
started_at=started_at,
|
|
407
|
+
finished_at=finished_at,
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
if exit_code != 0:
|
|
411
|
+
raise typer.Exit(code=exit_code) from None
|
|
412
|
+
|
|
413
|
+
except Exception as exc:
|
|
414
|
+
logger.error("webhooks_command_failed: %s", exc)
|
|
415
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
416
|
+
raise typer.Exit(code=1) from None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
@app.command("search")
|
|
420
|
+
def cmd_search(
|
|
421
|
+
ctx: typer.Context,
|
|
422
|
+
args: list[str] | None = typer.Argument(
|
|
423
|
+
None, help="Additional arguments for CQL search", show_default=False, metavar="ARGS"
|
|
424
|
+
),
|
|
425
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
426
|
+
cql: str = typer.Option(..., "--cql", "-q", help="CQL query string"),
|
|
427
|
+
limit: int = typer.Option(25, "--limit", "-l", help="Number of results"),
|
|
428
|
+
start: int = typer.Option(0, "--start", help="Pagination start"),
|
|
429
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
|
|
430
|
+
excerpt: str | None = typer.Option(None, "--excerpt", help="Excerpt strategy (e.g., 'highlighted', 'indexed')"),
|
|
431
|
+
advanced: bool = typer.Option(False, "--advanced", help="Use advanced CQL search with additional options"),
|
|
432
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
433
|
+
) -> None:
|
|
434
|
+
"""Native Python CQL search. Use --advanced for additional options like excerpt."""
|
|
435
|
+
parent_ctx = _parent_context(ctx)
|
|
436
|
+
started_at = datetime.now(UTC)
|
|
437
|
+
|
|
438
|
+
try:
|
|
439
|
+
settings = load_settings(strict=True)
|
|
440
|
+
server_key = _normalise_server(server, settings)
|
|
441
|
+
client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
|
|
442
|
+
|
|
443
|
+
# Use native Python implementation
|
|
444
|
+
expand_list = _expand_option(expand) if expand else None
|
|
445
|
+
if advanced or excerpt:
|
|
446
|
+
result = client.cql_advanced(cql, limit=limit, start=start, expand=expand_list, excerpt=excerpt)
|
|
447
|
+
else:
|
|
448
|
+
result = client.search_cql(cql, limit=limit, start=start, expand=expand_list)
|
|
449
|
+
|
|
450
|
+
if json_output or not sys.stdout.isatty():
|
|
451
|
+
_emit_json(result)
|
|
452
|
+
else:
|
|
453
|
+
results = result.get("results", [])
|
|
454
|
+
count = len(results)
|
|
455
|
+
total = result.get("size", 0)
|
|
456
|
+
typer.echo(f"Found {count} of {total} results for CQL: {cql}")
|
|
457
|
+
for item in results:
|
|
458
|
+
title = item.get("title", "Unknown")
|
|
459
|
+
page_id = item.get("id", "Unknown")
|
|
460
|
+
space = item.get("space", {}).get("key", "Unknown")
|
|
461
|
+
typer.echo(f" - {title} (ID: {page_id}, Space: {space})")
|
|
462
|
+
|
|
463
|
+
exit_code = 0
|
|
464
|
+
finished_at = datetime.now(UTC)
|
|
465
|
+
_record_summary(
|
|
466
|
+
parent_ctx,
|
|
467
|
+
command="search",
|
|
468
|
+
args=args or [],
|
|
469
|
+
server=server_key,
|
|
470
|
+
exit_code=exit_code,
|
|
471
|
+
started_at=started_at,
|
|
472
|
+
finished_at=finished_at,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
if exit_code != 0:
|
|
476
|
+
raise typer.Exit(code=exit_code) from None
|
|
477
|
+
|
|
478
|
+
except Exception as exc:
|
|
479
|
+
logger.error("search_command_failed: %s", exc)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@app.command("search-by-space-type")
|
|
483
|
+
def cmd_search_by_space_type(
|
|
484
|
+
ctx: typer.Context,
|
|
485
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
486
|
+
space_key: str | None = typer.Option(None, "--space", help="Space key to filter by"),
|
|
487
|
+
content_type: str | None = typer.Option(
|
|
488
|
+
None, "--type", help="Content type (e.g., 'page', 'blogpost', 'comment')"
|
|
489
|
+
),
|
|
490
|
+
limit: int = typer.Option(25, "--limit", "-l", help="Number of results"),
|
|
491
|
+
start: int = typer.Option(0, "--start", help="Pagination start"),
|
|
492
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
|
|
493
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
494
|
+
) -> None:
|
|
495
|
+
"""Search by space and/or content type (helper that builds CQL automatically)."""
|
|
496
|
+
parent_ctx = _parent_context(ctx)
|
|
497
|
+
started_at = datetime.now(UTC)
|
|
498
|
+
exit_code = 0
|
|
499
|
+
|
|
500
|
+
try:
|
|
501
|
+
if not space_key and not content_type:
|
|
502
|
+
raise typer.BadParameter("At least one of --space or --type must be provided")
|
|
503
|
+
|
|
504
|
+
settings = load_settings(strict=True)
|
|
505
|
+
server_key = _normalise_server(server, settings)
|
|
506
|
+
client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
|
|
507
|
+
|
|
508
|
+
expand_list = _expand_option(expand) if expand else None
|
|
509
|
+
result = client.search_by_space_and_type(
|
|
510
|
+
space_key=space_key,
|
|
511
|
+
content_type=content_type,
|
|
512
|
+
limit=limit,
|
|
513
|
+
start=start,
|
|
514
|
+
expand=expand_list,
|
|
515
|
+
)
|
|
516
|
+
|
|
517
|
+
if json_output or not sys.stdout.isatty():
|
|
518
|
+
_emit_json(result)
|
|
519
|
+
else:
|
|
520
|
+
results = result.get("results", [])
|
|
521
|
+
count = len(results)
|
|
522
|
+
total = result.get("size", 0)
|
|
523
|
+
typer.echo(f"Found {count} of {total} results")
|
|
524
|
+
for item in results:
|
|
525
|
+
title = item.get("title", "Unknown")
|
|
526
|
+
page_id = item.get("id", "Unknown")
|
|
527
|
+
space = item.get("space", {}).get("key", "Unknown")
|
|
528
|
+
item_type = item.get("type", "Unknown")
|
|
529
|
+
typer.echo(f" - {title} (ID: {page_id}, Space: {space}, Type: {item_type})")
|
|
530
|
+
|
|
531
|
+
exit_code = 0
|
|
532
|
+
except (typer.BadParameter, Exception) as exc:
|
|
533
|
+
exit_code = 1
|
|
534
|
+
logger.error("search_by_space_type_failed: %s", exc)
|
|
535
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
536
|
+
finally:
|
|
537
|
+
finished_at = datetime.now(UTC)
|
|
538
|
+
args_list: list[str] = []
|
|
539
|
+
if space_key:
|
|
540
|
+
args_list.append(f"space={space_key}")
|
|
541
|
+
if content_type:
|
|
542
|
+
args_list.append(f"type={content_type}")
|
|
543
|
+
_record_summary(
|
|
544
|
+
parent_ctx,
|
|
545
|
+
command="search-by-space-type",
|
|
546
|
+
args=args_list,
|
|
547
|
+
server=server_key if "server_key" in locals() else "internal",
|
|
548
|
+
exit_code=exit_code,
|
|
549
|
+
started_at=started_at,
|
|
550
|
+
finished_at=finished_at,
|
|
551
|
+
)
|
|
552
|
+
if exit_code != 0:
|
|
553
|
+
raise typer.Exit(code=exit_code) from None
|
|
554
|
+
|
|
555
|
+
|
|
556
|
+
@app.command("attachments")
|
|
557
|
+
def cmd_attachments(
|
|
558
|
+
ctx: typer.Context,
|
|
559
|
+
args: list[str] | None = typer.Argument(
|
|
560
|
+
None, help="Additional arguments for attachment operations", show_default=False, metavar="ARGS"
|
|
561
|
+
),
|
|
562
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
563
|
+
page_id: str | None = typer.Option(None, "--page", "-p", help="Page ID for attachments"),
|
|
564
|
+
list_attachments: bool = typer.Option(False, "--list", "-l", help="List attachments for page"),
|
|
565
|
+
upload_file: str | None = typer.Option(None, "--upload", "-f", help="File to upload"),
|
|
566
|
+
comment: str | None = typer.Option(None, "--comment", "-c", help="Upload comment"),
|
|
567
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
568
|
+
) -> None:
|
|
569
|
+
"""Native Python attachment management"""
|
|
570
|
+
parent_ctx = _parent_context(ctx)
|
|
571
|
+
started_at = datetime.now(UTC)
|
|
572
|
+
|
|
573
|
+
try:
|
|
574
|
+
settings = load_settings(strict=True)
|
|
575
|
+
server_key = _normalise_server(server, settings)
|
|
576
|
+
client = _build_content_client(settings, server_key, parent_ctx.obj.api_version)
|
|
577
|
+
|
|
578
|
+
if list_attachments or (page_id and not upload_file):
|
|
579
|
+
# List attachments
|
|
580
|
+
if not page_id:
|
|
581
|
+
typer.echo("Error: --page ID required for listing attachments", err=True)
|
|
582
|
+
raise typer.Exit(code=1) from None
|
|
583
|
+
|
|
584
|
+
result = client.list_attachments(page_id)
|
|
585
|
+
if json_output or not sys.stdout.isatty():
|
|
586
|
+
_emit_json(result)
|
|
587
|
+
else:
|
|
588
|
+
attachments = result.get("results", [])
|
|
589
|
+
count = len(attachments)
|
|
590
|
+
typer.echo(f"Found {count} attachments for page {page_id}:")
|
|
591
|
+
for attachment in attachments:
|
|
592
|
+
name = attachment.get("title", "Unknown")
|
|
593
|
+
size = attachment.get("size", 0)
|
|
594
|
+
typer.echo(f" - {name} ({size} bytes)")
|
|
595
|
+
|
|
596
|
+
elif upload_file and page_id:
|
|
597
|
+
# Upload attachment
|
|
598
|
+
file_path = Path(upload_file)
|
|
599
|
+
if not file_path.exists():
|
|
600
|
+
typer.echo(f"Error: File not found: {upload_file}", err=True)
|
|
601
|
+
raise typer.Exit(code=1) from None
|
|
602
|
+
|
|
603
|
+
result = client.upload_attachment(page_id, file_path, comment=comment)
|
|
604
|
+
if json_output or not sys.stdout.isatty():
|
|
605
|
+
_emit_json(result)
|
|
606
|
+
else:
|
|
607
|
+
attachment_id = result.get("id", "Unknown")
|
|
608
|
+
typer.echo(f"Uploaded attachment '{file_path.name}' with ID: {attachment_id}")
|
|
609
|
+
|
|
610
|
+
else:
|
|
611
|
+
typer.echo("Error: Either use --list with --page or provide --page and --upload", err=True)
|
|
612
|
+
raise typer.Exit(code=1) from None
|
|
613
|
+
|
|
614
|
+
exit_code = 0
|
|
615
|
+
finished_at = datetime.now(UTC)
|
|
616
|
+
_record_summary(
|
|
617
|
+
parent_ctx,
|
|
618
|
+
command="attachments",
|
|
619
|
+
args=args or [],
|
|
620
|
+
server=server_key,
|
|
621
|
+
exit_code=exit_code,
|
|
622
|
+
started_at=started_at,
|
|
623
|
+
finished_at=finished_at,
|
|
624
|
+
)
|
|
625
|
+
|
|
626
|
+
if exit_code != 0:
|
|
627
|
+
raise typer.Exit(code=exit_code) from None
|
|
628
|
+
|
|
629
|
+
except Exception as exc:
|
|
630
|
+
logger.error("attachments_command_failed: %s", exc)
|
|
631
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
632
|
+
raise typer.Exit(code=1) from None
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
@app.command("space-permissions")
|
|
636
|
+
def cmd_space_permissions(
|
|
637
|
+
ctx: typer.Context,
|
|
638
|
+
action: str = typer.Argument(..., help="Action: get, set, remove"),
|
|
639
|
+
space_key: str = typer.Option(..., "--space-key", "-k", help="Confluence space key"),
|
|
640
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
641
|
+
user_key: str | None = typer.Option(None, "--user", "-u", help="User key for user-targeted operations"),
|
|
642
|
+
group_name: str | None = typer.Option(None, "--group", "-g", help="Group name for group-targeted operations"),
|
|
643
|
+
anonymous: bool = typer.Option(False, "--anonymous", help="Target anonymous permissions"),
|
|
644
|
+
operations: list[str] = typer.Option(
|
|
645
|
+
[],
|
|
646
|
+
"--operation",
|
|
647
|
+
"-o",
|
|
648
|
+
help="Space permission operation key (repeatable, e.g., administer, read)",
|
|
649
|
+
),
|
|
650
|
+
permissions_file: Path | None = typer.Option(
|
|
651
|
+
None,
|
|
652
|
+
"--permissions-file",
|
|
653
|
+
help="JSON file with bulk permission definitions (uses set_permissions_to_multiple_items_for_space)",
|
|
654
|
+
),
|
|
655
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operations"),
|
|
656
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
657
|
+
) -> None:
|
|
658
|
+
"""Manage Confluence space permissions."""
|
|
659
|
+
|
|
660
|
+
parent_ctx = _parent_context(ctx)
|
|
661
|
+
started_at = datetime.now(UTC)
|
|
662
|
+
exit_code = 0
|
|
663
|
+
result_payload: dict[str, Any] = {}
|
|
664
|
+
args_summary: list[str] = []
|
|
665
|
+
server_key: str | None = None
|
|
666
|
+
settings: ConfluenceSettings | None = None
|
|
667
|
+
|
|
668
|
+
try:
|
|
669
|
+
settings = load_settings(strict=True)
|
|
670
|
+
server_key = _normalise_server(server, settings)
|
|
671
|
+
client = _build_http_client(settings, server_key)
|
|
672
|
+
|
|
673
|
+
resolved_operations = operations or None
|
|
674
|
+
args_summary = [
|
|
675
|
+
action,
|
|
676
|
+
f"space={space_key}",
|
|
677
|
+
f"user={user_key}",
|
|
678
|
+
f"group={group_name}",
|
|
679
|
+
f"anonymous={anonymous}",
|
|
680
|
+
f"operations={operations}",
|
|
681
|
+
f"permissions_file={permissions_file}",
|
|
682
|
+
]
|
|
683
|
+
|
|
684
|
+
if action == "get":
|
|
685
|
+
result = client.get_space_permissions(space_key)
|
|
686
|
+
result_payload = {"space": space_key, "permissions": result}
|
|
687
|
+
|
|
688
|
+
elif action == "set":
|
|
689
|
+
if not yes:
|
|
690
|
+
raise typer.BadParameter("--yes required for write operations")
|
|
691
|
+
|
|
692
|
+
targets = [bool(user_key), bool(group_name), anonymous, bool(permissions_file)]
|
|
693
|
+
if sum(1 for flag in targets if flag) != 1:
|
|
694
|
+
raise typer.BadParameter(
|
|
695
|
+
"Provide exactly one of --user, --group, --anonymous, or --permissions-file for set action"
|
|
696
|
+
)
|
|
697
|
+
|
|
698
|
+
if permissions_file:
|
|
699
|
+
file_content = _read_text_file(permissions_file)
|
|
700
|
+
try:
|
|
701
|
+
parsed = json.loads(file_content)
|
|
702
|
+
except json.JSONDecodeError as exc:
|
|
703
|
+
raise typer.BadParameter(f"Invalid JSON in permissions file: {exc}") from exc
|
|
704
|
+
|
|
705
|
+
if isinstance(parsed, dict):
|
|
706
|
+
items: list[dict[str, Any]] = [parsed]
|
|
707
|
+
elif isinstance(parsed, list):
|
|
708
|
+
if not all(isinstance(item, dict) for item in parsed):
|
|
709
|
+
raise typer.BadParameter("--permissions-file list entries must be JSON objects")
|
|
710
|
+
items = parsed # type: ignore[assignment]
|
|
711
|
+
else:
|
|
712
|
+
raise typer.BadParameter("--permissions-file must contain a JSON object or array")
|
|
713
|
+
|
|
714
|
+
result = client.set_permissions_to_multiple_items_for_space(space_key, items)
|
|
715
|
+
result_payload = {"space": space_key, "bulk": True, "items_processed": len(items), "result": result}
|
|
716
|
+
|
|
717
|
+
elif user_key:
|
|
718
|
+
if not resolved_operations:
|
|
719
|
+
raise typer.BadParameter("At least one --operation required when setting user permissions")
|
|
720
|
+
result = client.set_permissions_to_user_for_space(
|
|
721
|
+
space_key,
|
|
722
|
+
user_key,
|
|
723
|
+
operations=resolved_operations,
|
|
724
|
+
)
|
|
725
|
+
result_payload = {
|
|
726
|
+
"space": space_key,
|
|
727
|
+
"target": {"type": "user", "key": user_key},
|
|
728
|
+
"operations": resolved_operations,
|
|
729
|
+
"result": result,
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
elif group_name:
|
|
733
|
+
if not resolved_operations:
|
|
734
|
+
raise typer.BadParameter("At least one --operation required when setting group permissions")
|
|
735
|
+
result = client.set_permissions_to_group_for_space(
|
|
736
|
+
space_key,
|
|
737
|
+
group_name,
|
|
738
|
+
operations=resolved_operations,
|
|
739
|
+
)
|
|
740
|
+
result_payload = {
|
|
741
|
+
"space": space_key,
|
|
742
|
+
"target": {"type": "group", "name": group_name},
|
|
743
|
+
"operations": resolved_operations,
|
|
744
|
+
"result": result,
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
elif anonymous:
|
|
748
|
+
if not resolved_operations:
|
|
749
|
+
raise typer.BadParameter("At least one --operation required when setting anonymous permissions")
|
|
750
|
+
result = client.set_permissions_to_anonymous_for_space(
|
|
751
|
+
space_key,
|
|
752
|
+
operations=resolved_operations,
|
|
753
|
+
)
|
|
754
|
+
result_payload = {
|
|
755
|
+
"space": space_key,
|
|
756
|
+
"target": {"type": "anonymous"},
|
|
757
|
+
"operations": resolved_operations,
|
|
758
|
+
"result": result,
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
elif action == "remove":
|
|
762
|
+
if not yes:
|
|
763
|
+
raise typer.BadParameter("--yes required for write operations")
|
|
764
|
+
|
|
765
|
+
targets = [bool(user_key), bool(group_name), anonymous]
|
|
766
|
+
if sum(1 for flag in targets if flag) != 1:
|
|
767
|
+
raise typer.BadParameter("Provide exactly one of --user, --group, or --anonymous for remove action")
|
|
768
|
+
|
|
769
|
+
if user_key:
|
|
770
|
+
result = client.remove_permissions_from_user_for_space(space_key, user_key)
|
|
771
|
+
result_payload = {
|
|
772
|
+
"space": space_key,
|
|
773
|
+
"target": {"type": "user", "key": user_key},
|
|
774
|
+
"result": result,
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
elif group_name:
|
|
778
|
+
result = client.remove_permissions_from_group_for_space(space_key, group_name)
|
|
779
|
+
result_payload = {
|
|
780
|
+
"space": space_key,
|
|
781
|
+
"target": {"type": "group", "name": group_name},
|
|
782
|
+
"result": result,
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
elif anonymous:
|
|
786
|
+
result = client.remove_permissions_from_anonymous_for_space(space_key)
|
|
787
|
+
result_payload = {
|
|
788
|
+
"space": space_key,
|
|
789
|
+
"target": {"type": "anonymous"},
|
|
790
|
+
"result": result,
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
else:
|
|
794
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
795
|
+
|
|
796
|
+
if json_output or not sys.stdout.isatty():
|
|
797
|
+
_emit_json(result_payload)
|
|
798
|
+
else:
|
|
799
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
800
|
+
|
|
801
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
802
|
+
exit_code = 1
|
|
803
|
+
logger.error("space_permissions_command_failed: %s", exc)
|
|
804
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
805
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
806
|
+
exit_code = 1
|
|
807
|
+
logger.exception("space_permissions_command_unexpected")
|
|
808
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
809
|
+
finally:
|
|
810
|
+
finished_at = datetime.now(UTC)
|
|
811
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
812
|
+
_record_summary(
|
|
813
|
+
parent_ctx,
|
|
814
|
+
command="space-permissions",
|
|
815
|
+
args=args_summary,
|
|
816
|
+
server=server_label,
|
|
817
|
+
exit_code=exit_code,
|
|
818
|
+
started_at=started_at,
|
|
819
|
+
finished_at=finished_at,
|
|
820
|
+
)
|
|
821
|
+
if exit_code != 0:
|
|
822
|
+
raise typer.Exit(code=exit_code) from None
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
@app.command("space-management")
|
|
826
|
+
def cmd_space_management(
|
|
827
|
+
ctx: typer.Context,
|
|
828
|
+
action: str = typer.Argument(..., help="Action: archive, trash-list, trash-remove"),
|
|
829
|
+
space_key: str = typer.Option(..., "--space-key", "-k", help="Confluence space key"),
|
|
830
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
831
|
+
cursor: str | None = typer.Option(None, "--cursor", help="Pagination cursor for trash-list"),
|
|
832
|
+
limit: int = typer.Option(50, "--limit", help="Limit for trash-list (max 100)"),
|
|
833
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions for trash-list"),
|
|
834
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operations"),
|
|
835
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
836
|
+
) -> None:
|
|
837
|
+
"""Manage Confluence space lifecycle operations."""
|
|
838
|
+
|
|
839
|
+
parent_ctx = _parent_context(ctx)
|
|
840
|
+
started_at = datetime.now(UTC)
|
|
841
|
+
exit_code = 0
|
|
842
|
+
result_payload: dict[str, Any] = {}
|
|
843
|
+
args_summary: list[str] = []
|
|
844
|
+
server_key: str | None = None
|
|
845
|
+
settings: ConfluenceSettings | None = None
|
|
846
|
+
|
|
847
|
+
try:
|
|
848
|
+
if limit < 1 or limit > 100:
|
|
849
|
+
raise typer.BadParameter("--limit must be between 1 and 100")
|
|
850
|
+
|
|
851
|
+
settings = load_settings(strict=True)
|
|
852
|
+
server_key = _normalise_server(server, settings)
|
|
853
|
+
client = _build_http_client(settings, server_key)
|
|
854
|
+
|
|
855
|
+
expand_value = expand if expand else None
|
|
856
|
+
args_summary = [
|
|
857
|
+
action,
|
|
858
|
+
f"space={space_key}",
|
|
859
|
+
f"cursor={cursor}",
|
|
860
|
+
f"limit={limit}",
|
|
861
|
+
f"expand={expand_value}",
|
|
862
|
+
]
|
|
863
|
+
|
|
864
|
+
if action == "archive":
|
|
865
|
+
if not yes:
|
|
866
|
+
raise typer.BadParameter("--yes required for archive")
|
|
867
|
+
result = client.archive_space(space_key)
|
|
868
|
+
result_payload = {"space": space_key, "action": "archive", "result": result}
|
|
869
|
+
|
|
870
|
+
elif action == "trash-list":
|
|
871
|
+
result = client.get_trashed_contents_by_space(
|
|
872
|
+
space_key,
|
|
873
|
+
cursor=cursor,
|
|
874
|
+
expand=expand_value,
|
|
875
|
+
limit=limit,
|
|
876
|
+
)
|
|
877
|
+
result_payload = {
|
|
878
|
+
"space": space_key,
|
|
879
|
+
"action": "trash-list",
|
|
880
|
+
"result": result,
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
elif action == "trash-remove":
|
|
884
|
+
if not yes:
|
|
885
|
+
raise typer.BadParameter("--yes required for trash-remove")
|
|
886
|
+
result = client.remove_trashed_contents_by_space(space_key)
|
|
887
|
+
result_payload = {"space": space_key, "action": "trash-remove", "result": result}
|
|
888
|
+
|
|
889
|
+
else:
|
|
890
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
891
|
+
|
|
892
|
+
if json_output or not sys.stdout.isatty():
|
|
893
|
+
_emit_json(result_payload)
|
|
894
|
+
else:
|
|
895
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
896
|
+
|
|
897
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
898
|
+
exit_code = 1
|
|
899
|
+
logger.error("space_management_failed: %s", exc)
|
|
900
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
901
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
902
|
+
exit_code = 1
|
|
903
|
+
logger.exception("space_management_unexpected_error")
|
|
904
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
905
|
+
finally:
|
|
906
|
+
finished_at = datetime.now(UTC)
|
|
907
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
908
|
+
_record_summary(
|
|
909
|
+
parent_ctx,
|
|
910
|
+
command="space-management",
|
|
911
|
+
args=args_summary,
|
|
912
|
+
server=server_label,
|
|
913
|
+
exit_code=exit_code,
|
|
914
|
+
started_at=started_at,
|
|
915
|
+
finished_at=finished_at,
|
|
916
|
+
)
|
|
917
|
+
if exit_code != 0:
|
|
918
|
+
raise typer.Exit(code=exit_code) from None
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
@app.command("group")
|
|
922
|
+
def cmd_group(
|
|
923
|
+
ctx: typer.Context,
|
|
924
|
+
action: str = typer.Argument(..., help="Action: list, members"),
|
|
925
|
+
group_name: str | None = typer.Option(None, "--group", "-g", help="Group name (for members)"),
|
|
926
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
927
|
+
start: int = typer.Option(0, "--start", help="Start index for pagination"),
|
|
928
|
+
limit: int = typer.Option(1000, "--limit", help="Limit for pagination (max 1000)"),
|
|
929
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
930
|
+
) -> None:
|
|
931
|
+
"""Manage Confluence groups."""
|
|
932
|
+
parent_ctx = _parent_context(ctx)
|
|
933
|
+
started_at = datetime.now(UTC)
|
|
934
|
+
exit_code = 0
|
|
935
|
+
result_payload: dict[str, Any] = {}
|
|
936
|
+
args_summary: list[str] = []
|
|
937
|
+
server_key: str | None = None
|
|
938
|
+
settings: ConfluenceSettings | None = None
|
|
939
|
+
|
|
940
|
+
try:
|
|
941
|
+
if limit < 1 or limit > 1000:
|
|
942
|
+
raise typer.BadParameter("--limit must be between 1 and 1000")
|
|
943
|
+
|
|
944
|
+
settings = load_settings(strict=True)
|
|
945
|
+
server_key = _normalise_server(server, settings)
|
|
946
|
+
client = _build_http_client(settings, server_key)
|
|
947
|
+
|
|
948
|
+
args_summary = [action, f"start={start}", f"limit={limit}"]
|
|
949
|
+
|
|
950
|
+
if action == "list":
|
|
951
|
+
result = client.get_all_groups(start=start, limit=limit)
|
|
952
|
+
result_payload = {"count": len(result), "groups": result}
|
|
953
|
+
|
|
954
|
+
elif action == "members":
|
|
955
|
+
if not group_name:
|
|
956
|
+
raise typer.BadParameter("--group required for members action")
|
|
957
|
+
args_summary.append(f"group={group_name}")
|
|
958
|
+
result = client.get_group_members(group_name, start=start, limit=limit)
|
|
959
|
+
result_payload = {"group": group_name, "count": len(result), "members": result}
|
|
960
|
+
|
|
961
|
+
else:
|
|
962
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
963
|
+
|
|
964
|
+
if json_output or not sys.stdout.isatty():
|
|
965
|
+
_emit_json(result_payload)
|
|
966
|
+
else:
|
|
967
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
968
|
+
|
|
969
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
970
|
+
exit_code = 1
|
|
971
|
+
logger.error("group_failed: %s", exc)
|
|
972
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
973
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
974
|
+
exit_code = 1
|
|
975
|
+
logger.exception("group_unexpected_error")
|
|
976
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
977
|
+
finally:
|
|
978
|
+
finished_at = datetime.now(UTC)
|
|
979
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
980
|
+
_record_summary(
|
|
981
|
+
parent_ctx,
|
|
982
|
+
command="group",
|
|
983
|
+
args=args_summary,
|
|
984
|
+
server=server_label,
|
|
985
|
+
exit_code=exit_code,
|
|
986
|
+
started_at=started_at,
|
|
987
|
+
finished_at=finished_at,
|
|
988
|
+
)
|
|
989
|
+
if exit_code != 0:
|
|
990
|
+
raise typer.Exit(code=exit_code) from None
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
@app.command("user")
|
|
994
|
+
def cmd_user(
|
|
995
|
+
ctx: typer.Context,
|
|
996
|
+
action: str = typer.Argument(..., help="Action: get, password, group-add, group-remove"),
|
|
997
|
+
username: str | None = typer.Option(None, "--username", "-u", help="Username"),
|
|
998
|
+
userkey: str | None = typer.Option(None, "--userkey", help="User key"),
|
|
999
|
+
group_name: str | None = typer.Option(None, "--group", "-g", help="Group name (for group-add/group-remove)"),
|
|
1000
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1001
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma-separated fields to expand (for get)"),
|
|
1002
|
+
new_password: str | None = typer.Option(None, "--new-password", help="New password (for password)"),
|
|
1003
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operations"),
|
|
1004
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1005
|
+
) -> None:
|
|
1006
|
+
"""Manage Confluence users."""
|
|
1007
|
+
parent_ctx = _parent_context(ctx)
|
|
1008
|
+
started_at = datetime.now(UTC)
|
|
1009
|
+
exit_code = 0
|
|
1010
|
+
result_payload: dict[str, Any] = {}
|
|
1011
|
+
args_summary: list[str] = []
|
|
1012
|
+
server_key: str | None = None
|
|
1013
|
+
settings: ConfluenceSettings | None = None
|
|
1014
|
+
|
|
1015
|
+
try:
|
|
1016
|
+
settings = load_settings(strict=True)
|
|
1017
|
+
server_key = _normalise_server(server, settings)
|
|
1018
|
+
client = _build_http_client(settings, server_key)
|
|
1019
|
+
|
|
1020
|
+
args_summary = [action]
|
|
1021
|
+
|
|
1022
|
+
if action == "get":
|
|
1023
|
+
if not username and not userkey:
|
|
1024
|
+
raise typer.BadParameter("Provide --username or --userkey for get action")
|
|
1025
|
+
if username and userkey:
|
|
1026
|
+
raise typer.BadParameter("Provide either --username or --userkey, not both")
|
|
1027
|
+
expand_value = expand if expand else None
|
|
1028
|
+
args_summary.append(f"username={username}" if username else f"userkey={userkey}")
|
|
1029
|
+
if expand_value:
|
|
1030
|
+
args_summary.append(f"expand={expand_value}")
|
|
1031
|
+
|
|
1032
|
+
if username:
|
|
1033
|
+
result = client.get_user_details_by_username(username, expand=expand_value)
|
|
1034
|
+
else:
|
|
1035
|
+
result = client.get_user_details_by_userkey(userkey, expand=expand_value)
|
|
1036
|
+
result_payload = {"user": result}
|
|
1037
|
+
|
|
1038
|
+
elif action == "password":
|
|
1039
|
+
if not yes:
|
|
1040
|
+
raise typer.BadParameter("--yes required for password change")
|
|
1041
|
+
if not username:
|
|
1042
|
+
raise typer.BadParameter("--username required for password action")
|
|
1043
|
+
if not new_password:
|
|
1044
|
+
raise typer.BadParameter("--new-password required for password action")
|
|
1045
|
+
args_summary.append(f"username={username}")
|
|
1046
|
+
client.change_user_password(username, new_password)
|
|
1047
|
+
result_payload = {"username": username, "status": "password_changed"}
|
|
1048
|
+
|
|
1049
|
+
elif action == "group-add":
|
|
1050
|
+
if not yes:
|
|
1051
|
+
raise typer.BadParameter("--yes required for group-add")
|
|
1052
|
+
if not username:
|
|
1053
|
+
raise typer.BadParameter("--username required for group-add")
|
|
1054
|
+
if not group_name:
|
|
1055
|
+
raise typer.BadParameter("--group required for group-add")
|
|
1056
|
+
args_summary.append(f"username={username}")
|
|
1057
|
+
args_summary.append(f"group={group_name}")
|
|
1058
|
+
client.add_user_to_group(username, group_name)
|
|
1059
|
+
result_payload = {"username": username, "group": group_name, "status": "added"}
|
|
1060
|
+
|
|
1061
|
+
elif action == "group-remove":
|
|
1062
|
+
if not yes:
|
|
1063
|
+
raise typer.BadParameter("--yes required for group-remove")
|
|
1064
|
+
if not username:
|
|
1065
|
+
raise typer.BadParameter("--username required for group-remove")
|
|
1066
|
+
if not group_name:
|
|
1067
|
+
raise typer.BadParameter("--group required for group-remove")
|
|
1068
|
+
args_summary.append(f"username={username}")
|
|
1069
|
+
args_summary.append(f"group={group_name}")
|
|
1070
|
+
client.remove_user_from_group(username, group_name)
|
|
1071
|
+
result_payload = {"username": username, "group": group_name, "status": "removed"}
|
|
1072
|
+
|
|
1073
|
+
else:
|
|
1074
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1075
|
+
|
|
1076
|
+
if json_output or not sys.stdout.isatty():
|
|
1077
|
+
_emit_json(result_payload)
|
|
1078
|
+
else:
|
|
1079
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
1080
|
+
|
|
1081
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
1082
|
+
exit_code = 1
|
|
1083
|
+
logger.error("user_failed: %s", exc)
|
|
1084
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1085
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1086
|
+
exit_code = 1
|
|
1087
|
+
logger.exception("user_unexpected_error")
|
|
1088
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1089
|
+
finally:
|
|
1090
|
+
finished_at = datetime.now(UTC)
|
|
1091
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
1092
|
+
_record_summary(
|
|
1093
|
+
parent_ctx,
|
|
1094
|
+
command="user",
|
|
1095
|
+
args=args_summary,
|
|
1096
|
+
server=server_label,
|
|
1097
|
+
exit_code=exit_code,
|
|
1098
|
+
started_at=started_at,
|
|
1099
|
+
finished_at=finished_at,
|
|
1100
|
+
)
|
|
1101
|
+
if exit_code != 0:
|
|
1102
|
+
raise typer.Exit(code=exit_code) from None
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
@app.command("export")
|
|
1106
|
+
def cmd_export(
|
|
1107
|
+
ctx: typer.Context,
|
|
1108
|
+
action: str = typer.Argument(..., help="Action: page, space"),
|
|
1109
|
+
page_id: str | None = typer.Option(None, "--page-id", help="Page ID (for page)"),
|
|
1110
|
+
space_key: str | None = typer.Option(None, "--space-key", "-k", help="Space key (for space)"),
|
|
1111
|
+
export_type: str | None = typer.Option(None, "--export-type", help="Export type: pdf, html, xml (for space)"),
|
|
1112
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1113
|
+
output: Path | None = typer.Option(None, "--output", "-o", help="Output file path (for page PDF)"),
|
|
1114
|
+
api_version: str | None = typer.Option(None, "--api-version", help="API version: cloud, server (for page, auto-detected if not specified)"),
|
|
1115
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1116
|
+
) -> None:
|
|
1117
|
+
"""Export Confluence pages or spaces."""
|
|
1118
|
+
parent_ctx = _parent_context(ctx)
|
|
1119
|
+
started_at = datetime.now(UTC)
|
|
1120
|
+
exit_code = 0
|
|
1121
|
+
result_payload: dict[str, Any] = {}
|
|
1122
|
+
args_summary: list[str] = []
|
|
1123
|
+
server_key: str | None = None
|
|
1124
|
+
settings: ConfluenceSettings | None = None
|
|
1125
|
+
|
|
1126
|
+
try:
|
|
1127
|
+
settings = load_settings(strict=True)
|
|
1128
|
+
server_key = _normalise_server(server, settings)
|
|
1129
|
+
client = _build_http_client(settings, server_key)
|
|
1130
|
+
|
|
1131
|
+
args_summary = [action]
|
|
1132
|
+
|
|
1133
|
+
if action == "page":
|
|
1134
|
+
if not page_id:
|
|
1135
|
+
raise typer.BadParameter("--page-id required for page action")
|
|
1136
|
+
args_summary.append(f"page_id={page_id}")
|
|
1137
|
+
if api_version:
|
|
1138
|
+
args_summary.append(f"api_version={api_version}")
|
|
1139
|
+
|
|
1140
|
+
pdf_bytes = client.export_page(page_id, api_version=api_version)
|
|
1141
|
+
|
|
1142
|
+
if output:
|
|
1143
|
+
output.write_bytes(pdf_bytes)
|
|
1144
|
+
result_payload = {"page_id": page_id, "output_file": str(output), "size_bytes": len(pdf_bytes)}
|
|
1145
|
+
typer.echo(f"Exported page {page_id} to {output} ({len(pdf_bytes)} bytes)")
|
|
1146
|
+
else:
|
|
1147
|
+
# Write to stdout as binary
|
|
1148
|
+
sys.stdout.buffer.write(pdf_bytes)
|
|
1149
|
+
result_payload = {"page_id": page_id, "size_bytes": len(pdf_bytes), "format": "pdf"}
|
|
1150
|
+
|
|
1151
|
+
elif action == "space":
|
|
1152
|
+
if not space_key:
|
|
1153
|
+
raise typer.BadParameter("--space-key required for space action")
|
|
1154
|
+
if not export_type:
|
|
1155
|
+
raise typer.BadParameter("--export-type required for space action")
|
|
1156
|
+
if export_type not in ("pdf", "html", "xml"):
|
|
1157
|
+
raise typer.BadParameter("--export-type must be one of: pdf, html, xml")
|
|
1158
|
+
args_summary.append(f"space_key={space_key}")
|
|
1159
|
+
args_summary.append(f"export_type={export_type}")
|
|
1160
|
+
|
|
1161
|
+
download_url = client.get_space_export(space_key, export_type)
|
|
1162
|
+
result_payload = {"space_key": space_key, "export_type": export_type, "download_url": download_url}
|
|
1163
|
+
|
|
1164
|
+
if json_output or not sys.stdout.isatty():
|
|
1165
|
+
_emit_json(result_payload)
|
|
1166
|
+
else:
|
|
1167
|
+
typer.echo(f"Space export URL: {download_url}")
|
|
1168
|
+
|
|
1169
|
+
else:
|
|
1170
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1171
|
+
|
|
1172
|
+
# For page action, JSON output only if explicitly requested or output file specified
|
|
1173
|
+
if action == "page" and (json_output or output):
|
|
1174
|
+
if json_output:
|
|
1175
|
+
_emit_json(result_payload)
|
|
1176
|
+
|
|
1177
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
1178
|
+
exit_code = 1
|
|
1179
|
+
logger.error("export_failed: %s", exc)
|
|
1180
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1181
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1182
|
+
exit_code = 1
|
|
1183
|
+
logger.exception("export_unexpected_error")
|
|
1184
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1185
|
+
finally:
|
|
1186
|
+
finished_at = datetime.now(UTC)
|
|
1187
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
1188
|
+
_record_summary(
|
|
1189
|
+
parent_ctx,
|
|
1190
|
+
command="export",
|
|
1191
|
+
args=args_summary,
|
|
1192
|
+
server=server_label,
|
|
1193
|
+
exit_code=exit_code,
|
|
1194
|
+
started_at=started_at,
|
|
1195
|
+
finished_at=finished_at,
|
|
1196
|
+
)
|
|
1197
|
+
if exit_code != 0:
|
|
1198
|
+
raise typer.Exit(code=exit_code) from None
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
@app.command("draft")
|
|
1202
|
+
def cmd_draft(
|
|
1203
|
+
ctx: typer.Context,
|
|
1204
|
+
action: str = typer.Argument(..., help="Action: get, list, remove"),
|
|
1205
|
+
page_id: str | None = typer.Option(None, "--page-id", help="Page ID (for get/remove)"),
|
|
1206
|
+
space_key: str | None = typer.Option(None, "--space-key", "-k", help="Space key (for list)"),
|
|
1207
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1208
|
+
limit: int = typer.Option(25, "--limit", help="Limit for list (max 100)"),
|
|
1209
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operations (for remove)"),
|
|
1210
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1211
|
+
) -> None:
|
|
1212
|
+
"""Manage Confluence draft pages (Server-only)."""
|
|
1213
|
+
parent_ctx = _parent_context(ctx)
|
|
1214
|
+
started_at = datetime.now(UTC)
|
|
1215
|
+
exit_code = 0
|
|
1216
|
+
result_payload: dict[str, Any] = {}
|
|
1217
|
+
args_summary: list[str] = []
|
|
1218
|
+
server_key: str | None = None
|
|
1219
|
+
settings: ConfluenceSettings | None = None
|
|
1220
|
+
|
|
1221
|
+
try:
|
|
1222
|
+
if limit < 1 or limit > 100:
|
|
1223
|
+
raise typer.BadParameter("--limit must be between 1 and 100")
|
|
1224
|
+
|
|
1225
|
+
settings = load_settings(strict=True)
|
|
1226
|
+
server_key = _normalise_server(server, settings)
|
|
1227
|
+
client = _build_http_client(settings, server_key)
|
|
1228
|
+
|
|
1229
|
+
args_summary = [action]
|
|
1230
|
+
|
|
1231
|
+
if action == "get":
|
|
1232
|
+
if not page_id:
|
|
1233
|
+
raise typer.BadParameter("--page-id required for get action")
|
|
1234
|
+
args_summary.append(f"page_id={page_id}")
|
|
1235
|
+
result = client.get_draft_page_by_id(page_id)
|
|
1236
|
+
result_payload = {"draft_page": result}
|
|
1237
|
+
|
|
1238
|
+
elif action == "list":
|
|
1239
|
+
if not space_key:
|
|
1240
|
+
raise typer.BadParameter("--space-key required for list action")
|
|
1241
|
+
args_summary.append(f"space_key={space_key}")
|
|
1242
|
+
args_summary.append(f"limit={limit}")
|
|
1243
|
+
result = client.get_all_draft_pages_from_space(space_key, limit=limit)
|
|
1244
|
+
result_payload = {"space": space_key, "count": len(result), "drafts": result}
|
|
1245
|
+
|
|
1246
|
+
elif action == "remove":
|
|
1247
|
+
if not yes:
|
|
1248
|
+
raise typer.BadParameter("--yes required for remove action")
|
|
1249
|
+
if not page_id:
|
|
1250
|
+
raise typer.BadParameter("--page-id required for remove action")
|
|
1251
|
+
args_summary.append(f"page_id={page_id}")
|
|
1252
|
+
client.remove_page_as_draft(page_id)
|
|
1253
|
+
result_payload = {"page_id": page_id, "status": "removed"}
|
|
1254
|
+
|
|
1255
|
+
else:
|
|
1256
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1257
|
+
|
|
1258
|
+
if json_output or not sys.stdout.isatty():
|
|
1259
|
+
_emit_json(result_payload)
|
|
1260
|
+
else:
|
|
1261
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
1262
|
+
|
|
1263
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
1264
|
+
exit_code = 1
|
|
1265
|
+
logger.error("draft_failed: %s", exc)
|
|
1266
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1267
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1268
|
+
exit_code = 1
|
|
1269
|
+
logger.exception("draft_unexpected_error")
|
|
1270
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1271
|
+
finally:
|
|
1272
|
+
finished_at = datetime.now(UTC)
|
|
1273
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
1274
|
+
_record_summary(
|
|
1275
|
+
parent_ctx,
|
|
1276
|
+
command="draft",
|
|
1277
|
+
args=args_summary,
|
|
1278
|
+
server=server_label,
|
|
1279
|
+
exit_code=exit_code,
|
|
1280
|
+
started_at=started_at,
|
|
1281
|
+
finished_at=finished_at,
|
|
1282
|
+
)
|
|
1283
|
+
if exit_code != 0:
|
|
1284
|
+
raise typer.Exit(code=exit_code) from None
|
|
1285
|
+
|
|
1286
|
+
|
|
1287
|
+
@app.command("history")
|
|
1288
|
+
def cmd_history(
|
|
1289
|
+
ctx: typer.Context,
|
|
1290
|
+
action: str = typer.Argument(..., help="Action: get, version, remove"),
|
|
1291
|
+
page_id: str | None = typer.Option(None, "--page-id", help="Page ID"),
|
|
1292
|
+
version: int | None = typer.Option(None, "--version", help="Version number (for version/remove)"),
|
|
1293
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1294
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operations (for remove)"),
|
|
1295
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1296
|
+
) -> None:
|
|
1297
|
+
"""Manage Confluence page history and versioning."""
|
|
1298
|
+
parent_ctx = _parent_context(ctx)
|
|
1299
|
+
started_at = datetime.now(UTC)
|
|
1300
|
+
exit_code = 0
|
|
1301
|
+
result_payload: dict[str, Any] = {}
|
|
1302
|
+
args_summary: list[str] = []
|
|
1303
|
+
server_key: str | None = None
|
|
1304
|
+
settings: ConfluenceSettings | None = None
|
|
1305
|
+
|
|
1306
|
+
try:
|
|
1307
|
+
settings = load_settings(strict=True)
|
|
1308
|
+
server_key = _normalise_server(server, settings)
|
|
1309
|
+
client = _build_http_client(settings, server_key)
|
|
1310
|
+
|
|
1311
|
+
args_summary = [action]
|
|
1312
|
+
|
|
1313
|
+
if action == "get":
|
|
1314
|
+
if not page_id:
|
|
1315
|
+
raise typer.BadParameter("--page-id required for get action")
|
|
1316
|
+
args_summary.append(f"page_id={page_id}")
|
|
1317
|
+
result = client.history(page_id)
|
|
1318
|
+
result_payload = {"page_id": page_id, "history": result}
|
|
1319
|
+
|
|
1320
|
+
elif action == "version":
|
|
1321
|
+
if not page_id:
|
|
1322
|
+
raise typer.BadParameter("--page-id required for version action")
|
|
1323
|
+
if version is None:
|
|
1324
|
+
raise typer.BadParameter("--version required for version action")
|
|
1325
|
+
args_summary.append(f"page_id={page_id}")
|
|
1326
|
+
args_summary.append(f"version={version}")
|
|
1327
|
+
result = client.get_content_history_by_version_number(page_id, version)
|
|
1328
|
+
result_payload = {"page_id": page_id, "version": version, "content": result}
|
|
1329
|
+
|
|
1330
|
+
elif action == "remove":
|
|
1331
|
+
if not yes:
|
|
1332
|
+
raise typer.BadParameter("--yes required for remove action (experimental feature)")
|
|
1333
|
+
if not page_id:
|
|
1334
|
+
raise typer.BadParameter("--page-id required for remove action")
|
|
1335
|
+
if version is None:
|
|
1336
|
+
raise typer.BadParameter("--version required for remove action")
|
|
1337
|
+
args_summary.append(f"page_id={page_id}")
|
|
1338
|
+
args_summary.append(f"version={version}")
|
|
1339
|
+
typer.echo("Warning: remove_content_history is an experimental feature", err=True)
|
|
1340
|
+
client.remove_content_history(page_id, version)
|
|
1341
|
+
result_payload = {"page_id": page_id, "version": version, "status": "removed"}
|
|
1342
|
+
|
|
1343
|
+
else:
|
|
1344
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1345
|
+
|
|
1346
|
+
if json_output or not sys.stdout.isatty():
|
|
1347
|
+
_emit_json(result_payload)
|
|
1348
|
+
else:
|
|
1349
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
1350
|
+
|
|
1351
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
1352
|
+
exit_code = 1
|
|
1353
|
+
logger.error("history_failed: %s", exc)
|
|
1354
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1355
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1356
|
+
exit_code = 1
|
|
1357
|
+
logger.exception("history_unexpected_error")
|
|
1358
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1359
|
+
finally:
|
|
1360
|
+
finished_at = datetime.now(UTC)
|
|
1361
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
1362
|
+
_record_summary(
|
|
1363
|
+
parent_ctx,
|
|
1364
|
+
command="history",
|
|
1365
|
+
args=args_summary,
|
|
1366
|
+
server=server_label,
|
|
1367
|
+
exit_code=exit_code,
|
|
1368
|
+
started_at=started_at,
|
|
1369
|
+
finished_at=finished_at,
|
|
1370
|
+
)
|
|
1371
|
+
if exit_code != 0:
|
|
1372
|
+
raise typer.Exit(code=exit_code) from None
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
@app.command("cache")
|
|
1376
|
+
def cmd_cache(
|
|
1377
|
+
ctx: typer.Context,
|
|
1378
|
+
action: str = typer.Argument(..., help="Action: statistics, flush, size"),
|
|
1379
|
+
cache_name: str | None = typer.Option(None, "--cache-name", help="Cache name (for flush)"),
|
|
1380
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1381
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operation (for flush)"),
|
|
1382
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1383
|
+
) -> None:
|
|
1384
|
+
"""Manage Confluence cache (Server-only)."""
|
|
1385
|
+
parent_ctx = _parent_context(ctx)
|
|
1386
|
+
started_at = datetime.now(UTC)
|
|
1387
|
+
exit_code = 0
|
|
1388
|
+
result_payload: dict[str, Any] = {}
|
|
1389
|
+
args_summary: list[str] = []
|
|
1390
|
+
server_key: str | None = None
|
|
1391
|
+
settings: ConfluenceSettings | None = None
|
|
1392
|
+
|
|
1393
|
+
try:
|
|
1394
|
+
settings = load_settings(strict=True)
|
|
1395
|
+
server_key = _normalise_server(server, settings)
|
|
1396
|
+
client = _build_http_client(settings, server_key)
|
|
1397
|
+
|
|
1398
|
+
args_summary = [action]
|
|
1399
|
+
|
|
1400
|
+
if action == "statistics":
|
|
1401
|
+
result = client.get_cache_statistics()
|
|
1402
|
+
result_payload = {"cache_statistics": result}
|
|
1403
|
+
elif action == "flush":
|
|
1404
|
+
if not yes:
|
|
1405
|
+
raise typer.BadParameter("Refusing to flush cache without --yes")
|
|
1406
|
+
if cache_name:
|
|
1407
|
+
args_summary.append(f"cache_name={cache_name}")
|
|
1408
|
+
client.flush_cache(cache_name=cache_name)
|
|
1409
|
+
result_payload = {"status": "success", "cache_flushed": cache_name or "all"}
|
|
1410
|
+
elif action == "size":
|
|
1411
|
+
result = client.get_cache_size()
|
|
1412
|
+
result_payload = {"cache_size": result}
|
|
1413
|
+
else:
|
|
1414
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1415
|
+
|
|
1416
|
+
if json_output or not sys.stdout.isatty():
|
|
1417
|
+
_emit_json(result_payload)
|
|
1418
|
+
else:
|
|
1419
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
1420
|
+
|
|
1421
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
1422
|
+
exit_code = 1
|
|
1423
|
+
logger.error("cache_failed: %s", exc)
|
|
1424
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1425
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1426
|
+
exit_code = 1
|
|
1427
|
+
logger.exception("cache_unexpected_error")
|
|
1428
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1429
|
+
finally:
|
|
1430
|
+
finished_at = datetime.now(UTC)
|
|
1431
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
1432
|
+
_record_summary(
|
|
1433
|
+
parent_ctx,
|
|
1434
|
+
command="cache",
|
|
1435
|
+
args=args_summary,
|
|
1436
|
+
server=server_label,
|
|
1437
|
+
exit_code=exit_code,
|
|
1438
|
+
started_at=started_at,
|
|
1439
|
+
finished_at=finished_at,
|
|
1440
|
+
)
|
|
1441
|
+
if exit_code != 0:
|
|
1442
|
+
raise typer.Exit(code=exit_code) from None
|
|
1443
|
+
|
|
1444
|
+
|
|
1445
|
+
@app.command("get")
|
|
1446
|
+
def cmd_get(
|
|
1447
|
+
ctx: typer.Context,
|
|
1448
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
1449
|
+
expand: str | None = typer.Option(
|
|
1450
|
+
None, "--expand", "-e", help="Comma separated expansions (e.g., 'body.storage,version')"
|
|
1451
|
+
),
|
|
1452
|
+
format: str | None = typer.Option(None, "--format", "-f", help="Output format (storage|editor|view)"),
|
|
1453
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1454
|
+
) -> None:
|
|
1455
|
+
"""Get Confluence content by page ID."""
|
|
1456
|
+
parent_ctx = _parent_context(ctx)
|
|
1457
|
+
_ensure_v1_api(parent_ctx, "get")
|
|
1458
|
+
started_at = datetime.now(UTC)
|
|
1459
|
+
settings = load_settings(strict=True)
|
|
1460
|
+
server_key = _normalise_server(server, settings)
|
|
1461
|
+
|
|
1462
|
+
try:
|
|
1463
|
+
client = ContentClientV2(_build_http_client(settings, server_key))
|
|
1464
|
+
content = client.get_content(page_id, expand=_expand_option(expand), format=format)
|
|
1465
|
+
|
|
1466
|
+
if content:
|
|
1467
|
+
_emit_json(content)
|
|
1468
|
+
else:
|
|
1469
|
+
typer.echo(f"Content not found: {page_id}", err=True)
|
|
1470
|
+
raise typer.Exit(code=1) from None
|
|
1471
|
+
|
|
1472
|
+
finished_at = datetime.now(UTC)
|
|
1473
|
+
_record_summary(
|
|
1474
|
+
parent_ctx,
|
|
1475
|
+
command="get",
|
|
1476
|
+
args=[page_id],
|
|
1477
|
+
server=server_key,
|
|
1478
|
+
exit_code=0,
|
|
1479
|
+
started_at=started_at,
|
|
1480
|
+
finished_at=finished_at,
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
except Exception as exc:
|
|
1484
|
+
logger.error("get_command_failed: %s", exc)
|
|
1485
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1486
|
+
raise typer.Exit(code=1) from None
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
# ---------------------------------------------------------------------------
|
|
1490
|
+
# Native content commands
|
|
1491
|
+
# ---------------------------------------------------------------------------
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
@batch_app.command("scan")
|
|
1495
|
+
def batch_scan(
|
|
1496
|
+
ctx: typer.Context,
|
|
1497
|
+
cql: str = typer.Option(..., "--cql", "-q", help="CQL query string"),
|
|
1498
|
+
limit: int = typer.Option(25, "--limit", help="Number of results per request"),
|
|
1499
|
+
max_results: int | None = typer.Option(None, "--max-results", help="Total results to fetch"),
|
|
1500
|
+
start: int = typer.Option(0, "--start", help="Pagination start offset"),
|
|
1501
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
|
|
1502
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1503
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1504
|
+
) -> None:
|
|
1505
|
+
parent_ctx = _parent_context(ctx)
|
|
1506
|
+
_ensure_v1_api(parent_ctx, "batch.scan")
|
|
1507
|
+
started_at = datetime.now(UTC)
|
|
1508
|
+
settings = load_settings(strict=True)
|
|
1509
|
+
server_key = _normalise_server(server, settings)
|
|
1510
|
+
runner = BatchRunner(_build_http_client(settings, server_key), expand=_expand_option(expand))
|
|
1511
|
+
results: list[dict[str, Any]] = []
|
|
1512
|
+
for item in runner.scan(cql, limit=limit, max_results=max_results, start=start):
|
|
1513
|
+
results.append(item)
|
|
1514
|
+
metrics = runner.last_metrics
|
|
1515
|
+
payload: dict[str, Any] = {
|
|
1516
|
+
"cql": cql,
|
|
1517
|
+
"count": len(results),
|
|
1518
|
+
"limit": limit,
|
|
1519
|
+
"maxResults": max_results,
|
|
1520
|
+
"start": start,
|
|
1521
|
+
"results": results,
|
|
1522
|
+
}
|
|
1523
|
+
if metrics is not None:
|
|
1524
|
+
payload["metrics"] = {
|
|
1525
|
+
"requests": metrics.requests,
|
|
1526
|
+
"pagesProcessed": metrics.pages_processed,
|
|
1527
|
+
"durationSeconds": metrics.duration_seconds,
|
|
1528
|
+
}
|
|
1529
|
+
if json_output or not sys.stdout.isatty():
|
|
1530
|
+
_emit_json(payload)
|
|
1531
|
+
else:
|
|
1532
|
+
typer.echo(
|
|
1533
|
+
f"Scanned {payload['count']} results (requests={payload['metrics']['requests'] if 'metrics' in payload else '?'})"
|
|
1534
|
+
)
|
|
1535
|
+
finished_at = datetime.now(UTC)
|
|
1536
|
+
_record_summary(
|
|
1537
|
+
parent_ctx,
|
|
1538
|
+
command="batch.scan",
|
|
1539
|
+
args=[cql, f"limit={limit}", f"start={start}"],
|
|
1540
|
+
server=server_key,
|
|
1541
|
+
exit_code=0,
|
|
1542
|
+
started_at=started_at,
|
|
1543
|
+
finished_at=finished_at,
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
|
|
1547
|
+
@batch_app.command("snapshot")
|
|
1548
|
+
def batch_snapshot(
|
|
1549
|
+
ctx: typer.Context,
|
|
1550
|
+
cql: str = typer.Option(..., "--cql", "-q", help="CQL query string"),
|
|
1551
|
+
limit: int = typer.Option(25, "--limit", help="Number of results per request"),
|
|
1552
|
+
max_results: int | None = typer.Option(None, "--max-results", help="Total results to fetch"),
|
|
1553
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
|
|
1554
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1555
|
+
) -> None:
|
|
1556
|
+
parent_ctx = _parent_context(ctx)
|
|
1557
|
+
_ensure_v1_api(parent_ctx, "batch.snapshot")
|
|
1558
|
+
started_at = datetime.now(UTC)
|
|
1559
|
+
settings = load_settings(strict=True)
|
|
1560
|
+
server_key = _normalise_server(server, settings)
|
|
1561
|
+
runner = BatchRunner(_build_http_client(settings, server_key))
|
|
1562
|
+
payload = runner.snapshot(cql, limit=limit, max_results=max_results, expand=_expand_option(expand))
|
|
1563
|
+
_emit_json(payload)
|
|
1564
|
+
finished_at = datetime.now(UTC)
|
|
1565
|
+
_record_summary(
|
|
1566
|
+
parent_ctx,
|
|
1567
|
+
command="batch.snapshot",
|
|
1568
|
+
args=[cql, f"limit={limit}"],
|
|
1569
|
+
server=server_key,
|
|
1570
|
+
exit_code=0,
|
|
1571
|
+
started_at=started_at,
|
|
1572
|
+
finished_at=finished_at,
|
|
1573
|
+
)
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
@batch_app.command("poll")
|
|
1577
|
+
def batch_poll(
|
|
1578
|
+
ctx: typer.Context,
|
|
1579
|
+
cql: str = typer.Option(..., "--cql", "-q", help="Base CQL query (will be augmented with lastmodified filter)"),
|
|
1580
|
+
state_file: Path = typer.Option(..., "--state-file", help="Path to persistent cursor file (JSON)"),
|
|
1581
|
+
limit: int = typer.Option(25, "--limit", help="Page size per request"),
|
|
1582
|
+
max_results: int | None = typer.Option(None, "--max-results", help="Max results this run"),
|
|
1583
|
+
expand: str | None = typer.Option(
|
|
1584
|
+
"version", "--expand", help="Comma separated expansions (default includes 'version')"
|
|
1585
|
+
),
|
|
1586
|
+
out_jsonl: Path | None = typer.Option(None, "--out-jsonl", help="Write results to JSONL (one object per line)"),
|
|
1587
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1588
|
+
) -> None:
|
|
1589
|
+
parent_ctx = _parent_context(ctx)
|
|
1590
|
+
_ensure_v1_api(parent_ctx, "batch.poll")
|
|
1591
|
+
started_at = datetime.now(UTC)
|
|
1592
|
+
try:
|
|
1593
|
+
settings = load_settings(strict=True)
|
|
1594
|
+
server_key = _normalise_server(server, settings)
|
|
1595
|
+
client = _build_http_client(settings, server_key)
|
|
1596
|
+
poller = Poller(client)
|
|
1597
|
+
expand_list = _expand_option(expand) or ["version"]
|
|
1598
|
+
count = poller.poll_once(
|
|
1599
|
+
base_cql=cql,
|
|
1600
|
+
state_file=state_file,
|
|
1601
|
+
limit=limit,
|
|
1602
|
+
max_results=max_results,
|
|
1603
|
+
expand=expand_list,
|
|
1604
|
+
out_jsonl=out_jsonl,
|
|
1605
|
+
)
|
|
1606
|
+
typer.echo(f"Polled {count} items")
|
|
1607
|
+
except Exception as exc:
|
|
1608
|
+
logger.exception("batch.poll failed", extra={"cql": cql})
|
|
1609
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1610
|
+
raise typer.Exit(code=1) from None
|
|
1611
|
+
finally:
|
|
1612
|
+
finished_at = datetime.now(UTC)
|
|
1613
|
+
_record_summary(
|
|
1614
|
+
parent_ctx,
|
|
1615
|
+
command="batch.poll",
|
|
1616
|
+
args=[cql, f"limit={limit}", f"out={str(out_jsonl) if out_jsonl else '-'}"],
|
|
1617
|
+
server=server_key if "server_key" in locals() else "",
|
|
1618
|
+
exit_code=0,
|
|
1619
|
+
started_at=started_at,
|
|
1620
|
+
finished_at=finished_at,
|
|
1621
|
+
)
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
@webhook_app.command("create")
|
|
1625
|
+
def webhook_create(
|
|
1626
|
+
ctx: typer.Context,
|
|
1627
|
+
name: str = typer.Option(..., "--name", "-n", help="Webhook name"),
|
|
1628
|
+
url: str = typer.Option(..., "--url", help="Destination URL"),
|
|
1629
|
+
event: list[str] = typer.Option(["page_created"], "--event", "-e", help="Event identifier"),
|
|
1630
|
+
active: bool = typer.Option(True, "--active/--inactive", help="Toggle webhook activation"),
|
|
1631
|
+
filters: str | None = typer.Option(None, "--filters", help="JSON object filters"),
|
|
1632
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1633
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1634
|
+
) -> None:
|
|
1635
|
+
parent_ctx = _parent_context(ctx)
|
|
1636
|
+
_ensure_v1_api(parent_ctx, "webhook.create")
|
|
1637
|
+
started_at = datetime.now(UTC)
|
|
1638
|
+
settings = load_settings(strict=True)
|
|
1639
|
+
server_key = _normalise_server(server, settings)
|
|
1640
|
+
client = WebhookClient(_build_http_client(settings, server_key))
|
|
1641
|
+
payload = client.create(
|
|
1642
|
+
name=name,
|
|
1643
|
+
url=url,
|
|
1644
|
+
events=event,
|
|
1645
|
+
active=active,
|
|
1646
|
+
filters=_parse_json_option(filters, "--filters"),
|
|
1647
|
+
)
|
|
1648
|
+
if json_output:
|
|
1649
|
+
_emit_json(payload)
|
|
1650
|
+
else:
|
|
1651
|
+
typer.echo(f"Created webhook {payload.get('id')}")
|
|
1652
|
+
finished_at = datetime.now(UTC)
|
|
1653
|
+
_record_summary(
|
|
1654
|
+
parent_ctx,
|
|
1655
|
+
command="webhook.create",
|
|
1656
|
+
args=[name, f"events={len(event)}"],
|
|
1657
|
+
server=server_key,
|
|
1658
|
+
exit_code=0,
|
|
1659
|
+
started_at=started_at,
|
|
1660
|
+
finished_at=finished_at,
|
|
1661
|
+
)
|
|
1662
|
+
|
|
1663
|
+
|
|
1664
|
+
@webhook_app.command("list")
|
|
1665
|
+
def webhook_list(
|
|
1666
|
+
ctx: typer.Context,
|
|
1667
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1668
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
1669
|
+
) -> None:
|
|
1670
|
+
parent_ctx = _parent_context(ctx)
|
|
1671
|
+
_ensure_v1_api(parent_ctx, "webhook.list")
|
|
1672
|
+
started_at = datetime.now(UTC)
|
|
1673
|
+
settings = load_settings(strict=True)
|
|
1674
|
+
server_key = _normalise_server(server, settings)
|
|
1675
|
+
client = WebhookClient(_build_http_client(settings, server_key))
|
|
1676
|
+
payload = client.list()
|
|
1677
|
+
if json_output:
|
|
1678
|
+
_emit_json(payload)
|
|
1679
|
+
else:
|
|
1680
|
+
typer.echo(f"{len(payload)} webhooks")
|
|
1681
|
+
finished_at = datetime.now(UTC)
|
|
1682
|
+
_record_summary(
|
|
1683
|
+
parent_ctx,
|
|
1684
|
+
command="webhook.list",
|
|
1685
|
+
args=[f"count={len(payload)}"],
|
|
1686
|
+
server=server_key,
|
|
1687
|
+
exit_code=0,
|
|
1688
|
+
started_at=started_at,
|
|
1689
|
+
finished_at=finished_at,
|
|
1690
|
+
)
|
|
1691
|
+
|
|
1692
|
+
|
|
1693
|
+
@webhook_app.command("delete")
|
|
1694
|
+
def webhook_delete(
|
|
1695
|
+
ctx: typer.Context,
|
|
1696
|
+
webhook_id: str = typer.Argument(..., help="Webhook identifier"),
|
|
1697
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1698
|
+
) -> None:
|
|
1699
|
+
parent_ctx = _parent_context(ctx)
|
|
1700
|
+
_ensure_v1_api(parent_ctx, "webhook.delete")
|
|
1701
|
+
started_at = datetime.now(UTC)
|
|
1702
|
+
settings = load_settings(strict=True)
|
|
1703
|
+
server_key = _normalise_server(server, settings)
|
|
1704
|
+
client = WebhookClient(_build_http_client(settings, server_key))
|
|
1705
|
+
client.delete(webhook_id)
|
|
1706
|
+
typer.echo(f"Deleted webhook {webhook_id}")
|
|
1707
|
+
finished_at = datetime.now(UTC)
|
|
1708
|
+
_record_summary(
|
|
1709
|
+
parent_ctx,
|
|
1710
|
+
command="webhook.delete",
|
|
1711
|
+
args=[webhook_id],
|
|
1712
|
+
server=server_key,
|
|
1713
|
+
exit_code=0,
|
|
1714
|
+
started_at=started_at,
|
|
1715
|
+
finished_at=finished_at,
|
|
1716
|
+
)
|
|
1717
|
+
|
|
1718
|
+
|
|
1719
|
+
@content_app.command("page")
|
|
1720
|
+
def content_page(
|
|
1721
|
+
ctx: typer.Context,
|
|
1722
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
1723
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
|
|
1724
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1725
|
+
) -> None:
|
|
1726
|
+
parent_ctx = _parent_context(ctx)
|
|
1727
|
+
started_at = datetime.now(UTC)
|
|
1728
|
+
try:
|
|
1729
|
+
settings = load_settings(strict=True)
|
|
1730
|
+
server_key = _normalise_server(server, settings)
|
|
1731
|
+
cli_ctx = _get_cli_ctx(parent_ctx)
|
|
1732
|
+
client = _build_content_client(settings, server_key, cli_ctx.api_version)
|
|
1733
|
+
expand_list = _expand_option(expand)
|
|
1734
|
+
payload = client.get_page(page_id, expand=expand_list)
|
|
1735
|
+
_emit_json(payload)
|
|
1736
|
+
except Exception as exc:
|
|
1737
|
+
logger.exception("content.page failed", extra={"page_id": page_id})
|
|
1738
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1739
|
+
raise typer.Exit(code=1) from None
|
|
1740
|
+
else:
|
|
1741
|
+
finished_at = datetime.now(UTC)
|
|
1742
|
+
_record_summary(
|
|
1743
|
+
parent_ctx,
|
|
1744
|
+
command="content.page",
|
|
1745
|
+
args=[page_id],
|
|
1746
|
+
server=server_key,
|
|
1747
|
+
exit_code=0,
|
|
1748
|
+
started_at=started_at,
|
|
1749
|
+
finished_at=finished_at,
|
|
1750
|
+
)
|
|
1751
|
+
|
|
1752
|
+
|
|
1753
|
+
@content_app.command("search")
|
|
1754
|
+
def content_search(
|
|
1755
|
+
ctx: typer.Context,
|
|
1756
|
+
cql: str = typer.Argument(..., help="CQL query string"),
|
|
1757
|
+
limit: int = typer.Option(25, "--limit", help="Number of results"),
|
|
1758
|
+
start: int = typer.Option(0, "--start", help="Pagination start"),
|
|
1759
|
+
expand: str | None = typer.Option(None, "--expand", help="Comma separated expansions"),
|
|
1760
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1761
|
+
) -> None:
|
|
1762
|
+
parent_ctx = _parent_context(ctx)
|
|
1763
|
+
started_at = datetime.now(UTC)
|
|
1764
|
+
exit_code = 0
|
|
1765
|
+
server_key: str = ""
|
|
1766
|
+
try:
|
|
1767
|
+
settings = load_settings(strict=True)
|
|
1768
|
+
server_key = _normalise_server(server, settings)
|
|
1769
|
+
cli_ctx = _get_cli_ctx(parent_ctx)
|
|
1770
|
+
client = _build_content_client(settings, server_key, cli_ctx.api_version)
|
|
1771
|
+
expand_list = _expand_option(expand)
|
|
1772
|
+
payload = client.search_cql(cql, limit=limit, start=start, expand=expand_list)
|
|
1773
|
+
_emit_json(payload)
|
|
1774
|
+
except Exception as exc:
|
|
1775
|
+
exit_code = 1
|
|
1776
|
+
logger.exception("content.search failed", extra={"cql": cql})
|
|
1777
|
+
# Provide targeted guidance for CQL auth failures
|
|
1778
|
+
from .errors import ConfluenceAuthError # local import to avoid top-level cycles
|
|
1779
|
+
|
|
1780
|
+
if isinstance(exc, ConfluenceAuthError):
|
|
1781
|
+
typer.echo("Error: Authentication failed for CQL search.", err=True)
|
|
1782
|
+
typer.echo("Hint: We default to Basic auth (VDS_USERNAME/PASSWORD). If using token, ensure:", err=True)
|
|
1783
|
+
typer.echo(" - Token has permission to use CQL (/rest/api/*search)", err=True)
|
|
1784
|
+
typer.echo(" - Or set VDS_USERNAME/VDS_PASSWORD to use Basic auth", err=True)
|
|
1785
|
+
typer.echo(
|
|
1786
|
+
" - Quick check: curl -u \"$VDS_USERNAME:$VDS_PASSWORD\" 'http://confluence.digital.vn/rest/api/search?cql=type=page&limit=1'",
|
|
1787
|
+
err=True,
|
|
1788
|
+
)
|
|
1789
|
+
else:
|
|
1790
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1791
|
+
finally:
|
|
1792
|
+
finished_at = datetime.now(UTC)
|
|
1793
|
+
_record_summary(
|
|
1794
|
+
parent_ctx,
|
|
1795
|
+
command="content.search",
|
|
1796
|
+
args=_content_summary_args(cql=cql, limit=limit, start=start),
|
|
1797
|
+
server=server_key,
|
|
1798
|
+
exit_code=exit_code,
|
|
1799
|
+
started_at=started_at,
|
|
1800
|
+
finished_at=finished_at,
|
|
1801
|
+
)
|
|
1802
|
+
if exit_code != 0:
|
|
1803
|
+
raise typer.Exit(code=exit_code) from None
|
|
1804
|
+
|
|
1805
|
+
|
|
1806
|
+
@content_app.command("templates")
|
|
1807
|
+
def content_templates(
|
|
1808
|
+
ctx: typer.Context,
|
|
1809
|
+
space_key: str | None = typer.Option(None, "--space", help="Space key"),
|
|
1810
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1811
|
+
) -> None:
|
|
1812
|
+
parent_ctx = _parent_context(ctx)
|
|
1813
|
+
started_at = datetime.now(UTC)
|
|
1814
|
+
try:
|
|
1815
|
+
settings = load_settings(strict=True)
|
|
1816
|
+
server_key = _normalise_server(server, settings)
|
|
1817
|
+
cli_ctx = _get_cli_ctx(parent_ctx)
|
|
1818
|
+
client = _build_content_client(settings, server_key, cli_ctx.api_version)
|
|
1819
|
+
if hasattr(client, "list_templates"):
|
|
1820
|
+
payload = client.list_templates(space_key=space_key)
|
|
1821
|
+
else:
|
|
1822
|
+
payload = {"templates": [], "message": "Templates API not available for this server"}
|
|
1823
|
+
_emit_json(payload)
|
|
1824
|
+
except Exception as exc:
|
|
1825
|
+
logger.exception("content.templates failed", extra={"space": space_key})
|
|
1826
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1827
|
+
raise typer.Exit(code=1) from None
|
|
1828
|
+
else:
|
|
1829
|
+
finished_at = datetime.now(UTC)
|
|
1830
|
+
_record_summary(
|
|
1831
|
+
parent_ctx,
|
|
1832
|
+
command="content.templates",
|
|
1833
|
+
args=_content_summary_args(space=space_key or "*"),
|
|
1834
|
+
server=server_key,
|
|
1835
|
+
exit_code=0,
|
|
1836
|
+
started_at=started_at,
|
|
1837
|
+
finished_at=finished_at,
|
|
1838
|
+
)
|
|
1839
|
+
|
|
1840
|
+
|
|
1841
|
+
@content_app.command("create-page")
|
|
1842
|
+
def content_create_page(
|
|
1843
|
+
ctx: typer.Context,
|
|
1844
|
+
space_key: str = typer.Option(..., "--space", "-S", help="Space key to create the page in"),
|
|
1845
|
+
title: str = typer.Option(..., "--title", "-t", help="Page title"),
|
|
1846
|
+
body_file: Path = typer.Option(
|
|
1847
|
+
..., "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
|
|
1848
|
+
),
|
|
1849
|
+
parent_id: str | None = typer.Option(None, "--parent", help="Ancestor page id"),
|
|
1850
|
+
representation: str = typer.Option(
|
|
1851
|
+
"storage", "--representation", help="Content representation (storage, wiki, view)"
|
|
1852
|
+
),
|
|
1853
|
+
status: str = typer.Option("current", "--status", help="Target page status (current, draft)"),
|
|
1854
|
+
notify_watchers: bool = typer.Option(
|
|
1855
|
+
False, "--notify-watchers/--no-notify-watchers", help="Send notifications to watchers"
|
|
1856
|
+
),
|
|
1857
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1858
|
+
) -> None:
|
|
1859
|
+
parent_ctx = _parent_context(ctx)
|
|
1860
|
+
_ensure_v1_api(parent_ctx, "content.create-page")
|
|
1861
|
+
started_at = datetime.now(UTC)
|
|
1862
|
+
try:
|
|
1863
|
+
settings = load_settings(strict=True)
|
|
1864
|
+
server_key = _normalise_server(server, settings)
|
|
1865
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
1866
|
+
body = _read_text_file(body_file)
|
|
1867
|
+
payload = client.create_page(
|
|
1868
|
+
space_key=space_key,
|
|
1869
|
+
title=title,
|
|
1870
|
+
body=body,
|
|
1871
|
+
parent_id=parent_id,
|
|
1872
|
+
representation=representation,
|
|
1873
|
+
status=status,
|
|
1874
|
+
notify_watchers=notify_watchers,
|
|
1875
|
+
)
|
|
1876
|
+
_emit_json(payload)
|
|
1877
|
+
except Exception as exc:
|
|
1878
|
+
logger.exception("content.create-page failed", extra={"space": space_key, "title": title})
|
|
1879
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1880
|
+
raise typer.Exit(code=1) from None
|
|
1881
|
+
else:
|
|
1882
|
+
finished_at = datetime.now(UTC)
|
|
1883
|
+
_record_summary(
|
|
1884
|
+
parent_ctx,
|
|
1885
|
+
command="content.create-page",
|
|
1886
|
+
args=[f"space={space_key}", f"title={title}"],
|
|
1887
|
+
server=server_key,
|
|
1888
|
+
exit_code=0,
|
|
1889
|
+
started_at=started_at,
|
|
1890
|
+
finished_at=finished_at,
|
|
1891
|
+
)
|
|
1892
|
+
|
|
1893
|
+
|
|
1894
|
+
@content_app.command("update-page")
|
|
1895
|
+
def content_update_page(
|
|
1896
|
+
ctx: typer.Context,
|
|
1897
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
1898
|
+
title: str | None = typer.Option(None, "--title", help="New page title"),
|
|
1899
|
+
body_file: Path | None = typer.Option(
|
|
1900
|
+
None, "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
|
|
1901
|
+
),
|
|
1902
|
+
space_key: str | None = typer.Option(None, "--space", help="Move page to this space"),
|
|
1903
|
+
version: int | None = typer.Option(None, "--version", help="Explicit version number"),
|
|
1904
|
+
representation: str = typer.Option("storage", "--representation", help="Content representation for --body-file"),
|
|
1905
|
+
minor_edit: bool = typer.Option(False, "--minor-edit/--no-minor-edit", help="Mark version as minor edit"),
|
|
1906
|
+
message: str | None = typer.Option(None, "--message", help="Version comment"),
|
|
1907
|
+
status: str = typer.Option("current", "--status", help="Target page status"),
|
|
1908
|
+
notify_watchers: bool = typer.Option(
|
|
1909
|
+
False, "--notify-watchers/--no-notify-watchers", help="Send notifications to watchers"
|
|
1910
|
+
),
|
|
1911
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1912
|
+
) -> None:
|
|
1913
|
+
parent_ctx = _parent_context(ctx)
|
|
1914
|
+
_ensure_v1_api(parent_ctx, "content.update-page")
|
|
1915
|
+
if body_file is None and title is None and space_key is None:
|
|
1916
|
+
raise typer.BadParameter("Provide at least one of --title, --body-file or --space")
|
|
1917
|
+
body = _read_text_file(body_file) if body_file else None
|
|
1918
|
+
started_at = datetime.now(UTC)
|
|
1919
|
+
try:
|
|
1920
|
+
settings = load_settings(strict=True)
|
|
1921
|
+
server_key = _normalise_server(server, settings)
|
|
1922
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
1923
|
+
payload = client.update_page(
|
|
1924
|
+
page_id,
|
|
1925
|
+
title=title,
|
|
1926
|
+
body=body,
|
|
1927
|
+
space_key=space_key,
|
|
1928
|
+
representation=representation,
|
|
1929
|
+
version=version,
|
|
1930
|
+
minor_edit=minor_edit,
|
|
1931
|
+
message=message,
|
|
1932
|
+
status=status,
|
|
1933
|
+
notify_watchers=notify_watchers,
|
|
1934
|
+
)
|
|
1935
|
+
_emit_json(payload)
|
|
1936
|
+
except Exception as exc:
|
|
1937
|
+
logger.exception("content.update-page failed", extra={"page_id": page_id})
|
|
1938
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1939
|
+
raise typer.Exit(code=1) from None
|
|
1940
|
+
else:
|
|
1941
|
+
finished_at = datetime.now(UTC)
|
|
1942
|
+
args = [page_id]
|
|
1943
|
+
if version is not None:
|
|
1944
|
+
args.append(f"version={version}")
|
|
1945
|
+
if title is not None:
|
|
1946
|
+
args.append(f"title={title}")
|
|
1947
|
+
_record_summary(
|
|
1948
|
+
parent_ctx,
|
|
1949
|
+
command="content.update-page",
|
|
1950
|
+
args=args,
|
|
1951
|
+
server=server_key,
|
|
1952
|
+
exit_code=0,
|
|
1953
|
+
started_at=started_at,
|
|
1954
|
+
finished_at=finished_at,
|
|
1955
|
+
)
|
|
1956
|
+
|
|
1957
|
+
|
|
1958
|
+
@content_app.command("delete-page")
|
|
1959
|
+
def content_delete_page(
|
|
1960
|
+
ctx: typer.Context,
|
|
1961
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
1962
|
+
status: str = typer.Option("current", "--status", help="Specify current or draft"),
|
|
1963
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
1964
|
+
) -> None:
|
|
1965
|
+
parent_ctx = _parent_context(ctx)
|
|
1966
|
+
_ensure_v1_api(parent_ctx, "content.delete-page")
|
|
1967
|
+
started_at = datetime.now(UTC)
|
|
1968
|
+
try:
|
|
1969
|
+
settings = load_settings(strict=True)
|
|
1970
|
+
server_key = _normalise_server(server, settings)
|
|
1971
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
1972
|
+
client.delete_page(page_id, status=status)
|
|
1973
|
+
typer.echo(f"Deleted page {page_id} (status={status})")
|
|
1974
|
+
except Exception as exc:
|
|
1975
|
+
logger.exception("content.delete-page failed", extra={"page_id": page_id})
|
|
1976
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1977
|
+
raise typer.Exit(code=1) from None
|
|
1978
|
+
else:
|
|
1979
|
+
finished_at = datetime.now(UTC)
|
|
1980
|
+
_record_summary(
|
|
1981
|
+
parent_ctx,
|
|
1982
|
+
command="content.delete-page",
|
|
1983
|
+
args=[page_id, f"status={status}"],
|
|
1984
|
+
server=server_key,
|
|
1985
|
+
exit_code=0,
|
|
1986
|
+
started_at=started_at,
|
|
1987
|
+
finished_at=finished_at,
|
|
1988
|
+
)
|
|
1989
|
+
|
|
1990
|
+
|
|
1991
|
+
@content_app.command("add-attachment")
|
|
1992
|
+
def content_add_attachment(
|
|
1993
|
+
ctx: typer.Context,
|
|
1994
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
1995
|
+
file_path: Path = typer.Option(..., "--file", exists=True, file_okay=True, dir_okay=False, help="File to upload"),
|
|
1996
|
+
filename: str | None = typer.Option(None, "--filename", help="Override uploaded filename"),
|
|
1997
|
+
content_type: str | None = typer.Option(None, "--content-type", help="MIME type for the attachment"),
|
|
1998
|
+
comment: str | None = typer.Option(None, "--comment", help="Attachment comment"),
|
|
1999
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2000
|
+
) -> None:
|
|
2001
|
+
parent_ctx = _parent_context(ctx)
|
|
2002
|
+
_ensure_v1_api(parent_ctx, "content.add-attachment")
|
|
2003
|
+
started_at = datetime.now(UTC)
|
|
2004
|
+
try:
|
|
2005
|
+
settings = load_settings(strict=True)
|
|
2006
|
+
server_key = _normalise_server(server, settings)
|
|
2007
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
2008
|
+
payload = client.upload_attachment(
|
|
2009
|
+
page_id,
|
|
2010
|
+
str(file_path),
|
|
2011
|
+
filename=filename,
|
|
2012
|
+
content_type=content_type,
|
|
2013
|
+
comment=comment,
|
|
2014
|
+
)
|
|
2015
|
+
_emit_json(payload)
|
|
2016
|
+
except Exception as exc:
|
|
2017
|
+
logger.exception("content.add-attachment failed", extra={"page_id": page_id, "file": str(file_path)})
|
|
2018
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2019
|
+
raise typer.Exit(code=1) from None
|
|
2020
|
+
else:
|
|
2021
|
+
finished_at = datetime.now(UTC)
|
|
2022
|
+
_record_summary(
|
|
2023
|
+
parent_ctx,
|
|
2024
|
+
command="content.add-attachment",
|
|
2025
|
+
args=[page_id, file_path.name],
|
|
2026
|
+
server=server_key,
|
|
2027
|
+
exit_code=0,
|
|
2028
|
+
started_at=started_at,
|
|
2029
|
+
finished_at=finished_at,
|
|
2030
|
+
)
|
|
2031
|
+
|
|
2032
|
+
|
|
2033
|
+
@content_app.command("update-attachment")
|
|
2034
|
+
def content_update_attachment(
|
|
2035
|
+
ctx: typer.Context,
|
|
2036
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
2037
|
+
attachment_id: str = typer.Argument(..., help="Attachment ID"),
|
|
2038
|
+
file_path: Path = typer.Option(..., "--file", exists=True, file_okay=True, dir_okay=False, help="Replacement file"),
|
|
2039
|
+
filename: str | None = typer.Option(None, "--filename", help="Override uploaded filename"),
|
|
2040
|
+
content_type: str | None = typer.Option(None, "--content-type", help="MIME type for the attachment"),
|
|
2041
|
+
comment: str | None = typer.Option(None, "--comment", help="Attachment comment"),
|
|
2042
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2043
|
+
) -> None:
|
|
2044
|
+
parent_ctx = _parent_context(ctx)
|
|
2045
|
+
_ensure_v1_api(parent_ctx, "content.update-attachment")
|
|
2046
|
+
started_at = datetime.now(UTC)
|
|
2047
|
+
try:
|
|
2048
|
+
settings = load_settings(strict=True)
|
|
2049
|
+
server_key = _normalise_server(server, settings)
|
|
2050
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
2051
|
+
payload = client.update_attachment(
|
|
2052
|
+
page_id,
|
|
2053
|
+
attachment_id,
|
|
2054
|
+
str(file_path),
|
|
2055
|
+
filename=filename,
|
|
2056
|
+
content_type=content_type,
|
|
2057
|
+
comment=comment,
|
|
2058
|
+
)
|
|
2059
|
+
_emit_json(payload)
|
|
2060
|
+
except Exception as exc:
|
|
2061
|
+
logger.exception("content.update-attachment failed", extra={"page_id": page_id, "attachment_id": attachment_id})
|
|
2062
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2063
|
+
raise typer.Exit(code=1) from None
|
|
2064
|
+
else:
|
|
2065
|
+
finished_at = datetime.now(UTC)
|
|
2066
|
+
_record_summary(
|
|
2067
|
+
parent_ctx,
|
|
2068
|
+
command="content.update-attachment",
|
|
2069
|
+
args=[page_id, attachment_id],
|
|
2070
|
+
server=server_key,
|
|
2071
|
+
exit_code=0,
|
|
2072
|
+
started_at=started_at,
|
|
2073
|
+
finished_at=finished_at,
|
|
2074
|
+
)
|
|
2075
|
+
|
|
2076
|
+
|
|
2077
|
+
@content_app.command("delete-attachment")
|
|
2078
|
+
def content_delete_attachment(
|
|
2079
|
+
ctx: typer.Context,
|
|
2080
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
2081
|
+
attachment_id: str | None = typer.Argument(None, help="Attachment ID"),
|
|
2082
|
+
version: int | None = typer.Option(None, "--version", help="Delete a specific attachment version"),
|
|
2083
|
+
filename: str | None = typer.Option(None, "--filename", help="Attachment filename (delete all versions)"),
|
|
2084
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2085
|
+
) -> None:
|
|
2086
|
+
parent_ctx = _parent_context(ctx)
|
|
2087
|
+
_ensure_v1_api(parent_ctx, "content.delete-attachment")
|
|
2088
|
+
started_at = datetime.now(UTC)
|
|
2089
|
+
try:
|
|
2090
|
+
settings = load_settings(strict=True)
|
|
2091
|
+
server_key = _normalise_server(server, settings)
|
|
2092
|
+
if not attachment_id and not filename:
|
|
2093
|
+
raise typer.BadParameter("Provide either attachment_id or --filename")
|
|
2094
|
+
|
|
2095
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
2096
|
+
client.delete_attachment(page_id, attachment_id=attachment_id, version=version, filename=filename)
|
|
2097
|
+
target = filename or attachment_id
|
|
2098
|
+
typer.echo(f"Deleted attachment {target} from page {page_id}")
|
|
2099
|
+
except Exception as exc:
|
|
2100
|
+
logger.exception("content.delete-attachment failed", extra={"page_id": page_id, "attachment_id": attachment_id})
|
|
2101
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2102
|
+
raise typer.Exit(code=1) from None
|
|
2103
|
+
else:
|
|
2104
|
+
finished_at = datetime.now(UTC)
|
|
2105
|
+
_record_summary(
|
|
2106
|
+
parent_ctx,
|
|
2107
|
+
command="content.delete-attachment",
|
|
2108
|
+
args=[page_id, filename or attachment_id or "<unspecified>", f"version={version}" if version is not None else "all_versions"],
|
|
2109
|
+
server=server_key,
|
|
2110
|
+
exit_code=0,
|
|
2111
|
+
started_at=started_at,
|
|
2112
|
+
finished_at=finished_at,
|
|
2113
|
+
)
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
@content_app.command("ancestors")
|
|
2117
|
+
def content_ancestors(
|
|
2118
|
+
ctx: typer.Context,
|
|
2119
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
2120
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2121
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
2122
|
+
) -> None:
|
|
2123
|
+
"""Get page ancestors (parent pages)."""
|
|
2124
|
+
parent_ctx = _parent_context(ctx)
|
|
2125
|
+
started_at = datetime.now(UTC)
|
|
2126
|
+
exit_code = 0
|
|
2127
|
+
result_payload: dict[str, Any] = {}
|
|
2128
|
+
server_key: str | None = None
|
|
2129
|
+
|
|
2130
|
+
try:
|
|
2131
|
+
settings = load_settings(strict=True)
|
|
2132
|
+
server_key = _normalise_server(server, settings)
|
|
2133
|
+
client = _build_http_client(settings, server_key)
|
|
2134
|
+
|
|
2135
|
+
result = client.get_page_ancestors(page_id)
|
|
2136
|
+
result_payload = {"page_id": page_id, "count": len(result), "ancestors": result}
|
|
2137
|
+
|
|
2138
|
+
if json_output or not sys.stdout.isatty():
|
|
2139
|
+
_emit_json(result_payload)
|
|
2140
|
+
else:
|
|
2141
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
2142
|
+
|
|
2143
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
2144
|
+
exit_code = 1
|
|
2145
|
+
logger.error("content.ancestors_failed: %s", exc)
|
|
2146
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2147
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
2148
|
+
exit_code = 1
|
|
2149
|
+
logger.exception("content.ancestors_unexpected_error")
|
|
2150
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2151
|
+
finally:
|
|
2152
|
+
finished_at = datetime.now(UTC)
|
|
2153
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
2154
|
+
_record_summary(
|
|
2155
|
+
parent_ctx,
|
|
2156
|
+
command="content.ancestors",
|
|
2157
|
+
args=[page_id],
|
|
2158
|
+
server=server_label,
|
|
2159
|
+
exit_code=exit_code,
|
|
2160
|
+
started_at=started_at,
|
|
2161
|
+
finished_at=finished_at,
|
|
2162
|
+
)
|
|
2163
|
+
if exit_code != 0:
|
|
2164
|
+
raise typer.Exit(code=exit_code) from None
|
|
2165
|
+
|
|
2166
|
+
|
|
2167
|
+
@content_app.command("move")
|
|
2168
|
+
def content_move(
|
|
2169
|
+
ctx: typer.Context,
|
|
2170
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
2171
|
+
target_title: str = typer.Option(..., "--target-title", help="Target page title to move under"),
|
|
2172
|
+
position: str = typer.Option("append", "--position", help="Position: append, before, after"),
|
|
2173
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2174
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm move operation"),
|
|
2175
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
2176
|
+
) -> None:
|
|
2177
|
+
"""Move page to a new location."""
|
|
2178
|
+
parent_ctx = _parent_context(ctx)
|
|
2179
|
+
started_at = datetime.now(UTC)
|
|
2180
|
+
exit_code = 0
|
|
2181
|
+
result_payload: dict[str, Any] = {}
|
|
2182
|
+
server_key: str | None = None
|
|
2183
|
+
|
|
2184
|
+
try:
|
|
2185
|
+
if not yes:
|
|
2186
|
+
raise typer.BadParameter("--yes required for move operation")
|
|
2187
|
+
if position not in ("append", "before", "after"):
|
|
2188
|
+
raise typer.BadParameter("--position must be one of: append, before, after")
|
|
2189
|
+
|
|
2190
|
+
settings = load_settings(strict=True)
|
|
2191
|
+
server_key = _normalise_server(server, settings)
|
|
2192
|
+
client = _build_http_client(settings, server_key)
|
|
2193
|
+
|
|
2194
|
+
result = client.move_page(page_id, target_title, position=position)
|
|
2195
|
+
result_payload = {"page_id": page_id, "target_title": target_title, "position": position, "result": result}
|
|
2196
|
+
|
|
2197
|
+
if json_output or not sys.stdout.isatty():
|
|
2198
|
+
_emit_json(result_payload)
|
|
2199
|
+
else:
|
|
2200
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
2201
|
+
|
|
2202
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
2203
|
+
exit_code = 1
|
|
2204
|
+
logger.error("content.move_failed: %s", exc)
|
|
2205
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2206
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
2207
|
+
exit_code = 1
|
|
2208
|
+
logger.exception("content.move_unexpected_error")
|
|
2209
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2210
|
+
finally:
|
|
2211
|
+
finished_at = datetime.now(UTC)
|
|
2212
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
2213
|
+
_record_summary(
|
|
2214
|
+
parent_ctx,
|
|
2215
|
+
command="content.move",
|
|
2216
|
+
args=[page_id, f"target={target_title}", f"position={position}"],
|
|
2217
|
+
server=server_label,
|
|
2218
|
+
exit_code=exit_code,
|
|
2219
|
+
started_at=started_at,
|
|
2220
|
+
finished_at=finished_at,
|
|
2221
|
+
)
|
|
2222
|
+
if exit_code != 0:
|
|
2223
|
+
raise typer.Exit(code=exit_code) from None
|
|
2224
|
+
|
|
2225
|
+
|
|
2226
|
+
@content_app.command("tables")
|
|
2227
|
+
def content_tables(
|
|
2228
|
+
ctx: typer.Context,
|
|
2229
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
2230
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2231
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
2232
|
+
) -> None:
|
|
2233
|
+
"""Extract tables from page."""
|
|
2234
|
+
parent_ctx = _parent_context(ctx)
|
|
2235
|
+
started_at = datetime.now(UTC)
|
|
2236
|
+
exit_code = 0
|
|
2237
|
+
result_payload: dict[str, Any] = {}
|
|
2238
|
+
server_key: str | None = None
|
|
2239
|
+
|
|
2240
|
+
try:
|
|
2241
|
+
settings = load_settings(strict=True)
|
|
2242
|
+
server_key = _normalise_server(server, settings)
|
|
2243
|
+
client = _build_http_client(settings, server_key)
|
|
2244
|
+
|
|
2245
|
+
result = client.get_tables_from_page(page_id)
|
|
2246
|
+
result_payload = {"page_id": page_id, "count": len(result), "tables": result}
|
|
2247
|
+
|
|
2248
|
+
if json_output or not sys.stdout.isatty():
|
|
2249
|
+
_emit_json(result_payload)
|
|
2250
|
+
else:
|
|
2251
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
2252
|
+
|
|
2253
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
2254
|
+
exit_code = 1
|
|
2255
|
+
logger.error("content.tables_failed: %s", exc)
|
|
2256
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2257
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
2258
|
+
exit_code = 1
|
|
2259
|
+
logger.exception("content.tables_unexpected_error")
|
|
2260
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2261
|
+
finally:
|
|
2262
|
+
finished_at = datetime.now(UTC)
|
|
2263
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
2264
|
+
_record_summary(
|
|
2265
|
+
parent_ctx,
|
|
2266
|
+
command="content.tables",
|
|
2267
|
+
args=[page_id],
|
|
2268
|
+
server=server_label,
|
|
2269
|
+
exit_code=exit_code,
|
|
2270
|
+
started_at=started_at,
|
|
2271
|
+
finished_at=finished_at,
|
|
2272
|
+
)
|
|
2273
|
+
if exit_code != 0:
|
|
2274
|
+
raise typer.Exit(code=exit_code) from None
|
|
2275
|
+
|
|
2276
|
+
|
|
2277
|
+
@content_app.command("regex")
|
|
2278
|
+
def content_regex(
|
|
2279
|
+
ctx: typer.Context,
|
|
2280
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
2281
|
+
pattern: str = typer.Option(..., "--pattern", help="Regex pattern to match"),
|
|
2282
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2283
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
2284
|
+
) -> None:
|
|
2285
|
+
"""Extract regex matches from page."""
|
|
2286
|
+
parent_ctx = _parent_context(ctx)
|
|
2287
|
+
started_at = datetime.now(UTC)
|
|
2288
|
+
exit_code = 0
|
|
2289
|
+
result_payload: dict[str, Any] = {}
|
|
2290
|
+
server_key: str | None = None
|
|
2291
|
+
|
|
2292
|
+
try:
|
|
2293
|
+
settings = load_settings(strict=True)
|
|
2294
|
+
server_key = _normalise_server(server, settings)
|
|
2295
|
+
client = _build_http_client(settings, server_key)
|
|
2296
|
+
|
|
2297
|
+
result = client.scrap_regex_from_page(page_id, pattern)
|
|
2298
|
+
result_payload = {"page_id": page_id, "pattern": pattern, "count": len(result), "matches": result}
|
|
2299
|
+
|
|
2300
|
+
if json_output or not sys.stdout.isatty():
|
|
2301
|
+
_emit_json(result_payload)
|
|
2302
|
+
else:
|
|
2303
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
2304
|
+
|
|
2305
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
2306
|
+
exit_code = 1
|
|
2307
|
+
logger.error("content.regex_failed: %s", exc)
|
|
2308
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2309
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
2310
|
+
exit_code = 1
|
|
2311
|
+
logger.exception("content.regex_unexpected_error")
|
|
2312
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2313
|
+
finally:
|
|
2314
|
+
finished_at = datetime.now(UTC)
|
|
2315
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
2316
|
+
_record_summary(
|
|
2317
|
+
parent_ctx,
|
|
2318
|
+
command="content.regex",
|
|
2319
|
+
args=[page_id, f"pattern={pattern}"],
|
|
2320
|
+
server=server_label,
|
|
2321
|
+
exit_code=exit_code,
|
|
2322
|
+
started_at=started_at,
|
|
2323
|
+
finished_at=finished_at,
|
|
2324
|
+
)
|
|
2325
|
+
if exit_code != 0:
|
|
2326
|
+
raise typer.Exit(code=exit_code) from None
|
|
2327
|
+
|
|
2328
|
+
|
|
2329
|
+
@content_app.command("restrictions")
|
|
2330
|
+
def content_restrictions(
|
|
2331
|
+
ctx: typer.Context,
|
|
2332
|
+
page_id: str = typer.Argument(..., help="Confluence page ID"),
|
|
2333
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2334
|
+
json_output: bool = typer.Option(False, "--json", help="Emit JSON payload"),
|
|
2335
|
+
) -> None:
|
|
2336
|
+
"""Get all restrictions for content."""
|
|
2337
|
+
parent_ctx = _parent_context(ctx)
|
|
2338
|
+
started_at = datetime.now(UTC)
|
|
2339
|
+
exit_code = 0
|
|
2340
|
+
result_payload: dict[str, Any] = {}
|
|
2341
|
+
server_key: str | None = None
|
|
2342
|
+
|
|
2343
|
+
try:
|
|
2344
|
+
settings = load_settings(strict=True)
|
|
2345
|
+
server_key = _normalise_server(server, settings)
|
|
2346
|
+
client = _build_http_client(settings, server_key)
|
|
2347
|
+
|
|
2348
|
+
result = client.get_all_restrictions_for_content(page_id)
|
|
2349
|
+
result_payload = {"page_id": page_id, "restrictions": result}
|
|
2350
|
+
|
|
2351
|
+
if json_output or not sys.stdout.isatty():
|
|
2352
|
+
_emit_json(result_payload)
|
|
2353
|
+
else:
|
|
2354
|
+
typer.echo(json.dumps(result_payload, indent=2, sort_keys=True))
|
|
2355
|
+
|
|
2356
|
+
except (ConfluenceClientError, typer.BadParameter) as exc:
|
|
2357
|
+
exit_code = 1
|
|
2358
|
+
logger.error("content.restrictions_failed: %s", exc)
|
|
2359
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2360
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
2361
|
+
exit_code = 1
|
|
2362
|
+
logger.exception("content.restrictions_unexpected_error")
|
|
2363
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2364
|
+
finally:
|
|
2365
|
+
finished_at = datetime.now(UTC)
|
|
2366
|
+
server_label = server_key or (server.lower() if server else "internal")
|
|
2367
|
+
_record_summary(
|
|
2368
|
+
parent_ctx,
|
|
2369
|
+
command="content.restrictions",
|
|
2370
|
+
args=[page_id],
|
|
2371
|
+
server=server_label,
|
|
2372
|
+
exit_code=exit_code,
|
|
2373
|
+
started_at=started_at,
|
|
2374
|
+
finished_at=finished_at,
|
|
2375
|
+
)
|
|
2376
|
+
if exit_code != 0:
|
|
2377
|
+
raise typer.Exit(code=exit_code) from None
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
@content_app.command("create-template")
|
|
2381
|
+
def content_create_template(
|
|
2382
|
+
ctx: typer.Context,
|
|
2383
|
+
name: str = typer.Option(..., "--name", "-n", help="Template name"),
|
|
2384
|
+
body_file: Path = typer.Option(
|
|
2385
|
+
..., "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
|
|
2386
|
+
),
|
|
2387
|
+
template_type: str = typer.Option("page", "--type", help="Template type (page or other supported types)"),
|
|
2388
|
+
space_key: str | None = typer.Option(None, "--space", help="Limit template to space key"),
|
|
2389
|
+
description: str | None = typer.Option(None, "--description", help="Template description"),
|
|
2390
|
+
representation: str = typer.Option("storage", "--representation", help="Content representation"),
|
|
2391
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2392
|
+
) -> None:
|
|
2393
|
+
parent_ctx = _parent_context(ctx)
|
|
2394
|
+
_ensure_v1_api(parent_ctx, "content.create-template")
|
|
2395
|
+
started_at = datetime.now(UTC)
|
|
2396
|
+
try:
|
|
2397
|
+
settings = load_settings(strict=True)
|
|
2398
|
+
server_key = _normalise_server(server, settings)
|
|
2399
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
2400
|
+
body = _read_text_file(body_file)
|
|
2401
|
+
payload = client.create_template(
|
|
2402
|
+
name=name,
|
|
2403
|
+
body=body,
|
|
2404
|
+
template_type=template_type,
|
|
2405
|
+
space_key=space_key,
|
|
2406
|
+
description=description,
|
|
2407
|
+
representation=representation,
|
|
2408
|
+
)
|
|
2409
|
+
_emit_json(payload)
|
|
2410
|
+
except Exception as exc:
|
|
2411
|
+
logger.exception("content.create-template failed", extra={"name": name, "space": space_key})
|
|
2412
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2413
|
+
raise typer.Exit(code=1) from None
|
|
2414
|
+
else:
|
|
2415
|
+
finished_at = datetime.now(UTC)
|
|
2416
|
+
args = [name, f"type={template_type}"]
|
|
2417
|
+
if space_key:
|
|
2418
|
+
args.append(f"space={space_key}")
|
|
2419
|
+
_record_summary(
|
|
2420
|
+
parent_ctx,
|
|
2421
|
+
command="content.create-template",
|
|
2422
|
+
args=args,
|
|
2423
|
+
server=server_key,
|
|
2424
|
+
exit_code=0,
|
|
2425
|
+
started_at=started_at,
|
|
2426
|
+
finished_at=finished_at,
|
|
2427
|
+
)
|
|
2428
|
+
|
|
2429
|
+
|
|
2430
|
+
@content_app.command("update-template")
|
|
2431
|
+
def content_update_template(
|
|
2432
|
+
ctx: typer.Context,
|
|
2433
|
+
template_id: str = typer.Argument(..., help="Template identifier"),
|
|
2434
|
+
name: str | None = typer.Option(None, "--name", help="Template name"),
|
|
2435
|
+
body_file: Path | None = typer.Option(
|
|
2436
|
+
None, "--body-file", exists=True, file_okay=True, dir_okay=False, help="Path to storage-format body"
|
|
2437
|
+
),
|
|
2438
|
+
description: str | None = typer.Option(None, "--description", help="Template description"),
|
|
2439
|
+
template_type: str | None = typer.Option(None, "--type", help="Template type"),
|
|
2440
|
+
representation: str = typer.Option("storage", "--representation", help="Representation for --body-file"),
|
|
2441
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2442
|
+
) -> None:
|
|
2443
|
+
parent_ctx = _parent_context(ctx)
|
|
2444
|
+
_ensure_v1_api(parent_ctx, "content.update-template")
|
|
2445
|
+
if name is None and body_file is None and description is None and template_type is None:
|
|
2446
|
+
raise typer.BadParameter("Provide at least one of --name, --body-file, --description, or --type")
|
|
2447
|
+
body = _read_text_file(body_file) if body_file else None
|
|
2448
|
+
started_at = datetime.now(UTC)
|
|
2449
|
+
try:
|
|
2450
|
+
settings = load_settings(strict=True)
|
|
2451
|
+
server_key = _normalise_server(server, settings)
|
|
2452
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
2453
|
+
payload = client.update_template(
|
|
2454
|
+
template_id,
|
|
2455
|
+
name=name,
|
|
2456
|
+
body=body,
|
|
2457
|
+
description=description,
|
|
2458
|
+
representation=representation,
|
|
2459
|
+
template_type=template_type,
|
|
2460
|
+
)
|
|
2461
|
+
_emit_json(payload)
|
|
2462
|
+
except Exception as exc:
|
|
2463
|
+
logger.exception("content.update-template failed", extra={"template_id": template_id})
|
|
2464
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2465
|
+
raise typer.Exit(code=1) from None
|
|
2466
|
+
else:
|
|
2467
|
+
finished_at = datetime.now(UTC)
|
|
2468
|
+
args = [template_id]
|
|
2469
|
+
if name:
|
|
2470
|
+
args.append(f"name={name}")
|
|
2471
|
+
_record_summary(
|
|
2472
|
+
parent_ctx,
|
|
2473
|
+
command="content.update-template",
|
|
2474
|
+
args=args,
|
|
2475
|
+
server=server_key,
|
|
2476
|
+
exit_code=0,
|
|
2477
|
+
started_at=started_at,
|
|
2478
|
+
finished_at=finished_at,
|
|
2479
|
+
)
|
|
2480
|
+
|
|
2481
|
+
|
|
2482
|
+
@content_app.command("delete-template")
|
|
2483
|
+
def content_delete_template(
|
|
2484
|
+
ctx: typer.Context,
|
|
2485
|
+
template_id: str = typer.Argument(..., help="Template identifier"),
|
|
2486
|
+
server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
|
|
2487
|
+
) -> None:
|
|
2488
|
+
parent_ctx = _parent_context(ctx)
|
|
2489
|
+
_ensure_v1_api(parent_ctx, "content.delete-template")
|
|
2490
|
+
started_at = datetime.now(UTC)
|
|
2491
|
+
try:
|
|
2492
|
+
settings = load_settings(strict=True)
|
|
2493
|
+
server_key = _normalise_server(server, settings)
|
|
2494
|
+
client = ContentClient(_build_http_client(settings, server_key))
|
|
2495
|
+
client.delete_template(template_id)
|
|
2496
|
+
typer.echo(f"Deleted template {template_id}")
|
|
2497
|
+
except Exception as exc:
|
|
2498
|
+
logger.exception("content.delete-template failed", extra={"template_id": template_id})
|
|
2499
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2500
|
+
raise typer.Exit(code=1) from None
|
|
2501
|
+
else:
|
|
2502
|
+
finished_at = datetime.now(UTC)
|
|
2503
|
+
_record_summary(
|
|
2504
|
+
parent_ctx,
|
|
2505
|
+
command="content.delete-template",
|
|
2506
|
+
args=[template_id],
|
|
2507
|
+
server=server_key,
|
|
2508
|
+
exit_code=0,
|
|
2509
|
+
started_at=started_at,
|
|
2510
|
+
finished_at=finished_at,
|
|
2511
|
+
)
|
|
2512
|
+
|
|
2513
|
+
|
|
2514
|
+
# ---------------------------------------------------------------------------
|
|
2515
|
+
# Diagnostics
|
|
2516
|
+
# ---------------------------------------------------------------------------
|
|
2517
|
+
|
|
2518
|
+
|
|
2519
|
+
@app.command("env")
|
|
2520
|
+
def cmd_env(ctx: typer.Context) -> None:
|
|
2521
|
+
settings = load_settings(strict=False)
|
|
2522
|
+
typer.echo(
|
|
2523
|
+
f"Default server: {settings.default_server}\n"
|
|
2524
|
+
f"Internal URL: {settings.internal_url}\n"
|
|
2525
|
+
f"External URL: {settings.external_url}\n"
|
|
2526
|
+
f"Internal token set: {bool(settings.internal_token)}\n"
|
|
2527
|
+
f"External token set: {bool(settings.external_token)}"
|
|
2528
|
+
)
|
|
2529
|
+
|
|
2530
|
+
|
|
2531
|
+
if __name__ == "__main__":
|
|
2532
|
+
app()
|