Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/standalone-prefer-exe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@bradygaster/squad-cli": patch
---

Prefer `squad.exe` over `squad.cmd` when writing the `squad_state` MCP spec from a standalone bundle on Windows. Since the fix for CVE-2024-27980 Node refuses to spawn a `.cmd` without `shell: true`, so an MCP client that spawns the command directly would fail to start the server. Windows bundles now ship a real `squad.exe`, and the resolver picks it first.
76 changes: 75 additions & 1 deletion .github/workflows/squad-standalone-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ jobs:
- name: Install workspace dependencies
run: npm ci --no-audit --no-fund

- name: Ensure unzip is available
# GNU tar cannot read zip archives, so cross-building a Windows bundle
# on a Linux runner needs unzip to unpack the Node runtime.
if: matrix.platform == 'win32'
run: |
set -euo pipefail
command -v unzip >/dev/null 2>&1 || {
sudo apt-get update -qq && sudo apt-get install -y -qq unzip
}
unzip -v | head -n 1

- name: Build packages
run: npm run build

Expand Down Expand Up @@ -130,8 +141,13 @@ jobs:
test -f "${bundle}/BUNDLE-INFO.json"
test -d "${bundle}/app/node_modules/@bradygaster/squad-cli"
if [ "${TARGET_PLATFORM}" = "win32" ]; then
test -f "${bundle}/runtime/node.exe"
# squad.exe is a Node single executable built from the vendored
# runtime, so it replaces runtime/node.exe rather than sitting
# next to it. winget requires this real .exe — its portable
# installer does not accept .cmd targets.
test -f "${bundle}/squad.exe"
test -f "${bundle}/squad.cmd"
test ! -d "${bundle}/runtime"
else
test -f "${bundle}/runtime/bin/node"
test -f "${bundle}/squad"
Expand Down Expand Up @@ -178,3 +194,61 @@ jobs:
run: |
set -euo pipefail
gh release upload "${TAG}" ./* --clobber --repo "${GITHUB_REPOSITORY}"

packaging:
name: Generate Homebrew and winget manifests
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 #v6
with:
node-version: 22

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8
with:
path: artifacts
merge-multiple: true

- name: Generate manifests
env:
TAG: ${{ github.event.release.tag_name || github.ref_name }}
run: |
set -euo pipefail
cat artifacts/*.sha256 > "${RUNNER_TEMP}/SHA256SUMS.txt"
node scripts/generate-packaging.mjs \
--version "${TAG}" \
--checksums "${RUNNER_TEMP}/SHA256SUMS.txt" \
--out-dir dist-packaging

# Submitting to homebrew-core and winget-pkgs requires pushing to
# repositories this workflow has no credentials for, so the manifests are
# published as an artifact for a maintainer to submit. Automating the
# submission is follow-up work once the packages are accepted once.
- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a #v7
with:
name: packaging-manifests
path: dist-packaging/
retention-days: 30
if-no-files-found: error

- name: Summarize
env:
TAG: ${{ github.event.release.tag_name || github.ref_name }}
run: |
set -euo pipefail
{
echo "### Packaging manifests for ${TAG}"
echo ""
echo "Download the \`packaging-manifests\` artifact, then:"
echo ""
# shellcheck disable=SC2016
echo '- Homebrew — copy `homebrew/squad.rb` into the tap as `Casks/squad.rb`.'
# shellcheck disable=SC2016
echo '- winget — copy `winget/` into `winget-pkgs/manifests/b/bradygaster/Squad/<version>/`,'
# shellcheck disable=SC2016
echo ' validate with `winget validate --manifest <dir>`, then open the PR.'
} >> "${GITHUB_STEP_SUMMARY}"

1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
"Permadeath",
"permadeath",
"PKCE",
"pkgs",
"pluggability",
"Pomodoro",
"pomodoro",
Expand Down
70 changes: 60 additions & 10 deletions docs/src/content/docs/features/standalone-install.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,25 @@ curl -fsSL https://raw.githubusercontent.com/bradygaster/squad/dev/scripts/insta

### Windows

Download `squad-win32-x64.zip` (or `squad-win32-arm64.zip`) from the
Install with winget:

```powershell
winget install bradygaster.Squad
```

Or download `squad-win32-x64.zip` (or `squad-win32-arm64.zip`) from the
[releases page](https://github.com/bradygaster/squad/releases), unpack it, and
add the folder to your `PATH`. The bundle ships both `squad.cmd` and
`squad.ps1`.
add the folder to your `PATH`.

### macOS

Install with Homebrew:

```sh
brew install --cask bradygaster/squad/squad
```

Or use the install script above, which works on macOS too.

### Direct download

Expand All @@ -59,8 +74,8 @@ SHA256SUMS.txt

```
squad-<platform>-<arch>/
├── squad launcher (squad.cmd + squad.ps1 on Windows)
├── runtime/ vendored Node.js runtime
├── squad launcher (squad.exe + squad.cmd + squad.ps1 on Windows)
├── runtime/ vendored Node.js runtime (POSIX only — see below)
├── app/ squad-cli, squad-sdk, templates, presets
└── BUNDLE-INFO.json version, target, vendored Node version, build time
```
Expand All @@ -73,6 +88,23 @@ path at runtime, so the bundle deliberately preserves a real directory layout
rather than compiling to a single executable. That keeps behavior identical to
an npm install with no source changes.

### Why Windows bundles ship a real `squad.exe`

winget's portable installer only creates command aliases for `.exe` targets —
`.cmd` and `.bat` are explicitly unsupported — so a package pointing at
`squad.cmd` would install something that cannot be invoked.

Windows bundles therefore ship `squad.exe`, built with Node's
[single executable application](https://nodejs.org/api/single-executable-applications.html)
support. It is the vendored Node runtime with a small launcher embedded, so it
*replaces* `runtime/node.exe` rather than adding to the bundle: a Windows bundle
is the same size as before. `squad.cmd` and `squad.ps1` remain for anyone who
unpacks the archive and runs it in place.

The launcher resolves the bundle from its own location, following a symlink if
it finds one, because winget installs portables by symlinking the executable
into a links directory.

## Prerequisite: Copilot CLI

Squad drives the GitHub Copilot CLI, which is **not** included in the bundle.
Expand Down Expand Up @@ -174,20 +206,38 @@ need telemetry export or the `sql.js` state backend inside the bundle.
npm is still used at *build* time to resolve the dependency tree. It is the
**runtime** dependency on the registry that these bundles remove.

## Packaging manifests

Homebrew and winget both embed the release version and a SHA-256 per artifact,
so their manifests cannot be hand-maintained without going stale every release.
They are generated from the release's own `SHA256SUMS.txt`:

```sh
node scripts/generate-packaging.mjs --version v0.11.0
```

That writes `dist-packaging/homebrew/squad.rb` and the three winget manifests
(version, installer, locale). The release workflow runs this automatically and
attaches the result as a `packaging-manifests` artifact; a maintainer submits
them to the tap and to `winget-pkgs`.

## Known limitations

- **Cross-built bundles are not executed in CI.** A vendored runtime only runs
on a matching host, so the release workflow smoke-tests `linux-x64` by
invoking the CLI and verifies the other five targets structurally.
- **No code signing or notarization yet.** macOS Gatekeeper will quarantine the
downloaded bundle until it is signed and notarized.
downloaded bundle until it is signed and notarized, and injecting the SEA blob
invalidates the Node runtime's original Authenticode signature on Windows, so
SmartScreen will warn. This is the main gap before the Homebrew and winget
packages should be recommended widely.
- **Optional deps are excluded by default**, so the bundled CLI has no
OpenTelemetry exporter and no `sql.js` state backend unless it was built with
`--include-optional`. The container image builds with them included.
- **The installer is POSIX-only.** Windows installs are download-and-unpack;
winget and Homebrew packaging are not part of this yet.
- **`squad upgrade` does not manage bundles.** Re-run the install script (or
pull a newer image) to move between versions.
- **The install script is POSIX-only.** On Windows use winget or
download-and-unpack.
- **`squad upgrade` does not manage bundles.** Re-run the install script, use
your package manager, or pull a newer image to move between versions.

## Verifying a bundle locally

Expand Down
5 changes: 4 additions & 1 deletion packages/squad-cli/src/cli/core/mcp-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,11 @@ export function _resetMcpSpecCache(): void {
export function detectStandaloneLauncher(): string | null {
const home = process.env[STANDALONE_HOME_ENV];
if (!home) return null;
// Order matters on Windows: prefer the real executable. Since the fix for
// CVE-2024-27980, Node refuses to spawn a `.cmd`/`.bat` without `shell:true`,
// so an MCP client that spawns the command directly would fail on squad.cmd.
const candidates = process.platform === 'win32'
? ['squad.cmd', 'squad.ps1']
? ['squad.exe', 'squad.cmd', 'squad.ps1']
: ['squad'];
for (const candidate of candidates) {
const full = path.join(home, candidate);
Expand Down
Loading
Loading