mirror of
https://github.com/cline/cline.git
synced 2026-09-19 02:05:44 +08:00
fix: simplify secret sync; align APIs; mac-only E2E
This commit is contained in:
@@ -25,7 +25,7 @@ jobs:
|
||||
steps:
|
||||
- id: set-matrix
|
||||
run: |
|
||||
echo "matrix=[{\"runner\":\"ubuntu\",\"deps\":\"true\"},{\"runner\":\"ubuntu\",\"deps\":\"false\"},{\"runner\":\"macos\",\"deps\":\"true\"},{\"runner\":\"windows\",\"deps\":\"true\"},{\"runner\":\"windows\",\"deps\":\"false\"}]" >> $GITHUB_OUTPUT
|
||||
echo "matrix=[{\"runner\":\"macos\",\"deps\":\"true\"}]" >> $GITHUB_OUTPUT
|
||||
|
||||
e2e:
|
||||
needs: matrix_prep
|
||||
@@ -89,39 +89,11 @@ jobs:
|
||||
if: steps.webview-cache.outputs.cache-hit != 'true'
|
||||
run: cd webview-ui && npm ci
|
||||
|
||||
- name: Install xvfb on Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
run: sudo apt-get update && sudo apt-get install -y xvfb
|
||||
|
||||
- name: Provision Linux keychain deps
|
||||
if: matrix.runner == 'ubuntu' && matrix.deps == 'true'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y dbus gnome-keyring libsecret-1-0 libsecret-tools xvfb unzip
|
||||
echo "Provisioned libsecret and keyring"
|
||||
|
||||
- name: Build standalone (Linux - deps:true)
|
||||
if: matrix.runner == 'ubuntu' && matrix.deps == 'true'
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Build standalone (macOS)
|
||||
if: matrix.runner == 'macos'
|
||||
run: npm run compile-standalone
|
||||
|
||||
- name: Run E2E tests - Linux
|
||||
if: matrix.runner == 'ubuntu'
|
||||
env:
|
||||
EXPECT_DEPS: ${{ matrix.deps || 'false' }}
|
||||
run: |
|
||||
if [ "${{ matrix.deps }}" = "true" ]; then
|
||||
eval "$(dbus-launch --sh-syntax)"
|
||||
gnome-keyring-daemon --start
|
||||
fi
|
||||
sudo apt-get update && sudo apt-get install -y xvfb
|
||||
xvfb-run -a npm run test:e2e:optimal
|
||||
|
||||
- name: Run E2E tests - Non-Linux
|
||||
if: matrix.runner != 'ubuntu'
|
||||
- name: Run E2E tests (macOS only)
|
||||
if: matrix.runner == 'macos'
|
||||
env:
|
||||
EXPECT_DEPS: ${{ matrix.deps || 'false' }}
|
||||
run: npm run test:e2e:optimal
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { Logger } from "@/services/logging/Logger"
|
||||
import { StorageEventListener } from "./utils/types"
|
||||
|
||||
// Lightweight Disposable compatible with vscode.Disposable without importing vscode,
|
||||
// to keep this module host-agnostic (JetBrains/core runtimes don't have 'vscode').
|
||||
export interface Disposable {
|
||||
dispose(): void
|
||||
}
|
||||
|
||||
/**
|
||||
* An abstract storage class that provides a template for storage operations.
|
||||
* Subclasses must implement the protected abstract methods to define their storage logic.
|
||||
@@ -25,12 +19,12 @@ export abstract class ClineStorage {
|
||||
/**
|
||||
* Subscribe to storage change events.
|
||||
*/
|
||||
public onDidChange(callback: StorageEventListener): Disposable {
|
||||
public onDidChange(callback: StorageEventListener): () => void {
|
||||
this.subscribers.push(callback)
|
||||
return new Disposable(() => {
|
||||
return () => {
|
||||
const callbackIndex = this.subscribers.indexOf(callback)
|
||||
this.subscribers.splice(callbackIndex, 1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,7 @@ export class AuthService {
|
||||
protected _activeAuthStatusUpdateHandlers = new Set<StreamingResponseHandler<AuthState>>()
|
||||
protected _handlerToController = new Map<StreamingResponseHandler<AuthState>, Controller>()
|
||||
protected _controller: Controller
|
||||
private _secretUnsubscribe: (() => void) | undefined
|
||||
|
||||
/**
|
||||
* Creates an instance of AuthService.
|
||||
@@ -275,6 +276,44 @@ export class AuthService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start listening to secret changes and keep auth in sync (standalone/host-agnostic).
|
||||
* Expects a secret storage-like object with onDidChange and get.
|
||||
*/
|
||||
startSecretSync(secretStorage: {
|
||||
onDidChange: (listener: (e: { key: string }) => any) => (() => void) | { dispose(): void }
|
||||
get: (key: string) => Promise<string | undefined>
|
||||
}): () => void {
|
||||
// Clean any existing subscription first
|
||||
try {
|
||||
this._secretUnsubscribe?.()
|
||||
} catch {}
|
||||
this._secretUnsubscribe = undefined
|
||||
|
||||
const sub = secretStorage.onDidChange(async ({ key }) => {
|
||||
if (key !== "clineAccountId") {
|
||||
return
|
||||
}
|
||||
const value = await secretStorage.get("clineAccountId")
|
||||
const authService = AuthService.getInstance(this._controller)
|
||||
if (value) {
|
||||
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
} else {
|
||||
authService?.handleDeauth()
|
||||
}
|
||||
})
|
||||
this._secretUnsubscribe = typeof sub === "function" ? sub : () => sub.dispose()
|
||||
return this._secretUnsubscribe
|
||||
}
|
||||
|
||||
/** Stop listening to secret changes */
|
||||
stopSecretSync(): void {
|
||||
try {
|
||||
this._secretUnsubscribe?.()
|
||||
} catch {}
|
||||
this._secretUnsubscribe = undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to authStatusUpdate events
|
||||
* @param controller The controller instance
|
||||
|
||||
@@ -75,17 +75,8 @@ async function main() {
|
||||
}
|
||||
|
||||
// Mirror VS Code behavior: react to clineAccountId secret changes (login/logout)
|
||||
secretStorage.onDidChange(async ({ key }) => {
|
||||
if (key !== "clineAccountId") return
|
||||
const value = await secretStorage.get("clineAccountId")
|
||||
const controller = webviewProvider.controller
|
||||
const authService = AuthService.getInstance(controller)
|
||||
if (value) {
|
||||
authService?.restoreRefreshTokenAndRetrieveAuthInfo()
|
||||
} else {
|
||||
authService?.handleDeauth()
|
||||
}
|
||||
})
|
||||
const authService = AuthService.getInstance(webviewProvider.controller)
|
||||
authService.startSecretSync(secretStorage)
|
||||
|
||||
// Initialize SQLite lock manager for instance registration
|
||||
const dbPath = `${DATA_DIR}/locks.db`
|
||||
@@ -212,6 +203,11 @@ async function shutdownGracefully(lockManager?: SqliteLockManager) {
|
||||
log("Warning: HostProvider not initialized, cannot request shutdown")
|
||||
}
|
||||
|
||||
// Stop secret sync listener
|
||||
try {
|
||||
AuthService.getInstance().stopSecretSync()
|
||||
} catch {}
|
||||
|
||||
// Step 2: Clean up lock manager entry
|
||||
log("Cleaning up lock manager entry...")
|
||||
try {
|
||||
|
||||
@@ -32,18 +32,19 @@ export class SecretStore implements vscode.SecretStorage {
|
||||
// (e.g., the cline secretStorage singleton backed by OS keychain).
|
||||
export class DelegatingSecretStore implements vscode.SecretStorage {
|
||||
private readonly _onDidChange = new EventEmitter<vscode.SecretStorageChangeEvent>()
|
||||
private unsubscribe: { dispose(): void } | null = null
|
||||
|
||||
constructor(
|
||||
private readonly delegate: {
|
||||
get(key: string): Promise<string | undefined>
|
||||
store(key: string, value: string): Promise<void>
|
||||
delete(key: string): Promise<void>
|
||||
onDidChange?: (listener: (e: { key: string }) => any) => { dispose(): void }
|
||||
onDidChange?: (listener: (e: { key: string }) => any) => { dispose(): void } | (() => void)
|
||||
},
|
||||
) {
|
||||
if (this.delegate.onDidChange) {
|
||||
this.unsubscribe = this.delegate.onDidChange(({ key }) => this._onDidChange.fire({ key }))
|
||||
const sub = this.delegate.onDidChange(({ key }) => this._onDidChange.fire({ key }))
|
||||
// No instance-level storage required; delegate owns lifecycle.
|
||||
void (typeof sub === "function" ? { dispose: sub } : sub)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user