diff --git a/.changeset/README.md b/.changeset/README.md index 81712b2..f7e76aa 100644 --- a/.changeset/README.md +++ b/.changeset/README.md @@ -9,8 +9,9 @@ npm run changeset Choose `patch`, `minor`, or `major`, then describe the user-visible change. The release workflow collects pending changesets into a version pull request. -Merging that pull request packages the VSIX and creates a matching GitHub -release. Visual Studio Marketplace publishing is temporarily disabled. +Merging that pull request packages the VSIX, publishes it to the Visual Studio +Marketplace using the existing `VSCE_PAT` repository secret, and creates a matching +GitHub release. Documentation-only and internal maintenance changes may use an empty changeset: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 73d213c..3c078f8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,7 +15,7 @@ permissions: jobs: release: - name: Changesets and GitHub release + name: Changesets and Marketplace runs-on: ubuntu-latest steps: @@ -61,13 +61,19 @@ jobs: if: steps.release.outputs.publish == 'true' run: npm run package + - name: Publish to Visual Studio Marketplace + if: steps.release.outputs.publish == 'true' + env: + VSCE_PAT: ${{ secrets.VSCE_PAT }} + run: npx vsce publish --packagePath "opencode-bridge-for-copilot-chat-${{ steps.release.outputs.version }}.vsix" --skip-duplicate + - name: Create GitHub release if: steps.release.outputs.publish == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | gh release create "${{ steps.release.outputs.tag }}" \ - "opencode-bridge-copilot-chat-${{ steps.release.outputs.version }}.vsix" \ + "opencode-bridge-for-copilot-chat-${{ steps.release.outputs.version }}.vsix" \ --target "$GITHUB_SHA" \ --title "${{ steps.release.outputs.tag }}" \ --generate-notes diff --git a/CHANGELOG.md b/CHANGELOG.md index cd1303f..d9e6580 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.0.0 + +### Major Changes + +- Launch OpenCode Bridge for Copilot Chat v1.0.0 under the new extension ID `grikomsn.opencode-bridge-for-copilot-chat` and restore Visual Studio Marketplace publishing. Remove the temporary GitHub update notifier in favor of VS Code updates. Users of the previous extension should disable or uninstall it, install the new listing, and sign in again; existing `opencode.*` settings retain their names. + ## 0.8.0 ### Minor Changes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0831b73..8f41d59 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing -Thanks for helping improve OpenCode for Copilot Chat. +Thanks for helping improve OpenCode Bridge for Copilot Chat. ## Before opening work diff --git a/README.md b/README.md index 25186e5..92da4df 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ OpenCode and GitHub Copilot

-

OpenCode for Copilot Chat

+

OpenCode Bridge for Copilot Chat

Use OpenCode Console and OpenCode Go models directly from the GitHub Copilot Chat model picker in Visual Studio Code.

