Compare commits

...

2 Commits

Author SHA1 Message Date
celestial-vault e0a9510c1c using most up to date versions of these files 2025-09-22 19:03:26 -07:00
celestial-vault d03957f21d locking changes 2025-09-22 17:42:57 -07:00
12 changed files with 864 additions and 176 deletions
+14 -20
View File
@@ -6,12 +6,15 @@ import * as esbuild from "esbuild"
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const production = process.argv.includes("--production") || process.env["IS_DEBUG_BUILD"] === "false"
const production = process.argv.includes("--production")
const watch = process.argv.includes("--watch")
const standalone = process.argv.includes("--standalone")
const e2eBuild = process.argv.includes("--e2e-build")
const destDir = standalone ? "dist-standalone" : "dist"
// Read package.json to get version for build-time injection
const packageJson = JSON.parse(fs.readFileSync(path.join(__dirname, "package.json"), "utf8"))
/**
* @type {import('esbuild').Plugin}
*/
@@ -123,29 +126,15 @@ const copyWasmFiles = {
},
}
const buildEnvVars = { "import.meta.url": "_importMetaUrl" }
if (production) {
// IS_DEV is always disable in production builds.
buildEnvVars["process.env.IS_DEV"] = "false"
}
// Set the environment and telemetry env vars. The API key env vars need to be populated in the GitHub
// workflows from the secrets.
if (process.env.CLINE_ENVIRONMENT) {
buildEnvVars["process.env.CLINE_ENVIRONMENT"] = JSON.stringify(process.env.CLINE_ENVIRONMENT)
}
if (process.env.TELEMETRY_SERVICE_API_KEY) {
buildEnvVars["process.env.TELEMETRY_SERVICE_API_KEY"] = JSON.stringify(process.env.TELEMETRY_SERVICE_API_KEY)
}
if (process.env.ERROR_SERVICE_API_KEY) {
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
}
// Base configuration shared between extension and standalone builds
const baseConfig = {
bundle: true,
minify: production,
sourcemap: !production,
logLevel: "silent",
define: buildEnvVars,
define: production
? { "import.meta.url": "_importMetaUrl", "process.env.IS_DEV": JSON.stringify(!production) }
: { "import.meta.url": "_importMetaUrl" },
tsconfig: path.resolve(__dirname, "tsconfig.json"),
plugins: [
copyWasmFiles,
@@ -174,9 +163,14 @@ const standaloneConfig = {
...baseConfig,
entryPoints: ["src/standalone/cline-core.ts"],
outfile: `${destDir}/cline-core.js`,
// These modules need to load files from the module directory at runtime,
// so they cannot be bundled.
// These gRPC protos need to load files from the module directory at runtime,
// so they cannot be bundled. better-sqlite3 is a native module that also cannot be bundled.
external: ["vscode", "@grpc/reflection", "grpc-health-check", "better-sqlite3"],
// Inject version at build time for standalone builds
define: {
...baseConfig.define,
"process.env.CLINE_VERSION": JSON.stringify(packageJson.version),
},
}
// E2E build script configuration
+30 -22
View File
@@ -36,6 +36,7 @@
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"axios": "^1.8.2",
"better-sqlite3": "^12.4.1",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"chrome-launcher": "^1.1.2",
@@ -6279,6 +6280,20 @@
"node": ">=4"
}
},
"node_modules/better-sqlite3": {
"version": "12.4.1",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
"integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"bindings": "^1.5.0",
"prebuild-install": "^7.1.1"
},
"engines": {
"node": "20.x || 22.x || 23.x || 24.x"
}
},
"node_modules/big-integer": {
"version": "1.6.52",
"license": "Unlicense",
@@ -6326,6 +6341,15 @@
"url": "https://bevry.me/fund"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"license": "MIT",
"dependencies": {
"file-uri-to-path": "1.0.0"
}
},
"node_modules/bluebird": {
"version": "3.4.7",
"license": "MIT"
@@ -7278,7 +7302,6 @@
},
"node_modules/decompress-response": {
"version": "6.0.0",
"dev": true,
"license": "MIT",
"dependencies": {
"mimic-response": "^3.1.0"
@@ -7292,7 +7315,6 @@
},
"node_modules/decompress-response/node_modules/mimic-response": {
"version": "3.1.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -7314,7 +7336,6 @@
},
"node_modules/deep-extend": {
"version": "0.6.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4.0.0"
@@ -8266,7 +8287,6 @@
},
"node_modules/expand-template": {
"version": "2.0.3",
"dev": true,
"license": "(MIT OR WTFPL)",
"engines": {
"node": ">=6"
@@ -8535,6 +8555,12 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
"license": "MIT"
},
"node_modules/fill-keys": {
"version": "1.0.2",
"dev": true,
@@ -9104,7 +9130,6 @@
},
"node_modules/github-from-package": {
"version": "0.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/glob": {
@@ -9541,7 +9566,6 @@
},
"node_modules/ini": {
"version": "1.3.8",
"dev": true,
"license": "ISC"
},
"node_modules/internal-slot": {
@@ -11129,7 +11153,6 @@
},
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"dev": true,
"license": "MIT"
},
"node_modules/mocha": {
@@ -11357,7 +11380,6 @@
},
"node_modules/napi-build-utils": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/negotiator": {
@@ -11420,7 +11442,6 @@
},
"node_modules/node-abi": {
"version": "3.75.0",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.3.5"
@@ -12559,7 +12580,6 @@
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
"dev": true,
"license": "MIT",
"dependencies": {
"detect-libc": "^2.0.0",
@@ -12584,7 +12604,6 @@
},
"node_modules/prebuild-install/node_modules/bl": {
"version": "4.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer": "^5.5.0",
@@ -12594,7 +12613,6 @@
},
"node_modules/prebuild-install/node_modules/buffer": {
"version": "5.7.1",
"dev": true,
"funding": [
{
"type": "github",
@@ -12617,12 +12635,10 @@
},
"node_modules/prebuild-install/node_modules/chownr": {
"version": "1.1.4",
"dev": true,
"license": "ISC"
},
"node_modules/prebuild-install/node_modules/detect-libc": {
"version": "2.0.4",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -12630,7 +12646,6 @@
},
"node_modules/prebuild-install/node_modules/readable-stream": {
"version": "3.6.2",
"dev": true,
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
@@ -12643,7 +12658,6 @@
},
"node_modules/prebuild-install/node_modules/tar-fs": {
"version": "2.1.3",
"dev": true,
"license": "MIT",
"dependencies": {
"chownr": "^1.1.1",
@@ -12654,7 +12668,6 @@
},
"node_modules/prebuild-install/node_modules/tar-stream": {
"version": "2.2.0",
"dev": true,
"license": "MIT",
"dependencies": {
"bl": "^4.0.3",
@@ -12899,7 +12912,6 @@
},
"node_modules/rc": {
"version": "1.2.8",
"dev": true,
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
@@ -12924,7 +12936,6 @@
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@@ -13804,7 +13815,6 @@
},
"node_modules/simple-concat": {
"version": "1.0.1",
"dev": true,
"funding": [
{
"type": "github",
@@ -13823,7 +13833,6 @@
},
"node_modules/simple-get": {
"version": "4.0.1",
"dev": true,
"funding": [
{
"type": "github",
@@ -14741,7 +14750,6 @@
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
+1
View File
@@ -453,6 +453,7 @@
"@vscode/codicons": "^0.0.36",
"archiver": "^7.0.1",
"axios": "^1.8.2",
"better-sqlite3": "^12.4.1",
"cheerio": "^1.0.0",
"chokidar": "^4.0.1",
"chrome-launcher": "^1.1.2",
+38 -53
View File
@@ -1,14 +1,15 @@
syntax = "proto3";
package cline;
import "cline/common.proto";
import "cline/models.proto";
option java_package = "bot.cline.proto";
option java_multiple_files = true;
option go_package = "github.com/cline/grpc-go/cline";
service StateService {
rpc getLatestState(EmptyRequest) returns (State);
rpc updateTerminalConnectionTimeout(UpdateTerminalConnectionTimeoutRequest) returns (UpdateTerminalConnectionTimeoutResponse);
rpc updateTerminalReuseEnabled(BooleanRequest) returns (Empty);
rpc updateDefaultTerminalProfile(StringRequest) returns (TerminalProfileUpdateResponse);
rpc getAvailableTerminalProfiles(EmptyRequest) returns (TerminalProfiles);
rpc subscribeToState(EmptyRequest) returns (stream State);
rpc toggleFavoriteModel(StringRequest) returns (Empty);
@@ -18,6 +19,7 @@ service StateService {
rpc updateSettings(UpdateSettingsRequest) returns (Empty);
rpc updateTelemetrySetting(TelemetrySettingRequest) returns (Empty);
rpc setWelcomeViewCompleted(BooleanRequest) returns (Empty);
rpc getProcessInfo(EmptyRequest) returns (ProcessInfo);
}
message State {
@@ -56,7 +58,6 @@ enum OpenaiReasoningEffort {
LOW = 0;
MEDIUM = 1;
HIGH = 2;
MINIMAL = 3;
}
enum McpDisplayMode {
@@ -107,16 +108,6 @@ message TelemetrySettingRequest {
TelemetrySettingEnum setting = 2;
}
// Browser settings for UpdateSettingsRequest
message BrowserSettingsUpdate {
optional Viewport viewport = 1;
optional string remote_browser_host = 2;
optional bool remote_browser_enabled = 3;
optional string chrome_executable_path = 4;
optional bool disable_tool_use = 5;
optional string custom_args = 6;
}
// Message for updating settings
message UpdateSettingsRequest {
Metadata metadata = 1;
@@ -137,9 +128,6 @@ message UpdateSettingsRequest {
optional FocusChainSettings focus_chain_settings = 17;
optional bool use_auto_condense = 18;
optional string custom_prompt = 19;
optional BrowserSettingsUpdate browser_settings = 20;
optional string default_terminal_profile = 21;
optional bool yolo_mode_toggled = 22;
}
// Complete API Configuration message
@@ -151,10 +139,10 @@ message ApiConfiguration {
optional string lite_llm_base_url = 4;
optional string lite_llm_api_key = 5;
optional bool lite_llm_use_prompt_cache = 6;
map<string, string> open_ai_headers = 7;
optional string openai_headers = 7; // JSON string
optional string anthropic_base_url = 8;
optional string open_router_api_key = 9;
optional string open_router_provider_sorting = 10;
optional string openrouter_api_key = 9;
optional string openrouter_provider_sorting = 10;
optional string aws_access_key = 11;
optional string aws_secret_key = 12;
optional string aws_session_token = 13;
@@ -167,14 +155,14 @@ message ApiConfiguration {
optional string claude_code_path = 20;
optional string vertex_project_id = 21;
optional string vertex_region = 22;
optional string open_ai_base_url = 23;
optional string open_ai_api_key = 24;
optional string openai_base_url = 23;
optional string openai_api_key = 24;
optional string ollama_base_url = 25;
optional string ollama_api_options_ctx_num = 26;
optional string lm_studio_base_url = 27;
optional string gemini_api_key = 28;
optional string gemini_base_url = 29;
optional string open_ai_native_api_key = 30;
optional string openai_native_api_key = 30;
optional string deep_seek_api_key = 31;
optional string requesty_api_key = 32;
optional string requesty_base_url = 33;
@@ -210,65 +198,61 @@ message ApiConfiguration {
optional string qwen_code_oauth_path = 63;
optional string dify_api_key = 64;
optional string dify_base_url = 65;
optional string oca_base_url = 66;
optional string oca_api_key = 67;
optional string oca_refresh_token = 68;
// Plan mode configurations
optional ApiProvider plan_mode_api_provider = 100;
optional string plan_mode_api_provider = 100;
optional string plan_mode_api_model_id = 101;
optional int32 plan_mode_thinking_budget_tokens = 102;
optional string plan_mode_reasoning_effort = 103;
optional LanguageModelChatSelector plan_mode_vs_code_lm_model_selector = 104;
optional string plan_mode_vscode_lm_model_selector = 104; // JSON string
optional bool plan_mode_aws_bedrock_custom_selected = 105;
optional string plan_mode_aws_bedrock_custom_model_base_id = 106;
optional string plan_mode_open_router_model_id = 107;
optional OpenRouterModelInfo plan_mode_open_router_model_info = 108;
optional string plan_mode_open_ai_model_id = 109;
optional OpenAiCompatibleModelInfo plan_mode_open_ai_model_info = 110;
optional string plan_mode_openrouter_model_id = 107;
optional string plan_mode_openrouter_model_info = 108; // JSON string
optional string plan_mode_openai_model_id = 109;
optional string plan_mode_openai_model_info = 110; // JSON string
optional string plan_mode_ollama_model_id = 111;
optional string plan_mode_lm_studio_model_id = 112;
optional string plan_mode_lite_llm_model_id = 113;
optional LiteLLMModelInfo plan_mode_lite_llm_model_info = 114;
optional string plan_mode_lite_llm_model_info = 114; // JSON string
optional string plan_mode_requesty_model_id = 115;
optional OpenRouterModelInfo plan_mode_requesty_model_info = 116;
optional string plan_mode_requesty_model_info = 116; // JSON string
optional string plan_mode_together_model_id = 117;
optional string plan_mode_fireworks_model_id = 118;
optional string plan_mode_sap_ai_core_model_id = 119;
optional string plan_mode_huawei_cloud_maas_model_id = 120;
optional OpenRouterModelInfo plan_mode_huawei_cloud_maas_model_info = 121;
optional string plan_mode_huawei_cloud_maas_model_info = 121;
optional string plan_mode_vercel_ai_gateway_model_id = 122;
optional OpenRouterModelInfo plan_mode_vercel_ai_gateway_model_info = 123;
optional string plan_mode_oca_model_id = 124;
optional OcaModelInfo plan_mode_oca_model_info = 125;
optional string plan_mode_vercel_ai_gateway_model_info = 123;
// Act mode configurations
optional ApiProvider act_mode_api_provider = 200;
optional string act_mode_api_provider = 200;
optional string act_mode_api_model_id = 201;
optional int32 act_mode_thinking_budget_tokens = 202;
optional string act_mode_reasoning_effort = 203;
optional LanguageModelChatSelector act_mode_vs_code_lm_model_selector = 204;
optional string act_mode_vscode_lm_model_selector = 204; // JSON string
optional bool act_mode_aws_bedrock_custom_selected = 205;
optional string act_mode_aws_bedrock_custom_model_base_id = 206;
optional string act_mode_open_router_model_id = 207;
optional OpenRouterModelInfo act_mode_open_router_model_info = 208;
optional string act_mode_open_ai_model_id = 209;
optional OpenAiCompatibleModelInfo act_mode_open_ai_model_info = 210;
optional string act_mode_openrouter_model_id = 207;
optional string act_mode_openrouter_model_info = 208; // JSON string
optional string act_mode_openai_model_id = 209;
optional string act_mode_openai_model_info = 210; // JSON string
optional string act_mode_ollama_model_id = 211;
optional string act_mode_lm_studio_model_id = 212;
optional string act_mode_lite_llm_model_id = 213;
optional LiteLLMModelInfo act_mode_lite_llm_model_info = 214;
optional string act_mode_lite_llm_model_info = 214; // JSON string
optional string act_mode_requesty_model_id = 215;
optional OpenRouterModelInfo act_mode_requesty_model_info = 216;
optional string act_mode_requesty_model_info = 216; // JSON string
optional string act_mode_together_model_id = 217;
optional string act_mode_fireworks_model_id = 218;
optional string act_mode_sap_ai_core_model_id = 219;
optional string act_mode_huawei_cloud_maas_model_id = 220;
optional OpenRouterModelInfo act_mode_huawei_cloud_maas_model_info = 221;
optional string act_mode_huawei_cloud_maas_model_info = 221;
optional string act_mode_vercel_ai_gateway_model_id = 222;
optional OpenRouterModelInfo act_mode_vercel_ai_gateway_model_info = 223;
optional string act_mode_oca_model_id = 224;
optional OcaModelInfo act_mode_oca_model_info = 225;
optional string act_mode_vercel_ai_gateway_model_info = 223;
// Favorited model IDs
repeated string favorited_model_ids = 300;
// Extension fields for Bedrock Api Keys
optional string aws_authentication = 301;
@@ -286,11 +270,12 @@ message FocusChainSettings {
int32 remind_cline_interval = 2;
}
message Viewport {
int32 width = 1;
int32 height = 2;
}
message UpdateTerminalConnectionTimeoutResponse {
optional int32 timeout_ms = 1;
}
message ProcessInfo {
int32 process_id = 1;
optional string version = 2;
optional int64 uptime_ms = 3;
}
@@ -0,0 +1,20 @@
import { EmptyRequest } from "@shared/proto/cline/common"
import { ProcessInfo } from "@shared/proto/cline/state"
import { Controller } from ".."
/**
* Gets process information including PID, version, and uptime
* @param controller The controller instance
* @param request Empty request
* @returns ProcessInfo with process details
*/
export async function getProcessInfo(controller: Controller, request: EmptyRequest): Promise<ProcessInfo> {
// Get the current state to access the version (same source as webview)
const state = await controller.getStateToPostToWebview()
return ProcessInfo.create({
processId: process.pid,
version: state.version || "unknown",
uptimeMs: Math.floor(process.uptime() * 1000), // Convert seconds to milliseconds
})
}
+229
View File
@@ -0,0 +1,229 @@
import Database from "better-sqlite3"
import { existsSync, mkdirSync, unlinkSync } from "fs"
import * as path from "path"
import type { InstanceLockData, SqliteLockManagerOptions } from "./types"
export class SqliteLockManager {
private db!: Database.Database
private instanceAddress: string
private dbPath: string
private readonly STALE_LOCK_TIMEOUT = 1 * 60 * 1000 // 1 minute in milliseconds
constructor(options: SqliteLockManagerOptions) {
this.instanceAddress = options.instanceAddress
this.dbPath = options.dbPath
// Ensure the directory exists before creating the database
const dbDir = path.dirname(this.dbPath)
try {
mkdirSync(dbDir, { recursive: true })
} catch (error) {
console.error(`CRITICAL ERROR: Failed to create SQLite database directory ${dbDir}:`, error)
throw new Error(`Failed to create SQLite database directory: ${error}`)
}
try {
this.initializeDatabaseWithLockSync()
} catch (error) {
console.error(`CRITICAL ERROR: Failed to initialize SQLite database at ${this.dbPath}:`, error)
throw new Error(`Failed to initialize SQLite database: ${error}`)
}
}
private initializeDatabaseWithLockSync(): void {
const lockFile = `${this.dbPath}.lock`
// Clean up stale lock files first
this.cleanupStaleLockSync(lockFile)
try {
// Try to acquire exclusive file lock for database creation
const fs = require("fs")
let fd: number | null = null
try {
fd = fs.openSync(lockFile, "wx") // Exclusive creation - fails if file exists
// Write timestamp to lock file for stale lock detection
fs.writeFileSync(fd, Date.now().toString())
// Check if database already exists
const dbExists = existsSync(this.dbPath)
if (!dbExists) {
// Database doesn't exist, create it
this.db = new Database(this.dbPath)
this.initializeDatabase()
} else {
// Database exists, just open it
this.db = new Database(this.dbPath)
}
} finally {
// Always clean up the lock file
if (fd !== null) {
fs.closeSync(fd)
}
try {
unlinkSync(lockFile)
} catch {} // Ignore errors if file was already deleted
}
} catch (error: any) {
if (error.code === "EEXIST") {
// Another process is initializing the database, wait and retry
const delay = 100 + Math.random() * 100 // Add jitter
this.sleepSync(delay)
this.initializeDatabaseWithLockSync()
return
}
throw error
}
}
private sleepSync(ms: number) {
// Non-spinning, synchronous sleep using Atomics.wait
// Works in Node main thread (since v12.16+) and worker threads.
const sab = new SharedArrayBuffer(4)
const ia = new Int32Array(sab)
Atomics.wait(ia, 0, 0, Math.max(0, Math.floor(ms)))
}
private cleanupStaleLockSync(lockFile: string): void {
try {
if (!existsSync(lockFile)) {
return // Lock file doesn't exist, nothing to clean up
}
const fs = require("fs")
try {
const timestampStr = fs.readFileSync(lockFile, "utf8").trim()
const timestamp = parseInt(timestampStr, 10)
if (isNaN(timestamp) || Date.now() - timestamp > this.STALE_LOCK_TIMEOUT) {
// Stale lock, remove it
unlinkSync(lockFile)
console.warn(`Removed stale database lock file: ${lockFile}`)
}
} catch (readError) {
// If we can't read the timestamp, assume it's stale
unlinkSync(lockFile)
console.warn(`Removed unreadable database lock file: ${lockFile}`)
}
} catch (error: any) {
if (error.code !== "ENOENT") {
// Lock file doesn't exist, which is fine
console.warn(`Error checking lock file ${lockFile}:`, error)
}
}
}
private initializeDatabase() {
// Create the locks table with the unified schema (matches cli/pkg/common/schema.go)
this.db.exec(`
CREATE TABLE IF NOT EXISTS locks (
id INTEGER PRIMARY KEY,
held_by TEXT NOT NULL,
lock_type TEXT NOT NULL CHECK (lock_type IN ('file', 'instance', 'folder')),
lock_target TEXT NOT NULL,
locked_at INTEGER NOT NULL,
UNIQUE(lock_type, lock_target)
);
`)
// Create indexes for performance (matches cli/pkg/common/schema.go)
this.db.exec(`
CREATE INDEX IF NOT EXISTS idx_locks_held_by ON locks(held_by);
CREATE INDEX IF NOT EXISTS idx_locks_type ON locks(lock_type);
CREATE INDEX IF NOT EXISTS idx_locks_target ON locks(lock_target);
`)
}
/**
* Register this instance in the locks table
*/
async registerInstance(data: {
corePort: number
hostPort: number
version?: string
status?: InstanceLockData["status"]
}): Promise<void> {
const now = Date.now()
const hostAddress = `localhost:${data.hostPort}`
// Create instance lock entry
const insertLock = this.db.prepare(`
INSERT OR REPLACE INTO locks (held_by, lock_type, lock_target, locked_at)
VALUES (?, 'instance', ?, ?)
`)
insertLock.run(this.instanceAddress, hostAddress, now)
}
/**
* Update the timestamp for this instance (touch)
*/
touchInstance(): void {
const now = Date.now()
const updateLock = this.db.prepare(`
UPDATE locks
SET locked_at = ?
WHERE held_by = ? AND lock_type = 'instance'
`)
updateLock.run(now, this.instanceAddress)
}
/**
* Remove this instance from the locks table
*/
unregisterInstance(): void {
const deleteLock = this.db.prepare(`
DELETE FROM locks
WHERE held_by = ? AND lock_type = 'instance'
`)
deleteLock.run(this.instanceAddress)
}
/**
* Query the registry for any instance registered on the given port
*/
getInstanceByPort(port: number): { instanceAddress: string; hostAddress: string } | null {
const query = this.db.prepare(`
SELECT held_by, lock_target
FROM locks
WHERE lock_type = 'instance'
AND (held_by LIKE '%:' || ? OR lock_target LIKE '%:' || ?)
`)
const result = query.get(port, port) as { held_by: string; lock_target: string } | undefined
if (result) {
return {
instanceAddress: result.held_by,
hostAddress: result.lock_target,
}
}
return null
}
/**
* Remove a specific instance entry from the registry
*/
removeInstanceByAddress(instanceAddress: string): void {
const deleteLock = this.db.prepare(`
DELETE FROM locks
WHERE held_by = ? AND lock_type = 'instance'
`)
deleteLock.run(instanceAddress)
}
/**
* Close the database connection
*/
close(): void {
this.db.close()
}
}
+28
View File
@@ -0,0 +1,28 @@
export type LockType = "file" | "instance" | "folder"
export type LockStatus = "starting" | "healthy" | "unhealthy"
export interface LockRow {
id: number
held_by: string // address:port of instance holding the lock
lock_type: LockType
lock_target: string // varies by type: file path, host address, or folder path
locked_at: number // timestamp when lock was acquired
}
export interface InstanceLockData {
address: string
core_port: number
host_port: number
status: LockStatus
last_seen: string
process_pid: number
version?: string
created_at: string
metadata?: Record<string, any>
}
export interface SqliteLockManagerOptions {
dbPath: string
instanceAddress: string // host:port format
}
+29
View File
@@ -0,0 +1,29 @@
/**
* TypeScript equivalent of the Go common.RetryOperation utility
* Performs an operation with retry logic and timeout handling
*/
export async function retryOperation<T>(maxRetries: number, timeoutPerAttempt: number, operation: () => Promise<T>): Promise<T> {
let lastError: Error | undefined
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// Create a timeout promise
const timeoutPromise = new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error("Operation timeout")), timeoutPerAttempt),
)
// Race the operation against timeout
const result = await Promise.race([operation(), timeoutPromise])
return result // Success - return result
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error))
if (attempt < maxRetries) {
// Brief delay before retry
await new Promise((resolve) => setTimeout(resolve, 500))
}
}
}
throw new Error(`Operation failed after ${maxRetries} attempts: ${lastError?.message}`)
}
+234 -30
View File
@@ -2,66 +2,211 @@ import { ExternalDiffViewProvider } from "@hosts/external/ExternalDiffviewProvid
import { ExternalWebviewProvider } from "@hosts/external/ExternalWebviewProvider"
import { ExternalHostBridgeClientManager } from "@hosts/external/host-bridge-client-manager"
import { WebviewProviderType } from "@shared/webview/types"
import * as path from "path"
import { retryOperation } from "@utils/retry"
import os from "os"
import { initialize, tearDown } from "@/common"
import { SqliteLockManager } from "@/core/locks/SqliteLockManager"
import { WebviewProvider } from "@/core/webview"
import { AuthHandler } from "@/hosts/external/AuthHandler"
import { HostProvider } from "@/hosts/host-provider"
import { DiffViewProvider } from "@/integrations/editor/DiffViewProvider"
import { waitForHostBridgeReady } from "./hostbridge-client"
import { startProtobusService } from "./protobus-service"
import { log } from "./utils"
import { DATA_DIR, EXTENSION_DIR, extensionContext } from "./vscode-context"
import { checkPortAvailability } from "./port-checker"
import { startProtobusService, waitForHostBridgeReady } from "./protobus-service"
import { log, SETTINGS_SUBFOLDER } from "./utils"
import { createExtensionContext } from "./vscode-context"
// Default ports
export const DEFAULT_PROTOBUS_PORT = 26040
export const DEFAULT_HOSTBRIDGE_PORT = 26041
// Parse command line arguments
interface CliArgs {
port?: number
hostBridgePort?: number
config?: string
help?: boolean
}
function parseArgs(): CliArgs {
const args: CliArgs = {}
const argv = process.argv.slice(2)
for (let i = 0; i < argv.length; i++) {
const arg = argv[i]
switch (arg) {
case "--port":
case "-p":
args.port = parseInt(argv[++i], 10)
break
case "--host-bridge-port":
args.hostBridgePort = parseInt(argv[++i], 10)
break
case "--config":
case "-c":
args.config = argv[++i]
break
case "--help":
case "-h":
args.help = true
break
}
}
return args
}
function showHelp() {
console.log(`
Cline Core - Standalone Server
Usage: node cline-core.js [options]
Options:
-p, --port <port> Port for the main gRPC service (default: ${DEFAULT_PROTOBUS_PORT})
--host-bridge-port <port> Port for the host bridge service (default: ${DEFAULT_HOSTBRIDGE_PORT})
-c, --config <path> Directory for Cline data storage (default: ~/.cline)
-h, --help Show this help message
Environment Variables:
PROTOBUS_ADDRESS Override the main service address (format: host:port)
HOSTBRIDGE_ADDRESS Override the host bridge address (format: host:port)
`)
}
async function main() {
// Parse command line arguments
const args = parseArgs()
// Show help if requested
if (args.help) {
showHelp()
process.exit(0)
}
// Configure ports from arguments
let protobusPort = DEFAULT_PROTOBUS_PORT
let hostBridgePort = DEFAULT_HOSTBRIDGE_PORT
if (args.port) {
protobusPort = args.port
// If only port is specified, calculate hostbridge port as port + 1000
if (!args.hostBridgePort) {
hostBridgePort = protobusPort + 1000
}
}
if (args.hostBridgePort) {
hostBridgePort = args.hostBridgePort
}
// Set environment variables for the services to use
if (!process.env.PROTOBUS_ADDRESS) {
process.env.PROTOBUS_ADDRESS = `localhost:${protobusPort}`
}
if (!process.env.HOSTBRIDGE_ADDRESS) {
process.env.HOSTBRIDGE_ADDRESS = `localhost:${hostBridgePort}`
}
// Configure Cline directory from arguments
const clineDir = args.config || `${os.homedir()}/.cline`
log("\n\n\nStarting cline-core service...\n\n\n")
log(`Using Protobus port: ${protobusPort}`)
log(`Using Host Bridge port: ${hostBridgePort}`)
log(`Using Cline directory: ${clineDir}`)
await waitForHostBridgeReady()
// Initialize SQLite lock manager for instance registration
const dbPath = `${clineDir}/${SETTINGS_SUBFOLDER}/locks.db`
// Use host:port everywhere (no scheme)
const fullAddress = `localhost:${protobusPort}`
let lockManager: SqliteLockManager | undefined
try {
lockManager = new SqliteLockManager({
dbPath,
instanceAddress: fullAddress,
})
// The host bridge should be available before creating the host provider because it depends on the host bridge.
setupHostProvider()
// Check port availability before proceeding
log(`Checking port availability for ${protobusPort}...`)
const portCheck = await checkPortAvailability(protobusPort, lockManager)
if (!portCheck.canProceed) {
log(`STARTUP BLOCKED: ${portCheck.error}`)
lockManager.close()
process.exit(1)
}
await lockManager.registerInstance({
corePort: protobusPort,
hostPort: hostBridgePort,
version: process.env.CLINE_VERSION,
status: "starting",
})
log(`Registered instance in SQLite locks: ${fullAddress}`)
} catch (err) {
log(`CRITICAL ERROR: Failed to register instance in SQLite locks: ${String(err)}`)
log(`This is a fatal error - cline-core cannot start without proper instance registration`)
if (lockManager) {
try {
lockManager.close()
} catch {}
}
process.exit(1)
}
try {
await waitForHostBridgeReady()
log("HostBridge is serving; continuing startup")
} catch (err) {
log(`ERROR: HostBridge error: ${String(err)}`)
// Cleanup lock manager entry if startup fails
if (lockManager) {
try {
lockManager.unregisterInstance()
lockManager.close()
} catch {}
}
process.exit(1)
}
// Create extension context with the configured directory
const extensionContext = createExtensionContext(clineDir)
setupHostProvider(extensionContext)
// Set up global error handlers to prevent process crashes
setupGlobalErrorHandlers()
setupGlobalErrorHandlers(lockManager)
const webviewProvider = await initialize(extensionContext)
// Enable the localhost HTTP server that handles auth redirects.
AuthHandler.getInstance().setEnabled(true)
startProtobusService(webviewProvider.controller)
// Mark instance healthy after services are up
try {
lockManager?.touchInstance()
} catch {}
}
function setupHostProvider() {
function setupHostProvider(extensionContext: any) {
const createWebview = (_: WebviewProviderType): WebviewProvider => {
return new ExternalWebviewProvider(extensionContext, WebviewProviderType.SIDEBAR)
}
const createDiffView = (): DiffViewProvider => {
return new ExternalDiffViewProvider()
}
const getCallbackUrl = (): Promise<string> => {
return AuthHandler.getInstance().getCallbackUrl()
const getCallbackUri = (): Promise<string> => {
return AuthHandler.getInstance().getCallbackUri()
}
// cline-core expects the binaries to be unpacked in the directory where it is running.
const getBinaryLocation = async (name: string): Promise<string> => path.join(process.cwd(), name)
HostProvider.initialize(
createWebview,
createDiffView,
new ExternalHostBridgeClientManager(),
log,
getCallbackUrl,
getBinaryLocation,
EXTENSION_DIR,
DATA_DIR,
)
HostProvider.initialize(createWebview, createDiffView, new ExternalHostBridgeClientManager(), log, getCallbackUri)
}
/**
* Sets up global error handlers to prevent the process from crashing
* on unhandled exceptions and promise rejections
*/
function setupGlobalErrorHandlers() {
function setupGlobalErrorHandlers(lockManager?: SqliteLockManager) {
// Handle unhandled exceptions
process.on("uncaughtException", (error: Error) => {
log(`ERROR: Uncaught exception: ${error.message}`)
@@ -86,15 +231,74 @@ function setupGlobalErrorHandlers() {
// Graceful shutdown handlers
process.on("SIGINT", () => {
log("Received SIGINT, shutting down gracefully...")
process.exit(0)
shutdownGracefully(lockManager)
})
process.on("SIGTERM", () => {
log("Received SIGTERM, shutting down gracefully...")
tearDown()
process.exit(0)
shutdownGracefully(lockManager)
})
}
/**
* Request host bridge shutdown with retry logic and timeout handling.
* Uses best-effort approach - logs failures but doesn't block shutdown.
*/
async function requestHostBridgeShutdown(): Promise<void> {
try {
await retryOperation(3, 2000, async () => {
await HostProvider.env.shutdown({})
})
log("Host bridge shutdown requested successfully")
} catch (error) {
log(`Warning: Failed to request host bridge shutdown: ${error}`)
log("Proceeding with cleanup")
}
}
/**
* Gracefully shutdown the cline-core process by:
* 1. Calling shutdown RPC on the paired host bridge
* 2. Cleaning up the lock manager entry
* 3. Tearing down services
* 4. Exiting the process
*/
async function shutdownGracefully(lockManager?: SqliteLockManager) {
try {
// Step 1: Tell the paired host bridge to shut down
log("Requesting host bridge shutdown...")
if (HostProvider.isInitialized()) {
await requestHostBridgeShutdown()
} else {
log("Warning: HostProvider not initialized, cannot request shutdown")
}
// Step 2: Clean up lock manager entry
log("Cleaning up lock manager entry...")
try {
lockManager?.unregisterInstance()
lockManager?.close()
log("Lock manager entry cleaned up successfully")
} catch (error) {
log(`Warning: Failed to clean up lock manager: ${error}`)
}
// Step 3: Tear down services
log("Tearing down services...")
try {
tearDown()
log("Services torn down successfully")
} catch (error) {
log(`Warning: Failed to tear down services: ${error}`)
}
log("Graceful shutdown completed")
} catch (error) {
log(`Error during graceful shutdown: ${error}`)
} finally {
// Step 4: Exit the process
process.exit(0)
}
}
main()
+166
View File
@@ -0,0 +1,166 @@
import * as grpc from "@grpc/grpc-js"
import * as protoLoader from "@grpc/proto-loader"
import * as health from "grpc-health-check"
import { SqliteLockManager } from "@/core/locks/SqliteLockManager"
import { log } from "./utils"
const SERVING_STATUS = 1
interface PortCheckResult {
canProceed: boolean
error?: string
}
/**
* Creates a gRPC health client for the given address
*/
function createHealthClient(address: string): any {
const healthDef = protoLoader.loadSync(health.protoPath)
const grpcObj = grpc.loadPackageDefinition(healthDef) as unknown as any
const Health = grpcObj.grpc.health.v1.Health
return new Health(address, grpc.credentials.createInsecure())
}
/**
* Performs a single health check on the given address
*/
async function checkHealthOnce(address: string): Promise<{ success: boolean; status?: number; error?: Error }> {
const client = createHealthClient(address)
return new Promise((resolve) => {
const timeout = setTimeout(() => {
try {
client.close?.()
} catch {}
resolve({ success: false, error: new Error("Health check timeout") })
}, 5000) // 5 second timeout
client.check({ service: "" }, (err: unknown, resp: any) => {
clearTimeout(timeout)
try {
client.close?.()
} catch {}
if (err) {
resolve({ success: false, error: err as Error })
} else {
resolve({ success: true, status: resp?.status })
}
})
})
}
/**
* Attempts to shut down a host bridge instance
*/
async function shutdownHostBridge(hostAddress: string): Promise<boolean> {
try {
log(`Attempting to shutdown host bridge at ${hostAddress}`)
// This would need to be implemented - we need a way to send shutdown to a specific host
// For now, we'll just log that we would do this
log(`Would send shutdown command to host bridge at ${hostAddress}`)
return true
} catch (error) {
log(`Failed to shutdown host bridge at ${hostAddress}: ${error}`)
return false
}
}
/**
* Checks if a port is available for binding, following the registry-first approach
*/
export async function checkPortAvailability(port: number, lockManager: SqliteLockManager): Promise<PortCheckResult> {
log(`Checking port availability for port ${port}`)
// Step 1: Check registry first
const registryEntry = lockManager.getInstanceByPort(port)
if (!registryEntry) {
log(`No registry entry found for port ${port}, free to bind`)
return { canProceed: true }
}
log(`Found registry entry for port ${port}: instance=${registryEntry.instanceAddress}, host=${registryEntry.hostAddress}`)
// Step 2: Perform health check on the registered instance
const coreAddress = registryEntry.instanceAddress
const performHealthCheck = async (): Promise<{ success: boolean; status?: number; error?: Error }> => {
return await checkHealthOnce(coreAddress)
}
// First health check attempt
let healthResult = await performHealthCheck()
if (!healthResult.success) {
// Health check ERROR - not our process
log(`Health check failed for ${coreAddress}: ${healthResult.error?.message}`)
log(`This indicates a non-Cline process is using port ${port}`)
// Attempt to shutdown the registered host bridge
const shutdownSuccess = await shutdownHostBridge(registryEntry.hostAddress)
if (shutdownSuccess) {
log(`Successfully requested shutdown of host bridge ${registryEntry.hostAddress}`)
}
// Remove from registry
lockManager.removeInstanceByAddress(registryEntry.instanceAddress)
log(`Removed stale registry entry for ${registryEntry.instanceAddress}`)
return {
canProceed: false,
error: `Port ${port} is occupied by a non-Cline process. Registry has been cleaned up.`,
}
}
// Health check succeeded - it's our process
if (healthResult.status === SERVING_STATUS) {
// Healthy Cline instance already running
log(`Healthy Cline instance already running on port ${port}`)
return {
canProceed: false,
error: `A healthy Cline instance is already running on port ${port}`,
}
}
// Health check succeeded but status is not SERVING - unhealthy Cline instance
log(`Unhealthy Cline instance detected on port ${port} (status: ${healthResult.status}), retrying in 1 second`)
// Wait 1 second and retry
await new Promise((resolve) => setTimeout(resolve, 1000))
// Second health check attempt
healthResult = await performHealthCheck()
if (!healthResult.success) {
// Now it's erroring - something changed
log(`Health check now failing after retry for ${coreAddress}: ${healthResult.error?.message}`)
// Clean up registry since the instance is no longer responding
lockManager.removeInstanceByAddress(registryEntry.instanceAddress)
log(`Removed non-responsive registry entry for ${registryEntry.instanceAddress}`)
return {
canProceed: false,
error: `Port ${port} had an unhealthy Cline instance that is now non-responsive. Registry cleaned up.`,
}
}
if (healthResult.status === SERVING_STATUS) {
// Instance recovered
log(`Cline instance on port ${port} has recovered and is now healthy`)
return {
canProceed: false,
error: `Cline instance on port ${port} has recovered and is now serving`,
}
}
// Still unhealthy after retry
log(`Cline instance on port ${port} remains unhealthy after retry (status: ${healthResult.status})`)
return {
canProceed: false,
error: `Cline instance on port ${port} is unhealthy and did not recover after retry`,
}
}
+5 -1
View File
@@ -3,6 +3,9 @@ import * as fs from "fs"
import * as health from "grpc-health-check"
import { StreamingCallbacks } from "@/hosts/host-provider-types"
// WE WILL HAVE TO MIGRATE THIS FROM DATA TO v1 LATER
const SETTINGS_SUBFOLDER = "data"
const log = (...args: unknown[]) => {
const now = new Date()
const year = now.getFullYear()
@@ -20,6 +23,7 @@ const log = (...args: unknown[]) => {
function getPackageDefinition() {
// Load service definitions.
// When running as standalone, the descriptor set is in the same directory as the standalone.js file
const descriptorSet = fs.readFileSync("proto/descriptor_set.pb")
const options = { longs: Number } // Encode int64 fields as numbers
const descriptorDefs = protoLoader.loadFileDescriptorSetFromBuffer(descriptorSet, options)
@@ -51,4 +55,4 @@ async function asyncIteratorToCallbacks<T>(stream: AsyncIterable<T>, callbacks:
}
}
export { getPackageDefinition, log, asyncIteratorToCallbacks }
export { getPackageDefinition, log, asyncIteratorToCallbacks, SETTINGS_SUBFOLDER }
+70 -50
View File
@@ -1,69 +1,89 @@
import { mkdirSync } from "node:fs"
import os from "os"
import { mkdirSync } from "fs"
import path from "path"
import type { Extension, ExtensionContext } from "vscode"
import { ExtensionKind, ExtensionMode } from "vscode"
import { URI } from "vscode-uri"
import { ExtensionRegistryInfo } from "@/registry"
// @ts-ignore
import { StandaloneTerminalManager } from "../../standalone/runtime-files/vscode/enhanced-terminal"
import { log } from "./utils"
import { EnvironmentVariableCollection, MementoStore, readJson, SecretStore } from "./vscode-context-utils"
import { EnvironmentVariableCollection, MementoStore, SecretStore } from "./vscode-context-utils"
log("Running standalone cline", ExtensionRegistryInfo.version)
log(`CLINE_ENVIRONMENT: ${process.env.CLINE_ENVIRONMENT}`)
export const CLINE_DIR = process.env.CLINE_DIR || `${os.homedir()}/.cline`
export const DATA_DIR = path.join(CLINE_DIR, "data")
const INSTALL_DIR = process.env.INSTALL_DIR || __dirname
const WORKSPACE_STORAGE_DIR = process.env.WORKSPACE_STORAGE_DIR || path.join(DATA_DIR, "workspace")
mkdirSync(DATA_DIR, { recursive: true })
mkdirSync(WORKSPACE_STORAGE_DIR, { recursive: true })
log("Using settings dir:", DATA_DIR)
export const EXTENSION_DIR = path.join(INSTALL_DIR, "extension")
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
const extension: Extension<void> = {
id: ExtensionRegistryInfo.id,
isActive: true,
extensionPath: EXTENSION_DIR,
extensionUri: URI.file(EXTENSION_DIR),
packageJSON: readJson(path.join(EXTENSION_DIR, "package.json")),
exports: undefined, // There are no API exports in the standalone version.
activate: async () => {},
extensionKind: ExtensionKind.UI,
function getPackageVersion(): string {
// Use build-time injected version (only method)
return process.env.CLINE_VERSION || "unknown"
}
const extensionContext: ExtensionContext = {
extension: extension,
extensionMode: EXTENSION_MODE,
const VERSION = getPackageVersion()
log("Running standalone cline ", VERSION)
// Set up KV stores.
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
function createExtensionContext(clineDir: string): ExtensionContext {
const DATA_DIR = path.join(clineDir, "data")
const INSTALL_DIR = process.env.INSTALL_DIR || path.join(clineDir, "core", VERSION)
mkdirSync(DATA_DIR, { recursive: true })
log("Using settings dir:", DATA_DIR)
// Set up URIs.
storageUri: URI.file(WORKSPACE_STORAGE_DIR),
storagePath: WORKSPACE_STORAGE_DIR, // Deprecated, not used in cline.
globalStorageUri: URI.file(DATA_DIR),
globalStoragePath: DATA_DIR, // Deprecated, not used in cline.
const EXTENSION_DIR = path.join(INSTALL_DIR, "extension")
const EXTENSION_MODE = process.env.IS_DEV === "true" ? ExtensionMode.Development : ExtensionMode.Production
// Logs are global per extension, not per workspace.
logUri: URI.file(DATA_DIR),
logPath: DATA_DIR, // Deprecated, not used in cline.
// Static package.json data for extension context (no filesystem reading)
function getExtensionPackageJson(): any {
return {
name: "claude-dev",
displayName: "Cline",
version: VERSION,
publisher: "saoudrizwan",
}
}
extensionUri: URI.file(EXTENSION_DIR),
extensionPath: EXTENSION_DIR, // Deprecated, not used in cline.
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
const extension: Extension<void> = {
id: "saoudrizwan.claude-dev",
isActive: true,
extensionPath: EXTENSION_DIR,
extensionUri: URI.file(EXTENSION_DIR),
packageJSON: getExtensionPackageJson(),
exports: undefined, // There are no API exports in the standalone version.
activate: async () => {},
extensionKind: ExtensionKind.UI,
}
subscriptions: [], // These need to be destroyed when the extension is deactivated.
const extensionContext: ExtensionContext = {
extension: extension,
extensionMode: EXTENSION_MODE,
environmentVariableCollection: new EnvironmentVariableCollection(),
// Set up KV stores.
globalState: new MementoStore(path.join(DATA_DIR, "globalState.json")),
secrets: new SecretStore(path.join(DATA_DIR, "secrets.json")),
// Workspace state is per project/workspace when WORKSPACE_STORAGE_DIR is provided by the host.
workspaceState: new MementoStore(path.join(WORKSPACE_STORAGE_DIR, "workspaceState.json")),
// Set up URIs.
storageUri: URI.file(DATA_DIR),
storagePath: DATA_DIR, // Deprecated, not used in cline.
globalStorageUri: URI.file(DATA_DIR),
globalStoragePath: DATA_DIR, // Deprecated, not used in cline.
logUri: URI.file(DATA_DIR),
logPath: DATA_DIR, // Deprecated, not used in cline.
extensionUri: URI.file(EXTENSION_DIR),
extensionPath: EXTENSION_DIR, // Deprecated, not used in cline.
asAbsolutePath: (relPath: string) => path.join(EXTENSION_DIR, relPath),
subscriptions: [], // These need to be destroyed when the extension is deactivated.
environmentVariableCollection: new EnvironmentVariableCollection(),
// TODO(sjf): Workspace state needs to be per project/workspace.
workspaceState: new MementoStore(path.join(DATA_DIR, "workspaceState.json")),
}
return extensionContext
}
// Initialize the standalone terminal manager for use by Task instances
const standaloneTerminalManager = new StandaloneTerminalManager()
// Set it as a global so Task constructor can access it
;(global as any).standaloneTerminalManager = standaloneTerminalManager
console.log("Finished loading vscode context...")
export { extensionContext }
export { createExtensionContext }