mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-03 22:13:19 +08:00
Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d0752b94d | ||
|
|
8c65b47abe | ||
|
|
9c9e57daa1 | ||
|
|
c7ca76cb4f | ||
|
|
06bd3999bf | ||
|
|
821097079a |
@@ -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.
|
||||
@@ -1,5 +1,11 @@
|
||||
# @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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.9.0",
|
||||
"version": "0.9.1",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user