diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml new file mode 100644 index 00000000..77842ab3 --- /dev/null +++ b/.github/workflows/cli.yml @@ -0,0 +1,52 @@ +name: CLI + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +jobs: + test: + name: Rust CLI + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 + - uses: voidzero-dev/setup-vp@250f29ce396baf5e8f24498e17c0dfdebabc26eb # v1 + with: + version: 0.1.24 + node-version-file: '.node-version' + sfw: true + cache: true + run-install: | + - args: ['--frozen-lockfile'] + - name: Install Rust + working-directory: apps/cli + run: rustup show + - name: Check generated manifest and versions + run: | + pnpm --filter @marimo-hub/api test cliManifest.spec + pnpm cli:version-check + - name: Validate distribution plan + working-directory: apps/cli + run: | + curl --proto '=https' --tlsv1.2 -LsSf \ + https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh + dist plan --allow-dirty + - name: Format + working-directory: apps/cli + run: cargo fmt --all -- --check + - name: Clippy + working-directory: apps/cli + run: cargo clippy --all-targets --locked -- -D warnings + - name: Test + working-directory: apps/cli + run: cargo test --locked diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a9744b6b..424421f0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,10 +2,10 @@ name: Release # Triggered by a semver tag, normally pushed by release-tag.yml when a # "release: X.Y.Z" PR is merged (see development_docs/releasing.md). -# This publishes the container image and the Helm chart to GHCR, with the -# chart version, chart appVersion, and image tag all pinned to the git tag, -# plus a GitHub release with a changelog generated from the commits since the -# previous tag. An end-user upgrades with: +# This publishes the container image and Helm chart to GHCR, builds native CLI +# archives and binary wheels, and creates a GitHub release with checksums, +# provenance, an SBOM, and a changelog generated from commits since the prior +# tag. The chart version, appVersion, and image tag are pinned to the git tag. # helm upgrade --install marimohub oci://ghcr.io/marimo-team/charts/marimohub \ # --version 1.4.2 -n marimohub -f values.yaml on: @@ -20,6 +20,140 @@ env: CHART_REGISTRY: oci://ghcr.io/${{ github.repository_owner }}/charts jobs: + cli: + name: Build CLI (${{ matrix.target }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 25 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + - os: macos-15-intel + target: x86_64-apple-darwin + - os: macos-15 + target: aarch64-apple-darwin + - os: windows-latest + target: x86_64-pc-windows-msvc + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Install Rust target + shell: bash + working-directory: apps/cli + run: | + rustup show + rustup target add "${{ matrix.target }}" + - name: Install dist + shell: bash + run: | + curl --proto '=https' --tlsv1.2 -LsSf \ + https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh + - name: Build and test + shell: bash + working-directory: apps/cli + run: | + cargo test --locked + cargo run --locked --example generate-assets -- generated + dist build --artifacts=local --target "${{ matrix.target }}" \ + --tag "$GITHUB_REF_NAME" --allow-dirty + - name: Build binary wheel + shell: bash + working-directory: apps/cli + run: | + python -m pip install 'maturin==1.9.4' + python -m maturin build --release --locked \ + --target "${{ matrix.target }}" \ + --out ../../wheelhouse + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: mohub-${{ matrix.target }} + path: | + apps/cli/target/distrib/*.tar.gz + apps/cli/target/distrib/*.zip + apps/cli/target/distrib/*.msi + apps/cli/target/distrib/*.sha256 + apps/cli/target/distrib/mohub-*-update* + wheelhouse/*.whl + if-no-files-found: error + + cli-linux-x64: + name: Build CLI (x86_64-unknown-linux-gnu, glibc 2.28) + runs-on: ubuntu-latest + container: quay.io/pypa/manylinux_2_28_x86_64:latest + timeout-minutes: 25 + permissions: + contents: read + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Install build tools + shell: bash + run: | + dnf install -y dbus-devel python3-pip + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal + echo "$HOME/.cargo/bin" >> "$GITHUB_PATH" + - name: Install dist + shell: bash + run: | + curl --proto '=https' --tlsv1.2 -LsSf \ + https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh + - name: Build and test + shell: bash + working-directory: apps/cli + run: | + cargo test --locked + cargo run --locked --example generate-assets -- generated + dist build --artifacts=local --target x86_64-unknown-linux-gnu \ + --tag "$GITHUB_REF_NAME" --allow-dirty + - name: Build binary wheel + shell: bash + working-directory: apps/cli + run: | + /opt/python/cp39-cp39/bin/python -m pip install 'maturin==1.9.4' + /opt/python/cp39-cp39/bin/python -m maturin build \ + --release --locked --out ../../wheelhouse + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: mohub-x86_64-unknown-linux-gnu + path: | + apps/cli/target/distrib/*.tar.gz + apps/cli/target/distrib/*.sha256 + apps/cli/target/distrib/mohub-*-update* + wheelhouse/*.whl + if-no-files-found: error + + cli-installers: + name: Build CLI installers + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [cli, cli-linux-x64] + permissions: + contents: read + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + - name: Install dist + run: | + curl --proto '=https' --tlsv1.2 -LsSf \ + https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh + - name: Build installers + working-directory: apps/cli + run: | + cargo run --locked --example generate-assets -- generated + dist build --artifacts=global --tag "$GITHUB_REF_NAME" --allow-dirty + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: mohub-installers + path: | + apps/cli/target/distrib/*.sh + apps/cli/target/distrib/*.ps1 + apps/cli/target/distrib/*.rb + apps/cli/target/distrib/*.tar.gz + apps/cli/target/distrib/*.sha256 + apps/cli/target/distrib/*.sum + if-no-files-found: error + image: name: Build & push image runs-on: ubuntu-latest @@ -134,29 +268,56 @@ jobs: name: Publish GitHub release notes runs-on: ubuntu-latest timeout-minutes: 10 + needs: [cli, cli-linux-x64, cli-installers] permissions: contents: write + id-token: write + attestations: write steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 with: # changelogithub diffs against the previous tag, so it needs full history. fetch-depth: 0 + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + pattern: mohub-* + path: release-assets + merge-multiple: true + + - name: Create checksums and dependency SBOM + run: | + cargo install cargo-cyclonedx --locked --version 0.5.7 + (cd apps/cli && cargo cyclonedx \ + --format json --override-filename mohub.cdx.json) + cp apps/cli/mohub.cdx.json release-assets/mohub.cdx.json + (cd release-assets && sha256sum ./* > SHA256SUMS) + + - name: Attest CLI artifacts + uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2 + with: + subject-path: 'release-assets/*' + - name: Generate changelog and create GitHub release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: npx changelogithub@14.0.0 + - name: Attach CLI artifacts + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: gh release upload "$GITHUB_REF_NAME" release-assets/* --clobber + notify: name: Notify Slack runs-on: ubuntu-latest timeout-minutes: 5 # Report the overall outcome once every release job has finished. - needs: [image, chart, release-notes] + needs: [cli, cli-linux-x64, cli-installers, image, chart, release-notes] if: always() steps: - name: Notify Slack - Release Success - if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + if: ${{ !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') && !contains(needs.*.result, 'skipped') }} uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL_RELEASES }} @@ -176,7 +337,7 @@ jobs: } - name: Notify Slack - Release Failed - if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} + if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') || contains(needs.*.result, 'skipped') }} uses: slackapi/slack-github-action@0d95c9a7becc1e6e297d76df9bc735c44f4cbcbc # v3.0.5 with: webhook: ${{ secrets.SLACK_WEBHOOK_URL_RELEASES }} diff --git a/.gitignore b/.gitignore index bc6b5a09..8cbd9f06 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ node_modules +/apps/cli/target +/apps/cli/*.cdx.json +/apps/cli/generated/completions/ +/apps/cli/generated/man/ dist coverage *.log diff --git a/apps/cli/Cargo.lock b/apps/cli/Cargo.lock new file mode 100644 index 00000000..f854d4b8 --- /dev/null +++ b/apps/cli/Cargo.lock @@ -0,0 +1,2649 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "assert_cmd" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-link", +] + +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytecount" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures", + "rand_core", +] + +[[package]] +name = "clap" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_complete" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be2ad0423bdbbb0e25bc89add796f3559706d4a95e1bc98e4d9662a957b6a19" +dependencies = [ + "clap", +] + +[[package]] +name = "clap_complete_nushell" +version = "4.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb66bc82eb9c92b1727310ae2c5868df22ae7cf46185bc5c544a4fa71955e49" +dependencies = [ + "clap", + "clap_complete", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clap_mangen" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c630ddc90572b3d9abd3f887f0007b4e048faf5c5a59ae607f8ac1c5e3790873" +dependencies = [ + "clap", + "roff", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "console" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fe5f465a4f6fee88fad41b85d990f84c835335e85b5d9e6e63e0d06d28cba7c" +dependencies = [ + "encode_unicode", + "libc", + "unicode-width 0.2.2", + "windows-sys 0.61.2", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cookie" +version = "0.18.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a373e3602691c3cdea496d2f0ee5935151e6168fe87739483c463db1b2f2f87" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206" +dependencies = [ + "cookie", + "document-features", + "idna", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "time", + "url", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[package]] +name = "csv" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde_core", +] + +[[package]] +name = "csv-core" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" +dependencies = [ + "memchr", +] + +[[package]] +name = "dbus" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab69f03cc8c4340c9c8e315114e1658e6775a9b16a04357973aa21cec22b32e" +dependencies = [ + "libc", + "libdbus-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "dbus-secret-service" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6" +dependencies = [ + "dbus", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "directories" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.61.2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "etcetera" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c7b13d0780cb82722fd59f6f57f925e143427e4a75313a6c77243bf5326ae6" +dependencies = [ + "cfg-if", + "home", + "windows-sys 0.59.0", +] + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b09cf3155332e944990140d967ff5eceb70df778b34f77d8075db46e4704e6d8" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "futures-channel" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" + +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-sink" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" + +[[package]] +name = "futures-task" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" + +[[package]] +name = "futures-util" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" +dependencies = [ + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi", + "rand_core", + "wasm-bindgen", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", + "webpki-roots 1.0.9", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "indicatif" +version = "0.18.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9433806cd6b4ec1aba79c021c7e4c58fb4c3b9977c085062e611ac929998fb0c" +dependencies = [ + "console", + "portable-atomic", + "unicode-width 0.2.2", + "unit-prefix", + "web-time", +] + +[[package]] +name = "inquire" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6654738b8024300cf062d04a1c13c10c8e2cea598ec1c47dc9b6641159429756" +dependencies = [ + "bitflags", + "crossterm", + "dyn-clone", + "unicode-segmentation", + "unicode-width 0.2.2", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "keyring" +version = "3.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" +dependencies = [ + "byteorder", + "dbus-secret-service", + "log", + "security-framework 2.11.1", + "security-framework 3.7.0", + "windows-sys 0.60.2", + "zeroize", +] + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "libdbus-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "328c4789d42200f1eeec05bd86c9c13c7f091d2ba9a6ea35acdf51f31bc0f043" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "libredox" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa" +dependencies = [ + "libc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miette" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f98efec8807c63c752b5bd61f862c165c115b0a35685bdcfd9238c7aeb592b7" +dependencies = [ + "backtrace", + "backtrace-ext", + "cfg-if", + "miette-derive", + "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", + "textwrap", + "unicode-width 0.1.14", +] + +[[package]] +name = "miette-derive" +version = "7.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db5b29714e950dbb20d5e6f74f9dcec4edbcc1067bb7f8ed198c097b8c1a818b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mohub" +version = "0.3.1" +dependencies = [ + "assert_cmd", + "clap", + "clap_complete", + "clap_complete_nushell", + "clap_mangen", + "csv", + "directories", + "fs2", + "indicatif", + "inquire", + "keyring", + "miette", + "predicates", + "secrecy", + "serde", + "serde_json", + "tabled", + "tempfile", + "thiserror", + "update-informer", + "ureq 2.12.1", + "url", + "urlencoding", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "owo-colors" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" + +[[package]] +name = "papergrid" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0984e668274d34691bc2b262ef0d115de5fa9973bcdee7ae32213f93099153e" +dependencies = [ + "bytecount", + "fnv", + "unicode-width 0.2.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 1.0.9", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "roff" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "323c417e1d9665a65b263ec744ba09030cfb277e9daa0b018a4ab62e57bc8189" + +[[package]] +name = "rustc-demangle" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b56ffa8bb2830709a538c2cbcae9aa062db0d2a42563bfb09bdaae44020eb" + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0527518605e68109d875e248ea259b6758801cf165e4b2c2733ae3b51f12535a" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secrecy" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e891af845473308773346dc847b2c23ee78fe442e0472ac50e22a18a93d3ae5a" +dependencies = [ + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "socket2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e396b6523b11ccb83120b115a0b7366de372751aa6edf19844dfb13a6af97e91" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tabled" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5dc662e6da844ad6e428ad16b57967c9d33c82e16bb1c258326c0c078605dff" +dependencies = [ + "papergrid", + "testing_table", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.3", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "testing_table" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f8daae29995a24f65619e19d8d31dea5b389f3d853d8bf297bbf607cd0014cc" +dependencies = [ + "unicode-width 0.2.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "unicode-linebreak", + "unicode-width 0.2.2", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unit-prefix" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "update-informer" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b27dcf766dc6ad64c2085201626e1a7955dc1983532bfc8406d552903ace2a" +dependencies = [ + "etcetera", + "reqwest", + "semver", + "serde", + "serde_json", + "ureq 3.4.0", +] + +[[package]] +name = "ureq" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d1a66277ed75f640d608235660df48c8e3c19f3b4edb6a263315626cc3c01d" +dependencies = [ + "base64 0.22.1", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "url", + "webpki-roots 0.26.11", +] + +[[package]] +name = "ureq" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972d7902c8735f2695410b8aed7df6ed12a47394aa1c8d7af49f0497b731a94d" +dependencies = [ + "base64 0.23.1", + "cookie_store", + "flate2", + "log", + "percent-encoding", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "ureq-proto", + "utf8-zero", + "webpki-roots 1.0.9", +] + +[[package]] +name = "ureq-proto" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5f78b09e6941e1a0f2e30e695e4b120377b54d5e0aec11b594bb57b3971613" +dependencies = [ + "base64 0.23.1", + "http", + "httparse", + "log", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8-zero" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" +dependencies = [ + "webpki-roots 1.0.9", +] + +[[package]] +name = "webpki-roots" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/apps/cli/Cargo.toml b/apps/cli/Cargo.toml new file mode 100644 index 00000000..e2d27f60 --- /dev/null +++ b/apps/cli/Cargo.toml @@ -0,0 +1,54 @@ +[package] +name = "mohub" +version = "0.3.1" +edition = "2021" +license = "Apache-2.0" +repository = "https://github.com/marimo-team/marimohub" +rust-version = "1.88" +description = "Fast, cross-platform CLI for marimohub" +authors = ["marimo team"] +homepage = "https://github.com/marimo-team/marimohub" +readme = "../../README.md" + +[dependencies] +clap = { version = "4.5.32", features = ["env", "string"] } +clap_complete = "4.5.46" +clap_complete_nushell = "4.6.2" +clap_mangen = "0.3.2" +csv = "1.3.1" +directories = "6.0.0" +fs2 = "0.4.3" +indicatif = "0.18.6" +inquire = { version = "0.9.4", default-features = false, features = ["crossterm"] } +keyring = { version = "3.6.2", features = [ + "apple-native", + "windows-native", + "sync-secret-service", +] } +miette = { version = "7.6.0", features = ["fancy"] } +secrecy = "0.10.3" +serde = { version = "1.0.219", features = ["derive"] } +serde_json = "1.0.140" +tabled = { version = "0.21.0", default-features = false, features = ["std"] } +tempfile = "3.20.0" +thiserror = "2.0.12" +update-informer = { version = "1.3.0", default-features = false, features = [ + "github", + "ureq", + "rustls-tls", +] } +ureq = { version = "2.12.1", features = ["json"] } +url = "2.5.4" +urlencoding = "2.1.3" + +[dev-dependencies] +assert_cmd = "2.2.2" +predicates = "3.1.3" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = true + +[profile.dist] +inherits = "release" diff --git a/apps/cli/dist-workspace.toml b/apps/cli/dist-workspace.toml new file mode 100644 index 00000000..bf992b41 --- /dev/null +++ b/apps/cli/dist-workspace.toml @@ -0,0 +1,25 @@ +[workspace] +members = ["cargo:."] + +[dist] +cargo-dist-version = "0.32.0" +ci = ["github"] +allow-dirty = ["ci", "msi"] +targets = [ + "aarch64-apple-darwin", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "x86_64-pc-windows-msvc", + "x86_64-unknown-linux-gnu", +] +installers = ["shell", "powershell", "homebrew", "msi", "npm"] +include = ["generated/completions", "generated/man"] +install-path = "~/.local/bin" +install-updater = true +npm-scope = "@marimo-team" +npm-package = "mohub" +tap = "marimo-team/homebrew-tap" +unix-archive = ".tar.gz" + +[dist.min-glibc-version] +x86_64-unknown-linux-gnu = "2.28" diff --git a/apps/cli/examples/generate-assets.rs b/apps/cli/examples/generate-assets.rs new file mode 100644 index 00000000..c1609d27 --- /dev/null +++ b/apps/cli/examples/generate-assets.rs @@ -0,0 +1,40 @@ +use std::fs; +use std::io; +use std::path::PathBuf; + +use clap_complete::{generate_to, Shell}; + +fn main() -> io::Result<()> { + let output = std::env::args_os() + .nth(1) + .map(PathBuf::from) + .unwrap_or_else(|| PathBuf::from("target/generated-assets")); + let completions = output.join("completions"); + let manpages = output.join("man"); + fs::create_dir_all(&completions)?; + fs::create_dir_all(&manpages)?; + + let manifest = mohub::manifest::load(); + for shell in [ + Shell::Bash, + Shell::Elvish, + Shell::Fish, + Shell::PowerShell, + Shell::Zsh, + ] { + generate_to( + shell, + &mut mohub::cli::build(&manifest), + "mohub", + &completions, + )?; + } + generate_to( + clap_complete_nushell::Nushell, + &mut mohub::cli::build(&manifest), + "mohub", + &completions, + )?; + clap_mangen::generate_to(mohub::cli::build(&manifest), &manpages)?; + Ok(()) +} diff --git a/apps/cli/generated/cli-manifest.json b/apps/cli/generated/cli-manifest.json new file mode 100644 index 00000000..f6f3667b --- /dev/null +++ b/apps/cli/generated/cli-manifest.json @@ -0,0 +1,2425 @@ +{ + "version": 1, + "api_version": "1.0.0", + "operations": [ + { + "id": "admin.config.get", + "command": ["admin", "config", "get"], + "method": "GET", + "path": "/api/v1/admin/config", + "summary": "Describe the deployment's configuration", + "description": "Read-only view of every configuration group (storage, compute, auth, …) as resolved from the serving replica's environment at boot; secret values are never included, only whether they are set. Super-admin only, session-only.", + "parameters": [], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": true, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "admin.users.list", + "command": ["admin", "users", "list"], + "method": "GET", + "path": "/api/v1/admin/users", + "summary": "List all users in the identity directory", + "description": "Every user who has signed in at least once, name-sorted. Currently a single page (`next_cursor` is always null). Super-admin only, and session-only: a PAT — even a super admin’s — is rejected with 403.", + "parameters": [], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": true, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "audit.list", + "command": ["audit", "list"], + "method": "GET", + "path": "/api/v1/events", + "summary": "List deployment audit events", + "description": "Deployment-wide audit trail, newest first. Super-admin only. Date ranges are inclusive and limited to 30 UTC days.", + "parameters": [ + { + "name": "actor", + "cli_name": "actor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "event", + "cli_name": "event", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "from", + "cli_name": "from", + "in": "query", + "required": false, + "description": "UTC calendar date", + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + }, + { + "name": "project_id", + "cli_name": "project-id", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "to", + "cli_name": "to", + "in": "query", + "required": false, + "description": "UTC calendar date", + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "auth.tokens.create", + "command": ["auth", "tokens", "create"], + "method": "POST", + "path": "/api/v1/me/tokens", + "summary": "Create a personal access token", + "description": "Mint a machine credential that acts as the calling user (CI, scripts, the CLI): send it as `Authorization: Bearer mhub_pat_…`. The plaintext token is returned once, in this response, and never again. Requires session (SSO) auth — a token cannot mint tokens.", + "parameters": [], + "body": { + "required": true, + "properties": [ + { + "name": "expires_in_days", + "cli_name": "expires-in-days", + "required": false, + "description": "Days until expiry; omit for a non-expiring token.", + "value_type": "integer", + "repeatable": false + }, + { + "name": "name", + "cli_name": "name", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": true, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "auth.tokens.list", + "command": ["auth", "tokens", "list"], + "method": "GET", + "path": "/api/v1/me/tokens", + "summary": "List the caller's personal access tokens", + "description": "Metadata only — the secret is never retrievable after creation.", + "parameters": [], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": true, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "auth.tokens.revoke", + "command": ["auth", "tokens", "revoke"], + "method": "DELETE", + "path": "/api/v1/me/tokens/{tokenId}", + "summary": "Revoke a personal access token", + "description": "Deletes the token; API requests using it fail within the verification-cache TTL (~30 seconds) on other replicas, immediately on this one.", + "parameters": [ + { + "name": "tokenId", + "cli_name": "token-id", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": true, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "capabilities", + "command": ["capabilities"], + "method": "GET", + "path": "/api/v1/capabilities", + "summary": "Get deployment capability flags", + "parameters": [], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.kinds.list", + "command": ["integrations", "kinds", "list"], + "method": "GET", + "path": "/api/v1/integrations/kinds", + "summary": "List available integration kinds (schemas drive the config forms)", + "parameters": [], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.org.create", + "command": ["integrations", "org", "create"], + "method": "POST", + "path": "/api/v1/org/integrations", + "summary": "Create an org-wide integration (super admin only)", + "parameters": [], + "body": { + "required": true, + "properties": [ + { + "name": "change_note", + "cli_name": "change-note", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "config", + "cli_name": "config", + "required": true, + "value_type": "object", + "repeatable": false + }, + { + "name": "kind", + "cli_name": "kind", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "name", + "cli_name": "name", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.org.delete", + "command": ["integrations", "org", "delete"], + "method": "DELETE", + "path": "/api/v1/org/integrations/{iid}", + "summary": "Delete an org-wide integration and its version history (super admin only)", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "integrations.org.get" + }, + { + "id": "integrations.org.get", + "command": ["integrations", "org", "get"], + "method": "GET", + "path": "/api/v1/org/integrations/{iid}", + "summary": "Get an org-wide integration with its redacted config (super admin only)", + "parameters": [ + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.org.list", + "command": ["integrations", "org", "list"], + "method": "GET", + "path": "/api/v1/org/integrations", + "summary": "List org-wide integrations (super admin only)", + "parameters": [ + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.org.test", + "command": ["integrations", "org", "test"], + "method": "POST", + "path": "/api/v1/org/integrations/test", + "summary": "Probe connectivity for an unsaved or stored org config (super admin only)", + "parameters": [], + "body": { + "required": true, + "properties": [] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.org.update", + "command": ["integrations", "org", "update"], + "method": "PATCH", + "path": "/api/v1/org/integrations/{iid}", + "summary": "Update an org-wide integration (super admin only)", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "change_note", + "cli_name": "change-note", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "config", + "cli_name": "config", + "required": false, + "value_type": "object", + "repeatable": false + }, + { + "name": "enabled", + "cli_name": "enabled", + "required": false, + "value_type": "boolean", + "repeatable": false + }, + { + "name": "name", + "cli_name": "name", + "required": false, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "integrations.org.get" + }, + { + "id": "integrations.org.versions", + "command": ["integrations", "org", "versions"], + "method": "GET", + "path": "/api/v1/org/integrations/{iid}/versions", + "summary": "List an org-wide integration's config versions (super admin only)", + "parameters": [ + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.project.copy", + "command": ["integrations", "project", "copy"], + "method": "POST", + "path": "/api/v1/projects/{pid}/integrations/copy", + "summary": "Copy an integration from another project (manager of both projects)", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "name", + "cli_name": "name", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "source_integration_id", + "cli_name": "source-integration-id", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "source_project_id", + "cli_name": "source-project-id", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.project.create", + "command": ["integrations", "project", "create"], + "method": "POST", + "path": "/api/v1/projects/{pid}/integrations", + "summary": "Create an integration (manager only)", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "change_note", + "cli_name": "change-note", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "config", + "cli_name": "config", + "required": true, + "value_type": "object", + "repeatable": false + }, + { + "name": "kind", + "cli_name": "kind", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "name", + "cli_name": "name", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.project.delete", + "command": ["integrations", "project", "delete"], + "method": "DELETE", + "path": "/api/v1/projects/{pid}/integrations/{iid}", + "summary": "Delete an integration and its version history (manager only)", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "integrations.project.get" + }, + { + "id": "integrations.project.get", + "command": ["integrations", "project", "get"], + "method": "GET", + "path": "/api/v1/projects/{pid}/integrations/{iid}", + "summary": "Get an integration with its redacted config", + "parameters": [ + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.project.list", + "command": ["integrations", "project", "list"], + "method": "GET", + "path": "/api/v1/projects/{pid}/integrations", + "summary": "List a project's integrations", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.project.test", + "command": ["integrations", "project", "test"], + "method": "POST", + "path": "/api/v1/projects/{pid}/integrations/test", + "summary": "Probe connectivity for an unsaved config or a stored instance (manager only)", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "integrations.project.update", + "command": ["integrations", "project", "update"], + "method": "PATCH", + "path": "/api/v1/projects/{pid}/integrations/{iid}", + "summary": "Update an integration (manager only); a config change appends a version", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "change_note", + "cli_name": "change-note", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "config", + "cli_name": "config", + "required": false, + "value_type": "object", + "repeatable": false + }, + { + "name": "enabled", + "cli_name": "enabled", + "required": false, + "value_type": "boolean", + "repeatable": false + }, + { + "name": "name", + "cli_name": "name", + "required": false, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "integrations.project.get" + }, + { + "id": "integrations.project.versions", + "command": ["integrations", "project", "versions"], + "method": "GET", + "path": "/api/v1/projects/{pid}/integrations/{iid}/versions", + "summary": "List an integration's config versions (metadata only)", + "parameters": [ + { + "name": "iid", + "cli_name": "iid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "me", + "command": ["me"], + "method": "GET", + "path": "/api/v1/me", + "summary": "Get current user info", + "parameters": [], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.content", + "command": ["notebooks", "content"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/content", + "summary": "Get notebook code", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.create", + "command": ["notebooks", "create"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks", + "summary": "Create a notebook", + "parameters": [ + { + "name": "idempotency-key", + "cli_name": "idempotency-key", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "base_image", + "cli_name": "base-image", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "code", + "cli_name": "code", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "compute_profile", + "cli_name": "compute-profile", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "deps", + "cli_name": "deps", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "description", + "cli_name": "description", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "readme", + "cli_name": "readme", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "runtime", + "cli_name": "runtime", + "required": false, + "value_type": "object", + "repeatable": false + }, + { + "name": "tags", + "cli_name": "tags", + "required": false, + "value_type": "string", + "repeatable": true + }, + { + "name": "title", + "cli_name": "title", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": true + }, + { + "id": "notebooks.create-git", + "command": ["notebooks", "create-git"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks/git", + "summary": "Create a git-synced workspace notebook", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "base_image", + "cli_name": "base-image", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "branch", + "cli_name": "branch", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "compute_profile", + "cli_name": "compute-profile", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "description", + "cli_name": "description", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "entry_notebook", + "cli_name": "entry-notebook", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "provider", + "cli_name": "provider", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "readme", + "cli_name": "readme", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "repo", + "cli_name": "repo", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "root_path", + "cli_name": "root-path", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "runtime", + "cli_name": "runtime", + "required": false, + "value_type": "object", + "repeatable": false + }, + { + "name": "tags", + "cli_name": "tags", + "required": false, + "value_type": "string", + "repeatable": true + }, + { + "name": "title", + "cli_name": "title", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.delete", + "command": ["notebooks", "delete"], + "method": "DELETE", + "path": "/api/v1/projects/{pid}/notebooks/{nid}", + "summary": "Delete a notebook (soft-delete)", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "notebooks.get" + }, + { + "id": "notebooks.duplicate", + "command": ["notebooks", "duplicate"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/duplicate", + "summary": "Duplicate a notebook", + "parameters": [ + { + "name": "idempotency-key", + "cli_name": "idempotency-key", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "title", + "cli_name": "title", + "required": false, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": true + }, + { + "id": "notebooks.get", + "command": ["notebooks", "get"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}", + "summary": "Get notebook metadata", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.html", + "command": ["notebooks", "html"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/html", + "summary": "Latest HTML snapshot of the notebook's outputs", + "description": "Serves the newest version's HTML snapshot (captured best-effort at session teardown) raw — the static outputs shown to viewers under MARIMOHUB_VIEWER_MODE=static. `X-Marimohub-Version-Id` / `X-Marimohub-Captured-At` identify the snapshot. 404 with code `NO_HTML_SNAPSHOT` when no version has one.", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "raw", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.list", + "command": ["notebooks", "list"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks", + "summary": "List notebooks in a project", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.rotate-sync-token", + "command": ["notebooks", "rotate-sync-token"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/sync-token/rotate", + "summary": "Rotate a notebook sync token", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.update", + "command": ["notebooks", "update"], + "method": "PATCH", + "path": "/api/v1/projects/{pid}/notebooks/{nid}", + "summary": "Update a notebook", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "base_image", + "cli_name": "base-image", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "code", + "cli_name": "code", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "compute_profile", + "cli_name": "compute-profile", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "deps", + "cli_name": "deps", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "description", + "cli_name": "description", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "message", + "cli_name": "message", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "readme", + "cli_name": "readme", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "tags", + "cli_name": "tags", + "required": false, + "value_type": "string", + "repeatable": true + }, + { + "name": "title", + "cli_name": "title", + "required": false, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "notebooks.get" + }, + { + "id": "notebooks.update-source", + "command": ["notebooks", "update-source"], + "method": "PATCH", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/source", + "summary": "Update a git-synced notebook source", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "branch", + "cli_name": "branch", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "entry_notebook", + "cli_name": "entry-notebook", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "repo", + "cli_name": "repo", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "root_path", + "cli_name": "root-path", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.versions.get", + "command": ["notebooks", "versions", "get"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/versions/{vid}", + "summary": "Get a specific version", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "vid", + "cli_name": "vid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.versions.html", + "command": ["notebooks", "versions", "html"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/versions/{vid}/html", + "summary": "One version's HTML snapshot of the notebook's outputs", + "description": "Serves the HTML snapshot captured for this specific version, raw. 404 with code `NO_HTML_SNAPSHOT` when the version captured none.", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "vid", + "cli_name": "vid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "raw", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.versions.list", + "command": ["notebooks", "versions", "list"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/versions", + "summary": "List notebook versions", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "notebooks.versions.restore", + "command": ["notebooks", "versions", "restore"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/versions/{vid}/restore", + "summary": "Restore a version as a new save", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "vid", + "cli_name": "vid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.audit.list", + "command": ["projects", "audit", "list"], + "method": "GET", + "path": "/api/v1/projects/{pid}/events", + "summary": "List a project's audit events for one day", + "description": "Catalog mutation audit trail (project/notebook lifecycle, membership changes), one UTC day at a time. Manager-only: events may record member management and deletions.", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "date", + "cli_name": "date", + "in": "query", + "required": false, + "description": "UTC day (defaults to today)", + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.create", + "command": ["projects", "create"], + "method": "POST", + "path": "/api/v1/projects", + "summary": "Create a project", + "parameters": [ + { + "name": "idempotency-key", + "cli_name": "idempotency-key", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "description", + "cli_name": "description", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "federation", + "cli_name": "federation", + "required": false, + "value_type": "object", + "repeatable": false + }, + { + "name": "name", + "cli_name": "name", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "tags", + "cli_name": "tags", + "required": false, + "value_type": "string", + "repeatable": true + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": true + }, + { + "id": "projects.delete", + "command": ["projects", "delete"], + "method": "DELETE", + "path": "/api/v1/projects/{pid}", + "summary": "Delete a project", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "projects.get" + }, + { + "id": "projects.get", + "command": ["projects", "get"], + "method": "GET", + "path": "/api/v1/projects/{pid}", + "summary": "Get a project", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.list", + "command": ["projects", "list"], + "method": "GET", + "path": "/api/v1/projects", + "summary": "List all projects", + "parameters": [ + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.members.add", + "command": ["projects", "members", "add"], + "method": "POST", + "path": "/api/v1/projects/{pid}/members", + "summary": "Add a project member", + "description": "Add a member by user id or email. A known email resolves to its user id; an unknown email becomes a pending invite that grants access when that person first signs in.", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "email", + "cli_name": "email", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "role", + "cli_name": "role", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "user_id", + "cli_name": "user-id", + "required": false, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.members.list", + "command": ["projects", "members", "list"], + "method": "GET", + "path": "/api/v1/projects/{pid}/members", + "summary": "List project members", + "description": "Pending email invites are visible only to project managers (plus the invitee themself); other callers see the id-keyed rows only.", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.members.remove", + "command": ["projects", "members", "remove"], + "method": "DELETE", + "path": "/api/v1/projects/{pid}/members/{uid}", + "summary": "Remove a project member", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "uid", + "cli_name": "uid", + "in": "path", + "required": true, + "description": "The member's user id, or the (URL-encoded) email of a pending invite", + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.members.update", + "command": ["projects", "members", "update"], + "method": "PUT", + "path": "/api/v1/projects/{pid}/members/{uid}", + "summary": "Change a member's role", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "uid", + "cli_name": "uid", + "in": "path", + "required": true, + "description": "The member's user id, or the (URL-encoded) email of a pending invite", + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "role", + "cli_name": "role", + "required": true, + "value_type": "object", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "projects.update", + "command": ["projects", "update"], + "method": "PATCH", + "path": "/api/v1/projects/{pid}", + "summary": "Update a project", + "parameters": [ + { + "name": "if-match", + "cli_name": "if-match", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "description", + "cli_name": "description", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "federation", + "cli_name": "federation", + "required": false, + "value_type": "object", + "repeatable": false + }, + { + "name": "name", + "cli_name": "name", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "tags", + "cli_name": "tags", + "required": false, + "value_type": "string", + "repeatable": true + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": true, + "accepts_idempotency_key": false, + "preflight_operation_id": "projects.get" + }, + { + "id": "sessions.create", + "command": ["sessions", "create"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/sessions", + "summary": "Create a session and provision a sandbox", + "description": "Create or reuse a notebook sandbox. Edit-session reuse follows the configured editor sandbox-sharing policy. App-session reuse is shared per notebook.", + "parameters": [ + { + "name": "idempotency-key", + "cli_name": "idempotency-key", + "in": "header", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": false, + "properties": [ + { + "name": "compute_profile", + "cli_name": "compute-profile", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "edit_intent", + "cli_name": "edit-intent", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "mode", + "cli_name": "mode", + "required": false, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": true + }, + { + "id": "sessions.editor.get", + "command": ["sessions", "editor", "get"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/editor-session", + "summary": "Inspect persistent editor ownership", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "sessions.editor.takeover", + "command": ["sessions", "editor", "takeover"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/editor-session/takeover", + "summary": "Gracefully take over an exclusive editor session", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "body": { + "required": true, + "properties": [ + { + "name": "acknowledge_disruption", + "cli_name": "acknowledge-disruption", + "required": true, + "value_type": "boolean", + "repeatable": false + }, + { + "name": "expected_activity", + "cli_name": "expected-activity", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "expected_holder_session_id", + "cli_name": "expected-holder-session-id", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "takeover_id", + "cli_name": "takeover-id", + "required": true, + "value_type": "string", + "repeatable": false + } + ] + }, + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "sessions.get", + "command": ["sessions", "get"], + "method": "GET", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/sessions/{sid}", + "summary": "Get a session (status + kernel URL)", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "sid", + "cli_name": "sid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "sessions.heartbeat", + "command": ["sessions", "heartbeat"], + "method": "POST", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/sessions/{sid}/heartbeat", + "summary": "Update session heartbeat", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "sid", + "cli_name": "sid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "sessions.list", + "command": ["sessions", "list"], + "method": "GET", + "path": "/api/v1/projects/{pid}/sessions", + "summary": "List active sessions for a project", + "parameters": [ + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "cursor", + "cli_name": "cursor", + "in": "query", + "required": false, + "value_type": "string", + "repeatable": false + }, + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + } + ], + "destructive": false, + "paginated": true, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "sessions.terminate", + "command": ["sessions", "terminate"], + "method": "DELETE", + "path": "/api/v1/projects/{pid}/notebooks/{nid}/sessions/{sid}", + "summary": "Terminate a session and destroy sandbox", + "parameters": [ + { + "name": "nid", + "cli_name": "nid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "pid", + "cli_name": "pid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + }, + { + "name": "sid", + "cli_name": "sid", + "in": "path", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": true, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "users.resolve", + "command": ["users", "resolve"], + "method": "GET", + "path": "/api/v1/users", + "summary": "Resolve user ids to display identities", + "description": "Batch-resolve opaque user ids (the auth `sub` stored as a notebook `author` or session `user_id`) into `{ id, email, name, picture_url }`. Ids with no recorded identity are omitted from the result map.", + "parameters": [ + { + "name": "ids", + "cli_name": "ids", + "in": "query", + "required": false, + "description": "Comma-separated user ids.", + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "users.search", + "command": ["users", "search"], + "method": "GET", + "path": "/api/v1/users/search", + "summary": "Search the user directory", + "description": "Case-insensitive substring search over email, name, and id, for the add-member picker. Only users who have signed in at least once are in the directory. Under MARIMOHUB_DEFAULT_ROLE=none the caller must own or belong to at least one project — a signed-in account with no involvement cannot enumerate the directory; with a default role set, every authenticated user may search.", + "parameters": [ + { + "name": "limit", + "cli_name": "limit", + "in": "query", + "required": false, + "value_type": "integer", + "repeatable": false + }, + { + "name": "q", + "cli_name": "q", + "in": "query", + "required": true, + "value_type": "string", + "repeatable": false + } + ], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + }, + { + "id": "version", + "command": ["version"], + "method": "GET", + "path": "/api/v1/version", + "summary": "Get the deployment version", + "description": "Just the version string. The rest of the build/runtime identity (image, replica, backends, …) is super-admin material on `GET /api/v1/admin/config`.", + "parameters": [], + "destructive": false, + "paginated": false, + "response_kind": "json", + "session_only": false, + "accepts_if_match": false, + "accepts_idempotency_key": false + } + ] +} diff --git a/apps/cli/pyproject.toml b/apps/cli/pyproject.toml new file mode 100644 index 00000000..492cdd6e --- /dev/null +++ b/apps/cli/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["maturin>=1.8,<2"] +build-backend = "maturin" + +[project] +name = "marimohub-cli" +dynamic = ["version"] +description = "Fast, cross-platform CLI for marimohub" +requires-python = ">=3.9" +license = "Apache-2.0" + +[tool.maturin] +bindings = "bin" diff --git a/apps/cli/rust-toolchain.toml b/apps/cli/rust-toolchain.toml new file mode 100644 index 00000000..079e4082 --- /dev/null +++ b/apps/cli/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.88.0" +profile = "minimal" +components = ["clippy", "rustfmt"] diff --git a/apps/cli/src/cli.rs b/apps/cli/src/cli.rs new file mode 100644 index 00000000..7aaea982 --- /dev/null +++ b/apps/cli/src/cli.rs @@ -0,0 +1,295 @@ +use std::collections::BTreeMap; + +use clap::{value_parser, Arg, ArgAction, Command, ValueHint}; + +use crate::manifest::{BodyProperty, Manifest, Operation, Parameter, ParameterLocation}; + +#[derive(Default)] +struct Node<'a> { + operation: Option<&'a Operation>, + children: BTreeMap<&'a str, Node<'a>>, +} + +fn add_operation<'a>(node: &mut Node<'a>, operation: &'a Operation, depth: usize) { + if depth == operation.command.len() { + node.operation = Some(operation); + return; + } + let child = node.children.entry(&operation.command[depth]).or_default(); + add_operation(child, operation, depth + 1); +} + +fn parameter_arg(parameter: &Parameter) -> Option { + let lower = parameter.name.to_ascii_lowercase(); + if parameter.location == ParameterLocation::Header + && (lower == "if-match" || lower == "idempotency-key") + { + return None; + } + let id = format!("parameter:{}", parameter.name); + let mut arg = Arg::new(id) + .long(parameter.cli_name.clone()) + .value_name(parameter.value_type.to_ascii_uppercase()) + .required(parameter.required) + .action(if parameter.repeatable { + ArgAction::Append + } else { + ArgAction::Set + }); + if let Some(description) = ¶meter.description { + arg = arg.help(description.clone()); + } + Some(arg) +} + +fn body_arg(property: &BodyProperty) -> Arg { + let id = format!("body:{}", property.name); + let mut arg = Arg::new(id) + .long(property.cli_name.clone()) + .value_name(property.value_type.to_ascii_uppercase()) + .action(if property.repeatable { + ArgAction::Append + } else { + ArgAction::Set + }); + if let Some(description) = &property.description { + arg = arg.help(description.clone()); + } + arg +} + +fn operation_args(mut command: Command, operation: &Operation) -> Command { + for parameter in &operation.parameters { + if let Some(arg) = parameter_arg(parameter) { + command = command.arg(arg); + } + } + if let Some(body) = &operation.body { + command = command.arg( + Arg::new("raw-body") + .long("body") + .value_name("JSON|@FILE|-") + .value_hint(ValueHint::FilePath) + .help("Complete JSON request body; @FILE reads a file and - reads stdin"), + ); + for property in &body.properties { + command = command.arg(body_arg(property)); + } + } + if operation.accepts_if_match { + command = command + .arg( + Arg::new("if-match") + .long("if-match") + .value_name("ETAG") + .help("Use this ETag instead of fetching the current resource"), + ) + .arg( + Arg::new("no-if-match") + .long("no-if-match") + .action(ArgAction::SetTrue) + .conflicts_with("if-match") + .help("Skip optimistic concurrency protection"), + ); + } + if operation.accepts_idempotency_key { + command = command.arg( + Arg::new("idempotency-key") + .long("idempotency-key") + .value_name("KEY") + .help("Override the generated idempotency key"), + ); + } + if operation.destructive { + command = command.arg( + Arg::new("yes") + .short('y') + .long("yes") + .action(ArgAction::SetTrue) + .help("Confirm the destructive operation"), + ); + } + if operation.paginated { + command = command.arg( + Arg::new("all") + .long("all") + .action(ArgAction::SetTrue) + .help("Fetch every page"), + ); + } + command +} + +fn command_from_node(name: &str, node: &Node<'_>) -> Command { + let mut command = Command::new(name.to_owned()); + if let Some(operation) = node.operation { + command = command.about(operation.summary.clone()); + if let Some(description) = &operation.description { + command = command.long_about(description.clone()); + } + command = operation_args(command, operation); + } + for (child_name, child) in &node.children { + command = command.subcommand(command_from_node(child_name, child)); + } + command +} + +fn profile_command() -> Command { + Command::new("profile") + .about("Manage server profiles and native credentials") + .subcommand_required(true) + .subcommand(Command::new("list").about("List profiles")) + .subcommand( + Command::new("set") + .about("Create or update a profile") + .arg(Arg::new("name").required(true)) + .arg( + Arg::new("base-url") + .long("base-url") + .required(true) + .value_name("URL"), + ) + .arg( + Arg::new("token-stdin") + .long("token-stdin") + .action(ArgAction::SetTrue) + .conflicts_with_all(["token", "token-file"]) + .help("Read a token from stdin and store it in the OS credential store"), + ), + ) + .subcommand( + Command::new("use") + .about("Select the default profile") + .arg(Arg::new("name").required(true)), + ) + .subcommand( + Command::new("remove") + .about("Remove a profile and its stored credential") + .arg(Arg::new("name").required(true)), + ) +} + +fn login_command() -> Command { + Command::new("login") + .about("Validate and store a token for the selected profile") + .arg( + Arg::new("token-stdin") + .long("token-stdin") + .action(ArgAction::SetTrue) + .conflicts_with_all(["token", "token-file"]) + .help("Read the token from stdin instead of an argument or file"), + ) +} + +pub fn build(manifest: &Manifest) -> Command { + let mut root = Node::default(); + for operation in &manifest.operations { + add_operation(&mut root, operation, 0); + } + + let mut command = Command::new("mohub") + .version(env!("CARGO_PKG_VERSION")) + .about("Command-line client for marimohub") + .subcommand_required(true) + .arg_required_else_help(true) + .arg( + Arg::new("base-url") + .long("base-url") + .global(true) + .env("MARIMOHUB_URL") + .value_name("URL"), + ) + .arg( + Arg::new("profile-name") + .long("profile") + .global(true) + .env("MARIMOHUB_PROFILE") + .value_name("NAME"), + ) + .arg( + Arg::new("token") + .long("token") + .global(true) + .env("MARIMOHUB_TOKEN") + .hide_env_values(true) + .conflicts_with("token-file") + .value_name("TOKEN"), + ) + .arg( + Arg::new("token-file") + .long("token-file") + .global(true) + .env("MARIMOHUB_TOKEN_FILE") + .conflicts_with("token") + .value_hint(ValueHint::FilePath) + .value_name("PATH"), + ) + .arg( + Arg::new("timeout") + .long("timeout") + .global(true) + .env("MARIMOHUB_TIMEOUT") + .default_value("30") + .value_parser(value_parser!(u64)) + .value_name("SECONDS"), + ) + .arg( + Arg::new("output") + .long("output") + .short('o') + .global(true) + .default_value("json") + .value_parser(["json", "jsonl", "raw", "table", "csv"]), + ) + .arg( + Arg::new("raw-envelope") + .long("raw-envelope") + .global(true) + .action(ArgAction::SetTrue) + .help("Print the complete API response envelope"), + ) + .arg( + Arg::new("no-update-check") + .long("no-update-check") + .global(true) + .env("MARIMOHUB_NO_UPDATE_CHECK") + .action(ArgAction::SetTrue) + .help("Do not check GitHub Releases for a newer mohub version"), + ) + .subcommand(profile_command()) + .subcommand(login_command()) + .subcommand( + Command::new("status").about("Validate authentication for the selected profile"), + ) + .subcommand( + Command::new("logout").about("Remove the stored token for the selected profile"), + ) + .subcommand( + Command::new("completions") + .about("Generate shell completions") + .arg(Arg::new("shell").required(true).value_parser([ + "bash", + "elvish", + "fish", + "nushell", + "powershell", + "zsh", + ])), + ); + + for (name, node) in &root.children { + command = command.subcommand(command_from_node(name, node)); + } + command +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn generated_command_tree_is_valid() { + build(&crate::manifest::load()).debug_assert(); + } +} diff --git a/apps/cli/src/client.rs b/apps/cli/src/client.rs new file mode 100644 index 00000000..917d347b --- /dev/null +++ b/apps/cli/src/client.rs @@ -0,0 +1,883 @@ +use std::collections::{BTreeMap, HashSet}; +use std::fs; +use std::io::{self, IsTerminal, Read, Write}; +use std::thread; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use clap::ArgMatches; +use indicatif::{ProgressBar, ProgressStyle}; +use inquire::Confirm; +use secrecy::{ExposeSecret, SecretString}; +use serde_json::{Map, Value}; +use tabled::{builder::Builder, settings::Style}; +use url::Url; + +use crate::manifest::{ + BodyProperty, Manifest, Operation, Parameter, ParameterLocation, ResponseKind, +}; +use crate::Error; + +struct HttpResponse { + status: u16, + headers: BTreeMap, + body: Vec, +} + +pub struct Runtime<'a> { + pub base_url: &'a str, + pub token: Option<&'a SecretString>, + pub timeout: Duration, + pub output: &'a str, + pub raw_envelope: bool, +} + +fn values(matches: &ArgMatches, id: &str, repeatable: bool) -> Vec { + if repeatable { + matches + .get_many::(id) + .map(|items| items.cloned().collect()) + .unwrap_or_default() + } else { + matches.get_one::(id).cloned().into_iter().collect() + } +} + +fn parameter_values(matches: &ArgMatches, parameter: &Parameter) -> Vec { + values( + matches, + &format!("parameter:{}", parameter.name), + parameter.repeatable, + ) +} + +fn body_values(matches: &ArgMatches, property: &BodyProperty) -> Vec { + values( + matches, + &format!("body:{}", property.name), + property.repeatable, + ) +} + +fn parse_scalar(value: &str, value_type: &str) -> Result { + match value_type { + "boolean" => value + .parse::() + .map(Value::Bool) + .map_err(|_| Error::Usage(format!("expected a boolean, got {value:?}"))), + "integer" => value + .parse::() + .map(Into::into) + .map_err(|_| Error::Usage(format!("expected an integer, got {value:?}"))), + "number" => value + .parse::() + .map_err(|_| Error::Usage(format!("expected a number, got {value:?}"))) + .and_then(|number| { + serde_json::Number::from_f64(number) + .map(Value::Number) + .ok_or_else(|| Error::Usage("number must be finite".into())) + }), + "object" => Ok(serde_json::from_str(value)?), + _ => Ok(Value::String(value.to_owned())), + } +} + +fn read_raw_body(source: &str) -> Result, Error> { + let bytes = if source == "-" { + let mut bytes = Vec::new(); + io::stdin().read_to_end(&mut bytes)?; + bytes + } else if let Some(path) = source.strip_prefix('@') { + fs::read(path)? + } else { + source.as_bytes().to_vec() + }; + serde_json::from_slice::(&bytes)?; + Ok(bytes) +} + +fn request_body(operation: &Operation, matches: &ArgMatches) -> Result>, Error> { + let Some(body) = &operation.body else { + return Ok(None); + }; + if let Some(source) = matches.get_one::("raw-body") { + let has_typed = body + .properties + .iter() + .any(|property| !body_values(matches, property).is_empty()); + if has_typed { + return Err(Error::Usage( + "--body cannot be combined with typed request-body flags".into(), + )); + } + return read_raw_body(source).map(Some); + } + + let mut document = Map::new(); + for property in &body.properties { + let supplied = body_values(matches, property); + if property.required && supplied.is_empty() { + return Err(Error::Usage(format!( + "--{} is required unless --body is supplied", + property.cli_name + ))); + } + if supplied.is_empty() { + continue; + } + let value = if property.repeatable { + Value::Array( + supplied + .iter() + .map(|value| parse_scalar(value, &property.value_type)) + .collect::>()?, + ) + } else { + parse_scalar(&supplied[0], &property.value_type)? + }; + document.insert(property.name.clone(), value); + } + if document.is_empty() && body.required { + return Err(Error::Usage("this operation requires a JSON body".into())); + } + if document.is_empty() { + Ok(None) + } else { + Ok(Some(serde_json::to_vec(&document)?)) + } +} + +fn build_url( + runtime: &Runtime<'_>, + operation: &Operation, + matches: &ArgMatches, + cursor_override: Option<&str>, +) -> Result { + let mut path = operation.path.clone(); + for parameter in operation + .parameters + .iter() + .filter(|parameter| parameter.location == ParameterLocation::Path) + { + let supplied = parameter_values(matches, parameter); + let value = supplied + .first() + .ok_or_else(|| Error::Usage(format!("missing --{}", parameter.cli_name)))?; + path = path.replace( + &format!("{{{}}}", parameter.name), + &urlencoding::encode(value), + ); + } + let mut url = Url::parse(&format!( + "{}{}", + runtime.base_url.trim_end_matches('/'), + path + ))?; + { + let mut query = url.query_pairs_mut(); + for parameter in operation + .parameters + .iter() + .filter(|parameter| parameter.location == ParameterLocation::Query) + { + if parameter.name == "cursor" { + if let Some(cursor) = cursor_override { + query.append_pair(¶meter.name, cursor); + continue; + } + } + for value in parameter_values(matches, parameter) { + query.append_pair(¶meter.name, &value); + } + } + } + Ok(url) +} + +fn request_headers( + operation: &Operation, + matches: &ArgMatches, + if_match: Option<&str>, + idempotency_key: Option<&str>, +) -> BTreeMap { + let mut headers = BTreeMap::new(); + for parameter in operation + .parameters + .iter() + .filter(|parameter| parameter.location == ParameterLocation::Header) + { + let lower = parameter.name.to_ascii_lowercase(); + if lower == "if-match" || lower == "idempotency-key" { + continue; + } + if let Some(value) = parameter_values(matches, parameter).first() { + headers.insert(parameter.name.clone(), value.clone()); + } + } + if let Some(value) = if_match { + headers.insert("If-Match".into(), value.into()); + } + if let Some(value) = idempotency_key { + headers.insert("Idempotency-Key".into(), value.into()); + } + headers +} + +fn send_once( + agent: &ureq::Agent, + runtime: &Runtime<'_>, + method: &str, + url: &Url, + headers: &BTreeMap, + body: Option<&[u8]>, +) -> Result { + let mut request = agent + .request(method, url.as_str()) + .set("Accept", "application/json"); + if let Some(token) = runtime.token { + request = request.set( + "Authorization", + &format!("Bearer {}", token.expose_secret()), + ); + } + for (name, value) in headers { + request = request.set(name, value); + } + let result = match body { + Some(bytes) => request + .set("Content-Type", "application/json") + .send_bytes(bytes), + None => request.call(), + }; + let response = match result { + Ok(response) => response, + Err(ureq::Error::Status(_, response)) => response, + Err(error) => return Err(error.into()), + }; + let status = response.status(); + let mut response_headers = BTreeMap::new(); + for name in response.headers_names() { + if let Some(value) = response.header(&name) { + response_headers.insert(name.to_ascii_lowercase(), value.to_owned()); + } + } + let mut response_body = Vec::new(); + response.into_reader().read_to_end(&mut response_body)?; + Ok(HttpResponse { + status, + headers: response_headers, + body: response_body, + }) +} + +fn send( + agent: &ureq::Agent, + runtime: &Runtime<'_>, + operation: &Operation, + url: &Url, + headers: &BTreeMap, + body: Option<&[u8]>, +) -> Result { + let retryable = operation.method == "GET" + || operation.method == "HEAD" + || headers.contains_key("Idempotency-Key"); + for attempt in 0..3 { + match send_once(agent, runtime, &operation.method, url, headers, body) { + Ok(response) + if retryable && attempt < 2 && matches!(response.status, 500 | 502 | 503 | 504) => { + } + Ok(response) => return Ok(response), + Err(error) if retryable && attempt < 2 => { + if !matches!(error, Error::Transport(_)) { + return Err(error); + } + } + Err(error) => return Err(error), + } + thread::sleep(Duration::from_millis(150 * (1 << attempt))); + } + unreachable!() +} + +fn generated_idempotency_key() -> String { + let nanos = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_nanos(); + format!("mohub-{}-{nanos}", std::process::id()) +} + +fn confirm(operation: &Operation, matches: &ArgMatches) -> Result<(), Error> { + if !operation.destructive || matches.get_flag("yes") { + return Ok(()); + } + if !io::stdin().is_terminal() { + return Err(Error::Usage(format!( + "{} is destructive; pass --yes in non-interactive environments", + operation.id + ))); + } + match Confirm::new(&format!("Run destructive operation {}?", operation.id)) + .with_default(false) + .prompt() + { + Ok(true) => Ok(()), + Ok(false) + | Err(inquire::InquireError::OperationCanceled) + | Err(inquire::InquireError::OperationInterrupted) => Err(Error::Cancelled), + Err(error) => Err(Error::Prompt(error.to_string())), + } +} + +fn ensure_success(response: &HttpResponse) -> Result { + let parsed: Value = serde_json::from_slice(&response.body).map_err(|error| { + Error::Http(format!( + "server returned HTTP {} with an invalid JSON response: {error}", + response.status + )) + })?; + if !(200..300).contains(&response.status) { + let code = parsed.pointer("/error/code").and_then(Value::as_str); + let message = parsed.pointer("/error/message").and_then(Value::as_str); + return Err(Error::Http(match (code, message) { + (Some(code), Some(message)) => format!("HTTP {} {code}: {message}", response.status), + _ => format!("HTTP {}: {}", response.status, parsed), + })); + } + Ok(parsed) +} + +fn preflight_etag( + manifest: &Manifest, + agent: &ureq::Agent, + runtime: &Runtime<'_>, + operation: &Operation, + matches: &ArgMatches, +) -> Result, Error> { + if !operation.accepts_if_match || matches.get_flag("no-if-match") { + return Ok(None); + } + if let Some(value) = matches.get_one::("if-match") { + return Ok(Some(value.clone())); + } + let preflight_id = operation.preflight_operation_id.as_ref().ok_or_else(|| { + Error::Usage(format!( + "{} needs --if-match because no preflight GET is available", + operation.id + )) + })?; + let preflight = manifest + .operations + .iter() + .find(|candidate| &candidate.id == preflight_id) + .ok_or_else(|| Error::Manifest(format!("missing preflight operation {preflight_id}")))?; + let url = build_url(runtime, preflight, matches, None)?; + let response = send(agent, runtime, preflight, &url, &BTreeMap::new(), None)?; + ensure_success(&response)?; + response + .headers + .get("etag") + .cloned() + .map(Some) + .ok_or_else(|| Error::Http("preflight GET did not return an ETag".into())) +} + +fn write_json_to(writer: &mut impl Write, value: &Value, mode: &str) -> Result<(), Error> { + match mode { + "json" => writeln!(writer, "{}", serde_json::to_string_pretty(value)?)?, + "raw" => match value { + Value::String(value) => writeln!(writer, "{value}")?, + _ => writeln!(writer, "{}", serde_json::to_string(value)?)?, + }, + "jsonl" => { + let values = value + .as_array() + .or_else(|| value.get("items").and_then(Value::as_array)) + .map(Vec::as_slice) + .unwrap_or(std::slice::from_ref(value)); + for item in values { + writeln!(writer, "{}", serde_json::to_string(item)?)?; + } + } + "table" => write_table(writer, value)?, + "csv" => write_csv(writer, value)?, + _ => unreachable!(), + } + Ok(()) +} + +fn is_broken_pipe(error: &Error) -> bool { + match error { + Error::Io(error) => error.kind() == io::ErrorKind::BrokenPipe, + Error::Csv(error) => { + matches!(error.kind(), csv::ErrorKind::Io(error) if error.kind() == io::ErrorKind::BrokenPipe) + } + _ => false, + } +} + +fn normalize_stdout_result(result: Result<(), Error>) -> Result<(), Error> { + match result { + Err(error) if is_broken_pipe(&error) => Ok(()), + result => result, + } +} + +fn write_json(value: &Value, mode: &str) -> Result<(), Error> { + let stdout = io::stdout(); + let mut writer = stdout.lock(); + normalize_stdout_result(write_json_to(&mut writer, value, mode)) +} + +fn output_rows(value: &Value) -> Vec<&Value> { + value + .as_array() + .map(Vec::as_slice) + .or_else(|| { + value + .get("items") + .and_then(Value::as_array) + .map(Vec::as_slice) + }) + .unwrap_or(std::slice::from_ref(value)) + .iter() + .collect() +} + +fn cell(value: Option<&Value>) -> String { + match value { + None | Some(Value::Null) => String::new(), + Some(Value::String(value)) => value.clone(), + Some(value) => serde_json::to_string(value).unwrap_or_default(), + } +} + +fn columns(rows: &[&Value]) -> Vec { + let mut columns = Vec::new(); + for row in rows { + if let Some(object) = row.as_object() { + for name in object.keys() { + if !columns.contains(name) { + columns.push(name.clone()); + } + } + } + } + columns +} + +fn write_table(writer: &mut impl Write, value: &Value) -> Result<(), Error> { + let rows = output_rows(value); + let columns = columns(&rows); + if columns.is_empty() { + writeln!(writer, "{}", cell(Some(value)))?; + return Ok(()); + } + let mut builder = Builder::default(); + builder.push_record(columns.iter().map(|column| column.to_ascii_uppercase())); + for row in rows { + builder.push_record( + columns + .iter() + .map(|column| cell(row.get(column))) + .collect::>(), + ); + } + let mut table = builder.build(); + table.with(Style::rounded()); + writeln!(writer, "{table}")?; + Ok(()) +} + +fn write_csv(writer: &mut impl Write, value: &Value) -> Result<(), Error> { + let rows = output_rows(value); + let columns = columns(&rows); + if columns.is_empty() { + return Err(Error::Usage( + "--output csv requires an object or an array of objects".into(), + )); + } + let mut writer = csv::Writer::from_writer(writer); + writer.write_record(&columns)?; + for row in rows { + writer.write_record(columns.iter().map(|column| cell(row.get(column))))?; + } + writer.flush()?; + Ok(()) +} + +fn pagination_spinner() -> ProgressBar { + let progress = ProgressBar::new_spinner(); + let template = if std::env::var_os("NO_COLOR").is_some() { + "{spinner} Fetching pages ({pos} items)" + } else { + "{spinner:.cyan} Fetching pages ({pos} items)" + }; + progress.set_style(ProgressStyle::with_template(template).expect("valid progress template")); + progress.enable_steady_tick(Duration::from_millis(100)); + progress +} + +pub fn current_user(manifest: &Manifest, runtime: &Runtime<'_>) -> Result { + let agent = ureq::AgentBuilder::new().timeout(runtime.timeout).build(); + let operation = manifest + .operations + .iter() + .find(|operation| operation.id == "me") + .ok_or_else(|| Error::Manifest("missing me operation".into()))?; + let url = Url::parse(&format!( + "{}{}", + runtime.base_url.trim_end_matches('/'), + operation.path + ))?; + let response = send(&agent, runtime, operation, &url, &BTreeMap::new(), None)?; + let envelope = ensure_success(&response)?; + envelope + .get("data") + .cloned() + .ok_or_else(|| Error::Http("response envelope has no data".into())) +} + +pub fn execute( + manifest: &Manifest, + runtime: &Runtime<'_>, + operation: &Operation, + matches: &ArgMatches, +) -> Result<(), Error> { + if operation.session_only { + return Err(Error::Usage(format!( + "{} requires a browser session, which the CLI does not support", + operation.id + ))); + } + let agent = ureq::AgentBuilder::new().timeout(runtime.timeout).build(); + confirm(operation, matches)?; + let body = request_body(operation, matches)?; + let if_match = preflight_etag(manifest, &agent, runtime, operation, matches)?; + let generated_key = operation + .accepts_idempotency_key + .then(generated_idempotency_key); + let idempotency_key = matches + .try_get_one::("idempotency-key") + .ok() + .flatten() + .map(String::as_str) + .or(generated_key.as_deref()); + let headers = request_headers(operation, matches, if_match.as_deref(), idempotency_key); + + if operation.paginated && matches.get_flag("all") { + if runtime.raw_envelope { + return Err(Error::Usage( + "--raw-envelope cannot be combined with --all".into(), + )); + } + let mut cursor: Option = None; + let mut seen_cursors = HashSet::new(); + let mut items = Vec::new(); + let progress = pagination_spinner(); + loop { + let url = build_url(runtime, operation, matches, cursor.as_deref())?; + let response = send(&agent, runtime, operation, &url, &headers, body.as_deref())?; + let envelope = ensure_success(&response)?; + let data = envelope + .get("data") + .ok_or_else(|| Error::Http("response envelope has no data".into()))?; + let page_items = data + .get("items") + .and_then(Value::as_array) + .ok_or_else(|| Error::Http("paginated response has no items array".into()))?; + items.extend(page_items.iter().cloned()); + progress.set_position(items.len() as u64); + cursor = data + .get("next_cursor") + .and_then(Value::as_str) + .map(str::to_owned); + match cursor.as_ref() { + None => break, + Some(cursor) if !seen_cursors.insert(cursor.clone()) => { + return Err(Error::Http("server repeated a pagination cursor".into())); + } + Some(_) => {} + } + } + progress.finish_and_clear(); + return write_json(&Value::Array(items), runtime.output); + } + + let url = build_url(runtime, operation, matches, None)?; + let response = send(&agent, runtime, operation, &url, &headers, body.as_deref())?; + if operation.response_kind == ResponseKind::Raw && (200..300).contains(&response.status) { + let stdout = io::stdout(); + let mut writer = stdout.lock(); + return normalize_stdout_result(writer.write_all(&response.body).map_err(Error::from)); + } + let envelope = ensure_success(&response)?; + let value = if runtime.raw_envelope { + &envelope + } else { + envelope.get("data").unwrap_or(&envelope) + }; + write_json(value, runtime.output) +} + +#[cfg(test)] +mod tests { + use std::io::{self, BufRead, BufReader}; + use std::net::TcpListener; + + use super::*; + + struct BrokenPipeWriter; + + impl Write for BrokenPipeWriter { + fn write(&mut self, _buffer: &[u8]) -> io::Result { + Err(io::Error::new(io::ErrorKind::BrokenPipe, "closed pipe")) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } + } + + fn operation<'a>(manifest: &'a Manifest, id: &str) -> &'a Operation { + manifest + .operations + .iter() + .find(|operation| operation.id == id) + .expect("operation exists") + } + + fn serve_once(status: u16, body: &str) -> (String, thread::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let address = listener.local_addr().expect("test server address"); + let body = body.to_owned(); + let handle = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept request"); + let mut request = [0; 4096]; + let length = stream.read(&mut request).expect("read request"); + let request = String::from_utf8_lossy(&request[..length]); + assert!(request.starts_with("GET /api/v1/me HTTP/1.1")); + assert!(request + .to_ascii_lowercase() + .contains("authorization: bearer mhub_pat_test")); + write!( + stream, + "HTTP/1.1 {status} Test\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len() + ) + .expect("write response"); + }); + (format!("http://{address}"), handle) + } + + fn serve_twice_on_one_connection(body: &str) -> (String, thread::JoinHandle<()>) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let address = listener.local_addr().expect("test server address"); + let body = body.to_owned(); + let handle = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept request"); + let mut reader = BufReader::new(stream.try_clone().expect("clone test stream")); + for _ in 0..2 { + loop { + let mut line = String::new(); + reader.read_line(&mut line).expect("read request line"); + if line == "\r\n" { + break; + } + } + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{body}", + body.len(), + ) + .expect("write response"); + stream.flush().expect("flush response"); + } + }); + (format!("http://{address}"), handle) + } + + #[test] + fn current_user_validates_the_token_with_me() { + let (base_url, server) = serve_once( + 200, + r#"{"success":true,"data":{"id":"user-1","email":"user@example.com"}}"#, + ); + let token = SecretString::from("mhub_pat_test".to_owned()); + let runtime = Runtime { + base_url: &base_url, + token: Some(&token), + timeout: Duration::from_secs(1), + output: "json", + raw_envelope: false, + }; + + let user = current_user(&crate::manifest::load(), &runtime).expect("valid token"); + server.join().expect("test server"); + assert_eq!(user["email"], "user@example.com"); + } + + #[test] + fn current_user_rejects_an_invalid_token() { + let (base_url, server) = serve_once( + 401, + r#"{"success":false,"error":{"code":"UNAUTHORIZED","message":"Authentication required"}}"#, + ); + let token = SecretString::from("mhub_pat_test".to_owned()); + let runtime = Runtime { + base_url: &base_url, + token: Some(&token), + timeout: Duration::from_secs(1), + output: "json", + raw_envelope: false, + }; + + let error = current_user(&crate::manifest::load(), &runtime).expect_err("invalid token"); + server.join().expect("test server"); + assert!(matches!(error, Error::Http(message) if message.contains("HTTP 401 UNAUTHORIZED"))); + } + + #[test] + fn agent_reuses_a_connection_across_requests() { + let (base_url, server) = + serve_twice_on_one_connection(r#"{"success":true,"data":{"enabled":true}}"#); + let manifest = crate::manifest::load(); + let operation = operation(&manifest, "capabilities"); + let runtime = Runtime { + base_url: &base_url, + token: None, + timeout: Duration::from_secs(1), + output: "json", + raw_envelope: false, + }; + let agent = ureq::AgentBuilder::new().timeout(runtime.timeout).build(); + let url = Url::parse(&format!("{base_url}{}", operation.path)).expect("valid URL"); + + for _ in 0..2 { + let response = send(&agent, &runtime, operation, &url, &BTreeMap::new(), None) + .expect("request succeeds"); + ensure_success(&response).expect("successful response"); + } + + server.join().expect("test server"); + } + + #[test] + fn typed_flags_build_a_json_body() { + let manifest = crate::manifest::load(); + let matches = crate::cli::build(&manifest) + .try_get_matches_from([ + "mohub", + "projects", + "create", + "--name", + "Analysis", + "--description", + "Models", + "--tags", + "one", + "--tags", + "two", + ]) + .expect("valid command"); + let leaf = matches + .subcommand_matches("projects") + .and_then(|matches| matches.subcommand_matches("create")) + .expect("projects create matches"); + let body = request_body(operation(&manifest, "projects.create"), leaf) + .expect("valid body") + .expect("body present"); + let value: Value = serde_json::from_slice(&body).expect("JSON body"); + assert_eq!(value["name"], "Analysis"); + assert_eq!(value["tags"], serde_json::json!(["one", "two"])); + } + + #[test] + fn path_and_query_values_are_url_encoded() { + let manifest = crate::manifest::load(); + let matches = crate::cli::build(&manifest) + .try_get_matches_from([ + "mohub", + "users", + "search", + "--q", + "Ada Lovelace", + "--limit", + "5", + ]) + .expect("valid command"); + let leaf = matches + .subcommand_matches("users") + .and_then(|matches| matches.subcommand_matches("search")) + .expect("users search matches"); + let runtime = Runtime { + base_url: "https://hub.example.com/", + token: None, + timeout: Duration::from_secs(1), + output: "json", + raw_envelope: false, + }; + let url = build_url(&runtime, operation(&manifest, "users.search"), leaf, None) + .expect("valid URL"); + assert_eq!( + url.as_str(), + "https://hub.example.com/api/v1/users/search?limit=5&q=Ada+Lovelace" + ); + } + + #[test] + fn table_columns_follow_first_seen_order() { + let value = serde_json::json!([ + {"id": "one", "name": "First"}, + {"id": "two", "email": "two@example.com"} + ]); + let rows = output_rows(&value); + + assert_eq!(columns(&rows), ["id", "name", "email"]); + } + + #[test] + fn output_modes_return_broken_pipe_errors_without_panicking() { + let value = serde_json::json!({"id": "one"}); + + for mode in ["json", "raw", "jsonl", "table", "csv"] { + let error = write_json_to(&mut BrokenPipeWriter, &value, mode) + .expect_err("closed output should return an error"); + assert!(is_broken_pipe(&error), "unexpected {mode} error: {error}"); + assert!(normalize_stdout_result(Err(error)).is_ok()); + } + } + + #[test] + fn session_only_operations_are_rejected_before_http() { + let manifest = crate::manifest::load(); + let matches = crate::cli::build(&manifest) + .try_get_matches_from(["mohub", "auth", "tokens", "list"]) + .expect("valid command"); + let leaf = matches + .subcommand_matches("auth") + .and_then(|matches| matches.subcommand_matches("tokens")) + .and_then(|matches| matches.subcommand_matches("list")) + .expect("auth tokens list matches"); + let runtime = Runtime { + base_url: "http://127.0.0.1:9", + token: None, + timeout: Duration::from_secs(1), + output: "json", + raw_envelope: false, + }; + + let error = execute( + &manifest, + &runtime, + operation(&manifest, "auth.tokens.list"), + leaf, + ) + .expect_err("session-only operation should be rejected locally"); + + assert!(matches!( + error, + Error::Usage(message) + if message.contains("requires a browser session") + )); + } +} diff --git a/apps/cli/src/config.rs b/apps/cli/src/config.rs new file mode 100644 index 00000000..379d924e --- /dev/null +++ b/apps/cli/src/config.rs @@ -0,0 +1,642 @@ +use std::collections::BTreeMap; +#[cfg(any(target_os = "linux", test))] +use std::collections::BTreeSet; +use std::fs::{self, File, OpenOptions}; +use std::io::{self, Read, Write}; +use std::path::{Path, PathBuf}; +#[cfg(target_os = "linux")] +use std::sync::Once; + +use directories::ProjectDirs; +use fs2::FileExt; +use keyring::Entry; +use secrecy::{ExposeSecret, SecretString}; +use serde::{Deserialize, Serialize}; +use tempfile::NamedTempFile; + +use crate::Error; + +const KEYRING_SERVICE: &str = "dev.marimo.marimohub.mohub"; +#[cfg(target_os = "linux")] +static FILE_CREDENTIAL_WARNING: Once = Once::new(); + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct Config { + #[serde(default)] + pub current_profile: Option, + #[serde(default)] + pub profiles: BTreeMap, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Profile { + pub base_url: String, +} + +#[cfg(any(target_os = "linux", test))] +#[derive(Default, Deserialize, Serialize)] +struct FileCredentials { + #[serde(default)] + tokens: BTreeMap, + // File state stays authoritative so a stale keyring entry cannot resurface. + #[serde(default, skip_serializing_if = "BTreeSet::is_empty")] + deleted: BTreeSet, +} + +#[cfg(any(target_os = "linux", test))] +#[derive(Clone, Debug, PartialEq, Eq)] +enum FileCredential { + Missing, + Token(String), + Deleted, +} + +#[cfg(any(target_os = "linux", test))] +impl FileCredentials { + fn get(&self, profile: &str) -> FileCredential { + if let Some(token) = self.tokens.get(profile) { + FileCredential::Token(token.clone()) + } else if self.deleted.contains(profile) { + FileCredential::Deleted + } else { + FileCredential::Missing + } + } + + fn set(&mut self, profile: &str, credential: FileCredential) { + self.tokens.remove(profile); + self.deleted.remove(profile); + match credential { + FileCredential::Missing => {} + FileCredential::Token(token) => { + self.tokens.insert(profile.to_owned(), token); + } + FileCredential::Deleted => { + self.deleted.insert(profile.to_owned()); + } + } + } +} + +#[cfg(any(target_os = "linux", test))] +fn resolve_file_credential( + file: FileCredential, + keyring_token: impl FnOnce() -> Result, Error>, +) -> Result, Error> { + match file { + FileCredential::Token(token) => Ok(Some(SecretString::from(token))), + FileCredential::Deleted => Ok(None), + FileCredential::Missing => Ok(keyring_token()?.map(SecretString::from)), + } +} + +#[cfg(any(target_os = "linux", test))] +fn set_linux_credential( + current: FileCredential, + token: &str, + set_keyring: impl FnOnce(&str) -> keyring::Result<()>, + set_file: impl FnOnce(FileCredential) -> Result<(), Error>, + warn: impl FnOnce(&keyring::Error), +) -> Result<(), Error> { + if current != FileCredential::Missing { + return set_file(FileCredential::Token(token.to_owned())); + } + + match set_keyring(token) { + Ok(()) => Ok(()), + Err(error @ (keyring::Error::PlatformFailure(_) | keyring::Error::NoStorageAccess(_))) => { + warn(&error); + set_file(FileCredential::Token(token.to_owned())) + } + Err(error) => Err(Error::Credential(error.to_string())), + } +} + +#[cfg(any(target_os = "linux", test))] +fn delete_linux_credential( + current: FileCredential, + delete_keyring: impl FnOnce() -> keyring::Result<()>, + set_file: impl FnOnce(FileCredential) -> Result<(), Error>, + warn: impl FnOnce(&keyring::Error), +) -> Result<(), Error> { + let has_file_state = current != FileCredential::Missing; + + match delete_keyring() { + Ok(()) | Err(keyring::Error::NoEntry) => { + if has_file_state { + set_file(FileCredential::Missing) + } else { + Ok(()) + } + } + Err(error @ (keyring::Error::PlatformFailure(_) | keyring::Error::NoStorageAccess(_))) => { + warn(&error); + if current == FileCredential::Deleted { + Ok(()) + } else { + set_file(FileCredential::Deleted) + } + } + Err(error) => Err(Error::Credential(error.to_string())), + } +} + +pub fn path() -> Result { + let dirs = ProjectDirs::from("dev", "marimo", "mohub") + .ok_or_else(|| Error::Config("could not locate the user configuration directory".into()))?; + Ok(dirs.config_dir().join("config.json")) +} + +pub fn load() -> Result { + load_from(&path()?) +} + +fn load_from(path: &Path) -> Result { + match fs::read(path) { + Ok(bytes) => Ok(serde_json::from_slice(&bytes)?), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(Config::default()), + Err(error) => Err(error.into()), + } +} + +fn lock_for(path: &Path) -> Result { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let lock_path = path.with_extension("lock"); + let lock = OpenOptions::new() + .create(true) + .read(true) + .write(true) + .truncate(false) + .open(lock_path)?; + lock.lock_exclusive()?; + Ok(lock) +} + +fn atomic_write(path: &Path, bytes: &[u8], secret: bool) -> Result<(), Error> { + let parent = path + .parent() + .ok_or_else(|| Error::Config("configuration path has no parent directory".into()))?; + fs::create_dir_all(parent)?; + let mut temporary = NamedTempFile::new_in(parent)?; + #[cfg(unix)] + if secret { + use std::os::unix::fs::PermissionsExt; + temporary + .as_file() + .set_permissions(fs::Permissions::from_mode(0o600))?; + } + #[cfg(not(unix))] + let _ = secret; + temporary.write_all(bytes)?; + temporary.as_file().sync_all()?; + temporary + .persist(path) + .map_err(|error| Error::Io(error.error))?; + #[cfg(unix)] + File::open(parent)?.sync_all()?; + Ok(()) +} + +#[cfg(target_os = "linux")] +fn remove_file_if_exists(path: &Path) -> Result<(), Error> { + match fs::remove_file(path) { + Ok(()) => { + #[cfg(unix)] + if let Some(parent) = path.parent() { + File::open(parent)?.sync_all()?; + } + Ok(()) + } + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()), + Err(error) => Err(error.into()), + } +} + +fn save_to(path: &Path, config: &Config) -> Result<(), Error> { + atomic_write(path, &serde_json::to_vec_pretty(config)?, false) +} + +pub fn update(change: impl FnOnce(&mut Config) -> Result) -> Result { + let path = path()?; + update_at(&path, change) +} + +fn update_at( + path: &Path, + change: impl FnOnce(&mut Config) -> Result, +) -> Result { + let _lock = lock_for(path)?; + let mut config = load_from(path)?; + let result = change(&mut config)?; + save_to(path, &config)?; + Ok(result) +} + +pub fn remove_profile(name: &str) -> Result<(), Error> { + let path = path()?; + remove_profile_from(&path, name, || delete_token(name)) +} + +fn remove_profile_from( + path: &Path, + name: &str, + remove_credential: impl FnOnce() -> Result<(), Error>, +) -> Result<(), Error> { + let _lock = lock_for(path)?; + let mut config = load_from(path)?; + let original = config.clone(); + if config.profiles.remove(name).is_none() { + return Err(Error::Config(format!("profile {name:?} does not exist"))); + } + if config.current_profile.as_deref() == Some(name) { + config.current_profile = None; + } + save_to(path, &config)?; + if let Err(error) = remove_credential() { + return match save_to(path, &original) { + Ok(()) => Err(error), + Err(rollback) => Err(Error::Config(format!( + "credential removal failed ({error}); restoring the profile also failed: {rollback}" + ))), + }; + } + Ok(()) +} + +fn keyring(profile: &str) -> Result { + Entry::new(KEYRING_SERVICE, profile).map_err(|error| Error::Credential(error.to_string())) +} + +pub fn get_token(profile: &str) -> Result, Error> { + #[cfg(target_os = "linux")] + let _operation_lock = credential_operation_lock()?; + #[cfg(target_os = "linux")] + { + let file = get_file_credential(profile)?; + return resolve_file_credential(file, || match keyring(profile)?.get_password() { + Ok(token) => Ok(Some(token)), + Err(keyring::Error::NoEntry) => Ok(None), + Err( + error @ (keyring::Error::PlatformFailure(_) | keyring::Error::NoStorageAccess(_)), + ) => { + warn_file_credentials(&error); + Ok(None) + } + Err(error) => Err(Error::Credential(error.to_string())), + }); + } + + #[cfg(not(target_os = "linux"))] + match keyring(profile)?.get_password() { + Ok(token) => Ok(Some(SecretString::from(token))), + Err(keyring::Error::NoEntry) => Ok(None), + Err(error) => Err(Error::Credential(error.to_string())), + } +} + +pub fn read_token(mut reader: impl Read) -> Result { + let mut token = String::new(); + reader.read_to_string(&mut token)?; + let token = token.trim(); + if token.is_empty() { + return Err(Error::Config( + "credential input did not contain a token".into(), + )); + } + Ok(SecretString::from(token.to_owned())) +} + +pub fn set_token(profile: &str, token: &SecretString) -> Result<(), Error> { + #[cfg(target_os = "linux")] + { + return set_token_linux(profile, token); + } + + #[cfg(not(target_os = "linux"))] + match keyring(profile)?.set_password(token.expose_secret()) { + Ok(()) => Ok(()), + Err(error) => Err(Error::Credential(error.to_string())), + } +} + +pub fn delete_token(profile: &str) -> Result<(), Error> { + #[cfg(target_os = "linux")] + { + return delete_token_linux(profile); + } + + #[cfg(not(target_os = "linux"))] + match keyring(profile)?.delete_credential() { + Ok(()) | Err(keyring::Error::NoEntry) => Ok(()), + Err(error) => Err(Error::Credential(error.to_string())), + } +} + +#[cfg(target_os = "linux")] +fn set_token_linux(profile: &str, token: &SecretString) -> Result<(), Error> { + let _operation_lock = credential_operation_lock()?; + let current = get_file_credential(profile)?; + set_linux_credential( + current, + token.expose_secret(), + |token| Entry::new(KEYRING_SERVICE, profile)?.set_password(token), + |credential| set_file_credential(profile, credential), + warn_file_credentials, + ) +} + +#[cfg(target_os = "linux")] +fn delete_token_linux(profile: &str) -> Result<(), Error> { + let _operation_lock = credential_operation_lock()?; + let current = get_file_credential(profile)?; + delete_linux_credential( + current, + || Entry::new(KEYRING_SERVICE, profile)?.delete_credential(), + |credential| set_file_credential(profile, credential), + warn_file_credentials, + ) +} + +#[cfg(target_os = "linux")] +fn credentials_path() -> Result { + Ok(path()?.with_file_name("credentials.json")) +} + +#[cfg(target_os = "linux")] +fn credential_operation_lock() -> Result { + let path = credentials_path()?.with_file_name("credentials-operation.json"); + lock_for(&path) +} + +#[cfg(target_os = "linux")] +fn read_file_credentials(path: &Path) -> Result { + match fs::read(path) { + Ok(bytes) => Ok(serde_json::from_slice(&bytes)?), + Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(FileCredentials::default()), + Err(error) => Err(error.into()), + } +} + +#[cfg(target_os = "linux")] +fn update_file_credentials( + change: impl FnOnce(&mut FileCredentials), +) -> Result { + let path = credentials_path()?; + let _lock = lock_for(&path)?; + let mut credentials = read_file_credentials(&path)?; + change(&mut credentials); + if credentials.tokens.is_empty() && credentials.deleted.is_empty() { + remove_file_if_exists(&path)?; + } else { + atomic_write(&path, &serde_json::to_vec_pretty(&credentials)?, true)?; + } + Ok(credentials) +} + +#[cfg(target_os = "linux")] +fn get_file_credential(profile: &str) -> Result { + let path = credentials_path()?; + let _lock = lock_for(&path)?; + let credentials = read_file_credentials(&path)?; + Ok(credentials.get(profile)) +} + +#[cfg(target_os = "linux")] +fn set_file_credential(profile: &str, credential: FileCredential) -> Result<(), Error> { + update_file_credentials(|credentials| { + credentials.set(profile, credential); + })?; + Ok(()) +} + +#[cfg(target_os = "linux")] +fn warn_file_credentials(error: &keyring::Error) { + FILE_CREDENTIAL_WARNING.call_once(|| { + eprintln!( + "Warning: the system credential store is unavailable ({error}). Tokens are stored in a user-only credentials file." + ); + }); +} + +#[cfg(test)] +mod tests { + use std::cell::{Cell, RefCell}; + use std::sync::Arc; + use std::thread; + + use super::*; + + #[test] + fn token_input_is_trimmed() { + assert_eq!( + read_token(" mhub_pat_example\n".as_bytes()) + .unwrap() + .expose_secret(), + "mhub_pat_example" + ); + } + + #[test] + fn empty_token_input_is_rejected() { + assert!(matches!( + read_token(" \n".as_bytes()), + Err(Error::Config(_)) + )); + } + + #[test] + fn file_token_overrides_a_stale_keyring_value() { + let resolved = + resolve_file_credential(FileCredential::Token("current-file-token".into()), || { + Ok(Some("stale-keyring-token".into())) + }) + .unwrap() + .unwrap(); + + assert_eq!(resolved.expose_secret(), "current-file-token"); + } + + #[test] + fn deletion_marker_hides_a_stale_keyring_value() { + let mut credentials = FileCredentials::default(); + credentials.set("default", FileCredential::Deleted); + + assert_eq!(credentials.get("default"), FileCredential::Deleted); + let restored: FileCredentials = + serde_json::from_str(&serde_json::to_string(&credentials).unwrap()).unwrap(); + assert_eq!(restored.get("default"), FileCredential::Deleted); + + let resolved = resolve_file_credential(restored.get("default"), || { + Ok(Some("stale-keyring-token".into())) + }) + .unwrap(); + assert!(resolved.is_none()); + } + + #[test] + fn replacing_a_file_credential_clears_the_previous_state() { + let mut credentials = FileCredentials::default(); + credentials.set("default", FileCredential::Deleted); + credentials.set("default", FileCredential::Token("replacement".into())); + assert!(!credentials.deleted.contains("default")); + + credentials.set("default", FileCredential::Missing); + assert_eq!(credentials.get("default"), FileCredential::Missing); + } + + #[test] + fn failed_keyring_write_does_not_stage_the_new_token() { + let file_write_called = Cell::new(false); + let result = set_linux_credential( + FileCredential::Missing, + "new-token", + |_| Err(keyring::Error::Invalid("profile".into(), "invalid".into())), + |_| { + file_write_called.set(true); + Err(Error::Config("file write failed".into())) + }, + |_| {}, + ); + + assert!(matches!(result, Err(Error::Credential(_)))); + assert!(!file_write_called.get()); + } + + #[test] + fn failed_keyring_delete_does_not_stage_a_deletion() { + let file_write_called = Cell::new(false); + let result = delete_linux_credential( + FileCredential::Missing, + || Err(keyring::Error::Invalid("profile".into(), "invalid".into())), + |_| { + file_write_called.set(true); + Err(Error::Config("file write failed".into())) + }, + |_| {}, + ); + + assert!(matches!(result, Err(Error::Credential(_)))); + assert!(!file_write_called.get()); + } + + #[test] + fn deletion_marker_retries_keyring_and_clears_when_empty() { + for keyring_result in [Ok(()), Err(keyring::Error::NoEntry)] { + let keyring_delete_called = Cell::new(false); + let stored = RefCell::new(Vec::new()); + delete_linux_credential( + FileCredential::Deleted, + || { + keyring_delete_called.set(true); + keyring_result + }, + |credential| { + stored.borrow_mut().push(credential); + Ok(()) + }, + |_| {}, + ) + .unwrap(); + + assert!(keyring_delete_called.get()); + assert_eq!(stored.into_inner(), vec![FileCredential::Missing]); + } + } + + #[test] + fn file_backed_profile_stays_file_backed() { + let keyring_write_called = Cell::new(false); + let stored = RefCell::new(None); + set_linux_credential( + FileCredential::Token("old-token".into()), + "new-token", + |_| { + keyring_write_called.set(true); + Ok(()) + }, + |credential| { + stored.replace(Some(credential)); + Ok(()) + }, + |_| {}, + ) + .unwrap(); + + assert!(!keyring_write_called.get()); + assert_eq!( + stored.into_inner(), + Some(FileCredential::Token("new-token".into())) + ); + } + + #[test] + fn concurrent_updates_do_not_lose_profiles() { + let directory = tempfile::tempdir().unwrap(); + let path = Arc::new(directory.path().join("config.json")); + let threads = (0..16) + .map(|index| { + let path = Arc::clone(&path); + thread::spawn(move || { + update_at(&path, |config| { + config.profiles.insert( + format!("profile-{index}"), + Profile { + base_url: format!("https://{index}.example.com"), + }, + ); + Ok(()) + }) + .unwrap(); + }) + }) + .collect::>(); + + for thread in threads { + thread.join().unwrap(); + } + + assert_eq!(load_from(&path).unwrap().profiles.len(), 16); + } + + #[test] + fn failed_credential_removal_restores_the_profile() { + let directory = tempfile::tempdir().unwrap(); + let path = directory.path().join("config.json"); + update_at(&path, |config| { + config.current_profile = Some("default".into()); + config.profiles.insert( + "default".into(), + Profile { + base_url: "https://hub.example.com".into(), + }, + ); + Ok(()) + }) + .unwrap(); + + let credential = RefCell::new(FileCredential::Token("current-token".into())); + let result = remove_profile_from(&path, "default", || { + let current = credential.borrow().clone(); + delete_linux_credential( + current, + || Err(keyring::Error::Invalid("profile".into(), "invalid".into())), + |next| { + credential.replace(next); + Ok(()) + }, + |_| {}, + ) + }); + + assert!(matches!(result, Err(Error::Credential(_)))); + let config = load_from(&path).unwrap(); + assert_eq!(config.current_profile.as_deref(), Some("default")); + assert!(config.profiles.contains_key("default")); + assert_eq!( + credential.into_inner(), + FileCredential::Token("current-token".into()) + ); + } +} diff --git a/apps/cli/src/lib.rs b/apps/cli/src/lib.rs new file mode 100644 index 00000000..008f7b3a --- /dev/null +++ b/apps/cli/src/lib.rs @@ -0,0 +1,57 @@ +pub mod cli; +pub mod client; +pub mod config; +pub mod manifest; + +use std::io; + +use miette::Diagnostic; +use thiserror::Error; + +#[derive(Debug, Diagnostic, Error)] +pub enum Error { + #[error("{0}")] + #[diagnostic(code(mohub::usage))] + Usage(String), + #[error("configuration error: {0}")] + #[diagnostic(code(mohub::configuration))] + Config(String), + #[error("credential store error: {0}")] + #[diagnostic(code(mohub::credential_store))] + Credential(String), + #[error("manifest error: {0}")] + #[diagnostic(code(mohub::manifest))] + Manifest(String), + #[error("{0}")] + #[diagnostic(code(mohub::http))] + Http(String), + #[error("authentication failed for {server}: {reason}")] + #[diagnostic( + code(mohub::authentication), + help("Create a token in Account → API tokens, then run `mohub login --token-stdin`.") + )] + Authentication { server: String, reason: String }, + #[error("operation cancelled")] + #[diagnostic(code(mohub::cancelled))] + Cancelled, + #[error(transparent)] + Io(#[from] io::Error), + #[error(transparent)] + Json(#[from] serde_json::Error), + #[error(transparent)] + Csv(#[from] csv::Error), + #[error(transparent)] + Url(#[from] url::ParseError), + #[error("network error: {0}")] + #[diagnostic(code(mohub::network))] + Transport(String), + #[error("terminal prompt error: {0}")] + #[diagnostic(code(mohub::prompt))] + Prompt(String), +} + +impl From for Error { + fn from(error: ureq::Error) -> Self { + Self::Transport(error.to_string()) + } +} diff --git a/apps/cli/src/main.rs b/apps/cli/src/main.rs new file mode 100644 index 00000000..3061abe5 --- /dev/null +++ b/apps/cli/src/main.rs @@ -0,0 +1,585 @@ +use std::fs; +use std::io::{self, IsTerminal, Write}; +use std::path::Path; +use std::process::ExitCode; +use std::time::Duration; + +use clap::ArgMatches; +use inquire::Password; +use mohub::{cli, client, config, manifest, Error}; +use secrecy::SecretString; +use update_informer::{registry, Check}; + +fn write_stdout(arguments: std::fmt::Arguments<'_>) -> Result<(), Error> { + match io::stdout().lock().write_fmt(arguments) { + Ok(()) => Ok(()), + Err(error) if error.kind() == io::ErrorKind::BrokenPipe => Ok(()), + Err(error) => Err(error.into()), + } +} + +fn selected_command(matches: &ArgMatches) -> (Vec, &ArgMatches) { + let mut path = Vec::new(); + let mut current = matches; + while let Some((name, child)) = current.subcommand() { + path.push(name.to_owned()); + current = child; + } + (path, current) +} + +fn supplied_token(matches: &ArgMatches) -> Result, Error> { + let token_stdin = matches + .try_get_one::("token-stdin") + .ok() + .flatten() + .copied() + .unwrap_or(false); + if token_stdin { + return config::read_token(io::stdin()).map(Some); + } + if let Some(token) = matches.get_one::("token") { + let token = token.trim(); + if token.is_empty() { + return Err(Error::Config("token cannot be empty".into())); + } + return Ok(Some(SecretString::from(token.to_owned()))); + } + if let Some(path) = matches.get_one::("token-file") { + return config::read_token(fs::File::open(path)?).map(Some); + } + Ok(None) +} + +fn handle_profile(matches: &ArgMatches) -> Result<(), Error> { + match matches.subcommand() { + Some(("list", _)) => { + let config = config::load()?; + for (name, profile) in &config.profiles { + let marker = if config.current_profile.as_deref() == Some(name) { + "*" + } else { + " " + }; + write_stdout(format_args!("{marker} {name}\t{}\n", profile.base_url))?; + } + } + Some(("set", args)) => { + let token = supplied_token(args)?; + let name = args.get_one::("name").expect("required by clap"); + let base_url = args + .get_one::("base-url") + .expect("required by clap"); + let base_url = normalize_base_url(base_url)?; + config::update(|config| { + config.profiles.insert( + name.clone(), + config::Profile { + base_url: base_url.clone(), + }, + ); + if config.current_profile.is_none() { + config.current_profile = Some(name.clone()); + } + Ok(()) + })?; + if let Some(token) = token { + config::set_token(name, &token)?; + } + } + Some(("use", args)) => { + let name = args.get_one::("name").expect("required by clap"); + config::update(|config| { + if !config.profiles.contains_key(name) { + return Err(Error::Config(format!("profile {name:?} does not exist"))); + } + config.current_profile = Some(name.clone()); + Ok(()) + })?; + } + Some(("remove", args)) => { + let name = args.get_one::("name").expect("required by clap"); + config::remove_profile(name)?; + } + _ => unreachable!("profile subcommand required by clap"), + } + Ok(()) +} + +fn normalize_base_url(value: &str) -> Result { + let url = url::Url::parse(value)?; + if !matches!(url.scheme(), "http" | "https") || url.host().is_none() { + return Err(Error::Usage( + "server URL must be an absolute HTTP or HTTPS URL".into(), + )); + } + if !url.username().is_empty() || url.password().is_some() { + return Err(Error::Usage( + "server URL must not contain a username or password".into(), + )); + } + if url.query().is_some() || url.fragment().is_some() { + return Err(Error::Usage( + "server URL must not contain a query string or fragment".into(), + )); + } + Ok(url.as_str().trim_end_matches('/').to_owned()) +} + +fn server_label(value: &str) -> String { + url::Url::parse(value) + .map(|url| url.origin().ascii_serialization()) + .unwrap_or_else(|_| "the configured server".into()) +} + +fn matching_server(override_url: Option<&str>, profile_url: Option<&str>) -> Result { + match (override_url, profile_url) { + (Some(override_url), Some(profile_url)) => { + Ok(normalize_base_url(override_url)? == normalize_base_url(profile_url)?) + } + _ => Ok(true), + } +} + +fn stored_token_for_server( + profile_name: &str, + override_url: Option<&str>, + profile_url: Option<&str>, + token: Option, +) -> Result, Error> { + if token.is_some() && !matching_server(override_url, profile_url)? { + return Err(Error::Usage(format!( + "refusing to send the stored token for profile {profile_name:?} to a different server; pass --token or --token-file explicitly" + ))); + } + Ok(token) +} + +fn selected_profile(matches: &ArgMatches) -> Result<(String, String), Error> { + let config = config::load()?; + let name = matches + .get_one::("profile-name") + .cloned() + .or(config.current_profile) + .ok_or_else(|| { + Error::Config( + "no profile selected; create one with `mohub profile set NAME --base-url URL`" + .into(), + ) + })?; + let profile = config + .profiles + .get(&name) + .ok_or_else(|| Error::Config(format!("profile {name:?} does not exist")))?; + let profile_url = normalize_base_url(&profile.base_url)?; + if !matching_server( + matches.get_one::("base-url").map(String::as_str), + Some(&profile_url), + )? { + return Err(Error::Usage(format!( + "server override does not match profile {name:?}; update the profile or select a matching one" + ))); + } + Ok((name, profile_url)) +} + +fn auth_runtime<'a>( + matches: &'a ArgMatches, + base_url: &'a str, + token: &'a SecretString, +) -> client::Runtime<'a> { + client::Runtime { + base_url, + token: Some(token), + timeout: Duration::from_secs( + *matches + .get_one::("timeout") + .expect("defaulted by clap"), + ), + output: matches + .get_one::("output") + .expect("defaulted by clap"), + raw_envelope: matches.get_flag("raw-envelope"), + } +} + +fn user_label(user: &serde_json::Value) -> &str { + user.get("email") + .and_then(serde_json::Value::as_str) + .or_else(|| user.get("id").and_then(serde_json::Value::as_str)) + .unwrap_or("unknown user") +} + +fn upgrade_hint(executable: &Path) -> &'static str { + let path = executable.to_string_lossy(); + let parent = executable.parent(); + let updater = if cfg!(windows) { + "mohub-update.exe" + } else { + "mohub-update" + }; + if parent.is_some_and(|parent| parent.join(updater).is_file()) { + "Run `mohub-update` to upgrade." + } else if path.contains("/uv/tools/") || path.contains("\\uv\\tools\\") { + "Run `uv tool upgrade marimohub-cli` to upgrade." + } else if path.contains("/Cellar/") || path.contains("\\Homebrew\\") { + "Run `brew upgrade mohub` to upgrade." + } else if path.contains("node_modules") { + "Run `npm update -g @marimo-team/mohub` to upgrade." + } else { + "Use your package manager to upgrade, or download the release from GitHub." + } +} + +fn check_for_update(matches: &ArgMatches) { + if matches.get_flag("no-update-check") || !io::stderr().is_terminal() { + return; + } + let informer = update_informer::new( + registry::GitHub, + "marimo-team/marimohub", + env!("CARGO_PKG_VERSION"), + ) + .timeout(Duration::from_secs(1)); + if let Ok(Some(version)) = informer.check_version() { + let executable = std::env::current_exe().unwrap_or_default(); + eprintln!( + "mohub {version} is available. {}", + upgrade_hint(&executable) + ); + } +} + +fn handle_login( + manifest: &manifest::Manifest, + matches: &ArgMatches, + leaf: &ArgMatches, +) -> Result<(), Error> { + let (profile, base_url) = selected_profile(matches)?; + let token = match supplied_token(leaf)? { + Some(token) => token, + None if io::stdin().is_terminal() => SecretString::from( + Password::new("Token:") + .without_confirmation() + .prompt() + .map_err(|error| Error::Prompt(error.to_string()))?, + ), + None => { + return Err(Error::Usage( + "no token supplied; pipe one to `mohub login --token-stdin` or use --token-file" + .into(), + )); + } + }; + let user = client::current_user(manifest, &auth_runtime(matches, &base_url, &token)).map_err( + |error| Error::Authentication { + server: server_label(&base_url), + reason: error.to_string(), + }, + )?; + config::set_token(&profile, &token)?; + write_stdout(format_args!( + "Logged in to {base_url} as {} (profile {profile}).\n", + user_label(&user), + ))?; + Ok(()) +} + +fn handle_status(manifest: &manifest::Manifest, matches: &ArgMatches) -> Result<(), Error> { + let (profile, base_url) = selected_profile(matches)?; + let supplied = supplied_token(matches)?; + let token = match supplied { + Some(token) => token, + None => config::get_token(&profile)?.ok_or_else(|| { + Error::Config(format!( + "not logged in for profile {profile:?}; run `mohub login --token-stdin`" + )) + })?, + }; + let user = client::current_user(manifest, &auth_runtime(matches, &base_url, &token))?; + write_stdout(format_args!( + "Authenticated to {base_url} as {} (profile {profile}).\n", + user_label(&user), + ))?; + Ok(()) +} + +fn handle_logout(matches: &ArgMatches) -> Result<(), Error> { + let (profile, _) = selected_profile(matches)?; + config::delete_token(&profile)?; + write_stdout(format_args!("Logged out of profile {profile}.\n"))?; + Ok(()) +} + +fn resolve_runtime(matches: &ArgMatches) -> Result<(String, Option), Error> { + let config = config::load()?; + let profile_name = matches + .get_one::("profile-name") + .cloned() + .or(config.current_profile.clone()); + let profile = profile_name + .as_ref() + .and_then(|name| config.profiles.get(name)); + if let Some(name) = &profile_name { + if profile.is_none() { + return Err(Error::Config(format!("profile {name:?} does not exist"))); + } + } + let supplied_token = supplied_token(matches)?; + let override_url = matches + .get_one::("base-url") + .map(|value| normalize_base_url(value)) + .transpose()?; + let profile_url = profile + .map(|profile| normalize_base_url(&profile.base_url)) + .transpose()?; + let base_url = override_url + .clone() + .or_else(|| profile_url.clone()) + .ok_or_else(|| { + Error::Config( + "no server URL; pass --base-url, set MARIMOHUB_URL, or configure a profile".into(), + ) + })?; + let token = if let Some(token) = supplied_token { + Some(token) + } else if let Some(name) = profile_name.as_deref() { + stored_token_for_server( + name, + override_url.as_deref(), + profile_url.as_deref(), + config::get_token(name)?, + )? + } else { + None + }; + Ok((base_url, token)) +} + +fn run() -> Result<(), Error> { + let manifest = manifest::load(); + let mut command = cli::build(&manifest); + let matches = command.clone().get_matches(); + let (path, leaf) = selected_command(&matches); + let result = run_command(&manifest, &mut command, &matches, &path, leaf); + if result.is_ok() && path.first().map(String::as_str) != Some("completions") { + check_for_update(&matches); + } + result +} + +fn run_command( + manifest: &manifest::Manifest, + command: &mut clap::Command, + matches: &ArgMatches, + path: &[String], + leaf: &ArgMatches, +) -> Result<(), Error> { + if path.first().map(String::as_str) == Some("profile") { + return handle_profile(matches.subcommand().expect("selected profile").1); + } + if path.first().map(String::as_str) == Some("completions") { + let shell = leaf.get_one::("shell").expect("required by clap"); + if shell == "nushell" { + clap_complete::generate( + clap_complete_nushell::Nushell, + command, + "mohub", + &mut io::stdout(), + ); + } else { + let shell = shell + .parse::() + .map_err(Error::Usage)?; + clap_complete::generate(shell, command, "mohub", &mut io::stdout()); + } + return Ok(()); + } + if path.first().map(String::as_str) == Some("login") { + return handle_login(manifest, matches, leaf); + } + if path.first().map(String::as_str) == Some("status") { + return handle_status(manifest, matches); + } + if path.first().map(String::as_str) == Some("logout") { + return handle_logout(matches); + } + let operation = manifest + .operations + .iter() + .find(|operation| operation.command == path) + .ok_or_else(|| Error::Manifest(format!("no operation for command {}", path.join(" "))))?; + let (base_url, token) = resolve_runtime(matches)?; + let runtime = client::Runtime { + base_url: &base_url, + token: token.as_ref(), + timeout: Duration::from_secs( + *matches + .get_one::("timeout") + .expect("defaulted by clap"), + ), + output: matches + .get_one::("output") + .expect("defaulted by clap"), + raw_envelope: matches.get_flag("raw-envelope"), + }; + client::execute(manifest, &runtime, operation, leaf) +} + +fn main() -> ExitCode { + match run() { + Ok(()) => ExitCode::SUCCESS, + Err(Error::Cancelled) => ExitCode::from(130), + Err(error) => { + eprintln!("{:?}", miette::Report::new(error)); + ExitCode::from(1) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use secrecy::ExposeSecret; + + #[test] + fn embedded_manifest_covers_the_api() { + let manifest = manifest::load(); + assert_eq!(manifest.operations.len(), 59); + assert_eq!(manifest.api_version, "1.0.0"); + assert_eq!( + manifest + .operations + .iter() + .filter(|operation| operation.paginated) + .count(), + 10 + ); + assert!(manifest.operations.iter().all(|operation| { + !operation.accepts_if_match || operation.preflight_operation_id.is_some() + })); + } + + #[test] + fn server_labels_never_include_credentials_or_paths() { + assert_eq!( + server_label("https://user:secret@example.com:8443/api?token=value"), + "https://example.com:8443" + ); + } + + #[test] + fn base_urls_reject_embedded_credentials() { + assert!(matches!( + normalize_base_url("https://user:secret@example.com/api"), + Err(Error::Usage(_)) + )); + } + + #[test] + fn server_overrides_must_match_profile_urls() { + assert!(matching_server( + Some("https://hub.example.com/"), + Some("https://hub.example.com"), + ) + .unwrap()); + assert!(!matching_server( + Some("https://other.example.com"), + Some("https://hub.example.com"), + ) + .unwrap()); + } + + #[test] + fn stored_tokens_are_only_rejected_for_other_servers() { + let token = SecretString::from("mhub_pat_test".to_owned()); + assert!(matches!( + stored_token_for_server( + "default", + Some("https://other.example.com"), + Some("https://hub.example.com"), + Some(token), + ), + Err(Error::Usage(_)) + )); + assert!(stored_token_for_server( + "default", + Some("https://other.example.com"), + Some("https://hub.example.com"), + None, + ) + .unwrap() + .is_none()); + assert!(stored_token_for_server( + "default", + Some("https://hub.example.com/"), + Some("https://hub.example.com"), + Some(SecretString::from("mhub_pat_test".to_owned())), + ) + .unwrap() + .is_some()); + } + + #[test] + fn profile_set_consumes_the_global_token_flag() { + let matches = cli::build(&manifest::load()) + .try_get_matches_from([ + "mohub", + "profile", + "set", + "work", + "--base-url", + "https://hub.example.com", + "--token", + "mhub_pat_test", + ]) + .expect("valid profile command"); + let (_, leaf) = selected_command(&matches); + + assert_eq!( + supplied_token(leaf) + .unwrap() + .as_ref() + .map(ExposeSecret::expose_secret), + Some("mhub_pat_test") + ); + } + + #[test] + fn login_accepts_token_stdin() { + let matches = cli::build(&manifest::load()) + .try_get_matches_from(["mohub", "login", "--token-stdin"]) + .expect("valid login command"); + let (path, leaf) = selected_command(&matches); + + assert_eq!(path, ["login"]); + assert!(leaf.get_flag("token-stdin")); + } + + #[test] + fn login_rejects_multiple_token_sources() { + let result = cli::build(&manifest::load()).try_get_matches_from([ + "mohub", + "login", + "--token-stdin", + "--token", + "mhub_pat_test", + ]); + + assert!(result.is_err()); + } + + #[test] + fn update_hint_recognizes_uv_and_homebrew_paths() { + assert_eq!( + upgrade_hint(Path::new( + "/home/me/.local/share/uv/tools/marimohub-cli/bin/mohub" + )), + "Run `uv tool upgrade marimohub-cli` to upgrade." + ); + assert_eq!( + upgrade_hint(Path::new("/opt/homebrew/Cellar/mohub/0.3.1/bin/mohub")), + "Run `brew upgrade mohub` to upgrade." + ); + } +} diff --git a/apps/cli/src/manifest.rs b/apps/cli/src/manifest.rs new file mode 100644 index 00000000..eea32730 --- /dev/null +++ b/apps/cli/src/manifest.rs @@ -0,0 +1,90 @@ +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +pub struct Manifest { + pub version: u32, + pub api_version: String, + pub operations: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct Operation { + pub id: String, + pub command: Vec, + pub method: String, + pub path: String, + pub summary: String, + #[serde(default)] + pub description: Option, + #[serde(default)] + pub parameters: Vec, + #[serde(default)] + pub body: Option, + pub destructive: bool, + pub paginated: bool, + pub response_kind: ResponseKind, + pub session_only: bool, + pub accepts_if_match: bool, + pub accepts_idempotency_key: bool, + #[serde(default)] + pub preflight_operation_id: Option, +} + +#[derive(Debug, Deserialize)] +pub struct Parameter { + pub name: String, + pub cli_name: String, + #[serde(rename = "in")] + pub location: ParameterLocation, + pub required: bool, + #[serde(default)] + pub description: Option, + pub value_type: String, + pub repeatable: bool, +} + +#[derive(Debug, Deserialize)] +pub struct Body { + pub required: bool, + pub properties: Vec, +} + +#[derive(Debug, Deserialize)] +pub struct BodyProperty { + pub name: String, + pub cli_name: String, + pub required: bool, + #[serde(default)] + pub description: Option, + pub value_type: String, + pub repeatable: bool, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ParameterLocation { + Path, + Query, + Header, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ResponseKind { + Json, + Raw, +} + +pub fn load() -> Manifest { + let manifest: Manifest = serde_json::from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/generated/cli-manifest.json" + ))) + .expect("embedded CLI manifest must be valid JSON"); + assert_eq!(manifest.version, 1, "unsupported embedded CLI manifest"); + assert!( + !manifest.api_version.is_empty(), + "manifest API version is empty" + ); + manifest +} diff --git a/apps/cli/tests/cli.rs b/apps/cli/tests/cli.rs new file mode 100644 index 00000000..6f2beed6 --- /dev/null +++ b/apps/cli/tests/cli.rs @@ -0,0 +1,110 @@ +use std::io::{Read, Write}; +use std::net::TcpListener; +use std::thread; + +use assert_cmd::cargo::cargo_bin_cmd; +use predicates::prelude::*; + +fn serve_once(status: u16, body: &str) -> (String, thread::JoinHandle) { + let listener = TcpListener::bind("127.0.0.1:0").expect("bind test server"); + let address = listener.local_addr().expect("test server address"); + let body = body.to_owned(); + let handle = thread::spawn(move || { + let (mut stream, _) = listener.accept().expect("accept request"); + let mut request = [0; 8192]; + let length = stream.read(&mut request).expect("read request"); + let request = String::from_utf8_lossy(&request[..length]).into_owned(); + write!( + stream, + "HTTP/1.1 {status} Test\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", + body.len(), + ) + .expect("write response"); + request + }); + (format!("http://{address}"), handle) +} + +#[test] +fn help_lists_human_and_machine_output_modes() { + cargo_bin_cmd!("mohub") + .arg("--help") + .assert() + .success() + .stdout(predicate::str::contains("jsonl")) + .stdout(predicate::str::contains("table")) + .stdout(predicate::str::contains("csv")); +} + +#[test] +fn table_output_is_human_readable_and_keeps_secrets_out_of_stdout() { + let (base_url, server) = serve_once( + 200, + r#"{"success":true,"data":[{"name":"sessions","enabled":true},{"name":"audit","enabled":false}]}"#, + ); + + cargo_bin_cmd!("mohub") + .args([ + "--base-url", + &base_url, + "--token", + "mhub_pat_test", + "--output", + "table", + "capabilities", + ]) + .assert() + .success() + .stdout(predicate::str::contains("NAME")) + .stdout(predicate::str::contains("sessions")) + .stdout(predicate::str::contains("mhub_pat_test").not()); + + let request = server.join().expect("test server completed"); + assert!(request.starts_with("GET /api/v1/capabilities HTTP/1.1")); + assert!(request + .to_ascii_lowercase() + .contains("authorization: bearer mhub_pat_test")); +} + +#[test] +fn csv_output_has_no_terminal_decoration() { + let (base_url, server) = serve_once( + 200, + r#"{"success":true,"data":[{"enabled":true,"name":"sessions"}]}"#, + ); + + cargo_bin_cmd!("mohub") + .args([ + "--base-url", + &base_url, + "--token", + "mhub_pat_test", + "--output", + "csv", + "capabilities", + ]) + .assert() + .success() + .stdout("enabled,name\ntrue,sessions\n") + .stderr(""); + + server.join().expect("test server completed"); +} + +#[test] +fn http_errors_are_diagnostic_and_do_not_echo_the_token() { + let (base_url, server) = serve_once( + 401, + r#"{"success":false,"error":{"code":"UNAUTHORIZED","message":"Authentication required"}}"#, + ); + + cargo_bin_cmd!("mohub") + .args(["--base-url", &base_url, "--token", "mhub_pat_secret", "me"]) + .assert() + .failure() + .stdout("") + .stderr(predicate::str::contains("HTTP 401 UNAUTHORIZED")) + .stderr(predicate::str::contains("mhub_pat_secret").not()); + + server.join().expect("test server completed"); +} diff --git a/apps/docs/.vitepress/config.mts b/apps/docs/.vitepress/config.mts index 7dc408c9..a72d113e 100644 --- a/apps/docs/.vitepress/config.mts +++ b/apps/docs/.vitepress/config.mts @@ -128,11 +128,12 @@ export default defineConfig({ }, { text: 'Reference', - activeMatch: '^/(configuration|api|architecture|agent-guide)', + activeMatch: '^/(configuration|api|cli|architecture|agent-guide)', items: [ { text: 'Configuration', link: '/configuration' }, { text: 'API & client', link: '/api' }, { text: 'API tokens', link: '/api-tokens' }, + { text: 'CLI', link: '/cli' }, { text: 'How it works', link: '/architecture' }, { text: 'Agent guide', link: '/agent-guide' }, ], @@ -195,6 +196,7 @@ export default defineConfig({ { text: 'Configuration', link: '/configuration' }, { text: 'API & client', link: '/api' }, { text: 'API tokens', link: '/api-tokens' }, + { text: 'CLI', link: '/cli' }, { text: 'How it works', link: '/architecture' }, { text: 'Agent guide', link: '/agent-guide' }, ], diff --git a/development_docs/releasing.md b/development_docs/releasing.md index 6aa3ff40..65c1ad52 100644 --- a/development_docs/releasing.md +++ b/development_docs/releasing.md @@ -4,19 +4,30 @@ Releases are cut via a PR, never by pushing to `main` or hand-pushing tags. -1. Run `pnpm release `. This bumps `version` in - the root `package.json` on a fresh branch off `origin/main` and opens a PR - titled `release: X.Y.Z`. +1. Run `pnpm release `. This keeps the root, + Cargo package, and lockfile versions in sync on a fresh branch off + `origin/main`, then opens a PR titled `release: X.Y.Z`. The binary wheel + derives its version from Cargo. 2. Merge it. [`release-tag.yml`](../.github/workflows/release-tag.yml) verifies the PR title matches `package.json`, then creates and pushes the `vX.Y.Z` tag using a GitHub App token (tags pushed with the default `GITHUB_TOKEN` do not trigger workflows). 3. The tag push triggers [`release.yml`](../.github/workflows/release.yml), - which publishes the container image and the Helm chart to GHCR, all pinned - to `X.Y.Z`, and creates a GitHub release whose changelog is generated from + which publishes the container image and the Helm chart to GHCR, builds the + cross-platform `mohub` binaries and binary-only wheels, and creates a GitHub + release whose changelog is generated from the commits since the previous tag ([changelogithub](https://github.com/antfu/changelogithub), so conventional-commit prefixes like `feat:`/`fix:` drive the grouping). +The x86-64 Linux build uses a glibc 2.28 image. Each native archive contains +shell completions and man pages. + +[`apps/cli/dist-workspace.toml`](../apps/cli/dist-workspace.toml) defines shell, +PowerShell, Homebrew, MSI, and npm installers. It also defines the standalone +`mohub-update` program. Run `dist plan --allow-dirty` from `apps/cli` to check +this configuration. The existing workflow remains the release owner while the +team creates the Homebrew tap and configures npm credentials. + End users then upgrade with: ```sh diff --git a/docs/cli.md b/docs/cli.md new file mode 100644 index 00000000..2f7c7181 --- /dev/null +++ b/docs/cli.md @@ -0,0 +1,139 @@ +--- +description: Install the mohub CLI, sign in, and run common commands. +--- + +# Command-line interface + +Use `mohub` to manage marimohub projects, notebooks, sessions, and integrations. + +## Install + +Download the wheel for your platform from +[GitHub Releases](https://github.com/marimo-team/marimohub/releases). Install the +wheel with `uv`: + +```bash +uv tool install ./marimohub_cli-*.whl +mohub --version +``` + +To try the wheel without an installation, run: + +```bash +uv tool run --from ./marimohub_cli-*.whl mohub --version +``` + +If you do not use `uv`, download the standalone archive. Extract it and put +`mohub` (`mohub.exe` on Windows) on your `PATH`. + +## Sign in + +Sign in to marimohub in your browser. Open the user menu, select **API tokens**, +and create a token with an expiry date. The app shows the token one time. + +Create a profile for your server: + +```bash +mohub profile set default --base-url https://hub.example.com +``` + +Run `mohub login` and paste the token at the hidden prompt: + +```bash +mohub login +``` + +For a script, send the token through standard input: + +```bash +printf '%s' "$MARIMOHUB_TOKEN" | mohub login --token-stdin +``` + +Make sure that the connection works: + +```bash +mohub status +mohub me +``` + +`mohub` stores the server URL in its profile file. It stores the token in the +operating system credential store. On Linux systems without a Secret Service, +it uses a user-only credentials file instead. + +## Use the CLI + +```bash +mohub projects list --all +mohub notebooks list --pid --all +mohub notebooks create --pid --title analysis \ + --code 'import marimo as mo' --description 'Analysis notebook' +mohub sessions list --pid --all +``` + +Use `--help` to see commands and options: + +```bash +mohub --help +mohub notebooks --help +mohub projects --help +``` + +The default output is JSON. Select another output format with `--output`: + +```bash +mohub projects list --all --output table +mohub projects list --all --output csv +mohub projects list --all --output jsonl +``` + +Use `json`, `jsonl`, `raw`, or `csv` in scripts. These formats do not contain +terminal colors. Progress and update notices use standard error. + +Use `--no-update-check` or `MARIMOHUB_NO_UPDATE_CHECK=1` to turn off the daily +release check. + +## Profiles + +Use profiles to connect to more than one marimohub deployment: + +```bash +mohub profile list +mohub profile set work --base-url https://work.example.com +mohub profile use work +``` + +Use `--profile ` to select a profile for one command. + +## Shell completions + +Generate a completion file for Bash, Elvish, Fish, Nushell, PowerShell, or Zsh: + +```bash +mohub completions zsh > ~/.zfunc/_mohub +``` + +Standalone release archives also contain completion files and man pages. + +## Upgrade + +Use the same tool that installed `mohub`: + +```bash +uv tool upgrade marimohub-cli +``` + +Standalone shell and PowerShell installations include `mohub-update`. Homebrew, +npm, and MSI installations use their package manager. + +## Log out + +Remove the token from your local credential store: + +```bash +mohub logout +``` + +This command does not revoke the token. Revoke an exposed token from **API tokens** +in the web app. + +For more information about token expiry and access, see [API tokens](./api-tokens.md). diff --git a/package.json b/package.json index 94f1a9c0..baa61ab5 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,9 @@ "test:coverage:web": "pnpm --filter @marimo-hub/web test --coverage", "audit:prod": "node scripts/audit-production.mjs", "e2e": "pnpm --filter @marimo-hub/e2e e2e", - "schemas:generate": "pnpm --filter @marimo-hub/api openapi:generate && pnpm --filter @marimo-hub/core schemas:generate && vp fmt internal/schemas && pnpm --filter @marimo-hub/client generate && pnpm --filter @marimo-hub/docs integrations:generate", + "schemas:generate": "pnpm --filter @marimo-hub/api openapi:generate && pnpm --filter @marimo-hub/api cli:generate && pnpm --filter @marimo-hub/core schemas:generate && vp fmt internal/schemas && pnpm --filter @marimo-hub/client generate && pnpm --filter @marimo-hub/docs integrations:generate", "build": "vp run -r build", + "cli:version-check": "node scripts/check-cli-version.mjs", "release": "node scripts/release.mjs", "prepare": "vp config" }, diff --git a/packages/api/openapi.yaml b/packages/api/openapi.yaml index 21961f7f..ed8855f9 100644 --- a/packages/api/openapi.yaml +++ b/packages/api/openapi.yaml @@ -1506,6 +1506,7 @@ components: paths: /api/v1/me: get: + operationId: me tags: - Auth summary: Get current user info @@ -1534,6 +1535,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/version: get: + operationId: version tags: - System summary: Get the deployment version @@ -1565,6 +1567,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/capabilities: get: + operationId: capabilities tags: - System summary: Get deployment capability flags @@ -1593,6 +1596,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects: get: + operationId: projects.list tags: - Projects summary: List all projects @@ -1672,6 +1676,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + operationId: projects.create tags: - Projects summary: Create a project @@ -1766,6 +1771,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}: get: + operationId: projects.get tags: - Projects summary: Get a project @@ -1849,6 +1855,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' patch: + operationId: projects.update tags: - Projects summary: Update a project @@ -1969,6 +1976,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' delete: + operationId: projects.delete tags: - Projects summary: Delete a project @@ -2051,6 +2059,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/members: get: + operationId: projects.members.list tags: - Projects summary: List project members @@ -2128,6 +2137,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + operationId: projects.members.add tags: - Projects summary: Add a project member @@ -2236,6 +2246,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/members/{uid}: put: + operationId: projects.members.update tags: - Projects summary: Change a member's role @@ -2342,6 +2353,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' delete: + operationId: projects.members.remove tags: - Projects summary: Remove a project member @@ -2426,6 +2438,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/events: get: + operationId: audit.list tags: - Audit summary: List deployment audit events @@ -2550,6 +2563,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/events: get: + operationId: projects.audit.list tags: - Projects summary: List a project's audit events for one day @@ -2644,6 +2658,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/admin/users: get: + operationId: admin.users.list tags: - Admin summary: List all users in the identity directory @@ -2715,6 +2730,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/admin/config: get: + operationId: admin.config.get tags: - Admin summary: Describe the deployment's configuration @@ -2786,6 +2802,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks: get: + operationId: notebooks.list tags: - Notebooks summary: List notebooks in a project @@ -2878,6 +2895,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + operationId: notebooks.create tags: - Notebooks summary: Create a notebook @@ -3012,6 +3030,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/git: post: + operationId: notebooks.create-git tags: - Notebooks summary: Create a git-synced workspace notebook @@ -3160,6 +3179,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/sync-token/rotate: post: + operationId: notebooks.rotate-sync-token + x-cli-destructive: true tags: - Notebooks summary: Rotate a notebook sync token @@ -3253,6 +3274,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/source: patch: + operationId: notebooks.update-source tags: - Notebooks summary: Update a git-synced notebook source @@ -3436,6 +3458,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}: get: + operationId: notebooks.get tags: - Notebooks summary: Get notebook metadata @@ -3526,6 +3549,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' patch: + operationId: notebooks.update tags: - Notebooks summary: Update a notebook @@ -3675,6 +3699,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' delete: + operationId: notebooks.delete tags: - Notebooks summary: Delete a notebook (soft-delete) @@ -3764,6 +3789,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/content: get: + operationId: notebooks.content tags: - Notebooks summary: Get notebook code @@ -3850,6 +3876,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/versions: get: + operationId: notebooks.versions.list tags: - Notebooks summary: List notebook versions @@ -3950,6 +3977,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/versions/{vid}: get: + operationId: notebooks.versions.get tags: - Notebooks summary: Get a specific version @@ -4046,6 +4074,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/html: get: + operationId: notebooks.html tags: - Notebooks summary: Latest HTML snapshot of the notebook's outputs @@ -4122,6 +4151,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/versions/{vid}/html: get: + operationId: notebooks.versions.html tags: - Notebooks summary: One version's HTML snapshot of the notebook's outputs @@ -4202,6 +4232,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/versions/{vid}/restore: post: + operationId: notebooks.versions.restore + x-cli-destructive: true tags: - Notebooks summary: Restore a version as a new save @@ -4302,6 +4334,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/duplicate: post: + operationId: notebooks.duplicate tags: - Notebooks summary: Duplicate a notebook @@ -4408,6 +4441,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/sessions: get: + operationId: sessions.list tags: - Sessions summary: List active sessions for a project @@ -4501,6 +4535,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/sessions/{sid}: get: + operationId: sessions.get tags: - Sessions summary: Get a session (status + kernel URL) @@ -4588,6 +4623,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' delete: + operationId: sessions.terminate tags: - Sessions summary: Terminate a session and destroy sandbox @@ -4672,6 +4708,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/editor-session: get: + operationId: sessions.editor.get tags: - Sessions summary: Inspect persistent editor ownership @@ -4759,6 +4796,8 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/editor-session/takeover: post: + operationId: sessions.editor.takeover + x-cli-destructive: true tags: - Sessions summary: Gracefully take over an exclusive editor session @@ -4854,6 +4893,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/sessions: post: + operationId: sessions.create tags: - Sessions summary: Create a session and provision a sandbox @@ -4985,6 +5025,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/notebooks/{nid}/sessions/{sid}/heartbeat: post: + operationId: sessions.heartbeat tags: - Sessions summary: Update session heartbeat @@ -5079,6 +5120,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/integrations/kinds: get: + operationId: integrations.kinds.list tags: - Integrations summary: List available integration kinds (schemas drive the config forms) @@ -5141,6 +5183,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/integrations: get: + operationId: integrations.project.list tags: - Integrations summary: List a project's integrations @@ -5239,6 +5282,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + operationId: integrations.project.create tags: - Integrations summary: Create an integration (manager only) @@ -5350,6 +5394,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/integrations/{iid}: get: + operationId: integrations.project.get tags: - Integrations summary: Get an integration with its redacted config @@ -5446,6 +5491,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' patch: + operationId: integrations.project.update tags: - Integrations summary: Update an integration (manager only); a config change appends a version @@ -5573,6 +5619,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' delete: + operationId: integrations.project.delete tags: - Integrations summary: Delete an integration and its version history (manager only) @@ -5662,6 +5709,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/integrations/{iid}/versions: get: + operationId: integrations.project.versions tags: - Integrations summary: List an integration's config versions (metadata only) @@ -5768,6 +5816,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/integrations/copy: post: + operationId: integrations.project.copy tags: - Integrations summary: Copy an integration from another project (manager of both projects) @@ -5855,6 +5904,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/projects/{pid}/integrations/test: post: + operationId: integrations.project.test tags: - Integrations summary: Probe connectivity for an unsaved config or a stored instance (manager @@ -5956,6 +6006,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/org/integrations: get: + operationId: integrations.org.list tags: - Integrations summary: List org-wide integrations (super admin only) @@ -6047,6 +6098,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' post: + operationId: integrations.org.create tags: - Integrations summary: Create an org-wide integration (super admin only) @@ -6144,6 +6196,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/org/integrations/{iid}: get: + operationId: integrations.org.get tags: - Integrations summary: Get an org-wide integration with its redacted config (super admin only) @@ -6233,6 +6286,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' patch: + operationId: integrations.org.update tags: - Integrations summary: Update an org-wide integration (super admin only) @@ -6353,6 +6407,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' delete: + operationId: integrations.org.delete tags: - Integrations summary: Delete an org-wide integration and its version history (super admin only) @@ -6435,6 +6490,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/org/integrations/{iid}/versions: get: + operationId: integrations.org.versions tags: - Integrations summary: List an org-wide integration's config versions (super admin only) @@ -6534,6 +6590,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/org/integrations/test: post: + operationId: integrations.org.test tags: - Integrations summary: Probe connectivity for an unsaved or stored org config (super admin only) @@ -6626,6 +6683,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/users/search: get: + operationId: users.search tags: - Users summary: Search the user directory @@ -6691,6 +6749,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/users: get: + operationId: users.resolve tags: - Users summary: Resolve user ids to display identities @@ -6733,6 +6792,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/me/tokens: post: + operationId: auth.tokens.create tags: - Auth summary: Create a personal access token @@ -6837,6 +6897,7 @@ paths: schema: $ref: '#/components/schemas/ErrorResponse' get: + operationId: auth.tokens.list tags: - Auth summary: List the caller's personal access tokens @@ -6907,6 +6968,7 @@ paths: $ref: '#/components/schemas/ErrorResponse' /api/v1/me/tokens/{tokenId}: delete: + operationId: auth.tokens.revoke tags: - Auth summary: Revoke a personal access token diff --git a/packages/api/package.json b/packages/api/package.json index bb385da4..9943c08f 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -16,6 +16,7 @@ "build": "vp pack", "test": "vp test", "check": "vp check", + "cli:generate": "UPDATE_CLI_MANIFEST=1 vp test cliManifest.spec && pnpm --dir ../.. exec vp fmt apps/cli/generated/cli-manifest.json", "openapi:generate": "UPDATE_OPENAPI=1 vp test openapi.spec && vp fmt openapi.yaml", "typecheck": "tsc --noEmit" }, diff --git a/packages/api/src/cliManifest.spec.test.ts b/packages/api/src/cliManifest.spec.test.ts new file mode 100644 index 00000000..a733bd9e --- /dev/null +++ b/packages/api/src/cliManifest.spec.test.ts @@ -0,0 +1,123 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { generateCliManifest } from './cliManifest'; +import { generateOpenApiDocument } from './createApi'; + +const manifestPath = fileURLToPath( + new URL('../../../apps/cli/generated/cli-manifest.json', import.meta.url), +); + +describe('CLI manifest', () => { + it('matches the API contract', async () => { + const manifest = generateCliManifest(generateOpenApiDocument()); + if (process.env.UPDATE_CLI_MANIFEST === '1') { + await writeFile(manifestPath, `${JSON.stringify(manifest, null, '\t')}\n`); + return; + } + expect(JSON.parse(await readFile(manifestPath, 'utf8'))).toEqual(manifest); + }); + + it('has a unique command for every documented operation', () => { + const manifest = generateCliManifest(generateOpenApiDocument()); + expect(new Set(manifest.operations.map((operation) => operation.id)).size).toBe( + manifest.operations.length, + ); + expect(new Set(manifest.operations.map((operation) => operation.command.join('\0'))).size).toBe( + manifest.operations.length, + ); + }); + + it('lets operation parameters override matching path parameters', () => { + const manifest = generateCliManifest({ + openapi: '3.1.0', + info: { title: 'Test', version: '1.0.0' }, + paths: { + '/items/{id}': { + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'Path-level description', + schema: { type: 'string' }, + }, + { name: 'id', in: 'query', schema: { type: 'string' } }, + ], + get: { + operationId: 'items.get', + parameters: [ + { + name: 'id', + in: 'path', + required: true, + description: 'Operation-level description', + schema: { type: 'integer' }, + }, + ], + responses: { + 200: { + description: 'Item', + content: { 'application/json': { schema: { type: 'object' } } }, + }, + }, + }, + }, + }, + }); + + expect(manifest.operations[0]?.parameters).toEqual([ + { + name: 'id', + cli_name: 'id', + in: 'path', + required: true, + description: 'Operation-level description', + value_type: 'integer', + repeatable: false, + }, + { + name: 'id', + cli_name: 'id', + in: 'query', + required: false, + value_type: 'string', + repeatable: false, + }, + ]); + }); + + it('classifies disruptive non-DELETE operations explicitly', () => { + const operations = new Map( + generateCliManifest(generateOpenApiDocument()).operations.map((operation) => [ + operation.id, + operation, + ]), + ); + + expect(operations.get('notebooks.rotate-sync-token')?.destructive).toBe(true); + expect(operations.get('notebooks.versions.restore')?.destructive).toBe(true); + expect(operations.get('sessions.editor.takeover')?.destructive).toBe(true); + expect(operations.get('projects.delete')?.destructive).toBe(true); + expect(operations.get('projects.create')?.destructive).toBe(false); + expect(operations.get('projects.update')?.destructive).toBe(false); + }); + + it('reads disruptive behavior from OpenAPI operation metadata', () => { + const manifest = generateCliManifest({ + openapi: '3.1.0', + info: { title: 'Test', version: '1.0.0' }, + paths: { + '/items/rotate': { + post: { + operationId: 'items.rotate', + 'x-cli-destructive': true, + responses: { 204: { description: 'Rotated' } }, + }, + }, + }, + }); + + expect(manifest.operations[0]?.destructive).toBe(true); + }); +}); diff --git a/packages/api/src/cliManifest.ts b/packages/api/src/cliManifest.ts new file mode 100644 index 00000000..277a77c3 --- /dev/null +++ b/packages/api/src/cliManifest.ts @@ -0,0 +1,258 @@ +type JsonObject = Record; + +export interface CliParameter { + name: string; + cli_name: string; + in: 'path' | 'query' | 'header'; + required: boolean; + description?: string; + value_type: string; + repeatable: boolean; +} + +export interface CliBodyProperty { + name: string; + cli_name: string; + required: boolean; + description?: string; + value_type: string; + repeatable: boolean; +} + +export interface CliOperation { + id: string; + command: string[]; + method: string; + path: string; + summary: string; + description?: string; + parameters: CliParameter[]; + body?: { + required: boolean; + properties: CliBodyProperty[]; + }; + destructive: boolean; + paginated: boolean; + response_kind: 'json' | 'raw'; + session_only: boolean; + accepts_if_match: boolean; + accepts_idempotency_key: boolean; + preflight_operation_id?: string; +} + +export interface CliManifest { + version: 1; + api_version: string; + operations: CliOperation[]; +} + +const HTTP_METHODS = ['delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace']; + +function object(value: unknown, context: string): JsonObject { + if (value === null || typeof value !== 'object' || Array.isArray(value)) { + throw new Error(`Expected ${context} to be an object`); + } + return value as JsonObject; +} + +function optionalObject(value: unknown): JsonObject | undefined { + if (value === null || typeof value !== 'object' || Array.isArray(value)) return undefined; + return value as JsonObject; +} + +function resolve(document: JsonObject, value: unknown): JsonObject { + const candidate = object(value, 'OpenAPI reference'); + const reference = candidate.$ref; + if (typeof reference !== 'string') return candidate; + if (!reference.startsWith('#/')) throw new Error(`Unsupported external reference: ${reference}`); + let current: unknown = document; + for (const part of reference.slice(2).split('/')) { + current = object(current, reference)[part.replaceAll('~1', '/').replaceAll('~0', '~')]; + } + return resolve(document, current); +} + +function schemaType(document: JsonObject, value: unknown): { type: string; repeatable: boolean } { + const schema = resolve(document, value); + if (schema.type === 'array') { + const item = optionalObject(schema.items); + return { type: item ? schemaType(document, item).type : 'string', repeatable: true }; + } + if (typeof schema.type === 'string') return { type: schema.type, repeatable: false }; + if (Array.isArray(schema.type)) { + const type = schema.type.find((item) => item !== 'null'); + if (typeof type === 'string') return { type, repeatable: false }; + } + if (schema.properties || schema.oneOf || schema.anyOf || schema.allOf) { + return { type: 'object', repeatable: false }; + } + return { type: 'string', repeatable: false }; +} + +function cliName(name: string): string { + return name + .replaceAll(/([a-z\d])([A-Z])/g, '$1-$2') + .replaceAll('_', '-') + .toLowerCase(); +} + +function parametersFor( + document: JsonObject, + pathItem: JsonObject, + operation: JsonObject, +): CliParameter[] { + const parameters = new Map(); + for (const value of Array.isArray(pathItem.parameters) ? pathItem.parameters : []) { + const parameter = resolve(document, value); + parameters.set(`${String(parameter.in)}\0${String(parameter.name)}`, parameter); + } + for (const value of Array.isArray(operation.parameters) ? operation.parameters : []) { + const parameter = resolve(document, value); + parameters.set(`${String(parameter.in)}\0${String(parameter.name)}`, parameter); + } + return [...parameters.values()] + .map((parameter): CliParameter => { + const location = parameter.in; + if (location !== 'path' && location !== 'query' && location !== 'header') { + throw new Error(`Unsupported parameter location: ${String(location)}`); + } + const name = String(parameter.name); + const type = schemaType(document, parameter.schema); + return { + name, + cli_name: cliName(name), + in: location, + required: parameter.required === true, + ...(typeof parameter.description === 'string' + ? { description: parameter.description } + : {}), + value_type: type.type, + repeatable: type.repeatable, + }; + }) + .sort((a, b) => a.in.localeCompare(b.in) || a.name.localeCompare(b.name)); +} + +function bodyFor(document: JsonObject, operation: JsonObject): CliOperation['body'] { + if (!operation.requestBody) return undefined; + const requestBody = resolve(document, operation.requestBody); + const content = object(requestBody.content, 'request body content'); + const media = optionalObject(content['application/json']); + if (!media?.schema) return undefined; + const schema = resolve(document, media.schema); + const properties = optionalObject(schema.properties) ?? {}; + const required = new Set(Array.isArray(schema.required) ? schema.required : []); + return { + required: requestBody.required === true, + properties: Object.entries(properties) + .map(([name, value]) => { + const property = resolve(document, value); + const type = schemaType(document, property); + return { + name, + cli_name: cliName(name), + required: required.has(name), + ...(typeof property.description === 'string' + ? { description: property.description } + : {}), + value_type: type.type, + repeatable: type.repeatable, + }; + }) + .sort((a, b) => a.name.localeCompare(b.name)), + }; +} + +function successResponse(document: JsonObject, operation: JsonObject): JsonObject | undefined { + const responses = optionalObject(operation.responses) ?? {}; + const entry = Object.entries(responses) + .filter(([status]) => /^2\d\d$/.test(status)) + .sort(([a], [b]) => a.localeCompare(b))[0]; + return entry ? resolve(document, entry[1]) : undefined; +} + +function responseKind(document: JsonObject, operation: JsonObject): 'json' | 'raw' { + const response = successResponse(document, operation); + const content = optionalObject(response?.content); + return content?.['application/json'] ? 'json' : 'raw'; +} + +function isPaginated(document: JsonObject, operation: JsonObject): boolean { + const response = successResponse(document, operation); + const content = optionalObject(response?.content); + const media = optionalObject(content?.['application/json']); + if (!media?.schema) return false; + const envelope = resolve(document, media.schema); + const envelopeProperties = optionalObject(envelope.properties); + if (!envelopeProperties?.data) return false; + const data = resolve(document, envelopeProperties.data); + const properties = optionalObject(data.properties); + return Boolean(properties?.items && properties.next_cursor); +} + +function isSessionOnly(operation: JsonObject): boolean { + if (!Array.isArray(operation.security) || operation.security.length !== 1) return false; + const requirement = optionalObject(operation.security[0]); + return Boolean(requirement && Object.keys(requirement).length === 1 && requirement.cookieAuth); +} + +export function generateCliManifest(documentValue: Record): CliManifest { + const document = documentValue; + const paths = object(document.paths, 'OpenAPI paths'); + const operations: CliOperation[] = []; + + for (const [path, pathValue] of Object.entries(paths)) { + const pathItem = object(pathValue, `path ${path}`); + for (const method of HTTP_METHODS) { + const operationValue = pathItem[method]; + if (!operationValue) continue; + const operation = object(operationValue, `${method.toUpperCase()} ${path}`); + if (typeof operation.operationId !== 'string') { + throw new TypeError(`${method.toUpperCase()} ${path} has no operationId`); + } + const parameters = parametersFor(document, pathItem, operation); + const body = bodyFor(document, operation); + operations.push({ + id: operation.operationId, + command: operation.operationId.split('.'), + method: method.toUpperCase(), + path, + summary: typeof operation.summary === 'string' ? operation.summary : operation.operationId, + ...(typeof operation.description === 'string' + ? { description: operation.description } + : {}), + parameters, + ...(body ? { body } : {}), + destructive: method === 'delete' || operation['x-cli-destructive'] === true, + paginated: isPaginated(document, operation), + response_kind: responseKind(document, operation), + session_only: isSessionOnly(operation), + accepts_if_match: parameters.some( + (parameter) => parameter.name.toLowerCase() === 'if-match', + ), + accepts_idempotency_key: parameters.some( + (parameter) => parameter.name.toLowerCase() === 'idempotency-key', + ), + }); + } + } + + operations.sort((a, b) => a.id.localeCompare(b.id)); + const getByPath = new Map( + operations + .filter((operation) => operation.method === 'GET') + .map((operation) => [operation.path, operation]), + ); + for (const operation of operations) { + if (!operation.accepts_if_match) continue; + const preflight = getByPath.get(operation.path); + if (preflight) operation.preflight_operation_id = preflight.id; + } + + const info = object(document.info, 'OpenAPI info'); + return { + version: 1, + api_version: String(info.version), + operations, + }; +} diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 3f53c121..7c386a58 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -1,4 +1,6 @@ export { createApi, generateOpenApiDocument } from './createApi'; +export { generateCliManifest } from './cliManifest'; +export type { CliManifest, CliOperation } from './cliManifest'; export type { ApiDeps, ConfigSummary, diff --git a/packages/api/src/routes/admin.ts b/packages/api/src/routes/admin.ts index 31f66b95..8e1ef32d 100644 --- a/packages/api/src/routes/admin.ts +++ b/packages/api/src/routes/admin.ts @@ -16,6 +16,7 @@ import { pageSchema } from '../pagination'; const listUsers = createRoute({ method: 'get', path: '/admin/users', + operationId: 'admin.users.list', tags: ['Admin'], summary: 'List all users in the identity directory', description: @@ -39,6 +40,7 @@ const listUsers = createRoute({ const getConfig = createRoute({ method: 'get', path: '/admin/config', + operationId: 'admin.config.get', tags: ['Admin'], summary: "Describe the deployment's configuration", description: diff --git a/packages/api/src/routes/events.ts b/packages/api/src/routes/events.ts index 71faa9a1..38c3259d 100644 --- a/packages/api/src/routes/events.ts +++ b/packages/api/src/routes/events.ts @@ -71,6 +71,7 @@ function auditLogEntry(event: Event) { const listGlobalEvents = createRoute({ method: 'get', path: '/events', + operationId: 'audit.list', tags: ['Audit'], summary: 'List deployment audit events', description: @@ -93,6 +94,7 @@ const listGlobalEvents = createRoute({ const listEvents = createRoute({ method: 'get', path: '/projects/{pid}/events', + operationId: 'projects.audit.list', tags: ['Projects'], summary: "List a project's audit events for one day", description: diff --git a/packages/api/src/routes/integrations.ts b/packages/api/src/routes/integrations.ts index d9671d05..f1e50c3d 100644 --- a/packages/api/src/routes/integrations.ts +++ b/packages/api/src/routes/integrations.ts @@ -209,6 +209,7 @@ const CopyIntegrationBody = z const listKinds = createRoute({ method: 'get', path: '/integrations/kinds', + operationId: 'integrations.kinds.list', tags: ['Integrations'], summary: 'List available integration kinds (schemas drive the config forms)', responses: { @@ -223,6 +224,7 @@ const listKinds = createRoute({ const listIntegrations = createRoute({ method: 'get', path: '/projects/{pid}/integrations', + operationId: 'integrations.project.list', tags: ['Integrations'], summary: "List a project's integrations", request: { params: ProjectIdParam, query: PaginationQuery }, @@ -242,6 +244,7 @@ const listIntegrations = createRoute({ const createIntegration = createRoute({ method: 'post', path: '/projects/{pid}/integrations', + operationId: 'integrations.project.create', tags: ['Integrations'], summary: 'Create an integration (manager only)', request: { params: ProjectIdParam, body: jsonBody(CreateIntegrationBody) }, @@ -258,6 +261,7 @@ const createIntegration = createRoute({ const getIntegration = createRoute({ method: 'get', path: '/projects/{pid}/integrations/{iid}', + operationId: 'integrations.project.get', tags: ['Integrations'], summary: 'Get an integration with its redacted config', request: { params: IntegrationIdParam }, @@ -275,6 +279,7 @@ const getIntegration = createRoute({ const updateIntegration = createRoute({ method: 'patch', path: '/projects/{pid}/integrations/{iid}', + operationId: 'integrations.project.update', tags: ['Integrations'], summary: 'Update an integration (manager only); a config change appends a version', request: { @@ -296,6 +301,7 @@ const updateIntegration = createRoute({ const deleteIntegration = createRoute({ method: 'delete', path: '/projects/{pid}/integrations/{iid}', + operationId: 'integrations.project.delete', tags: ['Integrations'], summary: 'Delete an integration and its version history (manager only)', request: { params: IntegrationIdParam, headers: IfMatchHeader }, @@ -309,6 +315,7 @@ const deleteIntegration = createRoute({ const listIntegrationVersions = createRoute({ method: 'get', path: '/projects/{pid}/integrations/{iid}/versions', + operationId: 'integrations.project.versions', tags: ['Integrations'], summary: "List an integration's config versions (metadata only)", request: { params: IntegrationIdParam, query: PaginationQuery }, @@ -328,6 +335,7 @@ const listIntegrationVersions = createRoute({ const copyIntegration = createRoute({ method: 'post', path: '/projects/{pid}/integrations/copy', + operationId: 'integrations.project.copy', tags: ['Integrations'], summary: 'Copy an integration from another project (manager of both projects)', request: { params: ProjectIdParam, body: jsonBody(CopyIntegrationBody) }, @@ -344,6 +352,7 @@ const copyIntegration = createRoute({ const testIntegration = createRoute({ method: 'post', path: '/projects/{pid}/integrations/test', + operationId: 'integrations.project.test', tags: ['Integrations'], summary: 'Probe connectivity for an unsaved config or a stored instance (manager only)', request: { params: ProjectIdParam, body: jsonBody(TestIntegrationBody) }, @@ -363,6 +372,7 @@ const testIntegration = createRoute({ const listOrgIntegrations = createRoute({ method: 'get', path: '/org/integrations', + operationId: 'integrations.org.list', tags: ['Integrations'], summary: 'List org-wide integrations (super admin only)', request: { query: PaginationQuery }, @@ -382,6 +392,7 @@ const listOrgIntegrations = createRoute({ const createOrgIntegration = createRoute({ method: 'post', path: '/org/integrations', + operationId: 'integrations.org.create', tags: ['Integrations'], summary: 'Create an org-wide integration (super admin only)', request: { body: jsonBody(CreateIntegrationBody) }, @@ -398,6 +409,7 @@ const createOrgIntegration = createRoute({ const getOrgIntegration = createRoute({ method: 'get', path: '/org/integrations/{iid}', + operationId: 'integrations.org.get', tags: ['Integrations'], summary: 'Get an org-wide integration with its redacted config (super admin only)', request: { params: OrgIntegrationIdParam }, @@ -415,6 +427,7 @@ const getOrgIntegration = createRoute({ const updateOrgIntegration = createRoute({ method: 'patch', path: '/org/integrations/{iid}', + operationId: 'integrations.org.update', tags: ['Integrations'], summary: 'Update an org-wide integration (super admin only)', request: { @@ -436,6 +449,7 @@ const updateOrgIntegration = createRoute({ const deleteOrgIntegration = createRoute({ method: 'delete', path: '/org/integrations/{iid}', + operationId: 'integrations.org.delete', tags: ['Integrations'], summary: 'Delete an org-wide integration and its version history (super admin only)', request: { params: OrgIntegrationIdParam, headers: IfMatchHeader }, @@ -449,6 +463,7 @@ const deleteOrgIntegration = createRoute({ const listOrgIntegrationVersions = createRoute({ method: 'get', path: '/org/integrations/{iid}/versions', + operationId: 'integrations.org.versions', tags: ['Integrations'], summary: "List an org-wide integration's config versions (super admin only)", request: { params: OrgIntegrationIdParam, query: PaginationQuery }, @@ -468,6 +483,7 @@ const listOrgIntegrationVersions = createRoute({ const testOrgIntegration = createRoute({ method: 'post', path: '/org/integrations/test', + operationId: 'integrations.org.test', tags: ['Integrations'], summary: 'Probe connectivity for an unsaved or stored org config (super admin only)', request: { body: jsonBody(TestIntegrationBody) }, diff --git a/packages/api/src/routes/notebooks.ts b/packages/api/src/routes/notebooks.ts index 8688db9e..329bca1b 100644 --- a/packages/api/src/routes/notebooks.ts +++ b/packages/api/src/routes/notebooks.ts @@ -182,6 +182,7 @@ const VersionIdParam = NotebookIdParam.extend({ const listNotebooks = createRoute({ method: 'get', path: '/projects/{pid}/notebooks', + operationId: 'notebooks.list', tags: ['Notebooks'], summary: 'List notebooks in a project', request: { params: ProjectIdParam, query: PaginationQuery }, @@ -201,6 +202,7 @@ const listNotebooks = createRoute({ const createNotebook = createRoute({ method: 'post', path: '/projects/{pid}/notebooks', + operationId: 'notebooks.create', tags: ['Notebooks'], summary: 'Create a notebook', request: { @@ -221,6 +223,7 @@ const createNotebook = createRoute({ const createGitNotebook = createRoute({ method: 'post', path: '/projects/{pid}/notebooks/git', + operationId: 'notebooks.create-git', tags: ['Notebooks'], summary: 'Create a git-synced workspace notebook', request: { params: ProjectIdParam, body: jsonBody(CreateGitNotebookBody) }, @@ -237,6 +240,8 @@ const createGitNotebook = createRoute({ const rotateSyncToken = createRoute({ method: 'post', path: '/projects/{pid}/notebooks/{nid}/sync-token/rotate', + operationId: 'notebooks.rotate-sync-token', + 'x-cli-destructive': true, tags: ['Notebooks'], summary: 'Rotate a notebook sync token', request: { params: NotebookIdParam }, @@ -253,6 +258,7 @@ const rotateSyncToken = createRoute({ const updateGitSource = createRoute({ method: 'patch', path: '/projects/{pid}/notebooks/{nid}/source', + operationId: 'notebooks.update-source', tags: ['Notebooks'], summary: 'Update a git-synced notebook source', request: { params: NotebookIdParam, body: jsonBody(UpdateGitSourceBody) }, @@ -272,6 +278,7 @@ const updateGitSource = createRoute({ const getNotebook = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}', + operationId: 'notebooks.get', tags: ['Notebooks'], summary: 'Get notebook metadata', request: { params: NotebookIdParam }, @@ -289,6 +296,7 @@ const getNotebook = createRoute({ const getNotebookContent = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}/content', + operationId: 'notebooks.content', tags: ['Notebooks'], summary: 'Get notebook code', request: { params: NotebookIdParam }, @@ -308,6 +316,7 @@ const getNotebookContent = createRoute({ const updateNotebook = createRoute({ method: 'patch', path: '/projects/{pid}/notebooks/{nid}', + operationId: 'notebooks.update', tags: ['Notebooks'], summary: 'Update a notebook', request: { params: NotebookIdParam, headers: IfMatchHeader, body: jsonBody(UpdateNotebookBody) }, @@ -325,6 +334,7 @@ const updateNotebook = createRoute({ const deleteNotebook = createRoute({ method: 'delete', path: '/projects/{pid}/notebooks/{nid}', + operationId: 'notebooks.delete', tags: ['Notebooks'], summary: 'Delete a notebook (soft-delete)', request: { params: NotebookIdParam, headers: IfMatchHeader }, @@ -338,6 +348,7 @@ const deleteNotebook = createRoute({ const listVersions = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}/versions', + operationId: 'notebooks.versions.list', tags: ['Notebooks'], summary: 'List notebook versions', request: { params: NotebookIdParam, query: PaginationQuery }, @@ -357,6 +368,7 @@ const listVersions = createRoute({ const getVersion = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}/versions/{vid}', + operationId: 'notebooks.versions.get', tags: ['Notebooks'], summary: 'Get a specific version', request: { params: VersionIdParam }, @@ -376,6 +388,7 @@ const getVersion = createRoute({ const getNotebookHtml = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}/html', + operationId: 'notebooks.html', tags: ['Notebooks'], summary: "Latest HTML snapshot of the notebook's outputs", description: @@ -397,6 +410,7 @@ const getNotebookHtml = createRoute({ const getVersionHtml = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}/versions/{vid}/html', + operationId: 'notebooks.versions.html', tags: ['Notebooks'], summary: "One version's HTML snapshot of the notebook's outputs", description: @@ -416,6 +430,8 @@ const getVersionHtml = createRoute({ const restoreVersion = createRoute({ method: 'post', path: '/projects/{pid}/notebooks/{nid}/versions/{vid}/restore', + operationId: 'notebooks.versions.restore', + 'x-cli-destructive': true, tags: ['Notebooks'], summary: 'Restore a version as a new save', request: { params: VersionIdParam }, @@ -432,6 +448,7 @@ const restoreVersion = createRoute({ const duplicateNotebook = createRoute({ method: 'post', path: '/projects/{pid}/notebooks/{nid}/duplicate', + operationId: 'notebooks.duplicate', tags: ['Notebooks'], summary: 'Duplicate a notebook', request: { diff --git a/packages/api/src/routes/projects.ts b/packages/api/src/routes/projects.ts index b6c38e98..74b76a5d 100644 --- a/packages/api/src/routes/projects.ts +++ b/packages/api/src/routes/projects.ts @@ -122,6 +122,7 @@ function projectResponse(project: Project, subject: AuthSubject, policy?: AuthzP const listProjects = createRoute({ method: 'get', path: '/projects', + operationId: 'projects.list', tags: ['Projects'], summary: 'List all projects', request: { query: PaginationQuery }, @@ -141,6 +142,7 @@ const listProjects = createRoute({ const createProject = createRoute({ method: 'post', path: '/projects', + operationId: 'projects.create', tags: ['Projects'], summary: 'Create a project', request: { headers: IdempotencyKeyHeader, body: jsonBody(CreateProjectBody) }, @@ -156,6 +158,7 @@ const createProject = createRoute({ const getProject = createRoute({ method: 'get', path: '/projects/{pid}', + operationId: 'projects.get', tags: ['Projects'], summary: 'Get a project', request: { params: ProjectIdParam }, @@ -173,6 +176,7 @@ const getProject = createRoute({ const updateProject = createRoute({ method: 'patch', path: '/projects/{pid}', + operationId: 'projects.update', tags: ['Projects'], summary: 'Update a project', request: { params: ProjectIdParam, headers: IfMatchHeader, body: jsonBody(UpdateProjectBody) }, @@ -190,6 +194,7 @@ const updateProject = createRoute({ const deleteProject = createRoute({ method: 'delete', path: '/projects/{pid}', + operationId: 'projects.delete', tags: ['Projects'], summary: 'Delete a project', request: { params: ProjectIdParam, headers: IfMatchHeader }, @@ -203,6 +208,7 @@ const deleteProject = createRoute({ const listMembers = createRoute({ method: 'get', path: '/projects/{pid}/members', + operationId: 'projects.members.list', tags: ['Projects'], summary: 'List project members', description: @@ -222,6 +228,7 @@ const listMembers = createRoute({ const addMember = createRoute({ method: 'post', path: '/projects/{pid}/members', + operationId: 'projects.members.add', tags: ['Projects'], summary: 'Add a project member', description: @@ -241,6 +248,7 @@ const addMember = createRoute({ const updateMember = createRoute({ method: 'put', path: '/projects/{pid}/members/{uid}', + operationId: 'projects.members.update', tags: ['Projects'], summary: "Change a member's role", request: { params: MemberIdParam, body: jsonBody(UpdateMemberRoleBody) }, @@ -257,6 +265,7 @@ const updateMember = createRoute({ const removeMember = createRoute({ method: 'delete', path: '/projects/{pid}/members/{uid}', + operationId: 'projects.members.remove', tags: ['Projects'], summary: 'Remove a project member', request: { params: MemberIdParam }, diff --git a/packages/api/src/routes/sessions.ts b/packages/api/src/routes/sessions.ts index de1add13..6a00167f 100644 --- a/packages/api/src/routes/sessions.ts +++ b/packages/api/src/routes/sessions.ts @@ -105,6 +105,7 @@ const SessionCreateResponseSchema = SessionResponseSchema.extend({ const listSessions = createRoute({ method: 'get', path: '/projects/{pid}/sessions', + operationId: 'sessions.list', tags: ['Sessions'], summary: 'List active sessions for a project', request: { params: ProjectIdParam, query: PaginationQuery }, @@ -142,6 +143,7 @@ const SessionCreateBodySchema = z const createSession = createRoute({ method: 'post', path: '/projects/{pid}/notebooks/{nid}/sessions', + operationId: 'sessions.create', tags: ['Sessions'], summary: 'Create a session and provision a sandbox', // `Idempotency-Key` is accepted and documented, but this route is already @@ -172,6 +174,7 @@ const createSession = createRoute({ const deleteSession = createRoute({ method: 'delete', path: '/projects/{pid}/notebooks/{nid}/sessions/{sid}', + operationId: 'sessions.terminate', tags: ['Sessions'], summary: 'Terminate a session and destroy sandbox', request: { params: SessionIdParam }, @@ -185,6 +188,7 @@ const deleteSession = createRoute({ const getSession = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}/sessions/{sid}', + operationId: 'sessions.get', tags: ['Sessions'], summary: 'Get a session (status + kernel URL)', request: { params: SessionIdParam }, @@ -201,6 +205,7 @@ const getSession = createRoute({ const heartbeatSession = createRoute({ method: 'post', path: '/projects/{pid}/notebooks/{nid}/sessions/{sid}/heartbeat', + operationId: 'sessions.heartbeat', tags: ['Sessions'], summary: 'Update session heartbeat', request: { params: SessionIdParam }, @@ -239,6 +244,7 @@ const EditorSessionStateSchema = z const getEditorSession = createRoute({ method: 'get', path: '/projects/{pid}/notebooks/{nid}/editor-session', + operationId: 'sessions.editor.get', tags: ['Sessions'], summary: 'Inspect persistent editor ownership', request: { params: NotebookIdParam }, @@ -264,6 +270,8 @@ const TakeoverBodySchema = z const takeoverEditorSession = createRoute({ method: 'post', path: '/projects/{pid}/notebooks/{nid}/editor-session/takeover', + operationId: 'sessions.editor.takeover', + 'x-cli-destructive': true, tags: ['Sessions'], summary: 'Gracefully take over an exclusive editor session', request: { diff --git a/packages/api/src/routes/system.ts b/packages/api/src/routes/system.ts index 9b4beb4e..9eb6adc3 100644 --- a/packages/api/src/routes/system.ts +++ b/packages/api/src/routes/system.ts @@ -30,6 +30,7 @@ const app = createApp(); const meRoute = createRoute({ method: 'get', path: '/me', + operationId: 'me', tags: ['Auth'], summary: 'Get current user info', responses: { @@ -58,6 +59,7 @@ app.openapi(meRoute, (c) => { const versionRoute = createRoute({ method: 'get', path: '/version', + operationId: 'version', tags: ['System'], summary: 'Get the deployment version', description: @@ -80,6 +82,7 @@ app.openapi(versionRoute, (c) => { const capabilitiesRoute = createRoute({ method: 'get', path: '/capabilities', + operationId: 'capabilities', tags: ['System'], summary: 'Get deployment capability flags', responses: { diff --git a/packages/api/src/routes/tokens.ts b/packages/api/src/routes/tokens.ts index 4002b872..8ce181df 100644 --- a/packages/api/src/routes/tokens.ts +++ b/packages/api/src/routes/tokens.ts @@ -54,6 +54,7 @@ const TokenIdParam = z.object({ const createToken = createRoute({ method: 'post', path: '/me/tokens', + operationId: 'auth.tokens.create', tags: ['Auth'], summary: 'Create a personal access token', description: @@ -75,6 +76,7 @@ const createToken = createRoute({ const listTokens = createRoute({ method: 'get', path: '/me/tokens', + operationId: 'auth.tokens.list', tags: ['Auth'], summary: "List the caller's personal access tokens", description: 'Metadata only — the secret is never retrievable after creation.', @@ -92,6 +94,7 @@ const listTokens = createRoute({ const revokeToken = createRoute({ method: 'delete', path: '/me/tokens/{tokenId}', + operationId: 'auth.tokens.revoke', tags: ['Auth'], summary: 'Revoke a personal access token', description: diff --git a/packages/api/src/routes/users.ts b/packages/api/src/routes/users.ts index 9d31c2c6..7238fa5c 100644 --- a/packages/api/src/routes/users.ts +++ b/packages/api/src/routes/users.ts @@ -13,6 +13,7 @@ import { const resolveUsers = createRoute({ method: 'get', path: '/users', + operationId: 'users.resolve', tags: ['Users'], summary: 'Resolve user ids to display identities', description: @@ -43,6 +44,7 @@ const resolveUsers = createRoute({ const searchUsers = createRoute({ method: 'get', path: '/users/search', + operationId: 'users.search', tags: ['Users'], summary: 'Search the user directory', description: diff --git a/packages/client/src/schema.ts b/packages/client/src/schema.ts index e78c01a5..79f3dd31 100644 --- a/packages/client/src/schema.ts +++ b/packages/client/src/schema.ts @@ -12,39 +12,7 @@ export interface paths { cookie?: never; }; /** Get current user info */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Current user information */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Me']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['me']; put?: never; post?: never; delete?: never; @@ -64,39 +32,7 @@ export interface paths { * Get the deployment version * @description Just the version string. The rest of the build/runtime identity (image, replica, backends, …) is super-admin material on `GET /api/v1/admin/config`. */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deployment version information */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['DeploymentInfo']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['version']; put?: never; post?: never; delete?: never; @@ -113,39 +49,7 @@ export interface paths { cookie?: never; }; /** Get deployment capability flags */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deployment capability flags */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Capabilities']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['capabilities']; put?: never; post?: never; delete?: never; @@ -162,180 +66,10 @@ export interface paths { cookie?: never; }; /** List all projects */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of projects with notebook summaries, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['ProjectPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['projects.list']; put?: never; /** Create a project */ - post: { - parameters: { - query?: never; - header?: { - 'idempotency-key'?: string; - }; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example Data Science */ - name: string; - /** @example Exploratory analysis notebooks */ - description: string; - /** - * @example [ - * "analytics" - * ] - */ - tags?: string[]; - federation?: components['schemas']['ProjectFederationInput']; - }; - }; - }; - responses: { - /** @description Project created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Project']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['projects.create']; delete?: never; options?: never; head?: never; @@ -350,308 +84,15 @@ export interface paths { cookie?: never; }; /** Get a project */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Project details */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Project']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['projects.get']; put?: never; post?: never; /** Delete a project */ - delete: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Project deleted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + delete: operations['projects.delete']; options?: never; head?: never; /** Update a project */ - patch: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example ML Pipeline */ - name?: string; - description?: string; - tags?: string[]; - federation?: components['schemas']['ProjectFederationInput']; - }; - }; - }; - responses: { - /** @description Project updated */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Project']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + patch: operations['projects.update']; trace?: never; }; '/api/v1/projects/{pid}/members': { @@ -665,206 +106,13 @@ export interface paths { * List project members * @description Pending email invites are visible only to project managers (plus the invitee themself); other callers see the id-keyed rows only. */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Project members */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['ProjectMember'][]; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['projects.members.list']; put?: never; /** * Add a project member * @description Add a member by user id or email. A known email resolves to its user id; an unknown email becomes a pending invite that grants access when that person first signs in. */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example user_01HXY00000000000000000000 */ - user_id?: string; - /** - * Format: email - * @example teammate@example.com - */ - email?: string; - role: components['schemas']['AssignableRole']; - }; - }; - }; - responses: { - /** @description Member added */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Project']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['projects.members.add']; delete?: never; options?: never; head?: never; @@ -880,214 +128,10 @@ export interface paths { }; get?: never; /** Change a member's role */ - put: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - /** @description The member's user id, or the (URL-encoded) email of a pending invite */ - uid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - role: components['schemas']['AssignableRole'] & unknown; - }; - }; - }; - responses: { - /** @description Member role updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Project']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + put: operations['projects.members.update']; post?: never; /** Remove a project member */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - /** @description The member's user id, or the (URL-encoded) email of a pending invite */ - uid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Member removed */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + delete: operations['projects.members.remove']; options?: never; head?: never; patch?: never; @@ -1104,105 +148,7 @@ export interface paths { * List deployment audit events * @description Deployment-wide audit trail, newest first. Super-admin only. Date ranges are inclusive and limited to 30 UTC days. */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - /** @description UTC calendar date */ - from?: string; - /** @description UTC calendar date */ - to?: string; - event?: string; - actor?: string; - project_id?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Deployment audit events, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['AuditLogPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['audit.list']; put?: never; post?: never; delete?: never; @@ -1222,100 +168,7 @@ export interface paths { * List a project's audit events for one day * @description Catalog mutation audit trail (project/notebook lifecycle, membership changes), one UTC day at a time. Manager-only: events may record member management and deletions. */ - get: { - parameters: { - query?: { - /** @description UTC day (defaults to today) */ - date?: string; - }; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The day's audit events for this project, in append order */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['AuditEvent'][]; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['projects.audit.list']; put?: never; post?: never; delete?: never; @@ -1335,86 +188,7 @@ export interface paths { * List all users in the identity directory * @description Every user who has signed in at least once, name-sorted. Currently a single page (`next_cursor` is always null). Super-admin only, and session-only: a PAT — even a super admin’s — is rejected with 403. */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The user directory, name-sorted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['AdminUserPage']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['admin.users.list']; put?: never; post?: never; delete?: never; @@ -1434,86 +208,7 @@ export interface paths { * Describe the deployment's configuration * @description Read-only view of every configuration group (storage, compute, auth, …) as resolved from the serving replica's environment at boot; secret values are never included, only whether they are set. Super-admin only, session-only. */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The deployment configuration, secrets redacted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['DeploymentConfig']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['admin.config.get']; put?: never; post?: never; delete?: never; @@ -1530,222 +225,10 @@ export interface paths { cookie?: never; }; /** List notebooks in a project */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of notebooks, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['NotebookPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['notebooks.list']; put?: never; /** Create a notebook */ - post: { - parameters: { - query?: never; - header?: { - 'idempotency-key'?: string; - }; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example Revenue Analysis */ - title: string; - /** @example Monthly revenue breakdown */ - description: string; - /** @example import marimo as mo */ - code: string; - /** - * @example [ - * "finance" - * ] - */ - tags?: string[]; - readme?: string; - deps?: string; - runtime?: { - python_version?: string; - marimo_version?: string; - }; - /** @example ghcr.io/orgname/marimo-gpu:latest */ - base_image?: string; - /** @example large */ - compute_profile?: string; - }; - }; - }; - responses: { - /** @description Notebook created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['NotebookMeta']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['notebooks.create']; delete?: never; options?: never; head?: never; @@ -1762,141 +245,7 @@ export interface paths { get?: never; put?: never; /** Create a git-synced workspace notebook */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example GitHub app */ - title: string; - /** @example Synced from a git repository */ - description: string; - /** - * @example github - * @enum {string} - */ - provider?: 'github' | 'gitlab'; - /** @example marimo-team/marimohub */ - repo: string; - /** @example main */ - branch: string; - /** @example apps */ - root_path?: string; - /** @example my_app.py */ - entry_notebook: string; - /** - * @example [ - * "git" - * ] - */ - tags?: string[]; - readme?: string; - runtime?: { - python_version?: string; - marimo_version?: string; - }; - base_image?: string; - compute_profile?: string; - }; - }; - }; - responses: { - /** @description Git-synced notebook created */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['GitNotebookCreateResult']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['notebooks.create-git']; delete?: never; options?: never; head?: never; @@ -1913,107 +262,7 @@ export interface paths { get?: never; put?: never; /** Rotate a notebook sync token */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Sync token rotated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['SyncToken']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['notebooks.rotate-sync-token']; delete?: never; options?: never; head?: never; @@ -2034,150 +283,7 @@ export interface paths { options?: never; head?: never; /** Update a git-synced notebook source */ - patch: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example marimo-team/marimohub */ - repo: string; - /** @example main */ - branch: string; - /** @example apps */ - root_path: string; - /** @example my_app.py */ - entry_notebook: string; - }; - }; - }; - responses: { - /** @description Git source updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: { - source: { - /** @enum {string} */ - type: 'git'; - /** @enum {string|null} */ - provider: 'github' | 'gitlab' | null; - repo: string; - branch: string; - root_path: string; - entry_notebook: string; - pending_config?: components['schemas']['GitSourceConfig']; - /** @enum {string} */ - sync_mode: 'push'; - current_version_id: string | null; - commit: string | null; - /** - * Format: date-time - * @example 2025-03-05T14:00:00Z - */ - last_synced_at: string | null; - }; - }; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + patch: operations['notebooks.update-source']; trace?: never; }; '/api/v1/projects/{pid}/notebooks/{nid}': { @@ -2188,325 +294,15 @@ export interface paths { cookie?: never; }; /** Get notebook metadata */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Notebook detail */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['NotebookDetail']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['notebooks.get']; put?: never; post?: never; /** Delete a notebook (soft-delete) */ - delete: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Notebook deleted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + delete: operations['notebooks.delete']; options?: never; head?: never; /** Update a notebook */ - patch: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - title?: string; - description?: string; - code?: string; - tags?: string[]; - readme?: string; - deps?: string; - /** @example Add regional breakdown */ - message?: string; - base_image?: string | null; - compute_profile?: string | null; - }; - }; - }; - responses: { - /** @description Notebook updated */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['NotebookMeta']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + patch: operations['notebooks.update']; trace?: never; }; '/api/v1/projects/{pid}/notebooks/{nid}/content': { @@ -2517,91 +313,7 @@ export interface paths { cookie?: never; }; /** Get notebook code */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Notebook source code */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: { - code: string; - }; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['notebooks.content']; put?: never; post?: never; delete?: never; @@ -2618,101 +330,7 @@ export interface paths { cookie?: never; }; /** List notebook versions */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description List of versions, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['NotebookVersionPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['notebooks.versions.list']; put?: never; post?: never; delete?: never; @@ -2729,93 +347,7 @@ export interface paths { cookie?: never; }; /** Get a specific version */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - vid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Version details with code */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: { - version: components['schemas']['NotebookVersion']; - code: string; - }; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['notebooks.versions.get']; put?: never; post?: never; delete?: never; @@ -2835,85 +367,7 @@ export interface paths { * Latest HTML snapshot of the notebook's outputs * @description Serves the newest version's HTML snapshot (captured best-effort at session teardown) raw — the static outputs shown to viewers under MARIMOHUB_VIEWER_MODE=static. `X-Marimohub-Version-Id` / `X-Marimohub-Captured-At` identify the snapshot. 404 with code `NO_HTML_SNAPSHOT` when no version has one. */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The HTML snapshot, served sandboxed (CSP forces an opaque origin) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/html': string; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['notebooks.html']; put?: never; post?: never; delete?: never; @@ -2933,86 +387,7 @@ export interface paths { * One version's HTML snapshot of the notebook's outputs * @description Serves the HTML snapshot captured for this specific version, raw. 404 with code `NO_HTML_SNAPSHOT` when the version captured none. */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - vid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description The HTML snapshot, served sandboxed (CSP forces an opaque origin) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'text/html': string; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['notebooks.versions.html']; put?: never; post?: never; delete?: never; @@ -3031,108 +406,7 @@ export interface paths { get?: never; put?: never; /** Restore a version as a new save */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - vid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Version restored as a new save; returns the updated notebook */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['NotebookMeta']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['notebooks.versions.restore']; delete?: never; options?: never; head?: never; @@ -3149,107 +423,7 @@ export interface paths { get?: never; put?: never; /** Duplicate a notebook */ - post: { - parameters: { - query?: never; - header?: { - 'idempotency-key'?: string; - }; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example Revenue Analysis (copy) */ - title?: string; - }; - }; - }; - responses: { - /** @description Notebook duplicated; returns the new notebook */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['NotebookMeta']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['notebooks.duplicate']; delete?: never; options?: never; head?: never; @@ -3264,100 +438,7 @@ export interface paths { cookie?: never; }; /** List active sessions for a project */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Active sessions for the project, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['SessionPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['sessions.list']; put?: never; post?: never; delete?: never; @@ -3374,182 +455,11 @@ export interface paths { cookie?: never; }; /** Get a session (status + kernel URL) */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - sid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Session */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Session']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['sessions.get']; put?: never; post?: never; /** Terminate a session and destroy sandbox */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - sid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Session terminated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + delete: operations['sessions.terminate']; options?: never; head?: never; patch?: never; @@ -3563,98 +473,7 @@ export interface paths { cookie?: never; }; /** Inspect persistent editor ownership */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Persistent editor ownership and current activity */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['EditorSessionState']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['sessions.editor.get']; put?: never; post?: never; delete?: never; @@ -3673,116 +492,7 @@ export interface paths { get?: never; put?: never; /** Gracefully take over an exclusive editor session */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['EditorTakeoverBody']; - }; - }; - responses: { - /** @description The prior editor was saved and stopped */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['sessions.editor.takeover']; delete?: never; options?: never; head?: never; @@ -3802,134 +512,7 @@ export interface paths { * Create a session and provision a sandbox * @description Create or reuse a notebook sandbox. Edit-session reuse follows the configured editor sandbox-sharing policy. App-session reuse is shared per notebook. */ - post: { - parameters: { - query?: never; - header?: { - 'idempotency-key'?: string; - }; - path: { - pid: string; - nid: string; - }; - cookie?: never; - }; - /** @description Optional; omit for an edit session. */ - requestBody?: { - content: { - 'application/json': components['schemas']['SessionCreateBody']; - }; - }; - responses: { - /** @description Session created or reused */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['SessionCreateResult']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Resource limit reached */ - 429: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['sessions.create']; delete?: never; options?: never; head?: never; @@ -3946,99 +529,7 @@ export interface paths { get?: never; put?: never; /** Update session heartbeat */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - nid: string; - sid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Heartbeat updated */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['Session']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['sessions.heartbeat']; delete?: never; options?: never; head?: never; @@ -4053,77 +544,7 @@ export interface paths { cookie?: never; }; /** List available integration kinds (schemas drive the config forms) */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Registered integration kinds */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationKind'][]; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['integrations.kinds.list']; put?: never; post?: never; delete?: never; @@ -4140,223 +561,10 @@ export interface paths { cookie?: never; }; /** List a project's integrations */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Integration instances (no config) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['integrations.project.list']; put?: never; /** Create an integration (manager only) */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - kind: string; - /** @example prod */ - name: string; - config: { - [key: string]: unknown; - }; - change_note?: string; - }; - }; - }; - responses: { - /** @description Integration created (config redacted) */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationDetail']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['integrations.project.create']; delete?: never; options?: never; head?: never; @@ -4371,321 +579,15 @@ export interface paths { cookie?: never; }; /** Get an integration with its redacted config */ - get: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - iid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Integration detail (config redacted) */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationDetail']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['integrations.project.get']; put?: never; post?: never; /** Delete an integration and its version history (manager only) */ - delete: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - pid: string; - iid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Integration deleted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + delete: operations['integrations.project.delete']; options?: never; head?: never; /** Update an integration (manager only); a config change appends a version */ - patch: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - pid: string; - iid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - name?: string; - enabled?: boolean; - config?: { - [key: string]: unknown; - }; - change_note?: string; - }; - }; - }; - responses: { - /** @description Integration updated (config redacted) */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationDetail']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + patch: operations['integrations.project.update']; trace?: never; }; '/api/v1/projects/{pid}/integrations/{iid}/versions': { @@ -4696,110 +598,7 @@ export interface paths { cookie?: never; }; /** List an integration's config versions (metadata only) */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path: { - pid: string; - iid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Version history, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationVersionPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['integrations.project.versions']; put?: never; post?: never; delete?: never; @@ -4818,101 +617,7 @@ export interface paths { get?: never; put?: never; /** Copy an integration from another project (manager of both projects) */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['IntegrationCopyRequest']; - }; - }; - responses: { - /** @description Integration copied (inline secrets re-encrypted; external references preserved) */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationDetail']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['integrations.project.copy']; delete?: never; options?: never; head?: never; @@ -4929,112 +634,7 @@ export interface paths { get?: never; put?: never; /** Probe connectivity for an unsaved config or a stored instance (manager only) */ - post: { - parameters: { - query?: never; - header?: never; - path: { - pid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['IntegrationTestRequest']; - }; - }; - responses: { - /** @description Probe outcome (never secret material) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationTestResult']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Resource limit reached */ - 429: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['integrations.project.test']; delete?: never; options?: never; head?: never; @@ -5049,210 +649,10 @@ export interface paths { cookie?: never; }; /** List org-wide integrations (super admin only) */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Org integration instances (no config) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['integrations.org.list']; put?: never; /** Create an org-wide integration (super admin only) */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - kind: string; - /** @example prod */ - name: string; - config: { - [key: string]: unknown; - }; - change_note?: string; - }; - }; - }; - responses: { - /** @description Integration created (config redacted) */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationDetail']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['integrations.org.create']; delete?: never; options?: never; head?: never; @@ -5267,318 +667,15 @@ export interface paths { cookie?: never; }; /** Get an org-wide integration with its redacted config (super admin only) */ - get: { - parameters: { - query?: never; - header?: never; - path: { - iid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Integration detail (config redacted) */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationDetail']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['integrations.org.get']; put?: never; post?: never; /** Delete an org-wide integration and its version history (super admin only) */ - delete: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - iid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Integration deleted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + delete: operations['integrations.org.delete']; options?: never; head?: never; /** Update an org-wide integration (super admin only) */ - patch: { - parameters: { - query?: never; - header?: { - 'if-match'?: string; - }; - path: { - iid: string; - }; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - name?: string; - enabled?: boolean; - config?: { - [key: string]: unknown; - }; - change_note?: string; - }; - }; - }; - responses: { - /** @description Integration updated (config redacted) */ - 200: { - headers: { - /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ - ETag: string; - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationDetail']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Precondition failed (If-Match did not match the current version) */ - 412: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + patch: operations['integrations.org.update']; trace?: never; }; '/api/v1/org/integrations/{iid}/versions': { @@ -5589,109 +686,7 @@ export interface paths { cookie?: never; }; /** List an org-wide integration's config versions (super admin only) */ - get: { - parameters: { - query?: { - limit?: number; - cursor?: string; - }; - header?: never; - path: { - iid: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Version history, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationVersionPage']; - }; - }; - }; - /** @description Bad request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['integrations.org.versions']; put?: never; post?: never; delete?: never; @@ -5710,110 +705,7 @@ export interface paths { get?: never; put?: never; /** Probe connectivity for an unsaved or stored org config (super admin only) */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': components['schemas']['IntegrationTestRequest']; - }; - }; - responses: { - /** @description Probe outcome (never secret material) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['IntegrationTestResult']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Resource limit reached */ - 429: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['integrations.org.test']; delete?: never; options?: never; head?: never; @@ -5831,60 +723,7 @@ export interface paths { * Search the user directory * @description Case-insensitive substring search over email, name, and id, for the add-member picker. Only users who have signed in at least once are in the directory. Under MARIMOHUB_DEFAULT_ROLE=none the caller must own or belong to at least one project — a signed-in account with no involvement cannot enumerate the directory; with a default role set, every authenticated user may search. */ - get: { - parameters: { - query: { - q: string; - limit?: number; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Matching users, name-sorted */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['User'][]; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['users.search']; put?: never; post?: never; delete?: never; @@ -5904,44 +743,7 @@ export interface paths { * Resolve user ids to display identities * @description Batch-resolve opaque user ids (the auth `sub` stored as a notebook `author` or session `user_id`) into `{ id, email, name, picture_url }`. Ids with no recorded identity are omitted from the result map. */ - get: { - parameters: { - query?: { - /** @description Comma-separated user ids. */ - ids?: string; - }; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Map of user id → resolved identity (unknown ids omitted) */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: { - [key: string]: components['schemas']['User']; - }; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['users.resolve']; put?: never; post?: never; delete?: never; @@ -5961,194 +763,13 @@ export interface paths { * List the caller's personal access tokens * @description Metadata only — the secret is never retrievable after creation. */ - get: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Tokens, newest first */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['ApiToken'][]; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + get: operations['auth.tokens.list']; put?: never; /** * Create a personal access token * @description Mint a machine credential that acts as the calling user (CI, scripts, the CLI): send it as `Authorization: Bearer mhub_pat_…`. The plaintext token is returned once, in this response, and never again. Requires session (SSO) auth — a token cannot mint tokens. */ - post: { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - requestBody: { - content: { - 'application/json': { - /** @example ci-deploy */ - name: string; - /** - * @description Days until expiry; omit for a non-expiring token. - * @example 90 - */ - expires_in_days?: number; - }; - }; - }; - responses: { - /** @description The new token — copy it now; it is never shown again */ - 201: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': { - /** @enum {boolean} */ - success: true; - data: components['schemas']['ApiTokenCreated']; - }; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Resource limit reached */ - 429: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + post: operations['auth.tokens.create']; delete?: never; options?: never; head?: never; @@ -6169,93 +790,7 @@ export interface paths { * Revoke a personal access token * @description Deletes the token; API requests using it fail within the verification-cache TTL (~30 seconds) on other replicas, immediately on this one. */ - delete: { - parameters: { - query?: never; - header?: never; - path: { - tokenId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description Token revoked */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['SuccessResponse']; - }; - }; - /** @description Authentication required */ - 401: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Insufficient role */ - 403: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Not found */ - 404: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Request body too large */ - 413: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Validation error */ - 422: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Internal server error */ - 500: { - headers: { - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - /** @description Service unavailable */ - 503: { - headers: { - /** @description Seconds to wait before retrying. */ - 'Retry-After': string; - [name: string]: unknown; - }; - content: { - 'application/json': components['schemas']['ErrorResponse']; - }; - }; - }; - }; + delete: operations['auth.tokens.revoke']; options?: never; head?: never; patch?: never; @@ -6865,4 +1400,5529 @@ export interface components { pathItems: never; } export type $defs = Record; -export type operations = Record; +export interface operations { + me: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Current user information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Me']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + version: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deployment version information */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['DeploymentInfo']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + capabilities: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deployment capability flags */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Capabilities']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.list': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of projects with notebook summaries, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['ProjectPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.create': { + parameters: { + query?: never; + header?: { + 'idempotency-key'?: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example Data Science */ + name: string; + /** @example Exploratory analysis notebooks */ + description: string; + /** + * @example [ + * "analytics" + * ] + */ + tags?: string[]; + federation?: components['schemas']['ProjectFederationInput']; + }; + }; + }; + responses: { + /** @description Project created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Project']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.get': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Project details */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Project']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.delete': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Project deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.update': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example ML Pipeline */ + name?: string; + description?: string; + tags?: string[]; + federation?: components['schemas']['ProjectFederationInput']; + }; + }; + }; + responses: { + /** @description Project updated */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Project']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.members.list': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Project members */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['ProjectMember'][]; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.members.add': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example user_01HXY00000000000000000000 */ + user_id?: string; + /** + * Format: email + * @example teammate@example.com + */ + email?: string; + role: components['schemas']['AssignableRole']; + }; + }; + }; + responses: { + /** @description Member added */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Project']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.members.update': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + /** @description The member's user id, or the (URL-encoded) email of a pending invite */ + uid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + role: components['schemas']['AssignableRole'] & unknown; + }; + }; + }; + responses: { + /** @description Member role updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Project']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.members.remove': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + /** @description The member's user id, or the (URL-encoded) email of a pending invite */ + uid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Member removed */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'audit.list': { + parameters: { + query?: { + limit?: number; + cursor?: string; + /** @description UTC calendar date */ + from?: string; + /** @description UTC calendar date */ + to?: string; + event?: string; + actor?: string; + project_id?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deployment audit events, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['AuditLogPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'projects.audit.list': { + parameters: { + query?: { + /** @description UTC day (defaults to today) */ + date?: string; + }; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The day's audit events for this project, in append order */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['AuditEvent'][]; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'admin.users.list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The user directory, name-sorted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['AdminUserPage']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'admin.config.get': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The deployment configuration, secrets redacted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['DeploymentConfig']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.list': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of notebooks, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['NotebookPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.create': { + parameters: { + query?: never; + header?: { + 'idempotency-key'?: string; + }; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example Revenue Analysis */ + title: string; + /** @example Monthly revenue breakdown */ + description: string; + /** @example import marimo as mo */ + code: string; + /** + * @example [ + * "finance" + * ] + */ + tags?: string[]; + readme?: string; + deps?: string; + runtime?: { + python_version?: string; + marimo_version?: string; + }; + /** @example ghcr.io/orgname/marimo-gpu:latest */ + base_image?: string; + /** @example large */ + compute_profile?: string; + }; + }; + }; + responses: { + /** @description Notebook created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['NotebookMeta']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.create-git': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example GitHub app */ + title: string; + /** @example Synced from a git repository */ + description: string; + /** + * @example github + * @enum {string} + */ + provider?: 'github' | 'gitlab'; + /** @example marimo-team/marimohub */ + repo: string; + /** @example main */ + branch: string; + /** @example apps */ + root_path?: string; + /** @example my_app.py */ + entry_notebook: string; + /** + * @example [ + * "git" + * ] + */ + tags?: string[]; + readme?: string; + runtime?: { + python_version?: string; + marimo_version?: string; + }; + base_image?: string; + compute_profile?: string; + }; + }; + }; + responses: { + /** @description Git-synced notebook created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['GitNotebookCreateResult']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.rotate-sync-token': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Sync token rotated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['SyncToken']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.update-source': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example marimo-team/marimohub */ + repo: string; + /** @example main */ + branch: string; + /** @example apps */ + root_path: string; + /** @example my_app.py */ + entry_notebook: string; + }; + }; + }; + responses: { + /** @description Git source updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: { + source: { + /** @enum {string} */ + type: 'git'; + /** @enum {string|null} */ + provider: 'github' | 'gitlab' | null; + repo: string; + branch: string; + root_path: string; + entry_notebook: string; + pending_config?: components['schemas']['GitSourceConfig']; + /** @enum {string} */ + sync_mode: 'push'; + current_version_id: string | null; + commit: string | null; + /** + * Format: date-time + * @example 2025-03-05T14:00:00Z + */ + last_synced_at: string | null; + }; + }; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.get': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Notebook detail */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['NotebookDetail']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.delete': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Notebook deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.update': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + title?: string; + description?: string; + code?: string; + tags?: string[]; + readme?: string; + deps?: string; + /** @example Add regional breakdown */ + message?: string; + base_image?: string | null; + compute_profile?: string | null; + }; + }; + }; + responses: { + /** @description Notebook updated */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['NotebookMeta']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.content': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Notebook source code */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: { + code: string; + }; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.versions.list': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of versions, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['NotebookVersionPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.versions.get': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + vid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Version details with code */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: { + version: components['schemas']['NotebookVersion']; + code: string; + }; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.html': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The HTML snapshot, served sandboxed (CSP forces an opaque origin) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/html': string; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.versions.html': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + vid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description The HTML snapshot, served sandboxed (CSP forces an opaque origin) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'text/html': string; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.versions.restore': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + vid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Version restored as a new save; returns the updated notebook */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['NotebookMeta']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'notebooks.duplicate': { + parameters: { + query?: never; + header?: { + 'idempotency-key'?: string; + }; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example Revenue Analysis (copy) */ + title?: string; + }; + }; + }; + responses: { + /** @description Notebook duplicated; returns the new notebook */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['NotebookMeta']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'sessions.list': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active sessions for the project, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['SessionPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'sessions.get': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + sid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Session */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Session']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'sessions.terminate': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + sid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Session terminated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'sessions.editor.get': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Persistent editor ownership and current activity */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['EditorSessionState']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'sessions.editor.takeover': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['EditorTakeoverBody']; + }; + }; + responses: { + /** @description The prior editor was saved and stopped */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'sessions.create': { + parameters: { + query?: never; + header?: { + 'idempotency-key'?: string; + }; + path: { + pid: string; + nid: string; + }; + cookie?: never; + }; + /** @description Optional; omit for an edit session. */ + requestBody?: { + content: { + 'application/json': components['schemas']['SessionCreateBody']; + }; + }; + responses: { + /** @description Session created or reused */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['SessionCreateResult']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Conflict */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Resource limit reached */ + 429: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'sessions.heartbeat': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + nid: string; + sid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Heartbeat updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['Session']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.kinds.list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Registered integration kinds */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationKind'][]; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.list': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Integration instances (no config) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.create': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + kind: string; + /** @example prod */ + name: string; + config: { + [key: string]: unknown; + }; + change_note?: string; + }; + }; + }; + responses: { + /** @description Integration created (config redacted) */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationDetail']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.get': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + iid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Integration detail (config redacted) */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationDetail']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.delete': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + pid: string; + iid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Integration deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.update': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + pid: string; + iid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name?: string; + enabled?: boolean; + config?: { + [key: string]: unknown; + }; + change_note?: string; + }; + }; + }; + responses: { + /** @description Integration updated (config redacted) */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationDetail']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.versions': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path: { + pid: string; + iid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Version history, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationVersionPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.copy': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['IntegrationCopyRequest']; + }; + }; + responses: { + /** @description Integration copied (inline secrets re-encrypted; external references preserved) */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationDetail']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.project.test': { + parameters: { + query?: never; + header?: never; + path: { + pid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['IntegrationTestRequest']; + }; + }; + responses: { + /** @description Probe outcome (never secret material) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationTestResult']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Resource limit reached */ + 429: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.org.list': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Org integration instances (no config) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.org.create': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + kind: string; + /** @example prod */ + name: string; + config: { + [key: string]: unknown; + }; + change_note?: string; + }; + }; + }; + responses: { + /** @description Integration created (config redacted) */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationDetail']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.org.get': { + parameters: { + query?: never; + header?: never; + path: { + iid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Integration detail (config redacted) */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationDetail']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.org.delete': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + iid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Integration deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.org.update': { + parameters: { + query?: never; + header?: { + 'if-match'?: string; + }; + path: { + iid: string; + }; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + name?: string; + enabled?: boolean; + config?: { + [key: string]: unknown; + }; + change_note?: string; + }; + }; + }; + responses: { + /** @description Integration updated (config redacted) */ + 200: { + headers: { + /** @description Strong validator (the resource version). Echo as `If-Match` to guard a write. */ + ETag: string; + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationDetail']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Precondition failed (If-Match did not match the current version) */ + 412: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.org.versions': { + parameters: { + query?: { + limit?: number; + cursor?: string; + }; + header?: never; + path: { + iid: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Version history, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationVersionPage']; + }; + }; + }; + /** @description Bad request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'integrations.org.test': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': components['schemas']['IntegrationTestRequest']; + }; + }; + responses: { + /** @description Probe outcome (never secret material) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['IntegrationTestResult']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Resource limit reached */ + 429: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'users.search': { + parameters: { + query: { + q: string; + limit?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Matching users, name-sorted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['User'][]; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'users.resolve': { + parameters: { + query?: { + /** @description Comma-separated user ids. */ + ids?: string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Map of user id → resolved identity (unknown ids omitted) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: { + [key: string]: components['schemas']['User']; + }; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'auth.tokens.list': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Tokens, newest first */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['ApiToken'][]; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'auth.tokens.create': { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + 'application/json': { + /** @example ci-deploy */ + name: string; + /** + * @description Days until expiry; omit for a non-expiring token. + * @example 90 + */ + expires_in_days?: number; + }; + }; + }; + responses: { + /** @description The new token — copy it now; it is never shown again */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': { + /** @enum {boolean} */ + success: true; + data: components['schemas']['ApiTokenCreated']; + }; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Resource limit reached */ + 429: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; + 'auth.tokens.revoke': { + parameters: { + query?: never; + header?: never; + path: { + tokenId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Token revoked */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['SuccessResponse']; + }; + }; + /** @description Authentication required */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Insufficient role */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Validation error */ + 422: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Internal server error */ + 500: { + headers: { + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + /** @description Service unavailable */ + 503: { + headers: { + /** @description Seconds to wait before retrying. */ + 'Retry-After': string; + [name: string]: unknown; + }; + content: { + 'application/json': components['schemas']['ErrorResponse']; + }; + }; + }; + }; +} diff --git a/scripts/check-cli-version.mjs b/scripts/check-cli-version.mjs new file mode 100644 index 00000000..dd88c40e --- /dev/null +++ b/scripts/check-cli-version.mjs @@ -0,0 +1,14 @@ +import { readFileSync } from 'node:fs'; + +const rootVersion = JSON.parse( + readFileSync(new URL('../package.json', import.meta.url), 'utf8'), +).version; +const cargo = readFileSync(new URL('../apps/cli/Cargo.toml', import.meta.url), 'utf8'); + +const cargoVersion = cargo.match(/^version = "([^"]+)"$/m)?.[1]; +if (cargoVersion !== rootVersion) { + console.error( + `version mismatch: package.json=${rootVersion}, apps/cli/Cargo.toml=${cargoVersion}`, + ); + process.exit(1); +} diff --git a/scripts/release.mjs b/scripts/release.mjs index 7bbfd994..71d757a6 100644 --- a/scripts/release.mjs +++ b/scripts/release.mjs @@ -14,6 +14,7 @@ if (!arg) { const pkgPath = new URL('../package.json', import.meta.url); const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')); +const cargoPath = new URL('../apps/cli/Cargo.toml', import.meta.url); let version = arg; if (arg === 'patch' || arg === 'minor' || arg === 'major') { @@ -40,7 +41,19 @@ run('git', 'checkout', '-b', `release/${version}`, 'origin/main'); pkg.version = version; writeFileSync(pkgPath, `${JSON.stringify(pkg, null, '\t')}\n`); -run('git', 'add', 'package.json'); +const replaceVersion = (path, pattern, replacement, label) => { + const current = readFileSync(path, 'utf8'); + if (!pattern.test(current)) { + console.error(`could not find the current ${label} version`); + process.exit(1); + } + writeFileSync(path, current.replace(pattern, replacement)); +}; + +replaceVersion(cargoPath, /^version = "[^"]+"$/m, `version = "${version}"`, 'Cargo package'); +run('cargo', 'update', '--manifest-path', 'apps/cli/Cargo.toml', '--package', 'mohub'); + +run('git', 'add', 'package.json', 'apps/cli/Cargo.toml', 'apps/cli/Cargo.lock'); run('git', 'commit', '-m', `release: ${version}`); run('git', 'push', '-u', 'origin', `release/${version}`); execFileSync( @@ -51,7 +64,7 @@ execFileSync( '--title', `release: ${version}`, '--body', - `Merging this PR tags \`v${version}\` and publishes the container image and Helm chart to GHCR.`, + `Merging this PR tags \`v${version}\` and publishes the container image, Helm chart, and mohub CLI.`, ], { stdio: 'inherit' }, );