specfact-cli 0.46.9__tar.gz → 0.46.17__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 (278) hide show
  1. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/PKG-INFO +1 -1
  2. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/pyproject.toml +1 -1
  3. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/__init__.py +1 -1
  4. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/__init__.py +1 -1
  5. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/cli.py +104 -26
  6. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/upgrade/module-package.yaml +3 -3
  7. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/upgrade/src/commands.py +152 -87
  8. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_discovery.py +29 -18
  9. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/.gitignore +0 -0
  10. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/LICENSE +0 -0
  11. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/README.md +0 -0
  12. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/bundled-module-registry/index.json +0 -0
  13. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/keys/README.md +0 -0
  14. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/keys/module-signing-public.pem +0 -0
  15. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/mappings/node-async.yaml +0 -0
  16. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/mappings/python-async.yaml +0 -0
  17. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/mappings/speckit-default.yaml +0 -0
  18. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/schemas/deviation.schema.json +0 -0
  19. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/schemas/plan.schema.json +0 -0
  20. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/schemas/protocol.schema.json +0 -0
  21. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/github-action.yml.j2 +0 -0
  22. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/persona/architect.md.j2 +0 -0
  23. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/persona/developer.md.j2 +0 -0
  24. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/persona/product-owner.md.j2 +0 -0
  25. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/plan.bundle.yaml.j2 +0 -0
  26. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/policies/kanban.yaml +0 -0
  27. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/policies/mixed.yaml +0 -0
  28. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/policies/safe.yaml +0 -0
  29. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/policies/scrum.yaml +0 -0
  30. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/pr-template.md.j2 +0 -0
  31. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/protocol.yaml.j2 +0 -0
  32. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/resources/templates/telemetry.yaml.example +0 -0
  33. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/__main__.py +0 -0
  34. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/__init__.py +0 -0
  35. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/ado.py +0 -0
  36. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/backlog_base.py +0 -0
  37. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/base.py +0 -0
  38. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/github.py +0 -0
  39. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/openspec.py +0 -0
  40. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/openspec_parser.py +0 -0
  41. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/registry.py +0 -0
  42. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/adapters/speckit.py +0 -0
  43. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/agents/__init__.py +0 -0
  44. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/agents/analyze_agent.py +0 -0
  45. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/agents/base.py +0 -0
  46. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/agents/plan_agent.py +0 -0
  47. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/agents/registry.py +0 -0
  48. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/agents/sync_agent.py +0 -0
  49. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/__init__.py +0 -0
  50. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
  51. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
  52. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
  53. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
  54. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
  55. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
  56. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
  57. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
  58. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
  59. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/__init__.py +0 -0
  60. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
  61. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/adapters/base.py +0 -0
  62. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/converter.py +0 -0
  63. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/filters.py +0 -0
  64. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/mappers/__init__.py +0 -0
  65. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/mappers/ado_mapper.py +0 -0
  66. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/mappers/base.py +0 -0
  67. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/mappers/github_mapper.py +0 -0
  68. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/backlog/mappers/template_config.py +0 -0
  69. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/__init__.py +0 -0
  70. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/_bundle_shim.py +0 -0
  71. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/analyze.py +0 -0
  72. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/contract_cmd.py +0 -0
  73. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/drift.py +0 -0
  74. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/enforce.py +0 -0
  75. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/generate.py +0 -0
  76. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/import_cmd.py +0 -0
  77. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/init.py +0 -0
  78. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/migrate.py +0 -0
  79. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/plan.py +0 -0
  80. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/project_cmd.py +0 -0
  81. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/repro.py +0 -0
  82. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/sdd.py +0 -0
  83. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/spec.py +0 -0
  84. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/sync.py +0 -0
  85. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/update.py +0 -0
  86. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/commands/validate.py +0 -0
  87. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/common/__init__.py +0 -0
  88. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/common/bundle_factory.py +0 -0
  89. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/common/logger_setup.py +0 -0
  90. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/common/logging_utils.py +0 -0
  91. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/common/text_utils.py +0 -0
  92. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/common/utils.py +0 -0
  93. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/comparators/__init__.py +0 -0
  94. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/comparators/plan_comparator.py +0 -0
  95. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/contracts/__init__.py +0 -0
  96. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/contracts/crosshair_props.py +0 -0
  97. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/contracts/module_interface.py +0 -0
  98. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
  99. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
  100. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/__init__.py +0 -0
  101. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/contract_generator.py +0 -0
  102. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/openapi_extractor.py +0 -0
  103. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/persona_exporter.py +0 -0
  104. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/plan_generator.py +0 -0
  105. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/protocol_generator.py +0 -0
  106. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/report_generator.py +0 -0
  107. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/task_generator.py +0 -0
  108. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/test_to_openapi.py +0 -0
  109. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/generators/workflow_generator.py +0 -0
  110. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/groups/__init__.py +0 -0
  111. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/groups/codebase_group.py +0 -0
  112. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/groups/govern_group.py +0 -0
  113. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/groups/member_group.py +0 -0
  114. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/groups/project_group.py +0 -0
  115. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/groups/spec_group.py +0 -0
  116. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/importers/__init__.py +0 -0
  117. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/importers/speckit_converter.py +0 -0
  118. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/importers/speckit_scanner.py +0 -0
  119. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/integrations/__init__.py +0 -0
  120. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/integrations/specmatic.py +0 -0
  121. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/merge/__init__.py +0 -0
  122. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/merge/resolver.py +0 -0
  123. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/migrations/__init__.py +0 -0
  124. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/migrations/plan_migrator.py +0 -0
  125. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/__init__.py +0 -0
  126. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/backlog_item.py +0 -0
  127. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/bridge.py +0 -0
  128. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/capabilities.py +0 -0
  129. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/change.py +0 -0
  130. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/contract.py +0 -0
  131. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/deviation.py +0 -0
  132. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/dor_config.py +0 -0
  133. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/enforcement.py +0 -0
  134. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/module_package.py +0 -0
  135. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/persona_template.py +0 -0
  136. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/plan.py +0 -0
  137. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/project.py +0 -0
  138. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/protocol.py +0 -0
  139. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/quality.py +0 -0
  140. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/sdd.py +0 -0
  141. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/source_tracking.py +0 -0
  142. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/task.py +0 -0
  143. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/models/validation.py +0 -0
  144. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modes/__init__.py +0 -0
  145. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modes/detector.py +0 -0
  146. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modes/router.py +0 -0
  147. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/__init__.py +0 -0
  148. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/_bundle_import.py +0 -0
  149. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/init/module-package.yaml +0 -0
  150. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/init/src/__init__.py +0 -0
  151. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/init/src/app.py +0 -0
  152. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/init/src/commands.py +0 -0
  153. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/init/src/first_run_selection.py +0 -0
  154. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/module_io_shim.py +0 -0
  155. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/module_registry/module-package.yaml +0 -0
  156. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/module_registry/src/__init__.py +0 -0
  157. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/module_registry/src/app.py +0 -0
  158. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/module_registry/src/commands.py +0 -0
  159. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/upgrade/src/__init__.py +0 -0
  160. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/modules/upgrade/src/app.py +0 -0
  161. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/parsers/__init__.py +0 -0
  162. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/parsers/persona_importer.py +0 -0
  163. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/__init__.py +0 -0
  164. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/alias_manager.py +0 -0
  165. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/bootstrap.py +0 -0
  166. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/bridge_registry.py +0 -0
  167. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/crypto_validator.py +0 -0
  168. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/custom_registries.py +0 -0
  169. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/dependency_resolver.py +0 -0
  170. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/extension_registry.py +0 -0
  171. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/help_cache.py +0 -0
  172. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/marketplace_client.py +0 -0
  173. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/metadata.py +0 -0
  174. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_availability.py +0 -0
  175. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_grouping.py +0 -0
  176. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_installer.py +0 -0
  177. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_lifecycle.py +0 -0
  178. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_packages.py +0 -0
  179. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_security.py +0 -0
  180. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/module_state.py +0 -0
  181. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/registry/registry.py +0 -0
  182. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/resources/semgrep/async.yml +0 -0
  183. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
  184. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
  185. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/runtime.py +0 -0
  186. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/__init__.py +0 -0
  187. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_probe.py +0 -0
  188. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_sync.py +0 -0
  189. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_sync_openspec_md_parse.py +0 -0
  190. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_sync_requirement_from_proposal.py +0 -0
  191. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_sync_requirement_helpers.py +0 -0
  192. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_sync_tasks_from_proposal.py +0 -0
  193. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_sync_what_changes_format.py +0 -0
  194. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_sync_write_openspec_from_proposal.py +0 -0
  195. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/bridge_watch.py +0 -0
  196. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/change_detector.py +0 -0
  197. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/code_to_spec.py +0 -0
  198. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/drift_detector.py +0 -0
  199. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/repository_sync.py +0 -0
  200. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/spec_to_code.py +0 -0
  201. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/spec_to_tests.py +0 -0
  202. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/watcher.py +0 -0
  203. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
  204. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/telemetry.py +0 -0
  205. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/__init__.py +0 -0
  206. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
  207. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
  208. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
  209. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
  210. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
  211. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
  212. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
  213. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/registry.py +0 -0
  214. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/templates/specification_templates.py +0 -0
  215. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/__init__.py +0 -0
  216. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
  217. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/auth_tokens.py +0 -0
  218. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/bundle_converters.py +0 -0
  219. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/bundle_loader.py +0 -0
  220. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/code_change_detector.py +0 -0
  221. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/console.py +0 -0
  222. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/content_sanitizer.py +0 -0
  223. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/context_detection.py +0 -0
  224. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/contract_predicates.py +0 -0
  225. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/enrichment_context.py +0 -0
  226. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/enrichment_parser.py +0 -0
  227. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/env_manager.py +0 -0
  228. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/feature_keys.py +0 -0
  229. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/git.py +0 -0
  230. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/github_annotations.py +0 -0
  231. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/icontract_helpers.py +0 -0
  232. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/ide_setup.py +0 -0
  233. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/incremental_check.py +0 -0
  234. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/metadata.py +0 -0
  235. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/optional_deps.py +0 -0
  236. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/performance.py +0 -0
  237. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/persona_ownership.py +0 -0
  238. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/progress.py +0 -0
  239. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
  240. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/project_artifact_write.py +0 -0
  241. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/prompts.py +0 -0
  242. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/sdd_discovery.py +0 -0
  243. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/source_scanner.py +0 -0
  244. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/startup_checks.py +0 -0
  245. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/structure.py +0 -0
  246. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/structured_io.py +0 -0
  247. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/suggestions.py +0 -0
  248. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/terminal.py +0 -0
  249. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/utils/yaml_utils.py +0 -0
  250. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validation/__init__.py +0 -0
  251. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validation/command_audit.py +0 -0
  252. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/__init__.py +0 -0
  253. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/agile_validation.py +0 -0
  254. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
  255. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/cli_first_validator.py +0 -0
  256. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/contract_validator.py +0 -0
  257. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/fsm.py +0 -0
  258. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/repro_checker.py +0 -0
  259. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/schema.py +0 -0
  260. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
  261. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
  262. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
  263. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
  264. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
  265. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
  266. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
  267. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
  268. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
  269. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
  270. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
  271. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
  272. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
  273. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/models.py +0 -0
  274. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
  275. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
  276. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
  277. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/versioning/__init__.py +0 -0
  278. {specfact_cli-0.46.9 → specfact_cli-0.46.17}/src/specfact_cli/versioning/analyzer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: specfact-cli
