118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
import { afterEach, describe, expect, test } from "bun:test"
|
|
import { promises as fs } from "fs"
|
|
import os from "os"
|
|
import path from "path"
|
|
|
|
const tempDirs: string[] = []
|
|
|
|
async function makeTempDir(prefix: string): Promise<string> {
|
|
const dir = await fs.mkdtemp(path.join(os.tmpdir(), prefix))
|
|
tempDirs.push(dir)
|
|
return dir
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
tempDirs.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })),
|
|
)
|
|
})
|
|
|
|
const installScript = path.join(
|
|
import.meta.dir,
|
|
"..",
|
|
".cline",
|
|
"scripts",
|
|
"install-skills.sh",
|
|
)
|
|
|
|
const manualSkill = "lfg"
|
|
const invocableSkill = "ce-plan"
|
|
|
|
type RunResult = {
|
|
exitCode: number
|
|
stdout: string
|
|
stderr: string
|
|
}
|
|
|
|
async function runInstall(
|
|
env: Record<string, string | undefined>,
|
|
args: string[] = ["--global"],
|
|
): Promise<RunResult> {
|
|
const proc = Bun.spawn(["bash", installScript, ...args], {
|
|
cwd: path.join(import.meta.dir, ".."),
|
|
env: { ...process.env, ...env },
|
|
stderr: "pipe",
|
|
stdout: "pipe",
|
|
})
|
|
|
|
const [exitCode, stdout, stderr] = await Promise.all([
|
|
proc.exited,
|
|
new Response(proc.stdout).text(),
|
|
new Response(proc.stderr).text(),
|
|
])
|
|
|
|
return { exitCode, stdout, stderr }
|
|
}
|
|
|
|
async function pathExists(filePath: string): Promise<boolean> {
|
|
try {
|
|
await fs.access(filePath)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
describe("cline install-skills.sh", () => {
|
|
test("does not remove unrelated manual-only skill symlinks", async () => {
|
|
const dest = await makeTempDir("cline-skills-dest-")
|
|
const userSkill = await makeTempDir("cline-user-lfg-")
|
|
await fs.writeFile(path.join(userSkill, "SKILL.md"), "# user lfg\n")
|
|
|
|
await fs.symlink(userSkill, path.join(dest, manualSkill))
|
|
|
|
const result = await runInstall({
|
|
CLINE_SKILLS_DIR: dest,
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(await pathExists(path.join(dest, manualSkill))).toBe(true)
|
|
expect(result.stderr).not.toContain("removed lfg")
|
|
})
|
|
|
|
test("does not overwrite an existing user-managed symlink for an invocable skill", async () => {
|
|
const dest = await makeTempDir("cline-skills-user-invocable-")
|
|
const userSkill = await makeTempDir("cline-user-ce-plan-")
|
|
await fs.writeFile(path.join(userSkill, "SKILL.md"), "# user ce-plan\n")
|
|
|
|
const link = path.join(dest, invocableSkill)
|
|
await fs.symlink(userSkill, link)
|
|
|
|
const result = await runInstall({
|
|
CLINE_SKILLS_DIR: dest,
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(await fs.realpath(link)).toBe(await fs.realpath(userSkill))
|
|
expect(result.stderr).toContain(
|
|
`skip ${invocableSkill}: ${link} is an existing user-managed symlink (not overwritten)`,
|
|
)
|
|
})
|
|
|
|
test("removes stale CE-owned manual-only symlinks on default install", async () => {
|
|
const dest = await makeTempDir("cline-skills-ce-")
|
|
const repoRoot = path.join(import.meta.dir, "..")
|
|
const ceManualSkill = path.join(repoRoot, "skills", manualSkill)
|
|
|
|
await fs.symlink(ceManualSkill, path.join(dest, manualSkill))
|
|
|
|
const result = await runInstall({
|
|
CLINE_SKILLS_DIR: dest,
|
|
})
|
|
|
|
expect(result.exitCode).toBe(0)
|
|
expect(await pathExists(path.join(dest, manualSkill))).toBe(false)
|
|
expect(result.stderr).toContain("removed lfg: stale CE manual-only symlink")
|
|
})
|
|
})
|