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
12 changes: 10 additions & 2 deletions src/agent/configurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -990,21 +990,29 @@ function writeTarget(path: string): string {
}
}

export function configurationTargetIdentity(
targetPath: string,
platform: NodeJS.Platform = process.platform,
): string {
return platform === 'win32' ? targetPath.toLowerCase() : targetPath;
}

function hasWeakPermissions(mode: number): boolean {
return process.platform !== 'win32' && (mode & 0o077) !== 0;
}

function assertDistinctConfigurationTargets(prepared: PreparedAgentFile[]): void {
const targets = new Set<string>();
for (const file of prepared) {
if (targets.has(file.targetPath)) {
const identity = configurationTargetIdentity(file.targetPath);
if (targets.has(identity)) {
throw new CLIError(
`Multiple agent configuration paths resolve to ${file.targetPath}.`,
ExitCode.GENERAL,
'No files were changed. Use distinct configuration paths and retry.',
);
}
targets.add(file.targetPath);
targets.add(identity);
}
}

Expand Down
21 changes: 21 additions & 0 deletions test/agent/configurator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { parse as parseYaml } from 'yaml';

import {
applyAgentConfigurations,
configurationTargetIdentity,
prepareAgentConfigurations,
} from '../../src/agent/configurator';
import { AGENT_IDS, type AgentId, type AgentSetupOptions } from '../../src/agent/types';
Expand Down Expand Up @@ -462,6 +463,26 @@ describe('agent configurator', () => {
expect(existsSync(shared)).toBe(false);
});

it('compares configuration target paths case-insensitively on Windows', () => {
expect(configurationTargetIdentity('C:\\Users\\Alice\\settings.json', 'win32'))
.toBe('c:\\users\\alice\\settings.json');
expect(configurationTargetIdentity('/Users/Alice/settings.json', 'linux'))
.toBe('/Users/Alice/settings.json');
});

it('rejects duplicate targets that differ only by case on Windows', () => {
if (process.platform !== 'win32') return;

const shared = join(home, 'shared-agent-config');
mkdirSync(shared, { recursive: true });
const differentlyCased = shared.replace('shared-agent-config', 'SHARED-AGENT-CONFIG');

expect(() => prepareAgentConfigurations(setupOptions(
['claude-code', 'pi'],
{ env: { CLAUDE_CONFIG_DIR: shared, PI_CODING_AGENT_DIR: differentlyCased } },
))).toThrow('Multiple agent configuration paths');
});

it('removes the setup lock when installation receives an exit signal', async () => {
if (process.platform === 'win32') return;
const configuratorUrl = pathToFileURL(
Expand Down