Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 45 additions & 4 deletions Sources/SkillDeck/Models/AgentType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ enum AgentType: String, CaseIterable, Identifiable, Codable {
case qoder = "qoder" // Qoder: AI coding agent (https://qoder.ai)
case qclaw = "qclaw" // QClaw: AI coding assistant (skills in ~/.qclaw/skills)
case workbuddy = "workbuddy" // WorkBuddy: AI coding assistant (skills in ~/.workbuddy/skills)
case joyCode = "joycode"
case reasonix = "reasonix"
case qwen = "qwen"

// Identifiable protocol requirement (similar to Java's Comparable), needed for SwiftUI list rendering
var id: String { rawValue }
Expand All @@ -37,6 +40,9 @@ enum AgentType: String, CaseIterable, Identifiable, Codable {
case .qoder: "Qoder"
case .qclaw: "QClaw"
case .workbuddy: "WorkBuddy"
case .joyCode: "JoyCode"
case .reasonix: "Reasonix"
case .qwen: "Qwen"
}
}

Expand All @@ -58,6 +64,7 @@ enum AgentType: String, CaseIterable, Identifiable, Codable {
case .qoder: "orange"
case .qclaw: "mint"
case .workbuddy: "yellow"
case .joyCode, .reasonix, .qwen: "gray"
}
}

Expand All @@ -79,6 +86,7 @@ enum AgentType: String, CaseIterable, Identifiable, Codable {
case .qoder: "q.circle" // Letter Q icon for Qoder
case .qclaw: "hand.raised.circle" // Hand/claw icon for QClaw
case .workbuddy: "w.circle" // Letter W icon for WorkBuddy
case .joyCode, .reasonix, .qwen: "terminal" // Default icon until a dedicated symbol is available
}
}

Expand Down Expand Up @@ -119,6 +127,12 @@ enum AgentType: String, CaseIterable, Identifiable, Codable {
return "~/.qclaw/skills" // QClaw AI assistant skills directory
case .workbuddy:
return "~/.workbuddy/skills" // WorkBuddy AI assistant skills directory
case .joyCode:
return "~/.joycode/skills"
case .reasonix:
return "~/.reasonix/skills"
case .qwen:
return "~/.qwen/skills"
}
}

Expand All @@ -145,6 +159,9 @@ enum AgentType: String, CaseIterable, Identifiable, Codable {
case .qoder: "~/.qoder"
case .qclaw: "~/.qclaw"
case .workbuddy: "~/.workbuddy"
case .joyCode: "~/.joycode"
case .reasonix: "~/.reasonix"
case .qwen: "~/.qwen"
}
}

Expand All @@ -165,16 +182,40 @@ enum AgentType: String, CaseIterable, Identifiable, Codable {
case .qoder: "qoder"
case .qclaw: "qclaw"
case .workbuddy: "workbuddy"
case .joyCode: "joycode"
case .reasonix: "reasonix"
case .qwen: "qwen"
}
}

/// Shared canonical skills directory URL (~/.agents/skills/)
/// Used by SkillScanner and agents that read from the shared directory (e.g., OpenCode).
/// Defined here as a single source of truth to avoid duplicating the path string.
static let sharedSkillsDirectoryURL: URL = {
let path = NSString(string: "~/.agents/skills").expandingTildeInPath
return URL(fileURLWithPath: path)
}()
static var sharedSkillsDirectoryURL: URL { SkillStorageSettings.globalSkillsURL }

/// Project-local paths mirror each tool's user-level layout without inheriting user-only overrides.
/// For example, a Docker-specific OpenClaw path must not be written into an unrelated project.
var projectSkillsRelativePath: String {
switch self {
case .claudeCode: ".claude/skills"
case .codex: ".codex/skills"
case .geminiCLI: ".gemini/skills"
case .copilotCLI: ".copilot/skills"
case .openCode: ".config/opencode/skills"
case .antigravity: ".gemini/antigravity/skills"
case .cursor: ".cursor/skills"
case .kiro: ".kiro/skills"
case .codeBuddy: ".codebuddy/skills"
case .openClaw: ".openclaw/skills"
case .trae: ".trae/skills"
case .qoder: ".qoder/skills"
case .qclaw: ".qclaw/skills"
case .workbuddy: ".workbuddy/skills"
case .joyCode: ".joycode/skills"
case .reasonix: ".reasonix/skills"
case .qwen: ".qwen/skills"
}
}