3
- Version: 0.46.9
3
+ Version: 0.46.17
4
4
  Summary: The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases.
5
5
  Project-URL: Homepage, https://github.com/nold-ai/specfact-cli
6
6
  Project-URL: Repository, https://github.com/nold-ai/specfact-cli.git
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "specfact-cli"
7
- version = "0.46.9"
7
+ version = "0.46.17"
8
8
  description = "The swiss knife CLI for agile DevOps teams. Keep backlog, specs, tests, and code in sync with validation and contract enforcement for new projects and long-lived codebases."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -3,4 +3,4 @@ SpecFact CLI - Spec→Contract→Sentinel tool for contract-driven development.
3
3
  """
4
4
 
5
5
  # Package version: keep in sync with pyproject.toml, setup.py, src/specfact_cli/__init__.py
6
- __version__ = "0.46.9"
6
+ __version__ = "0.46.17"
@@ -45,6 +45,6 @@ def _bootstrap_bundle_paths() -> None:
45
45
 
46
46
  _bootstrap_bundle_paths()
47
47
 
48
- __version__ = "0.46.9"
48
+ __version__ = "0.46.17"
49
49
 
50
50
  __all__ = ["__version__"]
@@ -14,7 +14,7 @@ from collections.abc import Callable, Mapping
14
14
  from dataclasses import dataclass
15
15
  from datetime import datetime
16
16
  from pathlib import Path
17
- from typing import Annotated, Any, cast
17
+ from typing import Annotated, Any, NoReturn, cast
18
18
 
19
19
 
20
20
  _DetectShellFn = Callable[..., tuple[str | None, str | None]]
@@ -532,6 +532,84 @@ def _lazy_delegate_cmd_name_ready(self: _LazyDelegateGroup) -> bool:
532
532
  return len(self._lazy_cmd_name) > 0
533
533
 
534
534
 
535
+ def _args_request_help(args: tuple[str, ...] | list[str]) -> bool:
536
+ """Return True when delegated args are asking only for command help."""
537
+ return any(arg in ("--help", "-h", "--help-advanced", "-ha") for arg in args)
538
+
539
+
540
+ def _delegated_help_path(cmd_name: str, args: tuple[str, ...] | list[str]) -> str:
541
+ """Build a stable command path for fallback help output."""
542
+ path_parts = [cmd_name]
543
+ for arg in args:
544
+ if arg.startswith("-"):
545
+ continue
546
+ path_parts.append(arg)
547
+ return " ".join(path_parts)
548
+
549
+
550
+ def _print_lazy_help_fallback(cmd_name: str, args: tuple[str, ...] | list[str]) -> None:
551
+ """Print minimal help when Typer cannot materialize a command for a loaded bundle."""
552
+ command_path = _delegated_help_path(cmd_name, args)
553
+ get_configured_console().print(
554
+ f"[bold]{command_path}[/bold]\n\n"
555
+ "Help is available for this installed command path, but the command metadata could not be "
556
+ "materialized in this runtime. Reinstall the providing module or run the command without "
557
+ "`--help` to execute it."
558
+ )
559
+
560
+
561
+ def _raise_lazy_delegate_click_exception(exc: Exception) -> NoReturn:
562
+ raise click.ClickException(str(exc)) from exc
563
+
564
+
565
+ def _load_lazy_delegate_typer(cmd_name: str) -> typer.Typer:
566
+ resolved_name = resolve_command(cmd_name)
567
+ try:
568
+ return CommandRegistry.get_typer(resolved_name)
569
+ except ValueError as exc:
570
+ if cmd_name in KNOWN_BUNDLE_GROUP_OR_SHIM_NAMES:
571
+ _print_missing_bundle_command_help(cmd_name)
572
+ raise SystemExit(1) from None
573
+ _raise_lazy_delegate_click_exception(exc)
574
+ raise AssertionError("unreachable") from None
575
+
576
+
577
+ def _build_lazy_delegate_click_command(cmd_name: str, args: tuple[str, ...], real_typer: typer.Typer) -> click.Command:
578
+ from typer.main import get_command
579
+
580
+ try:
581
+ return get_command(real_typer)
582
+ except (RuntimeError, ValueError) as exc:
583
+ if _args_request_help(args):
584
+ _print_lazy_help_fallback(cmd_name, args)
585
+ raise SystemExit(0) from None
586
+ _raise_lazy_delegate_click_exception(exc)
587
+ raise AssertionError("unreachable") from None
588
+
589
+
590
+ def _lazy_delegate_prog_name(ctx: click.Context, cmd_name: str) -> str:
591
+ parts: list[str] = []
592
+ parent = ctx.parent
593
+ while parent and getattr(parent, "command", None):
594
+ name = getattr(parent.command, "name", None)
595
+ if name and name != "__delegate__":
596
+ parts.append(name)
597
+ parent = getattr(parent, "parent", None)
598
+ if parts:
599
+ return " ".join(reversed(parts))
600
+ original_prog_name = ctx.meta.get("original_prog_name")
601
+ if isinstance(original_prog_name, str) and original_prog_name:
602
+ return original_prog_name
603
+ return cmd_name
604
+
605
+
606
+ def _strip_redundant_single_command_arg(click_cmd: click.Command, args: tuple[str, ...]) -> list[str]:
607
+ args_list = list(args)
608
+ if not isinstance(click_cmd, click.Group) and args_list and args_list[0] == getattr(click_cmd, "name", None):
609
+ return args_list[1:]
610
+ return args_list
611
+
612
+
535
613
  class _LazyDelegateGroup(click.Group):
536
614
  """Click Group that delegates all args to the real command (lazy-loaded)."""
537
615
 
@@ -544,6 +622,8 @@ class _LazyDelegateGroup(click.Group):
544
622
  name=name or cmd_name,
545
623
  help=help or help_str,
546
624
  context_settings={"ignore_unknown_options": True},
625
+ invoke_without_command=True,
626
+ no_args_is_help=False,
547
627
  )
548
628
  self._lazy_cmd_name = cmd_name
549
629
  self._lazy_help_str = help_str
@@ -553,31 +633,15 @@ class _LazyDelegateGroup(click.Group):
553
633
  cmd_name = self._lazy_cmd_name
554
634
 
555
635
  def _invoke(args: tuple[str, ...]) -> None:
556
- from typer.main import get_command
557
-
558
636
  ctx = click.get_current_context()
559
- resolved_name = resolve_command(cmd_name)
560
- real_typer = CommandRegistry.get_typer(resolved_name)
561
- click_cmd = get_command(real_typer)
637
+ real_typer = _load_lazy_delegate_typer(cmd_name)
638
+ click_cmd = _build_lazy_delegate_click_command(cmd_name, args, real_typer)
562
639
  # Build full prog name from root (e.g. "specfact sync") so usage shows "specfact sync bridge", not "sync sync bridge"
563
- parts: list[str] = []
564
- p = ctx.parent
565
- while p and getattr(p, "command", None):
566
- name = getattr(p.command, "name", None)
567
- if name and name != "__delegate__":
568
- parts.append(name)
569
- p = getattr(p, "parent", None)
570
- prog_name = " ".join(reversed(parts)) if parts else cmd_name
571
- args_list = list(args)
640
+ prog_name = _lazy_delegate_prog_name(ctx, cmd_name)
572
641
  # When the real app is a single command (e.g. drift has only "detect"), Typer
573
642
  # builds a TyperCommand, not a Group. Then args are ["detect", "bundle", "--repo", ...]
574
643
  # and the command expects ["bundle", "--repo", ...] (no leading "detect").
575
- if (
576
- not isinstance(click_cmd, click.Group)
577
- and args_list
578
- and args_list[0] == getattr(click_cmd, "name", None)
579
- ):
580
- args_list = args_list[1:]
644
+ args_list = _strip_redundant_single_command_arg(click_cmd, args)
581
645
  exit_code = click_cmd.main(args=args_list, prog_name=prog_name, standalone_mode=False)
582
646
  if exit_code and exit_code != 0:
583
647
  raise SystemExit(exit_code)
@@ -590,6 +654,14 @@ class _LazyDelegateGroup(click.Group):
590
654
  add_help_option=False, # Pass --help through to real Typer so "specfact backlog daily ado --help" shows correct usage
591
655
  )
592
656
 
657
+ @require(lambda ctx: ctx is not None, "ctx must not be None")
658
+ @ensure(lambda result: result is None or isinstance(result, int), "result must be None or an exit code")
659
+ def invoke(self, ctx: click.Context) -> Any:
660
+ if ctx.invoked_subcommand is None and not ctx.args:
661
+ ctx.meta["original_prog_name"] = ctx.command_path
662
+ return self._delegate_cmd.main(args=[], prog_name=ctx.command_path, standalone_mode=False)
663
+ return super().invoke(ctx)
664
+
593
665
  @require(_lazy_delegate_cmd_name_ready, "lazy command name must be set")
594
666
  @ensure(lambda result: isinstance(result, tuple) and len(result) == 3, "result must be a 3-tuple")
595
667
  def resolve_command(
@@ -597,7 +669,7 @@ class _LazyDelegateGroup(click.Group):
597
669
  ) -> tuple[str | None, click.Command | None, list[str]]:
598
670
  # Pass through all args to the delegate so "plan init bundle" becomes args for the real plan Typer.
599
671
  if not args:
600
- return None, None, []
672
+ return self._delegate_cmd.name, self._delegate_cmd, []
601
673
  return self._delegate_cmd.name, self._delegate_cmd, list(args)
602
674
 
603
675
  @ensure(lambda result: isinstance(result, list), "result must be a list of command names")
@@ -621,8 +693,11 @@ class _LazyDelegateGroup(click.Group):
621
693
  from typer.main import get_command
622
694
 
623
695
  resolved_name = resolve_command(self._lazy_cmd_name)
624
- real_typer = CommandRegistry.get_typer(resolved_name)
625
- click_cmd = get_command(real_typer)
696
+ try:
697
+ real_typer = CommandRegistry.get_typer(resolved_name)
698
+ click_cmd = get_command(real_typer)
699
+ except (RuntimeError, ValueError):
700
+ return None
626
701
  if isinstance(click_cmd, click.Group):
627
702
  return click_cmd
628
703
  return None
@@ -633,8 +708,11 @@ class _LazyDelegateGroup(click.Group):
633
708
  from typer.main import get_command
634
709
 
635
710
  resolved_name = resolve_command(self._lazy_cmd_name)
636
- real_typer = CommandRegistry.get_typer(resolved_name)
637
- click_cmd = get_command(real_typer)
711
+ try:
712
+ real_typer = CommandRegistry.get_typer(resolved_name)
713
+ click_cmd = get_command(real_typer)
714
+ except (RuntimeError, ValueError):
715
+ return
638
716
  prog_name = (
639
717
  f"{ctx.parent.command.name} {self._lazy_cmd_name}"
640
718
  if ctx.parent and ctx.parent.command
@@ -1,5 +1,5 @@
1
1
  name: upgrade
2
- version: 0.1.4
2
+ version: 0.1.15
3
3
  commands:
4
4
  - upgrade
5
5
  category: core
@@ -17,5 +17,5 @@ publisher:
17
17
  description: Check and apply SpecFact CLI version upgrades.
18
18
  license: Apache-2.0
19
19
  integrity:
20
- checksum: sha256:0648be45eb877287ebef717d38c71d48c1e17191dfb24b0c8dde57015f7ba144
21
- signature: ZMw8ljS+0f4TYg2WVAqQCpgaae1d8z7wT/1r2yxuM6ZeZjMejhgeBuOyXopda5LOXjioxTxOlWZmGN94cCC3Ag==
20
+ checksum: sha256:134da8c48f8be96e9a4337a7cc2076bbf8b19e0d3f69161636bcec050f97e4c1
21
+ signature: CK8Gq+/d0zWlU1Lo1I6EcgPiT3FzVsXj+CXFn7+zUnpXRdpYZxccLLZqh1Hvl2IPv3Y4yecVlOEcWZKtn0tJBg==
@@ -9,6 +9,8 @@ CrossHair: skip (subprocess-based installation checks are intentionally side-eff
9
9
 
10
10
  from __future__ import annotations
11
11
 
12
+ import os
13
+ import shlex
12
14
  import subprocess
13
15
  import sys
14
16
  from datetime import UTC
@@ -45,7 +47,7 @@ validate_bundle = module_io_shim.validate_bundle
45
47
  class InstallationMethod(NamedTuple):
46
48
  """Installation method information."""
47
49
 
48
- method: str # "pip", "uvx", "pipx", or "unknown"
50
+ method: str # "pip", "uv", "uvx", "pipx", or "unknown"
49
51
  command: str # Command to run for update
50
52
  location: str | None # Installation location if known
51
53
 
@@ -53,39 +55,102 @@ class InstallationMethod(NamedTuple):
53
55
  @beartype
54
56
  @ensure(lambda result: isinstance(result, InstallationMethod), "Must return InstallationMethod")
55
57
  def detect_installation_method() -> InstallationMethod:
56
- """
57
- Detect how SpecFact CLI was installed.
58
+ """Detect how SpecFact CLI was installed."""
59
+ executable_path = str(Path(sys.executable))
60
+
61
+ uvx_method = _detect_uvx_installation(executable_path)
62
+ if uvx_method:
63
+ return uvx_method
64
+
65
+ uv_method = _detect_uv_project_installation(executable_path)
66
+ if uv_method:
67
+ return uv_method
68
+
69
+ pipx_method = _detect_pipx_installation()
70
+ if pipx_method:
71
+ return pipx_method
72
+
73
+ uv_method = _detect_uv_tool_installation()
74
+ if uv_method:
75
+ return uv_method
76
+
77
+ pip_method = _detect_pip_installation()
78
+ if pip_method:
79
+ return pip_method
80
+
81
+ quoted_executable = shlex.quote(sys.executable)
82
+ return InstallationMethod(
83
+ method="pip",
84
+ command=f"{quoted_executable} -m pip install --upgrade specfact-cli",
85
+ location=None,
86
+ )
87
+
88
+
89
+ def _detect_uvx_installation(executable_path: str) -> InstallationMethod | None:
90
+ if _path_segments_contain_uvx(sys.argv[0]) or _path_segments_contain_uvx(executable_path):
91
+ return InstallationMethod(method="uvx", command="uvx --from specfact-cli specfact --version", location=None)
92
+ return None
93
+
94
+
95
+ def _path_segments_contain_uvx(path_value: str) -> bool:
96
+ segments = [segment for segment in path_value.replace("\\", "/").split("/") if segment]
97
+ return any(_is_uvx_executable_name(segment) for segment in segments)
58
98
 
