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
2 changes: 1 addition & 1 deletion src/features/commands/antigravity-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ export class AntigravityCommand extends ToolCommand {
// But we DO need to update the body to include the specific workflow header.
body = `# Workflow: ${trigger}\n\n${body}${turboDirective}`;

const description = rulesyncFrontmatter.description;
const description = rulesyncFrontmatter.description ?? "";

const antigravityFrontmatter: AntigravityCommandFrontmatter = {
description,
Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/claudecode-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export class ClaudecodeCommand extends ToolCommand {
const claudecodeFields = rulesyncFrontmatter.claudecode ?? {};

const claudecodeFrontmatter: ClaudecodeCommandFrontmatter = {
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
...claudecodeFields,
};

Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/copilot-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class CopilotCommand extends ToolCommand {
const copilotFields = rulesyncFrontmatter.copilot ?? {};

const copilotFrontmatter: CopilotCommandFrontmatter = {
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
...copilotFields,
};

Expand Down
6 changes: 3 additions & 3 deletions src/features/commands/geminicli-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ prompt = "Unclosed string`;

expect(command.getBody()).toBe("This is a test prompt without description.\n");
expect(command.getFrontmatter()).toEqual({
description: "",
description: undefined,
prompt: "This is a test prompt without description.\n",
});
});
Expand Down Expand Up @@ -134,7 +134,7 @@ prompt = "Unclosed string`;
});

const frontmatter = command.getFrontmatter() as GeminiCliCommandFrontmatter;
expect(frontmatter.description).toBe("");
expect(frontmatter.description).toBeUndefined();
expect(frontmatter.prompt).toBe("This is a test prompt without description.\n");
});

Expand Down Expand Up @@ -223,7 +223,7 @@ prompt = "Unclosed string`;

