Compare commits

...
Author SHA1 Message Date
Tabish Bidiwale 5d0752b94d chore: add changeset for cross-platform fixes release
Add changeset for patch release including fixes for joinPath behavior and slash command path resolution across platforms.
2025-10-11 02:33:24 +11:00
Tabish Bidiwale 8c65b47abe Fix cross-platform joinPath behavior (#145) 2025-10-11 01:46:32 +11:00
Tabish Bidiwale 9c9e57daa1 Ensure slash command paths resolve on Windows platforms (#144)
* Ensure slash command paths work on Windows

* Add Linux home path coverage for joinPath
2025-10-11 00:27:16 +11:00
5 changed files with 94 additions and 9 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@fission-ai/openspec": patch
---
Fix cross-platform path handling issues. This release includes fixes for joinPath behavior and slash command path resolution to ensure OpenSpec works correctly across all platforms.
+3 -4
View File
@@ -1,4 +1,3 @@
import path from 'path';
import { FileSystemUtils } from '../../../utils/file-system.js';
import { TemplateManager, SlashCommandId } from '../../templates/index.js';
import { OPENSPEC_MARKERS } from '../../config.js';
@@ -28,7 +27,7 @@ export abstract class SlashCommandConfigurator {
for (const target of this.getTargets()) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
const filePath = path.join(projectPath, target.path);
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateBody(filePath, body);
@@ -53,7 +52,7 @@ export abstract class SlashCommandConfigurator {
const updated: string[] = [];
for (const target of this.getTargets()) {
const filePath = path.join(projectPath, target.path);
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateBody(filePath, body);
@@ -71,7 +70,7 @@ export abstract class SlashCommandConfigurator {
// to redirect to tool-specific locations (e.g., global directories).
resolveAbsolutePath(projectPath: string, id: SlashCommandId): string {
const rel = this.getRelativePath(id);
return path.join(projectPath, rel);
return FileSystemUtils.joinPath(projectPath, rel);
}
protected async updateBody(filePath: string, body: string): Promise<void> {
+11 -5
View File
@@ -49,8 +49,8 @@ $ARGUMENTS`,
private getGlobalPromptsDir(): string {
const home = (process.env.CODEX_HOME && process.env.CODEX_HOME.trim())
? process.env.CODEX_HOME.trim()
: path.join(os.homedir(), ".codex");
return path.join(home, "prompts");
: FileSystemUtils.joinPath(os.homedir(), ".codex");
return FileSystemUtils.joinPath(home, "prompts");
}
// Codex discovers prompts globally. Generate directly in the global directory
@@ -60,7 +60,10 @@ $ARGUMENTS`,
for (const target of this.getTargets()) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
const promptsDir = this.getGlobalPromptsDir();
const filePath = path.join(promptsDir, path.basename(target.path));
const filePath = FileSystemUtils.joinPath(
promptsDir,
path.basename(target.path)
);
await FileSystemUtils.createDirectory(path.dirname(filePath));
@@ -83,7 +86,10 @@ $ARGUMENTS`,
const updated: string[] = [];
for (const target of this.getTargets()) {
const promptsDir = this.getGlobalPromptsDir();
const filePath = path.join(promptsDir, path.basename(target.path));
const filePath = FileSystemUtils.joinPath(
promptsDir,
path.basename(target.path)
);
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateFullFile(filePath, target.id, body);
@@ -115,6 +121,6 @@ $ARGUMENTS`,
resolveAbsolutePath(_projectPath: string, id: SlashCommandId): string {
const promptsDir = this.getGlobalPromptsDir();
const fileName = path.basename(FILE_PATHS[id]);
return path.join(promptsDir, fileName);
return FileSystemUtils.joinPath(promptsDir, fileName);
}
}
+27
View File
@@ -42,6 +42,33 @@ function findMarkerIndex(
}
export class FileSystemUtils {
private static isWindowsBasePath(basePath: string): boolean {
return /^[A-Za-z]:[\\/]/.test(basePath) || basePath.startsWith('\\');
}
private static normalizeSegments(segments: string[]): string[] {
return segments
.flatMap((segment) => segment.split(/[\\/]+/u))
.filter((part) => part.length > 0);
}
static joinPath(basePath: string, ...segments: string[]): string {
const normalizedSegments = this.normalizeSegments(segments);
if (this.isWindowsBasePath(basePath)) {
const normalizedBasePath = path.win32.normalize(basePath);
return normalizedSegments.length
? path.win32.join(normalizedBasePath, ...normalizedSegments)
: normalizedBasePath;
}
const posixBasePath = basePath.replace(/\\/g, '/');
return normalizedSegments.length
? path.posix.join(posixBasePath, ...normalizedSegments)
: path.posix.normalize(posixBasePath);
}
static async createDirectory(dirPath: string): Promise<void> {
await fs.mkdir(dirPath, { recursive: true });
}
+48
View File
@@ -160,4 +160,52 @@ describe('FileSystemUtils', () => {
expect(hasPermission).toBe(true);
});
});
describe('joinPath', () => {
it('should join POSIX-style paths', () => {
const result = FileSystemUtils.joinPath(
'/tmp/project',
'.claude/commands/openspec/proposal.md'
);
expect(result).toBe('/tmp/project/.claude/commands/openspec/proposal.md');
});
it('should join Linux home directory paths', () => {
const result = FileSystemUtils.joinPath(
'/home/dev/workspace/openspec',
'.cursor/commands/install.md'
);
expect(result).toBe('/home/dev/workspace/openspec/.cursor/commands/install.md');
});
it('should join Windows drive-letter paths with backslashes', () => {
const result = FileSystemUtils.joinPath(
'C:\\Users\\dev\\project',
'.claude/commands/openspec/proposal.md'
);
expect(result).toBe(
'C:\\Users\\dev\\project\\.claude\\commands\\openspec\\proposal.md'
);
});
it('should join Windows paths that use forward slashes', () => {
const result = FileSystemUtils.joinPath(
'D:/workspace/app',
'.cursor/commands/openspec-apply.md'
);
expect(result).toBe(
'D:\\workspace\\app\\.cursor\\commands\\openspec-apply.md'
);
});
it('should join UNC-style Windows paths', () => {
const result = FileSystemUtils.joinPath(
'\\server\\share\\repo',
'.windsurf/workflows/openspec-archive.md'
);
expect(result).toBe(
'\\server\\share\\repo\\.windsurf\\workflows\\openspec-archive.md'
);
});
});
});