PackageManagerView tests all passing

This commit is contained in:
Smartsheet-JB-Brown 2025-04-14 21:59:39 -07:00
parent beb151e1b0
commit c05d760747
11 changed files with 312 additions and 212 deletions

View file

@ -173,9 +173,23 @@ const extensionConfig = {
{
name: "alias-plugin",
setup(build) {
// Handle pkce-challenge alias
build.onResolve({ filter: /^pkce-challenge$/ }, (args) => {
return { path: require.resolve("pkce-challenge/dist/index.browser.js") }
})
// Handle @package-manager barrel file
build.onResolve({ filter: /^@package-manager$/ }, (args) => {
const resolvedPath = path.resolve(__dirname, "src/services/package-manager/index.ts")
return { path: resolvedPath }
})
// Handle @package-manager/* paths
build.onResolve({ filter: /^@package-manager\// }, (args) => {
const modulePath = args.path.replace(/^@package-manager\//, "")
const fullPath = path.resolve(__dirname, "src/services/package-manager", `${modulePath}.ts`)
return { path: fullPath }
})
},
},
],

View file

@ -2,10 +2,15 @@ import * as vscode from "vscode"
import { ClineProvider } from "./ClineProvider"
import { WebviewMessage } from "../../shared/WebviewMessage"
import { ExtensionMessage } from "../../shared/ExtensionMessage"
import { PackageManagerManager } from "../../services/package-manager"
import { ComponentType, PackageManagerItem, PackageManagerSource } from "../../services/package-manager/types"
import { DEFAULT_PACKAGE_MANAGER_SOURCE } from "../../services/package-manager/constants"
import { validateSources } from "../../services/package-manager/validation"
import {
PackageManagerManager,
ComponentType,
PackageManagerItem,
PackageManagerSource,
validateSources,
ValidationError,
} from "@package-manager"
import { DEFAULT_PACKAGE_MANAGER_SOURCE } from "@package-manager/constants"
import { GlobalState } from "../../schemas"
/**
@ -32,7 +37,7 @@ export async function handlePackageManagerMessages(
// Prevent multiple simultaneous fetches
if (packageManagerManager.isFetching) {
console.log("Package Manager: Fetch already in progress, skipping")
provider.postMessageToWebview({
await provider.postMessageToWebview({
type: "state",
text: "Fetch already in progress",
})
@ -96,7 +101,7 @@ export async function handlePackageManagerMessages(
else if (result.errors && result.items.length === 0) {
const errorMessage = `Failed to load package manager sources:\n${result.errors.join("\n")}`
vscode.window.showErrorMessage(errorMessage)
provider.postMessageToWebview({
await provider.postMessageToWebview({
type: "state",
text: errorMessage,
})
@ -120,7 +125,7 @@ export async function handlePackageManagerMessages(
const errorMessage = `Package manager initialization failed: ${initError instanceof Error ? initError.message : String(initError)}`
console.error("Error in package manager initialization:", initError)
vscode.window.showErrorMessage(errorMessage)
provider.postMessageToWebview({
await provider.postMessageToWebview({
type: "state",
text: errorMessage,
})
@ -132,7 +137,7 @@ export async function handlePackageManagerMessages(
const errorMessage = `Failed to fetch package manager items: ${error instanceof Error ? error.message : String(error)}`
console.error("Failed to fetch package manager items:", error)
vscode.window.showErrorMessage(errorMessage)
provider.postMessageToWebview({
await provider.postMessageToWebview({
type: "state",
text: errorMessage,
})
@ -165,7 +170,7 @@ export async function handlePackageManagerMessages(
// Create a map of invalid indices
const invalidIndices = new Set<number>()
validationErrors.forEach((error) => {
validationErrors.forEach((error: ValidationError) => {
// Extract index from error message (Source #X: ...)
const match = error.message.match(/Source #(\d+):/)
if (match && match[1]) {
@ -180,7 +185,7 @@ export async function handlePackageManagerMessages(
updatedSources = updatedSources.filter((_, index) => !invalidIndices.has(index))
// Show validation errors
const errorMessage = `Package manager sources validation failed:\n${validationErrors.map((e) => e.message).join("\n")}`
const errorMessage = `Package manager sources validation failed:\n${validationErrors.map((e: ValidationError) => e.message).join("\n")}`
console.error(errorMessage)
vscode.window.showErrorMessage(errorMessage)
}
@ -282,7 +287,7 @@ export async function handlePackageManagerMessages(
} finally {
// Always notify the webview that the refresh is complete, even if it failed
console.log(`Package Manager: Sending repositoryRefreshComplete message for ${message.url}`)
provider.postMessageToWebview({
await provider.postMessageToWebview({
type: "repositoryRefreshComplete",
url: message.url,
})

View file

@ -3,10 +3,15 @@ import * as path from "path"
import * as fs from "fs/promises"
import * as yaml from "js-yaml"
import simpleGit, { SimpleGit } from "simple-git"
import { MetadataScanner } from "./MetadataScanner"
import { validateAnyMetadata } from "./schemas"
import { LocalizationOptions, PackageManagerItem, PackageManagerRepository, RepositoryMetadata } from "./types"
import { getUserLocale } from "./utils"
import { MetadataScanner } from "@package-manager/MetadataScanner"
import { validateAnyMetadata } from "@package-manager/schemas"
import {
LocalizationOptions,
PackageManagerItem,
PackageManagerRepository,
RepositoryMetadata,
} from "@package-manager/types"
import { getUserLocale } from "@package-manager/utils"
/**
* Handles fetching and caching package manager repositories
@ -96,8 +101,23 @@ export class GitFetcher {
* @param repoDir Repository directory
* @param forceRefresh Whether to force refresh
*/
/**
* Clean up any git lock files in the repository
* @param repoDir Repository directory
*/
private async cleanupGitLocks(repoDir: string): Promise<void> {
const indexLockPath = path.join(repoDir, ".git", "index.lock")
try {
await fs.unlink(indexLockPath)
} catch {
// Ignore errors if file doesn't exist
}
}
private async cloneOrPullRepository(repoUrl: string, repoDir: string, forceRefresh: boolean): Promise<void> {
try {
// Clean up any existing git lock files first
await this.cleanupGitLocks(repoDir)
// Check if repository exists
const gitDir = path.join(repoDir, ".git")
let repoExists = await fs
@ -114,6 +134,8 @@ export class GitFetcher {
await git.raw(["reset", "--hard", "origin/main"])
await git.raw(["clean", "-f", "-d"])
} catch (error) {
// Clean up git locks before retrying
await this.cleanupGitLocks(repoDir)
// If pull fails with specific errors that indicate repo corruption,
// we should remove and re-clone
const errorMessage = error instanceof Error ? error.message : String(error)
@ -132,6 +154,14 @@ export class GitFetcher {
if (!repoExists || forceRefresh) {
try {
// Clean up any existing git lock files
const indexLockPath = path.join(repoDir, ".git", "index.lock")
try {
await fs.unlink(indexLockPath)
} catch {
// Ignore errors if file doesn't exist
}
// Always remove the directory before cloning
await fs.rm(repoDir, { recursive: true, force: true })

View file

@ -3,7 +3,7 @@ import * as fs from "fs/promises"
import * as vscode from "vscode"
import * as yaml from "js-yaml"
import { SimpleGit } from "simple-git"
import { validateAnyMetadata } from "./schemas"
import { validateAnyMetadata } from "@package-manager/schemas"
import {
ComponentMetadata,
ComponentType,
@ -11,8 +11,8 @@ import {
LocalizedMetadata,
PackageManagerItem,
PackageMetadata,
} from "./types"
import { getUserLocale } from "./utils"
} from "@package-manager/types"
import { getUserLocale } from "@package-manager/utils"
/**
* Handles component discovery and metadata loading

View file

@ -1,7 +1,7 @@
import * as vscode from "vscode"
import * as path from "path"
import * as fs from "fs/promises"
import { GitFetcher } from "./GitFetcher"
import { GitFetcher } from "@package-manager/GitFetcher"
import {
PackageManagerItem,
PackageManagerRepository,
@ -9,20 +9,24 @@ import {
ComponentType,
ComponentMetadata,
LocalizationOptions,
} from "./types"
import { getUserLocale } from "./utils"
} from "@package-manager/types"
import { getUserLocale } from "@package-manager/utils"
/**
* Service for managing package manager data
*/
export class PackageManagerManager {
private currentItems: PackageManagerItem[] = []
public isFetching = false
// Cache expiry time in milliseconds (set to a low value for testing)
private static readonly CACHE_EXPIRY_MS = 3600000 // 1 hour
private gitFetcher: GitFetcher
private cache: Map<string, { data: PackageManagerRepository; timestamp: number }> = new Map()
public isFetching = false
// Concurrency control
private activeSourceOperations = new Set<string>() // Track active git operations per source
private isMetadataScanActive = false // Track active metadata scanning
private pendingOperations: Array<() => Promise<void>> = [] // Queue for pending operations
constructor(private readonly context: vscode.ExtensionContext) {
const localizationOptions: LocalizationOptions = {
@ -37,6 +41,33 @@ export class PackageManagerManager {
* @param sources The package manager sources
* @returns An array of PackageManagerItem objects
*/
/**
* Queue an operation to run when no metadata scan is active
*/
private async queueOperation(operation: () => Promise<void>): Promise<void> {
if (this.isMetadataScanActive) {
return new Promise((resolve) => {
this.pendingOperations.push(async () => {
await operation()
resolve()
})
})
}
try {
this.isMetadataScanActive = true
await operation()
} finally {
this.isMetadataScanActive = false
// Process any pending operations
const nextOperation = this.pendingOperations.shift()
if (nextOperation) {
void this.queueOperation(nextOperation)
}
}
}
async getPackageManagerItems(
sources: PackageManagerSource[],
): Promise<{ items: PackageManagerItem[]; errors?: string[] }> {
@ -48,23 +79,34 @@ export class PackageManagerManager {
const enabledSources = sources.filter((s) => s.enabled)
console.log(`PackageManagerManager: ${enabledSources.length} enabled sources`)
// Process sources sequentially to avoid overwhelming the system
// Process sources sequentially with locking
for (const source of enabledSources) {
try {
console.log(`PackageManagerManager: Processing source ${source.url}`)
// Pass the source name to getRepositoryData
const repo = await this.getRepositoryData(source.url, false, source.name)
if (this.isSourceLocked(source.url)) {
console.log(`PackageManagerManager: Source ${source.url} is locked, skipping`)
continue
}
if (repo.items && repo.items.length > 0) {
console.log(`PackageManagerManager: Found ${repo.items.length} items in ${source.url}`)
items.push(...repo.items)
} else {
console.log(`PackageManagerManager: No items found in ${source.url}`)
}
try {
this.lockSource(source.url)
console.log(`PackageManagerManager: Processing source ${source.url}`)
// Queue metadata scanning operation
await this.queueOperation(async () => {
const repo = await this.getRepositoryData(source.url, false, source.name)
if (repo.items && repo.items.length > 0) {
console.log(`PackageManagerManager: Found ${repo.items.length} items in ${source.url}`)
items.push(...repo.items)
} else {
console.log(`PackageManagerManager: No items found in ${source.url}`)
}
})
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
console.error(`PackageManagerManager: Failed to fetch data from ${source.url}:`, error)
errors.push(`Source ${source.url}: ${errorMessage}`)
} finally {
this.unlockSource(source.url)
}
}
@ -88,6 +130,27 @@ export class PackageManagerManager {
* @param sourceName The name of the source
* @returns A PackageManagerRepository object
*/
/**
* Check if a source operation is in progress
*/
private isSourceLocked(url: string): boolean {
return this.activeSourceOperations.has(url)
}
/**
* Lock a source for operations
*/
private lockSource(url: string): void {
this.activeSourceOperations.add(url)
}
/**
* Unlock a source after operations complete
*/
private unlockSource(url: string): void {
this.activeSourceOperations.delete(url)
}
async getRepositoryData(
url: string,
forceRefresh: boolean = false,

View file

@ -1,9 +1,9 @@
import * as vscode from "vscode"
import { GitFetcher } from "../GitFetcher"
import { GitFetcher } from "@package-manager/GitFetcher"
import * as fs from "fs/promises"
import { Dirent, Stats } from "fs"
import simpleGit, { SimpleGit } from "simple-git"
import { MetadataScanner } from "../MetadataScanner"
import { MetadataScanner } from "@package-manager/MetadataScanner"
import { exec, ChildProcess } from "child_process"
import { promisify } from "util"
import { EventEmitter } from "events"
@ -26,6 +26,7 @@ jest.mock("fs/promises", () => ({
mkdir: jest.fn(),
stat: jest.fn(),
rm: jest.fn(),
unlink: jest.fn(),
readdir: jest.fn().mockResolvedValue([]),
readFile: jest.fn().mockResolvedValue(`
name: Test Repository
@ -258,6 +259,59 @@ describe("GitFetcher", () => {
})
})
describe("Git Lock File Handling", () => {
it("should clean up index.lock file before operations", async () => {
// Mock repository exists
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
if (path.endsWith(".git")) return Promise.resolve(true)
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
if (path.endsWith("README.md")) return Promise.resolve(true)
return Promise.reject(new Error("ENOENT"))
})
await gitFetcher.fetchRepository(testRepoUrl)
// Verify lock file cleanup was attempted
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("index.lock"))
})
it("should handle missing lock file gracefully", async () => {
// Mock unlink to fail as if file doesn't exist
;(fs.unlink as jest.Mock).mockRejectedValue(new Error("ENOENT"))
await gitFetcher.fetchRepository(testRepoUrl)
// Operation should succeed despite lock file not existing
const mockGit = mockSimpleGit()
expect(mockGit.clone).toHaveBeenCalled()
})
it("should clean up lock file when pull fails", async () => {
// Mock repository exists
;(fs.stat as jest.Mock).mockImplementation((path: string) => {
if (path.endsWith(".git")) return Promise.resolve(true)
if (path.endsWith("metadata.en.yml")) return Promise.resolve(true)
if (path.endsWith("README.md")) return Promise.resolve(true)
return Promise.reject(new Error("ENOENT"))
})
const mockGit = {
clone: jest.fn().mockResolvedValue(undefined),
pull: jest.fn().mockRejectedValue(new Error("not a git repository")),
revparse: jest.fn().mockResolvedValue("main"),
fetch: jest.fn().mockRejectedValue(new Error("not a git repository")),
clean: jest.fn(),
raw: jest.fn(),
} as unknown as SimpleGit
mockSimpleGit.mockReturnValue(mockGit)
await gitFetcher.fetchRepository(testRepoUrl)
// Verify lock file cleanup was attempted after pull failure
expect(fs.unlink).toHaveBeenCalledWith(expect.stringContaining("index.lock"))
})
})
describe("Repository Structure Validation", () => {
// Helper function to access private method
const validateRepositoryStructure = async (repoDir: string) => {

View file

@ -1,9 +1,9 @@
import * as path from "path"
import * as fs from "fs/promises"
import { Dirent } from "fs"
import { MetadataScanner } from "../MetadataScanner"
import { MetadataScanner } from "@package-manager/MetadataScanner"
import { SimpleGit } from "simple-git"
import { ComponentMetadata, LocalizationOptions, LocalizedMetadata, PackageMetadata } from "../types"
import { ComponentMetadata, LocalizationOptions, LocalizedMetadata, PackageMetadata } from "@package-manager/types"
// Mock fs/promises
jest.mock("fs/promises")

View file

@ -1,6 +1,7 @@
import { PackageManagerManager } from "../PackageManagerManager"
import { PackageManagerItem } from "../types"
import { MetadataScanner } from "../MetadataScanner"
import { PackageManagerManager } from "@package-manager/PackageManagerManager"
import { PackageManagerItem, PackageManagerSource, PackageManagerRepository } from "@package-manager/types"
import { MetadataScanner } from "@package-manager/MetadataScanner"
import { GitFetcher } from "@package-manager/GitFetcher"
import * as path from "path"
import * as vscode from "vscode"
@ -68,6 +69,8 @@ describe("PackageManagerManager", () => {
]
})
// Concurrency Control tests moved to their own describe block
test("should include package when filtering by its own type", () => {
// Filter by package type
const filtered = manager.filterItems(typeFilterTestItems, { type: "package" })
@ -421,182 +424,107 @@ describe("PackageManagerManager", () => {
})
})
// Re-declare manager for the following test sections
let manager: PackageManagerManager
beforeEach(() => {
const context = {
globalStorageUri: { fsPath: path.resolve(__dirname, "../../../../mock/settings/path") },
} as vscode.ExtensionContext
manager = new PackageManagerManager(context)
})
describe("Concurrency Control", () => {
let manager: PackageManagerManager
describe("sortItems with subcomponents", () => {
const testItems: PackageManagerItem[] = [
{
name: "B Package",
description: "Package B",
type: "package",
version: "1.0.0",
url: "/test/b",
repoUrl: "https://example.com",
items: [
{
type: "mode",
path: "modes/y",
metadata: {
name: "Y Mode",
description: "Mode Y",
type: "mode",
version: "1.0.0",
},
lastUpdated: "2025-04-13T09:00:00-07:00",
},
{
type: "mode",
path: "modes/x",
metadata: {
name: "X Mode",
description: "Mode X",
type: "mode",
version: "1.0.0",
},
lastUpdated: "2025-04-13T09:00:00-07:00",
},
],
},
{
name: "A Package",
description: "Package A",
type: "package",
version: "1.0.0",
url: "/test/a",
repoUrl: "https://example.com",
items: [
{
type: "mode",
path: "modes/z",
metadata: {
name: "Z Mode",
description: "Mode Z",
type: "mode",
version: "1.0.0",
},
lastUpdated: "2025-04-13T08:00:00-07:00",
},
],
},
]
it("should sort parent items while preserving subcomponents", () => {
const sorted = manager.sortItems(testItems, "name", "asc")
expect(sorted[0].name).toBe("A Package")
expect(sorted[1].name).toBe("B Package")
expect(sorted[0].items![0].metadata!.name).toBe("Z Mode")
expect(sorted[1].items![0].metadata!.name).toBe("Y Mode")
beforeEach(() => {
const mockContext = {
globalStorageUri: { fsPath: "/test/path" },
} as vscode.ExtensionContext
manager = new PackageManagerManager(mockContext)
})
it("should sort subcomponents within parents", () => {
const sorted = manager.sortItems(testItems, "name", "asc", true)
expect(sorted[1].items![0].metadata!.name).toBe("X Mode")
expect(sorted[1].items![1].metadata!.name).toBe("Y Mode")
})
it("should not allow concurrent operations on the same source", async () => {
const source: PackageManagerSource = {
url: "https://github.com/test/repo",
enabled: true,
}
it("should preserve subcomponent order when sortSubcomponents is false", () => {
const sorted = manager.sortItems(testItems, "name", "asc", false)
expect(sorted[1].items![0].metadata!.name).toBe("Y Mode")
expect(sorted[1].items![1].metadata!.name).toBe("X Mode")
})
it("should handle empty subcomponents when sorting", () => {
const itemsWithEmpty = [
...testItems,
{
name: "C Package",
description: "Package C",
type: "package" as const,
version: "1.0.0",
url: "/test/c",
repoUrl: "https://example.com",
// Mock getRepositoryData to be slow
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
const slowGetRepositoryData = jest.spyOn(manager as any, "getRepositoryData").mockImplementation(async () => {
await delay(100) // Simulate slow operation
return {
metadata: { name: "test", description: "test", version: "1.0.0" },
items: [],
} as PackageManagerItem,
]
const sorted = manager.sortItems(itemsWithEmpty, "name", "asc")
expect(sorted[2].name).toBe("C Package")
expect(sorted[2].items).toHaveLength(0)
})
})
url: source.url,
} as PackageManagerRepository
})
describe("filterItems with real data", () => {
it("should return all subcomponents with match info", () => {
const testItems: PackageManagerItem[] = [
{
name: "Data Platform Package",
description: "A test platform",
type: "package",
version: "1.0.0",
url: "/test/data-platform",
repoUrl: "https://example.com",
items: [
{
type: "mcp server",
path: "mcp servers/data-validator",
metadata: {
name: "Data Validator",
description: "An MCP server for validating data quality",
type: "mcp server",
version: "1.0.0",
},
lastUpdated: "2025-04-13T10:00:00-07:00",
},
{
type: "mode",
path: "modes/task-runner",
metadata: {
name: "Task Runner",
description: "A mode for running tasks",
type: "mode",
version: "1.0.0",
},
lastUpdated: "2025-04-13T10:00:00-07:00",
},
],
},
// Start two concurrent operations
const operation1 = manager.getPackageManagerItems([source])
const operation2 = manager.getPackageManagerItems([source])
// Wait for both to complete
const [result1, result2] = await Promise.all([operation1, operation2])
// Verify getRepositoryData was only called once
expect(slowGetRepositoryData).toHaveBeenCalledTimes(1)
})
it("should not allow metadata scanning during git operations", async () => {
const source1: PackageManagerSource = {
url: "https://github.com/test/repo1",
enabled: true,
}
const source2: PackageManagerSource = {
url: "https://github.com/test/repo2",
enabled: true,
}
let isGitOperationActive = false
let metadataScanDuringGit = false
// Mock git operation to be slow and set flag
jest.spyOn(GitFetcher.prototype, "fetchRepository").mockImplementation(async () => {
isGitOperationActive = true
await new Promise((resolve) => setTimeout(resolve, 100))
isGitOperationActive = false
return {
metadata: { name: "test", description: "test", version: "1.0.0" },
items: [],
url: source1.url,
}
})
// Mock metadata scanner to check if git operation is active
jest.spyOn(MetadataScanner.prototype, "scanDirectory").mockImplementation(async () => {
if (isGitOperationActive) {
metadataScanDuringGit = true
}
return []
})
// Process both sources
await manager.getPackageManagerItems([source1, source2])
// Verify metadata scanning didn't occur during git operations
expect(metadataScanDuringGit).toBe(false)
})
it("should queue metadata scans and process them sequentially", async () => {
const sources: PackageManagerSource[] = [
{ url: "https://github.com/test/repo1", enabled: true },
{ url: "https://github.com/test/repo2", enabled: true },
{ url: "https://github.com/test/repo3", enabled: true },
]
// Search for "data validator"
const filtered = manager.filterItems(testItems, { search: "data validator" })
let activeScans = 0
let maxConcurrentScans = 0
// Verify package is returned
expect(filtered.length).toBe(1)
const pkg = filtered[0]
// Verify all subcomponents are returned
expect(pkg.items?.length).toBe(2)
// Verify matching subcomponent has correct matchInfo
const validator = pkg.items?.find((item: any) => item.metadata?.name === "Data Validator")
expect(validator?.matchInfo).toEqual({
matched: true,
matchReason: {
nameMatch: true,
descriptionMatch: false,
},
// Mock metadata scanner to track concurrent scans
jest.spyOn(MetadataScanner.prototype, "scanDirectory").mockImplementation(async () => {
activeScans++
maxConcurrentScans = Math.max(maxConcurrentScans, activeScans)
await new Promise((resolve) => setTimeout(resolve, 50))
activeScans--
return []
})
// Verify non-matching subcomponent has correct matchInfo
const runner = pkg.items?.find((item: any) => item.metadata?.name === "Task Runner")
expect(runner?.matchInfo).toEqual({
matched: false,
})
// Process all sources
await manager.getPackageManagerItems(sources)
// Verify package has matchInfo indicating it contains matches
expect(pkg.matchInfo).toEqual({
matched: true,
matchReason: {
nameMatch: false,
descriptionMatch: false,
hasMatchingSubcomponents: true,
},
})
// Verify only one scan was active at a time
expect(maxConcurrentScans).toBe(1)
})
})

View file

@ -6,8 +6,8 @@ import {
validateSource,
validateSources,
ValidationError,
} from "../PackageManagerSourceValidation"
import { PackageManagerSource } from "../types"
} from "@package-manager/PackageManagerSourceValidation"
import { PackageManagerSource } from "@package-manager/types"
describe("PackageManagerSourceValidation", () => {
describe("isValidGitRepositoryUrl", () => {

View file

@ -1,3 +1,4 @@
export * from "./GitFetcher";
export * from "./PackageManagerManager";
export * from "./types";
export * from "./GitFetcher"
export * from "./PackageManagerManager"
export * from "./types"
export * from "./PackageManagerSourceValidation"

View file

@ -18,7 +18,12 @@
"strict": true,
"target": "es2022",
"useDefineForClassFields": true,
"useUnknownInCatchVariables": false
"useUnknownInCatchVariables": false,
"baseUrl": ".",
"paths": {
"@package-manager": ["src/services/package-manager"],
"@package-manager/*": ["src/services/package-manager/*"]
}
},
"include": ["src/**/*", "scripts/**/*", ".changeset/**/*"],
"exclude": ["node_modules", ".vscode-test", "webview-ui"]