Add `--to windsurf` target for the converter CLI with full spec compliance
per docs/specs/windsurf.md:
- Claude agents → Windsurf skills (skills/{name}/SKILL.md)
- Claude commands → Windsurf workflows (workflows/{name}.md, flat)
- Pass-through skills copy unchanged
- MCP servers → mcp_config.json (merged with existing, 0o600 permissions)
- Hooks skipped with warning, CLAUDE.md skipped
Global scope support via generic --scope flag (Windsurf as first adopter):
- --to windsurf defaults to global (~/.codeium/windsurf/)
- --scope workspace for project-level .windsurf/ output
- --output overrides scope-derived paths
Shared utilities extracted (resolveTargetOutputRoot, hasPotentialSecrets)
to eliminate duplication across CLI commands.
68 new tests (converter, writer, scope resolution).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.6 KiB
TypeScript
85 lines
2.6 KiB
TypeScript
import { promises as fs } from "fs"
|
|
import path from "path"
|
|
|
|
export async function backupFile(filePath: string): Promise<string | null> {
|
|
if (!(await pathExists(filePath))) return null
|
|
|
|
try {
|
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-")
|
|
const backupPath = `${filePath}.bak.${timestamp}`
|
|
await fs.copyFile(filePath, backupPath)
|
|
return backupPath
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
export async function pathExists(filePath: string): Promise<boolean> {
|
|
try {
|
|
await fs.access(filePath)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
export async function ensureDir(dirPath: string): Promise<void> {
|
|
await fs.mkdir(dirPath, { recursive: true })
|
|
}
|
|
|
|
export async function readText(filePath: string): Promise<string> {
|
|
return fs.readFile(filePath, "utf8")
|
|
}
|
|
|
|
export async function readJson<T>(filePath: string): Promise<T> {
|
|
const raw = await readText(filePath)
|
|
return JSON.parse(raw) as T
|
|
}
|
|
|
|
export async function writeText(filePath: string, content: string): Promise<void> {
|
|
await ensureDir(path.dirname(filePath))
|
|
await fs.writeFile(filePath, content, "utf8")
|
|
}
|
|
|
|
export async function writeJson(filePath: string, data: unknown): Promise<void> {
|
|
const content = JSON.stringify(data, null, 2)
|
|
await writeText(filePath, content + "\n")
|
|
}
|
|
|
|
/** Write JSON with restrictive permissions (0o600) for files containing secrets */
|
|
export async function writeJsonSecure(filePath: string, data: unknown): Promise<void> {
|
|
const content = JSON.stringify(data, null, 2)
|
|
await ensureDir(path.dirname(filePath))
|
|
await fs.writeFile(filePath, content + "\n", { encoding: "utf8", mode: 0o600 })
|
|
}
|
|
|
|
export async function walkFiles(root: string): Promise<string[]> {
|
|
const entries = await fs.readdir(root, { withFileTypes: true })
|
|
const results: string[] = []
|
|
for (const entry of entries) {
|
|
const fullPath = path.join(root, entry.name)
|
|
if (entry.isDirectory()) {
|
|
const nested = await walkFiles(fullPath)
|
|
results.push(...nested)
|
|
} else if (entry.isFile()) {
|
|
results.push(fullPath)
|
|
}
|
|
}
|
|
return results
|
|
}
|
|
|
|
export async function copyDir(sourceDir: string, targetDir: string): Promise<void> {
|
|
await ensureDir(targetDir)
|
|
const entries = await fs.readdir(sourceDir, { withFileTypes: true })
|
|
for (const entry of entries) {
|
|
const sourcePath = path.join(sourceDir, entry.name)
|
|
const targetPath = path.join(targetDir, entry.name)
|
|
if (entry.isDirectory()) {
|
|
await copyDir(sourcePath, targetPath)
|
|
} else if (entry.isFile()) {
|
|
await ensureDir(path.dirname(targetPath))
|
|
await fs.copyFile(sourcePath, targetPath)
|
|
}
|
|
}
|
|
}
|