diff --git a/README.md b/README.md
index 57675ad..21626e4 100644
--- a/README.md
+++ b/README.md
@@ -1,45 +1,282 @@
# JetBrains Toolbox Plugin Development Template
-A starter template for building JetBrains Toolbox App plugins with Gradle. Use this project as a foundation for creating your own Toolbox plugins.
+A starter template for building JetBrains Toolbox App plugins with Gradle. Use this project as a foundation for creating your own Toolbox plugin that lists and connects to remote development environments.
-### Prerequisites
+The template ships with a working mock provider: build it, install it, and two fake environments ("Backend Dev Space" and "Frontend Dev Space") appear in Toolbox. From there, implementing your own plugin is mostly a matter of replacing the mock data source and choosing how connections are made.
+
+## Table of Contents
+
+- [Prerequisites](#prerequisites)
+- [Quick Start](#quick-start)
+- [Architecture](#architecture)
+- [Project Layout](#project-layout)
+- [Where to Change What](#where-to-change-what)
+- [Implementation Guide](#implementation-guide)
+- [Build System](#build-system)
+- [Commands](#commands)
+- [Installing Your Plugin](#installing-your-plugin)
+- [Publishing to the JetBrains Marketplace](#publishing-to-the-jetbrains-marketplace)
+- [Known Issues](#known-issues)
+- [Resources](#resources)
+
+## Prerequisites
- **JDK 21** or later
-- **JetBrains Toolbox App**
+- **JetBrains Toolbox App** (2.x)
+
+## Quick Start
+
+```bash
+# Build and install the plugin into your local Toolbox App
+./gradlew installPlugin
+
+# Then fully restart the Toolbox App (Quit, not just close the window).
+# A "Sample Provider" section with two mock environments should appear.
+```
+
+If the provider doesn't show up, check the Toolbox logs (macOS: `~/Library/Logs/JetBrains/Toolbox/toolbox.log`) — the plugin logs a line like `Sample Remote Provider initialized with MockEnvironmentDataSource` on successful load.
+
+## Architecture
+
+The plugin follows a layered design that keeps **data fetching** (your API, CLI, config files) separate from the **Toolbox API surface** (what Toolbox renders and connects to). You should be able to build a real integration by only touching the data-source layer and the connection strategy, without restructuring anything else.
+
+```mermaid
+flowchart LR
+ subgraph datasource["datasource/ — your integration"]
+ DS["EnvironmentDataSource
(interface)"]
+ MOCK["MockEnvironmentDataSource
(default, hardcoded data)"]
+ MOCK -. implements .-> DS
+ end
+
+ subgraph core["Core plumbing (rarely needs changes)"]
+ REPO["EnvironmentRepository
polling · caching · lifecycle"]
+ PROV["SampleRemoteProvider
(RemoteProvider)"]
+ end
+
+ subgraph environment["environment/ — Toolbox-facing"]
+ CFG["EnvironmentConfig
(plain data)"]
+ ENV["RemoteEnvironment
(reactive wrapper)"]
+ VF["EnvironmentContentsViewFactory
(connection strategy)"]
+ end
+
+ TB["Toolbox App UI"]
+
+ DS -- "fetchEnvironments()
List<EnvironmentConfig>" --> REPO
+ REPO -- "creates / updates" --> ENV
+ CFG --> ENV
+ ENV -- "getContentsView()" --> VF
+ REPO -- "StateFlow of environments" --> PROV
+ PROV --> TB
+```
+
+### How it boots and runs
+
+```mermaid
+sequenceDiagram
+ participant TB as Toolbox App
+ participant EXT as SampleRemoteDevExtension
+ participant REPO as EnvironmentRepository
+ participant DS as EnvironmentDataSource
+ participant ENV as RemoteEnvironment
+
+ TB->>EXT: discovers via META-INF/services, calls createRemoteProviderPluginInstance()
+ EXT->>REPO: construct with data source + services from ServiceLocator
+ EXT->>REPO: startPolling()
+ loop every refreshInterval (default 10 min)
+ REPO->>DS: fetchEnvironments()
+ DS-->>REPO: List
+ REPO->>ENV: create new / update changed / evict removed
+ REPO-->>TB: environments StateFlow emits
+ end
+ TB->>ENV: user expands an environment → getContentsView()
+ ENV-->>TB: EnvironmentContentsView (IDEs + projects, or SSH connection)
+```
+
+### Component responsibilities
+
+| Component | File | Responsibility |
+|---|---|---|
+| `SampleRemoteDevExtension` | `SamplePlugin.kt` | **Entry point.** Toolbox instantiates this via the `META-INF/services` file. Wires the data source, repository, and provider together using services (logger, coroutine scope, localization) from the `ServiceLocator`. |
+| `EnvironmentDataSource` | `datasource/EnvironmentDataSource.kt` | Interface with one method: `fetchEnvironments(): List`. Pure data fetching — no Toolbox types, no lifecycle. This is the main thing you implement. |
+| `MockEnvironmentDataSource` | `datasource/MockEnvironmentDataSource.kt` | Default implementation returning two hardcoded environments. Replace it, or keep it around as test/fallback data. |
+| `EnvironmentConfig` | `environment/EnvironmentConfig.kt` | Immutable snapshot of one environment (id, name, host, port, IDE codes, project paths, tags). The contract between your data source and the rest of the plugin. |
+| `EnvironmentRepository` | `EnvironmentRepository.kt` | Orchestrator. Polls the data source, caches `RemoteEnvironment` instances by id (so reactive subscriptions survive refreshes), updates configs in place, evicts environments that disappeared, and exposes everything as a `StateFlow`. |
+| `RemoteEnvironment` | `environment/RemoteEnvironment.kt` | Implements Toolbox's `RemoteProviderEnvironment`. Wraps a config in reactive state (`name`, `state`, `description` flows) that the Toolbox UI observes, and produces the contents view on demand. |
+| `EnvironmentContentsViewFactory` | `environment/EnvironmentContentsViewFactory.kt` | Strategy interface for *how Toolbox connects* to an environment. Swap the implementation here to change the connection type without touching the repository. |
+| `ManualContentsViewFactory` | `environment/ManualContentsViewFactory.kt` | Default strategy: a static list of IDEs and projects built from the config (`ManualEnvironmentContentsView`). No live connection. |
+| `SampleRemoteProvider` | `SampleRemoteProvider.kt` | Implements Toolbox's `RemoteProvider`. Thin — delegates the environment list to the repository. Also where the provider name, `handleUri`, and new-environment capability live. |
+
+### Why the layering?
+
+- **Data sources return `EnvironmentConfig`, not `RemoteEnvironment`.** Configs are plain data, so a data source can be unit-tested without any Toolbox dependencies, and the repository stays the single owner of environment lifecycle.
+- **The repository caches environments by id.** On each refresh it *updates* existing `RemoteEnvironment` instances instead of recreating them. This matters because Toolbox holds reactive subscriptions to each environment's flows — recreating instances every poll would break UI state.
+- **The contents-view factory is injected.** Connection strategy (manual list vs. SSH vs. custom agent) is orthogonal to where environment data comes from, so they're separate seams.
+
+## Project Layout
+
+```
+ToolboxSamplePlugin/
+├── settings.gradle.kts # Root name, includes :plugin, wires build-logic + Toolbox Maven repo
+├── gradle/libs.versions.toml # All dependency versions (Toolbox API, Kotlin, coroutines)
+├── build-logic/ # Custom Gradle plugins (packaging, install, publish)
+│ └── src/main/kotlin/toolbox/buildlogic/
+│ ├── ToolboxGenerateJsonExtension.kt # Generates extension.json (plugin metadata)
+│ ├── InstallToolboxPlugin.kt # `installPlugin` task → copies into local Toolbox
+│ └── PublishToolboxPlugin.kt # `packagePlugin` + `publishPlugin` tasks → Marketplace
+└── plugin/ # The actual Toolbox plugin
+ ├── build.gradle.kts # Plugin ID (group), version, vendor
+ └── src/main/
+ ├── kotlin/toolbox/gateway/sample/
+ │ ├── SamplePlugin.kt # Entry point (SampleRemoteDevExtension)
+ │ ├── SampleRemoteProvider.kt # RemoteProvider implementation
+ │ ├── EnvironmentRepository.kt # Polling, caching, lifecycle
+ │ ├── datasource/ # ← implement your data fetching here
+ │ └── environment/ # ← Toolbox-facing config, environment, views
+ └── resources/
+ ├── META-INF/services/com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension
+ │ # Registers your extension class — update if you rename/move it
+ ├── dependencies.json # Third-party licenses shown to users — keep in sync with libs.versions.toml
+ ├── icon.svg # Plugin icon
+ └── localization/defaultMessages.po
+```
+
+## Where to Change What
+
+The quick-reference map. "I want to…" → edit this:
+
+| I want to… | Edit |
+|---|---|
+| Fetch environments from my real backend/API/CLI | Create a new class implementing `EnvironmentDataSource` in `datasource/`, then return it from `createDataSource()` in `SamplePlugin.kt` |
+| Change what data an environment carries (add fields) | `environment/EnvironmentConfig.kt`, then use the new fields in your contents-view factory |
+| Change how Toolbox connects (SSH, custom agent, port forwarding) | New `EnvironmentContentsViewFactory` implementation in `environment/`, injected via the `contentsViewFactory` parameter of `EnvironmentRepository` in `SamplePlugin.kt` |
+| Change the provider name shown in Toolbox | `SampleRemoteProvider.kt` — the string passed to `RemoteProvider("Sample Provider")` |
+| Change the plugin ID / version / vendor | `plugin/build.gradle.kts` — `group` (this **is** the plugin ID), `version`, `extra["vendor"]` |
+| Change the Marketplace-facing name, description, URL | `build-logic/.../ToolboxGenerateJsonExtension.kt` — `metaName`, `metaDescription`, `metaUrl` |
+| Change the polling frequency | `refreshInterval` parameter of `EnvironmentRepository` (default 10 minutes) |
+| Support deep links (`jetbrains://…`) | `handleUri()` in `SampleRemoteProvider.kt` |
+| Let users create environments from Toolbox | `canCreateNewEnvironments` in `SampleRemoteProvider.kt` (plus the related `RemoteProvider` overrides) |
+| React to environment status changes (health checks, errors) | Call `repository.updateEnvironmentState(id, state, errorMessage)` — see `EnvironmentRepository.kt` |
+| Clean up when a user removes an environment | `onDelete()` in `environment/RemoteEnvironment.kt` |
+| Change the plugin icon | `plugin/src/main/resources/icon.svg` |
+| Rename the entry-point class or package | Update the class **and** the line inside `resources/META-INF/services/com.jetbrains.toolbox.api.remoteDev.RemoteDevExtension` — Toolbox won't find your plugin otherwise |
+| Bump the Toolbox API or Kotlin version | `gradle/libs.versions.toml` (and mirror versions in `resources/dependencies.json`) |
+| Change Marketplace release notes | `PublishToolboxPlugin.kt` — the `"Bug fixes and improvements"` string in `PublishTask` |
+
+## Implementation Guide
+
+A suggested order for turning the template into your plugin.
+
+### 1. Claim your plugin identity
+
+In `plugin/build.gradle.kts`:
+
+```kotlin
+group = "com.yourcompany.toolbox.yourplugin" // becomes the plugin ID everywhere
+version = "1.0.0"
+extra["vendor"] = "Your Company"
+```
+
+The `group` is used as the plugin ID in `extension.json`, the install directory name, the ZIP layout, and the Marketplace XML ID — you set it once here and the build logic propagates it. Also update the display name/description in `ToolboxGenerateJsonExtension.kt` and the provider name in `SampleRemoteProvider.kt`.
+
+### 2. Implement your data source
+
+Create a class in `datasource/` implementing the single-method interface:
+
+```kotlin
+class MyApiDataSource(private val logger: Logger) : EnvironmentDataSource {
+ override suspend fun fetchEnvironments(): List {
+ // call your API / run your CLI / read your config
+ // map results to EnvironmentConfig
+ // wrap failures in DataSourceException so the repository logs them
+ }
+}
+```
+
+Then swap it in at the one seam designed for it — `createDataSource()` in `SamplePlugin.kt`. Throw `DataSourceException` on failure: the repository catches it, logs it, and keeps the previously loaded environments instead of wiping the list.
+
+### 3. Choose your connection strategy
+
+This is the first major design decision: how does Toolbox actually connect to an environment when the user clicks it? The strategy lives entirely in your `EnvironmentContentsViewFactory` implementation.
+
+| View type | When to use | Effort |
+|---|---|---|
+| `ManualEnvironmentContentsView` (template default) | You know IDEs/projects upfront; no live connection. Good for early development and as fallback data. | Low |
+| `SshEnvironmentContentsView` | Environments are reachable over SSH and Toolbox should manage the connection. | **Low — recommended starting point for real connections** |
+| `AgentConnectionBasedEnvironmentContentsView` | You run your own agent/protocol inside the environment. | High |
+| `PortForwardingCapableEnvironmentContentsView` | Complex scenarios needing port forwarding. | High |
+
+`EnvironmentConfig` already carries `host`, `port`, and `username` for an SSH-based factory. You can also mix strategies per environment — the factory receives the config, so it can pick a view type based on tags or reachability, and fall back to a manual view when a connection isn't possible.
+
+### 4. Report real environment states
+
+The template marks everything `Active`. For a real integration, map your backend's status (starting, stopped, unreachable…) to `StandardRemoteEnvironmentState` values — either in the polling path or by calling `repository.updateEnvironmentState()` from a health check. The state drives the status badge users see in Toolbox.
+
+### 5. Optional polish
+
+- **Deep links:** implement `handleUri()` in `SampleRemoteProvider.kt` so links can open specific environments.
+- **UI / authentication:** this template contains no custom UI. If you need login screens or settings pages, see the [Toolbox UI API](https://www.jetbrains.com/help/toolbox-app/ui-api.html#toolboxUI).
+- **Licenses:** update `resources/dependencies.json` to list the third-party libraries your plugin actually ships.
+- **Cleanup:** put teardown logic in `RemoteEnvironment.onDelete()` and `SampleRemoteProvider.close()`.
+
+## Build System
+
+Build logic is decoupled from the plugin itself: `build-logic/` is an [included build](https://docs.gradle.org/current/userguide/composite_builds.html) providing three Gradle plugins, all applied in `plugin/build.gradle.kts`. You generally don't need to touch these unless you're changing packaging or metadata behavior.
+
+| Gradle plugin | Task(s) it adds | What it does |
+|---|---|---|
+| `com.jetbrains.toolbox.packaging` | `generateExtensionJson` (hooked into `assemble`) | Generates `build/generated/extension.json` — the manifest Toolbox reads — from `group`, `version`, `vendor`, and the metadata in `ToolboxGenerateJsonExtension.kt` |
+| `com.jetbrains.toolbox.install` | `installPlugin` | Builds, then copies the jar + `extension.json` + `dependencies.json` + `icon.svg` into your local Toolbox plugins directory |
+| `com.jetbrains.toolbox.publish` | `packagePlugin`, `publishPlugin` | Zips the plugin in the Marketplace-required layout and uploads it |
+
+The Marketplace ZIP produced by `packagePlugin` looks like:
+
+```
+plugin-1.1.0.zip
+├── extension.json # at the root
+└── / # directory named after your group
+ ├── dependencies.json
+ ├── icon.svg
+ └── lib/
+ └── plugin-1.1.0.jar
+```
+
+The Toolbox plugin API dependencies are `compileOnly` — the Toolbox App provides them at runtime, so they are deliberately not bundled in the jar.
+
+## Commands
+
+| Command | Description |
+|---|---|
+| `./gradlew :plugin:assemble` | Compile and generate `extension.json` |
+| `./gradlew :plugin:build` | Build and run tests (none here) |
+| `./gradlew installPlugin` | Build and install directly into the local Toolbox App |
+| `./gradlew packagePlugin` | Produce the Marketplace-ready ZIP in `plugin/build/distributions/` |
+| `./gradlew publishPlugin` | Package and upload to the JetBrains Marketplace |
+| `./gradlew clean` | Clean all build outputs |
-### Implementation Notes
+## Installing Your Plugin
-- The first major decision is whether you want to connect via `AgentConnectionBasedEnvironmentContentsView` or `SshEnvironmentContentsView`
- - `SshEnvironmentContentsView` is a much easier implementation.
-- There are no examples of UI in this template, you can make one that includes additional authentication as needed.
- - More details on that, see the [Toolbox UI](https://www.jetbrains.com/help/toolbox-app/ui-api.html#toolboxUI)
-- This is separated as logically as I could with the following details:
- - `/datasource` should contain all logic pertaining to retreiving and managing the environment data.
- - `/environment` should contain all logic pertaining to making that data accessable to Toolbox.
- - I use `ManualEnvironmentContentsView` because the data in this sample all hardcoded.
- - You can use `AgentConnectionBasedEnvironmentContentsView` or `SshEnvironmentContentsView` instead and rely on `ManualEnvironmentContentsView` for fallback data.
- - For more complex scenarios, you can use `PortForwardingCapableEnvironmentContentsView`.
+`./gradlew installPlugin` handles this for you. To install manually instead, copy the jar, `extension.json`, `dependencies.json`, and `icon.svg` into a directory named after your plugin ID:
-### Commands
+- **Windows:** `%LocalAppData%/JetBrains/Toolbox/cache/plugins/`
+- **macOS:** `~/Library/Caches/JetBrains/Toolbox/plugins/`
+- **Linux:** `~/.local/share/JetBrains/Toolbox/plugins/`
-| Command | Description |
-|------------------------------|---------------------------------------|
-| `./gradlew :plugin:assemble` | Build the plugin ZIP |
-| `./gradlew :plugin:build` | Build and run tests (none here) |
-| `./gradlew clean` | Clean all build outputs |
-| `./gradlew installPlugin` | Build and install directly to Toolbox |
+Fully restart the Toolbox App after installing — it only scans for plugins on startup.
+## Publishing to the JetBrains Marketplace
-### Installing Your Plugin
+1. Generate a token from your [JetBrains Marketplace](https://plugins.jetbrains.com) account.
+2. Export it: `export JETBRAINS_MARKETPLACE_PUBLISH_TOKEN=`
+3. Run `./gradlew publishPlugin`.
-Once built and assembled, you can install the plugin directly into Toolbox using `./gradlew installPlugin` task or adding the plugin files to the folowing default directory:
+First-time uploads are created **hidden** on the Marketplace with the Apache 2.0 license and `toolbox`/`gateway` tags — review the settings in `PublishToolboxPlugin.kt` before publishing (the license choice is yours; the tags and product family must stay as-is). Subsequent runs upload an update to the existing plugin; remember to change the hardcoded release notes in `PublishTask` for each release.
-- Windows: `%LocalAppData%/JetBrains/Toolbox/cache/plugins/plugin-id`
-- macOS: `~/Library/Caches/JetBrains/Toolbox/plugins/plugin-id`
-- Linux: `~/.local/share/JetBrains/Toolbox/plugins/plugin-id`
+## Known Issues
-### Resources
+- **Kotlin 2.1.0 + `MutableStateFlow`:** a compiler bug ([KT-73951](https://youtrack.jetbrains.com/issue/KT-73951)) requires the `-Xdisable-phases=ConstEvaluationLowering` workaround present in both `plugin/build.gradle.kts` and `build-logic/build.gradle.kts`. Remove it once you upgrade to a Kotlin version containing the fix.
-See the [Toolbox API documentation](https://www.jetbrains.com/help/toolbox-app/) for detailed usage.
+## Resources
-See [Coder's Open-Sourced Plugin](https://github.com/coder/coder-jetbrains-toolbox/tree/main) for a live example of a Toolbox Plugin.
+- [Toolbox App plugin documentation](https://www.jetbrains.com/help/toolbox-app/) — official API docs
+- [Toolbox UI API](https://www.jetbrains.com/help/toolbox-app/ui-api.html#toolboxUI) — for building custom UI/auth flows
+- [Coder's open-source Toolbox plugin](https://github.com/coder/coder-jetbrains-toolbox/tree/main) — a production plugin using the same API
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
index 9e224d5..5f74b7d 100644
--- a/build-logic/build.gradle.kts
+++ b/build-logic/build.gradle.kts
@@ -16,6 +16,7 @@ repositories {
dependencies {
implementation(libs.plugin.structure)
implementation(libs.jackson.kotlin)
+ implementation(libs.marketplace.client)
}
gradlePlugin {
@@ -32,6 +33,12 @@ gradlePlugin {
displayName = "Install Toolbox Plugin"
description = "Installs the plugin into the local Toolbox directory"
}
+ create("toolboxPublish") {
+ id = "com.jetbrains.toolbox.publish"
+ implementationClass = "com.jetbrains.toolbox.buildlogic.PublishToolboxPlugin"
+ displayName = "Publish Toolbox Plugin"
+ description = "Packages and publishes a JetBrains Toolbox plugin to the JetBrains Marketplace"
+ }
}
}
diff --git a/build-logic/src/main/kotlin/toolbox/buildlogic/PublishToolboxPlugin.kt b/build-logic/src/main/kotlin/toolbox/buildlogic/PublishToolboxPlugin.kt
new file mode 100644
index 0000000..4702466
--- /dev/null
+++ b/build-logic/src/main/kotlin/toolbox/buildlogic/PublishToolboxPlugin.kt
@@ -0,0 +1,105 @@
+package com.jetbrains.toolbox.buildlogic
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.InputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.bundling.Zip
+import org.gradle.work.DisableCachingByDefault
+import org.jetbrains.intellij.pluginRepository.PluginRepositoryFactory
+import org.jetbrains.intellij.pluginRepository.model.LicenseUrl
+import org.jetbrains.intellij.pluginRepository.model.ProductFamily
+
+/**
+ * Gradle plugin that packages and publishes a JetBrains Toolbox plugin to the
+ * [JetBrains Marketplace](https://plugins.jetbrains.com).
+ */
+class PublishToolboxPlugin : Plugin {
+ override fun apply(target: Project) {
+ val packageTask = target.tasks.register("packagePlugin", Zip::class.java) {
+ dependsOn(target.tasks.named("assemble"))
+ from(target.layout.buildDirectory.file("generated/extension.json"))
+ from(target.file("src/main/resources")) {
+ include("dependencies.json")
+ include("icon.svg")
+ into("${target.group}")
+ }
+ from(target.tasks.named("jar")) {
+ into("${target.group}/lib")
+ }
+ }
+
+ target.tasks.register("publishPlugin", PublishTask::class.java) {
+ dependsOn(packageTask)
+ extensionId.set(target.group.toString())
+ pluginZipFile.set(packageTask.flatMap { it.archiveFile })
+ vendor.set(target.extensions.extraProperties["vendor"].toString())
+ }
+ }
+
+ /**
+ * Task that uploads the packaged plugin ZIP to the JetBrains Marketplace.
+ *
+ * Requires the `JETBRAINS_MARKETPLACE_PUBLISH_TOKEN` environment variable to be set.
+ * The token can be generated from your JetBrains Marketplace account.
+ */
+ @DisableCachingByDefault(because = "Publishes plugin to JetBrains Marketplace")
+ abstract class PublishTask : DefaultTask() {
+ @get:Input
+ abstract val extensionId: Property
+
+ /** Path to the plugin ZIP archive produced by the `packagePlugin` task. */
+ @get:InputFile
+ abstract val pluginZipFile: RegularFileProperty
+
+ @get:Input
+ abstract val vendor: Property
+
+ @TaskAction
+ fun publish() {
+ val jbMarketplaceToken: String? = System.getenv("JETBRAINS_MARKETPLACE_PUBLISH_TOKEN")
+ if (jbMarketplaceToken.isNullOrBlank()) {
+ error(
+ "Environment variable `JETBRAINS_MARKETPLACE_PUBLISH_TOKEN` is not set. " + "Please obtain a token from https://plugins.jetbrains.com and set it."
+ )
+ }
+
+ println("Publishing plugin ${extensionId.get()} to JetBrains Marketplace...")
+ println("Token prefix: ${jbMarketplaceToken.take(5)}*****")
+
+ val instance = PluginRepositoryFactory.create(
+ "https://plugins.jetbrains.com", jbMarketplaceToken
+ )
+
+ val existingPlugin = instance.pluginManager.getPluginByXmlId(
+ extensionId.get(), ProductFamily.TOOLBOX
+ )
+
+ if (existingPlugin != null) {
+ instance.uploader.uploadUpdateByXmlIdAndFamily(
+ extensionId.get(),
+ ProductFamily.TOOLBOX,
+ pluginZipFile.get().asFile,
+ null, // channel – not yet supported for Toolbox plugins.
+ "Bug fixes and improvements", // please make sure to update version notes here.
+ false
+ )
+ } else {
+ println("Plugin not found on Marketplace. Uploading as new plugin...")
+ instance.uploader.uploadNewPlugin(
+ pluginZipFile.get().asFile, // do not change
+ listOf("toolbox", "gateway"), // do not change
+ LicenseUrl.APACHE_2_0, // choose wisely
+ ProductFamily.TOOLBOX, // do not change
+ vendor = vendor.get(), // do not change
+ isHidden = true,
+ )
+ }
+ println("Plugin published successfully!")
+ }
+}
+}
diff --git a/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt b/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt
index 4d3beaa..3216816 100644
--- a/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt
+++ b/build-logic/src/main/kotlin/toolbox/buildlogic/ToolboxGenerateJsonExtension.kt
@@ -71,9 +71,9 @@ class ToolboxGenerateJsonExtension : Plugin {
val gen = target.tasks.register("generateExtensionJson", GenerateExtensionJsonTask::class.java) {
extensionId.set(target.group.toString())
extensionVersion.set(target.version.toString())
- metaName.set("Toolbox Sample Plugin")
+ metaName.set("Toolbox Sample Plugin Template")
metaDescription.set("Sample Plugin for JetBrains Toolbox")
- metaVendor.set("JetBrains")
+ metaVendor.set(target.extensions.extraProperties["vendor"].toString())
metaUrl.set("https://www.jetbrains.com/toolbox/")
destinationFile.set(extensionJsonFile)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index a9ca2f3..a2316e0 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -2,12 +2,14 @@
coroutines = "1.10.1"
jackson = "2.18.2"
kotlin = "2.1.0"
+marketplace-client = "2.0.50"
toolbox-plugin-api = "1.8.65679"
plugin-structure = "3.320"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" }
+marketplace-client = { module = "org.jetbrains.intellij:plugin-repository-rest-client", version.ref = "marketplace-client" }
toolbox-core-api = { module = "com.jetbrains.toolbox:core-api", version.ref = "toolbox-plugin-api" }
toolbox-remote-dev-api = { module = "com.jetbrains.toolbox:remote-dev-api", version.ref = "toolbox-plugin-api" }
toolbox-ui-api = { module = "com.jetbrains.toolbox:ui-api", version.ref = "toolbox-plugin-api" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index e644113..61285a6 100644
Binary files a/gradle/wrapper/gradle-wrapper.jar and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index a441313..19a6bde 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
diff --git a/gradlew b/gradlew
index 8fd453c..adff685 100755
--- a/gradlew
+++ b/gradlew
@@ -1,7 +1,7 @@
#!/bin/sh
#
-# Copyright © 2015-2021 the original authors.
+# Copyright © 2015 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
+# SPDX-License-Identifier: Apache-2.0
+#
##############################################################################
#
@@ -84,7 +86,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
-APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -112,99 +114,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
-CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
-
-
-# GRADLE JVM WRAPPER START MARKER
-BUILD_DIR="jvm"
-JVM_ARCH=$(uname -m)
-JVM_TEMP_FILE=$BUILD_DIR/gradle-jvm-temp.tar.gz
-if [ "$darwin" = "true" ]; then
- case $JVM_ARCH in
- x86_64)
- JVM_URL=https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-osx-x64-b631.28.tar.gz
- JVM_TARGET_DIR=$BUILD_DIR/jbr_jcef-21.0.5-osx-x64-b631.28-be8dc8
- ;;
- arm64)
- JVM_URL=https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-osx-aarch64-b631.28.tar.gz
- JVM_TARGET_DIR=$BUILD_DIR/jbr_jcef-21.0.5-osx-aarch64-b631.28-f458b6
- ;;
- *)
- die "Unknown architecture $JVM_ARCH"
- ;;
- esac
-elif [ "$cygwin" = "true" ] || [ "$msys" = "true" ]; then
- JVM_URL=https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-windows-x64-b631.28.tar.gz
- JVM_TARGET_DIR=$BUILD_DIR/jbr_jcef-21.0.5-windows-x64-b631.28-eedcdf
-else
- JVM_ARCH=$(linux$(getconf LONG_BIT) uname -m)
- case $JVM_ARCH in
- x86_64)
- JVM_URL=https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-linux-x64-b631.28.tar.gz
- JVM_TARGET_DIR=$BUILD_DIR/jbr_jcef-21.0.5-linux-x64-b631.28-322c17
- ;;
- aarch64)
- JVM_URL=https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-linux-aarch64-b631.28.tar.gz
- JVM_TARGET_DIR=$BUILD_DIR/jbr_jcef-21.0.5-linux-aarch64-b631.28-3ac5ea
- ;;
- *)
- die "Unknown architecture $JVM_ARCH"
- ;;
- esac
-fi
-
-set -e
-
-if [ -e "$JVM_TARGET_DIR/.flag" ] && [ -n "$(ls "$JVM_TARGET_DIR")" ] && [ "x$(cat "$JVM_TARGET_DIR/.flag")" = "x${JVM_URL}" ]; then
- # Everything is up-to-date in $JVM_TARGET_DIR, do nothing
- true
-else
- echo "Downloading $JVM_URL to $JVM_TEMP_FILE"
-
- rm -f "$JVM_TEMP_FILE"
- mkdir -p "$BUILD_DIR"
- if command -v curl >/dev/null 2>&1; then
- if [ -t 1 ]; then CURL_PROGRESS="--progress-bar"; else CURL_PROGRESS="--silent --show-error"; fi
- # shellcheck disable=SC2086
- curl $CURL_PROGRESS -L --output "${JVM_TEMP_FILE}" "$JVM_URL" 2>&1
- elif command -v wget >/dev/null 2>&1; then
- if [ -t 1 ]; then WGET_PROGRESS=""; else WGET_PROGRESS="-nv"; fi
- wget $WGET_PROGRESS -O "${JVM_TEMP_FILE}" "$JVM_URL" 2>&1
- else
- die "ERROR: Please install wget or curl"
- fi
-
- echo "Extracting $JVM_TEMP_FILE to $JVM_TARGET_DIR"
- rm -rf "$JVM_TARGET_DIR"
- mkdir -p "$JVM_TARGET_DIR"
-
- case "$JVM_URL" in
- *".zip") unzip "$JVM_TEMP_FILE" -d "$JVM_TARGET_DIR" ;;
- *) tar -x -f "$JVM_TEMP_FILE" -C "$JVM_TARGET_DIR" ;;
- esac
-
- rm -f "$JVM_TEMP_FILE"
-
- echo "$JVM_URL" >"$JVM_TARGET_DIR/.flag"
-fi
-
-JAVA_HOME=
-for d in "$JVM_TARGET_DIR" "$JVM_TARGET_DIR"/* "$JVM_TARGET_DIR"/Contents/Home "$JVM_TARGET_DIR"/*/Contents/Home; do
- if [ -e "$d/bin/java" ]; then
- JAVA_HOME="$d"
- fi
-done
-
-if [ '!' -e "$JAVA_HOME/bin/java" ]; then
- die "Unable to find bin/java under $JVM_TARGET_DIR"
-fi
-
-# Make it available for child processes
-export JAVA_HOME
-
-set +e
-# GRADLE JVM WRAPPER END MARKER
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
@@ -261,7 +171,6 @@ fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
- CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
@@ -294,15 +203,14 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
-# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
- -classpath "$CLASSPATH" \
- org.gradle.wrapper.GradleWrapperMain \
+ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
diff --git a/gradlew.bat b/gradlew.bat
index 41e7d98..c4bdd3a 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@@ -36,82 +38,6 @@ for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
-@rem GRADLE JVM WRAPPER START MARKER
-
-setlocal
-set BUILD_DIR=jvm
-set JVM_TARGET_DIR=%BUILD_DIR%\jbr_jcef-21.0.5-windows-x64-b631.28-eedcdf\
-
-set JVM_URL=https://cache-redirector.jetbrains.com/intellij-jbr/jbr_jcef-21.0.5-windows-x64-b631.28.tar.gz
-
-set IS_TAR_GZ=0
-set JVM_TEMP_FILE=gradle-jvm.zip
-
-if /I "%JVM_URL:~-7%"==".tar.gz" (
- set IS_TAR_GZ=1
- set JVM_TEMP_FILE=gradle-jvm.tar.gz
-)
-
-set POWERSHELL=%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe
-
-if not exist "%JVM_TARGET_DIR%" MD "%JVM_TARGET_DIR%"
-
-if not exist "%JVM_TARGET_DIR%.flag" goto downloadAndExtractJvm
-
-set /p CURRENT_FLAG=<"%JVM_TARGET_DIR%.flag"
-if "%CURRENT_FLAG%" == "%JVM_URL%" goto continueWithJvm
-
-:downloadAndExtractJvm
-
-PUSHD "%BUILD_DIR%"
-if errorlevel 1 goto fail
-
-echo Downloading %JVM_URL% to %BUILD_DIR%\%JVM_TEMP_FILE%
-if exist "%JVM_TEMP_FILE%" DEL /F "%JVM_TEMP_FILE%"
-"%POWERSHELL%" -nologo -noprofile -Command "Set-StrictMode -Version 3.0; $ErrorActionPreference = \"Stop\"; (New-Object Net.WebClient).DownloadFile('%JVM_URL%', '%JVM_TEMP_FILE%')"
-if errorlevel 1 goto fail
-
-POPD
-
-RMDIR /S /Q "%JVM_TARGET_DIR%"
-if errorlevel 1 goto fail
-
-MKDIR "%JVM_TARGET_DIR%"
-if errorlevel 1 goto fail
-
-PUSHD "%JVM_TARGET_DIR%"
-if errorlevel 1 goto fail
-
-echo Extracting %BUILD_DIR%\%JVM_TEMP_FILE% to %JVM_TARGET_DIR%
-
-if "%IS_TAR_GZ%"=="1" (
- tar xf "..\\%JVM_TEMP_FILE%"
-) else (
- "%POWERSHELL%" -nologo -noprofile -command "Set-StrictMode -Version 3.0; $ErrorActionPreference = \"Stop\"; Add-Type -A 'System.IO.Compression.FileSystem'; [IO.Compression.ZipFile]::ExtractToDirectory('..\\%JVM_TEMP_FILE%', '.');"
-)
-if errorlevel 1 goto fail
-
-DEL /F "..\%JVM_TEMP_FILE%"
-if errorlevel 1 goto fail
-
-POPD
-
-echo %JVM_URL%>"%JVM_TARGET_DIR%.flag"
-if errorlevel 1 goto fail
-
-:continueWithJvm
-
-set JAVA_HOME=
-for /d %%d in ("%JVM_TARGET_DIR%"*) do if exist "%%d\bin\java.exe" set JAVA_HOME=%%d
-if not exist "%JAVA_HOME%\bin\java.exe" (
- echo Unable to find java.exe under %JVM_TARGET_DIR%
- goto fail
-)
-
-endlocal & set JAVA_HOME=%JAVA_HOME%
-
-@rem GRADLE JVM WRAPPER END MARKER
-
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
@@ -144,11 +70,10 @@ goto fail
:execute
@rem Setup the command line
-set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
-"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
:end
@rem End local scope for the variables with windows NT shell
diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts
index 75877db..56af029 100644
--- a/plugin/build.gradle.kts
+++ b/plugin/build.gradle.kts
@@ -3,12 +3,15 @@ plugins {
`kotlin-dsl`
id("com.jetbrains.toolbox.packaging")
id("com.jetbrains.toolbox.install")
+ id("com.jetbrains.toolbox.publish")
`java-library`
}
group = "com.jetbrains.toolbox.sample"
version = "1.1.0"
+extra["vendor"] = "JetBrains"
+
kotlin {
jvmToolchain(21)
}