59
- Returns:
60
- InstallationMethod with detected method and update command
61
- """
62
- # Check if running via uvx
63
- if "uvx" in sys.argv[0] or "uvx" in str(Path(sys.executable)):
64
- return InstallationMethod(
65
- method="uvx",
66
- command="uvx --from specfact-cli specfact --version",
67
- location=None,
68
- )
69
99
 
70
- # Check if running via pipx
100
+ def _is_uvx_executable_name(segment: str) -> bool:
101
+ lower_segment = segment.lower()
102
+ for suffix in (".exe", ".cmd", ".bat"):
103
+ if lower_segment.endswith(suffix):
104
+ lower_segment = lower_segment[: -len(suffix)]
105
+ break
106
+ return lower_segment == "uvx"
107
+
108
+
109
+ def _detect_uv_project_installation(executable_path: str) -> InstallationMethod | None:
110
+ uv_project_env = os.environ.get("UV_PROJECT_ENVIRONMENT", "").strip()
111
+ if uv_project_env:
112
+ try:
113
+ uv_root = Path(uv_project_env).resolve()
114
+ executable = Path(executable_path).resolve()
115
+ except OSError:
116
+ return None
117
+ if executable == uv_root or uv_root in executable.parents:
118
+ executable_text = str(executable)
119
+ return InstallationMethod(
120
+ method="uv",
121
+ command=f"uv pip install --python {shlex.quote(executable_text)} --upgrade specfact-cli",
122
+ location=executable_text,
123
+ )
124
+ return None
125
+
126
+
127
+ def _detect_uv_tool_installation() -> InstallationMethod | None:
71
128
  try:
72
129
  result = subprocess.run(
73
- ["pipx", "list"],
130
+ ["uv", "tool", "list"],
74
131
  capture_output=True,
75
132
  text=True,
76
133
  timeout=5,
77
134
  check=False,
78
135
  )
79
- if "specfact-cli" in result.stdout:
80
- return InstallationMethod(
81
- method="pipx",
82
- command="pipx upgrade specfact-cli",
83
- location=None,
84
- )
85
136
  except (subprocess.TimeoutExpired, FileNotFoundError):
86
- pass
137
+ return None
138
+ if "specfact-cli" in result.stdout:
139
+ return InstallationMethod(method="uv", command="uv tool upgrade specfact-cli", location=None)
140
+ return None
141
+
87
142
 
88
- # Check if installed via pip (user or system)
143
+ def _detect_pipx_installation() -> InstallationMethod | None:
144
+ try:
145
+ result = subprocess.run(["pipx", "list"], capture_output=True, text=True, timeout=5, check=False)
146
+ except (subprocess.TimeoutExpired, FileNotFoundError):
147
+ return None
148
+ if "specfact-cli" in result.stdout:
149
+ return InstallationMethod(method="pipx", command="pipx upgrade specfact-cli", location=None)
150
+ return None
151
+
152
+
153
+ def _detect_pip_installation() -> InstallationMethod | None:
89
154
  try:
90
155
  result = subprocess.run(
91
156
  [sys.executable, "-m", "pip", "show", "specfact-cli"],
@@ -94,95 +159,88 @@ def detect_installation_method() -> InstallationMethod:
94
159
  timeout=5,
95
160
  check=False,
96
161
  )
97
- if result.returncode == 0:
98
- # Parse location from output
99
- location = None
100
- for line in result.stdout.splitlines():
101
- if line.startswith("Location:"):
102
- location = line.split(":", 1)[1].strip()
103
- break
104
-
105
- return InstallationMethod(
106
- method="pip",
107
- command=f"{sys.executable} -m pip install --upgrade specfact-cli",
108
- location=location,
109
- )
110
162
  except (subprocess.TimeoutExpired, FileNotFoundError):
111
- pass
163
+ return None
164
+
165
+ if result.returncode != 0:
166
+ return None
112
167
 
113
- # Fallback: assume pip
168
+ location = None
169
+ for line in result.stdout.splitlines():
170
+ if line.startswith("Location:"):
171
+ location = line.split(":", 1)[1].strip()
172
+ break
173
+ quoted_executable = shlex.quote(sys.executable)
114
174
  return InstallationMethod(
115
175
  method="pip",
116
- command="pip install --upgrade specfact-cli",
117
- location=None,
176
+ command=f"{quoted_executable} -m pip install --upgrade specfact-cli",
177
+ location=location,
118
178
  )
119
179
 
120
180
 
121
181
  @beartype
122
182
  @ensure(lambda result: isinstance(result, bool), "Must return bool")
123
183
  def install_update(method: InstallationMethod, yes: bool = False) -> bool:
124
- """
125
- Install update using the detected installation method.
126
-
127
- Args:
128
- method: InstallationMethod with update command
129
- yes: If True, skip confirmation prompt
130
-
131
- Returns:
132
- True if update was successful, False otherwise
133
- """
184
+ """Install update using the detected installation method."""
134
185
  if not yes:
135
186
  console.print(f"[yellow]This will update SpecFact CLI using:[/yellow] [cyan]{method.command}[/cyan]")
136
187
  if not Confirm.ask("Continue?", default=True):
137
188
  console.print("[dim]Update cancelled[/dim]")
138
189
  return False
139
190
 
140
- try:
141
- console.print("[cyan]Updating SpecFact CLI...[/cyan]")
142
- # Split command into parts for subprocess
143
- if method.method == "pipx":
144
- cmd = ["pipx", "upgrade", "specfact-cli"]
145
- elif method.method == "pip":
146
- # Handle both formats: "python -m pip" and "pip"
147
- if " -m pip" in method.command:
148
- parts = method.command.split()
149
- cmd = [parts[0], "-m", "pip", "install", "--upgrade", "specfact-cli"]
150
- else:
151
- cmd = ["pip", "install", "--upgrade", "specfact-cli"]
152
- else:
153
- # uvx - just inform user
154
- console.print(
155
- "[yellow]uvx automatically uses the latest version.[/yellow]\n"
156
- "[dim]No update needed. If you want to force a refresh, run:[/dim]\n"
157
- "[cyan]uvx --from specfact-cli@latest specfact --version[/cyan]"
158
- )
159
- return True
160
-
161
- result = subprocess.run(
162
- cmd,
163
- check=False,
164
- timeout=300, # 5 minute timeout
191
+ if method.method == "uvx":
192
+ console.print(
193
+ "[yellow]uvx automatically uses the latest version.[/yellow]\n"
194
+ "[dim]No update needed. If you want to force a refresh, run:[/dim]\n"
195
+ "[cyan]uvx --from specfact-cli@latest specfact --version[/cyan]"
165
196
  )
197
+ return True
166
198
 
167
- if result.returncode == 0:
168
- console.print("[green]✓ Update successful![/green]")
169
- # Update metadata to reflect new version
170
- from datetime import datetime
171
-
172
- update_metadata(
173
- last_checked_version=__version__,
174
- last_version_check_timestamp=datetime.now(UTC).isoformat(),
175
- )
176
- return True
177
- console.print(f"[red]✗ Update failed with exit code {result.returncode}[/red]")
199
+ command = _build_upgrade_command(method)
200
+ if command is None:
201
+ console.print(f"[red]✗ Unsupported installation method: {method.method}[/red]")
178
202
  return False
179
203
 
204
+ return _execute_upgrade_command(command)
205
+
206
+
207
+ def _build_upgrade_command(method: InstallationMethod) -> list[str] | None:
208
+ if method.method == "pipx":
209
+ return ["pipx", "upgrade", "specfact-cli"]
210
+ if method.method == "uv":
211
+ if "uv tool" in method.command:
212
+ return ["uv", "tool", "upgrade", "specfact-cli"]
213
+ python_target = method.location or sys.executable
214
+ return ["uv", "pip", "install", "--python", python_target, "--upgrade", "specfact-cli"]
215
+ if method.method == "pip":
216
+ parts = shlex.split(method.command)
217
+ if len(parts) >= 3 and parts[1:3] == ["-m", "pip"]:
218
+ return [parts[0], "-m", "pip", "install", "--upgrade", "specfact-cli"]
219
+ return ["pip", "install", "--upgrade", "specfact-cli"]
220
+ return None
221
+
222
+
223
+ def _execute_upgrade_command(command: list[str]) -> bool:
224
+ try:
225
+ console.print("[cyan]Updating SpecFact CLI...[/cyan]")
226
+ result = subprocess.run(command, check=False, timeout=300)
180
227
  except subprocess.TimeoutExpired:
181
228
  console.print("[red]✗ Update timed out (exceeded 5 minutes)[/red]")
182
229
  return False
183
- except Exception as e:
230
+ except OSError as e:
184
231
  console.print(f"[red]✗ Update failed: {e}[/red]")
185
232
  return False
233
+ if result.returncode != 0:
234
+ console.print(f"[red]✗ Update failed with exit code {result.returncode}[/red]")
235
+ return False
236
+ console.print("[green]✓ Update successful![/green]")
237
+ from datetime import datetime
238
+
239
+ try:
240
+ update_metadata(last_checked_version=__version__, last_version_check_timestamp=datetime.now(UTC).isoformat())
241
+ except (OSError, TypeError) as exc:
242
+ console.print(f"[yellow]Update succeeded, but metadata update failed: {exc}[/yellow]")
243
+ return True
186
244
 
187
245
 
188
246
  def _upgrade_log_started(check_only: bool, yes: bool) -> None:
@@ -246,6 +304,13 @@ def _upgrade_render_update_panel(version_result: Any) -> None:
246
304
  def _upgrade_install_or_check_only(version_result: Any, check_only: bool, yes: bool) -> None:
247
305
  if check_only:
248
306
  method = detect_installation_method()
307
+ if method.method == "uvx":
308
+ console.print(
309
+ "[yellow]uvx automatically uses the latest version.[/yellow]\n"
310
+ "[dim]No update needed. If you want to force a refresh, run:[/dim]\n"
311
+ "[cyan]uvx --from specfact-cli@latest specfact --version[/cyan]"
312
+ )
313
+ return
249
314
  console.print(f"\n[yellow]To upgrade, run:[/yellow] [cyan]{method.command}[/cyan]")
250
315
  console.print("[dim]Or run:[/dim] [cyan]specfact upgrade --yes[/cyan]")
251
316
  return
@@ -2,6 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ import os
5
6
  from dataclasses import dataclass
6
7
  from pathlib import Path
7
8
  from typing import Any
@@ -48,18 +49,18 @@ class _DiscoveryMergeState:
48
49
 
49
50
 
50
51
  def _resolve_include_legacy_roots(
51
- include_legacy_roots: bool | None,
52
- builtin_root: Path | None,
53
- user_root: Path | None,
54
- marketplace_root: Path | None,
55
- custom_root: Path | None,
56
- project_base_path: Path | None = None,
52
+ options: _DiscoveryRootOptions,
57
53
  ) -> bool:
58
- if include_legacy_roots is not None:
59
- return include_legacy_roots
60
- if project_base_path is not None:
54
+ if options.include_legacy_roots is not None:
55
+ return options.include_legacy_roots
56
+ if options.project_base_path is not None:
61
57
  return False
62
- return builtin_root is None and user_root is None and marketplace_root is None and custom_root is None
58
+ return (
59
+ options.builtin_root is None
60
+ and options.user_root is None
61
+ and options.marketplace_root is None
62
+ and options.custom_root is None
63
+ )
63
64
 
64
65
 
65
66
  def _append_legacy_module_roots(roots: list[tuple[str, Path]]) -> None:
@@ -74,6 +75,22 @@ def _append_legacy_module_roots(roots: list[tuple[str, Path]]) -> None:
74
75
  roots.append(("custom", extra_root))
75
76
 
76
77
 
78
+ def _append_explicit_module_roots(roots: list[tuple[str, Path]]) -> None:
79
+ seen_root_paths = {path.resolve() for _source, path in roots}
80
+ for raw_root in os.environ.get("SPECFACT_MODULES_ROOTS", "").split(os.pathsep):
81
+ candidate = raw_root.strip()
82
+ if not candidate:
83
+ continue
84
+ path = Path(candidate).expanduser()
85
+ if not path.exists():
86
+ continue
87
+ resolved = path.resolve()
88
+ if resolved in seen_root_paths:
89
+ continue
90
+ seen_root_paths.add(resolved)
91
+ roots.append(("custom", path))
92
+
93
+
77
94
  def _discovery_root_list(options: _DiscoveryRootOptions) -> list[tuple[str, Path]]:
78
95
  from specfact_cli.registry.module_packages import get_modules_root, get_workspace_modules_root
79
96
 
@@ -93,6 +110,7 @@ def _discovery_root_list(options: _DiscoveryRootOptions) -> list[tuple[str, Path
93
110
 
94
111
  if effective_project_root is not None and not project_matches_user_root:
95
112
  roots.append(("project", effective_project_root))
113
+ _append_explicit_module_roots(roots)
96
114
  roots.extend(
97
115
  [
98
116
  ("user", effective_user_root),
@@ -101,14 +119,7 @@ def _discovery_root_list(options: _DiscoveryRootOptions) -> list[tuple[str, Path
101
119
  ]
102
120
  )
103
121
 
104
- legacy = _resolve_include_legacy_roots(
105
- options.include_legacy_roots,
106
- options.builtin_root,
107
- options.user_root,
108
- options.marketplace_root,
109
- options.custom_root,
110
- options.project_base_path,
111
- )
122
+ legacy = _resolve_include_legacy_roots(options)
112
123
  if legacy:
113
124
  _append_legacy_module_roots(roots)
114
125
  return roots
File without changes
File without changes
File without changes