hap-cli 0.7.1__tar.gz → 0.7.2__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.7.1 → hap_cli-0.7.2}/MANIFEST.in +5 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/PKG-INFO +1 -1
- {hap_cli-0.7.1 → hap_cli-0.7.2}/app_live_tests/harness.py +7 -4
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/__init__.py +1 -1
- hap_cli-0.7.2/hap_cli/commands/app_editor_cmd.py +137 -0
- hap_cli-0.7.2/hap_cli/core/editor/__init__.py +12 -0
- hap_cli-0.7.2/hap_cli/core/editor/_hapmeta/__init__.py +5 -0
- hap_cli-0.7.2/hap_cli/core/editor/_hapmeta/control_type_codes.py +58 -0
- hap_cli-0.7.2/hap_cli/core/editor/apply.py +51 -0
- hap_cli-0.7.2/hap_cli/core/editor/componentlower.py +80 -0
- hap_cli-0.7.2/hap_cli/core/editor/editspec.py +119 -0
- hap_cli-0.7.2/hap_cli/core/editor/errors.py +55 -0
- hap_cli-0.7.2/hap_cli/core/editor/fieldlower.py +66 -0
- hap_cli-0.7.2/hap_cli/core/editor/jsonschema_mini.py +136 -0
- hap_cli-0.7.2/hap_cli/core/editor/models.py +39 -0
- hap_cli-0.7.2/hap_cli/core/editor/ops.py +543 -0
- hap_cli-0.7.2/hap_cli/core/editor/planner.py +53 -0
- hap_cli-0.7.2/hap_cli/core/editor/reader.py +291 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/hap_cli.py +2 -0
- hap_cli-0.7.2/hap_cli/tests/test_app_editor.py +247 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli.egg-info/PKG-INFO +1 -1
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli.egg-info/SOURCES.txt +27 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/setup.py +20 -5
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/application.schema.json +59 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/chatbot.schema.json +52 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/component.schema.json +65 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +55 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/custom-page.schema.json +49 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/envelope.schema.json +86 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/field.schema.json +84 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/node.schema.json +73 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/role.schema.json +90 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/view.schema.json +64 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/workflow.schema.json +60 -0
- hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/worksheet.schema.json +51 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/README.md +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/app_live_tests/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/app_live_tests/__main__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/app_live_tests/config.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/app_live_tests/smoke.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/app_live_tests/state.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/README.md +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/README_CN.md +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/app_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/auth_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/calendar_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/chart_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/chat_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/chatbot_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/config_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/contact_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/department_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/group_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/icon_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/instance_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/node_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/optionset_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/page_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/post_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/record_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/region_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/role_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/upload_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/v3_registry.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/workflow_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/commands/worksheet_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/context.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/action_spec_adapter.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/app.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/auth.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/calendar_mod.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/chart.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/chat.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/chatbot.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/contact.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/control_type_codes.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/department.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/field_normalizer.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/field_spec_adapter.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/filter_translator.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/flow_node.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/global_meta.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/group.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/icon_index.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/instance.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/logger.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/optionset.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/page.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/post.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/record.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/response_crypto.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/role.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/role_perm_builder.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/session.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/token_crypto.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/upload.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/v3/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/v3/dispatcher.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/v3/render.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/v3/schema.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/view_spec_adapter.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/workflow.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/workflow_node_dsl.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/worksheet.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/core/worksheet_templates.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/i18n.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/locale/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/locale/messages.json +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/locale/messages.schema.json +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/conftest.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_auth_prerelease.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_chart.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_config_cmd.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_core.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_full_e2e.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_global_meta.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_i18n.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_approval.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_approval_actions.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_calendar.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_destructive.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_misc.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_post.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_social.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_v3.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_workflow.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_org_id_cli.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_org_id_docs.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_parameter_conventions.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_v3_dispatcher.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_v3_session.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/tests/test_worksheet_crud_cli.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/utils/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/utils/formatting.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/utils/options.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli/utils/parameter_mapping.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli.egg-info/dependency_links.txt +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli.egg-info/entry_points.txt +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli.egg-info/requires.txt +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/hap_cli.egg-info/top_level.txt +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/__main__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/_wftest.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/charts.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/cleanup.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/compiler.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/config.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/errors.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/executor.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/fields.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/hap.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/recording/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/recording/console.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/recording/jsonl.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/recording/mirror.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/recording/report.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/schema.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/seed/__init__.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/seed/cli.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/seed/executor.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/seed/template.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/selftest.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/smoke.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/step.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/steps.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/store.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/live_tests/workflow_dsl.py +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/setup.cfg +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/INDEX.json +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/create_optionset.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/create_role.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/delete_optionset.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/delete_record.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/delete_role.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_app_info.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_details.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_list.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_logs.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_relations.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_regions.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_role_details.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/get_role_list.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/knowledge_search.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
- {hap_cli-0.7.1 → hap_cli-0.7.2}/sources/v3-api-schema/update_optionset.yaml +0 -0
|
@@ -11,3 +11,8 @@ include sources/v3-api-schema/INDEX.json
|
|
|
11
11
|
include hap_cli/README.md
|
|
12
12
|
include hap_cli/README_CN.md
|
|
13
13
|
recursive-include hap_cli/locale *.json
|
|
14
|
+
|
|
15
|
+
# edit-spec schemas are canonical in the hap-cli-app-editor skill; ship them
|
|
16
|
+
# in the sdist so setup.py's build step can mirror them into
|
|
17
|
+
# hap_cli/core/editor/editspec/ for the ``hap app-editor`` runtime loader.
|
|
18
|
+
recursive-include skills/hap-cli-app-editor/scripts/editspec *.json
|
|
@@ -53,11 +53,14 @@ def hap(args: list[str], *, check: bool = True) -> Any:
|
|
|
53
53
|
|
|
54
54
|
|
|
55
55
|
def editor(args: list[str]) -> tuple[int, str, str]:
|
|
56
|
-
"""Run the editor
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
"""Run the editor via the installed CLI: ``hap app-editor <args>``.
|
|
57
|
+
|
|
58
|
+
The editor engine was migrated into hap-cli, so the entry point is now
|
|
59
|
+
the real command (not ``python -m scripts``).
|
|
60
|
+
"""
|
|
61
|
+
argv = [config.HAP_BIN, "app-editor", *args]
|
|
59
62
|
proc = subprocess.run(argv, capture_output=True, text=True,
|
|
60
|
-
|
|
63
|
+
timeout=config.TIMEOUT)
|
|
61
64
|
return proc.returncode, proc.stdout, proc.stderr
|
|
62
65
|
|
|
63
66
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""CLI commands for fine-grained HAP app element editing (``hap app-editor``).
|
|
2
|
+
|
|
3
|
+
Thin Click layer over the editor engine in ``hap_cli.core.editor``:
|
|
4
|
+
edit-spec validation, live inspection, dry-run planning, and applying
|
|
5
|
+
single-element changes (worksheet / field / view / role / custom-action /
|
|
6
|
+
workflow / custom-page / component / chatbot / node / app / section).
|
|
7
|
+
|
|
8
|
+
The edit-spec schemas are documented and shipped with the
|
|
9
|
+
``hap-cli-app-editor`` skill; this command resolves them at runtime.
|
|
10
|
+
"""
|
|
11
|
+
import json
|
|
12
|
+
|
|
13
|
+
import click
|
|
14
|
+
|
|
15
|
+
from hap_cli.context import pass_context
|
|
16
|
+
from hap_cli.core.editor import apply as apply_mod
|
|
17
|
+
from hap_cli.core.editor import editspec, planner
|
|
18
|
+
from hap_cli.core.editor.errors import EditorError, EditSpecError
|
|
19
|
+
from hap_cli.core.editor.reader import AppIndex
|
|
20
|
+
from hap_cli.utils.options import org_id_option
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group("app-editor", help="Fine-grained edit of an existing HAP app's "
|
|
24
|
+
"elements via a structured edit-spec.")
|
|
25
|
+
def app_editor():
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@app_editor.command("validate")
|
|
30
|
+
@click.argument("spec_path", type=click.Path(exists=True, dir_okay=False))
|
|
31
|
+
@pass_context
|
|
32
|
+
def app_editor_validate(ctx, spec_path):
|
|
33
|
+
"""Validate an edit-spec locally (no API calls)."""
|
|
34
|
+
try:
|
|
35
|
+
editspec.load_spec(spec_path)
|
|
36
|
+
except EditSpecError as exc:
|
|
37
|
+
if ctx.json_mode:
|
|
38
|
+
ctx.output({"valid": False, "problems": exc.problems})
|
|
39
|
+
else:
|
|
40
|
+
click.echo("edit-spec invalid:", err=True)
|
|
41
|
+
for p in exc.problems:
|
|
42
|
+
click.echo(f" - {p}", err=True)
|
|
43
|
+
raise SystemExit(2)
|
|
44
|
+
ctx.output({"valid": True}, lambda d: click.echo("edit-spec OK"))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _resolve_app_ref(spec, app_override):
|
|
48
|
+
return app_override or spec.get("app")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app_editor.command("inspect")
|
|
52
|
+
@click.argument("app_ref")
|
|
53
|
+
@org_id_option()
|
|
54
|
+
@pass_context
|
|
55
|
+
def app_editor_inspect(ctx, app_ref, org_id):
|
|
56
|
+
"""Print the live name->id structure of an app (worksheets, views,
|
|
57
|
+
roles, workflows, pages, ...) — author edit-specs against it."""
|
|
58
|
+
try:
|
|
59
|
+
session = ctx.get_session()
|
|
60
|
+
idx = AppIndex.load(session, app_ref, org_id)
|
|
61
|
+
ctx.output(idx.summary(),
|
|
62
|
+
lambda d: click.echo(json.dumps(d, ensure_ascii=False, indent=2)))
|
|
63
|
+
except EditorError as e:
|
|
64
|
+
ctx.handle_error(e)
|
|
65
|
+
except Exception as e: # noqa: BLE001
|
|
66
|
+
ctx.handle_error(e)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@app_editor.command("plan")
|
|
70
|
+
@click.argument("spec_path", type=click.Path(exists=True, dir_okay=False))
|
|
71
|
+
@click.option("--app", "app_override", default="",
|
|
72
|
+
help="Override the spec's target app (id or name).")
|
|
73
|
+
@org_id_option()
|
|
74
|
+
@pass_context
|
|
75
|
+
def app_editor_plan(ctx, spec_path, app_override, org_id):
|
|
76
|
+
"""Dry run: validate, read live structure, and show what apply would do."""
|
|
77
|
+
try:
|
|
78
|
+
spec = editspec.load_spec(spec_path)
|
|
79
|
+
session = ctx.get_session()
|
|
80
|
+
idx = AppIndex.load(session, _resolve_app_ref(spec, app_override), org_id)
|
|
81
|
+
planned = planner.build_plan(spec, idx)
|
|
82
|
+
ctx.output(
|
|
83
|
+
{"ops": [{"index": p.index, "type": p.op.get("type"),
|
|
84
|
+
"actions": [a.description for a in p.actions]}
|
|
85
|
+
for p in planned]},
|
|
86
|
+
lambda d: click.echo(planner.render_plan(planned)))
|
|
87
|
+
except EditSpecError as exc:
|
|
88
|
+
for p in exc.problems:
|
|
89
|
+
click.echo(f" - {p}", err=True)
|
|
90
|
+
raise SystemExit(2)
|
|
91
|
+
except Exception as e: # noqa: BLE001
|
|
92
|
+
ctx.handle_error(e)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@app_editor.command("apply")
|
|
96
|
+
@click.argument("spec_path", type=click.Path(exists=True, dir_okay=False))
|
|
97
|
+
@click.option("--app", "app_override", default="",
|
|
98
|
+
help="Override the spec's target app (id or name).")
|
|
99
|
+
@org_id_option()
|
|
100
|
+
@click.option("--continue", "cont", is_flag=True, default=False,
|
|
101
|
+
help="Keep going after an op fails (default: stop).")
|
|
102
|
+
@pass_context
|
|
103
|
+
def app_editor_apply(ctx, spec_path, app_override, org_id, cont):
|
|
104
|
+
"""Apply an edit-spec against the live app, op by op."""
|
|
105
|
+
try:
|
|
106
|
+
spec = editspec.load_spec(spec_path)
|
|
107
|
+
session = ctx.get_session()
|
|
108
|
+
idx = AppIndex.load(session, _resolve_app_ref(spec, app_override), org_id)
|
|
109
|
+
|
|
110
|
+
def _echo(outcome):
|
|
111
|
+
if not ctx.json_mode:
|
|
112
|
+
mark = "OK " if outcome.status == "ok" else "ERR"
|
|
113
|
+
tail = f" — {outcome.detail}" if outcome.detail else ""
|
|
114
|
+
click.echo(f" [{mark}] [{outcome.index}] {outcome.op_type}{tail}")
|
|
115
|
+
|
|
116
|
+
outcomes = apply_mod.apply_spec(spec, idx, continue_on_error=cont,
|
|
117
|
+
on_outcome=_echo)
|
|
118
|
+
errors = [o for o in outcomes if o.status == "error"]
|
|
119
|
+
ctx.output(
|
|
120
|
+
{"applied": len(outcomes) - len(errors), "total": len(outcomes),
|
|
121
|
+
"failed": len(errors),
|
|
122
|
+
"outcomes": [{"index": o.index, "type": o.op_type,
|
|
123
|
+
"status": o.status, "detail": o.detail}
|
|
124
|
+
for o in outcomes]},
|
|
125
|
+
lambda d: click.echo(
|
|
126
|
+
f"applied {d['applied']}/{d['total']} ops"
|
|
127
|
+
+ (f", {d['failed']} failed" if d['failed'] else "")))
|
|
128
|
+
if errors:
|
|
129
|
+
raise SystemExit(1)
|
|
130
|
+
except EditSpecError as exc:
|
|
131
|
+
for p in exc.problems:
|
|
132
|
+
click.echo(f" - {p}", err=True)
|
|
133
|
+
raise SystemExit(2)
|
|
134
|
+
except SystemExit:
|
|
135
|
+
raise
|
|
136
|
+
except Exception as e: # noqa: BLE001
|
|
137
|
+
ctx.handle_error(e)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Fine-grained HAP app element editor — engine for ``hap app-editor``.
|
|
2
|
+
|
|
3
|
+
Migrated from the standalone ``hap-cli-app-editor`` skill so the logic
|
|
4
|
+
lives once in hap-cli and is shared as a CLI command. Unlike the old
|
|
5
|
+
skill (which shelled out to the ``hap`` binary), this calls
|
|
6
|
+
``hap_cli.core.*`` directly — no subprocess, and API errors surface as
|
|
7
|
+
exceptions.
|
|
8
|
+
|
|
9
|
+
The edit-spec schemas remain canonical in the skill
|
|
10
|
+
(``skills/hap-cli-app-editor/scripts/editspec/``); :mod:`editspec`
|
|
11
|
+
resolves them at runtime (repo layout) or from the wheel-bundled copy.
|
|
12
|
+
"""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""HAP field (control) type codes — name <-> numeric type id.
|
|
2
|
+
|
|
3
|
+
Authoritative source: ``hap worksheet field-types``. This is a baked
|
|
4
|
+
copy so the skill ships self-contained; refresh it from that command if
|
|
5
|
+
HAP adds types. ``resolve(t)`` accepts a friendly code ("Text"), a
|
|
6
|
+
legacy name ("TEXT"), or an int / numeric string and returns the int.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
# code -> numeric type id. Region has variants (19 county / 23 / 24);
|
|
11
|
+
# default to 19, override with an explicit int when needed.
|
|
12
|
+
CODE_TO_ID: dict[str, int] = {
|
|
13
|
+
"Text": 2, "PhoneNumber": 3, "LandlinePhone": 4, "Email": 5,
|
|
14
|
+
"Number": 6, "Certificate": 7, "Currency": 8, "SingleSelect": 9,
|
|
15
|
+
"MultipleSelect": 10, "Dropdown": 11, "Attachment": 14, "Date": 15,
|
|
16
|
+
"DateTime": 16, "Region": 19, "DynamicLink": 21, "Divider": 22,
|
|
17
|
+
"AmountInWords": 25, "Collaborator": 26, "Department": 27, "Rating": 28,
|
|
18
|
+
"Relation": 29, "Lookup": 30, "Formula": 31, "Concatenate": 32,
|
|
19
|
+
"AutoNumber": 33, "SubTable": 34, "CascadingSelect": 35, "Checkbox": 36,
|
|
20
|
+
"Rollup": 37, "DateFormula": 38, "CodeScan": 39, "Location": 40,
|
|
21
|
+
"RichText": 41, "Signature": 42, "OCR": 43, "Role": 44, "Embed": 45,
|
|
22
|
+
"Time": 46, "Barcode": 47, "OrgRole": 48, "Button": 49, "APIQuery": 50,
|
|
23
|
+
"QueryRecord": 51, "Section": 52, "FunctionFormula": 53, "CustomField": 54,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# legacy builder name -> numeric type id (accepted as an alias).
|
|
27
|
+
LEGACY_TO_ID: dict[str, int] = {
|
|
28
|
+
"TEXT": 2, "MOBILE_PHONE": 3, "TELEPHONE": 4, "EMAIL": 5, "NUMBER": 6,
|
|
29
|
+
"CRED": 7, "MONEY": 8, "FLAT_MENU": 9, "MULTI_SELECT": 10, "DROP_DOWN": 11,
|
|
30
|
+
"ATTACHMENT": 14, "DATE": 15, "DATE_TIME": 16, "AREA_COUNTY": 19,
|
|
31
|
+
"RELATION": 21, "SPLIT_LINE": 22, "MONEY_CN": 25, "USER_PICKER": 26,
|
|
32
|
+
"DEPARTMENT": 27, "SCORE": 28, "RELATE_SHEET": 29, "SHEET_FIELD": 30,
|
|
33
|
+
"FORMULA_NUMBER": 31, "CONCATENATE": 32, "AUTO_ID": 33, "SUB_LIST": 34,
|
|
34
|
+
"CASCADER": 35, "SWITCH": 36, "SUBTOTAL": 37, "FORMULA_DATE": 38,
|
|
35
|
+
"LOCATION": 40, "RICH_TEXT": 41, "SIGNATURE": 42, "OCR": 43, "EMBED": 45,
|
|
36
|
+
"TIME": 46, "BAR_CODE": 47, "ORG_ROLE": 48, "SEARCH_BTN": 49,
|
|
37
|
+
"SEARCH": 50, "RELATION_SEARCH": 51, "SECTION": 52, "FORMULA_FUNC": 53,
|
|
38
|
+
"CUSTOM": 54,
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Type ids that hold a select-style ``options`` list.
|
|
42
|
+
SELECT_TYPE_IDS = {9, 10, 11}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def resolve(t) -> int:
|
|
46
|
+
"""Return the numeric type id for a code / legacy name / int."""
|
|
47
|
+
if isinstance(t, bool):
|
|
48
|
+
raise ValueError(f"invalid field type: {t!r}")
|
|
49
|
+
if isinstance(t, int):
|
|
50
|
+
return t
|
|
51
|
+
if isinstance(t, str):
|
|
52
|
+
if t.isdigit():
|
|
53
|
+
return int(t)
|
|
54
|
+
if t in CODE_TO_ID:
|
|
55
|
+
return CODE_TO_ID[t]
|
|
56
|
+
if t in LEGACY_TO_ID:
|
|
57
|
+
return LEGACY_TO_ID[t]
|
|
58
|
+
raise ValueError(f"unknown field type: {t!r}")
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Execute an edit-spec against the live app (direct core calls)."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Callable, Optional
|
|
5
|
+
|
|
6
|
+
from hap_cli.core.editor import ops as ops_mod
|
|
7
|
+
from hap_cli.core.editor.errors import EditorError
|
|
8
|
+
from hap_cli.core.editor.models import OpOutcome
|
|
9
|
+
from hap_cli.core.editor.planner import check_confirm
|
|
10
|
+
from hap_cli.core.editor.reader import AppIndex
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def apply_spec(
|
|
14
|
+
spec: dict,
|
|
15
|
+
idx: AppIndex,
|
|
16
|
+
*,
|
|
17
|
+
continue_on_error: bool = False,
|
|
18
|
+
on_outcome: Optional[Callable[[OpOutcome], None]] = None,
|
|
19
|
+
) -> list[OpOutcome]:
|
|
20
|
+
"""Apply every op against ``idx``'s app. Returns outcomes.
|
|
21
|
+
|
|
22
|
+
Actions are built op-by-op against a freshly-refreshed index so an op
|
|
23
|
+
can reference an element a previous op just created (intra-spec
|
|
24
|
+
chaining). The confirm gate is enforced up front before anything runs.
|
|
25
|
+
Each Action's ``call`` performs the mutation via ``hap_cli.core.*``;
|
|
26
|
+
any error (API or resolution) is captured as an error outcome.
|
|
27
|
+
"""
|
|
28
|
+
ops = spec.get("ops", [])
|
|
29
|
+
for op in ops: # pre-flight: fail fast on missing confirm
|
|
30
|
+
check_confirm(op)
|
|
31
|
+
|
|
32
|
+
session = idx.session
|
|
33
|
+
outcomes: list[OpOutcome] = []
|
|
34
|
+
for i, op in enumerate(ops):
|
|
35
|
+
op_type = op.get("type", "")
|
|
36
|
+
responses: list = []
|
|
37
|
+
try:
|
|
38
|
+
idx.refresh() # see effects of prior ops
|
|
39
|
+
builder = ops_mod.REGISTRY[op_type]
|
|
40
|
+
for action in builder(op, idx):
|
|
41
|
+
responses.append(action.call(session))
|
|
42
|
+
outcome = OpOutcome(i, op_type, "ok", responses=responses)
|
|
43
|
+
except (EditorError, Exception) as exc: # noqa: BLE001 — record + decide
|
|
44
|
+
outcome = OpOutcome(i, op_type, "error", detail=str(exc),
|
|
45
|
+
responses=responses)
|
|
46
|
+
outcomes.append(outcome)
|
|
47
|
+
if on_outcome:
|
|
48
|
+
on_outcome(outcome)
|
|
49
|
+
if outcome.status == "error" and not continue_on_error:
|
|
50
|
+
break
|
|
51
|
+
return outcomes
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""Lower a clean custom-page component definition into the wire shape.
|
|
2
|
+
|
|
3
|
+
Covered well: value-based widgets — richText (2), embedUrl (3), image (8).
|
|
4
|
+
For data-bound widgets (chart/view/filter/button) that need resolved ids
|
|
5
|
+
or nested config, pass the wire object under ``raw`` (merged last, wins).
|
|
6
|
+
|
|
7
|
+
Wire shape mirrors pd-openweb ``src/pages/customPage/util.js`` and the
|
|
8
|
+
hap-cli-app-creator component builders: a widget carries ``type`` (int),
|
|
9
|
+
``value``/config, ``name``, and per-platform ``web``/``mobile`` blocks
|
|
10
|
+
holding the grid ``layout`` (48-col).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
# clean type name -> numeric widget type.
|
|
17
|
+
COMPONENT_TYPE = {
|
|
18
|
+
"chart": 1, "analysis": 1,
|
|
19
|
+
"richText": 2, "rich_text": 2,
|
|
20
|
+
"embedUrl": 3, "embed_url": 3,
|
|
21
|
+
"button": 4,
|
|
22
|
+
"view": 5,
|
|
23
|
+
"filter": 6,
|
|
24
|
+
"image": 8,
|
|
25
|
+
"carousel": 9,
|
|
26
|
+
"tabs": 10,
|
|
27
|
+
"card": 11,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# default 48-col grid size per type name.
|
|
31
|
+
_DEFAULT_WH = {
|
|
32
|
+
"richText": (48, 5), "rich_text": (48, 5),
|
|
33
|
+
"embedUrl": (24, 12), "embed_url": (24, 12),
|
|
34
|
+
"image": (24, 12), "view": (48, 12), "chart": (24, 10),
|
|
35
|
+
"button": (24, 6), "filter": (24, 3),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def resolve_type(t) -> int:
|
|
40
|
+
if isinstance(t, int):
|
|
41
|
+
return t
|
|
42
|
+
if isinstance(t, str):
|
|
43
|
+
if t.isdigit():
|
|
44
|
+
return int(t)
|
|
45
|
+
if t in COMPONENT_TYPE:
|
|
46
|
+
return COMPONENT_TYPE[t]
|
|
47
|
+
raise ValueError(f"unknown component type: {t!r}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def lower_component(comp: dict[str, Any]) -> dict[str, Any]:
|
|
51
|
+
"""Return the wire component dict for one clean component definition.
|
|
52
|
+
|
|
53
|
+
Clean form: ``{name, type, [value], [layout:{x,y,w,h}], [raw:{...}]}``.
|
|
54
|
+
"""
|
|
55
|
+
type_name = comp["type"]
|
|
56
|
+
type_id = resolve_type(type_name)
|
|
57
|
+
w, h = _DEFAULT_WH.get(type_name, (24, 8))
|
|
58
|
+
layout = dict(comp.get("layout") or {})
|
|
59
|
+
layout.setdefault("x", 0)
|
|
60
|
+
layout.setdefault("y", 0)
|
|
61
|
+
layout.setdefault("w", w)
|
|
62
|
+
layout.setdefault("h", h)
|
|
63
|
+
layout.setdefault("minW", 2)
|
|
64
|
+
layout.setdefault("minH", 4)
|
|
65
|
+
# HAP does not round-trip a top-level ``name`` on a page component —
|
|
66
|
+
# the queryable/display name lives in ``web.title``. Set both so the
|
|
67
|
+
# editor can resolve the component by name on a later read.
|
|
68
|
+
wire: dict[str, Any] = {
|
|
69
|
+
"type": type_id,
|
|
70
|
+
"name": comp["name"],
|
|
71
|
+
"web": {"title": comp["name"], "titleVisible": True, "visible": True,
|
|
72
|
+
"layout": layout},
|
|
73
|
+
"mobile": {"title": comp["name"], "titleVisible": True,
|
|
74
|
+
"visible": True, "layout": None},
|
|
75
|
+
}
|
|
76
|
+
if "value" in comp:
|
|
77
|
+
wire["value"] = comp["value"]
|
|
78
|
+
if isinstance(comp.get("raw"), dict):
|
|
79
|
+
wire.update(comp["raw"])
|
|
80
|
+
return wire
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Load + validate an edit-spec against the per-module schemas.
|
|
2
|
+
|
|
3
|
+
Validation is two-stage and module-local: the envelope schema checks the
|
|
4
|
+
wrapper and each op's shared ``type``/``confirm`` fields; then, per op,
|
|
5
|
+
only the one module schema named by the op.type prefix is loaded and used
|
|
6
|
+
for deep validation (dispatched to the matching verb branch for precise
|
|
7
|
+
errors).
|
|
8
|
+
|
|
9
|
+
Schemas are canonical in the skill
|
|
10
|
+
(``skills/hap-cli-app-editor/scripts/editspec/``). :func:`schema_dir`
|
|
11
|
+
resolves them for both the editable/repo layout and a pip-installed wheel
|
|
12
|
+
(where ``setup.py`` mirrors them into ``hap_cli/core/editor/editspec/``,
|
|
13
|
+
the same trick used for the V3 schemas).
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
from functools import lru_cache
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from hap_cli.core.editor import jsonschema_mini
|
|
23
|
+
from hap_cli.core.editor.errors import EditSpecError
|
|
24
|
+
|
|
25
|
+
# op.type prefix -> module schema filename.
|
|
26
|
+
_MODULE_BY_PREFIX = {
|
|
27
|
+
"worksheet": "worksheet.schema.json",
|
|
28
|
+
"field": "field.schema.json",
|
|
29
|
+
"view": "view.schema.json",
|
|
30
|
+
"role": "role.schema.json",
|
|
31
|
+
"custom-action": "custom-action.schema.json",
|
|
32
|
+
"chatbot": "chatbot.schema.json",
|
|
33
|
+
"custom-page": "custom-page.schema.json",
|
|
34
|
+
"workflow": "workflow.schema.json",
|
|
35
|
+
"component": "component.schema.json",
|
|
36
|
+
"node": "node.schema.json",
|
|
37
|
+
"app": "application.schema.json",
|
|
38
|
+
"section": "application.schema.json",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
DESTRUCTIVE_TYPES = {
|
|
42
|
+
"worksheet.delete", "field.delete", "view.delete", "role.delete",
|
|
43
|
+
"custom-action.delete", "chatbot.delete", "custom-page.delete",
|
|
44
|
+
"workflow.delete", "component.delete", "node.delete", "section.delete",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_SKILL_REL = ("skills", "hap-cli-app-editor", "scripts", "editspec")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@lru_cache(maxsize=1)
|
|
51
|
+
def schema_dir() -> Path:
|
|
52
|
+
"""Locate the edit-spec schema directory.
|
|
53
|
+
|
|
54
|
+
Resolution order:
|
|
55
|
+
1. Wheel-bundled copy next to this module (``editspec/``), created
|
|
56
|
+
by setup.py's build step for installed packages.
|
|
57
|
+
2. Repo/editable layout: ``<repo>/skills/hap-cli-app-editor/
|
|
58
|
+
scripts/editspec`` found by walking up from this file.
|
|
59
|
+
"""
|
|
60
|
+
bundled = Path(__file__).resolve().parent / "editspec"
|
|
61
|
+
if bundled.is_dir() and any(bundled.glob("*.schema.json")):
|
|
62
|
+
return bundled
|
|
63
|
+
here = Path(__file__).resolve()
|
|
64
|
+
for parent in here.parents:
|
|
65
|
+
cand = parent.joinpath(*_SKILL_REL)
|
|
66
|
+
if cand.is_dir():
|
|
67
|
+
return cand
|
|
68
|
+
raise EditSpecError([
|
|
69
|
+
"could not locate edit-spec schemas (neither the bundled copy nor "
|
|
70
|
+
"skills/hap-cli-app-editor/scripts/editspec)"])
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load_schema(filename: str) -> dict[str, Any]:
|
|
74
|
+
with (schema_dir() / filename).open(encoding="utf-8") as fh:
|
|
75
|
+
return json.load(fh)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_spec(path: Path) -> dict[str, Any]:
|
|
79
|
+
"""Read an edit-spec JSON file and validate it. Returns the spec dict."""
|
|
80
|
+
try:
|
|
81
|
+
with Path(path).open(encoding="utf-8") as fh:
|
|
82
|
+
spec = json.load(fh)
|
|
83
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
84
|
+
raise EditSpecError([f"could not read edit-spec {path}: {exc}"]) from exc
|
|
85
|
+
validate_spec(spec)
|
|
86
|
+
return spec
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def validate_spec(spec: Any) -> None:
|
|
90
|
+
"""Validate an already-parsed edit-spec; raise EditSpecError on problems."""
|
|
91
|
+
problems: list[str] = []
|
|
92
|
+
problems += jsonschema_mini.validate(spec, _load_schema("envelope.schema.json"))
|
|
93
|
+
|
|
94
|
+
if isinstance(spec, dict) and isinstance(spec.get("ops"), list):
|
|
95
|
+
for i, op in enumerate(spec["ops"]):
|
|
96
|
+
if not isinstance(op, dict):
|
|
97
|
+
continue
|
|
98
|
+
op_type = op.get("type")
|
|
99
|
+
if not isinstance(op_type, str) or "." not in op_type:
|
|
100
|
+
continue
|
|
101
|
+
prefix, verb = op_type.split(".", 1)
|
|
102
|
+
module = _MODULE_BY_PREFIX.get(prefix)
|
|
103
|
+
if module is None:
|
|
104
|
+
problems.append(f"ops[{i}].type: '{op_type}' has no module "
|
|
105
|
+
f"schema (prefix '{prefix}' unsupported)")
|
|
106
|
+
continue
|
|
107
|
+
schema = _load_schema(module)
|
|
108
|
+
defs = schema.get("$defs") or {}
|
|
109
|
+
branch = defs.get(verb) or defs.get(
|
|
110
|
+
op_type.replace(".", "_").replace("-", "_"))
|
|
111
|
+
if branch is None:
|
|
112
|
+
problems.append(f"ops[{i}].type: '{op_type}' has no '{verb}' "
|
|
113
|
+
f"branch in {module}")
|
|
114
|
+
continue
|
|
115
|
+
problems += jsonschema_mini.validate_against(
|
|
116
|
+
op, branch, schema, f"ops[{i}]")
|
|
117
|
+
|
|
118
|
+
if problems:
|
|
119
|
+
raise EditSpecError(problems)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""Exception types for the editor framework.
|
|
2
|
+
|
|
3
|
+
Kept separate so handlers, the planner, and the CLI dispatch can all
|
|
4
|
+
catch the same hierarchy without importing each other.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class EditorError(Exception):
|
|
10
|
+
"""Base class for every error this framework raises."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class EditSpecError(EditorError):
|
|
14
|
+
"""The edit-spec is structurally invalid (schema or shape).
|
|
15
|
+
|
|
16
|
+
Carries a list of human-readable problems, each prefixed with the
|
|
17
|
+
JSON path where it occurred, so the user can fix the spec before any
|
|
18
|
+
API call is made.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, problems: list[str]):
|
|
22
|
+
self.problems = problems
|
|
23
|
+
super().__init__("; ".join(problems) if problems else "invalid edit-spec")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ResolveError(EditorError):
|
|
27
|
+
"""A logical name (worksheet/field/view/...) could not be resolved to
|
|
28
|
+
an id against the live app structure."""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfirmRequiredError(EditorError):
|
|
32
|
+
"""A destructive op was requested without ``confirm: true``.
|
|
33
|
+
|
|
34
|
+
Destructive operations (delete / overwrite) refuse to run unless the
|
|
35
|
+
op object explicitly opts in — a guard against accidental data loss.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class HapCommandError(EditorError):
|
|
40
|
+
"""The ``hap`` binary exited non-zero or returned an API error."""
|
|
41
|
+
|
|
42
|
+
def __init__(self, message: str, *, argv=None, returncode=None,
|
|
43
|
+
stderr: str = ""):
|
|
44
|
+
self.argv = argv
|
|
45
|
+
self.returncode = returncode
|
|
46
|
+
self.stderr = stderr
|
|
47
|
+
super().__init__(message)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class NotLoggedInError(HapCommandError):
|
|
51
|
+
"""The ``hap`` session is not authenticated.
|
|
52
|
+
|
|
53
|
+
The user must run ``hap auth login`` (and select an org) before any
|
|
54
|
+
live read or write can happen.
|
|
55
|
+
"""
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Lower a clean field definition into a raw HAP control dict.
|
|
2
|
+
|
|
3
|
+
The clean form is what an edit-spec carries:
|
|
4
|
+
``{"name": "金额", "type": "Number", "required": true, ...}``. HAP's
|
|
5
|
+
``AddWorksheetControls`` / ``SaveWorksheetControls`` accept partial
|
|
6
|
+
control objects and fill per-type defaults server-side, so the lowering
|
|
7
|
+
only needs to set the discriminating keys.
|
|
8
|
+
|
|
9
|
+
Covered well: scalar types (Text/Number/Date/Email/...) and select types
|
|
10
|
+
(SingleSelect/MultipleSelect/Dropdown, with options). For advanced types
|
|
11
|
+
(Relation/Lookup/Rollup/Formula/SubTable) pass a raw override under the
|
|
12
|
+
field's ``control`` key — it is merged last and wins.
|
|
13
|
+
"""
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import uuid
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from hap_cli.core.editor._hapmeta import control_type_codes as codes
|
|
20
|
+
|
|
21
|
+
_OPTION_COLORS = [
|
|
22
|
+
"#C9E6FC", "#C3F2F2", "#C2F1D2", "#FFE7B1", "#FBD2BF",
|
|
23
|
+
"#FDD2DC", "#E6CFF1", "#D2D2D2",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _normalize_options(opts: list) -> list[dict[str, Any]]:
|
|
28
|
+
"""Mirror the CLI's option shape (key/value/index/checked/color)."""
|
|
29
|
+
out: list[dict[str, Any]] = []
|
|
30
|
+
any_default = any(isinstance(o, dict) and o.get("checked") for o in opts)
|
|
31
|
+
for i, o in enumerate(opts):
|
|
32
|
+
if isinstance(o, dict):
|
|
33
|
+
out.append({
|
|
34
|
+
"key": o.get("key") or str(uuid.uuid4()),
|
|
35
|
+
"value": o["value"],
|
|
36
|
+
"isDeleted": o.get("isDeleted", False),
|
|
37
|
+
"index": o.get("index", i + 1),
|
|
38
|
+
"checked": o.get("checked", False),
|
|
39
|
+
"color": o.get("color", _OPTION_COLORS[i % len(_OPTION_COLORS)]),
|
|
40
|
+
})
|
|
41
|
+
else:
|
|
42
|
+
out.append({
|
|
43
|
+
"key": str(uuid.uuid4()),
|
|
44
|
+
"value": str(o),
|
|
45
|
+
"isDeleted": False,
|
|
46
|
+
"index": i + 1,
|
|
47
|
+
"checked": (not any_default and i == 0),
|
|
48
|
+
"color": _OPTION_COLORS[i % len(_OPTION_COLORS)],
|
|
49
|
+
})
|
|
50
|
+
return out
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def lower_field(field: dict[str, Any]) -> dict[str, Any]:
|
|
54
|
+
"""Return the raw control dict for one clean field definition."""
|
|
55
|
+
type_id = codes.resolve(field["type"])
|
|
56
|
+
ctrl: dict[str, Any] = {"type": type_id, "controlName": field["name"]}
|
|
57
|
+
if field.get("required"):
|
|
58
|
+
ctrl["required"] = True
|
|
59
|
+
if field.get("unique"):
|
|
60
|
+
ctrl["unique"] = True
|
|
61
|
+
if type_id in codes.SELECT_TYPE_IDS and field.get("options") is not None:
|
|
62
|
+
ctrl["options"] = _normalize_options(field["options"])
|
|
63
|
+
# Raw escape hatch for advanced types — merged last, wins.
|
|
64
|
+
if isinstance(field.get("control"), dict):
|
|
65
|
+
ctrl.update(field["control"])
|
|
66
|
+
return ctrl
|