fix: sanitize colons in skill/agent names for Windows path compatibility (#398)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { sanitizePathName } from "../utils/files"
|
||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
||||
import type {
|
||||
CopilotAgent,
|
||||
@@ -21,9 +22,9 @@ export function convertClaudeToCopilot(
|
||||
|
||||
const agents = plugin.agents.map((agent) => convertAgent(agent, usedAgentNames))
|
||||
|
||||
// Reserve skill names first so generated skills (from commands) don't collide
|
||||
// Reserve sanitized skill names so generated skills (from commands) don't collide on disk
|
||||
const skillDirs = plugin.skills.map((skill) => {
|
||||
usedSkillNames.add(skill.name)
|
||||
usedSkillNames.add(sanitizePathName(skill.name))
|
||||
return {
|
||||
name: skill.name,
|
||||
sourceDir: skill.sourceDir,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { sanitizePathName } from "../utils/files"
|
||||
import type {
|
||||
ClaudeAgent,
|
||||
ClaudeCommand,
|
||||
@@ -33,9 +34,9 @@ export function convertClaudeToOpenClaw(
|
||||
}))
|
||||
|
||||
const allSkillDirs = [
|
||||
...agentSkills.map((s) => s.dir),
|
||||
...commandSkills.map((s) => s.dir),
|
||||
...plugin.skills.map((s) => s.name),
|
||||
...agentSkills.map((s) => sanitizePathName(s.dir)),
|
||||
...commandSkills.map((s) => sanitizePathName(s.dir)),
|
||||
...plugin.skills.map((s) => sanitizePathName(s.name)),
|
||||
]
|
||||
|
||||
const manifest = buildManifest(plugin, allSkillDirs)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { sanitizePathName } from "../utils/files"
|
||||
import { findServersWithPotentialSecrets } from "../utils/secrets"
|
||||
import type { ClaudeAgent, ClaudeCommand, ClaudeMcpServer, ClaudePlugin } from "../types/claude"
|
||||
import type { WindsurfBundle, WindsurfGeneratedSkill, WindsurfMcpConfig, WindsurfMcpServerEntry, WindsurfWorkflow } from "../types/windsurf"
|
||||
@@ -20,8 +21,9 @@ export function convertClaudeToWindsurf(
|
||||
sourceDir: skill.sourceDir,
|
||||
}))
|
||||
|
||||
// Convert agents to skills (seed usedNames with pass-through skill names)
|
||||
const usedSkillNames = new Set<string>(skillDirs.map((s) => s.name))
|
||||
// Convert agents to skills (seed usedNames with sanitized pass-through skill names
|
||||
// so generated agent skills detect collisions that would occur on disk)
|
||||
const usedSkillNames = new Set<string>(skillDirs.map((s) => sanitizePathName(s.name)))
|
||||
const agentSkills = plugin.agents.map((agent) =>
|
||||
convertAgentToSkill(agent, knownAgentNames, usedSkillNames),
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import path from "path"
|
||||
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
||||
import type { ClaudePlugin } from "../types/claude"
|
||||
import { backupFile, writeText } from "../utils/files"
|
||||
import { backupFile, resolveCommandPath, sanitizePathName, writeText } from "../utils/files"
|
||||
import { convertClaudeToCodex } from "../converters/claude-to-codex"
|
||||
import { convertClaudeToCopilot } from "../converters/claude-to-copilot"
|
||||
import { convertClaudeToDroid } from "../converters/claude-to-droid"
|
||||
@@ -57,7 +57,7 @@ export async function syncOpenCodeCommands(
|
||||
const bundle = convertClaudeToOpenCode(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
|
||||
for (const commandFile of bundle.commandFiles) {
|
||||
const commandPath = path.join(outputRoot, "commands", `${commandFile.name}.md`)
|
||||
const commandPath = await resolveCommandPath(path.join(outputRoot, "commands"), commandFile.name, ".md")
|
||||
const backupPath = await backupFile(commandPath)
|
||||
if (backupPath) {
|
||||
console.log(`Backed up existing command file to ${backupPath}`)
|
||||
@@ -78,7 +78,7 @@ export async function syncCodexCommands(
|
||||
await writeText(path.join(outputRoot, "prompts", `${prompt.name}.md`), prompt.content + "\n")
|
||||
}
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
|
||||
await writeText(path.join(outputRoot, "skills", sanitizePathName(skill.name), "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export async function syncCopilotCommands(
|
||||
const bundle = convertClaudeToCopilot(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
|
||||
await writeText(path.join(outputRoot, "skills", sanitizePathName(skill.name), "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,7 +147,7 @@ export async function syncKiroCommands(
|
||||
const plugin = buildClaudeHomePlugin(config)
|
||||
const bundle = convertClaudeToKiro(plugin, DEFAULT_SYNC_OPTIONS)
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(outputRoot, "skills", skill.name, "SKILL.md"), skill.content + "\n")
|
||||
await writeText(path.join(outputRoot, "skills", sanitizePathName(skill.name), "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import type { ClaudeHomeConfig } from "../parsers/claude-home"
|
||||
import type { ClaudeMcpServer } from "../types/claude"
|
||||
import { sanitizePathName } from "../utils/files"
|
||||
import { syncGeminiCommands } from "./commands"
|
||||
import { mergeJsonConfigAtKey } from "./json-config"
|
||||
import { syncSkills } from "./skills"
|
||||
@@ -85,7 +86,7 @@ async function removeGeminiMirrorConflicts(
|
||||
sharedSkillsDir: string,
|
||||
): Promise<void> {
|
||||
for (const skill of skills) {
|
||||
const duplicatePath = path.join(skillsDir, skill.name)
|
||||
const duplicatePath = path.join(skillsDir, sanitizePathName(skill.name))
|
||||
|
||||
let stat
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "path"
|
||||
import type { ClaudeSkill } from "../types/claude"
|
||||
import { ensureDir } from "../utils/files"
|
||||
import { ensureDir, sanitizePathName } from "../utils/files"
|
||||
import { forceSymlink, isValidSkillName } from "../utils/symlink"
|
||||
|
||||
export async function syncSkills(
|
||||
@@ -9,13 +9,21 @@ export async function syncSkills(
|
||||
): Promise<void> {
|
||||
await ensureDir(skillsDir)
|
||||
|
||||
const seen = new Set<string>()
|
||||
for (const skill of skills) {
|
||||
if (!isValidSkillName(skill.name)) {
|
||||
console.warn(`Skipping skill with invalid name: ${skill.name}`)
|
||||
continue
|
||||
}
|
||||
|
||||
const target = path.join(skillsDir, skill.name)
|
||||
const safeName = sanitizePathName(skill.name)
|
||||
if (seen.has(safeName)) {
|
||||
console.warn(`Skipping skill "${skill.name}": sanitized name "${safeName}" collides with another skill`)
|
||||
continue
|
||||
}
|
||||
seen.add(safeName)
|
||||
|
||||
const target = path.join(skillsDir, safeName)
|
||||
await forceSymlink(skill.sourceDir, target)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { backupFile, copySkillDir, ensureDir, writeText } from "../utils/files"
|
||||
import { backupFile, copySkillDir, ensureDir, sanitizePathName, writeText } from "../utils/files"
|
||||
import type { CodexBundle } from "../types/codex"
|
||||
import type { ClaudeMcpServer } from "../types/claude"
|
||||
import { transformContentForCodex } from "../utils/codex-content"
|
||||
@@ -20,7 +20,7 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copySkillDir(
|
||||
skill.sourceDir,
|
||||
path.join(skillsRoot, skill.name),
|
||||
path.join(skillsRoot, sanitizePathName(skill.name)),
|
||||
(content) => transformContentForCodex(content, bundle.invocationTargets, {
|
||||
unknownSlashBehavior: "preserve",
|
||||
}),
|
||||
@@ -31,7 +31,7 @@ export async function writeCodexBundle(outputRoot: string, bundle: CodexBundle):
|
||||
if (bundle.generatedSkills.length > 0) {
|
||||
const skillsRoot = path.join(codexRoot, "skills")
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(skillsRoot, skill.name, "SKILL.md"), skill.content + "\n")
|
||||
await writeText(path.join(skillsRoot, sanitizePathName(skill.name), "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { backupFile, copySkillDir, ensureDir, writeJson, writeText } from "../utils/files"
|
||||
import { backupFile, copySkillDir, ensureDir, sanitizePathName, writeJson, writeText } from "../utils/files"
|
||||
import { transformContentForCopilot } from "../converters/claude-to-copilot"
|
||||
import type { CopilotBundle } from "../types/copilot"
|
||||
|
||||
@@ -10,21 +10,21 @@ export async function writeCopilotBundle(outputRoot: string, bundle: CopilotBund
|
||||
if (bundle.agents.length > 0) {
|
||||
const agentsDir = path.join(paths.githubDir, "agents")
|
||||
for (const agent of bundle.agents) {
|
||||
await writeText(path.join(agentsDir, `${agent.name}.agent.md`), agent.content + "\n")
|
||||
await writeText(path.join(agentsDir, `${sanitizePathName(agent.name)}.agent.md`), agent.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.generatedSkills.length > 0) {
|
||||
const skillsDir = path.join(paths.githubDir, "skills")
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
||||
await writeText(path.join(skillsDir, sanitizePathName(skill.name), "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
const skillsDir = path.join(paths.githubDir, "skills")
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copySkillDir(skill.sourceDir, path.join(skillsDir, skill.name), transformContentForCopilot)
|
||||
await copySkillDir(skill.sourceDir, path.join(skillsDir, sanitizePathName(skill.name)), transformContentForCopilot)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { copySkillDir, ensureDir, resolveCommandPath, writeText } from "../utils/files"
|
||||
import { copySkillDir, ensureDir, resolveCommandPath, sanitizePathName, writeText } from "../utils/files"
|
||||
import { transformContentForDroid } from "../converters/claude-to-droid"
|
||||
import type { DroidBundle } from "../types/droid"
|
||||
|
||||
@@ -18,14 +18,14 @@ export async function writeDroidBundle(outputRoot: string, bundle: DroidBundle):
|
||||
if (bundle.droids.length > 0) {
|
||||
await ensureDir(paths.droidsDir)
|
||||
for (const droid of bundle.droids) {
|
||||
await writeText(path.join(paths.droidsDir, `${droid.name}.md`), droid.content + "\n")
|
||||
await writeText(path.join(paths.droidsDir, `${sanitizePathName(droid.name)}.md`), droid.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
await ensureDir(paths.skillsDir)
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, skill.name), transformContentForDroid)
|
||||
await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, sanitizePathName(skill.name)), transformContentForDroid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { backupFile, copySkillDir, ensureDir, pathExists, readJson, resolveCommandPath, writeJson, writeText } from "../utils/files"
|
||||
import { backupFile, copySkillDir, ensureDir, pathExists, readJson, resolveCommandPath, sanitizePathName, writeJson, writeText } from "../utils/files"
|
||||
import { transformContentForGemini } from "../converters/claude-to-gemini"
|
||||
import type { GeminiBundle } from "../types/gemini"
|
||||
|
||||
@@ -9,13 +9,13 @@ export async function writeGeminiBundle(outputRoot: string, bundle: GeminiBundle
|
||||
|
||||
if (bundle.generatedSkills.length > 0) {
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(paths.skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
||||
await writeText(path.join(paths.skillsDir, sanitizePathName(skill.name), "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
}
|
||||
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, skill.name), transformContentForGemini)
|
||||
await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, sanitizePathName(skill.name)), transformContentForGemini)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { backupFile, copySkillDir, ensureDir, pathExists, readJson, writeJson, writeText } from "../utils/files"
|
||||
import { backupFile, copySkillDir, ensureDir, pathExists, readJson, sanitizePathName, writeJson, writeText } from "../utils/files"
|
||||
import { transformContentForKiro } from "../converters/claude-to-kiro"
|
||||
import type { KiroBundle } from "../types/kiro"
|
||||
|
||||
@@ -15,13 +15,13 @@ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): P
|
||||
|
||||
// Write agent JSON config
|
||||
await writeJson(
|
||||
path.join(paths.agentsDir, `${agent.name}.json`),
|
||||
path.join(paths.agentsDir, `${sanitizePathName(agent.name)}.json`),
|
||||
agent.config,
|
||||
)
|
||||
|
||||
// Write agent prompt file
|
||||
await writeText(
|
||||
path.join(paths.agentsDir, "prompts", `${agent.name}.md`),
|
||||
path.join(paths.agentsDir, "prompts", `${sanitizePathName(agent.name)}.md`),
|
||||
agent.promptContent + "\n",
|
||||
)
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): P
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
validatePathSafe(skill.name, "skill")
|
||||
await writeText(
|
||||
path.join(paths.skillsDir, skill.name, "SKILL.md"),
|
||||
path.join(paths.skillsDir, sanitizePathName(skill.name), "SKILL.md"),
|
||||
skill.content + "\n",
|
||||
)
|
||||
}
|
||||
@@ -42,7 +42,7 @@ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): P
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
for (const skill of bundle.skillDirs) {
|
||||
validatePathSafe(skill.name, "skill directory")
|
||||
const destDir = path.join(paths.skillsDir, skill.name)
|
||||
const destDir = path.join(paths.skillsDir, sanitizePathName(skill.name))
|
||||
|
||||
// Validate destination doesn't escape skills directory
|
||||
const resolvedDest = path.resolve(destDir)
|
||||
@@ -63,7 +63,7 @@ export async function writeKiroBundle(outputRoot: string, bundle: KiroBundle): P
|
||||
for (const file of bundle.steeringFiles) {
|
||||
validatePathSafe(file.name, "steering file")
|
||||
await writeText(
|
||||
path.join(paths.steeringDir, `${file.name}.md`),
|
||||
path.join(paths.steeringDir, `${sanitizePathName(file.name)}.md`),
|
||||
file.content + "\n",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import path from "path"
|
||||
import { promises as fs } from "fs"
|
||||
import { backupFile, copyDir, ensureDir, pathExists, readJson, walkFiles, writeJson, writeText } from "../utils/files"
|
||||
import { backupFile, copyDir, ensureDir, pathExists, readJson, sanitizePathName, walkFiles, writeJson, writeText } from "../utils/files"
|
||||
import type { OpenClawBundle } from "../types/openclaw"
|
||||
|
||||
export async function writeOpenClawBundle(outputRoot: string, bundle: OpenClawBundle): Promise<void> {
|
||||
@@ -18,7 +18,7 @@ export async function writeOpenClawBundle(outputRoot: string, bundle: OpenClawBu
|
||||
|
||||
// Write generated skills (agents + commands converted to SKILL.md)
|
||||
for (const skill of bundle.skills) {
|
||||
const skillDir = path.join(paths.skillsDir, skill.dir)
|
||||
const skillDir = path.join(paths.skillsDir, sanitizePathName(skill.dir))
|
||||
await ensureDir(skillDir)
|
||||
await writeText(path.join(skillDir, "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export async function writeOpenClawBundle(outputRoot: string, bundle: OpenClawBu
|
||||
// Copy original skill directories (preserving references/, assets/, scripts/)
|
||||
// and rewrite .claude/ paths to .openclaw/ in markdown files
|
||||
for (const skill of bundle.skillDirCopies) {
|
||||
const destDir = path.join(paths.skillsDir, skill.name)
|
||||
const destDir = path.join(paths.skillsDir, sanitizePathName(skill.name))
|
||||
await copyDir(skill.sourceDir, destDir)
|
||||
await rewritePathsInDir(destDir)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { backupFile, copyDir, ensureDir, pathExists, readJson, resolveCommandPath, writeJson, writeText } from "../utils/files"
|
||||
import { backupFile, copyDir, ensureDir, pathExists, readJson, resolveCommandPath, sanitizePathName, writeJson, writeText } from "../utils/files"
|
||||
import type { OpenCodeBundle, OpenCodeConfig } from "../types/opencode"
|
||||
|
||||
// Merges plugin config into existing opencode.json. User keys win on conflict. See ADR-002.
|
||||
@@ -70,8 +70,15 @@ export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBu
|
||||
}
|
||||
|
||||
const agentsDir = openCodePaths.agentsDir
|
||||
const seenAgents = new Set<string>()
|
||||
for (const agent of bundle.agents) {
|
||||
await writeText(path.join(agentsDir, `${agent.name}.md`), agent.content + "\n")
|
||||
const safeName = sanitizePathName(agent.name)
|
||||
if (seenAgents.has(safeName)) {
|
||||
console.warn(`Skipping agent "${agent.name}": sanitized name "${safeName}" collides with another agent`)
|
||||
continue
|
||||
}
|
||||
seenAgents.add(safeName)
|
||||
await writeText(path.join(agentsDir, `${safeName}.md`), agent.content + "\n")
|
||||
}
|
||||
|
||||
for (const commandFile of bundle.commandFiles) {
|
||||
@@ -93,7 +100,7 @@ export async function writeOpenCodeBundle(outputRoot: string, bundle: OpenCodeBu
|
||||
if (bundle.skillDirs.length > 0) {
|
||||
const skillsRoot = openCodePaths.skillsDir
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
||||
await copyDir(skill.sourceDir, path.join(skillsRoot, sanitizePathName(skill.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ensureDir,
|
||||
pathExists,
|
||||
readText,
|
||||
sanitizePathName,
|
||||
writeJson,
|
||||
writeText,
|
||||
} from "../utils/files"
|
||||
@@ -34,15 +35,15 @@ export async function writePiBundle(outputRoot: string, bundle: PiBundle): Promi
|
||||
await ensureDir(paths.extensionsDir)
|
||||
|
||||
for (const prompt of bundle.prompts) {
|
||||
await writeText(path.join(paths.promptsDir, `${prompt.name}.md`), prompt.content + "\n")
|
||||
await writeText(path.join(paths.promptsDir, `${sanitizePathName(prompt.name)}.md`), prompt.content + "\n")
|
||||
}
|
||||
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, skill.name), transformContentForPi)
|
||||
await copySkillDir(skill.sourceDir, path.join(paths.skillsDir, sanitizePathName(skill.name)), transformContentForPi)
|
||||
}
|
||||
|
||||
for (const skill of bundle.generatedSkills) {
|
||||
await writeText(path.join(paths.skillsDir, skill.name, "SKILL.md"), skill.content + "\n")
|
||||
await writeText(path.join(paths.skillsDir, sanitizePathName(skill.name), "SKILL.md"), skill.content + "\n")
|
||||
}
|
||||
|
||||
for (const extension of bundle.extensions) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { backupFile, copyDir, ensureDir, resolveCommandPath, writeJson, writeText } from "../utils/files"
|
||||
import { backupFile, copyDir, ensureDir, resolveCommandPath, sanitizePathName, writeJson, writeText } from "../utils/files"
|
||||
import type { QwenBundle, QwenExtensionConfig } from "../types/qwen"
|
||||
|
||||
export async function writeQwenBundle(outputRoot: string, bundle: QwenBundle): Promise<void> {
|
||||
@@ -24,7 +24,7 @@ export async function writeQwenBundle(outputRoot: string, bundle: QwenBundle): P
|
||||
await ensureDir(agentsDir)
|
||||
for (const agent of bundle.agents) {
|
||||
const ext = agent.format === "yaml" ? "yaml" : "md"
|
||||
await writeText(path.join(agentsDir, `${agent.name}.${ext}`), agent.content + "\n")
|
||||
await writeText(path.join(agentsDir, `${sanitizePathName(agent.name)}.${ext}`), agent.content + "\n")
|
||||
}
|
||||
|
||||
// Write commands
|
||||
@@ -40,7 +40,7 @@ export async function writeQwenBundle(outputRoot: string, bundle: QwenBundle): P
|
||||
const skillsRoot = qwenPaths.skillsDir
|
||||
await ensureDir(skillsRoot)
|
||||
for (const skill of bundle.skillDirs) {
|
||||
await copyDir(skill.sourceDir, path.join(skillsRoot, skill.name))
|
||||
await copyDir(skill.sourceDir, path.join(skillsRoot, sanitizePathName(skill.name)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from "path"
|
||||
import { backupFile, copySkillDir, ensureDir, pathExists, readJson, writeJsonSecure, writeText } from "../utils/files"
|
||||
import { backupFile, copySkillDir, ensureDir, pathExists, readJson, sanitizePathName, writeJsonSecure, writeText } from "../utils/files"
|
||||
import { formatFrontmatter } from "../utils/frontmatter"
|
||||
import { transformContentForWindsurf } from "../converters/claude-to-windsurf"
|
||||
import type { WindsurfBundle } from "../types/windsurf"
|
||||
@@ -20,7 +20,7 @@ export async function writeWindsurfBundle(outputRoot: string, bundle: WindsurfBu
|
||||
await ensureDir(skillsDir)
|
||||
for (const skill of bundle.agentSkills) {
|
||||
validatePathSafe(skill.name, "agent skill")
|
||||
const destDir = path.join(skillsDir, skill.name)
|
||||
const destDir = path.join(skillsDir, sanitizePathName(skill.name))
|
||||
|
||||
const resolvedDest = path.resolve(destDir)
|
||||
if (!resolvedDest.startsWith(path.resolve(skillsDir))) {
|
||||
@@ -51,7 +51,7 @@ export async function writeWindsurfBundle(outputRoot: string, bundle: WindsurfBu
|
||||
await ensureDir(skillsDir)
|
||||
for (const skill of bundle.skillDirs) {
|
||||
validatePathSafe(skill.name, "skill directory")
|
||||
const destDir = path.join(skillsDir, skill.name)
|
||||
const destDir = path.join(skillsDir, sanitizePathName(skill.name))
|
||||
|
||||
const resolvedDest = path.resolve(destDir)
|
||||
if (!resolvedDest.startsWith(path.resolve(skillsDir))) {
|
||||
|
||||
@@ -75,6 +75,16 @@ export async function walkFiles(root: string): Promise<string[]> {
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a name for use as a filesystem path component.
|
||||
* Replaces colons with hyphens so colon-namespaced names
|
||||
* (e.g. "ce:brainstorm") become flat directory names ("ce-brainstorm")
|
||||
* instead of failing on Windows where colons are illegal in filenames.
|
||||
*/
|
||||
export function sanitizePathName(name: string): string {
|
||||
return name.replace(/:/g, "-")
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a colon-separated command name into a filesystem path.
|
||||
* e.g. resolveCommandPath("/commands", "ce:plan", ".md") -> "/commands/ce/plan.md"
|
||||
|
||||
Reference in New Issue
Block a user