specfact-cli 0.26.2__tar.gz → 0.26.5__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.
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/PKG-INFO +1 -1
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/pyproject.toml +1 -1
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/__init__.py +1 -1
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/__init__.py +1 -1
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/ado.py +140 -17
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/cli.py +9 -1
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/auth.py +190 -6
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/init.py +20 -19
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/runtime.py +31 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/.gitignore +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/LICENSE.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/README.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/mappings/node-async.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/mappings/python-async.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/mappings/speckit-default.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/shared/cli-enforcement.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.01-import.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.02-plan.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.03-review.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.04-sdd.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.05-enforce.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.06-sync.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.07-contracts.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.backlog-refine.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.compare.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.sync-backlog.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/prompts/specfact.validate.md +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/schemas/deviation.schema.json +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/schemas/plan.schema.json +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/schemas/protocol.schema.json +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/defect_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/enabler_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/spike_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/defaults/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/frameworks/safe/safe_feature_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/frameworks/scrum/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/personas/developer/developer_task_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/personas/product-owner/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/backlog/providers/ado/work_item_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/github-action.yml.j2 +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/persona/architect.md.j2 +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/persona/developer.md.j2 +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/persona/product-owner.md.j2 +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/plan.bundle.yaml.j2 +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/pr-template.md.j2 +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/protocol.yaml.j2 +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/resources/templates/telemetry.yaml.example +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/backlog_base.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/base.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/github.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/openspec.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/openspec_parser.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/registry.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/adapters/speckit.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/analyze_agent.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/base.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/plan_agent.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/registry.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/agents/sync_agent.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/ambiguity_scanner.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/code_analyzer.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/constitution_evidence_extractor.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/contract_extractor.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/control_flow_analyzer.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/graph_analyzer.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/relationship_mapper.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/requirement_extractor.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/analyzers/test_pattern_extractor.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/adapters/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/adapters/base.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/adapters/local_yaml_adapter.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/ai_refiner.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/converter.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/filters.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/format_detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/base.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/markdown_format.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/formats/structured_format.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/backlog/template_detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/analyze.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/backlog_commands.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/contract_cmd.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/drift.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/enforce.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/generate.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/import_cmd.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/migrate.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/plan.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/project_cmd.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/repro.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/sdd.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/spec.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/sync.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/commands/validate.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/logger_setup.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/logging_utils.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/text_utils.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/common/utils.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/comparators/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/comparators/plan_comparator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/contracts/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/contracts/crosshair_props.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/enrichers/constitution_enricher.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/enrichers/plan_enricher.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/contract_generator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/openapi_extractor.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/persona_exporter.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/plan_generator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/protocol_generator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/report_generator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/task_generator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/test_to_openapi.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/generators/workflow_generator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/importers/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/importers/speckit_converter.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/importers/speckit_scanner.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/integrations/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/integrations/specmatic.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/merge/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/merge/resolver.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/migrations/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/migrations/plan_migrator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/backlog_item.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/bridge.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/capabilities.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/change.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/contract.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/deviation.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/dor_config.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/enforcement.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/persona_template.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/plan.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/project.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/protocol.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/quality.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/sdd.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/source_tracking.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/models/task.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/modes/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/modes/detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/modes/router.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/parsers/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/parsers/persona_importer.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/resources/semgrep/async.yml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/resources/semgrep/code-quality.yml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/resources/semgrep/feature-detection.yml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/bridge_probe.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/bridge_sync.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/bridge_watch.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/change_detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/code_to_spec.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/drift_detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/repository_sync.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/spec_to_code.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/spec_to_tests.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/watcher.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/sync/watcher_enhanced.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/telemetry.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/bridge_templates.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/defect_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/enabler_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/spike_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/defaults/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/frameworks/scrum/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/personas/product-owner/user_story_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/providers/ado/work_item_v1.yaml +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/registry.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/templates/specification_templates.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/acceptance_criteria.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/auth_tokens.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/bundle_loader.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/code_change_detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/console.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/content_sanitizer.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/context_detection.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/enrichment_context.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/enrichment_parser.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/env_manager.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/feature_keys.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/git.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/github_annotations.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/ide_setup.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/incremental_check.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/optional_deps.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/performance.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/progress.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/progressive_disclosure.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/prompts.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/sdd_discovery.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/source_scanner.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/structure.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/structured_io.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/suggestions.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/terminal.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/utils/yaml_utils.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/agile_validation.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/change_proposal_integration.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/cli_first_validator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/contract_validator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/fsm.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/repro_checker.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/schema.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/contract_populator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/crosshair_runner.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/crosshair_summary.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/dependency_installer.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/framework_detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/base.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/django.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/drf.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/fastapi.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/frameworks/flask.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/harness_generator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/models.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/orchestrator.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/specmatic_runner.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/validators/sidecar/unannotated_detector.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/src/specfact_cli/versioning/__init__.py +0 -0
- {specfact_cli-0.26.2 → specfact_cli-0.26.5}/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.26.
|
|
3
|
+
Version: 0.26.5
|
|
4
4
|
Summary: Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions.
|
|
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.26.
|
|
7
|
+
version = "0.26.5"
|
|
8
8
|
description = "Brownfield-first CLI: Reverse engineer legacy Python → specs → enforced contracts. Automate legacy code documentation and prevent modernization regressions."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -29,7 +29,8 @@ from specfact_cli.models.backlog_item import BacklogItem
|
|
|
29
29
|
from specfact_cli.models.bridge import BridgeConfig
|
|
30
30
|
from specfact_cli.models.capabilities import ToolCapabilities
|
|
31
31
|
from specfact_cli.models.change import ChangeProposal, ChangeTracking
|
|
32
|
-
from specfact_cli.
|
|
32
|
+
from specfact_cli.runtime import debug_print
|
|
33
|
+
from specfact_cli.utils.auth_tokens import get_token, set_token
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
console = Console()
|
|
@@ -75,10 +76,45 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
75
76
|
elif os.environ.get("AZURE_DEVOPS_TOKEN"):
|
|
76
77
|
self.api_token = os.environ.get("AZURE_DEVOPS_TOKEN")
|
|
77
78
|
self.auth_scheme = "basic"
|
|
78
|
-
elif stored_token := get_token("azure-devops"):
|
|
79
|
+
elif stored_token := get_token("azure-devops", allow_expired=False):
|
|
80
|
+
# Valid, non-expired token found
|
|
79
81
|
self.api_token = stored_token.get("access_token")
|
|
80
82
|
token_type = (stored_token.get("token_type") or "bearer").lower()
|
|
81
83
|
self.auth_scheme = "bearer" if token_type == "bearer" else "basic"
|
|
84
|
+
elif stored_token_expired := get_token("azure-devops", allow_expired=True):
|
|
85
|
+
# Token exists but is expired - try to refresh using persistent cache
|
|
86
|
+
expires_at = stored_token_expired.get("expires_at", "unknown")
|
|
87
|
+
token_type = (stored_token_expired.get("token_type") or "bearer").lower()
|
|
88
|
+
if token_type == "bearer":
|
|
89
|
+
# OAuth token expired - try automatic refresh using persistent cache (like Azure CLI)
|
|
90
|
+
refreshed_token = self._try_refresh_oauth_token()
|
|
91
|
+
if refreshed_token:
|
|
92
|
+
self.api_token = refreshed_token.get("access_token")
|
|
93
|
+
self.auth_scheme = "bearer"
|
|
94
|
+
# Update stored token with refreshed token
|
|
95
|
+
set_token("azure-devops", refreshed_token)
|
|
96
|
+
debug_print(f"[dim]OAuth token automatically refreshed (was expired at {expires_at})[/dim]")
|
|
97
|
+
else:
|
|
98
|
+
# Refresh failed - provide helpful guidance
|
|
99
|
+
console.print(
|
|
100
|
+
f"[yellow]⚠[/yellow] Stored OAuth token expired at {expires_at}. "
|
|
101
|
+
"Attempting automatic refresh..."
|
|
102
|
+
)
|
|
103
|
+
console.print("[yellow]⚠[/yellow] Automatic refresh failed. OAuth tokens expire after ~1 hour.")
|
|
104
|
+
console.print(
|
|
105
|
+
"[dim]Options:[/dim]\n"
|
|
106
|
+
" 1. Use a Personal Access Token (PAT) with longer expiration (up to 1 year):\n"
|
|
107
|
+
" - Create PAT: https://dev.azure.com/{org}/_usersSettings/tokens\n"
|
|
108
|
+
" - Store PAT: specfact auth azure-devops --pat your_pat_token\n"
|
|
109
|
+
" 2. Re-authenticate: specfact auth azure-devops\n"
|
|
110
|
+
" 3. Use --ado-token option with a valid token"
|
|
111
|
+
)
|
|
112
|
+
self.api_token = None
|
|
113
|
+
self.auth_scheme = None
|
|
114
|
+
else:
|
|
115
|
+
# PAT token - no expiration tracking, assume still valid
|
|
116
|
+
self.api_token = stored_token_expired.get("access_token")
|
|
117
|
+
self.auth_scheme = "basic"
|
|
82
118
|
else:
|
|
83
119
|
self.api_token = None
|
|
84
120
|
self.auth_scheme = None
|
|
@@ -117,8 +153,9 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
117
153
|
Full URL with proper format based on cloud vs on-premise
|
|
118
154
|
|
|
119
155
|
Note:
|
|
120
|
-
For
|
|
121
|
-
|
|
156
|
+
For project-based permissions in larger organizations, org must be part of the
|
|
157
|
+
_apis URL path before the project. This ensures proper permission scoping.
|
|
158
|
+
Format: {base_url}/{org}/{project}/_apis/...
|
|
122
159
|
"""
|
|
123
160
|
if not self.project:
|
|
124
161
|
raise ValueError(f"project required to build ADO URL (project={self.project!r})")
|
|
@@ -150,8 +187,16 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
150
187
|
has_collection_in_base = has_tfs or len(parts) > 1
|
|
151
188
|
|
|
152
189
|
if has_collection_in_base:
|
|
153
|
-
# Collection already in base_url,
|
|
154
|
-
|
|
190
|
+
# Collection already in base_url, but for project-based permissions, we still need org in path
|
|
191
|
+
# Include org before project to ensure proper permission scoping
|
|
192
|
+
if self.org:
|
|
193
|
+
url = f"{base_url_normalized}/{self.org}/{self.project}/{path_normalized}?api-version={api_version}"
|
|
194
|
+
else:
|
|
195
|
+
# Fallback: if org not provided but collection in base_url, use project directly
|
|
196
|
+
console.print(
|
|
197
|
+
"[yellow]Warning:[/yellow] Collection in base_url but org not provided. Using project directly."
|
|
198
|
+
)
|
|
199
|
+
url = f"{base_url_normalized}/{self.project}/{path_normalized}?api-version={api_version}"
|
|
155
200
|
elif self.org:
|
|
156
201
|
# Collection not in base_url, need to add it
|
|
157
202
|
# For on-premise, typically use /tfs/{collection} format unless explicitly newer format
|
|
@@ -1199,6 +1244,62 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
1199
1244
|
|
|
1200
1245
|
return base64.b64encode(f":{token}".encode()).decode()
|
|
1201
1246
|
|
|
1247
|
+
def _try_refresh_oauth_token(self) -> dict[str, Any] | None:
|
|
1248
|
+
"""
|
|
1249
|
+
Attempt to refresh expired OAuth token using persistent token cache.
|
|
1250
|
+
|
|
1251
|
+
This uses the same persistent cache as the auth command, allowing automatic
|
|
1252
|
+
token refresh without user interaction (like Azure CLI).
|
|
1253
|
+
|
|
1254
|
+
Returns:
|
|
1255
|
+
Refreshed token data dict if successful, None if refresh failed
|
|
1256
|
+
"""
|
|
1257
|
+
try:
|
|
1258
|
+
from azure.identity import ( # type: ignore[reportMissingImports]
|
|
1259
|
+
DeviceCodeCredential,
|
|
1260
|
+
TokenCachePersistenceOptions,
|
|
1261
|
+
)
|
|
1262
|
+
|
|
1263
|
+
# Use the same cache name as auth command for shared cache
|
|
1264
|
+
# Try encrypted first, fall back to unencrypted if libsecret unavailable
|
|
1265
|
+
cache_options = None
|
|
1266
|
+
try:
|
|
1267
|
+
try:
|
|
1268
|
+
cache_options = TokenCachePersistenceOptions(
|
|
1269
|
+
name="specfact-azure-devops",
|
|
1270
|
+
allow_unencrypted_cache=False, # Prefer encrypted
|
|
1271
|
+
)
|
|
1272
|
+
except Exception:
|
|
1273
|
+
# Encrypted cache not available, try unencrypted
|
|
1274
|
+
cache_options = TokenCachePersistenceOptions(
|
|
1275
|
+
name="specfact-azure-devops",
|
|
1276
|
+
allow_unencrypted_cache=True, # Fallback: unencrypted
|
|
1277
|
+
)
|
|
1278
|
+
except Exception:
|
|
1279
|
+
# Persistent cache completely unavailable, can't refresh
|
|
1280
|
+
return None
|
|
1281
|
+
|
|
1282
|
+
# Create credential with same cache - it will use cached refresh token
|
|
1283
|
+
credential = DeviceCodeCredential(cache_persistence_options=cache_options)
|
|
1284
|
+
# Use the same resource as auth command
|
|
1285
|
+
azure_devops_resource = "499b84ac-1321-427f-aa17-267ca6975798/.default"
|
|
1286
|
+
token = credential.get_token(azure_devops_resource)
|
|
1287
|
+
|
|
1288
|
+
# Return refreshed token data
|
|
1289
|
+
from datetime import UTC, datetime
|
|
1290
|
+
|
|
1291
|
+
expires_at = datetime.fromtimestamp(token.expires_on, tz=UTC).isoformat()
|
|
1292
|
+
return {
|
|
1293
|
+
"access_token": token.token,
|
|
1294
|
+
"token_type": "bearer",
|
|
1295
|
+
"expires_at": expires_at,
|
|
1296
|
+
"resource": azure_devops_resource,
|
|
1297
|
+
"issued_at": datetime.now(tz=UTC).isoformat(),
|
|
1298
|
+
}
|
|
1299
|
+
except Exception:
|
|
1300
|
+
# Refresh failed (no cached refresh token, refresh token expired, etc.)
|
|
1301
|
+
return None
|
|
1302
|
+
|
|
1202
1303
|
def _auth_headers(self) -> dict[str, str]:
|
|
1203
1304
|
"""Return authorization headers based on token type."""
|
|
1204
1305
|
if not self.api_token:
|
|
@@ -2306,11 +2407,26 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
2306
2407
|
Uses ADO Work Items API to query work items.
|
|
2307
2408
|
"""
|
|
2308
2409
|
if not self.api_token:
|
|
2309
|
-
msg =
|
|
2410
|
+
msg = (
|
|
2411
|
+
"Azure DevOps API token required to fetch backlog items.\n"
|
|
2412
|
+
"Options:\n"
|
|
2413
|
+
" 1. Set AZURE_DEVOPS_TOKEN environment variable\n"
|
|
2414
|
+
" 2. Use --ado-token option\n"
|
|
2415
|
+
" 3. Store token via specfact auth azure-devops"
|
|
2416
|
+
)
|
|
2310
2417
|
raise ValueError(msg)
|
|
2311
2418
|
|
|
2312
|
-
if not self.org
|
|
2313
|
-
msg =
|
|
2419
|
+
if not self.org:
|
|
2420
|
+
msg = (
|
|
2421
|
+
"org (organization) required to fetch backlog items.\n"
|
|
2422
|
+
"For Azure DevOps Services (cloud), org is always required.\n"
|
|
2423
|
+
"For Azure DevOps Server (on-premise), org is the collection name.\n"
|
|
2424
|
+
"Provide via --ado-org option or ensure it's set in adapter configuration."
|
|
2425
|
+
)
|
|
2426
|
+
raise ValueError(msg)
|
|
2427
|
+
|
|
2428
|
+
if not self.project:
|
|
2429
|
+
msg = "project required to fetch backlog items. Provide via --ado-project option."
|
|
2314
2430
|
raise ValueError(msg)
|
|
2315
2431
|
|
|
2316
2432
|
# Build WIQL (Work Item Query Language) query
|
|
@@ -2344,14 +2460,23 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
2344
2460
|
# POST to project-level endpoint: {org}/{project}/_apis/wit/wiql?api-version=7.1
|
|
2345
2461
|
url = self._build_ado_url("_apis/wit/wiql", api_version="7.1")
|
|
2346
2462
|
headers = {
|
|
2347
|
-
|
|
2463
|
+
**self._auth_headers(),
|
|
2348
2464
|
"Content-Type": "application/json",
|
|
2349
2465
|
"Accept": "application/json",
|
|
2350
2466
|
}
|
|
2351
2467
|
payload = {"query": wiql}
|
|
2352
2468
|
|
|
2353
|
-
# Debug: Log URL construction for troubleshooting
|
|
2354
|
-
|
|
2469
|
+
# Debug: Log URL construction and auth status for troubleshooting
|
|
2470
|
+
debug_print(f"[dim]ADO WIQL URL: {url}[/dim]")
|
|
2471
|
+
if "Authorization" in headers:
|
|
2472
|
+
auth_header_preview = (
|
|
2473
|
+
headers["Authorization"][:20] + "..."
|
|
2474
|
+
if len(headers["Authorization"]) > 20
|
|
2475
|
+
else headers["Authorization"]
|
|
2476
|
+
)
|
|
2477
|
+
debug_print(f"[dim]ADO Auth: {auth_header_preview}[/dim]")
|
|
2478
|
+
else:
|
|
2479
|
+
debug_print("[yellow]Warning: No Authorization header in request[/yellow]")
|
|
2355
2480
|
|
|
2356
2481
|
try:
|
|
2357
2482
|
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
|
@@ -2430,14 +2555,12 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
2430
2555
|
|
|
2431
2556
|
# Headers for work items batch GET (organization-level endpoint)
|
|
2432
2557
|
workitems_headers = {
|
|
2433
|
-
|
|
2434
|
-
if self.auth_scheme
|
|
2435
|
-
else f"Basic {self.api_token}",
|
|
2558
|
+
**self._auth_headers(),
|
|
2436
2559
|
"Accept": "application/json",
|
|
2437
2560
|
}
|
|
2438
2561
|
|
|
2439
2562
|
# Debug: Log URL construction for troubleshooting
|
|
2440
|
-
|
|
2563
|
+
debug_print(f"[dim]ADO WorkItems URL: {url}&ids={ids_str}[/dim]")
|
|
2441
2564
|
|
|
2442
2565
|
try:
|
|
2443
2566
|
response = requests.get(url, headers=workitems_headers, params=params, timeout=30)
|
|
@@ -2545,7 +2668,7 @@ class AdoAdapter(BridgeAdapter, BacklogAdapterMixin, BacklogAdapter):
|
|
|
2545
2668
|
work_item_id = int(item.id)
|
|
2546
2669
|
url = self._build_ado_url(f"_apis/wit/workitems/{work_item_id}", api_version="7.1")
|
|
2547
2670
|
headers = {
|
|
2548
|
-
|
|
2671
|
+
**self._auth_headers(),
|
|
2549
2672
|
"Content-Type": "application/json-patch+json",
|
|
2550
2673
|
}
|
|
2551
2674
|
|
|
@@ -73,7 +73,7 @@ from specfact_cli.commands import (
|
|
|
73
73
|
validate,
|
|
74
74
|
)
|
|
75
75
|
from specfact_cli.modes import OperationalMode, detect_mode
|
|
76
|
-
from specfact_cli.runtime import get_configured_console
|
|
76
|
+
from specfact_cli.runtime import get_configured_console, set_debug_mode
|
|
77
77
|
from specfact_cli.utils.progressive_disclosure import ProgressiveDisclosureGroup
|
|
78
78
|
from specfact_cli.utils.structured_io import StructuredFormat
|
|
79
79
|
|
|
@@ -236,6 +236,11 @@ def main(
|
|
|
236
236
|
callback=mode_callback,
|
|
237
237
|
help="Operational mode: cicd (fast, deterministic) or copilot (enhanced, interactive)",
|
|
238
238
|
),
|
|
239
|
+
debug: bool = typer.Option(
|
|
240
|
+
False,
|
|
241
|
+
"--debug",
|
|
242
|
+
help="Enable debug output (shows detailed logging and diagnostic information)",
|
|
243
|
+
),
|
|
239
244
|
input_format: Annotated[
|
|
240
245
|
StructuredFormat,
|
|
241
246
|
typer.Option(
|
|
@@ -278,6 +283,9 @@ def main(
|
|
|
278
283
|
# Set banner flag based on --no-banner option
|
|
279
284
|
_show_banner = not no_banner
|
|
280
285
|
|
|
286
|
+
# Set debug mode
|
|
287
|
+
set_debug_mode(debug)
|
|
288
|
+
|
|
281
289
|
runtime.configure_io_formats(input_format=input_format, output_format=output_format)
|
|
282
290
|
# Invert logic: --interactive means not non-interactive, --no-interactive means non-interactive
|
|
283
291
|
if interaction is not None:
|
|
@@ -153,10 +153,54 @@ def _poll_github_device_token(
|
|
|
153
153
|
|
|
154
154
|
|
|
155
155
|
@app.command("azure-devops")
|
|
156
|
-
def auth_azure_devops(
|
|
157
|
-
|
|
156
|
+
def auth_azure_devops(
|
|
157
|
+
pat: str | None = typer.Option(
|
|
158
|
+
None,
|
|
159
|
+
"--pat",
|
|
160
|
+
help="Store a Personal Access Token (PAT) directly. PATs can have expiration up to 1 year, "
|
|
161
|
+
"unlike OAuth tokens which expire after ~1 hour. Create PAT at: "
|
|
162
|
+
"https://dev.azure.com/{org}/_usersSettings/tokens",
|
|
163
|
+
),
|
|
164
|
+
use_device_code: bool = typer.Option(
|
|
165
|
+
False,
|
|
166
|
+
"--use-device-code",
|
|
167
|
+
help="Force device code flow instead of trying interactive browser first. "
|
|
168
|
+
"Useful for SSH/headless environments where browser cannot be opened.",
|
|
169
|
+
),
|
|
170
|
+
) -> None:
|
|
171
|
+
"""
|
|
172
|
+
Authenticate to Azure DevOps using OAuth (device code or interactive browser) or Personal Access Token (PAT).
|
|
173
|
+
|
|
174
|
+
**Token Options:**
|
|
175
|
+
|
|
176
|
+
1. **Personal Access Token (PAT)** - Recommended for long-lived authentication:
|
|
177
|
+
- Use --pat option to store a PAT directly
|
|
178
|
+
- PATs can have expiration up to 1 year (maximum allowed)
|
|
179
|
+
- Create PAT at: https://dev.azure.com/{org}/_usersSettings/tokens
|
|
180
|
+
- Select required scopes (e.g., "Work Items: Read & Write")
|
|
181
|
+
- Example: specfact auth azure-devops --pat your_pat_token
|
|
182
|
+
|
|
183
|
+
2. **OAuth Flow** (default, when no PAT provided):
|
|
184
|
+
- **First tries interactive browser** (opens browser automatically, better UX)
|
|
185
|
+
- **Falls back to device code** if browser unavailable (SSH/headless environments)
|
|
186
|
+
- Access tokens expire after ~1 hour, refresh tokens last 90 days
|
|
187
|
+
- Automatic token refresh via persistent cache (no re-authentication needed)
|
|
188
|
+
- Example: specfact auth azure-devops
|
|
189
|
+
|
|
190
|
+
3. **Force Device Code Flow** (--use-device-code):
|
|
191
|
+
- Skip interactive browser, use device code directly
|
|
192
|
+
- Useful for SSH/headless environments or when browser cannot be opened
|
|
193
|
+
- Example: specfact auth azure-devops --use-device-code
|
|
194
|
+
|
|
195
|
+
**For Long-Lived Tokens:**
|
|
196
|
+
Use a PAT with 90 days or 1 year expiration instead of OAuth tokens to avoid
|
|
197
|
+
frequent re-authentication. PATs are stored securely and work the same way as OAuth tokens.
|
|
198
|
+
"""
|
|
158
199
|
try:
|
|
159
|
-
from azure.identity import
|
|
200
|
+
from azure.identity import ( # type: ignore[reportMissingImports]
|
|
201
|
+
DeviceCodeCredential,
|
|
202
|
+
InteractiveBrowserCredential,
|
|
203
|
+
)
|
|
160
204
|
except ImportError:
|
|
161
205
|
console.print("[bold red]✗[/bold red] azure-identity is not installed.")
|
|
162
206
|
console.print("Install dependencies with: pip install specfact-cli")
|
|
@@ -171,9 +215,145 @@ def auth_azure_devops() -> None:
|
|
|
171
215
|
console.print(f"Enter the code: [bold]{user_code}[/bold]")
|
|
172
216
|
console.print(f"Code expires at: {expires_at.isoformat()}")
|
|
173
217
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
218
|
+
# If PAT is provided, store it directly (no expiration for PATs stored as Basic auth)
|
|
219
|
+
if pat:
|
|
220
|
+
console.print("[bold]Storing Personal Access Token (PAT)...[/bold]")
|
|
221
|
+
# PATs are stored as Basic auth tokens (no expiration date set by default)
|
|
222
|
+
# Users can create PATs with up to 1 year expiration in Azure DevOps UI
|
|
223
|
+
token_data = {
|
|
224
|
+
"access_token": pat,
|
|
225
|
+
"token_type": "basic", # PATs use Basic authentication
|
|
226
|
+
"issued_at": datetime.now(tz=UTC).isoformat(),
|
|
227
|
+
# Note: PAT expiration is managed by Azure DevOps, not stored locally
|
|
228
|
+
# Users should set expiration when creating PAT (up to 1 year)
|
|
229
|
+
}
|
|
230
|
+
set_token("azure-devops", token_data)
|
|
231
|
+
console.print("[bold green]✓[/bold green] Personal Access Token stored")
|
|
232
|
+
console.print(
|
|
233
|
+
"[dim]PAT stored successfully. PATs can have expiration up to 1 year when created in Azure DevOps.[/dim]"
|
|
234
|
+
)
|
|
235
|
+
console.print("[dim]Create/manage PATs at: https://dev.azure.com/{org}/_usersSettings/tokens[/dim]")
|
|
236
|
+
return
|
|
237
|
+
|
|
238
|
+
# OAuth flow with persistent token cache (automatic refresh)
|
|
239
|
+
# Try interactive browser first, fall back to device code if it fails
|
|
240
|
+
console.print("[bold]Starting Azure DevOps OAuth authentication...[/bold]")
|
|
241
|
+
|
|
242
|
+
# Enable persistent token cache for automatic token refresh (like Azure CLI)
|
|
243
|
+
# This allows tokens to be refreshed automatically without re-authentication
|
|
244
|
+
cache_options = None
|
|
245
|
+
use_unencrypted_cache = False
|
|
246
|
+
try:
|
|
247
|
+
from azure.identity import TokenCachePersistenceOptions # type: ignore[reportMissingImports]
|
|
248
|
+
|
|
249
|
+
# Try encrypted cache first (secure), fall back to unencrypted if libsecret unavailable
|
|
250
|
+
try:
|
|
251
|
+
cache_options = TokenCachePersistenceOptions(
|
|
252
|
+
name="specfact-azure-devops", # Shared cache name across processes
|
|
253
|
+
allow_unencrypted_cache=False, # Prefer encrypted storage
|
|
254
|
+
)
|
|
255
|
+
console.print(
|
|
256
|
+
"[dim]Persistent token cache enabled (encrypted) - tokens will refresh automatically (like Azure CLI)[/dim]"
|
|
257
|
+
)
|
|
258
|
+
except Exception:
|
|
259
|
+
# Encrypted cache not available (e.g., libsecret missing on Linux), try unencrypted
|
|
260
|
+
try:
|
|
261
|
+
cache_options = TokenCachePersistenceOptions(
|
|
262
|
+
name="specfact-azure-devops",
|
|
263
|
+
allow_unencrypted_cache=True, # Fallback: unencrypted storage
|
|
264
|
+
)
|
|
265
|
+
use_unencrypted_cache = True
|
|
266
|
+
console.print(
|
|
267
|
+
"[yellow]Note:[/yellow] Using unencrypted token cache (libsecret unavailable). "
|
|
268
|
+
"Tokens will refresh automatically but stored without encryption."
|
|
269
|
+
)
|
|
270
|
+
except Exception:
|
|
271
|
+
# Persistent cache completely unavailable, use in-memory only
|
|
272
|
+
console.print(
|
|
273
|
+
"[yellow]Note:[/yellow] Persistent cache not available, using in-memory cache only. "
|
|
274
|
+
"Tokens will need to be refreshed manually after ~1 hour."
|
|
275
|
+
)
|
|
276
|
+
except ImportError:
|
|
277
|
+
# TokenCachePersistenceOptions not available in this version
|
|
278
|
+
pass
|
|
279
|
+
|
|
280
|
+
# Helper function to try authentication with fallback to unencrypted cache or no cache
|
|
281
|
+
def try_authenticate_with_fallback(credential_class, credential_kwargs):
|
|
282
|
+
"""Try authentication, falling back to unencrypted cache or no cache if encrypted cache fails."""
|
|
283
|
+
nonlocal cache_options, use_unencrypted_cache
|
|
284
|
+
# First try with current cache_options
|
|
285
|
+
try:
|
|
286
|
+
credential = credential_class(cache_persistence_options=cache_options, **credential_kwargs)
|
|
287
|
+
return credential.get_token(AZURE_DEVOPS_RESOURCE)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
error_msg = str(e).lower()
|
|
290
|
+
# Check if error is about cache encryption and we haven't already tried unencrypted
|
|
291
|
+
if (
|
|
292
|
+
("cache encryption" in error_msg or "libsecret" in error_msg)
|
|
293
|
+
and cache_options
|
|
294
|
+
and not use_unencrypted_cache
|
|
295
|
+
):
|
|
296
|
+
# Try again with unencrypted cache
|
|
297
|
+
console.print("[yellow]Note:[/yellow] Encrypted cache unavailable, trying unencrypted cache...")
|
|
298
|
+
try:
|
|
299
|
+
from azure.identity import TokenCachePersistenceOptions # type: ignore[reportMissingImports]
|
|
300
|
+
|
|
301
|
+
unencrypted_cache = TokenCachePersistenceOptions(
|
|
302
|
+
name="specfact-azure-devops",
|
|
303
|
+
allow_unencrypted_cache=True,
|
|
304
|
+
)
|
|
305
|
+
credential = credential_class(cache_persistence_options=unencrypted_cache, **credential_kwargs)
|
|
306
|
+
token = credential.get_token(AZURE_DEVOPS_RESOURCE)
|
|
307
|
+
console.print(
|
|
308
|
+
"[yellow]Note:[/yellow] Using unencrypted token cache (libsecret unavailable). "
|
|
309
|
+
"Tokens will refresh automatically but stored without encryption."
|
|
310
|
+
)
|
|
311
|
+
# Update global cache_options for future use
|
|
312
|
+
cache_options = unencrypted_cache
|
|
313
|
+
use_unencrypted_cache = True
|
|
314
|
+
return token
|
|
315
|
+
except Exception as e2:
|
|
316
|
+
# Unencrypted cache also failed - check if it's the same error
|
|
317
|
+
error_msg2 = str(e2).lower()
|
|
318
|
+
if "cache encryption" in error_msg2 or "libsecret" in error_msg2:
|
|
319
|
+
# Still failing on cache, try without cache entirely
|
|
320
|
+
console.print("[yellow]Note:[/yellow] Persistent cache unavailable, trying without cache...")
|
|
321
|
+
try:
|
|
322
|
+
credential = credential_class(**credential_kwargs)
|
|
323
|
+
token = credential.get_token(AZURE_DEVOPS_RESOURCE)
|
|
324
|
+
console.print(
|
|
325
|
+
"[yellow]Note:[/yellow] Using in-memory cache only. "
|
|
326
|
+
"Tokens will need to be refreshed manually after ~1 hour."
|
|
327
|
+
)
|
|
328
|
+
return token
|
|
329
|
+
except Exception:
|
|
330
|
+
# Even without cache it failed, re-raise original
|
|
331
|
+
raise e from e2
|
|
332
|
+
# Different error, re-raise
|
|
333
|
+
raise e2 from e
|
|
334
|
+
# Not a cache encryption error, re-raise
|
|
335
|
+
raise
|
|
336
|
+
|
|
337
|
+
# Try interactive browser first (better UX), fall back to device code if it fails
|
|
338
|
+
token = None
|
|
339
|
+
if not use_device_code:
|
|
340
|
+
try:
|
|
341
|
+
console.print("[dim]Trying interactive browser authentication...[/dim]")
|
|
342
|
+
token = try_authenticate_with_fallback(InteractiveBrowserCredential, {})
|
|
343
|
+
console.print("[bold green]✓[/bold green] Interactive browser authentication successful")
|
|
344
|
+
except Exception as e:
|
|
345
|
+
# Interactive browser failed (no display, headless environment, etc.)
|
|
346
|
+
console.print(f"[yellow]⚠[/yellow] Interactive browser unavailable: {type(e).__name__}")
|
|
347
|
+
console.print("[dim]Falling back to device code flow...[/dim]")
|
|
348
|
+
|
|
349
|
+
# Use device code flow if interactive browser failed or was explicitly requested
|
|
350
|
+
if token is None:
|
|
351
|
+
console.print("[bold]Using device code authentication...[/bold]")
|
|
352
|
+
try:
|
|
353
|
+
token = try_authenticate_with_fallback(DeviceCodeCredential, {"prompt_callback": prompt_callback})
|
|
354
|
+
except Exception as e:
|
|
355
|
+
console.print(f"[bold red]✗[/bold red] Authentication failed: {e}")
|
|
356
|
+
raise typer.Exit(1) from e
|
|
177
357
|
|
|
178
358
|
expires_at = datetime.fromtimestamp(token.expires_on, tz=UTC).isoformat()
|
|
179
359
|
token_data = {
|
|
@@ -187,6 +367,10 @@ def auth_azure_devops() -> None:
|
|
|
187
367
|
|
|
188
368
|
console.print("[bold green]✓[/bold green] Azure DevOps authentication complete")
|
|
189
369
|
console.print("Stored token for provider: azure-devops")
|
|
370
|
+
console.print(
|
|
371
|
+
f"[yellow]⚠[/yellow] Token expires at: {expires_at}\n"
|
|
372
|
+
"[dim]For longer-lived tokens (up to 1 year), use --pat option with a Personal Access Token.[/dim]"
|
|
373
|
+
)
|
|
190
374
|
|
|
191
375
|
|
|
192
376
|
@app.command("github")
|
|
@@ -17,6 +17,7 @@ from icontract import ensure, require
|
|
|
17
17
|
from rich.console import Console
|
|
18
18
|
from rich.panel import Panel
|
|
19
19
|
|
|
20
|
+
from specfact_cli.runtime import debug_print
|
|
20
21
|
from specfact_cli.telemetry import telemetry
|
|
21
22
|
from specfact_cli.utils.env_manager import EnvManager, build_tool_command, detect_env_manager
|
|
22
23
|
from specfact_cli.utils.ide_setup import (
|
|
@@ -258,12 +259,12 @@ def init(
|
|
|
258
259
|
# Try 1: Development mode - relative to repo root
|
|
259
260
|
dev_templates_dir = (repo_path / "resources" / "prompts").resolve()
|
|
260
261
|
tried_locations.append(dev_templates_dir)
|
|
261
|
-
|
|
262
|
+
debug_print(f"[dim]Debug:[/dim] Trying development path: {dev_templates_dir}")
|
|
262
263
|
if dev_templates_dir.exists():
|
|
263
264
|
templates_dir = dev_templates_dir
|
|
264
265
|
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
265
266
|
else:
|
|
266
|
-
|
|
267
|
+
debug_print("[dim]Debug:[/dim] Development path not found, trying installed package...")
|
|
267
268
|
# Try 2: Installed package - use importlib.resources
|
|
268
269
|
# Note: importlib is part of Python's standard library (since Python 3.1)
|
|
269
270
|
# importlib.resources.files() is available since Python 3.9
|
|
@@ -273,7 +274,7 @@ def init(
|
|
|
273
274
|
try:
|
|
274
275
|
import importlib.resources
|
|
275
276
|
|
|
276
|
-
|
|
277
|
+
debug_print("[dim]Debug:[/dim] Using importlib.resources.files() API...")
|
|
277
278
|
# Use files() API (Python 3.9+) - recommended approach
|
|
278
279
|
resources_ref = importlib.resources.files("specfact_cli")
|
|
279
280
|
templates_ref = resources_ref / "resources" / "prompts"
|
|
@@ -282,7 +283,7 @@ def init(
|
|
|
282
283
|
# Use resolve() to handle Windows/Linux/macOS path differences
|
|
283
284
|
package_templates_dir = Path(str(templates_ref)).resolve()
|
|
284
285
|
tried_locations.append(package_templates_dir)
|
|
285
|
-
|
|
286
|
+
debug_print(f"[dim]Debug:[/dim] Package templates path: {package_templates_dir}")
|
|
286
287
|
if package_templates_dir.exists():
|
|
287
288
|
templates_dir = package_templates_dir
|
|
288
289
|
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
@@ -292,20 +293,20 @@ def init(
|
|
|
292
293
|
console.print(
|
|
293
294
|
f"[yellow]⚠[/yellow] importlib.resources not available or module not found: {type(e).__name__}: {e}"
|
|
294
295
|
)
|
|
295
|
-
|
|
296
|
+
debug_print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
|
|
296
297
|
except (TypeError, AttributeError, ValueError) as e:
|
|
297
298
|
console.print(f"[yellow]⚠[/yellow] Error converting Traversable to Path: {e}")
|
|
298
|
-
|
|
299
|
+
debug_print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
|
|
299
300
|
except Exception as e:
|
|
300
301
|
console.print(f"[yellow]⚠[/yellow] Unexpected error with importlib.resources: {type(e).__name__}: {e}")
|
|
301
|
-
|
|
302
|
+
debug_print("[dim]Debug:[/dim] Falling back to importlib.util.find_spec()...")
|
|
302
303
|
|
|
303
304
|
# Fallback: importlib.util.find_spec() + comprehensive package location search
|
|
304
305
|
if not templates_dir or not templates_dir.exists():
|
|
305
306
|
try:
|
|
306
307
|
import importlib.util
|
|
307
308
|
|
|
308
|
-
|
|
309
|
+
debug_print("[dim]Debug:[/dim] Using importlib.util.find_spec() fallback...")
|
|
309
310
|
spec = importlib.util.find_spec("specfact_cli")
|
|
310
311
|
if spec and spec.origin:
|
|
311
312
|
# spec.origin points to __init__.py
|
|
@@ -314,8 +315,8 @@ def init(
|
|
|
314
315
|
package_root = Path(spec.origin).parent.resolve()
|
|
315
316
|
package_templates_dir = (package_root / "resources" / "prompts").resolve()
|
|
316
317
|
tried_locations.append(package_templates_dir)
|
|
317
|
-
|
|
318
|
-
|
|
318
|
+
debug_print(f"[dim]Debug:[/dim] Package root from spec.origin: {package_root}")
|
|
319
|
+
debug_print(f"[dim]Debug:[/dim] Templates path from spec: {package_templates_dir}")
|
|
319
320
|
if package_templates_dir.exists():
|
|
320
321
|
templates_dir = package_templates_dir
|
|
321
322
|
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|
|
@@ -324,20 +325,20 @@ def init(
|
|
|
324
325
|
else:
|
|
325
326
|
console.print("[yellow]⚠[/yellow] Could not find specfact_cli module spec")
|
|
326
327
|
if spec is None:
|
|
327
|
-
|
|
328
|
+
debug_print("[dim]Debug:[/dim] spec is None")
|
|
328
329
|
elif not spec.origin:
|
|
329
|
-
|
|
330
|
+
debug_print("[dim]Debug:[/dim] spec.origin is None or empty")
|
|
330
331
|
except Exception as e:
|
|
331
332
|
console.print(f"[yellow]⚠[/yellow] Error with importlib.util.find_spec(): {type(e).__name__}: {e}")
|
|
332
333
|
|
|
333
334
|
# Fallback: Comprehensive package location search (cross-platform)
|
|
334
335
|
if not templates_dir or not templates_dir.exists():
|
|
335
336
|
try:
|
|
336
|
-
|
|
337
|
+
debug_print("[dim]Debug:[/dim] Searching all package installation locations...")
|
|
337
338
|
package_locations = get_package_installation_locations("specfact_cli")
|
|
338
|
-
|
|
339
|
+
debug_print(f"[dim]Debug:[/dim] Found {len(package_locations)} possible package location(s)")
|
|
339
340
|
for i, loc in enumerate(package_locations, 1):
|
|
340
|
-
|
|
341
|
+
debug_print(f"[dim]Debug:[/dim] {i}. {loc}")
|
|
341
342
|
# Check for resources/prompts in this package location
|
|
342
343
|
resource_path = (loc / "resources" / "prompts").resolve()
|
|
343
344
|
tried_locations.append(resource_path)
|
|
@@ -347,7 +348,7 @@ def init(
|
|
|
347
348
|
break
|
|
348
349
|
if not templates_dir or not templates_dir.exists():
|
|
349
350
|
# Try using the helper function as a final attempt
|
|
350
|
-
|
|
351
|
+
debug_print("[dim]Debug:[/dim] Trying find_package_resources_path() helper...")
|
|
351
352
|
resource_path = find_package_resources_path("specfact_cli", "resources/prompts")
|
|
352
353
|
if resource_path and resource_path.exists():
|
|
353
354
|
tried_locations.append(resource_path)
|
|
@@ -361,15 +362,15 @@ def init(
|
|
|
361
362
|
# Try 3: Fallback - relative to this file (for edge cases)
|
|
362
363
|
if not templates_dir or not templates_dir.exists():
|
|
363
364
|
try:
|
|
364
|
-
|
|
365
|
+
debug_print("[dim]Debug:[/dim] Trying fallback: relative to __file__...")
|
|
365
366
|
# Get the directory containing this file (init.py)
|
|
366
367
|
# init.py is in: src/specfact_cli/commands/init.py
|
|
367
368
|
# Go up: commands -> specfact_cli -> src -> project root
|
|
368
369
|
current_file = Path(__file__).resolve()
|
|
369
370
|
fallback_dir = (current_file.parent.parent.parent.parent / "resources" / "prompts").resolve()
|
|
370
371
|
tried_locations.append(fallback_dir)
|
|
371
|
-
|
|
372
|
-
|
|
372
|
+
debug_print(f"[dim]Debug:[/dim] Current file: {current_file}")
|
|
373
|
+
debug_print(f"[dim]Debug:[/dim] Fallback templates path: {fallback_dir}")
|
|
373
374
|
if fallback_dir.exists():
|
|
374
375
|
templates_dir = fallback_dir
|
|
375
376
|
console.print(f"[green]✓[/green] Found templates at: {templates_dir}")
|