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
12 changed files with 17 additions and 157 deletions
-5
View File
@@ -1,5 +0,0 @@
---
"@fission-ai/openspec": minor
---
Improve init wizard Enter key behavior to allow proceeding through prompts more naturally
-12
View File
@@ -1,17 +1,5 @@
# @fission-ai/openspec
## 0.9.2
### Patch Changes
- 2ae0484: 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.
## 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,14 +0,0 @@
## Why
- Users frequently scroll to a tool and press Enter without toggling it, resulting in no configuration changes.
- The current workflow deviates from common CLI expectations where Enter confirms the highlighted item.
- Aligning behavior with user expectations reduces friction during onboarding.
## What Changes
- Update the init wizard so pressing Enter on a highlighted tool selects it before moving to the review step.
- Adjust interactive instructions to clarify Enter selects the current tool and Space still toggles selections.
- Refresh specs to capture the clarified behavior for the interactive menu.
## Impact
- Users who press Enter without toggling now configure the highlighted tool instead of exiting with no selections.
- Spacebar multi-select support remains unchanged for power users.
- Documentation better reflects how the wizard behaves.
@@ -1,10 +0,0 @@
## MODIFIED Requirements
### Requirement: Interactive Mode
The command SHALL provide an interactive menu for AI tool selection with clear navigation instructions.
#### Scenario: Displaying interactive menu
- **WHEN** run in fresh or extend mode
- **THEN** present a looping select menu that lets users toggle tools with Space and review selections with Enter
- **AND** when Enter is pressed on a highlighted selectable tool that is not already selected, automatically add it to the selection before moving to review so the highlighted tool is configured
- **AND** label already configured tools with "(already configured)" while keeping disabled options marked "coming soon"
- **AND** change the prompt copy in extend mode to "Which AI tools would you like to add or refresh?"
- **AND** display inline instructions clarifying that Space toggles tools and Enter selects the highlighted tool before reviewing selections
@@ -1,8 +0,0 @@
## 1. Implementation
- [x] Update the tool selection wizard to auto-select the highlighted tool when Enter is pressed without prior toggles.
- [x] Refresh inline instructions copy so Enter behavior is clear.
- [x] Adjust or add tests if needed to cover the new selection flow.
## 2. Validation
- [x] Run `pnpm run build`.
- [x] Run `pnpm test` (or targeted suite) if applicable.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@fission-ai/openspec",
"version": "0.9.2",
"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);
}
}
+1 -11
View File
@@ -220,16 +220,6 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
}
if (isEnterKey(key)) {
const current = config.choices[cursor];
if (
current &&
current.selectable &&
!selectedSet.has(current.value)
) {
const next = new Set(selected);
next.add(current.value);
updateSelected(next);
}
setStep('review');
setError(null);
return;
@@ -308,7 +298,7 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
lines.push(PALETTE.white(config.baseMessage));
lines.push(
PALETTE.midGray(
'Use ↑/↓ to move · Space to toggle · Enter selects highlighted tool and reviews.'
'Use ↑/↓ to move · Space to toggle · Enter to review selections.'
)
);
lines.push('');
+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'
);
});
});
});