Repository navigation
fix: OpenCode 2.0.29 GA RPC compatibility and Windows support #40
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
fa214c3
a5c9a94
e10d5be
4e66490
970bea9
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,7 +23,7 @@ import { | |
| type OAuthLoginRequest, | ||
| performOAuthLogin, | ||
| } from './plugin/oauth-login' | ||
| import { persistAccountPool } from './plugin/persist-account-pool' | ||
| import { commitLogins, persistAccountPool } from './plugin/persist-account-pool' | ||
| import { checkAccountsQuotaStandalone } from './plugin/quota' | ||
| import { startOAuthListener } from './plugin/server' | ||
| import { getStoragePath, loadAccounts } from './plugin/storage' | ||
|
|
@@ -400,7 +400,12 @@ const execFileAsync = promisify(execFile) | |
|
|
||
| async function openBrowserDefault(url: string): Promise<void> { | ||
| if (process.platform === 'win32') { | ||
| await execFileAsync('cmd', ['/c', 'start', '', url]) | ||
| // `cmd.exe` treats `&` in the URL as a command separator, so the URL must be | ||
| // passed as a single quoted token or everything after the first `&` (for | ||
| // example `response_type`) is lost. | ||
| await execFileAsync('cmd', ['/c', 'start', '""', `"${url}"`], { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Quoting does not disable Prompt for AI agents |
||
| windowsVerbatimArguments: true, | ||
| }) | ||
| return | ||
| } | ||
| await execFileAsync(process.platform === 'darwin' ? 'open' : 'xdg-open', [ | ||
|
|
@@ -437,9 +442,13 @@ export function createDefaultCliDependencies(): CliDependencies { | |
| exchange: exchangeAntigravity, | ||
| startListener: startOAuthListener, | ||
| openBrowser, | ||
| announce: (url) => { | ||
| process.stdout.write(`Open this URL to sign in:\n${url}\n`) | ||
| }, | ||
| upsert: async (result) => { | ||
| await persistAccountPool( | ||
| [result], | ||
| await persistCliLogin( | ||
| resolve(getStoragePath()), | ||
| result, | ||
| request.startFresh && request.accounts.length === 0, | ||
| ) | ||
| }, | ||
|
|
@@ -548,6 +557,48 @@ async function readAccountStore(legacyPath: string): Promise<CliAccountsRead> { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Persist one successful OAuth login to the active account source. A migrated | ||
| * account store owns the retired pool file and refuses the pre-store writer, | ||
| * so a login must go through the store repository; an uninitialized or legacy | ||
| * layout still writes the pre-store pool file. | ||
| * | ||
| * The store path uses `commitLogins` (the same add/upsert the OpenCode sign-in | ||
| * uses) rather than a pool replacement: a replacement leaves a resumable | ||
| * management journal that does not complete reliably on Windows, and a single | ||
| * interactive login is an add, not a wipe. | ||
| */ | ||
| async function persistCliLogin( | ||
| legacyPath: string, | ||
| result: AntigravityTokenExchangeSuccess, | ||
| replaceAll: boolean, | ||
| ): Promise<void> { | ||
| const modules = await loadCommonAuthStoreModules() | ||
| const admission = await readAccountStoreBinding(legacyPath, modules, Date.now) | ||
| if (admission.status !== 'bound') { | ||
| await persistAccountPool([result], replaceAll) | ||
| return | ||
| } | ||
| const repository = createAccountRepositoryFactory(modules)({ | ||
| paths: admission.paths, | ||
| now: Date.now, | ||
| exchange: async ({ refreshToken }) => { | ||
| const refreshed = await refreshAntigravityToken(refreshToken) | ||
| return { | ||
| accessToken: refreshed.access, | ||
| refreshToken: refreshed.refresh, | ||
| expiresAt: refreshed.expires, | ||
| } | ||
| }, | ||
| }) | ||
| try { | ||
| const [committed] = await commitLogins(repository, [result]) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: A repeat standalone Prompt for AI agents |
||
| if (committed?.status === 'refused') throw new Error(committed.message) | ||
| } finally { | ||
| await repository.dispose() | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * The account-store operations over the embedded common-auth `./store` and | ||
| * `./fs` modules. The operator's `--offline` flag and confirmation are what | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { describe, expect, it } from 'bun:test' | ||
|
|
||
| import { toRpcJson } from './index.ts' | ||
|
|
||
| describe('toRpcJson', () => { | ||
| it('drops undefined-valued keys so the answer is a JSON value', () => { | ||
| const value = toRpcJson({ | ||
| id: 'acct-0', | ||
| cooldownUntil: undefined, | ||
| quota: { gemini: undefined, 'non-gemini': { remainingPercent: 100 } }, | ||
| nested: [{ keep: 1, drop: undefined }], | ||
| }) | ||
| expect(value).toEqual({ | ||
| id: 'acct-0', | ||
| quota: { 'non-gemini': { remainingPercent: 100 } }, | ||
| nested: [{ keep: 1 }], | ||
| }) | ||
| expect(JSON.stringify(value)).toBe( | ||
| '{"id":"acct-0","quota":{"non-gemini":{"remainingPercent":100}},"nested":[{"keep":1}]}', | ||
| ) | ||
| }) | ||
|
|
||
| it('keeps null, false, zero and empty strings, and returns primitives unchanged', () => { | ||
| expect(toRpcJson(null)).toBeNull() | ||
| expect(toRpcJson(false)).toBe(false) | ||
| expect(toRpcJson(0)).toBe(0) | ||
| expect(toRpcJson('')).toBe('') | ||
| expect(toRpcJson({ a: null, b: false, c: 0, d: '' })).toEqual({ | ||
| a: null, | ||
| b: false, | ||
| c: 0, | ||
| d: '', | ||
| }) | ||
| }) | ||
|
|
||
| it('preserves non-plain objects instead of inspecting their internals', () => { | ||
| const date = new Date(0) | ||
| expect(toRpcJson(date)).toBe(date) | ||
| const array = [date] | ||
| expect(toRpcJson(array)[0]).toBe(date) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,8 @@ export interface OAuthLoginDependencies { | |
| exchange: typeof exchangeAntigravity | ||
| startListener: typeof startOAuthListener | ||
| openBrowser(url: string): Promise<void> | ||
| /** Reports the authorization URL when the browser is not opened (headless / --no-browser). */ | ||
| announce?(url: string): void | ||
| upsert(result: AntigravityTokenExchangeSuccess): Promise<void> | ||
| } | ||
|
|
||
|
|
@@ -56,6 +58,8 @@ export async function performOAuthLogin( | |
| const authorization = await deps.authorize(request.projectId ?? '') | ||
| if (!request.noBrowser && !request.isHeadless) { | ||
| await deps.openBrowser(authorization.url) | ||
| } else { | ||
| deps.announce?.(authorization.url) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: This can leave OpenCode Prompt for AI agents |
||
| } | ||
|
|
||
| const params = callbackParams( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,7 @@ async function verifiedCompiler() { | |
| const installed = await realpath( | ||
| resolve(toolRoot, 'node_modules', name, 'package.json'), | ||
| ) | ||
| if (!installed.startsWith(`${toolRoot}/node_modules/`)) | ||
| if (!installed.startsWith(`${toolRoot}${sep}node_modules${sep}`)) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: On Windows, Prompt for AI agents |
||
| throw new Error(`Private tool root escaped: ${name}`) | ||
| if (JSON.parse(await readFile(installed, 'utf8')).version !== version) | ||
| throw new Error(`Private tool version mismatch: ${name}`) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: This says the plugin registers nothing without
providers.google, but setup still registers the RPC, bridge, hooks, and/antigravitycommand; only the catalog update is skipped. Say it registers no Antigravity models.Prompt for AI agents
-The directory example follows the shipped forwarders, not a verified GA npm-installer command. The pinned harness uses an instrumented wrapper directory; direct npm-spec registration/installer syntax is not established here. Consult the matching host's install/config documentation rather than assuming V1's
opencode pluginortui.jsonbehavior. Do not copy the harness's synthetic Google API key or mockbaseURLinto a real config. The harness's default fixture config has no user model map; no copied model-map requirement is established. Models are served undergoogle/…, and the package supplies the Antigravity catalog.+The package attaches its Antigravity catalog to the host's
googleprovider, so the host must expose one. OpenCode 2.0.22 shipped agoogleprovider by default; hosts that do not (for example later 2.x builds whose catalog omits it until configured) register nothing untilproviders.googleexists. Declare it alongside the plugin:+
+```jsonc
</file context>