mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
Migrate mode definitions from JSON to YAML
This commit is contained in:
parent
5fb9af40fb
commit
5568768e22
7 changed files with 464 additions and 56 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -17,6 +17,7 @@
|
|||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"@types/turndown": "^5.0.5",
|
||||
|
|
@ -39,6 +40,7 @@
|
|||
"i18next": "^24.2.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"js-tiktoken": "^1.0.19",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"node-ipc": "^12.0.0",
|
||||
|
|
@ -8912,6 +8914,11 @@
|
|||
"pretty-format": "^29.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-yaml": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz",
|
||||
"integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz",
|
||||
|
|
@ -9841,8 +9848,7 @@
|
|||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
|
||||
},
|
||||
"node_modules/array-buffer-byte-length": {
|
||||
"version": "1.0.1",
|
||||
|
|
@ -15385,7 +15391,6 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"argparse": "^2.0.1"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -399,6 +399,7 @@
|
|||
"@mistralai/mistralai": "^1.3.6",
|
||||
"@modelcontextprotocol/sdk": "^1.7.0",
|
||||
"@types/clone-deep": "^4.0.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/pdf-parse": "^1.1.4",
|
||||
"@types/tmp": "^0.2.6",
|
||||
"@types/turndown": "^5.0.5",
|
||||
|
|
@ -421,6 +422,7 @@
|
|||
"i18next": "^24.2.2",
|
||||
"isbinaryfile": "^5.0.2",
|
||||
"js-tiktoken": "^1.0.19",
|
||||
"js-yaml": "^4.1.0",
|
||||
"mammoth": "^1.8.0",
|
||||
"monaco-vscode-textmate-theme-converter": "^0.1.7",
|
||||
"node-ipc": "^12.0.0",
|
||||
|
|
|
|||
|
|
@ -3,12 +3,14 @@ import * as path from "path"
|
|||
import * as fs from "fs/promises"
|
||||
import { customModesSettingsSchema } from "../../schemas"
|
||||
import { ModeConfig } from "../../shared/modes"
|
||||
import { fileExistsAtPath } from "../../utils/fs"
|
||||
import { fileExistsAtPath, ensureDirectory } from "../../utils/fs"
|
||||
import { arePathsEqual, getWorkspacePath } from "../../utils/path"
|
||||
import { logger } from "../../utils/logging"
|
||||
import { GlobalFileNames } from "../../shared/globalFileNames"
|
||||
import { GlobalFileNames, ModeFileLocations } from "../../shared/globalFileNames"
|
||||
import { readYamlFile, writeYamlFile } from "../../utils/yaml"
|
||||
import { loadModeFromYaml, loadAllModesFromYaml, saveModeToYaml } from "../../utils/modes"
|
||||
|
||||
const ROOMODES_FILENAME = ".roomodes"
|
||||
const ROOMODES_FILENAME = ModeFileLocations.LEGACY_ROOMODES
|
||||
|
||||
export class CustomModesManager {
|
||||
private disposables: vscode.Disposable[] = []
|
||||
|
|
@ -20,7 +22,130 @@ export class CustomModesManager {
|
|||
private readonly onUpdate: () => Promise<void>,
|
||||
) {
|
||||
// TODO: We really shouldn't have async methods in the constructor.
|
||||
this.watchCustomModesFiles()
|
||||
// First migrate legacy modes to YAML, then watch for changes
|
||||
this.migrateToYaml()
|
||||
.then(() => {
|
||||
this.watchCustomModesFiles()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to migrate modes to YAML:", error)
|
||||
// Fall back to standard initialization
|
||||
this.watchCustomModesFiles()
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically migrate modes from JSON to YAML format
|
||||
*/
|
||||
private async migrateToYaml(): Promise<void> {
|
||||
// First try to migrate workspace-specific modes
|
||||
await this.migrateWorkspaceModes()
|
||||
|
||||
// Then migrate global modes
|
||||
await this.migrateGlobalModes()
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate workspace-specific modes from .roomodes to YAML
|
||||
*/
|
||||
private async migrateWorkspaceModes(): Promise<void> {
|
||||
const roomodesPath = await this.getWorkspaceRoomodes()
|
||||
|
||||
// Skip if no .roomodes file exists
|
||||
if (!roomodesPath) return
|
||||
|
||||
// Check if we've already migrated (based on a flag in globalState)
|
||||
const isMigrated = this.context.globalState.get(`migrated-workspace-${roomodesPath}`, false)
|
||||
if (isMigrated) return
|
||||
|
||||
try {
|
||||
// Read and parse .roomodes
|
||||
const modesJson = await this.loadModesFromFile(roomodesPath)
|
||||
if (modesJson.length === 0) return
|
||||
|
||||
// Create .roo/modes directory
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
const modesDir = path.join(workspaceRoot, ModeFileLocations.MODES_DIRECTORY)
|
||||
await ensureDirectory(modesDir)
|
||||
|
||||
let migratedCount = 0
|
||||
|
||||
// Process each mode
|
||||
for (const mode of modesJson) {
|
||||
// Look for corresponding rules file
|
||||
const rulesPath = path.join(workspaceRoot, `.roorules-${mode.slug}`)
|
||||
let rules = ""
|
||||
|
||||
if (await fileExistsAtPath(rulesPath)) {
|
||||
rules = await fs.readFile(rulesPath, "utf-8")
|
||||
}
|
||||
|
||||
// Write YAML file - don't include rules in the YAML file since we're keeping separate rules files
|
||||
await saveModeToYaml(mode, this.context.globalStorageUri.fsPath)
|
||||
migratedCount++
|
||||
|
||||
// No need to migrate rules since we're keeping them in separate files
|
||||
}
|
||||
|
||||
// Set flag to avoid migrating again
|
||||
await this.context.globalState.update(`migrated-workspace-${roomodesPath}`, true)
|
||||
|
||||
if (migratedCount > 0) {
|
||||
// Notify user (subtle notification)
|
||||
vscode.window.showInformationMessage(
|
||||
`Successfully migrated ${migratedCount} workspace custom modes to YAML format in .roo/modes/`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during workspace mode migration:", error)
|
||||
// Don't throw - we'll fall back to legacy formats
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate global modes from JSON settings to YAML
|
||||
*/
|
||||
private async migrateGlobalModes(): Promise<void> {
|
||||
// Check if we've already migrated global modes
|
||||
const isMigrated = this.context.globalState.get("migrated-global-modes", false)
|
||||
if (isMigrated) return
|
||||
|
||||
try {
|
||||
// Get the global JSON modes file
|
||||
const settingsPath = await this.getCustomModesFilePath()
|
||||
const globalModes = await this.loadModesFromFile(settingsPath)
|
||||
|
||||
if (globalModes.length === 0) return
|
||||
|
||||
// Create global modes directory
|
||||
const globalModesDir = path.join(this.context.globalStorageUri.fsPath, "modes")
|
||||
await ensureDirectory(globalModesDir)
|
||||
|
||||
let migratedCount = 0
|
||||
|
||||
// Process each mode
|
||||
for (const mode of globalModes) {
|
||||
// Skip if already in project mode
|
||||
if (mode.source === "project") continue
|
||||
|
||||
// Write YAML file
|
||||
await saveModeToYaml(mode, this.context.globalStorageUri.fsPath)
|
||||
migratedCount++
|
||||
}
|
||||
|
||||
// Set flag to avoid migrating again
|
||||
await this.context.globalState.update("migrated-global-modes", true)
|
||||
|
||||
if (migratedCount > 0) {
|
||||
// Notify user (subtle notification)
|
||||
vscode.window.showInformationMessage(
|
||||
`Successfully migrated ${migratedCount} global custom modes to YAML format`,
|
||||
)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error during global mode migration:", error)
|
||||
// Don't throw - we'll fall back to legacy formats
|
||||
}
|
||||
}
|
||||
|
||||
private async queueWrite(operation: () => Promise<void>): Promise<void> {
|
||||
|
|
@ -183,37 +308,40 @@ export class CustomModesManager {
|
|||
}
|
||||
|
||||
async getCustomModes(): Promise<ModeConfig[]> {
|
||||
// Get modes from settings file
|
||||
// First check for modes in YAML format
|
||||
const yamlProjectModes = await loadAllModesFromYaml(true) // Workspace YAML modes
|
||||
const yamlGlobalModes = await loadAllModesFromYaml(false, this.context.globalStorageUri.fsPath) // Global YAML modes
|
||||
|
||||
// Then check legacy formats
|
||||
const settingsPath = await this.getCustomModesFilePath()
|
||||
const settingsModes = await this.loadModesFromFile(settingsPath)
|
||||
|
||||
// Get modes from .roomodes if it exists
|
||||
const roomodesPath = await this.getWorkspaceRoomodes()
|
||||
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
|
||||
|
||||
// Create maps to store modes by source
|
||||
const projectModes = new Map<string, ModeConfig>()
|
||||
const globalModes = new Map<string, ModeConfig>()
|
||||
// Create a map to merge modes with correct precedence
|
||||
// Order of precedence: YAML project > JSON project > YAML global > JSON global
|
||||
const slugsMap = new Map<string, ModeConfig>()
|
||||
|
||||
// Add project modes (they take precedence)
|
||||
for (const mode of roomodesModes) {
|
||||
projectModes.set(mode.slug, { ...mode, source: "project" as const })
|
||||
}
|
||||
|
||||
// Add global modes
|
||||
// Add in reverse order of precedence
|
||||
for (const mode of settingsModes) {
|
||||
if (!projectModes.has(mode.slug)) {
|
||||
globalModes.set(mode.slug, { ...mode, source: "global" as const })
|
||||
}
|
||||
slugsMap.set(mode.slug, { ...mode, source: "global" })
|
||||
}
|
||||
|
||||
// Combine modes in the correct order: project modes first, then global modes
|
||||
const mergedModes = [
|
||||
...roomodesModes.map((mode) => ({ ...mode, source: "project" as const })),
|
||||
...settingsModes
|
||||
.filter((mode) => !projectModes.has(mode.slug))
|
||||
.map((mode) => ({ ...mode, source: "global" as const })),
|
||||
]
|
||||
for (const mode of yamlGlobalModes) {
|
||||
slugsMap.set(mode.slug, mode)
|
||||
}
|
||||
|
||||
for (const mode of roomodesModes) {
|
||||
slugsMap.set(mode.slug, { ...mode, source: "project" })
|
||||
}
|
||||
|
||||
for (const mode of yamlProjectModes) {
|
||||
slugsMap.set(mode.slug, mode)
|
||||
}
|
||||
|
||||
// Convert map back to array
|
||||
const mergedModes = Array.from(slugsMap.values())
|
||||
|
||||
await this.context.globalState.update("customModes", mergedModes)
|
||||
return mergedModes
|
||||
|
|
@ -221,7 +349,6 @@ export class CustomModesManager {
|
|||
async updateCustomMode(slug: string, config: ModeConfig): Promise<void> {
|
||||
try {
|
||||
const isProjectMode = config.source === "project"
|
||||
let targetPath: string
|
||||
|
||||
if (isProjectMode) {
|
||||
const workspaceFolders = vscode.workspace.workspaceFolders
|
||||
|
|
@ -229,22 +356,30 @@ export class CustomModesManager {
|
|||
logger.error("Failed to update project mode: No workspace folder found", { slug })
|
||||
throw new Error("No workspace folder found for project-specific mode")
|
||||
}
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
targetPath = path.join(workspaceRoot, ROOMODES_FILENAME)
|
||||
const exists = await fileExistsAtPath(targetPath)
|
||||
logger.info(`${exists ? "Updating" : "Creating"} project mode in ${ROOMODES_FILENAME}`, {
|
||||
slug,
|
||||
workspace: workspaceRoot,
|
||||
})
|
||||
} else {
|
||||
targetPath = await this.getCustomModesFilePath()
|
||||
}
|
||||
|
||||
// Ensure source is set correctly
|
||||
const modeWithSource = {
|
||||
...config,
|
||||
source: isProjectMode ? ("project" as const) : ("global" as const),
|
||||
}
|
||||
|
||||
await this.queueWrite(async () => {
|
||||
// Ensure source is set correctly based on target file
|
||||
const modeWithSource = {
|
||||
...config,
|
||||
source: isProjectMode ? ("project" as const) : ("global" as const),
|
||||
// Save to YAML format
|
||||
await saveModeToYaml(modeWithSource, this.context.globalStorageUri.fsPath)
|
||||
|
||||
// Also update the legacy format for backward compatibility
|
||||
let targetPath: string
|
||||
|
||||
if (isProjectMode) {
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
targetPath = path.join(workspaceRoot, ROOMODES_FILENAME)
|
||||
logger.info(`Updating ${slug} in both YAML and legacy JSON formats`, {
|
||||
slug,
|
||||
workspace: workspaceRoot,
|
||||
})
|
||||
} else {
|
||||
targetPath = await this.getCustomModesFilePath()
|
||||
}
|
||||
|
||||
await this.updateModesInFile(targetPath, (modes) => {
|
||||
|
|
@ -282,12 +417,39 @@ export class CustomModesManager {
|
|||
}
|
||||
|
||||
private async refreshMergedState(): Promise<void> {
|
||||
const settingsPath = await this.getCustomModesFilePath()
|
||||
const roomodesPath = await this.getWorkspaceRoomodes()
|
||||
// Get all modes from both formats
|
||||
const yamlProjectModes = await loadAllModesFromYaml(true) // Workspace YAML modes
|
||||
const yamlGlobalModes = await loadAllModesFromYaml(false, this.context.globalStorageUri.fsPath) // Global YAML modes
|
||||
|
||||
// Legacy JSON formats
|
||||
const settingsPath = await this.getCustomModesFilePath()
|
||||
const settingsModes = await this.loadModesFromFile(settingsPath)
|
||||
|
||||
const roomodesPath = await this.getWorkspaceRoomodes()
|
||||
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
|
||||
const mergedModes = await this.mergeCustomModes(roomodesModes, settingsModes)
|
||||
|
||||
// Use the same merging logic as in getCustomModes
|
||||
const slugsMap = new Map<string, ModeConfig>()
|
||||
|
||||
// Add in reverse order of precedence
|
||||
for (const mode of settingsModes) {
|
||||
slugsMap.set(mode.slug, { ...mode, source: "global" })
|
||||
}
|
||||
|
||||
for (const mode of yamlGlobalModes) {
|
||||
slugsMap.set(mode.slug, mode)
|
||||
}
|
||||
|
||||
for (const mode of roomodesModes) {
|
||||
slugsMap.set(mode.slug, { ...mode, source: "project" })
|
||||
}
|
||||
|
||||
for (const mode of yamlProjectModes) {
|
||||
slugsMap.set(mode.slug, mode)
|
||||
}
|
||||
|
||||
// Convert map back to array
|
||||
const mergedModes = Array.from(slugsMap.values())
|
||||
|
||||
await this.context.globalState.update("customModes", mergedModes)
|
||||
await this.onUpdate()
|
||||
|
|
@ -295,27 +457,46 @@ export class CustomModesManager {
|
|||
|
||||
async deleteCustomMode(slug: string): Promise<void> {
|
||||
try {
|
||||
// Check legacy JSON formats
|
||||
const settingsPath = await this.getCustomModesFilePath()
|
||||
const roomodesPath = await this.getWorkspaceRoomodes()
|
||||
|
||||
const settingsModes = await this.loadModesFromFile(settingsPath)
|
||||
const roomodesModes = roomodesPath ? await this.loadModesFromFile(roomodesPath) : []
|
||||
|
||||
// Find the mode in either file
|
||||
// Find the mode in either JSON file
|
||||
const projectMode = roomodesModes.find((m) => m.slug === slug)
|
||||
const globalMode = settingsModes.find((m) => m.slug === slug)
|
||||
|
||||
if (!projectMode && !globalMode) {
|
||||
throw new Error("Write error: Mode not found")
|
||||
// Also check YAML formats
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
const projectYamlPath = path.join(workspaceRoot, ModeFileLocations.MODES_DIRECTORY, `${slug}.yaml`)
|
||||
const globalYamlPath = path.join(this.context.globalStorageUri.fsPath, "modes", `${slug}.yaml`)
|
||||
|
||||
const projectYamlExists = await fileExistsAtPath(projectYamlPath)
|
||||
const globalYamlExists = await fileExistsAtPath(globalYamlPath)
|
||||
|
||||
if (!projectMode && !globalMode && !projectYamlExists && !globalYamlExists) {
|
||||
throw new Error("Write error: Mode not found in any format")
|
||||
}
|
||||
|
||||
await this.queueWrite(async () => {
|
||||
// Delete from project first if it exists there
|
||||
// Delete from YAML formats first
|
||||
if (projectYamlExists) {
|
||||
await fs.unlink(projectYamlPath)
|
||||
logger.info(`Deleted YAML mode file: ${projectYamlPath}`)
|
||||
}
|
||||
|
||||
if (globalYamlExists) {
|
||||
await fs.unlink(globalYamlPath)
|
||||
logger.info(`Deleted YAML mode file: ${globalYamlPath}`)
|
||||
}
|
||||
|
||||
// Delete from legacy JSON formats for backward compatibility
|
||||
if (projectMode && roomodesPath) {
|
||||
await this.updateModesInFile(roomodesPath, (modes) => modes.filter((m) => m.slug !== slug))
|
||||
}
|
||||
|
||||
// Delete from global settings if it exists there
|
||||
if (globalMode) {
|
||||
await this.updateModesInFile(settingsPath, (modes) => modes.filter((m) => m.slug !== slug))
|
||||
}
|
||||
|
|
@ -323,9 +504,9 @@ export class CustomModesManager {
|
|||
await this.refreshMergedState()
|
||||
})
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to delete custom mode: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logger.error("Failed to delete custom mode", { slug, error: errorMessage })
|
||||
vscode.window.showErrorMessage(`Failed to delete custom mode: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -337,14 +518,50 @@ export class CustomModesManager {
|
|||
|
||||
async resetCustomModes(): Promise<void> {
|
||||
try {
|
||||
// Clear legacy JSON format
|
||||
const filePath = await this.getCustomModesFilePath()
|
||||
await fs.writeFile(filePath, JSON.stringify({ customModes: [] }, null, 2))
|
||||
|
||||
// Clear global YAML modes
|
||||
try {
|
||||
const globalModesDir = path.join(this.context.globalStorageUri.fsPath, "modes")
|
||||
if (await fileExistsAtPath(globalModesDir)) {
|
||||
const files = await fs.readdir(globalModesDir)
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
||||
await fs.unlink(path.join(globalModesDir, file))
|
||||
logger.info(`Deleted global YAML mode: ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (yamlError) {
|
||||
console.error("Error clearing global YAML modes:", yamlError)
|
||||
}
|
||||
|
||||
// Clear workspace YAML modes
|
||||
try {
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
const workspaceModesDir = path.join(workspaceRoot, ModeFileLocations.MODES_DIRECTORY)
|
||||
if (await fileExistsAtPath(workspaceModesDir)) {
|
||||
const files = await fs.readdir(workspaceModesDir)
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
||||
await fs.unlink(path.join(workspaceModesDir, file))
|
||||
logger.info(`Deleted workspace YAML mode: ${file}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (yamlError) {
|
||||
console.error("Error clearing workspace YAML modes:", yamlError)
|
||||
}
|
||||
|
||||
// Update global state
|
||||
await this.context.globalState.update("customModes", [])
|
||||
await this.onUpdate()
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Failed to reset custom modes: ${error instanceof Error ? error.message : String(error)}`,
|
||||
)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
logger.error("Failed to reset custom modes", { error: errorMessage })
|
||||
vscode.window.showErrorMessage(`Failed to reset custom modes: ${errorMessage}`)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,3 +8,11 @@ export const GlobalFileNames = {
|
|||
unboundModels: "unbound_models.json",
|
||||
customModes: "custom_modes.json",
|
||||
}
|
||||
|
||||
/**
|
||||
* File locations for mode configuration
|
||||
*/
|
||||
export enum ModeFileLocations {
|
||||
LEGACY_ROOMODES = ".roomodes",
|
||||
MODES_DIRECTORY = ".roo/modes",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,3 +45,13 @@ export async function fileExistsAtPath(filePath: string): Promise<boolean> {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a directory exists, creating it if it doesn't
|
||||
*
|
||||
* @param dirPath - The path to the directory to ensure exists
|
||||
* @returns A promise that resolves when the directory exists
|
||||
*/
|
||||
export async function ensureDirectory(dirPath: string): Promise<void> {
|
||||
await fs.mkdir(dirPath, { recursive: true })
|
||||
}
|
||||
|
|
|
|||
126
src/utils/modes.ts
Normal file
126
src/utils/modes.ts
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
import { ModeConfig } from "../shared/modes"
|
||||
import { ModeFileLocations } from "../shared/globalFileNames"
|
||||
import { fileExistsAtPath, ensureDirectory } from "./fs"
|
||||
import { readYamlFile, writeYamlFile } from "./yaml"
|
||||
import { getWorkspacePath } from "./path"
|
||||
|
||||
/**
|
||||
* Get the directory path for storing mode YAML files
|
||||
*
|
||||
* @param workspace - Whether to use workspace or global directory
|
||||
* @returns The path to the modes directory
|
||||
*/
|
||||
export async function getModesDirectory(workspace = false, globalStoragePath?: string): Promise<string> {
|
||||
if (workspace) {
|
||||
const workspaceRoot = getWorkspacePath()
|
||||
const modesDir = path.join(workspaceRoot, ModeFileLocations.MODES_DIRECTORY)
|
||||
await ensureDirectory(modesDir)
|
||||
return modesDir
|
||||
} else if (globalStoragePath) {
|
||||
const modesDir = path.join(globalStoragePath, "modes")
|
||||
await ensureDirectory(modesDir)
|
||||
return modesDir
|
||||
} else {
|
||||
throw new Error("Global storage path is required for global modes directory")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the file path for a specific mode's YAML file
|
||||
*
|
||||
* @param slug - The slug of the mode
|
||||
* @param workspace - Whether to use workspace or global directory
|
||||
* @returns The path to the mode YAML file
|
||||
*/
|
||||
export async function getModeYamlPath(slug: string, workspace = false, globalStoragePath?: string): Promise<string> {
|
||||
const modesDir = await getModesDirectory(workspace, globalStoragePath)
|
||||
return path.join(modesDir, `${slug}.yaml`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a mode from its YAML file
|
||||
*
|
||||
* @param slug - The slug of the mode to load
|
||||
* @param workspace - Whether to load from workspace or global directory
|
||||
* @returns The loaded mode or null if not found
|
||||
*/
|
||||
export async function loadModeFromYaml(
|
||||
slug: string,
|
||||
workspace = false,
|
||||
globalStoragePath?: string,
|
||||
): Promise<ModeConfig | null> {
|
||||
const yamlPath = await getModeYamlPath(slug, workspace, globalStoragePath)
|
||||
try {
|
||||
const exists = await fileExistsAtPath(yamlPath)
|
||||
if (!exists) return null
|
||||
|
||||
const modeData = await readYamlFile<Omit<ModeConfig, "source">>(yamlPath, {} as Omit<ModeConfig, "source">)
|
||||
if (modeData) {
|
||||
return {
|
||||
...modeData,
|
||||
source: workspace ? "project" : "global",
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to load mode from YAML: ${yamlPath}`, error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a mode to its YAML file
|
||||
*
|
||||
* @param mode - The mode to save
|
||||
* @param globalStoragePath - The path to global storage (required for global modes)
|
||||
*/
|
||||
export async function saveModeToYaml(mode: ModeConfig, globalStoragePath?: string): Promise<void> {
|
||||
const workspace = mode.source === "project"
|
||||
const yamlPath = await getModeYamlPath(mode.slug, workspace, globalStoragePath)
|
||||
|
||||
// Extract source and rules from the mode data
|
||||
const { source, ...modeData } = mode
|
||||
|
||||
await writeYamlFile(yamlPath, modeData)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all YAML mode files from a directory
|
||||
*
|
||||
* @param workspace - Whether to load from workspace or global directory
|
||||
* @returns Array of loaded modes
|
||||
*/
|
||||
export async function loadAllModesFromYaml(workspace: boolean, globalStoragePath?: string): Promise<ModeConfig[]> {
|
||||
const modes: ModeConfig[] = []
|
||||
try {
|
||||
const modesDir = await getModesDirectory(workspace, globalStoragePath)
|
||||
const exists = await fileExistsAtPath(modesDir)
|
||||
|
||||
if (exists) {
|
||||
const files = await fs.readdir(modesDir)
|
||||
for (const file of files) {
|
||||
if (file.endsWith(".yaml") || file.endsWith(".yml")) {
|
||||
const slug = path.basename(file, path.extname(file))
|
||||
const mode = await loadModeFromYaml(slug, workspace, globalStoragePath)
|
||||
if (mode) modes.push(mode)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error loading YAML modes from ${workspace ? "workspace" : "global"} directory:`, error)
|
||||
}
|
||||
|
||||
return modes
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate a mode from JSON to YAML format
|
||||
*
|
||||
* @param mode - The mode to migrate
|
||||
* @param globalStoragePath - The path to global storage (required for global modes)
|
||||
*/
|
||||
export async function migrateModeToYaml(mode: ModeConfig, globalStoragePath?: string): Promise<void> {
|
||||
// Simply save the mode to YAML format
|
||||
await saveModeToYaml(mode, globalStoragePath)
|
||||
}
|
||||
40
src/utils/yaml.ts
Normal file
40
src/utils/yaml.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import fs from "fs/promises"
|
||||
import yaml from "js-yaml"
|
||||
import { fileExistsAtPath } from "./fs"
|
||||
|
||||
/**
|
||||
* Read a YAML file and parse its contents
|
||||
* @param filePath Path to the YAML file
|
||||
* @param defaultValue Default value to return if the file doesn't exist or can't be parsed
|
||||
* @returns Parsed YAML content or the default value
|
||||
*/
|
||||
export async function readYamlFile<T>(filePath: string, defaultValue: T): Promise<T> {
|
||||
try {
|
||||
if (await fileExistsAtPath(filePath)) {
|
||||
const content = await fs.readFile(filePath, "utf-8")
|
||||
return yaml.load(content) as T
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error reading YAML file ${filePath}:`, error)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* Write data to a YAML file
|
||||
* @param filePath Path to the YAML file
|
||||
* @param data Data to write
|
||||
*/
|
||||
export async function writeYamlFile<T>(filePath: string, data: T): Promise<void> {
|
||||
try {
|
||||
const content = yaml.dump(data, {
|
||||
lineWidth: -1, // Don't wrap lines
|
||||
noRefs: true, // Don't use references
|
||||
indent: 2, // 2-space indentation
|
||||
})
|
||||
await fs.writeFile(filePath, content, "utf-8")
|
||||
} catch (error) {
|
||||
console.error(`Error writing YAML file ${filePath}:`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue