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.
- tan/__init__.py +0 -0
- tan/__main__.py +120 -0
- tan/cli.py +939 -0
- tan/command_table.py +265 -0
- tan/commands/__init__.py +8 -0
- tan/commands/bootstrap_cmd.py +4856 -0
- tan/commands/bootstrap_patches.py +164 -0
- tan/commands/bootstrap_toolchain_fetch.py +155 -0
- tan/commands/bootstrap_toolchain_pin.py +36 -0
- tan/commands/build/__init__.py +1 -0
- tan/commands/build/configure_inputs.py +277 -0
- tan/commands/build/execute.py +1852 -0
- tan/commands/build/host_python.py +59 -0
- tan/commands/build/link_stale.py +78 -0
- tan/commands/build/manifest.py +428 -0
- tan/commands/build/materialise.py +81 -0
- tan/commands/build/token_substitution.py +381 -0
- tan/commands/build/toolchain.py +482 -0
- tan/commands/build/user_defines_wipe.py +115 -0
- tan/commands/build/workspace_patches.py +65 -0
- tan/commands/build_cmd.py +2236 -0
- tan/commands/build_output.py +331 -0
- tan/commands/clean_cmd.py +1183 -0
- tan/commands/completion_cmd.py +786 -0
- tan/commands/debug_config_cmd.py +2425 -0
- tan/commands/diff_cmd.py +940 -0
- tan/commands/doctor_cmd.py +4767 -0
- tan/commands/examples_cmd.py +644 -0
- tan/commands/explain_cmd.py +1319 -0
- tan/commands/faultdecode_cmd.py +931 -0
- tan/commands/flash_atoc_guard.py +240 -0
- tan/commands/flash_cmd.py +5731 -0
- tan/commands/flash_mram_guard.py +150 -0
- tan/commands/flash_ram.py +578 -0
- tan/commands/flash_raw.py +583 -0
- tan/commands/generate_cmd.py +1456 -0
- tan/commands/image_cmd.py +753 -0
- tan/commands/init_cmd.py +1740 -0
- tan/commands/inspect_cmd.py +377 -0
- tan/commands/kconfig_cmd.py +734 -0
- tan/commands/model_cmd.py +1514 -0
- tan/commands/model_device_cmd.py +351 -0
- tan/commands/model_device_live.py +234 -0
- tan/commands/model_host_cmd.py +430 -0
- tan/commands/model_zoo_cmd.py +363 -0
- tan/commands/monitor_actions.py +125 -0
- tan/commands/monitor_cmd.py +626 -0
- tan/commands/monitor_session.py +505 -0
- tan/commands/new_som_cmd.py +1522 -0
- tan/commands/pinmux_cmd.py +568 -0
- tan/commands/presets_cmd.py +1123 -0
- tan/commands/probe_cmd.py +421 -0
- tan/commands/reset_cmd.py +354 -0
- tan/commands/reset_confirm.py +168 -0
- tan/commands/run_cmd.py +637 -0
- tan/commands/scaffold_cmd.py +490 -0
- tan/commands/sdk_cmd.py +1723 -0
- tan/commands/size_cmd.py +1020 -0
- tan/commands/support_bundle_cmd.py +1073 -0
- tan/commands/trace_cmd.py +341 -0
- tan/commands/validate_cmd.py +2009 -0
- tan/commands/west_forward_cmd.py +736 -0
- tan/commands/workspace_patch_check.py +296 -0
- tan/core/__init__.py +1 -0
- tan/core/alp_conf_pregen.py +100 -0
- tan/core/artifact_provenance.py +240 -0
- tan/core/atoc_guard.py +276 -0
- tan/core/atoc_guard_messages.py +232 -0
- tan/core/atoc_replacement.py +118 -0
- tan/core/atomic_write.py +178 -0
- tan/core/board_context.py +351 -0
- tan/core/board_diagnostic.py +111 -0
- tan/core/board_files.py +221 -0
- tan/core/board_validator.py +542 -0
- tan/core/board_validator_compat.py +196 -0
- tan/core/board_validator_run.py +129 -0
- tan/core/board_validator_schema.py +186 -0
- tan/core/board_validator_skew.py +46 -0
- tan/core/board_yaml_edit.py +295 -0
- tan/core/board_yaml_pos.py +118 -0
- tan/core/bootstrap.py +2572 -0
- tan/core/build_plan.py +625 -0
- tan/core/consent.py +104 -0
- tan/core/console_filter.py +245 -0
- tan/core/debug_launch.py +1803 -0
- tan/core/devicetree_lint.py +168 -0
- tan/core/dir_removal.py +229 -0
- tan/core/doctor_git.py +172 -0
- tan/core/doctor_libraries.py +253 -0
- tan/core/doctor_render.py +185 -0
- tan/core/doctor_scope.py +66 -0
- tan/core/doctor_setools.py +237 -0
- tan/core/doctor_stale.py +180 -0
- tan/core/document_guards.py +723 -0
- tan/core/dp_id.py +97 -0
- tan/core/elf_load.py +63 -0
- tan/core/error_catalog.py +171 -0
- tan/core/example_catalog.py +499 -0
- tan/core/example_facets.py +151 -0
- tan/core/faultdecode.py +521 -0
- tan/core/flash_plan.py +2501 -0
- tan/core/flow_d_report.py +381 -0
- tan/core/flow_d_uptime.py +164 -0
- tan/core/fs_confine.py +130 -0
- tan/core/global_flags.py +230 -0
- tan/core/host_python.py +210 -0
- tan/core/image_bundle.py +247 -0
- tan/core/inert.py +147 -0
- tan/core/jlink_binary.py +128 -0
- tan/core/jlink_probe.py +481 -0
- tan/core/jsonc_splice.py +404 -0
- tan/core/launch_provenance.py +257 -0
- tan/core/link_refusal.py +76 -0
- tan/core/metadata_schema.py +376 -0
- tan/core/model_check.py +227 -0
- tan/core/model_device.py +449 -0
- tan/core/model_doctor.py +198 -0
- tan/core/model_host.py +48 -0
- tan/core/model_list.py +112 -0
- tan/core/model_zoo.py +353 -0
- tan/core/module_template.py +340 -0
- tan/core/mram_link.py +87 -0
- tan/core/new_som_ospi.py +94 -0
- tan/core/os_class.py +159 -0
- tan/core/patch_paths.py +87 -0
- tan/core/pending.py +40 -0
- tan/core/plain_zephyr_plan.py +138 -0
- tan/core/plan_exec.py +373 -0
- tan/core/plan_tokens.py +600 -0
- tan/core/probe.py +155 -0
- tan/core/probe_plan.py +237 -0
- tan/core/proxy.py +168 -0
- tan/core/publish.py +31 -0
- tan/core/python_floor.py +178 -0
- tan/core/ram_run.py +694 -0
- tan/core/ram_watch.py +231 -0
- tan/core/raw_write.py +160 -0
- tan/core/reset_plan.py +56 -0
- tan/core/run.py +161 -0
- tan/core/scaffold.py +2203 -0
- tan/core/scaffold_selftest_identity.py +225 -0
- tan/core/sdk_default_registry.py +565 -0
- tan/core/sdk_discovery.py +996 -0
- tan/core/sdk_removal.py +465 -0
- tan/core/serial_actions.py +175 -0
- tan/core/serial_capture.py +185 -0
- tan/core/serial_url.py +45 -0
- tan/core/setools.py +608 -0
- tan/core/setools_scratch.py +243 -0
- tan/core/shapes.py +194 -0
- tan/core/size.py +610 -0
- tan/core/som_buildability.py +204 -0
- tan/core/som_schema_version.py +62 -0
- tan/core/subprocess_env.py +201 -0
- tan/core/sysbuild_image.py +94 -0
- tan/core/system_manifest.py +827 -0
- tan/core/text_layout.py +73 -0
- tan/core/timestamp.py +182 -0
- tan/core/tool_lookup.py +383 -0
- tan/core/toolchain_pin.py +117 -0
- tan/core/toolchain_provision.py +1224 -0
- tan/core/trusted_path.py +134 -0
- tan/core/uboot_breakin.py +154 -0
- tan/core/uri_reference.py +415 -0
- tan/core/user_defines.py +152 -0
- tan/core/venv.py +332 -0
- tan/core/west_patches.py +143 -0
- tan/core/west_workspace_refusal.py +159 -0
- tan/core/zephyr_env.py +124 -0
- tan/env.py +185 -0
- tan/envelope.py +647 -0
- tan/exit_codes.py +12 -0
- tan/lazy_typer.py +185 -0
- tan/model/__init__.py +2 -0
- tan/model/_gen_fixture.py +132 -0
- tan/model/adapters/__init__.py +116 -0
- tan/model/adapters/cpu.py +26 -0
- tan/model/adapters/deepx.py +94 -0
- tan/model/adapters/drpai.py +242 -0
- tan/model/adapters/ethos_u.py +951 -0
- tan/model/adapters/executorch.py +55 -0
- tan/model/analyze.py +694 -0
- tan/model/build.py +304 -0
- tan/model/check.py +682 -0
- tan/model/manifest.py +625 -0
- tan/model/measure.py +216 -0
- tan/model/npu_ops_validate.py +130 -0
- tan/model/package.py +125 -0
- tan/model/perf.py +635 -0
- tan/model/perf_apply.py +721 -0
- tan/model/prep.py +193 -0
- tan/model/sram_fit.py +502 -0
- tan/model/targets.py +374 -0
- tan/model/tensorio.py +287 -0
- tan/net.py +73 -0
- tan/output_format.py +213 -0
- tan/planner/__init__.py +219 -0
- tan/planner/aperture.py +202 -0
- tan/planner/buildplan.py +1092 -0
- tan/planner/camera_owner.py +192 -0
- tan/planner/cameras.py +132 -0
- tan/planner/carveout.py +755 -0
- tan/planner/cli.py +274 -0
- tan/planner/headers.py +315 -0
- tan/planner/kconfig.py +2435 -0
- tan/planner/kconfig_symbols.py +473 -0
- tan/planner/libraries.py +677 -0
- tan/planner/link_target.py +237 -0
- tan/planner/linux_ownership.py +216 -0
- tan/planner/loader.py +1525 -0
- tan/planner/manifest.py +116 -0
- tan/planner/memory.py +161 -0
- tan/planner/memregion.py +56 -0
- tan/planner/models.py +533 -0
- tan/planner/orchestrator.py +874 -0
- tan/planner/ownership.py +203 -0
- tan/planner/partition.py +1160 -0
- tan/planner/paths.py +32 -0
- tan/planner/plain_slice.py +139 -0
- tan/planner/project_emit/__init__.py +60 -0
- tan/planner/project_emit/bom_netlist.py +375 -0
- tan/planner/project_emit/dts.py +580 -0
- tan/planner/project_emit/hw_info.py +221 -0
- tan/planner/project_emit/native_sim.py +136 -0
- tan/planner/project_emit/west_libs.py +213 -0
- tan/planner/project_loader.py +264 -0
- tan/planner/sdk_capability.py +203 -0
- tan/planner/sdk_compat.py +395 -0
- tan/planner/secure.py +350 -0
- tan/planner/slugs.py +394 -0
- tan/planner/som_metadata.py +325 -0
- tan/planner/strict_loaders.py +131 -0
- tan/planner/template.py +1037 -0
- tan/planner/template_pins.py +535 -0
- tan/planner/template_rewrite.py +714 -0
- tan/planner/topology.py +208 -0
- tan/planner/validate.py +365 -0
- tan/planner/whole_device_alias.py +67 -0
- tan/planner/zephyr_board.py +3510 -0
- tan/planner_cli.py +97 -0
- tan/planner_emit.py +617 -0
- tan/planner_root.py +119 -0
- tan/soc_ref.py +58 -0
- tan/templates/__init__.py +34 -0
- tan/templates/vendored/MANIFEST.md +1862 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-AEN801/testcase.yaml +53 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/board.yaml +38 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.conf +11 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/boards/native_sim_native_64.overlay +29 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/src/main.c +266 -0
- tan/templates/vendored/diagnostics/E1M-V2N101/testcase.yaml +53 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/board.yaml +38 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-AEN801/testcase.yaml +62 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/CMakeLists.txt +10 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/README.md +75 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/board.yaml +37 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.c +371 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/cold_chain.h +178 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/src/main.c +373 -0
- tan/templates/vendored/edge-ai/E1M-V2N101/testcase.yaml +62 -0
- tan/templates/vendored/iot/E1M-AEN801/CMakeLists.txt +14 -0
- tan/templates/vendored/iot/E1M-AEN801/README.md +103 -0
- tan/templates/vendored/iot/E1M-AEN801/board.yaml +69 -0
- tan/templates/vendored/iot/E1M-AEN801/prj.conf +15 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.c +196 -0
- tan/templates/vendored/iot/E1M-AEN801/src/cc3501e_bridge.h +173 -0
- tan/templates/vendored/iot/E1M-AEN801/src/main.c +262 -0
- tan/templates/vendored/iot/E1M-AEN801/testcase.yaml +68 -0
- tan/templates/vendored/minimal/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/README.md +108 -0
- tan/templates/vendored/minimal/E1M-AEN801/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-AEN801/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-AEN801/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-AEN801/testcase.yaml +49 -0
- tan/templates/vendored/minimal/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/README.md +108 -0
- tan/templates/vendored/minimal/E1M-V2N101/board.yaml +32 -0
- tan/templates/vendored/minimal/E1M-V2N101/prj.conf +8 -0
- tan/templates/vendored/minimal/E1M-V2N101/src/main.c +111 -0
- tan/templates/vendored/minimal/E1M-V2N101/testcase.yaml +49 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/README.md +120 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/boards/native_sim_native_64.overlay +40 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/CMakeLists.txt +13 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/main.c +171 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/prj.conf +17 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/peer/testcase.yaml +23 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/prj.conf +22 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/src/main.c +185 -0
- tan/templates/vendored/multicore-mailbox/E1M-AEN801/testcase.yaml +26 -0
- tan/templates/vendored/sensor/E1M-AEN801/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-AEN801/README.md +112 -0
- tan/templates/vendored/sensor/E1M-AEN801/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-AEN801/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-AEN801/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-AEN801/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-AEN801/testcase.yaml +52 -0
- tan/templates/vendored/sensor/E1M-V2N101/CMakeLists.txt +8 -0
- tan/templates/vendored/sensor/E1M-V2N101/README.md +112 -0
- tan/templates/vendored/sensor/E1M-V2N101/board.yaml +57 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.conf +10 -0
- tan/templates/vendored/sensor/E1M-V2N101/boards/native_sim_native_64.overlay +28 -0
- tan/templates/vendored/sensor/E1M-V2N101/prj.conf +5 -0
- tan/templates/vendored/sensor/E1M-V2N101/src/main.c +186 -0
- tan/templates/vendored/sensor/E1M-V2N101/testcase.yaml +52 -0
- tan/version.py +102 -0
- tan_cli-0.7.0.dist-info/METADATA +125 -0
- tan_cli-0.7.0.dist-info/RECORD +330 -0
- tan_cli-0.7.0.dist-info/WHEEL +5 -0
- tan_cli-0.7.0.dist-info/entry_points.txt +2 -0
- tan_cli-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
"""`tan examples` -- the SDK's ready-made example projects, as a catalogue the
|
|
3
|
+
New Project flow scaffolds from via `tan init --from-example <sourceDir>`.
|
|
4
|
+
|
|
5
|
+
Mirrors `crates/tan-cli/src/commands/examples.rs` plus the two `tan-core`
|
|
6
|
+
helpers it composes (`wizard/filesystem.rs::discover_examples`,
|
|
7
|
+
`wizard/service/example_catalog.rs`). Kept in ONE module rather than split
|
|
8
|
+
across a `tan.core` twin: the whole thing is a directory walk and two README
|
|
9
|
+
scans, and a second file would only add an import.
|
|
10
|
+
|
|
11
|
+
**Which rule decides where the catalogue comes from.** The example list is
|
|
12
|
+
alp-sdk content, so the two candidate readings are "vendor a capture like
|
|
13
|
+
`tan init` does" and "read the resolved checkout". This reads the checkout, on
|
|
14
|
+
two invariants from
|
|
15
|
+
`alp-sdk/docs/superpowers/specs/2026-07-29-tan-port-invariants.md`:
|
|
16
|
+
|
|
17
|
+
* **I-32** bans *shelling* the SDK -- `tan init`/`tan scaffold` read a vendored
|
|
18
|
+
`--emit scaffold` tree "without ever shelling the SDK" because they must work
|
|
19
|
+
with NO checkout present at all. Nothing here spawns a subprocess; a
|
|
20
|
+
`read_dir` over `<sdk>/examples` is not a shell-out, so I-32 is untouched. Note
|
|
21
|
+
the anti-pattern it names (#22 in that doc) is *shelling* init instead of
|
|
22
|
+
reading the vendored tree -- not "reading a checkout".
|
|
23
|
+
* **I-26** ("tan must never learn a hardware fact ... `metadata/**` stays in
|
|
24
|
+
alp-sdk") is what forbids the vendored alternative HERE. A baked example
|
|
25
|
+
catalogue is a copy of alp-sdk content that drifts from the customer's actual
|
|
26
|
+
checkout, and every `sourceDir` it hands back is a path
|
|
27
|
+
`tan init --from-example` then cannot find. `tan examples`' requirement is the
|
|
28
|
+
inverse of `tan init`'s: with no checkout resolvable the catalogue is
|
|
29
|
+
legitimately EMPTY (exit 0, `examples: []`), which is exactly what the Rust
|
|
30
|
+
oracle does -- so it has no reason to carry a capture.
|
|
31
|
+
|
|
32
|
+
**One DELIBERATE divergence from the oracle** (tan-cli#400): an unresolvable
|
|
33
|
+
checkout also emits an `examples.sdk-root-unresolved` warning, which
|
|
34
|
+
`crates/tan-cli/src/commands/examples.rs` does not. That is a contract
|
|
35
|
+
ADDITION, not a port regression -- the oracle's own renderer-only hint is the
|
|
36
|
+
defect, and `crates/` ships to nobody (the release assets are PyInstaller
|
|
37
|
+
freezes of `python/tan`). Do not "restore parity" by deleting it; nothing in
|
|
38
|
+
`tests/parity/` pins this command's text or issues.
|
|
39
|
+
|
|
40
|
+
**Every failure is an envelope, never a traceback.** Discovery swallows the IO
|
|
41
|
+
errors the Rust swallows (an unreadable root or category is skipped, a README
|
|
42
|
+
that will not read or is not UTF-8 is treated as absent -- `read_to_string().ok()`
|
|
43
|
+
on the Rust side), and the catch-all in `examples` is the backstop for whatever
|
|
44
|
+
nobody enumerated. There is no subprocess to time out: this command spawns
|
|
45
|
+
nothing.
|
|
46
|
+
|
|
47
|
+
No `--os`, no `--backend`, no SKU list, no hardware facts -- the catalogue is
|
|
48
|
+
whatever directories carry a `board.yaml`, and nothing here reads one.
|
|
49
|
+
|
|
50
|
+
**tan-cli#484** adds `category`/`som`/`board`/`cores`/`coreCount`/`osSet`/
|
|
51
|
+
`declares` to each row, additively, when alp-sdk's generated
|
|
52
|
+
`metadata/catalog.json` has an entry for that `sourceDir`
|
|
53
|
+
(`tan.core.example_facets.load_example_facets`). That file is itself a
|
|
54
|
+
pre-computed artefact of alp-sdk's topology resolver, so this still does not
|
|
55
|
+
read a `board.yaml` or re-derive a hardware fact -- it reads one more
|
|
56
|
+
generated JSON file, the same shape `tan.core.error_catalog` already reads
|
|
57
|
+
`metadata/error-catalog.json` in.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from __future__ import annotations
|
|
61
|
+
|
|
62
|
+
import json
|
|
63
|
+
import os
|
|
64
|
+
import sys
|
|
65
|
+
from dataclasses import dataclass
|
|
66
|
+
from pathlib import Path
|
|
67
|
+
|
|
68
|
+
import typer
|
|
69
|
+
|
|
70
|
+
from tan.core.example_facets import ExampleFacets, load_example_facets
|
|
71
|
+
from tan.core.sdk_discovery import (
|
|
72
|
+
global_default_foreign_project_issue,
|
|
73
|
+
project_pin_issue,
|
|
74
|
+
resolve_sdk_root_wide,
|
|
75
|
+
sdk_ladder_divergence_issue,
|
|
76
|
+
with_sdk_search,
|
|
77
|
+
)
|
|
78
|
+
from tan.core.shapes import rejected_sdk_root_message
|
|
79
|
+
from tan.core.global_flags import accept_global_flags
|
|
80
|
+
from tan.envelope import Envelope, Issue, Project, SdkInfo, emit
|
|
81
|
+
from tan.exit_codes import ExitCode
|
|
82
|
+
from tan.output_format import FORMAT_HELP, OutputFormat
|
|
83
|
+
|
|
84
|
+
#: `data.schemaVersion` for this command's payload.
|
|
85
|
+
DATA_SCHEMA_VERSION = "1"
|
|
86
|
+
|
|
87
|
+
#: First bytes that mark a README line as markdown STRUCTURE rather than prose.
|
|
88
|
+
#: Verbatim from `is_readme_prose` (`wizard/service/example_catalog.rs`) --
|
|
89
|
+
#: headings, blockquotes, HTML, links, badges, tables, list/rule markers, code.
|
|
90
|
+
_NON_PROSE_PREFIXES = "#><[!|-*+=`"
|
|
91
|
+
|
|
92
|
+
#: tan-cli#400: "no checkout resolved" is a DIAGNOSIS, not a shrug. Until this
|
|
93
|
+
#: landed the condition existed only as a parenthetical question in the text
|
|
94
|
+
#: renderer; the envelope carried `ok:true`, `examples: []`, `issues: []` --
|
|
95
|
+
#: indistinguishable from a healthy SDK that genuinely ships no examples. The
|
|
96
|
+
#: extension's picker therefore rendered an empty list with no error, no log
|
|
97
|
+
#: line and no warning, and `tan init --from-example <sourceDir>` was
|
|
98
|
+
#: unreachable with nothing pointing at the misconfigured root.
|
|
99
|
+
#:
|
|
100
|
+
#: Deliberately mirrors `presets_cmd.py`'s `SDK_UNRESOLVED_CODE` /
|
|
101
|
+
#: `SDK_UNRESOLVED_MESSAGE` -- same `<command>.sdk-root-unresolved` spelling and
|
|
102
|
+
#: same "condition, then what you got instead, then the remediation" shape that
|
|
103
|
+
#: `presets`, `pinmux`, `build`, `generate`, `init`, `model` and `trace` all
|
|
104
|
+
#: already use for this identical bad path. `reserved`, not `frozen`, in
|
|
105
|
+
#: `contract/issue-codes.json`: no consumer matches it with `===` yet.
|
|
106
|
+
SDK_UNRESOLVED_CODE = "examples.sdk-root-unresolved"
|
|
107
|
+
SDK_UNRESOLVED_MESSAGE = (
|
|
108
|
+
"alp-sdk root is unresolved. Returning an empty example catalogue; "
|
|
109
|
+
"pass --sdk-root <path> to name the checkout."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
#: What the reader GOT instead, for the `--sdk-root`-was-given-and-rejected
|
|
113
|
+
#: branch (`tan.core.shapes.rejected_sdk_root_message`, tan-cli#497). The remediation
|
|
114
|
+
#: clause above is dropped there: recommending the flag the caller just typed
|
|
115
|
+
#: is what made the old message self-defeating.
|
|
116
|
+
SDK_UNRESOLVED_CONSEQUENCE = "Returning an empty example catalogue."
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclass(frozen=True)
|
|
120
|
+
class Example:
|
|
121
|
+
"""One catalogue entry. `id`/`sourceDir`/`title`/`description` are the
|
|
122
|
+
emitted JSON order the frozen oracle also uses, matching `ExampleEntry`'s
|
|
123
|
+
declaration order in `examples.rs`. `facets` (tan-cli#484) is an ADDITIVE
|
|
124
|
+
Python-only extension the oracle never carried -- when
|
|
125
|
+
`metadata/catalog.json` has an entry for this `sourceDir`, its fields are
|
|
126
|
+
appended after the original four, in the order alp-sdk's own issue
|
|
127
|
+
illustrated: `category`, `som`, `board`, `cores`, `coreCount`, `osSet`,
|
|
128
|
+
`declares`. `facets is None` (an older SDK, or an example the catalogue
|
|
129
|
+
does not know about yet) reproduces the pre-#484 four-key wire shape
|
|
130
|
+
exactly -- no existing reader breaks."""
|
|
131
|
+
|
|
132
|
+
id: str
|
|
133
|
+
source_dir: str
|
|
134
|
+
title: str
|
|
135
|
+
description: str
|
|
136
|
+
facets: ExampleFacets | None = None
|
|
137
|
+
|
|
138
|
+
def as_dict(self) -> dict[str, object]:
|
|
139
|
+
out: dict[str, object] = {
|
|
140
|
+
"id": self.id,
|
|
141
|
+
"sourceDir": self.source_dir,
|
|
142
|
+
"title": self.title,
|
|
143
|
+
"description": self.description,
|
|
144
|
+
}
|
|
145
|
+
facets = self.facets
|
|
146
|
+
if facets is not None:
|
|
147
|
+
out["category"] = facets.category
|
|
148
|
+
if facets.som is not None:
|
|
149
|
+
out["som"] = facets.som
|
|
150
|
+
if facets.board is not None:
|
|
151
|
+
out["board"] = facets.board
|
|
152
|
+
if facets.cores is not None:
|
|
153
|
+
out["cores"] = [dict(core) for core in facets.cores]
|
|
154
|
+
if facets.core_count is not None:
|
|
155
|
+
out["coreCount"] = facets.core_count
|
|
156
|
+
if facets.os_set is not None:
|
|
157
|
+
out["osSet"] = list(facets.os_set)
|
|
158
|
+
if facets.declares is not None:
|
|
159
|
+
out["declares"] = dict(facets.declares)
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
# Discovery
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _read_readme(example_dir: Path) -> str | None:
|
|
169
|
+
"""A `README.md`'s text, or `None` when there is not a usable one.
|
|
170
|
+
|
|
171
|
+
Missing, unreadable, and non-UTF-8 all collapse to `None`, matching Rust's
|
|
172
|
+
`std::fs::read_to_string(...).ok()`. `encoding="utf-8"` is explicit (I-27):
|
|
173
|
+
the default would decode with the host locale, so a README carrying a `⚠️`
|
|
174
|
+
-- alp-sdk already ships several -- raises `UnicodeDecodeError` on a cp1252
|
|
175
|
+
Windows host and passes on ubuntu CI.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
return (example_dir / "README.md").read_text(encoding="utf-8")
|
|
179
|
+
except (OSError, UnicodeDecodeError, ValueError):
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _subdirectories(root: Path) -> list[Path]:
|
|
184
|
+
"""`root`'s immediate subdirectories, or `[]` when it will not read.
|
|
185
|
+
|
|
186
|
+
`follow_symlinks=False` deliberately: Rust's `DirEntry::file_type()` does
|
|
187
|
+
not traverse a symlink, so a symlinked example directory is not a directory
|
|
188
|
+
to the oracle and is skipped. (The `board.yaml` probe below DOES follow, as
|
|
189
|
+
`Path::is_file()` does.) Order is not relied on -- results are sorted.
|
|
190
|
+
"""
|
|
191
|
+
try:
|
|
192
|
+
with os.scandir(root) as entries:
|
|
193
|
+
return [
|
|
194
|
+
Path(entry.path)
|
|
195
|
+
for entry in entries
|
|
196
|
+
if _is_dir_no_follow(entry)
|
|
197
|
+
]
|
|
198
|
+
except OSError:
|
|
199
|
+
return []
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _is_dir_no_follow(entry: os.DirEntry) -> bool:
|
|
203
|
+
try:
|
|
204
|
+
return entry.is_dir(follow_symlinks=False)
|
|
205
|
+
except OSError:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def discover_examples(
|
|
210
|
+
examples_root: Path,
|
|
211
|
+
facets_by_source_dir: dict[str, ExampleFacets] | None = None,
|
|
212
|
+
) -> list[Example]:
|
|
213
|
+
"""Scan `<sdk>/examples` exactly two levels deep for `<category>/<name>`
|
|
214
|
+
directories carrying a `board.yaml`, sorted by source dir.
|
|
215
|
+
|
|
216
|
+
A missing or unreadable root is an EMPTY list, never an error: "no SDK" and
|
|
217
|
+
"no examples" are the same renderable answer, and `tan examples` returning
|
|
218
|
+
exit 0 with `examples: []` is what lets the New Project flow say "no
|
|
219
|
+
examples available" instead of showing a failure.
|
|
220
|
+
|
|
221
|
+
`facets_by_source_dir` (tan-cli#484, `tan.core.example_facets`) is looked
|
|
222
|
+
up by the SAME `source_dir` this function derives -- a directory this walk
|
|
223
|
+
finds but the catalogue does not know about (a newer example than the
|
|
224
|
+
checkout's committed `metadata/catalog.json`) simply gets no facets, same
|
|
225
|
+
as an omitted or unreadable catalogue; discovery is never gated on it.
|
|
226
|
+
"""
|
|
227
|
+
facets_by_source_dir = facets_by_source_dir or {}
|
|
228
|
+
found: list[Example] = []
|
|
229
|
+
for category in _subdirectories(examples_root):
|
|
230
|
+
for project in _subdirectories(category):
|
|
231
|
+
try:
|
|
232
|
+
has_board = (project / "board.yaml").is_file()
|
|
233
|
+
except OSError:
|
|
234
|
+
has_board = False
|
|
235
|
+
if not has_board:
|
|
236
|
+
continue
|
|
237
|
+
# Always `/`-joined: `sourceDir` is handed straight back to
|
|
238
|
+
# `tan init --from-example`, and a Windows `\` there is not a path
|
|
239
|
+
# the SDK examples tree can be indexed by.
|
|
240
|
+
source_dir = f"{category.name}/{project.name}"
|
|
241
|
+
readme = _read_readme(project)
|
|
242
|
+
found.append(
|
|
243
|
+
Example(
|
|
244
|
+
id=example_id_from_source_dir(source_dir),
|
|
245
|
+
source_dir=source_dir,
|
|
246
|
+
title=example_title_from_readme(readme, source_dir),
|
|
247
|
+
description=example_description_from_readme(readme),
|
|
248
|
+
facets=facets_by_source_dir.get(source_dir),
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
found.sort(key=lambda e: e.source_dir)
|
|
252
|
+
return found
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
# ---------------------------------------------------------------------------
|
|
256
|
+
# README-derived metadata (pure)
|
|
257
|
+
# ---------------------------------------------------------------------------
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def example_id_from_source_dir(source_dir: str) -> str:
|
|
261
|
+
"""The source dir is already unique across the catalogue, so it doubles as
|
|
262
|
+
the id. Kept as a named function because `id == sourceDir` is a contract the
|
|
263
|
+
extension may not assume forever, not an accident to inline away."""
|
|
264
|
+
return source_dir
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def example_title_from_readme(readme: str | None, source_dir: str) -> str:
|
|
268
|
+
"""The README's first markdown heading, else a title-cased leaf name."""
|
|
269
|
+
for line in (readme or "").splitlines():
|
|
270
|
+
trimmed = line.strip()
|
|
271
|
+
if trimmed.startswith("#"):
|
|
272
|
+
title = trimmed.lstrip("#").strip()
|
|
273
|
+
if title:
|
|
274
|
+
return title
|
|
275
|
+
return _title_case_leaf(source_dir)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def example_description_from_readme(readme: str | None) -> str:
|
|
279
|
+
"""The README's first prose line -- skipping blanks, headings, blockquotes,
|
|
280
|
+
badges/links, HTML, tables, list markers and rules. `""` when there is
|
|
281
|
+
none."""
|
|
282
|
+
for line in (readme or "").splitlines():
|
|
283
|
+
trimmed = line.strip()
|
|
284
|
+
if trimmed and trimmed[0] not in _NON_PROSE_PREFIXES:
|
|
285
|
+
return trimmed
|
|
286
|
+
return ""
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _title_case_leaf(source_dir: str) -> str:
|
|
290
|
+
"""`audio/i2s-tone` -> `I2s Tone`: split the leaf on `-`/`_`, upper-case each
|
|
291
|
+
word's first character only, and leave the rest of the word as found (so it
|
|
292
|
+
really is `I2s`, not `I2S` -- the fallback is deliberately dumb, a README
|
|
293
|
+
heading is how an example spells its own name)."""
|
|
294
|
+
leaf = source_dir.rsplit("/", 1)[-1]
|
|
295
|
+
words = [w for w in leaf.replace("_", "-").split("-") if w]
|
|
296
|
+
return " ".join(w[:1].upper() + w[1:] for w in words)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def strip_markdown_noise(text: str) -> str:
|
|
300
|
+
"""Drop `` image/badge spans and `**` emphasis markers.
|
|
301
|
+
|
|
302
|
+
TEXT MODE ONLY -- `data.examples[]` keeps the raw README-derived value, so
|
|
303
|
+
the JSON a consumer parses is unaffected. A `![` with no closing `)` is not
|
|
304
|
+
markdown: keep it verbatim rather than eating the rest of the line.
|
|
305
|
+
"""
|
|
306
|
+
out: list[str] = []
|
|
307
|
+
rest = text
|
|
308
|
+
while True:
|
|
309
|
+
start = rest.find("![")
|
|
310
|
+
if start == -1:
|
|
311
|
+
break
|
|
312
|
+
out.append(rest[:start])
|
|
313
|
+
end = rest.find(")", start)
|
|
314
|
+
if end == -1:
|
|
315
|
+
out.append(rest[start:])
|
|
316
|
+
rest = ""
|
|
317
|
+
break
|
|
318
|
+
rest = rest[end + 1 :]
|
|
319
|
+
out.append(rest)
|
|
320
|
+
return "".join(out).replace("**", "").strip()
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
# Filtering and text rendering
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def example_matches_filter(entry: Example, needle: str) -> bool:
|
|
329
|
+
"""Case-insensitive substring of `id` or `title`. Deliberately NOT
|
|
330
|
+
`sourceDir`/`description`: the former is the same string as `id`, the latter
|
|
331
|
+
is too free-form for a quick catalogue scan to key on."""
|
|
332
|
+
lowered = needle.lower()
|
|
333
|
+
return lowered in entry.id.lower() or lowered in entry.title.lower()
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def example_category(entry: Example) -> str:
|
|
337
|
+
"""The example's top-level directory -- `ai` for `ai/cold-chain-monitor`.
|
|
338
|
+
Prefers the catalogue's `category` facet (tan-cli#484) when the bound
|
|
339
|
+
checkout's `metadata/catalog.json` carries one for this example -- the
|
|
340
|
+
same producer-wins fallback order `alp-sdk-vscode`'s own
|
|
341
|
+
`exampleCategory()` uses ("the day tan forwards a category that
|
|
342
|
+
disagrees with the directory, the producer is right") -- and falls back
|
|
343
|
+
to the id prefix only when no facet is present (an older SDK, or an
|
|
344
|
+
example the catalogue does not know about yet)."""
|
|
345
|
+
facets = entry.facets
|
|
346
|
+
if facets is not None:
|
|
347
|
+
return facets.category
|
|
348
|
+
return entry.id.split("/", 1)[0]
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def example_matches_category(entry: Example, category: str) -> bool:
|
|
352
|
+
return example_category(entry).lower() == category.lower()
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def render_examples_text(
|
|
356
|
+
examples: list[Example],
|
|
357
|
+
filter_: str | None,
|
|
358
|
+
category: str | None,
|
|
359
|
+
verbose: bool,
|
|
360
|
+
sdk_resolved: bool,
|
|
361
|
+
) -> list[str]:
|
|
362
|
+
"""One `id title` line per example, `id` column aligned to the longest id in
|
|
363
|
+
THIS (possibly filtered) result set; `--verbose` appends the description.
|
|
364
|
+
|
|
365
|
+
Regression (tan-cli#164): this used to print only the count line, never the
|
|
366
|
+
entries `--from-example` needs a source dir from.
|
|
367
|
+
|
|
368
|
+
`sdk_resolved` splits the three ways an EMPTY result happens, which used to
|
|
369
|
+
collapse into one line that was wrong for two of them (tan-cli#400):
|
|
370
|
+
|
|
371
|
+
* **No checkout at all.** A headline only -- the diagnosis is the
|
|
372
|
+
`examples.sdk-root-unresolved` warning `examples()` prints straight after,
|
|
373
|
+
so the reason lives in exactly one place and reads the same in both output
|
|
374
|
+
modes. The filter branch is skipped HERE and not merely reordered: with no
|
|
375
|
+
catalogue to filter, `--filter blinky` used to answer `no example projects
|
|
376
|
+
match --filter "blinky".` and send the reader after a filter that had
|
|
377
|
+
nothing to do with it.
|
|
378
|
+
* **A checkout that ships no `examples/` tree.** Named as the checkout's own
|
|
379
|
+
answer. Byte-identical to the case above before this change, and the old
|
|
380
|
+
"is the alp-sdk checkout resolvable?" wording actively misdirected: it is,
|
|
381
|
+
and it has nothing.
|
|
382
|
+
* **A filter that matched nothing** -- unchanged, and now only reachable
|
|
383
|
+
when there really was a catalogue to filter.
|
|
384
|
+
"""
|
|
385
|
+
if not examples:
|
|
386
|
+
if not sdk_resolved:
|
|
387
|
+
return ["examples: no example projects found."]
|
|
388
|
+
if filter_ is not None:
|
|
389
|
+
# `ensure_ascii=False` so a non-ASCII filter is echoed as typed,
|
|
390
|
+
# which is the closest one-liner to Rust's `{:?}` quoting.
|
|
391
|
+
return [
|
|
392
|
+
f"examples: no example projects match --filter "
|
|
393
|
+
f"{json.dumps(filter_, ensure_ascii=False)}."
|
|
394
|
+
]
|
|
395
|
+
if category is not None:
|
|
396
|
+
return [
|
|
397
|
+
f"examples: no example projects in category "
|
|
398
|
+
f"{json.dumps(category, ensure_ascii=False)}."
|
|
399
|
+
]
|
|
400
|
+
return ["examples: the resolved alp-sdk checkout ships no example projects."]
|
|
401
|
+
id_width = max(len(e.id) for e in examples)
|
|
402
|
+
lines = [f"examples: {len(examples)} example project(s) available"]
|
|
403
|
+
for e in examples:
|
|
404
|
+
line = f" {strip_markdown_noise(e.id):<{id_width}} {strip_markdown_noise(e.title)}"
|
|
405
|
+
if verbose:
|
|
406
|
+
description = strip_markdown_noise(e.description)
|
|
407
|
+
if description:
|
|
408
|
+
line += f" -- {description}"
|
|
409
|
+
lines.append(line)
|
|
410
|
+
if filter_ is None and category is None:
|
|
411
|
+
categories = sorted({example_category(e) for e in examples})
|
|
412
|
+
lines.append("")
|
|
413
|
+
lines.append(f"categories: {' '.join(categories)}")
|
|
414
|
+
lines.append("narrow with --category <NAME>, or --filter <TEXT> to search.")
|
|
415
|
+
return lines
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
# ---------------------------------------------------------------------------
|
|
419
|
+
# Entry point
|
|
420
|
+
# ---------------------------------------------------------------------------
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
@dataclass(frozen=True)
|
|
424
|
+
class _ResolvedSdk:
|
|
425
|
+
"""`_resolve_sdk`'s own return shape -- a named result, not a growing
|
|
426
|
+
tuple (the same reasoning `tan.core.sdk_discovery.SdkRootResolution` documents): a fact
|
|
427
|
+
one caller needs (`foreign_global_default_for`, tan-cli#464) must not
|
|
428
|
+
force every field before it to be re-counted by position.
|
|
429
|
+
|
|
430
|
+
`.path`/`.display` are `None` together, on the SAME two conditions
|
|
431
|
+
`SdkRootResolution.path` itself is `None` for: no `--sdk-root` and
|
|
432
|
+
nothing else resolved, or a rejected `--sdk-root` (tan-cli#900). Never a
|
|
433
|
+
bare `_resolve_sdk(...) is None` any more -- see that function's own
|
|
434
|
+
docstring for why."""
|
|
435
|
+
|
|
436
|
+
path: Path | None
|
|
437
|
+
display: str | None
|
|
438
|
+
tier: str
|
|
439
|
+
broken_project_pin: str | None = None
|
|
440
|
+
foreign_global_default_for: str | None = None
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _resolve_sdk(sdk_root: str | None, workspace_root: Path) -> _ResolvedSdk:
|
|
444
|
+
"""The resolved checkout as a [`_ResolvedSdk`] -- ALWAYS one, never a bare
|
|
445
|
+
`None` (tan-cli#900; the same fix tan-cli#468 made to
|
|
446
|
+
`presets_cmd.resolve_sdk`, applied here to this command's own WIDE ladder
|
|
447
|
+
instead of copying its narrow one). Collapsing to `None` the moment
|
|
448
|
+
nothing resolved is exactly what discarded `.broken_project_pin`/
|
|
449
|
+
`.foreign_global_default_for` on that path: a workspace whose
|
|
450
|
+
`.alp/sdk-path` names a checkout that no longer exists, with nothing else
|
|
451
|
+
resolvable, reported `examples.sdk-root-unresolved` alone -- the customer
|
|
452
|
+
was told the SDK could not be resolved but not that their own project pin
|
|
453
|
+
was the broken thing. Callers now check `.path is None`, the same
|
|
454
|
+
"unresolved" signal `SdkRootResolution.path` itself already carries.
|
|
455
|
+
|
|
456
|
+
`--sdk-root` is TERMINAL (I-31): a value that is not a real checkout
|
|
457
|
+
resolves to `.path is None` rather than falling through to discovery, so a
|
|
458
|
+
typo'd flag yields an empty catalogue instead of silently listing some
|
|
459
|
+
other checkout's examples.
|
|
460
|
+
|
|
461
|
+
`.display` is separate from `.path` because they are not interchangeable:
|
|
462
|
+
`Path("./sdk")` stringifies to `sdk`, dropping the `./` the caller typed,
|
|
463
|
+
while Rust records `--sdk-root` as-is -- and `sdk.root` is on the wire.
|
|
464
|
+
|
|
465
|
+
`.broken_project_pin` (tan-cli#263 review) is `None` on the `--sdk-root`
|
|
466
|
+
branch (nothing to fall through from), else whatever
|
|
467
|
+
`resolve_sdk_root_wide` carried through -- `examples()` turns it into the
|
|
468
|
+
shared `sdk.project-pin-unresolved` warning, unconditionally, whether or
|
|
469
|
+
not `.path` resolved. `.foreign_global_default_for` (tan-cli#464) is the
|
|
470
|
+
same carry-through for the `globalDefault` tier's own silence;
|
|
471
|
+
`examples()` turns it into `sdk.global-default-foreign-project`.
|
|
472
|
+
"""
|
|
473
|
+
if sdk_root:
|
|
474
|
+
path = Path(sdk_root)
|
|
475
|
+
if (path / "scripts" / "alp_project.py").is_file():
|
|
476
|
+
return _ResolvedSdk(path, sdk_root, "sdkRootFlag")
|
|
477
|
+
return _ResolvedSdk(None, None, "sdkRootFlag")
|
|
478
|
+
# `.alp/sdk-path` project pin > the machine-global default
|
|
479
|
+
# (`~/.alp/sdk-default`) > the WIDE positional walk
|
|
480
|
+
# (`resolve_sdk_root_wide`) -- previously this went straight to the
|
|
481
|
+
# positional walk, silently ignoring `tan init`'s pin. Wide, not the narrow
|
|
482
|
+
# ladder thirteen other commands take: the oracle's `examples` lists the
|
|
483
|
+
# child `<ws>/alp-sdk`'s catalogue over a competing `../alp-sdk`'s
|
|
484
|
+
# (tan-cli#263). No `ALP_SDK_ROOT` tier (tried and reverted -- see
|
|
485
|
+
# `resolve_sdk_root_ladder`'s own docstring).
|
|
486
|
+
resolution = resolve_sdk_root_wide(None, workspace_root)
|
|
487
|
+
found = resolution.path
|
|
488
|
+
return _ResolvedSdk(
|
|
489
|
+
found,
|
|
490
|
+
str(found).replace("\\", "/") if found is not None else None,
|
|
491
|
+
resolution.tier,
|
|
492
|
+
resolution.broken_project_pin,
|
|
493
|
+
resolution.foreign_global_default_for,
|
|
494
|
+
)
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def examples(
|
|
498
|
+
filter_: str = typer.Option(
|
|
499
|
+
None, "--filter", metavar="TEXT", help="Only examples whose id or title contains TEXT."
|
|
500
|
+
),
|
|
501
|
+
category: str = typer.Option(
|
|
502
|
+
None,
|
|
503
|
+
"--category",
|
|
504
|
+
metavar="NAME",
|
|
505
|
+
help="Only examples in this catalogue category (a bare `tan examples` "
|
|
506
|
+
"prints the list).",
|
|
507
|
+
),
|
|
508
|
+
verbose: bool = typer.Option(False, "--verbose", help="Append each example's description."),
|
|
509
|
+
project: str = typer.Option(
|
|
510
|
+
None, "--project", metavar="PATH", help="Project root (defaults to '.')."
|
|
511
|
+
),
|
|
512
|
+
sdk_root: str = typer.Option(None, "--sdk-root", metavar="PATH", help="alp-sdk checkout root."),
|
|
513
|
+
output_format: OutputFormat = typer.Option(OutputFormat.TEXT, "--format", help=FORMAT_HELP),
|
|
514
|
+
) -> None:
|
|
515
|
+
"""List the SDK's ready-made example projects."""
|
|
516
|
+
json_mode = output_format == "json"
|
|
517
|
+
|
|
518
|
+
sdk: _ResolvedSdk | None = None
|
|
519
|
+
found: list[Example] = []
|
|
520
|
+
issues: list[Issue] = []
|
|
521
|
+
exit_code = ExitCode.SUCCESS
|
|
522
|
+
try:
|
|
523
|
+
# Unnormalised join, matching `cli_workspace_root` -- discovery probes
|
|
524
|
+
# this path's own parent, so collapsing a trailing `..` here would move
|
|
525
|
+
# which directory the sibling `alp-sdk` is looked for beside.
|
|
526
|
+
workspace_root = Path.cwd() / project if project else Path.cwd()
|
|
527
|
+
sdk = _resolve_sdk(sdk_root, workspace_root)
|
|
528
|
+
# tan-cli#900: computed unconditionally, BEFORE the `sdk.path is None`
|
|
529
|
+
# branch below -- `_resolve_sdk` always returns a `.broken_project_pin`/
|
|
530
|
+
# `.foreign_global_default_for` pair now, whether or not `.path`
|
|
531
|
+
# resolved, and a workspace pinned to a gone checkout with nothing
|
|
532
|
+
# else resolvable deserves the same disclosure a resolved run gets,
|
|
533
|
+
# not silence just because nothing happened to fall back to.
|
|
534
|
+
pin_issue = project_pin_issue(sdk.broken_project_pin, sdk.tier)
|
|
535
|
+
if pin_issue is not None:
|
|
536
|
+
issues.append(pin_issue)
|
|
537
|
+
# tan-cli#464: the catalogue this command just listed can come from
|
|
538
|
+
# a `globalDefault` pointer a DIFFERENT project's bootstrap
|
|
539
|
+
# relocation actually wrote -- the same silence `sdk.project-
|
|
540
|
+
# pin-unresolved` above closes one tier up.
|
|
541
|
+
foreign_issue = global_default_foreign_project_issue(sdk.foreign_global_default_for)
|
|
542
|
+
if foreign_issue is not None:
|
|
543
|
+
issues.append(foreign_issue)
|
|
544
|
+
if sdk.path is not None:
|
|
545
|
+
# tan-cli#484: an OPTIONAL enrichment pass, looked up by
|
|
546
|
+
# `sourceDir` -- `load_example_facets` never raises, so a
|
|
547
|
+
# checkout predating `metadata/catalog.json` (or one where it
|
|
548
|
+
# fails to parse) still lists every example exactly as before,
|
|
549
|
+
# just without the extra keys.
|
|
550
|
+
found = discover_examples(
|
|
551
|
+
sdk.path / "examples", load_example_facets(sdk.path)
|
|
552
|
+
)
|
|
553
|
+
# tan-cli#407: the catalogue this command just listed can come from
|
|
554
|
+
# a different checkout than `tan build` plans against, with both
|
|
555
|
+
# envelopes reporting `sourceTier: "discovery"`. `sdk_root` is the
|
|
556
|
+
# RAW flag, not `sdk.display`'s resolved text -- naming a root
|
|
557
|
+
# explicitly puts both ladders on the same terminal tier, and the
|
|
558
|
+
# helper returns `None` there rather than warning about a collision
|
|
559
|
+
# the user never reached.
|
|
560
|
+
divergence = sdk_ladder_divergence_issue(sdk_root, workspace_root, wide=True)
|
|
561
|
+
if divergence is not None:
|
|
562
|
+
issues.append(divergence)
|
|
563
|
+
else:
|
|
564
|
+
# tan-cli#400. NOT an error: `examples: []` from no checkout is a
|
|
565
|
+
# renderable answer the New Project flow is built on, so this stays
|
|
566
|
+
# `warning`/`ExitCode.SUCCESS` and the `examples-catalog` golden's
|
|
567
|
+
# `ok:true` at rc 0 is untouched. What changes is that the empty
|
|
568
|
+
# catalogue now carries a REASON. Its presence is also the JSON-side
|
|
569
|
+
# discriminator between the two empty answers -- a checkout that
|
|
570
|
+
# resolves and ships no `examples/` tree emits no issue at all,
|
|
571
|
+
# because that is not a misconfiguration to warn anybody about.
|
|
572
|
+
# tan-cli#497 defect 7: when `--sdk-root` WAS given and rejected,
|
|
573
|
+
# name the value. `SDK_UNRESOLVED_MESSAGE`'s remediation is "pass
|
|
574
|
+
# --sdk-root <path>" -- the flag they just passed -- and the failing
|
|
575
|
+
# path appeared nowhere in the envelope or the stderr text.
|
|
576
|
+
issues.append(
|
|
577
|
+
Issue(
|
|
578
|
+
SDK_UNRESOLVED_CODE,
|
|
579
|
+
"warning",
|
|
580
|
+
rejected_sdk_root_message(sdk_root, SDK_UNRESOLVED_CONSEQUENCE)
|
|
581
|
+
if sdk_root
|
|
582
|
+
else with_sdk_search(SDK_UNRESOLVED_MESSAGE, workspace_root),
|
|
583
|
+
)
|
|
584
|
+
)
|
|
585
|
+
if filter_ is not None:
|
|
586
|
+
found = [e for e in found if example_matches_filter(e, filter_)]
|
|
587
|
+
if category is not None:
|
|
588
|
+
found = [e for e in found if example_matches_category(e, category)]
|
|
589
|
+
except Exception as err: # noqa: BLE001 -- the backstop; see the module docstring
|
|
590
|
+
# No registry entry applies (`contract/issue-codes.json` covers
|
|
591
|
+
# `bootstrap.*`/`debug-config.*` only), so this follows the port's
|
|
592
|
+
# existing `<command>.internal-failure` spelling. Unreachable through
|
|
593
|
+
# any enumerated path -- every IO error above is already swallowed the
|
|
594
|
+
# way the oracle swallows it -- and here so that the one nobody thought
|
|
595
|
+
# of still arrives as an envelope instead of a traceback on stderr with
|
|
596
|
+
# an empty stdout the extension renders as nothing.
|
|
597
|
+
found = []
|
|
598
|
+
issues = [
|
|
599
|
+
Issue(
|
|
600
|
+
"examples.internal-failure",
|
|
601
|
+
"error",
|
|
602
|
+
f"examples failed unexpectedly: {err.__class__.__name__}: {err}",
|
|
603
|
+
)
|
|
604
|
+
]
|
|
605
|
+
exit_code = ExitCode.INTERNAL_FAILURE
|
|
606
|
+
|
|
607
|
+
if json_mode:
|
|
608
|
+
emit(
|
|
609
|
+
Envelope(
|
|
610
|
+
"examples",
|
|
611
|
+
# Both null, always: the catalogue is an SDK-wide fact, so there
|
|
612
|
+
# is no project this answer belongs to. Pinned by
|
|
613
|
+
# `contract/envelopes/examples-catalog`.
|
|
614
|
+
Project(root=None, board_yaml=None),
|
|
615
|
+
{
|
|
616
|
+
"schemaVersion": DATA_SCHEMA_VERSION,
|
|
617
|
+
"examples": [e.as_dict() for e in found],
|
|
618
|
+
},
|
|
619
|
+
issues,
|
|
620
|
+
exit_code,
|
|
621
|
+
sdk=(
|
|
622
|
+
SdkInfo.from_resolution(sdk.display, sdk)
|
|
623
|
+
if sdk is not None and sdk.path is not None
|
|
624
|
+
else None
|
|
625
|
+
),
|
|
626
|
+
)
|
|
627
|
+
)
|
|
628
|
+
else:
|
|
629
|
+
resolved = sdk is not None and sdk.path is not None
|
|
630
|
+
for line in render_examples_text(found, filter_, category, verbose, resolved):
|
|
631
|
+
print(line, file=sys.stderr)
|
|
632
|
+
for issue in issues:
|
|
633
|
+
print(f"examples: {issue.message}", file=sys.stderr)
|
|
634
|
+
raise typer.Exit(int(exit_code))
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
# tan-cli#261: adds the seven oracle `GlobalArgs` flags this command was
|
|
638
|
+
# still missing (`--all`/`--board-yaml`/`--ci`/`--no-color`/
|
|
639
|
+
# `--non-interactive`/`--quiet`/`--target`) on top of `--verbose`, already
|
|
640
|
+
# declared and read above; see `tan.core.global_flags`. Every one of them is
|
|
641
|
+
# genuinely inert for `examples` -- its envelope's `project` is always
|
|
642
|
+
# `Project(root=None, board_yaml=None)`, an SDK-wide catalogue with no
|
|
643
|
+
# project of its own to anchor a `--board-yaml`/`--target` on.
|
|
644
|
+
examples = accept_global_flags(examples)
|