expect(rulesyncCommand.getFrontmatter()).toEqual({
targets: ["geminicli"],
description: "",
description: undefined,
});
});
});
Expand Down
10 changes: 6 additions & 4 deletions src/features/commands/geminicli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export class GeminiCliCommand extends ToolCommand {
// Preserve all fields including unknown ones (looseObject passthrough)
return {
...result.data,
description: result.data.description || "",
description: result.data.description,
};
} catch (error) {
throw new Error(
Expand All @@ -84,7 +84,7 @@ export class GeminiCliCommand extends ToolCommand {

const rulesyncFrontmatter: RulesyncCommandFrontmatter = {
targets: ["geminicli"],
description: description ?? "",
description,
// Preserve extra fields in geminicli section (excluding prompt which is the body)
...(Object.keys(restFields).length > 0 && { geminicli: restFields }),
};
Expand Down Expand Up @@ -123,8 +123,10 @@ export class GeminiCliCommand extends ToolCommand {
// Generate proper file content with TOML format
// Note: TOML format only supports description and prompt fields
// Extra fields from geminicli section are stored in the object but not serialized to TOML
const tomlContent = `description = "${geminiFrontmatter.description}"
prompt = """
const descriptionLine = geminiFrontmatter.description
? `description = "${geminiFrontmatter.description}"\n`
: "";
const tomlContent = `${descriptionLine}prompt = """
${geminiFrontmatter.prompt}
"""`;

Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/opencode-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ export class OpenCodeCommand extends ToolCommand {
const opencodeFields = rulesyncFrontmatter.opencode ?? {};

const opencodeFrontmatter: OpenCodeCommandFrontmatter = {
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
...opencodeFields,
};

Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/roo-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ export class RooCommand extends ToolCommand {
const rooFields = rulesyncFrontmatter.roo ?? {};

const rooFrontmatter: RooCommandFrontmatter = {
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
...rooFields,
};

Expand Down
33 changes: 16 additions & 17 deletions src/features/commands/rulesync-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,19 @@ describe("RulesyncCommand", () => {
expect(() => new RulesyncCommand(paramsWithInvalidData)).toThrow();
});

it("should throw error for missing required frontmatter fields", () => {
const incompleteFrontmatter = {
it("should not throw error for missing optional description", () => {
const frontmatterWithoutDescription = {
targets: ["cursor"],
// Missing description
// Missing description (now optional)
};

const paramsWithIncompleteData: RulesyncCommandParams = {
const paramsWithoutDescription: RulesyncCommandParams = {
...validParams,
frontmatter: incompleteFrontmatter as any,
validate: true, // Enable validation
frontmatter: frontmatterWithoutDescription as any,
validate: true,
};

expect(() => new RulesyncCommand(paramsWithIncompleteData)).toThrow();
expect(() => new RulesyncCommand(paramsWithoutDescription)).not.toThrow();
});

it("should generate correct file content from frontmatter and body", () => {
Expand Down Expand Up @@ -181,22 +181,21 @@ describe("RulesyncCommand", () => {
expect(result.error).toBeDefined();
});

it("should return failure for missing required fields", () => {
const incompleteFrontmatter = {
it("should return success for missing optional description", () => {
const frontmatterWithoutDescription = {
targets: ["cursor"],
// Missing description
// Missing description (now optional)
};

const command = new RulesyncCommand({
...validParams,
frontmatter: incompleteFrontmatter as any,
frontmatter: frontmatterWithoutDescription as any,
validate: false,
});

const result = command.validate();

expect(result.success).toBe(false);
expect(result.error).toBeDefined();
expect(result.success).toBe(true);
});
});

Expand Down Expand Up @@ -227,14 +226,14 @@ describe("RulesyncCommand", () => {
expect(result.success).toBe(false);
});

it("should reject missing description", () => {
const invalidData = {
it("should accept missing description (now optional)", () => {
const dataWithoutDescription = {
targets: ["cursor"],
};

const result = RulesyncCommandFrontmatterSchema.safeParse(invalidData);
const result = RulesyncCommandFrontmatterSchema.safeParse(dataWithoutDescription);

expect(result.success).toBe(false);
expect(result.success).toBe(true);
});

it("should reject non-string description", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/rulesync-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { parseFrontmatter, stringifyFrontmatter } from "../../utils/frontmatter.
// Tool-specific sections (e.g., claudecode:, copilot:) are preserved as additional keys
export const RulesyncCommandFrontmatterSchema = z.looseObject({
targets: z._default(RulesyncTargetsSchema, ["*"]),
description: z.string(),
description: z.optional(z.string()),
});

// Input type allows targets to be omitted (will use default value)
Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/simulated-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export abstract class SimulatedCommand extends ToolCommand {
const rulesyncFrontmatter = rulesyncCommand.getFrontmatter();

const claudecodeFrontmatter: SimulatedCommandFrontmatter = {
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
};

const body = rulesyncCommand.getBody();
Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/codexcli-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export class CodexCliSubagent extends ToolSubagent {
const rulesyncFrontmatter: RulesyncSubagentFrontmatter = {
targets: ["codexcli"],
name,
description: description ?? "",
description,
// Only include codexcli section if there are fields
...(Object.keys(codexcliSection).length > 0 && { codexcli: codexcliSection }),
};
Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/copilot-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export class CopilotSubagent extends ToolSubagent {

const copilotFrontmatter: CopilotSubagentFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
...copilotSection,
...(mergedTools.length > 0 && { tools: mergedTools }),
};
Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/cursor-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export class CursorSubagent extends ToolSubagent {

const cursorFrontmatter: CursorSubagentFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
...cursorSection,
};

Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/kiro-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class KiroSubagent extends ToolSubagent {
const rulesyncFrontmatter: RulesyncSubagentFrontmatter = {
targets: ["kiro"],
name,
description: description ?? "",
description: description ?? undefined,
// Only include kiro section if there are fields
...(Object.keys(kiroSection).length > 0 && { kiro: kiroSection }),
};
Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/opencode-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export class OpenCodeSubagent extends ToolSubagent {

const opencodeFrontmatter: OpenCodeSubagentFrontmatter = {
...opencodeSection,
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
mode: "subagent",
...(rulesyncFrontmatter.name && { name: rulesyncFrontmatter.name }),
};
Expand Down
32 changes: 15 additions & 17 deletions src/features/subagents/rulesync-subagent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ describe("RulesyncSubagentFrontmatterSchema", () => {
};

expect(() => RulesyncSubagentFrontmatterSchema.parse(missingName)).toThrow();
expect(() => RulesyncSubagentFrontmatterSchema.parse(missingDescription)).toThrow();
expect(() => RulesyncSubagentFrontmatterSchema.parse(missingDescription)).not.toThrow();
});

it("should use default targets when omitted", () => {
Expand Down Expand Up @@ -149,22 +149,22 @@ describe("RulesyncSubagent", () => {
expect(subagent.getFrontmatter().claudecode?.model).toBe("opus");
});

it("should throw error with invalid frontmatter", () => {
const invalidFrontmatter = {
it("should not throw error for missing optional description", () => {
const frontmatterWithoutDescription = {
targets: ["*"],
name: "test-subagent",
// missing description
// missing description (now optional)
};

expect(() => {
const _instance = new RulesyncSubagent({
baseDir: ".",
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
relativeFilePath: "invalid.md",
frontmatter: invalidFrontmatter as any,
relativeFilePath: "valid.md",
frontmatter: frontmatterWithoutDescription as any,
body: "Test body",
});
}).toThrow();
}).not.toThrow();
});

it("should skip validation when validate=false", () => {
Expand Down Expand Up @@ -298,23 +298,22 @@ describe("RulesyncSubagent", () => {
expect(result.error).toBe(null);
});

it("should return error for invalid frontmatter", () => {
it("should return success for missing optional description", () => {
const subagent = new RulesyncSubagent({
baseDir: ".",
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
relativeFilePath: "invalid-validate.md",
relativeFilePath: "valid-validate.md",
frontmatter: {
targets: ["*"],
name: "invalid-subagent",
// missing description
name: "valid-subagent",
// missing description (now optional)
} as any,
body: "Invalid body",
validate: false, // Skip validation in constructor for testing
body: "Valid body",
validate: false,
});

const result = subagent.validate();
expect(result.success).toBe(false);
expect(result.error).toBeInstanceOf(Error);
expect(result.success).toBe(true);
});
});

Expand Down Expand Up @@ -412,8 +411,7 @@ Nested content.`;
const filePath = join(subagentsDir, "test-fromfile-invalid.md");
const fileContent = `---
targets: ["*"]
name: invalid-subagent
# missing description
# missing name (required field)
---
Invalid content.`;

Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/rulesync-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { parseFrontmatter, stringifyFrontmatter } from "../../utils/frontmatter.
export const RulesyncSubagentFrontmatterSchema = z.looseObject({
targets: z._default(RulesyncTargetsSchema, ["*"]),
name: z.string(),
description: z.string(),
description: z.optional(z.string()),
});

// Input type allows targets to be omitted (will use default value)
Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/simulated-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export abstract class SimulatedSubagent extends ToolSubagent {

const simulatedFrontmatter: SimulatedSubagentFrontmatter = {
name: rulesyncFrontmatter.name,
description: rulesyncFrontmatter.description,
description: rulesyncFrontmatter.description ?? "",
};

const body = rulesyncSubagent.getBody();
Expand Down