hap-cli 0.7.0__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.
Files changed (204) hide show
  1. {hap_cli-0.7.0 → hap_cli-0.7.2}/MANIFEST.in +5 -0
  2. {hap_cli-0.7.0 → hap_cli-0.7.2}/PKG-INFO +1 -1
  3. {hap_cli-0.7.0 → hap_cli-0.7.2}/app_live_tests/__init__.py +1 -1
  4. {hap_cli-0.7.0 → hap_cli-0.7.2}/app_live_tests/config.py +1 -1
  5. {hap_cli-0.7.0 → hap_cli-0.7.2}/app_live_tests/harness.py +7 -4
  6. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/__init__.py +1 -1
  7. hap_cli-0.7.2/hap_cli/commands/app_editor_cmd.py +137 -0
  8. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/instance_cmd.py +10 -4
  9. hap_cli-0.7.2/hap_cli/core/editor/__init__.py +12 -0
  10. hap_cli-0.7.2/hap_cli/core/editor/_hapmeta/__init__.py +5 -0
  11. hap_cli-0.7.2/hap_cli/core/editor/_hapmeta/control_type_codes.py +58 -0
  12. hap_cli-0.7.2/hap_cli/core/editor/apply.py +51 -0
  13. hap_cli-0.7.2/hap_cli/core/editor/componentlower.py +80 -0
  14. hap_cli-0.7.2/hap_cli/core/editor/editspec.py +119 -0
  15. hap_cli-0.7.2/hap_cli/core/editor/errors.py +55 -0
  16. hap_cli-0.7.2/hap_cli/core/editor/fieldlower.py +66 -0
  17. hap_cli-0.7.2/hap_cli/core/editor/jsonschema_mini.py +136 -0
  18. hap_cli-0.7.2/hap_cli/core/editor/models.py +39 -0
  19. hap_cli-0.7.2/hap_cli/core/editor/ops.py +543 -0
  20. hap_cli-0.7.2/hap_cli/core/editor/planner.py +53 -0
  21. hap_cli-0.7.2/hap_cli/core/editor/reader.py +291 -0
  22. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/instance.py +45 -32
  23. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/workflow_node_dsl.py +10 -2
  24. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/hap_cli.py +2 -0
  25. hap_cli-0.7.2/hap_cli/tests/test_app_editor.py +247 -0
  26. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_core.py +93 -28
  27. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_approval.py +83 -0
  28. hap_cli-0.7.2/hap_cli/tests/test_integration_approval_actions.py +237 -0
  29. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_destructive.py +4 -4
  30. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_worksheet_crud_cli.py +1 -1
  31. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli.egg-info/PKG-INFO +1 -1
  32. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli.egg-info/SOURCES.txt +28 -0
  33. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/seed/template.py +1 -1
  34. {hap_cli-0.7.0 → hap_cli-0.7.2}/setup.py +20 -5
  35. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/application.schema.json +59 -0
  36. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/chatbot.schema.json +52 -0
  37. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/component.schema.json +65 -0
  38. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/custom-action.schema.json +55 -0
  39. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/custom-page.schema.json +49 -0
  40. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/envelope.schema.json +86 -0
  41. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/field.schema.json +84 -0
  42. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/node.schema.json +73 -0
  43. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/role.schema.json +90 -0
  44. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/view.schema.json +64 -0
  45. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/workflow.schema.json +60 -0
  46. hap_cli-0.7.2/skills/hap-cli-app-editor/scripts/editspec/worksheet.schema.json +51 -0
  47. {hap_cli-0.7.0 → hap_cli-0.7.2}/README.md +0 -0
  48. {hap_cli-0.7.0 → hap_cli-0.7.2}/app_live_tests/__main__.py +0 -0
  49. {hap_cli-0.7.0 → hap_cli-0.7.2}/app_live_tests/smoke.py +0 -0
  50. {hap_cli-0.7.0 → hap_cli-0.7.2}/app_live_tests/state.py +0 -0
  51. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/README.md +0 -0
  52. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/README_CN.md +0 -0
  53. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/__init__.py +0 -0
  54. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/app_cmd.py +0 -0
  55. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/auth_cmd.py +0 -0
  56. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/calendar_cmd.py +0 -0
  57. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/chart_cmd.py +0 -0
  58. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/chat_cmd.py +0 -0
  59. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/chatbot_cmd.py +0 -0
  60. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/config_cmd.py +0 -0
  61. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/contact_cmd.py +0 -0
  62. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/department_cmd.py +0 -0
  63. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/group_cmd.py +0 -0
  64. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/icon_cmd.py +0 -0
  65. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/node_cmd.py +0 -0
  66. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/optionset_cmd.py +0 -0
  67. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/page_cmd.py +0 -0
  68. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/post_cmd.py +0 -0
  69. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/record_cmd.py +0 -0
  70. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/region_cmd.py +0 -0
  71. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/role_cmd.py +0 -0
  72. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/upload_cmd.py +0 -0
  73. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/v3_registry.py +0 -0
  74. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/workflow_cmd.py +0 -0
  75. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/commands/worksheet_cmd.py +0 -0
  76. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/context.py +0 -0
  77. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/__init__.py +0 -0
  78. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/action_spec_adapter.py +0 -0
  79. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/app.py +0 -0
  80. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/auth.py +0 -0
  81. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/calendar_mod.py +0 -0
  82. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/chart.py +0 -0
  83. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/chat.py +0 -0
  84. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/chatbot.py +0 -0
  85. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/contact.py +0 -0
  86. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/control_type_codes.py +0 -0
  87. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/department.py +0 -0
  88. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/field_normalizer.py +0 -0
  89. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/field_spec_adapter.py +0 -0
  90. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/filter_translator.py +0 -0
  91. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/flow_node.py +0 -0
  92. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/global_meta.py +0 -0
  93. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/group.py +0 -0
  94. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/icon_index.py +0 -0
  95. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/logger.py +0 -0
  96. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/optionset.py +0 -0
  97. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/page.py +0 -0
  98. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/post.py +0 -0
  99. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/record.py +0 -0
  100. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/response_crypto.py +0 -0
  101. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/role.py +0 -0
  102. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/role_perm_builder.py +0 -0
  103. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/session.py +0 -0
  104. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/token_crypto.py +0 -0
  105. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/upload.py +0 -0
  106. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/v3/__init__.py +0 -0
  107. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/v3/dispatcher.py +0 -0
  108. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/v3/render.py +0 -0
  109. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/v3/schema.py +0 -0
  110. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/view_spec_adapter.py +0 -0
  111. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/workflow.py +0 -0
  112. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/worksheet.py +0 -0
  113. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/core/worksheet_templates.py +0 -0
  114. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/i18n.py +0 -0
  115. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/locale/__init__.py +0 -0
  116. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/locale/messages.json +0 -0
  117. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/locale/messages.schema.json +0 -0
  118. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/__init__.py +0 -0
  119. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/conftest.py +0 -0
  120. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_auth_prerelease.py +0 -0
  121. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_chart.py +0 -0
  122. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_config_cmd.py +0 -0
  123. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_full_e2e.py +0 -0
  124. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_global_meta.py +0 -0
  125. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_i18n.py +0 -0
  126. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration.py +0 -0
  127. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_calendar.py +0 -0
  128. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_misc.py +0 -0
  129. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_post.py +0 -0
  130. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_social.py +0 -0
  131. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_v3.py +0 -0
  132. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_workflow.py +0 -0
  133. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_integration_worksheet_extra.py +0 -0
  134. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_org_id_cli.py +0 -0
  135. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_org_id_docs.py +0 -0
  136. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_parameter_conventions.py +0 -0
  137. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_parameter_mapping_registry.py +0 -0
  138. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_v3_api_schema_loader.py +0 -0
  139. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_v3_dispatcher.py +0 -0
  140. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_v3_registry_coverage.py +0 -0
  141. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_v3_session.py +0 -0
  142. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/tests/test_v3_yaml_translation.py +0 -0
  143. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/utils/__init__.py +0 -0
  144. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/utils/formatting.py +0 -0
  145. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/utils/options.py +0 -0
  146. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli/utils/parameter_mapping.py +0 -0
  147. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli.egg-info/dependency_links.txt +0 -0
  148. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli.egg-info/entry_points.txt +0 -0
  149. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli.egg-info/requires.txt +0 -0
  150. {hap_cli-0.7.0 → hap_cli-0.7.2}/hap_cli.egg-info/top_level.txt +0 -0
  151. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/__init__.py +0 -0
  152. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/__main__.py +0 -0
  153. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/_wftest.py +0 -0
  154. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/charts.py +0 -0
  155. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/cleanup.py +0 -0
  156. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/compiler.py +0 -0
  157. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/config.py +0 -0
  158. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/errors.py +0 -0
  159. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/executor.py +0 -0
  160. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/fields.py +0 -0
  161. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/hap.py +0 -0
  162. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/recording/__init__.py +0 -0
  163. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/recording/console.py +0 -0
  164. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/recording/jsonl.py +0 -0
  165. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/recording/mirror.py +0 -0
  166. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/recording/report.py +0 -0
  167. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/schema.py +0 -0
  168. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/seed/__init__.py +0 -0
  169. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/seed/cli.py +0 -0
  170. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/seed/executor.py +0 -0
  171. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/selftest.py +0 -0
  172. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/smoke.py +0 -0
  173. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/step.py +0 -0
  174. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/steps.py +0 -0
  175. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/store.py +0 -0
  176. {hap_cli-0.7.0 → hap_cli-0.7.2}/live_tests/workflow_dsl.py +0 -0
  177. {hap_cli-0.7.0 → hap_cli-0.7.2}/setup.cfg +0 -0
  178. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/INDEX.json +0 -0
  179. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/add_member_to_role.yaml +0 -0
  180. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/batch_delete_records.yaml +0 -0
  181. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/create_optionset.yaml +0 -0
  182. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/create_role.yaml +0 -0
  183. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/delete_optionset.yaml +0 -0
  184. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/delete_record.yaml +0 -0
  185. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/delete_role.yaml +0 -0
  186. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/delete_worksheet.yaml +0 -0
  187. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_app_info.yaml +0 -0
  188. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_app_knowledge_list.yaml +0 -0
  189. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_app_worksheets_list.yaml +0 -0
  190. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_optionset_list.yaml +0 -0
  191. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_details.yaml +0 -0
  192. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_discussions.yaml +0 -0
  193. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_list.yaml +0 -0
  194. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_logs.yaml +0 -0
  195. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_pivot_data.yaml +0 -0
  196. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_relations.yaml +0 -0
  197. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_record_share_link.yaml +0 -0
  198. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_regions.yaml +0 -0
  199. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_role_details.yaml +0 -0
  200. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/get_role_list.yaml +0 -0
  201. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/knowledge_search.yaml +0 -0
  202. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/leave_all_roles.yaml +0 -0
  203. {hap_cli-0.7.0 → hap_cli-0.7.2}/sources/v3-api-schema/remove_member_from_role.yaml +0 -0
  204. {hap_cli-0.7.0 → 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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hap-cli
3
- Version: 0.7.0
3
+ Version: 0.7.2
4
4
  Summary: CLI harness for MingDAO HAP - Enterprise no-code platform
5
5
  Author: hap-cli
6
6
  License: Apache-2.0
@@ -1,4 +1,4 @@
1
- """Repeatable live smoke tests for the hap-app-editor skill.
1
+ """Repeatable live smoke tests for the hap-cli-app-editor skill.
2
2
 
3
3
  This is a top-level, stdlib-only package (no ``test_*.py`` files, so the
4
4
  default pytest run never collects it — mirror of ``live_tests/``). It
@@ -12,7 +12,7 @@ PYTHON_BIN = os.environ.get("HAP_EDITOR_PYTHON", "") # "" -> sys.executable
12
12
 
13
13
  _PKG_DIR = Path(__file__).resolve().parent
14
14
  REPO_ROOT = _PKG_DIR.parent
15
- SKILL_DIR = REPO_ROOT / "skills" / "hap-app-editor"
15
+ SKILL_DIR = REPO_ROOT / "skills" / "hap-cli-app-editor"
16
16
 
17
17
  STATE_DIR = _PKG_DIR / "state"
18
18
  STATE_FILE = STATE_DIR / "current_app.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 skill (``python -m scripts <args>``) in the skill dir."""
57
- py = config.PYTHON_BIN or sys.executable
58
- argv = [py, "-m", "scripts", *args]
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
- cwd=str(config.SKILL_DIR), timeout=config.TIMEOUT)
63
+ timeout=config.TIMEOUT)
61
64
  return proc.returncode, proc.stdout, proc.stderr
