Skip to content

Commit 4c0bc7e

Browse files
committed
fix: use posix project paths in cli output
1 parent a270b19 commit 4c0bc7e

3 files changed

Lines changed: 51 additions & 8 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -853,7 +853,7 @@ def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None =
853853
os.chmod(script, new_mode)
854854
updated += 1
855855
except Exception as e:
856-
failures.append(f"{script.relative_to(project_path)}: {e}")
856+
failures.append(f"{_display_project_path(project_path, script)}: {e}")
857857
if tracker:
858858
detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "")
859859
tracker.add("chmod", "Set script permissions recursively")
@@ -2088,6 +2088,19 @@ def _set_default_integration_or_exit(*args: Any, **kwargs: Any) -> None:
20882088
raise typer.Exit(1)
20892089

20902090

2091+
def _display_project_path(project_root: Path, path: str | Path) -> str:
2092+
"""Return a stable POSIX-style display path for paths under a project."""
2093+
path_obj = Path(path)
2094+
try:
2095+
rel_path = path_obj.relative_to(project_root) if path_obj.is_absolute() else path_obj
2096+
except ValueError:
2097+
try:
2098+
rel_path = path_obj.resolve().relative_to(project_root.resolve())
2099+
except (OSError, ValueError):
2100+
return path_obj.as_posix()
2101+
return rel_path.as_posix()
2102+
2103+
20912104
def _require_specify_project() -> Path:
20922105
"""Return the current project root if it is a spec-kit project, else exit."""
20932106
project_root = Path.cwd()
@@ -2542,7 +2555,7 @@ def integration_uninstall(
25422555
if skipped:
25432556
console.print(f"\n[yellow]⚠[/yellow] {len(skipped)} modified file(s) were preserved:")
25442557
for path in skipped:
2545-
rel = path.relative_to(project_root) if path.is_absolute() else path
2558+
rel = _display_project_path(project_root, path)
25462559
console.print(f" {rel}")
25472560

25482561

@@ -3741,7 +3754,7 @@ def preset_catalog_list():
37413754
except PresetValidationError:
37423755
proj_loaded = False
37433756
if proj_loaded:
3744-
console.print(f"[dim]Config: {config_path.relative_to(project_root)}[/dim]")
3757+
console.print(f"[dim]Config: {_display_project_path(project_root, config_path)}[/dim]")
37453758
else:
37463759
try:
37473760
user_loaded = user_config_path.exists() and catalog._load_catalog_config(user_config_path) is not None
@@ -3788,7 +3801,8 @@ def preset_catalog_add(
37883801
try:
37893802
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
37903803
except Exception as e:
3791-
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
3804+
config_label = _display_project_path(project_root, config_path)
3805+
console.print(f"[red]Error:[/red] Failed to read {config_label}: {e}")
37923806
raise typer.Exit(1)
37933807
else:
37943808
config = {}
@@ -3820,7 +3834,7 @@ def preset_catalog_add(
38203834
console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})")
38213835
console.print(f" URL: {url}")
38223836
console.print(f" Priority: {priority}")
3823-
console.print(f"\nConfig saved to {config_path.relative_to(project_root)}")
3837+
console.print(f"\nConfig saved to {_display_project_path(project_root, config_path)}")
38243838

38253839

38263840
@preset_catalog_app.command("remove")
@@ -4058,7 +4072,7 @@ def catalog_list():
40584072
except ValidationError:
40594073
proj_loaded = False
40604074
if proj_loaded:
4061-
console.print(f"[dim]Config: {config_path.relative_to(project_root)}[/dim]")
4075+
console.print(f"[dim]Config: {_display_project_path(project_root, config_path)}[/dim]")
40624076
else:
40634077
try:
40644078
user_loaded = user_config_path.exists() and catalog._load_catalog_config(user_config_path) is not None
@@ -4105,7 +4119,8 @@ def catalog_add(
41054119
try:
41064120
config = yaml.safe_load(config_path.read_text(encoding="utf-8")) or {}
41074121
except Exception as e:
4108-
console.print(f"[red]Error:[/red] Failed to read {config_path}: {e}")
4122+
config_label = _display_project_path(project_root, config_path)
4123+
console.print(f"[red]Error:[/red] Failed to read {config_label}: {e}")
41094124
raise typer.Exit(1)
41104125
else:
41114126
config = {}
@@ -4137,7 +4152,7 @@ def catalog_add(
41374152
console.print(f"\n[green]✓[/green] Added catalog '[bold]{name}[/bold]' ({install_label})")
41384153
console.print(f" URL: {url}")
41394154
console.print(f" Priority: {priority}")
4140-
console.print(f"\nConfig saved to {config_path.relative_to(project_root)}")
4155+
console.print(f"\nConfig saved to {_display_project_path(project_root, config_path)}")
41414156

41424157

41434158
@catalog_app.command("remove")

tests/integrations/test_cli.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,33 @@ def test_project_scoped_commands_require_specify_directory(self, tmp_path):
10711071
assert result.exit_code == 1, failure_context
10721072
assert "Not a spec-kit project" in result.output, failure_context
10731073

1074+
def test_catalog_config_output_uses_posix_paths(self, tmp_path):
1075+
project = self._make_project(tmp_path)
1076+
1077+
preset_add = self._invoke([
1078+
"preset", "catalog", "add",
1079+
"https://example.com/preset-catalog.yml",
1080+
"--name", "demo-presets",
1081+
], project)
1082+
assert preset_add.exit_code == 0, preset_add.output
1083+
assert "Config saved to .specify/preset-catalogs.yml" in preset_add.output
1084+
1085+
preset_list = self._invoke(["preset", "catalog", "list"], project)
1086+
assert preset_list.exit_code == 0, preset_list.output
1087+
assert "Config: .specify/preset-catalogs.yml" in preset_list.output
1088+
1089+
extension_add = self._invoke([
1090+
"extension", "catalog", "add",
1091+
"https://example.com/extension-catalog.yml",
1092+
"--name", "demo-extensions",
1093+
], project)
1094+
assert extension_add.exit_code == 0, extension_add.output
1095+
assert "Config saved to .specify/extension-catalogs.yml" in extension_add.output
1096+
1097+
extension_list = self._invoke(["extension", "catalog", "list"], project)
1098+
assert extension_list.exit_code == 0, extension_list.output
1099+
assert "Config: .specify/extension-catalogs.yml" in extension_list.output
1100+
10741101
# -- search ------------------------------------------------------------
10751102

10761103
def test_search_lists_all(self, tmp_path, monkeypatch):

tests/integrations/test_integration_subcommand.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,7 @@ def test_uninstall_preserves_modified_files(self, tmp_path):
397397
os.chdir(old_cwd)
398398
assert result.exit_code == 0
399399
assert "preserved" in result.output
400+
assert ".claude/skills/speckit-plan/SKILL.md" in result.output
400401

401402
# Modified file kept
402403
assert plan_file.exists()

0 commit comments

Comments
 (0)