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
JavaScript
#!/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