Compare commits

...
4 changed files with 99 additions and 11 deletions
+1 -1
View File
@@ -162,7 +162,7 @@ const standaloneConfig = {
outfile: `${destDir}/cline-core.js`,
// These modules need to load files from the module directory at runtime,
// so they cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check", "better-sqlite3"],
external: ["vscode", "@grpc/reflection", "grpc-health-check", "better-sqlite3", "keytar"],
}
// E2E build script configuration
+18 -4
View File
@@ -23,7 +23,9 @@ const TARGET_PLATFORMS = [
{ platform: "darwin", arch: "arm64", targetDir: "darwin-arm64" },
{ platform: "linux", arch: "x64", targetDir: "linux-x64" },
]
const SUPPORTED_BINARY_MODULES = ["better-sqlite3"]
// Split native modules into required (must prebuild) and optional (never fail build)
const REQUIRED_BINARY_MODULES = ["better-sqlite3"]
const OPTIONAL_BINARY_MODULES = ["keytar"]
const UNIVERSAL_BUILD = !process.argv.includes("-s")
const IS_VERBOSE = process.argv.includes("-v") || process.argv.includes("--verbose")
@@ -64,8 +66,9 @@ async function installNodeDependencies() {
async function packageAllBinaryDeps() {
// Check for native .node modules.
const allNativeModules = await glob("**/*.node", { cwd: path.join(BUILD_DIR, "node_modules"), nodir: true })
const isAllowed = (path) => SUPPORTED_BINARY_MODULES.some((allowed) => path.includes(allowed))
const blocked = allNativeModules.filter((x) => !isAllowed(x))
const isRequired = (modulePath) => REQUIRED_BINARY_MODULES.some((moduleName) => modulePath.includes(moduleName))
const isOptional = (modulePath) => OPTIONAL_BINARY_MODULES.some((moduleName) => modulePath.includes(moduleName))
const blocked = allNativeModules.filter((modulePath) => !isRequired(modulePath) && !isOptional(modulePath))
if (blocked.length > 0) {
console.error(`Error: Native node modules cannot be included in the standalone distribution:\n\n${blocked.join("\n")}`)
@@ -75,7 +78,8 @@ async function packageAllBinaryDeps() {
process.exit(1)
}
for (const module of SUPPORTED_BINARY_MODULES) {
// Prebuild only required native modules
for (const module of REQUIRED_BINARY_MODULES) {
console.log(`Installing binaries for ${module}...`)
const src = path.join(BUILD_DIR, "node_modules", module)
if (!fs.existsSync(src)) {
@@ -103,6 +107,16 @@ async function packageAllBinaryDeps() {
await rmrf(src)
log_verbose("")
}
// On universal builds, avoid shipping host-only optional native modules
if (UNIVERSAL_BUILD) {
for (const module of OPTIONAL_BINARY_MODULES) {
const modPath = path.join(BUILD_DIR, "node_modules", module)
if (fs.existsSync(modPath)) {
await rmrf(modPath)
}
}
}
}
async function zipDistribution() {
+79 -6
View File
@@ -1,33 +1,106 @@
import * as fs from "fs"
import type { EnvironmentVariableMutator, EnvironmentVariableMutatorOptions, EnvironmentVariableScope } from "vscode"
import * as vscode from "vscode"
let keytar: any | null = null
try {
// Runtime require to avoid bundling native module and keep VS Code build clean
// eslint-disable-next-line @typescript-eslint/no-var-requires
// @ts-ignore
keytar = require("keytar")
} catch (err) {
console.warn("[cline] Failed to load keytar; falling back to JSON secret store.", err)
keytar = null
}
const SERVICE_NAME = "cline"
export class SecretStore implements vscode.SecretStorage {
private data: JsonKeyValueStore<string>
private readonly _onDidChange = new EventEmitter<vscode.SecretStorageChangeEvent>()
private readonly jsonStore: JsonKeyValueStore<string>
private readonly jsonFilePath: string
constructor(filepath: string) {
this.data = new JsonKeyValueStore(filepath)
// JSON store always created as fallback, even when keytar is available
this.jsonStore = new JsonKeyValueStore<string>(filepath)
this.jsonFilePath = filepath
if (keytar) {
void this.migrateSecretsToKeytarIfNeeded()
}
}
readonly onDidChange: vscode.Event<vscode.SecretStorageChangeEvent> = this._onDidChange.event
get(key: string): Thenable<string | undefined> {
return Promise.resolve(this.data.get(key))
if (keytar) {
return keytar.getPassword(SERVICE_NAME, key).then((v: string | null) => (v === null ? undefined : v))
}
return Promise.resolve(this.jsonStore.get(key))
}
store(key: string, value: string): Thenable<void> {
this.data.put(key, value)
if (keytar) {
return keytar.setPassword(SERVICE_NAME, key, value).then(() => {
this._onDidChange.fire({ key })
})
}
this.jsonStore.put(key, value)
this._onDidChange.fire({ key })
return Promise.resolve()
}
delete(key: string): Thenable<void> {
this.data.delete(key)
if (keytar) {
return keytar.deletePassword(SERVICE_NAME, key).then(() => {
this._onDidChange.fire({ key })
})
}
this.jsonStore.delete(key)
this._onDidChange.fire({ key })
return Promise.resolve()
}
}
private async migrateSecretsToKeytarIfNeeded(): Promise<void> {
try {
// Skip if keytar already has secrets or no JSON file exists
const existingSecrets = await keytar!.findCredentials(SERVICE_NAME)
if (existingSecrets?.length > 0 || !fs.existsSync(this.jsonFilePath)) return
const data = JSON.parse(fs.readFileSync(this.jsonFilePath, "utf-8"))
const secrets = Object.entries(data || {}).filter(([, v]) => typeof v === "string" && (v as string).length > 0)
if (secrets.length === 0) {
fs.unlinkSync(this.jsonFilePath)
return
}
const migratedKeys: string[] = []
for (const [k, v] of secrets) {
try {
await keytar!.setPassword(SERVICE_NAME, k, v as string)
migratedKeys.push(k)
} catch (err) {
console.warn(`[cline] Failed to migrate secret for key "${k}" to keytar. Rolling back.`, err)
// Roll back any migrated keys, keep JSON intact
await Promise.all(
migratedKeys.map(async (mk) => {
try {
await keytar!.deletePassword(SERVICE_NAME, mk)
} catch (rollbackErr) {
console.warn(`[cline] Failed to rollback migrated key "${mk}" from keytar.`, rollbackErr)
}
}),
)
return
}
}
fs.unlinkSync(this.jsonFilePath)
} catch (err) {
console.warn("[cline] Secret migration to keytar failed. Keeping JSON store.", err)
}
}
}
// Create a class that implements Memento interface with the required setKeysForSync method
export class MementoStore implements vscode.Memento {
private data: JsonKeyValueStore<any>
+1
View File
@@ -7,6 +7,7 @@
"@grpc/reflection": "^1.0.4",
"better-sqlite3": "^12.2.0",
"grpc-health-check": "^2.0.2",
"keytar": "^7.9.0",
"open": "^10.1.2",
"vscode-uri": "^3.1.0"
}