Skip to content

feat(dist): Homebrew and winget packaging, plus a real squad.exe (#1593) - #1611

Merged
bradygaster merged 6 commits into
devfrom
squad/1593-packaging-channels
Aug 5, 2026
Merged

bradygaster merged 6 commits into
devfrom
squad/1593-packaging-channels

Conversation

@tamirdresher

Copy link
Copy Markdown
Collaborator

What

Third slice of the distribution work: Homebrew and winget, plus the Windows executable that makes winget possible at all.

Based on dev, but note it builds on the standalone bundle work in #1610 (still open). The two touch scripts/build-standalone.mjs and mcp-spec.ts, so #1610 should merge first.

winget install bradygaster.Squad
brew install --cask bradygaster/squad/squad

Why this wasn't just two config files

I started by reading the winget spec rather than writing a manifest, and hit a blocker immediately:

winget's portable installer only creates command aliases for .exe targets. .cmd and .bat are explicitly unsupported — the install "succeeds" and produces a symlink that cannot be invoked.

Our Windows bundle's entry point was squad.cmd. A winget manifest pointing at it would have shipped a package that installs cleanly and then doesn't work. Homebrew has no such restriction and works fine with the existing shell launcher.

For reference, GitHub.Copilot solves this the same way — InstallerType: zip + NestedInstallerType: portable with a real copilot.exe in the archive.

How

Windows bundles now ship a real squad.exe, built with Node's single executable application support. It is the vendored runtime with a small launcher embedded, so it replaces runtime/node.exe rather than adding to the bundle — a Windows bundle is still 114 MB. squad.cmd and squad.ps1 stay for people who unpack and run in place.

The embedded launcher resolves the bundle from its own location and follows a symlink if it finds one, since winget installs portables by symlinking the exe into a links directory.

Manifests are generated, not hand-written. Both ecosystems embed a version and a per-artifact SHA-256, so maintaining them by hand guarantees they go stale — exactly the standing cost I flagged in #1593. scripts/generate-packaging.mjs renders the cask and all three winget manifests from the release's own SHA256SUMS.txt, and the release workflow runs it and attaches the output as an artifact.

.mcp.json now prefers squad.exe over squad.cmd. Since the fix for CVE-2024-27980, Node refuses to spawn() a .cmd without shell: true — so an MCP client spawning the command directly would fail to start squad_state.

Two bugs found while testing — both would have hit CI

1. GNU tar cannot read zip archives. bsdtar (Windows/macOS) can, which is why this worked locally. The release workflow builds every target on ubuntu-latest, so all six win32 bundles would have failed to unpack their Node runtime. Now uses unzip on POSIX hosts, and the workflow installs it.

2. execFileSync with shell: true splits an unquoted path on spaces, so invoking process.execPath from C:\Program Files\nodejs failed outright. The run() helper now quotes.

Verification

SEA blob is version-locked — this one cost me a debugging cycle worth recording. A blob built by my host Node 23.5.0 and injected into the vendored 22.20.0 produced an exe that died with 0xC0000005 (access violation) and no other diagnostic. The builder now downloads a matching host Node purely to generate the blob.

Cross-build verified end to end, because this is the production path:

Step Where Result
Unzip Windows Node runtime Linux container ✅ 85 MB node.exe
Generate SEA blob (Node 22.20.0) Linux container ✅
Inject with postject Linux container ✅ PE magic MZ
Run the resulting exe Windows ✅ ran correctly

A Windows executable built entirely on Linux runs on Windows. That was the riskiest assumption in the winget path.

Also: squad.exe / squad.cmd / squad.ps1 all return 0.11.0 from a real bundle · squad init from squad.exe scaffolds 5 agents and writes an npx-free .mcp.json · generator output parses as YAML, all three winget manifests agree on identifier/version/schema, both installers point at .exe · 67 tests pass (18 new for the generator, 8 new for the exe and extraction) · lint, cspell, markdownlint clean.

What is NOT verified

  • No submission has been made to homebrew-core/a tap or to winget-pkgs. Neither can be tested without a published release carrying bundle assets — the same chicken-and-egg as feat(dist): standalone npm-free bundles for squad-cli (#1593) #1595 and feat(dist): npm-free MCP spec, squad-init action, and container image (#1593) #1610.
  • Code signing and notarization are still absent. Worse here than before: injecting the SEA blob invalidates the Node runtime's Authenticode signature, so SmartScreen will warn on Windows. macOS Gatekeeper will still quarantine. This is the main gap before either package should be recommended widely, and I'd treat it as a blocker for actually submitting.
  • The winget symlink path is unverified. I could not create symlinks locally (needs admin/developer mode), so the launcher's realpathSync fallback is defensive rather than proven.

Incidental

Dropped the shebang from generate-packaging.mjs so vitest can import it on Windows — scripts/pr-readiness.mjs, the repo's other importable script, has none either. Worth noting separately: the same shebang interaction is why test/promote-insider-tag.test.ts is one of the pre-existing Windows test failures. Left alone as out of scope, but it's a one-line fix if you want it.


⚠️ Quick Check

  • Changeset added (.changeset/standalone-prefer-exe.md)

PR Readiness Checklist

Branch & Commit

Build & Test

Docs

  • Docs feature page updated — winget/Homebrew install, why Windows ships an exe, manifest generation

Exports

  • N/A — no new SDK modules

Breaking Changes

None for npm users. For anyone using a previous standalone Windows bundle, runtime/node.exe no longer exists — squad.exe replaces it. Both squad.cmd and squad.ps1 continue to work unchanged, and no released bundle has shipped yet, so nothing in the wild is affected.

Waivers

None requested. Flagging rather than waiving: the SEA-signed-binary question is a genuine open risk. Blob injection breaks the Authenticode signature, and I would not submit to winget until that is resolved. Happy to split the winget half out if you'd rather land Homebrew first.

tamirdresher and others added 4 commits August 5, 2026 09:46
…#1593)

Second slice of the standalone distribution work. Builds on the bundle
mechanism from the previous commit and closes the gap that made it
incomplete.

The gap: installing Squad from a bundle removed npm from *installation*, but
`squad init` still wrote an npx-based `squad_state` MCP entry into
`.mcp.json`, and probed registry.npmjs.org to choose the version. On a
firewalled machine the CLI installed fine and then the MCP server silently
failed to launch on first use — the exact failure mode this work exists to
prevent.

What changed:

- mcp-spec.ts gains a tier-0 ahead of the two npx tiers. When
  SQUAD_STANDALONE_HOME is set (exported by the bundle launchers) the spec
  points at that bundle's launcher by absolute path with args ['state-mcp'].
  It short-circuits before the registry probe, so a machine with no route to
  npm makes no network call here at all. Absolute because Copilot spawns the
  MCP server in its own environment, where neither PATH nor that variable is
  guaranteed.
- describeMcpSpec() previously read args[1] to name the package; standalone
  specs have no package there and would have printed '<unknown>'. It now
  reports the launcher path.
- build-standalone.mjs launchers (sh, cmd, ps1) export SQUAD_STANDALONE_HOME.
- .github/actions/squad-init — composite action that installs from a release
  bundle and runs squad init. This is the direct replacement for the
  `npx @bradygaster/squad-cli init` step in the gh-aw activation job (#1587).
  It also fails the build if the resulting .mcp.json still mentions npx.
- Dockerfile + .dockerignore — image built from a bundle instead of
  `npm install -g`, honoring the existing container contract in
  docs/reference/container-image.md (WORKDIR /app, non-root uid 1001,
  SIGTERM, `squad watch --execute`). Sets SQUAD_STANDALONE_HOME=/opt/squad.
  Built with --include-optional because the contract documents OTLP export,
  which lives in the optional OpenTelemetry packages.
- .gitattributes pins LF for Dockerfile and .dockerignore. CRLF breaks RUN
  line continuations when the image builds on Linux.

Verified:

- 13 new tests in mcp-spec-init.test.ts, including an explicit assertion that
  the registry probe never fires in standalone mode. 23/23 pass.
- Built a bundle and ran `squad init` from it on win32-x64: the generated
  .mcp.json contains command=<bundle>/squad.cmd, args=['state-mcp'], no npx.
- Runtime stage of the Dockerfile built and exercised on linux/amd64 with a
  host-built bundle: `squad --version` returns 0.11.0 as the non-root squad
  user, `which squad` resolves to /opt/squad/squad, `squad init` writes
  command=/opt/squad/squad, and `squad state-mcp` answers a JSON-RPC
  initialize handshake with serverInfo.name=squad-state.
- lint, docs-build, mcp-root-write, state-mcp, cspell, markdownlint all clean.

Not verified: the Dockerfile builder stage. This machine intercepts TLS, so
every container gets ERR_SSL_SSL/TLS_ALERT_HANDSHAKE_FAILURE reaching
registry.npmjs.org — reproducible with a bare
`docker run node:22-slim npm view lodash`. Not a Dockerfile defect, but the
full image has never been built end to end and CI must prove it. Likewise the
squad-init action cannot run until a release with bundle assets exists.

Refs #1593, #1587

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
Addresses both Copilot review comments on #1610.

1. The action fetched scripts/install.sh from the repository HEAD over the
   network. That is unpinned remote code execution, and the installer could
   drift from the release tag being installed. It now runs the install.sh that
   ships alongside the action, resolved from github.action_path. Consumers pin
   the action to a SHA, so the installer is pinned with it.

2. The npx check only wrote a warning to the step summary and exited 0, which
   did not match what the PR claimed. An npx-based squad_state entry is
   unrunnable on exactly the firewalled runners this action targets, and it
   fails later at Copilot start rather than here, so it now fails the step.

Verified the action_path walk resolves correctly on Linux (alpine container:
/repo/.github/actions/squad-init -> /repo/scripts/install.sh) rather than
assuming the relative depth.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
…tests

Found while running the installer end to end for the first time: building a
linux or darwin bundle from a Windows host failed.

  node-v22.20.0-linux-x64/bin/npx: Can't create '...': Invalid argument
  tar: Error exit delayed from previous errors.

The builder extracted the whole Node.js archive, and the posix archives carry
symlinks (bin/npm, bin/npx, bin/corepack) that Windows cannot create. Only the
node binary is ever shipped, so extraction is now scoped to that single
member. Extraction failures also report the underlying tar error instead of a
bare non-zero exit.

This would have bitten anyone building non-linux targets from a Windows
runner, and the release workflow's structural checks would not have caught it.

Also adds test/standalone-build.test.ts (11 cases) covering the behaviors that
break silently: selective extraction, SQUAD_STANDALONE_HOME in all three
launchers, symlink resolution in the posix launcher, optional-dep omission,
checksum verification, VERSION/PREFIX/REPO overrides, and an assertion that
install.sh never reaches the npm registry.

Verified the full install path in a container with no Node and no npm: served
a packaged bundle over HTTP, ran the real install.sh against it, and got
squad --version -> 0.11.0 through the PATH symlink. Corrupting the asset
made the installer abort with a checksum mismatch, as intended.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
Third slice of the distribution work. Adds the two package managers people
actually expect, and the Windows executable that makes one of them possible.

The blocker I found first: winget's portable installer only creates command
aliases for .exe targets — .cmd and .bat are explicitly unsupported. Our
Windows bundle's entry point was squad.cmd, so a winget manifest would have
installed something that could not be invoked. Homebrew has no such
restriction and works with the existing shell launcher.

So Windows bundles now ship a real squad.exe, built with Node's single
executable application support. It is the vendored runtime with a small
launcher embedded, so it REPLACES runtime/node.exe rather than adding to the
bundle — a Windows bundle is still 114 MB. The launcher resolves the bundle
from its own location and follows a symlink if it finds one, because winget
installs portables by symlinking the exe into a links directory.

Also here:

- scripts/generate-packaging.mjs renders the Homebrew cask and the three
  winget manifests from a release's SHA256SUMS.txt. Both ecosystems embed a
  version and a per-artifact SHA-256, so hand-maintaining them guarantees they
  go stale — this is exactly the standing cost flagged in #1593.
- The release workflow generates them after publishing and attaches them as an
  artifact for a maintainer to submit.
- mcp-spec now prefers squad.exe over squad.cmd on Windows. Since the fix for
  CVE-2024-27980 Node refuses to spawn a .cmd without shell:true, so an MCP
  client spawning the command directly would fail to start squad_state.

Two real bugs found while testing, both of which CI would have hit:

1. GNU tar cannot read zip archives; bsdtar (Windows, macOS) can. The release
   workflow builds every target on ubuntu-latest, so every win32 bundle would
   have failed to unpack its Node runtime. Now uses unzip on posix hosts, and
   the workflow installs it for win32 targets.
2. execFileSync with shell:true splits an unquoted command path on spaces, so
   invoking process.execPath from "C:\Program Files\nodejs" failed. The run()
   helper now quotes.

Verified:

- squad.exe, squad.cmd and squad.ps1 all return 0.11.0 from a real bundle
- squad init from squad.exe scaffolds 5 agents and writes an npx-free
  .mcp.json pointing at the bundle launcher
- SEA blob is version-locked: a blob built by Node 23.5 injected into the
  vendored 22.20.0 produced an exe that died with an access violation
  (0xC0000005). The builder now downloads a matching host Node to generate it.
- Cross-build works: a Linux container unzipped the Windows runtime, generated
  the blob with Linux Node 22.20.0, injected it with postject, and the
  resulting PE ran correctly on Windows. That is the production path, since
  the workflow builds all six targets on ubuntu-latest.
- Generator output parses as YAML, all three winget manifests agree on
  identifier/version/schema, and both installers point at .exe
- 67 tests pass (18 new for the generator, 8 new for the exe and extraction)

Dropped the shebang from generate-packaging.mjs so vitest can import it on
Windows; scripts/pr-readiness.mjs, the repo's other importable script, has no
shebang either. Worth noting separately: the same shebang interaction is why
test/promote-insider-tag.test.ts is one of the pre-existing Windows failures.
Left alone as out of scope.

Not verified: no winget or Homebrew submission has been made, because neither
can be tested without a published release carrying bundle assets. Code signing
and notarization are still absent, and injecting the SEA blob invalidates the
Node runtime's Authenticode signature, so SmartScreen will warn on Windows.
That is the main gap before either package should be recommended widely.

Refs #1593

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d2a1f506-af41-4d6d-b497-d0a5b255d4cf
Copilot AI lite review requested due to automatic review settings August 5, 2026 16:00
@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

🏗️ Architectural Review

⚠️ Architectural review: 1 warning(s), 1 info.

Severity Category Finding Files
🟡 warning bootstrap-area 1 file(s) in the bootstrap area (packages/squad-cli/src/cli/core/) were modified. These files must maintain zero external dependencies. Review carefully. packages/squad-cli/src/cli/core/mcp-spec.ts
ℹ️ info template-sync Template files changed in .github/workflows/ but not in other template locations. If these templates should stay in sync, consider updating the others too. Changed: .github/workflows/, Unchanged: templates/, .squad-templates/, packages/squad-cli/templates/

Automated architectural review — informational only.

@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

🟠 Impact Analysis — PR #1611

Risk tier: 🟠 HIGH

📊 Summary

Metric Count
Files changed 10
Files added 3
Files modified 7
Files deleted 0
Modules touched 6

🎯 Risk Factors

  • 10 files changed (6-20 → MEDIUM)
  • 6 modules touched (5-8 → HIGH)

📦 Modules Affected

ci-workflows (1 file)
  • .github/workflows/squad-standalone-release.yml
docs (1 file)
  • docs/src/content/docs/features/standalone-install.md
root (2 files)
  • .changeset/standalone-prefer-exe.md
  • cspell.json
scripts (2 files)
  • scripts/build-standalone.mjs
  • scripts/generate-packaging.mjs
squad-cli (1 file)
  • packages/squad-cli/src/cli/core/mcp-spec.ts
tests (3 files)
  • test/mcp-spec-init.test.ts
  • test/packaging-manifests.test.ts
  • test/standalone-build.test.ts

This report is generated automatically for every PR. See #733 for details.

@github-actions

github-actions Bot commented Aug 5, 2026 •

Copy link
Copy Markdown
Contributor

🛫 PR Readiness Check

ℹ️ This comment updates on each push. Last checked: commit df19288

PR Scope: 📦🔧 Mixed (product + infrastructure)

⚠️ 4 item(s) to address before review

Status Check Details
❌ Single commit 6 commits — consider squashing before review
✅ Not in draft Ready for review
❌ Branch up to date dev is 1 commit(s) ahead — rebase recommended
❌ Copilot review No Copilot review yet — it may still be processing
✅ Changeset present Changeset file found
✅ Scope clean No .squad/ or docs/proposals/ files
✅ No merge conflicts Merge status unknown — GitHub is still computing
✅ Copilot threads resolved 0 active Copilot thread(s) resolved (2 outdated skipped)
❌ CI passing 12 check(s) still running

Files Changed (10 files, +857 −40)

File +/−
.changeset/standalone-prefer-exe.md +5 −0
.github/workflows/squad-standalone-release.yml +75 −1
cspell.json +1 −0
docs/src/content/docs/features/standalone-install.md +60 −10
packages/squad-cli/src/cli/core/mcp-spec.ts +4 −1
scripts/build-standalone.mjs +185 −27
scripts/generate-packaging.mjs +310 −0
test/mcp-spec-init.test.ts +11 −0
test/packaging-manifests.test.ts +152 −0
test/standalone-build.test.ts +54 −1

Total: +857 −40


This check runs automatically on every push. Fix any ❌ items and push again.
See CONTRIBUTING.md and PR Requirements for details.

@github-actions

github-actions Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

🔒 Security Review

🔒 Security review: 1 warning(s).

Severity Category Finding Location
🟡 warning pii-env-var Environment variable with sensitive name pattern (CREDENTIALS) — ensure this isn't hardcoded. .github/workflows/squad-standalone-release.yml:227

Automated security review — informational only.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends Squad’s standalone distribution story by adding generated Homebrew cask and winget manifests, and by updating the Windows standalone bundle to ship a real squad.exe (SEA-based) so winget portable installs are invokable. It also updates MCP wiring so standalone installs prefer an executable launcher and avoid npx/registry dependencies.

Changes:

  • Build Windows standalone bundles with a SEA-based squad.exe and adjust launchers + workflow assertions accordingly.
  • Add a packaging-manifest generator (scripts/generate-packaging.mjs) plus CI workflow job to emit Homebrew/winget manifests from SHA256SUMS.txt.
  • Add/extend tests and docs to cover standalone extraction, MCP standalone tier behavior, and packaging output.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/standalone-build.test.ts Adds structural tests covering standalone build/extraction and Windows SEA behavior.
test/packaging-manifests.test.ts Adds unit tests for checksum parsing and manifest rendering for Homebrew/winget.
test/mcp-spec-init.test.ts Extends MCP spec tests for standalone tier and Windows .exe preference; covers describer output.
scripts/generate-packaging.mjs New script to render Homebrew cask + winget manifests from release checksums.
scripts/build-standalone.mjs Updates runtime extraction, Windows SEA squad.exe build, launcher env export, and quoting for Windows shell execution.
packages/squad-cli/src/cli/core/upgrade.ts Updates MCP spec description logic for standalone specs.
packages/squad-cli/src/cli/core/mcp-spec.ts Adds standalone detection tier, launcher detection helper, and .exe preference on Windows.
docs/src/content/docs/features/standalone-install.md Documents winget/Homebrew install paths, Windows squad.exe rationale, container notes, and MCP spec behavior.
Dockerfile Adds bundle-based container build that avoids npm registry at runtime.
cspell.json Adds “pkgs” word for spell-checking.
.github/workflows/squad-standalone-release.yml Ensures unzip availability for win32 cross-builds; adds packaging-manifest generation job/artifact.
.github/actions/squad-init/action.yml Adds composite action to install from release bundle and run squad init, verifying npm-free MCP wiring.
.gitattributes Enforces LF endings for Docker-related files to avoid Linux build issues.
.dockerignore Minimizes Docker build context and avoids copying host artifacts.
.changeset/standalone-prefer-exe.md Changeset for preferring squad.exe in standalone MCP spec on Windows.
.changeset/standalone-mcp-spec-1593.md Changeset for emitting npx-free MCP spec when running from a standalone bundle.
.changeset/standalone-cross-build-fix.md Changeset for extraction behavior that enables cross-building bundles reliably.

Comment on lines 208 to 212
if (platform === 'win32') {
// Windows archives keep node.exe plus its ICU data at the archive root.
for (const entry of readdirSync(extracted)) {
if (entry === 'node.exe' || entry.endsWith('.dll') || entry.endsWith('.dat')) {
cpSync(path.join(extracted, entry), path.join(runtimeDir, entry));
}
}
if (!existsSync(path.join(runtimeDir, 'node.exe'))) {
throw new Error('node.exe not found in downloaded runtime');
}
const exeSrc = path.join(extracted, 'node.exe');
if (!existsSync(exeSrc)) throw new Error('node.exe not found in downloaded runtime');
cpSync(exeSrc, path.join(runtimeDir, 'node.exe'));
} else {
Comment on lines +74 to +75
set -euo pipefail
# These values reach a shell command below, so constrain them tightly.
bradygaster and others added 2 commits August 5, 2026 18:31
…workflow

The single-quoted backticks are intentional markdown formatting in the
step summary, not shell expressions. Disable SC2016 for those lines.

Closes #1611

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…hannels

# Conflicts:
#	docs/src/content/docs/features/standalone-install.md
#	packages/squad-cli/src/cli/core/mcp-spec.ts
#	scripts/build-standalone.mjs
#	test/mcp-spec-init.test.ts
#	test/standalone-build.test.ts
@bradygaster
bradygaster merged commit a501cf3 into dev Aug 5, 2026
11 checks passed
@bradygaster
bradygaster deleted the squad/1593-packaging-channels branch August 5, 2026 18:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants