tina4-python 3.13.8__tar.gz → 3.13.10__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 (157) hide show
  1. {tina4_python-3.13.8 → tina4_python-3.13.10}/.gitignore +5 -0
  2. {tina4_python-3.13.8 → tina4_python-3.13.10}/PKG-INFO +1 -1
  3. {tina4_python-3.13.8 → tina4_python-3.13.10}/pyproject.toml +1 -1
  4. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/__init__.py +1 -1
  5. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/ai/__init__.py +140 -4
  6. {tina4_python-3.13.8 → tina4_python-3.13.10}/README.md +0 -0
  7. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/CLAUDE.md +0 -0
  8. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/HtmlElement.py +0 -0
  9. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/Testing.py +0 -0
  10. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/api/__init__.py +0 -0
  11. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/auth/__init__.py +0 -0
  12. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/cache/__init__.py +0 -0
  13. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/cli/__init__.py +0 -0
  14. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/container/__init__.py +0 -0
  15. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/__init__.py +0 -0
  16. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/cache.py +0 -0
  17. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/constants.py +0 -0
  18. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/events.py +0 -0
  19. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/middleware.py +0 -0
  20. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/rate_limiter.py +0 -0
  21. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/request.py +0 -0
  22. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/response.py +0 -0
  23. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/router.py +0 -0
  24. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/core/server.py +0 -0
  25. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/crud/__init__.py +0 -0
  26. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/__init__.py +0 -0
  27. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/adapter.py +0 -0
  28. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/connection.py +0 -0
  29. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/firebird.py +0 -0
  30. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/mongodb.py +0 -0
  31. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/mssql.py +0 -0
  32. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/mysql.py +0 -0
  33. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/odbc.py +0 -0
  34. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/postgres.py +0 -0
  35. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/database/sqlite.py +0 -0
  36. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/debug/__init__.py +0 -0
  37. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/debug/error_overlay.py +0 -0
  38. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/dev_admin/__init__.py +0 -0
  39. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/dev_admin/metrics.py +0 -0
  40. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/dev_admin/plan.py +0 -0
  41. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/dev_admin/project_index.py +0 -0
  42. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/docs.py +0 -0
  43. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/dotenv/__init__.py +0 -0
  44. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/env.py +0 -0
  45. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/frond/FROND.md +0 -0
  46. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/frond/__init__.py +0 -0
  47. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/frond/engine.py +0 -0
  48. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/auth/meta.json +0 -0
  49. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/auth/src/routes/api/gallery_auth.py +0 -0
  50. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/database/meta.json +0 -0
  51. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/database/src/routes/api/gallery_db.py +0 -0
  52. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/error-overlay/meta.json +0 -0
  53. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/error-overlay/src/routes/api/gallery_crash.py +0 -0
  54. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/orm/meta.json +0 -0
  55. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/orm/src/orm/Product.py +0 -0
  56. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/orm/src/routes/api/gallery_products.py +0 -0
  57. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/queue/meta.json +0 -0
  58. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/queue/src/routes/api/gallery_queue.py +0 -0
  59. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/rest-api/meta.json +0 -0
  60. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/rest-api/src/routes/api/gallery_hello.py +0 -0
  61. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/templates/meta.json +0 -0
  62. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/templates/src/routes/gallery_page.py +0 -0
  63. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/gallery/templates/src/templates/gallery_page.twig +0 -0
  64. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/graphql/__init__.py +0 -0
  65. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/i18n/__init__.py +0 -0
  66. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/mcp/__init__.py +0 -0
  67. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/mcp/protocol.py +0 -0
  68. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/mcp/tools.py +0 -0
  69. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/messenger/__init__.py +0 -0
  70. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/migration/__init__.py +0 -0
  71. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/migration/runner.py +0 -0
  72. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/orm/__init__.py +0 -0
  73. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/orm/fields.py +0 -0
  74. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/orm/model.py +0 -0
  75. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/__feedback/widget.js +0 -0
  76. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/css/tina4.css +0 -0
  77. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/css/tina4.min.css +0 -0
  78. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/favicon.ico +0 -0
  79. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/images/logo.svg +0 -0
  80. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/images/tina4-logo-icon.webp +0 -0
  81. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/js/frond.js +0 -0
  82. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/js/frond.min.js +0 -0
  83. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/js/tina4-dev-admin.js +0 -0
  84. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/js/tina4-dev-admin.min.js +0 -0
  85. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/js/tina4.min.js +0 -0
  86. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/js/tina4js.min.js +0 -0
  87. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/swagger/index.html +0 -0
  88. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/public/swagger/oauth2-redirect.html +0 -0
  89. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/query_builder/__init__.py +0 -0
  90. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue/__init__.py +0 -0
  91. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue/job.py +0 -0
  92. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue/kafka_backend.py +0 -0
  93. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue/lite_backend.py +0 -0
  94. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue/mongo_backend.py +0 -0
  95. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue/rabbitmq_backend.py +0 -0
  96. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue_backends/__init__.py +0 -0
  97. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue_backends/kafka_backend.py +0 -0
  98. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue_backends/mongo_backend.py +0 -0
  99. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/queue_backends/rabbitmq_backend.py +0 -0
  100. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/__init__.py +0 -0
  101. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_alerts.scss +0 -0
  102. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_badges.scss +0 -0
  103. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_buttons.scss +0 -0
  104. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_cards.scss +0 -0
  105. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_forms.scss +0 -0
  106. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_grid.scss +0 -0
  107. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_modals.scss +0 -0
  108. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_nav.scss +0 -0
  109. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_reset.scss +0 -0
  110. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_tables.scss +0 -0
  111. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_typography.scss +0 -0
  112. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_utilities.scss +0 -0
  113. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/_variables.scss +0 -0
  114. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/base.scss +0 -0
  115. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/colors.scss +0 -0
  116. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/scss/tina4css/tina4.scss +0 -0
  117. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/seeder/__init__.py +0 -0
  118. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/service/__init__.py +0 -0
  119. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/session/__init__.py +0 -0
  120. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/session_handlers/__init__.py +0 -0
  121. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/session_handlers/mongodb_handler.py +0 -0
  122. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/session_handlers/redis_handler.py +0 -0
  123. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/session_handlers/valkey_handler.py +0 -0
  124. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/swagger/__init__.py +0 -0
  125. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/components/crud.twig +0 -0
  126. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/docker/distroless/Dockerfile +0 -0
  127. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/docker/poetry/Dockerfile +0 -0
  128. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/docker/python/Dockerfile +0 -0
  129. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/docker/uv/Dockerfile +0 -0
  130. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/302.twig +0 -0
  131. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/401.twig +0 -0
  132. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/403.twig +0 -0
  133. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/404.twig +0 -0
  134. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/500.twig +0 -0
  135. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/502.twig +0 -0
  136. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/503.twig +0 -0
  137. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/errors/base.twig +0 -0
  138. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/frontend/README.md +0 -0
  139. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/templates/readme.md +0 -0
  140. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/test/__init__.py +0 -0
  141. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/test_client/__init__.py +0 -0
  142. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/af/LC_MESSAGES/messages.mo +0 -0
  143. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/af/LC_MESSAGES/messages.po +0 -0
  144. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/en/LC_MESSAGES/messages.mo +0 -0
  145. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/en/LC_MESSAGES/messages.po +0 -0
  146. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/es/LC_MESSAGES/messages.mo +0 -0
  147. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/es/LC_MESSAGES/messages.po +0 -0
  148. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/fr/LC_MESSAGES/messages.mo +0 -0
  149. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/fr/LC_MESSAGES/messages.po +0 -0
  150. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/ja/LC_MESSAGES/messages.mo +0 -0
  151. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/ja/LC_MESSAGES/messages.po +0 -0
  152. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/zh/LC_MESSAGES/messages.mo +0 -0
  153. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/translations/zh/LC_MESSAGES/messages.po +0 -0
  154. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/validator/__init__.py +0 -0
  155. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/websocket/__init__.py +0 -0
  156. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/websocket/backplane.py +0 -0
  157. {tina4_python-3.13.8 → tina4_python-3.13.10}/tina4_python/wsdl/__init__.py +0 -0