/// Skills directories of other Agents that this Agent can read in addition to its own skills directory
///
Expand Down
120 changes: 120 additions & 0 deletions Sources/SkillDeck/Resources/Localizations/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,123 @@

"dashboard.language.menuLabel" = "Language";
"dashboard.language.menuHelp" = "Switch app language";

"common.cancel" = "Cancel";
"common.close" = "Close";
"common.done" = "Done";
"common.ok" = "OK";
"common.sourcePath" = "Source: %@";
"common.showInFinder" = "Show in Finder";
"sidebar.globalSync" = "Global Sync";
"sidebar.projects" = "Projects";
"content.preparingGlobalSync" = "Preparing global sync manager";
"content.preparingProjects" = "Preparing project manager";
"projects.title" = "Projects";
"projects.empty.title" = "No Projects Added";
"projects.empty.description" = "After you add a project root, SkillDeck scans only its direct .agents/skills directory.";
"projects.add" = "Add Project";
"projects.rescan" = "Rescan Projects";
"projects.chooseRoot" = "Choose Project Root";
"projects.remove" = "Remove Project";
"projects.skillCount" = "%d skills";
"projects.syncedToolsCount" = "%d/%d tools fully synced";
"projects.select.title" = "Select a Project";
"projects.select.description" = "Select a project in the middle list to view its skill sync status.";
"projects.rules.title" = "Shared Rules";
"projects.rules.syncedCount" = "%d/%d synced";
"projects.rules.edit" = "Edit shared rules";
"projects.rules.syncAll" = "Sync All Rules";
"projects.rules.remove" = "Remove Rule Links";
"projects.rules.create" = "Create Shared Rules";
"projects.skills.missing.title" = "Project Source Skills Not Found";
"projects.skills.missing.description" = "Add skills containing SKILL.md under the project's .agents/skills, then sync the whole directory.";
"projects.skills.empty.title" = "No Project Skills";
"projects.skills.empty.description" = "Add skills containing SKILL.md under the project's .agents/skills.";
"projects.skillDetail.project" = "Project skill. Click to view and update.";
"projects.skillDetail.global" = "Global source skill. Click to view and update.";
"projects.skillDetail.help" = "View and update project skill";
"projects.sync.allTools" = "Sync All Tools";
"projects.sync.directory" = "Sync Entire Skills Directory";
"projects.sync.removeDirectory" = "Remove Directory Sync Link";
"projects.sync.sourceSkillCount" = "%d source skills";
"projects.change.title" = "Project Sync Changes";
"projects.change.destination" = "Each target skills directory will point to: %@";
"projects.change.containsSkills" = "Contains %d source skills";
"projects.sync.failed" = "Sync Failed";
"projects.sync.result" = "Sync Result";
"projects.rules.change.title" = "Shared Rule Changes";
"projects.rules.change.description" = "All target rules will reference the same AGENTS.md; Cursor uses a recognized .mdc wrapper rule.";
"projects.rules.failed" = "Rule Sync Failed";
"projects.rules.result" = "Rule Sync Result";
"globalSync.rescan" = "Rescan Global Skills";
"projectDetail.update" = "Project Updates";
"projectDetail.source" = "Source";
"projectDetail.repository" = "Repository";
"projectDetail.updatedAt" = "Updated";
"projectDetail.updateFound" = "Update Available";
"projectDetail.updateNow" = "Update";
"projectDetail.checkUpdate" = "Check for Updates";
"projectDetail.linkRepository" = "Link Repository";
"projectDetail.readFailed" = "Unable to Read SKILL.md";
"projectDetail.openTerminal" = "Open in Terminal";
"projectDetail.openEditor" = "Open SKILL.md in Default Editor";
"dashboard.scope" = "Skill Scope";
"dashboard.project" = "Project";
"dashboard.allProjects" = "All Projects";
"dashboard.disabled.title" = "Disabled Skills";
"dashboard.disabled.count" = "Disabled Skills (%d)";
"dashboard.disabled.help" = "View and restore disabled skills";
"dashboard.disable" = "Disable";
"dashboard.disable.description" = "\"%@\" will be moved to ~/.agents/.skilldeck-disabled, not deleted. You can restore it from Disabled Skills.";
"dashboard.disabled.empty" = "No Disabled Skills";
"dashboard.restore" = "Restore";
"dashboard.scope.all" = "All";
"dashboard.scope.global" = "Global";
"dashboard.scope.project" = "Project";

