UNPKG

rulesync

Version:

Unified AI rules management CLI tool that generates configuration files for various AI development tools

1,645 lines (1,610 loc) 324 kB
#!/usr/bin/env node // src/cli/index.ts import { Command } from "commander"; // src/constants/announcements.ts var ANNOUNCEMENT = "".trim(); // src/types/features.ts import { z } from "zod/mini"; var ALL_FEATURES = ["rules", "ignore", "mcp", "subagents", "commands", "skills"]; var ALL_FEATURES_WITH_WILDCARD = [...ALL_FEATURES, "*"]; var FeatureSchema = z.enum(ALL_FEATURES); var FeaturesSchema = z.array(FeatureSchema); var RulesyncFeaturesSchema = z.array(z.enum(ALL_FEATURES_WITH_WILDCARD)); // src/utils/error.ts import { ZodError } from "zod"; function isZodErrorLike(error) { return error !== null && typeof error === "object" && "issues" in error && Array.isArray(error.issues) && error.issues.every( (issue) => issue !== null && typeof issue === "object" && "path" in issue && Array.isArray(issue.path) && "message" in issue && typeof issue.message === "string" ); } function formatError(error) { if (error instanceof ZodError || isZodErrorLike(error)) { return `Zod raw error: ${JSON.stringify(error.issues)}`; } if (error instanceof Error) { return `${error.name}: ${error.message}`; } return String(error); } // src/utils/logger.ts import { consola } from "consola"; // src/utils/vitest.ts var isEnvTest = process.env.NODE_ENV === "test"; // src/utils/logger.ts var Logger = class { _verbose = false; console = consola.withDefaults({ tag: "rulesync" }); setVerbose(verbose) { this._verbose = verbose; } get verbose() { return this._verbose; } info(message, ...args) { if (isEnvTest) return; this.console.info(message, ...args); } // Success (always shown) success(message, ...args) { if (isEnvTest) return; this.console.success(message, ...args); } // Warning (always shown) warn(message, ...args) { if (isEnvTest) return; this.console.warn(message, ...args); } // Error (always shown) error(message, ...args) { if (isEnvTest) return; this.console.error(message, ...args); } // Debug level (shown only in verbose mode) debug(message, ...args) { if (isEnvTest) return; if (this._verbose) { this.console.info(message, ...args); } } }; var logger = new Logger(); // src/cli/commands/generate.ts import { intersection } from "es-toolkit"; // src/config/config-resolver.ts import { resolve as resolve2 } from "path"; import { parse as parseJsonc } from "jsonc-parser"; // src/utils/file.ts import { globSync } from "fs"; import { mkdir, readdir, readFile, rm, stat, writeFile } from "fs/promises"; import os from "os"; import { dirname, join, relative, resolve } from "path"; import { kebabCase } from "es-toolkit"; async function ensureDir(dirPath) { try { await stat(dirPath); } catch { await mkdir(dirPath, { recursive: true }); } } async function readOrInitializeFileContent(filePath, initialContent = "") { if (await fileExists(filePath)) { return await readFileContent(filePath); } else { await ensureDir(dirname(filePath)); await writeFileContent(filePath, initialContent); return initialContent; } } function checkPathTraversal({ relativePath, intendedRootDir }) { const segments = relativePath.split(/[/\\]/); if (segments.includes("..")) { throw new Error(`Path traversal detected: ${relativePath}`); } const resolved = resolve(intendedRootDir, relativePath); const rel = relative(intendedRootDir, resolved); if (rel.startsWith("..") || resolve(resolved) !== resolved) { throw new Error(`Path traversal detected: ${relativePath}`); } } function resolvePath(relativePath, baseDir) { if (!baseDir) return relativePath; checkPathTraversal({ relativePath, intendedRootDir: baseDir }); return resolve(baseDir, relativePath); } async function directoryExists(dirPath) { try { const stats = await stat(dirPath); return stats.isDirectory(); } catch { return false; } } async function readFileContent(filepath) { logger.debug(`Reading file: ${filepath}`); return readFile(filepath, "utf-8"); } async function readFileBuffer(filepath) { logger.debug(`Reading file buffer: ${filepath}`); return readFile(filepath); } function addTrailingNewline(content) { if (!content) { return "\n"; } return content.trimEnd() + "\n"; } async function writeFileContent(filepath, content) { logger.debug(`Writing file: ${filepath}`); await ensureDir(dirname(filepath)); await writeFile(filepath, content, "utf-8"); } async function fileExists(filepath) { try { await stat(filepath); return true; } catch { return false; } } async function listDirectoryFiles(dir) { try { return await readdir(dir); } catch { return []; } } async function findFilesByGlobs(globs, options = {}) { const { type = "all" } = options; const items = globSync(globs, { withFileTypes: true }); switch (type) { case "file": return items.filter((item) => item.isFile()).map((item) => join(item.parentPath, item.name)); case "dir": return items.filter((item) => item.isDirectory()).map((item) => join(item.parentPath, item.name)); case "all": return items.map((item) => join(item.parentPath, item.name)); default: throw new Error(`Invalid type: ${type}`); } } async function removeDirectory(dirPath) { const dangerousPaths = [".", "/", "~", "src", "node_modules"]; if (dangerousPaths.includes(dirPath) || dirPath === "") { logger.warn(`Skipping deletion of dangerous path: ${dirPath}`); return; } try { if (await fileExists(dirPath)) { await rm(dirPath, { recursive: true, force: true }); } } catch (error) { logger.warn(`Failed to remove directory ${dirPath}:`, error); } } async function removeFile(filepath) { logger.debug(`Removing file: ${filepath}`); try { if (await fileExists(filepath)) { await rm(filepath); } } catch (error) { logger.warn(`Failed to remove file ${filepath}:`, error); } } function getHomeDirectory() { if (isEnvTest) { throw new Error("getHomeDirectory() must be mocked in test environment"); } return os.homedir(); } function validateBaseDir(baseDir) { if (baseDir.trim() === "") { throw new Error("baseDir cannot be an empty string"); } checkPathTraversal({ relativePath: baseDir, intendedRootDir: process.cwd() }); } function toKebabCaseFilename(filename) { const lastDotIndex = filename.lastIndexOf("."); const extension = lastDotIndex > 0 ? filename.slice(lastDotIndex) : ""; const nameWithoutExt = lastDotIndex > 0 ? filename.slice(0, lastDotIndex) : filename; const kebabName = kebabCase(nameWithoutExt); return kebabName + extension; } // src/config/config.ts import { optional, z as z3 } from "zod/mini"; // src/types/tool-targets.ts import { z as z2 } from "zod/mini"; var ALL_TOOL_TARGETS = [ "agentsmd", "amazonqcli", "antigravity", "augmentcode", "augmentcode-legacy", "claudecode", "claudecode-legacy", "cline", "codexcli", "copilot", "cursor", "geminicli", "junie", "kiro", "opencode", "qwencode", "roo", "warp", "windsurf" ]; var ALL_TOOL_TARGETS_WITH_WILDCARD = [...ALL_TOOL_TARGETS, "*"]; var ToolTargetSchema = z2.enum(ALL_TOOL_TARGETS); var ToolTargetsSchema = z2.array(ToolTargetSchema); var RulesyncTargetsSchema = z2.array(z2.enum(ALL_TOOL_TARGETS_WITH_WILDCARD)); // src/config/config.ts var ConfigParamsSchema = z3.object({ baseDirs: z3.array(z3.string()), targets: RulesyncTargetsSchema, features: RulesyncFeaturesSchema, verbose: z3.boolean(), delete: z3.boolean(), // New non-experimental options global: optional(z3.boolean()), simulateCommands: optional(z3.boolean()), simulateSubagents: optional(z3.boolean()), simulateSkills: optional(z3.boolean()), modularMcp: optional(z3.boolean()) }); var PartialConfigParamsSchema = z3.partial(ConfigParamsSchema); var ConfigFileSchema = z3.object({ $schema: optional(z3.string()), ...z3.partial(ConfigParamsSchema).shape }); var RequiredConfigParamsSchema = z3.required(ConfigParamsSchema); var CONFLICTING_TARGET_PAIRS = [ ["augmentcode", "augmentcode-legacy"], ["claudecode", "claudecode-legacy"] ]; var LEGACY_TARGETS = ["augmentcode-legacy", "claudecode-legacy"]; var Config = class { baseDirs; targets; features; verbose; delete; global; simulateCommands; simulateSubagents; simulateSkills; modularMcp; constructor({ baseDirs, targets, features, verbose, delete: isDelete, global, simulateCommands, simulateSubagents, simulateSkills, modularMcp }) { this.validateConflictingTargets(targets); this.baseDirs = baseDirs; this.targets = targets; this.features = features; this.verbose = verbose; this.delete = isDelete; this.global = global ?? false; this.simulateCommands = simulateCommands ?? false; this.simulateSubagents = simulateSubagents ?? false; this.simulateSkills = simulateSkills ?? false; this.modularMcp = modularMcp ?? false; } validateConflictingTargets(targets) { for (const [target1, target2] of CONFLICTING_TARGET_PAIRS) { const hasTarget1 = targets.includes(target1); const hasTarget2 = targets.includes(target2); if (hasTarget1 && hasTarget2) { throw new Error( `Conflicting targets: '${target1}' and '${target2}' cannot be used together. Please choose one.` ); } } } getBaseDirs() { return this.baseDirs; } getTargets() { if (this.targets.includes("*")) { return ALL_TOOL_TARGETS.filter( // eslint-disable-next-line no-type-assertion/no-type-assertion (target) => !LEGACY_TARGETS.includes(target) ); } return this.targets.filter((target) => target !== "*"); } getFeatures() { if (this.features.includes("*")) { return [...ALL_FEATURES]; } return this.features.filter((feature) => feature !== "*"); } getVerbose() { return this.verbose; } getDelete() { return this.delete; } getGlobal() { return this.global; } getSimulateCommands() { return this.simulateCommands; } getSimulateSubagents() { return this.simulateSubagents; } getSimulateSkills() { return this.simulateSkills; } getModularMcp() { return this.modularMcp; } }; // src/config/config-resolver.ts var getDefaults = () => ({ targets: ["agentsmd"], features: ["rules"], verbose: false, delete: false, baseDirs: [process.cwd()], configPath: "rulesync.jsonc", global: false, simulateCommands: false, simulateSubagents: false, simulateSkills: false, modularMcp: false }); var ConfigResolver = class { static async resolve({ targets, features, verbose, delete: isDelete, baseDirs, configPath = getDefaults().configPath, global, simulateCommands, simulateSubagents, simulateSkills, modularMcp }) { const validatedConfigPath = resolvePath(configPath, process.cwd()); let configByFile = {}; if (await fileExists(validatedConfigPath)) { try { const fileContent = await readFileContent(validatedConfigPath); const jsonData = parseJsonc(fileContent); const parsed = ConfigFileSchema.parse(jsonData); const { $schema: _schema, ...configParams2 } = parsed; configByFile = configParams2; } catch (error) { logger.error(`Failed to load config file: ${formatError(error)}`); throw error; } } const resolvedGlobal = global ?? configByFile.global ?? getDefaults().global; const resolvedSimulateCommands = simulateCommands ?? configByFile.simulateCommands ?? getDefaults().simulateCommands; const resolvedSimulateSubagents = simulateSubagents ?? configByFile.simulateSubagents ?? getDefaults().simulateSubagents; const resolvedSimulateSkills = simulateSkills ?? configByFile.simulateSkills ?? getDefaults().simulateSkills; const configParams = { targets: targets ?? configByFile.targets ?? getDefaults().targets, features: features ?? configByFile.features ?? getDefaults().features, verbose: verbose ?? configByFile.verbose ?? getDefaults().verbose, delete: isDelete ?? configByFile.delete ?? getDefaults().delete, baseDirs: getBaseDirsInLightOfGlobal({ baseDirs: baseDirs ?? configByFile.baseDirs ?? getDefaults().baseDirs, global: resolvedGlobal }), global: resolvedGlobal, simulateCommands: resolvedSimulateCommands, simulateSubagents: resolvedSimulateSubagents, simulateSkills: resolvedSimulateSkills, modularMcp: modularMcp ?? configByFile.modularMcp ?? getDefaults().modularMcp }; return new Config(configParams); } }; function getBaseDirsInLightOfGlobal({ baseDirs, global }) { if (global) { return [getHomeDirectory()]; } const resolvedBaseDirs = baseDirs.map((baseDir) => resolve2(baseDir)); resolvedBaseDirs.forEach((baseDir) => { validateBaseDir(baseDir); }); return resolvedBaseDirs; } // src/constants/rulesync-paths.ts import { join as join2 } from "path"; var RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc"; var RULESYNC_RELATIVE_DIR_PATH = ".rulesync"; var RULESYNC_RULES_RELATIVE_DIR_PATH = join2(RULESYNC_RELATIVE_DIR_PATH, "rules"); var RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join2(RULESYNC_RELATIVE_DIR_PATH, "commands"); var RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join2(RULESYNC_RELATIVE_DIR_PATH, "subagents"); var RULESYNC_MCP_RELATIVE_FILE_PATH = join2(RULESYNC_RELATIVE_DIR_PATH, "mcp.json"); var RULESYNC_AIIGNORE_FILE_NAME = ".aiignore"; var RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join2(RULESYNC_RELATIVE_DIR_PATH, ".aiignore"); var RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore"; var RULESYNC_OVERVIEW_FILE_NAME = "overview.md"; var RULESYNC_SKILLS_RELATIVE_DIR_PATH = join2(RULESYNC_RELATIVE_DIR_PATH, "skills"); // src/features/commands/commands-processor.ts import { basename as basename12, join as join14 } from "path"; import { z as z12 } from "zod/mini"; // src/types/feature-processor.ts var FeatureProcessor = class { baseDir; constructor({ baseDir = process.cwd() }) { this.baseDir = baseDir; } /** * Return tool targets that this feature supports. */ static getToolTargets(_params = {}) { throw new Error("Not implemented"); } /** * Once converted to rulesync/tool files, write them to the filesystem. * Returns the number of files written. */ async writeAiFiles(aiFiles) { for (const aiFile of aiFiles) { const contentWithNewline = addTrailingNewline(aiFile.getFileContent()); await writeFileContent(aiFile.getFilePath(), contentWithNewline); } return aiFiles.length; } async removeAiFiles(aiFiles) { for (const aiFile of aiFiles) { await removeFile(aiFile.getFilePath()); } } }; // src/features/commands/agentsmd-command.ts import { basename as basename2, join as join4 } from "path"; // src/utils/frontmatter.ts import matter from "gray-matter"; function isPlainObject(value) { if (value === null || typeof value !== "object") return false; const prototype = Object.getPrototypeOf(value); return prototype === Object.prototype || prototype === null; } function deepRemoveNullishValue(value) { if (value === null || value === void 0) { return void 0; } if (Array.isArray(value)) { const cleanedArray = value.map((item) => deepRemoveNullishValue(item)).filter((item) => item !== void 0); return cleanedArray; } if (isPlainObject(value)) { const result = {}; for (const [key, val] of Object.entries(value)) { const cleaned = deepRemoveNullishValue(val); if (cleaned !== void 0) { result[key] = cleaned; } } return result; } return value; } function deepRemoveNullishObject(obj) { if (!obj || typeof obj !== "object") { return {}; } const result = {}; for (const [key, val] of Object.entries(obj)) { const cleaned = deepRemoveNullishValue(val); if (cleaned !== void 0) { result[key] = cleaned; } } return result; } function stringifyFrontmatter(body, frontmatter) { const cleanFrontmatter = deepRemoveNullishObject(frontmatter); return matter.stringify(body, cleanFrontmatter); } function parseFrontmatter(content) { const { data: frontmatter, content: body } = matter(content); return { frontmatter, body }; } // src/features/commands/simulated-command.ts import { basename, join as join3 } from "path"; import { z as z4 } from "zod/mini"; // src/types/ai-file.ts import path, { relative as relative2, resolve as resolve3 } from "path"; var AiFile = class { /** * @example "." */ baseDir; /** * @example ".claude/agents" */ relativeDirPath; /** * @example "planner.md" */ relativeFilePath; /** * Whole raw file content */ fileContent; /** * @example true */ global; constructor({ baseDir = process.cwd(), relativeDirPath, relativeFilePath, fileContent, global = false }) { this.baseDir = baseDir; this.relativeDirPath = relativeDirPath; this.relativeFilePath = relativeFilePath; this.fileContent = fileContent; this.global = global; } static async fromFile(_params) { throw new Error("Please implement this method in the subclass."); } getBaseDir() { return this.baseDir; } getRelativeDirPath() { return this.relativeDirPath; } getRelativeFilePath() { return this.relativeFilePath; } getFilePath() { const fullPath = path.join(this.baseDir, this.relativeDirPath, this.relativeFilePath); const resolvedFull = resolve3(fullPath); const resolvedBase = resolve3(this.baseDir); const rel = relative2(resolvedBase, resolvedFull); if (rel.startsWith("..") || path.isAbsolute(rel)) { throw new Error( `Path traversal detected: Final path escapes baseDir. baseDir="${this.baseDir}", relativeDirPath="${this.relativeDirPath}", relativeFilePath="${this.relativeFilePath}"` ); } return fullPath; } getFileContent() { return this.fileContent; } getRelativePathFromCwd() { return path.join(this.relativeDirPath, this.relativeFilePath); } setFileContent(newFileContent) { this.fileContent = newFileContent; } /** * Returns whether this file can be deleted by rulesync. * Override in subclasses that should not be deleted (e.g., user-managed config files). */ isDeletable() { return true; } }; // src/features/commands/tool-command.ts var ToolCommand = class extends AiFile { static getSettablePaths() { throw new Error("Please implement this method in the subclass."); } /** * Load a command from a tool-specific file path. * * This method should: * 1. Read the file content * 2. Parse tool-specific frontmatter format * 3. Validate the parsed data * 4. Return a concrete ToolCommand instance * * @param params - Parameters including the file path to load * @returns Promise resolving to a concrete ToolCommand instance */ static async fromFile(_params) { throw new Error("Please implement this method in the subclass."); } /** * Convert a RulesyncCommand to the tool-specific command format. * * This method should: * 1. Extract relevant data from the RulesyncCommand * 2. Transform frontmatter to tool-specific format * 3. Transform body content if needed * 4. Return a concrete ToolCommand instance * * @param params - Parameters including the RulesyncCommand to convert * @returns A concrete ToolCommand instance */ static fromRulesyncCommand(_params) { throw new Error("Please implement this method in the subclass."); } /** * Check if this tool is targeted by a RulesyncCommand based on its targets field. * Subclasses should override this to provide specific targeting logic. * * @param rulesyncCommand - The RulesyncCommand to check * @returns True if this tool is targeted by the command */ static isTargetedByRulesyncCommand(_rulesyncCommand) { throw new Error("Please implement this method in the subclass."); } /** * Default implementation for checking if a tool is targeted by a RulesyncCommand. * Checks if the command's targets include the tool target or a wildcard. * * @param params - Parameters including the RulesyncCommand and tool target * @returns True if the tool target is included in the command's targets */ static isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget }) { const targets = rulesyncCommand.getFrontmatter().targets; if (!targets) { return true; } if (targets.includes("*")) { return true; } if (targets.includes(toolTarget)) { return true; } return false; } }; // src/features/commands/simulated-command.ts var SimulatedCommandFrontmatterSchema = z4.object({ description: z4.string() }); var SimulatedCommand = class _SimulatedCommand extends ToolCommand { frontmatter; body; constructor({ frontmatter, body, ...rest }) { if (rest.validate) { const result = SimulatedCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error( `Invalid frontmatter in ${join3(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}` ); } } super({ ...rest, fileContent: stringifyFrontmatter(body, frontmatter) }); this.frontmatter = frontmatter; this.body = body; } getBody() { return this.body; } getFrontmatter() { return this.frontmatter; } toRulesyncCommand() { throw new Error("Not implemented because it is a SIMULATED file."); } static fromRulesyncCommandDefault({ baseDir = process.cwd(), rulesyncCommand, validate = true }) { const rulesyncFrontmatter = rulesyncCommand.getFrontmatter(); const claudecodeFrontmatter = { description: rulesyncFrontmatter.description }; const body = rulesyncCommand.getBody(); return { baseDir, frontmatter: claudecodeFrontmatter, body, relativeDirPath: this.getSettablePaths().relativeDirPath, relativeFilePath: rulesyncCommand.getRelativeFilePath(), validate }; } validate() { if (!this.frontmatter) { return { success: true, error: null }; } const result = SimulatedCommandFrontmatterSchema.safeParse(this.frontmatter); if (result.success) { return { success: true, error: null }; } else { return { success: false, error: new Error( `Invalid frontmatter in ${join3(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}` ) }; } } static async fromFileDefault({ baseDir = process.cwd(), relativeFilePath, validate = true }) { const filePath = join3( baseDir, _SimulatedCommand.getSettablePaths().relativeDirPath, relativeFilePath ); const fileContent = await readFileContent(filePath); const { frontmatter, body: content } = parseFrontmatter(fileContent); const result = SimulatedCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`); } return { baseDir, relativeDirPath: _SimulatedCommand.getSettablePaths().relativeDirPath, relativeFilePath: basename(relativeFilePath), frontmatter: result.data, body: content.trim(), validate }; } }; // src/features/commands/agentsmd-command.ts var AgentsmdCommand = class _AgentsmdCommand extends SimulatedCommand { static getSettablePaths() { return { relativeDirPath: join4(".agents", "commands") }; } static fromRulesyncCommand({ baseDir = process.cwd(), rulesyncCommand, validate = true }) { return new _AgentsmdCommand( this.fromRulesyncCommandDefault({ baseDir, rulesyncCommand, validate }) ); } static async fromFile({ baseDir = process.cwd(), relativeFilePath, validate = true }) { const filePath = join4( baseDir, _AgentsmdCommand.getSettablePaths().relativeDirPath, relativeFilePath ); const fileContent = await readFileContent(filePath); const { frontmatter, body: content } = parseFrontmatter(fileContent); const result = SimulatedCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`); } return new _AgentsmdCommand({ baseDir, relativeDirPath: _AgentsmdCommand.getSettablePaths().relativeDirPath, relativeFilePath: basename2(relativeFilePath), frontmatter: result.data, body: content.trim(), validate }); } static isTargetedByRulesyncCommand(rulesyncCommand) { return this.isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget: "agentsmd" }); } }; // src/features/commands/antigravity-command.ts import { basename as basename4, join as join6 } from "path"; import { z as z6 } from "zod/mini"; // src/features/commands/rulesync-command.ts import { basename as basename3, join as join5 } from "path"; import { z as z5 } from "zod/mini"; // src/types/rulesync-file.ts var RulesyncFile = class extends AiFile { static async fromFile(_params) { throw new Error("Please implement this method in the subclass."); } static async fromFileLegacy(_params) { throw new Error("Please implement this method in the subclass."); } }; // src/features/commands/rulesync-command.ts var RulesyncCommandFrontmatterSchema = z5.looseObject({ targets: RulesyncTargetsSchema, description: z5.string() }); var RulesyncCommand = class _RulesyncCommand extends RulesyncFile { frontmatter; body; constructor({ frontmatter, body, ...rest }) { if (rest.validate) { const result = RulesyncCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error( `Invalid frontmatter in ${join5(rest.baseDir ?? process.cwd(), rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}` ); } } super({ ...rest, fileContent: stringifyFrontmatter(body, frontmatter) }); this.frontmatter = frontmatter; this.body = body; } static getSettablePaths() { return { relativeDirPath: RULESYNC_COMMANDS_RELATIVE_DIR_PATH }; } getFrontmatter() { return this.frontmatter; } getBody() { return this.body; } validate() { if (!this.frontmatter) { return { success: true, error: null }; } const result = RulesyncCommandFrontmatterSchema.safeParse(this.frontmatter); if (result.success) { return { success: true, error: null }; } else { return { success: false, error: new Error( `Invalid frontmatter in ${join5(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}` ) }; } } static async fromFile({ relativeFilePath }) { const filePath = join5( process.cwd(), _RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath ); const fileContent = await readFileContent(filePath); const { frontmatter, body: content } = parseFrontmatter(fileContent); const result = RulesyncCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error(`Invalid frontmatter in ${relativeFilePath}: ${formatError(result.error)}`); } const filename = basename3(relativeFilePath); return new _RulesyncCommand({ baseDir: process.cwd(), relativeDirPath: _RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath: filename, frontmatter: result.data, body: content.trim(), fileContent }); } }; // src/features/commands/antigravity-command.ts var AntigravityCommandFrontmatterSchema = z6.object({ description: z6.string() }); var AntigravityCommand = class _AntigravityCommand extends ToolCommand { frontmatter; body; static getSettablePaths() { return { relativeDirPath: join6(".agent", "workflows") }; } constructor({ frontmatter, body, ...rest }) { if (rest.validate) { const result = AntigravityCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error( `Invalid frontmatter in ${join6(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}` ); } } super({ ...rest, fileContent: stringifyFrontmatter(body, frontmatter) }); this.frontmatter = frontmatter; this.body = body; } getBody() { return this.body; } getFrontmatter() { return this.frontmatter; } toRulesyncCommand() { const rulesyncFrontmatter = { targets: ["antigravity"], description: this.frontmatter.description }; const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter); return new RulesyncCommand({ baseDir: ".", // RulesyncCommand baseDir is always the project root directory frontmatter: rulesyncFrontmatter, body: this.body, relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath: this.relativeFilePath, fileContent, validate: true }); } static fromRulesyncCommand({ baseDir = process.cwd(), rulesyncCommand, validate = true }) { const rulesyncFrontmatter = rulesyncCommand.getFrontmatter(); const antigravityFrontmatter = { description: rulesyncFrontmatter.description }; const body = rulesyncCommand.getBody(); const fileContent = stringifyFrontmatter(body, antigravityFrontmatter); return new _AntigravityCommand({ baseDir, frontmatter: antigravityFrontmatter, body, relativeDirPath: _AntigravityCommand.getSettablePaths().relativeDirPath, relativeFilePath: rulesyncCommand.getRelativeFilePath(), fileContent, validate }); } validate() { if (!this.frontmatter) { return { success: true, error: null }; } const result = AntigravityCommandFrontmatterSchema.safeParse(this.frontmatter); if (result.success) { return { success: true, error: null }; } else { return { success: false, error: new Error( `Invalid frontmatter in ${join6(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}` ) }; } } static isTargetedByRulesyncCommand(rulesyncCommand) { return this.isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget: "antigravity" }); } static async fromFile({ baseDir = process.cwd(), relativeFilePath, validate = true }) { const filePath = join6( baseDir, _AntigravityCommand.getSettablePaths().relativeDirPath, relativeFilePath ); const fileContent = await readFileContent(filePath); const { frontmatter, body: content } = parseFrontmatter(fileContent); const result = AntigravityCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`); } return new _AntigravityCommand({ baseDir, relativeDirPath: _AntigravityCommand.getSettablePaths().relativeDirPath, relativeFilePath: basename4(relativeFilePath), frontmatter: result.data, body: content.trim(), fileContent, validate }); } }; // src/features/commands/claudecode-command.ts import { basename as basename5, join as join7 } from "path"; import { z as z7 } from "zod/mini"; var ClaudecodeCommandFrontmatterSchema = z7.looseObject({ description: z7.string(), "allowed-tools": z7.optional(z7.union([z7.string(), z7.array(z7.string())])), "argument-hint": z7.optional(z7.string()), model: z7.optional(z7.string()), "disable-model-invocation": z7.optional(z7.boolean()) }); var ClaudecodeCommand = class _ClaudecodeCommand extends ToolCommand { frontmatter; body; constructor({ frontmatter, body, ...rest }) { if (rest.validate) { const result = ClaudecodeCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error( `Invalid frontmatter in ${join7(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}` ); } } super({ ...rest, fileContent: stringifyFrontmatter(body, frontmatter) }); this.frontmatter = frontmatter; this.body = body; } static getSettablePaths(_options = {}) { return { relativeDirPath: join7(".claude", "commands") }; } getBody() { return this.body; } getFrontmatter() { return this.frontmatter; } toRulesyncCommand() { const { description, ...restFields } = this.frontmatter; const rulesyncFrontmatter = { targets: ["*"], description, // Preserve extra fields in claudecode section ...Object.keys(restFields).length > 0 && { claudecode: restFields } }; const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter); return new RulesyncCommand({ baseDir: ".", // RulesyncCommand baseDir is always the project root directory frontmatter: rulesyncFrontmatter, body: this.body, relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath: this.relativeFilePath, fileContent, validate: true }); } static fromRulesyncCommand({ baseDir = process.cwd(), rulesyncCommand, validate = true, global = false }) { const rulesyncFrontmatter = rulesyncCommand.getFrontmatter(); const claudecodeFields = rulesyncFrontmatter.claudecode ?? {}; const claudecodeFrontmatter = { description: rulesyncFrontmatter.description, ...claudecodeFields }; const body = rulesyncCommand.getBody(); const paths = this.getSettablePaths({ global }); return new _ClaudecodeCommand({ baseDir, frontmatter: claudecodeFrontmatter, body, relativeDirPath: paths.relativeDirPath, relativeFilePath: rulesyncCommand.getRelativeFilePath(), validate }); } validate() { if (!this.frontmatter) { return { success: true, error: null }; } const result = ClaudecodeCommandFrontmatterSchema.safeParse(this.frontmatter); if (result.success) { return { success: true, error: null }; } else { return { success: false, error: new Error( `Invalid frontmatter in ${join7(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}` ) }; } } static isTargetedByRulesyncCommand(rulesyncCommand) { return this.isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget: "claudecode" }); } static async fromFile({ baseDir = process.cwd(), relativeFilePath, validate = true, global = false }) { const paths = this.getSettablePaths({ global }); const filePath = join7(baseDir, paths.relativeDirPath, relativeFilePath); const fileContent = await readFileContent(filePath); const { frontmatter, body: content } = parseFrontmatter(fileContent); const result = ClaudecodeCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`); } return new _ClaudecodeCommand({ baseDir, relativeDirPath: paths.relativeDirPath, relativeFilePath: basename5(relativeFilePath), frontmatter: result.data, body: content.trim(), validate }); } }; // src/features/commands/codexcli-command.ts import { basename as basename6, join as join8 } from "path"; var CodexcliCommand = class _CodexcliCommand extends ToolCommand { static getSettablePaths({ global } = {}) { if (!global) { throw new Error("CodexcliCommand only supports global mode. Please pass { global: true }."); } return { relativeDirPath: join8(".codex", "prompts") }; } toRulesyncCommand() { const rulesyncFrontmatter = { targets: ["*"], description: "" }; return new RulesyncCommand({ baseDir: ".", // RulesyncCommand baseDir is always the project root directory frontmatter: rulesyncFrontmatter, body: this.getFileContent(), relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath: this.relativeFilePath, fileContent: this.getFileContent(), validate: true }); } static fromRulesyncCommand({ baseDir = process.cwd(), rulesyncCommand, validate = true, global = false }) { const paths = this.getSettablePaths({ global }); return new _CodexcliCommand({ baseDir, fileContent: rulesyncCommand.getBody(), relativeDirPath: paths.relativeDirPath, relativeFilePath: rulesyncCommand.getRelativeFilePath(), validate }); } validate() { return { success: true, error: null }; } getBody() { return this.getFileContent(); } static isTargetedByRulesyncCommand(rulesyncCommand) { return this.isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget: "codexcli" }); } static async fromFile({ baseDir = process.cwd(), relativeFilePath, validate = true, global = false }) { const paths = this.getSettablePaths({ global }); const filePath = join8(baseDir, paths.relativeDirPath, relativeFilePath); const fileContent = await readFileContent(filePath); const { body: content } = parseFrontmatter(fileContent); return new _CodexcliCommand({ baseDir, relativeDirPath: paths.relativeDirPath, relativeFilePath: basename6(relativeFilePath), fileContent: content.trim(), validate }); } }; // src/features/commands/copilot-command.ts import { basename as basename7, join as join9 } from "path"; import { z as z8 } from "zod/mini"; var CopilotCommandFrontmatterSchema = z8.looseObject({ mode: z8.literal("agent"), description: z8.string() }); var CopilotCommand = class _CopilotCommand extends ToolCommand { frontmatter; body; constructor({ frontmatter, body, ...rest }) { if (rest.validate) { const result = CopilotCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error( `Invalid frontmatter in ${join9(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}` ); } } super({ ...rest, fileContent: stringifyFrontmatter(body, frontmatter) }); this.frontmatter = frontmatter; this.body = body; } static getSettablePaths() { return { relativeDirPath: join9(".github", "prompts") }; } getBody() { return this.body; } getFrontmatter() { return this.frontmatter; } toRulesyncCommand() { const { mode: _mode, description, ...restFields } = this.frontmatter; const rulesyncFrontmatter = { targets: ["*"], description, // Preserve extra fields in copilot section (excluding mode which is fixed) ...Object.keys(restFields).length > 0 && { copilot: restFields } }; const originalFilePath = this.relativeFilePath; const relativeFilePath = originalFilePath.replace(/\.prompt\.md$/, ".md"); return new RulesyncCommand({ baseDir: this.baseDir, frontmatter: rulesyncFrontmatter, body: this.body, relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath, fileContent: this.getFileContent(), validate: true }); } validate() { if (!this.frontmatter) { return { success: true, error: null }; } const result = CopilotCommandFrontmatterSchema.safeParse(this.frontmatter); if (result.success) { return { success: true, error: null }; } else { return { success: false, error: new Error( `Invalid frontmatter in ${join9(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}` ) }; } } static fromRulesyncCommand({ baseDir = process.cwd(), rulesyncCommand, validate = true }) { const paths = this.getSettablePaths(); const rulesyncFrontmatter = rulesyncCommand.getFrontmatter(); const copilotFields = rulesyncFrontmatter.copilot ?? {}; const copilotFrontmatter = { mode: "agent", description: rulesyncFrontmatter.description, ...copilotFields }; const body = rulesyncCommand.getBody(); const originalFilePath = rulesyncCommand.getRelativeFilePath(); const relativeFilePath = originalFilePath.replace(/\.md$/, ".prompt.md"); return new _CopilotCommand({ baseDir, frontmatter: copilotFrontmatter, body, relativeDirPath: paths.relativeDirPath, relativeFilePath, validate }); } static async fromFile({ baseDir = process.cwd(), relativeFilePath, validate = true }) { const paths = this.getSettablePaths(); const filePath = join9(baseDir, paths.relativeDirPath, relativeFilePath); const fileContent = await readFileContent(filePath); const { frontmatter, body: content } = parseFrontmatter(fileContent); const result = CopilotCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`); } return new _CopilotCommand({ baseDir, relativeDirPath: paths.relativeDirPath, relativeFilePath: basename7(relativeFilePath), frontmatter: result.data, body: content.trim(), validate }); } static isTargetedByRulesyncCommand(rulesyncCommand) { return this.isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget: "copilot" }); } }; // src/features/commands/cursor-command.ts import { basename as basename8, join as join10 } from "path"; var CursorCommand = class _CursorCommand extends ToolCommand { static getSettablePaths(_options = {}) { return { relativeDirPath: join10(".cursor", "commands") }; } toRulesyncCommand() { const rulesyncFrontmatter = { targets: ["*"], description: "" }; return new RulesyncCommand({ baseDir: process.cwd(), // RulesyncCommand baseDir is always the project root directory frontmatter: rulesyncFrontmatter, body: this.getFileContent(), relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath: this.relativeFilePath, fileContent: this.getFileContent(), validate: true }); } static fromRulesyncCommand({ baseDir = process.cwd(), rulesyncCommand, validate = true, global = false }) { const paths = this.getSettablePaths({ global }); return new _CursorCommand({ baseDir, fileContent: rulesyncCommand.getBody(), relativeDirPath: paths.relativeDirPath, relativeFilePath: rulesyncCommand.getRelativeFilePath(), validate }); } validate() { return { success: true, error: null }; } getBody() { return this.getFileContent(); } static isTargetedByRulesyncCommand(rulesyncCommand) { return this.isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget: "cursor" }); } static async fromFile({ baseDir = process.cwd(), relativeFilePath, validate = true, global = false }) { const paths = this.getSettablePaths({ global }); const filePath = join10(baseDir, paths.relativeDirPath, relativeFilePath); const fileContent = await readFileContent(filePath); const { body: content } = parseFrontmatter(fileContent); return new _CursorCommand({ baseDir, relativeDirPath: paths.relativeDirPath, relativeFilePath: basename8(relativeFilePath), fileContent: content.trim(), validate }); } }; // src/features/commands/geminicli-command.ts import { basename as basename9, join as join11 } from "path"; import { parse as parseToml } from "smol-toml"; import { z as z9 } from "zod/mini"; var GeminiCliCommandFrontmatterSchema = z9.looseObject({ description: z9.optional(z9.string()), prompt: z9.string() }); var GeminiCliCommand = class _GeminiCliCommand extends ToolCommand { frontmatter; body; constructor(params) { super(params); const parsed = this.parseTomlContent(this.fileContent); this.frontmatter = parsed; this.body = parsed.prompt; } static getSettablePaths(_options = {}) { return { relativeDirPath: join11(".gemini", "commands") }; } parseTomlContent(content) { try { const parsed = parseToml(content); const result = GeminiCliCommandFrontmatterSchema.safeParse(parsed); if (!result.success) { throw new Error( `Invalid frontmatter in Gemini CLI command file: ${formatError(result.error)}` ); } return { ...result.data, description: result.data.description || "" }; } catch (error) { throw new Error(`Failed to parse TOML command file: ${error}`, { cause: error }); } } getBody() { return this.body; } getFrontmatter() { return { description: this.frontmatter.description, prompt: this.frontmatter.prompt }; } toRulesyncCommand() { const { description, prompt: _prompt, ...restFields } = this.frontmatter; const rulesyncFrontmatter = { targets: ["geminicli"], description: description ?? "", // Preserve extra fields in geminicli section (excluding prompt which is the body) ...Object.keys(restFields).length > 0 && { geminicli: restFields } }; const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter); return new RulesyncCommand({ baseDir: process.cwd(), // RulesyncCommand baseDir is always the project root directory frontmatter: rulesyncFrontmatter, body: this.body, relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath: this.relativeFilePath, fileContent, validate: true }); } static fromRulesyncCommand({ baseDir = process.cwd(), rulesyncCommand, validate = true, global = false }) { const rulesyncFrontmatter = rulesyncCommand.getFrontmatter(); const geminicliFields = rulesyncFrontmatter.geminicli ?? {}; const geminiFrontmatter = { description: rulesyncFrontmatter.description, prompt: rulesyncCommand.getBody(), ...geminicliFields }; const tomlContent = `description = "${geminiFrontmatter.description}" prompt = """ ${geminiFrontmatter.prompt} """`; const paths = this.getSettablePaths({ global }); return new _GeminiCliCommand({ baseDir, relativeDirPath: paths.relativeDirPath, relativeFilePath: rulesyncCommand.getRelativeFilePath().replace(".md", ".toml"), fileContent: tomlContent, validate }); } static async fromFile({ baseDir = process.cwd(), relativeFilePath, validate = true, global = false }) { const paths = this.getSettablePaths({ global }); const filePath = join11(baseDir, paths.relativeDirPath, relativeFilePath); const fileContent = await readFileContent(filePath); return new _GeminiCliCommand({ baseDir, relativeDirPath: paths.relativeDirPath, relativeFilePath: basename9(relativeFilePath), fileContent, validate }); } validate() { try { this.parseTomlContent(this.fileContent); return { success: true, error: null }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(String(error)) }; } } static isTargetedByRulesyncCommand(rulesyncCommand) { return this.isTargetedByRulesyncCommandDefault({ rulesyncCommand, toolTarget: "geminicli" }); } }; // src/features/commands/opencode-command.ts import { basename as basename10, join as join12 } from "path"; import { optional as optional2, z as z10 } from "zod/mini"; var OpenCodeCommandFrontmatterSchema = z10.looseObject({ description: z10.string(), agent: optional2(z10.string()), subtask: optional2(z10.boolean()), model: optional2(z10.string()) }); var OpenCodeCommand = class _OpenCodeCommand extends ToolCommand { frontmatter; body; constructor({ frontmatter, body, ...rest }) { if (rest.validate) { const result = OpenCodeCommandFrontmatterSchema.safeParse(frontmatter); if (!result.success) { throw new Error( `Invalid frontmatter in ${join12(rest.relativeDirPath, rest.relativeFilePath)}: ${formatError(result.error)}` ); } } super({ ...rest, fileContent: stringifyFrontmatter(body, frontmatter) }); this.frontmatter = frontmatter; this.body = body; } static getSettablePaths({ global } = {}) { return { relativeDirPath: global ? join12(".config", "opencode", "command") : join12(".opencode", "command") }; } getBody() { return this.body; } getFrontmatter() { return this.frontmatter; } toRulesyncCommand() { const { description, ...restFields } = this.frontmatter; const rulesyncFrontmatter = { targets: ["*"], description, ...Object.keys(restFields).length > 0 && { opencode: restFields } }; const fileContent = stringifyFrontmatter(this.body, rulesyncFrontmatter); return new RulesyncCommand({ baseDir: process.cwd(), frontmatter: rulesyncFrontmatter, body: this.body, relativeDirPath: RulesyncCommand.getSettablePaths().relativeDirPath, relativeFilePath: this.relativeFilePath, fileCo