@@ -44,6 +44,11 @@ test_queue*.db
44
44
  /publish.sh
45
45
  /src/scss/
46
46
  /broken
47
+ # Framework runtime artefacts — BrokenTracker writes import-time and
48
+ # route-time failure dumps here. They're transient (timestamped per
49
+ # error) and contain stack traces, never source-of-truth content.
50
+ /data/broken/
51
+ /data/.broken/
47
52
  __pycache__/
48
53
  .DS_Store
49
54
  .coverage
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tina4-python
3
- Version: 3.13.8
3
+ Version: 3.13.10
4
4
  Summary: Tina4 Python v3 — Zero-dependency, lightweight web framework
5
5
  Author-email: Andre van Zuydam <andrevanzuydam@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "tina4-python"
3
- version = "3.13.8"
3
+ version = "3.13.10"
4
4
  description = "Tina4 Python v3 — Zero-dependency, lightweight web framework"
5
5
  authors = [
6
6
  {name = "Andre van Zuydam", email = "andrevanzuydam@gmail.com"}
@@ -8,7 +8,7 @@ Tina4 Python v3.0 — Zero-dependency, lightweight web framework.
8
8
 
9
9
  One import, everything works.
10
10
  """
11
- __version__ = "3.13.8"
11
+ __version__ = "3.13.10"
12
12
 
13
13
  # ── Route decorators ──
14
14
  from tina4_python.core.router import ( # noqa: E402, F401
@@ -25,7 +25,14 @@ AI_TOOLS = [
25
25
  {"name": "aider", "description": "Aider", "context_file": "CONVENTIONS.md", "config_dir": None},
26
26
  {"name": "cline", "description": "Cline", "context_file": ".clinerules", "config_dir": None},
27
27
  {"name": "codex", "description": "OpenAI Codex", "context_file": "AGENTS.md", "config_dir": None},
28
- {"name": "antigravity", "description": "Google Antigravity", "context_file": ".antigravity/context.md", "config_dir": ".antigravity"},
28
+ # Google Antigravity is intentionally NOT listed as a separate tool.
29
+ # Per Antigravity docs (v1.20.3+, March 2026), it reads AGENTS.md
30
+ # from the repo root — the same cross-tool standard Codex pioneered
31
+ # and Cursor / Claude Code already adopt. The Codex entry above
32
+ # already writes our Tina4 skill block there, so Antigravity picks
33
+ # up the same content with zero extra installer work. If you also
34
+ # want Antigravity-specific tuning, write to .agents/rules/tina4.md
35
+ # by hand — see https://antigravity.google/docs/rules-workflows.
29
36
  ]
30
37
 
31
38
 
@@ -161,6 +168,136 @@ def install_context(root: str = ".", tools: "list[str] | None" = None) -> list[s
161
168
  return install_selected(root, selection)
162
169
 
163
170
 
171
+ # ── v3.13.9: non-destructive context-file writer ───────────────────────────
172
+ #
173
+ # Pre-v3.13.9 the installer wrote a full developer guide to CLAUDE.md
174
+ # (and the other context files) on every run, clobbering whatever the
175
+ # user had put there. Now it writes only a marker-bracketed Tina4 skill
176
+ # block — pointing the assistant at .claude/skills/tina4-*/SKILL.md —
177
+ # and leaves the rest of the file alone.
178
+
179
+
180
+ def _markers_for(context_file: str) -> "tuple[str, str]":
181
+ """Return ``(start_marker, end_marker)`` for a context file.
182
+
183
+ Markdown files get HTML comment markers (invisible when rendered).
184
+ Rule files (.cursorules, .windsurfrules, .clinerules) use ``#``
185
+ line comments which most rule loaders treat as comments.
186
+ """
187
+ if context_file.lower().endswith(".md"):
188
+ return ("<!-- tina4-skills:start -->", "<!-- tina4-skills:end -->")
189
+ return ("# tina4-skills:start", "# tina4-skills:end")
190
+
191
+
192
+ def _skill_block(context_file: str) -> str:
193
+ """Return the marker-bracketed Tina4 skill registration block."""
194
+ start, end = _markers_for(context_file)
195
+ if context_file.lower().endswith(".md"):
196
+ body = (
197
+ "## Tina4 Skills\n\n"
198
+ "When working on this Tina4 project, these skills give the "
199
+ "assistant project-aware behaviour:\n\n"
200
+ "- **tina4-developer** — Read `.claude/skills/tina4-developer/SKILL.md` before building features.\n"
201
+ "- **tina4-js** — Read `.claude/skills/tina4-js/SKILL.md` for frontend work.\n"
202
+ "- **tina4-maintainer** — Read `.claude/skills/tina4-maintainer/SKILL.md` for framework-level changes.\n\n"
203
+ "See https://tina4.com for full docs."
204
+ )
205
+ else:
206
+ body = (
207
+ "Tina4 Skills — read these files before working on this project:\n"
208
+ " .claude/skills/tina4-developer/SKILL.md (feature development)\n"
209
+ " .claude/skills/tina4-js/SKILL.md (frontend / tina4-js)\n"
210
+ " .claude/skills/tina4-maintainer/SKILL.md (framework-level changes)\n"
211
+ "Docs: https://tina4.com"
212
+ )
213
+ return f"{start}\n{body}\n{end}"
214
+
215
+
216
+ def _has_markers(existing: str, start: str, end: str) -> bool:
217
+ """True iff both start and end markers appear in order."""
218
+ s_idx = existing.find(start)
219
+ if s_idx == -1:
220
+ return False
221
+ return existing.find(end, s_idx + len(start)) != -1
222
+
223
+
224
+ def _replace_marker_block(existing: str, block: str, start: str, end: str) -> str:
225
+ """Replace the bracketed block in ``existing`` with ``block``."""
226
+ s_idx = existing.find(start)
227
+ if s_idx == -1:
228
+ return existing.rstrip() + "\n\n" + block + "\n"
229
+ e_idx = existing.find(end, s_idx + len(start))
230
+ if e_idx == -1:
231
+ return existing.rstrip() + "\n\n" + block + "\n"
232
+ before = existing[:s_idx].rstrip()
233
+ after = existing[e_idx + len(end):].lstrip("\n")
234
+ glue_before = "\n\n" if before else ""
235
+ glue_after = "\n" + after if after else "\n"
236
+ return f"{before}{glue_before}{block}{glue_after}"
237
+
238
+
239
+ # Headers the pre-v3.13.9 installer wrote at the top of CLAUDE.md.
240
+ # Detecting these lets us migrate cleanly off the old clobber install.
241
+ _OLD_FRAMEWORK_HEADERS = (
242
+ "# Tina4 Python",
243
+ "# Tina4 PHP",
244
+ "# Tina4 Ruby",
245
+ "# CLAUDE.md — AI Developer Guide for tina4-nodejs",
246
+ "# CLAUDE.md - AI Developer Guide for tina4-nodejs",
247
+ )
248
+
249
+
250
+ def _looks_like_old_framework_install(existing: str) -> bool:
251
+ """True if the file starts with a header the pre-v3.13.9 installer
252
+ wrote. Used to migrate one-time off the old clobber-style install."""
253
+ head = existing.lstrip()[:400]
254
+ return any(head.startswith(m) for m in _OLD_FRAMEWORK_HEADERS)
255
+
256
+
257
+ def _write_or_merge(context_path: "Path", context_file: str, framework_guide: str) -> str:
258
+ """Write the context file non-destructively. Returns a human-readable
259
+ action verb for the caller's log line.
260
+
261
+ Four branches:
262
+ 1. Doesn't exist → write framework guide + skill block
263
+ 2. Has markers → refresh just the skill block (idempotent)
264
+ 3. Old header → migrate: replace old dump with new guide + block
265
+ 4. User content → append the skill block, preserve everything else
266
+ """
267
+ block = _skill_block(context_file)
268
+ start, end = _markers_for(context_file)
269
+
270
+ if not context_path.exists():
271
+ context_path.write_text(
272
+ framework_guide.rstrip() + "\n\n" + block + "\n",
273
+ encoding="utf-8",
274
+ )
275
+ return "Installed"
276
+
277
+ existing = context_path.read_text(encoding="utf-8")
278
+ if _has_markers(existing, start, end):
279
+ new_content = _replace_marker_block(existing, block, start, end)
280
+ context_path.write_text(new_content, encoding="utf-8")
281
+ return "Refreshed skill block in"
282
+
283
+ if _looks_like_old_framework_install(existing):
284
+ head = existing.lstrip()
285
+ preamble = existing[: len(existing) - len(head)]
286
+ new_content = (
287
+ (preamble.rstrip() + "\n\n" if preamble.strip() else "")
288
+ + framework_guide.rstrip()
289
+ + "\n\n"
290
+ + block
291
+ + "\n"
292
+ )
293
+ context_path.write_text(new_content, encoding="utf-8")
294
+ return "Migrated (replaced old framework dump in)"
295
+
296
+ new_content = existing.rstrip() + "\n\n" + block + "\n"
297
+ context_path.write_text(new_content, encoding="utf-8")
298
+ return "Appended skill block to"
299
+
300
+
164
301
  def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
165
302
  """Install context file for a single tool."""
166
303
  created = []
@@ -171,9 +308,8 @@ def _install_for_tool(root: Path, tool: dict, context: str) -> list[str]:
171
308
  (root / tool["config_dir"]).mkdir(parents=True, exist_ok=True)
172
309
  context_path.parent.mkdir(parents=True, exist_ok=True)
173
310
 
174
- # Always overwriteuser chose to install
175
- context_path.write_text(context, encoding="utf-8")
176
- action = "Updated" if context_path.exists() else "Installed"
311
+ # v3.13.9: non-destructive write see _write_or_merge below.
312
+ action = _write_or_merge(context_path, tool["context_file"], context)
177
313
  rel = str(context_path.relative_to(root))
178
314
  created.append(rel)
179
315
  try:
File without changes