"sync.state.linked" = "Synced";
"sync.state.directoryLinked" = "Entire Directory Synced";
"sync.state.missing" = "Not Synced";
"sync.state.broken" = "Broken Symlink";
"sync.state.foreignLink" = "Points Elsewhere";
"sync.state.occupied" = "Existing File";
"sync.state.rootConflict" = "Target Directory Conflict";
"sync.change.createDirectory" = "Create Directory Symlink";
"sync.change.replaceDirectory" = "Back Up and Replace Directory";
"sync.change.removeDirectory" = "Remove Directory Symlink";
"sync.summary.replaceDirectory" = "The target skills directory already has content. It will be backed up, then replaced with a link to the project's .agents/skills.";
"sync.summary.createDirectory" = "Create a symlink for the entire skills directory to the project's .agents/skills.";
"sync.summary.removeDirectory" = "Remove only the target directory symlink; project source skills are not deleted.";
"sync.result.keepDirectory" = "Keep target skills directory";
"sync.result.backup" = "Backed up to %@";
"sync.result.applied" = "Applied: %@";
"rule.target.claudeDirectory" = "Claude Rules Directory";
"rule.target.cursor" = "Cursor Project Rules";
"rule.target.codex" = "Codex / Generic Agent";
"rule.change.create" = "Create Rule Symlink";
"rule.change.replace" = "Back Up and Replace Rule";
"rule.change.remove" = "Remove Rule Symlink";
"rule.summary.replace" = "An existing rule file or external symlink will be backed up and replaced with the shared rule.";
"rule.summary.cursorCreate" = "Create a Cursor rule file that references the shared AGENTS.md.";
"rule.summary.create" = "Create a rule symlink to the shared AGENTS.md.";
"rule.summary.remove" = "Remove only the SkillDeck-created rule symlink; the shared rule source is not deleted.";
"rule.result.keep" = "Keep existing rules for %@";
"rule.result.backup" = "%@ backed up to %@";
"rule.result.applied" = "Applied %@: %@";
"sync.action.backupAll" = "Back Up and Sync %d Tools";
"sync.action.all" = "Sync %d Tools";
"sync.action.backupReplace" = "Back Up and Replace Directory";
"sync.action.removeLink" = "Remove Directory Symlink";
"sync.action.createLink" = "Create Directory Symlink";
"rule.action.backup" = "Back Up and Sync %d Rules";
"rule.action.remove" = "Remove %d Rule Links";
"rule.action.sync" = "Sync %d Rules";
"projectDetail.status.linkedAndSynced" = "Repository linked and current project skill synced";
"projectDetail.status.updateAvailable" = "Update available";
"projectDetail.status.upToDate" = "Already up to date";
"projectDetail.status.updated" = "Project skill updated to the latest version";
"projectDetail.error.linkRequired" = "Link a GitHub repository first";
"projectDetail.error.notFound" = "Skill not found in repository: %@";
"dashboard.error.disabledReadFailed" = "Failed to read disabled skills: %@";
"dashboard.error.disableFailed" = "Failed to disable skill: %@";
"dashboard.error.restoreConflict" = "Unable to restore; a skill already exists at the original location: %@";
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,123 @@

"dashboard.language.menuLabel" = "语言";
"dashboard.language.menuHelp" = "切换应用语言";

