Compare commits

..
Author SHA1 Message Date
Tabish Bidiwale e82eef9969 Fix package name to use scoped package @fission-ai/openspec 2025-10-13 19:37:37 +11:00
Tabish Bidiwale 6bfc82b60a Add changeset for Amazon Q Developer integration 2025-10-13 19:28:42 +11:00
Brian AndersonandBrian Anderson 56d57da119 Amazon Q Developer integration (#160)
* feat: add Amazon Q Developer CLI integration

- Add AmazonQSlashCommandConfigurator for .amazonq/prompts/ support
- Register Amazon Q in SlashCommandRegistry and AI_TOOLS config
- Generate slash commands compatible with Amazon Q CLI (@-syntax)
- Update README.md with Amazon Q Developer in tools table

* test: add Amazon Q Developer integration tests

- Add init tests for Amazon Q prompt file creation and configuration detection
- Add update tests for Amazon Q prompt refresh and missing file handling
- Follow same test patterns as GitHub Copilot integration
- Verify .amazonq/prompts/ directory structure and file content

---------

Co-authored-by: Brian Anderson <brian@popsofviolet.com>
2025-10-12 18:17:01 +11:00
github-actions[bot]andTabish Bidiwale f56189a8f7 chore(release): version packages (#158)
* Version Packages

* chore: trigger CI

* chore: trigger CI again

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-10-12 15:06:46 +11:00
Tabish Bidiwale d7e0ce85e5 Add changeset for init wizard Enter key improvements (#157) 2025-10-12 14:59:47 +11:00
Tabish Bidiwale eb0d50c094 feat: improve init wizard Enter key behavior (#156)
Update the tool selection wizard so pressing Enter on a highlighted tool
automatically selects it before proceeding to the review step. This aligns
with common CLI expectations where Enter confirms the highlighted item.

Changes:
- Add logic to select the currently highlighted tool when Enter is pressed
- Update help text to clarify that Enter selects the highlighted tool
- Maintain Space key for toggling multiple selections

This reduces friction during onboarding by matching user expectations,
especially for users who navigate to a tool and press Enter without
first toggling it with Space.
2025-10-12 14:53:44 +11:00
github-actions[bot]andTabish Bidiwale c482f1b47a chore(release): version packages (#150)
* chore: trigger CI

* chore: trigger CI

* Version Packages

---------

Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2025-10-11 11:47:37 +11:00
Tabish Bidiwale 2ae0484ac7 chore: add changeset for cross-platform fixes release (#147)
Add changeset for patch release including fixes for joinPath behavior and slash command path resolution across platforms.
2025-10-11 11:27:26 +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
github-actions[bot]andTabish Bidiwale c7ca76cb4f chore(release): version packages (#138)
* Version Packages

* chore: trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
2025-10-09 17:48:59 +11:00
06bd3999bf chore(release): version packages (#137)
* Version Packages

* empty

* RUN CI

* trigger CI

* empty

* trigger CI

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tabish Bidiwale <tabishbidiwale@gmail.com>
Co-authored-by: Tabish Bidiwale <30385142+TabishB@users.noreply.github.com>
2025-10-09 17:38:55 +11:00
Tabish Bidiwale 821097079a chore: add changeset for Windows OpenSpec fix (#136)
Add changeset for patch release to fix OpenSpec not working on Windows
when Codex integration is selected. Includes cross-platform path handling
and normalization fixes.
2025-10-09 17:33:16 +11:00
Tabish Bidiwale 42e3118b0c fix: normalize paths for cross-platform consistency in logging (#135)
- Use POSIX-style forward slashes in FILE_PATHS for consistent logging
- Normalize backslashes to forward slashes in update command output
- Improves Windows compatibility and log readability
2025-10-09 17:20:33 +11:00
Tabish Bidiwale a785c2a99a fix: use path.join for cross-platform compatibility in Codex FILE_PATHS (#134)
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 16:51:23 +11:00
18 changed files with 333 additions and 17 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@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.
+18
View File
@@ -1,5 +1,23 @@
# @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
+1
View File
@@ -97,6 +97,7 @@ 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`.
@@ -0,0 +1,14 @@
## 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.
@@ -0,0 +1,10 @@
## 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
@@ -0,0 +1,8 @@
## 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.0",
"version": "0.10.0",
"description": "AI-native system for spec-driven development",
"keywords": [
"openspec",
+1
View File
@@ -24,5 +24,6 @@ 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' }
];
+51
View File
@@ -0,0 +1,51 @@
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];
}
}
+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> {
+15 -8
View File
@@ -5,10 +5,11 @@ 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: path.join(".codex", "prompts", "openspec-proposal.md"),
apply: path.join(".codex", "prompts", "openspec-apply.md"),
archive: path.join(".codex", "prompts", "openspec-archive.md"),
proposal: ".codex/prompts/openspec-proposal.md",
apply: ".codex/prompts/openspec-apply.md",
archive: ".codex/prompts/openspec-archive.md",
};
export class CodexSlashCommandConfigurator extends SlashCommandConfigurator {
@@ -48,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
@@ -59,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));
@@ -82,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);
@@ -114,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);
}
}
+3
View File
@@ -6,6 +6,7 @@ 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();
@@ -18,6 +19,7 @@ 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);
@@ -26,6 +28,7 @@ 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 {
+11 -1
View File
@@ -220,6 +220,16 @@ 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;
@@ -298,7 +308,7 @@ const toolSelectionWizard = createPrompt<string[], ToolWizardConfig>(
lines.push(PALETTE.white(config.baseMessage));
lines.push(
PALETTE.midGray(
'Use ↑/↓ to move · Space to toggle · Enter to review selections.'
'Use ↑/↓ to move · Space to toggle · Enter selects highlighted tool and reviews.'
)
);
lines.push('');
+3 -3
View File
@@ -106,9 +106,9 @@ export class UpdateCommand {
}
if (updatedSlashFiles.length > 0) {
summaryParts.push(
`Updated slash commands: ${updatedSlashFiles.join(', ')}`
);
// Normalize to forward slashes for cross-platform log consistency
const normalized = updatedSlashFiles.map((p) => p.replace(/\\/g, '/'));
summaryParts.push(`Updated slash commands: ${normalized.join(', ')}`);
}
const failedItems = [
+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
@@ -570,6 +570,54 @@ 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', () => {
+66
View File
@@ -377,6 +377,72 @@ 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,
+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'
);
});
});
});