@@ -14,9 +14,6 @@ This native VS Code `LanguageModelChatProvider` registers Console and Go as separate provider groups and streams their responses into Copilot Chat without a local proxy. VS Code 1.125 exposes the provider-entry configuration through the built-in Language Models UI; the packaged extension does not enable proposed APIs. -> [!IMPORTANT] -> Visual Studio Marketplace publishing is temporarily paused. Install and update the extension by sideloading the VSIX attached to the [latest GitHub release](https://github.com/grikomsn/opencode-copilot-chat/releases/latest). - ## Highlights - Separate OpenCode Console and Go model groups @@ -29,15 +26,17 @@ This native VS Code `LanguageModelChatProvider` registers Console and Go as sepa ## Quick start -1. Download the `.vsix` asset from the [latest GitHub release](https://github.com/grikomsn/opencode-copilot-chat/releases/latest). You need VS Code 1.125 or newer and GitHub Copilot Chat. -2. In VS Code, open **Extensions**, select the **…** menu, choose **Install from VSIX…**, and select the downloaded file. Alternatively, run `code --install-extension ./opencode-bridge-copilot-chat-.vsix --force`. +1. Install [OpenCode Bridge for Copilot Chat](https://marketplace.visualstudio.com/items?itemName=grikomsn.opencode-bridge-for-copilot-chat) from the Visual Studio Marketplace. You need VS Code 1.125 or newer and GitHub Copilot Chat. +2. Alternatively, run `code --install-extension grikomsn.opencode-bridge-for-copilot-chat`. For manual installation, download the `.vsix` from the [latest GitHub release](https://github.com/grikomsn/opencode-copilot-chat/releases/latest) and run `code --install-extension ./opencode-bridge-for-copilot-chat-.vsix --force`. 3. For Console or Go, open **Manage Language Models**, choose **Add Models**, select the provider, name the entry, and paste its service-account API key (Console entries can omit the key and use a device-code profile instead). 4. For Console device-code sign-in, run **OpenCode: Add Console Account**, choose a profile ID, then add an OpenCode Console entry with the same ID in **Manage Language Models**. Go can also sign in with the same Console device-code flow from the Manage Connection menu. 5. Repeat either flow for another account or key, then enable the models you want in Copilot Chat. -VS Code cannot automatically update a sideloaded build from GitHub. To update, download the newest VSIX from the latest release and repeat step 2; the newer build replaces the installed version while preserving the extension's stored settings and credentials. +Marketplace installations receive updates through VS Code. Manual VSIX installations can be updated by installing the newest release asset. + +### Moving from the previous extension -While Marketplace publishing is paused, the extension checks GitHub Releases at most once every 24 hours. When a newer VSIX is available, it shows a one-time notification with a link to the release and sideloading guidance. +Version 1.0.0 starts a new listing with ID `grikomsn.opencode-bridge-for-copilot-chat`. Disable or uninstall `grikomsn.opencode-bridge-copilot-chat` before installing the new extension to avoid duplicate providers and commands. Sign in again or re-enter your API keys in the new extension; its Secret Storage and local usage state are separate. Existing `opencode.*` workspace settings retain their names. Composer controls override workspace defaults; ordered effort controls default to High, binary controls default On, and Qwen defaults Auto. Console and Go use each entry's authenticated live catalog, while a Console device-code profile shows only models enabled for that profile's selected organization. Click the OpenCode status-bar item to inspect tokens for the most recently used entry. diff --git a/SECURITY.md b/SECURITY.md index 9583540..05ea8c2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported versions -Security fixes target the latest version published on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=grikomsn.opencode-bridge-copilot-chat). Update to the latest release before reporting an issue that may already be fixed. +Security fixes target the latest version published on the [Visual Studio Marketplace](https://marketplace.visualstudio.com/items?itemName=grikomsn.opencode-bridge-for-copilot-chat). Update to the latest release before reporting an issue that may already be fixed. ## Reporting a vulnerability diff --git a/docs/development.md b/docs/development.md index 537c2c7..c861168 100644 --- a/docs/development.md +++ b/docs/development.md @@ -7,3 +7,9 @@ npm run package ``` The extension host is the right place to test authenticated inference. Unit tests use injected fetchers and fake Secret Storage; they never contact OpenCode with real credentials. + +## Releases + +User-visible changes use Changesets. The release workflow opens a version pull request; after it is merged, CI validates and packages the extension, publishes it to the Visual Studio Marketplace using the existing `VSCE_PAT` repository secret, and attaches the VSIX to a GitHub release. + +The extension ID is `grikomsn.opencode-bridge-for-copilot-chat`; release assets use `opencode-bridge-for-copilot-chat-.vsix`. `npm run release` validates and publishes locally when `VSCE_PAT` is available. diff --git a/package-lock.json b/package-lock.json index 1b21759..f9fefba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "opencode-bridge-copilot-chat", - "version": "0.4.7", + "name": "opencode-bridge-for-copilot-chat", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "opencode-bridge-copilot-chat", - "version": "0.4.7", + "name": "opencode-bridge-for-copilot-chat", + "version": "1.0.0", "license": "MIT", "devDependencies": { "@changesets/cli": "^2.31.0", diff --git a/package.json b/package.json index e1c288d..f73d68f 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { - "name": "opencode-bridge-copilot-chat", - "displayName": "OpenCode for Copilot Chat", + "name": "opencode-bridge-for-copilot-chat", + "displayName": "OpenCode Bridge for Copilot Chat", "description": "Use OpenCode Console and Go models in GitHub Copilot Chat.", - "version": "0.8.0", + "version": "1.0.0", "publisher": "grikomsn", "license": "MIT", "icon": "assets/icon.png", @@ -83,7 +83,7 @@ } ], "configuration": { - "title": "OpenCode for Copilot Chat", + "title": "OpenCode Bridge for Copilot Chat", "properties": { "opencode.defaultMode": { "type": "string", @@ -412,7 +412,7 @@ "check": "npm test", "changeset": "changeset", "version": "changeset version", - "release": "npm run package", + "release": "npm run check && vsce publish --skip-duplicate", "prepackage": "npm run check", "package": "vsce package", "vscode:prepublish": "npm run compile" diff --git a/src/commands/commands.ts b/src/commands/commands.ts index 2895447..b285dea 100644 --- a/src/commands/commands.ts +++ b/src/commands/commands.ts @@ -363,7 +363,7 @@ async function diagnostics(auth: OpenCodeAuth, providers: OpenCodeProviders): Pr const session = await auth.getConsoleSession(activeConsole); const apiKeys = await auth.getApiKeys(); const lines = [ - "# OpenCode for Copilot Chat diagnostics", "", `- VS Code: ${vscode.version}`, + "# OpenCode Bridge for Copilot Chat diagnostics", "", `- VS Code: ${vscode.version}`, `- Console profiles: ${profiles.length ? profiles.join(", ") : "none"}`, `- Active Console profile: ${activeConsole}`, `- Active Console session: ${session ? "present" : "missing"}`, diff --git a/src/extension.ts b/src/extension.ts index 7f146ab..0878996 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,7 +2,6 @@ import * as vscode from "vscode"; import { registerInlineCompletions } from "./autocomplete"; import { OpenCodeAuth } from "./auth/auth"; import { registerCommands } from "./commands/commands"; -import { messageOf } from "./errors"; import { OpenCodeProvider } from "./provider"; import { ModelCatalog } from "./models/catalog"; import { ModelsDevMetadata } from "./models/metadata"; @@ -10,14 +9,10 @@ import { OPENCODE_PROVIDER_DEFINITIONS } from "./provider/definitions"; import { formatUsageStatus, formatUsageTooltip } from "./usage/presentation"; import type { OpenCodeUsageSnapshot } from "./usage/domain"; import { activeConsoleProfileFromState } from "./provider-profile"; -import { isNewerVersion, parseExtensionRelease, RELEASES_LATEST_API_URL } from "./updates/releases"; const LEGACY_USAGE_STATE_KEY = "opencode.usageSnapshot.v1"; const USAGE_STATE_KEY = "opencode.usageSnapshots.v2"; const ACTIVE_CONSOLE_PROFILE_STATE_KEY = "opencode.activeConsoleProfile.v1"; -const RELEASE_CHECKED_AT_STATE_KEY = "opencode.releaseCheckedAt.v1"; -const RELEASE_NOTIFIED_TAG_STATE_KEY = "opencode.releaseNotifiedTag.v1"; -const RELEASE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1_000; export function activate(context: vscode.ExtensionContext): void { const output = vscode.window.createOutputChannel("OpenCode"); @@ -88,38 +83,7 @@ export function activate(context: vscode.ExtensionContext): void { userAgent, }), ); - const releaseTimer = setInterval(() => void checkForRelease(context, version, userAgent, output), RELEASE_CHECK_INTERVAL_MS); - context.subscriptions.push(new vscode.Disposable(() => clearInterval(releaseTimer))); - void checkForRelease(context, version, userAgent, output); - output.appendLine(`[activate] OpenCode for Copilot Chat ${version} on VS Code ${vscode.version}`); -} - -async function checkForRelease( - context: vscode.ExtensionContext, - currentVersion: string, - userAgent: string, - output: vscode.OutputChannel, -): Promise { - const checkedAt = context.globalState.get(RELEASE_CHECKED_AT_STATE_KEY) ?? 0; - if (Date.now() - checkedAt < RELEASE_CHECK_INTERVAL_MS) return; - await context.globalState.update(RELEASE_CHECKED_AT_STATE_KEY, Date.now()); - try { - const response = await fetch(RELEASES_LATEST_API_URL, { - headers: { Accept: "application/vnd.github+json", "User-Agent": userAgent, "X-GitHub-Api-Version": "2022-11-28" }, - }); - if (!response.ok) throw new Error(`GitHub release check returned HTTP ${response.status}`); - const release = parseExtensionRelease(await response.json()); - if (!release || !isNewerVersion(release.version, currentVersion)) return; - if (context.globalState.get(RELEASE_NOTIFIED_TAG_STATE_KEY) === release.tag) return; - await context.globalState.update(RELEASE_NOTIFIED_TAG_STATE_KEY, release.tag); - const action = await vscode.window.showInformationMessage( - `OpenCode for Copilot Chat ${release.version} is available. Download the VSIX, then use Extensions → … → Install from VSIX… to update.`, - "View Release", - ); - if (action === "View Release") await vscode.env.openExternal(vscode.Uri.parse(release.pageUrl)); - } catch (error) { - output.appendLine(`[updates] daily GitHub release check failed: ${messageOf(error)}`); - } + output.appendLine(`[activate] OpenCode Bridge for Copilot Chat ${version} on VS Code ${vscode.version}`); } function renderUsageStatus(item: vscode.StatusBarItem, snapshot: OpenCodeUsageSnapshot): void { diff --git a/src/updates/releases.test.ts b/src/updates/releases.test.ts deleted file mode 100644 index c202c38..0000000 --- a/src/updates/releases.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import assert from "node:assert/strict"; -import { describe, it } from "node:test"; -import { isNewerVersion, parseExtensionRelease } from "./releases"; - -describe("extension release updates", () => { - it("parses a stable GitHub release with a VSIX asset", () => { - assert.deepEqual(parseExtensionRelease({ - tag_name: "v0.4.9", - html_url: "https://github.com/grikomsn/opencode-copilot-chat/releases/tag/v0.4.9", - assets: [{ - name: "opencode-bridge-copilot-chat-0.4.9.vsix", - browser_download_url: "https://github.com/grikomsn/opencode-copilot-chat/releases/download/v0.4.9/opencode-bridge-copilot-chat-0.4.9.vsix", - }], - }), { - tag: "v0.4.9", - version: "0.4.9", - pageUrl: "https://github.com/grikomsn/opencode-copilot-chat/releases/tag/v0.4.9", - vsixUrl: "https://github.com/grikomsn/opencode-copilot-chat/releases/download/v0.4.9/opencode-bridge-copilot-chat-0.4.9.vsix", - }); - }); - - it("rejects malformed releases, prereleases, missing assets, and untrusted URLs", () => { - assert.equal(parseExtensionRelease(undefined), undefined); - assert.equal(parseExtensionRelease({ tag_name: "v0.5.0-beta.1", html_url: "https://github.com/example", assets: [] }), undefined); - assert.equal(parseExtensionRelease({ tag_name: "v0.5.0", html_url: "https://github.com/example", assets: [] }), undefined); - assert.equal(parseExtensionRelease({ - tag_name: "v0.5.0", - html_url: "https://example.com/releases/v0.5.0", - assets: [{ name: "extension.vsix", browser_download_url: "https://example.com/extension.vsix" }], - }), undefined); - assert.equal(parseExtensionRelease({ - tag_name: "v0.5.0", - html_url: "https://github.com/another/repository/releases/tag/v0.5.0", - assets: [{ name: "extension.vsix", browser_download_url: "https://github.com/another/repository/releases/download/v0.5.0/extension.vsix" }], - }), undefined); - }); - - it("compares stable semantic versions", () => { - assert.equal(isNewerVersion("0.4.9", "0.4.8"), true); - assert.equal(isNewerVersion("0.5.0", "0.4.9"), true); - assert.equal(isNewerVersion("1.0.0", "0.99.99"), true); - assert.equal(isNewerVersion("0.4.8", "0.4.8"), false); - assert.equal(isNewerVersion("0.4.7", "0.4.8"), false); - assert.equal(isNewerVersion("latest", "0.4.8"), false); - }); -}); diff --git a/src/updates/releases.ts b/src/updates/releases.ts deleted file mode 100644 index 42fcd32..0000000 --- a/src/updates/releases.ts +++ /dev/null @@ -1,58 +0,0 @@ -export const RELEASES_LATEST_API_URL = "https://api.github.com/repos/grikomsn/opencode-copilot-chat/releases/latest"; - -export interface ExtensionRelease { - readonly tag: string; - readonly version: string; - readonly pageUrl: string; - readonly vsixUrl: string; -} - -export function parseExtensionRelease(value: unknown): ExtensionRelease | undefined { - if (!isRecord(value) || typeof value.tag_name !== "string" || typeof value.html_url !== "string" || !Array.isArray(value.assets)) return undefined; - const version = normalizedVersion(value.tag_name); - if (!version || !isTrustedReleaseUrl(value.html_url)) return undefined; - const asset = value.assets.find((candidate) => isRecord(candidate) - && typeof candidate.name === "string" - && candidate.name.endsWith(".vsix") - && typeof candidate.browser_download_url === "string" - && isTrustedReleaseUrl(candidate.browser_download_url)); - if (!isRecord(asset) || typeof asset.browser_download_url !== "string") return undefined; - return { tag: value.tag_name, version, pageUrl: value.html_url, vsixUrl: asset.browser_download_url }; -} - -export function isNewerVersion(candidate: string, current: string): boolean { - const candidateParts = versionParts(candidate); - const currentParts = versionParts(current); - if (!candidateParts || !currentParts) return false; - for (let index = 0; index < candidateParts.length; index += 1) { - if (candidateParts[index] !== currentParts[index]) return candidateParts[index] > currentParts[index]; - } - return false; -} - -function normalizedVersion(value: string): string | undefined { - const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(value.trim()); - return match ? `${match[1]}.${match[2]}.${match[3]}` : undefined; -} - -function versionParts(value: string): readonly [number, number, number] | undefined { - const normalized = normalizedVersion(value); - if (!normalized) return undefined; - const parts = normalized.split(".").map(Number); - return [parts[0], parts[1], parts[2]]; -} - -function isTrustedReleaseUrl(value: string): boolean { - try { - const url = new URL(value); - return url.protocol === "https:" - && url.hostname === "github.com" - && url.pathname.startsWith("/grikomsn/opencode-copilot-chat/releases/"); - } catch { - return false; - } -} - -function isRecord(value: unknown): value is Record { - return typeof value === "object" && value !== null; -}