Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/cli/commands/status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,11 @@ export async function statusCommand(): Promise<void> {
rule.frontmatter.targets[0] === "*" ? config.defaultTargets : rule.frontmatter.targets;

for (const target of targets) {
if (target in targetCounts) {
targetCounts[target as keyof typeof targetCounts]++;
}
if (target === "copilot") targetCounts.copilot++;
else if (target === "cursor") targetCounts.cursor++;
else if (target === "cline") targetCounts.cline++;
else if (target === "claudecode") targetCounts.claudecode++;
else if (target === "roo") targetCounts.roo++;
}
}

Expand Down
12 changes: 9 additions & 3 deletions src/core/mcp-parser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as fs from "node:fs";
import * as path from "node:path";
import type { RulesyncMcpConfig } from "../types/mcp.js";
import type { RulesyncMcpConfig, RulesyncMcpServer } from "../types/mcp.js";

export function parseMcpConfig(projectRoot: string): RulesyncMcpConfig | null {
const mcpPath = path.join(projectRoot, ".rulesync", ".mcp.json");
Expand All @@ -11,7 +11,13 @@ export function parseMcpConfig(projectRoot: string): RulesyncMcpConfig | null {

try {
const content = fs.readFileSync(mcpPath, "utf-8");
const rawConfig = JSON.parse(content) as Record<string, unknown>;
const parsed = JSON.parse(content);

if (!parsed || typeof parsed !== "object") {
throw new Error("Invalid mcp.json: must be an object");
}

const rawConfig = parsed as Record<string, unknown>;

// Handle legacy 'servers' field and migrate to 'mcpServers'
if (rawConfig.servers && !rawConfig.mcpServers) {
Expand All @@ -28,7 +34,7 @@ export function parseMcpConfig(projectRoot: string): RulesyncMcpConfig | null {
delete rawConfig.tools;
}

return { mcpServers: rawConfig.mcpServers } as RulesyncMcpConfig;
return { mcpServers: rawConfig.mcpServers as Record<string, RulesyncMcpServer> };
} catch (error) {
throw new Error(
`Failed to parse mcp.json: ${error instanceof Error ? error.message : String(error)}`,
Expand Down
1 change: 1 addition & 0 deletions src/core/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export async function parseRuleFile(filepath: string): Promise<ParsedRule> {
// Validate frontmatter
validateFrontmatter(parsed.data, filepath);

// After validation, we can safely cast since validateFrontmatter ensures the shape
const frontmatter = parsed.data as RuleFrontmatter;
const filename = basename(filepath, ".md");

Expand Down
4 changes: 3 additions & 1 deletion src/generators/mcp/claudecode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export function generateClaudeMcpConfiguration(

// Only add transport if it's supported by Claude
if (transport && transport !== "stdio") {
claudeServer.transport = transport as "sse" | "http";
if (transport === "sse" || transport === "http") {
claudeServer.transport = transport;
}
}

settings.mcpServers![serverName] = claudeServer;
Expand Down
2 changes: 1 addition & 1 deletion src/generators/mcp/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export function generateCursorMcpConfiguration(
continue;
}

// Cast to RulesyncMcpServer for type safety
// Cast to RulesyncMcpServer after type check
const serverObj = server as RulesyncMcpServer;

// Check if this server should be included for cursor
Expand Down
2 changes: 1 addition & 1 deletion src/generators/mcp/roo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export function generateRooMcpConfiguration(
continue;
}

// Cast to RulesyncMcpServer for type safety
// Cast to RulesyncMcpServer after type check
const serverObj = server as RulesyncMcpServer;

// Check if this server should be included for roo
Expand Down
10 changes: 7 additions & 3 deletions src/parsers/claudecode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,13 @@ async function parseClaudeSettings(settingsPath: string): Promise<ClaudeSettings

// Extract ignore patterns from permissions.deny
if (typeof settings === "object" && settings !== null && "permissions" in settings) {
const permissions = settings.permissions as Record<string, unknown>;
if (permissions && "deny" in permissions && Array.isArray(permissions.deny)) {
const readPatterns = permissions.deny
const permissions = settings.permissions;
if (typeof permissions !== "object" || permissions === null) {
return { errors };
}
const permsObj = permissions as Record<string, unknown>;
if (permsObj && "deny" in permsObj && Array.isArray(permsObj.deny)) {
const readPatterns = permsObj.deny
.filter(
(rule): rule is string =>
typeof rule === "string" && rule.startsWith("Read(") && rule.endsWith(")"),
Expand Down
16 changes: 13 additions & 3 deletions src/parsers/cursor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ const customMatterOptions = {
// But exclude array literals (starting with [ or already quoted strings)
.replace(/^(\s*globs:\s*)([^\s"'[\n][^"'[\n]*?)(\s*)$/gm, '$1"$2"$3');

return load(preprocessed, { schema: DEFAULT_SCHEMA }) as object;
const result = load(preprocessed, { schema: DEFAULT_SCHEMA });
return result as object;
} catch (error) {
// If that fails, try with FAILSAFE_SCHEMA as a fallback
try {
return load(str, { schema: FAILSAFE_SCHEMA }) as object;
const result = load(str, { schema: FAILSAFE_SCHEMA });
return result as object;
} catch {
// If all else fails, throw the original error
throw error;
Expand All @@ -56,6 +58,9 @@ function convertCursorMdcFrontmatter(
_filename: string,
): RuleFrontmatter {
// Type guard to ensure we have an object
if (!cursorFrontmatter || typeof cursorFrontmatter !== "object") {
throw new Error("Invalid frontmatter: expected object");
}
const frontmatter = cursorFrontmatter as Record<string, unknown>;

// Normalize values according to term definitions
Expand Down Expand Up @@ -274,7 +279,12 @@ export async function parseCursorConfiguration(
try {
const content = await readFileContent(cursorMcpPath);
const mcp = JSON.parse(content);
if (mcp.mcpServers && Object.keys(mcp.mcpServers).length > 0) {
if (
mcp &&
typeof mcp === "object" &&
mcp.mcpServers &&
Object.keys(mcp.mcpServers).length > 0
) {
mcpServers = mcp.mcpServers as Record<string, RulesyncMcpServer>;
}
} catch (error) {
Expand Down
7 changes: 6 additions & 1 deletion src/parsers/geminicli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,12 @@ async function parseGeminiSettings(settingsPath: string): Promise<GeminiSettings
const settings = JSON.parse(content);

// Extract MCP servers
if (settings.mcpServers && Object.keys(settings.mcpServers).length > 0) {
if (
settings &&
typeof settings === "object" &&
settings.mcpServers &&
Object.keys(settings.mcpServers).length > 0
) {
mcpServers = settings.mcpServers as Record<string, RulesyncMcpServer>;
}
} catch (error) {
Expand Down