@scemoon/cdh 1.0.8 → 1.0.9-beta.2
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.
- package/ai-dlc-skill/SKILL.md +106 -0
- package/ai-dlc-skill/architecture/project-structure.md +78 -0
- package/ai-dlc-skill/brownfield/README.md +36 -0
- package/ai-dlc-skill/brownfield/entry.md +45 -0
- package/ai-dlc-skill/brownfield/generate-context.sh +64 -0
- package/ai-dlc-skill/brownfield/scripts/deps.sh +27 -0
- package/ai-dlc-skill/brownfield/scripts/discover.sh +34 -0
- package/ai-dlc-skill/brownfield/scripts/extract-api.sh +45 -0
- package/ai-dlc-skill/components/backend.md +47 -0
- package/ai-dlc-skill/components/desktop.md +63 -0
- package/ai-dlc-skill/components/mya.md +219 -0
- package/ai-dlc-skill/components/native.md +73 -0
- package/ai-dlc-skill/components/tta.md +235 -0
- package/ai-dlc-skill/components/web.md +77 -0
- package/ai-dlc-skill/components/wxa.md +219 -0
- package/ai-dlc-skill/contracts/README.md +25 -0
- package/ai-dlc-skill/core/adaptive-flow.md +107 -0
- package/ai-dlc-skill/core/security.md +29 -0
- package/ai-dlc-skill/core/task-registry.md +153 -0
- package/ai-dlc-skill/cross-tool/README.md +20 -0
- package/ai-dlc-skill/cross-tool/cline/export.sh +38 -0
- package/ai-dlc-skill/cross-tool/copilot/export.sh +41 -0
- package/ai-dlc-skill/cross-tool/cursor/export.sh +53 -0
- package/ai-dlc-skill/cross-tool/onecode/export.sh +27 -0
- package/ai-dlc-skill/cross-tool/opencode/export.sh +38 -0
- package/ai-dlc-skill/phases/debug/entry.md +48 -0
- package/ai-dlc-skill/phases/debug/lifecycle.md +284 -0
- package/ai-dlc-skill/phases/deliver/entry.md +35 -0
- package/ai-dlc-skill/phases/deliver/lifecycle.md +185 -0
- package/ai-dlc-skill/phases/deliver/prompt.md +54 -0
- package/ai-dlc-skill/phases/deliver/rules.md +61 -0
- package/ai-dlc-skill/phases/plan/design_system/accessibility.md +268 -0
- package/ai-dlc-skill/phases/plan/design_system/atomic_design.md +341 -0
- package/ai-dlc-skill/phases/plan/design_system/component_spec.md +398 -0
- package/ai-dlc-skill/phases/plan/design_system/design_tokens.md +190 -0
- package/ai-dlc-skill/phases/plan/design_system/iconography.md +299 -0
- package/ai-dlc-skill/phases/plan/design_system/platform_ui/desktop.md +318 -0
- package/ai-dlc-skill/phases/plan/design_system/platform_ui/mya.md +354 -0
- package/ai-dlc-skill/phases/plan/design_system/platform_ui/native.md +328 -0
- package/ai-dlc-skill/phases/plan/design_system/platform_ui/tta.md +394 -0
- package/ai-dlc-skill/phases/plan/design_system/platform_ui/web.md +287 -0
- package/ai-dlc-skill/phases/plan/design_system/platform_ui/wxa.md +374 -0
- package/ai-dlc-skill/phases/plan/design_system/theme_system.md +318 -0
- package/ai-dlc-skill/phases/plan/entry.md +33 -0
- package/ai-dlc-skill/phases/plan/lifecycle.md +260 -0
- package/ai-dlc-skill/phases/plan/prompt.md +75 -0
- package/ai-dlc-skill/phases/plan/rules.md +60 -0
- package/ai-dlc-skill/phases/understand/entry.md +36 -0
- package/ai-dlc-skill/phases/understand/lifecycle.md +184 -0
- package/ai-dlc-skill/phases/understand/prompt.md +58 -0
- package/ai-dlc-skill/phases/understand/rules.md +49 -0
- package/ai-dlc-skill/phases/verify/config/base_url.md +65 -0
- package/ai-dlc-skill/phases/verify/entry.md +59 -0
- package/ai-dlc-skill/phases/verify/gates/browser_e2e.md +96 -0
- package/ai-dlc-skill/phases/verify/lifecycle.md +217 -0
- package/ai-dlc-skill/phases/verify/prompt.md +73 -0
- package/ai-dlc-skill/phases/verify/rules.md +91 -0
- package/ai-dlc-skill/phases/verify/runners/playwright_local.md +103 -0
- package/ai-dlc-skill/practices/bdd.md +119 -0
- package/ai-dlc-skill/practices/sdd.md +84 -0
- package/ai-dlc-skill/practices/tdd.md +128 -0
- package/ai-dlc-skill/providers/README.md +114 -0
- package/ai-dlc-skill/providers/aliyun/SKILL.md +204 -0
- package/ai-dlc-skill/providers/aliyun/api/rest-api.md +214 -0
- package/ai-dlc-skill/providers/aliyun/api/sdk.md +307 -0
- package/ai-dlc-skill/providers/aliyun/best-practices/database-design.md +363 -0
- package/ai-dlc-skill/providers/aliyun/best-practices/function-design.md +424 -0
- package/ai-dlc-skill/providers/aliyun/best-practices/troubleshooting.md +258 -0
- package/ai-dlc-skill/providers/aliyun/cli/database.md +263 -0
- package/ai-dlc-skill/providers/aliyun/cli/functions.md +263 -0
- package/ai-dlc-skill/providers/aliyun/cli/hosting.md +226 -0
- package/ai-dlc-skill/providers/aliyun/cli/setup.md +235 -0
- package/ai-dlc-skill/providers/aliyun/cli/storage.md +315 -0
- package/ai-dlc-skill/providers/aliyun/deployment.yaml +66 -0
- package/ai-dlc-skill/providers/aliyun/integration/ci-cd.md +288 -0
- package/ai-dlc-skill/providers/aliyun/preview.yaml +84 -0
- package/ai-dlc-skill/providers/aliyun/provider.yaml +160 -0
- package/ai-dlc-skill/providers/aliyun/security/authentication.md +254 -0
- package/ai-dlc-skill/providers/aliyun/services/cdn-hosting.md +220 -0
- package/ai-dlc-skill/providers/aliyun/services/database.md +319 -0
- package/ai-dlc-skill/providers/aliyun/services/function-compute.md +273 -0
- package/ai-dlc-skill/providers/aliyun/services/overview.md +163 -0
- package/ai-dlc-skill/providers/aliyun/services/serverless-app-engine.md +266 -0
- package/ai-dlc-skill/providers/aliyun/services/storage.md +269 -0
- package/ai-dlc-skill/providers/tcb/SKILL.md +208 -0
- package/ai-dlc-skill/providers/tcb/api/rest-api.md +338 -0
- package/ai-dlc-skill/providers/tcb/api/sdk.md +288 -0
- package/ai-dlc-skill/providers/tcb/best-practices/database-design.md +452 -0
- package/ai-dlc-skill/providers/tcb/best-practices/function-design.md +502 -0
- package/ai-dlc-skill/providers/tcb/best-practices/troubleshooting.md +462 -0
- package/ai-dlc-skill/providers/tcb/cli/cls.md +133 -0
- package/ai-dlc-skill/providers/tcb/cli/database.md +230 -0
- package/ai-dlc-skill/providers/tcb/cli/env.md +207 -0
- package/ai-dlc-skill/providers/tcb/cli/functions.md +276 -0
- package/ai-dlc-skill/providers/tcb/cli/hosting.md +219 -0
- package/ai-dlc-skill/providers/tcb/cli/setup.md +218 -0
- package/ai-dlc-skill/providers/tcb/cli/storage.md +233 -0
- package/ai-dlc-skill/providers/tcb/deployment.yaml +111 -0
- package/ai-dlc-skill/providers/tcb/integration/ci-cd.md +309 -0
- package/ai-dlc-skill/providers/tcb/integration/mcp-server.md +371 -0
- package/ai-dlc-skill/providers/tcb/preview.yaml +83 -0
- package/ai-dlc-skill/providers/tcb/provider.yaml +172 -0
- package/ai-dlc-skill/providers/tcb/security/authentication.md +241 -0
- package/ai-dlc-skill/providers/tcb/services/cloud-database.md +299 -0
- package/ai-dlc-skill/providers/tcb/services/cloud-functions.md +265 -0
- package/ai-dlc-skill/providers/tcb/services/cloud-hosting.md +264 -0
- package/ai-dlc-skill/providers/tcb/services/cloud-run.md +285 -0
- package/ai-dlc-skill/providers/tcb/services/cloud-storage.md +245 -0
- package/ai-dlc-skill/providers/tcb/services/overview.md +174 -0
- package/ai-dlc-skill/pybdd/README.md +41 -0
- package/ai-dlc-skill/pybdd/conftest.py +17 -0
- package/ai-dlc-skill/pybdd/example/auth/login.feature +32 -0
- package/ai-dlc-skill/pybdd/pytest.ini +5 -0
- package/ai-dlc-skill/pybdd/steps/.gitkeep +1 -0
- package/ai-dlc-skill/pybdd/steps/test_example_steps.py +65 -0
- package/ai-dlc-skill/requirements.md +140 -0
- package/ai-dlc-skill/skill.yaml +135 -0
- package/ai-dlc-skill/templates/brownfield/context.md +78 -0
- package/ai-dlc-skill/templates/integration/CHANGELOG.md +41 -0
- package/ai-dlc-skill/templates/integration/asyncapi.yaml +43 -0
- package/ai-dlc-skill/templates/integration/contract-diff.md +63 -0
- package/ai-dlc-skill/templates/integration/contract-spec.md +49 -0
- package/ai-dlc-skill/templates/integration/openapi.yaml +18 -0
- package/ai-dlc-skill/templates/integration/runtime-contract.yaml +36 -0
- package/ai-dlc-skill/templates/plan/design.md +49 -0
- package/ai-dlc-skill/templates/plan/task-list.md +32 -0
- package/ai-dlc-skill/templates/project/README.md +126 -0
- package/ai-dlc-skill/templates/project/template.md +119 -0
- package/ai-dlc-skill/templates/understand/feature.feature +30 -0
- package/ai-dlc-skill/templates/understand/intent.md +29 -0
- package/ai-dlc-skill/templates/understand/spec-delta.md +47 -0
- package/ai-dlc-skill/templates/verify/test-unit.py +27 -0
- package/ai-dlc-skill/walkthrough/collect.sh +50 -0
- package/ai-dlc-skill/walkthrough/template.md +54 -0
- package/cdh/__init__.py +0 -0
- package/cdh/__main__.py +4 -0
- package/cdh/__pycache__/__init__.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
- package/cdh/__pycache__/cli_logging.cpython-314.pyc +0 -0
- package/cdh/__pycache__/config.cpython-314.pyc +0 -0
- package/cdh/__pycache__/project_loader.cpython-314.pyc +0 -0
- package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
- package/cdh/__pycache__/session_store.cpython-314.pyc +0 -0
- package/cdh/cdh_session_aggregator.py +258 -0
- package/cdh/cli.py +3000 -0
- package/cdh/cli_logging.py +45 -0
- package/cdh/config.py +28 -0
- package/cdh/event_loop/__init__.py +7 -0
- package/cdh/event_loop/bus.py +123 -0
- package/cdh/event_loop/events.py +25 -0
- package/cdh/event_loop/runner.py +28 -0
- package/cdh/event_loop/scheduler.py +158 -0
- package/cdh/generators/__init__.py +35 -0
- package/cdh/generators/_runtime.py +176 -0
- package/cdh/generators/ada/MANIFEST.toml +14 -0
- package/cdh/generators/ada/template.ads.tmpl +7 -0
- package/cdh/generators/bash/MANIFEST.toml +14 -0
- package/cdh/generators/bash/template.sh.tmpl +16 -0
- package/cdh/generators/builtin_index.json +590 -0
- package/cdh/generators/c/MANIFEST.toml +14 -0
- package/cdh/generators/c/template.h.tmpl +20 -0
- package/cdh/generators/cli.py +379 -0
- package/cdh/generators/clojure/MANIFEST.toml +14 -0
- package/cdh/generators/clojure/template.clj.tmpl +13 -0
- package/cdh/generators/cobol/MANIFEST.toml +14 -0
- package/cdh/generators/cobol/template.cbl.tmpl +14 -0
- package/cdh/generators/cpp/MANIFEST.toml +14 -0
- package/cdh/generators/cpp/template.hpp.tmpl +32 -0
- package/cdh/generators/crystal/MANIFEST.toml +14 -0
- package/cdh/generators/crystal/template.cr.tmpl +11 -0
- package/cdh/generators/csharp/MANIFEST.toml +23 -0
- package/cdh/generators/csharp/template.cs.tmpl +14 -0
- package/cdh/generators/cython/MANIFEST.toml +14 -0
- package/cdh/generators/cython/template.pyx.tmpl +17 -0
- package/cdh/generators/d/MANIFEST.toml +14 -0
- package/cdh/generators/d/template.d.tmpl +24 -0
- package/cdh/generators/dart/MANIFEST.toml +17 -0
- package/cdh/generators/dart/template.dart.tmpl +27 -0
- package/cdh/generators/elixir/MANIFEST.toml +17 -0
- package/cdh/generators/elixir/template.ex.tmpl +27 -0
- package/cdh/generators/elm/MANIFEST.toml +14 -0
- package/cdh/generators/elm/template.elm.tmpl +20 -0
- package/cdh/generators/erlang/MANIFEST.toml +14 -0
- package/cdh/generators/erlang/template.erl.tmpl +18 -0
- package/cdh/generators/fortran/MANIFEST.toml +14 -0
- package/cdh/generators/fortran/template.f90.tmpl +24 -0
- package/cdh/generators/fsharp/MANIFEST.toml +14 -0
- package/cdh/generators/fsharp/template.fs.tmpl +17 -0
- package/cdh/generators/go/MANIFEST.toml +18 -0
- package/cdh/generators/go/imports.go.tmpl +3 -0
- package/cdh/generators/go/template.go.tmpl +9 -0
- package/cdh/generators/graphql-schema/MANIFEST.toml +18 -0
- package/cdh/generators/graphql-schema/template.graphqls.tmpl +12 -0
- package/cdh/generators/groovy/MANIFEST.toml +14 -0
- package/cdh/generators/groovy/template.groovy.tmpl +28 -0
- package/cdh/generators/hack/MANIFEST.toml +14 -0
- package/cdh/generators/hack/template.hh.tmpl +24 -0
- package/cdh/generators/haskell/MANIFEST.toml +14 -0
- package/cdh/generators/haskell/template.hs.tmpl +20 -0
- package/cdh/generators/haxe/MANIFEST.toml +14 -0
- package/cdh/generators/haxe/template.hx.tmpl +20 -0
- package/cdh/generators/idris/MANIFEST.toml +14 -0
- package/cdh/generators/idris/template.idr.tmpl +22 -0
- package/cdh/generators/java/MANIFEST.toml +18 -0
- package/cdh/generators/java/template.java.tmpl +13 -0
- package/cdh/generators/julia/MANIFEST.toml +14 -0
- package/cdh/generators/julia/template.jl.tmpl +27 -0
- package/cdh/generators/kotlin/MANIFEST.toml +18 -0
- package/cdh/generators/kotlin/template.kt.tmpl +12 -0
- package/cdh/generators/lua/MANIFEST.toml +14 -0
- package/cdh/generators/lua/template.lua.tmpl +27 -0
- package/cdh/generators/matlab/MANIFEST.toml +14 -0
- package/cdh/generators/matlab/template.m.tmpl +26 -0
- package/cdh/generators/nim/MANIFEST.toml +14 -0
- package/cdh/generators/nim/template.nim.tmpl +12 -0
- package/cdh/generators/nix/MANIFEST.toml +14 -0
- package/cdh/generators/nix/template.nix.tmpl +8 -0
- package/cdh/generators/objective-c/MANIFEST.toml +14 -0
- package/cdh/generators/objective-c/template.m.tmpl +31 -0
- package/cdh/generators/ocaml/MANIFEST.toml +14 -0
- package/cdh/generators/ocaml/template.ml.tmpl +13 -0
- package/cdh/generators/pascal/MANIFEST.toml +14 -0
- package/cdh/generators/pascal/template.pas.tmpl +5 -0
- package/cdh/generators/perl/MANIFEST.toml +14 -0
- package/cdh/generators/perl/template.pm.tmpl +27 -0
- package/cdh/generators/php/MANIFEST.toml +17 -0
- package/cdh/generators/php/template.php.tmpl +31 -0
- package/cdh/generators/powershell/MANIFEST.toml +14 -0
- package/cdh/generators/powershell/template.ps1.tmpl +20 -0
- package/cdh/generators/protobuf/MANIFEST.toml +18 -0
- package/cdh/generators/protobuf/template.proto.tmpl +13 -0
- package/cdh/generators/python/MANIFEST.toml +18 -0
- package/cdh/generators/python/template.py.tmpl +12 -0
- package/cdh/generators/r/MANIFEST.toml +14 -0
- package/cdh/generators/r/template.R.tmpl +29 -0
- package/cdh/generators/racket/MANIFEST.toml +14 -0
- package/cdh/generators/racket/template.rkt.tmpl +15 -0
- package/cdh/generators/registry.py +211 -0
- package/cdh/generators/ruby/MANIFEST.toml +17 -0
- package/cdh/generators/ruby/template.rb.tmpl +27 -0
- package/cdh/generators/rust/MANIFEST.toml +18 -0
- package/cdh/generators/rust/template.rs.tmpl +16 -0
- package/cdh/generators/scala/MANIFEST.toml +20 -0
- package/cdh/generators/scala/template.scala.tmpl +13 -0
- package/cdh/generators/scheme/MANIFEST.toml +14 -0
- package/cdh/generators/scheme/template.scm.tmpl +16 -0
- package/cdh/generators/solidity/MANIFEST.toml +14 -0
- package/cdh/generators/solidity/template.sol.tmpl +24 -0
- package/cdh/generators/swift/MANIFEST.toml +18 -0
- package/cdh/generators/swift/template.swift.tmpl +10 -0
- package/cdh/generators/typescript/MANIFEST.toml +18 -0
- package/cdh/generators/typescript/template.ts.tmpl +7 -0
- package/cdh/generators/version.py +214 -0
- package/cdh/generators/zig/MANIFEST.toml +14 -0
- package/cdh/generators/zig/template.zig.tmpl +20 -0
- package/cdh/logging.py +314 -0
- package/cdh/optimizer/__init__.py +7 -0
- package/cdh/optimizer/loop.py +201 -0
- package/cdh/optimizer/mutation.py +85 -0
- package/cdh/optimizer/reward.py +51 -0
- package/cdh/optimizer/tracker.py +118 -0
- package/cdh/project_loader.py +1022 -0
- package/cdh/rules/patterns.yaml +132 -0
- package/cdh/rules/spectral.yaml +301 -0
- package/cdh/scaffold.py +1436 -0
- package/cdh/session_store.py +221 -0
- package/cdh/state_schema.py +127 -0
- package/cdh/tools/__init__.py +57 -0
- package/cdh/tools/cls_search.py +406 -0
- package/cdh/tools/contract_diff.py +644 -0
- package/cdh/tools/deploy_stack.py +590 -0
- package/cdh/tools/generate_shared.py +300 -0
- package/cdh/tools/iac_gen.py +224 -0
- package/cdh/tools/otel_export.py +125 -0
- package/cdh/tools/pattern_detect.py +451 -0
- package/cdh/tools/retrofit_labels.py +505 -0
- package/cdh/trace/__init__.py +101 -0
- package/cdh/trace/__pycache__/__init__.cpython-314.pyc +0 -0
- package/cdh/trace/dashboard/__init__.py +61 -0
- package/cdh/trace/dashboard/api.py +1032 -0
- package/cdh/trace/dashboard/app.html +1321 -0
- package/cdh/trace/dashboard/server.py +310 -0
- package/cdh/trace/otel_exporter.py +462 -0
- package/cdh/trace/otel_tracer.py +281 -0
- package/cdh/tui.py +1084 -0
- package/cdh/validators/__init__.py +16 -0
- package/cdh/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/cdh/validators/__pycache__/bdd.cpython-314.pyc +0 -0
- package/cdh/validators/__pycache__/dag.cpython-314.pyc +0 -0
- package/cdh/validators/__pycache__/ears.cpython-314.pyc +0 -0
- package/cdh/validators/__pycache__/fr_namespace.cpython-314.pyc +0 -0
- package/cdh/validators/bdd.py +144 -0
- package/cdh/validators/dag.py +179 -0
- package/cdh/validators/ears.py +452 -0
- package/cdh/validators/fr_namespace.py +143 -0
- package/cdh/validators/spectral.py +475 -0
- package/cdh/verification/__init__.py +3 -0
- package/cdh/verification/aggregation.py +53 -0
- package/cdh/verification/gates/__init__.py +5 -0
- package/cdh/verification/gates/base.py +18 -0
- package/cdh/verification/gates/lint_gate.py +37 -0
- package/cdh/verification/gates/test_gate.py +41 -0
- package/cdh/verification/loop.py +93 -0
- package/cdh/verification/policy.py +24 -0
- package/onecode/__init__.py +1 -0
- package/onecode/__main__.py +4 -0
- package/onecode/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
- package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
- package/onecode/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
- package/onecode/agent/__init__.py +0 -0
- package/onecode/agent/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/checkpoint.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/permissions_store.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
- package/onecode/agent/__pycache__/turn_record.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__init__.py +29 -0
- package/onecode/agent/agents/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
- package/onecode/agent/agents/prompts/build-agent.md +50 -0
- package/onecode/agent/agents/prompts/compaction-instructions.md +12 -0
- package/onecode/agent/agents/prompts/explore-agent.md +42 -0
- package/onecode/agent/agents/prompts/plan-agent.md +44 -0
- package/onecode/agent/agents/prompts/plan-gate-hard.md +4 -0
- package/onecode/agent/agents/prompts/plan-gate-soft.md +3 -0
- package/onecode/agent/agents/prompts/solo-agent.md +57 -0
- package/onecode/agent/agents/prompts/subagent-constraints.md +10 -0
- package/onecode/agent/agents/prompts/summary-instructions.md +7 -0
- package/onecode/agent/agents/prompts/title-instructions.md +3 -0
- package/onecode/agent/agents/prompts/tool-descriptions.md +129 -0
- package/onecode/agent/agents/types.py +786 -0
- package/onecode/agent/attachments.py +112 -0
- package/onecode/agent/cdh_loader.py +3 -0
- package/onecode/agent/checkpoint.py +285 -0
- package/onecode/agent/context.py +465 -0
- package/onecode/agent/engine.py +3547 -0
- package/onecode/agent/event_bridge.py +75 -0
- package/onecode/agent/hooks.py +86 -0
- package/onecode/agent/onecode_agent_acp.py +2993 -0
- package/onecode/agent/permissions.py +150 -0
- package/onecode/agent/permissions_store.py +73 -0
- package/onecode/agent/project_doc.py +19 -0
- package/onecode/agent/question_detect.py +230 -0
- package/onecode/agent/session.py +3 -0
- package/onecode/agent/snapshot.py +144 -0
- package/onecode/agent/tools/__init__.py +17 -0
- package/onecode/agent/tools/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/errors.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/permissions.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/schema_validation.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
- package/onecode/agent/tools/agent_tools.py +128 -0
- package/onecode/agent/tools/apply_patch_tool.py +138 -0
- package/onecode/agent/tools/bash_tool.py +121 -0
- package/onecode/agent/tools/communication_tools.py +154 -0
- package/onecode/agent/tools/config_tool.py +130 -0
- package/onecode/agent/tools/cron_tools.py +178 -0
- package/onecode/agent/tools/errors.py +17 -0
- package/onecode/agent/tools/file_ops.py +221 -0
- package/onecode/agent/tools/file_tools.py +225 -0
- package/onecode/agent/tools/git_tools.py +88 -0
- package/onecode/agent/tools/lsp_tools.py +404 -0
- package/onecode/agent/tools/mcp_tools.py +211 -0
- package/onecode/agent/tools/permission_handler.py +46 -0
- package/onecode/agent/tools/permissions.py +77 -0
- package/onecode/agent/tools/protocol.py +20 -0
- package/onecode/agent/tools/registry.py +210 -0
- package/onecode/agent/tools/sandbox.py +460 -0
- package/onecode/agent/tools/schema_validation.py +100 -0
- package/onecode/agent/tools/skill_tools.py +52 -0
- package/onecode/agent/tools/todo_tools.py +208 -0
- package/onecode/agent/tools/web_tools.py +530 -0
- package/onecode/agent/turn_record.py +28 -0
- package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
- package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
- package/onecode/builtin_skills/cloudbase/SKILL.md +162 -0
- package/onecode/builtin_skills/cloudbase/skill.yaml +7 -0
- package/onecode/builtin_skills/git/SKILL.md +64 -0
- package/onecode/builtin_skills/git/skill.yaml +7 -0
- package/onecode/builtin_skills/shell/SKILL.md +58 -0
- package/onecode/builtin_skills/shell/skill.yaml +8 -0
- package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
- package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
- package/onecode/cli.py +1325 -0
- package/onecode/codebase/__init__.py +69 -0
- package/onecode/codebase/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
- package/onecode/codebase/__pycache__/tools.cpython-314.pyc +0 -0
- package/onecode/codebase/chunker.py +125 -0
- package/onecode/codebase/indexer.py +154 -0
- package/onecode/codebase/retriever.py +240 -0
- package/onecode/codebase/storage.py +153 -0
- package/onecode/codebase/tools.py +59 -0
- package/onecode/commands.py +673 -0
- package/onecode/config.py +539 -0
- package/onecode/config_screen.py +486 -0
- package/onecode/cron/__init__.py +3 -0
- package/onecode/lsp/__init__.py +3 -0
- package/onecode/mcp/__init__.py +11 -0
- package/onecode/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/mcp/__pycache__/client.cpython-314.pyc +0 -0
- package/onecode/mcp/__pycache__/cloudbase.cpython-314.pyc +0 -0
- package/onecode/mcp/__pycache__/config.cpython-314.pyc +0 -0
- package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
- package/onecode/mcp/__pycache__/oauth.cpython-314.pyc +0 -0
- package/onecode/mcp/client.py +305 -0
- package/onecode/mcp/cloudbase.py +188 -0
- package/onecode/mcp/config.py +363 -0
- package/onecode/mcp/manager.py +603 -0
- package/onecode/mcp/oauth.py +199 -0
- package/onecode/memory/__init__.py +106 -0
- package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
- package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
- package/onecode/memory/backend.py +179 -0
- package/onecode/memory/pyramid.py +145 -0
- package/onecode/memory/recall.py +322 -0
- package/onecode/migrate.py +122 -0
- package/onecode/models/__init__.py +4 -0
- package/onecode/models/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/ollama_manager.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
- package/onecode/models/__pycache__/registry.cpython-314.pyc +0 -0
- package/onecode/models/errors.py +104 -0
- package/onecode/models/messages.py +750 -0
- package/onecode/models/ollama_manager.py +167 -0
- package/onecode/models/provider.py +730 -0
- package/onecode/models/providers/__init__.py +17 -0
- package/onecode/models/providers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
- package/onecode/models/providers/anthropic_provider.py +286 -0
- package/onecode/models/providers/deepseek_provider.py +221 -0
- package/onecode/models/providers/glm_provider.py +219 -0
- package/onecode/models/providers/minimax_provider.py +182 -0
- package/onecode/models/providers/minimaxi_provider.py +426 -0
- package/onecode/models/providers/ollama_provider.py +183 -0
- package/onecode/models/providers/openai_provider.py +234 -0
- package/onecode/models/registry.py +349 -0
- package/onecode/server/__init__.py +3 -0
- package/onecode/server/app.py +251 -0
- package/onecode/skills/__init__.py +11 -0
- package/onecode/skills/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/create.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/frontmatter.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
- package/onecode/skills/__pycache__/model.cpython-314.pyc +0 -0
- package/onecode/skills/argument_substitution.py +38 -0
- package/onecode/skills/bootstrap.py +37 -0
- package/onecode/skills/create.py +56 -0
- package/onecode/skills/frontmatter.py +33 -0
- package/onecode/skills/loader.py +215 -0
- package/onecode/skills/manager.py +75 -0
- package/onecode/skills/model.py +43 -0
- package/onecode/storage/__init__.py +0 -0
- package/onecode/storage/activity.py +63 -0
- package/onecode/storage/project.py +54 -0
- package/onecode/storage/session.py +112 -0
- package/onecode/tasks/__init__.py +26 -0
- package/onecode/tasks/manager.py +232 -0
- package/onecode/tasks/models.py +240 -0
- package/onecode/trace/__init__.py +0 -0
- package/onecode/trace/tracer.py +173 -0
- package/onecode/utils/__init__.py +0 -0
- package/onecode/utils/helpers.py +20 -0
- package/onecode/verification/__init__.py +5 -0
- package/onecode/verification/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/verification/__pycache__/aggregation.cpython-314.pyc +0 -0
- package/onecode/verification/__pycache__/loop.cpython-314.pyc +0 -0
- package/onecode/verification/__pycache__/policy.cpython-314.pyc +0 -0
- package/onecode/verification/aggregation.py +53 -0
- package/onecode/verification/gates/__init__.py +6 -0
- package/onecode/verification/gates/__pycache__/__init__.cpython-314.pyc +0 -0
- package/onecode/verification/gates/__pycache__/base.cpython-314.pyc +0 -0
- package/onecode/verification/gates/__pycache__/lint_gate.cpython-314.pyc +0 -0
- package/onecode/verification/gates/__pycache__/test_gate.cpython-314.pyc +0 -0
- package/onecode/verification/gates/__pycache__/type_gate.cpython-314.pyc +0 -0
- package/onecode/verification/gates/base.py +20 -0
- package/onecode/verification/gates/lint_gate.py +45 -0
- package/onecode/verification/gates/test_gate.py +62 -0
- package/onecode/verification/gates/type_gate.py +46 -0
- package/onecode/verification/loop.py +95 -0
- package/onecode/verification/policy.py +29 -0
- package/package.json +2 -2
- package/pyproject.toml +84 -0
- package/tui/__init__.py +46 -0
- package/tui/__main__.py +4 -0
- package/tui/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/__pycache__/_loop.cpython-314.pyc +0 -0
- package/tui/__pycache__/_path_fuzzy_search.cpython-314.pyc +0 -0
- package/tui/__pycache__/_path_match.cpython-314.pyc +0 -0
- package/tui/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/__pycache__/agent_schema.cpython-314.pyc +0 -0
- package/tui/__pycache__/agents.cpython-314.pyc +0 -0
- package/tui/__pycache__/answer.cpython-314.pyc +0 -0
- package/tui/__pycache__/app.cpython-314.pyc +0 -0
- package/tui/__pycache__/atomic.cpython-314.pyc +0 -0
- package/tui/__pycache__/complete.cpython-314.pyc +0 -0
- package/tui/__pycache__/constants.cpython-314.pyc +0 -0
- package/tui/__pycache__/conversation_markdown.cpython-314.pyc +0 -0
- package/tui/__pycache__/db.cpython-314.pyc +0 -0
- package/tui/__pycache__/dec.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory.cpython-314.pyc +0 -0
- package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
- package/tui/__pycache__/format_path.cpython-314.pyc +0 -0
- package/tui/__pycache__/fuzzy.cpython-314.pyc +0 -0
- package/tui/__pycache__/fuzzy_index.cpython-314.pyc +0 -0
- package/tui/__pycache__/history.cpython-314.pyc +0 -0
- package/tui/__pycache__/jsonrpc.cpython-314.pyc +0 -0
- package/tui/__pycache__/menus.cpython-314.pyc +0 -0
- package/tui/__pycache__/message_log.cpython-314.pyc +0 -0
- package/tui/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
- package/tui/__pycache__/path_filter.cpython-314.pyc +0 -0
- package/tui/__pycache__/paths.cpython-314.pyc +0 -0
- package/tui/__pycache__/pill.cpython-314.pyc +0 -0
- package/tui/__pycache__/platform_commands.cpython-314.pyc +0 -0
- package/tui/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings.cpython-314.pyc +0 -0
- package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
- package/tui/__pycache__/shell.cpython-314.pyc +0 -0
- package/tui/__pycache__/shell_read.cpython-314.pyc +0 -0
- package/tui/__pycache__/slash_command.cpython-314.pyc +0 -0
- package/tui/__pycache__/version.cpython-314.pyc +0 -0
- package/tui/_loop.py +86 -0
- package/tui/_path_fuzzy_search.py +48 -0
- package/tui/_path_match.py +141 -0
- package/tui/about.py +95 -0
- package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/encode_tool_call_id.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/event_tap.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
- package/tui/acp/agent.py +1212 -0
- package/tui/acp/api.py +79 -0
- package/tui/acp/encode_tool_call_id.py +12 -0
- package/tui/acp/event_tap.py +107 -0
- package/tui/acp/messages.py +222 -0
- package/tui/acp/prompt.py +53 -0
- package/tui/acp/protocol.py +551 -0
- package/tui/agent.py +70 -0
- package/tui/agent_schema.py +76 -0
- package/tui/agents.py +46 -0
- package/tui/ansi/__init__.py +1 -0
- package/tui/ansi/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_ansi.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_ansi_colors.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_control_codes.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_keys.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_sgr_styles.cpython-314.pyc +0 -0
- package/tui/ansi/__pycache__/_stream_parser.cpython-314.pyc +0 -0
- package/tui/ansi/_ansi.py +1689 -0
- package/tui/ansi/_ansi_colors.py +264 -0
- package/tui/ansi/_control_codes.py +37 -0
- package/tui/ansi/_keys.py +251 -0
- package/tui/ansi/_sgr_styles.py +64 -0
- package/tui/ansi/_stream_parser.py +418 -0
- package/tui/answer.py +12 -0
- package/tui/app.py +1195 -0
- package/tui/atomic.py +37 -0
- package/tui/cli.py +208 -0
- package/tui/code_analyze.py +27 -0
- package/tui/complete.py +38 -0
- package/tui/constants.py +61 -0
- package/tui/conversation_markdown.py +30 -0
- package/tui/danger.py +372 -0
- package/tui/data/agents/ampcode.com.toml +52 -0
- package/tui/data/agents/augmentcode.com.toml +41 -0
- package/tui/data/agents/cdh.cloud-dev-harness.toml +23 -0
- package/tui/data/agents/claude.com.toml +41 -0
- package/tui/data/agents/copilot.github.com.toml +29 -0
- package/tui/data/agents/crowai.dev.toml +36 -0
- package/tui/data/agents/cursor.com.toml +38 -0
- package/tui/data/agents/deepagents.langchain.com +34 -0
- package/tui/data/agents/docker.com.toml +60 -0
- package/tui/data/agents/geminicli.com.toml +28 -0
- package/tui/data/agents/goose.ai.toml +52 -0
- package/tui/data/agents/inference.huggingface.co.toml +41 -0
- package/tui/data/agents/kimi.com.toml +35 -0
- package/tui/data/agents/kiro.dev.toml +43 -0
- package/tui/data/agents/onecode.dev.toml +22 -0
- package/tui/data/agents/openai.com.toml +53 -0
- package/tui/data/agents/openclaw.ai.toml +37 -0
- package/tui/data/agents/opencode.ai.toml +39 -0
- package/tui/data/agents/openhands.dev.toml +46 -0
- package/tui/data/agents/stakpak.dev.toml +62 -0
- package/tui/data/agents/vibe.mistral.ai.toml +31 -0
- package/tui/data/agents/vtcode.dev.toml +63 -0
- package/tui/data/images/frog.png +0 -0
- package/tui/data/sounds/question.wav +0 -0
- package/tui/data/sounds/turn-over.wav +0 -0
- package/tui/db.py +213 -0
- package/tui/dec.py +332 -0
- package/tui/diff_utils.py +131 -0
- package/tui/directory.py +234 -0
- package/tui/directory_suggester.py +93 -0
- package/tui/directory_watcher.py +231 -0
- package/tui/format_path.py +25 -0
- package/tui/fuzzy.py +140 -0
- package/tui/fuzzy_index.py +275 -0
- package/tui/gist.py +33 -0
- package/tui/history.py +138 -0
- package/tui/jsonrpc.py +599 -0
- package/tui/menus.py +14 -0
- package/tui/message_log.py +69 -0
- package/tui/messages.py +144 -0
- package/tui/option_content.py +51 -0
- package/tui/os.py +0 -0
- package/tui/path_complete.py +217 -0
- package/tui/path_filter.py +124 -0
- package/tui/paths.py +87 -0
- package/tui/pill.py +23 -0
- package/tui/platform_commands.py +52 -0
- package/tui/prompt/__pycache__/extract.cpython-314.pyc +0 -0
- package/tui/prompt/__pycache__/resource.cpython-314.pyc +0 -0
- package/tui/prompt/extract.py +19 -0
- package/tui/prompt/resource.py +68 -0
- package/tui/protocol.py +28 -0
- package/tui/screens/__init__.py +0 -0
- package/tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
- package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
- package/tui/screens/action_modal.py +94 -0
- package/tui/screens/agent_modal.py +175 -0
- package/tui/screens/command_edit_modal.py +74 -0
- package/tui/screens/diff_explorer_screen.py +419 -0
- package/tui/screens/diff_screen.py +84 -0
- package/tui/screens/ide_screen.py +461 -0
- package/tui/screens/main.py +397 -0
- package/tui/screens/main.tcss +662 -0
- package/tui/screens/permissions.py +404 -0
- package/tui/screens/permissions.tcss +72 -0
- package/tui/screens/projects.tcss +84 -0
- package/tui/screens/projects_app.py +341 -0
- package/tui/screens/projects_screen.py +277 -0
- package/tui/screens/session_resume_modal.py +172 -0
- package/tui/screens/session_resume_modal.tcss +35 -0
- package/tui/screens/sessions.py +171 -0
- package/tui/screens/sessions.tcss +119 -0
- package/tui/screens/settings.py +293 -0
- package/tui/screens/settings.tcss +113 -0
- package/tui/screens/store.py +502 -0
- package/tui/screens/store.tcss +274 -0
- package/tui/screens/tool_content_screen.py +59 -0
- package/tui/screens/trace.py +317 -0
- package/tui/session_tracker.py +123 -0
- package/tui/settings.py +353 -0
- package/tui/settings_schema.py +467 -0
- package/tui/shell.py +304 -0
- package/tui/shell_read.py +42 -0
- package/tui/slash_command.py +34 -0
- package/tui/tui.tcss +246 -0
- package/tui/twitter.py +38 -0
- package/tui/version.py +80 -0
- package/tui/visuals/__init__.py +0 -0
- package/tui/visuals/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
- package/tui/visuals/columns.py +272 -0
- package/tui/widgets/__init__.py +0 -0
- package/tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/aidlc.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/condensed_path.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/context_stats.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/flash.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/highlighted_textarea.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/menu.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/non_selectable_label.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/note.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/path_search.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/pending_prompts.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/prompt.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/session_tabs.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/shell_terminal.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/slash_complete.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/strike_text.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/throbber.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
- package/tui/widgets/__pycache__/user_input.cpython-314.pyc +0 -0
- package/tui/widgets/acp_content.py +70 -0
- package/tui/widgets/agent_response.py +76 -0
- package/tui/widgets/agent_thought.py +167 -0
- package/tui/widgets/aidlc.py +334 -0
- package/tui/widgets/ask_user.py +604 -0
- package/tui/widgets/command_pane.py +223 -0
- package/tui/widgets/condensed_path.py +115 -0
- package/tui/widgets/context_stats.py +66 -0
- package/tui/widgets/conversation.py +3235 -0
- package/tui/widgets/danger_warning.py +65 -0
- package/tui/widgets/diff_view.py +69 -0
- package/tui/widgets/directory_input.py +8 -0
- package/tui/widgets/edit_field.py +206 -0
- package/tui/widgets/flash.py +81 -0
- package/tui/widgets/future_text.py +126 -0
- package/tui/widgets/grid_select.py +234 -0
- package/tui/widgets/highlighted_textarea.py +183 -0
- package/tui/widgets/load_more.py +24 -0
- package/tui/widgets/mandelbrot.py +294 -0
- package/tui/widgets/markdown_note.py +13 -0
- package/tui/widgets/menu.py +147 -0
- package/tui/widgets/model_status.py +230 -0
- package/tui/widgets/modified_files.py +315 -0
- package/tui/widgets/non_selectable_label.py +5 -0
- package/tui/widgets/note.py +18 -0
- package/tui/widgets/path_search.py +492 -0
- package/tui/widgets/pending_prompts.py +36 -0
- package/tui/widgets/plan.py +285 -0
- package/tui/widgets/project_directory_tree.py +102 -0
- package/tui/widgets/project_grid_select.py +68 -0
- package/tui/widgets/project_summary.py +33 -0
- package/tui/widgets/prompt.py +935 -0
- package/tui/widgets/question.py +360 -0
- package/tui/widgets/session_grid_select.py +230 -0
- package/tui/widgets/session_summary.py +115 -0
- package/tui/widgets/session_tabs.py +194 -0
- package/tui/widgets/shell_result.py +35 -0
- package/tui/widgets/shell_terminal.py +40 -0
- package/tui/widgets/side_bar.py +124 -0
- package/tui/widgets/slash_complete.py +235 -0
- package/tui/widgets/strike_text.py +70 -0
- package/tui/widgets/subagent.py +287 -0
- package/tui/widgets/subagent_screen.py +715 -0
- package/tui/widgets/terminal.py +576 -0
- package/tui/widgets/terminal_tool.py +360 -0
- package/tui/widgets/throbber.py +100 -0
- package/tui/widgets/tool_call.py +432 -0
- package/tui/widgets/user_input.py +23 -0
- package/tui/widgets/version.py +5 -0
- package/tui/widgets/welcome.py +31 -0
package/cdh/cli.py
ADDED
|
@@ -0,0 +1,3000 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import click
|
|
6
|
+
|
|
7
|
+
from cdh.scaffold import (
|
|
8
|
+
COMPONENTS,
|
|
9
|
+
add_component,
|
|
10
|
+
init_dlc_project,
|
|
11
|
+
scaffold_dlc_project,
|
|
12
|
+
check_dlc_project,
|
|
13
|
+
)
|
|
14
|
+
from cdh.validators import run_ears_check, run_fr_check, run_bdd_check, run_dag_check
|
|
15
|
+
from cdh.cli_logging import setup_logging
|
|
16
|
+
from cdh.config import write_active_project
|
|
17
|
+
from onecode.cli import cli as onecode_cli
|
|
18
|
+
from onecode import __version__ as _VERSION
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
_CDH_DIR = Path.home() / ".cdh"
|
|
22
|
+
|
|
23
|
+
_COMMON_HELP = """
|
|
24
|
+
\b
|
|
25
|
+
Usage:
|
|
26
|
+
cdh Launch TUI (agent store)
|
|
27
|
+
cdh tui Launch TUI (agent store)
|
|
28
|
+
cdh onecode <sub> onecode CLI surface (config / codebase / skill / mcp / help)
|
|
29
|
+
cdh aidlc [project|phase|gate|sync|update] AIDLC project management
|
|
30
|
+
cdh session list|load Session management
|
|
31
|
+
cdh trace list|view|dashboard Trace management (agenttrace)
|
|
32
|
+
cdh cls search Search TCB/SCF logs via CLS (requestId tracing)
|
|
33
|
+
cdh uninstall Remove ~/.cdh/ global state
|
|
34
|
+
cdh version Show version information
|
|
35
|
+
|
|
36
|
+
\b
|
|
37
|
+
Paths:
|
|
38
|
+
Config ~/.cdh/onecode.config.yaml
|
|
39
|
+
Logs ~/.cdh/logs/
|
|
40
|
+
Projects ~/.cdh/projects/
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@click.group(
|
|
45
|
+
invoke_without_command=True,
|
|
46
|
+
cls=type(onecode_cli),
|
|
47
|
+
context_settings=dict(max_content_width=100),
|
|
48
|
+
short_help="Cloud Dev Harness - AI agent framework with TUI.",
|
|
49
|
+
epilog=_COMMON_HELP,
|
|
50
|
+
)
|
|
51
|
+
@click.version_option(version=_VERSION, prog_name="cdh")
|
|
52
|
+
@click.pass_context
|
|
53
|
+
def cli(ctx):
|
|
54
|
+
"""
|
|
55
|
+
CDH (Cloud Dev Harness) is an AI agent framework with LLM provider
|
|
56
|
+
integration, session management, and a Textual-based TUI.
|
|
57
|
+
|
|
58
|
+
Run without arguments to open the agent store, where you can pick
|
|
59
|
+
a launcher agent.
|
|
60
|
+
|
|
61
|
+
All onecode-specific commands live under `cdh onecode <sub>` — e.g.
|
|
62
|
+
`cdh onecode config`, `cdh onecode codebase`, `cdh onecode skill`.
|
|
63
|
+
"""
|
|
64
|
+
if ctx.invoked_subcommand is None:
|
|
65
|
+
setup_logging("INFO")
|
|
66
|
+
from tui.app import A2TUIApp
|
|
67
|
+
A2TUIApp().run()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# --- onecode sub-namespace ---
|
|
71
|
+
#
|
|
72
|
+
# `cdh onecode <sub>` exposes the onecode CLI surface under the cdh
|
|
73
|
+
# namespace. The onecode group owns the canonical subcommands
|
|
74
|
+
# (config / codebase / skill / mcp / help); running
|
|
75
|
+
# `cdh onecode config` with no subcommand opens the same interactive
|
|
76
|
+
# TUI editor as before, while `cdh onecode config <sub>` dispatches
|
|
77
|
+
# straight to the onecode implementation.
|
|
78
|
+
|
|
79
|
+
@cli.group(
|
|
80
|
+
"onecode",
|
|
81
|
+
invoke_without_command=True,
|
|
82
|
+
short_help="onecode CLI surface (config / codebase / skill / mcp / help)",
|
|
83
|
+
)
|
|
84
|
+
@click.pass_context
|
|
85
|
+
def onecode_group(ctx):
|
|
86
|
+
"""The onecode command surface.
|
|
87
|
+
|
|
88
|
+
onecode is the agent framework that powers the TUI. This group
|
|
89
|
+
exposes its full CLI: configuration, codebase indexing, skills,
|
|
90
|
+
MCP servers, and help.
|
|
91
|
+
|
|
92
|
+
\b
|
|
93
|
+
Sub-commands:
|
|
94
|
+
config Manage onecode configuration (mode, model, provider, log-level, list)
|
|
95
|
+
codebase Build, search, and inspect the onecode codebase index
|
|
96
|
+
skill Install, enable, disable, or remove onecode skills
|
|
97
|
+
mcp Configure MCP servers that onecode connects to at runtime
|
|
98
|
+
help Print the onecode help screen
|
|
99
|
+
version Print the onecode version string
|
|
100
|
+
|
|
101
|
+
\b
|
|
102
|
+
Examples:
|
|
103
|
+
cdh onecode config Open the onecode configuration editor
|
|
104
|
+
cdh onecode config model get Show the current default model
|
|
105
|
+
cdh onecode config list Dump the full YAML configuration
|
|
106
|
+
cdh onecode config provider set openai
|
|
107
|
+
cdh onecode codebase index Build the onecode codebase index
|
|
108
|
+
cdh onecode codebase search "auth flow"
|
|
109
|
+
cdh onecode skill list List installed onecode skills
|
|
110
|
+
cdh onecode skill add my-skill Scaffold a new onecode skill
|
|
111
|
+
cdh onecode mcp list List onecode MCP servers
|
|
112
|
+
cdh onecode mcp add my-server https://example.com/mcp
|
|
113
|
+
"""
|
|
114
|
+
if ctx.invoked_subcommand is None:
|
|
115
|
+
# No subcommand: show the onecode help screen (matches
|
|
116
|
+
# `onecode --help` so the user can browse what's available).
|
|
117
|
+
click.echo(onecode_cli.get_help(click.Context(onecode_cli)))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# Mount every onecode top-level command (codebase / skill / mcp /
|
|
121
|
+
# help / version) under `cdh onecode <sub>`. We deliberately skip
|
|
122
|
+
# `config` because the onecode CLI's bare `onecode config` only prints
|
|
123
|
+
# `--help`; the user-facing behaviour for `cdh onecode config` is to
|
|
124
|
+
# open the TUI editor, so we install our own config sub-group below.
|
|
125
|
+
_ONECODE_SKIP_AS_CDH_ONECODE = {"tui", "config"}
|
|
126
|
+
for _cmd_name in onecode_cli.commands:
|
|
127
|
+
if _cmd_name in _ONECODE_SKIP_AS_CDH_ONECODE:
|
|
128
|
+
continue
|
|
129
|
+
_cmd = onecode_cli.get_command(None, _cmd_name)
|
|
130
|
+
if _cmd is not None and _cmd_name not in onecode_group.commands:
|
|
131
|
+
onecode_group.add_command(_cmd)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# Override the onecode CLI's CDH-flavoured help text so users see
|
|
135
|
+
# onecode-centric descriptions when they run `cdh onecode --help` or
|
|
136
|
+
# `cdh help onecode`. The onecode CLI itself still ships its own copy
|
|
137
|
+
# (used by `onecode --help`); we don't mutate it.
|
|
138
|
+
|
|
139
|
+
_ONECODE_HELP_OVERRIDES: dict[str, tuple[str, str]] = {
|
|
140
|
+
"config": (
|
|
141
|
+
"Open the onecode configuration editor",
|
|
142
|
+
"Manage onecode configuration: agent mode, default model, LLM "
|
|
143
|
+
"provider, log level, skills, MCP servers. With no subcommand, "
|
|
144
|
+
"launches the interactive Textual editor.",
|
|
145
|
+
),
|
|
146
|
+
"codebase": (
|
|
147
|
+
"Manage the onecode codebase index",
|
|
148
|
+
"Build and query onecode's local codebase index. Used by the "
|
|
149
|
+
"agent to ground answers in the project's own source files. "
|
|
150
|
+
"Index stored at ~/.onecode/codebase/indexes/. "
|
|
151
|
+
"Sub-commands: index, reindex, status, search.",
|
|
152
|
+
),
|
|
153
|
+
"memory": (
|
|
154
|
+
"Manage onecode long-term memory",
|
|
155
|
+
"View and manage onecode's long-term conversation memory. "
|
|
156
|
+
"Memory stored at ~/.onecode/memory/ with BM25 recall. "
|
|
157
|
+
"Sub-commands: status, clear, count.",
|
|
158
|
+
),
|
|
159
|
+
"skill": (
|
|
160
|
+
"Manage onecode skills",
|
|
161
|
+
"Install, enable, disable, or remove onecode skills. Skills are "
|
|
162
|
+
"reusable instruction sets the agent loads on demand to extend "
|
|
163
|
+
"its behaviour. Sub-commands: list, add, remove, enable, disable.",
|
|
164
|
+
),
|
|
165
|
+
"mcp": (
|
|
166
|
+
"Manage onecode MCP servers",
|
|
167
|
+
"Configure Model Context Protocol servers that onecode connects "
|
|
168
|
+
"to at runtime to expose additional tools and resources. "
|
|
169
|
+
"Sub-commands: list, add, remove, enable, disable.",
|
|
170
|
+
),
|
|
171
|
+
"help": (
|
|
172
|
+
"Show onecode CLI help",
|
|
173
|
+
"Print the onecode CLI help screen with every available command.",
|
|
174
|
+
),
|
|
175
|
+
"version": (
|
|
176
|
+
"Show onecode version",
|
|
177
|
+
"Print the onecode version string.",
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
for _cmd_name, (_sh, _doc) in _ONECODE_HELP_OVERRIDES.items():
|
|
182
|
+
_mounted = onecode_group.commands.get(_cmd_name)
|
|
183
|
+
if _mounted is not None:
|
|
184
|
+
_mounted.short_help = _sh
|
|
185
|
+
_mounted.help = _doc
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# `cdh onecode config` — wrapper around `onecode config` that opens the
|
|
189
|
+
# interactive TUI editor when invoked with no subcommand, and forwards
|
|
190
|
+
# every subcommand (mode / model / provider / skill / mcp / log-level /
|
|
191
|
+
# list) verbatim to onecode.
|
|
192
|
+
|
|
193
|
+
@onecode_group.group(
|
|
194
|
+
"config",
|
|
195
|
+
invoke_without_command=True,
|
|
196
|
+
short_help="Open the onecode configuration editor (or run `cdh onecode config <sub>`)",
|
|
197
|
+
)
|
|
198
|
+
@click.pass_context
|
|
199
|
+
def onecode_config(ctx):
|
|
200
|
+
"""Open the onecode configuration editor.
|
|
201
|
+
|
|
202
|
+
With a subcommand, get or set the corresponding onecode setting
|
|
203
|
+
directly from the shell. With no subcommand, launch the interactive
|
|
204
|
+
Textual editor for all onecode settings.
|
|
205
|
+
|
|
206
|
+
\b
|
|
207
|
+
Sub-commands:
|
|
208
|
+
mode Get or set onecode's default agent mode (build / plan / solo)
|
|
209
|
+
model Get or set onecode's default LLM model
|
|
210
|
+
provider Get or set onecode's default LLM provider
|
|
211
|
+
log-level Get or set onecode's root logger verbosity
|
|
212
|
+
skill Manage onecode skills (alias for `cdh onecode skill`)
|
|
213
|
+
mcp Manage onecode MCP servers (alias for `cdh onecode mcp`)
|
|
214
|
+
list Dump the full onecode YAML configuration
|
|
215
|
+
|
|
216
|
+
\b
|
|
217
|
+
Examples:
|
|
218
|
+
cdh onecode config Open the interactive editor
|
|
219
|
+
cdh onecode config model get Show the current default model
|
|
220
|
+
cdh onecode config provider set openai
|
|
221
|
+
cdh onecode config list Dump the full YAML configuration
|
|
222
|
+
"""
|
|
223
|
+
if ctx.invoked_subcommand is None:
|
|
224
|
+
from onecode.config_screen import main as config_main
|
|
225
|
+
config_main()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
_onecode_config_group = onecode_cli.get_command(None, "config")
|
|
229
|
+
if _onecode_config_group is not None:
|
|
230
|
+
for _cfg_cmd in _onecode_config_group.commands.values():
|
|
231
|
+
if _cfg_cmd.name not in onecode_config.commands and _cfg_cmd.name != "tui":
|
|
232
|
+
onecode_config.add_command(_cfg_cmd)
|
|
233
|
+
# Override the sub-command help to be onecode-centric, not
|
|
234
|
+
# CDH-centric, since users reach these via
|
|
235
|
+
# `cdh onecode config <sub>`.
|
|
236
|
+
_sub_overrides = {
|
|
237
|
+
"mode": (
|
|
238
|
+
"Manage onecode agent mode",
|
|
239
|
+
"Get or set onecode's default agent mode "
|
|
240
|
+
"(build / plan / solo).",
|
|
241
|
+
),
|
|
242
|
+
"model": (
|
|
243
|
+
"Manage onecode's default LLM model",
|
|
244
|
+
"Get or set the default LLM model onecode uses when "
|
|
245
|
+
"starting a new agent session.",
|
|
246
|
+
),
|
|
247
|
+
"provider": (
|
|
248
|
+
"Manage onecode's default LLM provider",
|
|
249
|
+
"Get or set the default LLM provider onecode routes "
|
|
250
|
+
"chat-completions requests to.",
|
|
251
|
+
),
|
|
252
|
+
"log-level": (
|
|
253
|
+
"Manage onecode log level",
|
|
254
|
+
"Get or set onecode's root logger verbosity "
|
|
255
|
+
"(debug / info / warn / error).",
|
|
256
|
+
),
|
|
257
|
+
"skill": (
|
|
258
|
+
"Manage onecode skills (alias for `cdh onecode skill`)",
|
|
259
|
+
"Same skill-management surface as `cdh onecode skill`.",
|
|
260
|
+
),
|
|
261
|
+
"mcp": (
|
|
262
|
+
"Manage onecode MCP servers (alias for `cdh onecode mcp`)",
|
|
263
|
+
"Same MCP server surface as `cdh onecode mcp`.",
|
|
264
|
+
),
|
|
265
|
+
"list": (
|
|
266
|
+
"Show onecode's full YAML configuration",
|
|
267
|
+
"Print every onecode setting currently in effect, "
|
|
268
|
+
"as YAML.",
|
|
269
|
+
),
|
|
270
|
+
}
|
|
271
|
+
if _cfg_cmd.name in _sub_overrides:
|
|
272
|
+
_sh, _doc = _sub_overrides[_cfg_cmd.name]
|
|
273
|
+
_cfg_cmd.short_help = _sh
|
|
274
|
+
_cfg_cmd.help = _doc
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
# --- (removed) backward-compatible aliases ---
|
|
278
|
+
#
|
|
279
|
+
# Earlier versions exposed `cdh config` and `cdh codebase` at the top
|
|
280
|
+
# level. Those aliases were removed — use `cdh onecode config` and
|
|
281
|
+
# `cdh onecode codebase` instead. The behaviour is identical:
|
|
282
|
+
# - `cdh onecode config` (no subcommand) opens the same TUI editor.
|
|
283
|
+
# - `cdh onecode config <sub>` is the same as the old `cdh config <sub>`.
|
|
284
|
+
# - `cdh onecode codebase <sub>` is the same as the old `cdh codebase <sub>`.
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# --- aidlc group (project / phase / gate / sync / update) ---
|
|
288
|
+
|
|
289
|
+
def _get_projects():
|
|
290
|
+
projects_dir = _CDH_DIR / "projects"
|
|
291
|
+
projects_dir.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
return sorted(list(projects_dir.glob("*.yaml")) + list(projects_dir.glob("*.json")))
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _load_project_by_name(name: str) -> Optional[tuple]:
|
|
296
|
+
projects_dir = _CDH_DIR / "projects"
|
|
297
|
+
for ext in ["yaml", "yml", "json"]:
|
|
298
|
+
pf = projects_dir / f"{name}.{ext}"
|
|
299
|
+
if pf.exists():
|
|
300
|
+
import yaml
|
|
301
|
+
proj_data = yaml.safe_load(pf.read_text()) if ext in ["yaml", "yml"] else __import__("json").loads(pf.read_text())
|
|
302
|
+
return name, proj_data.get("path", ".")
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
def _parse_component_selection(text: str, valid_ids: list[str]) -> list[str]:
|
|
307
|
+
valid_ids = list(valid_ids)
|
|
308
|
+
text = text.strip().lower()
|
|
309
|
+
if not text:
|
|
310
|
+
return []
|
|
311
|
+
if text in ("all", "*"):
|
|
312
|
+
return valid_ids
|
|
313
|
+
tokens = [t.strip() for t in text.replace(" ", "").split(",") if t.strip()]
|
|
314
|
+
selected: list[str] = []
|
|
315
|
+
for tok in tokens:
|
|
316
|
+
if tok in valid_ids:
|
|
317
|
+
if tok not in selected:
|
|
318
|
+
selected.append(tok)
|
|
319
|
+
continue
|
|
320
|
+
if "-" in tok:
|
|
321
|
+
try:
|
|
322
|
+
a, b = tok.split("-", 1)
|
|
323
|
+
lo, hi = int(a), int(b)
|
|
324
|
+
except ValueError:
|
|
325
|
+
continue
|
|
326
|
+
if lo > hi:
|
|
327
|
+
lo, hi = hi, lo
|
|
328
|
+
for i in range(lo, hi + 1):
|
|
329
|
+
if 1 <= i <= len(valid_ids) and valid_ids[i - 1] not in selected:
|
|
330
|
+
selected.append(valid_ids[i - 1])
|
|
331
|
+
else:
|
|
332
|
+
try:
|
|
333
|
+
idx = int(tok)
|
|
334
|
+
except ValueError:
|
|
335
|
+
continue
|
|
336
|
+
if 1 <= idx <= len(valid_ids) and valid_ids[idx - 1] not in selected:
|
|
337
|
+
selected.append(valid_ids[idx - 1])
|
|
338
|
+
return selected
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _prompt_components(
|
|
342
|
+
spec_label: str,
|
|
343
|
+
specs: tuple,
|
|
344
|
+
allow_empty: bool = False,
|
|
345
|
+
) -> list[str]:
|
|
346
|
+
valid_ids = [s.id for s in specs]
|
|
347
|
+
click.echo(f"Select {spec_label} for the project:")
|
|
348
|
+
for i, s in enumerate(specs, 1):
|
|
349
|
+
click.echo(f" {i}) {s.label:<22s} \u2014 {s.description}")
|
|
350
|
+
if allow_empty:
|
|
351
|
+
hint = "[optional, e.g. 1,2 or 'all' or <Enter> to skip]"
|
|
352
|
+
else:
|
|
353
|
+
hint = "[required, e.g. 1,2 or 'all']"
|
|
354
|
+
click.echo("")
|
|
355
|
+
prompt_text = f"{spec_label.capitalize()} {hint}: "
|
|
356
|
+
|
|
357
|
+
for attempt in range(2):
|
|
358
|
+
raw = click.prompt(prompt_text, default="", show_default=False)
|
|
359
|
+
selected = _parse_component_selection(raw, valid_ids)
|
|
360
|
+
if selected:
|
|
361
|
+
return selected
|
|
362
|
+
if allow_empty and not raw.strip():
|
|
363
|
+
return []
|
|
364
|
+
if attempt == 0:
|
|
365
|
+
if allow_empty:
|
|
366
|
+
click.echo(
|
|
367
|
+
"No valid selection parsed. Try again (or press Enter to skip)."
|
|
368
|
+
)
|
|
369
|
+
else:
|
|
370
|
+
click.echo(
|
|
371
|
+
"At least one selection is required. Try again."
|
|
372
|
+
)
|
|
373
|
+
raise click.Abort()
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _resolve_components_flag(components: Optional[str]) -> list[str]:
|
|
377
|
+
if not components:
|
|
378
|
+
return []
|
|
379
|
+
valid_ids = [c.id for c in COMPONENTS]
|
|
380
|
+
selected = _parse_component_selection(components, valid_ids)
|
|
381
|
+
if not selected:
|
|
382
|
+
raise click.BadParameter(
|
|
383
|
+
f"No valid component ids in '{components}'. "
|
|
384
|
+
f"Valid ids: {', '.join(valid_ids)}."
|
|
385
|
+
)
|
|
386
|
+
return selected
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@cli.group("aidlc", short_help="AIDLC project management", invoke_without_command=True)
|
|
390
|
+
@click.pass_context
|
|
391
|
+
def aidlc(ctx):
|
|
392
|
+
"""Manage AIDLC projects.
|
|
393
|
+
|
|
394
|
+
\b
|
|
395
|
+
Sub-commands:
|
|
396
|
+
project list|new|init|check|load Project management
|
|
397
|
+
phase <phase> Set the AI-DLC phase
|
|
398
|
+
gate <name> --status <passed|failed> Record a quality gate result
|
|
399
|
+
validate Validate spec quality (EARS/FR/BDD/DAG)
|
|
400
|
+
generators list|search|info|install|init|validate Manage code-generator plugins
|
|
401
|
+
tools install|status|update Manage AIDLC tools (generate_shared/contract_diff/deploy_stack)
|
|
402
|
+
status Show project health overview
|
|
403
|
+
dashboard [--watch] [--export PATH] Render the AIDLC dashboard TUI
|
|
404
|
+
config show|component|provider Manage project configuration
|
|
405
|
+
sync Regenerate AGENTS.md and CLAUDE.md
|
|
406
|
+
update Alias for sync (deprecated)
|
|
407
|
+
"""
|
|
408
|
+
if ctx.invoked_subcommand is None:
|
|
409
|
+
click.echo(ctx.get_help())
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
@aidlc.command("dashboard")
|
|
413
|
+
@click.argument("path", required=False, default=".")
|
|
414
|
+
@click.option("--watch", "-w", is_flag=True, default=False, help="Refresh every --interval seconds (Ctrl-C to stop).")
|
|
415
|
+
@click.option("--interval", "-n", default=2.0, type=float, show_default=True, help="Refresh interval in seconds (--watch).")
|
|
416
|
+
@click.option("--export", "export_path", default=None, type=click.Path(), help="Write a one-shot snapshot to PATH (.md or .html) instead of TUI.")
|
|
417
|
+
def dashboard_cmd(path, watch, interval, export_path):
|
|
418
|
+
"""Render the AIDLC dashboard TUI.
|
|
419
|
+
|
|
420
|
+
Auto-detects the .cdh/ directory by walking up from PATH (default: cwd)
|
|
421
|
+
and shows six widgets in a 2x3 grid:
|
|
422
|
+
|
|
423
|
+
* Phase Progress - current AI-DLC phase + progress bar
|
|
424
|
+
* Quality Gates - pass/fail/warn icons for recorded gates
|
|
425
|
+
* Spec Quality - EARS/FR/BDD/DAG check pass rates
|
|
426
|
+
* AIDLC Tools - installed vs stub tools
|
|
427
|
+
* FR Coverage - spec FR -> BDD coverage percentage
|
|
428
|
+
* Deployment - preview/staging/production env status
|
|
429
|
+
|
|
430
|
+
Use --watch for live refresh, or --export PATH to save a snapshot
|
|
431
|
+
(Markdown by default; .html for HTML).
|
|
432
|
+
|
|
433
|
+
\b
|
|
434
|
+
Examples:
|
|
435
|
+
cdh aidlc dashboard One-shot render
|
|
436
|
+
cdh aidlc dashboard --watch Live refresh every 2s
|
|
437
|
+
cdh aidlc dashboard --watch --interval 5 Live refresh every 5s
|
|
438
|
+
cdh aidlc dashboard --export report.md Save Markdown snapshot
|
|
439
|
+
cdh aidlc dashboard --export report.html Save HTML snapshot
|
|
440
|
+
"""
|
|
441
|
+
from cdh.tui import run_dashboard
|
|
442
|
+
|
|
443
|
+
target = Path(path).expanduser().resolve()
|
|
444
|
+
export = Path(export_path).expanduser().resolve() if export_path else None
|
|
445
|
+
setup_logging("WARNING")
|
|
446
|
+
rc = run_dashboard(
|
|
447
|
+
target,
|
|
448
|
+
watch=watch,
|
|
449
|
+
interval=interval,
|
|
450
|
+
export_path=export,
|
|
451
|
+
)
|
|
452
|
+
if rc:
|
|
453
|
+
raise click.Abort()
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@aidlc.group("project", short_help="Project management (list/new/init/check/load)", invoke_without_command=True)
|
|
457
|
+
@click.pass_context
|
|
458
|
+
def project(ctx):
|
|
459
|
+
"""Manage AIDLC projects.
|
|
460
|
+
|
|
461
|
+
Without subcommand, opens the project management TUI.
|
|
462
|
+
|
|
463
|
+
\b
|
|
464
|
+
Sub-commands:
|
|
465
|
+
list List all registered projects
|
|
466
|
+
new <name> [path] Create a new project
|
|
467
|
+
init [path] Initialize .cdh in an existing directory
|
|
468
|
+
check [path] Check whether a directory is a valid AIDLC project
|
|
469
|
+
load <name> Load a registered project as the current project
|
|
470
|
+
"""
|
|
471
|
+
if ctx.invoked_subcommand is None:
|
|
472
|
+
from tui.screens.projects_app import main as projects_main
|
|
473
|
+
projects_main()
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
@project.command("list")
|
|
477
|
+
def project_list():
|
|
478
|
+
"""List all registered AIDLC projects."""
|
|
479
|
+
projects_dir = _CDH_DIR / "projects"
|
|
480
|
+
projects_dir.mkdir(parents=True, exist_ok=True)
|
|
481
|
+
project_files = sorted(list(projects_dir.glob("*.yaml")) + list(projects_dir.glob("*.json")))
|
|
482
|
+
if not project_files:
|
|
483
|
+
click.echo("No projects found.")
|
|
484
|
+
return
|
|
485
|
+
click.echo("Projects:")
|
|
486
|
+
for pf in project_files:
|
|
487
|
+
click.echo(f" {pf.stem}")
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
@project.command("new")
|
|
491
|
+
@click.argument("name", required=False)
|
|
492
|
+
@click.argument("path", required=False, default=".")
|
|
493
|
+
@click.option("--components", default=None, help="Comma-separated component ids (e.g. 'web,backend') or 'all'. Skips the interactive prompt.")
|
|
494
|
+
@click.option("--with-ci", is_flag=True, default=False, help="Generate CI templates (GitHub Actions + pre-commit)")
|
|
495
|
+
@click.option("--with-tests", is_flag=True, default=False, help="Generate test templates (conftest.py, pyproject.toml)")
|
|
496
|
+
@click.option("--with-local", is_flag=True, default=False, help="Generate local dev templates (docker-compose.yaml, .env.local)")
|
|
497
|
+
@click.option("--provider", default=None, help="Cloud provider: tcb (default) or aliyun")
|
|
498
|
+
def project_new(name, path, components, with_ci, with_tests, with_local, provider):
|
|
499
|
+
"""Create a new AIDLC project with interactive component selection."""
|
|
500
|
+
import yaml
|
|
501
|
+
from cdh.project_loader import CdhProjectLoader
|
|
502
|
+
projects_dir = _CDH_DIR / "projects"
|
|
503
|
+
projects_dir.mkdir(parents=True, exist_ok=True)
|
|
504
|
+
if not name:
|
|
505
|
+
name = click.prompt("Project name", default="").strip()
|
|
506
|
+
if not name:
|
|
507
|
+
click.echo("Project name is required.")
|
|
508
|
+
raise click.Abort()
|
|
509
|
+
ws = Path(path).expanduser().resolve()
|
|
510
|
+
project_file = projects_dir / f"{name}.yaml"
|
|
511
|
+
if project_file.exists():
|
|
512
|
+
click.echo(f"Error: project '{name}' already exists in the project list.")
|
|
513
|
+
raise click.Abort()
|
|
514
|
+
if components is not None:
|
|
515
|
+
selected_components = _resolve_components_flag(components)
|
|
516
|
+
else:
|
|
517
|
+
selected_components = _prompt_components(
|
|
518
|
+
"application components", COMPONENTS, allow_empty=False
|
|
519
|
+
)
|
|
520
|
+
try:
|
|
521
|
+
scaffold_dlc_project(
|
|
522
|
+
ws, name, components=selected_components,
|
|
523
|
+
with_ci=with_ci, with_tests=with_tests,
|
|
524
|
+
with_local=with_local, provider=provider,
|
|
525
|
+
)
|
|
526
|
+
except (ValueError, RuntimeError) as e:
|
|
527
|
+
click.echo(f"Error: {str(e) or type(e).__name__}")
|
|
528
|
+
raise click.Abort()
|
|
529
|
+
CdhProjectLoader.init_project(ws, name)
|
|
530
|
+
proj_data = {"name": name, "path": str(ws), "description": ""}
|
|
531
|
+
project_file.write_text(yaml.dump(proj_data))
|
|
532
|
+
write_active_project(name, str(ws))
|
|
533
|
+
click.echo(
|
|
534
|
+
f"Created AIDLC project '{name}' at {ws} "
|
|
535
|
+
f"(components: {', '.join(selected_components)})"
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@project.command("init")
|
|
540
|
+
@click.argument("path", required=False, default=".")
|
|
541
|
+
@click.option("--components", default=None, help="Comma-separated component ids (e.g. 'web,backend') or 'all'. Skips the interactive prompt.")
|
|
542
|
+
@click.option("--with-ci", is_flag=True, default=False, help="Generate CI templates (GitHub Actions + pre-commit)")
|
|
543
|
+
@click.option("--with-tests", is_flag=True, default=False, help="Generate test templates (conftest.py, pyproject.toml)")
|
|
544
|
+
@click.option("--with-local", is_flag=True, default=False, help="Generate local dev templates (docker-compose.yaml, .env.local)")
|
|
545
|
+
@click.option("--provider", default=None, help="Cloud provider: tcb (default) or aliyun")
|
|
546
|
+
def project_init(path, components, with_ci, with_tests, with_local, provider):
|
|
547
|
+
"""Initialize .cdh in an existing directory as an AIDLC project."""
|
|
548
|
+
import yaml
|
|
549
|
+
from cdh.project_loader import CdhProjectLoader
|
|
550
|
+
from cdh.scaffold import _scaffold_ci_templates, _scaffold_test_templates, _scaffold_local_env, _scaffold_provider_templates
|
|
551
|
+
projects_dir = _CDH_DIR / "projects"
|
|
552
|
+
projects_dir.mkdir(parents=True, exist_ok=True)
|
|
553
|
+
target = Path(path).expanduser().resolve()
|
|
554
|
+
project_name = target.name
|
|
555
|
+
proj_file = projects_dir / f"{project_name}.yaml"
|
|
556
|
+
if proj_file.exists():
|
|
557
|
+
click.echo(f"Error: project '{project_name}' already exists in the project list.")
|
|
558
|
+
raise click.Abort()
|
|
559
|
+
if components is not None:
|
|
560
|
+
selected_components = _resolve_components_flag(components)
|
|
561
|
+
else:
|
|
562
|
+
selected_components = _prompt_components(
|
|
563
|
+
"application components", COMPONENTS, allow_empty=True
|
|
564
|
+
)
|
|
565
|
+
try:
|
|
566
|
+
init_dlc_project(target, project_name, with_ci=with_ci, with_tests=with_tests, with_local=with_local, provider=provider)
|
|
567
|
+
except (ValueError, RuntimeError) as e:
|
|
568
|
+
click.echo(f"Error: {str(e) or type(e).__name__}")
|
|
569
|
+
raise click.Abort()
|
|
570
|
+
for cid in selected_components:
|
|
571
|
+
try:
|
|
572
|
+
add_component(target, cid)
|
|
573
|
+
except (ValueError, FileNotFoundError) as e:
|
|
574
|
+
click.echo(f"Error: {str(e) or type(e).__name__}")
|
|
575
|
+
CdhProjectLoader.init_project(target, project_name)
|
|
576
|
+
proj_data = {"name": project_name, "path": str(target), "description": ""}
|
|
577
|
+
proj_file.write_text(yaml.dump(proj_data))
|
|
578
|
+
suffix = (
|
|
579
|
+
f" (components: {', '.join(selected_components)})"
|
|
580
|
+
if selected_components
|
|
581
|
+
else ""
|
|
582
|
+
)
|
|
583
|
+
click.echo(f"Initialized AIDLC project in {target}{suffix}")
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
@project.command("check")
|
|
587
|
+
@click.argument("path", required=False, default=".")
|
|
588
|
+
def project_check(path):
|
|
589
|
+
"""Check whether a directory is a valid AIDLC project."""
|
|
590
|
+
from cdh.scaffold import check_dlc_project
|
|
591
|
+
target = Path(path).expanduser().resolve()
|
|
592
|
+
result = check_dlc_project(target)
|
|
593
|
+
if result["valid"]:
|
|
594
|
+
click.echo(f"\u2713 Valid AIDLC project: {result['name']}")
|
|
595
|
+
click.echo(f" Location: {result['path']}")
|
|
596
|
+
if result["components"]:
|
|
597
|
+
click.echo(f" Components: {', '.join(result['components'])}")
|
|
598
|
+
else:
|
|
599
|
+
click.echo(" Components: (none)")
|
|
600
|
+
click.echo(
|
|
601
|
+
f" CDH state: \u2713 initialized" if result["has_cdh"]
|
|
602
|
+
else " CDH state: \u2717 not initialized"
|
|
603
|
+
)
|
|
604
|
+
else:
|
|
605
|
+
click.echo(f"\u2717 Not a valid AIDLC project: {target}")
|
|
606
|
+
for s in result["suggestions"]:
|
|
607
|
+
click.echo(f" \u2022 {s}")
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
@project.command("load")
|
|
611
|
+
@click.argument("name", required=False)
|
|
612
|
+
def project_load(name):
|
|
613
|
+
"""Load a registered AIDLC project as the current project."""
|
|
614
|
+
import yaml
|
|
615
|
+
projects_dir = _CDH_DIR / "projects"
|
|
616
|
+
projects_dir.mkdir(parents=True, exist_ok=True)
|
|
617
|
+
if not name:
|
|
618
|
+
click.echo("Usage: cdh aidlc project load <name>")
|
|
619
|
+
return
|
|
620
|
+
for ext in ["yaml", "yml", "json"]:
|
|
621
|
+
pf = projects_dir / f"{name}.{ext}"
|
|
622
|
+
if pf.exists():
|
|
623
|
+
proj_data = yaml.safe_load(pf.read_text()) if ext in ["yaml", "yml"] else __import__("json").loads(pf.read_text())
|
|
624
|
+
project_path = proj_data.get("path", ".")
|
|
625
|
+
write_active_project(name, project_path)
|
|
626
|
+
click.echo(f"Loaded project '{name}' (path: {project_path})")
|
|
627
|
+
return
|
|
628
|
+
click.echo(f"Project '{name}' not found.")
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
@aidlc.command("phase")
|
|
632
|
+
@click.argument("phase", required=False)
|
|
633
|
+
@click.argument("path", required=False, default=".")
|
|
634
|
+
def phase_cmd(phase, path):
|
|
635
|
+
"""Set the current AI-DLC phase (init|understand|plan|verify|deliver)."""
|
|
636
|
+
from cdh.project_loader import CdhProjectLoader
|
|
637
|
+
valid = {"init", "understand", "plan", "verify", "deliver"}
|
|
638
|
+
if not phase or phase not in valid:
|
|
639
|
+
click.echo(f"Usage: cdh aidlc phase <{'|'.join(sorted(valid))}>")
|
|
640
|
+
raise click.Abort()
|
|
641
|
+
target = Path(path).expanduser().resolve()
|
|
642
|
+
ok = CdhProjectLoader.advance_phase(target, phase)
|
|
643
|
+
if ok:
|
|
644
|
+
click.echo(f"Phase set to: {phase}")
|
|
645
|
+
else:
|
|
646
|
+
click.echo(
|
|
647
|
+
"Error: Invalid phase transition. "
|
|
648
|
+
"Phases must advance one step at a time "
|
|
649
|
+
"(init\u2192understand\u2192plan\u2192verify\u2192deliver). "
|
|
650
|
+
"Use 'init' to reset."
|
|
651
|
+
)
|
|
652
|
+
raise click.Abort()
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
@aidlc.command("gate")
|
|
656
|
+
@click.argument("name", required=False)
|
|
657
|
+
@click.argument("path", required=False, default=".")
|
|
658
|
+
@click.option("--status", required=True, help="Gate status: passed or failed")
|
|
659
|
+
def gate_cmd(name, path, status):
|
|
660
|
+
"""Record a quality gate result (requires --status passed|failed)."""
|
|
661
|
+
from cdh.project_loader import CdhProjectLoader
|
|
662
|
+
if not name or status not in ("passed", "failed"):
|
|
663
|
+
click.echo("Usage: cdh aidlc gate <name> --status <passed|failed>")
|
|
664
|
+
raise click.Abort()
|
|
665
|
+
target = Path(path).expanduser().resolve()
|
|
666
|
+
ok = CdhProjectLoader.record_gate_result(target, name, status)
|
|
667
|
+
if ok:
|
|
668
|
+
click.echo(f"Gate '{name}': {status}")
|
|
669
|
+
else:
|
|
670
|
+
click.echo("No .cdh/ directory found. Run 'cdh aidlc init' first.")
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
@aidlc.command("sync")
|
|
674
|
+
@click.argument("path", required=False, default=".")
|
|
675
|
+
def sync_cmd(path):
|
|
676
|
+
"""Regenerate AGENTS.md and CLAUDE.md from aidlc/project.yaml."""
|
|
677
|
+
from cdh.scaffold import _regenerate_agents_and_claude_md, scaffold_dlc_project
|
|
678
|
+
from cdh.project_loader import CdhProjectLoader
|
|
679
|
+
target = Path(path).expanduser().resolve()
|
|
680
|
+
project_yaml = target / "aidlc" / "project.yaml"
|
|
681
|
+
if project_yaml.exists():
|
|
682
|
+
_regenerate_agents_and_claude_md(target)
|
|
683
|
+
click.echo("Regenerated AGENTS.md and CLAUDE.md")
|
|
684
|
+
else:
|
|
685
|
+
project_name = target.name
|
|
686
|
+
scaffold_dlc_project(target, project_name)
|
|
687
|
+
CdhProjectLoader.init_project(target, project_name)
|
|
688
|
+
click.echo("Initialized AIDLC project and regenerated AGENTS.md/CLAUDE.md")
|
|
689
|
+
|
|
690
|
+
|
|
691
|
+
@aidlc.command("update")
|
|
692
|
+
@click.argument("path", required=False, default=".")
|
|
693
|
+
@click.pass_context
|
|
694
|
+
def update_cmd(ctx, path):
|
|
695
|
+
"""Alias for sync (deprecated)."""
|
|
696
|
+
ctx.invoke(sync_cmd, path=path)
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
# ── validate ─────────────────────────────────────────────────
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
@aidlc.group("validate", short_help="Validate AIDLC spec quality", invoke_without_command=True)
|
|
703
|
+
@click.pass_context
|
|
704
|
+
def validate_group(ctx):
|
|
705
|
+
"""Validate AIDLC spec quality: EARS, FR, BDD, DAG.
|
|
706
|
+
|
|
707
|
+
\b
|
|
708
|
+
Sub-commands:
|
|
709
|
+
run Run all quality checks (default)
|
|
710
|
+
history Show recent validate runs
|
|
711
|
+
metrics Show aggregated validate metrics
|
|
712
|
+
"""
|
|
713
|
+
if ctx.invoked_subcommand is None:
|
|
714
|
+
# Default: run all checks with default options
|
|
715
|
+
ctx.invoke(validate_cmd, path=".", ears_only=False, fr_only=False,
|
|
716
|
+
bdd_only=False, dag_only=False, all_checks=True,
|
|
717
|
+
output_format="text", record=True, validate_state=True)
|
|
718
|
+
|
|
719
|
+
|
|
720
|
+
@validate_group.command("run")
|
|
721
|
+
@click.argument("path", required=False, default=".")
|
|
722
|
+
@click.option("--ears", "ears_only", is_flag=True, help="EARS format check only")
|
|
723
|
+
@click.option("--fr", "fr_only", is_flag=True, help="FR namespace consistency check only")
|
|
724
|
+
@click.option("--bdd", "bdd_only", is_flag=True, help="BDD scenario coverage check only")
|
|
725
|
+
@click.option("--dag", "dag_only", is_flag=True, help="Task DAG cycle check only")
|
|
726
|
+
@click.option("--all", "all_checks", is_flag=True, default=False, help="Run all checks (default)")
|
|
727
|
+
@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text", help="Output format (default: text)")
|
|
728
|
+
@click.option("--record/--no-record", "record", default=True, help="Persist run history & metrics (default: record)")
|
|
729
|
+
@click.option("--validate-state", is_flag=True, help="Validate .cdh/state.json schema")
|
|
730
|
+
@click.pass_context
|
|
731
|
+
def validate_cmd(ctx, path, ears_only, fr_only, bdd_only, dag_only, all_checks, output_format, record, validate_state):
|
|
732
|
+
"""Run all AIDLC spec quality checks."""
|
|
733
|
+
_validate_impl(ctx, path, ears_only, fr_only, bdd_only, dag_only, all_checks, output_format, record, validate_state)
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
@validate_group.command("history")
|
|
737
|
+
@click.argument("path", required=False, default=".")
|
|
738
|
+
@click.option("--last", "n", default=10, show_default=True, help="Show last N runs")
|
|
739
|
+
def validate_history_cmd(path, n):
|
|
740
|
+
"""Show recent validate runs recorded in .cdh/validate_history.json."""
|
|
741
|
+
from cdh.project_loader import CdhProjectLoader
|
|
742
|
+
|
|
743
|
+
target = Path(path).expanduser().resolve()
|
|
744
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(target)
|
|
745
|
+
if cdh_dir is None:
|
|
746
|
+
click.echo("No .cdh/ directory found.")
|
|
747
|
+
return
|
|
748
|
+
|
|
749
|
+
history = CdhProjectLoader.get_validate_history(cdh_dir)
|
|
750
|
+
if not history:
|
|
751
|
+
click.echo("No validate history recorded yet. Run 'cdh aidlc validate' to populate.")
|
|
752
|
+
return
|
|
753
|
+
|
|
754
|
+
recent = history[-n:]
|
|
755
|
+
click.echo(f"Last {len(recent)} of {len(history)} runs:\n")
|
|
756
|
+
click.echo(f" {'TIMESTAMP':<22} {'PASS':<6} {'DUR(ms)':<8} CHECKS")
|
|
757
|
+
click.echo(f" {'─' * 22} {'─' * 6} {'─' * 8} {'─' * 30}")
|
|
758
|
+
for entry in recent:
|
|
759
|
+
ts = entry.get("timestamp", "?")
|
|
760
|
+
passed = "✓" if entry.get("passed") else "✗"
|
|
761
|
+
dur = entry.get("duration_ms", 0)
|
|
762
|
+
checks = ", ".join(entry.get("checks_run", [])) or "-"
|
|
763
|
+
failed = entry.get("failed_checks", [])
|
|
764
|
+
if failed:
|
|
765
|
+
checks = f"{checks} (failed: {', '.join(failed)})"
|
|
766
|
+
click.echo(f" {ts:<22} {passed:<6} {dur:<8} {checks}")
|
|
767
|
+
|
|
768
|
+
|
|
769
|
+
@validate_group.command("metrics")
|
|
770
|
+
@click.argument("path", required=False, default=".")
|
|
771
|
+
def validate_metrics_cmd(path):
|
|
772
|
+
"""Show aggregated metrics across all recorded validate runs."""
|
|
773
|
+
from cdh.project_loader import CdhProjectLoader
|
|
774
|
+
|
|
775
|
+
target = Path(path).expanduser().resolve()
|
|
776
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(target)
|
|
777
|
+
if cdh_dir is None:
|
|
778
|
+
click.echo("No .cdh/ directory found.")
|
|
779
|
+
return
|
|
780
|
+
|
|
781
|
+
metrics = CdhProjectLoader.get_metrics(cdh_dir)
|
|
782
|
+
if not metrics:
|
|
783
|
+
click.echo("No metrics recorded yet. Run 'cdh aidlc validate' to populate.")
|
|
784
|
+
return
|
|
785
|
+
|
|
786
|
+
click.echo("Validate Metrics:\n")
|
|
787
|
+
click.echo(f" Total runs: {metrics.get('total_runs', 0)}")
|
|
788
|
+
click.echo(f" Avg duration (ms): {metrics.get('average_duration_ms', 0)}")
|
|
789
|
+
ts = metrics.get("last_run_timestamp", "?")
|
|
790
|
+
click.echo(f" Last run: {ts}")
|
|
791
|
+
|
|
792
|
+
per_check = metrics.get("per_check", {})
|
|
793
|
+
if per_check:
|
|
794
|
+
click.echo("\n Per-check pass/fail:")
|
|
795
|
+
click.echo(f" {'CHECK':<30} {'RUNS':<6} {'PASS':<6} {'FAIL':<6}")
|
|
796
|
+
for name, slot in per_check.items():
|
|
797
|
+
click.echo(
|
|
798
|
+
f" {name:<30} {slot.get('runs', 0):<6} "
|
|
799
|
+
f"{slot.get('passes', 0):<6} {slot.get('fails', 0):<6}"
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
mttd = metrics.get("mttd_per_check", {})
|
|
803
|
+
if mttd:
|
|
804
|
+
click.echo("\n MTTD per check (ms-to-first-failure):")
|
|
805
|
+
for name, slot in mttd.items():
|
|
806
|
+
click.echo(
|
|
807
|
+
f" {name:<30} mttd={slot.get('mttd_ms', '?')}ms "
|
|
808
|
+
f"(samples={slot.get('samples', 0)})"
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
|
|
812
|
+
def _validate_impl(ctx, path, ears_only, fr_only, bdd_only, dag_only, all_checks, output_format, record, validate_state=False):
|
|
813
|
+
"""Run all AIDLC spec quality checks.
|
|
814
|
+
|
|
815
|
+
Helper used by both `cdh aidlc validate` and `cdh aidlc validate run`.
|
|
816
|
+
"""
|
|
817
|
+
import json as json_module
|
|
818
|
+
import time
|
|
819
|
+
from datetime import datetime, timezone
|
|
820
|
+
from cdh.project_loader import CdhProjectLoader
|
|
821
|
+
|
|
822
|
+
target = Path(path).expanduser().resolve()
|
|
823
|
+
|
|
824
|
+
selected = []
|
|
825
|
+
if ears_only:
|
|
826
|
+
selected = ["ears"]
|
|
827
|
+
elif fr_only:
|
|
828
|
+
selected = ["fr"]
|
|
829
|
+
elif bdd_only:
|
|
830
|
+
selected = ["bdd"]
|
|
831
|
+
elif dag_only:
|
|
832
|
+
selected = ["dag"]
|
|
833
|
+
else:
|
|
834
|
+
selected = ["ears", "fr", "bdd", "dag"]
|
|
835
|
+
|
|
836
|
+
runners = {
|
|
837
|
+
"ears": ("EARS Format", run_ears_check),
|
|
838
|
+
"fr": ("FR Namespace", run_fr_check),
|
|
839
|
+
"bdd": ("BDD Coverage", run_bdd_check),
|
|
840
|
+
"dag": ("Task DAG", run_dag_check),
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
started_at = time.monotonic()
|
|
844
|
+
started_iso = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
845
|
+
|
|
846
|
+
# OTLP span instrumentation — wraps the whole validate run in a parent span
|
|
847
|
+
# and each individual check in its own child span. Works with or without the
|
|
848
|
+
# opentelemetry-api package; see cdh.trace.otel_tracer.
|
|
849
|
+
from cdh.trace.otel_tracer import init_tracer, span as otel_span
|
|
850
|
+
|
|
851
|
+
init_tracer(service_name="cdh-validate")
|
|
852
|
+
|
|
853
|
+
all_passed = True
|
|
854
|
+
results = {}
|
|
855
|
+
run_span_attrs = {
|
|
856
|
+
"cdh.path": str(target),
|
|
857
|
+
"cdh.checks_requested": ",".join(selected),
|
|
858
|
+
"cdh.output_format": output_format,
|
|
859
|
+
}
|
|
860
|
+
with otel_span("aidlc.validate", attributes=run_span_attrs) as run_span:
|
|
861
|
+
for key in selected:
|
|
862
|
+
label, runner = runners[key]
|
|
863
|
+
check_started = time.monotonic()
|
|
864
|
+
with otel_span(
|
|
865
|
+
f"aidlc.validate.{key}",
|
|
866
|
+
attributes={"cdh.check.name": label, "cdh.check.kind": key},
|
|
867
|
+
parent=run_span,
|
|
868
|
+
) as check_span:
|
|
869
|
+
try:
|
|
870
|
+
result = runner(target)
|
|
871
|
+
except Exception as exc:
|
|
872
|
+
check_span.record_exception(exc)
|
|
873
|
+
check_span.set_status("ERROR", str(exc))
|
|
874
|
+
raise
|
|
875
|
+
results[key] = result
|
|
876
|
+
check_ms = int((time.monotonic() - check_started) * 1000)
|
|
877
|
+
check_span.set_attribute("cdh.check.passed", bool(result["passed"]))
|
|
878
|
+
check_span.set_attribute("cdh.check.duration_ms", check_ms)
|
|
879
|
+
sub_checks = result.get("checks", [])
|
|
880
|
+
check_span.set_attribute("cdh.check.sub_count", len(sub_checks))
|
|
881
|
+
failed_sub = [
|
|
882
|
+
c.get("name", "?") for c in sub_checks if c.get("status") == "fail"
|
|
883
|
+
]
|
|
884
|
+
if failed_sub:
|
|
885
|
+
check_span.set_attribute(
|
|
886
|
+
"cdh.check.failed_subs", ",".join(failed_sub)
|
|
887
|
+
)
|
|
888
|
+
fr_count = sum(
|
|
889
|
+
int(c.get("fr_count", 0))
|
|
890
|
+
for c in sub_checks
|
|
891
|
+
if isinstance(c.get("fr_count"), int)
|
|
892
|
+
)
|
|
893
|
+
if fr_count:
|
|
894
|
+
check_span.set_attribute("cdh.check.fr_count", fr_count)
|
|
895
|
+
bdd_count = sum(
|
|
896
|
+
int(c.get("scenario_count", 0))
|
|
897
|
+
for c in sub_checks
|
|
898
|
+
if isinstance(c.get("scenario_count"), int)
|
|
899
|
+
)
|
|
900
|
+
if bdd_count:
|
|
901
|
+
check_span.set_attribute(
|
|
902
|
+
"cdh.check.bdd_scenario_count", bdd_count
|
|
903
|
+
)
|
|
904
|
+
check_span.set_status("OK" if result["passed"] else "ERROR")
|
|
905
|
+
|
|
906
|
+
if not result["passed"]:
|
|
907
|
+
all_passed = False
|
|
908
|
+
|
|
909
|
+
if output_format == "json":
|
|
910
|
+
continue
|
|
911
|
+
|
|
912
|
+
click.echo(f"\n── {label} ─{'─' * max(0, 48 - len(label))}")
|
|
913
|
+
click.echo(f" Result: {'✓ PASS' if result['passed'] else '✗ FAIL'}")
|
|
914
|
+
|
|
915
|
+
for check in result.get("checks", []):
|
|
916
|
+
icon = {"pass": "✓", "fail": "✗", "warn": "!"}.get(
|
|
917
|
+
check["status"], "?"
|
|
918
|
+
)
|
|
919
|
+
click.echo(f" {icon} {check['name']}: {check['message']}")
|
|
920
|
+
|
|
921
|
+
if not result["passed"]:
|
|
922
|
+
all_passed = False
|
|
923
|
+
|
|
924
|
+
run_span.set_attribute("cdh.validate.duration_ms", int((time.monotonic() - started_at) * 1000))
|
|
925
|
+
run_span.set_attribute("cdh.validate.passed", all_passed)
|
|
926
|
+
run_span.set_attribute(
|
|
927
|
+
"cdh.validate.checks_run",
|
|
928
|
+
sum(len(results[k].get("checks", [])) for k in selected),
|
|
929
|
+
)
|
|
930
|
+
run_span.set_status("OK" if all_passed else "ERROR")
|
|
931
|
+
|
|
932
|
+
if validate_state:
|
|
933
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(target)
|
|
934
|
+
state_valid, state_errors = (False, ["No .cdh/ directory found."]) if cdh_dir is None else CdhProjectLoader.validate_state_schema(cdh_dir)
|
|
935
|
+
if output_format != "json":
|
|
936
|
+
click.echo(f"\n── State Schema ─────────────────────────────────")
|
|
937
|
+
click.echo(f" Result: {'✓ PASS' if state_valid else '✗ FAIL'}")
|
|
938
|
+
for error in state_errors:
|
|
939
|
+
click.echo(f" ✗ {error}")
|
|
940
|
+
if not state_valid:
|
|
941
|
+
all_passed = False
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
failed_checks: list[str] = []
|
|
945
|
+
checks_run: list[str] = []
|
|
946
|
+
duration_ms = int((time.monotonic() - started_at) * 1000)
|
|
947
|
+
for key in selected:
|
|
948
|
+
for check in results[key].get("checks", []):
|
|
949
|
+
name = check.get("name", key)
|
|
950
|
+
checks_run.append(name)
|
|
951
|
+
if check.get("status") == "fail":
|
|
952
|
+
failed_checks.append(name)
|
|
953
|
+
|
|
954
|
+
if record:
|
|
955
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(target)
|
|
956
|
+
if cdh_dir is not None:
|
|
957
|
+
history_entry = {
|
|
958
|
+
"timestamp": started_iso,
|
|
959
|
+
"checks_run": checks_run,
|
|
960
|
+
"passed": all_passed,
|
|
961
|
+
"duration_ms": duration_ms,
|
|
962
|
+
"failed_checks": failed_checks,
|
|
963
|
+
}
|
|
964
|
+
try:
|
|
965
|
+
CdhProjectLoader.append_validate_history(cdh_dir, history_entry)
|
|
966
|
+
CdhProjectLoader.record_metrics(cdh_dir, history_entry)
|
|
967
|
+
except Exception as exc:
|
|
968
|
+
click.echo(f" (warn) failed to persist history/metrics: {exc}", err=True)
|
|
969
|
+
|
|
970
|
+
if output_format == "json":
|
|
971
|
+
summary = {
|
|
972
|
+
"passed": all_passed,
|
|
973
|
+
"duration_ms": duration_ms,
|
|
974
|
+
"checks": {
|
|
975
|
+
key: {
|
|
976
|
+
"passed": results[key]["passed"],
|
|
977
|
+
"checks": results[key].get("checks", []),
|
|
978
|
+
}
|
|
979
|
+
for key in selected
|
|
980
|
+
},
|
|
981
|
+
}
|
|
982
|
+
click.echo(json_module.dumps(summary, indent=2, ensure_ascii=False))
|
|
983
|
+
if not all_passed:
|
|
984
|
+
raise click.Abort()
|
|
985
|
+
return
|
|
986
|
+
|
|
987
|
+
if all_passed:
|
|
988
|
+
click.echo(f"\n{'=' * 52}\n All checks passed ✓")
|
|
989
|
+
else:
|
|
990
|
+
click.echo(f"\n{'=' * 52}\n Some checks failed — review the {''}above")
|
|
991
|
+
raise click.Abort()
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
# ── check (pattern detection) ─────────────────────────────────
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
@aidlc.group("check", short_help="Pattern detection (Semgrep-style)")
|
|
998
|
+
@click.pass_context
|
|
999
|
+
def check_group(ctx):
|
|
1000
|
+
"""Run Semgrep-style pattern detection for brownfield migration and code quality.
|
|
1001
|
+
|
|
1002
|
+
\b
|
|
1003
|
+
Sub-commands:
|
|
1004
|
+
patterns Scan for code patterns matching built-in or custom rules
|
|
1005
|
+
rules List available pattern rule categories
|
|
1006
|
+
"""
|
|
1007
|
+
pass
|
|
1008
|
+
|
|
1009
|
+
|
|
1010
|
+
@check_group.command("patterns", short_help="Scan for code patterns")
|
|
1011
|
+
@click.argument("path", required=False, default=".")
|
|
1012
|
+
@click.option(
|
|
1013
|
+
"--rules",
|
|
1014
|
+
"-r",
|
|
1015
|
+
"rules_file",
|
|
1016
|
+
type=click.Path(),
|
|
1017
|
+
default=None,
|
|
1018
|
+
help="Path to rules file (YAML/JSON). Default: built-in rules.",
|
|
1019
|
+
)
|
|
1020
|
+
@click.option(
|
|
1021
|
+
"--format",
|
|
1022
|
+
"-f",
|
|
1023
|
+
"output_format",
|
|
1024
|
+
type=click.Choice(["text", "json", "sarif"]),
|
|
1025
|
+
default="text",
|
|
1026
|
+
help="Output format (default: text). sarif for GitHub/code scanning.",
|
|
1027
|
+
)
|
|
1028
|
+
@click.option(
|
|
1029
|
+
"--severity",
|
|
1030
|
+
"-s",
|
|
1031
|
+
"min_severity",
|
|
1032
|
+
type=click.Choice(["INFO", "WARNING", "ERROR"]),
|
|
1033
|
+
default="INFO",
|
|
1034
|
+
help="Minimum severity to report",
|
|
1035
|
+
)
|
|
1036
|
+
@click.option(
|
|
1037
|
+
"--exclude",
|
|
1038
|
+
"-e",
|
|
1039
|
+
"exclude_paths",
|
|
1040
|
+
multiple=True,
|
|
1041
|
+
help="Paths/patterns to exclude (repeatable)",
|
|
1042
|
+
)
|
|
1043
|
+
@click.option("--fail-on-error", is_flag=True, help="Exit non-zero if any ERROR finding")
|
|
1044
|
+
def check_patterns_cmd(path, rules_file, output_format, min_severity, exclude_paths, fail_on_error):
|
|
1045
|
+
"""Scan source files for pattern matches using Semgrep-style rules.
|
|
1046
|
+
|
|
1047
|
+
\b
|
|
1048
|
+
Examples:
|
|
1049
|
+
cdh aidlc check patterns src/
|
|
1050
|
+
cdh aidlc check patterns --rules my-rules.yaml --format json src/
|
|
1051
|
+
cdh aidlc check patterns --severity WARNING --exclude "**/test/**" src/
|
|
1052
|
+
|
|
1053
|
+
Rules are loaded from (in order):
|
|
1054
|
+
1. --rules FILE (if specified)
|
|
1055
|
+
2. ~/.cdh/rules/patterns.yaml (if exists)
|
|
1056
|
+
3. Built-in cdh/rules/patterns.yaml
|
|
1057
|
+
"""
|
|
1058
|
+
import os
|
|
1059
|
+
import json as json_module
|
|
1060
|
+
from pathlib import Path
|
|
1061
|
+
|
|
1062
|
+
from cdh.tools.pattern_detect import PatternEngine, load_rules, Finding
|
|
1063
|
+
|
|
1064
|
+
target = Path(path).expanduser().resolve()
|
|
1065
|
+
if not target.exists():
|
|
1066
|
+
click.echo(f"Error: Path not found: {target}", err=True)
|
|
1067
|
+
raise click.Abort()
|
|
1068
|
+
|
|
1069
|
+
rules_path = rules_file
|
|
1070
|
+
if rules_path is None:
|
|
1071
|
+
user_rules = Path.home() / ".cdh" / "rules" / "patterns.yaml"
|
|
1072
|
+
if user_rules.is_file():
|
|
1073
|
+
rules_path = str(user_rules)
|
|
1074
|
+
else:
|
|
1075
|
+
rules_path = str(Path(__file__).parent.parent / "rules" / "patterns.yaml")
|
|
1076
|
+
|
|
1077
|
+
rules = load_rules(rules_path) if rules_path else []
|
|
1078
|
+
if not rules:
|
|
1079
|
+
click.echo("Warning: No rules loaded. Pattern detection skipped.", err=True)
|
|
1080
|
+
return
|
|
1081
|
+
|
|
1082
|
+
engine = PatternEngine(rules)
|
|
1083
|
+
findings = engine.scan(
|
|
1084
|
+
[str(target)],
|
|
1085
|
+
exclude_paths=list(exclude_paths),
|
|
1086
|
+
gitignore=True,
|
|
1087
|
+
)
|
|
1088
|
+
|
|
1089
|
+
severity_order = {"INFO": 0, "WARNING": 1, "ERROR": 2}
|
|
1090
|
+
min_level = severity_order.get(min_severity, 0)
|
|
1091
|
+
filtered = [f for f in findings if severity_order.get(f.severity, 0) >= min_level]
|
|
1092
|
+
|
|
1093
|
+
if output_format == "json":
|
|
1094
|
+
result = {
|
|
1095
|
+
"results": [f.to_dict() for f in filtered],
|
|
1096
|
+
"total": len(filtered),
|
|
1097
|
+
"rules_loaded": len(rules),
|
|
1098
|
+
}
|
|
1099
|
+
click.echo(json_module.dumps(result, indent=2))
|
|
1100
|
+
if fail_on_error and any(f.severity == "ERROR" for f in filtered):
|
|
1101
|
+
raise click.Abort()
|
|
1102
|
+
return
|
|
1103
|
+
|
|
1104
|
+
if output_format == "sarif":
|
|
1105
|
+
sarif = _to_sarif(filtered, rules_path or "built-in")
|
|
1106
|
+
click.echo(json_module.dumps(sarif, indent=2))
|
|
1107
|
+
if fail_on_error and any(f.severity == "ERROR" for f in filtered):
|
|
1108
|
+
raise click.Abort()
|
|
1109
|
+
return
|
|
1110
|
+
|
|
1111
|
+
# Text format
|
|
1112
|
+
if not filtered:
|
|
1113
|
+
click.echo("No findings. Pattern detection passed.")
|
|
1114
|
+
return
|
|
1115
|
+
|
|
1116
|
+
error_count = sum(1 for f in filtered if f.severity == "ERROR")
|
|
1117
|
+
warn_count = sum(1 for f in filtered if f.severity == "WARNING")
|
|
1118
|
+
info_count = sum(1 for f in filtered if f.severity == "INFO")
|
|
1119
|
+
|
|
1120
|
+
click.echo(f"\nPattern Detection Summary:")
|
|
1121
|
+
click.echo(f" Rules loaded: {len(rules)}")
|
|
1122
|
+
click.echo(f" Files scanned: {len(set(f.file for f in filtered))}")
|
|
1123
|
+
click.echo(f" Findings: {error_count} errors, {warn_count} warnings, {info_count} info")
|
|
1124
|
+
click.echo(f"\n{'=' * 70}")
|
|
1125
|
+
|
|
1126
|
+
for f in sorted(filtered, key=lambda x: (severity_order.get(x.severity, 0), x.file, x.line)):
|
|
1127
|
+
sev_indicator = {"INFO": "I", "WARNING": "W", "ERROR": "E"}.get(f.severity, "?")
|
|
1128
|
+
click.echo(f" [{sev_indicator}] {f.severity:<8} {f.file}:{f.line} {f.message}")
|
|
1129
|
+
if f.matched_text and len(f.matched_text) < 80:
|
|
1130
|
+
click.echo(f" matched: {f.matched_text!r}")
|
|
1131
|
+
|
|
1132
|
+
if fail_on_error and error_count > 0:
|
|
1133
|
+
raise click.Abort()
|
|
1134
|
+
|
|
1135
|
+
|
|
1136
|
+
def _to_sarif(findings: list[Finding], rules_path: str) -> dict:
|
|
1137
|
+
"""Convert findings to SARIF format."""
|
|
1138
|
+
rules_map = {}
|
|
1139
|
+
for f in findings:
|
|
1140
|
+
if f.rule_id not in rules_map:
|
|
1141
|
+
rules_map[f.rule_id] = {
|
|
1142
|
+
"id": f.rule_id,
|
|
1143
|
+
"name": f.rule_id,
|
|
1144
|
+
"shortDescription": {"text": f.message},
|
|
1145
|
+
"properties": {"severity": f.severity.lower()},
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
results = []
|
|
1149
|
+
for f in findings:
|
|
1150
|
+
results.append({
|
|
1151
|
+
"ruleId": f.rule_id,
|
|
1152
|
+
"level": {"INFO": "note", "WARNING": "warning", "ERROR": "error"}.get(f.severity.lower(), "warning"),
|
|
1153
|
+
"message": {"text": f.message},
|
|
1154
|
+
"locations": [{
|
|
1155
|
+
"physicalLocation": {
|
|
1156
|
+
"artifactLocation": {"uri": f.file},
|
|
1157
|
+
"region": {"startLine": f.line, "startColumn": f.column or 1, "endLine": f.end_line or f.line},
|
|
1158
|
+
}
|
|
1159
|
+
}],
|
|
1160
|
+
})
|
|
1161
|
+
|
|
1162
|
+
return {
|
|
1163
|
+
"version": "2.1.0",
|
|
1164
|
+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
|
1165
|
+
"runs": [{
|
|
1166
|
+
"tool": {
|
|
1167
|
+
"driver": {
|
|
1168
|
+
"name": "aidlc",
|
|
1169
|
+
"version": "1.0.0",
|
|
1170
|
+
"rules": list(rules_map.values()),
|
|
1171
|
+
}
|
|
1172
|
+
},
|
|
1173
|
+
"results": results,
|
|
1174
|
+
}]
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
|
|
1178
|
+
@check_group.command("rules", short_help="List available pattern rules")
|
|
1179
|
+
@click.option("--category", "-c", help="Filter by rule category prefix")
|
|
1180
|
+
def check_rules_cmd(category):
|
|
1181
|
+
"""List all available pattern detection rules.
|
|
1182
|
+
|
|
1183
|
+
\b
|
|
1184
|
+
Examples:
|
|
1185
|
+
cdh aidlc check rules
|
|
1186
|
+
cdh aidlc check rules --category api
|
|
1187
|
+
"""
|
|
1188
|
+
import os
|
|
1189
|
+
import yaml
|
|
1190
|
+
from pathlib import Path
|
|
1191
|
+
|
|
1192
|
+
rules_path = None
|
|
1193
|
+
user_rules = Path.home() / ".cdh" / "rules" / "patterns.yaml"
|
|
1194
|
+
if user_rules.is_file():
|
|
1195
|
+
rules_path = str(user_rules)
|
|
1196
|
+
else:
|
|
1197
|
+
default_rules = Path(__file__).parent.parent / "rules" / "patterns.yaml"
|
|
1198
|
+
if default_rules.is_file():
|
|
1199
|
+
rules_path = str(default_rules)
|
|
1200
|
+
|
|
1201
|
+
if not rules_path or not os.path.isfile(rules_path):
|
|
1202
|
+
click.echo("No rules file found.")
|
|
1203
|
+
return
|
|
1204
|
+
|
|
1205
|
+
with open(rules_path) as f:
|
|
1206
|
+
doc = yaml.safe_load(f)
|
|
1207
|
+
|
|
1208
|
+
click.echo(f"Available rules from: {rules_path}\n")
|
|
1209
|
+
click.echo(f"{'RULE ID':<30} {'SEVERITY':<10} DESCRIPTION")
|
|
1210
|
+
click.echo("-" * 80)
|
|
1211
|
+
|
|
1212
|
+
count = 0
|
|
1213
|
+
for cat_name, cat_rules in (doc or {}).items():
|
|
1214
|
+
if not isinstance(cat_rules, dict):
|
|
1215
|
+
continue
|
|
1216
|
+
for rule_id, rule_def in cat_rules.items():
|
|
1217
|
+
if not isinstance(rule_def, dict):
|
|
1218
|
+
continue
|
|
1219
|
+
full_id = f"{cat_name}.{rule_id}"
|
|
1220
|
+
if category and not full_id.startswith(category):
|
|
1221
|
+
continue
|
|
1222
|
+
sev = rule_def.get("severity", "?").upper()
|
|
1223
|
+
msg = rule_def.get("message", "")
|
|
1224
|
+
if len(msg) > 50:
|
|
1225
|
+
msg = msg[:47] + "..."
|
|
1226
|
+
click.echo(f"{full_id:<30} {sev:<10} {msg}")
|
|
1227
|
+
count += 1
|
|
1228
|
+
|
|
1229
|
+
click.echo(f"\n{count} rules shown.")
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
@aidlc.group("generators", short_help="Manage code generators (type generators)")
|
|
1233
|
+
def generators_group():
|
|
1234
|
+
"""Manage code generators for OpenAPI/AsyncAPI → typed code.
|
|
1235
|
+
|
|
1236
|
+
\b
|
|
1237
|
+
Sub-commands:
|
|
1238
|
+
list List installed plugins (built-in + user + project)
|
|
1239
|
+
search [query] Search the built-in plugin index
|
|
1240
|
+
info <name> Show details about an installed plugin
|
|
1241
|
+
install <name> Install a built-in plugin into current project
|
|
1242
|
+
init <name> Scaffold a new plugin directory
|
|
1243
|
+
validate <dir> Validate a plugin's MANIFEST.toml + template
|
|
1244
|
+
"""
|
|
1245
|
+
|
|
1246
|
+
|
|
1247
|
+
@generators_group.command("list")
|
|
1248
|
+
@click.option(
|
|
1249
|
+
"--source",
|
|
1250
|
+
type=click.Choice(["all", "built-in", "user", "project"]),
|
|
1251
|
+
default="all",
|
|
1252
|
+
help="Which source to list (default: all, de-duped by name).",
|
|
1253
|
+
)
|
|
1254
|
+
@click.option(
|
|
1255
|
+
"--path",
|
|
1256
|
+
"path",
|
|
1257
|
+
type=click.Path(path_type=Path),
|
|
1258
|
+
default=None,
|
|
1259
|
+
help="Project root for the 'project' source (default: cwd).",
|
|
1260
|
+
)
|
|
1261
|
+
def generators_list(source, path):
|
|
1262
|
+
"""List all discovered generator plugins."""
|
|
1263
|
+
from cdh.generators.cli import (
|
|
1264
|
+
discover_all_plugins,
|
|
1265
|
+
discover_all_plugins_merged,
|
|
1266
|
+
filter_by_source,
|
|
1267
|
+
)
|
|
1268
|
+
|
|
1269
|
+
project_root = Path(path).expanduser().resolve() if path else Path.cwd()
|
|
1270
|
+
discovered = discover_all_plugins(project_root=project_root)
|
|
1271
|
+
|
|
1272
|
+
if source == "all":
|
|
1273
|
+
merged = discover_all_plugins_merged(project_root=project_root)
|
|
1274
|
+
if not merged:
|
|
1275
|
+
click.echo("No plugins found.")
|
|
1276
|
+
return
|
|
1277
|
+
click.echo(f"{'NAME':<16} {'SOURCE':<10} {'DISPLAY':<20} {'EXT':<8} FEATURES")
|
|
1278
|
+
click.echo("-" * 76)
|
|
1279
|
+
for name, (src, plugin) in sorted(merged.items()):
|
|
1280
|
+
features = ",".join(plugin.supports) or "-"
|
|
1281
|
+
click.echo(
|
|
1282
|
+
f"{name:<16} {src:<10} {plugin.display_name:<20} "
|
|
1283
|
+
f"{plugin.file_extension:<8} {features}"
|
|
1284
|
+
)
|
|
1285
|
+
return
|
|
1286
|
+
|
|
1287
|
+
filtered = filter_by_source(discovered, source)
|
|
1288
|
+
if not filtered:
|
|
1289
|
+
click.echo(f"No plugins found in source '{source}'.")
|
|
1290
|
+
return
|
|
1291
|
+
click.echo(f"{'NAME':<16} {'DISPLAY':<20} {'EXT':<8} FEATURES")
|
|
1292
|
+
click.echo("-" * 60)
|
|
1293
|
+
for _src, plugin in filtered:
|
|
1294
|
+
features = ",".join(plugin.supports) or "-"
|
|
1295
|
+
click.echo(
|
|
1296
|
+
f"{plugin.name:<16} {plugin.display_name:<20} "
|
|
1297
|
+
f"{plugin.file_extension:<8} {features}"
|
|
1298
|
+
)
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
@generators_group.command("search")
|
|
1302
|
+
@click.argument("query", required=False)
|
|
1303
|
+
@click.option("--tag", help="Filter by tag")
|
|
1304
|
+
@click.option("--language-family", help="Filter by language family")
|
|
1305
|
+
def generators_search(query, tag, language_family):
|
|
1306
|
+
"""Search the built-in plugin index for community-available generators."""
|
|
1307
|
+
from cdh.generators.cli import search_index
|
|
1308
|
+
|
|
1309
|
+
entries = search_index(query)
|
|
1310
|
+
|
|
1311
|
+
if tag:
|
|
1312
|
+
entries = [
|
|
1313
|
+
e for e in entries
|
|
1314
|
+
if tag.lower() in [str(t).lower() for t in e.get("tags", []) or []]
|
|
1315
|
+
]
|
|
1316
|
+
if language_family:
|
|
1317
|
+
entries = [
|
|
1318
|
+
e for e in entries
|
|
1319
|
+
if str(e.get("language_family", "")).lower() == language_family.lower()
|
|
1320
|
+
]
|
|
1321
|
+
|
|
1322
|
+
if not entries:
|
|
1323
|
+
if query:
|
|
1324
|
+
click.echo(f"No plugins matched '{query}'.")
|
|
1325
|
+
else:
|
|
1326
|
+
click.echo("No plugins found in the built-in index.")
|
|
1327
|
+
return
|
|
1328
|
+
|
|
1329
|
+
click.echo(f"{'NAME':<16} {'LANG':<12} {'VERSION':<8} DESCRIPTION")
|
|
1330
|
+
click.echo("-" * 80)
|
|
1331
|
+
for entry in entries:
|
|
1332
|
+
name = entry.get("name", "?")
|
|
1333
|
+
lang = entry.get("language_family", "?")
|
|
1334
|
+
version = entry.get("version", "?")
|
|
1335
|
+
description = entry.get("description", "")
|
|
1336
|
+
if len(description) > 50:
|
|
1337
|
+
description = description[:47] + "..."
|
|
1338
|
+
click.echo(f"{name:<16} {lang:<12} {version:<8} {description}")
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
@generators_group.command("info")
|
|
1342
|
+
@click.argument("name")
|
|
1343
|
+
@click.option(
|
|
1344
|
+
"--path",
|
|
1345
|
+
"path",
|
|
1346
|
+
type=click.Path(path_type=Path),
|
|
1347
|
+
default=None,
|
|
1348
|
+
help="Project root for the 'project' source (default: cwd).",
|
|
1349
|
+
)
|
|
1350
|
+
def generators_info(name, path):
|
|
1351
|
+
"""Show details about an installed or indexed plugin."""
|
|
1352
|
+
from cdh.generators.cli import (
|
|
1353
|
+
discover_all_plugins_merged,
|
|
1354
|
+
search_index,
|
|
1355
|
+
)
|
|
1356
|
+
|
|
1357
|
+
project_root = Path(path).expanduser().resolve() if path else Path.cwd()
|
|
1358
|
+
merged = discover_all_plugins_merged(project_root=project_root)
|
|
1359
|
+
if name in merged:
|
|
1360
|
+
source, plugin = merged[name]
|
|
1361
|
+
click.echo(f"Plugin: {plugin.name}")
|
|
1362
|
+
click.echo(f" Source: {source}")
|
|
1363
|
+
click.echo(f" Display name: {plugin.display_name}")
|
|
1364
|
+
click.echo(f" File ext: {plugin.file_extension}")
|
|
1365
|
+
click.echo(f" MIME type: {plugin.mime_type}")
|
|
1366
|
+
click.echo(f" Outdir: {plugin.default_outdir}")
|
|
1367
|
+
click.echo(f" Filename tmpl: {plugin.output_filename_template}")
|
|
1368
|
+
click.echo(f" Package: {plugin.package_name_default}")
|
|
1369
|
+
click.echo(f" Features: {', '.join(plugin.supports) or '-'}")
|
|
1370
|
+
if plugin.format_hints:
|
|
1371
|
+
click.echo(f" Format hints: {', '.join(plugin.format_hints)}")
|
|
1372
|
+
if plugin.imports:
|
|
1373
|
+
click.echo(f" Imports: {plugin.imports}")
|
|
1374
|
+
click.echo(f" Directory: {plugin.directory}")
|
|
1375
|
+
click.echo(f" Template: {plugin.template_path}")
|
|
1376
|
+
if plugin.imports_template_path:
|
|
1377
|
+
click.echo(f" Imports tmpl: {plugin.imports_template_path}")
|
|
1378
|
+
return
|
|
1379
|
+
|
|
1380
|
+
index_entries = search_index(name)
|
|
1381
|
+
matches = [e for e in index_entries if e.get("name") == name]
|
|
1382
|
+
if matches:
|
|
1383
|
+
_print_index_entry(matches[0])
|
|
1384
|
+
return
|
|
1385
|
+
|
|
1386
|
+
click.echo(f"Plugin '{name}' not found locally or in the built-in index.")
|
|
1387
|
+
click.echo("Try `cdh aidlc generators search` to look it up.")
|
|
1388
|
+
raise click.Abort()
|
|
1389
|
+
|
|
1390
|
+
|
|
1391
|
+
def _print_index_entry(entry: dict) -> None:
|
|
1392
|
+
click.echo(f"Plugin: {entry.get('name', '?')}")
|
|
1393
|
+
click.echo(f" Display name: {entry.get('display_name', '?')}")
|
|
1394
|
+
click.echo(f" Version: {entry.get('version', '?')}")
|
|
1395
|
+
click.echo(f" Author: {entry.get('author', '?')}")
|
|
1396
|
+
click.echo(f" Language: {entry.get('language_family', '?')}")
|
|
1397
|
+
description = entry.get("description", "")
|
|
1398
|
+
if description:
|
|
1399
|
+
click.echo(f" Description: {description}")
|
|
1400
|
+
tags = entry.get("tags") or []
|
|
1401
|
+
if tags:
|
|
1402
|
+
click.echo(f" Tags: {', '.join(str(tag) for tag in tags)}")
|
|
1403
|
+
tested_with = entry.get("tested_with") or []
|
|
1404
|
+
if tested_with:
|
|
1405
|
+
click.echo(f" Tested with: {', '.join(str(name) for name in tested_with)}")
|
|
1406
|
+
download_url = entry.get("download_url", "")
|
|
1407
|
+
if download_url:
|
|
1408
|
+
click.echo(f" Download URL: {download_url}")
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
@generators_group.command("install")
|
|
1412
|
+
@click.argument("name")
|
|
1413
|
+
@click.option(
|
|
1414
|
+
"--target-dir",
|
|
1415
|
+
default="./aidlc/generators",
|
|
1416
|
+
help="Where to install (default: ./aidlc/generators)",
|
|
1417
|
+
)
|
|
1418
|
+
def generators_install(name, target_dir):
|
|
1419
|
+
"""Install a built-in plugin into current project (aidlc/generators/)."""
|
|
1420
|
+
from cdh.generators.cli import install_plugin
|
|
1421
|
+
|
|
1422
|
+
target = Path(target_dir).expanduser().resolve()
|
|
1423
|
+
try:
|
|
1424
|
+
installed = install_plugin(name, target)
|
|
1425
|
+
except ValueError as exc:
|
|
1426
|
+
click.echo(f"Error: {exc}")
|
|
1427
|
+
raise click.Abort()
|
|
1428
|
+
except (FileNotFoundError, FileExistsError) as exc:
|
|
1429
|
+
click.echo(f"Error: {exc}")
|
|
1430
|
+
raise click.Abort()
|
|
1431
|
+
click.echo(f"Installed '{name}' to {installed}")
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
@generators_group.command("init")
|
|
1435
|
+
@click.argument("name")
|
|
1436
|
+
@click.option(
|
|
1437
|
+
"--target-dir",
|
|
1438
|
+
default="./aidlc/generators",
|
|
1439
|
+
help="Where to scaffold (default: ./aidlc/generators)",
|
|
1440
|
+
)
|
|
1441
|
+
def generators_init(name, target_dir):
|
|
1442
|
+
"""Scaffold a new plugin directory with MANIFEST.toml + template stub."""
|
|
1443
|
+
from cdh.generators.cli import create_plugin_scaffold
|
|
1444
|
+
|
|
1445
|
+
target = Path(target_dir).expanduser().resolve()
|
|
1446
|
+
try:
|
|
1447
|
+
created = create_plugin_scaffold(name, target)
|
|
1448
|
+
except (ValueError, FileExistsError) as exc:
|
|
1449
|
+
click.echo(f"Error: {exc}")
|
|
1450
|
+
raise click.Abort()
|
|
1451
|
+
click.echo(f"Scaffolded plugin '{name}' at {created}")
|
|
1452
|
+
click.echo(f" - {created / 'MANIFEST.toml'}")
|
|
1453
|
+
click.echo(f" - {created / f'template.{name}.tmpl'}")
|
|
1454
|
+
click.echo("Next: edit the template, then run")
|
|
1455
|
+
click.echo(f" cdh aidlc generators validate {created}")
|
|
1456
|
+
|
|
1457
|
+
|
|
1458
|
+
@generators_group.command("validate")
|
|
1459
|
+
@click.argument("path")
|
|
1460
|
+
def generators_validate(path):
|
|
1461
|
+
"""Validate a plugin's MANIFEST.toml + template (run smoke test)."""
|
|
1462
|
+
from cdh.generators.cli import validate_plugin
|
|
1463
|
+
|
|
1464
|
+
target = Path(path).expanduser().resolve()
|
|
1465
|
+
ok, errors = validate_plugin(target)
|
|
1466
|
+
if ok:
|
|
1467
|
+
click.echo(f"✓ Plugin at {target} is valid.")
|
|
1468
|
+
return
|
|
1469
|
+
click.echo(f"✗ Plugin at {target} is invalid:")
|
|
1470
|
+
for error in errors:
|
|
1471
|
+
click.echo(f" - {error}")
|
|
1472
|
+
raise click.Abort()
|
|
1473
|
+
|
|
1474
|
+
|
|
1475
|
+
@generators_group.command("version")
|
|
1476
|
+
@click.argument("name", required=False)
|
|
1477
|
+
@click.option("--path", "project_path", type=click.Path(path_type=Path), default=None, help="Project root")
|
|
1478
|
+
def generators_version(name, project_path):
|
|
1479
|
+
"""Show plugin version info (SemVer 2.0.0).
|
|
1480
|
+
|
|
1481
|
+
Without args: show all installed plugins and their versions.
|
|
1482
|
+
With <name>: show detailed version info for a specific plugin.
|
|
1483
|
+
|
|
1484
|
+
\b
|
|
1485
|
+
Examples:
|
|
1486
|
+
cdh aidlc generators version
|
|
1487
|
+
cdh aidlc generators version typescript
|
|
1488
|
+
"""
|
|
1489
|
+
from cdh.generators.cli import discover_all_plugins_merged
|
|
1490
|
+
from cdh.generators.version import SemVer
|
|
1491
|
+
|
|
1492
|
+
project_root = Path(project_path).expanduser().resolve() if project_path else Path.cwd()
|
|
1493
|
+
merged = discover_all_plugins_merged(project_root=project_root)
|
|
1494
|
+
|
|
1495
|
+
if not merged:
|
|
1496
|
+
click.echo("No plugins found.")
|
|
1497
|
+
return
|
|
1498
|
+
|
|
1499
|
+
if not name:
|
|
1500
|
+
click.echo(f"{'NAME':<16} {'SOURCE':<10} {'VERSION':<12} STATUS")
|
|
1501
|
+
click.echo("-" * 50)
|
|
1502
|
+
for n, (source, plugin) in sorted(merged.items()):
|
|
1503
|
+
version = plugin.directory.name if hasattr(plugin, 'directory') else "?"
|
|
1504
|
+
click.echo(f"{n:<16} {source:<10} {version:<12} OK")
|
|
1505
|
+
return
|
|
1506
|
+
|
|
1507
|
+
if name in merged:
|
|
1508
|
+
source, plugin = merged[name]
|
|
1509
|
+
click.echo(f"Plugin: {name}")
|
|
1510
|
+
click.echo(f" Source: {source}")
|
|
1511
|
+
click.echo(f" Directory: {plugin.directory}")
|
|
1512
|
+
click.echo(f" Template: {plugin.template_path.name}")
|
|
1513
|
+
click.echo(f" Features: {', '.join(plugin.supports) or '-'}")
|
|
1514
|
+
try:
|
|
1515
|
+
index_entry = None
|
|
1516
|
+
from cdh.generators.cli import search_index
|
|
1517
|
+
for e in search_index(name):
|
|
1518
|
+
if e.get("name") == name:
|
|
1519
|
+
index_entry = e
|
|
1520
|
+
break
|
|
1521
|
+
if index_entry:
|
|
1522
|
+
click.echo(f" Index ver: {index_entry.get('version', '?')}")
|
|
1523
|
+
click.echo(f" Author: {index_entry.get('author', '?')}")
|
|
1524
|
+
except Exception:
|
|
1525
|
+
pass
|
|
1526
|
+
return
|
|
1527
|
+
|
|
1528
|
+
click.echo(f"Plugin '{name}' not found.")
|
|
1529
|
+
raise click.Abort()
|
|
1530
|
+
|
|
1531
|
+
|
|
1532
|
+
@generators_group.command("deps")
|
|
1533
|
+
@click.argument("name")
|
|
1534
|
+
@click.option("--path", "project_path", type=click.Path(path_type=Path), default=None)
|
|
1535
|
+
def generators_deps(name, project_path):
|
|
1536
|
+
"""Show dependencies and reverse-dependencies for a plugin.
|
|
1537
|
+
|
|
1538
|
+
\b
|
|
1539
|
+
Examples:
|
|
1540
|
+
cdh aidlc generators deps typescript
|
|
1541
|
+
"""
|
|
1542
|
+
from cdh.generators.cli import discover_all_plugins_merged
|
|
1543
|
+
|
|
1544
|
+
project_root = Path(project_path).expanduser().resolve() if project_path else Path.cwd()
|
|
1545
|
+
merged = discover_all_plugins_merged(project_root=project_root)
|
|
1546
|
+
|
|
1547
|
+
if name not in merged:
|
|
1548
|
+
click.echo(f"Plugin '{name}' not found.")
|
|
1549
|
+
raise click.Abort()
|
|
1550
|
+
|
|
1551
|
+
source, plugin = merged[name]
|
|
1552
|
+
click.echo(f"Plugin: {name} (from {source})")
|
|
1553
|
+
|
|
1554
|
+
manifest_file = plugin.directory / "MANIFEST.toml"
|
|
1555
|
+
if manifest_file.is_file():
|
|
1556
|
+
import tomllib
|
|
1557
|
+
try:
|
|
1558
|
+
doc = tomllib.loads(manifest_file.read_text(encoding="utf-8"))
|
|
1559
|
+
deps = doc.get("dependencies", [])
|
|
1560
|
+
if deps:
|
|
1561
|
+
click.echo(f" Dependencies ({len(deps)}):")
|
|
1562
|
+
for dep in deps:
|
|
1563
|
+
dep_name = dep.get("name", "?")
|
|
1564
|
+
dep_ver = dep.get("version", "*")
|
|
1565
|
+
click.echo(f" {dep_name} {dep_ver}")
|
|
1566
|
+
else:
|
|
1567
|
+
click.echo(" No dependencies declared.")
|
|
1568
|
+
except Exception as e:
|
|
1569
|
+
click.echo(f" (Could not parse dependencies: {e})")
|
|
1570
|
+
|
|
1571
|
+
click.echo(" Reverse dependencies (plugins that depend on this):")
|
|
1572
|
+
reverse_deps = []
|
|
1573
|
+
for other_name, (other_src, other_plugin) in merged.items():
|
|
1574
|
+
if other_name == name:
|
|
1575
|
+
continue
|
|
1576
|
+
other_manifest = other_plugin.directory / "MANIFEST.toml"
|
|
1577
|
+
if other_manifest.is_file():
|
|
1578
|
+
import tomllib
|
|
1579
|
+
try:
|
|
1580
|
+
doc = tomllib.loads(other_manifest.read_text(encoding="utf-8"))
|
|
1581
|
+
for dep in doc.get("dependencies", []):
|
|
1582
|
+
if dep.get("name") == name:
|
|
1583
|
+
reverse_deps.append(other_name)
|
|
1584
|
+
break
|
|
1585
|
+
except Exception:
|
|
1586
|
+
pass
|
|
1587
|
+
|
|
1588
|
+
if reverse_deps:
|
|
1589
|
+
for rd in reverse_deps:
|
|
1590
|
+
click.echo(f" {rd}")
|
|
1591
|
+
else:
|
|
1592
|
+
click.echo(" (none)")
|
|
1593
|
+
|
|
1594
|
+
|
|
1595
|
+
# ── tools ────────────────────────────────────────────────────
|
|
1596
|
+
|
|
1597
|
+
|
|
1598
|
+
@aidlc.group("tools", short_help="Manage AIDLC tools (install/status/update)")
|
|
1599
|
+
def tools_group():
|
|
1600
|
+
"""Manage AIDLC core tools: generate_shared, contract_diff, deploy_stack.
|
|
1601
|
+
|
|
1602
|
+
Sub-commands:
|
|
1603
|
+
install Install real tool implementations (replaces stubs)
|
|
1604
|
+
status Show installation status of each tool
|
|
1605
|
+
update Update installed tools to latest version
|
|
1606
|
+
"""
|
|
1607
|
+
|
|
1608
|
+
|
|
1609
|
+
@aidlc.group("trace", short_help="Manage and export AIDLC traces (OTLP)")
|
|
1610
|
+
def trace_group():
|
|
1611
|
+
"""Manage AIDLC trace data — list, view, export via OTLP/HTTP.
|
|
1612
|
+
|
|
1613
|
+
The ``export`` subcommand converts agenttrace SQLite rows to OTLP/HTTP
|
|
1614
|
+
JSON and ships them to a collector (Jaeger, Tempo, Honeycomb, etc.).
|
|
1615
|
+
|
|
1616
|
+
\b
|
|
1617
|
+
Sub-commands:
|
|
1618
|
+
export [--endpoint URL] [--since DATE] [--service-name NAME]
|
|
1619
|
+
"""
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
@trace_group.command("export", short_help="Export traces to OTLP/HTTP collector")
|
|
1623
|
+
@click.option(
|
|
1624
|
+
"--endpoint",
|
|
1625
|
+
default=None,
|
|
1626
|
+
help="OTLP/HTTP base endpoint (default: $OTEL_EXPORTER_OTLP_ENDPOINT or http://localhost:4318)",
|
|
1627
|
+
)
|
|
1628
|
+
@click.option(
|
|
1629
|
+
"--db-path",
|
|
1630
|
+
type=click.Path(),
|
|
1631
|
+
default=None,
|
|
1632
|
+
help="Path to the agenttrace sqlite DB (default: ~/.cdh/traces/traces.db)",
|
|
1633
|
+
)
|
|
1634
|
+
@click.option(
|
|
1635
|
+
"--service-name",
|
|
1636
|
+
default=None,
|
|
1637
|
+
help="Value of service.name resource attribute (default: $OTEL_SERVICE_NAME or 'cdh')",
|
|
1638
|
+
)
|
|
1639
|
+
@click.option(
|
|
1640
|
+
"--since",
|
|
1641
|
+
default=None,
|
|
1642
|
+
help="Only export rows with timestamp >= this ISO date/datetime",
|
|
1643
|
+
)
|
|
1644
|
+
@click.option(
|
|
1645
|
+
"--session",
|
|
1646
|
+
default=None,
|
|
1647
|
+
help="Restrict export to a single session_id",
|
|
1648
|
+
)
|
|
1649
|
+
@click.option("--batch-size", type=int, default=256, show_default=True, help="Spans per HTTP POST")
|
|
1650
|
+
@click.option("--timeout", type=float, default=10.0, show_default=True, help="Per-request HTTP timeout (s)")
|
|
1651
|
+
@click.option("--dry-run", is_flag=True, help="Build payloads but don't POST")
|
|
1652
|
+
@click.option(
|
|
1653
|
+
"--header",
|
|
1654
|
+
"headers",
|
|
1655
|
+
multiple=True,
|
|
1656
|
+
metavar="K=V",
|
|
1657
|
+
help="Extra HTTP header (repeatable), e.g. --header 'Authorization=Bearer xxx'",
|
|
1658
|
+
)
|
|
1659
|
+
def trace_export_cmd(
|
|
1660
|
+
endpoint, db_path, service_name, since, session, batch_size, timeout, dry_run, headers
|
|
1661
|
+
):
|
|
1662
|
+
"""Export agenttrace spans from the local SQLite DB to an OTLP/HTTP collector.
|
|
1663
|
+
|
|
1664
|
+
\b
|
|
1665
|
+
Examples:
|
|
1666
|
+
cdh aidlc trace export
|
|
1667
|
+
cdh aidlc trace export --endpoint http://tempo:4318 --since 2026-07-01
|
|
1668
|
+
cdh aidlc trace export --service-name cdh-prod --dry-run
|
|
1669
|
+
cdh aidlc trace export --header "Authorization=Bearer xxx"
|
|
1670
|
+
|
|
1671
|
+
Honors the OTEL_EXPORTER_OTLP_ENDPOINT and OTEL_EXPORTER_OTLP_HEADERS
|
|
1672
|
+
environment variables when --endpoint / --header are not supplied.
|
|
1673
|
+
"""
|
|
1674
|
+
import os as _os
|
|
1675
|
+
from cdh.trace.otel_exporter import DEFAULT_DB_PATH, OtlpExporter
|
|
1676
|
+
|
|
1677
|
+
eff_endpoint = endpoint or _os.environ.get(
|
|
1678
|
+
"OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4318"
|
|
1679
|
+
)
|
|
1680
|
+
eff_service = service_name or _os.environ.get("OTEL_SERVICE_NAME", "cdh")
|
|
1681
|
+
eff_db_path = Path(db_path).expanduser() if db_path else DEFAULT_DB_PATH
|
|
1682
|
+
|
|
1683
|
+
hdrs: dict[str, str] = {}
|
|
1684
|
+
for h in headers:
|
|
1685
|
+
if "=" in h:
|
|
1686
|
+
k, v = h.split("=", 1)
|
|
1687
|
+
hdrs[k.strip()] = v.strip()
|
|
1688
|
+
env_hdrs = _os.environ.get("OTEL_EXPORTER_OTLP_HEADERS", "")
|
|
1689
|
+
for part in env_hdrs.split(","):
|
|
1690
|
+
if "=" not in part:
|
|
1691
|
+
continue
|
|
1692
|
+
k, v = part.split("=", 1)
|
|
1693
|
+
k = k.strip()
|
|
1694
|
+
if k:
|
|
1695
|
+
hdrs[k] = v.strip()
|
|
1696
|
+
|
|
1697
|
+
exporter = OtlpExporter(
|
|
1698
|
+
db_path=eff_db_path,
|
|
1699
|
+
endpoint=eff_endpoint,
|
|
1700
|
+
service_name=eff_service,
|
|
1701
|
+
headers=hdrs,
|
|
1702
|
+
batch_size=batch_size,
|
|
1703
|
+
timeout_s=timeout,
|
|
1704
|
+
dry_run=dry_run,
|
|
1705
|
+
)
|
|
1706
|
+
|
|
1707
|
+
counters = exporter.export_since(since=since, session_id=session)
|
|
1708
|
+
mode = "(dry-run) " if dry_run else ""
|
|
1709
|
+
click.echo(
|
|
1710
|
+
f"{mode}OTLP export summary: "
|
|
1711
|
+
f"read={counters['read']} sent={counters['sent']} "
|
|
1712
|
+
f"failed={counters['failed']} batches={counters['batches']}"
|
|
1713
|
+
)
|
|
1714
|
+
if not dry_run and counters["failed"] > 0:
|
|
1715
|
+
raise click.Abort()
|
|
1716
|
+
|
|
1717
|
+
|
|
1718
|
+
@trace_group.command("logs", short_help="Export logs to OTLP/HTTP collector")
|
|
1719
|
+
@click.option(
|
|
1720
|
+
"--endpoint",
|
|
1721
|
+
default=None,
|
|
1722
|
+
help="OTLP/HTTP endpoint (default: $OTEL_EXPORTER_OTLP_ENDPOINT or http://localhost:4318)",
|
|
1723
|
+
)
|
|
1724
|
+
@click.option(
|
|
1725
|
+
"--log-file",
|
|
1726
|
+
type=click.Path(),
|
|
1727
|
+
default=None,
|
|
1728
|
+
help="Path to the NDJSON log file (default: ~/.cdh/logs/app.jsonl)",
|
|
1729
|
+
)
|
|
1730
|
+
@click.option(
|
|
1731
|
+
"--since",
|
|
1732
|
+
default=None,
|
|
1733
|
+
help="ISO date/datetime; only export logs after this timestamp",
|
|
1734
|
+
)
|
|
1735
|
+
@click.option(
|
|
1736
|
+
"--min-level",
|
|
1737
|
+
type=click.Choice(["DEBUG", "INFO", "WARN", "ERROR", "FATAL"]),
|
|
1738
|
+
default="DEBUG",
|
|
1739
|
+
help="Minimum log level to export",
|
|
1740
|
+
)
|
|
1741
|
+
@click.option("--dry-run", is_flag=True, help="Show what would be exported without sending")
|
|
1742
|
+
def trace_logs_cmd(endpoint, log_file, since, min_level, dry_run):
|
|
1743
|
+
"""Export structured CDH logs to an OTLP/HTTP collector.
|
|
1744
|
+
|
|
1745
|
+
Logs are written as NDJSON to ~/.cdh/logs/app.jsonl. This command
|
|
1746
|
+
converts them to OTLP logs format and ships them to a collector.
|
|
1747
|
+
|
|
1748
|
+
\b
|
|
1749
|
+
Examples:
|
|
1750
|
+
cdh aidlc trace logs
|
|
1751
|
+
cdh aidlc trace logs --endpoint http://tempo:4318 --min-level WARN
|
|
1752
|
+
cdh aidlc trace logs --dry-run
|
|
1753
|
+
"""
|
|
1754
|
+
import os as _os
|
|
1755
|
+
from datetime import datetime, timezone as _tz
|
|
1756
|
+
from pathlib import Path as _Path
|
|
1757
|
+
|
|
1758
|
+
from cdh.logging import LogLevel, export_logs
|
|
1759
|
+
|
|
1760
|
+
eff_endpoint = endpoint or _os.environ.get("OTEL_EXPORTER_OTLP_ENDPOINT")
|
|
1761
|
+
log_path = _Path(log_file).expanduser() if log_file else None
|
|
1762
|
+
|
|
1763
|
+
since_dt = None
|
|
1764
|
+
if since:
|
|
1765
|
+
try:
|
|
1766
|
+
since_dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
|
1767
|
+
except ValueError:
|
|
1768
|
+
since_dt = None
|
|
1769
|
+
|
|
1770
|
+
level = LogLevel[min_level.upper()]
|
|
1771
|
+
|
|
1772
|
+
counters = export_logs(
|
|
1773
|
+
log_file=log_path,
|
|
1774
|
+
since=since_dt,
|
|
1775
|
+
min_level=level,
|
|
1776
|
+
endpoint=eff_endpoint,
|
|
1777
|
+
dry_run=dry_run,
|
|
1778
|
+
)
|
|
1779
|
+
|
|
1780
|
+
mode = "(dry-run) " if dry_run else ""
|
|
1781
|
+
click.echo(
|
|
1782
|
+
f"{mode}Log export summary: "
|
|
1783
|
+
f"read={counters['read']} exported={counters['exported']} "
|
|
1784
|
+
f"failed={counters['failed']}"
|
|
1785
|
+
)
|
|
1786
|
+
if not dry_run and counters["failed"] > 0:
|
|
1787
|
+
raise click.Abort()
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
@trace_group.command("tail", short_help="Tail live CDH log stream")
|
|
1791
|
+
@click.option("--lines", "-n", type=int, default=50, help="Number of recent lines to show")
|
|
1792
|
+
@click.option("--follow", "-f", is_flag=True, help="Follow log file (like tail -f)")
|
|
1793
|
+
@click.option("--level", type=click.Choice(["DEBUG", "INFO", "WARN", "ERROR"]), default="INFO", help="Minimum level")
|
|
1794
|
+
def trace_tail_cmd(lines, follow, level):
|
|
1795
|
+
"""Tail recent CDH log entries or follow live.
|
|
1796
|
+
|
|
1797
|
+
\b
|
|
1798
|
+
Examples:
|
|
1799
|
+
cdh aidlc trace tail
|
|
1800
|
+
cdh aidlc trace tail -n 100 -f
|
|
1801
|
+
cdh aidlc trace tail --level ERROR
|
|
1802
|
+
"""
|
|
1803
|
+
import os as _os
|
|
1804
|
+
from pathlib import Path as _Path
|
|
1805
|
+
|
|
1806
|
+
from cdh.logging import DEFAULT_LOG_DIR, DEFAULT_LOG_FILE, LogLevel
|
|
1807
|
+
|
|
1808
|
+
log_path = _Path(os.environ.get("CDH_LOG_DIR", str(DEFAULT_LOG_DIR))) / DEFAULT_LOG_FILE
|
|
1809
|
+
|
|
1810
|
+
if not log_path.exists():
|
|
1811
|
+
click.echo(f"No log file found at {log_path}", err=True)
|
|
1812
|
+
return
|
|
1813
|
+
|
|
1814
|
+
import threading
|
|
1815
|
+
import time
|
|
1816
|
+
|
|
1817
|
+
min_level = LogLevel[level.upper()].python_level
|
|
1818
|
+
|
|
1819
|
+
def _tail():
|
|
1820
|
+
with open(log_path, "r", encoding="utf-8", errors="replace") as fh:
|
|
1821
|
+
if not follow:
|
|
1822
|
+
# Show last N lines
|
|
1823
|
+
all_lines = fh.readlines()
|
|
1824
|
+
for line in all_lines[-lines:]:
|
|
1825
|
+
_print_line(line, min_level)
|
|
1826
|
+
return
|
|
1827
|
+
|
|
1828
|
+
# Follow mode
|
|
1829
|
+
fh.seek(0, 2)
|
|
1830
|
+
while True:
|
|
1831
|
+
line = fh.readline()
|
|
1832
|
+
if not line:
|
|
1833
|
+
time.sleep(0.1)
|
|
1834
|
+
continue
|
|
1835
|
+
_print_line(line, min_level)
|
|
1836
|
+
|
|
1837
|
+
def _print_line(line, min_level):
|
|
1838
|
+
import logging
|
|
1839
|
+
try:
|
|
1840
|
+
import json
|
|
1841
|
+
rec = json.loads(line.strip())
|
|
1842
|
+
level_str = rec.get("severityText", "INFO")
|
|
1843
|
+
level_map = {"DEBUG": logging.DEBUG, "INFO": logging.INFO, "WARN": logging.WARNING, "ERROR": logging.ERROR, "FATAL": logging.CRITICAL}
|
|
1844
|
+
if level_map.get(level_str, logging.INFO) < min_level:
|
|
1845
|
+
return
|
|
1846
|
+
ts = rec.get("timeUnixNano", "")
|
|
1847
|
+
if ts:
|
|
1848
|
+
try:
|
|
1849
|
+
from datetime import datetime
|
|
1850
|
+
dt = datetime.fromtimestamp(int(ts[:10]) / 1e9, tz=_tz.utc)
|
|
1851
|
+
ts = dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
1852
|
+
except Exception:
|
|
1853
|
+
ts = ts[:10]
|
|
1854
|
+
body = rec.get("body", "")
|
|
1855
|
+
attrs = {a["key"]: _attr_value(a["value"]) for a in rec.get("attributes", []) if a["key"].startswith("cdh.")}
|
|
1856
|
+
fr_id = attrs.get("cdh.fr_id", "")
|
|
1857
|
+
session = attrs.get("cdh.session_id", "")
|
|
1858
|
+
extra = []
|
|
1859
|
+
if fr_id:
|
|
1860
|
+
extra.append(fr_id)
|
|
1861
|
+
if session:
|
|
1862
|
+
extra.append(session[:8])
|
|
1863
|
+
extra_str = f" [{', '.join(extra)}]" if extra else ""
|
|
1864
|
+
click.echo(f"{ts} {level_str:5} {body}{extra_str}")
|
|
1865
|
+
except Exception:
|
|
1866
|
+
pass
|
|
1867
|
+
|
|
1868
|
+
def _attr_value(v):
|
|
1869
|
+
for k in ("stringValue", "intValue", "doubleValue", "boolValue"):
|
|
1870
|
+
if k in v:
|
|
1871
|
+
return v[k]
|
|
1872
|
+
return str(v)
|
|
1873
|
+
|
|
1874
|
+
if follow:
|
|
1875
|
+
t = threading.Thread(target=_tail, daemon=True)
|
|
1876
|
+
t.start()
|
|
1877
|
+
t.join()
|
|
1878
|
+
else:
|
|
1879
|
+
_tail()
|
|
1880
|
+
|
|
1881
|
+
|
|
1882
|
+
@tools_group.command("install")
|
|
1883
|
+
@click.argument("path", required=False, default=".")
|
|
1884
|
+
def tools_install(path):
|
|
1885
|
+
"""Install real AIDLC tool implementations into aidlc/tools/.
|
|
1886
|
+
|
|
1887
|
+
Replaces stub files generated by 'cdh aidlc project init' with
|
|
1888
|
+
working implementations (generate_shared, contract_diff, deploy_stack).
|
|
1889
|
+
"""
|
|
1890
|
+
from cdh.tools import install_tools as _do_install
|
|
1891
|
+
target = Path(path).expanduser().resolve()
|
|
1892
|
+
installed = _do_install(target)
|
|
1893
|
+
if installed:
|
|
1894
|
+
click.echo(f"Installed {len(installed)} tools:")
|
|
1895
|
+
for t in installed:
|
|
1896
|
+
click.echo(f" ✓ {t}")
|
|
1897
|
+
else:
|
|
1898
|
+
click.echo("No tools needed installation (all up to date)")
|
|
1899
|
+
|
|
1900
|
+
|
|
1901
|
+
@tools_group.command("status")
|
|
1902
|
+
@click.argument("path", required=False, default=".")
|
|
1903
|
+
def tools_status(path):
|
|
1904
|
+
"""Show installation status of each AIDLC tool."""
|
|
1905
|
+
from cdh.tools import tools_status as _do_status
|
|
1906
|
+
target = Path(path).expanduser().resolve()
|
|
1907
|
+
results = _do_status(target)
|
|
1908
|
+
click.echo("Tool Status:")
|
|
1909
|
+
for name, status in results.items():
|
|
1910
|
+
icon = "✓" if status == "installed" else "!" if status == "stub" else "✗"
|
|
1911
|
+
click.echo(f" {icon} {name}: {status}")
|
|
1912
|
+
if any(s == "stub" for s in results.values()):
|
|
1913
|
+
click.echo("\nRun 'cdh aidlc tools install' to replace stubs with real implementations.")
|
|
1914
|
+
|
|
1915
|
+
|
|
1916
|
+
@tools_group.command("update")
|
|
1917
|
+
@click.argument("path", required=False, default=".")
|
|
1918
|
+
def tools_update(path):
|
|
1919
|
+
"""Update installed AIDLC tools to the latest version."""
|
|
1920
|
+
from cdh.tools import update_tools as _do_update
|
|
1921
|
+
target = Path(path).expanduser().resolve()
|
|
1922
|
+
updated = _do_update(target)
|
|
1923
|
+
if updated:
|
|
1924
|
+
click.echo(f"Updated: {', '.join(updated)}")
|
|
1925
|
+
else:
|
|
1926
|
+
click.echo("All tools are up to date")
|
|
1927
|
+
|
|
1928
|
+
|
|
1929
|
+
@tools_group.command("test")
|
|
1930
|
+
@click.argument("path", required=False, default=".")
|
|
1931
|
+
@click.option("--tool", default=None, help="Test specific tool: generate_shared, contract_diff, deploy_stack")
|
|
1932
|
+
def tools_test(path, tool):
|
|
1933
|
+
"""Run self-tests for installed AIDLC tools."""
|
|
1934
|
+
import subprocess
|
|
1935
|
+
import sys
|
|
1936
|
+
target = Path(path).expanduser().resolve()
|
|
1937
|
+
tools_dir = target / "aidlc" / "tools"
|
|
1938
|
+
if not tools_dir.exists():
|
|
1939
|
+
click.echo("No tools directory found. Run 'cdh aidlc tools install' first.")
|
|
1940
|
+
raise click.Abort()
|
|
1941
|
+
|
|
1942
|
+
tools_to_test = ["generate_shared", "contract_diff", "deploy_stack"]
|
|
1943
|
+
if tool:
|
|
1944
|
+
if tool not in tools_to_test:
|
|
1945
|
+
click.echo(f"Unknown tool: {tool}")
|
|
1946
|
+
raise click.Abort()
|
|
1947
|
+
tools_to_test = [tool]
|
|
1948
|
+
|
|
1949
|
+
all_passed = True
|
|
1950
|
+
for t in tools_to_test:
|
|
1951
|
+
tool_file = tools_dir / f"{t}.py"
|
|
1952
|
+
if not tool_file.exists():
|
|
1953
|
+
click.echo(f" ✗ {t}: not installed")
|
|
1954
|
+
all_passed = False
|
|
1955
|
+
continue
|
|
1956
|
+
# Run tool with --self-test or -h to verify it runs
|
|
1957
|
+
try:
|
|
1958
|
+
result = subprocess.run(
|
|
1959
|
+
[sys.executable, str(tool_file), "--help"],
|
|
1960
|
+
capture_output=True,
|
|
1961
|
+
timeout=10,
|
|
1962
|
+
)
|
|
1963
|
+
if result.returncode == 0:
|
|
1964
|
+
click.echo(f" ✓ {t}: runs successfully")
|
|
1965
|
+
else:
|
|
1966
|
+
click.echo(f" ✗ {t}: failed (exit code {result.returncode})")
|
|
1967
|
+
if result.stderr:
|
|
1968
|
+
click.echo(f" stderr: {result.stderr.decode()[:200]}")
|
|
1969
|
+
all_passed = False
|
|
1970
|
+
except subprocess.TimeoutExpired:
|
|
1971
|
+
click.echo(f" ✗ {t}: timeout")
|
|
1972
|
+
all_passed = False
|
|
1973
|
+
except Exception as e:
|
|
1974
|
+
click.echo(f" ✗ {t}: error - {e}")
|
|
1975
|
+
all_passed = False
|
|
1976
|
+
|
|
1977
|
+
if not all_passed:
|
|
1978
|
+
raise click.Abort()
|
|
1979
|
+
|
|
1980
|
+
|
|
1981
|
+
# ── status ───────────────────────────────────────────────────
|
|
1982
|
+
|
|
1983
|
+
|
|
1984
|
+
@aidlc.command("status")
|
|
1985
|
+
@click.argument("path", required=False, default=".")
|
|
1986
|
+
@click.option("--with-history", is_flag=True, help="Include last 5 validate runs")
|
|
1987
|
+
def status_cmd(path, with_history):
|
|
1988
|
+
"""Show AIDLC project health overview.
|
|
1989
|
+
|
|
1990
|
+
Aggregates phase, gates, tools status, and basic project info.
|
|
1991
|
+
Use --with-history to include the last 5 validate runs.
|
|
1992
|
+
"""
|
|
1993
|
+
from cdh.project_loader import CdhProjectLoader
|
|
1994
|
+
|
|
1995
|
+
target = Path(path).expanduser().resolve()
|
|
1996
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(target)
|
|
1997
|
+
if cdh_dir is None:
|
|
1998
|
+
click.echo("No .cdh/ directory found. Run 'cdh aidlc project init' first.")
|
|
1999
|
+
return
|
|
2000
|
+
|
|
2001
|
+
state = CdhProjectLoader.load_project_state(cdh_dir)
|
|
2002
|
+
config_data = CdhProjectLoader.load_project_config(cdh_dir)
|
|
2003
|
+
|
|
2004
|
+
click.echo("╔══════════════════════════════════════════╗")
|
|
2005
|
+
click.echo("║ AIDLC Project Status ║")
|
|
2006
|
+
click.echo("╚══════════════════════════════════════════╝")
|
|
2007
|
+
click.echo(f" Name: {config_data.get('name', target.name)}")
|
|
2008
|
+
click.echo(f" Path: {target}")
|
|
2009
|
+
|
|
2010
|
+
phase = state.get("current_phase", "?")
|
|
2011
|
+
click.echo(f" Phase: {phase}")
|
|
2012
|
+
completed = state.get("completed_phases", [])
|
|
2013
|
+
if completed:
|
|
2014
|
+
click.echo(f" Done: {', '.join(completed)}")
|
|
2015
|
+
|
|
2016
|
+
gates = state.get("gate_results", {})
|
|
2017
|
+
if gates:
|
|
2018
|
+
click.echo(f" Gates: {len(gates)} recorded")
|
|
2019
|
+
for name, g in list(gates.items())[:5]:
|
|
2020
|
+
icon = "✓" if g.get("status") == "passed" else "✗"
|
|
2021
|
+
click.echo(f" {icon} {name}: {g.get('status', '?')}")
|
|
2022
|
+
else:
|
|
2023
|
+
click.echo(" Gates: (none)")
|
|
2024
|
+
|
|
2025
|
+
project_yaml = target / "aidlc" / "project.yaml"
|
|
2026
|
+
if project_yaml.exists():
|
|
2027
|
+
import yaml
|
|
2028
|
+
data = yaml.safe_load(project_yaml.read_text(encoding="utf-8")) or {}
|
|
2029
|
+
comps = data.get("stack", {}).get("components", [])
|
|
2030
|
+
click.echo(f" Components: {len(comps)} defined")
|
|
2031
|
+
for c in comps:
|
|
2032
|
+
click.echo(f" - {c.get('id', '?')} ({c.get('fr_prefix', '?')})")
|
|
2033
|
+
|
|
2034
|
+
tools_dir = target / "aidlc" / "tools"
|
|
2035
|
+
if tools_dir.exists():
|
|
2036
|
+
tool_files = list(tools_dir.glob("*.py"))
|
|
2037
|
+
click.echo(f" Tools: {len(tool_files)} files in aidlc/tools/")
|
|
2038
|
+
stub_count = 0
|
|
2039
|
+
for tf in tool_files:
|
|
2040
|
+
content = tf.read_text(encoding="utf-8")
|
|
2041
|
+
if "not yet implemented" in content:
|
|
2042
|
+
stub_count += 1
|
|
2043
|
+
if stub_count:
|
|
2044
|
+
click.echo(f" ⚠ {stub_count} tool(s) are still stubs — run 'cdh aidlc tools install'")
|
|
2045
|
+
else:
|
|
2046
|
+
click.echo(" Tools: aidlc/tools/ not found")
|
|
2047
|
+
|
|
2048
|
+
if with_history:
|
|
2049
|
+
history = CdhProjectLoader.get_validate_history(cdh_dir)
|
|
2050
|
+
if history:
|
|
2051
|
+
recent = history[-5:]
|
|
2052
|
+
click.echo("\n Validate history (last 5):")
|
|
2053
|
+
click.echo(f" {'TIMESTAMP':<22} {'PASS':<6} {'DUR(ms)':<8}")
|
|
2054
|
+
for entry in recent:
|
|
2055
|
+
ts = entry.get("timestamp", "?")
|
|
2056
|
+
passed = "✓" if entry.get("passed") else "✗"
|
|
2057
|
+
dur = entry.get("duration_ms", 0)
|
|
2058
|
+
click.echo(f" {ts:<22} {passed:<6} {dur:<8}")
|
|
2059
|
+
else:
|
|
2060
|
+
click.echo("\n Validate history: (none recorded)")
|
|
2061
|
+
|
|
2062
|
+
|
|
2063
|
+
# ── config ───────────────────────────────────────────────────
|
|
2064
|
+
|
|
2065
|
+
|
|
2066
|
+
@aidlc.group("config", short_help="Manage project configuration")
|
|
2067
|
+
def config_group():
|
|
2068
|
+
"""Manage AIDLC project configuration (project.yaml).
|
|
2069
|
+
|
|
2070
|
+
\b
|
|
2071
|
+
Sub-commands:
|
|
2072
|
+
show Display current configuration
|
|
2073
|
+
component <id> Add a component to the project
|
|
2074
|
+
provider <name> Set the cloud provider (tcb|aliyun)
|
|
2075
|
+
list List all components (kind/tech/owns/fr_prefix)
|
|
2076
|
+
rm <component-id> Remove a component and delete apps/<owns>/
|
|
2077
|
+
diff <other-path> Show diff of components/configs vs another project
|
|
2078
|
+
validate Validate project.yaml schema & FR prefix consistency
|
|
2079
|
+
export <file> Export effective config as JSON
|
|
2080
|
+
import <file> Import config from JSON
|
|
2081
|
+
provider aliyun Generate aidlc/providers/aliyun/ templates if missing
|
|
2082
|
+
"""
|
|
2083
|
+
|
|
2084
|
+
|
|
2085
|
+
@config_group.command("validate-state")
|
|
2086
|
+
@click.argument("path", required=False, default=".")
|
|
2087
|
+
def config_validate_state(path):
|
|
2088
|
+
"""Validate .cdh/state.json against its JSON Schema."""
|
|
2089
|
+
from cdh.project_loader import CdhProjectLoader
|
|
2090
|
+
target = Path(path).expanduser().resolve()
|
|
2091
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(target)
|
|
2092
|
+
if cdh_dir is None:
|
|
2093
|
+
click.echo("No .cdh/ directory found.")
|
|
2094
|
+
raise click.Abort()
|
|
2095
|
+
valid, errors = CdhProjectLoader.validate_state_schema(cdh_dir)
|
|
2096
|
+
if valid:
|
|
2097
|
+
click.echo("state.json: valid")
|
|
2098
|
+
return
|
|
2099
|
+
click.echo("state.json: invalid")
|
|
2100
|
+
for error in errors:
|
|
2101
|
+
click.echo(f"- {error}")
|
|
2102
|
+
raise click.Abort()
|
|
2103
|
+
|
|
2104
|
+
|
|
2105
|
+
@click.argument("path", required=False, default=".")
|
|
2106
|
+
def config_show(path):
|
|
2107
|
+
"""Display the current AIDLC project configuration."""
|
|
2108
|
+
target = Path(path).expanduser().resolve()
|
|
2109
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2110
|
+
if not yaml_path.exists():
|
|
2111
|
+
click.echo("aidlc/project.yaml not found")
|
|
2112
|
+
return
|
|
2113
|
+
import yaml
|
|
2114
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8"))
|
|
2115
|
+
click.echo(yaml.dump(data, default_flow_style=False).strip())
|
|
2116
|
+
|
|
2117
|
+
|
|
2118
|
+
@config_group.command("component")
|
|
2119
|
+
@click.argument("component_id")
|
|
2120
|
+
@click.argument("path", required=False, default=".")
|
|
2121
|
+
def config_component(component_id, path):
|
|
2122
|
+
"""Add a component to the project (e.g. web, backend, wxa)."""
|
|
2123
|
+
target = Path(path).expanduser().resolve()
|
|
2124
|
+
try:
|
|
2125
|
+
added = add_component(target, component_id)
|
|
2126
|
+
except (ValueError, FileNotFoundError) as e:
|
|
2127
|
+
click.echo(f"Error: {str(e) or type(e).__name__}")
|
|
2128
|
+
raise click.Abort()
|
|
2129
|
+
if added:
|
|
2130
|
+
click.echo(f"Component '{component_id}' added")
|
|
2131
|
+
else:
|
|
2132
|
+
click.echo(f"Component '{component_id}' already exists")
|
|
2133
|
+
|
|
2134
|
+
|
|
2135
|
+
@config_group.command("provider")
|
|
2136
|
+
@click.argument("provider", type=click.Choice(["tcb", "aliyun"]))
|
|
2137
|
+
@click.argument("path", required=False, default=".")
|
|
2138
|
+
def config_provider(provider, path):
|
|
2139
|
+
"""Set the cloud provider (tcb or aliyun)."""
|
|
2140
|
+
target = Path(path).expanduser().resolve()
|
|
2141
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2142
|
+
if not yaml_path.exists():
|
|
2143
|
+
click.echo("aidlc/project.yaml not found. Run 'cdh aidlc project init' first.")
|
|
2144
|
+
return
|
|
2145
|
+
import yaml
|
|
2146
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
2147
|
+
cross = data.setdefault("stack", {}).setdefault("cross_cutting", {})
|
|
2148
|
+
cross["provider"] = provider
|
|
2149
|
+
yaml_path.write_text(yaml.dump(data, default_flow_style=False), encoding="utf-8")
|
|
2150
|
+
|
|
2151
|
+
providers_dir = target / "aidlc" / "providers"
|
|
2152
|
+
if providers_dir.exists():
|
|
2153
|
+
click.echo(f"Provider set to: {provider}")
|
|
2154
|
+
click.echo(f" Provider config at: aidlc/providers/{provider}/")
|
|
2155
|
+
for pf in (providers_dir / provider).glob("*.yaml"):
|
|
2156
|
+
click.echo(f" - {pf.name}")
|
|
2157
|
+
else:
|
|
2158
|
+
click.echo(f"Provider set to: {provider}")
|
|
2159
|
+
click.echo(" (aidlc/providers/ directory not scaffolded — run init with --with-ci)")
|
|
2160
|
+
|
|
2161
|
+
|
|
2162
|
+
@config_group.command("list")
|
|
2163
|
+
@click.argument("path", required=False, default=".")
|
|
2164
|
+
def config_list(path):
|
|
2165
|
+
"""List all components with kind/tech/owns/fr_prefix."""
|
|
2166
|
+
target = Path(path).expanduser().resolve()
|
|
2167
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2168
|
+
if not yaml_path.exists():
|
|
2169
|
+
click.echo("aidlc/project.yaml not found")
|
|
2170
|
+
return
|
|
2171
|
+
import yaml
|
|
2172
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
2173
|
+
comps = data.get("stack", {}).get("components", []) or []
|
|
2174
|
+
if not comps:
|
|
2175
|
+
click.echo("(no components defined)")
|
|
2176
|
+
return
|
|
2177
|
+
click.echo(f"{'ID':<10} {'KIND':<14} {'TECH':<10} {'OWNS':<22} {'FR_PREFIX'}")
|
|
2178
|
+
click.echo("-" * 70)
|
|
2179
|
+
for c in comps:
|
|
2180
|
+
click.echo(
|
|
2181
|
+
f"{c.get('id', '?'):<10} "
|
|
2182
|
+
f"{c.get('kind', '?'):<14} "
|
|
2183
|
+
f"{c.get('tech', '?'):<10} "
|
|
2184
|
+
f"{c.get('owns', '?'):<22} "
|
|
2185
|
+
f"{c.get('fr_prefix', '?')}"
|
|
2186
|
+
)
|
|
2187
|
+
|
|
2188
|
+
|
|
2189
|
+
@config_group.command("rm")
|
|
2190
|
+
@click.argument("component_id")
|
|
2191
|
+
@click.argument("path", required=False, default=".")
|
|
2192
|
+
@click.option("--keep-files", is_flag=True, help="Don't delete apps/<owns>/ on disk")
|
|
2193
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
2194
|
+
def config_rm(component_id, path, keep_files, yes):
|
|
2195
|
+
"""Remove a component from project.yaml and delete apps/<owns>/."""
|
|
2196
|
+
target = Path(path).expanduser().resolve()
|
|
2197
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2198
|
+
if not yaml_path.exists():
|
|
2199
|
+
click.echo("aidlc/project.yaml not found")
|
|
2200
|
+
raise click.Abort()
|
|
2201
|
+
import yaml
|
|
2202
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
2203
|
+
comps = data.get("stack", {}).get("components", []) or []
|
|
2204
|
+
|
|
2205
|
+
match = next((c for c in comps if c.get("id") == component_id), None)
|
|
2206
|
+
if match is None:
|
|
2207
|
+
click.echo(f"Component '{component_id}' not found in project.yaml")
|
|
2208
|
+
raise click.Abort()
|
|
2209
|
+
|
|
2210
|
+
owns = match.get("owns", "")
|
|
2211
|
+
if owns and (target / owns).exists() and not keep_files:
|
|
2212
|
+
if not yes:
|
|
2213
|
+
click.confirm(
|
|
2214
|
+
f"Delete directory '{owns}/' and all its contents?",
|
|
2215
|
+
abort=True,
|
|
2216
|
+
)
|
|
2217
|
+
|
|
2218
|
+
comps = [c for c in comps if c.get("id") != component_id]
|
|
2219
|
+
data["stack"]["components"] = comps
|
|
2220
|
+
yaml_path.write_text(yaml.dump(data, default_flow_style=False), encoding="utf-8")
|
|
2221
|
+
|
|
2222
|
+
deleted = False
|
|
2223
|
+
if owns and not keep_files:
|
|
2224
|
+
import shutil
|
|
2225
|
+
target_dir = target / owns
|
|
2226
|
+
if target_dir.exists():
|
|
2227
|
+
shutil.rmtree(target_dir)
|
|
2228
|
+
deleted = True
|
|
2229
|
+
|
|
2230
|
+
click.echo(f"Component '{component_id}' removed from project.yaml")
|
|
2231
|
+
if deleted:
|
|
2232
|
+
click.echo(f" Deleted directory: {owns}/")
|
|
2233
|
+
elif keep_files and owns:
|
|
2234
|
+
click.echo(f" Kept directory: {owns}/ (--keep-files)")
|
|
2235
|
+
|
|
2236
|
+
|
|
2237
|
+
@config_group.command("diff")
|
|
2238
|
+
@click.argument("other_path")
|
|
2239
|
+
@click.argument("path", required=False, default=".")
|
|
2240
|
+
def config_diff(other_path, path):
|
|
2241
|
+
"""Show difference of components/configs between two AIDLC projects."""
|
|
2242
|
+
left = Path(path).expanduser().resolve()
|
|
2243
|
+
right = Path(other_path).expanduser().resolve()
|
|
2244
|
+
|
|
2245
|
+
left_yaml = left / "aidlc" / "project.yaml"
|
|
2246
|
+
right_yaml = right / "aidlc" / "project.yaml"
|
|
2247
|
+
|
|
2248
|
+
if not left_yaml.exists():
|
|
2249
|
+
click.echo(f"Left project missing: {left_yaml}")
|
|
2250
|
+
raise click.Abort()
|
|
2251
|
+
if not right_yaml.exists():
|
|
2252
|
+
click.echo(f"Right project missing: {right_yaml}")
|
|
2253
|
+
raise click.Abort()
|
|
2254
|
+
|
|
2255
|
+
import yaml
|
|
2256
|
+
left_data = yaml.safe_load(left_yaml.read_text(encoding="utf-8")) or {}
|
|
2257
|
+
right_data = yaml.safe_load(right_yaml.read_text(encoding="utf-8")) or {}
|
|
2258
|
+
|
|
2259
|
+
left_comps = {c.get("id"): c for c in left_data.get("stack", {}).get("components", []) or []}
|
|
2260
|
+
right_comps = {c.get("id"): c for c in right_data.get("stack", {}).get("components", []) or []}
|
|
2261
|
+
|
|
2262
|
+
click.echo(f"Diff: {left} vs {right}")
|
|
2263
|
+
click.echo("=" * 70)
|
|
2264
|
+
|
|
2265
|
+
only_left = sorted(set(left_comps) - set(right_comps))
|
|
2266
|
+
only_right = sorted(set(right_comps) - set(left_comps))
|
|
2267
|
+
common = sorted(set(left_comps) & set(right_comps))
|
|
2268
|
+
|
|
2269
|
+
if only_left:
|
|
2270
|
+
click.echo(f"Only in {left.name}:")
|
|
2271
|
+
for cid in only_left:
|
|
2272
|
+
click.echo(f" - {cid}")
|
|
2273
|
+
if only_right:
|
|
2274
|
+
click.echo(f"Only in {right.name}:")
|
|
2275
|
+
for cid in only_right:
|
|
2276
|
+
click.echo(f" + {cid}")
|
|
2277
|
+
|
|
2278
|
+
changed_comps = [cid for cid in common if left_comps[cid] != right_comps[cid]]
|
|
2279
|
+
if changed_comps:
|
|
2280
|
+
click.echo("Changed components:")
|
|
2281
|
+
for cid in changed_comps:
|
|
2282
|
+
click.echo(f" ~ {cid}")
|
|
2283
|
+
l, r = left_comps[cid], right_comps[cid]
|
|
2284
|
+
for key in sorted(set(l) | set(r)):
|
|
2285
|
+
if l.get(key) != r.get(key):
|
|
2286
|
+
click.echo(f" {key}: {l.get(key)!r} -> {r.get(key)!r}")
|
|
2287
|
+
|
|
2288
|
+
left_cross = left_data.get("stack", {}).get("cross_cutting", {}) or {}
|
|
2289
|
+
right_cross = right_data.get("stack", {}).get("cross_cutting", {}) or {}
|
|
2290
|
+
cross_changed = [
|
|
2291
|
+
k for k in (set(left_cross) | set(right_cross))
|
|
2292
|
+
if left_cross.get(k) != right_cross.get(k)
|
|
2293
|
+
]
|
|
2294
|
+
if cross_changed:
|
|
2295
|
+
click.echo("Cross-cutting changed:")
|
|
2296
|
+
for key in cross_changed:
|
|
2297
|
+
click.echo(f" ~ {key}: {left_cross.get(key)!r} -> {right_cross.get(key)!r}")
|
|
2298
|
+
|
|
2299
|
+
if not (only_left or only_right or changed_comps or cross_changed):
|
|
2300
|
+
click.echo("(no differences — projects are equivalent)")
|
|
2301
|
+
|
|
2302
|
+
|
|
2303
|
+
@config_group.command("validate")
|
|
2304
|
+
@click.argument("path", required=False, default=".")
|
|
2305
|
+
def config_validate(path):
|
|
2306
|
+
"""Validate project.yaml schema and FR prefix consistency."""
|
|
2307
|
+
target = Path(path).expanduser().resolve()
|
|
2308
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2309
|
+
if not yaml_path.exists():
|
|
2310
|
+
click.echo("aidlc/project.yaml not found")
|
|
2311
|
+
raise click.Abort()
|
|
2312
|
+
|
|
2313
|
+
errors: list[str] = []
|
|
2314
|
+
warnings: list[str] = []
|
|
2315
|
+
|
|
2316
|
+
import yaml
|
|
2317
|
+
try:
|
|
2318
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
2319
|
+
except yaml.YAMLError as e:
|
|
2320
|
+
click.echo(f"YAML parse error: {e}")
|
|
2321
|
+
raise click.Abort()
|
|
2322
|
+
|
|
2323
|
+
if not isinstance(data, dict):
|
|
2324
|
+
errors.append("top-level must be a mapping")
|
|
2325
|
+
data = {}
|
|
2326
|
+
|
|
2327
|
+
if "name" not in data:
|
|
2328
|
+
warnings.append("missing top-level 'name'")
|
|
2329
|
+
if "stack" not in data:
|
|
2330
|
+
errors.append("missing top-level 'stack'")
|
|
2331
|
+
else:
|
|
2332
|
+
stack = data["stack"]
|
|
2333
|
+
if not isinstance(stack, dict):
|
|
2334
|
+
errors.append("'stack' must be a mapping")
|
|
2335
|
+
else:
|
|
2336
|
+
comps = stack.get("components", []) or []
|
|
2337
|
+
if not isinstance(comps, list):
|
|
2338
|
+
errors.append("'stack.components' must be a list")
|
|
2339
|
+
else:
|
|
2340
|
+
seen_ids: set[str] = set()
|
|
2341
|
+
seen_prefixes: dict[str, str] = {}
|
|
2342
|
+
for i, c in enumerate(comps):
|
|
2343
|
+
if not isinstance(c, dict):
|
|
2344
|
+
errors.append(f"components[{i}] must be a mapping")
|
|
2345
|
+
continue
|
|
2346
|
+
cid = c.get("id")
|
|
2347
|
+
fp = c.get("fr_prefix")
|
|
2348
|
+
if not cid:
|
|
2349
|
+
errors.append(f"components[{i}] missing 'id'")
|
|
2350
|
+
elif cid in seen_ids:
|
|
2351
|
+
errors.append(f"duplicate component id: {cid}")
|
|
2352
|
+
else:
|
|
2353
|
+
seen_ids.add(cid)
|
|
2354
|
+
if not fp:
|
|
2355
|
+
errors.append(f"components[{i}] ({cid}) missing 'fr_prefix'")
|
|
2356
|
+
else:
|
|
2357
|
+
if fp in seen_prefixes and seen_prefixes[fp] != cid:
|
|
2358
|
+
warnings.append(
|
|
2359
|
+
f"fr_prefix '{fp}' shared by '{seen_prefixes[fp]}' and '{cid}'"
|
|
2360
|
+
)
|
|
2361
|
+
seen_prefixes[fp] = cid or f"#{i}"
|
|
2362
|
+
for key in ("kind", "owns", "tech"):
|
|
2363
|
+
if not c.get(key):
|
|
2364
|
+
warnings.append(f"components[{i}] ({cid}) missing '{key}'")
|
|
2365
|
+
cc = stack.get("cross_cutting", {}) or {}
|
|
2366
|
+
if cc and "fr_prefix" in cc:
|
|
2367
|
+
cc_fp = cc["fr_prefix"]
|
|
2368
|
+
for cid, comp in zip(
|
|
2369
|
+
[c.get("id") for c in comps if isinstance(c, dict)],
|
|
2370
|
+
[c.get("fr_prefix") for c in comps if isinstance(c, dict)],
|
|
2371
|
+
):
|
|
2372
|
+
if comp and comp == cc_fp:
|
|
2373
|
+
errors.append(
|
|
2374
|
+
f"component '{cid}' uses cross-cutting fr_prefix '{cc_fp}'"
|
|
2375
|
+
)
|
|
2376
|
+
|
|
2377
|
+
if errors:
|
|
2378
|
+
click.echo("✗ Validation FAILED")
|
|
2379
|
+
for e in errors:
|
|
2380
|
+
click.echo(f" ERROR: {e}")
|
|
2381
|
+
for w in warnings:
|
|
2382
|
+
click.echo(f" WARN: {w}")
|
|
2383
|
+
raise click.Abort()
|
|
2384
|
+
|
|
2385
|
+
click.echo("✓ Validation passed")
|
|
2386
|
+
if warnings:
|
|
2387
|
+
click.echo(f" ({len(warnings)} warning(s))")
|
|
2388
|
+
for w in warnings:
|
|
2389
|
+
click.echo(f" WARN: {w}")
|
|
2390
|
+
|
|
2391
|
+
|
|
2392
|
+
@config_group.command("export")
|
|
2393
|
+
@click.argument("output_file")
|
|
2394
|
+
@click.argument("path", required=False, default=".")
|
|
2395
|
+
def config_export(output_file, path):
|
|
2396
|
+
"""Export effective config as JSON."""
|
|
2397
|
+
import json
|
|
2398
|
+
from cdh.project_loader import CdhProjectLoader
|
|
2399
|
+
|
|
2400
|
+
target = Path(path).expanduser().resolve()
|
|
2401
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2402
|
+
if not yaml_path.exists():
|
|
2403
|
+
click.echo("aidlc/project.yaml not found")
|
|
2404
|
+
raise click.Abort()
|
|
2405
|
+
|
|
2406
|
+
out = Path(output_file).expanduser().resolve()
|
|
2407
|
+
import yaml
|
|
2408
|
+
data = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
2409
|
+
|
|
2410
|
+
cdh_dir = CdhProjectLoader.find_cdh_dir(target)
|
|
2411
|
+
if cdh_dir is not None:
|
|
2412
|
+
cdh_config = CdhProjectLoader.load_project_config(cdh_dir)
|
|
2413
|
+
if cdh_config:
|
|
2414
|
+
data["_cdh"] = cdh_config
|
|
2415
|
+
|
|
2416
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
2417
|
+
out.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
2418
|
+
click.echo(f"Exported config to {out}")
|
|
2419
|
+
|
|
2420
|
+
|
|
2421
|
+
@config_group.command("import")
|
|
2422
|
+
@click.argument("input_file")
|
|
2423
|
+
@click.argument("path", required=False, default=".")
|
|
2424
|
+
@click.option("--merge", is_flag=True, help="Merge into existing project.yaml instead of replacing")
|
|
2425
|
+
def config_import(input_file, path, merge):
|
|
2426
|
+
"""Import config from a JSON file produced by `config export`."""
|
|
2427
|
+
import json
|
|
2428
|
+
target = Path(path).expanduser().resolve()
|
|
2429
|
+
src = Path(input_file).expanduser().resolve()
|
|
2430
|
+
if not src.exists():
|
|
2431
|
+
click.echo(f"Input file not found: {src}")
|
|
2432
|
+
raise click.Abort()
|
|
2433
|
+
|
|
2434
|
+
payload = json.loads(src.read_text(encoding="utf-8"))
|
|
2435
|
+
payload.pop("_cdh", None)
|
|
2436
|
+
|
|
2437
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2438
|
+
if not yaml_path.exists():
|
|
2439
|
+
click.echo(f"aidlc/project.yaml not found at {target}")
|
|
2440
|
+
raise click.Abort()
|
|
2441
|
+
|
|
2442
|
+
import yaml
|
|
2443
|
+
if merge:
|
|
2444
|
+
existing = yaml.safe_load(yaml_path.read_text(encoding="utf-8")) or {}
|
|
2445
|
+
|
|
2446
|
+
def deep_merge(dst: dict, src_dict: dict) -> dict:
|
|
2447
|
+
for k, v in src_dict.items():
|
|
2448
|
+
if isinstance(v, dict) and isinstance(dst.get(k), dict):
|
|
2449
|
+
deep_merge(dst[k], v)
|
|
2450
|
+
else:
|
|
2451
|
+
dst[k] = v
|
|
2452
|
+
return dst
|
|
2453
|
+
|
|
2454
|
+
existing = deep_merge(existing, payload)
|
|
2455
|
+
out_data = existing
|
|
2456
|
+
else:
|
|
2457
|
+
out_data = payload
|
|
2458
|
+
|
|
2459
|
+
yaml_path.write_text(yaml.dump(out_data, default_flow_style=False), encoding="utf-8")
|
|
2460
|
+
click.echo(
|
|
2461
|
+
f"Imported config from {src} ({'merged' if merge else 'replaced'}) into {yaml_path}"
|
|
2462
|
+
)
|
|
2463
|
+
|
|
2464
|
+
|
|
2465
|
+
@config_group.command("provider-templates")
|
|
2466
|
+
@click.argument("provider", type=click.Choice(["aliyun", "tcb"]))
|
|
2467
|
+
@click.argument("path", required=False, default=".")
|
|
2468
|
+
def config_provider_templates(provider, path):
|
|
2469
|
+
"""Generate aidlc/providers/<name>/ templates if missing (e.g. aliyun)."""
|
|
2470
|
+
target = Path(path).expanduser().resolve()
|
|
2471
|
+
yaml_path = target / "aidlc" / "project.yaml"
|
|
2472
|
+
if not yaml_path.exists():
|
|
2473
|
+
click.echo("aidlc/project.yaml not found. Run 'cdh aidlc project init' first.")
|
|
2474
|
+
raise click.Abort()
|
|
2475
|
+
|
|
2476
|
+
from cdh.scaffold import (
|
|
2477
|
+
ALIYUN_PROVIDER_YAML,
|
|
2478
|
+
ALIYUN_DEPLOYMENT_YAML,
|
|
2479
|
+
ALIYUN_PREVIEW_YAML,
|
|
2480
|
+
TCB_PROVIDER_YAML,
|
|
2481
|
+
TCB_DEPLOYMENT_YAML,
|
|
2482
|
+
TCB_PREVIEW_YAML,
|
|
2483
|
+
)
|
|
2484
|
+
|
|
2485
|
+
templates = {
|
|
2486
|
+
"aliyun": ("provider.yaml", ALIYUN_PROVIDER_YAML,
|
|
2487
|
+
"deployment.yaml", ALIYUN_DEPLOYMENT_YAML,
|
|
2488
|
+
"preview.yaml", ALIYUN_PREVIEW_YAML),
|
|
2489
|
+
"tcb": ("provider.yaml", TCB_PROVIDER_YAML,
|
|
2490
|
+
"deployment.yaml", TCB_DEPLOYMENT_YAML,
|
|
2491
|
+
"preview.yaml", TCB_PREVIEW_YAML),
|
|
2492
|
+
}[provider]
|
|
2493
|
+
|
|
2494
|
+
out_dir = target / "aidlc" / "providers" / provider
|
|
2495
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
2496
|
+
|
|
2497
|
+
written: list[str] = []
|
|
2498
|
+
skipped: list[str] = []
|
|
2499
|
+
for fname, content in (
|
|
2500
|
+
(templates[0], templates[1]),
|
|
2501
|
+
(templates[2], templates[3]),
|
|
2502
|
+
(templates[4], templates[5]),
|
|
2503
|
+
):
|
|
2504
|
+
target_file = out_dir / fname
|
|
2505
|
+
if target_file.exists():
|
|
2506
|
+
skipped.append(fname)
|
|
2507
|
+
continue
|
|
2508
|
+
target_file.write_text(content, encoding="utf-8")
|
|
2509
|
+
written.append(fname)
|
|
2510
|
+
|
|
2511
|
+
click.echo(f"aidlc/providers/{provider}/:")
|
|
2512
|
+
for f in written:
|
|
2513
|
+
click.echo(f" + {f} (created)")
|
|
2514
|
+
for f in skipped:
|
|
2515
|
+
click.echo(f" = {f} (already exists, kept)")
|
|
2516
|
+
|
|
2517
|
+
|
|
2518
|
+
# --- session command ---
|
|
2519
|
+
|
|
2520
|
+
@cli.command(short_help="Session management")
|
|
2521
|
+
@click.argument("action", type=click.Choice(["list", "load"]), default="list")
|
|
2522
|
+
@click.argument("session_id", required=False, type=int)
|
|
2523
|
+
def session(action, session_id):
|
|
2524
|
+
"""Manage CDH sessions.
|
|
2525
|
+
|
|
2526
|
+
\b
|
|
2527
|
+
Actions:
|
|
2528
|
+
list List recent sessions
|
|
2529
|
+
load <id> Load a session by ID
|
|
2530
|
+
|
|
2531
|
+
\b
|
|
2532
|
+
Examples:
|
|
2533
|
+
cdh session List recent sessions
|
|
2534
|
+
cdh session list List recent sessions
|
|
2535
|
+
cdh session load 5 Load session with ID 5
|
|
2536
|
+
"""
|
|
2537
|
+
import asyncio
|
|
2538
|
+
from tui.db import DB
|
|
2539
|
+
|
|
2540
|
+
async def run():
|
|
2541
|
+
db = DB()
|
|
2542
|
+
if action == "list":
|
|
2543
|
+
recent = await db.session_get_recent(max_results=20)
|
|
2544
|
+
if not recent:
|
|
2545
|
+
click.echo("No sessions found.")
|
|
2546
|
+
return
|
|
2547
|
+
click.echo("Recent sessions:")
|
|
2548
|
+
for s in recent:
|
|
2549
|
+
title = s.get("title", "Untitled") or "Untitled"
|
|
2550
|
+
aid = s.get("agent_identity", "unknown")
|
|
2551
|
+
sid = s.get("agent_session_id", "")[:8]
|
|
2552
|
+
click.echo(f" session-{s['id']}: {title} ({aid[:30]}... {sid})")
|
|
2553
|
+
elif action == "load":
|
|
2554
|
+
if session_id is None:
|
|
2555
|
+
click.echo("Usage: cdh session load <id>")
|
|
2556
|
+
return
|
|
2557
|
+
s = None
|
|
2558
|
+
try:
|
|
2559
|
+
s = await db.session_get(int(session_id))
|
|
2560
|
+
except (ValueError, TypeError):
|
|
2561
|
+
pass
|
|
2562
|
+
if s is None:
|
|
2563
|
+
s = await db.session_get_by_agent_session_id(session_id)
|
|
2564
|
+
if s is None:
|
|
2565
|
+
click.echo(f"Session {session_id} not found.")
|
|
2566
|
+
return
|
|
2567
|
+
click.echo(f"Session {session_id}:")
|
|
2568
|
+
click.echo(f" Title: {s.get('title', 'Untitled')}")
|
|
2569
|
+
click.echo(f" Agent: {s.get('agent_identity', 'unknown')}")
|
|
2570
|
+
click.echo(f" Agent Session ID: {s.get('agent_session_id', 'N/A')}")
|
|
2571
|
+
|
|
2572
|
+
asyncio.run(run())
|
|
2573
|
+
|
|
2574
|
+
|
|
2575
|
+
# --- trace command ---
|
|
2576
|
+
|
|
2577
|
+
@cli.group(short_help="Trace management (agenttrace)")
|
|
2578
|
+
def trace():
|
|
2579
|
+
"""Manage CDH traces via agenttrace.
|
|
2580
|
+
|
|
2581
|
+
\b
|
|
2582
|
+
Commands:
|
|
2583
|
+
list List recent traces
|
|
2584
|
+
view <session_id> View trace spans for a session
|
|
2585
|
+
dashboard Open the agenttrace web dashboard
|
|
2586
|
+
"""
|
|
2587
|
+
|
|
2588
|
+
|
|
2589
|
+
@trace.command("list", short_help="List recent traces")
|
|
2590
|
+
@click.option("--limit", "-n", default=20, help="Number of traces to show")
|
|
2591
|
+
def trace_list(limit):
|
|
2592
|
+
"""List recent traces from the agenttrace SQLite database."""
|
|
2593
|
+
from cdh.trace import get_tracer, get_db_path
|
|
2594
|
+
db_path = get_db_path()
|
|
2595
|
+
if not db_path.exists():
|
|
2596
|
+
click.echo("No traces yet. Start an agent session to collect traces.")
|
|
2597
|
+
return
|
|
2598
|
+
tracer = get_tracer()
|
|
2599
|
+
try:
|
|
2600
|
+
traces = tracer.get_traces(limit=limit)
|
|
2601
|
+
except Exception as e:
|
|
2602
|
+
click.echo(f"Failed to read traces: {e}")
|
|
2603
|
+
return
|
|
2604
|
+
if not traces:
|
|
2605
|
+
click.echo("No traces found.")
|
|
2606
|
+
return
|
|
2607
|
+
click.echo(f"Recent traces ({len(traces)} spans):")
|
|
2608
|
+
click.echo("")
|
|
2609
|
+
for t in traces:
|
|
2610
|
+
ts = t.get("type", "?")
|
|
2611
|
+
fn = t.get("function", "?")
|
|
2612
|
+
dur = t.get("duration")
|
|
2613
|
+
sid = t.get("session_id", "")[:12]
|
|
2614
|
+
dur_str = f"{dur * 1000:.0f}ms" if dur is not None and dur < 1 else (f"{dur:.1f}s" if dur else "")
|
|
2615
|
+
tags = t.get("tags", {}) or {}
|
|
2616
|
+
agent = tags.get("agent", "") if isinstance(tags, dict) else ""
|
|
2617
|
+
click.echo(f" {ts:<12} {fn:<30} {dur_str:>8} {agent} [{sid}]")
|
|
2618
|
+
|
|
2619
|
+
|
|
2620
|
+
@trace.command("view", short_help="View trace spans for a session")
|
|
2621
|
+
@click.argument("session_id", required=True)
|
|
2622
|
+
def trace_view(session_id):
|
|
2623
|
+
"""View all spans for a given session_id."""
|
|
2624
|
+
from cdh.trace import get_tracer, get_db_path
|
|
2625
|
+
db_path = get_db_path()
|
|
2626
|
+
if not db_path.exists():
|
|
2627
|
+
click.echo("No traces yet.")
|
|
2628
|
+
return
|
|
2629
|
+
tracer = get_tracer()
|
|
2630
|
+
try:
|
|
2631
|
+
traces = tracer.get_traces(limit=500, session_id=session_id)
|
|
2632
|
+
except Exception as e:
|
|
2633
|
+
click.echo(f"Failed to read traces: {e}")
|
|
2634
|
+
return
|
|
2635
|
+
if not traces:
|
|
2636
|
+
click.echo(f"No traces found for session {session_id}.")
|
|
2637
|
+
return
|
|
2638
|
+
click.echo(f"Session: {session_id} | {len(traces)} spans")
|
|
2639
|
+
click.echo("")
|
|
2640
|
+
for t in traces:
|
|
2641
|
+
ts = t.get("type", "?")
|
|
2642
|
+
fn = t.get("function", "?")
|
|
2643
|
+
dur = t.get("duration")
|
|
2644
|
+
dur_str = f"{dur * 1000:.0f}ms" if dur is not None and dur < 1 else (f"{dur:.1f}s" if dur else "")
|
|
2645
|
+
tags = t.get("tags", {}) or {}
|
|
2646
|
+
update_type = tags.get("update_type", "") if isinstance(tags, dict) else ""
|
|
2647
|
+
click.echo(f" {ts:<12} {fn:<30} {dur_str:>8} {update_type}")
|
|
2648
|
+
|
|
2649
|
+
|
|
2650
|
+
@trace.command("prune", short_help="Remove old trace sessions")
|
|
2651
|
+
@click.option("--session", "-s", "session_id", help="Remove a specific session_id")
|
|
2652
|
+
@click.option("--keep-days", "-k", type=int, default=0, help="Keep only sessions from the last N days")
|
|
2653
|
+
@click.option("--before", "-b", "before_date", help="Remove sessions before this date (YYYY-MM-DD)")
|
|
2654
|
+
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be removed without deleting")
|
|
2655
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
2656
|
+
def trace_prune(session_id, keep_days, before_date, dry_run, yes):
|
|
2657
|
+
"""Remove old trace sessions to clean up stale data.
|
|
2658
|
+
|
|
2659
|
+
Examples:
|
|
2660
|
+
|
|
2661
|
+
\b
|
|
2662
|
+
cdh trace prune --keep-days 7 Keep only last 7 days
|
|
2663
|
+
cdh trace prune --before 2026-06-01 Remove sessions before June 1
|
|
2664
|
+
cdh trace prune --session 6f8e0db6 Remove a specific session
|
|
2665
|
+
cdh trace prune --dry-run --keep-days 7 Preview without deleting
|
|
2666
|
+
"""
|
|
2667
|
+
import json
|
|
2668
|
+
import sqlite3
|
|
2669
|
+
from datetime import datetime, timedelta
|
|
2670
|
+
|
|
2671
|
+
db_path = Path.home() / ".cdh" / "traces" / "traces.db"
|
|
2672
|
+
if not db_path.exists():
|
|
2673
|
+
click.echo("No traces database found.")
|
|
2674
|
+
return
|
|
2675
|
+
|
|
2676
|
+
conn = sqlite3.connect(str(db_path))
|
|
2677
|
+
try:
|
|
2678
|
+
# Build the WHERE clause
|
|
2679
|
+
conditions = []
|
|
2680
|
+
params = []
|
|
2681
|
+
|
|
2682
|
+
if session_id:
|
|
2683
|
+
conditions.append("session_id = ?")
|
|
2684
|
+
params.append(session_id)
|
|
2685
|
+
|
|
2686
|
+
if before_date:
|
|
2687
|
+
if len(before_date) == 10:
|
|
2688
|
+
before_date += "T23:59:59"
|
|
2689
|
+
conditions.append("timestamp < ?")
|
|
2690
|
+
params.append(before_date)
|
|
2691
|
+
|
|
2692
|
+
if keep_days > 0:
|
|
2693
|
+
cutoff = (datetime.utcnow() - timedelta(days=keep_days)).isoformat()
|
|
2694
|
+
conditions.append("timestamp < ?")
|
|
2695
|
+
params.append(cutoff)
|
|
2696
|
+
|
|
2697
|
+
if not conditions:
|
|
2698
|
+
click.echo("Specify --session, --keep-days, or --before to prune.")
|
|
2699
|
+
click.echo(" Use --dry-run to preview without deleting.")
|
|
2700
|
+
conn.close()
|
|
2701
|
+
return
|
|
2702
|
+
|
|
2703
|
+
where = " AND ".join(conditions)
|
|
2704
|
+
|
|
2705
|
+
# Find matching sessions
|
|
2706
|
+
stale = conn.execute(
|
|
2707
|
+
f"SELECT DISTINCT session_id FROM traces WHERE {where}", params
|
|
2708
|
+
).fetchall()
|
|
2709
|
+
|
|
2710
|
+
if not stale:
|
|
2711
|
+
click.echo("No matching sessions to prune.")
|
|
2712
|
+
conn.close()
|
|
2713
|
+
return
|
|
2714
|
+
|
|
2715
|
+
stale_ids = [r[0] for r in stale]
|
|
2716
|
+
span_count = conn.execute(
|
|
2717
|
+
f"SELECT COUNT(*) FROM traces WHERE {where}", params
|
|
2718
|
+
).fetchone()[0]
|
|
2719
|
+
|
|
2720
|
+
click.echo(f"Found {len(stale_ids)} session(s) ({span_count} spans) to remove:")
|
|
2721
|
+
for sid in stale_ids:
|
|
2722
|
+
cnt = conn.execute(
|
|
2723
|
+
"SELECT COUNT(*) FROM traces WHERE session_id = ?", (sid,)
|
|
2724
|
+
).fetchone()[0]
|
|
2725
|
+
# Show model info if present
|
|
2726
|
+
models = conn.execute(
|
|
2727
|
+
"SELECT DISTINCT {} FROM traces WHERE session_id = ? AND {} IS NOT NULL".format(
|
|
2728
|
+
"JSON_EXTRACT(data, '$.kwargs.model')", "JSON_EXTRACT(data, '$.kwargs.model')"
|
|
2729
|
+
),
|
|
2730
|
+
(sid,),
|
|
2731
|
+
).fetchall()
|
|
2732
|
+
model_str = f" models: {', '.join(m[0] for m in models if m[0])}" if models else ""
|
|
2733
|
+
click.echo(f" {sid[:20]}... ({cnt} spans){model_str}")
|
|
2734
|
+
|
|
2735
|
+
if dry_run:
|
|
2736
|
+
click.echo("Dry-run mode. No changes made. Pass --yes to actually delete.")
|
|
2737
|
+
conn.close()
|
|
2738
|
+
return
|
|
2739
|
+
|
|
2740
|
+
if not yes:
|
|
2741
|
+
click.confirm("Delete these sessions?", abort=True)
|
|
2742
|
+
|
|
2743
|
+
conn.execute(f"DELETE FROM traces WHERE {where}", params)
|
|
2744
|
+
conn.execute("PRAGMA optimize")
|
|
2745
|
+
conn.commit()
|
|
2746
|
+
click.echo(f"Removed {span_count} spans from {len(stale_ids)} session(s).")
|
|
2747
|
+
finally:
|
|
2748
|
+
conn.close()
|
|
2749
|
+
|
|
2750
|
+
|
|
2751
|
+
@trace.command("dashboard", short_help="Open the built-in trace web dashboard")
|
|
2752
|
+
@click.option("--port", "-p", default=5173, help="Port to serve on")
|
|
2753
|
+
def trace_dashboard(port):
|
|
2754
|
+
"""Start a built-in trace web dashboard (Python stdlib, no npm needed).
|
|
2755
|
+
|
|
2756
|
+
Opens a self-contained web UI at http://localhost:<port> showing
|
|
2757
|
+
all collected trace spans from the agenttrace SQLite database.
|
|
2758
|
+
"""
|
|
2759
|
+
from cdh.trace.dashboard import run_dashboard
|
|
2760
|
+
run_dashboard(port=port)
|
|
2761
|
+
|
|
2762
|
+
|
|
2763
|
+
# --- help command ---
|
|
2764
|
+
|
|
2765
|
+
# --- tui command ---
|
|
2766
|
+
|
|
2767
|
+
@cli.command(short_help="Launch the TUI")
|
|
2768
|
+
@click.option("--project-dir", "-d", default=".", help="Project directory")
|
|
2769
|
+
@click.pass_context
|
|
2770
|
+
def tui(ctx, project_dir):
|
|
2771
|
+
"""Launch the CDH TUI (Textual User Interface).
|
|
2772
|
+
|
|
2773
|
+
By default opens the agent store, showing your configured launcher
|
|
2774
|
+
agents. Pick an agent from the store to launch it.
|
|
2775
|
+
"""
|
|
2776
|
+
setup_logging("INFO")
|
|
2777
|
+
|
|
2778
|
+
ws = Path(project_dir).expanduser().resolve()
|
|
2779
|
+
|
|
2780
|
+
from tui.app import A2TUIApp
|
|
2781
|
+
app = A2TUIApp(project_dir=str(ws))
|
|
2782
|
+
app.run()
|
|
2783
|
+
|
|
2784
|
+
|
|
2785
|
+
# --- help command ---
|
|
2786
|
+
|
|
2787
|
+
@cli.command(short_help="Show help for commands")
|
|
2788
|
+
@click.argument("command", required=False)
|
|
2789
|
+
def help_cmd(command):
|
|
2790
|
+
"""Show help for cdh commands and usage information.
|
|
2791
|
+
|
|
2792
|
+
\b
|
|
2793
|
+
Examples:
|
|
2794
|
+
cdh help Show top-level help
|
|
2795
|
+
cdh help onecode Show `cdh onecode` sub-commands
|
|
2796
|
+
cdh help onecode config Show config sub-commands
|
|
2797
|
+
"""
|
|
2798
|
+
if command:
|
|
2799
|
+
cmd = cli.commands.get(command)
|
|
2800
|
+
if cmd:
|
|
2801
|
+
click.echo(cmd.get_help(click.Context(cmd)))
|
|
2802
|
+
else:
|
|
2803
|
+
click.echo(f"Unknown command: {command}")
|
|
2804
|
+
else:
|
|
2805
|
+
click.echo(cli.get_help(click.Context(cli)))
|
|
2806
|
+
|
|
2807
|
+
|
|
2808
|
+
# --- version command ---
|
|
2809
|
+
|
|
2810
|
+
@cli.command(short_help="Remove ~/.cdh/ global state")
|
|
2811
|
+
def uninstall():
|
|
2812
|
+
"""Remove CDH global state (~/.cdh/) and Python environment.
|
|
2813
|
+
|
|
2814
|
+
\b
|
|
2815
|
+
After running this, uninstall the package itself:
|
|
2816
|
+
pip uninstall cloud-dev-harness (if installed via pip)
|
|
2817
|
+
pnpm remove -g @scemoon/cdh (if installed via pnpm)
|
|
2818
|
+
npm uninstall -g @scemoon/cdh (if installed via npm)
|
|
2819
|
+
|
|
2820
|
+
\b
|
|
2821
|
+
Also check your shell config (~/.zshrc, ~/.bashrc, etc.) for
|
|
2822
|
+
PATH entries pointing to ~/.cdh/python/bin and remove them.
|
|
2823
|
+
"""
|
|
2824
|
+
import shutil
|
|
2825
|
+
|
|
2826
|
+
cdh_dir = Path.home() / ".cdh"
|
|
2827
|
+
|
|
2828
|
+
removed_anything = False
|
|
2829
|
+
|
|
2830
|
+
python_dir = cdh_dir / "python"
|
|
2831
|
+
if python_dir.exists():
|
|
2832
|
+
click.echo(f"Removing Python environment at {python_dir}...")
|
|
2833
|
+
shutil.rmtree(python_dir, ignore_errors=True)
|
|
2834
|
+
removed_anything = True
|
|
2835
|
+
|
|
2836
|
+
if cdh_dir.exists():
|
|
2837
|
+
click.echo(f"Removing global state at {cdh_dir}...")
|
|
2838
|
+
shutil.rmtree(cdh_dir, ignore_errors=True)
|
|
2839
|
+
removed_anything = True
|
|
2840
|
+
|
|
2841
|
+
if not removed_anything:
|
|
2842
|
+
click.echo("Nothing to remove (~/.cdh/ not found).")
|
|
2843
|
+
else:
|
|
2844
|
+
click.echo("")
|
|
2845
|
+
click.echo("Cleanup complete. To finish uninstall:")
|
|
2846
|
+
click.echo("")
|
|
2847
|
+
click.echo(" 1. Remove the package:")
|
|
2848
|
+
click.echo(" pip uninstall cloud-dev-harness")
|
|
2849
|
+
click.echo(" # or: pnpm remove -g @scemoon/cdh")
|
|
2850
|
+
click.echo(" # or: npm uninstall -g @scemoon/cdh")
|
|
2851
|
+
click.echo("")
|
|
2852
|
+
click.echo(" 2. Check your shell config (~/.zshrc, ~/.bashrc, etc.) for:")
|
|
2853
|
+
click.echo(' export PATH="$HOME/.cdh/python/bin:$PATH"')
|
|
2854
|
+
click.echo(" Remove this line if present.")
|
|
2855
|
+
|
|
2856
|
+
|
|
2857
|
+
@cli.command(short_help="Show version info")
|
|
2858
|
+
def version():
|
|
2859
|
+
"""Show CDH version and build information."""
|
|
2860
|
+
from onecode import __version__
|
|
2861
|
+
click.echo(f"cdh version {__version__}")
|
|
2862
|
+
|
|
2863
|
+
|
|
2864
|
+
# --- cls command ---
|
|
2865
|
+
|
|
2866
|
+
@cli.group(short_help="CLS log search for TCB/SCF")
|
|
2867
|
+
def cls():
|
|
2868
|
+
"""Search SCF function logs via Tencent Cloud CLS (Cloud Log Service).
|
|
2869
|
+
|
|
2870
|
+
\b
|
|
2871
|
+
Commands:
|
|
2872
|
+
search Search logs by requestId, keyword, or time range
|
|
2873
|
+
|
|
2874
|
+
\b
|
|
2875
|
+
Examples:
|
|
2876
|
+
cdh cls search --request-id req-xxxxx --function hello
|
|
2877
|
+
cdh cls search --function hello --keyword error
|
|
2878
|
+
cdh cls search --function hello --start-time "2026-08-04 10:00:00"
|
|
2879
|
+
|
|
2880
|
+
\b
|
|
2881
|
+
Environment variables:
|
|
2882
|
+
TENCENTCLOUD_SECRETID Tencent Cloud credential ID
|
|
2883
|
+
TENCENTCLOUD_SECRETKEY Tencent Cloud credential key
|
|
2884
|
+
TCB_ENV_ID TCB environment ID
|
|
2885
|
+
|
|
2886
|
+
\b
|
|
2887
|
+
Note: Requires tencentcloud-sdk-python. Install with:
|
|
2888
|
+
pip install tencentcloud-sdk-python
|
|
2889
|
+
"""
|
|
2890
|
+
|
|
2891
|
+
|
|
2892
|
+
@cls.command("search", short_help="Search SCF logs by requestId, keyword, or time range")
|
|
2893
|
+
@click.option("--request-id", help="SCF RequestId to search for")
|
|
2894
|
+
@click.option("--function", help="Function name to search")
|
|
2895
|
+
@click.option("--namespace", default="default", help="SCF namespace (default: default)")
|
|
2896
|
+
@click.option("--keyword", help="Keyword to search in SCF_Message")
|
|
2897
|
+
@click.option("--topic-id", help="CLS topic ID (auto-detected if not provided)")
|
|
2898
|
+
@click.option("--region", default="ap-shanghai", help="Tencent Cloud region")
|
|
2899
|
+
@click.option("--env", help="TCB environment ID (sets region context)")
|
|
2900
|
+
@click.option("--start-time", help="Start time (ISO format or Unix ms)")
|
|
2901
|
+
@click.option("--end-time", help="End time (ISO format or Unix ms)")
|
|
2902
|
+
@click.option("--limit", type=int, default=100, help="Max results (default: 100)")
|
|
2903
|
+
@click.option("--json", is_flag=True, help="JSON output")
|
|
2904
|
+
def cls_search(request_id, function, namespace, keyword, topic_id, region, env, start_time, end_time, limit, json):
|
|
2905
|
+
"""Search SCF function logs via Tencent Cloud CLS.
|
|
2906
|
+
|
|
2907
|
+
Supports requestId tracing, keyword search, and time-range queries.
|
|
2908
|
+
SCF logs are automatically shipped to CLS since 2021-01-29.
|
|
2909
|
+
|
|
2910
|
+
\b
|
|
2911
|
+
Examples:
|
|
2912
|
+
cdh cls search --request-id req-xxxxx --function hello
|
|
2913
|
+
cdh cls search --function hello --keyword error
|
|
2914
|
+
cdh cls search --function hello --start-time "2026-08-04 10:00:00" --end-time "2026-08-04 12:00:00"
|
|
2915
|
+
cdh cls search --function hello --json
|
|
2916
|
+
"""
|
|
2917
|
+
import os
|
|
2918
|
+
import sys
|
|
2919
|
+
|
|
2920
|
+
if env:
|
|
2921
|
+
os.environ.setdefault("TCB_ENV_ID", env)
|
|
2922
|
+
|
|
2923
|
+
try:
|
|
2924
|
+
from cdh.tools.cls_search import CLSLogSearcher
|
|
2925
|
+
|
|
2926
|
+
searcher = CLSLogSearcher(region=region)
|
|
2927
|
+
|
|
2928
|
+
if request_id:
|
|
2929
|
+
results = searcher.search_by_request_id(
|
|
2930
|
+
request_id=request_id,
|
|
2931
|
+
function_name=function,
|
|
2932
|
+
namespace=namespace,
|
|
2933
|
+
topic_id=topic_id,
|
|
2934
|
+
start_time=start_time,
|
|
2935
|
+
end_time=end_time,
|
|
2936
|
+
limit=limit,
|
|
2937
|
+
)
|
|
2938
|
+
else:
|
|
2939
|
+
results = searcher.search_scf_logs(
|
|
2940
|
+
query=keyword or "*",
|
|
2941
|
+
function_name=function,
|
|
2942
|
+
namespace=namespace,
|
|
2943
|
+
topic_id=topic_id,
|
|
2944
|
+
start_time=start_time,
|
|
2945
|
+
end_time=end_time,
|
|
2946
|
+
limit=limit,
|
|
2947
|
+
)
|
|
2948
|
+
|
|
2949
|
+
if json:
|
|
2950
|
+
import json as _json
|
|
2951
|
+
click.echo(_json.dumps(results, indent=2, ensure_ascii=False))
|
|
2952
|
+
else:
|
|
2953
|
+
for log in results:
|
|
2954
|
+
time_str = log.get("time", "")
|
|
2955
|
+
msg = log.get("message", "")
|
|
2956
|
+
func = log.get("function", "")
|
|
2957
|
+
req_id = log.get("request_id", "")
|
|
2958
|
+
level = log.get("level", "INFO")
|
|
2959
|
+
duration = log.get("duration_ms", 0)
|
|
2960
|
+
status = log.get("status_code", 0)
|
|
2961
|
+
|
|
2962
|
+
header = f"[{time_str}]"
|
|
2963
|
+
if req_id:
|
|
2964
|
+
header += f" {req_id[:16]}..."
|
|
2965
|
+
if func:
|
|
2966
|
+
header += f" {func}"
|
|
2967
|
+
header += f" [{level}]"
|
|
2968
|
+
if duration:
|
|
2969
|
+
header += f" {duration}ms"
|
|
2970
|
+
if status:
|
|
2971
|
+
header += f" {status}"
|
|
2972
|
+
|
|
2973
|
+
click.echo(header)
|
|
2974
|
+
if msg:
|
|
2975
|
+
click.echo(f" {msg}")
|
|
2976
|
+
click.echo("")
|
|
2977
|
+
|
|
2978
|
+
except ImportError as e:
|
|
2979
|
+
click.echo(f"Error: {e}", err=True)
|
|
2980
|
+
click.echo("", err=True)
|
|
2981
|
+
click.echo("Install tencentcloud-sdk-python:", err=True)
|
|
2982
|
+
click.echo(" pip install tencentcloud-sdk-python", err=True)
|
|
2983
|
+
sys.exit(1)
|
|
2984
|
+
except Exception as e:
|
|
2985
|
+
click.echo(f"Error: {e}", err=True)
|
|
2986
|
+
sys.exit(1)
|
|
2987
|
+
|
|
2988
|
+
|
|
2989
|
+
# All onecode subcommands are reachable through `cdh onecode <sub>`
|
|
2990
|
+
# (see the onecode_group defined above). The cdh top-level command
|
|
2991
|
+
# surface is intentionally limited to: tui, onecode, aidlc, session,
|
|
2992
|
+
# help, and version.
|
|
2993
|
+
|
|
2994
|
+
|
|
2995
|
+
def main():
|
|
2996
|
+
cli()
|
|
2997
|
+
|
|
2998
|
+
|
|
2999
|
+
if __name__ == "__main__":
|
|
3000
|
+
main()
|