@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.
Files changed (786) hide show
  1. package/ai-dlc-skill/SKILL.md +106 -0
  2. package/ai-dlc-skill/architecture/project-structure.md +78 -0
  3. package/ai-dlc-skill/brownfield/README.md +36 -0
  4. package/ai-dlc-skill/brownfield/entry.md +45 -0
  5. package/ai-dlc-skill/brownfield/generate-context.sh +64 -0
  6. package/ai-dlc-skill/brownfield/scripts/deps.sh +27 -0
  7. package/ai-dlc-skill/brownfield/scripts/discover.sh +34 -0
  8. package/ai-dlc-skill/brownfield/scripts/extract-api.sh +45 -0
  9. package/ai-dlc-skill/components/backend.md +47 -0
  10. package/ai-dlc-skill/components/desktop.md +63 -0
  11. package/ai-dlc-skill/components/mya.md +219 -0
  12. package/ai-dlc-skill/components/native.md +73 -0
  13. package/ai-dlc-skill/components/tta.md +235 -0
  14. package/ai-dlc-skill/components/web.md +77 -0
  15. package/ai-dlc-skill/components/wxa.md +219 -0
  16. package/ai-dlc-skill/contracts/README.md +25 -0
  17. package/ai-dlc-skill/core/adaptive-flow.md +107 -0
  18. package/ai-dlc-skill/core/security.md +29 -0
  19. package/ai-dlc-skill/core/task-registry.md +153 -0
  20. package/ai-dlc-skill/cross-tool/README.md +20 -0
  21. package/ai-dlc-skill/cross-tool/cline/export.sh +38 -0
  22. package/ai-dlc-skill/cross-tool/copilot/export.sh +41 -0
  23. package/ai-dlc-skill/cross-tool/cursor/export.sh +53 -0
  24. package/ai-dlc-skill/cross-tool/onecode/export.sh +27 -0
  25. package/ai-dlc-skill/cross-tool/opencode/export.sh +38 -0
  26. package/ai-dlc-skill/phases/debug/entry.md +48 -0
  27. package/ai-dlc-skill/phases/debug/lifecycle.md +284 -0
  28. package/ai-dlc-skill/phases/deliver/entry.md +35 -0
  29. package/ai-dlc-skill/phases/deliver/lifecycle.md +185 -0
  30. package/ai-dlc-skill/phases/deliver/prompt.md +54 -0
  31. package/ai-dlc-skill/phases/deliver/rules.md +61 -0
  32. package/ai-dlc-skill/phases/plan/design_system/accessibility.md +268 -0
  33. package/ai-dlc-skill/phases/plan/design_system/atomic_design.md +341 -0
  34. package/ai-dlc-skill/phases/plan/design_system/component_spec.md +398 -0
  35. package/ai-dlc-skill/phases/plan/design_system/design_tokens.md +190 -0
  36. package/ai-dlc-skill/phases/plan/design_system/iconography.md +299 -0
  37. package/ai-dlc-skill/phases/plan/design_system/platform_ui/desktop.md +318 -0
  38. package/ai-dlc-skill/phases/plan/design_system/platform_ui/mya.md +354 -0
  39. package/ai-dlc-skill/phases/plan/design_system/platform_ui/native.md +328 -0
  40. package/ai-dlc-skill/phases/plan/design_system/platform_ui/tta.md +394 -0
  41. package/ai-dlc-skill/phases/plan/design_system/platform_ui/web.md +287 -0
  42. package/ai-dlc-skill/phases/plan/design_system/platform_ui/wxa.md +374 -0
  43. package/ai-dlc-skill/phases/plan/design_system/theme_system.md +318 -0
  44. package/ai-dlc-skill/phases/plan/entry.md +33 -0
  45. package/ai-dlc-skill/phases/plan/lifecycle.md +260 -0
  46. package/ai-dlc-skill/phases/plan/prompt.md +75 -0
  47. package/ai-dlc-skill/phases/plan/rules.md +60 -0
  48. package/ai-dlc-skill/phases/understand/entry.md +36 -0
  49. package/ai-dlc-skill/phases/understand/lifecycle.md +184 -0
  50. package/ai-dlc-skill/phases/understand/prompt.md +58 -0
  51. package/ai-dlc-skill/phases/understand/rules.md +49 -0
  52. package/ai-dlc-skill/phases/verify/config/base_url.md +65 -0
  53. package/ai-dlc-skill/phases/verify/entry.md +59 -0
  54. package/ai-dlc-skill/phases/verify/gates/browser_e2e.md +96 -0
  55. package/ai-dlc-skill/phases/verify/lifecycle.md +217 -0
  56. package/ai-dlc-skill/phases/verify/prompt.md +73 -0
  57. package/ai-dlc-skill/phases/verify/rules.md +91 -0
  58. package/ai-dlc-skill/phases/verify/runners/playwright_local.md +103 -0
  59. package/ai-dlc-skill/practices/bdd.md +119 -0
  60. package/ai-dlc-skill/practices/sdd.md +84 -0
  61. package/ai-dlc-skill/practices/tdd.md +128 -0
  62. package/ai-dlc-skill/providers/README.md +114 -0
  63. package/ai-dlc-skill/providers/aliyun/SKILL.md +204 -0
  64. package/ai-dlc-skill/providers/aliyun/api/rest-api.md +214 -0
  65. package/ai-dlc-skill/providers/aliyun/api/sdk.md +307 -0
  66. package/ai-dlc-skill/providers/aliyun/best-practices/database-design.md +363 -0
  67. package/ai-dlc-skill/providers/aliyun/best-practices/function-design.md +424 -0
  68. package/ai-dlc-skill/providers/aliyun/best-practices/troubleshooting.md +258 -0
  69. package/ai-dlc-skill/providers/aliyun/cli/database.md +263 -0
  70. package/ai-dlc-skill/providers/aliyun/cli/functions.md +263 -0
  71. package/ai-dlc-skill/providers/aliyun/cli/hosting.md +226 -0
  72. package/ai-dlc-skill/providers/aliyun/cli/setup.md +235 -0
  73. package/ai-dlc-skill/providers/aliyun/cli/storage.md +315 -0
  74. package/ai-dlc-skill/providers/aliyun/deployment.yaml +66 -0
  75. package/ai-dlc-skill/providers/aliyun/integration/ci-cd.md +288 -0
  76. package/ai-dlc-skill/providers/aliyun/preview.yaml +84 -0
  77. package/ai-dlc-skill/providers/aliyun/provider.yaml +160 -0
  78. package/ai-dlc-skill/providers/aliyun/security/authentication.md +254 -0
  79. package/ai-dlc-skill/providers/aliyun/services/cdn-hosting.md +220 -0
  80. package/ai-dlc-skill/providers/aliyun/services/database.md +319 -0
  81. package/ai-dlc-skill/providers/aliyun/services/function-compute.md +273 -0
  82. package/ai-dlc-skill/providers/aliyun/services/overview.md +163 -0
  83. package/ai-dlc-skill/providers/aliyun/services/serverless-app-engine.md +266 -0
  84. package/ai-dlc-skill/providers/aliyun/services/storage.md +269 -0
  85. package/ai-dlc-skill/providers/tcb/SKILL.md +208 -0
  86. package/ai-dlc-skill/providers/tcb/api/rest-api.md +338 -0
  87. package/ai-dlc-skill/providers/tcb/api/sdk.md +288 -0
  88. package/ai-dlc-skill/providers/tcb/best-practices/database-design.md +452 -0
  89. package/ai-dlc-skill/providers/tcb/best-practices/function-design.md +502 -0
  90. package/ai-dlc-skill/providers/tcb/best-practices/troubleshooting.md +462 -0
  91. package/ai-dlc-skill/providers/tcb/cli/cls.md +133 -0
  92. package/ai-dlc-skill/providers/tcb/cli/database.md +230 -0
  93. package/ai-dlc-skill/providers/tcb/cli/env.md +207 -0
  94. package/ai-dlc-skill/providers/tcb/cli/functions.md +276 -0
  95. package/ai-dlc-skill/providers/tcb/cli/hosting.md +219 -0
  96. package/ai-dlc-skill/providers/tcb/cli/setup.md +218 -0
  97. package/ai-dlc-skill/providers/tcb/cli/storage.md +233 -0
  98. package/ai-dlc-skill/providers/tcb/deployment.yaml +111 -0
  99. package/ai-dlc-skill/providers/tcb/integration/ci-cd.md +309 -0
  100. package/ai-dlc-skill/providers/tcb/integration/mcp-server.md +371 -0
  101. package/ai-dlc-skill/providers/tcb/preview.yaml +83 -0
  102. package/ai-dlc-skill/providers/tcb/provider.yaml +172 -0
  103. package/ai-dlc-skill/providers/tcb/security/authentication.md +241 -0
  104. package/ai-dlc-skill/providers/tcb/services/cloud-database.md +299 -0
  105. package/ai-dlc-skill/providers/tcb/services/cloud-functions.md +265 -0
  106. package/ai-dlc-skill/providers/tcb/services/cloud-hosting.md +264 -0
  107. package/ai-dlc-skill/providers/tcb/services/cloud-run.md +285 -0
  108. package/ai-dlc-skill/providers/tcb/services/cloud-storage.md +245 -0
  109. package/ai-dlc-skill/providers/tcb/services/overview.md +174 -0
  110. package/ai-dlc-skill/pybdd/README.md +41 -0
  111. package/ai-dlc-skill/pybdd/conftest.py +17 -0
  112. package/ai-dlc-skill/pybdd/example/auth/login.feature +32 -0
  113. package/ai-dlc-skill/pybdd/pytest.ini +5 -0
  114. package/ai-dlc-skill/pybdd/steps/.gitkeep +1 -0
  115. package/ai-dlc-skill/pybdd/steps/test_example_steps.py +65 -0
  116. package/ai-dlc-skill/requirements.md +140 -0
  117. package/ai-dlc-skill/skill.yaml +135 -0
  118. package/ai-dlc-skill/templates/brownfield/context.md +78 -0
  119. package/ai-dlc-skill/templates/integration/CHANGELOG.md +41 -0
  120. package/ai-dlc-skill/templates/integration/asyncapi.yaml +43 -0
  121. package/ai-dlc-skill/templates/integration/contract-diff.md +63 -0
  122. package/ai-dlc-skill/templates/integration/contract-spec.md +49 -0
  123. package/ai-dlc-skill/templates/integration/openapi.yaml +18 -0
  124. package/ai-dlc-skill/templates/integration/runtime-contract.yaml +36 -0
  125. package/ai-dlc-skill/templates/plan/design.md +49 -0
  126. package/ai-dlc-skill/templates/plan/task-list.md +32 -0
  127. package/ai-dlc-skill/templates/project/README.md +126 -0
  128. package/ai-dlc-skill/templates/project/template.md +119 -0
  129. package/ai-dlc-skill/templates/understand/feature.feature +30 -0
  130. package/ai-dlc-skill/templates/understand/intent.md +29 -0
  131. package/ai-dlc-skill/templates/understand/spec-delta.md +47 -0
  132. package/ai-dlc-skill/templates/verify/test-unit.py +27 -0
  133. package/ai-dlc-skill/walkthrough/collect.sh +50 -0
  134. package/ai-dlc-skill/walkthrough/template.md +54 -0
  135. package/cdh/__init__.py +0 -0
  136. package/cdh/__main__.py +4 -0
  137. package/cdh/__pycache__/__init__.cpython-314.pyc +0 -0
  138. package/cdh/__pycache__/cli.cpython-314.pyc +0 -0
  139. package/cdh/__pycache__/cli_logging.cpython-314.pyc +0 -0
  140. package/cdh/__pycache__/config.cpython-314.pyc +0 -0
  141. package/cdh/__pycache__/project_loader.cpython-314.pyc +0 -0
  142. package/cdh/__pycache__/scaffold.cpython-314.pyc +0 -0
  143. package/cdh/__pycache__/session_store.cpython-314.pyc +0 -0
  144. package/cdh/cdh_session_aggregator.py +258 -0
  145. package/cdh/cli.py +3000 -0
  146. package/cdh/cli_logging.py +45 -0
  147. package/cdh/config.py +28 -0
  148. package/cdh/event_loop/__init__.py +7 -0
  149. package/cdh/event_loop/bus.py +123 -0
  150. package/cdh/event_loop/events.py +25 -0
  151. package/cdh/event_loop/runner.py +28 -0
  152. package/cdh/event_loop/scheduler.py +158 -0
  153. package/cdh/generators/__init__.py +35 -0
  154. package/cdh/generators/_runtime.py +176 -0
  155. package/cdh/generators/ada/MANIFEST.toml +14 -0
  156. package/cdh/generators/ada/template.ads.tmpl +7 -0
  157. package/cdh/generators/bash/MANIFEST.toml +14 -0
  158. package/cdh/generators/bash/template.sh.tmpl +16 -0
  159. package/cdh/generators/builtin_index.json +590 -0
  160. package/cdh/generators/c/MANIFEST.toml +14 -0
  161. package/cdh/generators/c/template.h.tmpl +20 -0
  162. package/cdh/generators/cli.py +379 -0
  163. package/cdh/generators/clojure/MANIFEST.toml +14 -0
  164. package/cdh/generators/clojure/template.clj.tmpl +13 -0
  165. package/cdh/generators/cobol/MANIFEST.toml +14 -0
  166. package/cdh/generators/cobol/template.cbl.tmpl +14 -0
  167. package/cdh/generators/cpp/MANIFEST.toml +14 -0
  168. package/cdh/generators/cpp/template.hpp.tmpl +32 -0
  169. package/cdh/generators/crystal/MANIFEST.toml +14 -0
  170. package/cdh/generators/crystal/template.cr.tmpl +11 -0
  171. package/cdh/generators/csharp/MANIFEST.toml +23 -0
  172. package/cdh/generators/csharp/template.cs.tmpl +14 -0
  173. package/cdh/generators/cython/MANIFEST.toml +14 -0
  174. package/cdh/generators/cython/template.pyx.tmpl +17 -0
  175. package/cdh/generators/d/MANIFEST.toml +14 -0
  176. package/cdh/generators/d/template.d.tmpl +24 -0
  177. package/cdh/generators/dart/MANIFEST.toml +17 -0
  178. package/cdh/generators/dart/template.dart.tmpl +27 -0
  179. package/cdh/generators/elixir/MANIFEST.toml +17 -0
  180. package/cdh/generators/elixir/template.ex.tmpl +27 -0
  181. package/cdh/generators/elm/MANIFEST.toml +14 -0
  182. package/cdh/generators/elm/template.elm.tmpl +20 -0
  183. package/cdh/generators/erlang/MANIFEST.toml +14 -0
  184. package/cdh/generators/erlang/template.erl.tmpl +18 -0
  185. package/cdh/generators/fortran/MANIFEST.toml +14 -0
  186. package/cdh/generators/fortran/template.f90.tmpl +24 -0
  187. package/cdh/generators/fsharp/MANIFEST.toml +14 -0
  188. package/cdh/generators/fsharp/template.fs.tmpl +17 -0
  189. package/cdh/generators/go/MANIFEST.toml +18 -0
  190. package/cdh/generators/go/imports.go.tmpl +3 -0
  191. package/cdh/generators/go/template.go.tmpl +9 -0
  192. package/cdh/generators/graphql-schema/MANIFEST.toml +18 -0
  193. package/cdh/generators/graphql-schema/template.graphqls.tmpl +12 -0
  194. package/cdh/generators/groovy/MANIFEST.toml +14 -0
  195. package/cdh/generators/groovy/template.groovy.tmpl +28 -0
  196. package/cdh/generators/hack/MANIFEST.toml +14 -0
  197. package/cdh/generators/hack/template.hh.tmpl +24 -0
  198. package/cdh/generators/haskell/MANIFEST.toml +14 -0
  199. package/cdh/generators/haskell/template.hs.tmpl +20 -0
  200. package/cdh/generators/haxe/MANIFEST.toml +14 -0
  201. package/cdh/generators/haxe/template.hx.tmpl +20 -0
  202. package/cdh/generators/idris/MANIFEST.toml +14 -0
  203. package/cdh/generators/idris/template.idr.tmpl +22 -0
  204. package/cdh/generators/java/MANIFEST.toml +18 -0
  205. package/cdh/generators/java/template.java.tmpl +13 -0
  206. package/cdh/generators/julia/MANIFEST.toml +14 -0
  207. package/cdh/generators/julia/template.jl.tmpl +27 -0
  208. package/cdh/generators/kotlin/MANIFEST.toml +18 -0
  209. package/cdh/generators/kotlin/template.kt.tmpl +12 -0
  210. package/cdh/generators/lua/MANIFEST.toml +14 -0
  211. package/cdh/generators/lua/template.lua.tmpl +27 -0
  212. package/cdh/generators/matlab/MANIFEST.toml +14 -0
  213. package/cdh/generators/matlab/template.m.tmpl +26 -0
  214. package/cdh/generators/nim/MANIFEST.toml +14 -0
  215. package/cdh/generators/nim/template.nim.tmpl +12 -0
  216. package/cdh/generators/nix/MANIFEST.toml +14 -0
  217. package/cdh/generators/nix/template.nix.tmpl +8 -0
  218. package/cdh/generators/objective-c/MANIFEST.toml +14 -0
  219. package/cdh/generators/objective-c/template.m.tmpl +31 -0
  220. package/cdh/generators/ocaml/MANIFEST.toml +14 -0
  221. package/cdh/generators/ocaml/template.ml.tmpl +13 -0
  222. package/cdh/generators/pascal/MANIFEST.toml +14 -0
  223. package/cdh/generators/pascal/template.pas.tmpl +5 -0
  224. package/cdh/generators/perl/MANIFEST.toml +14 -0
  225. package/cdh/generators/perl/template.pm.tmpl +27 -0
  226. package/cdh/generators/php/MANIFEST.toml +17 -0
  227. package/cdh/generators/php/template.php.tmpl +31 -0
  228. package/cdh/generators/powershell/MANIFEST.toml +14 -0
  229. package/cdh/generators/powershell/template.ps1.tmpl +20 -0
  230. package/cdh/generators/protobuf/MANIFEST.toml +18 -0
  231. package/cdh/generators/protobuf/template.proto.tmpl +13 -0
  232. package/cdh/generators/python/MANIFEST.toml +18 -0
  233. package/cdh/generators/python/template.py.tmpl +12 -0
  234. package/cdh/generators/r/MANIFEST.toml +14 -0
  235. package/cdh/generators/r/template.R.tmpl +29 -0
  236. package/cdh/generators/racket/MANIFEST.toml +14 -0
  237. package/cdh/generators/racket/template.rkt.tmpl +15 -0
  238. package/cdh/generators/registry.py +211 -0
  239. package/cdh/generators/ruby/MANIFEST.toml +17 -0
  240. package/cdh/generators/ruby/template.rb.tmpl +27 -0
  241. package/cdh/generators/rust/MANIFEST.toml +18 -0
  242. package/cdh/generators/rust/template.rs.tmpl +16 -0
  243. package/cdh/generators/scala/MANIFEST.toml +20 -0
  244. package/cdh/generators/scala/template.scala.tmpl +13 -0
  245. package/cdh/generators/scheme/MANIFEST.toml +14 -0
  246. package/cdh/generators/scheme/template.scm.tmpl +16 -0
  247. package/cdh/generators/solidity/MANIFEST.toml +14 -0
  248. package/cdh/generators/solidity/template.sol.tmpl +24 -0
  249. package/cdh/generators/swift/MANIFEST.toml +18 -0
  250. package/cdh/generators/swift/template.swift.tmpl +10 -0
  251. package/cdh/generators/typescript/MANIFEST.toml +18 -0
  252. package/cdh/generators/typescript/template.ts.tmpl +7 -0
  253. package/cdh/generators/version.py +214 -0
  254. package/cdh/generators/zig/MANIFEST.toml +14 -0
  255. package/cdh/generators/zig/template.zig.tmpl +20 -0
  256. package/cdh/logging.py +314 -0
  257. package/cdh/optimizer/__init__.py +7 -0
  258. package/cdh/optimizer/loop.py +201 -0
  259. package/cdh/optimizer/mutation.py +85 -0
  260. package/cdh/optimizer/reward.py +51 -0
  261. package/cdh/optimizer/tracker.py +118 -0
  262. package/cdh/project_loader.py +1022 -0
  263. package/cdh/rules/patterns.yaml +132 -0
  264. package/cdh/rules/spectral.yaml +301 -0
  265. package/cdh/scaffold.py +1436 -0
  266. package/cdh/session_store.py +221 -0
  267. package/cdh/state_schema.py +127 -0
  268. package/cdh/tools/__init__.py +57 -0
  269. package/cdh/tools/cls_search.py +406 -0
  270. package/cdh/tools/contract_diff.py +644 -0
  271. package/cdh/tools/deploy_stack.py +590 -0
  272. package/cdh/tools/generate_shared.py +300 -0
  273. package/cdh/tools/iac_gen.py +224 -0
  274. package/cdh/tools/otel_export.py +125 -0
  275. package/cdh/tools/pattern_detect.py +451 -0
  276. package/cdh/tools/retrofit_labels.py +505 -0
  277. package/cdh/trace/__init__.py +101 -0
  278. package/cdh/trace/__pycache__/__init__.cpython-314.pyc +0 -0
  279. package/cdh/trace/dashboard/__init__.py +61 -0
  280. package/cdh/trace/dashboard/api.py +1032 -0
  281. package/cdh/trace/dashboard/app.html +1321 -0
  282. package/cdh/trace/dashboard/server.py +310 -0
  283. package/cdh/trace/otel_exporter.py +462 -0
  284. package/cdh/trace/otel_tracer.py +281 -0
  285. package/cdh/tui.py +1084 -0
  286. package/cdh/validators/__init__.py +16 -0
  287. package/cdh/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  288. package/cdh/validators/__pycache__/bdd.cpython-314.pyc +0 -0
  289. package/cdh/validators/__pycache__/dag.cpython-314.pyc +0 -0
  290. package/cdh/validators/__pycache__/ears.cpython-314.pyc +0 -0
  291. package/cdh/validators/__pycache__/fr_namespace.cpython-314.pyc +0 -0
  292. package/cdh/validators/bdd.py +144 -0
  293. package/cdh/validators/dag.py +179 -0
  294. package/cdh/validators/ears.py +452 -0
  295. package/cdh/validators/fr_namespace.py +143 -0
  296. package/cdh/validators/spectral.py +475 -0
  297. package/cdh/verification/__init__.py +3 -0
  298. package/cdh/verification/aggregation.py +53 -0
  299. package/cdh/verification/gates/__init__.py +5 -0
  300. package/cdh/verification/gates/base.py +18 -0
  301. package/cdh/verification/gates/lint_gate.py +37 -0
  302. package/cdh/verification/gates/test_gate.py +41 -0
  303. package/cdh/verification/loop.py +93 -0
  304. package/cdh/verification/policy.py +24 -0
  305. package/onecode/__init__.py +1 -0
  306. package/onecode/__main__.py +4 -0
  307. package/onecode/__pycache__/__init__.cpython-314.pyc +0 -0
  308. package/onecode/__pycache__/cli.cpython-314.pyc +0 -0
  309. package/onecode/__pycache__/commands.cpython-314.pyc +0 -0
  310. package/onecode/__pycache__/config.cpython-314.pyc +0 -0
  311. package/onecode/__pycache__/migrate.cpython-314.pyc +0 -0
  312. package/onecode/agent/__init__.py +0 -0
  313. package/onecode/agent/__pycache__/__init__.cpython-314.pyc +0 -0
  314. package/onecode/agent/__pycache__/cdh_loader.cpython-314.pyc +0 -0
  315. package/onecode/agent/__pycache__/checkpoint.cpython-314.pyc +0 -0
  316. package/onecode/agent/__pycache__/context.cpython-314.pyc +0 -0
  317. package/onecode/agent/__pycache__/engine.cpython-314.pyc +0 -0
  318. package/onecode/agent/__pycache__/hooks.cpython-314.pyc +0 -0
  319. package/onecode/agent/__pycache__/onecode_agent_acp.cpython-314.pyc +0 -0
  320. package/onecode/agent/__pycache__/permissions.cpython-314.pyc +0 -0
  321. package/onecode/agent/__pycache__/permissions_store.cpython-314.pyc +0 -0
  322. package/onecode/agent/__pycache__/project_doc.cpython-314.pyc +0 -0
  323. package/onecode/agent/__pycache__/session.cpython-314.pyc +0 -0
  324. package/onecode/agent/__pycache__/turn_record.cpython-314.pyc +0 -0
  325. package/onecode/agent/agents/__init__.py +29 -0
  326. package/onecode/agent/agents/__pycache__/__init__.cpython-314.pyc +0 -0
  327. package/onecode/agent/agents/__pycache__/types.cpython-314.pyc +0 -0
  328. package/onecode/agent/agents/prompts/build-agent.md +50 -0
  329. package/onecode/agent/agents/prompts/compaction-instructions.md +12 -0
  330. package/onecode/agent/agents/prompts/explore-agent.md +42 -0
  331. package/onecode/agent/agents/prompts/plan-agent.md +44 -0
  332. package/onecode/agent/agents/prompts/plan-gate-hard.md +4 -0
  333. package/onecode/agent/agents/prompts/plan-gate-soft.md +3 -0
  334. package/onecode/agent/agents/prompts/solo-agent.md +57 -0
  335. package/onecode/agent/agents/prompts/subagent-constraints.md +10 -0
  336. package/onecode/agent/agents/prompts/summary-instructions.md +7 -0
  337. package/onecode/agent/agents/prompts/title-instructions.md +3 -0
  338. package/onecode/agent/agents/prompts/tool-descriptions.md +129 -0
  339. package/onecode/agent/agents/types.py +786 -0
  340. package/onecode/agent/attachments.py +112 -0
  341. package/onecode/agent/cdh_loader.py +3 -0
  342. package/onecode/agent/checkpoint.py +285 -0
  343. package/onecode/agent/context.py +465 -0
  344. package/onecode/agent/engine.py +3547 -0
  345. package/onecode/agent/event_bridge.py +75 -0
  346. package/onecode/agent/hooks.py +86 -0
  347. package/onecode/agent/onecode_agent_acp.py +2993 -0
  348. package/onecode/agent/permissions.py +150 -0
  349. package/onecode/agent/permissions_store.py +73 -0
  350. package/onecode/agent/project_doc.py +19 -0
  351. package/onecode/agent/question_detect.py +230 -0
  352. package/onecode/agent/session.py +3 -0
  353. package/onecode/agent/snapshot.py +144 -0
  354. package/onecode/agent/tools/__init__.py +17 -0
  355. package/onecode/agent/tools/__pycache__/__init__.cpython-314.pyc +0 -0
  356. package/onecode/agent/tools/__pycache__/agent_tools.cpython-314.pyc +0 -0
  357. package/onecode/agent/tools/__pycache__/apply_patch_tool.cpython-314.pyc +0 -0
  358. package/onecode/agent/tools/__pycache__/bash_tool.cpython-314.pyc +0 -0
  359. package/onecode/agent/tools/__pycache__/communication_tools.cpython-314.pyc +0 -0
  360. package/onecode/agent/tools/__pycache__/config_tool.cpython-314.pyc +0 -0
  361. package/onecode/agent/tools/__pycache__/cron_tools.cpython-314.pyc +0 -0
  362. package/onecode/agent/tools/__pycache__/errors.cpython-314.pyc +0 -0
  363. package/onecode/agent/tools/__pycache__/file_ops.cpython-314.pyc +0 -0
  364. package/onecode/agent/tools/__pycache__/file_tools.cpython-314.pyc +0 -0
  365. package/onecode/agent/tools/__pycache__/git_tools.cpython-314.pyc +0 -0
  366. package/onecode/agent/tools/__pycache__/lsp_tools.cpython-314.pyc +0 -0
  367. package/onecode/agent/tools/__pycache__/mcp_tools.cpython-314.pyc +0 -0
  368. package/onecode/agent/tools/__pycache__/permission_handler.cpython-314.pyc +0 -0
  369. package/onecode/agent/tools/__pycache__/permissions.cpython-314.pyc +0 -0
  370. package/onecode/agent/tools/__pycache__/protocol.cpython-314.pyc +0 -0
  371. package/onecode/agent/tools/__pycache__/registry.cpython-314.pyc +0 -0
  372. package/onecode/agent/tools/__pycache__/sandbox.cpython-314.pyc +0 -0
  373. package/onecode/agent/tools/__pycache__/schema_validation.cpython-314.pyc +0 -0
  374. package/onecode/agent/tools/__pycache__/skill_tools.cpython-314.pyc +0 -0
  375. package/onecode/agent/tools/__pycache__/todo_tools.cpython-314.pyc +0 -0
  376. package/onecode/agent/tools/__pycache__/web_tools.cpython-314.pyc +0 -0
  377. package/onecode/agent/tools/agent_tools.py +128 -0
  378. package/onecode/agent/tools/apply_patch_tool.py +138 -0
  379. package/onecode/agent/tools/bash_tool.py +121 -0
  380. package/onecode/agent/tools/communication_tools.py +154 -0
  381. package/onecode/agent/tools/config_tool.py +130 -0
  382. package/onecode/agent/tools/cron_tools.py +178 -0
  383. package/onecode/agent/tools/errors.py +17 -0
  384. package/onecode/agent/tools/file_ops.py +221 -0
  385. package/onecode/agent/tools/file_tools.py +225 -0
  386. package/onecode/agent/tools/git_tools.py +88 -0
  387. package/onecode/agent/tools/lsp_tools.py +404 -0
  388. package/onecode/agent/tools/mcp_tools.py +211 -0
  389. package/onecode/agent/tools/permission_handler.py +46 -0
  390. package/onecode/agent/tools/permissions.py +77 -0
  391. package/onecode/agent/tools/protocol.py +20 -0
  392. package/onecode/agent/tools/registry.py +210 -0
  393. package/onecode/agent/tools/sandbox.py +460 -0
  394. package/onecode/agent/tools/schema_validation.py +100 -0
  395. package/onecode/agent/tools/skill_tools.py +52 -0
  396. package/onecode/agent/tools/todo_tools.py +208 -0
  397. package/onecode/agent/tools/web_tools.py +530 -0
  398. package/onecode/agent/turn_record.py +28 -0
  399. package/onecode/builtin_skills/agent-browser/SKILL.md +212 -0
  400. package/onecode/builtin_skills/agent-browser/skill.yaml +8 -0
  401. package/onecode/builtin_skills/cloudbase/SKILL.md +162 -0
  402. package/onecode/builtin_skills/cloudbase/skill.yaml +7 -0
  403. package/onecode/builtin_skills/git/SKILL.md +64 -0
  404. package/onecode/builtin_skills/git/skill.yaml +7 -0
  405. package/onecode/builtin_skills/shell/SKILL.md +58 -0
  406. package/onecode/builtin_skills/shell/skill.yaml +8 -0
  407. package/onecode/builtin_skills/skill-creator/SKILL.md +106 -0
  408. package/onecode/builtin_skills/skill-creator/skill.yaml +8 -0
  409. package/onecode/cli.py +1325 -0
  410. package/onecode/codebase/__init__.py +69 -0
  411. package/onecode/codebase/__pycache__/__init__.cpython-314.pyc +0 -0
  412. package/onecode/codebase/__pycache__/chunker.cpython-314.pyc +0 -0
  413. package/onecode/codebase/__pycache__/indexer.cpython-314.pyc +0 -0
  414. package/onecode/codebase/__pycache__/retriever.cpython-314.pyc +0 -0
  415. package/onecode/codebase/__pycache__/storage.cpython-314.pyc +0 -0
  416. package/onecode/codebase/__pycache__/tools.cpython-314.pyc +0 -0
  417. package/onecode/codebase/chunker.py +125 -0
  418. package/onecode/codebase/indexer.py +154 -0
  419. package/onecode/codebase/retriever.py +240 -0
  420. package/onecode/codebase/storage.py +153 -0
  421. package/onecode/codebase/tools.py +59 -0
  422. package/onecode/commands.py +673 -0
  423. package/onecode/config.py +539 -0
  424. package/onecode/config_screen.py +486 -0
  425. package/onecode/cron/__init__.py +3 -0
  426. package/onecode/lsp/__init__.py +3 -0
  427. package/onecode/mcp/__init__.py +11 -0
  428. package/onecode/mcp/__pycache__/__init__.cpython-314.pyc +0 -0
  429. package/onecode/mcp/__pycache__/client.cpython-314.pyc +0 -0
  430. package/onecode/mcp/__pycache__/cloudbase.cpython-314.pyc +0 -0
  431. package/onecode/mcp/__pycache__/config.cpython-314.pyc +0 -0
  432. package/onecode/mcp/__pycache__/manager.cpython-314.pyc +0 -0
  433. package/onecode/mcp/__pycache__/oauth.cpython-314.pyc +0 -0
  434. package/onecode/mcp/client.py +305 -0
  435. package/onecode/mcp/cloudbase.py +188 -0
  436. package/onecode/mcp/config.py +363 -0
  437. package/onecode/mcp/manager.py +603 -0
  438. package/onecode/mcp/oauth.py +199 -0
  439. package/onecode/memory/__init__.py +106 -0
  440. package/onecode/memory/__pycache__/__init__.cpython-314.pyc +0 -0
  441. package/onecode/memory/__pycache__/backend.cpython-314.pyc +0 -0
  442. package/onecode/memory/__pycache__/pyramid.cpython-314.pyc +0 -0
  443. package/onecode/memory/__pycache__/recall.cpython-314.pyc +0 -0
  444. package/onecode/memory/backend.py +179 -0
  445. package/onecode/memory/pyramid.py +145 -0
  446. package/onecode/memory/recall.py +322 -0
  447. package/onecode/migrate.py +122 -0
  448. package/onecode/models/__init__.py +4 -0
  449. package/onecode/models/__pycache__/__init__.cpython-314.pyc +0 -0
  450. package/onecode/models/__pycache__/errors.cpython-314.pyc +0 -0
  451. package/onecode/models/__pycache__/messages.cpython-314.pyc +0 -0
  452. package/onecode/models/__pycache__/ollama_manager.cpython-314.pyc +0 -0
  453. package/onecode/models/__pycache__/provider.cpython-314.pyc +0 -0
  454. package/onecode/models/__pycache__/registry.cpython-314.pyc +0 -0
  455. package/onecode/models/errors.py +104 -0
  456. package/onecode/models/messages.py +750 -0
  457. package/onecode/models/ollama_manager.py +167 -0
  458. package/onecode/models/provider.py +730 -0
  459. package/onecode/models/providers/__init__.py +17 -0
  460. package/onecode/models/providers/__pycache__/__init__.cpython-314.pyc +0 -0
  461. package/onecode/models/providers/__pycache__/anthropic_provider.cpython-314.pyc +0 -0
  462. package/onecode/models/providers/__pycache__/deepseek_provider.cpython-314.pyc +0 -0
  463. package/onecode/models/providers/__pycache__/glm_provider.cpython-314.pyc +0 -0
  464. package/onecode/models/providers/__pycache__/minimax_provider.cpython-314.pyc +0 -0
  465. package/onecode/models/providers/__pycache__/minimaxi_provider.cpython-314.pyc +0 -0
  466. package/onecode/models/providers/__pycache__/ollama_provider.cpython-314.pyc +0 -0
  467. package/onecode/models/providers/__pycache__/openai_provider.cpython-314.pyc +0 -0
  468. package/onecode/models/providers/anthropic_provider.py +286 -0
  469. package/onecode/models/providers/deepseek_provider.py +221 -0
  470. package/onecode/models/providers/glm_provider.py +219 -0
  471. package/onecode/models/providers/minimax_provider.py +182 -0
  472. package/onecode/models/providers/minimaxi_provider.py +426 -0
  473. package/onecode/models/providers/ollama_provider.py +183 -0
  474. package/onecode/models/providers/openai_provider.py +234 -0
  475. package/onecode/models/registry.py +349 -0
  476. package/onecode/server/__init__.py +3 -0
  477. package/onecode/server/app.py +251 -0
  478. package/onecode/skills/__init__.py +11 -0
  479. package/onecode/skills/__pycache__/__init__.cpython-314.pyc +0 -0
  480. package/onecode/skills/__pycache__/argument_substitution.cpython-314.pyc +0 -0
  481. package/onecode/skills/__pycache__/bootstrap.cpython-314.pyc +0 -0
  482. package/onecode/skills/__pycache__/create.cpython-314.pyc +0 -0
  483. package/onecode/skills/__pycache__/frontmatter.cpython-314.pyc +0 -0
  484. package/onecode/skills/__pycache__/loader.cpython-314.pyc +0 -0
  485. package/onecode/skills/__pycache__/model.cpython-314.pyc +0 -0
  486. package/onecode/skills/argument_substitution.py +38 -0
  487. package/onecode/skills/bootstrap.py +37 -0
  488. package/onecode/skills/create.py +56 -0
  489. package/onecode/skills/frontmatter.py +33 -0
  490. package/onecode/skills/loader.py +215 -0
  491. package/onecode/skills/manager.py +75 -0
  492. package/onecode/skills/model.py +43 -0
  493. package/onecode/storage/__init__.py +0 -0
  494. package/onecode/storage/activity.py +63 -0
  495. package/onecode/storage/project.py +54 -0
  496. package/onecode/storage/session.py +112 -0
  497. package/onecode/tasks/__init__.py +26 -0
  498. package/onecode/tasks/manager.py +232 -0
  499. package/onecode/tasks/models.py +240 -0
  500. package/onecode/trace/__init__.py +0 -0
  501. package/onecode/trace/tracer.py +173 -0
  502. package/onecode/utils/__init__.py +0 -0
  503. package/onecode/utils/helpers.py +20 -0
  504. package/onecode/verification/__init__.py +5 -0
  505. package/onecode/verification/__pycache__/__init__.cpython-314.pyc +0 -0
  506. package/onecode/verification/__pycache__/aggregation.cpython-314.pyc +0 -0
  507. package/onecode/verification/__pycache__/loop.cpython-314.pyc +0 -0
  508. package/onecode/verification/__pycache__/policy.cpython-314.pyc +0 -0
  509. package/onecode/verification/aggregation.py +53 -0
  510. package/onecode/verification/gates/__init__.py +6 -0
  511. package/onecode/verification/gates/__pycache__/__init__.cpython-314.pyc +0 -0
  512. package/onecode/verification/gates/__pycache__/base.cpython-314.pyc +0 -0
  513. package/onecode/verification/gates/__pycache__/lint_gate.cpython-314.pyc +0 -0
  514. package/onecode/verification/gates/__pycache__/test_gate.cpython-314.pyc +0 -0
  515. package/onecode/verification/gates/__pycache__/type_gate.cpython-314.pyc +0 -0
  516. package/onecode/verification/gates/base.py +20 -0
  517. package/onecode/verification/gates/lint_gate.py +45 -0
  518. package/onecode/verification/gates/test_gate.py +62 -0
  519. package/onecode/verification/gates/type_gate.py +46 -0
  520. package/onecode/verification/loop.py +95 -0
  521. package/onecode/verification/policy.py +29 -0
  522. package/package.json +2 -2
  523. package/pyproject.toml +84 -0
  524. package/tui/__init__.py +46 -0
  525. package/tui/__main__.py +4 -0
  526. package/tui/__pycache__/__init__.cpython-314.pyc +0 -0
  527. package/tui/__pycache__/_loop.cpython-314.pyc +0 -0
  528. package/tui/__pycache__/_path_fuzzy_search.cpython-314.pyc +0 -0
  529. package/tui/__pycache__/_path_match.cpython-314.pyc +0 -0
  530. package/tui/__pycache__/agent.cpython-314.pyc +0 -0
  531. package/tui/__pycache__/agent_schema.cpython-314.pyc +0 -0
  532. package/tui/__pycache__/agents.cpython-314.pyc +0 -0
  533. package/tui/__pycache__/answer.cpython-314.pyc +0 -0
  534. package/tui/__pycache__/app.cpython-314.pyc +0 -0
  535. package/tui/__pycache__/atomic.cpython-314.pyc +0 -0
  536. package/tui/__pycache__/complete.cpython-314.pyc +0 -0
  537. package/tui/__pycache__/constants.cpython-314.pyc +0 -0
  538. package/tui/__pycache__/conversation_markdown.cpython-314.pyc +0 -0
  539. package/tui/__pycache__/db.cpython-314.pyc +0 -0
  540. package/tui/__pycache__/dec.cpython-314.pyc +0 -0
  541. package/tui/__pycache__/directory.cpython-314.pyc +0 -0
  542. package/tui/__pycache__/directory_watcher.cpython-314.pyc +0 -0
  543. package/tui/__pycache__/format_path.cpython-314.pyc +0 -0
  544. package/tui/__pycache__/fuzzy.cpython-314.pyc +0 -0
  545. package/tui/__pycache__/fuzzy_index.cpython-314.pyc +0 -0
  546. package/tui/__pycache__/history.cpython-314.pyc +0 -0
  547. package/tui/__pycache__/jsonrpc.cpython-314.pyc +0 -0
  548. package/tui/__pycache__/menus.cpython-314.pyc +0 -0
  549. package/tui/__pycache__/message_log.cpython-314.pyc +0 -0
  550. package/tui/__pycache__/messages.cpython-314.pyc +0 -0
  551. package/tui/__pycache__/path_complete.cpython-314.pyc +0 -0
  552. package/tui/__pycache__/path_filter.cpython-314.pyc +0 -0
  553. package/tui/__pycache__/paths.cpython-314.pyc +0 -0
  554. package/tui/__pycache__/pill.cpython-314.pyc +0 -0
  555. package/tui/__pycache__/platform_commands.cpython-314.pyc +0 -0
  556. package/tui/__pycache__/protocol.cpython-314.pyc +0 -0
  557. package/tui/__pycache__/session_tracker.cpython-314.pyc +0 -0
  558. package/tui/__pycache__/settings.cpython-314.pyc +0 -0
  559. package/tui/__pycache__/settings_schema.cpython-314.pyc +0 -0
  560. package/tui/__pycache__/shell.cpython-314.pyc +0 -0
  561. package/tui/__pycache__/shell_read.cpython-314.pyc +0 -0
  562. package/tui/__pycache__/slash_command.cpython-314.pyc +0 -0
  563. package/tui/__pycache__/version.cpython-314.pyc +0 -0
  564. package/tui/_loop.py +86 -0
  565. package/tui/_path_fuzzy_search.py +48 -0
  566. package/tui/_path_match.py +141 -0
  567. package/tui/about.py +95 -0
  568. package/tui/acp/__pycache__/agent.cpython-314.pyc +0 -0
  569. package/tui/acp/__pycache__/api.cpython-314.pyc +0 -0
  570. package/tui/acp/__pycache__/encode_tool_call_id.cpython-314.pyc +0 -0
  571. package/tui/acp/__pycache__/event_tap.cpython-314.pyc +0 -0
  572. package/tui/acp/__pycache__/messages.cpython-314.pyc +0 -0
  573. package/tui/acp/__pycache__/prompt.cpython-314.pyc +0 -0
  574. package/tui/acp/__pycache__/protocol.cpython-314.pyc +0 -0
  575. package/tui/acp/agent.py +1212 -0
  576. package/tui/acp/api.py +79 -0
  577. package/tui/acp/encode_tool_call_id.py +12 -0
  578. package/tui/acp/event_tap.py +107 -0
  579. package/tui/acp/messages.py +222 -0
  580. package/tui/acp/prompt.py +53 -0
  581. package/tui/acp/protocol.py +551 -0
  582. package/tui/agent.py +70 -0
  583. package/tui/agent_schema.py +76 -0
  584. package/tui/agents.py +46 -0
  585. package/tui/ansi/__init__.py +1 -0
  586. package/tui/ansi/__pycache__/__init__.cpython-314.pyc +0 -0
  587. package/tui/ansi/__pycache__/_ansi.cpython-314.pyc +0 -0
  588. package/tui/ansi/__pycache__/_ansi_colors.cpython-314.pyc +0 -0
  589. package/tui/ansi/__pycache__/_control_codes.cpython-314.pyc +0 -0
  590. package/tui/ansi/__pycache__/_keys.cpython-314.pyc +0 -0
  591. package/tui/ansi/__pycache__/_sgr_styles.cpython-314.pyc +0 -0
  592. package/tui/ansi/__pycache__/_stream_parser.cpython-314.pyc +0 -0
  593. package/tui/ansi/_ansi.py +1689 -0
  594. package/tui/ansi/_ansi_colors.py +264 -0
  595. package/tui/ansi/_control_codes.py +37 -0
  596. package/tui/ansi/_keys.py +251 -0
  597. package/tui/ansi/_sgr_styles.py +64 -0
  598. package/tui/ansi/_stream_parser.py +418 -0
  599. package/tui/answer.py +12 -0
  600. package/tui/app.py +1195 -0
  601. package/tui/atomic.py +37 -0
  602. package/tui/cli.py +208 -0
  603. package/tui/code_analyze.py +27 -0
  604. package/tui/complete.py +38 -0
  605. package/tui/constants.py +61 -0
  606. package/tui/conversation_markdown.py +30 -0
  607. package/tui/danger.py +372 -0
  608. package/tui/data/agents/ampcode.com.toml +52 -0
  609. package/tui/data/agents/augmentcode.com.toml +41 -0
  610. package/tui/data/agents/cdh.cloud-dev-harness.toml +23 -0
  611. package/tui/data/agents/claude.com.toml +41 -0
  612. package/tui/data/agents/copilot.github.com.toml +29 -0
  613. package/tui/data/agents/crowai.dev.toml +36 -0
  614. package/tui/data/agents/cursor.com.toml +38 -0
  615. package/tui/data/agents/deepagents.langchain.com +34 -0
  616. package/tui/data/agents/docker.com.toml +60 -0
  617. package/tui/data/agents/geminicli.com.toml +28 -0
  618. package/tui/data/agents/goose.ai.toml +52 -0
  619. package/tui/data/agents/inference.huggingface.co.toml +41 -0
  620. package/tui/data/agents/kimi.com.toml +35 -0
  621. package/tui/data/agents/kiro.dev.toml +43 -0
  622. package/tui/data/agents/onecode.dev.toml +22 -0
  623. package/tui/data/agents/openai.com.toml +53 -0
  624. package/tui/data/agents/openclaw.ai.toml +37 -0
  625. package/tui/data/agents/opencode.ai.toml +39 -0
  626. package/tui/data/agents/openhands.dev.toml +46 -0
  627. package/tui/data/agents/stakpak.dev.toml +62 -0
  628. package/tui/data/agents/vibe.mistral.ai.toml +31 -0
  629. package/tui/data/agents/vtcode.dev.toml +63 -0
  630. package/tui/data/images/frog.png +0 -0
  631. package/tui/data/sounds/question.wav +0 -0
  632. package/tui/data/sounds/turn-over.wav +0 -0
  633. package/tui/db.py +213 -0
  634. package/tui/dec.py +332 -0
  635. package/tui/diff_utils.py +131 -0
  636. package/tui/directory.py +234 -0
  637. package/tui/directory_suggester.py +93 -0
  638. package/tui/directory_watcher.py +231 -0
  639. package/tui/format_path.py +25 -0
  640. package/tui/fuzzy.py +140 -0
  641. package/tui/fuzzy_index.py +275 -0
  642. package/tui/gist.py +33 -0
  643. package/tui/history.py +138 -0
  644. package/tui/jsonrpc.py +599 -0
  645. package/tui/menus.py +14 -0
  646. package/tui/message_log.py +69 -0
  647. package/tui/messages.py +144 -0
  648. package/tui/option_content.py +51 -0
  649. package/tui/os.py +0 -0
  650. package/tui/path_complete.py +217 -0
  651. package/tui/path_filter.py +124 -0
  652. package/tui/paths.py +87 -0
  653. package/tui/pill.py +23 -0
  654. package/tui/platform_commands.py +52 -0
  655. package/tui/prompt/__pycache__/extract.cpython-314.pyc +0 -0
  656. package/tui/prompt/__pycache__/resource.cpython-314.pyc +0 -0
  657. package/tui/prompt/extract.py +19 -0
  658. package/tui/prompt/resource.py +68 -0
  659. package/tui/protocol.py +28 -0
  660. package/tui/screens/__init__.py +0 -0
  661. package/tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
  662. package/tui/screens/__pycache__/diff_screen.cpython-314.pyc +0 -0
  663. package/tui/screens/__pycache__/main.cpython-314.pyc +0 -0
  664. package/tui/screens/__pycache__/sessions.cpython-314.pyc +0 -0
  665. package/tui/screens/action_modal.py +94 -0
  666. package/tui/screens/agent_modal.py +175 -0
  667. package/tui/screens/command_edit_modal.py +74 -0
  668. package/tui/screens/diff_explorer_screen.py +419 -0
  669. package/tui/screens/diff_screen.py +84 -0
  670. package/tui/screens/ide_screen.py +461 -0
  671. package/tui/screens/main.py +397 -0
  672. package/tui/screens/main.tcss +662 -0
  673. package/tui/screens/permissions.py +404 -0
  674. package/tui/screens/permissions.tcss +72 -0
  675. package/tui/screens/projects.tcss +84 -0
  676. package/tui/screens/projects_app.py +341 -0
  677. package/tui/screens/projects_screen.py +277 -0
  678. package/tui/screens/session_resume_modal.py +172 -0
  679. package/tui/screens/session_resume_modal.tcss +35 -0
  680. package/tui/screens/sessions.py +171 -0
  681. package/tui/screens/sessions.tcss +119 -0
  682. package/tui/screens/settings.py +293 -0
  683. package/tui/screens/settings.tcss +113 -0
  684. package/tui/screens/store.py +502 -0
  685. package/tui/screens/store.tcss +274 -0
  686. package/tui/screens/tool_content_screen.py +59 -0
  687. package/tui/screens/trace.py +317 -0
  688. package/tui/session_tracker.py +123 -0
  689. package/tui/settings.py +353 -0
  690. package/tui/settings_schema.py +467 -0
  691. package/tui/shell.py +304 -0
  692. package/tui/shell_read.py +42 -0
  693. package/tui/slash_command.py +34 -0
  694. package/tui/tui.tcss +246 -0
  695. package/tui/twitter.py +38 -0
  696. package/tui/version.py +80 -0
  697. package/tui/visuals/__init__.py +0 -0
  698. package/tui/visuals/__pycache__/__init__.cpython-314.pyc +0 -0
  699. package/tui/visuals/__pycache__/columns.cpython-314.pyc +0 -0
  700. package/tui/visuals/columns.py +272 -0
  701. package/tui/widgets/__init__.py +0 -0
  702. package/tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
  703. package/tui/widgets/__pycache__/agent_response.cpython-314.pyc +0 -0
  704. package/tui/widgets/__pycache__/agent_thought.cpython-314.pyc +0 -0
  705. package/tui/widgets/__pycache__/aidlc.cpython-314.pyc +0 -0
  706. package/tui/widgets/__pycache__/ask_user.cpython-314.pyc +0 -0
  707. package/tui/widgets/__pycache__/condensed_path.cpython-314.pyc +0 -0
  708. package/tui/widgets/__pycache__/context_stats.cpython-314.pyc +0 -0
  709. package/tui/widgets/__pycache__/conversation.cpython-314.pyc +0 -0
  710. package/tui/widgets/__pycache__/diff_view.cpython-314.pyc +0 -0
  711. package/tui/widgets/__pycache__/flash.cpython-314.pyc +0 -0
  712. package/tui/widgets/__pycache__/future_text.cpython-314.pyc +0 -0
  713. package/tui/widgets/__pycache__/grid_select.cpython-314.pyc +0 -0
  714. package/tui/widgets/__pycache__/highlighted_textarea.cpython-314.pyc +0 -0
  715. package/tui/widgets/__pycache__/load_more.cpython-314.pyc +0 -0
  716. package/tui/widgets/__pycache__/menu.cpython-314.pyc +0 -0
  717. package/tui/widgets/__pycache__/modified_files.cpython-314.pyc +0 -0
  718. package/tui/widgets/__pycache__/non_selectable_label.cpython-314.pyc +0 -0
  719. package/tui/widgets/__pycache__/note.cpython-314.pyc +0 -0
  720. package/tui/widgets/__pycache__/path_search.cpython-314.pyc +0 -0
  721. package/tui/widgets/__pycache__/pending_prompts.cpython-314.pyc +0 -0
  722. package/tui/widgets/__pycache__/plan.cpython-314.pyc +0 -0
  723. package/tui/widgets/__pycache__/project_directory_tree.cpython-314.pyc +0 -0
  724. package/tui/widgets/__pycache__/prompt.cpython-314.pyc +0 -0
  725. package/tui/widgets/__pycache__/question.cpython-314.pyc +0 -0
  726. package/tui/widgets/__pycache__/session_grid_select.cpython-314.pyc +0 -0
  727. package/tui/widgets/__pycache__/session_summary.cpython-314.pyc +0 -0
  728. package/tui/widgets/__pycache__/session_tabs.cpython-314.pyc +0 -0
  729. package/tui/widgets/__pycache__/shell_terminal.cpython-314.pyc +0 -0
  730. package/tui/widgets/__pycache__/side_bar.cpython-314.pyc +0 -0
  731. package/tui/widgets/__pycache__/slash_complete.cpython-314.pyc +0 -0
  732. package/tui/widgets/__pycache__/strike_text.cpython-314.pyc +0 -0
  733. package/tui/widgets/__pycache__/terminal.cpython-314.pyc +0 -0
  734. package/tui/widgets/__pycache__/throbber.cpython-314.pyc +0 -0
  735. package/tui/widgets/__pycache__/tool_call.cpython-314.pyc +0 -0
  736. package/tui/widgets/__pycache__/user_input.cpython-314.pyc +0 -0
  737. package/tui/widgets/acp_content.py +70 -0
  738. package/tui/widgets/agent_response.py +76 -0
  739. package/tui/widgets/agent_thought.py +167 -0
  740. package/tui/widgets/aidlc.py +334 -0
  741. package/tui/widgets/ask_user.py +604 -0
  742. package/tui/widgets/command_pane.py +223 -0
  743. package/tui/widgets/condensed_path.py +115 -0
  744. package/tui/widgets/context_stats.py +66 -0
  745. package/tui/widgets/conversation.py +3235 -0
  746. package/tui/widgets/danger_warning.py +65 -0
  747. package/tui/widgets/diff_view.py +69 -0
  748. package/tui/widgets/directory_input.py +8 -0
  749. package/tui/widgets/edit_field.py +206 -0
  750. package/tui/widgets/flash.py +81 -0
  751. package/tui/widgets/future_text.py +126 -0
  752. package/tui/widgets/grid_select.py +234 -0
  753. package/tui/widgets/highlighted_textarea.py +183 -0
  754. package/tui/widgets/load_more.py +24 -0
  755. package/tui/widgets/mandelbrot.py +294 -0
  756. package/tui/widgets/markdown_note.py +13 -0
  757. package/tui/widgets/menu.py +147 -0
  758. package/tui/widgets/model_status.py +230 -0
  759. package/tui/widgets/modified_files.py +315 -0
  760. package/tui/widgets/non_selectable_label.py +5 -0
  761. package/tui/widgets/note.py +18 -0
  762. package/tui/widgets/path_search.py +492 -0
  763. package/tui/widgets/pending_prompts.py +36 -0
  764. package/tui/widgets/plan.py +285 -0
  765. package/tui/widgets/project_directory_tree.py +102 -0
  766. package/tui/widgets/project_grid_select.py +68 -0
  767. package/tui/widgets/project_summary.py +33 -0
  768. package/tui/widgets/prompt.py +935 -0
  769. package/tui/widgets/question.py +360 -0
  770. package/tui/widgets/session_grid_select.py +230 -0
  771. package/tui/widgets/session_summary.py +115 -0
  772. package/tui/widgets/session_tabs.py +194 -0
  773. package/tui/widgets/shell_result.py +35 -0
  774. package/tui/widgets/shell_terminal.py +40 -0
  775. package/tui/widgets/side_bar.py +124 -0
  776. package/tui/widgets/slash_complete.py +235 -0
  777. package/tui/widgets/strike_text.py +70 -0
  778. package/tui/widgets/subagent.py +287 -0
  779. package/tui/widgets/subagent_screen.py +715 -0
  780. package/tui/widgets/terminal.py +576 -0
  781. package/tui/widgets/terminal_tool.py +360 -0
  782. package/tui/widgets/throbber.py +100 -0
  783. package/tui/widgets/tool_call.py +432 -0
  784. package/tui/widgets/user_input.py +23 -0
  785. package/tui/widgets/version.py +5 -0
  786. 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()