mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-30 17:14:40 +08:00
Merge pull request #30 from Kilo-Org/kilocode-ignore-migration
feat(kilocode): add .kilocodeignore migration to permission rules
This commit is contained in:
@@ -32,6 +32,7 @@ import { ModesMigrator } from "../kilocode/modes-migrator" // kilocode_change
|
||||
import { RulesMigrator } from "../kilocode/rules-migrator" // kilocode_change
|
||||
import { WorkflowsMigrator } from "../kilocode/workflows-migrator" // kilocode_change
|
||||
import { McpMigrator } from "../kilocode/mcp-migrator" // kilocode_change
|
||||
import { IgnoreMigrator } from "../kilocode/ignore-migrator" // kilocode_change
|
||||
|
||||
export namespace Config {
|
||||
const log = Log.create({ service: "config" })
|
||||
@@ -114,6 +115,20 @@ export namespace Config {
|
||||
if (Object.keys(kilocodeMcp).length > 0) {
|
||||
result = mergeConfigConcatArrays(result, { mcp: kilocodeMcp })
|
||||
}
|
||||
|
||||
// Load Kilocode .kilocodeignore patterns (legacy fallback)
|
||||
try {
|
||||
const ignorePermission = await IgnoreMigrator.loadIgnoreConfig(Instance.directory)
|
||||
if (Object.keys(ignorePermission).length > 0) {
|
||||
result = mergeConfigConcatArrays(result, { permission: ignorePermission })
|
||||
log.debug("loaded kilocode ignore patterns", {
|
||||
hasRead: !!ignorePermission.read,
|
||||
hasEdit: !!ignorePermission.edit,
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
log.warn("failed to load kilocode ignore patterns", { error: err })
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// Load remote/well-known config (overrides Kilocode legacy configs)
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Config } from "../config/config"
|
||||
import { ModesMigrator } from "./modes-migrator"
|
||||
import { RulesMigrator } from "./rules-migrator" // kilocode_change
|
||||
import { WorkflowsMigrator } from "./workflows-migrator"
|
||||
import { IgnoreMigrator } from "./ignore-migrator" // kilocode_change
|
||||
|
||||
export namespace KilocodeConfigInjector {
|
||||
export interface InjectionResult {
|
||||
@@ -16,6 +17,8 @@ export namespace KilocodeConfigInjector {
|
||||
skipGlobalPaths?: boolean
|
||||
/** Include rules migration. Defaults to true. */
|
||||
includeRules?: boolean
|
||||
/** Include ignore migration. Defaults to true. */
|
||||
includeIgnore?: boolean
|
||||
}): Promise<InjectionResult> {
|
||||
const warnings: string[] = []
|
||||
|
||||
@@ -59,12 +62,49 @@ export namespace KilocodeConfigInjector {
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - Ignore migration
|
||||
if (options.includeIgnore !== false) {
|
||||
const ignoreMigration = await IgnoreMigrator.migrate({
|
||||
projectDir: options.projectDir,
|
||||
skipGlobalPaths: options.skipGlobalPaths,
|
||||
})
|
||||
|
||||
warnings.push(...ignoreMigration.warnings)
|
||||
|
||||
if (Object.keys(ignoreMigration.permission).length > 0) {
|
||||
config.permission = mergePermissions(config.permission, ignoreMigration.permission)
|
||||
}
|
||||
}
|
||||
// kilocode_change end
|
||||
|
||||
return {
|
||||
configJson: JSON.stringify(config),
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge permission configs, preserving order and handling duplicates.
|
||||
* Incoming rules take precedence (kilocode patterns override).
|
||||
*/
|
||||
function mergePermissions(existing: Config.Permission | undefined, incoming: Config.Permission): Config.Permission {
|
||||
if (!existing) return incoming
|
||||
|
||||
const result: Config.Permission = { ...existing }
|
||||
|
||||
for (const [key, value] of Object.entries(incoming)) {
|
||||
if (key === "read" || key === "edit") {
|
||||
const existingRules = (result[key] as Record<string, Config.PermissionAction>) ?? {}
|
||||
const incomingRules = value as Record<string, Config.PermissionAction>
|
||||
result[key] = { ...existingRules, ...incomingRules }
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getEnvVars(configJson: string): Record<string, string> {
|
||||
if (!configJson || configJson === "{}") {
|
||||
return {}
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
// kilocode_change - new file
|
||||
|
||||
import * as path from "path"
|
||||
import os from "os"
|
||||
import { Log } from "../util/log"
|
||||
import type { Config } from "../config/config"
|
||||
|
||||
export namespace IgnoreMigrator {
|
||||
const log = Log.create({ service: "kilocode.ignore-migrator" })
|
||||
|
||||
const KILOCODEIGNORE_FILE = ".kilocodeignore"
|
||||
const GLOBAL_KILOCODEIGNORE = path.join(os.homedir(), ".kilocode", KILOCODEIGNORE_FILE)
|
||||
|
||||
export interface IgnorePattern {
|
||||
pattern: string
|
||||
negated: boolean
|
||||
source: "global" | "project"
|
||||
}
|
||||
|
||||
export interface MigrationResult {
|
||||
permission: Config.Permission
|
||||
warnings: string[]
|
||||
patternCount: number
|
||||
}
|
||||
|
||||
async function fileExists(filepath: string): Promise<boolean> {
|
||||
return Bun.file(filepath).exists()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse .kilocodeignore content into patterns.
|
||||
* Follows gitignore syntax:
|
||||
* - Lines starting with # are comments
|
||||
* - Empty lines are ignored
|
||||
* - Lines starting with ! are negation patterns
|
||||
*/
|
||||
export function parseIgnoreContent(content: string): Array<{ pattern: string; negated: boolean }> {
|
||||
const patterns: Array<{ pattern: string; negated: boolean }> = []
|
||||
|
||||
for (const line of content.split("\n")) {
|
||||
const trimmed = line.trim()
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (!trimmed || trimmed.startsWith("#")) continue
|
||||
|
||||
// Handle negation patterns
|
||||
if (trimmed.startsWith("!")) {
|
||||
patterns.push({ pattern: trimmed.slice(1), negated: true })
|
||||
} else {
|
||||
patterns.push({ pattern: trimmed, negated: false })
|
||||
}
|
||||
}
|
||||
|
||||
return patterns
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert gitignore pattern to Opencode wildcard pattern.
|
||||
*
|
||||
* Opencode's Wildcard module uses simple patterns:
|
||||
* - `*` matches any characters (converted to `.*` regex)
|
||||
* - `?` matches single character
|
||||
*
|
||||
* Gitignore semantics we need to handle:
|
||||
* - `foo/` matches directory and all contents -> `foo/*`
|
||||
* - `*.env` matches anywhere in tree -> `*.env` (already works)
|
||||
* - `/foo` matches only at root -> `foo` (rooted)
|
||||
* - `foo` matches anywhere -> `*foo*` or just `foo` depending on context
|
||||
*
|
||||
* Note: Opencode's wildcard `*` already matches any path depth because
|
||||
* it's converted to `.*` regex which matches `/` characters.
|
||||
*/
|
||||
export function convertToGlob(pattern: string): string {
|
||||
let glob = pattern
|
||||
|
||||
// Directory patterns (ending with /) should match all contents
|
||||
if (glob.endsWith("/")) {
|
||||
glob = glob.slice(0, -1) + "/*"
|
||||
}
|
||||
|
||||
// Patterns starting with / are rooted - remove the leading /
|
||||
if (glob.startsWith("/")) {
|
||||
glob = glob.slice(1)
|
||||
}
|
||||
|
||||
// Remove **/ prefix if present - Opencode's * already matches paths
|
||||
if (glob.startsWith("**/")) {
|
||||
glob = "*" + glob.slice(3)
|
||||
}
|
||||
|
||||
// Replace **/ in the middle with just *
|
||||
glob = glob.replace(/\*\*\//g, "*")
|
||||
|
||||
// Replace trailing /** with /*
|
||||
if (glob.endsWith("/**")) {
|
||||
glob = glob.slice(0, -3) + "/*"
|
||||
}
|
||||
|
||||
return glob
|
||||
}
|
||||
|
||||
/**
|
||||
* Load patterns from a .kilocodeignore file
|
||||
*/
|
||||
async function loadIgnoreFile(filepath: string, source: "global" | "project"): Promise<IgnorePattern[]> {
|
||||
if (!(await fileExists(filepath))) return []
|
||||
|
||||
const content = await Bun.file(filepath).text()
|
||||
const parsed = parseIgnoreContent(content)
|
||||
|
||||
return parsed.map((p) => ({
|
||||
pattern: p.pattern,
|
||||
negated: p.negated,
|
||||
source,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Build permission rules from ignore patterns.
|
||||
*
|
||||
* Order matters! Patterns are evaluated in order:
|
||||
* 1. Start with "*": "allow" (default allow)
|
||||
* 2. Add deny patterns
|
||||
* 3. Add negated patterns (allow) last to override denies
|
||||
*/
|
||||
export function buildPermissionRules(patterns: IgnorePattern[]): Record<string, Config.PermissionAction> {
|
||||
const rules: Record<string, Config.PermissionAction> = {
|
||||
"*": "allow", // Default: allow all
|
||||
}
|
||||
|
||||
// First pass: add deny rules
|
||||
for (const p of patterns) {
|
||||
if (!p.negated) {
|
||||
const glob = convertToGlob(p.pattern)
|
||||
rules[glob] = "deny"
|
||||
}
|
||||
}
|
||||
|
||||
// Second pass: add negated (allow) rules - these override denies
|
||||
for (const p of patterns) {
|
||||
if (p.negated) {
|
||||
const glob = convertToGlob(p.pattern)
|
||||
rules[glob] = "allow"
|
||||
}
|
||||
}
|
||||
|
||||
return rules
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate .kilocodeignore to Opencode permission config
|
||||
*/
|
||||
export async function migrate(options: { projectDir: string; skipGlobalPaths?: boolean }): Promise<MigrationResult> {
|
||||
const warnings: string[] = []
|
||||
const allPatterns: IgnorePattern[] = []
|
||||
|
||||
// 1. Load global .kilocodeignore (lower priority)
|
||||
if (!options.skipGlobalPaths) {
|
||||
const globalPatterns = await loadIgnoreFile(GLOBAL_KILOCODEIGNORE, "global")
|
||||
allPatterns.push(...globalPatterns)
|
||||
|
||||
if (globalPatterns.length > 0) {
|
||||
log.debug("loaded global .kilocodeignore", { count: globalPatterns.length })
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Load project .kilocodeignore (higher priority - added last)
|
||||
const projectIgnorePath = path.join(options.projectDir, KILOCODEIGNORE_FILE)
|
||||
const projectPatterns = await loadIgnoreFile(projectIgnorePath, "project")
|
||||
allPatterns.push(...projectPatterns)
|
||||
|
||||
if (projectPatterns.length > 0) {
|
||||
log.debug("loaded project .kilocodeignore", { count: projectPatterns.length })
|
||||
}
|
||||
|
||||
// 3. Build permission rules
|
||||
if (allPatterns.length === 0) {
|
||||
return {
|
||||
permission: {},
|
||||
warnings,
|
||||
patternCount: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const rules = buildPermissionRules(allPatterns)
|
||||
|
||||
// 4. Create permission config for both read and edit
|
||||
const permission: Config.Permission = {
|
||||
read: rules,
|
||||
edit: rules,
|
||||
}
|
||||
|
||||
return {
|
||||
permission,
|
||||
warnings,
|
||||
patternCount: allPatterns.length,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load .kilocodeignore and return permission config.
|
||||
* Handles all logging internally.
|
||||
*/
|
||||
export async function loadIgnoreConfig(projectDir: string, skipGlobalPaths?: boolean): Promise<Config.Permission> {
|
||||
try {
|
||||
const result = await migrate({ projectDir, skipGlobalPaths })
|
||||
|
||||
if (result.patternCount > 0) {
|
||||
log.info("loaded .kilocodeignore patterns", {
|
||||
count: result.patternCount,
|
||||
})
|
||||
}
|
||||
|
||||
for (const warning of result.warnings) {
|
||||
log.warn("ignore migration warning", { warning })
|
||||
}
|
||||
|
||||
return result.permission
|
||||
} catch (err) {
|
||||
log.warn("failed to load .kilocodeignore", { error: err })
|
||||
return {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,5 +2,6 @@ export { ModesMigrator } from "./modes-migrator"
|
||||
export { RulesMigrator } from "./rules-migrator"
|
||||
export { WorkflowsMigrator } from "./workflows-migrator"
|
||||
export { McpMigrator } from "./mcp-migrator"
|
||||
export { IgnoreMigrator } from "./ignore-migrator" // kilocode_change
|
||||
export { KilocodeConfigInjector } from "./config-injector"
|
||||
export { KilocodePaths } from "./paths"
|
||||
|
||||
@@ -198,6 +198,92 @@ describe("KilocodeConfigInjector", () => {
|
||||
expect(result.warnings.some((w) => w.includes("Legacy"))).toBe(true)
|
||||
})
|
||||
// kilocode_change end
|
||||
|
||||
// kilocode_change start - Ignore migration tests
|
||||
test("includes ignore patterns in config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "secrets/\n*.env")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
const config = JSON.parse(result.configJson)
|
||||
|
||||
expect(config.permission).toBeDefined()
|
||||
expect(config.permission.read).toBeDefined()
|
||||
expect(config.permission.edit).toBeDefined()
|
||||
expect(config.permission.read["secrets/*"]).toBe("deny")
|
||||
expect(config.permission.read["*.env"]).toBe("deny")
|
||||
})
|
||||
|
||||
test("skips ignore when includeIgnore is false", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "secrets/")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
includeIgnore: false,
|
||||
})
|
||||
const config = JSON.parse(result.configJson)
|
||||
|
||||
expect(config.permission).toBeUndefined()
|
||||
})
|
||||
|
||||
test("combines ignore with other migrations", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
// Add custom mode
|
||||
await Bun.write(
|
||||
path.join(dir, ".kilocodemodes"),
|
||||
`customModes:
|
||||
- slug: translate
|
||||
name: Translate
|
||||
roleDefinition: You are a translator
|
||||
groups:
|
||||
- read`,
|
||||
)
|
||||
// Add ignore patterns
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "secrets/")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
const config = JSON.parse(result.configJson)
|
||||
|
||||
expect(config.agent).toBeDefined()
|
||||
expect(config.agent.translate).toBeDefined()
|
||||
expect(config.permission).toBeDefined()
|
||||
expect(config.permission.read["secrets/*"]).toBe("deny")
|
||||
})
|
||||
|
||||
test("handles negation patterns in ignore file", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "*.env\n!.env.example")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await KilocodeConfigInjector.buildConfig({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
const config = JSON.parse(result.configJson)
|
||||
|
||||
expect(config.permission.read["*.env"]).toBe("deny")
|
||||
expect(config.permission.read[".env.example"]).toBe("allow")
|
||||
})
|
||||
// kilocode_change end
|
||||
})
|
||||
|
||||
describe("getEnvVars", () => {
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
import { test, expect, describe } from "bun:test"
|
||||
import { IgnoreMigrator } from "../../src/kilocode/ignore-migrator"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import path from "path"
|
||||
|
||||
describe("IgnoreMigrator", () => {
|
||||
describe("parseIgnoreContent", () => {
|
||||
test("parses basic patterns", () => {
|
||||
const content = `
|
||||
# Comment
|
||||
secrets/
|
||||
*.env
|
||||
!.env.example
|
||||
`
|
||||
const patterns = IgnoreMigrator.parseIgnoreContent(content)
|
||||
|
||||
expect(patterns).toEqual([
|
||||
{ pattern: "secrets/", negated: false },
|
||||
{ pattern: "*.env", negated: false },
|
||||
{ pattern: ".env.example", negated: true },
|
||||
])
|
||||
})
|
||||
|
||||
test("ignores empty lines and comments", () => {
|
||||
const content = `
|
||||
# This is a comment
|
||||
pattern1
|
||||
|
||||
# Another comment
|
||||
pattern2
|
||||
`
|
||||
const patterns = IgnoreMigrator.parseIgnoreContent(content)
|
||||
|
||||
expect(patterns).toHaveLength(2)
|
||||
expect(patterns[0].pattern).toBe("pattern1")
|
||||
expect(patterns[1].pattern).toBe("pattern2")
|
||||
})
|
||||
|
||||
test("handles negation patterns", () => {
|
||||
const content = `*.log
|
||||
!important.log`
|
||||
const patterns = IgnoreMigrator.parseIgnoreContent(content)
|
||||
|
||||
expect(patterns[0]).toEqual({ pattern: "*.log", negated: false })
|
||||
expect(patterns[1]).toEqual({ pattern: "important.log", negated: true })
|
||||
})
|
||||
|
||||
test("handles empty content", () => {
|
||||
const patterns = IgnoreMigrator.parseIgnoreContent("")
|
||||
expect(patterns).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("handles content with only comments", () => {
|
||||
const content = `# Comment 1
|
||||
# Comment 2`
|
||||
const patterns = IgnoreMigrator.parseIgnoreContent(content)
|
||||
expect(patterns).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("trims whitespace from patterns", () => {
|
||||
const content = ` secrets/
|
||||
*.env `
|
||||
const patterns = IgnoreMigrator.parseIgnoreContent(content)
|
||||
|
||||
expect(patterns[0].pattern).toBe("secrets/")
|
||||
expect(patterns[1].pattern).toBe("*.env")
|
||||
})
|
||||
})
|
||||
|
||||
describe("convertToGlob", () => {
|
||||
test("converts directory pattern", () => {
|
||||
expect(IgnoreMigrator.convertToGlob("secrets/")).toBe("secrets/*")
|
||||
})
|
||||
|
||||
test("preserves simple filename pattern", () => {
|
||||
// Opencode's * already matches any path depth
|
||||
expect(IgnoreMigrator.convertToGlob("*.env")).toBe("*.env")
|
||||
})
|
||||
|
||||
test("converts rooted patterns", () => {
|
||||
expect(IgnoreMigrator.convertToGlob("/root-only")).toBe("root-only")
|
||||
})
|
||||
|
||||
test("preserves patterns with path separators", () => {
|
||||
expect(IgnoreMigrator.convertToGlob("src/secrets/")).toBe("src/secrets/*")
|
||||
})
|
||||
|
||||
test("handles double-star patterns", () => {
|
||||
expect(IgnoreMigrator.convertToGlob("**/deep/")).toBe("*deep/*")
|
||||
})
|
||||
|
||||
test("preserves simple filename without extension", () => {
|
||||
// Opencode's * already matches any path depth
|
||||
expect(IgnoreMigrator.convertToGlob("Dockerfile")).toBe("Dockerfile")
|
||||
})
|
||||
|
||||
test("preserves extension-only pattern", () => {
|
||||
expect(IgnoreMigrator.convertToGlob("*.log")).toBe("*.log")
|
||||
})
|
||||
|
||||
test("preserves nested path pattern", () => {
|
||||
expect(IgnoreMigrator.convertToGlob("config/secrets.json")).toBe("config/secrets.json")
|
||||
})
|
||||
})
|
||||
|
||||
describe("buildPermissionRules", () => {
|
||||
test("creates default allow rule", () => {
|
||||
const rules = IgnoreMigrator.buildPermissionRules([])
|
||||
expect(rules["*"]).toBe("allow")
|
||||
})
|
||||
|
||||
test("creates deny rules for patterns", () => {
|
||||
const patterns = [{ pattern: "secrets/", negated: false, source: "project" as const }]
|
||||
const rules = IgnoreMigrator.buildPermissionRules(patterns)
|
||||
|
||||
expect(rules["*"]).toBe("allow")
|
||||
expect(rules["secrets/*"]).toBe("deny")
|
||||
})
|
||||
|
||||
test("creates allow rules for negated patterns", () => {
|
||||
const patterns = [
|
||||
{ pattern: "*.env", negated: false, source: "project" as const },
|
||||
{ pattern: ".env.example", negated: true, source: "project" as const },
|
||||
]
|
||||
const rules = IgnoreMigrator.buildPermissionRules(patterns)
|
||||
|
||||
expect(rules["*.env"]).toBe("deny")
|
||||
expect(rules[".env.example"]).toBe("allow")
|
||||
})
|
||||
|
||||
test("handles multiple deny patterns", () => {
|
||||
const patterns = [
|
||||
{ pattern: "secrets/", negated: false, source: "project" as const },
|
||||
{ pattern: "*.env", negated: false, source: "project" as const },
|
||||
{ pattern: "*.key", negated: false, source: "project" as const },
|
||||
]
|
||||
const rules = IgnoreMigrator.buildPermissionRules(patterns)
|
||||
|
||||
expect(rules["secrets/*"]).toBe("deny")
|
||||
expect(rules["*.env"]).toBe("deny")
|
||||
expect(rules["*.key"]).toBe("deny")
|
||||
})
|
||||
|
||||
test("negated patterns override deny patterns", () => {
|
||||
const patterns = [
|
||||
{ pattern: "config/", negated: false, source: "project" as const },
|
||||
{ pattern: "config/public.json", negated: true, source: "project" as const },
|
||||
]
|
||||
const rules = IgnoreMigrator.buildPermissionRules(patterns)
|
||||
|
||||
expect(rules["config/*"]).toBe("deny")
|
||||
expect(rules["config/public.json"]).toBe("allow")
|
||||
})
|
||||
})
|
||||
|
||||
describe("migrate", () => {
|
||||
test("returns empty permission for project without .kilocodeignore", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const result = await IgnoreMigrator.migrate({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
|
||||
expect(result.patternCount).toBe(0)
|
||||
expect(Object.keys(result.permission)).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("loads project .kilocodeignore", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "secrets/\n*.env")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await IgnoreMigrator.migrate({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
|
||||
expect(result.patternCount).toBe(2)
|
||||
expect(result.permission.read).toBeDefined()
|
||||
expect(result.permission.edit).toBeDefined()
|
||||
})
|
||||
|
||||
test("applies patterns to both read and edit", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "secrets/")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await IgnoreMigrator.migrate({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
|
||||
const readRules = result.permission.read as Record<string, string>
|
||||
const editRules = result.permission.edit as Record<string, string>
|
||||
|
||||
expect(readRules["secrets/*"]).toBe("deny")
|
||||
expect(editRules["secrets/*"]).toBe("deny")
|
||||
})
|
||||
|
||||
test("handles negation patterns correctly", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "*.env\n!.env.example")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await IgnoreMigrator.migrate({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
|
||||
const readRules = result.permission.read as Record<string, string>
|
||||
|
||||
expect(readRules["*.env"]).toBe("deny")
|
||||
expect(readRules[".env.example"]).toBe("allow")
|
||||
})
|
||||
|
||||
test("handles complex .kilocodeignore file", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, ".kilocodeignore"),
|
||||
`# Secrets
|
||||
secrets/
|
||||
*.env
|
||||
!.env.example
|
||||
|
||||
# Keys
|
||||
*.key
|
||||
*.pem
|
||||
|
||||
# Config
|
||||
/config/private/
|
||||
!config/private/public.json
|
||||
`,
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
const result = await IgnoreMigrator.migrate({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
|
||||
expect(result.patternCount).toBe(7)
|
||||
|
||||
const readRules = result.permission.read as Record<string, string>
|
||||
expect(readRules["*"]).toBe("allow")
|
||||
expect(readRules["secrets/*"]).toBe("deny")
|
||||
expect(readRules["*.env"]).toBe("deny")
|
||||
expect(readRules[".env.example"]).toBe("allow")
|
||||
expect(readRules["*.key"]).toBe("deny")
|
||||
expect(readRules["*.pem"]).toBe("deny")
|
||||
expect(readRules["config/private/*"]).toBe("deny")
|
||||
expect(readRules["config/private/public.json"]).toBe("allow")
|
||||
})
|
||||
|
||||
test("returns empty warnings array", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "secrets/")
|
||||
},
|
||||
})
|
||||
|
||||
const result = await IgnoreMigrator.migrate({
|
||||
projectDir: tmp.path,
|
||||
skipGlobalPaths: true,
|
||||
})
|
||||
|
||||
expect(result.warnings).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("loadIgnoreConfig", () => {
|
||||
test("returns empty object for project without .kilocodeignore", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
const permission = await IgnoreMigrator.loadIgnoreConfig(tmp.path, true)
|
||||
|
||||
expect(Object.keys(permission)).toHaveLength(0)
|
||||
})
|
||||
|
||||
test("returns permission config for project with .kilocodeignore", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, ".kilocodeignore"), "secrets/")
|
||||
},
|
||||
})
|
||||
|
||||
const permission = await IgnoreMigrator.loadIgnoreConfig(tmp.path, true)
|
||||
|
||||
expect(permission.read).toBeDefined()
|
||||
expect(permission.edit).toBeDefined()
|
||||
})
|
||||
|
||||
test("handles errors gracefully", async () => {
|
||||
// Pass a non-existent directory
|
||||
const permission = await IgnoreMigrator.loadIgnoreConfig("/non/existent/path", true)
|
||||
|
||||
expect(Object.keys(permission)).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user