quadkit-cli 0.0.43__tar.gz → 0.0.45__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 (203) hide show
  1. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/PKG-INFO +5 -5
  2. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/README.md +4 -4
  3. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/pyproject.toml +1 -1
  4. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/new.py +4 -6
  5. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/__init__.py +2 -11
  6. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/_lazy_exports.py +2 -11
  7. quadkit_cli-0.0.45/src/quadkit/cli/registry/presets.py +104 -0
  8. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/template.py +9 -86
  9. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/scaffold/__init__.py +12 -4
  10. quadkit_cli-0.0.45/src/quadkit/cli/scaffold/metadata.py +204 -0
  11. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/scaffold/module_files.py +65 -24
  12. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/scaffold/render.py +85 -3
  13. quadkit_cli-0.0.45/src/quadkit/cli/scaffold/specs.py +90 -0
  14. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/scaffold/support_files.py +18 -4
  15. quadkit_cli-0.0.45/tests/unit/registry/test_presets.py +87 -0
  16. quadkit_cli-0.0.45/tests/unit/registry/test_template.py +134 -0
  17. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_new.py +5 -6
  18. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_project_layout.py +6 -8
  19. quadkit_cli-0.0.45/tests/unit/test_project_metadata.py +178 -0
  20. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_scaffold_and_validators.py +37 -28
  21. quadkit_cli-0.0.43/src/quadkit/cli/registry/presets.py +0 -219
  22. quadkit_cli-0.0.43/src/quadkit/cli/scaffold/specs.py +0 -203
  23. quadkit_cli-0.0.43/tests/unit/registry/test_presets.py +0 -120
  24. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/.gitignore +0 -0
  25. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/LICENSE +0 -0
  26. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/NOTICE +0 -0
  27. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/example.yaml +0 -0
  28. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/__init__.py +0 -0
  29. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/assembly/__init__.py +0 -0
  30. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/assembly/assembler.py +0 -0
  31. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/__init__.py +0 -0
  32. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/add.py +0 -0
  33. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/config.py +0 -0
  34. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/config_utils.py +0 -0
  35. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/contrib.py +0 -0
  36. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/db.py +0 -0
  37. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/db_bootstrap.py +0 -0
  38. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/db_commands_data.py +0 -0
  39. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/db_commands_migrations.py +0 -0
  40. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/db_commands_ops.py +0 -0
  41. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/dev.py +0 -0
  42. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/events.py +0 -0
  43. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/gen.py +0 -0
  44. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/init.py +0 -0
  45. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/inspect.py +0 -0
  46. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/meta.py +0 -0
  47. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/project.py +0 -0
  48. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/py.typed +0 -0
  49. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/run.py +0 -0
  50. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/shell.py +0 -0
  51. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/commands/system.py +0 -0
  52. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/config.py +0 -0
  53. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/constants.py +0 -0
  54. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/contributors/__init__.py +0 -0
  55. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/contributors/base.py +0 -0
  56. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/contributors/core.py +0 -0
  57. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/contributors/discovery.py +0 -0
  58. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/contributors/registry.py +0 -0
  59. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/contributors/runtime.py +0 -0
  60. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/decorators.py +0 -0
  61. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/di/__init__.py +0 -0
  62. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/di/provider.py +0 -0
  63. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/di/sub_providers/__init__.py +0 -0
  64. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/di/sub_providers/contributor.py +0 -0
  65. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/events.py +0 -0
  66. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/exceptions.py +0 -0
  67. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/generators/__init__.py +0 -0
  68. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/generators/base.py +0 -0
  69. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/generators/field_parser.py +0 -0
  70. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/generators/provider.py +0 -0
  71. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/generators/test.py +0 -0
  72. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/hooks.py +0 -0
  73. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/layout.py +0 -0
  74. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/lib/__init__.py +0 -0
  75. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/lib/config_loader.py +0 -0
  76. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/lib/entry_point.py +0 -0
  77. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/lib/naming.py +0 -0
  78. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/lib/templates.py +0 -0
  79. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/module.py +0 -0
  80. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/output/__init__.py +0 -0
  81. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/output/manager.py +0 -0
  82. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/package_names.py +0 -0
  83. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/protocols.py +0 -0
  84. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/py.typed +0 -0
  85. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/backend_base.py +0 -0
  86. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/command.py +0 -0
  87. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/completion.py +0 -0
  88. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/config.py +0 -0
  89. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/database.py +0 -0
  90. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/database_backends.py +0 -0
  91. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/environment.py +0 -0
  92. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/formatter.py +0 -0
  93. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/generator.py +0 -0
  94. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health.py +0 -0
  95. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/__init__.py +0 -0
  96. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/base.py +0 -0
  97. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/deps.py +0 -0
  98. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/project.py +0 -0
  99. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/providers.py +0 -0
  100. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/python_env.py +0 -0
  101. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/tools.py +0 -0
  102. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/health_checks/vcs_docker.py +0 -0
  103. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/hook.py +0 -0
  104. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/inspect.py +0 -0
  105. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/migration.py +0 -0
  106. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/provider.py +0 -0
  107. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/provider_catalog.py +0 -0
  108. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/provider_registry.py +0 -0
  109. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/secrets.py +0 -0
  110. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/serializer.py +0 -0
  111. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/server.py +0 -0
  112. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/task.py +0 -0
  113. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/telemetry.py +0 -0
  114. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/version.py +0 -0
  115. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/registry/watcher.py +0 -0
  116. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/runtime/__init__.py +0 -0
  117. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/runtime/context.py +0 -0
  118. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/runtime/error_handler.py +0 -0
  119. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/runtime/main.py +0 -0
  120. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/scaffold/app_files.py +0 -0
  121. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/scaffold_package.py +0 -0
  122. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/templates/provider.py.jinja2 +0 -0
  123. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/templates/py.typed +0 -0
  124. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/templates/test_unit.py.jinja2 +0 -0
  125. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/cli/types.py +0 -0
  126. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/src/quadkit/py.typed +0 -0
  127. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/conftest.py +0 -0
  128. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/integration/__init__.py +0 -0
  129. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/integration/test_cli_lifecycle.py +0 -0
  130. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/test_package_structure_p1.py +0 -0
  131. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/__init__.py +0 -0
  132. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/_test_cli_contribution_system_support.py +0 -0
  133. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/__init__.py +0 -0
  134. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_config.py +0 -0
  135. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_config_happy_paths.py +0 -0
  136. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_contrib.py +0 -0
  137. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_events_cmd.py +0 -0
  138. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_inspect.py +0 -0
  139. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_meta.py +0 -0
  140. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_run.py +0 -0
  141. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_shell.py +0 -0
  142. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/commands/test_system.py +0 -0
  143. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/di/test_config_alignment.py +0 -0
  144. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/lib/test_templates.py +0 -0
  145. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/__init__.py +0 -0
  146. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_completion.py +0 -0
  147. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_database.py +0 -0
  148. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_environment.py +0 -0
  149. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_formatter.py +0 -0
  150. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_health.py +0 -0
  151. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_hook.py +0 -0
  152. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_inspect_registry.py +0 -0
  153. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_migration.py +0 -0
  154. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_secrets.py +0 -0
  155. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_serializer.py +0 -0
  156. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_task.py +0 -0
  157. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_telemetry.py +0 -0
  158. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_version.py +0 -0
  159. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/registry/test_watcher.py +0 -0
  160. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_base_contributor.py +0 -0
  161. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_constants.py +0 -0
  162. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_context.py +0 -0
  163. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_contribution_system.py +0 -0
  164. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_contribution_system__testclicontributorsubprovider.py +0 -0
  165. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_errors.py +0 -0
  166. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_exceptions.py +0 -0
  167. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_import_isolation.py +0 -0
  168. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_module.py +0 -0
  169. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_cli_types.py +0 -0
  170. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_config_async.py +0 -0
  171. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_config_loader.py +0 -0
  172. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_config_save.py +0 -0
  173. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_config_validate.py +0 -0
  174. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_contributor_registry.py +0 -0
  175. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_contributor_runtime.py +0 -0
  176. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_core_contributor.py +0 -0
  177. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_db.py +0 -0
  178. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_decorators.py +0 -0
  179. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_dev_command.py +0 -0
  180. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_error_handler.py +0 -0
  181. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_events.py +0 -0
  182. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_exception_handling.py +0 -0
  183. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_field_parser.py +0 -0
  184. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_gen.py +0 -0
  185. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_gen_controller.py +0 -0
  186. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_gen_provider.py +0 -0
  187. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_generator_base.py +0 -0
  188. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_generator_option_help.py +0 -0
  189. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_generator_registry.py +0 -0
  190. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_generators_contract.py +0 -0
  191. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_health_checks.py +0 -0
  192. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_hooks.py +0 -0
  193. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_init_command.py +0 -0
  194. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_layout.py +0 -0
  195. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_main.py +0 -0
  196. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_output.py +0 -0
  197. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_output_json.py +0 -0
  198. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_project.py +0 -0
  199. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_provider_registry.py +0 -0
  200. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_round40_hardening.py +0 -0
  201. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_run_detection.py +0 -0
  202. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_server_registry.py +0 -0
  203. {quadkit_cli-0.0.43 → quadkit_cli-0.0.45}/tests/unit/test_storage_driver_generator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: quadkit-cli