62
65
 
63
66
 
@@ -1,3 +1,3 @@
1
1
  """CLI harness for MingDAO HAP (hap-cli)."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.7.2"
@@ -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)
@@ -294,18 +294,24 @@ def instance_history_detail(ctx, instance_id):
294
294
 
295
295
  @instance.command("batch")
296
296
  @click.option("--action", "-a", required=True, type=int, help="4=batch approve, 5=batch reject")
297
- @click.option("--type", "type_", default=-1, type=int, help="Task type filter")
297
+ @click.option("--type", "type_", default=4, type=int, help="Task type the selection came from (4=pending approval)")
298
298
  @click.option("--process-id", default="", help="Filter by process")
299
- @click.option("--select", "-s", multiple=True, help="Selected instance IDs")
299
+ @click.option("--select", "-s", multiple=True, help="Task to act on, as TASK_ID:WORK_ID (repeatable)")
300
+ @click.option("--opinion", default="", help="Comment applied to each task")
300
301
  @pass_context
301
- def instance_batch(ctx, action, type_, process_id, select):
302
- """Batch approve or reject tasks."""
302
+ def instance_batch(ctx, action, type_, process_id, select, opinion):
303
+ """Batch approve or reject tasks.
304
+
305
+ Pass each task as TASK_ID:WORK_ID (the IDs from the pending list); repeat
306
+ --select for more than one.
307
+ """
303
308
  try:
304
309
  session = ctx.get_session()
305
310
  data = inst_mod.batch_operation(
306
311
  session, action,
307
312
  type_=type_, process_id=process_id,
308
313
  selects=list(select) if select else None,
314
+ opinion=opinion,
309
315
  )
310
316
  ctx.output(data, lambda d: click.echo(f"Batch operation completed: {d}"))
311
317
  except Exception as 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,5 @@
1
+ """Self-contained HAP metadata (field type codes, ...) for the editor.
2
+
3
+ Copied into this skill so it is independently distributable — it does not
4
+ import hap_cli or the hap-cli-app-creator skill.
5
+ """
@@ -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
+ """