Skip to content
Merged
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
46 changes: 22 additions & 24 deletions src/features/commands/agentsmd-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,7 @@ This is the body of the agentsmd command.
It can be multiline.`;

const invalidMarkdownContent = `---
# Missing required description field
invalid: true
description: 123
---

Body content`;
Expand Down Expand Up @@ -105,20 +104,18 @@ Body content`;
expect(command).toBeInstanceOf(AgentsmdCommand);
});

it("should throw error for invalid frontmatter when validation is enabled", () => {
expect(
() =>
new AgentsmdCommand({
baseDir: testDir,
relativeDirPath: ".agents/commands",
relativeFilePath: "invalid-command.md",
frontmatter: {
// Missing required description field
} as SimulatedCommandFrontmatter,
body: "Body content",
validate: true,
}),
).toThrow();
it("should accept frontmatter without description (description is optional)", () => {
const command = new AgentsmdCommand({
baseDir: testDir,
relativeDirPath: ".agents/commands",
relativeFilePath: "no-desc-command.md",
frontmatter: {} as SimulatedCommandFrontmatter,
body: "Body content",
validate: true,
});

expect(command).toBeInstanceOf(AgentsmdCommand);
expect(command.getFrontmatter().description).toBeUndefined();
});
});

Expand Down Expand Up @@ -320,19 +317,20 @@ Body content`;
).rejects.toThrow();
});

it("should handle file without frontmatter", async () => {
it("should handle file without frontmatter (description is optional)", async () => {
const commandsDir = join(testDir, ".agents", "commands");
const filePath = join(commandsDir, "no-frontmatter.md");

await writeFileContent(filePath, markdownWithoutFrontmatter);

await expect(
AgentsmdCommand.fromFile({
baseDir: testDir,
relativeFilePath: "no-frontmatter.md",
validate: true,
}),
).rejects.toThrow();
const command = await AgentsmdCommand.fromFile({
baseDir: testDir,
relativeFilePath: "no-frontmatter.md",
validate: true,
});

expect(command).toBeInstanceOf(AgentsmdCommand);
expect(command.getFrontmatter().description).toBeUndefined();
});
});

Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/antigravity-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const AntigravityWorkflowFrontmatterSchema = z.looseObject({

// looseObject preserves unknown keys during parsing (like passthrough in Zod 3)
export const AntigravityCommandFrontmatterSchema = z.looseObject({
description: z.string(),
description: z.optional(z.string()),
// Support for workflow-specific configuration
...AntigravityWorkflowFrontmatterSchema.shape,
});
Expand Down
11 changes: 7 additions & 4 deletions src/features/commands/claudecode-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -580,11 +580,14 @@ Roundtrip body`;
}
});

it("should reject frontmatter without description", () => {
const invalidFrontmatter = {};
const result = ClaudecodeCommandFrontmatterSchema.safeParse(invalidFrontmatter);
it("should accept frontmatter without description (description is optional)", () => {
const frontmatter = {};
const result = ClaudecodeCommandFrontmatterSchema.safeParse(frontmatter);

expect(result.success).toBe(false);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.description).toBeUndefined();
}
});

