diff --git a/.github/workflows/plugin-cli-release.yml b/.github/workflows/plugin-cli-release.yml
new file mode 100644
index 0000000000..009319b128
--- /dev/null
+++ b/.github/workflows/plugin-cli-release.yml
@@ -0,0 +1,361 @@
+name: Plugin CLI Release
+
+on:
+ push:
+ tags:
+ - "plugin-cli-v*"
+ workflow_dispatch:
+ inputs:
+ version:
+ description: "Plugin CLI version to publish, for example 0.1.0"
+ required: true
+ default: "0.1.0"
+ npm-tag:
+ description: "npm distribution tag"
+ required: true
+ default: "latest"
+ type: choice
+ options:
+ - latest
+ - next
+
+permissions:
+ attestations: write
+ contents: write
+ id-token: write
+
+concurrency:
+ group: plugin-cli-release
+ cancel-in-progress: false
+
+jobs:
+ prepare:
+ name: Validate plugin CLI release
+ runs-on: ubuntu-24.04
+ outputs:
+ tag: ${{ steps.release.outputs.tag }}
+ version: ${{ steps.release.outputs.version }}
+ npm-tag: ${{ steps.release.outputs.npm-tag }}
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 22.13.0
+ registry-url: https://registry.npmjs.org
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - uses: actions/setup-go@v6
+ with:
+ go-version: "1.22.x"
+
+ - name: Validate release branch and versions
+ id: release
+ env:
+ INPUT_VERSION: ${{ inputs.version }}
+ INPUT_NPM_TAG: ${{ inputs.npm-tag }}
+ run: |
+ node <<'NODE'
+ const fs = require("fs");
+
+ let version;
+ let tag;
+ let npmTag;
+ if (process.env.GITHUB_EVENT_NAME === "push") {
+ tag = process.env.GITHUB_REF_NAME;
+ const match = /^plugin-cli-v(.+)$/.exec(tag);
+ if (!match) throw new Error(`Unexpected plugin CLI tag: ${tag}`);
+ version = match[1];
+ npmTag = version.includes("-") ? "next" : "latest";
+ } else {
+ if (process.env.GITHUB_REF !== "refs/heads/main") {
+ throw new Error(`Manual plugin CLI releases must run from main, received ${process.env.GITHUB_REF}.`);
+ }
+ version = process.env.INPUT_VERSION.trim();
+ tag = `plugin-cli-v${version}`;
+ npmTag = process.env.INPUT_NPM_TAG;
+ }
+ if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
+ throw new Error(`Invalid semantic version: ${version}`);
+ }
+ if (version.includes("-") && npmTag === "latest") {
+ throw new Error(`Prerelease ${version} cannot use the npm latest tag.`);
+ }
+
+ const packagePaths = [
+ "packages/plugin-cli/package.json",
+ "packages/plugin-cli-darwin-arm64/package.json",
+ "packages/plugin-cli-darwin-x64/package.json",
+ "packages/plugin-cli-linux-arm64-gnu/package.json",
+ "packages/plugin-cli-linux-x64-gnu/package.json",
+ "packages/plugin-cli-win32-arm64/package.json",
+ "packages/plugin-cli-win32-x64/package.json",
+ ];
+ for (const path of packagePaths) {
+ const pkg = JSON.parse(fs.readFileSync(path, "utf8"));
+ if (pkg.version !== version) {
+ throw new Error(`${path} has version ${pkg.version}; expected ${version}.`);
+ }
+ }
+
+ const cliCargo = fs.readFileSync("plugins/sdk/cli/Cargo.toml", "utf8");
+ const cliVersion = cliCargo.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
+ if (cliVersion !== version) {
+ throw new Error(`plugins/sdk/cli/Cargo.toml has version ${cliVersion}; expected ${version}.`);
+ }
+
+ const launcher = JSON.parse(fs.readFileSync(packagePaths[0], "utf8"));
+ for (const [dependency, dependencyVersion] of Object.entries(launcher.optionalDependencies ?? {})) {
+ if (dependencyVersion !== version) {
+ throw new Error(`${dependency} is pinned to ${dependencyVersion}; expected ${version}.`);
+ }
+ }
+
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`);
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${tag}\n`);
+ fs.appendFileSync(process.env.GITHUB_OUTPUT, `npm-tag=${npmTag}\n`);
+ NODE
+
+ - name: Check npm token
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ run: |
+ if [ -z "${NODE_AUTH_TOKEN}" ]; then
+ echo "::error::NPM_TOKEN secret is required to publish @dbx-app/plugin-cli."
+ exit 1
+ fi
+
+ - name: Test Rust CLI
+ run: cargo test --locked --manifest-path plugins/sdk/cli/Cargo.toml
+
+ - name: Test npm launcher
+ run: npm --prefix packages/plugin-cli test
+
+ - name: Verify packed npm installation
+ env:
+ DBX_PLUGIN_CLI_VERIFY_NATIVE: "1"
+ run: node scripts/verify-plugin-cli-package.mjs
+
+ - name: Create immutable source tag
+ if: github.event_name == 'workflow_dispatch'
+ env:
+ TAG: ${{ steps.release.outputs.tag }}
+ run: |
+ git fetch origin --tags --force
+ if git rev-parse "refs/tags/${TAG}" >/dev/null 2>&1; then
+ TAG_SHA="$(git rev-list -n 1 "refs/tags/${TAG}")"
+ HEAD_SHA="$(git rev-parse HEAD)"
+ if [ "${TAG_SHA}" != "${HEAD_SHA}" ]; then
+ echo "::error::${TAG} already points to ${TAG_SHA}, not ${HEAD_SHA}."
+ exit 1
+ fi
+ echo "${TAG} already points to this commit."
+ else
+ git tag "${TAG}" HEAD
+ git push origin "refs/tags/${TAG}:refs/tags/${TAG}"
+ fi
+
+ publish-platforms:
+ name: Publish ${{ matrix.package-name }}
+ needs: prepare
+ runs-on: ${{ matrix.runner }}
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - runner: macos-15
+ target: aarch64-apple-darwin
+ package-dir: plugin-cli-darwin-arm64
+ package-name: "@dbx-app/plugin-cli-darwin-arm64"
+ binary: dbx-plugin
+ - runner: macos-15-intel
+ target: x86_64-apple-darwin
+ package-dir: plugin-cli-darwin-x64
+ package-name: "@dbx-app/plugin-cli-darwin-x64"
+ binary: dbx-plugin
+ - runner: ubuntu-24.04-arm
+ target: aarch64-unknown-linux-gnu
+ package-dir: plugin-cli-linux-arm64-gnu
+ package-name: "@dbx-app/plugin-cli-linux-arm64-gnu"
+ binary: dbx-plugin
+ - runner: ubuntu-24.04
+ target: x86_64-unknown-linux-gnu
+ package-dir: plugin-cli-linux-x64-gnu
+ package-name: "@dbx-app/plugin-cli-linux-x64-gnu"
+ binary: dbx-plugin
+ - runner: windows-11-arm
+ target: aarch64-pc-windows-msvc
+ package-dir: plugin-cli-win32-arm64
+ package-name: "@dbx-app/plugin-cli-win32-arm64"
+ binary: dbx-plugin.exe
+ - runner: windows-2025
+ target: x86_64-pc-windows-msvc
+ package-dir: plugin-cli-win32-x64
+ package-name: "@dbx-app/plugin-cli-win32-x64"
+ binary: dbx-plugin.exe
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ needs.prepare.outputs.tag }}
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ targets: ${{ matrix.target }}
+
+ - uses: actions/setup-python@v5
+ if: runner.os == 'Linux'
+ with:
+ python-version: "3.x"
+
+ - name: Install Linux compatibility builder
+ if: runner.os == 'Linux'
+ run: pip install ziglang==0.14.0 cargo-zigbuild==0.23.0
+
+ - name: Build precompiled CLI
+ shell: bash
+ env:
+ CARGO_TARGET_DIR: ${{ github.workspace }}/target/plugin-cli
+ run: |
+ if [[ "${{ runner.os }}" == "Linux" ]]; then
+ cargo zigbuild --locked --release --manifest-path plugins/sdk/cli/Cargo.toml --target "${{ matrix.target }}.2.31"
+ else
+ cargo build --locked --release --manifest-path plugins/sdk/cli/Cargo.toml --target "${{ matrix.target }}"
+ fi
+
+ - name: Stage platform package
+ shell: bash
+ run: |
+ mkdir -p "packages/${{ matrix.package-dir }}/bin"
+ cp "target/plugin-cli/${{ matrix.target }}/release/${{ matrix.binary }}" "packages/${{ matrix.package-dir }}/bin/${{ matrix.binary }}"
+ if [[ "${{ runner.os }}" != "Windows" ]]; then
+ chmod +x "packages/${{ matrix.package-dir }}/bin/${{ matrix.binary }}"
+ fi
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 22.13.0
+ registry-url: https://registry.npmjs.org
+
+ - name: Publish platform package
+ shell: bash
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ VERSION: ${{ needs.prepare.outputs.version }}
+ NPM_TAG: ${{ needs.prepare.outputs.npm-tag }}
+ run: |
+ if npm view "${{ matrix.package-name }}@${VERSION}" version >/dev/null 2>&1; then
+ echo "${{ matrix.package-name }}@${VERSION} already exists; refreshing ${NPM_TAG}."
+ npm dist-tag add "${{ matrix.package-name }}@${VERSION}" "${NPM_TAG}"
+ else
+ npm publish "./packages/${{ matrix.package-dir }}" --access public --provenance --tag "${NPM_TAG}"
+ fi
+
+ publish-launcher:
+ name: Publish @dbx-app/plugin-cli
+ needs: [prepare, publish-platforms]
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/checkout@v5
+ with:
+ ref: ${{ needs.prepare.outputs.tag }}
+
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 22.13.0
+ registry-url: https://registry.npmjs.org
+
+ - name: Wait for platform packages
+ env:
+ VERSION: ${{ needs.prepare.outputs.version }}
+ run: |
+ wait_for_package() {
+ package="$1"
+ for attempt in $(seq 1 18); do
+ if npm view "${package}@${VERSION}" version >/dev/null 2>&1; then
+ return 0
+ fi
+ echo "Waiting for ${package}@${VERSION} (${attempt}/18)."
+ sleep 10
+ done
+ return 1
+ }
+
+ for package in \
+ @dbx-app/plugin-cli-darwin-arm64 \
+ @dbx-app/plugin-cli-darwin-x64 \
+ @dbx-app/plugin-cli-linux-arm64-gnu \
+ @dbx-app/plugin-cli-linux-x64-gnu \
+ @dbx-app/plugin-cli-win32-arm64 \
+ @dbx-app/plugin-cli-win32-x64; do
+ wait_for_package "${package}" || {
+ echo "::error::${package}@${VERSION} is unavailable; refusing to publish the launcher."
+ exit 1
+ }
+ done
+
+ - name: Publish launcher and bundled SDK
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+ VERSION: ${{ needs.prepare.outputs.version }}
+ NPM_TAG: ${{ needs.prepare.outputs.npm-tag }}
+ run: |
+ if npm view "@dbx-app/plugin-cli@${VERSION}" version >/dev/null 2>&1; then
+ echo "@dbx-app/plugin-cli@${VERSION} already exists; refreshing ${NPM_TAG}."
+ npm dist-tag add "@dbx-app/plugin-cli@${VERSION}" "${NPM_TAG}"
+ else
+ npm publish ./packages/plugin-cli --access public --provenance --tag "${NPM_TAG}"
+ fi
+
+ verify-published-package:
+ name: Verify clean npm installation
+ needs: [prepare, publish-launcher]
+ runs-on: ubuntu-24.04
+ steps:
+ - uses: actions/setup-node@v6
+ with:
+ node-version: 22.13.0
+ registry-url: https://registry.npmjs.org
+
+ - uses: dtolnay/rust-toolchain@stable
+
+ - uses: actions/setup-go@v6
+ with:
+ go-version: "1.22.x"
+
+ - name: Install from npm and package every template
+ env:
+ VERSION: ${{ needs.prepare.outputs.version }}
+ run: |
+ set -euo pipefail
+ work="$(mktemp -d)"
+ cd "${work}"
+ npm init -y >/dev/null
+
+ for attempt in $(seq 1 18); do
+ if npm view "@dbx-app/plugin-cli@${VERSION}" version >/dev/null 2>&1; then
+ break
+ fi
+ if [ "${attempt}" -eq 18 ]; then
+ echo "::error::@dbx-app/plugin-cli@${VERSION} did not become visible on npm."
+ exit 1
+ fi
+ sleep 10
+ done
+
+ npm install --ignore-scripts --no-audit --no-fund "@dbx-app/plugin-cli@${VERSION}"
+ ./node_modules/.bin/dbx-plugin --version
+
+ for template in frontend rust go; do
+ ./node_modules/.bin/dbx-plugin create "${template}-plugin" \
+ --template "${template}" \
+ --yes \
+ --id "com.example.npm-${template}" \
+ --name "npm ${template} smoke" \
+ --publisher example \
+ --description "Published npm package smoke"
+ ./node_modules/.bin/dbx-plugin package "${template}-plugin"
+ test "$(find "${template}-plugin/dist" -maxdepth 1 -name '*.dbxp' | wc -l)" -eq 1
+ done
diff --git a/.github/workflows/plugin-release-reusable.yml b/.github/workflows/plugin-release-reusable.yml
new file mode 100644
index 0000000000..0b55e1caf1
--- /dev/null
+++ b/.github/workflows/plugin-release-reusable.yml
@@ -0,0 +1,216 @@
+name: Reusable DBX plugin release
+
+on:
+ workflow_call:
+ inputs:
+ release-tag:
+ description: Existing GitHub Release tag that receives the plugin assets
+ required: true
+ type: string
+ package-command:
+ description: Command that builds one unsigned candidate .dbxp and one .artifact.json for DBX_PLUGIN_TARGET
+ required: true
+ type: string
+ working-directory:
+ description: Plugin repository directory containing the package command
+ default: .
+ required: false
+ type: string
+ package-path:
+ description: Glob relative to working-directory for built .dbxp files
+ default: dist/*.dbxp
+ required: false
+ type: string
+ metadata-path:
+ description: Glob relative to working-directory for built .artifact.json files
+ default: dist/*.artifact.json
+ required: false
+ type: string
+ node-version:
+ default: "22"
+ required: false
+ type: string
+ go-version:
+ default: "1.22.x"
+ required: false
+ type: string
+ rust-toolchain:
+ default: stable
+ required: false
+ type: string
+ sdk-ref:
+ description: DBX tag or commit used only when installing the plugin CLI from source
+ default: plugin-sdk-v1
+ required: false
+ type: string
+ plugin-cli-version:
+ description: Published @dbx-app/plugin-cli version used to package the plugin
+ default: "0.1.0"
+ required: false
+ type: string
+ install-plugin-cli:
+ description: Install the dbx-plugin CLI before packaging
+ default: true
+ required: false
+ type: boolean
+ install-plugin-cli-from-source:
+ description: Build the CLI from sdk-ref instead of installing its precompiled npm package
+ default: false
+ required: false
+ type: boolean
+ build-matrix:
+ description: JSON matrix with runner and DBX target pairs; use one universal entry for platform-independent plugins
+ default: >-
+ {"include":[{"runner":"ubuntu-24.04","target":"linux-x64"},{"runner":"ubuntu-24.04-arm","target":"linux-arm64"},{"runner":"windows-2022","target":"windows-x64"},{"runner":"macos-15-intel","target":"darwin-x64"},{"runner":"macos-15","target":"darwin-arm64"}]}
+ required: false
+ type: string
+permissions:
+ contents: read
+
+jobs:
+ build:
+ name: Build ${{ matrix.target }}
+ strategy:
+ fail-fast: false
+ matrix: ${{ fromJSON(inputs.build-matrix) }}
+ runs-on: ${{ matrix.runner }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: ${{ inputs.node-version }}
+
+ - uses: actions/setup-go@v5
+ with:
+ go-version: ${{ inputs.go-version }}
+
+ - uses: dtolnay/rust-toolchain@stable
+ with:
+ toolchain: ${{ inputs.rust-toolchain }}
+
+ - name: Checkout DBX plugin SDK
+ if: inputs.install-plugin-cli && inputs.install-plugin-cli-from-source
+ uses: actions/checkout@v4
+ with:
+ repository: t8y2/dbx
+ ref: ${{ inputs.sdk-ref }}
+ path: .dbx-plugin-sdk
+
+ - name: Install precompiled DBX plugin CLI
+ if: inputs.install-plugin-cli && !inputs.install-plugin-cli-from-source
+ shell: bash
+ run: npm install --global "@dbx-app/plugin-cli@${{ inputs.plugin-cli-version }}"
+
+ - name: Install DBX plugin CLI from source
+ if: inputs.install-plugin-cli && inputs.install-plugin-cli-from-source
+ shell: bash
+ run: cargo install --locked --path .dbx-plugin-sdk/plugins/sdk/cli
+
+ - name: Build unsigned plugin candidate
+ shell: bash
+ working-directory: ${{ inputs.working-directory }}
+ env:
+ DBX_PLUGIN_TARGET: ${{ matrix.target }}
+ DBX_PLUGIN_SDK_ROOT: ${{ inputs.install-plugin-cli-from-source && format('{0}/.dbx-plugin-sdk', github.workspace) || '' }}
+ run: ${{ inputs.package-command }}
+
+ - name: Upload target artifacts
+ uses: actions/upload-artifact@v4
+ with:
+ name: dbx-plugin-${{ matrix.target }}
+ if-no-files-found: error
+ retention-days: 1
+ path: |
+ ${{ inputs.working-directory }}/${{ inputs.package-path }}
+ ${{ inputs.working-directory }}/${{ inputs.metadata-path }}
+
+ publish:
+ name: Publish plugin release assets
+ needs: build
+ runs-on: ubuntu-24.04
+ permissions:
+ contents: write
+ steps:
+ - name: Download target artifacts
+ uses: actions/download-artifact@v4
+ with:
+ pattern: dbx-plugin-*
+ path: release-assets
+ merge-multiple: true
+
+ - name: Validate and merge candidate metadata
+ shell: bash
+ run: |
+ node <<'NODE'
+ const childProcess = require("node:child_process");
+ const crypto = require("node:crypto");
+ const fs = require("node:fs");
+ const path = require("node:path");
+
+ function filesUnder(root) {
+ return fs.readdirSync(root, { withFileTypes: true }).flatMap((entry) => {
+ const current = path.join(root, entry.name);
+ return entry.isDirectory() ? filesUnder(current) : [current];
+ });
+ }
+
+ const files = filesUnder("release-assets");
+ const metadataFiles = files.filter((file) => file.endsWith(".artifact.json"));
+ const packages = new Map();
+ for (const file of files.filter((file) => file.endsWith(".dbxp"))) {
+ const name = path.basename(file);
+ if (packages.has(name)) throw new Error(`Duplicate package filename ${name}`);
+ packages.set(name, file);
+ }
+ if (metadataFiles.length === 0 || packages.size === 0) throw new Error("Release produced no DBX plugin artifacts");
+
+ const targets = new Set();
+ let pluginIdentity;
+ const artifacts = metadataFiles.map((file) => {
+ const artifact = JSON.parse(fs.readFileSync(file, "utf8"));
+ if (!/^[a-z0-9-]{1,64}$/.test(artifact.target || "")) throw new Error(`Invalid artifact target in ${file}`);
+ if (!/^[a-fA-F0-9]{64}$/.test(artifact.sha256 || "")) throw new Error(`Invalid artifact SHA-256 in ${file}`);
+ if (artifact.signingKeyId !== undefined) throw new Error(`Candidate metadata ${file} must not declare signingKeyId`);
+ if (!Number.isSafeInteger(artifact.size) || artifact.size < 0) throw new Error(`Invalid artifact size in ${file}`);
+ const packageName = path.basename(new URL(artifact.url, "https://plugins.invalid/releases/").pathname);
+ const packageFile = packages.get(packageName);
+ if (!packageFile) throw new Error(`Artifact metadata ${file} references missing package ${packageName}`);
+ const packageBytes = fs.readFileSync(packageFile);
+ const packageSha256 = crypto.createHash("sha256").update(packageBytes).digest("hex");
+ if (artifact.size !== packageBytes.length) throw new Error(`Artifact metadata ${file} has the wrong package size`);
+ if (artifact.sha256.toLowerCase() !== packageSha256) throw new Error(`Artifact metadata ${file} has the wrong package SHA-256`);
+ const entries = childProcess.execFileSync("unzip", ["-Z1", packageFile], { encoding: "utf8" }).split(/\r?\n/);
+ if (entries.includes("signature.json")) throw new Error(`Candidate package ${packageName} must be unsigned`);
+ const manifest = JSON.parse(childProcess.execFileSync("unzip", ["-p", packageFile, "manifest.json"], { encoding: "utf8" }));
+ const identity = {
+ id: manifest.id,
+ name: manifest.name,
+ description: manifest.description || "",
+ publisher: manifest.publisher,
+ version: manifest.version,
+ permissions: [...(manifest.permissions || [])].sort(),
+ };
+ if (!identity.id || !identity.name || !identity.publisher || !identity.version) throw new Error(`Package ${packageName} has incomplete manifest identity`);
+ if (pluginIdentity && JSON.stringify(pluginIdentity) !== JSON.stringify(identity)) throw new Error(`Package ${packageName} manifest identity differs from other targets`);
+ pluginIdentity ||= identity;
+ if (targets.has(artifact.target)) throw new Error(`Duplicate artifact target ${artifact.target}`);
+ targets.add(artifact.target);
+ return artifact;
+ }).sort((left, right) => left.target.localeCompare(right.target));
+
+ fs.writeFileSync("release-assets/release-candidates.json", `${JSON.stringify({ plugin: pluginIdentity, artifacts }, null, 2)}\n`);
+ NODE
+
+ - name: Upload assets to GitHub Release
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ RELEASE_TAG: ${{ inputs.release-tag }}
+ run: |
+ mapfile -d '' packages < <(find release-assets -type f -name '*.dbxp' -print0)
+ if [ "${#packages[@]}" -eq 0 ]; then
+ echo "No .dbxp packages were downloaded" >&2
+ exit 1
+ fi
+ gh release upload "$RELEASE_TAG" "${packages[@]}" release-assets/release-candidates.json --clobber
diff --git a/.gitignore b/.gitignore
index ba7f012d58..24c04e2e27 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,6 +32,7 @@ plugins/jdbc/build/
plugins/jdbc/bin/main/
plugins/jdbc/bin/test/
plugins/jdbc/dist/
+packages/plugin-cli/sdk-root/
docs/.next/
docs/out/
docs/.source/
@@ -65,6 +66,7 @@ Thumbs.db
.env
.env.*
!.env.example
+.dbx-plugin-signing-key.env
# Temp
tmp/
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d6bf524497..9340b42ce3 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,6 +65,7 @@ For a real local Java agent test, build the target `shadowJar`, back up and repl
| `crates/dbx-web/` | Docker / Web HTTP backend |
| `packages/cli/` | `@dbx-app/cli` |
| `packages/mcp-server/` | `@dbx-app/mcp-server` |
+| `packages/plugin-cli/` | Precompiled `@dbx-app/plugin-cli` launcher and bundled plugin SDKs |
| `packages/mongo-shell/` | Private MongoDB editor parsing helpers |
| `docs/` | Official documentation site |
| `examples/` | Sample configs and automation scripts |
diff --git a/Cargo.lock b/Cargo.lock
index fea02f8576..2ae52b7acc 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1916,6 +1916,7 @@ dependencies = [
"cfg-if",
"cpufeatures 0.2.17",
"curve25519-dalek-derive",
+ "digest 0.10.7",
"fiat-crypto 0.2.9",
"rustc_version",
"subtle",
@@ -2092,9 +2093,11 @@ dependencies = [
"csv",
"dbx-sqlite-worker",
"deadpool-postgres",
+ "ed25519-dalek 2.2.0",
"encoding_rs",
"flate2",
"font-kit",
+ "fs2",
"futures",
"httpdate",
"iana-time-zone",
@@ -2126,6 +2129,7 @@ dependencies = [
"rust_decimal",
"rustls 0.23.43",
"rustls-pemfile 2.2.0",
+ "semver",
"serde",
"serde_json",
"serde_yaml",
@@ -2587,19 +2591,43 @@ dependencies = [
"digest 0.11.3",
"elliptic-curve",
"rfc6979",
- "signature",
+ "signature 3.0.0",
"spki 0.8.0-rc.4",
"zeroize",
]
+[[package]]
+name = "ed25519"
+version = "2.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
+dependencies = [
+ "pkcs8 0.10.2",
+ "signature 2.2.0",
+]
+
[[package]]
name = "ed25519"
version = "3.0.0-rc.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e914c7c52decb085cea910552e24c63ac019e3ab8bf001ff736da9a9d9d890"
dependencies = [
- "pkcs8",
- "signature",
+ "pkcs8 0.11.0-rc.11",
+ "signature 3.0.0",
+]
+
+[[package]]
+name = "ed25519-dalek"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
+dependencies = [
+ "curve25519-dalek 4.1.3",
+ "ed25519 2.2.3",
+ "serde",
+ "sha2 0.10.9",
+ "subtle",
+ "zeroize",
]
[[package]]
@@ -2609,11 +2637,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053618a4c3d3bc24f188aa660ae75a46eeab74ef07fb415c61431e5e7cd4749b"
dependencies = [
"curve25519-dalek 5.0.0-pre.6",
- "ed25519",
+ "ed25519 3.0.0-rc.4",
"rand_core 0.10.1",
"serde",
"sha2 0.11.0",
- "signature",
+ "signature 3.0.0",
"subtle",
"zeroize",
]
@@ -2638,7 +2666,7 @@ dependencies = [
"hybrid-array",
"once_cell",
"pem-rfc7468 1.0.0",
- "pkcs8",
+ "pkcs8 0.11.0-rc.11",
"rand_core 0.10.1",
"rustcrypto-ff",
"rustcrypto-group",
@@ -3035,6 +3063,16 @@ dependencies = [
"pkg-config",
]
+[[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
[[package]]
name = "fs_extra"
version = "1.3.0"
@@ -4210,7 +4248,7 @@ dependencies = [
"bcrypt-pbkdf",
"crypto-bigint",
"ecdsa",
- "ed25519-dalek",
+ "ed25519-dalek 3.0.0-pre.6",
"hex",
"hmac 0.13.0",
"num-bigint-dig",
@@ -4222,7 +4260,7 @@ dependencies = [
"sec1",
"sha1 0.11.0",
"sha2 0.11.0",
- "signature",
+ "signature 3.0.0",
"ssh-cipher",
"ssh-encoding",
"subtle",
@@ -6097,6 +6135,16 @@ dependencies = [
"spki 0.8.0-rc.4",
]
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der 0.7.10",
+ "spki 0.7.3",
+]
+
[[package]]
name = "pkcs8"
version = "0.11.0-rc.11"
@@ -7053,10 +7101,10 @@ dependencies = [
"crypto-primes",
"digest 0.11.3",
"pkcs1",
- "pkcs8",
+ "pkcs8 0.11.0-rc.11",
"rand_core 0.10.1",
"sha2 0.11.0",
- "signature",
+ "signature 3.0.0",
"spki 0.8.0-rc.4",
"zeroize",
]
@@ -7121,7 +7169,7 @@ dependencies = [
"der 0.8.1",
"digest 0.10.7",
"ecdsa",
- "ed25519-dalek",
+ "ed25519-dalek 3.0.0-pre.6",
"elliptic-curve",
"enum_dispatch",
"flate2",
@@ -7150,7 +7198,7 @@ dependencies = [
"pbkdf2 0.13.0",
"pkcs1",
"pkcs5",
- "pkcs8",
+ "pkcs8 0.11.0-rc.11",
"polyval 0.7.3",
"rand 0.10.2",
"rand_core 0.10.1",
@@ -7165,7 +7213,7 @@ dependencies = [
"sha2 0.10.9",
"sha2 0.11.0",
"sha3",
- "signature",
+ "signature 3.0.0",
"spki 0.8.0-rc.4",
"ssh-encoding",
"subtle",
@@ -8034,6 +8082,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core 0.6.4",
+]
+
[[package]]
name = "signature"
version = "3.0.0"
diff --git a/README.md b/README.md
index cd6f290dad..93b42359a4 100644
--- a/README.md
+++ b/README.md
@@ -458,6 +458,8 @@ make docs
The official DBX documentation site lives in `docs/`. If you want to improve the website content or documentation pages, edit the files under `docs/` and run `make docs` to preview the site locally.
+Plugin authors should start with [Develop and Submit DBX Plugins](https://dbxio.com/en/docs/plugin-development). Plugin source normally stays in its own repository; Marketplace listing pull requests go to [`t8y2/dbx-store`](https://github.com/t8y2/dbx-store), while plugin host, SDK, and CLI changes go to this repository.
+
For clean, reproducible local database instances, use the versioned Docker Compose recipes under [`deploy/database/`](deploy/database/README.md):
```bash
@@ -495,6 +497,7 @@ The installer will be in `src-tauri/target/release/bundle/`.
## Documentation
- [Official docs](https://dbxio.com/en/docs/what-is-dbx) — feature guides and tutorials
+- [Plugin development](https://dbxio.com/en/docs/plugin-development) — create plugins and submit Marketplace listings to the correct repository
- [Database Test Lab](https://dbxio.com/en/docs/database-lab) — local database recipes for development and verification
- [Contributing](CONTRIBUTING.md) — how to pick up issues and open PRs
- [Web API reference](docs/content/docs/web-api.mdx) — HTTP API for Docker/Web deployments
diff --git a/SECURITY.md b/SECURITY.md
index 31abe6da34..aba4865b51 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -26,5 +26,8 @@ Security-sensitive areas include:
- AI provider keys and OpenAI-compatible endpoint configuration.
- MCP and CLI access to local DBX connections.
- Docker web service authentication and data directory handling.
+- Plugin package verification, native sidecar execution, sandboxed plugin UI, host bridge permissions, and plugin connection secrets.
+
+Plugin connection secrets are stored outside ordinary connection JSON. Unencrypted sync snapshots are always redacted. If encrypted secret sync is enabled with a user-provided passphrase, plugin secrets are included only in the encrypted payload and are restored through the normal secret-store path.
Please avoid testing against systems you do not own or have explicit permission to assess.
diff --git a/apps/desktop/src/components/config/DriverStoreDialog.vue b/apps/desktop/src/components/config/DriverStoreDialog.vue
index a997fa7b33..e898a866f6 100644
--- a/apps/desktop/src/components/config/DriverStoreDialog.vue
+++ b/apps/desktop/src/components/config/DriverStoreDialog.vue
@@ -34,7 +34,7 @@ import {
type DriverInstallProgress,
} from "@/lib/connection/driverInstallProgressUi";
import { installRegisteredManagedJdbcDriver, isManagedJdbcDriver, managedJdbcDriverRows, uninstallRegisteredManagedJdbcDriver } from "@/lib/database/managedJdbcDrivers";
-import type { DriverStoreFocus } from "@/lib/connection/agentDriverInstallHint";
+import type { DriverStoreFocus, DriverStoreTab } from "@/lib/connection/agentDriverInstallHint";
import { isOfflineDriverPackage, webDriverImportAccept } from "@/lib/driverStore/driverImportSelection";
import { translateBackendError } from "@/i18n/backend-errors";
import { runAgentOfflineExportAction } from "@/lib/driverStore/agentOfflineExportFlow";
@@ -55,7 +55,7 @@ function backendError(e: unknown): string {
const props = withDefaults(
defineProps<{
updateNotificationsEnabled?: boolean;
- activeTab?: "agent" | "jdbc" | "storage" | "runtime";
+ activeTab?: DriverStoreTab;
focusTarget?: DriverStoreFocus | null;
}>(),
{
@@ -67,12 +67,12 @@ const props = withDefaults(
const emit = defineEmits<{
"update-count-change": [count: number];
- "update:activeTab": [tab: "agent" | "jdbc" | "storage" | "runtime"];
+ "update:activeTab": [tab: DriverStoreTab];
}>();
const driverStoreTab = computed({
get: () => props.activeTab,
- set: (tab: "agent" | "jdbc" | "storage" | "runtime") => emit("update:activeTab", tab),
+ set: (tab: DriverStoreTab) => emit("update:activeTab", tab),
});
// ──────────── Driver store path ────────────
@@ -1444,7 +1444,7 @@ watch(driverStoreTab, (tab) => {
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/plugins/PluginConnectionFields.spec.ts b/apps/desktop/src/components/plugins/PluginConnectionFields.spec.ts
new file mode 100644
index 0000000000..2731752bea
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginConnectionFields.spec.ts
@@ -0,0 +1,91 @@
+// @vitest-environment happy-dom
+
+import { createApp, defineComponent, h, nextTick, reactive, type App } from "vue";
+import { afterEach, describe, expect, it } from "vitest";
+import i18n from "@/i18n";
+import PluginConnectionFields from "./PluginConnectionFields.vue";
+import type { PluginConnectionProviderContribution, PluginFormFieldBinding, PluginFormFieldValue } from "@/types/database";
+
+const mountedApps: App[] = [];
+
+const contribution: PluginConnectionProviderContribution = {
+ type: "connection-provider",
+ id: "example.connection",
+ label: "Example connection",
+ database_type: "example",
+ fields: [
+ { key: "host", label: "Host", type: "text", required: true, placeholder: "localhost" },
+ { key: "password", label: "Password", type: "password" },
+ ],
+};
+
+async function mountFields(initialValues: Record
= {}, hiddenBindings: PluginFormFieldBinding[] = [], layout: "stacked" | "connection-dialog" = "stacked") {
+ const state = reactive({ values: initialValues });
+ const container = document.createElement("div");
+ document.body.append(container);
+ const app = createApp(
+ defineComponent({
+ setup() {
+ return () =>
+ h(PluginConnectionFields, {
+ contribution,
+ modelValue: state.values,
+ hiddenBindings,
+ layout,
+ "onUpdate:modelValue": (value: Record) => {
+ state.values = value;
+ },
+ });
+ },
+ }),
+ );
+ mountedApps.push(app);
+ app.use(i18n);
+ app.mount(container);
+ await nextTick();
+ return state;
+}
+
+afterEach(() => {
+ for (const app of mountedApps.splice(0)) app.unmount();
+ document.body.innerHTML = "";
+});
+
+describe("PluginConnectionFields", () => {
+ it("renders declared fields and emits immutable model updates", async () => {
+ const state = await mountFields({ password: "secret" });
+ const hostInput = document.querySelector("#example-connection-host");
+ const passwordInput = document.querySelector("#example-connection-password");
+
+ expect(hostInput?.placeholder).toBe("localhost");
+ expect(passwordInput?.value).toBe("secret");
+
+ if (!hostInput) throw new Error("host input not mounted");
+ hostInput.value = "db.internal";
+ hostInput.dispatchEvent(new Event("input", { bubbles: true }));
+ await nextTick();
+
+ expect(state.values).toEqual({ password: "secret", host: "db.internal" });
+ });
+
+ it("can hide host-owned common bindings", async () => {
+ contribution.fields[0].binding = "host";
+ await mountFields({}, ["host"]);
+ expect(document.querySelector("#example-connection-host")).toBeNull();
+ expect(document.querySelector("#example-connection-password")).not.toBeNull();
+ contribution.fields[0].binding = undefined;
+ });
+
+ it("uses the native four-column connection layout without a nested card", async () => {
+ await mountFields({}, [], "connection-dialog");
+
+ const root = document.querySelector(".contents");
+ const hostInput = document.querySelector("#example-connection-host");
+ const fieldRow = hostInput?.closest(".grid");
+
+ expect(root).not.toBeNull();
+ expect(root?.textContent).not.toContain("Example connection");
+ expect(fieldRow?.classList.contains("grid-cols-4")).toBe(true);
+ expect(document.querySelector(".rounded-lg.border")).toBeNull();
+ });
+});
diff --git a/apps/desktop/src/components/plugins/PluginConnectionFields.vue b/apps/desktop/src/components/plugins/PluginConnectionFields.vue
new file mode 100644
index 0000000000..57bc128727
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginConnectionFields.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+
{{ contribution.label }}
+
{{ contribution.description }}
+
+
+
+
{{ contribution.description }}
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ field.description }}
+
+
+
+
diff --git a/apps/desktop/src/components/plugins/PluginContributionsPanel.vue b/apps/desktop/src/components/plugins/PluginContributionsPanel.vue
new file mode 100644
index 0000000000..c92faf06f1
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginContributionsPanel.vue
@@ -0,0 +1,644 @@
+
+
+
+
+
+
{{ error }}
+
+
+
+ {{ t("pluginPlatform.marketplace") }}
+ {{ t("pluginPlatform.installed") }}
+ {{ t("pluginPlatform.settings") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ result.repository.name }}: {{ result.error }}
+
+
+
+
{{ t("pluginPlatform.loadingMarketplace") }}
+
+
+
{{ t("pluginPlatform.noMarketplacePlugins") }}
+
{{ t("pluginPlatform.noMarketplacePluginsDescription") }}
+
+
+
+
+
+
+
+ {{ listing.name }}
+ {{ t("pluginPlatform.verified") }}
+
+
{{ listing.plugin.publisher }} · {{ listing.repository.name }}
+
+
v{{ listing.plugin.latestVersion }}
+
+ {{ listing.description || t("pluginPlatform.noDescription") }}
+
+ {{ t("pluginPlatform.permissionsCount", { count: listing.plugin.permissions.length }) }}
+
+
+
+ {{ t("pluginPlatform.unsupportedTarget", { target: listing.target }) }}
+ {{ t("pluginPlatform.installedVersion", { version: listing.installed.manifest.version }) }}
+ {{ listing.plugin.license || t("pluginPlatform.licenseUnknown") }}
+
+
+
+
+
+
+
+
+
+ {{ t("common.loading") }}
+
+
+
{{ t("pluginPlatform.noInstalledPlugins") }}
+
{{ t("pluginPlatform.noInstalledPluginsDescription") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ selectedDefinition.plugin.manifest.name }}
+
{{ selectedDefinition.plugin.manifest.description }}
+
{{ selectedDefinition.plugin.manifest.id }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ selectedEntry.contribution.label }}
+
{{ selectedEntry.contribution.description || selectedEntry.contribution.database_type }}
+
+
{{ t("pluginPlatform.connectionProvider") }}
+
+
+
+
+
+
+
+
+
{{ t("pluginPlatform.connectionManagedInDialog") }}
+
+
+
+
+
{{ t("pluginPlatform.workbenches") }}
+
+
+
{{ entry.contribution.label }}
+
{{ entry.contribution.description || entry.contribution.id }}
+
+
+
+
+
+
+
{{ t("pluginPlatform.filesystemProviders") }}
+
+
+
{{ entry.contribution.label }}
+
{{ entry.contribution.schemes.join(", ") }} · {{ (entry.contribution.capabilities || []).join(", ") }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t("pluginPlatform.localInstallTitle") }}
+
{{ t("pluginPlatform.localInstallDescription") }}
+
+
+
+
+
{{ t("pluginPlatform.signedPackagesVerifiedAutomatically") }}
+
+
+
+
+
+
+
+
{{ t("pluginPlatform.repositoriesTitle") }}
+
{{ t("pluginPlatform.repositoriesDescription") }}
+
+
+
+
+
+
+ {{ repository.name }}{{ t(`pluginPlatform.repositoryKind.${repository.kind}`) }}
+
+
{{ repository.catalogUrl || t("pluginPlatform.repositoryNotConfigured") }}
+
+
{{ repository.enabled ? t("pluginPlatform.enabled") : t("pluginPlatform.disabled") }}
+
+
+
+
+
+
+
+
+
+
+
+
{{ t("pluginPlatform.developerOptionsTitle") }}
+
{{ t("pluginPlatform.developerOptionsDescription") }}
+
+
+
+
+
+
+
{{ t("pluginPlatform.developerOptionsWarning") }}
+
+
+
+
+
+
{{ t("pluginPlatform.allowUnsignedDevelopmentPackageDescription") }}
+
+
+
+
+
+
+
{{ t("pluginPlatform.repositoryTrustTitle") }}
+
{{ t("pluginPlatform.repositoryTrustDescription") }}
+
+
+
+
+
{{ key.keyId }}
+
{{ abbreviatedPublicKey(key.publicKey) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/plugins/PluginFileManager.vue b/apps/desktop/src/components/plugins/PluginFileManager.vue
new file mode 100644
index 0000000000..cf147454e9
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginFileManager.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
+
+
+
+
{{ t("pluginPlatform.loadingFiles") }}
+
{{ t("pluginPlatform.emptyDirectory") }}
+
+
+
+
+
+
+
+
+
+
{{ t("pluginPlatform.fileManagerHint") }}
+
+
+
{{ selected.name }}
+
{{ selected.uri }}
+
+
+
{{ t("pluginPlatform.loadingPreview") }}
+
+
+
+ {{ previewContentType }}
+ {{ t("pluginPlatform.previewTruncated") }}
+
+ {{ preview }}
+
+
{{ t("pluginPlatform.previewSelectedFileHint") }}
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/plugins/PluginFilesystemTab.vue b/apps/desktop/src/components/plugins/PluginFilesystemTab.vue
new file mode 100644
index 0000000000..864e5b4c9b
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginFilesystemTab.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
{{ t("pluginPlatform.loadingFilesystem") }}
+
+
+
{{ error || t("pluginPlatform.filesystemUnavailableFallback") }}
+
+
+
+
diff --git a/apps/desktop/src/components/plugins/PluginIcon.spec.ts b/apps/desktop/src/components/plugins/PluginIcon.spec.ts
new file mode 100644
index 0000000000..e132b744e1
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginIcon.spec.ts
@@ -0,0 +1,75 @@
+// @vitest-environment happy-dom
+
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { createApp, nextTick, type App } from "vue";
+import PluginIcon from "./PluginIcon.vue";
+
+const { readPluginAssetMock } = vi.hoisted(() => ({
+ readPluginAssetMock: vi.fn(),
+}));
+
+vi.mock("@/lib/backend/api", () => ({
+ readPluginAsset: readPluginAssetMock,
+}));
+
+const mountedApps: App[] = [];
+
+async function mountIcon(icon?: string) {
+ const container = document.createElement("div");
+ document.body.appendChild(container);
+ const app = createApp(PluginIcon, { pluginId: "example.plugin", icon });
+ app.mount(container);
+ mountedApps.push(app);
+ await Promise.resolve();
+ await nextTick();
+ return container;
+}
+
+beforeEach(() => {
+ readPluginAssetMock.mockReset();
+ vi.stubGlobal("URL", {
+ ...URL,
+ createObjectURL: vi.fn(() => "blob:plugin-icon"),
+ revokeObjectURL: vi.fn(),
+ });
+});
+
+afterEach(() => {
+ for (const app of mountedApps.splice(0)) app.unmount();
+ document.body.innerHTML = "";
+ vi.unstubAllGlobals();
+});
+
+describe("PluginIcon", () => {
+ it("renders a developer-provided image asset", async () => {
+ readPluginAssetMock.mockResolvedValue({ contentType: "image/svg+xml", dataBase64: "PHN2Zy8+", etag: "icon" });
+
+ const container = await mountIcon("assets/icon.svg");
+
+ expect(readPluginAssetMock).toHaveBeenCalledWith("example.plugin", "assets/icon.svg");
+ expect(container.querySelector("img")?.getAttribute("src")).toBe("blob:plugin-icon");
+ });
+
+ it("falls back when the asset cannot be loaded", async () => {
+ readPluginAssetMock.mockRejectedValue(new Error("missing"));
+
+ const container = await mountIcon("assets/missing.svg");
+
+ expect(container.querySelector("img")).toBeNull();
+ expect(container.querySelector("svg")).not.toBeNull();
+ });
+
+ it("uses the fallback without reading an undeclared asset", async () => {
+ const container = await mountIcon();
+
+ expect(readPluginAssetMock).not.toHaveBeenCalled();
+ expect(container.querySelector("svg")).not.toBeNull();
+ });
+
+ it("renders marketplace icon URLs without reading installed assets", async () => {
+ const container = await mountIcon("https://plugins.example.com/icon.svg");
+
+ expect(readPluginAssetMock).not.toHaveBeenCalled();
+ expect(container.querySelector("img")?.getAttribute("src")).toBe("https://plugins.example.com/icon.svg");
+ });
+});
diff --git a/apps/desktop/src/components/plugins/PluginIcon.vue b/apps/desktop/src/components/plugins/PluginIcon.vue
new file mode 100644
index 0000000000..95648e773d
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginIcon.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
diff --git a/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue b/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue
new file mode 100644
index 0000000000..f6167d9622
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginWorkbenchHost.vue
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+ {{ t("pluginPlatform.loadingTitle", { title }) }}
+
+
+
+
+
diff --git a/apps/desktop/src/components/plugins/PluginWorkbenchTab.vue b/apps/desktop/src/components/plugins/PluginWorkbenchTab.vue
new file mode 100644
index 0000000000..1356258ca5
--- /dev/null
+++ b/apps/desktop/src/components/plugins/PluginWorkbenchTab.vue
@@ -0,0 +1,77 @@
+
+
+
+
+
{{ t("pluginPlatform.loadingWorkbench") }}
+
+
+
{{ error || t("pluginPlatform.workbenchUnavailableFallback") }}
+
+
+
+
diff --git a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue
index 85ec274b17..007f4f1d14 100644
--- a/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue
+++ b/apps/desktop/src/components/sidebar/SidebarTreeRuntimeHost.vue
@@ -924,6 +924,9 @@ async function toggle(requestId = beginNavigationRequest()) {
await connectionStore.loadNacosNamespaces(node.connectionId, treeLoadSearchOptions);
} else if (config?.db_type === "mqtt") {
await connectionStore.loadMqttTopics(node.connectionId);
+ } else if (config?.db_type === "plugin") {
+ await queryStore.openPluginConnection(node.connectionId);
+ return;
} else {
await connectionStore.loadDatabases(node.connectionId, treeLoadSearchOptions);
}
@@ -1633,6 +1636,10 @@ async function openObjectBrowser(eventReadOnly = false, openEventEditor: boolean
const connection = connectionStore.getConfig(node.connectionId);
if (!connection) return;
+ if (connection.db_type === "plugin") {
+ await queryStore.openPluginConnection(node.connectionId);
+ return;
+ }
const options = await getDatabaseOptions(node.connectionId);
const database = resolveDefaultDatabase(connection, options);
if (database) {
diff --git a/apps/desktop/src/components/ui/PasswordInput.vue b/apps/desktop/src/components/ui/PasswordInput.vue
index a6603f3b6a..23fc155dbd 100644
--- a/apps/desktop/src/components/ui/PasswordInput.vue
+++ b/apps/desktop/src/components/ui/PasswordInput.vue
@@ -4,6 +4,8 @@ import { useI18n } from "vue-i18n";
import { Eye, EyeOff } from "@lucide/vue";
import { Input } from "@/components/ui/input";
+defineOptions({ inheritAttrs: false });
+
const model = defineModel();
const props = withDefaults(
diff --git a/apps/desktop/src/composables/__tests__/useTauriEvents.spec.ts b/apps/desktop/src/composables/__tests__/useTauriEvents.spec.ts
new file mode 100644
index 0000000000..e569cca8b0
--- /dev/null
+++ b/apps/desktop/src/composables/__tests__/useTauriEvents.spec.ts
@@ -0,0 +1,47 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const listeners = new Map void>();
+const unlisten = vi.fn();
+
+vi.mock("@tauri-apps/api/event", () => ({
+ listen: vi.fn(async (event: string, handler: (event: { payload: unknown }) => void) => {
+ listeners.set(event, handler);
+ return unlisten;
+ }),
+}));
+
+vi.mock("@/stores/connectionStore", () => ({
+ useConnectionStore: () => ({ connections: [], initFromDisk: vi.fn(), getConfig: vi.fn(), ensureConnected: vi.fn() }),
+}));
+
+vi.mock("@/stores/queryStore", () => ({
+ useQueryStore: () => ({ createTab: vi.fn(), showExecutedQueryResults: vi.fn() }),
+}));
+
+import { useTauriEvents } from "@/composables/useTauriEvents";
+
+describe("useTauriEvents", () => {
+ beforeEach(() => {
+ listeners.clear();
+ unlisten.mockClear();
+ });
+
+ it("routes the native macOS close-tab menu event to the active surface", async () => {
+ const closeActiveSurface = vi.fn();
+ const events = useTauriEvents({
+ openTableTarget: vi.fn(),
+ openSqlFilePath: vi.fn(),
+ openDbFilePath: vi.fn(),
+ openConnectionDeepLink: vi.fn(),
+ closeActiveSurface,
+ });
+
+ events.setupTauriListeners();
+ await vi.waitFor(() => expect(listeners.has("dbx-close-active-tab")).toBe(true));
+ listeners.get("dbx-close-active-tab")!({ payload: undefined });
+
+ expect(closeActiveSurface).toHaveBeenCalledOnce();
+ events.cleanupTauriListeners();
+ expect(unlisten).toHaveBeenCalled();
+ });
+});
diff --git a/apps/desktop/src/composables/useTauriEvents.ts b/apps/desktop/src/composables/useTauriEvents.ts
index a3325497f6..a6fd2a4f90 100644
--- a/apps/desktop/src/composables/useTauriEvents.ts
+++ b/apps/desktop/src/composables/useTauriEvents.ts
@@ -9,6 +9,7 @@ export function useTauriEvents(deps: {
openDbFilePath: (path: string) => Promise;
openConnectionDeepLink: (url: string) => Promise;
openAiConfigDeepLink: (url: string) => Promise;
+ closeActiveSurface: () => void;
}) {
const connectionStore = useConnectionStore();
const queryStore = useQueryStore();
@@ -116,6 +117,10 @@ export function useTauriEvents(deps: {
console.error("[DBX] dbx-open-ai-config-links error:", e);
}
}).then((unlisten) => unlistenHandles.push(unlisten));
+
+ listen("dbx-close-active-tab", () => {
+ deps.closeActiveSurface();
+ }).then((unlisten) => unlistenHandles.push(unlisten));
})
.catch(() => {});
}
diff --git a/apps/desktop/src/i18n/locales/en.ts b/apps/desktop/src/i18n/locales/en.ts
index 34ce14dad7..ba79132635 100644
--- a/apps/desktop/src/i18n/locales/en.ts
+++ b/apps/desktop/src/i18n/locales/en.ts
@@ -164,6 +164,109 @@ export default {
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "Plugin Center",
+ centerDescription: "Discover optional DBX capabilities in one marketplace, then manage installed plugins and repositories separately.",
+ marketplace: "Marketplace",
+ installed: "Installed",
+ settings: "Settings",
+ searchMarketplace: "Search plugins, publishers, or tags",
+ allRepositories: "All repositories",
+ loadingMarketplace: "Loading plugin marketplace",
+ noMarketplacePlugins: "No plugins to show",
+ noMarketplacePluginsDescription: "Configure a repository in Settings, or adjust the current search and filters.",
+ verified: "Verified",
+ noDescription: "No description provided.",
+ permissionsCount: "{count} permissions",
+ unsupportedTarget: "Not available for {target}",
+ installedVersion: "Installed v{version}",
+ licenseUnknown: "License not specified",
+ marketplaceStatus: {
+ install: "Install",
+ installed: "Installed",
+ update: "Update",
+ unsupported: "Unsupported",
+ },
+ updateSuccess: "Updated {name} to {version}",
+ noInstalledPlugins: "No plugins installed",
+ browseMarketplace: "Browse Marketplace",
+ localInstallTitle: "Install a local package",
+ localInstallDescription: "Install a signed .dbxp obtained outside the marketplace. DBX verifies package integrity and repository signatures automatically.",
+ repositoriesTitle: "Plugin repositories",
+ repositoriesDescription: "The official source is signed and managed by DBX. Custom repositories use one repository-level trust key configured below.",
+ repositoryFieldsRequired: "Repository ID, name, and catalog URL are required",
+ repositorySaved: "Plugin repository '{name}' saved",
+ removeRepositoryConfirm: "Remove plugin repository '{name}'? Installed plugins remain installed.",
+ repositoryRemoved: "Plugin repository '{name}' removed",
+ repositoryKind: {
+ official: "Official",
+ custom: "Custom",
+ enterprise: "Enterprise",
+ },
+ repositoryNotConfigured: "Catalog URL is not configured in this build",
+ enabled: "Enabled",
+ disabled: "Disabled",
+ enable: "Enable",
+ disable: "Disable",
+ repositoryIdPlaceholder: "Repository ID",
+ repositoryNamePlaceholder: "Repository name",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "Add repository",
+ signedPackagesVerifiedAutomatically: "Signatures are verified automatically",
+ developerOptionsTitle: "Third-party and developer options",
+ developerOptionsDescription: "Hidden by default. Use only for custom repositories or local plugin development.",
+ developerOptionsWarning: "Unsigned plugins can run native code with your user permissions. Enable this only for packages you built and trust.",
+ connectionManagedInDialog: "Create and edit connections in the unified New Connection dialog. This page only manages plugins and shows their capabilities.",
+ allowUnsignedDevelopmentPackage: "Allow unsigned development packages",
+ allowUnsignedDevelopmentPackageDescription: "Applies only to manual .dbxp installation and never relaxes official marketplace verification.",
+ installPackage: "Install .dbxp",
+ packageFileType: "DBX plugin package",
+ repositoryTrustTitle: "Custom repository trust",
+ repositoryTrustDescription: "A custom repository operator signs all approved packages with its repository key. Obtain that public key through a separate trusted channel.",
+ repositoryKeyAdded: "Repository signing key '{keyId}' added",
+ removeRepositoryKeyConfirm: "Remove repository signing key '{keyId}'? Installed plugins remain installed.",
+ repositoryKeyRemoved: "Repository signing key '{keyId}' removed",
+ repositoryKeyIdPlaceholder: "Repository key ID",
+ repositoryPublicKeyPlaceholder: "Base64 Ed25519 repository public key (32 bytes)",
+ trustRepository: "Trust repository",
+ noInstalledPluginsDescription: "Install a signed .dbxp, or explicitly enable unsigned packages for local development.",
+ compatible: "Compatible",
+ blocked: "Blocked",
+ rollback: "Rollback",
+ uninstall: "Uninstall",
+ rollbackConfirm: "Roll back this plugin to the previous installed version?",
+ rollbackSuccess: "Rolled back to {version}",
+ uninstallConfirm: "Uninstall {name}?",
+ uninstallSuccess: "Uninstalled {name}",
+ installSuccess: "Installed {name} {version}",
+ selectConnectionProvider: "Select a connection provider first",
+ connectionProvider: "Connection provider",
+ newConnection: "New",
+ workbenches: "Workbenches",
+ filesystemProviders: "Filesystem providers",
+ open: "Open",
+ browse: "Browse",
+ parentDirectory: "Parent directory",
+ filesystemUri: "Filesystem URI",
+ refresh: "Refresh",
+ loadingFiles: "Loading files",
+ emptyDirectory: "This directory is empty.",
+ loadMore: "Load more",
+ fileManagerHint: "Double-click a directory to open it, or a file to preview up to 256 KB.",
+ loadingPreview: "Loading preview",
+ previewTruncated: "Preview truncated at 256 KB",
+ previewSelectedFileHint: "Double-click the selected file to load its preview.",
+ entryCannotPreview: "This entry cannot be previewed.",
+ providerNoReadCapability: "The provider does not declare read capability.",
+ filesystemUnavailable: "Plugin filesystem '{pluginId}/{providerId}' is unavailable",
+ loadingFilesystem: "Loading plugin filesystem",
+ filesystemUnavailableFallback: "Plugin filesystem is unavailable",
+ workbenchUnavailable: "Plugin workbench '{pluginId}/{contributionId}' is unavailable",
+ loadingWorkbench: "Loading plugin workbench",
+ workbenchUnavailableFallback: "Plugin workbench is unavailable",
+ pluginIncompatible: "Plugin is incompatible",
+ loadingTitle: "Loading {title}",
+ },
auth: {
rateLimited: "Please try again in {seconds}s",
setupTitle: "Set up access password",
@@ -229,6 +332,7 @@ export default {
sqlOpenFailed: "Failed to open file: {message}",
sqlSaveFailed: "Failed to save file: {message}",
driverManager: "Driver Manager",
+ pluginCenter: "Plugin Center",
updatableDriverCount: "Updatable driver count",
mcpUpdateAvailable: "MCP server update available",
blockDangerousRedisCommands: "Block dangerous commands",
@@ -1005,6 +1109,9 @@ export default {
databaseCategoryTimeseries: "Time series",
databaseCategoryMq: "Message queues",
databaseCategoryRegistryConfig: "Registry/Config",
+ databaseCategoryPlugins: "Plugins",
+ pluginProviderUnavailable: "This plugin connection provider is unavailable. Reinstall or enable the plugin, then try again.",
+ pluginRequiredField: "{field} is required",
jdbcConnection: "JDBC connection",
iconView: "Icon view",
listView: "List view",
@@ -7941,6 +8048,13 @@ export default {
localInstall: "Local install",
localInstallHint: "For air-gapped machines: on another computer with internet access, download the official dbx-jdbc-plugin-*.zip from the DBX GitHub Releases page, transfer it here, and select it — no network access is needed on this machine.",
runtimeDrivers: "Runtime",
+ pluginsTab: "Plugins",
+ pluginContributionsTitle: "Frontend plugin contributions",
+ pluginContributionsDescription: "Installed plugins can describe connection forms and other UI contributions without injecting code into the main DBX window.",
+ noInstalledPlugins: "No installed plugins",
+ noInstalledPluginsDescription: "Install a backend plugin to preview its frontend contributions here.",
+ pluginHostPreview: "Host API preview",
+ pluginNoFrontendContribution: "This plugin does not declare a frontend contribution yet.",
storageTab: "Storage",
runtimeTitle: "Driver runtime",
runtimeHint: "Runtime metrics are collected only while this panel is open.",
diff --git a/apps/desktop/src/i18n/locales/es.ts b/apps/desktop/src/i18n/locales/es.ts
index 1e49303884..f70a569a37 100644
--- a/apps/desktop/src/i18n/locales/es.ts
+++ b/apps/desktop/src/i18n/locales/es.ts
@@ -166,6 +166,100 @@ export default withEnglishFallback({
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "Centro de plugins",
+ centerDescription: "Descubre capacidades opcionales de DBX en un marketplace unificado y administra por separado los plugins instalados y los repositorios.",
+ marketplace: "Marketplace",
+ installed: "Instalados",
+ settings: "Ajustes",
+ searchMarketplace: "Buscar plugins, editores o etiquetas",
+ allRepositories: "Todos los repositorios",
+ loadingMarketplace: "Cargando marketplace de plugins",
+ noMarketplacePlugins: "No hay plugins para mostrar",
+ noMarketplacePluginsDescription: "Configura un repositorio en Ajustes o cambia la búsqueda y los filtros.",
+ verified: "Verificado",
+ noDescription: "Sin descripción.",
+ permissionsCount: "{count} permisos",
+ unsupportedTarget: "No disponible para {target}",
+ installedVersion: "Instalado v{version}",
+ licenseUnknown: "Licencia no especificada",
+ marketplaceStatus: { install: "Instalar", installed: "Instalado", update: "Actualizar", unsupported: "No compatible" },
+ updateSuccess: "Se actualizó {name} a {version}",
+ noInstalledPlugins: "No hay plugins instalados",
+ browseMarketplace: "Explorar Marketplace",
+ localInstallTitle: "Instalar un paquete local",
+ localInstallDescription: "Instala un .dbxp firmado obtenido fuera del marketplace. DBX verifica automáticamente la integridad y la firma del repositorio.",
+ repositoriesTitle: "Repositorios de plugins",
+ repositoriesDescription: "DBX administra la fuente oficial. Los repositorios propios o de terceros usan la configuración de confianza del repositorio.",
+ repositoryFieldsRequired: "El ID, el nombre y la URL del catálogo son obligatorios",
+ repositorySaved: "Se guardó el repositorio de plugins '{name}'",
+ removeRepositoryConfirm: "¿Eliminar el repositorio de plugins '{name}'? Los plugins instalados permanecerán.",
+ repositoryRemoved: "Se eliminó el repositorio de plugins '{name}'",
+ repositoryKind: { official: "Oficial", custom: "Personalizado", enterprise: "Empresarial" },
+ repositoryNotConfigured: "La URL del catálogo no está configurada en esta compilación",
+ enabled: "Activado",
+ disabled: "Desactivado",
+ enable: "Activar",
+ disable: "Desactivar",
+ repositoryIdPlaceholder: "ID del repositorio",
+ repositoryNamePlaceholder: "Nombre del repositorio",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "Añadir repositorio",
+ signedPackagesVerifiedAutomatically: "Integridad y firmas verificadas automáticamente",
+ developerOptionsTitle: "Opciones de terceros y desarrollo",
+ developerOptionsDescription: "Ocultas de forma predeterminada. Úsalas solo para repositorios personalizados o desarrollo local de plugins.",
+ developerOptionsWarning: "Los plugins sin firma pueden ejecutar código nativo con tus permisos de usuario. Actívalo solo para paquetes creados y verificados por ti.",
+ connectionManagedInDialog: "Crea y edita conexiones en el cuadro unificado Nueva conexión. Esta página solo administra plugins y muestra sus capacidades.",
+ allowUnsignedDevelopmentPackage: "Permitir paquetes de desarrollo sin firmar",
+ allowUnsignedDevelopmentPackageDescription: "Solo afecta a la instalación manual de .dbxp y nunca reduce la verificación del marketplace oficial.",
+ installPackage: "Instalar .dbxp",
+ packageFileType: "Paquete de plugin de DBX",
+ repositoryTrustTitle: "Confianza del repositorio personalizado",
+ repositoryTrustDescription: "El operador del repositorio firma todos los paquetes aprobados con una única clave. Obtén la clave pública mediante un canal independiente y confiable.",
+ repositoryKeyAdded: "Se añadió la clave de firma del repositorio '{keyId}'",
+ removeRepositoryKeyConfirm: "¿Quitar la clave de firma del repositorio '{keyId}'? Los plugins instalados permanecerán instalados.",
+ repositoryKeyRemoved: "Se quitó la clave de firma del repositorio '{keyId}'",
+ repositoryKeyIdPlaceholder: "ID de clave del repositorio",
+ repositoryPublicKeyPlaceholder: "Clave pública Ed25519 del repositorio en Base64 (32 bytes)",
+ trustRepository: "Confiar en el repositorio",
+ noInstalledPluginsDescription: "Instala un .dbxp firmado o permite explícitamente paquetes sin firmar para desarrollo local.",
+ compatible: "Compatible",
+ blocked: "Bloqueado",
+ rollback: "Revertir",
+ uninstall: "Desinstalar",
+ rollbackConfirm: "¿Revertir este plugin a la versión instalada anterior?",
+ rollbackSuccess: "Se revirtió a {version}",
+ uninstallConfirm: "¿Desinstalar {name}?",
+ uninstallSuccess: "Se desinstaló {name}",
+ installSuccess: "Se instaló {name} {version}",
+ selectConnectionProvider: "Selecciona primero un proveedor de conexión",
+ connectionProvider: "Proveedor de conexión",
+ newConnection: "Nueva",
+ workbenches: "Áreas de trabajo",
+ filesystemProviders: "Proveedores de sistema de archivos",
+ open: "Abrir",
+ browse: "Explorar",
+ parentDirectory: "Directorio superior",
+ filesystemUri: "URI del sistema de archivos",
+ refresh: "Actualizar",
+ loadingFiles: "Cargando archivos",
+ emptyDirectory: "Este directorio está vacío.",
+ loadMore: "Cargar más",
+ fileManagerHint: "Haz doble clic en un directorio para abrirlo o en un archivo para previsualizar hasta 256 KB.",
+ loadingPreview: "Cargando vista previa",
+ previewTruncated: "Vista previa truncada a 256 KB",
+ previewSelectedFileHint: "Haz doble clic en el archivo seleccionado para cargar su vista previa.",
+ entryCannotPreview: "No se puede previsualizar este elemento.",
+ providerNoReadCapability: "El proveedor no declara capacidad de lectura.",
+ filesystemUnavailable: "El sistema de archivos del plugin '{pluginId}/{providerId}' no está disponible",
+ loadingFilesystem: "Cargando sistema de archivos del plugin",
+ filesystemUnavailableFallback: "El sistema de archivos del plugin no está disponible",
+ workbenchUnavailable: "El área de trabajo del plugin '{pluginId}/{contributionId}' no está disponible",
+ loadingWorkbench: "Cargando área de trabajo del plugin",
+ workbenchUnavailableFallback: "El área de trabajo del plugin no está disponible",
+ pluginIncompatible: "El plugin no es compatible",
+ loadingTitle: "Cargando {title}",
+ },
auth: {
rateLimited: "Vuelva a intentarlo en {seconds} s",
setupTitle: "Configurar contraseña de acceso",
@@ -231,6 +325,7 @@ export default withEnglishFallback({
sqlOpenFailed: "Error al abrir el archivo: {message}",
sqlSaveFailed: "Error al guardar el archivo: {message}",
driverManager: "Administrador de drivers",
+ pluginCenter: "Centro de plugins",
updatableDriverCount: "Cantidad de drivers actualizables",
mcpUpdateAvailable: "Actualización del servidor MCP disponible",
blockDangerousRedisCommands: "Bloquear comandos peligrosos",
@@ -815,6 +910,9 @@ export default withEnglishFallback({
databaseCategoryTimeseries: "Temporal",
databaseCategoryMq: "Colas",
databaseCategoryRegistryConfig: "Registro/config.",
+ databaseCategoryPlugins: "Plugins",
+ pluginProviderUnavailable: "Este proveedor de conexión del plugin no está disponible. Reinstala o habilita el plugin e inténtalo de nuevo.",
+ pluginRequiredField: "{field} es obligatorio",
jdbcConnection: "Conexión JDBC",
iconView: "Vista de íconos",
listView: "Vista de lista",
@@ -7498,6 +7596,13 @@ export default withEnglishFallback({
localInstall: "Instalación local",
localInstallHint: "Para entornos sin acceso a internet: en otro equipo con conexión, descarga el dbx-jdbc-plugin-*.zip oficial desde la página de Releases de DBX en GitHub, transfiérelo a esta máquina y selecciónalo; no se necesita acceso a la red en este equipo.",
runtimeDrivers: "Runtime",
+ pluginsTab: "Plugins",
+ pluginContributionsTitle: "Contribuciones de frontend de plugins",
+ pluginContributionsDescription: "Los plugins instalados pueden describir formularios de conexión y otras contribuciones de UI sin inyectar código en la ventana principal de DBX.",
+ noInstalledPlugins: "No hay plugins instalados",
+ noInstalledPluginsDescription: "Instala un plugin de backend para previsualizar aquí sus contribuciones de frontend.",
+ pluginHostPreview: "Vista previa de la API del host",
+ pluginNoFrontendContribution: "Este plugin aún no declara ninguna contribución de frontend.",
storageTab: "Almacenamiento",
runtimeTitle: "Runtime de drivers",
runtimeHint: "Las métricas de runtime se recopilan solo mientras este panel está abierto.",
diff --git a/apps/desktop/src/i18n/locales/it.ts b/apps/desktop/src/i18n/locales/it.ts
index 11b2acf9fa..e7aedd3ee8 100644
--- a/apps/desktop/src/i18n/locales/it.ts
+++ b/apps/desktop/src/i18n/locales/it.ts
@@ -165,6 +165,100 @@ export default withEnglishFallback({
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "Centro plugin",
+ centerDescription: "Scopri funzionalità DBX opzionali in un marketplace unico e gestisci separatamente plugin installati e repository.",
+ marketplace: "Marketplace",
+ installed: "Installati",
+ settings: "Impostazioni",
+ searchMarketplace: "Cerca plugin, autori o tag",
+ allRepositories: "Tutti i repository",
+ loadingMarketplace: "Caricamento marketplace plugin",
+ noMarketplacePlugins: "Nessun plugin da mostrare",
+ noMarketplacePluginsDescription: "Configura un repository nelle Impostazioni oppure modifica ricerca e filtri.",
+ verified: "Verificato",
+ noDescription: "Nessuna descrizione.",
+ permissionsCount: "{count} autorizzazioni",
+ unsupportedTarget: "Non disponibile per {target}",
+ installedVersion: "Installato v{version}",
+ licenseUnknown: "Licenza non specificata",
+ marketplaceStatus: { install: "Installa", installed: "Installato", update: "Aggiorna", unsupported: "Non supportato" },
+ updateSuccess: "{name} aggiornato alla versione {version}",
+ noInstalledPlugins: "Nessun plugin installato",
+ browseMarketplace: "Sfoglia Marketplace",
+ localInstallTitle: "Installa un pacchetto locale",
+ localInstallDescription: "Installa un .dbxp firmato ottenuto fuori dal marketplace. DBX verifica automaticamente integrità e firma del repository.",
+ repositoriesTitle: "Repository dei plugin",
+ repositoriesDescription: "La sorgente ufficiale è gestita da DBX. I repository self-hosted o di terze parti usano le impostazioni di attendibilità del repository.",
+ repositoryFieldsRequired: "ID, nome e URL del catalogo sono obbligatori",
+ repositorySaved: "Repository plugin '{name}' salvato",
+ removeRepositoryConfirm: "Rimuovere il repository plugin '{name}'? I plugin installati resteranno disponibili.",
+ repositoryRemoved: "Repository plugin '{name}' rimosso",
+ repositoryKind: { official: "Ufficiale", custom: "Personalizzato", enterprise: "Enterprise" },
+ repositoryNotConfigured: "URL del catalogo non configurato in questa build",
+ enabled: "Abilitato",
+ disabled: "Disabilitato",
+ enable: "Abilita",
+ disable: "Disabilita",
+ repositoryIdPlaceholder: "ID repository",
+ repositoryNamePlaceholder: "Nome repository",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "Aggiungi repository",
+ signedPackagesVerifiedAutomatically: "Integrità e firme verificate automaticamente",
+ developerOptionsTitle: "Opzioni di terze parti e sviluppo",
+ developerOptionsDescription: "Nascoste per impostazione predefinita. Usale solo per repository personalizzati o sviluppo locale di plugin.",
+ developerOptionsWarning: "I plugin non firmati possono eseguire codice nativo con i permessi dell'utente corrente. Abilitali solo per pacchetti creati e verificati da te.",
+ connectionManagedInDialog: "Crea e modifica le connessioni nella finestra unificata Nuova connessione. Questa pagina gestisce solo i plugin e ne mostra le capacità.",
+ allowUnsignedDevelopmentPackage: "Consenti pacchetti di sviluppo non firmati",
+ allowUnsignedDevelopmentPackageDescription: "Si applica solo all'installazione manuale di .dbxp e non riduce mai la verifica del marketplace ufficiale.",
+ installPackage: "Installa .dbxp",
+ packageFileType: "Pacchetto plugin DBX",
+ repositoryTrustTitle: "Attendibilità del repository personalizzato",
+ repositoryTrustDescription: "Il gestore del repository firma tutti i pacchetti approvati con una singola chiave. Ottieni la chiave pubblica tramite un canale separato e attendibile.",
+ repositoryKeyAdded: "Chiave di firma del repository '{keyId}' aggiunta",
+ removeRepositoryKeyConfirm: "Rimuovere la chiave di firma del repository '{keyId}'? I plugin installati resteranno installati.",
+ repositoryKeyRemoved: "Chiave di firma del repository '{keyId}' rimossa",
+ repositoryKeyIdPlaceholder: "ID chiave repository",
+ repositoryPublicKeyPlaceholder: "Chiave pubblica Ed25519 Base64 del repository (32 byte)",
+ trustRepository: "Considera attendibile il repository",
+ noInstalledPluginsDescription: "Installa un .dbxp firmato oppure abilita esplicitamente i pacchetti non firmati per lo sviluppo locale.",
+ compatible: "Compatibile",
+ blocked: "Bloccato",
+ rollback: "Ripristina",
+ uninstall: "Disinstalla",
+ rollbackConfirm: "Ripristinare questo plugin alla versione installata precedente?",
+ rollbackSuccess: "Ripristinata la versione {version}",
+ uninstallConfirm: "Disinstallare {name}?",
+ uninstallSuccess: "{name} disinstallato",
+ installSuccess: "Installato {name} {version}",
+ selectConnectionProvider: "Seleziona prima un provider di connessione",
+ connectionProvider: "Provider di connessione",
+ newConnection: "Nuova",
+ workbenches: "Aree di lavoro",
+ filesystemProviders: "Provider del file system",
+ open: "Apri",
+ browse: "Sfoglia",
+ parentDirectory: "Directory superiore",
+ filesystemUri: "URI del file system",
+ refresh: "Aggiorna",
+ loadingFiles: "Caricamento file",
+ emptyDirectory: "Questa directory è vuota.",
+ loadMore: "Carica altro",
+ fileManagerHint: "Fai doppio clic su una directory per aprirla o su un file per visualizzare fino a 256 KB.",
+ loadingPreview: "Caricamento anteprima",
+ previewTruncated: "Anteprima troncata a 256 KB",
+ previewSelectedFileHint: "Fai doppio clic sul file selezionato per caricarne l'anteprima.",
+ entryCannotPreview: "Questo elemento non può essere visualizzato in anteprima.",
+ providerNoReadCapability: "Il provider non dichiara la capacità di lettura.",
+ filesystemUnavailable: "Il file system del plugin '{pluginId}/{providerId}' non è disponibile",
+ loadingFilesystem: "Caricamento file system del plugin",
+ filesystemUnavailableFallback: "Il file system del plugin non è disponibile",
+ workbenchUnavailable: "L'area di lavoro del plugin '{pluginId}/{contributionId}' non è disponibile",
+ loadingWorkbench: "Caricamento area di lavoro del plugin",
+ workbenchUnavailableFallback: "L'area di lavoro del plugin non è disponibile",
+ pluginIncompatible: "Il plugin non è compatibile",
+ loadingTitle: "Caricamento di {title}",
+ },
auth: {
rateLimited: "Riprova tra {seconds} s",
setupTitle: "Imposta la password di accesso",
@@ -230,6 +324,7 @@ export default withEnglishFallback({
sqlOpenFailed: "Impossibile aprire il file: {message}",
sqlSaveFailed: "Impossibile salvare il file: {message}",
driverManager: "Gestione Driver",
+ pluginCenter: "Centro plugin",
updatableDriverCount: "Driver aggiornabili",
mcpUpdateAvailable: "Aggiornamento server MCP disponibile",
blockDangerousRedisCommands: "Blocca comandi pericolosi",
@@ -814,6 +909,9 @@ export default withEnglishFallback({
databaseCategoryTimeseries: "Temporali",
databaseCategoryMq: "Code messaggi",
databaseCategoryRegistryConfig: "Registro/config.",
+ databaseCategoryPlugins: "Plugin",
+ pluginProviderUnavailable: "Questo provider di connessione del plugin non è disponibile. Reinstalla o abilita il plugin e riprova.",
+ pluginRequiredField: "{field} è obbligatorio",
jdbcConnection: "Connessione JDBC",
iconView: "Vista icone",
listView: "Vista elenco",
@@ -7499,6 +7597,13 @@ export default withEnglishFallback({
localInstallHint:
"Per ambienti completamente isolati: su un altro computer con accesso a internet, scarica il pacchetto ufficiale dbx-jdbc-plugin-*.zip dalla pagina Releases di DBX su GitHub, trasferiscilo su questa macchina e selezionalo: non è richiesto alcun accesso alla rete su questa macchina.",
runtimeDrivers: "Runtime",
+ pluginsTab: "Plugin",
+ pluginContributionsTitle: "Contributi frontend dei plugin",
+ pluginContributionsDescription: "I plugin installati possono descrivere moduli di connessione e altri contributi UI senza iniettare codice nella finestra principale di DBX.",
+ noInstalledPlugins: "Nessun plugin installato",
+ noInstalledPluginsDescription: "Installa un plugin backend per visualizzare qui l'anteprima dei suoi contributi frontend.",
+ pluginHostPreview: "Anteprima API host",
+ pluginNoFrontendContribution: "Questo plugin non dichiara ancora alcun contributo frontend.",
storageTab: "Archiviazione",
runtimeTitle: "Runtime del driver",
runtimeHint: "Le metriche di runtime vengono raccolte solo mentre questo pannello è aperto.",
diff --git a/apps/desktop/src/i18n/locales/ja.ts b/apps/desktop/src/i18n/locales/ja.ts
index f2fdfe0078..e1ed93303b 100644
--- a/apps/desktop/src/i18n/locales/ja.ts
+++ b/apps/desktop/src/i18n/locales/ja.ts
@@ -166,6 +166,100 @@ export default withEnglishFallback({
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "プラグインセンター",
+ centerDescription: "統合マーケットプレイスで DBX の追加機能を探し、インストール済みプラグインとリポジトリを個別に管理します。",
+ marketplace: "マーケットプレイス",
+ installed: "インストール済み",
+ settings: "設定",
+ searchMarketplace: "プラグイン、発行者、タグを検索",
+ allRepositories: "すべてのリポジトリ",
+ loadingMarketplace: "プラグインマーケットプレイスを読み込み中",
+ noMarketplacePlugins: "表示できるプラグインがありません",
+ noMarketplacePluginsDescription: "設定でリポジトリを追加するか、検索条件を変更してください。",
+ verified: "認証済み",
+ noDescription: "説明はありません。",
+ permissionsCount: "権限 {count} 件",
+ unsupportedTarget: "{target} では利用できません",
+ installedVersion: "インストール済み v{version}",
+ licenseUnknown: "ライセンス未指定",
+ marketplaceStatus: { install: "インストール", installed: "インストール済み", update: "更新", unsupported: "非対応" },
+ updateSuccess: "{name} を {version} に更新しました",
+ noInstalledPlugins: "プラグインはまだありません",
+ browseMarketplace: "マーケットプレイスを見る",
+ localInstallTitle: "ローカルパッケージをインストール",
+ localInstallDescription: "マーケットプレイス外で入手した署名済み .dbxp をインストールします。DBX が整合性とリポジトリ署名を自動検証します。",
+ repositoriesTitle: "プラグインリポジトリ",
+ repositoriesDescription: "公式ソースは DBX が管理します。セルフホストまたは第三者のリポジトリでは、リポジトリの信頼設定を使用します。",
+ repositoryFieldsRequired: "リポジトリ ID、名前、カタログ URL は必須です",
+ repositorySaved: "プラグインリポジトリ「{name}」を保存しました",
+ removeRepositoryConfirm: "プラグインリポジトリ「{name}」を削除しますか?インストール済みプラグインは残ります。",
+ repositoryRemoved: "プラグインリポジトリ「{name}」を削除しました",
+ repositoryKind: { official: "公式", custom: "カスタム", enterprise: "エンタープライズ" },
+ repositoryNotConfigured: "このビルドにはカタログ URL が設定されていません",
+ enabled: "有効",
+ disabled: "無効",
+ enable: "有効化",
+ disable: "無効化",
+ repositoryIdPlaceholder: "リポジトリ ID",
+ repositoryNamePlaceholder: "リポジトリ名",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "リポジトリを追加",
+ signedPackagesVerifiedAutomatically: "整合性と署名を自動検証",
+ developerOptionsTitle: "サードパーティと開発者向けオプション",
+ developerOptionsDescription: "通常は非表示です。カスタムリポジトリまたはローカルプラグイン開発時のみ使用してください。",
+ developerOptionsWarning: "未署名プラグインは現在のユーザー権限でネイティブコードを実行できます。自分でビルドし信頼できる開発パッケージにのみ有効化してください。",
+ connectionManagedInDialog: "接続の作成と編集は共通の「新しい接続」ダイアログで行います。この画面はプラグイン管理と機能表示専用です。",
+ allowUnsignedDevelopmentPackage: "未署名の開発パッケージを許可",
+ allowUnsignedDevelopmentPackageDescription: "手動での .dbxp インストールにのみ適用され、公式マーケットプレイスの検証は緩和されません。",
+ installPackage: ".dbxp をインストール",
+ packageFileType: "DBX プラグインパッケージ",
+ repositoryTrustTitle: "カスタムリポジトリの信頼",
+ repositoryTrustDescription: "カスタムリポジトリ運営者は、承認したすべてのパッケージを単一のリポジトリ鍵で署名します。公開鍵は別の信頼できる経路で取得してください。",
+ repositoryKeyAdded: "リポジトリ署名鍵「{keyId}」を追加しました",
+ removeRepositoryKeyConfirm: "リポジトリ署名鍵「{keyId}」を削除しますか?インストール済みプラグインは残ります。",
+ repositoryKeyRemoved: "リポジトリ署名鍵「{keyId}」を削除しました",
+ repositoryKeyIdPlaceholder: "リポジトリ鍵 ID",
+ repositoryPublicKeyPlaceholder: "Base64 Ed25519 リポジトリ公開鍵(32 バイト)",
+ trustRepository: "リポジトリを信頼",
+ noInstalledPluginsDescription: "署名済み .dbxp をインストールするか、ローカル開発時のみ未署名パッケージを明示的に許可してください。",
+ compatible: "互換",
+ blocked: "ブロック済み",
+ rollback: "ロールバック",
+ uninstall: "アンインストール",
+ rollbackConfirm: "このプラグインを前のインストール済みバージョンへ戻しますか?",
+ rollbackSuccess: "{version} にロールバックしました",
+ uninstallConfirm: "{name} をアンインストールしますか?",
+ uninstallSuccess: "{name} をアンインストールしました",
+ installSuccess: "{name} {version} をインストールしました",
+ selectConnectionProvider: "先に接続プロバイダーを選択してください",
+ connectionProvider: "接続プロバイダー",
+ newConnection: "新規",
+ workbenches: "ワークベンチ",
+ filesystemProviders: "ファイルシステムプロバイダー",
+ open: "開く",
+ browse: "参照",
+ parentDirectory: "親ディレクトリ",
+ filesystemUri: "ファイルシステム URI",
+ refresh: "更新",
+ loadingFiles: "ファイルを読み込み中",
+ emptyDirectory: "このディレクトリは空です。",
+ loadMore: "さらに読み込む",
+ fileManagerHint: "ディレクトリをダブルクリックして開くか、ファイルをダブルクリックして最大 256 KB までプレビューします。",
+ loadingPreview: "プレビューを読み込み中",
+ previewTruncated: "プレビューは 256 KB で切り捨てられました",
+ previewSelectedFileHint: "選択したファイルをダブルクリックしてプレビューを読み込みます。",
+ entryCannotPreview: "この項目はプレビューできません。",
+ providerNoReadCapability: "プロバイダーは読み取り機能を宣言していません。",
+ filesystemUnavailable: "プラグインファイルシステム「{pluginId}/{providerId}」は利用できません",
+ loadingFilesystem: "プラグインファイルシステムを読み込み中",
+ filesystemUnavailableFallback: "プラグインファイルシステムは利用できません",
+ workbenchUnavailable: "プラグインワークベンチ「{pluginId}/{contributionId}」は利用できません",
+ loadingWorkbench: "プラグインワークベンチを読み込み中",
+ workbenchUnavailableFallback: "プラグインワークベンチは利用できません",
+ pluginIncompatible: "プラグインに互換性がありません",
+ loadingTitle: "{title} を読み込み中",
+ },
auth: {
rateLimited: "{seconds} 秒後に再試行してください",
setupTitle: "アクセスパスワードを設定",
@@ -231,6 +325,7 @@ export default withEnglishFallback({
sqlOpenFailed: "ファイルを開けませんでした: {message}",
sqlSaveFailed: "ファイルの保存に失敗しました: {message}",
driverManager: "ドライバーマネージャー",
+ pluginCenter: "プラグインセンター",
updatableDriverCount: "更新可能なドライバー数",
mcpUpdateAvailable: "MCPサーバーの更新があります",
blockDangerousRedisCommands: "危険なコマンドをブロック",
@@ -780,6 +875,9 @@ export default withEnglishFallback({
databaseCategoryTimeseries: "時系列DB",
databaseCategoryMq: "メッセージキュー",
databaseCategoryRegistryConfig: "登録/設定",
+ databaseCategoryPlugins: "プラグイン",
+ pluginProviderUnavailable: "このプラグイン接続プロバイダーは利用できません。プラグインを再インストールまたは有効化してください。",
+ pluginRequiredField: "{field} は必須です",
jdbcConnection: "JDBC 接続",
iconView: "アイコン表示",
listView: "リスト表示",
@@ -7550,6 +7648,13 @@ export default withEnglishFallback({
localInstall: "ローカルインストール",
localInstallHint: "完全に隔離されたネットワーク環境の場合:インターネットに接続できる別のコンピューターで DBX の GitHub Releases ページから公式の dbx-jdbc-plugin-*.zip をダウンロードし、このマシンに転送して選択してください。このマシンはネットワークに接続する必要はありません。",
runtimeDrivers: "ランタイム",
+ pluginsTab: "プラグイン",
+ pluginContributionsTitle: "フロントエンドプラグインコントリビューション",
+ pluginContributionsDescription: "インストール済みプラグインは、DBX のメインウィンドウにコードを注入せず、接続フォームやその他の UI コントリビューションを記述できます。",
+ noInstalledPlugins: "インストール済みプラグインはありません",
+ noInstalledPluginsDescription: "バックエンドプラグインをインストールすると、ここでフロントエンドコントリビューションをプレビューできます。",
+ pluginHostPreview: "ホスト API プレビュー",
+ pluginNoFrontendContribution: "このプラグインはまだフロントエンドコントリビューションを宣言していません。",
storageTab: "ストレージ",
runtimeTitle: "ドライバーランタイム",
runtimeHint: "ランタイムメトリクスはこのパネルが開いている間のみ収集されます。",
diff --git a/apps/desktop/src/i18n/locales/ko.ts b/apps/desktop/src/i18n/locales/ko.ts
index ada457d12d..976e9e9296 100644
--- a/apps/desktop/src/i18n/locales/ko.ts
+++ b/apps/desktop/src/i18n/locales/ko.ts
@@ -165,6 +165,109 @@ export default withEnglishFallback({
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "플러그인 센터",
+ centerDescription: "하나의 마켓플레이스에서 선택적 DBX 기능을 찾고, 설치된 플러그인과 저장소를 별도로 관리하세요.",
+ marketplace: "마켓플레이스",
+ installed: "설치됨",
+ settings: "설정",
+ searchMarketplace: "플러그인, 게시자 또는 태그 검색",
+ allRepositories: "모든 저장소",
+ loadingMarketplace: "플러그인 마켓플레이스 로드 중",
+ noMarketplacePlugins: "표시할 플러그인이 없습니다",
+ noMarketplacePluginsDescription: "설정에서 저장소를 구성하거나 현재 검색 및 필터를 조정하세요.",
+ verified: "검증됨",
+ noDescription: "설명이 제공되지 않았습니다.",
+ permissionsCount: "권한 {count}개",
+ unsupportedTarget: "{target}에서는 사용할 수 없음",
+ installedVersion: "설치된 버전 v{version}",
+ licenseUnknown: "라이선스 미지정",
+ marketplaceStatus: {
+ install: "설치",
+ installed: "설치됨",
+ update: "업데이트",
+ unsupported: "지원되지 않음",
+ },
+ updateSuccess: "{name} 업데이트 완료: {version}",
+ noInstalledPlugins: "설치된 플러그인이 없습니다",
+ browseMarketplace: "마켓플레이스 둘러보기",
+ localInstallTitle: "로컬 패키지 설치",
+ localInstallDescription: "마켓플레이스 외부에서 받은 서명된 .dbxp를 설치합니다. DBX가 패키지 무결성과 저장소 서명을 자동으로 검증합니다.",
+ repositoriesTitle: "플러그인 저장소",
+ repositoriesDescription: "공식 소스는 DBX에서 관리합니다. 자체 호스팅 또는 타사 저장소는 저장소 신뢰 설정을 사용합니다.",
+ repositoryFieldsRequired: "저장소 ID, 이름 및 카탈로그 URL은 필수입니다",
+ repositorySaved: "플러그인 저장소 '{name}'을 저장했습니다",
+ removeRepositoryConfirm: "플러그인 저장소 '{name}'을 제거하시겠습니까? 설치된 플러그인은 유지됩니다.",
+ repositoryRemoved: "플러그인 저장소 '{name}'을 제거했습니다",
+ repositoryKind: {
+ official: "공식",
+ custom: "사용자 지정",
+ enterprise: "엔터프라이즈",
+ },
+ repositoryNotConfigured: "이 빌드에는 카탈로그 URL이 구성되어 있지 않습니다",
+ enabled: "활성화됨",
+ disabled: "비활성화됨",
+ enable: "활성화",
+ disable: "비활성화",
+ repositoryIdPlaceholder: "저장소 ID",
+ repositoryNamePlaceholder: "저장소 이름",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "저장소 추가",
+ signedPackagesVerifiedAutomatically: "서명이 자동으로 검증됩니다",
+ developerOptionsTitle: "타사 및 개발자 옵션",
+ developerOptionsDescription: "기본적으로 숨겨져 있습니다. 사용자 지정 저장소 또는 로컬 플러그인 개발에만 사용하세요.",
+ developerOptionsWarning: "서명되지 않은 플러그인은 사용자 권한으로 네이티브 코드를 실행할 수 있습니다. 직접 빌드하고 신뢰하는 패키지에만 활성화하세요.",
+ connectionManagedInDialog: "통합된 새 연결 대화상자에서 연결을 만들고 편집하세요. 이 페이지는 플러그인 관리와 기능 표시에만 사용됩니다.",
+ allowUnsignedDevelopmentPackage: "서명되지 않은 개발 패키지 허용",
+ allowUnsignedDevelopmentPackageDescription: "수동 .dbxp 설치에만 적용되며 공식 마켓플레이스 검증은 완화하지 않습니다.",
+ installPackage: ".dbxp 설치",
+ packageFileType: "DBX 플러그인 패키지",
+ repositoryTrustTitle: "사용자 지정 저장소 신뢰",
+ repositoryTrustDescription: "사용자 지정 저장소 운영자는 하나의 저장소 키로 승인된 모든 패키지에 서명합니다. 별도의 신뢰할 수 있는 채널을 통해 공개 키를 받으세요.",
+ repositoryKeyAdded: "저장소 서명 키 '{keyId}'을 추가했습니다",
+ removeRepositoryKeyConfirm: "저장소 서명 키 '{keyId}'을 제거하시겠습니까? 설치된 플러그인은 유지됩니다.",
+ repositoryKeyRemoved: "저장소 서명 키 '{keyId}'을 제거했습니다",
+ repositoryKeyIdPlaceholder: "저장소 키 ID",
+ repositoryPublicKeyPlaceholder: "Base64 Ed25519 저장소 공개 키(32바이트)",
+ trustRepository: "저장소 신뢰",
+ noInstalledPluginsDescription: "서명된 .dbxp를 설치하거나 로컬 개발용 서명되지 않은 패키지를 명시적으로 허용하세요.",
+ compatible: "호환됨",
+ blocked: "차단됨",
+ rollback: "롤백",
+ uninstall: "제거",
+ rollbackConfirm: "이 플러그인을 이전 설치 버전으로 되돌리시겠습니까?",
+ rollbackSuccess: "{version}(으)로 롤백했습니다",
+ uninstallConfirm: "{name}을 제거하시겠습니까?",
+ uninstallSuccess: "{name}을 제거했습니다",
+ installSuccess: "{name} {version}을 설치했습니다",
+ selectConnectionProvider: "먼저 연결 제공자를 선택하세요",
+ connectionProvider: "연결 제공자",
+ newConnection: "새 연결",
+ workbenches: "워크벤치",
+ filesystemProviders: "파일 시스템 제공자",
+ open: "열기",
+ browse: "찾아보기",
+ parentDirectory: "상위 디렉터리",
+ filesystemUri: "파일 시스템 URI",
+ refresh: "새로 고침",
+ loadingFiles: "파일 로드 중",
+ emptyDirectory: "이 디렉터리는 비어 있습니다.",
+ loadMore: "더 불러오기",
+ fileManagerHint: "디렉터리를 두 번 클릭해 열거나 파일을 두 번 클릭해 최대 256KB까지 미리 봅니다.",
+ loadingPreview: "미리보기 로드 중",
+ previewTruncated: "미리보기가 256KB에서 잘렸습니다",
+ previewSelectedFileHint: "선택한 파일을 두 번 클릭해 미리보기를 로드하세요.",
+ entryCannotPreview: "이 항목은 미리 볼 수 없습니다.",
+ providerNoReadCapability: "제공자가 읽기 기능을 선언하지 않았습니다.",
+ filesystemUnavailable: "플러그인 파일 시스템 '{pluginId}/{providerId}'을 사용할 수 없습니다",
+ loadingFilesystem: "플러그인 파일 시스템 로드 중",
+ filesystemUnavailableFallback: "플러그인 파일 시스템을 사용할 수 없습니다",
+ workbenchUnavailable: "플러그인 워크벤치 '{pluginId}/{contributionId}'을 사용할 수 없습니다",
+ loadingWorkbench: "플러그인 워크벤치 로드 중",
+ workbenchUnavailableFallback: "플러그인 워크벤치를 사용할 수 없습니다",
+ pluginIncompatible: "플러그인이 호환되지 않습니다",
+ loadingTitle: "{title} 로드 중",
+ },
auth: {
rateLimited: "{seconds}초 후에 다시 시도해 주세요",
setupTitle: "접속 비밀번호 설정",
@@ -7222,6 +7325,13 @@ export default withEnglishFallback({
localInstall: "로컬 설치",
localInstallHint: "완전한 폐쇄망 환경의 경우: 인터넷에 연결된 다른 컴퓨터에서 DBX GitHub Releases 페이지의 공식 dbx-jdbc-plugin-*.zip을 다운로드한 뒤 이 기기로 옮겨 선택하세요. 이 기기는 네트워크에 연결할 필요가 없습니다.",
runtimeDrivers: "런타임",
+ pluginsTab: "플러그인",
+ pluginContributionsTitle: "프론트엔드 플러그인 확장",
+ pluginContributionsDescription: "설치된 플러그인은 DBX 기본 창에 코드를 주입하지 않고 연결 양식과 기타 UI 확장을 선언할 수 있습니다.",
+ noInstalledPlugins: "설치된 플러그인이 없습니다",
+ noInstalledPluginsDescription: "백엔드 플러그인을 설치하면 여기에서 프론트엔드 확장을 미리 볼 수 있습니다.",
+ pluginHostPreview: "호스트 API 미리보기",
+ pluginNoFrontendContribution: "이 플러그인은 아직 프론트엔드 확장을 선언하지 않았습니다.",
storageTab: "저장 공간",
runtimeTitle: "드라이버 런타임",
runtimeHint: "런타임 메트릭은 이 패널이 열려 있는 동안에만 수집됩니다.",
diff --git a/apps/desktop/src/i18n/locales/pt-BR.ts b/apps/desktop/src/i18n/locales/pt-BR.ts
index 728a632db1..b37016d37c 100644
--- a/apps/desktop/src/i18n/locales/pt-BR.ts
+++ b/apps/desktop/src/i18n/locales/pt-BR.ts
@@ -166,6 +166,100 @@ export default withEnglishFallback({
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "Central de plugins",
+ centerDescription: "Descubra recursos opcionais do DBX em um marketplace unificado e gerencie separadamente plugins instalados e repositórios.",
+ marketplace: "Marketplace",
+ installed: "Instalados",
+ settings: "Configurações",
+ searchMarketplace: "Buscar plugins, publicadores ou tags",
+ allRepositories: "Todos os repositórios",
+ loadingMarketplace: "Carregando marketplace de plugins",
+ noMarketplacePlugins: "Nenhum plugin para exibir",
+ noMarketplacePluginsDescription: "Configure um repositório nas Configurações ou ajuste a busca e os filtros.",
+ verified: "Verificado",
+ noDescription: "Nenhuma descrição fornecida.",
+ permissionsCount: "{count} permissões",
+ unsupportedTarget: "Indisponível para {target}",
+ installedVersion: "Instalado v{version}",
+ licenseUnknown: "Licença não informada",
+ marketplaceStatus: { install: "Instalar", installed: "Instalado", update: "Atualizar", unsupported: "Não compatível" },
+ updateSuccess: "{name} atualizado para {version}",
+ noInstalledPlugins: "Nenhum plugin instalado",
+ browseMarketplace: "Explorar Marketplace",
+ localInstallTitle: "Instalar pacote local",
+ localInstallDescription: "Instale um .dbxp assinado obtido fora do marketplace. O DBX verifica automaticamente a integridade e a assinatura do repositório.",
+ repositoriesTitle: "Repositórios de plugins",
+ repositoriesDescription: "A fonte oficial é gerenciada pelo DBX. Repositórios próprios ou de terceiros usam as configurações de confiança do repositório.",
+ repositoryFieldsRequired: "ID, nome e URL do catálogo são obrigatórios",
+ repositorySaved: "Repositório de plugins '{name}' salvo",
+ removeRepositoryConfirm: "Remover o repositório de plugins '{name}'? Os plugins instalados permanecerão.",
+ repositoryRemoved: "Repositório de plugins '{name}' removido",
+ repositoryKind: { official: "Oficial", custom: "Personalizado", enterprise: "Empresarial" },
+ repositoryNotConfigured: "A URL do catálogo não está configurada nesta compilação",
+ enabled: "Ativado",
+ disabled: "Desativado",
+ enable: "Ativar",
+ disable: "Desativar",
+ repositoryIdPlaceholder: "ID do repositório",
+ repositoryNamePlaceholder: "Nome do repositório",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "Adicionar repositório",
+ signedPackagesVerifiedAutomatically: "Integridade e assinaturas verificadas automaticamente",
+ developerOptionsTitle: "Opções de terceiros e desenvolvimento",
+ developerOptionsDescription: "Ocultas por padrão. Use somente para repositórios personalizados ou desenvolvimento local de plugins.",
+ developerOptionsWarning: "Plugins sem assinatura podem executar código nativo com suas permissões de usuário. Ative apenas para pacotes criados e verificados por você.",
+ connectionManagedInDialog: "Crie e edite conexões na janela unificada Nova conexão. Esta página apenas gerencia plugins e exibe seus recursos.",
+ allowUnsignedDevelopmentPackage: "Permitir pacotes de desenvolvimento não assinados",
+ allowUnsignedDevelopmentPackageDescription: "Afeta apenas a instalação manual de .dbxp e nunca reduz a verificação do marketplace oficial.",
+ installPackage: "Instalar .dbxp",
+ packageFileType: "Pacote de plugin do DBX",
+ repositoryTrustTitle: "Confiança do repositório personalizado",
+ repositoryTrustDescription: "O operador do repositório assina todos os pacotes aprovados com uma única chave. Obtenha a chave pública por um canal separado e confiável.",
+ repositoryKeyAdded: "Chave de assinatura do repositório '{keyId}' adicionada",
+ removeRepositoryKeyConfirm: "Remover a chave de assinatura do repositório '{keyId}'? Os plugins instalados permanecerão instalados.",
+ repositoryKeyRemoved: "Chave de assinatura do repositório '{keyId}' removida",
+ repositoryKeyIdPlaceholder: "ID da chave do repositório",
+ repositoryPublicKeyPlaceholder: "Chave pública Ed25519 do repositório em Base64 (32 bytes)",
+ trustRepository: "Confiar no repositório",
+ noInstalledPluginsDescription: "Instale um .dbxp assinado ou permita explicitamente pacotes não assinados para desenvolvimento local.",
+ compatible: "Compatível",
+ blocked: "Bloqueado",
+ rollback: "Reverter",
+ uninstall: "Desinstalar",
+ rollbackConfirm: "Reverter este plugin para a versão instalada anterior?",
+ rollbackSuccess: "Revertido para {version}",
+ uninstallConfirm: "Desinstalar {name}?",
+ uninstallSuccess: "{name} desinstalado",
+ installSuccess: "{name} {version} instalado",
+ selectConnectionProvider: "Selecione primeiro um provedor de conexão",
+ connectionProvider: "Provedor de conexão",
+ newConnection: "Nova",
+ workbenches: "Áreas de trabalho",
+ filesystemProviders: "Provedores de sistema de arquivos",
+ open: "Abrir",
+ browse: "Explorar",
+ parentDirectory: "Diretório superior",
+ filesystemUri: "URI do sistema de arquivos",
+ refresh: "Atualizar",
+ loadingFiles: "Carregando arquivos",
+ emptyDirectory: "Este diretório está vazio.",
+ loadMore: "Carregar mais",
+ fileManagerHint: "Clique duas vezes em um diretório para abri-lo ou em um arquivo para visualizar até 256 KB.",
+ loadingPreview: "Carregando visualização",
+ previewTruncated: "Visualização truncada em 256 KB",
+ previewSelectedFileHint: "Clique duas vezes no arquivo selecionado para carregar sua visualização.",
+ entryCannotPreview: "Este item não pode ser visualizado.",
+ providerNoReadCapability: "O provedor não declara capacidade de leitura.",
+ filesystemUnavailable: "O sistema de arquivos do plugin '{pluginId}/{providerId}' não está disponível",
+ loadingFilesystem: "Carregando sistema de arquivos do plugin",
+ filesystemUnavailableFallback: "O sistema de arquivos do plugin não está disponível",
+ workbenchUnavailable: "A área de trabalho do plugin '{pluginId}/{contributionId}' não está disponível",
+ loadingWorkbench: "Carregando área de trabalho do plugin",
+ workbenchUnavailableFallback: "A área de trabalho do plugin não está disponível",
+ pluginIncompatible: "O plugin não é compatível",
+ loadingTitle: "Carregando {title}",
+ },
auth: {
rateLimited: "Tente novamente em {seconds}s",
setupTitle: "Configurar senha de acesso",
@@ -231,6 +325,7 @@ export default withEnglishFallback({
sqlOpenFailed: "Falha ao abrir o arquivo: {message}",
sqlSaveFailed: "Falha ao salvar o arquivo: {message}",
driverManager: "Gerenciador de Drivers",
+ pluginCenter: "Central de plugins",
updatableDriverCount: "Quantidade de drivers atualizáveis",
mcpUpdateAvailable: "Atualização do servidor MCP disponível",
blockDangerousRedisCommands: "Bloquear comandos perigosos",
@@ -815,6 +910,9 @@ export default withEnglishFallback({
databaseCategoryTimeseries: "Séries",
databaseCategoryMq: "Filas",
databaseCategoryRegistryConfig: "Registro/config.",
+ databaseCategoryPlugins: "Plugins",
+ pluginProviderUnavailable: "Este provedor de conexão do plugin não está disponível. Reinstale ou habilite o plugin e tente novamente.",
+ pluginRequiredField: "{field} é obrigatório",
jdbcConnection: "Conexão JDBC",
iconView: "Visualização em ícones",
listView: "Visualização em lista",
@@ -7500,6 +7598,13 @@ export default withEnglishFallback({
localInstall: "Instalação local",
localInstallHint: "Para ambientes sem acesso à internet: em outro computador com internet, baixe o dbx-jdbc-plugin-*.zip oficial na página de Releases do DBX no GitHub, transfira-o para esta máquina e selecione-o — nenhum acesso à rede é necessário aqui.",
runtimeDrivers: "Runtime",
+ pluginsTab: "Plugins",
+ pluginContributionsTitle: "Contribuições de frontend de plugins",
+ pluginContributionsDescription: "Plugins instalados podem descrever formulários de conexão e outras contribuições de UI sem injetar código na janela principal do DBX.",
+ noInstalledPlugins: "Nenhum plugin instalado",
+ noInstalledPluginsDescription: "Instale um plugin de backend para visualizar aqui suas contribuições de frontend.",
+ pluginHostPreview: "Prévia da API do host",
+ pluginNoFrontendContribution: "Este plugin ainda não declara uma contribuição de frontend.",
storageTab: "Armazenamento",
runtimeTitle: "Runtime do driver",
runtimeHint: "As métricas de runtime são coletadas apenas enquanto este painel está aberto.",
diff --git a/apps/desktop/src/i18n/locales/zh-CN.ts b/apps/desktop/src/i18n/locales/zh-CN.ts
index 00793da293..47b92df2d4 100644
--- a/apps/desktop/src/i18n/locales/zh-CN.ts
+++ b/apps/desktop/src/i18n/locales/zh-CN.ts
@@ -89,6 +89,100 @@ export default withEnglishFallback({
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "插件中心",
+ centerDescription: "在统一商店中发现可选的 DBX 能力,并分别管理已安装插件和插件仓库。",
+ marketplace: "插件商店",
+ installed: "已安装",
+ settings: "设置",
+ searchMarketplace: "搜索插件、发布者或标签",
+ allRepositories: "全部仓库",
+ loadingMarketplace: "正在加载插件商店",
+ noMarketplacePlugins: "暂无可展示的插件",
+ noMarketplacePluginsDescription: "请在设置中配置插件仓库,或调整当前搜索与筛选条件。",
+ verified: "已认证",
+ noDescription: "开发者未提供说明。",
+ permissionsCount: "{count} 项权限",
+ unsupportedTarget: "暂不支持 {target}",
+ installedVersion: "已安装 v{version}",
+ licenseUnknown: "未声明许可证",
+ marketplaceStatus: { install: "安装", installed: "已安装", update: "更新", unsupported: "不支持" },
+ updateSuccess: "已将 {name} 更新到 {version}",
+ noInstalledPlugins: "尚未安装插件",
+ browseMarketplace: "浏览插件商店",
+ localInstallTitle: "安装本地插件包",
+ localInstallDescription: "安装从插件商店之外获得的已签名 .dbxp,DBX 会自动校验包完整性和仓库签名。",
+ repositoriesTitle: "插件仓库",
+ repositoriesDescription: "官方源由 DBX 管理;添加自托管或第三方仓库后,才会显示对应的仓库信任设置。",
+ repositoryFieldsRequired: "仓库 ID、名称和目录地址不能为空",
+ repositorySaved: "已保存插件仓库“{name}”",
+ removeRepositoryConfirm: "移除插件仓库“{name}”?已安装插件不会被卸载。",
+ repositoryRemoved: "已移除插件仓库“{name}”",
+ repositoryKind: { official: "官方", custom: "自定义", enterprise: "企业" },
+ repositoryNotConfigured: "当前构建未配置目录地址",
+ enabled: "已启用",
+ disabled: "已停用",
+ enable: "启用",
+ disable: "停用",
+ repositoryIdPlaceholder: "仓库 ID",
+ repositoryNamePlaceholder: "仓库名称",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "添加仓库",
+ signedPackagesVerifiedAutomatically: "自动校验完整性和签名",
+ developerOptionsTitle: "第三方与开发者选项",
+ developerOptionsDescription: "默认隐藏,仅在接入第三方仓库或开发本地插件时使用。",
+ developerOptionsWarning: "未签名插件可以使用当前用户权限运行原生代码。只应为自己构建且确认可信的开发包开启。",
+ connectionManagedInDialog: "连接统一在“新建连接”对话框中创建和编辑;这里仅负责插件管理与能力展示。",
+ allowUnsignedDevelopmentPackage: "允许安装未签名开发包",
+ allowUnsignedDevelopmentPackageDescription: "仅影响手动安装的 .dbxp,不会放宽官方插件商店的签名校验。",
+ installPackage: "安装 .dbxp",
+ packageFileType: "DBX 插件包",
+ repositoryTrustTitle: "自定义仓库信任",
+ repositoryTrustDescription: "自定义仓库运营方使用一个仓库密钥签署其审核通过的插件。请通过独立可信渠道获取仓库公钥。",
+ repositoryKeyAdded: "已添加仓库签名密钥“{keyId}”",
+ removeRepositoryKeyConfirm: "移除仓库签名密钥“{keyId}”?已安装的插件不会被卸载。",
+ repositoryKeyRemoved: "已移除仓库签名密钥“{keyId}”",
+ repositoryKeyIdPlaceholder: "仓库密钥 ID",
+ repositoryPublicKeyPlaceholder: "Base64 Ed25519 仓库公钥(32 字节)",
+ trustRepository: "信任仓库",
+ noInstalledPluginsDescription: "安装已签名的 .dbxp,或仅在本地开发时明确允许未签名包。",
+ compatible: "兼容",
+ blocked: "已阻止",
+ rollback: "回滚",
+ uninstall: "卸载",
+ rollbackConfirm: "将此插件回滚到上一个已安装版本?",
+ rollbackSuccess: "已回滚到 {version}",
+ uninstallConfirm: "卸载 {name}?",
+ uninstallSuccess: "已卸载 {name}",
+ installSuccess: "已安装 {name} {version}",
+ selectConnectionProvider: "请先选择连接提供者",
+ connectionProvider: "连接提供者",
+ newConnection: "新建",
+ workbenches: "工作台",
+ filesystemProviders: "文件系统提供者",
+ open: "打开",
+ browse: "浏览",
+ parentDirectory: "上级目录",
+ filesystemUri: "文件系统 URI",
+ refresh: "刷新",
+ loadingFiles: "正在加载文件",
+ emptyDirectory: "此目录为空。",
+ loadMore: "加载更多",
+ fileManagerHint: "双击目录可打开,双击文件可预览最多 256 KB 的内容。",
+ loadingPreview: "正在加载预览",
+ previewTruncated: "预览已在 256 KB 处截断",
+ previewSelectedFileHint: "双击所选文件以加载预览。",
+ entryCannotPreview: "此项目无法预览。",
+ providerNoReadCapability: "该提供者未声明读取能力。",
+ filesystemUnavailable: "插件文件系统“{pluginId}/{providerId}”不可用",
+ loadingFilesystem: "正在加载插件文件系统",
+ filesystemUnavailableFallback: "插件文件系统不可用",
+ workbenchUnavailable: "插件工作台“{pluginId}/{contributionId}”不可用",
+ loadingWorkbench: "正在加载插件工作台",
+ workbenchUnavailableFallback: "插件工作台不可用",
+ pluginIncompatible: "插件不兼容",
+ loadingTitle: "正在加载 {title}",
+ },
auth: {
rateLimited: "请 {seconds} 秒后再试",
setupTitle: "设置访问密码",
@@ -154,6 +248,7 @@ export default withEnglishFallback({
sqlOpenFailed: "打开文件失败:{message}",
sqlSaveFailed: "保存文件失败:{message}",
driverManager: "驱动管理",
+ pluginCenter: "插件中心",
updatableDriverCount: "可更新驱动数量",
mcpUpdateAvailable: "MCP 服务有可用更新",
blockDangerousRedisCommands: "拦截危险命令",
@@ -929,6 +1024,9 @@ export default withEnglishFallback({
databaseCategoryTimeseries: "时序数据库",
databaseCategoryMq: "消息队列",
databaseCategoryRegistryConfig: "注册与配置",
+ databaseCategoryPlugins: "插件",
+ pluginProviderUnavailable: "此插件连接提供者不可用,请重新安装或启用插件后再试。",
+ pluginRequiredField: "请填写{field}",
jdbcConnection: "JDBC 连接",
iconView: "图标视图",
listView: "列表视图",
@@ -7922,6 +8020,13 @@ export default withEnglishFallback({
localInstall: "本地安装",
localInstallHint: "适用于完全内网环境:在另一台可联网的电脑上,从 DBX 的 GitHub Releases 页面下载官方 dbx-jdbc-plugin-*.zip 安装包,拷贝到本机后在此选择该文件——本机无需联网即可完成安装。",
runtimeDrivers: "运行时",
+ pluginsTab: "插件",
+ pluginContributionsTitle: "前端插件贡献",
+ pluginContributionsDescription: "已安装插件可以声明连接表单和其他 UI 贡献点,不需要把第三方代码注入 DBX 主窗口。",
+ noInstalledPlugins: "暂无已安装插件",
+ noInstalledPluginsDescription: "安装后端插件后,可以在这里预览它声明的前端贡献。",
+ pluginHostPreview: "宿主 API 预览",
+ pluginNoFrontendContribution: "这个插件暂时还没有声明前端贡献点。",
storageTab: "存储",
runtimeTitle: "驱动运行时",
runtimeHint: "仅在打开此面板时采集运行时指标。",
diff --git a/apps/desktop/src/i18n/locales/zh-TW.ts b/apps/desktop/src/i18n/locales/zh-TW.ts
index 4ab6665f4a..4a9974d4ff 100644
--- a/apps/desktop/src/i18n/locales/zh-TW.ts
+++ b/apps/desktop/src/i18n/locales/zh-TW.ts
@@ -166,6 +166,100 @@ export default withEnglishFallback({
name: "DBX",
},
docs,
+ pluginPlatform: {
+ centerTitle: "外掛中心",
+ centerDescription: "在統一商店中探索選用的 DBX 能力,並分別管理已安裝外掛與外掛倉庫。",
+ marketplace: "外掛商店",
+ installed: "已安裝",
+ settings: "設定",
+ searchMarketplace: "搜尋外掛、發佈者或標籤",
+ allRepositories: "全部倉庫",
+ loadingMarketplace: "正在載入外掛商店",
+ noMarketplacePlugins: "沒有可顯示的外掛",
+ noMarketplacePluginsDescription: "請在設定中配置外掛倉庫,或調整目前的搜尋與篩選條件。",
+ verified: "已驗證",
+ noDescription: "開發者未提供說明。",
+ permissionsCount: "{count} 項權限",
+ unsupportedTarget: "暫不支援 {target}",
+ installedVersion: "已安裝 v{version}",
+ licenseUnknown: "未指定授權條款",
+ marketplaceStatus: { install: "安裝", installed: "已安裝", update: "更新", unsupported: "不支援" },
+ updateSuccess: "已將 {name} 更新至 {version}",
+ noInstalledPlugins: "尚未安裝外掛",
+ browseMarketplace: "瀏覽外掛商店",
+ localInstallTitle: "安裝本機外掛套件",
+ localInstallDescription: "安裝從外掛商店之外取得的已簽署 .dbxp,DBX 會自動驗證套件完整性與倉庫簽章。",
+ repositoriesTitle: "外掛倉庫",
+ repositoriesDescription: "官方來源由 DBX 管理;加入自託管或第三方倉庫後,才會顯示對應的倉庫信任設定。",
+ repositoryFieldsRequired: "倉庫 ID、名稱和目錄網址皆為必填",
+ repositorySaved: "已儲存外掛倉庫「{name}」",
+ removeRepositoryConfirm: "移除外掛倉庫「{name}」?已安裝的外掛不會被解除安裝。",
+ repositoryRemoved: "已移除外掛倉庫「{name}」",
+ repositoryKind: { official: "官方", custom: "自訂", enterprise: "企業" },
+ repositoryNotConfigured: "目前版本未配置目錄網址",
+ enabled: "已啟用",
+ disabled: "已停用",
+ enable: "啟用",
+ disable: "停用",
+ repositoryIdPlaceholder: "倉庫 ID",
+ repositoryNamePlaceholder: "倉庫名稱",
+ repositoryCatalogUrlPlaceholder: "https://plugins.example.com/catalog/index.json",
+ addRepository: "加入倉庫",
+ signedPackagesVerifiedAutomatically: "自動驗證完整性與簽章",
+ developerOptionsTitle: "第三方與開發者選項",
+ developerOptionsDescription: "預設隱藏,僅在接入第三方倉庫或開發本機外掛時使用。",
+ developerOptionsWarning: "未簽署外掛可以使用目前使用者權限執行原生程式碼。只應為自行建置且確認可信的開發套件啟用。",
+ connectionManagedInDialog: "連線統一在「新增連線」對話框中建立與編輯;此處僅負責外掛管理與能力展示。",
+ allowUnsignedDevelopmentPackage: "允許安裝未簽署開發套件",
+ allowUnsignedDevelopmentPackageDescription: "僅影響手動安裝的 .dbxp,不會放寬官方外掛商店的簽章驗證。",
+ installPackage: "安裝 .dbxp",
+ packageFileType: "DBX 外掛套件",
+ repositoryTrustTitle: "自訂倉庫信任",
+ repositoryTrustDescription: "自訂倉庫營運方使用一個倉庫金鑰簽署其審核通過的外掛。請透過獨立可信管道取得倉庫公鑰。",
+ repositoryKeyAdded: "已新增倉庫簽署金鑰「{keyId}」",
+ removeRepositoryKeyConfirm: "移除倉庫簽署金鑰「{keyId}」?已安裝的外掛不會被解除安裝。",
+ repositoryKeyRemoved: "已移除倉庫簽署金鑰「{keyId}」",
+ repositoryKeyIdPlaceholder: "倉庫金鑰 ID",
+ repositoryPublicKeyPlaceholder: "Base64 Ed25519 倉庫公鑰(32 位元組)",
+ trustRepository: "信任倉庫",
+ noInstalledPluginsDescription: "安裝已簽署的 .dbxp,或僅在本機開發時明確允許未簽署套件。",
+ compatible: "相容",
+ blocked: "已阻擋",
+ rollback: "回復版本",
+ uninstall: "解除安裝",
+ rollbackConfirm: "將此外掛回復到上一個已安裝版本?",
+ rollbackSuccess: "已回復到 {version}",
+ uninstallConfirm: "解除安裝 {name}?",
+ uninstallSuccess: "已解除安裝 {name}",
+ installSuccess: "已安裝 {name} {version}",
+ selectConnectionProvider: "請先選擇連線提供者",
+ connectionProvider: "連線提供者",
+ newConnection: "新增",
+ workbenches: "工作台",
+ filesystemProviders: "檔案系統提供者",
+ open: "開啟",
+ browse: "瀏覽",
+ parentDirectory: "上層目錄",
+ filesystemUri: "檔案系統 URI",
+ refresh: "重新整理",
+ loadingFiles: "正在載入檔案",
+ emptyDirectory: "此目錄為空。",
+ loadMore: "載入更多",
+ fileManagerHint: "按兩下目錄可開啟,按兩下檔案可預覽最多 256 KB 的內容。",
+ loadingPreview: "正在載入預覽",
+ previewTruncated: "預覽已在 256 KB 處截斷",
+ previewSelectedFileHint: "按兩下所選檔案以載入預覽。",
+ entryCannotPreview: "此項目無法預覽。",
+ providerNoReadCapability: "此提供者未宣告讀取能力。",
+ filesystemUnavailable: "外掛檔案系統「{pluginId}/{providerId}」無法使用",
+ loadingFilesystem: "正在載入外掛檔案系統",
+ filesystemUnavailableFallback: "外掛檔案系統無法使用",
+ workbenchUnavailable: "外掛工作台「{pluginId}/{contributionId}」無法使用",
+ loadingWorkbench: "正在載入外掛工作台",
+ workbenchUnavailableFallback: "外掛工作台無法使用",
+ pluginIncompatible: "外掛不相容",
+ loadingTitle: "正在載入 {title}",
+ },
auth: {
rateLimited: "請 {seconds} 秒後再試",
setupTitle: "設定存取密碼",
@@ -231,6 +325,7 @@ export default withEnglishFallback({
sqlOpenFailed: "開啟檔案失敗:{message}",
sqlSaveFailed: "儲存檔案失敗:{message}",
driverManager: "驅動程式管理器",
+ pluginCenter: "外掛中心",
updatableDriverCount: "可更新驅動程式數量",
mcpUpdateAvailable: "MCP 服務有可用更新",
blockDangerousRedisCommands: "攔截危險命令",
@@ -814,6 +909,9 @@ export default withEnglishFallback({
databaseCategoryTimeseries: "時序資料庫",
databaseCategoryMq: "訊息佇列",
databaseCategoryRegistryConfig: "註冊與設定",
+ databaseCategoryPlugins: "外掛",
+ pluginProviderUnavailable: "此外掛連線提供者不可用,請重新安裝或啟用外掛後再試。",
+ pluginRequiredField: "請填寫{field}",
jdbcConnection: "JDBC 連線",
iconView: "圖示檢視",
listView: "清單檢視",
@@ -6802,6 +6900,13 @@ export default withEnglishFallback({
localInstall: "本機安裝",
localInstallHint: "適用於完全內網環境:在另一台可連網的電腦上,從 DBX 的 GitHub Releases 頁面下載官方 dbx-jdbc-plugin-*.zip 安裝包,傳輸到本機後在此選取該檔案——本機不需要連網即可完成安裝。",
runtimeDrivers: "執行環境",
+ pluginsTab: "外掛程式",
+ pluginContributionsTitle: "前端外掛程式貢獻",
+ pluginContributionsDescription: "已安裝的外掛程式可以描述連線表單和其他 UI 貢獻,而無需將程式碼注入 DBX 主視窗。",
+ noInstalledPlugins: "尚未安裝外掛程式",
+ noInstalledPluginsDescription: "安裝後端外掛程式後,可在此預覽其前端貢獻。",
+ pluginHostPreview: "宿主 API 預覽",
+ pluginNoFrontendContribution: "此外掛程式尚未宣告前端貢獻。",
storageTab: "儲存",
runtimeTitle: "驅動程式執行環境",
runtimeHint: "僅在開啟此面板時收集執行環境指標。",
diff --git a/apps/desktop/src/lib/__tests__/app/openTabsPersistence.spec.ts b/apps/desktop/src/lib/__tests__/app/openTabsPersistence.spec.ts
index c3d91efa31..d25a55cc8a 100644
--- a/apps/desktop/src/lib/__tests__/app/openTabsPersistence.spec.ts
+++ b/apps/desktop/src/lib/__tests__/app/openTabsPersistence.spec.ts
@@ -177,4 +177,62 @@ describe("openTabsPersistence originalSql round-trip", () => {
expect(restored.originalSql).toBe("SELECT 1");
expect(restored.externalSqlFileMissing).toBe(true);
});
+
+ it("preserves plugin workbench identity and connection-safe context", () => {
+ const [restored] = roundTrip([
+ queryTab({
+ id: "plugin-tab",
+ title: "Hello connection · Workbench",
+ connectionId: "plugin-connection",
+ database: "",
+ mode: "plugin-workbench",
+ pluginWorkbench: {
+ pluginId: "dbx.example.hello",
+ contributionId: "dbx.example.hello.main",
+ context: {
+ connectionId: "plugin-connection",
+ providerId: "hello.connection",
+ connectionType: "hello",
+ },
+ },
+ }),
+ ]);
+
+ expect(restored.mode).toBe("plugin-workbench");
+ expect(restored.pluginWorkbench).toEqual({
+ pluginId: "dbx.example.hello",
+ contributionId: "dbx.example.hello.main",
+ context: {
+ connectionId: "plugin-connection",
+ providerId: "hello.connection",
+ connectionType: "hello",
+ },
+ });
+ });
+
+ it("preserves host-owned plugin filesystem navigation", () => {
+ const [restored] = roundTrip([
+ queryTab({
+ id: "plugin-files",
+ title: "Object storage · Files",
+ connectionId: "plugin-connection",
+ database: "",
+ mode: "plugin-filesystem",
+ pluginFilesystem: {
+ pluginId: "dbx.example.storage",
+ providerId: "dbx.example.storage.files",
+ rootUri: "s3://bucket/",
+ currentUri: "s3://bucket/reports/",
+ },
+ }),
+ ]);
+
+ expect(restored.mode).toBe("plugin-filesystem");
+ expect(restored.pluginFilesystem).toEqual({
+ pluginId: "dbx.example.storage",
+ providerId: "dbx.example.storage.files",
+ rootUri: "s3://bucket/",
+ currentUri: "s3://bucket/reports/",
+ });
+ });
});
diff --git a/apps/desktop/src/lib/__tests__/plugins/pluginCenterIntegration.spec.ts b/apps/desktop/src/lib/__tests__/plugins/pluginCenterIntegration.spec.ts
new file mode 100644
index 0000000000..7a06fb5cdc
--- /dev/null
+++ b/apps/desktop/src/lib/__tests__/plugins/pluginCenterIntegration.spec.ts
@@ -0,0 +1,58 @@
+import { readFileSync } from "node:fs";
+import { describe, expect, it } from "vitest";
+
+const appSource = readFileSync(new URL("../../../App.vue", import.meta.url), "utf8");
+const toolbarSource = readFileSync(new URL("../../../components/layout/AppToolbar.vue", import.meta.url), "utf8");
+const tabBarSource = readFileSync(new URL("../../../components/layout/AppTabBar.vue", import.meta.url), "utf8");
+const driverStoreSource = readFileSync(new URL("../../../components/config/DriverStoreDialog.vue", import.meta.url), "utf8");
+const appDialogsSource = readFileSync(new URL("../../../components/layout/AppDialogs.vue", import.meta.url), "utf8");
+const connectionDialogSource = readFileSync(new URL("../../../components/connection/ConnectionDialog.vue", import.meta.url), "utf8");
+const pluginCenterSource = readFileSync(new URL("../../../components/plugins/PluginContributionsPanel.vue", import.meta.url), "utf8");
+
+describe("plugin center integration", () => {
+ it("opens from the top toolbar as an independent app surface", () => {
+ expect(toolbarSource).toContain("toolbarItems.pluginCenter");
+ expect(toolbarSource).toContain("emit('open-plugin-center')");
+ expect(appSource).toContain('@open-plugin-center="openPluginCenterPage()"');
+ expect(appSource).toContain(" {
+ expect(driverStoreSource).not.toContain('value="plugins"');
+ expect(driverStoreSource).not.toContain("PluginContributionsPanel");
+ });
+
+ it("creates and edits plugin connections in the unified connection dialog", () => {
+ expect(appDialogsSource).toContain(':edit-config="editConfig"');
+ expect(appDialogsSource).toContain(':plugin-provider="connectionPluginProvider"');
+ expect(appDialogsSource).not.toContain("pluginEditConfig");
+ expect(connectionDialogSource).toContain("pluginConnectionProviderOptionValue");
+ expect(connectionDialogSource).toContain(" {
+ const template = pluginCenterSource.split("")[1];
+ const settingsIndex = template.indexOf('value="settings"');
+ const developerOptionsIndex = template.indexOf("pluginPlatform.developerOptionsTitle");
+ const allowUnsignedIndex = template.indexOf("pluginPlatform.allowUnsignedDevelopmentPackage");
+ expect(template).toContain('value="marketplace"');
+ expect(template).toContain('value="installed"');
+ expect(template).toContain('value="settings"');
+ expect(settingsIndex).toBeLessThan(developerOptionsIndex);
+ expect(developerOptionsIndex).toBeLessThan(allowUnsignedIndex);
+ expect(template).toContain("pluginPlatform.signedPackagesVerifiedAutomatically");
+ expect(template).toContain('');
+ expect(pluginCenterSource).toContain("customRepositories.value.length > 0 || trustedKeys.value.length > 0");
+ expect(template).not.toContain("pluginPlatform.advancedSecurityTitle");
+ expect(pluginCenterSource).toContain("fetchPluginMarketplaceCatalogs");
+ expect(template).not.toContain("pluginPlatform.centerTitle");
+ expect(template).not.toContain("marketplaceTarget");
+ expect(template).not.toContain("