Implement polyglot scripts to avoid os specific commands in commands#1610
Implement polyglot scripts to avoid os specific commands in commands#1610nacho4d wants to merge 1 commit intogithub:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR introduces “polyglot” wrapper scripts (extensionless entrypoints) so command templates and docs can reference a single script path across OSes, and updates packaging/tests to validate the new wrapper approach.
Changes:
- Add extensionless wrapper scripts in
scripts/plus a PowerShell arg-conversion helper. - Update command templates and extension docs to reference
scripts/<wrapper>instead of OS-specificscripts/bash/*.shandscripts/powershell/*.ps1. - Add CI + a shell test script to validate wrapper structure/line-endings; update release packaging extraction and chmod logic.
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated 22 comments.
Show a summary per file
| File | Description |
|---|---|
tests/test-polyglot-wrappers.sh |
Adds a local test runner for wrapper structure/execution/line-endings. |
templates/commands/analyze.md |
Switches script reference to scripts/check-prerequisites. |
templates/commands/checklist.md |
Switches script reference to scripts/check-prerequisites. |
templates/commands/clarify.md |
Switches script reference to scripts/check-prerequisites. |
templates/commands/implement.md |
Switches script reference to scripts/check-prerequisites. |
templates/commands/plan.md |
Switches script reference to scripts/setup-plan and scripts/update-agent-context. |
templates/commands/specify.md |
Switches script reference to scripts/create-new-feature. |
templates/commands/tasks.md |
Switches script reference to scripts/check-prerequisites. |
templates/commands/taskstoissues.md |
Switches script reference to scripts/check-prerequisites. |
src/specify_cli/__init__.py |
Extends chmod logic to cover extensionless wrappers under .specify/scripts. |
scripts/check-prerequisites |
Adds polyglot wrapper delegating to bash/PowerShell implementations. |
scripts/create-new-feature |
Adds polyglot wrapper delegating to bash/PowerShell implementations. |
scripts/setup-plan |
Adds polyglot wrapper delegating to bash/PowerShell implementations. |
scripts/update-agent-context |
Adds polyglot wrapper delegating to bash/PowerShell implementations. |
scripts/powershell/Convert-KebabToPascal.ps1 |
Adds helper to convert --kebab-case args into -PascalCase for PowerShell scripts. |
extensions/RFC-EXTENSION-SYSTEM.md |
Updates RFC examples to reference the wrapper path. |
extensions/EXTENSION-DEVELOPMENT-GUIDE.md |
Updates examples to reference wrapper-style script paths. |
AGENTS.md |
Documents the wrapper approach and the template expansion architecture. |
.github/workflows/test-polyglot-wrappers.yml |
Adds CI matrix to validate wrappers on Linux/macOS/Windows. |
.github/workflows/scripts/create-release-packages.sh |
Changes command generation to always extract wrapper path from sh:. |
.github/workflows/scripts/create-release-packages.ps1 |
Changes command generation to always extract wrapper path from sh:. |
.gitattributes |
Enforces LF for wrappers and sets explicit EOL rules for bash/PowerShell scripts. |
Comments suppressed due to low confidence (1)
.github/workflows/test-polyglot-wrappers.yml:102
- On Windows
cmd.exe, runningscripts\check-prerequisiteswill not execute an extensionless text file (PATHEXT only considers .cmd/.bat/.exe/etc.). This step is expected to fail unless you add a.cmdshim or invoke via an explicit interpreter (e.g.,bash scripts\check-prerequisitesorpowershell -File ...).
- name: Test wrapper execution via cmd.exe (check-prerequisites)
shell: cmd
run: |
echo Testing check-prerequisites wrapper via cmd.exe...
scripts\check-prerequisites --help
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # 2>nul & @echo off & goto :batch | ||
|
|
||
| # ===== UNIX SECTION ===== | ||
| "$(dirname "$0")/bash/script-name.sh" "$@" |
There was a problem hiding this comment.
In the example wrapper, the Unix section line is missing the bash invocation used in the real wrappers (it currently shows just a quoted path). As written, copying this snippet would fail; update it to match the actual wrapper pattern (invoke bash and keep quoting consistent).
| "$(dirname "$0")/bash/script-name.sh" "$@" | |
| bash "$(dirname "$0")/bash/script-name.sh" "$@" |
| sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks | ||
| ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks | ||
| sh: scripts/check-prerequisites --json --require-tasks --include-tasks | ||
| ps: scripts/check-prerequisites --json --require-tasks --include-tasks |
There was a problem hiding this comment.
The ps: script points to an extensionless wrapper (scripts/check-prerequisites). On Windows, PowerShell/cmd typically can’t execute an extensionless text file by path, so the generated commands may not be runnable in a PowerShell-based agent environment. Consider using a PowerShell-invokable entry (e.g., pwsh -File ...) or providing a .cmd shim that can be executed from PowerShell/cmd.
| ps: scripts/check-prerequisites --json --require-tasks --include-tasks | |
| ps: pwsh -File scripts/check-prerequisites --json --require-tasks --include-tasks |
| sh: scripts/bash/create-new-feature.sh --json "{ARGS}" | ||
| ps: scripts/powershell/create-new-feature.ps1 -Json "{ARGS}" | ||
| sh: scripts/create-new-feature --json "{ARGS}" | ||
| ps: scripts/create-new-feature --json "{ARGS}" |
There was a problem hiding this comment.
The ps: script points to an extensionless wrapper (scripts/create-new-feature). On Windows, PowerShell/cmd generally can’t execute extensionless text files directly, so --script ps projects may have non-runnable commands. Consider making the ps: entry explicitly call the PowerShell implementation (e.g., pwsh -File scripts/powershell/create-new-feature.ps1 ...) or provide a Windows-executable shim.
| ps: scripts/create-new-feature --json "{ARGS}" | |
| ps: pwsh -File scripts/powershell/create-new-feature.ps1 --json "{ARGS}" |
| scripts: | ||
| sh: scripts/bash/setup-plan.sh --json | ||
| ps: scripts/powershell/setup-plan.ps1 -Json | ||
| sh: scripts/setup-plan --json | ||
| ps: scripts/setup-plan --json | ||
| agent_scripts: | ||
| sh: scripts/bash/update-agent-context.sh __AGENT__ | ||
| ps: scripts/powershell/update-agent-context.ps1 -AgentType __AGENT__ | ||
| sh: scripts/update-agent-context __AGENT__ | ||
| ps: scripts/update-agent-context __AGENT__ |
There was a problem hiding this comment.
Both scripts: and agent_scripts: ps: entries point to extensionless wrappers (scripts/setup-plan, scripts/update-agent-context). These are unlikely to be directly executable from Windows PowerShell/cmd by path, which defeats the ps script-type package/default on Windows. Consider changing the ps: entries to explicit PowerShell invocations or adding Windows-executable shims that can be run from PowerShell/cmd.
See below for a potential fix:
ps: powershell -File scripts/setup-plan.ps1 --json
agent_scripts:
sh: scripts/update-agent-context __AGENT__
ps: powershell -File scripts/update-agent-context.ps1 __AGENT__
| echo Testing check-prerequisites wrapper via cmd.exe... | ||
| scripts\check-prerequisites --help | ||
|
|
||
| - name: Test wrapper execution via PowerShell (check-prerequisites) | ||
| shell: pwsh | ||
| run: | | ||
| Write-Host "Testing check-prerequisites wrapper via PowerShell..." | ||
| & "scripts/check-prerequisites" --help |
There was a problem hiding this comment.
Similarly, PowerShell won’t reliably execute an extensionless wrapper as a script/application when invoked by path (& "scripts/check-prerequisites"). Consider providing a Windows-invokable shim (e.g., .cmd) or changing the invocation to pwsh -File scripts/powershell/check-prerequisites.ps1 ... in this workflow.
This issue also appears on line 97 of the same file.
| echo Testing check-prerequisites wrapper via cmd.exe... | |
| scripts\check-prerequisites --help | |
| - name: Test wrapper execution via PowerShell (check-prerequisites) | |
| shell: pwsh | |
| run: | | |
| Write-Host "Testing check-prerequisites wrapper via PowerShell..." | |
| & "scripts/check-prerequisites" --help | |
| echo Testing check-prerequisites script via cmd.exe using PowerShell... | |
| pwsh -File scripts/powershell/check-prerequisites.ps1 --help | |
| - name: Test wrapper execution via PowerShell (check-prerequisites) | |
| shell: pwsh | |
| run: | | |
| Write-Host "Testing check-prerequisites script via PowerShell..." | |
| & "scripts/powershell/check-prerequisites.ps1" --help |
| sh: scripts/bash/check-prerequisites.sh --json --require-tasks --include-tasks | ||
| ps: scripts/powershell/check-prerequisites.ps1 -Json -RequireTasks -IncludeTasks | ||
| sh: scripts/check-prerequisites --json --require-tasks --include-tasks | ||
| ps: scripts/check-prerequisites --json --require-tasks --include-tasks |
There was a problem hiding this comment.
The ps: script points to an extensionless wrapper (scripts/check-prerequisites). If these commands are run under PowerShell on Windows, they’re likely not executable as written. Consider using a PowerShell-invokable command for ps: (e.g., pwsh -File ...) or ship a .cmd shim that the ps environment can execute.
| ps: scripts/check-prerequisites --json --require-tasks --include-tasks | |
| ps: bash scripts/check-prerequisites --json --require-tasks --include-tasks |
| sh: scripts/bash/check-prerequisites.sh --json | ||
| ps: scripts/powershell/check-prerequisites.ps1 -Json | ||
| sh: scripts/check-prerequisites --json | ||
| ps: scripts/check-prerequisites --json |
There was a problem hiding this comment.
The ps: script points to an extensionless wrapper (scripts/check-prerequisites). On Windows, PowerShell/cmd typically can’t execute an extensionless text file by path, so Windows --script ps installs may end up with command files that can’t run. Consider either making the Windows entry invoke pwsh -File scripts/powershell/check-prerequisites.ps1 ..., or shipping a Windows-invokable shim (e.g., .cmd) that the ps environment can run.
| ps: scripts/check-prerequisites --json | |
| ps: pwsh -File scripts/powershell/check-prerequisites.ps1 --json |
| @REM ===== WINDOWS SECTION ===== | ||
| @REM Convert kebab-case arguments to PowerShell PascalCase using helper script | ||
| @REM Set DEBUG_WRAPPER=1 to see argument conversion details | ||
| @powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0powershell\Convert-KebabToPascal.ps1" "%~dp0powershell\check-prerequisites.ps1" %* |
There was a problem hiding this comment.
The Windows section invokes powershell, which is Windows PowerShell 5.1 on most systems. The target scripts in scripts/powershell/*.ps1 start with a #!/usr/bin/env pwsh shebang, which will be parsed as invalid syntax in Windows PowerShell. Prefer invoking pwsh here (and optionally include -NoProfile) so the delegated .ps1 scripts can run reliably.
| @powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0powershell\Convert-KebabToPascal.ps1" "%~dp0powershell\check-prerequisites.ps1" %* | |
| @pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0powershell\Convert-KebabToPascal.ps1" "%~dp0powershell\check-prerequisites.ps1" %* |
|
|
||
| :batch | ||
| @REM ===== WINDOWS SECTION ===== | ||
| @powershell -ExecutionPolicy Bypass -File "%~dp0powershell\script-name.ps1" %* |
There was a problem hiding this comment.
The Windows example uses @powershell ..., but the repository’s PowerShell scripts are pwsh-targeted (e.g., scripts/powershell/*.ps1 start with #!/usr/bin/env pwsh). Using powershell here is likely to mislead users into running scripts with Windows PowerShell 5.1, which may fail on the shebang line; consider updating the example to use pwsh instead.
| @powershell -ExecutionPolicy Bypass -File "%~dp0powershell\script-name.ps1" %* | |
| @pwsh -ExecutionPolicy Bypass -File "%~dp0powershell\script-name.ps1" %* |
| # Process polyglot wrappers (extensionless files with shebang) | ||
| polyglot_wrappers = [ | ||
| "check-prerequisites", | ||
| "setup-plan", | ||
| "create-new-feature", | ||
| "update-agent-context" | ||
| ] | ||
|
|
||
| for wrapper_name in polyglot_wrappers: | ||
| wrapper = scripts_root / wrapper_name | ||
| if not wrapper.is_file(): | ||
| continue | ||
| try: | ||
| # Check for shebang | ||
| with wrapper.open("rb") as f: | ||
| if f.read(2) != b"#!": | ||
| continue | ||
|
|
||
| st = wrapper.stat() | ||
| mode = st.st_mode | ||
| if mode & 0o111: # Already executable | ||
| continue | ||
|
|
||
| # Set execute permissions | ||
| new_mode = mode | ||
| if mode & 0o400: new_mode |= 0o100 | ||
| if mode & 0o040: new_mode |= 0o010 | ||
| if mode & 0o004: new_mode |= 0o001 | ||
| if not (new_mode & 0o100): | ||
| new_mode |= 0o100 | ||
|
|
||
| os.chmod(wrapper, new_mode) | ||
| updated += 1 | ||
| except Exception as e: | ||
| failures.append(f"{wrapper_name}: {e}") |
There was a problem hiding this comment.
This hard-coded wrapper name list can easily drift from what’s actually shipped in .specify/scripts (e.g., new wrappers added later won’t get execute bits). Since you already detect wrappers via a #! prefix, consider scanning scripts_root for extensionless regular files with a shebang and chmod those instead of maintaining a static list.
| # Process polyglot wrappers (extensionless files with shebang) | |
| polyglot_wrappers = [ | |
| "check-prerequisites", | |
| "setup-plan", | |
| "create-new-feature", | |
| "update-agent-context" | |
| ] | |
| for wrapper_name in polyglot_wrappers: | |
| wrapper = scripts_root / wrapper_name | |
| if not wrapper.is_file(): | |
| continue | |
| try: | |
| # Check for shebang | |
| with wrapper.open("rb") as f: | |
| if f.read(2) != b"#!": | |
| continue | |
| st = wrapper.stat() | |
| mode = st.st_mode | |
| if mode & 0o111: # Already executable | |
| continue | |
| # Set execute permissions | |
| new_mode = mode | |
| if mode & 0o400: new_mode |= 0o100 | |
| if mode & 0o040: new_mode |= 0o010 | |
| if mode & 0o004: new_mode |= 0o001 | |
| if not (new_mode & 0o100): | |
| new_mode |= 0o100 | |
| os.chmod(wrapper, new_mode) | |
| updated += 1 | |
| except Exception as e: | |
| failures.append(f"{wrapper_name}: {e}") | |
| # Process polyglot wrappers: extensionless regular files with a shebang | |
| for wrapper in scripts_root.rglob("*"): | |
| try: | |
| if wrapper.is_symlink() or not wrapper.is_file(): | |
| continue | |
| # Skip files with an extension; we only want extensionless wrappers | |
| if wrapper.suffix: | |
| continue | |
| # Check for shebang | |
| try: | |
| with wrapper.open("rb") as f: | |
| if f.read(2) != b"#!": | |
| continue | |
| except Exception: | |
| continue | |
| st = wrapper.stat() | |
| mode = st.st_mode | |
| if mode & 0o111: # Already executable | |
| continue | |
| # Set execute permissions based on existing read bits | |
| new_mode = mode | |
| if mode & 0o400: | |
| new_mode |= 0o100 | |
| if mode & 0o040: | |
| new_mode |= 0o010 | |
| if mode & 0o004: | |
| new_mode |= 0o001 | |
| # Ensure at least owner execute is set | |
| if not (new_mode & 0o100): | |
| new_mode |= 0o100 | |
| os.chmod(wrapper, new_mode) | |
| updated += 1 | |
| except Exception as e: | |
| failures.append(f"{wrapper.relative_to(scripts_root)}: {e}") |
No description provided.