Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale d259fe6407 fix: use path.join for cross-platform compatibility in Codex FILE_PATHS
Fixes #132

The FILE_PATHS constant was using hardcoded forward slashes, which caused
path.basename() to fail on Windows. On Windows, path.basename() expects
backslashes as path separators, so it would return the entire string
instead of just the filename.

This broke Codex detection on Windows during init/update because the
resolveAbsolutePath() method would construct incorrect paths, causing
file existence checks to fail.

Changed FILE_PATHS to use path.join() which automatically uses the
correct platform-specific path separators (backslashes on Windows,
forward slashes on Unix).
2025-10-09 15:26:38 +11:00
8 changed files with 16 additions and 108 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@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.
-6
View File
@@ -1,11 +1,5 @@
# @fission-ai/openspec
## 0.9.1
### Patch Changes
- 8210970: Fix OpenSpec not working on Windows when Codex integration is selected. This release includes fixes for cross-platform path handling and normalization to ensure OpenSpec works correctly on Windows systems.
## 0.9.0
### Minor Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.9.1",
"version": "0.9.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+4 -3
View File
@@ -1,3 +1,4 @@
import path from 'path';
import { FileSystemUtils } from '../../../utils/file-system.js';
import { TemplateManager, SlashCommandId } from '../../templates/index.js';
import { OPENSPEC_MARKERS } from '../../config.js';
@@ -27,7 +28,7 @@ export abstract class SlashCommandConfigurator {
for (const target of this.getTargets()) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
const filePath = path.join(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
await this.updateBody(filePath, body);
@@ -52,7 +53,7 @@ export abstract class SlashCommandConfigurator {
const updated: string[] = [];
for (const target of this.getTargets()) {
const filePath = FileSystemUtils.joinPath(projectPath, target.path);
const filePath = path.join(projectPath, target.path);
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateBody(filePath, body);
@@ -70,7 +71,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 FileSystemUtils.joinPath(projectPath, rel);
return path.join(projectPath, rel);
}
protected async updateBody(filePath: string, body: string): Promise<void> {
+8 -15
View File
@@ -5,11 +5,10 @@ import { SlashCommandId, TemplateManager } from "../../templates/index.js";
import { FileSystemUtils } from "../../../utils/file-system.js";
import { OPENSPEC_MARKERS } from "../../config.js";
// Use POSIX-style paths for consistent logging across platforms.
const FILE_PATHS: Record<SlashCommandId, string> = {
proposal: ".codex/prompts/openspec-proposal.md",
apply: ".codex/prompts/openspec-apply.md",
archive: ".codex/prompts/openspec-archive.md",
proposal: path.join(".codex", "prompts", "openspec-proposal.md"),
apply: path.join(".codex", "prompts", "openspec-apply.md"),
archive: path.join(".codex", "prompts", "openspec-archive.md"),
};
export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
@@ -49,8 +48,8 @@ $ARGUMENTS`,
private getGlobalPromptsDir(): string {
const home = (process.env.CODEX_HOME && process.env.CODEX_HOME.trim())
? process.env.CODEX_HOME.trim()
: FileSystemUtils.joinPath(os.homedir(), ".codex");
return FileSystemUtils.joinPath(home, "prompts");
: path.join(os.homedir(), ".codex");
return path.join(home, "prompts");
}
// Codex discovers prompts globally. Generate directly in the global directory
@@ -60,10 +59,7 @@ $ARGUMENTS`,
for (const target of this.getTargets()) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
const promptsDir = this.getGlobalPromptsDir();
const filePath = FileSystemUtils.joinPath(
promptsDir,
path.basename(target.path)
);
const filePath = path.join(promptsDir, path.basename(target.path));
await FileSystemUtils.createDirectory(path.dirname(filePath));
@@ -86,10 +82,7 @@ $ARGUMENTS`,
const updated: string[] = [];
for (const target of this.getTargets()) {
const promptsDir = this.getGlobalPromptsDir();
const filePath = FileSystemUtils.joinPath(
promptsDir,
path.basename(target.path)
);
const filePath = path.join(promptsDir, path.basename(target.path));
if (await FileSystemUtils.fileExists(filePath)) {
const body = TemplateManager.getSlashCommandBody(target.id).trim();
await this.updateFullFile(filePath, target.id, body);
@@ -121,6 +114,6 @@ $ARGUMENTS`,
resolveAbsolutePath(_projectPath: string, id: SlashCommandId): string {
const promptsDir = this.getGlobalPromptsDir();
const fileName = path.basename(FILE_PATHS[id]);
return FileSystemUtils.joinPath(promptsDir, fileName);
return path.join(promptsDir, fileName);
}
}
+3 -3
View File
@@ -106,9 +106,9 @@ export class UpdateCommand {
}
if (updatedSlashFiles.length > 0) {
// Normalize to forward slashes for cross-platform log consistency
const normalized = updatedSlashFiles.map((p) => p.replace(/\\/g, '/'));
summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
summaryParts.push(
`Updated slash commands: ${updatedSlashFiles.join(', ')}`
);
}
const failedItems = [
-27
View File
@@ -42,33 +42,6 @@ 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,52 +160,4 @@ 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'
);
});
});
});