tan-cli 0.7.0__py3-none-any.whl

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 (330) hide show
  1. tan/__init__.py +0 -0
  2. tan/__main__.py +120 -0
  3. tan/cli.py +939 -0
  4. tan/command_table.py +265 -0
  5. tan/commands/__init__.py +8 -0
  6. tan/commands/bootstrap_cmd.py +4856 -0
  7. tan/commands/bootstrap_patches.py +164 -0
  8. tan/commands/bootstrap_toolchain_fetch.py +155 -0
  9. tan/commands/bootstrap_toolchain_pin.py +36 -0
  10. tan/commands/build/__init__.py +1 -0
  11. tan/commands/build/configure_inputs.py +277 -0
  12. tan/commands/build/execute.py +1852 -0
  13. tan/commands/build/host_python.py +59 -0
  14. tan/commands/build/link_stale.py +78 -0
  15. tan/commands/build/manifest.py +428 -0
  16. tan/commands/build/materialise.py +81 -0
  17. tan/commands/build/token_substitution.py +381 -0
  18. tan/commands/build/toolchain.py +482 -0
  19. tan/commands/build/user_defines_wipe.py +115 -0
  20. tan/commands/build/workspace_patches.py +65 -0
  21. tan/commands/build_cmd.py +2236 -0
  22. tan/commands/build_output.py +331 -0
  23. tan/commands/clean_cmd.py +1183 -0
  24. tan/commands/completion_cmd.py +786 -0
  25. tan/commands/debug_config_cmd.py +2425 -0
  26. tan/commands/diff_cmd.py +940 -0
  27. tan/commands/doctor_cmd.py +4767 -0
  28. tan/commands/examples_cmd.py +644 -0
  29. tan/commands/explain_cmd.py +1319 -0
  30. tan/commands/faultdecode_cmd.py +931 -0
  31. tan/commands/flash_atoc_guard.py +240 -0
  32. tan/commands/flash_cmd.py +5731 -0
  33. tan/commands/flash_mram_guard.py +150 -0
  34. tan/commands/flash_ram.py +578 -0
  35. tan/commands/flash_raw.py +583 -0
  36. tan/commands/generate_cmd.py +1456 -0
  37. tan/commands/image_cmd.py +753 -0
  38. tan/commands/init_cmd.py +1740 -0
  39. tan/commands/inspect_cmd.py +377 -0
  40. tan/commands/kconfig_cmd.py +734 -0
  41. tan/commands/model_cmd.py +1514 -0
  42. tan/commands/model_device_cmd.py +351 -0
  43. tan/commands/model_device_live.py +234 -0
  44. tan/commands/model_host_cmd.py +430 -0
  45. tan/commands/model_zoo_cmd.py +363 -0
  46. tan/commands/monitor_actions.py +125 -0
  47. tan/commands/monitor_cmd.py +626 -0
  48. tan/commands/monitor_session.py +505 -0
  49. tan/commands/new_som_cmd.py +1522 -0
  50. tan/commands/pinmux_cmd.py +568 -0
  51. tan/commands/presets_cmd.py +1123 -0
  52. tan/commands/probe_cmd.py +421 -0
  53. tan/commands/reset_cmd.py +354 -0
  54. tan/commands/reset_confirm.py +168 -0
  55. tan/commands/run_cmd.py +637 -0
  56. tan/commands/scaffold_cmd.py +490 -0
  57. tan/commands/sdk_cmd.py +1723 -0
  58. tan/commands/size_cmd.py +1020 -0
  59. tan/commands/support_bundle_cmd.py +1073 -0
  60. tan/commands/trace_cmd.py +341 -0
  61. tan/commands/validate_cmd.py +2009 -0
  62. tan/commands/west_forward_cmd.py +736 -0
  63. tan/commands/workspace_patch_check.py +296 -0
  64. tan/core/__init__.py +1 -0
  65. tan/core/alp_conf_pregen.py +100 -0
  66. tan/core/artifact_provenance.py +240 -0
  67. tan/core/atoc_guard.py +276 -0
  68. tan/core/atoc_guard_messages.py +232 -0
  69. tan/core/atoc_replacement.py +118 -0
  70. tan/core/atomic_write.py +178 -0
  71. tan/core/board_context.py +351 -0
  72. tan/core/board_diagnostic.py +111 -0
  73. tan/core/board_files.py +221 -0
  74. tan/core/board_validator.py +542 -0
  75. tan/core/board_validator_compat.py +196 -0
  76. tan/core/board_validator_run.py +129 -0
  77. tan/core/board_validator_schema.py +186 -0
  78. tan/core/board_validator_skew.py +46 -0
  79. tan/core/board_yaml_edit.py +295 -0
  80. tan/core/board_yaml_pos.py +118 -0
  81. tan/core/bootstrap.py +2572 -0
  82. tan/core/build_plan.py +625 -0
  83. tan/core/consent.py +104 -0
  84. tan/core/console_filter.py +245 -0
  85. tan/core/debug_launch.py +1803 -0
  86. tan/core/devicetree_lint.py +168 -0
  87. tan/core/dir_removal.py +229 -0
  88. tan/core/doctor_git.py +172 -0
  89. tan/core/doctor_libraries.py +253 -0
  90. tan/core/doctor_render.py +185 -0
  91. tan/core/doctor_scope.py +66 -0
  92. tan/core/doctor_setools.py +237 -0
  93. tan/core/doctor_stale.py +180 -0
  94. tan/core/document_guards.py +723 -0
  95. tan/core/dp_id.py +97 -0
  96. tan/core/elf_load.py +63 -0
  97. tan/core/error_catalog.py +171 -0
  98. tan/core/example_catalog.py +499 -0
  99. tan/core/example_facets.py +151 -0
  100. tan/core/faultdecode.py +521 -0
  101. tan/core/flash_plan.py +2501 -0
  102. tan/core/flow_d_report.py +381 -0
  103. tan/core/flow_d_uptime.py +164 -0
  104. tan/core/fs_confine.py +130 -0
  105. tan/core/global_flags.py +230 -0
  106. tan/core/host_python.py +210 -0
  107. tan/core/image_bundle.py +247 -0
  108. tan/core/inert.py +147 -0
  109. tan/core/jlink_binary.py +128 -0
  110. tan/core/jlink_probe.py +481 -0
  111. tan/core/jsonc_splice.py +404 -0
  112. tan/core/launch_provenance.py +257 -0
  113. tan/core/link_refusal.py +76 -0
  114. tan/core/metadata_schema.py +376 -0
  115. tan/core/model_check.py +227 -0
  116. tan/core/model_device.py +449 -0
  117. tan/core/model_doctor.py +198 -0
  118. tan/core/model_host.py +48 -0
  119. tan/core/model_list.py +112 -0
  120. tan/core/model_zoo.py +353 -0
  121. tan/core/module_template.py +340 -0
  122. tan/core/mram_link.py +87 -0
  123. tan/core/new_som_ospi.py +94 -0
  124. tan/core/os_class.py +159 -0
  125. tan/core/patch_paths.py +87 -0
  126. tan/core/pending.py +40 -0
  127. tan/core/plain_zephyr_plan.py +138 -0
  128. tan/core/plan_exec.py +373 -0
  129. tan/core/plan_tokens.py +600 -0
  130. tan/core/probe.py +155 -0
  131. tan/core/probe_plan.py +237 -0
  132. tan/core/proxy.py +168 -0
  133. tan/core/publish.py +31 -0
  134. tan/core/python_floor.py +178 -0
  135. tan/core/ram_run.py +694 -0
  136. tan/core/ram_watch.py +231 -0
  137. tan/core/raw_write.py +160 -0
  138. tan/core/reset_plan.py +56 -0
  139. tan/core/run.py +161 -0
  140. tan/core/scaffold.py +2203 -0
  141. tan/core/scaffold_selftest_identity.py +225 -0
  142. tan/core/sdk_default_registry.py +565 -0
  143. tan/core/sdk_discovery.py +996 -0
  144. tan/core/sdk_removal.py +465 -0
  145. tan/core/serial_actions.py +175 -0
  146. tan/core/serial_capture.py +185 -0
  147. tan/core/serial_url.py +45 -0
  148. tan/core/setools.py +608 -0
  149. tan/core/setools_scratch.py +243 -0
  150. tan/core/shapes.py +194 -0
  151. tan/core/size.py +610 -0
  152. tan/core/som_buildability.py +204 -0
  153. tan/core/som_schema_version.py +62 -0
  154. tan/core/subprocess_env.py +201 -0
  155. tan/core/sysbuild_image.py +94 -0
  156. tan/core/system_manifest.py +827 -0
  157. tan/core/text_layout.py +73 -0
  158. tan/core/timestamp.py +182 -0
  159. tan/core/tool_lookup.py +383 -0
  160. tan/core/toolchain_pin.py +117 -0
  161. tan/core/toolchain_provision.py +1224 -0
  162. tan/core/trusted_path.py +134 -0
  163. tan/core/uboot_breakin.py +154 -0
  164. tan/core/uri_reference.py +415 -0
  165. tan/core/user_defines.py +152 -0
  166. tan/core/venv.py +332 -0
  167. tan/core/west_patches.py +143 -0
  168. tan/core/west_workspace_refusal.py +159 -0
  169. tan/core/zephyr_env.py +124 -0
  170. tan/env.py +185 -0
  171. tan/envelope.py +647 -0
  172. tan/exit_codes.py +12 -0
  173. tan/lazy_typer.py +185 -0
  174. tan/model/__init__.py +2 -0
  175. tan/model/_gen_fixture.py +132 -0
  176. tan/model/adapters/__init__.py +116 -0
  177. tan/model/adapters/cpu.py +26 -0
  178. tan/model/adapters/deepx.py +94 -0
  179. tan/model/adapters/drpai.py +242 -0
  180. tan/model/adapters/ethos_u.py +951 -0
  181. tan/model/adapters/executorch.py +55 -0
  182. tan/model/analyze.py +694 -0
  183. tan/model/build.py +304 -0
  184. tan/model/check.py +682 -0
  185. tan/model/manifest.py +625 -0
  186. tan/model/measure.py +216 -0
  187. tan/model/npu_ops_validate.py +130 -0
  188. tan/model/package.py +125 -0
  189. tan/model/perf.py +635 -0
  190. tan/model/perf_apply.py +721 -0
  191. tan/model/prep.py +193 -0
  192. tan/model/sram_fit.py +502 -0
  193. tan/model/targets.py +374 -0
  194. tan/model/tensorio.py +287 -0
  195. tan/net.py +73 -0
  196. tan/output_format.py +213 -0
  197. tan/planner/__init__.py +219 -0
  198. tan/planner/aperture.py +202 -0
  199. tan/planner/buildplan.py +1092 -0
  200. tan/planner/camera_owner.py +192 -0
  201. tan/planner/cameras.py +132 -0
  202. tan/planner/carveout.py +755 -0
  203. tan/planner/cli.py +274 -0
  204. tan/planner/headers.py +315 -0
  205. tan/planner/kconfig.py +2435 -0
  206. tan/planner/kconfig_symbols.py +473 -0
  207. tan/planner/libraries.py +677 -0
  208. tan/planner/link_target.py +237 -0
  209. tan/planner/linux_ownership.py +216 -0
  210. tan/planner/loader.py +1525 -0
  211. tan/planner/manifest.py +116 -0
  212. tan/planner/memory.py +161 -0
  213. tan/planner/memregion.py +56 -0
  214. tan/planner/models.py +533 -0
  215. tan/planner/orchestrator.py +874 -0
  216. tan/planner/ownership.py +203 -0
  217. tan/planner/partition.py +1160 -0
  218. tan/planner/paths.py +32 -0
  219. tan/planner/plain_slice.py +139 -0
  220. tan/planner/project_emit/__init__.py +60 -0
  221. tan/planner/project_emit/bom_netlist.py +375 -0
  222. tan/planner/project_emit/dts.py +580 -0
  223. tan/planner/project_emit/hw_info.py +221 -0
  224. tan/planner/project_emit/native_sim.py +136 -0
  225. tan/planner/project_emit/west_libs.py +213 -0
  226. tan/planner/project_loader.py +264 -0
  227. tan/planner/sdk_capability.py +203 -0
  228. tan/planner/sdk_compat.py +395 -0
  229. tan/planner/secure.py +350 -0
  230. tan/planner/slugs.py +394 -0
  231. tan/planner/som_metadata.py +325 -0
  232. tan/planner/strict_loaders.py +131 -0
  233. tan/planner/template.py +1037 -0
  234. tan/planner/template_pins.py +535 -0
  235. tan/planner/template_rewrite.py +714 -0
  236. tan/planner/topology.py +208 -0
  237. tan/planner/validate.py +365 -0
  238. tan/planner/whole_device_alias.py +67 -0
  239. tan/planner/zephyr_board.py +3510 -0
  240. tan/planner_cli.py +97 -0
  241. tan/planner_emit.py +617 -0
  242. tan/planner_root.py +119 -0
  243. tan/soc_ref.py +58 -0
  244. tan/templates/__init__.py +34 -0
  245. tan/templates/vendored/MANIFEST.md +1862 -0
  246. tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
  247. tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
  248. tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
  249. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
  250. tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
  251. tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
  252. tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
  253. tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
  254. tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
  255. tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
  256. tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
  257. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
  258. tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
  259. tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
  260. tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
  261. tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
  262. tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
  263. tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
  264. tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
  265. tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
  266. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
  267. tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
  268. tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
  269. tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
  270. tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
  271. tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
  272. tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
  273. tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
  274. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
  275. tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
  276. tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
  277. tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
  278. tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
  279. tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
  280. tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
  281. tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
  282. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
  283. tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
  284. tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
  285. tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
  286. tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
  287. tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
  288. tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
  289. tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
  290. tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
  291. tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
  292. tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
  293. tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
  294. tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
  295. tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
  296. tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
  297. tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
  298. tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
  299. tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
  300. tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
  301. tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
  302. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
  303. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
  304. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
  305. tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
  306. tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
  307. tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
  308. tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
  309. tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
  310. tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
  311. tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
  312. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
  313. tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
  314. tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
  315. tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
  316. tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
  317. tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
  318. tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
  319. tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
  320. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
  321. tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
  322. tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
  323. tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
  324. tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
  325. tan/version.py +102 -0
  326. tan_cli-0.7.0.dist-info/METADATA +125 -0
  327. tan_cli-0.7.0.dist-info/RECORD +330 -0
  328. tan_cli-0.7.0.dist-info/WHEEL +5 -0
  329. tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
  330. tan_cli-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,490 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ """`tan scaffold` -- scaffold one module (a source/header pair + README) into
3
+ an EXISTING tan project. Distinct from `tan init`, which scaffolds a whole new
4
+ project: this command never WRITES `board.yaml`, never resolves an SDK, and
5
+ its template id space (`tan.core.module_template.MODULE_TEMPLATE_IDS`) is a
6
+ different, smaller registry than `tan init`'s own six.
7
+
8
+ It does now READ one (tan-cli#1031). Until then it never looked, so
9
+ `project.boardYaml` was hardcoded `null` and every generated module carried
10
+ `// Board context: unavailable` -- including a run that passed
11
+ `--board-yaml <p>/board.yaml`, whose own help text promises it "overrides
12
+ project resolution". That mattered beyond the comment: alp-sdk-vscode had just
13
+ DELETED its own TypeScript copy of this generator (alp-sdk-vscode#601/#633,
14
+ because a second copy is what let that copy's README lose its `## Wiring`
15
+ section and ship modules that never compiled), and that copy DID read
16
+ `board.yaml`. So the capability comes from here or from nowhere. The read goes
17
+ through `tan.core.board_context` -- `tan validate`'s own resolver, moved to a
18
+ shared home rather than cloned into a seventh one.
19
+
20
+ Composition, not logic: resolve the module name + template + destination
21
+ (`--name`/`--template`/`--destination`, and `tan.core.module_template`'s
22
+ registry) and the project's `board.yaml`, ask the registry for the planned
23
+ three files, diff them against disk
24
+ (`tan.core.scaffold.collect_file_changes`), then preview / guard / write --
25
+ folding whatever comes back into exactly one envelope. Mirrors
26
+ `crates/tan-cli/src/commands/scaffold.rs`, which the board read is a
27
+ deliberate, argued divergence from.
28
+
29
+ **`--name` is REQUIRED, with NO non-interactive default.** Unlike `tan init`'s
30
+ `--template` (defaults to `zephyr-app`) or `--name` (defaults to an empty
31
+ subdirectory), a module scaffold has no sane default name -- so the
32
+ non-interactive contract is a REFUSAL (`scaffold.name-required`, exit 2),
33
+ never a default (`crates/tan-cli/src/commands/scaffold.rs`'s own history,
34
+ CHANGELOG.md's #187-follow-up entry: "its non-interactive contract is a
35
+ refusal, not a default, since a module name has no sane one"). `--template`
36
+ DOES have a non-interactive default (`sensor-driver`, the registry's first
37
+ entry) when omitted.
38
+
39
+ **Interactivity mirrors the oracle's `GlobalArgs::can_prompt()`
40
+ (`crates/tan-cli/src/cli.rs`) exactly**: may only prompt when NOT
41
+ `--non-interactive`, NOT `--ci`, NOT `--format json`, AND both stdin and
42
+ stderr are real terminals. The last two matter more than they look: a
43
+ prompting library renders to stderr and reads through the controlling
44
+ terminal, so `stdin=tty, stderr=piped` -- every wrapper that captures output
45
+ while inheriting the terminal -- still cannot be prompted safely and must
46
+ refuse rather than hang. No CI runner, and no `pytest` subprocess, has a TTY,
47
+ so `--name`/`--template` are effectively always required in an automated run;
48
+ this is intentional (the module docstring for `can_prompt` documents the same
49
+ for the Rust binary: "no CI runner has a TTY"). The interactive fallback here
50
+ uses `click.prompt`/`click.Choice` rather than a `Select`/`Text` TUI widget,
51
+ the same simplification `tan.commands.new_som_cmd` already made and documents
52
+ (no arrow-key menu dependency for a path automated tests never exercise).
53
+
54
+ Every failure is a coded issue, never a traceback -- the backstop at the
55
+ bottom of [`scaffold`] converts any unexpected exception into
56
+ `scaffold.internal-failure` rather than letting it escape, matching
57
+ `init_cmd`/`debug_config_cmd`'s own catch-all.
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import sys
63
+ from dataclasses import dataclass, field
64
+ from pathlib import Path
65
+
66
+ import click
67
+ import typer
68
+
69
+ from tan.core.board_context import read_board_context, resolve_board_path
70
+ from tan.core.consent import can_prompt
71
+ from tan.core.module_template import (
72
+ DEFAULT_MODULE_TEMPLATE_ID,
73
+ MODULE_TEMPLATE_IDS,
74
+ create_module_scaffold_plan,
75
+ )
76
+ from tan.core.scaffold import FileChange, PlannedFile, ScaffoldWriteError, collect_file_changes
77
+ from tan.core.scaffold import scaffold_tree_preview as _tree_preview
78
+ from tan.core.scaffold import write_files
79
+ from tan.envelope import Envelope, Issue, Project, emit
80
+ from tan.exit_codes import ExitCode
81
+ from tan.output_format import FORMAT_HELP, OutputFormat, resolve_format
82
+
83
+ #: `data.schemaVersion` for this command's payload.
84
+ DATA_SCHEMA_VERSION = "1"
85
+
86
+
87
+ class ScaffoldError(Exception):
88
+ """A failure with its issue code and exit code already decided -- the
89
+ ONE exception type every resolution/planning/write step in [`scaffold`]
90
+ raises, mirrors `init_cmd.InitError`'s reason for existing: a single
91
+ exception class lets the whole computation run inside ONE `try`, with the
92
+ error-shaped envelope built exactly once, after it, in a SIBLING `except`
93
+ clause. Calling an emit-and-`typer.Exit` helper from a handler that is
94
+ itself still lexically nested INSIDE that same `try` does not work --
95
+ `typer.Exit` subclasses `RuntimeError`, so raising it from a nested
96
+ `except ScaffoldWriteError:` block is still within the outer try's
97
+ dynamic extent and gets re-caught by the outer `except Exception:`
98
+ backstop, turning a clean exit 3 into a misreported `scaffold.internal-
99
+ failure` at exit 5 (caught by this port's own oracle-diff smoke test,
100
+ not a golden -- there is no committed fixture for this shape).
101
+
102
+ `partial` carries the files that DID land when a write failed part-way:
103
+ `written: []` for a module half-written to disk would contradict the
104
+ filesystem, the same reasoning `InitError.partial` documents.
105
+ """
106
+
107
+ def __init__(
108
+ self,
109
+ code: str,
110
+ message: str,
111
+ exit_code: ExitCode,
112
+ *,
113
+ partial: tuple[list[str], list[str]] = ([], []),
114
+ ) -> None:
115
+ super().__init__(message)
116
+ self.code = code
117
+ self.message = message
118
+ self.exit_code = exit_code
119
+ self.partial = partial
120
+
121
+
122
+ @dataclass
123
+ class _Outcome:
124
+ """A completed (non-error) run: preview, overwrite-guard refusal, or
125
+ write. Built and returned rather than emitted in place, so the exception
126
+ guard in [`scaffold`] can wrap the whole computation without also
127
+ catching `typer.Exit`."""
128
+
129
+ template_id: str
130
+ module_name: str
131
+ normalized_name: str
132
+ destination: str
133
+ preview: bool
134
+ file_changes: list[FileChange]
135
+ files: list[PlannedFile]
136
+ written: list[str] = field(default_factory=list)
137
+ unchanged: list[str] = field(default_factory=list)
138
+ exit_code: ExitCode = ExitCode.SUCCESS
139
+ issue: Issue | None = None
140
+
141
+
142
+ # ---------------------------------------------------------------------------
143
+ # Interactivity
144
+ # ---------------------------------------------------------------------------
145
+
146
+
147
+ def _need_name() -> ScaffoldError:
148
+ return ScaffoldError(
149
+ "scaffold.name-required",
150
+ "Module name is required. Use --name <name> or run interactively.",
151
+ ExitCode.VALIDATION_FAILURE,
152
+ )
153
+
154
+
155
+ def _cancelled() -> ScaffoldError:
156
+ return ScaffoldError("scaffold.cancelled", "Cancelled.", ExitCode.RUNTIME_FAILURE)
157
+
158
+
159
+ def _resolve_module_name(name: str | None, interactive: bool) -> str:
160
+ if name is not None:
161
+ return name
162
+ if not interactive:
163
+ raise _need_name()
164
+ try:
165
+ # `err=True`: `click.prompt`'s own default (`err=False`) writes the
166
+ # question to stdout, which corrupts whatever this run is piping or
167
+ # redirecting there and leaves a real terminal on stderr showing
168
+ # nothing to answer (tan-cli#496 defect 6, measured against the
169
+ # frozen oracle: `inquire::Text` renders to stderr).
170
+ raw = click.prompt("Module name", err=True)
171
+ except click.exceptions.Abort as err:
172
+ raise _cancelled() from err
173
+ stripped = raw.strip()
174
+ if not stripped:
175
+ raise _need_name()
176
+ return stripped
177
+
178
+
179
+ def _resolve_template(template: str | None, interactive: bool) -> str:
180
+ if template is not None:
181
+ if template not in MODULE_TEMPLATE_IDS:
182
+ raise ScaffoldError(
183
+ "scaffold.invalid-template",
184
+ f"Unknown module template '{template}'.",
185
+ ExitCode.VALIDATION_FAILURE,
186
+ )
187
+ return template
188
+ if not interactive:
189
+ return DEFAULT_MODULE_TEMPLATE_ID
190
+ try:
191
+ # See `_resolve_module_name`'s `err=True` note -- same defect.
192
+ return click.prompt(
193
+ "Select a module template", type=click.Choice(MODULE_TEMPLATE_IDS), err=True
194
+ )
195
+ except click.exceptions.Abort as err:
196
+ raise _cancelled() from err
197
+
198
+
199
+ # ---------------------------------------------------------------------------
200
+ # Envelope assembly
201
+ # ---------------------------------------------------------------------------
202
+
203
+
204
+ def _data(
205
+ *,
206
+ template_id: str,
207
+ module_name: str,
208
+ normalized_module_name: str,
209
+ destination: str,
210
+ preview: bool,
211
+ file_changes: list[FileChange],
212
+ written: list[str],
213
+ unchanged: list[str],
214
+ ) -> dict:
215
+ return {
216
+ "schemaVersion": DATA_SCHEMA_VERSION,
217
+ "templateId": template_id,
218
+ "moduleName": module_name,
219
+ "normalizedModuleName": normalized_module_name,
220
+ "destination": destination,
221
+ "preview": preview,
222
+ "fileChanges": [{"relativePath": c.relative_path, "kind": c.kind} for c in file_changes],
223
+ "written": written,
224
+ "unchanged": unchanged,
225
+ }
226
+
227
+
228
+ _EMPTY_DATA_FIELDS = {
229
+ "template_id": "",
230
+ "module_name": "",
231
+ "normalized_module_name": "",
232
+ "destination": "",
233
+ "preview": False,
234
+ "file_changes": [],
235
+ }
236
+
237
+
238
+ def _stderr(line: str) -> None:
239
+ print(line, file=sys.stderr)
240
+
241
+
242
+ def _emit_error(json_mode: bool, err: ScaffoldError) -> None:
243
+ """An error before (or during) a write: `project.root: null`, every
244
+ plan-shaped field empty. `written`/`unchanged` are usually empty too, EXCEPT
245
+ on a part-way write failure (`err.partial`), where they carry whatever
246
+ landed before it -- reporting `written: []` there would contradict the
247
+ filesystem. Mirrors `error_run`/`write_error_run` in the Rust
248
+ (`scaffold.rs`), which the wire shape is otherwise identical between.
249
+ """
250
+ written, unchanged = err.partial
251
+ if json_mode:
252
+ emit(
253
+ Envelope(
254
+ "scaffold",
255
+ Project(root=None, board_yaml=None),
256
+ _data(**_EMPTY_DATA_FIELDS, written=written, unchanged=unchanged),
257
+ [Issue(err.code, "error", err.message)],
258
+ err.exit_code,
259
+ )
260
+ )
261
+ else:
262
+ _stderr(f"scaffold: {err.message}")
263
+ raise typer.Exit(int(err.exit_code))
264
+
265
+
266
+ def _emit_outcome(json_mode: bool, outcome: _Outcome, board_yaml: str | None) -> None:
267
+ """*board_yaml* is the RESOLVED path (tan-cli#1031): a PROJECT fact,
268
+ identical across all three outcome shapes, so not an `_Outcome` field.
269
+ `Project.resolved` is the blessed seam -- a path that resolved but is not
270
+ there reports `null`, as it does for `tan validate`. `_emit_error` keeps
271
+ its hardcoded `(None, None)`; that is the frozen error shape."""
272
+ project = Project.resolved(outcome.destination, board_yaml)
273
+ if json_mode:
274
+ emit(
275
+ Envelope(
276
+ "scaffold",
277
+ project,
278
+ _data(
279
+ template_id=outcome.template_id,
280
+ module_name=outcome.module_name,
281
+ normalized_module_name=outcome.normalized_name,
282
+ destination=outcome.destination,
283
+ preview=outcome.preview,
284
+ file_changes=outcome.file_changes,
285
+ written=outcome.written,
286
+ unchanged=outcome.unchanged,
287
+ ),
288
+ [outcome.issue] if outcome.issue is not None else [],
289
+ outcome.exit_code,
290
+ )
291
+ )
292
+ elif outcome.preview:
293
+ _stderr(
294
+ f"scaffold: preview for module '{outcome.normalized_name}' "
295
+ f"(template '{outcome.template_id}')"
296
+ )
297
+ # `_tree_preview` already ends in one `\n` (one per listed path); NOT
298
+ # stripped -- `_stderr`'s own `print()` adds a second, matching the
299
+ # oracle's own two-`CommandRun.text` -> `println!` shape byte-for-byte
300
+ # (measured: `tan scaffold --preview` ends the tree with `\n\n`).
301
+ _stderr(_tree_preview(outcome.files))
302
+ elif outcome.exit_code != ExitCode.SUCCESS:
303
+ # The overwrite guard. Deliberately NOT `outcome.issue.message` --
304
+ # the Rust's text-mode line here is a separate, shorter, hardcoded
305
+ # string (`scaffold.rs`'s guard block), not the JSON issue message
306
+ # ("One or more files would be overwritten. Use --force to allow
307
+ # updates.") rendered with a prefix; measured against the oracle.
308
+ _stderr("scaffold: would overwrite existing files; use --force to proceed.")
309
+ else:
310
+ _stderr(
311
+ f"scaffold: created module '{outcome.normalized_name}' "
312
+ f"(template '{outcome.template_id}')"
313
+ )
314
+ _stderr(f" written: {len(outcome.written)}, unchanged: {len(outcome.unchanged)}")
315
+ raise typer.Exit(int(outcome.exit_code))
316
+
317
+
318
+ # ---------------------------------------------------------------------------
319
+ # Entry point
320
+ # ---------------------------------------------------------------------------
321
+
322
+
323
+ def scaffold(
324
+ ctx: typer.Context,
325
+ template: str = typer.Option(
326
+ None,
327
+ "--template",
328
+ metavar="TEMPLATE",
329
+ help=f"Module template id ({', '.join(MODULE_TEMPLATE_IDS)}).",
330
+ ),
331
+ name: str = typer.Option(
332
+ None, "--name", metavar="NAME", help="Module name (required)."
333
+ ),
334
+ destination: str = typer.Option(
335
+ None,
336
+ "--destination",
337
+ metavar="DESTINATION",
338
+ help="Destination project root (default: current directory or --project).",
339
+ ),
340
+ preview: bool = typer.Option(
341
+ False, "--preview", help="Show planned files without writing anything."
342
+ ),
343
+ force: bool = typer.Option(
344
+ False, "--force", help="Allow overwriting existing files."
345
+ ),
346
+ project: str = typer.Option(
347
+ None, "--project", metavar="PATH", help="Project root (defaults to current directory)."
348
+ ),
349
+ board_yaml: str = typer.Option(
350
+ None,
351
+ "--board-yaml",
352
+ metavar="PATH",
353
+ help="Explicit board.yaml path (overrides project resolution).",
354
+ ),
355
+ sdk_root: str = typer.Option(
356
+ None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."
357
+ ),
358
+ target: str = typer.Option(
359
+ None,
360
+ "--target",
361
+ metavar="EMIT",
362
+ help="Generation target (e.g. zephyr-conf, dts-overlay, cmake-args, yocto-conf).",
363
+ ),
364
+ all_targets: bool = typer.Option(
365
+ False, "--all", help="Run command against all relevant targets."
366
+ ),
367
+ output_format: OutputFormat = typer.Option(None, "--format", help=FORMAT_HELP),
368
+ verbose: bool = typer.Option(False, "--verbose", help="Emit additional diagnostic detail."),
369
+ quiet: bool = typer.Option(False, "--quiet", help="Suppress non-essential output."),
370
+ no_color: bool = typer.Option(
371
+ False, "--no-color", help="Disable ANSI color in text output."
372
+ ),
373
+ non_interactive: bool = typer.Option(
374
+ False,
375
+ "--non-interactive",
376
+ help="Never prompt; fail instead of asking when a required value is missing.",
377
+ ),
378
+ ci: bool = typer.Option(
379
+ False, "--ci", help="CI mode: implies non-interactive and disables color."
380
+ ),
381
+ ) -> None:
382
+ """Scaffold a module into an existing project."""
383
+ # `--sdk-root`/`--target`/`--all`/`--verbose`/`--quiet`/`--no-color` are
384
+ # `GlobalArgs` members the oracle's `scaffold.rs::run` never reads;
385
+ # declared (not `hidden=True`) so `--help` matches its listing.
386
+ # `--board-yaml` was in this list until tan-cli#1031 and is READ now.
387
+ del sdk_root, target, all_targets, verbose, quiet, no_color
388
+
389
+ resolved_format = resolve_format(output_format, ctx.obj, choices=OutputFormat)
390
+ json_mode = resolved_format == "json"
391
+ interactive = can_prompt(non_interactive=non_interactive, ci=ci, json_mode=json_mode)
392
+
393
+ try:
394
+ module_name = _resolve_module_name(name, interactive)
395
+ template_id = _resolve_template(template, interactive)
396
+
397
+ dest = destination if destination else (project if project else ".")
398
+ project_root = Path(dest)
399
+ # tan-cli#1031. `tan validate`'s own resolver, MOVED to
400
+ # `tan.core.board_context` and called here -- not a second strategy.
401
+ _, board_path = resolve_board_path(dest, board_yaml)
402
+ board_context = read_board_context(board_path)
403
+
404
+ try:
405
+ plan = create_module_scaffold_plan(template_id, module_name, board_context)
406
+ except ValueError as err:
407
+ # Re-raised as `ScaffoldError`, never emitted from here directly:
408
+ # this `except` is still lexically INSIDE the outer `try` below,
409
+ # so a `typer.Exit` raised from an emit helper called here would
410
+ # be re-caught by this same function's own `except Exception`
411
+ # backstop (`typer.Exit` subclasses `RuntimeError`) -- see
412
+ # `ScaffoldError`'s own docstring for the mechanism and how this
413
+ # was actually caught (an oracle-diff smoke test, not a golden).
414
+ raise ScaffoldError(
415
+ "scaffold.invalid-name", str(err), ExitCode.VALIDATION_FAILURE
416
+ ) from err
417
+
418
+ changes = collect_file_changes(project_root, plan.files)
419
+ has_updates = any(c.kind == "update" for c in changes)
420
+
421
+ if preview:
422
+ # Before the overwrite guard, deliberately -- a preview touches no
423
+ # disk, so it has nothing to be guarded against (same ordering
424
+ # `tan init` learned the hard way; see `tan.core.scaffold`'s
425
+ # `write_files` docstring for the sibling incident).
426
+ outcome = _Outcome(
427
+ template_id=template_id,
428
+ module_name=module_name,
429
+ normalized_name=plan.normalized_name,
430
+ destination=dest,
431
+ preview=True,
432
+ file_changes=changes,
433
+ files=plan.files,
434
+ )
435
+ elif has_updates and not force:
436
+ outcome = _Outcome(
437
+ template_id=template_id,
438
+ module_name=module_name,
439
+ normalized_name=plan.normalized_name,
440
+ destination=dest,
441
+ preview=False,
442
+ file_changes=changes,
443
+ files=plan.files,
444
+ exit_code=ExitCode.WRITE_FAILURE,
445
+ issue=Issue(
446
+ "scaffold.would-overwrite",
447
+ "error",
448
+ "One or more files would be overwritten. Use --force to allow updates.",
449
+ ),
450
+ )
451
+ else:
452
+ try:
453
+ result = write_files(project_root, plan.files)
454
+ except ScaffoldWriteError as err:
455
+ raise ScaffoldError(
456
+ "scaffold.write-failed",
457
+ f"Failed to write files: {err}",
458
+ ExitCode.WRITE_FAILURE,
459
+ partial=(err.partial.written, err.partial.unchanged),
460
+ ) from err
461
+ outcome = _Outcome(
462
+ template_id=template_id,
463
+ module_name=module_name,
464
+ normalized_name=plan.normalized_name,
465
+ destination=dest,
466
+ preview=False,
467
+ file_changes=changes,
468
+ files=plan.files,
469
+ written=result.written,
470
+ unchanged=result.unchanged,
471
+ )
472
+ except ScaffoldError as err:
473
+ _emit_error(json_mode, err)
474
+ return
475
+ except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
476
+ # `typer.Exit` cannot reach here: it is only ever raised from
477
+ # `_emit_error`, called from the SIBLING `except ScaffoldError` clause
478
+ # above -- outside this try's dynamic extent, so it propagates
479
+ # straight out rather than looping back into this handler.
480
+ _emit_error(
481
+ json_mode,
482
+ ScaffoldError(
483
+ "scaffold.internal-failure",
484
+ f"scaffold failed unexpectedly: {err.__class__.__name__}: {err}",
485
+ ExitCode.INTERNAL_FAILURE,
486
+ ),
487
+ )
488
+ return
489
+
490
+ _emit_outcome(json_mode, outcome, board_path)