"common.cancel" = "取消";
"common.close" = "关闭";
"common.done" = "完成";
"common.ok" = "好";
"common.sourcePath" = "源:%@";
"common.showInFinder" = "在 Finder 中显示";
"sidebar.globalSync" = "全局同步";
"sidebar.projects" = "项目";
"content.preparingGlobalSync" = "正在准备全局同步管理器";
"content.preparingProjects" = "正在准备项目管理器";
"projects.title" = "项目";
"projects.empty.title" = "尚未添加项目";
"projects.empty.description" = "添加一个项目根目录后,SkillDeck 只会扫描其直接下的 .agents/skills。";
"projects.add" = "添加项目";
"projects.rescan" = "重新扫描项目";
"projects.chooseRoot" = "选择项目根目录";
"projects.remove" = "移除项目";
"projects.skillCount" = "%d 个技能";
"projects.syncedToolsCount" = "%d/%d 个工具已完全同步";
"projects.select.title" = "选择项目";
"projects.select.description" = "在中间列表中选择一个项目,查看它的技能同步状态。";
"projects.rules.title" = "统一规则";
"projects.rules.syncedCount" = "%d/%d 已同步";
"projects.rules.edit" = "编辑统一规则";
"projects.rules.syncAll" = "同步全部规则";
"projects.rules.remove" = "移除规则链接";
"projects.rules.create" = "新建统一规则";
"projects.skills.missing.title" = "未检测到项目源技能";
"projects.skills.missing.description" = "在项目 .agents/skills 下添加包含 SKILL.md 的技能后即可整体同步。";
"projects.skills.empty.title" = "没有项目技能";
"projects.skills.empty.description" = "在项目 .agents/skills 下添加包含 SKILL.md 的技能。";
"projects.skillDetail.project" = "项目技能,点击查看和更新";
"projects.skillDetail.global" = "全局源技能,点击查看和更新";
"projects.skillDetail.help" = "查看和更新项目技能";
"projects.sync.allTools" = "同步全部工具";
"projects.sync.directory" = "同步整个 skills 目录";
"projects.sync.removeDirectory" = "移除同步目录链接";
"projects.sync.sourceSkillCount" = "源技能 %d 项";
"projects.change.title" = "项目同步变更";
"projects.change.destination" = "同步会将每个目标 skills 目录指向:%@";
"projects.change.containsSkills" = "包含 %d 个源技能";
"projects.sync.failed" = "同步失败";
"projects.sync.result" = "同步结果";
"projects.rules.change.title" = "统一规则变更";
"projects.rules.change.description" = "所有目标规则将引用同一份 AGENTS.md;Cursor 使用可识别的 .mdc 包装规则。";
"projects.rules.failed" = "规则同步失败";
"projects.rules.result" = "规则同步结果";
"globalSync.rescan" = "重新扫描全局技能";
"projectDetail.update" = "项目更新";
"projectDetail.source" = "来源";
"projectDetail.repository" = "仓库";
"projectDetail.updatedAt" = "更新于";
"projectDetail.updateFound" = "发现更新";
"projectDetail.updateNow" = "更新";
"projectDetail.checkUpdate" = "检查更新";
"projectDetail.linkRepository" = "关联仓库";
"projectDetail.readFailed" = "无法读取 SKILL.md";
"projectDetail.openTerminal" = "在终端中打开";
"projectDetail.openEditor" = "用默认编辑器打开 SKILL.md";
"dashboard.scope" = "技能范围";
"dashboard.project" = "项目";
"dashboard.allProjects" = "全部项目";
"dashboard.disabled.title" = "已禁用技能";
"dashboard.disabled.count" = "已禁用技能(%d)";
"dashboard.disabled.help" = "查看并恢复已禁用的技能";
"dashboard.disable" = "禁用";
"dashboard.disable.description" = "\"%@\" 会移至 ~/.agents/.skilldeck-disabled,不会删除;可从“已禁用技能”恢复。";
"dashboard.disabled.empty" = "没有已禁用技能";
"dashboard.restore" = "恢复";
"dashboard.scope.all" = "全部";
"dashboard.scope.global" = "全局";
"dashboard.scope.project" = "项目";

"sync.state.linked" = "已同步";
"sync.state.directoryLinked" = "整目录已同步";
"sync.state.missing" = "未同步";
"sync.state.broken" = "失效软链";
"sync.state.foreignLink" = "指向其他位置";
"sync.state.occupied" = "已有真实文件";
"sync.state.rootConflict" = "目标目录冲突";
"sync.change.createDirectory" = "创建目录软链";
"sync.change.replaceDirectory" = "备份后整体替换";
"sync.change.removeDirectory" = "移除目录软链";
"sync.summary.replaceDirectory" = "目标 skills 目录已有内容。备份后将整个目录改为直接指向项目 .agents/skills。";
"sync.summary.createDirectory" = "创建整个 skills 目录软链,直接指向项目 .agents/skills。";
"sync.summary.removeDirectory" = "只移除目标目录软链,不删除项目源技能。";
"sync.result.keepDirectory" = "保留目标 skills 目录";
"sync.result.backup" = "已备份到 %@";
"sync.result.applied" = "已执行 %@";
"rule.target.claudeDirectory" = "Claude 规则目录";
"rule.target.cursor" = "Cursor 项目规则";
"rule.target.codex" = "Codex / 通用 Agent";
"rule.change.create" = "创建规则软链";
"rule.change.replace" = "备份后替换规则";
"rule.change.remove" = "移除规则软链";
"rule.summary.replace" = "已有规则文件或外部软链,备份后替换为统一规则。";
"rule.summary.cursorCreate" = "创建 Cursor 规则文件,并引用统一的 AGENTS.md。";
"rule.summary.create" = "创建规则软链,指向统一的 AGENTS.md。";
"rule.summary.remove" = "只移除由 SkillDeck 创建的规则软链,不删除统一规则源。";
"rule.result.keep" = "保留 %@ 的现有规则";
"rule.result.backup" = "%@ 已备份到 %@";
"rule.result.applied" = "已执行 %@:%@";
"sync.action.backupAll" = "备份后同步 %d 个工具";
"sync.action.all" = "同步 %d 个工具";
"sync.action.backupReplace" = "备份后整体替换";
"sync.action.removeLink" = "移除目录软链";
"sync.action.createLink" = "创建目录软链";
"rule.action.backup" = "备份后同步 %d 条规则";
"rule.action.remove" = "移除 %d 条规则链接";
"rule.action.sync" = "同步 %d 条规则";
"projectDetail.status.linkedAndSynced" = "已关联仓库,并已同步当前项目技能";
"projectDetail.status.updateAvailable" = "发现可用更新";
"projectDetail.status.upToDate" = "当前已是最新版本";
"projectDetail.status.updated" = "项目技能已更新到最新版本";
"projectDetail.error.linkRequired" = "请先关联 GitHub 仓库";
"projectDetail.error.notFound" = "仓库中未找到技能:%@";
"dashboard.error.disabledReadFailed" = "读取已禁用技能失败:%@";
"dashboard.error.disableFailed" = "禁用技能失败:%@";
"dashboard.error.restoreConflict" = "无法恢复,原位置已有技能:%@";
32 changes: 32 additions & 0 deletions Sources/SkillDeck/Services/AgentPathSettings.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,37 @@
import Foundation

/// SkillStorageSettings stores the canonical global skill directory selected by the user.
/// UserDefaults is suitable here because this is a small application preference, comparable to
/// a Java Preferences value rather than application data that needs a separate database.
enum SkillStorageSettings {
private static let globalSkillsPathKey = "globalSkillsPath"

static var globalSkillsPath: String {
let configuredPath = UserDefaults.standard.string(forKey: globalSkillsPathKey)
return configuredPath?.isEmpty == false ? configuredPath! : Constants.sharedSkillsPath
}

static var globalSkillsURL: URL {
URL(fileURLWithPath: NSString(string: globalSkillsPath).expandingTildeInPath)
}

static var lockFileURL: URL {
globalSkillsURL.deletingLastPathComponent().appendingPathComponent(".skill-lock.json")
}

static var cacheFileURL: URL {
globalSkillsURL.deletingLastPathComponent().appendingPathComponent(".skilldeck-cache.json")
}

static func setGlobalSkillsPath(_ path: String?) {
if let path, !path.isEmpty {
UserDefaults.standard.set(path, forKey: globalSkillsPathKey)
} else {
UserDefaults.standard.removeObject(forKey: globalSkillsPathKey)
}
}
}

/// AgentPathSettings manages custom directory paths for Agents
///
/// This allows users to configure non-default paths for Agent skills directories,
Expand Down
Loading