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.
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/AGENTS.md +13 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/CHANGELOG.md +18 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/CLAUDE.md +13 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/PKG-INFO +2 -2
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/README.md +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/adding-bundled-schemas.md +3 -3
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/getting-started/install.md +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/index.md +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/cli.md +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/schemas.md +2 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/_version.py +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cache/store.py +22 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/login_commands.py +49 -4
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/skill_commands.py +59 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/source.py +18 -3
- netbox_super_cli-1.0.3/nsc/schemas/bundled/manifest.yaml +9 -0
- netbox_super_cli-1.0.3/nsc/schemas/bundled/netbox-4.5.10.json.gz +0 -0
- netbox_super_cli-1.0.3/nsc/schemas/bundled/netbox-4.6.0.json.gz +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/pyproject.toml +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/skills/netbox-super-cli/SKILL.md +23 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cache/test_store.py +60 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_login_commands.py +125 -2
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_registration.py +2 -2
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_skill_commands.py +83 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_handlers_audit.py +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_source_ttl.py +89 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/scripts/test_gen_docs.py +1 -1
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/uv.lock +1 -1
- netbox_super_cli-1.0.2/nsc/schemas/bundled/manifest.yaml +0 -5
- netbox_super_cli-1.0.2/nsc/schemas/bundled/netbox-4.6.0-beta2.json.gz +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/agents-md-sync.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/bench.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/claude.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/docs.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/e2e.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/lint.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/release.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.github/workflows/test.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.gitignore +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.pre-commit-config.yaml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/.python-version +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/LICENSE +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/caching.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/command-generation.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/http-client.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/overview.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/architecture/schema-loading.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/branching.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/development.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/release-process.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/getting-started/concepts.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/getting-started/first-run.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/ci-and-automation.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/managing-profiles.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/output-formats.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/using-with-ai-agents.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/working-with-plugins.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/guides/writes-and-safety.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/config.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/reference/exit-codes.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/justfile +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/mkdocs.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/__main__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/aliases/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/aliases/resolver.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/auth/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/auth/verify.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/builder/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/builder/build.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cache/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/aliases_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/app.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/cache_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/commands_dump.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/config_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/globals.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/handlers.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/init_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/profiles_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/registration.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/runtime.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/apply.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/bulk.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/coercion.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/confirmation.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/input.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/cli/writes/preflight.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/loader.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/models.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/settings.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/config/writer.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/audit.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/client.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/errors.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/http/retry.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/model/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/model/command_model.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/csv_.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/errors.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/explain.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/flatten.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/headers.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/json_.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/jsonl.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/render.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/table.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/output/yaml_.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/hashing.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/loader.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schema/models.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schemas/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/schemas/bundled/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/nsc/skill/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/scripts/gen_docs.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/scripts/sync_agents_md.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/aliases/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/aliases/test_resolver.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/auth/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/auth/test_verify.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/benchmarks/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/benchmarks/test_startup.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_build.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_default_columns.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_redaction_marking.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/builder/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cache/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cache/test_prune.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_aliases_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_app_smoke.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_cache_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_commands_dump.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_completion_smoke.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_config_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_explain.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_handlers.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_init_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_meta_subcommands_under_broken_config.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_ndjson_input.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_profiles_commands.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_respx_integration.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_runtime.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_stdin_sniffer.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/test_writes_respx.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_apply.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_bulk.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_confirmation.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_handler_helpers.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_input.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/cli/writes/test_preflight.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_loader.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_models.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_writer_atomicity.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_writer_dotted_paths.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/config/test_writer_roundtrip.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/conftest.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/README.md +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/conftest.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/docker-compose.yml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_auth_error_envelope.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_bulk_create_with_loop_fallback.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_full_cycle.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_login.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_ndjson.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_preflight_blocks_apply.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/test_validation_error_envelope_from_netbox.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/e2e/wait_for_netbox.sh +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/profiles/single_profile.yaml +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/auth_401.json +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/circuits_providers_list.json +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/dcim_devices_get.json +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/dcim_devices_list_p1.json +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/fixtures/responses/dcim_devices_list_p2.json +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_audit.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_audit_redaction.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_client.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_client_redaction_threading.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_errors.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/http/test_retry.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/model/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/model/test_command_model.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/model/test_request_body_shape.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_csv.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_errors.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_errors_aliases.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_explain.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_flatten.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_input_error_envelope.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_json.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_jsonl.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_render_dispatch.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_table.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/output/test_yaml.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_hashing.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_loader.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_models.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/schema/test_source.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/scripts/__init__.py +0 -0
- {netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/tests/skill/__init__.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
{netbox_super_cli-1.0.2 → netbox_super_cli-1.0.3}/docs/contributing/adding-bundled-schemas.md
RENAMED
|
@@ -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
|
|
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
|
|
21
|
-
file: "netbox-4.6.0
|
|
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.
|
|
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
|
+
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
|
|
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`,
|
|
@@ -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
|
-
|
|
205
|
-
if not
|
|
211
|
+
manifest_path = pkg_dir / "manifest.yaml"
|
|
212
|
+
if not manifest_path.exists():
|
|
206
213
|
return None
|
|
207
|
-
|
|
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"
|
|
Binary file
|
|
Binary file
|
|
@@ -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())
|