codeplain 0.3.10.dev4__tar.gz → 0.3.11__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 (282) hide show
  1. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.flake8 +2 -0
  2. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/PKG-INFO +4 -4
  3. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/cli_output/status.py +13 -8
  4. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/codeplain_REST_api.py +32 -1
  5. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/file_utils.py +14 -0
  6. codeplain-0.3.11/git_preflight.py +41 -0
  7. codeplain-0.3.11/install/bash/examples.sh +175 -0
  8. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/install/bash/install.sh +4 -2
  9. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/install/powershell/examples.ps1 +65 -3
  10. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/install/powershell/install.ps1 +9 -2
  11. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code.py +76 -5
  12. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_console.py +5 -4
  13. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain_file.py +51 -18
  14. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain_modules.py +6 -6
  15. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/pyproject.toml +3 -3
  16. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/fix_conformance_test.py +11 -0
  17. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/fix_unit_tests.py +25 -0
  18. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/render_types.py +5 -0
  19. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/requirements.txt +3 -3
  20. codeplain-0.3.11/tests/data/plainfile/plain_source_with_missing_link.plain +9 -0
  21. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/task_manager_with_reference_links.plain +2 -2
  22. codeplain-0.3.11/tests/data/plainfileparser/cyclic_definitions.plain +9 -0
  23. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_defaults_exported.plain +13 -0
  24. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_imported_exported.plain +12 -0
  25. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_inherited_reexported.plain +15 -0
  26. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_malformed.plain +13 -0
  27. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_missing_definition.plain +14 -0
  28. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_missing_definition_example.plain +13 -0
  29. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_multiple_per_bullet.plain +14 -0
  30. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_multiple_per_bullet_undefined.plain +13 -0
  31. codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_no_definitions.plain +10 -0
  32. codeplain-0.3.10.dev4/tests/data/plainfileparser/exported_concepts_missing_example.plain → codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_not_declared_example.plain +1 -1
  33. codeplain-0.3.11/tests/data/plainfileparser/requires_without_definitions_example.plain +10 -0
  34. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_cli_output.py +15 -8
  35. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_console_log_styles.py +61 -5
  36. codeplain-0.3.11/tests/test_dry_run_link_validation.py +60 -0
  37. codeplain-0.3.11/tests/test_fix_unit_tests_conformance_context.py +230 -0
  38. codeplain-0.3.11/tests/test_git_preflight.py +98 -0
  39. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_plainfileparser.py +82 -22
  40. codeplain-0.3.11/tests/test_render_finished_report.py +200 -0
  41. codeplain-0.3.11/tests/test_tui_components.py +147 -0
  42. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/components.py +37 -10
  43. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/plain2code_tui.py +9 -0
  44. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/widget_helpers.py +11 -4
  45. codeplain-0.3.10.dev4/install/bash/examples.sh +0 -97
  46. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.claude/settings.json +0 -0
  47. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/dependabot.yml +0 -0
  48. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/workflows/e2e.yml +0 -0
  49. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/workflows/lint-and-test.yml +0 -0
  50. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/workflows/lint-powershell.yml +0 -0
  51. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/workflows/nofity-slack-on-main-merge.yml +0 -0
  52. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/workflows/publish-dev-to-pypi.yml +0 -0
  53. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/workflows/publish-install-script.yml +0 -0
  54. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.github/workflows/publish-to-pypi.yml +0 -0
  55. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.gitignore +0 -0
  56. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.vscode/launch.json +0 -0
  57. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/.vscode/settings.json +0 -0
  58. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/CLAUDE.md +0 -0
  59. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/CONTRIBUTING.md +0 -0
  60. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/LICENSE +0 -0
  61. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/README.md +0 -0
  62. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/change_detection.py +0 -0
  63. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/cli_output/__init__.py +0 -0
  64. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/cli_output/dry_run.py +0 -0
  65. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/cli_output/render_summary.py +0 -0
  66. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/concept_utils.py +0 -0
  67. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/config/__init__.py +0 -0
  68. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/config/system_config.yaml +0 -0
  69. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/conformance_fix_journal.py +0 -0
  70. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/diff_utils.py +0 -0
  71. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/docs/generate_cli.py +0 -0
  72. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/docs/plain2code_cli.md +0 -0
  73. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/docs/starting_a_plain_project_from_scratch.md +0 -0
  74. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/event_bus.py +0 -0
  75. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_golang/config.yaml +0 -0
  76. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_golang/harness_tests/hello_world_test.go +0 -0
  77. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_golang/hello_world_golang.plain +0 -0
  78. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_golang/run.sh +0 -0
  79. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_python/config.yaml +0 -0
  80. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_python/harness_tests/hello_world_display/test_hello_world.py +0 -0
  81. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_python/hello_world_python.plain +0 -0
  82. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_python/run.sh +0 -0
  83. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/config.yaml +0 -0
  84. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/e2e/hello_world.cy.ts +0 -0
  85. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress/support/e2e.ts +0 -0
  86. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/harness_tests/hello_world_display/cypress.config.ts +0 -0
  87. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/harness_tests/hello_world_display/package.json +0 -0
  88. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/harness_tests/hello_world_display/tsconfig.json +0 -0
  89. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/hello_world_react.plain +0 -0
  90. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/example_hello_world_react/run.sh +0 -0
  91. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/examples/run.sh +0 -0
  92. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/git_utils.py +0 -0
  93. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/install/bash/walkthrough.sh +0 -0
  94. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/install/powershell/walkthrough.ps1 +0 -0
  95. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/memory_management.py +0 -0
  96. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/metadata_utils.py +0 -0
  97. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/module_renderer.py +0 -0
  98. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/partial_rendering.py +0 -0
  99. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/path_resolution.py +0 -0
  100. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_arguments.py +0 -0
  101. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_cli.md +0 -0
  102. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_events.py +0 -0
  103. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_exceptions.py +0 -0
  104. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_logger.py +0 -0
  105. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_nodes.py +0 -0
  106. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_read_config.py +0 -0
  107. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_state.py +0 -0
  108. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_telemetry.py +0 -0
  109. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain2code_utils.py +0 -0
  110. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/plain_spec.py +0 -0
  111. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/pytest.ini +0 -0
  112. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/__init__.py +0 -0
  113. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/analyze_specification_ambiguity.py +0 -0
  114. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/base_action.py +0 -0
  115. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/commit_conformance_tests_changes.py +0 -0
  116. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/commit_implementation_code_changes.py +0 -0
  117. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/create_dist.py +0 -0
  118. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/distill_conformance_test_memory.py +0 -0
  119. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/exit_with_error.py +0 -0
  120. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/finish_functional_requirement.py +0 -0
  121. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/prepare_repositories.py +0 -0
  122. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/prepare_testing_environment.py +0 -0
  123. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/refactor_code.py +0 -0
  124. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/render_conformance_tests.py +0 -0
  125. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/render_functional_requirement.py +0 -0
  126. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/run_conformance_tests.py +0 -0
  127. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/run_unit_tests.py +0 -0
  128. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/actions/summarize_conformance_tests.py +0 -0
  129. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/code_renderer.py +0 -0
  130. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/conformance_tests.py +0 -0
  131. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/implementation_code_helpers.py +0 -0
  132. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/render_context.py +0 -0
  133. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/render_utils.py +0 -0
  134. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/state_machine_config.py +0 -0
  135. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/states.py +0 -0
  136. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/render_machine/triggers.py +0 -0
  137. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/resources/codeplain_overview.png +0 -0
  138. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/resources/plain_example.png +0 -0
  139. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/standard_template_library/golang-console-app-template.plain +0 -0
  140. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/standard_template_library/python-console-app-template.plain +0 -0
  141. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/standard_template_library/typescript-react-app-boilerplate.plain +0 -0
  142. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/standard_template_library/typescript-react-app-template.plain +0 -0
  143. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/system_config.py +0 -0
  144. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_conformance_tests_cypress.ps1 +0 -0
  145. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_conformance_tests_cypress.sh +0 -0
  146. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_conformance_tests_golang.ps1 +0 -0
  147. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_conformance_tests_golang.sh +0 -0
  148. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_conformance_tests_python.ps1 +0 -0
  149. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_conformance_tests_python.sh +0 -0
  150. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_flutter.ps1 +0 -0
  151. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_flutter.sh +0 -0
  152. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_golang.ps1 +0 -0
  153. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_golang.sh +0 -0
  154. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_python.ps1 +0 -0
  155. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_python.sh +0 -0
  156. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_react.ps1 +0 -0
  157. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/test_scripts/run_unittests_react.sh +0 -0
  158. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/__init__.py +0 -0
  159. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/conftest.py +0 -0
  160. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/acceptance_tests_warning/main_requiring_acceptance_tests.plain +0 -0
  161. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/acceptance_tests_warning/required_with_acceptance_tests.plain +0 -0
  162. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/circular_imports_1.plain +0 -0
  163. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/circular_imports_2.plain +0 -0
  164. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/circular_imports_main.plain +0 -0
  165. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/diamond_import_1.plain +0 -0
  166. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/diamond_import_2.plain +0 -0
  167. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/diamond_import_common.plain +0 -0
  168. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/diamond_imports_main.plain +0 -0
  169. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/import_with_functionalities.plain +0 -0
  170. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/imports_module_with_functionalities.plain +0 -0
  171. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/imports/non_existent_import.plain +0 -0
  172. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/partial_rendering/pr_code_var.plain +0 -0
  173. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/partial_rendering/pr_implement.plain +0 -0
  174. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/partial_rendering/pr_leaf.plain +0 -0
  175. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/partial_rendering/pr_middle.plain +0 -0
  176. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/partial_rendering/pr_root.plain +0 -0
  177. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/partial_rendering/pr_solo.plain +0 -0
  178. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/add_new_task_modal_specification.yaml +0 -0
  179. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/duplicate_specification_heading.plain +0 -0
  180. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/empty_functional_specs_section.plain +0 -0
  181. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/invalid_specification_order.plain +0 -0
  182. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/missing_non_functional_requirements.plain +0 -0
  183. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/no_functional_specs_section.plain +0 -0
  184. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/plain_source_with_absolute_link.plain +0 -0
  185. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/plain_source_with_url_link.plain +0 -0
  186. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/task_list_ui_specification.yaml +0 -0
  187. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfile/without_non_functional_requirement.plain +0 -0
  188. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_acceptance_tests.plain +0 -0
  189. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_acceptance_tests_nondefined.plain +0 -0
  190. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_defined_nondefined.plain +0 -0
  191. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_defined_nondefined_2.plain +0 -0
  192. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_definition.plain +0 -0
  193. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_noconcepts.plain +0 -0
  194. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_nonconcept.plain +0 -0
  195. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_nondefined.plain +0 -0
  196. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_redefinition.plain +0 -0
  197. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_several_concepts.plain +0 -0
  198. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/concept_validation_valid.plain +0 -0
  199. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts.plain +0 -0
  200. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts_base.plain +0 -0
  201. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts_example.plain +0 -0
  202. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts_nested.plain +0 -0
  203. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts_nested_example.plain +0 -0
  204. /codeplain-0.3.10.dev4/tests/data/plainfileparser/exported_concepts_missing.plain → /codeplain-0.3.11/tests/data/plainfileparser/exported_concepts_not_declared.plain +0 -0
  205. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts_transitive_example.plain +0 -0
  206. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts_transitive_l1.plain +0 -0
  207. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/exported_concepts_transitive_l2.plain +0 -0
  208. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/plain_file_parser_with_comments.plain +0 -0
  209. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/plain_file_with_comments_indented.plain +0 -0
  210. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/regular_plain_source.plain +0 -0
  211. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts.plain +0 -0
  212. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_defs.plain +0 -0
  213. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_example.plain +0 -0
  214. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_l1.plain +0 -0
  215. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_l2.plain +0 -0
  216. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_missing.plain +0 -0
  217. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_module.plain +0 -0
  218. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_partial.plain +0 -0
  219. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/required_concepts_partial_duplicate.plain +0 -0
  220. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/topological_sort.plain +0 -0
  221. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/plainfileparser/topological_sort_not_referenced.plain +0 -0
  222. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/circular_requires_main.plain +0 -0
  223. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/circular_requires_sub.plain +0 -0
  224. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/diamond_requires_1.plain +0 -0
  225. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/diamond_requires_2.plain +0 -0
  226. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/diamond_requires_common.plain +0 -0
  227. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/diamond_requires_main.plain +0 -0
  228. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/independent_requires_1.plain +0 -0
  229. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/independent_requires_2.plain +0 -0
  230. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/independent_requires_main.plain +0 -0
  231. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/non_existent_require.plain +0 -0
  232. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/normal_requires_1.plain +0 -0
  233. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/normal_requires_2.plain +0 -0
  234. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/normal_requires_common.plain +0 -0
  235. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/requires/normal_requires_main.plain +0 -0
  236. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/sample_base64_image.txt +0 -0
  237. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/simple.plain +0 -0
  238. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/templates/block_level_include.plain +0 -0
  239. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/templates/code_variables.plain +0 -0
  240. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/templates/header.plain +0 -0
  241. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/templates/implement.plain +0 -0
  242. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/templates/implement_2.plain +0 -0
  243. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/templates/template_include.plain +0 -0
  244. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/data/templates/test_hardest_problem.plain +0 -0
  245. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/e2e/Dockerfile +0 -0
  246. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/e2e/conftest.py +0 -0
  247. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/e2e/test_hello_world_python.py +0 -0
  248. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/e2e/test_hello_world_python_windows.py +0 -0
  249. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_arg_provenance.py +0 -0
  250. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_change_detection.py +0 -0
  251. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_conformance_fix_journal.py +0 -0
  252. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_distill_conformance_test_memory.py +0 -0
  253. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_file_utils.py +0 -0
  254. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_folder_path_resolution.py +0 -0
  255. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_git_utils.py +0 -0
  256. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_headless_logging.py +0 -0
  257. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_imports.py +0 -0
  258. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_log_file_name_resolution.py +0 -0
  259. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_logging_config_path_resolution.py +0 -0
  260. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_partial_rendering.py +0 -0
  261. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_path_resolution.py +0 -0
  262. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_plain2code.py +0 -0
  263. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_plain2code_state.py +0 -0
  264. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_plain2code_utils.py +0 -0
  265. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_plain_modules.py +0 -0
  266. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_plainfile.py +0 -0
  267. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_plainspec.py +0 -0
  268. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_prepare_repositories_layout.py +0 -0
  269. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_requires.py +0 -0
  270. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_resolve_config_file.py +0 -0
  271. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_script_path_resolution.py +0 -0
  272. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_system_config.py +0 -0
  273. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_telemetry.py +0 -0
  274. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_tui_usage.py +0 -0
  275. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tests/test_usage_summary.py +0 -0
  276. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/__init__.py +0 -0
  277. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/models.py +0 -0
  278. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/plain_module_render_choice_tui.py +0 -0
  279. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/spinner.py +0 -0
  280. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/state_handlers.py +0 -0
  281. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/tui/styles.css +0 -0
  282. {codeplain-0.3.10.dev4 → codeplain-0.3.11}/usage_summary.py +0 -0
