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) }