Ensure POSIX .sh scripts under .specify/scripts and .specify/extensions (recursively) have execute bits (no-op on Windows).
(project_path: Path, tracker: StepTracker | None = None)
| 207 | |
| 208 | |
| 209 | def ensure_executable_scripts(project_path: Path, tracker: StepTracker | None = None) -> None: |
| 210 | """Ensure POSIX .sh scripts under .specify/scripts and .specify/extensions (recursively) have execute bits (no-op on Windows).""" |
| 211 | if os.name == "nt": |
| 212 | return # Windows: skip silently |
| 213 | scan_roots = [ |
| 214 | project_path / ".specify" / "scripts", |
| 215 | project_path / ".specify" / "extensions", |
| 216 | ] |
| 217 | failures: list[str] = [] |
| 218 | updated = 0 |
| 219 | for scripts_root in scan_roots: |
| 220 | if not scripts_root.is_dir(): |
| 221 | continue |
| 222 | for script in scripts_root.rglob("*.sh"): |
| 223 | try: |
| 224 | if script.is_symlink() or not script.is_file(): |
| 225 | continue |
| 226 | try: |
| 227 | with script.open("rb") as f: |
| 228 | if f.read(2) != b"#!": |
| 229 | continue |
| 230 | except Exception: |
| 231 | continue |
| 232 | st = script.stat() |
| 233 | mode = st.st_mode |
| 234 | if mode & 0o111: |
| 235 | continue |
| 236 | new_mode = mode |
| 237 | if mode & 0o400: |
| 238 | new_mode |= 0o100 |
| 239 | if mode & 0o040: |
| 240 | new_mode |= 0o010 |
| 241 | if mode & 0o004: |
| 242 | new_mode |= 0o001 |
| 243 | if not (new_mode & 0o100): |
| 244 | new_mode |= 0o100 |
| 245 | os.chmod(script, new_mode) |
| 246 | updated += 1 |
| 247 | except Exception as e: |
| 248 | failures.append(f"{_display_project_path(project_path, script)}: {e}") |
| 249 | if tracker: |
| 250 | detail = f"{updated} updated" + (f", {len(failures)} failed" if failures else "") |
| 251 | tracker.add("chmod", "Set script permissions recursively") |
| 252 | (tracker.error if failures else tracker.complete)("chmod", detail) |
| 253 | else: |
| 254 | if updated: |
| 255 | console.print(f"[cyan]Updated execute permissions on {updated} script(s) recursively[/cyan]") |
| 256 | if failures: |
| 257 | console.print("[yellow]Some scripts could not be updated:[/yellow]") |
| 258 | for f in failures: |
| 259 | console.print(f" - {f}") |
| 260 | |
| 261 | # --------------------------------------------------------------------------- |
| 262 | # Skills directory helpers |
no test coverage detected