3
- Version: 0.0.43
3
+ Version: 0.0.45
4
4
  Summary: Command-line interface for Quadkit Framework - Project scaffolding, code generation, and development tools
5
5
  Project-URL: Homepage, https://dbtinoy-.github.io/quadkit/
6
6
  Project-URL: Repository, https://github.com/dbtinoy-/quadkit
@@ -94,7 +94,7 @@ Requires **Python >= 3.11**.
94
94
  There is no code to write — the product is the command line:
95
95
 
96
96
  ```bash
97
- # Create a new project (template defaults to web-api); the package
97
+ # Create a new project (single `api` template); the package
98
98
  # inside is my_api/ with app.py exposing create_app()
99
99
  quadkit new project my-api
100
100
  cd my-api
@@ -167,10 +167,10 @@ review release notes on upgrade. Report vulnerabilities privately per
167
167
 
168
168
  ## Stability
169
169
 
170
- Version `0.0.43` in the `0.x` series, released in lockstep with the
170
+ Version `0.0.45` in the `0.x` series, released in lockstep with the
171
171
  other four distributions; APIs may change between minor versions until
172
- 1.0 — pin an exact version (`quadkit-cli==0.0.43`) or a tight range
173
- (`>=0.0.43,<0.1.0`). Full policy:
172
+ 1.0 — pin an exact version (`quadkit-cli==0.0.45`) or a tight range
173
+ (`>=0.0.45,<0.1.0`). Full policy:
174
174
  [stability and compatibility](https://dbtinoy-.github.io/quadkit/reference/stability/).
175
175
 
176
176
  ## Links
@@ -38,7 +38,7 @@ Requires **Python >= 3.11**.
38
38
  There is no code to write — the product is the command line:
39
39
 
40
40
  ```bash
41
- # Create a new project (template defaults to web-api); the package
41
+ # Create a new project (single `api` template); the package
42
42
  # inside is my_api/ with app.py exposing create_app()
43
43
  quadkit new project my-api
44
44
  cd my-api
@@ -111,10 +111,10 @@ review release notes on upgrade. Report vulnerabilities privately per
111
111
 
112
112
  ## Stability
113
113
 
114
- Version `0.0.43` in the `0.x` series, released in lockstep with the
114
+ Version `0.0.45` in the `0.x` series, released in lockstep with the
115
115
  other four distributions; APIs may change between minor versions until
116
- 1.0 — pin an exact version (`quadkit-cli==0.0.43`) or a tight range
117
- (`>=0.0.43,<0.1.0`). Full policy:
116
+ 1.0 — pin an exact version (`quadkit-cli==0.0.45`) or a tight range
117
+ (`>=0.0.45,<0.1.0`). Full policy:
118
118
  [stability and compatibility](https://dbtinoy-.github.io/quadkit/reference/stability/).
119
119
 
120
120
  ## Links
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
 
7
7
  [project]
8
8
  name = "quadkit-cli"
9
- version = "0.0.43"
9
+ version = "0.0.45"
10
10
  description = "Command-line interface for Quadkit Framework - Project scaffolding, code generation, and development tools"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.11"
@@ -35,10 +35,7 @@ def main(
35
35
  "api",
36
36
  "--template",
37
37
  "-t",
38
- help=(
39
- "Project template "
40
- f"(available in this install: {', '.join(template_names())})"
41
- ),
38
+ help=f"Project template (the only template: {', '.join(template_names())})",
42
39
  ),
43
40
  directory: str = typer.Option(".", "--directory", "-d", help="Target directory"),
44
41
  interactive: bool = typer.Option(
@@ -53,13 +50,14 @@ def main(
53
50
  project_name = _valid_name(name, "Project")
54
51
  selected_template = template
55
52
 
56
- if interactive:
53
+ names = template_names()
54
+ if interactive and len(names) > 1:
57
55
  import click
58
56
 
59
57
  selected_template = typer.prompt(
60
58
  "Select a template",
61
59
  default="api",
62
- type=click.Choice(template_names()),
60
+ type=click.Choice(names),
63
61
  )
64
62
 
65
63
  try:
@@ -128,15 +128,10 @@ if TYPE_CHECKING:
128
128
  TasksInspector,
129
129
  )
130
130
  from quadkit.cli.registry.presets import (
131
- FullStackPreset,
132
- GraphQLPreset,
133
- MicroservicePreset,
134
- MinimalPreset,
131
+ ApiPreset,
135
132
  Preset,
136
133
  PresetDefinition,
137
134
  PresetRegistry,
138
- WebAPIPreset,
139
- WorkerPreset,
140
135
  )
141
136
  from quadkit.cli.registry.provider import (
142
137
  AdminProvider,
@@ -200,15 +195,11 @@ if TYPE_CHECKING:
200
195
  get_telemetry_status,
201
196
  )
202
197
  from quadkit.cli.registry.template import (
203
- FullStackTemplate,
204
- GraphQLTemplate,
205
- MinimalTemplate,
198
+ ApiTemplate,
206
199
  ProjectBuilder,
207
200
  ProjectTemplate,
208
201
  TemplateConfig,
209
202
  TemplateRegistry,
210
- WebAPITemplate,
211
- WorkerTemplate,
212
203
  )
213
204
  from quadkit.cli.registry.version import (
214
205
  VersionInfo,
@@ -11,6 +11,8 @@ from __future__ import annotations
11
11
  LAZY_IMPORTS: dict[str, str] = {
12
12
  "AIProvider": "quadkit.cli.registry.provider",
13
13
  "AdminProvider": "quadkit.cli.registry.provider",
14
+ "ApiPreset": "quadkit.cli.registry.presets",
15
+ "ApiTemplate": "quadkit.cli.registry.template",
14
16
  "AuthConfigValidator": "quadkit.cli.registry.config",
15
17
  "AuthProvider": "quadkit.cli.registry.provider",
16
18
  "BashCompletionGenerator": "quadkit.cli.registry.completion",
@@ -46,16 +48,12 @@ LAZY_IMPORTS: dict[str, str] = {
46
48
  "FishCompletionGenerator": "quadkit.cli.registry.completion",
47
49
  "FormatOptions": "quadkit.cli.registry.formatter",
48
50
  "FormatterRegistry": "quadkit.cli.registry.formatter",
49
- "FullStackPreset": "quadkit.cli.registry.presets",
50
- "FullStackTemplate": "quadkit.cli.registry.template",
51
51
  "GeneratorAdapter": "quadkit.cli.registry.generator",
52
52
  "GeneratorRegistry": "quadkit.cli.registry.generator",
53
53
  "GeneratorResult": "quadkit.cli.registry.generator",
54
54
  "GitCheck": "quadkit.cli.registry.health",
55
55
  "GranianBackend": "quadkit.cli.registry.server",
56
- "GraphQLPreset": "quadkit.cli.registry.presets",
57
56
  "GraphQLProvider": "quadkit.cli.registry.provider",
58
- "GraphQLTemplate": "quadkit.cli.registry.template",
59
57
  "GunicornBackend": "quadkit.cli.registry.server",
60
58
  "HealthCheck": "quadkit.cli.registry.health",
61
59
  "HealthCheckRegistry": "quadkit.cli.registry.health",
@@ -73,10 +71,7 @@ LAZY_IMPORTS: dict[str, str] = {
73
71
  "JSONFormatter": "quadkit.cli.registry.formatter",
74
72
  "LoggingHook": "quadkit.cli.registry.hook",
75
73
  "MessagingProvider": "quadkit.cli.registry.provider",
76
- "MicroservicePreset": "quadkit.cli.registry.presets",
77
74
  "MiddlewareInspector": "quadkit.cli.registry.inspect",
78
- "MinimalPreset": "quadkit.cli.registry.presets",
79
- "MinimalTemplate": "quadkit.cli.registry.template",
80
75
  "MonitorProvider": "quadkit.cli.registry.provider",
81
76
  "MonitoringProvider": "quadkit.cli.registry.provider",
82
77
  "MySQLBackend": "quadkit.cli.registry.database",
@@ -141,11 +136,7 @@ LAZY_IMPORTS: dict[str, str] = {
141
136
  "VersionInfo": "quadkit.cli.registry.version",
142
137
  "VersionRegistry": "quadkit.cli.registry.version",
143
138
  "VersionSource": "quadkit.cli.registry.version",
144
- "WebAPIPreset": "quadkit.cli.registry.presets",
145
- "WebAPITemplate": "quadkit.cli.registry.template",
146
139
  "WebProvider": "quadkit.cli.registry.provider",
147
- "WorkerPreset": "quadkit.cli.registry.presets",
148
- "WorkerTemplate": "quadkit.cli.registry.template",
149
140
  "YAMLFormatter": "quadkit.cli.registry.formatter",
150
141
  "ZshCompletionGenerator": "quadkit.cli.registry.completion",
151
142
  "create_database_connection": "quadkit.cli.registry.database",
@@ -0,0 +1,104 @@
1
+ """Presets registry for application presets.
2
+
3
+ This module provides a registry pattern for application presets.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import abc
9
+ from dataclasses import dataclass, field
10
+ from typing import Any, ClassVar
11
+
12
+ from quadkit.cli.package_names import packages_for
13
+
14
+
15
+ @dataclass
16
+ class Preset:
17
+ """A preset configuration of providers."""
18
+
19
+ name: str
20
+ description: str
21
+ packages: list[str] = field(default_factory=list)
22
+ config: dict[str, Any] = field(default_factory=dict)
23
+
24
+
25
+ class PresetDefinition(abc.ABC):
26
+ """Abstract base class for preset definitions."""
27
+
28
+ name: ClassVar[str]
29
+ description: ClassVar[str]
30
+ packages: ClassVar[list[str]] = []
31
+ config: ClassVar[dict[str, Any]] = {}
32
+
33
+ @abc.abstractmethod
34
+ def get_preset(self) -> Preset:
35
+ """Get the preset configuration."""
36
+
37
+
38
+ class ApiPreset(PresetDefinition):
39
+ """The single application preset: REST API with the web framework."""
40
+
41
+ name = "api"
42
+ description = "REST API with the web framework"
43
+ packages = packages_for("web")
44
+ config = {
45
+ "web": {"host": "0.0.0.0", "port": 8000}, # noqa: S104 — preset template default
46
+ }
47
+
48
+ def get_preset(self) -> Preset:
49
+ return Preset(
50
+ name=self.name,
51
+ description=self.description,
52
+ packages=self.packages.copy(),
53
+ config=self.config.copy(),
54
+ )
55
+
56
+
57
+ class PresetRegistry:
58
+ """Registry for application presets.
59
+
60
+ Instances are always empty — use :meth:`with_defaults` for the
61
+ in-package built-ins or :meth:`register` for plugin presets.
62
+ """
63
+
64
+ def __init__(self) -> None:
65
+ self._presets: dict[str, Preset] = {}
66
+
67
+ def register(self, preset_class: type[PresetDefinition]) -> None:
68
+ """Register a preset class."""
69
+ instance = preset_class()
70
+ preset = instance.get_preset()
71
+ self._presets[preset.name] = preset
72
+
73
+ def get(self, name: str) -> Preset | None:
74
+ """Get a preset by name."""
75
+ return self._presets.get(name)
76
+
77
+ def get_all(self) -> dict[str, Preset]:
78
+ """Get all registered presets."""
79
+ return self._presets.copy()
80
+
81
+ def get_choices(self) -> list[str]:
82
+ """Get list of available preset names."""
83
+ return list(self._presets.keys())
84
+
85
+ @classmethod
86
+ def _default_entries(cls) -> tuple[type[PresetDefinition], ...]:
87
+ """Return the built-in presets (a single one today)."""
88
+ return (ApiPreset,)
89
+
90
+ @classmethod
91
+ def with_defaults(cls) -> PresetRegistry:
92
+ """Return an instance populated with the built-in presets."""
93
+ registry = cls()
94
+ for entry in cls._default_entries():
95
+ registry.register(entry)
96
+ return registry
97
+
98
+
99
+ __all__ = [
100
+ "ApiPreset",
101
+ "Preset",
102
+ "PresetDefinition",
103
+ "PresetRegistry",
104
+ ]
@@ -9,7 +9,7 @@ import abc
9
9
  from dataclasses import dataclass, field
10
10
  from typing import TYPE_CHECKING, Any, ClassVar
11
11
 
12
- from quadkit.cli.package_names import packages_for, provider_available
12
+ from quadkit.cli.package_names import packages_for
13
13
 
14
14
  if TYPE_CHECKING:
15
15
  from pathlib import Path
@@ -43,76 +43,12 @@ class ProjectTemplate(abc.ABC):
43
43
  return self.packages.copy()
44
44
 
45
45
 
46
- class MinimalTemplate(ProjectTemplate):
47
- """Minimal template with just the core framework."""
46
+ class ApiTemplate(ProjectTemplate):
47
+ """The single project template: REST API with the web framework."""
48
48
 
49
- name = "minimal"
50
- description = "Bare minimum Quadkit application"
51
- packages = [
52
- "quadkit",
53
- ]
54
-
55
- def get_config(self) -> TemplateConfig:
56
- return TemplateConfig(
57
- name=self.name,
58
- description=self.description,
59
- packages=self.packages,
60
- )
61
-
62
-
63
- class WebAPITemplate(ProjectTemplate):
64
- """REST API template."""
65
-
66
- name = "web-api"
67
- description = "REST API with web framework"
68
- packages = packages_for("web", "sql")
69
-
70
- def get_config(self) -> TemplateConfig:
71
- return TemplateConfig(
72
- name=self.name,
73
- description=self.description,
74
- packages=self.packages,
75
- )
76
-
77
-
78
- class GraphQLTemplate(ProjectTemplate):
79
- """GraphQL API template."""
80
-
81
- name = "graphql"
82
- description = "GraphQL API with web framework"
83
- packages = packages_for("web", "sql", "graphql")
84
-
85
- def get_config(self) -> TemplateConfig:
86
- return TemplateConfig(
87
- name=self.name,
88
- description=self.description,
89
- packages=self.packages,
90
- )
91
-
92
-
93
- class WorkerTemplate(ProjectTemplate):
94
- """Background worker template."""
95
-
96
- name = "worker"
97
- description = "Background job processor"
98
- packages = packages_for("sql", "tasks", "queue")
99
-
100
- def get_config(self) -> TemplateConfig:
101
- return TemplateConfig(
102
- name=self.name,
103
- description=self.description,
104
- packages=self.packages,
105
- )
106
-
107
-
108
- class FullStackTemplate(ProjectTemplate):
109
- """Full-stack application template."""
110
-
111
- name = "fullstack"
112
- description = "Full application: auth, cache, storage, events, tasks, HTTP, monitoring"
113
- packages = packages_for(
114
- "web", "sql", "auth", "cache", "storage", "events", "tasks", "http", "monitor"
115
- )
49
+ name = "api"
50
+ description = "REST API with the web framework"
51
+ packages = packages_for("web")
116
52
 
117
53
  def get_config(self) -> TemplateConfig:
118
54
  return TemplateConfig(
@@ -151,17 +87,8 @@ class TemplateRegistry:
151
87
 
152
88
  @classmethod
153
89
  def _default_entries(cls) -> tuple[type[ProjectTemplate], ...]:
154
- """Return templates supported by installed optional providers."""
155
- entries: list[type[ProjectTemplate]] = [MinimalTemplate, WebAPITemplate]
156
- optional = (
157
- (GraphQLTemplate, ("sql", "graphql")),
158
- (WorkerTemplate, ("sql", "tasks", "queue")),
159
- (FullStackTemplate, ("web", "sql", "auth", "cache", "storage", "events", "tasks", "http", "monitor")),
160
- )
161
- for entry, features in optional:
162
- if all(provider_available(feature) for feature in features):
163
- entries.append(entry)
164
- return tuple(entries)
90
+ """Return the built-in templates (a single one today)."""
91
+ return (ApiTemplate,)
165
92
 
166
93
  @classmethod
167
94
  def with_defaults(cls) -> TemplateRegistry:
@@ -214,13 +141,9 @@ class ProjectBuilder:
214
141
 
215
142
 
216
143
  __all__ = [
217
- "FullStackTemplate",
218
- "GraphQLTemplate",
219
- "MinimalTemplate",
144
+ "ApiTemplate",
220
145
  "ProjectBuilder",
221
146
  "ProjectTemplate",
222
147
  "TemplateConfig",
223
148
  "TemplateRegistry",
224
- "WebAPITemplate",
225
- "WorkerTemplate",
226
149
  ]
@@ -4,9 +4,12 @@ Single source of truth for project bootstrapping — the catalogue lives in
4
4
  :mod:`quadkit.cli.scaffold.specs`, the rendered files in
5
5
  :mod:`quadkit.cli.scaffold.app_files`, :mod:`quadkit.cli.scaffold.module_files`
6
6
  and :mod:`quadkit.cli.scaffold.support_files`, and the writers in
7
- :mod:`quadkit.cli.scaffold.render`:
7
+ :mod:`quadkit.cli.scaffold.render`. Project metadata is rendered by
8
+ :func:`render_project_metadata` from an explicit :class:`ProjectMetadataContext`;
9
+ downstream builders can use this side-effect-free API without depending on
10
+ private template functions:
8
11
 
9
- - ``quadkit new project --template <name>`` (``commands/new.py``)
12
+ - ``quadkit new project`` (``commands/new.py``)
10
13
  - ``ProjectBuilder.create_project`` (``registry/template.py``)
11
14
  - scaffold alignment tests (``tests/unit/test_scaffold_alignment.py``)
12
15
 
@@ -15,7 +18,7 @@ Every generated file targets the **real** quadkit APIs (``Application``,
15
18
  generator map** in :mod:`quadkit.cli.layout`, so a scaffolded project and
16
19
  its generators stay aligned:
17
20
 
18
- quadkit new project myapp --template web-api
21
+ quadkit new project myapp
19
22
  cd myapp && pip install -e .
20
23
  quadkit gen controller users # src/myapp/controllers/users_controller.py
21
24
  quadkit dev # WebModule auto-discovers it
@@ -36,10 +39,13 @@ and directly in pytest fixtures.
36
39
 
37
40
  from __future__ import annotations
38
41
 
42
+ from quadkit.cli.scaffold.metadata import (
43
+ ProjectMetadataContext,
44
+ render_project_metadata,
45
+ )
39
46
  from quadkit.cli.scaffold.render import render_module, render_project
40
47
  from quadkit.cli.scaffold.specs import (
41
48
  _TEMPLATE_SPECS,
42
- TEMPLATE_ALIASES,
43
49
  TEMPLATES,
44
50
  ProjectTemplateSpec,
45
51
  component_packages,
@@ -50,9 +56,11 @@ from quadkit.cli.scaffold.specs import (
50
56
 
51
57
  __all__ = [
52
58
  "TEMPLATES",
59
+ "ProjectMetadataContext",
53
60
  "component_packages",
54
61
  "render_module",
55
62
  "render_project",
63
+ "render_project_metadata",
56
64
  "resolve_template",
57
65
  "root_output_dirs",
58
66
  "template_names",
@@ -0,0 +1,204 @@
1
+ """Project-level metadata renderers shared by CLI and downstream builders.
2
+
3
+ These renderers are intentionally side-effect free. Consumers provide the
4
+ facts from the project they are creating; the renderer returns a deterministic
5
+ map of files to add to that project. The generated-app profile is separate
6
+ from any framework website or release metadata profile.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Mapping
12
+ from dataclasses import dataclass
13
+ import re
14
+
15
+ __all__ = ["ProjectMetadataContext", "render_project_metadata"]
16
+
17
+
18
+ @dataclass(frozen=True, slots=True)
19
+ class ProjectMetadataContext:
20
+ """Facts needed to render concise, project-specific agent instructions.
21
+
22
+ Args:
23
+ project_name: Human-facing project name.
24
+ package_name: Importable Python package name.
25
+ summary: Short project description.
26
+ template_name: Scaffold/template identifier, not a release version.
27
+ features: Framework features actually present in the generated project.
28
+ dependencies: Direct requirements emitted into the project's manifest.
29
+ layout: Existing important paths and their purpose, relative to project
30
+ root. Paths must refer to this generated project, not the framework
31
+ checkout.
32
+ commands: Commands valid for the generated project and their purpose.
33
+ notes: Project-specific operational notes, e.g. a generated dev secret
34
+ must be replaced before deployment.
35
+ """
36
+
37
+ project_name: str
38
+ package_name: str
39
+ summary: str
40
+ template_name: str
41
+ features: tuple[str, ...]
42
+ dependencies: tuple[str, ...]
43
+ layout: tuple[tuple[str, str], ...]
44
+ commands: tuple[tuple[str, str], ...]
45
+ notes: tuple[str, ...] = ()
46
+
47
+ def __post_init__(self) -> None:
48
+ """Reject unsafe or malformed values before rendering Markdown."""
49
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_-]*", self.project_name):
50
+ raise ValueError("project_name must be a safe project slug")
51
+ if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", self.package_name):
52
+ raise ValueError("package_name must be a Python package identifier")
53
+ _require_single_line("summary", self.summary, allow_backticks=True)
54
+ _require_single_line("template_name", self.template_name)
55
+ for item in self.features:
56
+ _require_single_line("feature", item)
57
+ for item in self.dependencies:
58
+ _require_single_line("dependency", item)
59
+ if not self.summary.strip():
60
+ raise ValueError("summary must not be empty")
61
+ if not self.template_name.strip():
62
+ raise ValueError("template_name must not be empty")
63
+ if len(set(self.features)) != len(self.features):
64
+ raise ValueError("features must not contain duplicates")
65
+ if len(set(self.dependencies)) != len(self.dependencies):
66
+ raise ValueError("dependencies must not contain duplicates")
67
+ for path, purpose in self.layout:
68
+ _validate_project_path(path)
69
+ _require_single_line("layout purpose", purpose, allow_backticks=True)
70
+ for command, purpose in self.commands:
71
+ _require_single_line("command", command)
72
+ _require_single_line("command purpose", purpose, allow_backticks=True)
73
+ if not command.strip():
74
+ raise ValueError("commands must not contain empty commands")
75
+ for note in self.notes:
76
+ _require_single_line("note", note, allow_backticks=True)
77
+ if len({path for path, _ in self.layout}) != len(self.layout):
78
+ raise ValueError("layout paths must be unique")
79
+ if len({command for command, _ in self.commands}) != len(self.commands):
80
+ raise ValueError("commands must be unique")
81
+
82
+
83
+ def _require_single_line(
84
+ label: str, value: str, *, allow_backticks: bool = False
85
+ ) -> None:
86
+ if "\n" in value or "\r" in value or ("`" in value and not allow_backticks):
87
+ detail = "single line"
88
+ if not allow_backticks:
89
+ detail += " without backticks"
90
+ raise ValueError(f"{label} must be {detail}")
91
+
92
+
93
+ def _validate_project_path(path: str) -> None:
94
+ """Require a normalized, relative path that remains inside the project."""
95
+ _require_single_line("layout path", path)
96
+ if not path or path.startswith(("/", "\\")) or "\\" in path:
97
+ raise ValueError(
98
+ f"layout path must be a non-empty relative POSIX path: {path!r}"
99
+ )
100
+ normalized = path[:-1] if path.endswith("/") else path
101
+ parts = normalized.split("/")
102
+ if not normalized or any(part in {"", ".", ".."} for part in parts):
103
+ raise ValueError(f"layout path is not normalized: {path!r}")
104
+
105
+
106
+ def _escape_markdown(value: str) -> str:
107
+ """Escape inline Markdown punctuation in caller-supplied prose."""
108
+ return re.sub(r"([\\`*_{}\[\]()#+.!|>])", r"\\\1", value)
109
+
110
+
111
+ def _inline_code(value: str) -> str:
112
+ """Render one validated single-line value as inline code."""
113
+ return f"`{value}`"
114
+
115
+
116
+ def _render_agents_md(context: ProjectMetadataContext) -> str:
117
+ features = ", ".join(_inline_code(feature) for feature in context.features)
118
+ if not features:
119
+ features = "none declared by the selected template"
120
+
121
+ lines = [
122
+ f"# Agent Guide: {context.project_name}",
123
+ "",
124
+ "This file is project-specific guidance for coding agents. `README.md` is",
125
+ "the human quickstart; this file adds the project map, commands, and the",
126
+ "QuadKit conventions relevant to this scaffold. Update it when the project",
127
+ "layout or workflow changes.",
128
+ "",
129
+ "## Project summary",
130
+ "",
131
+ _escape_markdown(context.summary),
132
+ "",
133
+ f"- Scaffold: {_inline_code(context.template_name)}",
134
+ f"- Python package: {_inline_code(context.package_name)}",
135
+ f"- Framework features: {features}",
136
+ "",
137
+ "## Start here",
138
+ "",
139
+ "1. Read `README.md` for the project overview and quickstart.",
140
+ "2. Read `application.yaml` before changing runtime configuration.",
141
+ f"3. Treat `src/{context.package_name}/app.py` as the composition root; inspect it before changing module wiring.",
142
+ "4. Consult the linked QuadKit docs when a framework convention is unclear.",
143
+ "",
144
+ "## Useful commands",
145
+ "",
146
+ ]
147
+ if context.commands:
148
+ for command, purpose in context.commands:
149
+ lines.append(f"- {_inline_code(command)} — {_escape_markdown(purpose)}")
150
+ else:
151
+ lines.append("- No project commands were supplied by the scaffold context.")
152
+
153
+ lines += ["", "## Project map", ""]
154
+ for path, purpose in context.layout:
155
+ lines.append(f"- {_inline_code(path)} — {_escape_markdown(purpose)}")
156
+
157
+ lines += [
158
+ "",
159
+ "## QuadKit conventions",
160
+ "",
161
+ "- Preserve the existing project tree: feature code belongs in its feature",
162
+ " package; cross-cutting code belongs in `shared/`; bounded contexts live",
163
+ " under `modules/<name>/` when this project uses modules.",
164
+ "- Keep the composition root focused on assembling configuration, providers,",
165
+ " and modules. Put business behavior in services/domain types rather than",
166
+ " growing app wiring into a second service locator.",
167
+ "- Prefer the framework's typed protocols and constructor injection. Use",
168
+ " `Result[T, E]` for expected domain failures and exceptions for unexpected",
169
+ " infrastructure failures; follow the existing local patterns first.",
170
+ "- Keep I/O asynchronous where the surrounding API is asynchronous. Do not",
171
+ " introduce a parallel `models/` tree; follow the generated `domains/`",
172
+ " layout and the canonical QuadKit project-structure guide.",
173
+ "- Add or update tests with behavior changes. Keep configuration examples",
174
+ " and tests aligned with the actual package set in `pyproject.toml`.",
175
+ "",
176
+ "## QuadKit references",
177
+ "",
178
+ "- [For coding agents](https://oridecon.dev/quadkit/getting-started/for-coding-agents/)",
179
+ "- [Project structure](https://oridecon.dev/quadkit/getting-started/project-structure/)",
180
+ "- [Testing guide](https://oridecon.dev/quadkit/guides/testing/)",
181
+ "- [Common mistakes](https://oridecon.dev/quadkit/getting-started/common-mistakes/)",
182
+ ]
183
+
184
+ if context.dependencies:
185
+ lines += ["", "## Declared dependencies", ""]
186
+ lines.extend(f"- {_inline_code(item)}" for item in context.dependencies)
187
+
188
+ if context.notes:
189
+ lines += ["", "## Important project notes", ""]
190
+ lines.extend(f"- {_escape_markdown(note)}" for note in context.notes)
191
+
192
+ return "\n".join(lines) + "\n"
193
+
194
+
195
+ def render_project_metadata(
196
+ context: ProjectMetadataContext,
197
+ ) -> Mapping[str, str]:
198
+ """Return deterministic project metadata files for *context*.
199
+
200
+ The initial generated-app profile emits only ``AGENTS.md``. It deliberately
201
+ does not create website-only resources such as ``llms.txt``, crawler rules,
202
+ or an Agent Skills pack.
203
+ """
204
+ return {"AGENTS.md": _render_agents_md(context)}