netbox-super-cli 1.0.2__tar.gz → 1.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/AGENTS.md +13 -0
  2. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/CHANGELOG.md +18 -0
  3. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/CLAUDE.md +13 -0
  4. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/PKG-INFO +2 -2
  5. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/README.md +1 -1
  6. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/adding-bundled-schemas.md +3 -3
  7. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/getting-started/install.md +1 -1
  8. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/index.md +1 -1
  9. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/cli.md +1 -1
  10. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/schemas.md +2 -1
  11. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/_version.py +1 -1
  12. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cache/store.py +22 -0
  13. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/login_commands.py +49 -4
  14. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/skill_commands.py +59 -0
  15. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/source.py +18 -3
  16. netbox_super_cli-1.0.3/nsc/schemas/bundled/manifest.yaml +9 -0
  17. netbox_super_cli-1.0.3/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
  18. netbox_super_cli-1.0.3/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
  19. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/pyproject.toml +1 -1
  20. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/skills/netbox-super-cli/SKILL.md +23 -0
  21. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cache/test_store.py +60 -0
  22. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_login_commands.py +125 -2
  23. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_registration.py +2 -2
  24. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_skill_commands.py +83 -0
  25. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_handlers_audit.py +1 -1
  26. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_source_ttl.py +89 -0
  27. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/scripts/test_gen_docs.py +1 -1
  28. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/uv.lock +1 -1
  29. netbox_super_cli-1.0.2/nsc/schemas/bundled/manifest.yaml +0 -5
  30. netbox_super_cli-1.0.2/nsc/schemas/bundled/netbox-4.6.0-beta2.json.gz +0 -0
  31. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/agents-md-sync.yml +0 -0
  32. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/bench.yml +0 -0
  33. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/claude.yml +0 -0
  34. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/docs.yml +0 -0
  35. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/e2e.yml +0 -0
  36. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/lint.yml +0 -0
  37. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/release.yml +0 -0
  38. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/test.yml +0 -0
  39. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.gitignore +0 -0
  40. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.pre-commit-config.yaml +0 -0
  41. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.python-version +0 -0
  42. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/LICENSE +0 -0
  43. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/caching.md +0 -0
  44. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/command-generation.md +0 -0
  45. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/http-client.md +0 -0
  46. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/overview.md +0 -0
  47. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/schema-loading.md +0 -0
  48. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/branching.md +0 -0
  49. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/development.md +0 -0
  50. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/release-process.md +0 -0
  51. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/getting-started/concepts.md +0 -0
  52. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/getting-started/first-run.md +0 -0
  53. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/ci-and-automation.md +0 -0
  54. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/managing-profiles.md +0 -0
  55. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/output-formats.md +0 -0
  56. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/using-with-ai-agents.md +0 -0
  57. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/working-with-plugins.md +0 -0
  58. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/writes-and-safety.md +0 -0
  59. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/config.md +0 -0
  60. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/exit-codes.md +0 -0
  61. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/justfile +0 -0
  62. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/mkdocs.yml +0 -0
  63. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/__init__.py +0 -0
  64. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/__main__.py +0 -0
  65. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/aliases/__init__.py +0 -0
  66. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/aliases/resolver.py +0 -0
  67. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/auth/__init__.py +0 -0
  68. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/auth/verify.py +0 -0
  69. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/builder/__init__.py +0 -0
  70. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/builder/build.py +0 -0
  71. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cache/__init__.py +0 -0
  72. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/__init__.py +0 -0
  73. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/aliases_commands.py +0 -0
  74. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/app.py +0 -0
  75. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/cache_commands.py +0 -0
  76. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/commands_dump.py +0 -0
  77. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/config_commands.py +0 -0
  78. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/globals.py +0 -0
  79. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/handlers.py +0 -0
  80. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/init_commands.py +0 -0
  81. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/profiles_commands.py +0 -0
  82. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/registration.py +0 -0
  83. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/runtime.py +0 -0
  84. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/__init__.py +0 -0
  85. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/apply.py +0 -0
  86. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/bulk.py +0 -0
  87. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/coercion.py +0 -0
  88. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/confirmation.py +0 -0
  89. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/input.py +0 -0
  90. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/preflight.py +0 -0
  91. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/__init__.py +0 -0
  92. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/loader.py +0 -0
  93. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/models.py +0 -0
  94. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/settings.py +0 -0
  95. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/writer.py +0 -0
  96. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/__init__.py +0 -0
  97. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/audit.py +0 -0
  98. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/client.py +0 -0
  99. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/errors.py +0 -0
  100. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/retry.py +0 -0
  101. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/model/__init__.py +0 -0
  102. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/model/command_model.py +0 -0
  103. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/__init__.py +0 -0
  104. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/csv_.py +0 -0
  105. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/errors.py +0 -0
  106. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/explain.py +0 -0
  107. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/flatten.py +0 -0
  108. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/headers.py +0 -0
  109. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/json_.py +0 -0
  110. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/jsonl.py +0 -0
  111. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/render.py +0 -0
  112. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/table.py +0 -0
  113. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/yaml_.py +0 -0
  114. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/__init__.py +0 -0
  115. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/hashing.py +0 -0
  116. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/loader.py +0 -0
  117. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/models.py +0 -0
  118. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schemas/__init__.py +0 -0
  119. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schemas/bundled/__init__.py +0 -0
  120. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/skill/__init__.py +0 -0
  121. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/scripts/gen_docs.py +0 -0
  122. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/scripts/sync_agents_md.py +0 -0
  123. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/__init__.py +0 -0
  124. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/aliases/__init__.py +0 -0
  125. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/aliases/test_resolver.py +0 -0
  126. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/auth/__init__.py +0 -0
  127. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/auth/test_verify.py +0 -0
  128. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/benchmarks/__init__.py +0 -0
  129. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/benchmarks/test_startup.py +0 -0
  130. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/__init__.py +0 -0
  131. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_build.py +0 -0
  132. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_default_columns.py +0 -0
  133. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_redaction_marking.py +0 -0
  134. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_request_body_shape.py +0 -0
  135. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cache/__init__.py +0 -0
  136. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cache/test_prune.py +0 -0
  137. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/__init__.py +0 -0
  138. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_aliases_commands.py +0 -0
  139. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_app_smoke.py +0 -0
  140. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_cache_commands.py +0 -0
  141. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_commands_dump.py +0 -0
  142. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_completion_smoke.py +0 -0
  143. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_config_commands.py +0 -0
  144. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_explain.py +0 -0
  145. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_handlers.py +0 -0
  146. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_init_commands.py +0 -0
  147. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
  148. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_ndjson_input.py +0 -0
  149. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_profiles_commands.py +0 -0
  150. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_respx_integration.py +0 -0
  151. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_runtime.py +0 -0
  152. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_stdin_sniffer.py +0 -0
  153. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_writes_respx.py +0 -0
  154. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/__init__.py +0 -0
  155. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_apply.py +0 -0
  156. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_bulk.py +0 -0
  157. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_confirmation.py +0 -0
  158. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_handler_helpers.py +0 -0
  159. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_input.py +0 -0
  160. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_preflight.py +0 -0
  161. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/__init__.py +0 -0
  162. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_loader.py +0 -0
  163. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_models.py +0 -0
  164. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_writer_atomicity.py +0 -0
  165. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_writer_dotted_paths.py +0 -0
  166. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_writer_roundtrip.py +0 -0
  167. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/conftest.py +0 -0
  168. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/README.md +0 -0
  169. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/__init__.py +0 -0
  170. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/conftest.py +0 -0
  171. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/docker-compose.yml +0 -0
  172. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_audit_redaction.py +0 -0
  173. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_auth_error_envelope.py +0 -0
  174. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
  175. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_full_cycle.py +0 -0
  176. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_login.py +0 -0
  177. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_ndjson.py +0 -0
  178. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_preflight_blocks_apply.py +0 -0
  179. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
  180. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/wait_for_netbox.sh +0 -0
  181. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/profiles/single_profile.yaml +0 -0
  182. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/auth_401.json +0 -0
  183. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/circuits_providers_list.json +0 -0
  184. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/dcim_devices_get.json +0 -0
  185. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
  186. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
  187. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/__init__.py +0 -0
  188. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_audit.py +0 -0
  189. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_audit_redaction.py +0 -0
  190. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_client.py +0 -0
  191. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_client_redaction_threading.py +0 -0
  192. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_errors.py +0 -0
  193. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_retry.py +0 -0
  194. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/model/__init__.py +0 -0
  195. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/model/test_command_model.py +0 -0
  196. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/model/test_request_body_shape.py +0 -0
  197. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/__init__.py +0 -0
  198. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_csv.py +0 -0
  199. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_errors.py +0 -0
  200. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_errors_aliases.py +0 -0
  201. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_explain.py +0 -0
  202. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_flatten.py +0 -0
  203. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_input_error_envelope.py +0 -0
  204. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_json.py +0 -0
  205. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_jsonl.py +0 -0
  206. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_render_dispatch.py +0 -0
  207. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_table.py +0 -0
  208. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_yaml.py +0 -0
  209. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/__init__.py +0 -0
  210. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_hashing.py +0 -0
  211. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_loader.py +0 -0
  212. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_models.py +0 -0
  213. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_source.py +0 -0
  214. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/scripts/__init__.py +0 -0
  215. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/skill/__init__.py +0 -0
  216. {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/skill/test_bundle.py +0 -0
@@ -50,6 +50,19 @@ short-lived feature branches (`fix/<slug>`, `feat/<slug>`, `docs/<slug>`,
50
50
  Releases are *tags* on `main`, not branches. Full convention:
51
51
  [`docs/contributing/branching.md`](docs/contributing/branching.md).
52
52
 
53
+ ## Coordinating work on issues and PRs
54
+
55
+ When you start working on a GitHub issue or PR, post a comment so other agents (and humans) can see it's actively being worked. Use `gh issue comment <n> -b "..."` or `gh pr comment <n> -b "..."`.
56
+
57
+ Required updates:
58
+
59
+ - **On claim (start)** — before you begin, check existing comments. If someone else has claimed within the last 24h and has not posted a blocker/handoff, stop and surface the conflict to the user. Otherwise post a claim: what you're doing, branch name (if known), expected scope.
60
+ - **On milestones** — branch pushed, PR opened, CI green, review requested. One short line each.
61
+ - **On blockers** — when paused, blocked on input, or handing off. State *why* and what's needed to resume.
62
+ - **On completion** — when merged, closed, or abandoning. Final status so the next agent doesn't re-investigate.
63
+
64
+ Keep comments terse (1–3 lines). Sign with the agent identity already used in commits (e.g. `Co-Authored-By` line not needed in comments). Don't spam — skip a milestone update if the previous one is under ~5 minutes old and nothing materially changed.
65
+
53
66
  ## Where the design lives
54
67
 
55
68
  - `docs/superpowers/specs/2026-04-30-netbox-super-cli-design.md` — the full design.
@@ -2,6 +2,24 @@
2
2
 
3
3
  All notable changes to netbox-super-cli are tracked here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) loosely. From v1.0.0 onward, releases follow [Semantic Versioning](https://semver.org/) and the version in `pyproject.toml` matches the git tag. Pre-1.0 milestones (Phase 1-5) were pinned by tag while `pyproject.toml` stayed at `0.0.1`.
4
4
 
5
+ ## v1.0.3 — 2026-05-07
6
+
7
+ Third patch release. Headline changes: `nsc login --new` now prompts to fetch the live schema (issue #32), the schema TTL fast-path self-heals on hash-confirmed fetches (issue #39), and bundled schemas are updated to 4.5.10 and 4.6.0.
8
+
9
+ ### Added
10
+
11
+ - **`nsc login --new` prompts to fetch the live schema** (issue #32, PR #44). After a new profile is created, `nsc login` asks whether to fetch the live OpenAPI spec immediately. Answering yes primes the schema cache so the next command skips the bootstrap fetch entirely.
12
+ - **`nsc skill export <dir>`** (PR #37). Copies the bundled `SKILL.md` to an arbitrary directory so it can be dropped into any agent harness without `nsc` on the `PATH`. Useful in CI or shared-skill repos where the package is not installed globally.
13
+ - **Bundled schemas updated** (PR #36). Ships 4.5.10 and 4.6.0; drops the 4.6.0-beta2 snapshot. Offline fallback is now available for both stable release lines.
14
+
15
+ ### Fixed
16
+
17
+ - **Schema TTL fast-path now self-heals after a hash-confirmed fetch** (issue #39). When `nsc` fetched `/api/schema/` and the live hash matched an existing cache file, it returned the cached `CommandModel` without bumping the sidecar's `fetched_at`. So an aged-out sidecar — or a legacy cache from before the sidecar feature — never gained proof of freshness and every subsequent invocation refetched the schema, defeating the v1.0.2 fast-path. `_build_and_cache` now calls a new `CacheStore.touch_fetched_at` to refresh (or seed) the sidecar after any successful live fetch, so the next invocation hits the fast path.
18
+
19
+ ### Documentation
20
+
21
+ - Bundled Skill (`skills/netbox-super-cli/SKILL.md`) gained a scope-then-filter pattern for multi-device bulk reads (PR #41). The guidance shows how to narrow with `--site`, `--rack`, or `--role` before applying `--filter`, avoiding a full-table scan on large NetBox instances.
22
+
5
23
  ## v1.0.2 — 2026-05-07
6
24
 
7
25
  Second patch release. Headline change is the schema TTL fast-path (issue #34), which eliminates the per-invocation `GET /api/schema/` round-trip on warm caches. Also includes a small `nsc init` UX addition (verify_ssl prompt) and the documentation pass that landed since v1.0.1.
@@ -43,6 +43,19 @@ short-lived feature branches (`fix/<slug>`, `feat/<slug>`, `docs/<slug>`,
43
43
  Releases are *tags* on `main`, not branches. Full convention:
44
44
  [`docs/contributing/branching.md`](docs/contributing/branching.md).
45
45
 
46
+ ## Coordinating work on issues and PRs
47
+
48
+ When you start working on a GitHub issue or PR, post a comment so other agents (and humans) can see it's actively being worked. Use `gh issue comment <n> -b "..."` or `gh pr comment <n> -b "..."`.
49
+
50
+ Required updates:
51
+
52
+ - **On claim (start)** — before you begin, check existing comments. If someone else has claimed within the last 24h and has not posted a blocker/handoff, stop and surface the conflict to the user. Otherwise post a claim: what you're doing, branch name (if known), expected scope.
53
+ - **On milestones** — branch pushed, PR opened, CI green, review requested. One short line each.
54
+ - **On blockers** — when paused, blocked on input, or handing off. State *why* and what's needed to resume.
55
+ - **On completion** — when merged, closed, or abandoning. Final status so the next agent doesn't re-investigate.
56
+
57
+ Keep comments terse (1–3 lines). Sign with the agent identity already used in commits (e.g. `Co-Authored-By` line not needed in comments). Don't spam — skip a milestone update if the previous one is under ~5 minutes old and nothing materially changed.
58
+
46
59
  ## Where the design lives
47
60
 
48
61
  - `docs/superpowers/specs/2026-04-30-netbox-super-cli-design.md` — the full design.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: netbox-super-cli
3
- Version: 1.0.2
3
+ Version: 1.0.3
4
4
  Summary: Dynamic NetBox CLI generated from the live OpenAPI schema.
5
5
  Project-URL: Homepage, https://github.com/thomaschristory/netbox-super-cli
6
6
  Project-URL: Issues, https://github.com/thomaschristory/netbox-super-cli/issues
@@ -126,7 +126,7 @@ The wire body sent to NetBox is **not** redacted — only the audit log. A faile
126
126
 
127
127
  ```sh
128
128
  # Dump every endpoint in the bundled NetBox schema as JSON.
129
- uv run nsc commands --schema nsc/schemas/bundled/netbox-4.6.0-beta2.json.gz --output json | head
129
+ uv run nsc commands --schema nsc/schemas/bundled/netbox-4.6.0.json.gz --output json | head
130
130
 
131
131
  # Or against a live install.
132
132
  uv run nsc commands --schema https://netbox.example.com/api/schema/?format=json --output json
@@ -99,7 +99,7 @@ The wire body sent to NetBox is **not** redacted — only the audit log. A faile
99
99
 
100
100
  ```sh
101
101
  # Dump every endpoint in the bundled NetBox schema as JSON.
102
- uv run nsc commands --schema nsc/schemas/bundled/netbox-4.6.0-beta2.json.gz --output json | head
102
+ uv run nsc commands --schema nsc/schemas/bundled/netbox-4.6.0.json.gz --output json | head
103
103
 
104
104
  # Or against a live install.
105
105
  uv run nsc commands --schema https://netbox.example.com/api/schema/?format=json --output json
@@ -9,7 +9,7 @@ the repo and shipped inside the wheel.
9
9
  ```
10
10
  nsc/schemas/bundled/
11
11
  ├── manifest.yaml
12
- ├── netbox-4.6.0-beta2.json.gz
12
+ ├── netbox-4.6.0.json.gz
13
13
  └── netbox-<other-version>.json.gz
14
14
  ```
15
15
 
@@ -17,8 +17,8 @@ nsc/schemas/bundled/
17
17
 
18
18
  ```yaml
19
19
  schemas:
20
- - version: "4.6.0-beta2"
21
- file: "netbox-4.6.0-beta2.json.gz"
20
+ - version: "4.6.0"
21
+ file: "netbox-4.6.0.json.gz"
22
22
  ```
23
23
 
24
24
  ## Adding a new version
@@ -56,5 +56,5 @@ Restart your shell, then verify with `nsc <TAB><TAB>`.
56
56
  ## Requirements
57
57
 
58
58
  - Python ≥ 3.12.
59
- - A reachable NetBox install (any version with `/api/schema/` enabled — 4.4+).
59
+ - A reachable NetBox install (any version with `/api/schema/` enabled — 4.5+).
60
60
  - A NetBox API token (read-only is enough for read commands; writable for `--apply`).
@@ -1,7 +1,7 @@
1
1
  # netbox-super-cli
2
2
 
3
3
  Dynamic NetBox CLI driven by the live OpenAPI schema. The same `nsc` binary works
4
- against any NetBox version (4.4+) and exposes plugin-provided endpoints automatically
4
+ against any NetBox version (4.5+) and exposes plugin-provided endpoints automatically
5
5
  because the schema — not hand-written code — defines the surface.
6
6
 
7
7
  ## Why nsc
@@ -2,7 +2,7 @@
2
2
 
3
3
  <!-- This file is auto-generated by `scripts/gen_docs.py`. Do not edit by hand — re-run the generator instead. -->
4
4
 
5
- Generated from `netbox-4.6.0-beta2.json.gz` (NetBox `4.6.0-beta2 (4.6)`).
5
+ Generated from `netbox-4.6.0.json.gz` (NetBox `4.6.0-Docker-5.0.1 (4.6)`).
6
6
 
7
7
  Every endpoint exposed by NetBox surfaces as `nsc <tag> <resource> <verb>`.
8
8
  Verbs are derived from the `operationId`: `list`, `get`, `create`, `update`, `replace`, `delete`,
@@ -7,4 +7,5 @@ fallback when a live schema can't be fetched and no per-profile cache exists.
7
7
 
8
8
  | Version | File |
9
9
  |---|---|
10
- | `4.6.0-beta2` | `netbox-4.6.0-beta2.json.gz` |
10
+ | `4.5.10` | `netbox-4.5.10.json.gz` |
11
+ | `4.6.0` | `netbox-4.6.0.json.gz` |
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for the package version."""
2
2
 
3
- __version__ = "1.0.2"
3
+ __version__ = "1.0.3"
@@ -87,6 +87,28 @@ class CacheStore:
87
87
  _atomic_write(meta, json.dumps({"fetched_at": time.time()}))
88
88
  return target
89
89
 
90
+ def touch_fetched_at(self, profile: str, schema_hash: str) -> None:
91
+ """Bump only the sidecar's `fetched_at` to now, without rewriting
92
+ the cache file. Called after a live fetch confirms an existing
93
+ cache entry is still valid (its hash matches): we want the TTL
94
+ fast-path to trust it on the next invocation, but the JSON body
95
+ on disk is byte-identical so re-serializing it is wasteful.
96
+
97
+ Also seeds a sidecar for caches written before #35 (legacy
98
+ upgrade path) — without this, those entries never gain proof of
99
+ freshness and every invocation refetches `/api/schema/` even
100
+ though the schema is unchanged. No-op when the cache file is
101
+ missing (don't leave an orphaned sidecar)."""
102
+ self._validate_profile(profile)
103
+ if not _HASH_RE.match(schema_hash):
104
+ return
105
+ target = self._path_for(profile, schema_hash)
106
+ if not target.exists():
107
+ return
108
+ meta = self._meta_path_for(profile, schema_hash)
109
+ meta.parent.mkdir(parents=True, exist_ok=True)
110
+ _atomic_write(meta, json.dumps({"fetched_at": time.time()}))
111
+
90
112
  def load_fetched_at(self, profile: str, schema_hash: str) -> float | None:
91
113
  """Return epoch seconds when this cache entry was last fetched, or
92
114
  `None` if the sidecar is missing/unreadable. Used by the TTL
@@ -19,7 +19,7 @@ import typer
19
19
  from ruamel.yaml.comments import CommentedMap, TaggedScalar
20
20
 
21
21
  from nsc.auth.verify import VerifyError, verify
22
- from nsc.cli.runtime import emit_envelope
22
+ from nsc.cli.runtime import ResolvedProfile, emit_envelope
23
23
  from nsc.config.loader import ConfigParseError, load_config
24
24
  from nsc.config.models import OutputFormat, Profile
25
25
  from nsc.config.settings import default_paths
@@ -30,6 +30,7 @@ from nsc.config.writer import (
30
30
  load_round_trip,
31
31
  )
32
32
  from nsc.output.errors import ErrorEnvelope, ErrorType
33
+ from nsc.schema.source import resolve_command_model
33
34
 
34
35
 
35
36
  def _config_path() -> Path:
@@ -117,6 +118,33 @@ def _print_success(username: str, version: str) -> None:
117
118
  typer.echo(f"✓ authenticated as {username}, NetBox {version}")
118
119
 
119
120
 
121
+ def _fetch_schema_for_login(profile: Profile, token: str) -> None:
122
+ rp = ResolvedProfile(
123
+ name=profile.name,
124
+ url=profile.url,
125
+ token=token,
126
+ verify_ssl=profile.verify_ssl,
127
+ timeout=profile.timeout if profile.timeout is not None else 30.0,
128
+ schema_url=profile.schema_url,
129
+ )
130
+ schema_url = (
131
+ str(rp.schema_url)
132
+ if rp.schema_url is not None
133
+ else f"{str(rp.url).rstrip('/')}/api/schema/?format=json"
134
+ )
135
+ typer.echo(f"Fetching schema from {schema_url} ...")
136
+ try:
137
+ resolve_command_model(
138
+ paths=default_paths(),
139
+ profile=rp,
140
+ schema_override=None,
141
+ force_refresh=True,
142
+ )
143
+ typer.echo("Schema cached.")
144
+ except Exception as exc:
145
+ typer.echo(f"Warning: schema fetch failed ({exc}); skipping.", err=True)
146
+
147
+
120
148
  def register(app: typer.Typer) -> None:
121
149
  @app.command("login", help="Verify / create / rotate a profile's token.")
122
150
  def login_cmd(
@@ -136,20 +164,27 @@ def register(app: typer.Typer) -> None:
136
164
  help="Environment variable name (required when --store=env).",
137
165
  ),
138
166
  ] = None,
167
+ fetch_schema: Annotated[
168
+ bool,
169
+ typer.Option(
170
+ "--fetch-schema",
171
+ help="Fetch and cache the live OpenAPI schema (applies to verify and --new).",
172
+ ),
173
+ ] = False,
139
174
  ) -> None:
140
175
  if new and rotate:
141
176
  raise typer.BadParameter("--new and --rotate are mutually exclusive")
142
177
 
143
178
  if new:
144
- _do_login_new(profile, url, store, env_var)
179
+ _do_login_new(profile, url, store, env_var, fetch_schema=fetch_schema)
145
180
  return
146
181
  if rotate:
147
182
  _do_login_rotate(profile, store, env_var)
148
183
  return
149
- _do_login_verify(profile)
184
+ _do_login_verify(profile, fetch_schema=fetch_schema)
150
185
 
151
186
 
152
- def _do_login_verify(profile_name: str | None) -> None:
187
+ def _do_login_verify(profile_name: str | None, *, fetch_schema: bool = False) -> None:
153
188
  try:
154
189
  config = load_config(_config_path())
155
190
  except ConfigParseError as exc:
@@ -173,6 +208,12 @@ def _do_login_verify(profile_name: str | None) -> None:
173
208
  )
174
209
  raise typer.Exit(code=code) from exc
175
210
  _print_success(result.username, result.netbox_version)
211
+ if fetch_schema:
212
+ token = profile.token
213
+ if token is None:
214
+ typer.echo("Warning: token not available from config; skipping schema fetch.", err=True)
215
+ return
216
+ _fetch_schema_for_login(profile, token)
176
217
 
177
218
 
178
219
  def _do_login_new(
@@ -180,6 +221,8 @@ def _do_login_new(
180
221
  url: str | None,
181
222
  store: str,
182
223
  env_var: str | None,
224
+ *,
225
+ fetch_schema: bool = False,
183
226
  ) -> None:
184
227
  if profile_name is None:
185
228
  raise typer.BadParameter("--new requires --profile <name>")
@@ -224,6 +267,8 @@ def _do_login_new(
224
267
  set_default=set_default,
225
268
  )
226
269
  _print_success(result.username, result.netbox_version)
270
+ if fetch_schema or typer.confirm("Fetch and cache the live schema now?", default=True):
271
+ _fetch_schema_for_login(candidate, token_for_verify)
227
272
 
228
273
 
229
274
  def _do_login_rotate(
@@ -141,6 +141,30 @@ def _render_json(resolution: _Resolution, mode: str, written: bool, source: Path
141
141
  return json.dumps(payload)
142
142
 
143
143
 
144
+ def _render_export_table(dest_file: Path, source: Path, mode: str, written: bool) -> str:
145
+ lines: list[str] = []
146
+ if mode == "dry-run":
147
+ lines.append("nsc skill export (dry-run) — pass --apply to write")
148
+ lines.append(f" would write to {dest_file}")
149
+ lines.append(f" source: {source}")
150
+ elif written:
151
+ lines.append(f"✓ exported netbox-super-cli skill to {dest_file}")
152
+ else:
153
+ lines.append("nsc skill export (no-op)")
154
+ return "\n".join(lines)
155
+
156
+
157
+ def _render_export_json(dest_file: Path, source: Path, mode: str, written: bool) -> str:
158
+ payload: dict[str, object] = {
159
+ "mode": mode,
160
+ "destination": str(dest_file),
161
+ "source": str(source),
162
+ }
163
+ if mode == "apply":
164
+ payload["written"] = written
165
+ return json.dumps(payload)
166
+
167
+
144
168
  def register(app: typer.Typer) -> None:
145
169
  skill_app = typer.Typer(
146
170
  name="skill",
@@ -183,4 +207,39 @@ def register(app: typer.Typer) -> None:
183
207
  else:
184
208
  typer.echo(_render_table(resolution, mode, written, source))
185
209
 
210
+ @skill_app.command("export")
211
+ def export_cmd(
212
+ destination: Annotated[
213
+ Path,
214
+ typer.Argument(
215
+ help=(
216
+ "Directory to export the skill into. The skill is written "
217
+ "to <destination>/netbox-super-cli/SKILL.md."
218
+ ),
219
+ ),
220
+ ],
221
+ apply_: Annotated[
222
+ bool,
223
+ typer.Option("--apply", help="Actually copy the file (default: dry-run)."),
224
+ ] = False,
225
+ output: Annotated[
226
+ _OutputFormat,
227
+ typer.Option("--output", "-o", help="table|json"),
228
+ ] = _OutputFormat.TABLE,
229
+ ) -> None:
230
+ dest_file = (destination.expanduser() / BUNDLE_NAME / "SKILL.md").resolve()
231
+ mode = "apply" if apply_ else "dry-run"
232
+ written = False
233
+
234
+ with bundle_path() as source:
235
+ if apply_:
236
+ dest_file.parent.mkdir(parents=True, exist_ok=True)
237
+ shutil.copy2(source, dest_file)
238
+ written = True
239
+
240
+ if output is _OutputFormat.JSON:
241
+ typer.echo(_render_export_json(dest_file, source, mode, written))
242
+ else:
243
+ typer.echo(_render_export_table(dest_file, source, mode, written))
244
+
186
245
  app.add_typer(skill_app, name="skill")
@@ -20,6 +20,7 @@ import time
20
20
  from pathlib import Path
21
21
 
22
22
  import httpx
23
+ from ruamel.yaml import YAML
23
24
 
24
25
  from nsc.builder.build import build_command_model
25
26
  from nsc.cache.store import CacheStore
@@ -124,6 +125,12 @@ def _build_and_cache(loaded: LoadedSchema, paths: Paths, profile: ResolvedProfil
124
125
  store = CacheStore(root=paths.cache_dir)
125
126
  cached = store.load(profile.name, loaded.hash)
126
127
  if cached is not None:
128
+ # Issue #39: a live fetch just confirmed this hash is current.
129
+ # Bump the sidecar so the TTL fast-path trusts the cache on the
130
+ # next invocation, otherwise an aged-out sidecar (or a legacy
131
+ # cache from before sidecars existed) makes us refetch every
132
+ # invocation even though the schema hasn't moved.
133
+ store.touch_fetched_at(profile.name, loaded.hash)
127
134
  return cached
128
135
  model = build_command_model(loaded)
129
136
  store.save(profile.name, model)
@@ -201,8 +208,16 @@ def _find_fresh_cached(
201
208
 
202
209
  def _load_bundled_command_model() -> CommandModel | None:
203
210
  pkg_dir = Path(_bundled_pkg.__file__).resolve().parent
204
- candidates = sorted(list(pkg_dir.glob("*.json")) + list(pkg_dir.glob("*.json.gz")))
205
- if not candidates:
211
+ manifest_path = pkg_dir / "manifest.yaml"
212
+ if not manifest_path.exists():
206
213
  return None
207
- loaded = load_schema(str(candidates[0]))
214
+ manifest = YAML(typ="safe").load(manifest_path.read_text())
215
+ schemas = manifest.get("schemas") if isinstance(manifest, dict) else None
216
+ if not schemas:
217
+ return None
218
+ newest = schemas[-1]
219
+ schema_path = pkg_dir / str(newest["file"])
220
+ if not schema_path.exists():
221
+ return None
222
+ loaded = load_schema(str(schema_path))
208
223
  return build_command_model(loaded)
@@ -0,0 +1,9 @@
1
+ # Bundled NetBox OpenAPI snapshots. Newest entry last.
2
+ # When the offline fallback fires, `nsc/schema/source.py` loads the last
3
+ # entry; `scripts/gen_docs.py` does the same for the auto-generated CLI
4
+ # reference. To add a new version, see docs/contributing/adding-bundled-schemas.md.
5
+ schemas:
6
+ - version: "4.5.10"
7
+ file: "netbox-4.5.10.json.gz"
8
+ - version: "4.6.0"
9
+ file: "netbox-4.6.0.json.gz"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "netbox-super-cli"
3
- version = "1.0.2"
3
+ version = "1.0.3"
4
4
  description = "Dynamic NetBox CLI generated from the live OpenAPI schema."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -115,6 +115,29 @@ operation.
115
115
  nsc dcim interfaces list --device 42 --all --output json \
116
116
  | jq '[.[] | select(.enabled == true and (.name | startswith("Gi")))]'
117
117
  ```
118
+ - **Working across many devices? Scope once, filter locally.** Don't
119
+ loop `nsc dcim interfaces list --device <id>` over every device — pull
120
+ the broadest reasonable scope (a site, a role, a device type) in a
121
+ single call, then narrow with `jq`. Server-side filters compose, so
122
+ pick the tightest scope NetBox can apply for you:
123
+ ```bash
124
+ # GOOD — one call scoped to a site, local filtering by device list
125
+ nsc dcim interfaces list --site dc1 --all --output json \
126
+ | jq --argjson ids '[42, 43, 44]' \
127
+ '[.[] | select(.device.id as $d | $ids | index($d))]'
128
+
129
+ # GOOD — one call scoped to a role, then group by device
130
+ nsc dcim interfaces list --device_role leaf-switch --all --output json \
131
+ | jq 'group_by(.device.name)'
132
+
133
+ # BAD — N round-trips, one per device
134
+ for id in 42 43 44; do
135
+ nsc dcim interfaces list --device "$id" --all --output json
136
+ done
137
+ ```
138
+ The same pattern applies to IPs, cables, inventory items, and any
139
+ child resource: filter by the parent scope (site, tenant, role,
140
+ device-type), fetch once, partition locally.
118
141
  - **Pagination defaults:** `list` returns the first page (50 rows by
119
142
  default). Pass `--all` to follow `next` links until exhausted, or
120
143
  `--limit N` for a hard cap. `nsc describe <tag> <resource>` reveals
@@ -157,3 +157,63 @@ def test_load_fetched_at_handles_corrupt_sidecar(tmp_path: Path) -> None:
157
157
  sidecar = tmp_path / "prod" / ("a" * 64 + ".meta.json")
158
158
  sidecar.write_text("not json")
159
159
  assert store.load_fetched_at("prod", "a" * 64) is None
160
+
161
+
162
+ def test_touch_fetched_at_refreshes_existing_sidecar(tmp_path: Path) -> None:
163
+ """`touch_fetched_at` must update an existing sidecar's `fetched_at`
164
+ without rewriting the cache file. Used by the source resolver after
165
+ a live fetch confirms an existing-by-hash cache entry is still valid."""
166
+ store = CacheStore(root=tmp_path)
167
+ h = "a" * 64
168
+ store.save("prod", _model(h))
169
+ cache_file = tmp_path / "prod" / f"{h}.json"
170
+ original_cache_mtime = cache_file.stat().st_mtime
171
+
172
+ old_ts = time.time() - 10_000
173
+ sidecar = tmp_path / "prod" / f"{h}.meta.json"
174
+ sidecar.write_text(json.dumps({"fetched_at": old_ts}))
175
+
176
+ before = time.time()
177
+ store.touch_fetched_at("prod", h)
178
+ after = time.time()
179
+
180
+ assert cache_file.stat().st_mtime == original_cache_mtime
181
+ refreshed = json.loads(sidecar.read_text())["fetched_at"]
182
+ assert before <= refreshed <= after
183
+
184
+
185
+ def test_touch_fetched_at_creates_sidecar_for_legacy_cache(tmp_path: Path) -> None:
186
+ """A cache file written before the sidecar feature has no
187
+ `<hash>.meta.json`. After a live fetch confirms the hash matches,
188
+ `touch_fetched_at` must create the sidecar so the next invocation
189
+ can use the TTL fast-path."""
190
+ store = CacheStore(root=tmp_path)
191
+ h = "a" * 64
192
+ store.save("prod", _model(h))
193
+ sidecar = tmp_path / "prod" / f"{h}.meta.json"
194
+ sidecar.unlink()
195
+
196
+ before = time.time()
197
+ store.touch_fetched_at("prod", h)
198
+ after = time.time()
199
+
200
+ assert sidecar.exists()
201
+ fetched_at = json.loads(sidecar.read_text())["fetched_at"]
202
+ assert before <= fetched_at <= after
203
+
204
+
205
+ def test_touch_fetched_at_noop_when_cache_file_missing(tmp_path: Path) -> None:
206
+ """No cache file means there is nothing to mark fresh — must not
207
+ create an orphaned sidecar."""
208
+ store = CacheStore(root=tmp_path)
209
+ h = "a" * 64
210
+ store.touch_fetched_at("prod", h)
211
+ sidecar = tmp_path / "prod" / f"{h}.meta.json"
212
+ assert not sidecar.exists()
213
+
214
+
215
+ def test_touch_fetched_at_rejects_invalid_hash(tmp_path: Path) -> None:
216
+ store = CacheStore(root=tmp_path)
217
+ store.touch_fetched_at("prod", "not-a-hash")
218
+ profile_dir = tmp_path / "prod"
219
+ assert not profile_dir.exists() or not any(profile_dir.iterdir())