import type { Plugin } from "@opencode-ai/plugin"
import * as fs from "fs"
import * as path from "path"
import crypto from "crypto"
const logFile = path.join(__dirname, "install_error.log")
function log(message: string) {
const timestamp = new Date().toISOString()
fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`)
}
function getFileHash(filePath: string): string | null {
if (!fs.existsSync(filePath)) return null
try {
const content = fs.readFileSync(filePath, 'utf-8')
return crypto.createHash('md5').update(content).digest('hex')
} catch (error) {
return null
}
}
interface SkillState {
name: string
exists: boolean
hash: string | null
}
function findGitRoot(startDir: string): string {
let dir = startDir
while (dir !== path.dirname(dir)) {
if (fs.existsSync(path.join(dir, ".git"))) {
return dir
}
dir = path.dirname(dir)
}
return startDir
}
export const InstallSkillsPlugin: Plugin = async ({ $, directory }) => {
const rootDir = findGitRoot(directory)
const installSkills = async () => {
try {
const skillsToCheck = [
{ name: 'gitcode-pr', path: path.join(rootDir, ".claude", "skills", "gitcode-pr", "SKILL.md") },
{ name: 'gitcode-issue', path: path.join(rootDir, ".claude", "skills", "gitcode-issue", "SKILL.md") },
{ name: 'api-doc-generator', path: path.join(rootDir, ".claude", "skills", "api-doc-generator", "SKILL.md") },
{ name: 'gitcode-pipeline', path: path.join(rootDir, ".claude", "skills", "gitcode-pipeline", "SKILL.md") }
]
const beforeStates: SkillState[] = skillsToCheck.map(skill => ({
name: skill.name,
exists: fs.existsSync(skill.path),
hash: getFileHash(skill.path)
}))
const hasBash = (() => {
try {
require('child_process').execSync('bash --version', { stdio: 'ignore' })
return true
} catch {
return false
}
})()
if (!hasBash) {
process.stdout.write(`💡 提示:当前环境缺少 bash,请输入指令"安装默认skill"手动安装\n\n`)
return
}
const scriptPath = path.join(rootDir, ".claude", "skills", "default-skills", "scripts", "install-default-skills.sh")
await $`bash ${scriptPath} > /dev/null`
const afterStates: SkillState[] = skillsToCheck.map(skill => ({
name: skill.name,
exists: fs.existsSync(skill.path),
hash: getFileHash(skill.path)
}))
let hasChanges = false
const changedSkills: string[] = []
for (let i = 0; i < beforeStates.length; i++) {
const before = beforeStates[i]
const after = afterStates[i]
if (!before.exists && after.exists) {
hasChanges = true
changedSkills.push(`${before.name} 新安装`)
} else if (before.exists && after.exists && before.hash !== after.hash) {
hasChanges = true
changedSkills.push(`${before.name} 已更新`)
}
}
if (hasChanges && changedSkills.length > 0) {
setTimeout(() => {
process.stdout.write(`💡 ${changedSkills.join(', ')},重启opencode才能完全生效\n\n`)
}, 1000)
}
} catch (error) {
log(`Command failed: ${error.message}`)
if (error.stderr) log(`stderr from error: ${error.stderr}`)
const errorMarkerPath = path.join(rootDir, ".opencode_skills_error")
let detail = ""
if (error.message && error.message.includes("timed out")) {
detail = `网络连接超时,无法访问远程仓库。请检查网络连接后重试。\n${error.message}`
} else {
detail = error.stderr ? `${error.message}\n${error.stderr}` : error.message
}
const errorMessage = `❌ 安装默认技能时出错了,请输入指令"安装默认skill"重新安装\n错误详情: ${detail}\n`
fs.writeFileSync(errorMarkerPath, errorMessage)
setTimeout(() => {
process.stdout.write(`❌ 安装默认技能时出错了,请输入指令“安装默认skill”重新安装\n`)
process.stdout.write(` 错误详情: ${detail}\n`)
process.stdout.write(` 错误详情请查看: ${errorMarkerPath}\n\n`)
}, 2000)
}
};
installSkills();
return {
event: async ({ event }) => {}
}
}