it("should reject frontmatter with non-string 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 @@ -17,7 +17,7 @@ import {

// looseObject preserves unknown keys during parsing (like passthrough in Zod 3)
export const ClaudecodeCommandFrontmatterSchema = z.looseObject({
description: z.string(),
description: z.optional(z.string()),
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
"argument-hint": z.optional(z.string()),
model: z.optional(z.string()),
Expand Down
1 change: 0 additions & 1 deletion src/features/commands/cline-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ export class ClineCommand extends ToolCommand {
toRulesyncCommand(): RulesyncCommand {
const rulesyncFrontmatter: RulesyncCommandFrontmatter = {
targets: ["*"],
description: "",
};

return new RulesyncCommand({
Expand Down
1 change: 0 additions & 1 deletion src/features/commands/codexcli-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export class CodexcliCommand extends ToolCommand {
toRulesyncCommand(): RulesyncCommand {
const rulesyncFrontmatter: RulesyncCommandFrontmatter = {
targets: ["*"],
description: "",
};

return new RulesyncCommand({
Expand Down
53 changes: 27 additions & 26 deletions src/features/commands/copilot-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ This is the body of the copilot command.
It can be multiline.`;

const invalidMarkdownContent = `---
# Missing required description field
mode: agent
mode: 123
description: 456
---

Body content`;
Expand Down Expand Up @@ -114,20 +114,18 @@ Body content`;
expect(command).toBeInstanceOf(CopilotCommand);
});

it("should throw error for invalid frontmatter when validation is enabled", () => {
expect(
() =>
new CopilotCommand({
baseDir: testDir,
relativeDirPath: join(".github", "prompts"),
relativeFilePath: "invalid-command.prompt.md",
frontmatter: {
// Missing required mode and description field
} as CopilotCommandFrontmatter,
body: "Body content",
validate: true,
}),
).toThrow();
it("should accept frontmatter without description (description is optional)", () => {
const command = new CopilotCommand({
baseDir: testDir,
relativeDirPath: join(".github", "prompts"),
relativeFilePath: "no-desc-command.prompt.md",
frontmatter: {} as CopilotCommandFrontmatter,
body: "Body content",
validate: true,
});

expect(command).toBeInstanceOf(CopilotCommand);
expect(command.getFrontmatter().description).toBeUndefined();
});
});

Expand Down Expand Up @@ -351,18 +349,19 @@ Body content`;
).rejects.toThrow();
});

it("should handle file without frontmatter", async () => {
it("should handle file without frontmatter (description is optional)", async () => {
const commandsDir = join(testDir, ".github", "prompts");
const filePath = join(commandsDir, "no-frontmatter.prompt.md");

await writeFileContent(filePath, markdownWithoutFrontmatter);

await expect(
CopilotCommand.fromFile({
relativeFilePath: "no-frontmatter.prompt.md",
validate: true,
}),
).rejects.toThrow();
const command = await CopilotCommand.fromFile({
relativeFilePath: "no-frontmatter.prompt.md",
validate: true,
});

expect(command).toBeInstanceOf(CopilotCommand);
expect(command.getFrontmatter().description).toBeUndefined();
});
});

Expand Down Expand Up @@ -426,12 +425,14 @@ Body content`;
expect(result).toEqual(validFrontmatter);
});

it("should throw error for frontmatter without description", () => {
const invalidFrontmatter = {
it("should accept frontmatter without description (description is optional)", () => {
const frontmatter = {
mode: "agent",
};

expect(() => CopilotCommandFrontmatterSchema.parse(invalidFrontmatter)).toThrow();
const result = CopilotCommandFrontmatterSchema.parse(frontmatter);
expect(result.mode).toBe("agent");
expect(result.description).toBeUndefined();
});

it("should validate frontmatter with any string mode (mode is optional string)", () => {
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 @@ -18,7 +18,7 @@ import {
// looseObject preserves unknown keys during parsing (like passthrough in Zod 3)
export const CopilotCommandFrontmatterSchema = z.looseObject({
mode: z.optional(z.string()),
description: z.string(),
description: z.optional(z.string()),
});

export type CopilotCommandFrontmatter = z.infer<typeof CopilotCommandFrontmatterSchema>;
Expand Down
4 changes: 2 additions & 2 deletions src/features/commands/cursor-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ describe("CursorCommand", () => {
expect(rulesyncCommand.getFileContent()).toContain("Test body content");
});

it("should default description to empty string when not set", () => {
it("should propagate undefined description when not set", () => {
const command = new CursorCommand({
baseDir: testDir,
relativeDirPath: ".cursor/commands",
Expand All @@ -163,7 +163,7 @@ describe("CursorCommand", () => {
});

const rulesyncCommand = command.toRulesyncCommand();
expect(rulesyncCommand.getFrontmatter().description).toBe("");
expect(rulesyncCommand.getFrontmatter().description).toBeUndefined();
});

it("should preserve handoffs in cursor section", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/cursor-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export class CursorCommand extends ToolCommand {
}

toRulesyncCommand(): RulesyncCommand {
const { description = "", ...restFields } = this.frontmatter;
const { description, ...restFields } = this.frontmatter;

const rulesyncFrontmatter: RulesyncCommandFrontmatter = {
targets: ["*"],
Expand Down
3 changes: 1 addition & 2 deletions src/features/commands/factorydroid-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,7 @@ This is the body of the factorydroid command.
It can be multiline.`;

const invalidMarkdownContent = `---
# Missing required fields
invalid: true
description: 123
---

Body content`;
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
11 changes: 7 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: 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,11 @@ 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 !== undefined
? `description = "${geminiFrontmatter.description}"\n`
: "";
const tomlContent = `${descriptionLine}prompt = """
${geminiFrontmatter.prompt}
"""`;

Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/kilo-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Step 1`;
const rulesyncCommand = kiloCommand.toRulesyncCommand();

expect(rulesyncCommand).toBeInstanceOf(RulesyncCommand);
expect(rulesyncCommand.getFrontmatter()).toEqual({ targets: ["*"], description: "" });
expect(rulesyncCommand.getFrontmatter()).toEqual({ targets: ["*"] });
expect(rulesyncCommand.getBody()).toBe(validContent);
expect(rulesyncCommand.getRelativeDirPath()).toBe(RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
});
Expand Down
1 change: 0 additions & 1 deletion src/features/commands/kilo-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ export class KiloCommand extends ToolCommand {
toRulesyncCommand(): RulesyncCommand {
const rulesyncFrontmatter: RulesyncCommandFrontmatter = {
targets: ["*"],
description: "",
};

return new RulesyncCommand({
Expand Down
2 changes: 1 addition & 1 deletion src/features/commands/kiro-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Step 1`;
const rulesyncCommand = kiroCommand.toRulesyncCommand();

expect(rulesyncCommand).toBeInstanceOf(RulesyncCommand);
expect(rulesyncCommand.getFrontmatter()).toEqual({ targets: ["*"], description: "" });
expect(rulesyncCommand.getFrontmatter()).toEqual({ targets: ["*"] });
expect(rulesyncCommand.getBody()).toBe(validContent);
expect(rulesyncCommand.getRelativeDirPath()).toBe(RULESYNC_COMMANDS_RELATIVE_DIR_PATH);
});
Expand Down
1 change: 0 additions & 1 deletion src/features/commands/kiro-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export class KiroCommand extends ToolCommand {
toRulesyncCommand(): RulesyncCommand {
const rulesyncFrontmatter: RulesyncCommandFrontmatter = {
targets: ["*"],
description: "",
};

return new RulesyncCommand({
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 @@ -16,7 +16,7 @@ import {
} from "./tool-command.js";

export const OpenCodeCommandFrontmatterSchema = z.looseObject({
description: z.string(),
description: z.optional(z.string()),
agent: optional(z.string()),
subtask: optional(z.boolean()),
model: optional(z.string()),
Expand Down
14 changes: 9 additions & 5 deletions src/features/commands/roo-command.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,13 +422,17 @@ This file has invalid frontmatter`;
}
});

it("should reject frontmatter without description", () => {
const invalidFrontmatter = {
"argument-hint": "Missing description",
it("should accept frontmatter without description (description is optional)", () => {
const frontmatter = {
"argument-hint": "Has hint but no description",
};

const result = RooCommandFrontmatterSchema.safeParse(invalidFrontmatter);
expect(result.success).toBe(false);
const result = RooCommandFrontmatterSchema.safeParse(frontmatter);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.description).toBeUndefined();
expect(result.data["argument-hint"]).toBe("Has hint but no description");
}
});

it("should reject frontmatter with invalid description type", () => {
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 @@ -16,7 +16,7 @@ import {

// looseObject preserves unknown keys during parsing (like passthrough in Zod 3)
export const RooCommandFrontmatterSchema = z.looseObject({
description: z.string(),
description: z.optional(z.string()),
"argument-hint": optional(z.string()),
});

Expand Down
Loading