@@ -20,6 +20,8 @@ exclude =
20
20
  per-file-ignores =
21
21
  __init__.py: F401
22
22
  tests/*: U100
23
+ # require_git() runs between imports in plain_modules.py, so the imports below it trip E402
24
+ plain_modules.py: E402
23
25
  max-complexity = 15
24
26
  enable-extensions = U100
25
27
  # Plugin-specific configuration
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: codeplain
3
- Version: 0.3.10.dev4
3
+ Version: 0.3.11
4
4
  Summary: Transform plain language specifications into working code
5
5
  License-File: LICENSE
6
6
  Classifier: Environment :: Console
@@ -8,7 +8,7 @@ Classifier: Intended Audience :: Developers
8
8
  Classifier: Operating System :: OS Independent
9
9
  Classifier: Topic :: Software Development :: Code Generators
10
10
  Requires-Python: ==3.11.*
11
- Requires-Dist: gitpython==3.1.55
11
+ Requires-Dist: gitpython==3.1.59
12
12
  Requires-Dist: mistletoe==1.6.0
13
13
  Requires-Dist: networkx==3.6.1
14
14
  Requires-Dist: python-frontmatter==1.3.0
@@ -16,9 +16,9 @@ Requires-Dist: python-liquid2==0.4.0
16
16
  Requires-Dist: pyyaml==6.0.3
17
17
  Requires-Dist: requests==2.34.2
18
18
  Requires-Dist: rich==15.0.0
19
- Requires-Dist: sentry-sdk==2.66.1
19
+ Requires-Dist: sentry-sdk==2.68.1
20
20
  Requires-Dist: textual>=7.5.0
21
- Requires-Dist: tiktoken==0.13.0
21
+ Requires-Dist: tiktoken==0.14.0
22
22
  Requires-Dist: transitions==0.9.3
23
23
  Provides-Extra: dev
24
24
  Requires-Dist: black==26.5.1; extra == 'dev'
@@ -6,6 +6,8 @@ from typing import Optional
6
6
  import codeplain_REST_api as codeplain_api
7
7
  from plain2code_console import console
8
8
 
9
+ PLANS_URL = "https://platform.codeplain.ai/plans"
10
+
9
11
 
10
12
  def _create_progress_bar(remaining: int, total: int, width: int = 30) -> str:
11
13
  """Create a Unicode progress bar."""
@@ -42,12 +44,12 @@ def _display_credit_line(plan_credits: dict) -> None:
42
44
  bar = _create_progress_bar(remaining, total, width=30)
43
45
 
44
46
  # Format plan type label
45
- plan_label = "Free trial" if plan_type == "free" else plan_type.upper() + " plan"
47
+ plan_label = "Free credits" if plan_type == "free" else plan_type.upper() + " plan"
46
48
 
47
49
  if is_expired:
48
- console.print(f" {plan_label:10} {bar} expired {formatted_date}")
50
+ console.print(f" {plan_label:12} {bar} expired {formatted_date}")
49
51
  else:
50
- console.print(f" {plan_label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
52
+ console.print(f" {plan_label:12} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
51
53
 
52
54
 
53
55
  def _display_bucket_credit_line(bucket: dict, label: str) -> None:
@@ -71,9 +73,9 @@ def _display_bucket_credit_line(bucket: dict, label: str) -> None:
71
73
  bar = _create_progress_bar(remaining, total, width=30)
72
74
 
73
75
  if is_expired:
74
- console.print(f" {label:10} {bar} expired {formatted_date}")
76
+ console.print(f" {label:12} {bar} expired {formatted_date}")
75
77
  else:
76
- console.print(f" {label:10} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
78
+ console.print(f" {label:12} {bar} {remaining:2} of {total} remaining expires {formatted_date}")
77
79
 
78
80
 
79
81
  def _display_status_message(plan_credits: Optional[dict], purchased_credits: list, promo_credits: list) -> None:
@@ -107,7 +109,11 @@ def _display_status_message(plan_credits: Optional[dict], purchased_credits: lis
107
109
  break
108
110
 
109
111
  if not has_remaining:
110
- console.print("\nNo rendering credits remaining. Upgrade to continue rendering.")
112
+ console.print(
113
+ f"\nNo rendering credits remaining. Upgrade to the PRO plan at {PLANS_URL} to continue rendering."
114
+ )
115
+ else:
116
+ console.print(f"\nTo manage your plan navigate to {PLANS_URL}")
111
117
 
112
118
 
113
119
  def print_status(api_key: str, api_url: str, client_version: str) -> None:
@@ -148,7 +154,7 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
148
154
  # Display rendering credits section
149
155
  console.print("Rendering credits:")
150
156
 
151
- # Display plan credits (free trial or subscription)
157
+ # Display plan credits (free credits or subscription)
152
158
  if plan_credits:
153
159
  _display_credit_line(plan_credits)
154
160
 
@@ -162,4 +168,3 @@ def print_status(api_key: str, api_url: str, client_version: str) -> None:
162
168
 
163
169
  # Display status messages and management link
164
170
  _display_status_message(plan_credits, purchased_credits, promo_credits)
165
- console.print("\nTo manage your plan navigate to https://platform.codeplain.ai/plans")
@@ -11,6 +11,10 @@ from plain2code_state import RunState
11
11
  MAX_RETRIES = 4
12
12
  RETRY_DELAY = 3
13
13
 
14
+ # A report the render does not consume must not be able to hold the process open
15
+ # at exit, and requests waits indefinitely by default.
16
+ REPORT_TIMEOUT_SECONDS = 5
17
+
14
18
  RETRY_ERROR_CODES = [
15
19
  "LLMInternalError",
16
20
  ]
@@ -125,6 +129,7 @@ class CodeplainAPI:
125
129
  run_state: Optional[RunState],
126
130
  num_retries: int = MAX_RETRIES,
127
131
  silent: bool = False,
132
+ timeout: Optional[int] = None,
128
133
  ):
129
134
  if run_state is not None:
130
135
  self._extend_payload_with_run_state(payload, run_state)
@@ -134,7 +139,7 @@ class CodeplainAPI:
134
139
 
135
140
  for attempt in range(num_retries + 1):
136
141
  try:
137
- response = requests.post(endpoint_url, headers=headers, json=payload)
142
+ response = requests.post(endpoint_url, headers=headers, json=payload, timeout=timeout)
138
143
 
139
144
  try:
140
145
  response_json = response.json()
@@ -173,6 +178,27 @@ class CodeplainAPI:
173
178
  }
174
179
  return self.post_request(endpoint_url, headers, payload, None, num_retries=0, silent=True)
175
180
 
181
+ def render_finished(self, outcome: str, run_state: RunState):
182
+ """Report the terminal state a render reached.
183
+
184
+ Called once per render, whatever the outcome. The render state machine
185
+ lives here, so the API cannot tell a finished render from one still
186
+ going; it has to be told.
187
+
188
+ No retries and no console output: a report has no result the render
189
+ needs, and must not add minutes to a render that has already ended nor
190
+ print retry noise over the exit summary.
191
+ """
192
+ endpoint_url = f"{self.api_url}/render_finished"
193
+ headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
194
+ payload = {
195
+ "outcome": outcome,
196
+ }
197
+
198
+ return self.post_request(
199
+ endpoint_url, headers, payload, run_state, num_retries=0, silent=True, timeout=REPORT_TIMEOUT_SECONDS
200
+ )
201
+
176
202
  def render_functional_requirement(
177
203
  self,
178
204
  frid: str,
@@ -237,6 +263,7 @@ class CodeplainAPI:
237
263
  required_modules,
238
264
  unittests_issue,
239
265
  run_state: RunState,
266
+ conformance_tests_fixes: list[dict] | None = None,
240
267
  ):
241
268
  endpoint_url = f"{self.api_url}/fix_unittests_issue"
242
269
  headers = {"X-API-Key": self.api_key, "Content-Type": "application/json"}
@@ -251,6 +278,10 @@ class CodeplainAPI:
251
278
  "unittests_issue": unittests_issue,
252
279
  "unittest_batch_id": run_state.unittest_batch_id,
253
280
  }
281
+ # Implementation code changes made by the conformance tests fixer right before this unit tests run;
282
+ # only sent when unit tests are processed inside the conformance tests phase.
283
+ if conformance_tests_fixes is not None:
284
+ payload["conformance_tests_fixes"] = conformance_tests_fixes
254
285
 
255
286
  return self.post_request(endpoint_url, headers, payload, run_state)
256
287
 
@@ -180,6 +180,20 @@ def open_from(dirs, file_name):
180
180
  return None
181
181
 
182
182
 
183
+ def resolve_linked_resource(dirs, file_name):
184
+ """Return the first existing path for file_name across dirs, or None if not found.
185
+
186
+ Mirrors the search order used by open_from so that validating a linked resource and
187
+ loading it can never disagree.
188
+ """
189
+ for dir in dirs:
190
+ full_file_name = os.path.join(dir, file_name)
191
+ if os.path.exists(full_file_name):
192
+ return full_file_name
193
+
194
+ return None
195
+
196
+
183
197
  def load_linked_resources(template_dirs: list[str], resources_list, module_name: str):
184
198
  linked_resources = {}
185
199
 
@@ -0,0 +1,41 @@
1
+ """Verify a working git is available before GitPython is imported.
2
+
3
+ GitPython probes for the git executable on import and raises from module scope
4
+ if it is missing or broken, so the CLI would crash with a traceback before
5
+ main() could explain it.
6
+ """
7
+
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+
12
+ from plain2code_console import console
13
+
14
+ GIT_MISSING_MESSAGE = "git is not installed. Please install git and try again."
15
+ GIT_BROKEN_MESSAGE = "git is installed but not working. Please repair your git installation and try again."
16
+ MACOS_BROKEN_GIT_HINT = (
17
+ "This usually means the Command Line Tools are missing; install them with 'xcode-select --install'."
18
+ )
19
+
20
+
21
+ def git_runs(git_path):
22
+ """Whether `git version` succeeds. A git on PATH can still be unusable: on
23
+ macOS without the Command Line Tools /usr/bin/git is a stub that exits with
24
+ an xcrun error, and GIT_PYTHON_REFRESH=quiet does not suppress that."""
25
+ try:
26
+ return subprocess.run([git_path, "version"], capture_output=True, timeout=10).returncode == 0
27
+ except (OSError, subprocess.SubprocessError):
28
+ return False
29
+
30
+
31
+ def require_git():
32
+ """Exit with a clean message unless a working git is available."""
33
+ git_path = shutil.which("git")
34
+ if git_path is None:
35
+ console.error(f"{GIT_MISSING_MESSAGE}\n")
36
+ sys.exit(1)
37
+
38
+ if not git_runs(git_path):
39
+ hint = f"\n{MACOS_BROKEN_GIT_HINT}" if sys.platform == "darwin" else ""
40
+ console.error(f"{GIT_BROKEN_MESSAGE}{hint}\n")
41
+ sys.exit(1)
@@ -0,0 +1,175 @@
1
+ #!/bin/bash
2
+
3
+ set -euo pipefail
4
+
5
+ # Brand Colors (use exported colors if available, otherwise define them)
6
+ YELLOW="${YELLOW:-\033[38;2;224;255;110m}"
7
+ GREEN="${GREEN:-\033[38;2;121;252;150m}"
8
+ WHITE="${WHITE:-\033[38;2;255;255;255m}"
9
+ RED="${RED:-\033[38;2;239;68;68m}"
10
+ GRAY="${GRAY:-\033[38;2;128;128;128m}"
11
+ BOLD="${BOLD:-\033[1m}"
12
+ NC="${NC:-\033[0m}"
13
+
14
+ # Examples configuration
15
+ EXAMPLES_FOLDER_NAME="plainlang-examples"
16
+ EXAMPLES_DOWNLOAD_URL="https://codeplain.ai/examples/unix"
17
+
18
+ # Prompt the user, reading from the terminal when there is one. Falling back to
19
+ # stdin keeps the script usable in non-interactive environments instead of
20
+ # dying on an unreadable /dev/tty.
21
+ prompt_user() {
22
+ local prompt="$1"
23
+ local varname="$2"
24
+
25
+ # Test that /dev/tty can actually be opened: it exists but fails to open
26
+ # when the process has no controlling terminal.
27
+ if { : < /dev/tty; } 2>/dev/null; then
28
+ read -r -p "$prompt" "$varname" < /dev/tty || true
29
+ else
30
+ read -r -p "$prompt" "$varname" || true
31
+ fi
32
+ }
33
+
34
+ # Make the user acknowledge that the examples were not installed. The
35
+ # installation continues either way, but the failure must not scroll past
36
+ # unseen - the installer clears the screen in its next step.
37
+ confirm_continue_after_failure() {
38
+ echo ""
39
+ echo -e " ${YELLOW}The examples were not installed.${NC} ${GRAY}The installation will continue.${NC}"
40
+ echo ""
41
+ prompt_user "$(printf '%b' " Continue? Press ${WHITE}[Enter]${NC} to acknowledge: ")" _ACKNOWLEDGED
42
+ echo ""
43
+ }
44
+
45
+ # Tell the user how to get unzip on their system, then let the installer move on.
46
+ report_missing_unzip_tool() {
47
+ echo -e " ${RED}✗ Cannot extract the examples: no 'unzip' command found.${NC}"
48
+ echo ""
49
+ echo -e " ${GRAY}Install unzip and re-run the installer to get the examples:${NC}"
50
+ echo ""
51
+ case "$OSTYPE" in
52
+ darwin*)
53
+ echo -e " ${WHITE}${BOLD}brew install unzip${NC}"
54
+ ;;
55
+ *)
56
+ echo -e " ${WHITE}${BOLD}apt-get install unzip${NC} ${GRAY}(Debian/Ubuntu)${NC}"
57
+ echo -e " ${WHITE}${BOLD}dnf install unzip${NC} ${GRAY}(Fedora/RHEL)${NC}"
58
+ echo -e " ${WHITE}${BOLD}apk add unzip${NC} ${GRAY}(Alpine)${NC}"
59
+ ;;
60
+ esac
61
+ echo ""
62
+ echo -e " ${GRAY}The examples are also available at:${NC}"
63
+ echo -e " ${WHITE}https://github.com/Codeplain-ai/plainlang-examples${NC}"
64
+ echo ""
65
+ }
66
+
67
+ # Checked before anything is downloaded, so a missing unzip is reported up
68
+ # front instead of leaving the user with an installer that just stops.
69
+ if ! command -v unzip &> /dev/null; then
70
+ report_missing_unzip_tool
71
+ confirm_continue_after_failure
72
+ exit 0
73
+ fi
74
+
75
+ # Show current directory and ask for extraction path
76
+ CURRENT_DIR=$(pwd)
77
+ echo -e " Current folder: ${WHITE}${CURRENT_DIR}${NC}"
78
+ echo ""
79
+ echo -e " Extract examples here, or enter a different path:"
80
+ echo ""
81
+ prompt_user " [Enter for current, or type path]: " EXTRACT_PATH
82
+ echo ""
83
+
84
+ # Use current directory if empty
85
+ if [ -z "${EXTRACT_PATH:-}" ]; then
86
+ EXTRACT_PATH="$CURRENT_DIR"
87
+ fi
88
+
89
+ # Expand ~ to home directory
90
+ EXTRACT_PATH="${EXTRACT_PATH/#\~/$HOME}"
91
+
92
+ SKIP_DOWNLOAD=false
93
+
94
+ # Check if directory exists, create if not
95
+ if [ ! -d "$EXTRACT_PATH" ]; then
96
+ echo -e " ${GRAY}Creating directory...${NC}"
97
+ if ! mkdir -p "$EXTRACT_PATH" 2>/dev/null; then
98
+ echo -e " ${RED}✗ Failed to create directory: ${EXTRACT_PATH}${NC}"
99
+ confirm_continue_after_failure
100
+ SKIP_DOWNLOAD=true
101
+ fi
102
+ fi
103
+
104
+ EXAMPLES_INSTALLED=false
105
+
106
+ if [ "$SKIP_DOWNLOAD" = false ]; then
107
+ echo -e " ${GRAY}Downloading examples...${NC}"
108
+
109
+ # Download the zip file
110
+ TEMP_ZIP=$(mktemp)
111
+ DOWNLOAD_OK=false
112
+ DOWNLOAD_ERROR="Could not download ${EXAMPLES_DOWNLOAD_URL}"
113
+ if curl -L -s -o "$TEMP_ZIP" "$EXAMPLES_DOWNLOAD_URL" && [ -s "$TEMP_ZIP" ]; then
114
+ DOWNLOAD_OK=true
115
+ fi
116
+
117
+ # A server error page is a successful download of the wrong thing, so check
118
+ # for the zip magic bytes rather than handing unzip an HTML page.
119
+ if [ "$DOWNLOAD_OK" = true ] && [ "$(head -c 2 "$TEMP_ZIP")" != "PK" ]; then
120
+ DOWNLOAD_OK=false
121
+ DOWNLOAD_ERROR="The downloaded file is not a zip archive."
122
+ fi
123
+
124
+ if [ "$DOWNLOAD_OK" = true ]; then
125
+ echo -e " ${GRAY}Extracting to ${EXTRACT_PATH}...${NC}"
126
+
127
+ # Extract the zip file (contents are at the zip root, so extract into the target folder)
128
+ EXTRACTED_DIR="${EXTRACT_PATH}/${EXAMPLES_FOLDER_NAME}"
129
+ rm -rf "$EXTRACTED_DIR" 2>/dev/null || true # Remove existing if present
130
+
131
+ # Keep the extractor's own error output: it is the only clue about why
132
+ # extraction failed (corrupt download, no disk space, no permissions).
133
+ if EXTRACT_OUTPUT=$(unzip -q -o "$TEMP_ZIP" -d "$EXTRACTED_DIR" 2>&1); then
134
+ # Remove the .gitignore file from the root of the extracted directory
135
+ if [ -f "${EXTRACTED_DIR}/.gitignore" ]; then
136
+ rm -f "${EXTRACTED_DIR}/.gitignore"
137
+ fi
138
+
139
+ clear || true
140
+ echo ""
141
+ echo -e " ${GREEN}✓ Examples downloaded successfully!${NC}"
142
+ echo ""
143
+ echo -e " ${GRAY}Examples are in: ${EXTRACTED_DIR}${NC}"
144
+ echo ""
145
+ echo -e " ${WHITE}${BOLD}Try the hello, world example:${NC}"
146
+ echo ""
147
+ echo -e " ${GRAY}Example folder:${NC} ${WHITE}cd ${EXTRACTED_DIR}/hello-world/python${NC}"
148
+ echo ""
149
+ echo -e " ${GRAY}Render the example:${NC} ${WHITE}codeplain hello-world-python.plain${NC}"
150
+ echo ""
151
+ echo -e " ${GRAY}See hello-world/python/README.md for details.${NC}"
152
+ echo ""
153
+ EXAMPLES_INSTALLED=true
154
+ else
155
+ echo -e " ${RED}✗ Failed to extract examples.${NC}"
156
+ if [ -n "$EXTRACT_OUTPUT" ]; then
157
+ echo -e " ${GRAY}$(echo "$EXTRACT_OUTPUT" | tail -n 3)${NC}"
158
+ fi
159
+ fi
160
+
161
+ # Clean up temp file
162
+ rm -f "$TEMP_ZIP"
163
+ else
164
+ echo -e " ${RED}✗ Failed to download examples.${NC}"
165
+ echo -e " ${GRAY}${DOWNLOAD_ERROR}${NC}"
166
+ rm -f "$TEMP_ZIP"
167
+ fi
168
+
169
+ if [ "$EXAMPLES_INSTALLED" = true ]; then
170
+ echo ""
171
+ prompt_user "$(printf '%b' " Press ${WHITE}[Enter]${NC} to continue...")" _CONTINUE
172
+ else
173
+ confirm_continue_after_failure
174
+ fi
175
+ fi
@@ -425,9 +425,11 @@ else
425
425
  echo ""
426
426
  fi
427
427
 
428
- # Run examples download if user agrees
428
+ # Run examples download if user agrees. The examples are a nice-to-have, so a
429
+ # failure there must never abort the installation (set -e would otherwise take
430
+ # the whole installer down without printing anything).
429
431
  if [[ ! "${DOWNLOAD_EXAMPLES:-}" =~ ^[Nn]$ ]]; then
430
- run_script "examples.sh"
432
+ run_script "examples.sh" || echo -e "${GRAY}Example download did not complete. Continuing with the installation.${NC}"
431
433
  fi
432
434
 
433
435
  # Final verification: make sure the installed codeplain can actually run and
@@ -19,6 +19,39 @@ $CROSS = [char]0x2717
19
19
  $EXAMPLES_FOLDER_NAME = "plainlang-examples"
20
20
  $EXAMPLES_DOWNLOAD_URL = "https://codeplain.ai/examples/windows"
21
21
 
22
+ # Make the user acknowledge that the examples were not installed. The
23
+ # installation continues either way, but the failure must not scroll past
24
+ # unseen - the installer clears the screen in its next step.
25
+ function Confirm-ContinueAfterFailure {
26
+ Write-Host ""
27
+ Write-Host " ${YELLOW}The examples were not installed.${NC} ${GRAY}The installation will continue.${NC}"
28
+ Write-Host ""
29
+ Read-Host " Continue? Press ${WHITE}[Enter]${NC} to acknowledge"
30
+ Write-Host ""
31
+ }
32
+
33
+ # Expand-Archive lives in the Microsoft.PowerShell.Archive module, shipped with
34
+ # PowerShell 5.0 and later. Checked before anything is downloaded, so a machine
35
+ # without it is told what to do instead of just seeing an extraction failure.
36
+ function Show-MissingArchiveCmdletMessage {
37
+ Write-Host " ${RED}${CROSS} Cannot extract the examples: Expand-Archive is not available.${NC}"
38
+ Write-Host ""
39
+ Write-Host " ${GRAY}Expand-Archive requires PowerShell 5.0 or later. Install a newer${NC}"
40
+ Write-Host " ${GRAY}PowerShell and re-run the installer to get the examples:${NC}"
41
+ Write-Host ""
42
+ Write-Host " ${WHITE}${BOLD}https://aka.ms/powershell${NC}"
43
+ Write-Host ""
44
+ Write-Host " ${GRAY}The examples are also available at:${NC}"
45
+ Write-Host " ${WHITE}https://github.com/Codeplain-ai/plainlang-examples${NC}"
46
+ Write-Host ""
47
+ }
48
+
49
+ if (-not (Get-Command Expand-Archive -ErrorAction SilentlyContinue)) {
50
+ Show-MissingArchiveCmdletMessage
51
+ Confirm-ContinueAfterFailure
52
+ exit 0
53
+ }
54
+
22
55
  # Show current directory and ask for extraction path
23
56
  $CURRENT_DIR = Get-Location
24
57
  Write-Host " Current folder: ${WHITE}${CURRENT_DIR}${NC}"
@@ -47,11 +80,13 @@ if (-not (Test-Path $EXTRACT_PATH -PathType Container)) {
47
80
  New-Item -ItemType Directory -Path $EXTRACT_PATH -Force | Out-Null
48
81
  } catch {
49
82
  Write-Host " ${RED}${CROSS} Failed to create directory: ${EXTRACT_PATH}${NC}"
50
- Write-Host " ${GRAY}Skipping example download.${NC}"
83
+ Confirm-ContinueAfterFailure
51
84
  $SKIP_DOWNLOAD = $true
52
85
  }
53
86
  }
54
87
 
88
+ $EXAMPLES_INSTALLED = $false
89
+
55
90
  if (-not $SKIP_DOWNLOAD) {
56
91
  Write-Host " ${GRAY}Downloading examples...${NC}"
57
92
 
@@ -60,7 +95,26 @@ if (-not $SKIP_DOWNLOAD) {
60
95
  try {
61
96
  Invoke-WebRequest -Uri $EXAMPLES_DOWNLOAD_URL -OutFile $TEMP_ZIP -UseBasicParsing
62
97
 
98
+ # Check for the zip magic bytes rather than handing Expand-Archive a
99
+ # server error page. Read via a stream: 'Get-Content -Encoding Byte' is
100
+ # Windows PowerShell 5.1 only, '-AsByteStream' is PowerShell 6+ only.
101
+ $IS_ZIP = $false
63
102
  if (Test-Path $TEMP_ZIP) {
103
+ $HEADER = New-Object byte[] 2
104
+ $STREAM = [System.IO.File]::OpenRead($TEMP_ZIP)
105
+ try {
106
+ $BYTES_READ = $STREAM.Read($HEADER, 0, 2)
107
+ } finally {
108
+ $STREAM.Close()
109
+ }
110
+ $IS_ZIP = ($BYTES_READ -eq 2 -and $HEADER[0] -eq 0x50 -and $HEADER[1] -eq 0x4B)
111
+ }
112
+
113
+ if (-not $IS_ZIP -and (Test-Path $TEMP_ZIP)) {
114
+ Write-Host " ${RED}${CROSS} Failed to download examples.${NC}"
115
+ Write-Host " ${GRAY}The downloaded file is not a zip archive.${NC}"
116
+ Remove-Item -Path $TEMP_ZIP -Force -ErrorAction SilentlyContinue
117
+ } elseif ($IS_ZIP) {
64
118
  Write-Host " ${GRAY}Extracting to ${EXTRACT_PATH}...${NC}"
65
119
 
66
120
  try {
@@ -91,8 +145,12 @@ if (-not $SKIP_DOWNLOAD) {
91
145
  Write-Host ""
92
146
  Write-Host " ${GRAY}See hello-world/python/README.md for details.${NC}"
93
147
  Write-Host ""
148
+ $EXAMPLES_INSTALLED = $true
94
149
  } catch {
150
+ # Keep the cmdlet's own message: it is the only clue about why
151
+ # extraction failed (corrupt download, no space, no permissions).
95
152
  Write-Host " ${RED}${CROSS} Failed to extract examples.${NC}"
153
+ Write-Host " ${GRAY}$($_.Exception.Message)${NC}"
96
154
  }
97
155
 
98
156
  Remove-Item -Path $TEMP_ZIP -Force -ErrorAction SilentlyContinue
@@ -104,6 +162,10 @@ if (-not $SKIP_DOWNLOAD) {
104
162
  Remove-Item -Path $TEMP_ZIP -Force -ErrorAction SilentlyContinue
105
163
  }
106
164
 
107
- Write-Host ""
108
- Read-Host " Press ${WHITE}[Enter]${NC} to continue..."
165
+ if ($EXAMPLES_INSTALLED) {
166
+ Write-Host ""
167
+ Read-Host " Press ${WHITE}[Enter]${NC} to continue..."
168
+ } else {
169
+ Confirm-ContinueAfterFailure
170
+ }
109
171
  }
@@ -526,9 +526,16 @@ if ($nonInteractive) {
526
526
  Write-Host ""
527
527
  }
528
528
 
529
- # Run examples download if user agrees
529
+ # Run examples download if user agrees. The examples are a nice-to-have, so a
530
+ # failure there must never abort the installation: examples.ps1 runs in this
531
+ # runspace via the call operator, so a terminating error inside it would
532
+ # otherwise propagate here and take the whole installer down.
530
533
  if ($downloadExamples -notmatch '^[Nn]$') {
531
- Invoke-SubScript "examples.ps1"
534
+ try {
535
+ Invoke-SubScript "examples.ps1"
536
+ } catch {
537
+ Write-Host "${GRAY}Example download did not complete. Continuing with the installation.${NC}"
538
+ }
532
539
  }
533
540
 
534
541
  # Final verification: make sure the installed codeplain can actually run and