hap-cli 0.8.3__tar.gz → 0.8.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {hap_cli-0.8.3 → hap_cli-0.8.5}/PKG-INFO +9 -2
- {hap_cli-0.8.3/hap_cli → hap_cli-0.8.5}/README.md +8 -1
- {hap_cli-0.8.3 → hap_cli-0.8.5/hap_cli}/README.md +8 -1
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/README_CN.md +8 -1
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/__init__.py +1 -1
- hap_cli-0.8.5/hap_cli/__main__.py +11 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/app_cmd.py +0 -31
- hap_cli-0.8.5/hap_cli/commands/update_cmd.py +101 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/app.py +0 -49
- hap_cli-0.8.5/hap_cli/core/update.py +294 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/hap_cli.py +18 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/locale/messages.json +0 -21
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_core.py +1 -1
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_org_id_cli.py +0 -1
- hap_cli-0.8.5/hap_cli/tests/test_update.py +423 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli.egg-info/PKG-INFO +9 -2
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli.egg-info/SOURCES.txt +4 -1
- hap_cli-0.8.3/hap_cli/tests/test_app_restore.py +0 -123
- {hap_cli-0.8.3 → hap_cli-0.8.5}/MANIFEST.in +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/app_live_tests/config.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/app_live_tests/harness.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/app_live_tests/state.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/app_editor_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/context.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/department.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/_hapmeta/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/_hapmeta/control_type_codes.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/apply.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/componentlower.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/editspec.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/errors.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/fieldlower.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/jsonschema_mini.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/models.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/ops.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/planner.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/editor/reader.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/group.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/page.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/post.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/record.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/role.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/session.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/worksheet.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/core/worksheet_templates.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/i18n.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_app_editor.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_app_export_usage.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_bug_036430.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_destructive_confirm_cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_multi_profile.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_org_approval_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_record_batch_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_record_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_semantic_types_registry.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_workflow_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/tests/test_worksheet_v3_semantic_cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/__main__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/_wftest.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/charts.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/cleanup.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/compiler.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/config.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/errors.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/executor.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/fields.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/hap.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/recording/console.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/recording/report.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/schema.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/seed/template.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/selftest.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/smoke.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/step.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/steps.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/store.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/setup.cfg +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/setup.py +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/skills/hap-cli-app-editor/scripts/editspec/component.schema.json +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/skills/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/skills/hap-cli-app-editor/scripts/editspec/envelope.schema.json +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/skills/hap-cli-app-editor/scripts/editspec/field.schema.json +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.8.3 → hap_cli-0.8.5}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hap-cli
|
|
3
|
-
Version: 0.8.
|
|
3
|
+
Version: 0.8.5
|
|
4
4
|
Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
|
|
5
5
|
Author: hap-cli
|
|
6
6
|
License: Apache-2.0
|
|
@@ -155,7 +155,7 @@ discover every subcommand.
|
|
|
155
155
|
| `app create-section / edit-section / delete-section` | Section management |
|
|
156
156
|
| `app sort-sections / sort-worksheets` | Reorder sections / a section's worksheets |
|
|
157
157
|
| `app create-item` | Batch-create empty worksheets / custom pages |
|
|
158
|
-
| `app backup /
|
|
158
|
+
| `app backup / export / exports / backup-logs` | Backup & export |
|
|
159
159
|
| `app usage / logs` | Statistics and operation logs |
|
|
160
160
|
| `app role …` | Role CRUD, permissions, members, leave-all, applications |
|
|
161
161
|
| `app optionset …` | Option set CRUD and cross-app moves |
|
|
@@ -313,6 +313,13 @@ body, and a redacted `Authorization` header (first 16 chars). `password`
|
|
|
313
313
|
and other sensitive keys are masked. `HAP_LOG_LEVEL=DEBUG` temporarily
|
|
314
314
|
overrides the persisted level.
|
|
315
315
|
|
|
316
|
+
### update — keep the CLI current
|
|
317
|
+
|
|
318
|
+
| Command | Description |
|
|
319
|
+
|---|---|
|
|
320
|
+
| `update` | Check PyPI for a newer release and upgrade in place, using the same tool the CLI was installed with (pip / pipx / uv) |
|
|
321
|
+
| `update --check` | Only report whether a newer version exists, without installing |
|
|
322
|
+
|
|
316
323
|
---
|
|
317
324
|
|
|
318
325
|
## Default app & organization
|
|
@@ -126,7 +126,7 @@ discover every subcommand.
|
|
|
126
126
|
| `app create-section / edit-section / delete-section` | Section management |
|
|
127
127
|
| `app sort-sections / sort-worksheets` | Reorder sections / a section's worksheets |
|
|
128
128
|
| `app create-item` | Batch-create empty worksheets / custom pages |
|
|
129
|
-
| `app backup /
|
|
129
|
+
| `app backup / export / exports / backup-logs` | Backup & export |
|
|
130
130
|
| `app usage / logs` | Statistics and operation logs |
|
|
131
131
|
| `app role …` | Role CRUD, permissions, members, leave-all, applications |
|
|
132
132
|
| `app optionset …` | Option set CRUD and cross-app moves |
|
|
@@ -284,6 +284,13 @@ body, and a redacted `Authorization` header (first 16 chars). `password`
|
|
|
284
284
|
and other sensitive keys are masked. `HAP_LOG_LEVEL=DEBUG` temporarily
|
|
285
285
|
overrides the persisted level.
|
|
286
286
|
|
|
287
|
+
### update — keep the CLI current
|
|
288
|
+
|
|
289
|
+
| Command | Description |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `update` | Check PyPI for a newer release and upgrade in place, using the same tool the CLI was installed with (pip / pipx / uv) |
|
|
292
|
+
| `update --check` | Only report whether a newer version exists, without installing |
|
|
293
|
+
|
|
287
294
|
---
|
|
288
295
|
|
|
289
296
|
## Default app & organization
|
|
@@ -126,7 +126,7 @@ discover every subcommand.
|
|
|
126
126
|
| `app create-section / edit-section / delete-section` | Section management |
|
|
127
127
|
| `app sort-sections / sort-worksheets` | Reorder sections / a section's worksheets |
|
|
128
128
|
| `app create-item` | Batch-create empty worksheets / custom pages |
|
|
129
|
-
| `app backup /
|
|
129
|
+
| `app backup / export / exports / backup-logs` | Backup & export |
|
|
130
130
|
| `app usage / logs` | Statistics and operation logs |
|
|
131
131
|
| `app role …` | Role CRUD, permissions, members, leave-all, applications |
|
|
132
132
|
| `app optionset …` | Option set CRUD and cross-app moves |
|
|
@@ -284,6 +284,13 @@ body, and a redacted `Authorization` header (first 16 chars). `password`
|
|
|
284
284
|
and other sensitive keys are masked. `HAP_LOG_LEVEL=DEBUG` temporarily
|
|
285
285
|
overrides the persisted level.
|
|
286
286
|
|
|
287
|
+
### update — keep the CLI current
|
|
288
|
+
|
|
289
|
+
| Command | Description |
|
|
290
|
+
|---|---|
|
|
291
|
+
| `update` | Check PyPI for a newer release and upgrade in place, using the same tool the CLI was installed with (pip / pipx / uv) |
|
|
292
|
+
| `update --check` | Only report whether a newer version exists, without installing |
|
|
293
|
+
|
|
287
294
|
---
|
|
288
295
|
|
|
289
296
|
## Default app & organization
|
|
@@ -117,7 +117,7 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
117
117
|
| `app create-section / edit-section / delete-section` | 分组管理 |
|
|
118
118
|
| `app sort-sections / sort-worksheets` | 重排分组 / 分组内工作表顺序 |
|
|
119
119
|
| `app create-item` | 批量创建空白工作表/自定义页面 |
|
|
120
|
-
| `app backup /
|
|
120
|
+
| `app backup / export / exports / backup-logs` | 备份与导出 |
|
|
121
121
|
| `app usage / logs` | 使用统计与操作日志 |
|
|
122
122
|
| `app role …` | 角色 CRUD、权限、成员、leave-all、申请 |
|
|
123
123
|
| `app optionset …` | 选项集 CRUD 与跨应用迁移 |
|
|
@@ -260,6 +260,13 @@ hap --json worksheet record list WORKSHEET_ID
|
|
|
260
260
|
|
|
261
261
|
日志默认写入 `~/.hap-cli/hap-cli.log`,10 MB × 3 滚动。开启后每次 API 调用以 `INFO` 级别记录 URL、请求体、响应体,以及脱敏后的 `Authorization` 头(仅前 16 字符);`password` 等敏感字段会被自动打码。临时调试可设 `HAP_LOG_LEVEL=DEBUG`。
|
|
262
262
|
|
|
263
|
+
### update — 升级 CLI
|
|
264
|
+
|
|
265
|
+
| 命令 | 说明 |
|
|
266
|
+
|---|---|
|
|
267
|
+
| `update` | 检查 PyPI 上的最新版本,有新版则用安装时的工具(pip / pipx / uv)原地升级 |
|
|
268
|
+
| `update --check` | 只检查是否有新版本,不安装 |
|
|
269
|
+
|
|
263
270
|
---
|
|
264
271
|
|
|
265
272
|
## 默认应用与组织
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Allow ``python -m hap_cli`` — used by the detached background
|
|
2
|
+
update child (``hap update --background``), which is spawned with
|
|
3
|
+
``sys.executable -m hap_cli`` so it always runs in the same
|
|
4
|
+
environment as the parent process regardless of how the ``hap``
|
|
5
|
+
console script was installed.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from hap_cli.hap_cli import main
|
|
9
|
+
|
|
10
|
+
if __name__ == "__main__":
|
|
11
|
+
main()
|
|
@@ -410,37 +410,6 @@ def app_backup(ctx, app_id, org_id, no_data):
|
|
|
410
410
|
ctx.handle_error(e)
|
|
411
411
|
|
|
412
412
|
|
|
413
|
-
@app.command("restore")
|
|
414
|
-
@click.argument("app_id")
|
|
415
|
-
@org_id_option()
|
|
416
|
-
@click.option("--backup-id", "-b", default="", help="Restore from an existing backup (its ID)")
|
|
417
|
-
@click.option("--file-url", default="", help="Restore from an uploaded backup file (its URL)")
|
|
418
|
-
@click.option("--file-name", default="", help="File name to show for an uploaded backup file")
|
|
419
|
-
@click.option("--no-data", is_flag=True, help="Restore without data")
|
|
420
|
-
@click.option("--as-new", is_flag=True, help="Restore as a new app")
|
|
421
|
-
@pass_context
|
|
422
|
-
def app_restore(ctx, app_id, org_id, backup_id, file_url, file_name, no_data, as_new):
|
|
423
|
-
"""Restore an application, either from an existing backup or an uploaded file.
|
|
424
|
-
|
|
425
|
-
Provide exactly one source: --backup-id (an existing backup) or
|
|
426
|
-
--file-url (an uploaded backup file).
|
|
427
|
-
"""
|
|
428
|
-
try:
|
|
429
|
-
if bool(backup_id) == bool(file_url):
|
|
430
|
-
raise click.UsageError(
|
|
431
|
-
"Provide exactly one of --backup-id or --file-url."
|
|
432
|
-
)
|
|
433
|
-
session = ctx.get_session()
|
|
434
|
-
data = app_mod.restore_app(
|
|
435
|
-
session, app_id, require_api_project_id(session, org_id),
|
|
436
|
-
backup_id=backup_id, file_url=file_url, file_name=file_name,
|
|
437
|
-
contain_data=not no_data, is_restore_new=as_new,
|
|
438
|
-
)
|
|
439
|
-
ctx.output(data, lambda d: click.echo(f"Restore started: {d}"))
|
|
440
|
-
except Exception as e:
|
|
441
|
-
ctx.handle_error(e)
|
|
442
|
-
|
|
443
|
-
|
|
444
413
|
@app.command("export")
|
|
445
414
|
@click.argument("app_ids", nargs=-1, required=True)
|
|
446
415
|
@org_id_option()
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""CLI command for self-updating hap-cli from PyPI.
|
|
2
|
+
|
|
3
|
+
The hidden ``--background`` flag is the entry point for the detached
|
|
4
|
+
child process spawned by the exit-time hook in ``hap_cli.py`` — it is
|
|
5
|
+
internal plumbing and intentionally absent from ``--help`` and docs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import time
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
|
|
12
|
+
from hap_cli.context import pass_context
|
|
13
|
+
from hap_cli.core import update as upd
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@click.command("update")
|
|
17
|
+
@click.option(
|
|
18
|
+
"--check",
|
|
19
|
+
"check_only",
|
|
20
|
+
is_flag=True,
|
|
21
|
+
help="Only check whether a newer version exists; do not install.",
|
|
22
|
+
)
|
|
23
|
+
@click.option("--background", is_flag=True, hidden=True)
|
|
24
|
+
@pass_context
|
|
25
|
+
def update(ctx, check_only, background):
|
|
26
|
+
"""Upgrade hap-cli to the latest version from PyPI.
|
|
27
|
+
|
|
28
|
+
Checks for a newer release and installs it using the same tool the
|
|
29
|
+
CLI was installed with (pip, pipx, or uv). Installs that are not
|
|
30
|
+
managed from PyPI are left untouched.
|
|
31
|
+
"""
|
|
32
|
+
if background:
|
|
33
|
+
# Detached child: silent check-and-upgrade; results go to the
|
|
34
|
+
# state file and are surfaced on the next foreground run.
|
|
35
|
+
upd.run_background_update()
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
from hap_cli import __version__
|
|
39
|
+
|
|
40
|
+
kind = upd.get_install_kind()
|
|
41
|
+
if kind in ("editable", "unknown"):
|
|
42
|
+
ctx.output(
|
|
43
|
+
{"current": __version__, "install": kind, "updated": False},
|
|
44
|
+
lambda d: click.echo(
|
|
45
|
+
"This copy of hap-cli is not managed from PyPI "
|
|
46
|
+
f"(current version {__version__}) — nothing to update."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
latest = upd.fetch_latest_version()
|
|
52
|
+
if latest is None:
|
|
53
|
+
ctx.handle_error(Exception("Could not reach PyPI to check for updates."))
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
if not upd.is_newer(latest, __version__):
|
|
57
|
+
upd.save_state(
|
|
58
|
+
{
|
|
59
|
+
"last_check_at": time.time(),
|
|
60
|
+
"latest_version": latest,
|
|
61
|
+
"last_result": "up_to_date",
|
|
62
|
+
"notified": True,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
ctx.output(
|
|
66
|
+
{"current": __version__, "latest": latest, "updated": False},
|
|
67
|
+
lambda d: click.echo(
|
|
68
|
+
f"hap-cli {__version__} is already the latest version."
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if check_only:
|
|
74
|
+
ctx.output(
|
|
75
|
+
{"current": __version__, "latest": latest, "updated": False},
|
|
76
|
+
lambda d: click.echo(
|
|
77
|
+
f"A newer version is available: {latest} "
|
|
78
|
+
f"(current {__version__}). Run `hap update` to upgrade."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
click.echo(f"Upgrading hap-cli {__version__} -> {latest} ...")
|
|
84
|
+
ok, output = upd.perform_upgrade(kind)
|
|
85
|
+
upd.save_state(
|
|
86
|
+
{
|
|
87
|
+
"last_check_at": time.time(),
|
|
88
|
+
"latest_version": latest,
|
|
89
|
+
"last_result": "upgraded" if ok else "upgrade_failed",
|
|
90
|
+
"upgraded_to": latest if ok else None,
|
|
91
|
+
"notified": True,
|
|
92
|
+
}
|
|
93
|
+
)
|
|
94
|
+
if ok:
|
|
95
|
+
ctx.output(
|
|
96
|
+
{"current": __version__, "latest": latest, "updated": True},
|
|
97
|
+
lambda d: click.echo(f"Upgraded to hap-cli {latest}."),
|
|
98
|
+
)
|
|
99
|
+
else:
|
|
100
|
+
click.echo(output, err=True)
|
|
101
|
+
ctx.handle_error(Exception(f"Upgrade to {latest} failed."))
|
|
@@ -620,55 +620,6 @@ def backup_app(
|
|
|
620
620
|
)
|
|
621
621
|
|
|
622
622
|
|
|
623
|
-
def restore_app(
|
|
624
|
-
session: Session,
|
|
625
|
-
app_id: str,
|
|
626
|
-
project_id: str,
|
|
627
|
-
backup_id: str = "",
|
|
628
|
-
file_url: str = "",
|
|
629
|
-
file_name: str = "",
|
|
630
|
-
contain_data: bool = True,
|
|
631
|
-
is_restore_new: bool = False,
|
|
632
|
-
auto_end_maintain: bool = False,
|
|
633
|
-
backup_current_version: bool = False,
|
|
634
|
-
) -> dict[str, Any]:
|
|
635
|
-
"""Restore an application from a backup log id or an uploaded file.
|
|
636
|
-
|
|
637
|
-
The HAP UI has two mutually exclusive restore modes; the API field
|
|
638
|
-
set differs between them, so we must send only one mode's fields:
|
|
639
|
-
|
|
640
|
-
- Backup-log restore (``src/pages/AppSettings/components/
|
|
641
|
-
appBackupRestore/components/BackupFiles.jsx``): sends ``id`` (the
|
|
642
|
-
backup-restore operation-log id) plus ``autoEndMaintain``,
|
|
643
|
-
``backupCurrentVersion`` and ``isRestoreNew`` — no ``fileUrl``.
|
|
644
|
-
- Uploaded-file restore (``.../BackupFromFiles.jsx``): sends
|
|
645
|
-
``fileUrl`` + ``fileName`` + ``containData`` plus the same three
|
|
646
|
-
flags — no ``id``.
|
|
647
|
-
|
|
648
|
-
Sending both at once (as an earlier wrapper did, with all three
|
|
649
|
-
forced ``required``) made the command impossible to invoke. Exactly
|
|
650
|
-
one of ``backup_id`` / ``file_url`` must be supplied.
|
|
651
|
-
"""
|
|
652
|
-
if not backup_id and not file_url:
|
|
653
|
-
raise ValueError("restore requires either backup_id or file_url")
|
|
654
|
-
|
|
655
|
-
params: dict[str, Any] = {
|
|
656
|
-
"appId": app_id,
|
|
657
|
-
"projectId": project_id,
|
|
658
|
-
"autoEndMaintain": auto_end_maintain,
|
|
659
|
-
"backupCurrentVersion": backup_current_version,
|
|
660
|
-
"isRestoreNew": is_restore_new,
|
|
661
|
-
}
|
|
662
|
-
if backup_id:
|
|
663
|
-
# `id` is the backup-restore operation-log id (named `id` in the API).
|
|
664
|
-
params["id"] = backup_id
|
|
665
|
-
else:
|
|
666
|
-
params["fileUrl"] = file_url
|
|
667
|
-
params["fileName"] = file_name
|
|
668
|
-
params["containData"] = contain_data
|
|
669
|
-
return session.api_call("AppManagement", "Restore", params)
|
|
670
|
-
|
|
671
|
-
|
|
672
623
|
def export_apps(
|
|
673
624
|
session: Session,
|
|
674
625
|
project_id: str,
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
"""Self-update support for hap-cli.
|
|
2
|
+
|
|
3
|
+
Two entry paths share this module:
|
|
4
|
+
|
|
5
|
+
- ``hap update`` — synchronous, user-invoked check/upgrade.
|
|
6
|
+
- ``hap update --background`` — hidden; spawned as a detached child
|
|
7
|
+
process when a command exits and the last check is older than
|
|
8
|
+
``UPDATE_CHECK_INTERVAL_SECONDS``. It checks PyPI, auto-upgrades if
|
|
9
|
+
newer, and records the outcome in the state file so the *next* run
|
|
10
|
+
can surface a one-line notice.
|
|
11
|
+
|
|
12
|
+
Editable installs (PEP 660 ``direct_url.json`` with
|
|
13
|
+
``dir_info.editable: true``) and unrecognized install origins disable
|
|
14
|
+
the feature entirely — they are by definition not managed via PyPI.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import subprocess
|
|
22
|
+
import sys
|
|
23
|
+
import time
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
import requests
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("hap_cli.update")
|
|
29
|
+
|
|
30
|
+
PYPI_PACKAGE = "hap-cli"
|
|
31
|
+
PYPI_JSON_URL = f"https://pypi.org/pypi/{PYPI_PACKAGE}/json"
|
|
32
|
+
|
|
33
|
+
# Deliberately short while the tool is iterating quickly; once releases
|
|
34
|
+
# stabilize, bump this in a regular release (it ships with the code, so
|
|
35
|
+
# every upgrade can retune the cadence).
|
|
36
|
+
UPDATE_CHECK_INTERVAL_SECONDS = 3600
|
|
37
|
+
|
|
38
|
+
# Only final releases (digits and dots) are auto-upgrade candidates;
|
|
39
|
+
# pre/dev/post markers ("0.9.0rc1", "1.0.dev2") are skipped.
|
|
40
|
+
_FINAL_RELEASE_RE = re.compile(r"^\d+(\.\d+)*$")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _state_file():
|
|
44
|
+
# Resolve lazily through the session module so tests (and
|
|
45
|
+
# HAP_CLI_HOME) that repoint CONFIG_DIR are honored.
|
|
46
|
+
from hap_cli.core import session
|
|
47
|
+
|
|
48
|
+
return session.CONFIG_DIR / "update_check.json"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ── Install-kind detection ───────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def get_install_kind() -> str:
|
|
55
|
+
"""Classify how this copy of hap-cli was installed.
|
|
56
|
+
|
|
57
|
+
Returns one of ``editable`` / ``pip`` / ``pipx`` / ``uv`` /
|
|
58
|
+
``unknown``. Only ``pip`` / ``pipx`` / ``uv`` are upgradable from
|
|
59
|
+
PyPI; the other two disable the update feature.
|
|
60
|
+
"""
|
|
61
|
+
import importlib.metadata
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
dist = importlib.metadata.distribution(PYPI_PACKAGE)
|
|
65
|
+
direct_url_text = dist.read_text("direct_url.json")
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.warning("install-kind detection failed: %s", e)
|
|
68
|
+
return "unknown"
|
|
69
|
+
|
|
70
|
+
if direct_url_text:
|
|
71
|
+
try:
|
|
72
|
+
direct_url = json.loads(direct_url_text)
|
|
73
|
+
except ValueError:
|
|
74
|
+
return "unknown"
|
|
75
|
+
if direct_url.get("dir_info", {}).get("editable"):
|
|
76
|
+
return "editable"
|
|
77
|
+
# Installed from a direct URL/path but not editable — we don't
|
|
78
|
+
# know how to keep it current, so stay hands-off.
|
|
79
|
+
return "unknown"
|
|
80
|
+
|
|
81
|
+
prefix = sys.prefix.replace(os.sep, "/")
|
|
82
|
+
if "pipx/venvs" in prefix:
|
|
83
|
+
return "pipx"
|
|
84
|
+
if "uv/tools" in prefix:
|
|
85
|
+
return "uv"
|
|
86
|
+
return "pip"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_upgrade_command(kind: str) -> Optional[list]:
|
|
90
|
+
"""Upgrade argv for an install kind, or None if not upgradable."""
|
|
91
|
+
if kind == "pip":
|
|
92
|
+
return [sys.executable, "-m", "pip", "install", "--upgrade", PYPI_PACKAGE]
|
|
93
|
+
if kind == "pipx":
|
|
94
|
+
return ["pipx", "upgrade", PYPI_PACKAGE]
|
|
95
|
+
if kind == "uv":
|
|
96
|
+
return ["uv", "tool", "upgrade", PYPI_PACKAGE]
|
|
97
|
+
return None
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# ── Version handling ─────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _version_tuple(version: str) -> tuple:
|
|
104
|
+
return tuple(int(part) for part in version.split("."))
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def is_newer(latest: str, current: str) -> bool:
|
|
108
|
+
"""True if ``latest`` is strictly newer than ``current``."""
|
|
109
|
+
try:
|
|
110
|
+
return _version_tuple(latest) > _version_tuple(current)
|
|
111
|
+
except ValueError:
|
|
112
|
+
logger.warning(
|
|
113
|
+
"cannot compare versions latest=%r current=%r", latest, current
|
|
114
|
+
)
|
|
115
|
+
return False
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def fetch_latest_version(timeout: int = 5) -> Optional[str]:
|
|
119
|
+
"""Latest final (non-prerelease, non-yanked) version on PyPI.
|
|
120
|
+
|
|
121
|
+
Returns None on any network/parse error — callers treat that as
|
|
122
|
+
"couldn't check", never as an error surfaced to the user.
|
|
123
|
+
"""
|
|
124
|
+
from hap_cli.core.logger import format_response_for_log
|
|
125
|
+
|
|
126
|
+
logger.info("HAP GET %s params={} Authorization=<none>", PYPI_JSON_URL)
|
|
127
|
+
try:
|
|
128
|
+
response = requests.get(PYPI_JSON_URL, timeout=timeout)
|
|
129
|
+
logger.info(
|
|
130
|
+
"HAP GET %s -> %d %s",
|
|
131
|
+
PYPI_JSON_URL,
|
|
132
|
+
response.status_code,
|
|
133
|
+
format_response_for_log(getattr(response, "text", "") or ""),
|
|
134
|
+
)
|
|
135
|
+
response.raise_for_status()
|
|
136
|
+
payload = response.json()
|
|
137
|
+
except Exception as e:
|
|
138
|
+
logger.warning("PyPI version check failed: %s", e)
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
candidates = []
|
|
142
|
+
for version, files in (payload.get("releases") or {}).items():
|
|
143
|
+
if not _FINAL_RELEASE_RE.match(version):
|
|
144
|
+
continue
|
|
145
|
+
if not files or all(f.get("yanked") for f in files):
|
|
146
|
+
continue
|
|
147
|
+
candidates.append(version)
|
|
148
|
+
if not candidates:
|
|
149
|
+
return None
|
|
150
|
+
return max(candidates, key=_version_tuple)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
# ── State file ───────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def load_state() -> dict:
|
|
157
|
+
"""Read the update-check state; empty dict if missing/corrupt."""
|
|
158
|
+
try:
|
|
159
|
+
with open(_state_file(), encoding="utf-8") as f:
|
|
160
|
+
data = json.load(f)
|
|
161
|
+
return data if isinstance(data, dict) else {}
|
|
162
|
+
except FileNotFoundError:
|
|
163
|
+
return {}
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.warning("corrupt update state file, resetting: %s", e)
|
|
166
|
+
return {}
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def save_state(state: dict) -> None:
|
|
170
|
+
"""Atomically persist the update-check state."""
|
|
171
|
+
path = _state_file()
|
|
172
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
173
|
+
tmp = path.with_suffix(".json.tmp")
|
|
174
|
+
tmp.write_text(json.dumps(state, ensure_ascii=False), encoding="utf-8")
|
|
175
|
+
os.replace(tmp, path)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ── Background check orchestration ───────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def maybe_spawn_background_check() -> None:
|
|
182
|
+
"""Spawn a detached background update check if it is due.
|
|
183
|
+
|
|
184
|
+
Called when a CLI command finishes. The timestamp is bumped
|
|
185
|
+
*before* spawning so rapid consecutive commands cannot stack
|
|
186
|
+
duplicate children — at worst a failed child costs one interval.
|
|
187
|
+
"""
|
|
188
|
+
kind = get_install_kind()
|
|
189
|
+
if kind in ("editable", "unknown"):
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
state = load_state()
|
|
193
|
+
last = state.get("last_check_at") or 0
|
|
194
|
+
now = time.time()
|
|
195
|
+
if now - last < UPDATE_CHECK_INTERVAL_SECONDS:
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
state["last_check_at"] = now
|
|
199
|
+
save_state(state)
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
subprocess.Popen(
|
|
203
|
+
[sys.executable, "-m", "hap_cli", "update", "--background"],
|
|
204
|
+
stdin=subprocess.DEVNULL,
|
|
205
|
+
stdout=subprocess.DEVNULL,
|
|
206
|
+
stderr=subprocess.DEVNULL,
|
|
207
|
+
start_new_session=True,
|
|
208
|
+
)
|
|
209
|
+
logger.info("spawned background update check (install kind: %s)", kind)
|
|
210
|
+
except Exception as e:
|
|
211
|
+
logger.warning("failed to spawn background update check: %s", e)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def perform_upgrade(kind: str) -> tuple:
|
|
215
|
+
"""Run the upgrade command for ``kind``. Returns (ok, output)."""
|
|
216
|
+
cmd = build_upgrade_command(kind)
|
|
217
|
+
if cmd is None:
|
|
218
|
+
return False, f"no upgrade command for install kind {kind!r}"
|
|
219
|
+
logger.info("running upgrade command: %s", " ".join(cmd))
|
|
220
|
+
try:
|
|
221
|
+
proc = subprocess.run(
|
|
222
|
+
cmd, capture_output=True, text=True, timeout=600
|
|
223
|
+
)
|
|
224
|
+
except Exception as e:
|
|
225
|
+
logger.error("upgrade command failed to run: %s", e)
|
|
226
|
+
return False, str(e)
|
|
227
|
+
output = (proc.stdout or "") + (proc.stderr or "")
|
|
228
|
+
logger.info("upgrade command exited %d", proc.returncode)
|
|
229
|
+
return proc.returncode == 0, output
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def run_background_update() -> None:
|
|
233
|
+
"""Body of the hidden ``hap update --background`` child process.
|
|
234
|
+
|
|
235
|
+
Silent by design: outcomes go to the state file (surfaced by the
|
|
236
|
+
next foreground run) and the log, never to the terminal.
|
|
237
|
+
"""
|
|
238
|
+
from hap_cli import __version__
|
|
239
|
+
|
|
240
|
+
state = load_state()
|
|
241
|
+
state["last_check_at"] = time.time()
|
|
242
|
+
|
|
243
|
+
latest = fetch_latest_version()
|
|
244
|
+
if latest is None:
|
|
245
|
+
state["last_result"] = "check_failed"
|
|
246
|
+
save_state(state)
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
state["latest_version"] = latest
|
|
250
|
+
if not is_newer(latest, __version__):
|
|
251
|
+
state["last_result"] = "up_to_date"
|
|
252
|
+
save_state(state)
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
ok, output = perform_upgrade(get_install_kind())
|
|
256
|
+
if ok:
|
|
257
|
+
state["last_result"] = "upgraded"
|
|
258
|
+
state["upgraded_to"] = latest
|
|
259
|
+
else:
|
|
260
|
+
state["last_result"] = "upgrade_failed"
|
|
261
|
+
state["error"] = output[-500:]
|
|
262
|
+
logger.error("background upgrade to %s failed: %s", latest, output)
|
|
263
|
+
state["notified"] = False
|
|
264
|
+
save_state(state)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def consume_startup_notice() -> Optional[str]:
|
|
268
|
+
"""One-shot user notice from the last background run, if any.
|
|
269
|
+
|
|
270
|
+
Returns the message and marks it notified so it is shown exactly
|
|
271
|
+
once. Silent results (check_failed / up_to_date) return None.
|
|
272
|
+
"""
|
|
273
|
+
state = load_state()
|
|
274
|
+
if state.get("notified", True):
|
|
275
|
+
return None
|
|
276
|
+
|
|
277
|
+
result = state.get("last_result")
|
|
278
|
+
if result == "upgraded":
|
|
279
|
+
msg = (
|
|
280
|
+
f"hap-cli was auto-upgraded to {state.get('upgraded_to')} "
|
|
281
|
+
"(takes effect from this run)."
|
|
282
|
+
)
|
|
283
|
+
elif result == "upgrade_failed":
|
|
284
|
+
latest = state.get("latest_version") or "a newer version"
|
|
285
|
+
msg = (
|
|
286
|
+
f"Automatic upgrade to {latest} failed — "
|
|
287
|
+
"run `hap update` to upgrade manually."
|
|
288
|
+
)
|
|
289
|
+
else:
|
|
290
|
+
return None
|
|
291
|
+
|
|
292
|
+
state["notified"] = True
|
|
293
|
+
save_state(state)
|
|
294
|
+
return msg
|
|
@@ -41,6 +41,11 @@ from hap_cli.commands.region_cmd import region
|
|
|
41
41
|
from hap_cli.commands.icon_cmd import icon
|
|
42
42
|
from hap_cli.commands.upload_cmd import upload
|
|
43
43
|
from hap_cli.commands.app_editor_cmd import app_editor
|
|
44
|
+
from hap_cli.commands.update_cmd import update as update_cmd
|
|
45
|
+
from hap_cli.core.update import (
|
|
46
|
+
consume_startup_notice,
|
|
47
|
+
maybe_spawn_background_check,
|
|
48
|
+
)
|
|
44
49
|
|
|
45
50
|
|
|
46
51
|
def clone_group(source, *, name=None, hidden=False):
|
|
@@ -71,6 +76,18 @@ def cli(ctx, json_mode, profile):
|
|
|
71
76
|
ctx.obj.json_mode = json_mode
|
|
72
77
|
ctx.obj.profile = profile
|
|
73
78
|
|
|
79
|
+
# Self-update plumbing. Both halves are local-file-only on the hot
|
|
80
|
+
# path: the notice reads a small JSON state file, and the spawn hook
|
|
81
|
+
# only forks a detached child when the last check is older than the
|
|
82
|
+
# interval (the child does the actual network I/O). Skipped for
|
|
83
|
+
# `update` itself (it manages the state directly) and `repl`
|
|
84
|
+
# (cli() re-runs per REPL line).
|
|
85
|
+
notice = consume_startup_notice()
|
|
86
|
+
if notice:
|
|
87
|
+
click.echo(notice, err=True)
|
|
88
|
+
if ctx.invoked_subcommand not in ("update", "repl"):
|
|
89
|
+
ctx.call_on_close(maybe_spawn_background_check)
|
|
90
|
+
|
|
74
91
|
|
|
75
92
|
# Register command groups
|
|
76
93
|
app.add_command(clone_group(role, name="role"))
|
|
@@ -127,6 +144,7 @@ cli.add_command(icon)
|
|
|
127
144
|
cli.add_command(upload)
|
|
128
145
|
cli.add_command(config_group)
|
|
129
146
|
cli.add_command(app_editor)
|
|
147
|
+
cli.add_command(update_cmd)
|
|
130
148
|
|
|
131
149
|
|
|
132
150
|
# Hidden top-level alias for the old `hap completion` command. The
|
|
@@ -900,27 +900,6 @@
|
|
|
900
900
|
"app optionset update --options-json": {
|
|
901
901
|
"en": "Option list. [JSON value] [required]"
|
|
902
902
|
},
|
|
903
|
-
"app restore": {
|
|
904
|
-
"en": "Restore an application from backup."
|
|
905
|
-
},
|
|
906
|
-
"app restore --as-new": {
|
|
907
|
-
"en": "Restore as a new app"
|
|
908
|
-
},
|
|
909
|
-
"app restore --backup-id": {
|
|
910
|
-
"en": "Backup ID"
|
|
911
|
-
},
|
|
912
|
-
"app restore --file-name": {
|
|
913
|
-
"en": "Backup file name"
|
|
914
|
-
},
|
|
915
|
-
"app restore --file-url": {
|
|
916
|
-
"en": "Backup file URL"
|
|
917
|
-
},
|
|
918
|
-
"app restore --no-data": {
|
|
919
|
-
"en": "Restore without data"
|
|
920
|
-
},
|
|
921
|
-
"app restore --org-id": {
|
|
922
|
-
"en": "Organization ID"
|
|
923
|
-
},
|
|
924
903
|
"app role": {
|
|
925
904
|
"en": "Role management."
|
|
926
905
|
},
|