From 3e81359c6b6392bd3fc1c4d14ac48138b6d84051 Mon Sep 17 00:00:00 2001 From: Abhi <43648792+abhipatel12@users.noreply.github.com> Date: Fri, 25 Jul 2025 23:27:23 -0400 Subject: [PATCH] (fix): Custom Commands follow symlinks (#4907) --- .../src/services/FileCommandLoader.test.ts | 57 +++++++++++++++++++ .../cli/src/services/FileCommandLoader.ts | 1 + 2 files changed, 58 insertions(+) diff --git a/packages/cli/src/services/FileCommandLoader.test.ts b/packages/cli/src/services/FileCommandLoader.test.ts index b86d36ac..0e3d781b 100644 --- a/packages/cli/src/services/FileCommandLoader.test.ts +++ b/packages/cli/src/services/FileCommandLoader.test.ts @@ -54,6 +54,63 @@ describe('FileCommandLoader', () => { } }); + // Symlink creation on Windows requires special permissions that are not + // available in the standard CI environment. Therefore, we skip these tests + // on Windows to prevent CI failures. The core functionality is still + // validated on Linux and macOS. + const itif = (condition: boolean) => (condition ? it : it.skip); + + itif(process.platform !== 'win32')( + 'loads commands from a symlinked directory', + async () => { + const userCommandsDir = getUserCommandsDir(); + const realCommandsDir = '/real/commands'; + mock({ + [realCommandsDir]: { + 'test.toml': 'prompt = "This is a test prompt"', + }, + // Symlink the user commands directory to the real one + [userCommandsDir]: mock.symlink({ + path: realCommandsDir, + }), + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + const command = commands[0]; + expect(command).toBeDefined(); + expect(command.name).toBe('test'); + }, + ); + + itif(process.platform !== 'win32')( + 'loads commands from a symlinked subdirectory', + async () => { + const userCommandsDir = getUserCommandsDir(); + const realNamespacedDir = '/real/namespaced-commands'; + mock({ + [userCommandsDir]: { + namespaced: mock.symlink({ + path: realNamespacedDir, + }), + }, + [realNamespacedDir]: { + 'my-test.toml': 'prompt = "This is a test prompt"', + }, + }); + + const loader = new FileCommandLoader(null as unknown as Config); + const commands = await loader.loadCommands(signal); + + expect(commands).toHaveLength(1); + const command = commands[0]; + expect(command).toBeDefined(); + expect(command.name).toBe('namespaced:my-test'); + }, + ); + it('loads multiple commands', async () => { const userCommandsDir = getUserCommandsDir(); mock({ diff --git a/packages/cli/src/services/FileCommandLoader.ts b/packages/cli/src/services/FileCommandLoader.ts index 994762c1..23d5af19 100644 --- a/packages/cli/src/services/FileCommandLoader.ts +++ b/packages/cli/src/services/FileCommandLoader.ts @@ -71,6 +71,7 @@ export class FileCommandLoader implements ICommandLoader { nodir: true, dot: true, signal, + follow: true, }; try {