mirror of
https://github.com/Fission-AI/OpenSpec.git
synced 2026-10-04 06:18:24 +08:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17771b80c4 |
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@fission-ai/openspec": minor
|
||||
---
|
||||
|
||||
Add Amazon Q Developer CLI integration. OpenSpec now supports Amazon Q Developer with automatic prompt generation in `.amazonq/prompts/` directory, allowing you to use OpenSpec slash commands with Amazon Q's @-syntax.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@fission-ai/openspec": minor
|
||||
---
|
||||
|
||||
Add support for Codex and GitHub Copilot slash commands with YAML frontmatter and $ARGUMENTS
|
||||
@@ -1,29 +1,5 @@
|
||||
# @fission-ai/openspec
|
||||
|
||||
## 0.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- d7e0ce8: Improve init wizard Enter key behavior to allow proceeding through prompts more naturally
|
||||
|
||||
## 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
|
||||
|
||||
- efbbf3b: Add support for Codex and GitHub Copilot slash commands with YAML frontmatter and $ARGUMENTS
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -97,7 +97,6 @@ These tools have built-in OpenSpec commands. Select the OpenSpec integration whe
|
||||
| **Windsurf** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.windsurf/workflows/`) |
|
||||
| **Codex** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (global: `~/.codex/prompts`, auto-installed) |
|
||||
| **GitHub Copilot** | `/openspec-proposal`, `/openspec-apply`, `/openspec-archive` (`.github/prompts/`) |
|
||||
| **Amazon Q Developer** | `@openspec-proposal`, `@openspec-apply`, `@openspec-archive` (`.amazonq/prompts/`) |
|
||||
|
||||
Kilo Code discovers team workflows automatically. Save the generated files under `.kilocode/workflows/` and trigger them from the command palette with `/openspec-proposal.md`, `/openspec-apply.md`, or `/openspec-archive.md`.
|
||||
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@fission-ai/openspec",
|
||||
"version": "0.10.0",
|
||||
"version": "0.8.1",
|
||||
"description": "AI-native system for spec-driven development",
|
||||
"keywords": [
|
||||
"openspec",
|
||||
|
||||
@@ -24,6 +24,5 @@ export const AI_TOOLS: AIToolOption[] = [
|
||||
{ name: 'Windsurf', value: 'windsurf', available: true, successLabel: 'Windsurf' },
|
||||
{ name: 'Codex', value: 'codex', available: true, successLabel: 'Codex' },
|
||||
{ name: 'GitHub Copilot', value: 'github-copilot', available: true, successLabel: 'GitHub Copilot' },
|
||||
{ name: 'Amazon Q Developer', value: 'amazon-q', available: true, successLabel: 'Amazon Q Developer' },
|
||||
{ name: 'AGENTS.md (works with Amp, VS Code, …)', value: 'agents', available: false, successLabel: 'your AGENTS.md-compatible assistant' }
|
||||
];
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import { SlashCommandConfigurator } from './base.js';
|
||||
import { SlashCommandId } from '../../templates/index.js';
|
||||
|
||||
const FILE_PATHS: Record<SlashCommandId, string> = {
|
||||
proposal: '.amazonq/prompts/openspec-proposal.md',
|
||||
apply: '.amazonq/prompts/openspec-apply.md',
|
||||
archive: '.amazonq/prompts/openspec-archive.md'
|
||||
};
|
||||
|
||||
const FRONTMATTER: Record<SlashCommandId, string> = {
|
||||
proposal: `---
|
||||
description: Scaffold a new OpenSpec change and validate strictly.
|
||||
---
|
||||
|
||||
The user has requested the following change proposal. Use the openspec instructions to create their change proposal.
|
||||
|
||||
<UserRequest>
|
||||
$ARGUMENTS
|
||||
</UserRequest>`,
|
||||
apply: `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
|
||||
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
|
||||
|
||||
<ChangeId>
|
||||
$ARGUMENTS
|
||||
</ChangeId>`,
|
||||
archive: `---
|
||||
description: Archive a deployed OpenSpec change and update specs.
|
||||
---
|
||||
|
||||
The user wants to archive the following deployed change. Use the openspec instructions to archive the change and update specs.
|
||||
|
||||
<ChangeId>
|
||||
$ARGUMENTS
|
||||
</ChangeId>`
|
||||
};
|
||||
|
||||
export class AmazonQSlashCommandConfigurator extends SlashCommandConfigurator {
|
||||
readonly toolId = 'amazon-q';
|
||||
readonly isAvailable = true;
|
||||
|
||||
protected getRelativePath(id: SlashCommandId): string {
|
||||
return FILE_PATHS[id];
|
||||
}
|
||||
|
||||
protected getFrontmatter(id: SlashCommandId): string {
|
||||
return FRONTMATTER[id];
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
@@ -5,7 +5,6 @@ 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",
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import { KiloCodeSlashCommandConfigurator } from './kilocode.js';
|
||||
import { OpenCodeSlashCommandConfigurator } from './opencode.js';
|
||||
import { CodexSlashCommandConfigurator } from './codex.js';
|
||||
import { GitHubCopilotSlashCommandConfigurator } from './github-copilot.js';
|
||||
import { AmazonQSlashCommandConfigurator } from './amazon-q.js';
|
||||
|
||||
export class SlashCommandRegistry {
|
||||
private static configurators: Map<string, SlashCommandConfigurator> = new Map();
|
||||
@@ -19,7 +18,6 @@ export class SlashCommandRegistry {
|
||||
const opencode = new OpenCodeSlashCommandConfigurator();
|
||||
const codex = new CodexSlashCommandConfigurator();
|
||||
const githubCopilot = new GitHubCopilotSlashCommandConfigurator();
|
||||
const amazonQ = new AmazonQSlashCommandConfigurator();
|
||||
|
||||
this.configurators.set(claude.toolId, claude);
|
||||
this.configurators.set(cursor.toolId, cursor);
|
||||
@@ -28,7 +26,6 @@ export class SlashCommandRegistry {
|
||||
this.configurators.set(opencode.toolId, opencode);
|
||||
this.configurators.set(codex.toolId, codex);
|
||||
this.configurators.set(githubCopilot.toolId, githubCopilot);
|
||||
this.configurators.set(amazonQ.toolId, amazonQ);
|
||||
}
|
||||
|
||||
static register(configurator: SlashCommandConfigurator): void {
|
||||
|
||||
+1
-11
@@ -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
@@ -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 = [
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -570,54 +570,6 @@ describe('InitCommand', () => {
|
||||
);
|
||||
expect(githubCopilotChoice.configured).toBe(true);
|
||||
});
|
||||
|
||||
it('should create Amazon Q Developer prompt files with templates', async () => {
|
||||
queueSelections('amazon-q', DONE);
|
||||
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const proposalPath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-proposal.md'
|
||||
);
|
||||
const applyPath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-apply.md'
|
||||
);
|
||||
const archivePath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-archive.md'
|
||||
);
|
||||
|
||||
expect(await fileExists(proposalPath)).toBe(true);
|
||||
expect(await fileExists(applyPath)).toBe(true);
|
||||
expect(await fileExists(archivePath)).toBe(true);
|
||||
|
||||
const proposalContent = await fs.readFile(proposalPath, 'utf-8');
|
||||
expect(proposalContent).toContain('---');
|
||||
expect(proposalContent).toContain('description: Scaffold a new OpenSpec change and validate strictly.');
|
||||
expect(proposalContent).toContain('$ARGUMENTS');
|
||||
expect(proposalContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(proposalContent).toContain('**Guardrails**');
|
||||
|
||||
const applyContent = await fs.readFile(applyPath, 'utf-8');
|
||||
expect(applyContent).toContain('---');
|
||||
expect(applyContent).toContain('description: Implement an approved OpenSpec change and keep tasks in sync.');
|
||||
expect(applyContent).toContain('$ARGUMENTS');
|
||||
expect(applyContent).toContain('<!-- OPENSPEC:START -->');
|
||||
});
|
||||
|
||||
it('should mark Amazon Q Developer as already configured during extend mode', async () => {
|
||||
queueSelections('amazon-q', DONE, 'amazon-q', DONE);
|
||||
await initCommand.execute(testDir);
|
||||
await initCommand.execute(testDir);
|
||||
|
||||
const secondRunArgs = mockPrompt.mock.calls[1][0];
|
||||
const amazonQChoice = secondRunArgs.choices.find(
|
||||
(choice: any) => choice.value === 'amazon-q'
|
||||
);
|
||||
expect(amazonQChoice.configured).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
|
||||
@@ -377,72 +377,6 @@ Old body
|
||||
await expect(FileSystemUtils.fileExists(ghArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should refresh existing Amazon Q Developer prompts', async () => {
|
||||
const aqPath = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-apply.md'
|
||||
);
|
||||
await fs.mkdir(path.dirname(aqPath), { recursive: true });
|
||||
const initialContent = `---
|
||||
description: Implement an approved OpenSpec change and keep tasks in sync.
|
||||
---
|
||||
|
||||
The user wants to apply the following change. Use the openspec instructions to implement the approved change.
|
||||
|
||||
<ChangeId>
|
||||
$ARGUMENTS
|
||||
</ChangeId>
|
||||
<!-- OPENSPEC:START -->
|
||||
Old body
|
||||
<!-- OPENSPEC:END -->`;
|
||||
await fs.writeFile(aqPath, initialContent);
|
||||
|
||||
const consoleSpy = vi.spyOn(console, 'log');
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const updatedContent = await fs.readFile(aqPath, 'utf-8');
|
||||
expect(updatedContent).toContain('**Guardrails**');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:START -->');
|
||||
expect(updatedContent).toContain('<!-- OPENSPEC:END -->');
|
||||
expect(updatedContent).not.toContain('Old body');
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('.amazonq/prompts/openspec-apply.md')
|
||||
);
|
||||
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should not create missing Amazon Q Developer prompts on update', async () => {
|
||||
const aqApply = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-apply.md'
|
||||
);
|
||||
|
||||
// Only create apply; leave proposal and archive missing
|
||||
await fs.mkdir(path.dirname(aqApply), { recursive: true });
|
||||
await fs.writeFile(
|
||||
aqApply,
|
||||
'---\ndescription: Old\n---\n\nThe user wants to apply the following change.\n\n<ChangeId>\n $ARGUMENTS\n</ChangeId>\n<!-- OPENSPEC:START -->\nOld\n<!-- OPENSPEC:END -->'
|
||||
);
|
||||
|
||||
await updateCommand.execute(testDir);
|
||||
|
||||
const aqProposal = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-proposal.md'
|
||||
);
|
||||
const aqArchive = path.join(
|
||||
testDir,
|
||||
'.amazonq/prompts/openspec-archive.md'
|
||||
);
|
||||
|
||||
// Confirm they weren't created by update
|
||||
await expect(FileSystemUtils.fileExists(aqProposal)).resolves.toBe(false);
|
||||
await expect(FileSystemUtils.fileExists(aqArchive)).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it('should preserve Windsurf content outside markers during update', async () => {
|
||||
const wsPath = path.join(
|
||||
testDir,
|
||||
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user