mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2026-07-03 12:27:41 +08:00
### What this PR does Before this PR: `.claude/skills/<name>/SKILL.md` files were **copied** from `.agents/skills/<name>/SKILL.md` via `pnpm skills:sync`. A dedicated `skills-check-windows` CI job ran on `windows-latest` to verify cross-platform file-copy compatibility. After this PR: `.claude/skills/<name>` entries are **directory symlinks** pointing to `../../.agents/skills/<name>`, following the Single Source of Truth (SSoT) principle. The Windows-specific CI job is removed; Windows developers are expected to enable symlink support. ### Why we need it and why it was done in this way The following tradeoffs were made: - Windows developers must now manually enable symlink support (Developer Mode + `git config --global core.symlinks true`). This is acceptable because: 1. The existing `AGENTS.md` is already a symlink, so Windows compatibility was never fully enforced. 2. Symlinks eliminate the need for file-copy synchronization, reducing maintenance complexity. 3. Contributors are expected to have sufficient technical capability to configure their environments. The following alternatives were considered: - Keeping file-copy sync: rejected because it duplicates content and requires extra CI to verify consistency. ### Breaking changes Windows developers who clone without symlink support enabled will get plain text files instead of symlinks. They must: 1. Enable Developer Mode or grant `SeCreateSymbolicLinkPrivilege` 2. Run `git config --global core.symlinks true` 3. Re-clone or run `pnpm skills:sync` ### Special notes for your reviewer - The `.github/workflows/ci.yml` diff includes minor quote-style changes (`'` → `"`) from the YAML formatter — these are cosmetic only. ### Checklist - [x] PR: The PR description is expressive enough and will help future contributors - [x] Code: [Write code that humans can understand](https://en.wikiquote.org/wiki/Martin_Fowler#code-for-humans) and [Keep it simple](https://en.wikipedia.org/wiki/KISS_principle) - [x] Refactor: You have [left the code cleaner than you found it (Boy Scout Rule)](https://learning.oreilly.com/library/view/97-things-every/9780596809515/ch08.html) - [x] Upgrade: Impact of this change on upgrade flows was considered and addressed if required - [x] Documentation: A [user-guide update](https://docs.cherry-ai.com) was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior. - [ ] Self-review: I have reviewed my own code (e.g., via [`/gh-pr-review`](/.claude/skills/gh-pr-review/SKILL.md), `gh pr diff`, or GitHub UI) before requesting review from others ### Release note ```release-note NONE ``` Signed-off-by: icarus <eurfelux@gmail.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
110 lines
2.9 KiB
TypeScript
110 lines
2.9 KiB
TypeScript
import * as fs from 'fs'
|
|
import * as path from 'path'
|
|
|
|
export const ROOT_DIR = path.join(__dirname, '..')
|
|
export const AGENTS_SKILLS_DIR = path.join(ROOT_DIR, '.agents', 'skills')
|
|
export const CLAUDE_SKILLS_DIR = path.join(ROOT_DIR, '.claude', 'skills')
|
|
export const AGENTS_SKILLS_GITIGNORE = path.join(AGENTS_SKILLS_DIR, '.gitignore')
|
|
export const CLAUDE_SKILLS_GITIGNORE = path.join(CLAUDE_SKILLS_DIR, '.gitignore')
|
|
export const PUBLIC_SKILLS_FILE = path.join(AGENTS_SKILLS_DIR, 'public-skills.txt')
|
|
|
|
const SKILL_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/
|
|
|
|
export function listSkillNames(): string[] {
|
|
const content = readFileSafe(PUBLIC_SKILLS_FILE)
|
|
if (content === null) {
|
|
throw new Error('.agents/skills/public-skills.txt is missing')
|
|
}
|
|
|
|
const names: string[] = []
|
|
const seen = new Set<string>()
|
|
const lines = content.split('\n')
|
|
|
|
for (const [index, rawLine] of lines.entries()) {
|
|
const trimmedLine = rawLine.trim()
|
|
if (trimmedLine === '' || trimmedLine.startsWith('#')) {
|
|
continue
|
|
}
|
|
|
|
if (trimmedLine.includes('#')) {
|
|
throw new Error(
|
|
`inline comments are not allowed at .agents/skills/public-skills.txt:${index + 1}; ` +
|
|
'put comments on the previous line'
|
|
)
|
|
}
|
|
|
|
const name = trimmedLine
|
|
|
|
if (!SKILL_NAME_PATTERN.test(name)) {
|
|
throw new Error(`invalid skill name '${name}' at .agents/skills/public-skills.txt:${index + 1}`)
|
|
}
|
|
|
|
if (seen.has(name)) {
|
|
throw new Error(`duplicate skill name '${name}' at .agents/skills/public-skills.txt:${index + 1}`)
|
|
}
|
|
|
|
seen.add(name)
|
|
names.push(name)
|
|
}
|
|
|
|
return names.sort((a, b) => a.localeCompare(b))
|
|
}
|
|
|
|
export function buildAgentsSkillsGitignore(skillNames: string[]): string {
|
|
const lines = [
|
|
'# AUTO-GENERATED by `pnpm skills:sync`.',
|
|
'# Do not edit manually.',
|
|
'*',
|
|
'!.gitignore',
|
|
'!README*.md',
|
|
'!public-skills.txt'
|
|
]
|
|
|
|
for (const skillName of skillNames) {
|
|
lines.push(`!${skillName}/`)
|
|
lines.push(`!${skillName}/**`)
|
|
}
|
|
|
|
return `${lines.join('\n')}\n`
|
|
}
|
|
|
|
export function buildClaudeSkillsGitignore(skillNames: string[]): string {
|
|
const lines = [
|
|
'# AUTO-GENERATED by `pnpm skills:sync`.',
|
|
'# Do not edit manually.',
|
|
'*',
|
|
'!.gitignore',
|
|
'!README*.md'
|
|
]
|
|
|
|
for (const skillName of skillNames) {
|
|
lines.push(`!${skillName}`)
|
|
}
|
|
|
|
return `${lines.join('\n')}\n`
|
|
}
|
|
|
|
export function writeFileIfChanged(filePath: string, content: string): boolean {
|
|
let current = ''
|
|
try {
|
|
current = fs.readFileSync(filePath, 'utf-8')
|
|
} catch (error) {
|
|
const nodeError = error as NodeJS.ErrnoException
|
|
if (nodeError.code !== 'ENOENT') {
|
|
throw error
|
|
}
|
|
}
|
|
if (current === content) {
|
|
return false
|
|
}
|
|
fs.writeFileSync(filePath, content, 'utf-8')
|
|
return true
|
|
}
|
|
|
|
export function readFileSafe(filePath: string): string | null {
|
|
if (!fs.existsSync(filePath)) {
|
|
return null
|
|
}
|
|
return fs.readFileSync(filePath, 'utf-8')
|
|
}
|