diff --git a/.agents/manifests/specs.yaml b/.agents/manifests/specs.yaml index 70b67dc..85c34e1 100644 --- a/.agents/manifests/specs.yaml +++ b/.agents/manifests/specs.yaml @@ -15,7 +15,12 @@ globs: - examples/**/.simulator-broker/*.json - spec/** - references/** + - Casks/** + - Formula/** + - packages/simbroker/** - scripts/package_cli.sh + - scripts/package_npm.sh + - scripts/sync_homebrew_tap.sh owner: spec-steward required_skills: - harness-engineering @@ -44,7 +49,12 @@ allowed_paths: - examples/**/.simulator-broker/*.json - spec/** - references/** + - Casks/** + - Formula/** + - packages/simbroker/** - scripts/package_cli.sh + - scripts/package_npm.sh + - scripts/sync_homebrew_tap.sh forbidden_paths: [] required_evidence: - structured-git-commit diff --git a/.agents/verify/spec-only.yaml b/.agents/verify/spec-only.yaml index d49ec11..fbc0718 100644 --- a/.agents/verify/spec-only.yaml +++ b/.agents/verify/spec-only.yaml @@ -4,7 +4,7 @@ covers_scenarios: [] covers_boundaries: [] commands: - id: spec-diff-check - run: git diff --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client && git diff --cached --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client + run: git diff --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client Formula Casks packages && git diff --cached --check -- WORKFLOW.md AGENTS.md CLAUDE.md README.md LICENSE SECURITY.md CONTRIBUTING.md CODE_OF_CONDUCT.md CHANGELOG.md docs spec .agents agent-harness package.json package-lock.json .gitignore .github .codex script scripts references app broker-core client Formula Casks packages - id: public-front-door-check run: node --test docs/test/*.test.mjs - id: instruction-parity-check diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 655a8f3..1da143f 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -7,8 +7,10 @@ body: attributes: value: | Simulator Broker is **Alpha**, **macOS-only**, and needs **Xcode**. - It is maintenance-only infrastructure. There is no Homebrew formula, - notarized app, or npm package yet. + It is maintenance-only infrastructure. The CLI installs through + Homebrew or the packable `simbroker` npm package. A signed, + notarized operator app zip is not attached to the current Alpha + release. Maintainers may decline requests that add a feature, commercialization, or UX roadmap. diff --git a/.github/ISSUE_TEMPLATE/install-failure.yml b/.github/ISSUE_TEMPLATE/install-failure.yml index 7faaca5..c4938d7 100644 --- a/.github/ISSUE_TEMPLATE/install-failure.yml +++ b/.github/ISSUE_TEMPLATE/install-failure.yml @@ -7,8 +7,11 @@ body: attributes: value: | Simulator Broker is **Alpha**, **macOS-only**, and needs **Xcode** to - create or run iOS Simulators. There is no Homebrew formula, notarized - app, or npm package yet. + create or run iOS Simulators. Install the CLI with + `brew install fiveonecode/simulator-broker/simbroker` or + `npm install -g` of the `simbroker-0.1.0-alpha.1.tgz` from GitHub + Releases. A signed, notarized operator app zip is not attached to + this Alpha release. Do not paste credentials, private home paths, customer data, or live lease files. Redact logs first. Security reports go through @@ -19,6 +22,8 @@ body: attributes: label: What install path did you use? options: + - Homebrew (`brew install fiveonecode/simulator-broker/simbroker`) + - npm (`npm install -g` of `simbroker-0.1.0-alpha.1.tgz` from GitHub Releases) - CLI-only (`bash scripts/install_local.sh --cli-only`) - Contributor app + CLI (`npm run install:local`) - Alpha CLI tarball from GitHub Releases diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ea1533f..c14a87e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,6 +1,6 @@ -# Publish the Alpha CLI tarball when a version tag is pushed. -# After merge, create the matching tag (example: v0.1.0-alpha.1). -# This workflow does not build or attach the macOS app. +# Publish the Alpha CLI tarball and the packable npm CLI when a version +# tag is pushed. After merge, create the matching tag (example: +# v0.1.0-alpha.1). This workflow does not build or attach the macOS app. name: Release CLI on: @@ -13,7 +13,7 @@ permissions: jobs: release: - name: attach CLI tarball + name: attach CLI and npm packages runs-on: ubuntu-latest timeout-minutes: 30 steps: @@ -38,8 +38,10 @@ jobs: npm run test:client npm run test:harness-adoption - - name: Package CLI tarball - run: npm run package:cli + - name: Package CLI tarball and npm CLI + run: | + npm run package:cli + npm run package:npm - name: Create GitHub Release env: @@ -48,13 +50,15 @@ jobs: version="$(node -p "require('./package.json').version")" asset="artifacts/cli/simulator-broker-${version}-cli.tar.gz" checksum="${asset}.sha256" + npm_asset="artifacts/npm/simbroker-${version}.tgz" prerelease_args=() if [[ "$version" == *alpha* || "$version" == *beta* || "$version" == *rc* ]]; then prerelease_args+=(--prerelease) fi gh release create "${GITHUB_REF_NAME}" \ --title "Simulator Broker ${GITHUB_REF_NAME}" \ - --notes "Alpha CLI tarball. Extract it and run \`./bin/simbroker --help\`. Node.js 20 or newer is required. macOS and Xcode are still required to create and run iOS Simulators. This release is not a Homebrew formula, notarized app, or npm package. See CHANGELOG.md." \ + --notes "Alpha CLI. Install with \`brew install fiveonecode/simulator-broker/simbroker\` or \`npm install -g\` the attached \`simbroker-${version}.tgz\`. Node.js 20 or newer is required. macOS and Xcode are still required to create and run iOS Simulators. A signed, notarized operator app is not attached. See CHANGELOG.md." \ "${prerelease_args[@]}" \ "$asset" \ - "$checksum" + "$checksum" \ + "$npm_asset" diff --git a/CHANGELOG.md b/CHANGELOG.md index ea7b0cd..52dd3a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to Simulator Broker are documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Added + +- Homebrew formula `Formula/simbroker.rb` installs the Alpha CLI tarball + (`brew install fiveonecode/simulator-broker/simbroker`). Homebrew clones + [`fiveonecode/homebrew-simulator-broker`](https://github.com/fiveonecode/homebrew-simulator-broker) + for that tap name. `Formula/` and `Casks/` here stay the source of truth. +- Homebrew cask `Casks/simulator-broker.rb` installs `Simulator Broker.app` + from the signed, notarized GitHub Release zip `Simulator-Broker-.zip` + when that zip is attached. +- Packable npm CLI `packages/simbroker` (`npm run package:npm`) with a `bin` + field. The repo-root package stays private. Tag-driven + `.github/workflows/release.yml` attaches that tarball and no longer + claims the release is not a Homebrew formula or npm package. + ## [0.1.0-alpha.1] - 2026-08-18 First tagged Alpha. The CLI, local `brokerd` service, and macOS operator app diff --git a/Casks/simulator-broker.rb b/Casks/simulator-broker.rb new file mode 100644 index 0000000..d6c78c5 --- /dev/null +++ b/Casks/simulator-broker.rb @@ -0,0 +1,21 @@ +cask "simulator-broker" do + version "0.1.0-alpha.1" + sha256 :no_check + + url "https://github.com/fiveonecode/simulator-broker/releases/download/v#{version}/Simulator-Broker-#{version}.zip" + name "Simulator Broker" + desc "macOS operator app for the local iOS Simulator control plane" + homepage "https://github.com/fiveonecode/simulator-broker" + + depends_on macos: :ventura + + app "Simulator Broker.app" + + caveats <<~EOS + This cask installs Simulator Broker.app from the signed, notarized + GitHub Release zip Simulator-Broker-#{version}.zip. Produce that zip + with npm run package:distribution (Developer ID Application + notarytool), + using payload/app/Simulator Broker.app from the distribution bundle. + The current Alpha GitHub Release attaches the CLI tarball, not this zip. + EOS +end diff --git a/Formula/simbroker.rb b/Formula/simbroker.rb new file mode 100644 index 0000000..27c9ee9 --- /dev/null +++ b/Formula/simbroker.rb @@ -0,0 +1,22 @@ +class Simbroker < Formula + desc "Local iOS Simulator control plane CLI" + homepage "https://github.com/fiveonecode/simulator-broker" + url "https://github.com/fiveonecode/simulator-broker/releases/download/v0.1.0-alpha.1/simulator-broker-0.1.0-alpha.1-cli.tar.gz" + sha256 "699695bc65a5bcb25b9c9b5d01494fcc3f25a40dcc90b8b5bf1ec61ea87a8522" + license "MIT" + + depends_on :macos + depends_on "node" + + def install + libexec.install Dir["*"] + (bin/"simbroker").write <<~SH + #!/bin/bash + exec "#{formula_opt_bin("node")}/node" "#{libexec}/client/bin/simbroker.mjs" "$@" + SH + end + + test do + assert_match "simbroker", shell_output("#{bin}/simbroker --help") + end +end diff --git a/README.md b/README.md index 4a8701c..2d72079 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ A local control plane so humans, AI agents, and CI jobs can share iOS Simulators on one Mac without stealing devices from each other. > **Alpha.** macOS only. Xcode is required to create or run iOS Simulators. -> Interfaces can change. There is no Homebrew formula, notarized app, or npm -> package yet. +> Interfaces can change. Install the CLI with Homebrew or the `simbroker` +> npm package. A signed, notarized operator app is not attached to this +> Alpha release yet. [![Node tests](https://github.com/fiveonecode/simulator-broker/actions/workflows/ci.yml/badge.svg)](https://github.com/fiveonecode/simulator-broker/actions/workflows/ci.yml) @@ -27,7 +28,24 @@ If you only ever use one simulator by hand, you may not need this yet. ## Use it -From a clone, with Node.js 20+ on `PATH`: +One-line CLI install on macOS, with Node.js 20+ still required at runtime: + +```bash +brew install fiveonecode/simulator-broker/simbroker +simbroker --help +``` + +Homebrew clones +[`fiveonecode/homebrew-simulator-broker`](https://github.com/fiveonecode/homebrew-simulator-broker) +for that tap name. `Formula/` and `Casks/` in this repository stay the +source of truth. + +```bash +npm install -g https://github.com/fiveonecode/simulator-broker/releases/download/v0.1.0-alpha.1/simbroker-0.1.0-alpha.1.tgz +simbroker --help +``` + +From a clone: ```bash bash scripts/install_local.sh --cli-only @@ -35,21 +53,22 @@ command -v simbroker simbroker --help ``` -That copies the CLI runtime only. It does not run XcodeGen or build the macOS -app. If Homebrew is installed, `simbroker` lands in `$(brew --prefix)/bin`. -Otherwise the installer writes `~/.local/bin/simbroker` and one guarded -login-shell PATH line. Open a new terminal if this shell still cannot resolve -`simbroker`. `source .../env.sh` remains a fallback. +The clone installer copies the CLI runtime only. It does not run XcodeGen or +build the macOS app. If Homebrew is installed, `simbroker` lands in +`$(brew --prefix)/bin`. Otherwise the installer writes `~/.local/bin/simbroker` +and one guarded login-shell PATH line. Open a new terminal if this shell still +cannot resolve `simbroker`. `source .../env.sh` remains a fallback. -To try a tagged build without cloning, download -`simulator-broker--cli.tar.gz` from -[Releases](https://github.com/fiveonecode/simulator-broker/releases), extract -it, and run `./bin/simbroker --help`. Node.js 20+ is still required. +The Homebrew cask `fiveonecode/simulator-broker/simulator-broker` installs +`Simulator Broker.app` from the signed, notarized GitHub Release zip +`Simulator-Broker-.zip`. That zip is not attached to the current +Alpha release. Produce it with `npm run package:distribution` after Developer +ID signing and notarytool notarization. Xcode is still required to create and run iOS Simulators. Alpha CLI tarballs are attached to [GitHub Releases](https://github.com/fiveonecode/simulator-broker/releases). -There is no Homebrew formula or npm package yet. +Extract a tarball and run `./bin/simbroker --help`. `simbroker` help and `simbroker doctor` print human-readable text by default. Pass `--json` for machine-readable payloads. diff --git a/SECURITY.md b/SECURITY.md index fa8657c..f2462d7 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,10 @@ Security fixes are considered for the current `main` branch and for the latest tagged Alpha (`0.1.0-alpha.1`). Older Alpha tags are not supported. The -published artifact is the CLI tarball on GitHub Releases, not a Homebrew -formula, notarized app, or npm package. +published CLI artifacts are the GitHub Release tarball, the Homebrew +formula that installs that tarball, and the packable `simbroker` npm +package. A signed, notarized operator app zip is not attached to this +Alpha release. ## Reporting A Vulnerability diff --git a/docs/getting-started.md b/docs/getting-started.md index 57ab507..0222441 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -9,8 +9,9 @@ A small public patch follows the public-patches track in [CONTRIBUTING.md](../CONTRIBUTING.md); it does not require the agent harness. > **Alpha.** macOS only. Xcode is required to create or run iOS Simulators. -> Interfaces can change. There is no Homebrew formula, notarized app, or npm -> package yet. +> Interfaces can change. Install the CLI with Homebrew or the `simbroker` +> npm package. A signed, notarized operator app is not attached to this +> Alpha release yet. ## Prerequisites @@ -21,6 +22,31 @@ A small public patch follows the public-patches track in Repo-owned macOS app entrypoints fail fast when `xcodegen` or `xcodebuild` is missing or unusable. The CLI-only installer does not call them. +## Install the CLI with Homebrew + +```bash +brew install fiveonecode/simulator-broker/simbroker +command -v simbroker +simbroker --help +``` + +That formula installs the tagged Alpha CLI tarball from GitHub Releases. +Homebrew clones +[`fiveonecode/homebrew-simulator-broker`](https://github.com/fiveonecode/homebrew-simulator-broker) +for the tap name `fiveonecode/simulator-broker`. `Formula/` and `Casks/` +in this repository stay the source of truth. + +## Install the CLI with npm + +```bash +npm install -g https://github.com/fiveonecode/simulator-broker/releases/download/v0.1.0-alpha.1/simbroker-0.1.0-alpha.1.tgz +command -v simbroker +simbroker --help +``` + +From a clone, `npm run package:npm` writes the same tarball. The repo-root +package stays private. + ## Install the CLI from this checkout ```bash @@ -145,8 +171,12 @@ SIMBROKER_DISTRIBUTION_SIGNING_IDENTITY='Developer ID Application: Example (TEAM npm run package:distribution ``` -GitHub Releases attach the Alpha CLI tarball. A signed, notarized app is not -published there yet. +GitHub Releases attach the Alpha CLI tarball. The Homebrew cask +`fiveonecode/simulator-broker/simulator-broker` installs +`Simulator Broker.app` from `Simulator-Broker-.zip` on that +release. Produce the zip with `npm run package:distribution` after Developer +ID signing and notarytool notarization. The current Alpha release does not +attach that zip. ## Report a problem diff --git a/docs/status.md b/docs/status.md index f137ef1..ef678a9 100644 --- a/docs/status.md +++ b/docs/status.md @@ -29,6 +29,12 @@ This Alpha already includes: harness-adoption, and public-surface checks - GitHub issue forms for install failure, bug, and feature, plus a light PR template that does not require a harness session +- Homebrew formula `fiveonecode/simulator-broker/simbroker` for the Alpha CLI + tarball. Homebrew clones `fiveonecode/homebrew-simulator-broker` for that + tap name; `Formula/` and `Casks/` in this repository stay the source of + truth. Packable `simbroker` npm CLI (`npm run package:npm`), and a + Homebrew cask that installs `Simulator Broker.app` from the signed, + notarized GitHub Release zip when that zip is attached - local-debug packaging through `npm run package:local` - signed distribution packaging through `npm run package:distribution` - `simbroker project init` for `.simulator-broker/project.json` @@ -64,4 +70,5 @@ node client/bin/simbroker.mjs simulators boot --alias ui-1 ## Lower-priority public follow-through -- Homebrew formula, notarized app, and npm package +- Attach a signed, notarized `Simulator-Broker-.zip` to GitHub + Releases so the Homebrew cask has a downloadable app artifact diff --git a/docs/test/front-door.test.mjs b/docs/test/front-door.test.mjs index 30c9e75..bb0b56c 100644 --- a/docs/test/front-door.test.mjs +++ b/docs/test/front-door.test.mjs @@ -138,7 +138,11 @@ test("README advertises GitHub Releases and the public Node CI badge", () => { assert.ok(readme.includes("github.com/fiveonecode/simulator-broker/releases")); assert.ok(readme.includes("./bin/simbroker --help")); assert.equal(readme.includes("or GitHub Release yet"), false); - assert.ok(readme.includes("There is no Homebrew formula or npm package yet.")); + assert.equal(readme.includes("There is no Homebrew formula or npm package yet."), false); + assert.ok(readme.includes("brew install fiveonecode/simulator-broker/simbroker")); + assert.ok(readme.includes("homebrew-simulator-broker")); + assert.ok(readme.includes("npm install -g")); + assert.ok(readme.includes("simbroker-0.1.0-alpha.1.tgz")); }); test("CHANGELOG and package.json name the Alpha version", () => { @@ -170,9 +174,16 @@ test("release workflow packages the CLI tarball on version tags", () => { assert.ok(release.includes("tags:")); assert.ok(release.includes("npm run package:cli")); + assert.ok(release.includes("npm run package:npm")); + assert.ok(release.includes("artifacts/npm/simbroker-${version}.tgz")); assert.ok(release.includes("gh release create")); assert.ok(release.includes("--prerelease")); assert.equal(release.includes("test:app"), false); + assert.equal( + release.replace(/\s+/g, " ").includes("This release is not a Homebrew formula, notarized app, or npm package."), + false, + "future release notes must not deny Homebrew and npm", + ); }); test("issue forms cover install failure, bug, and feature and state Alpha limits", () => { @@ -187,12 +198,20 @@ test("issue forms cover install failure, bug, and feature and state Alpha limits assert.ok(bug.includes("name: Bug")); assert.ok(feature.includes("name: Feature")); assert.ok(install.includes("install_local.sh --cli-only")); + assert.ok(install.includes("brew install fiveonecode/simulator-broker/simbroker")); + assert.ok(install.includes("npm install -g")); + assert.ok(install.includes("simbroker-0.1.0-alpha.1.tgz")); assert.ok(bug.includes("labels:")); assert.ok(feature.includes("enhancement")); for (const body of [install, bug, feature, config]) { assert.ok(body.includes("Alpha"), "community forms must say Alpha"); assert.ok(body.includes("macOS"), "community forms must say macOS"); assert.ok(body.includes("Xcode"), "community forms must say Xcode"); + assert.equal( + body.replace(/\s+/g, " ").includes("There is no Homebrew formula, notarized app, or npm package yet."), + false, + "issue forms must not claim Homebrew/npm do not exist", + ); } }); @@ -226,6 +245,112 @@ test("status and contributing point strangers at issue forms, not later-sequence assert.ok(readme.includes("issues/new/choose")); }); +test("Homebrew one-liner is documented against the homebrew-simulator-broker tap", () => { + const readme = readRepoFile("README.md"); + const gettingStarted = readRepoFile("docs/getting-started.md"); + const structure = readRepoFile("spec/project-structure.md"); + const changelog = readRepoFile("CHANGELOG.md"); + const syncScript = readRepoFile("scripts/sync_homebrew_tap.sh"); + + for (const body of [readme, gettingStarted, structure, changelog]) { + assert.ok( + body.includes("brew install fiveonecode/simulator-broker/simbroker"), + "docs/specs must keep the advertised brew one-liner", + ); + assert.ok( + body.includes("homebrew-simulator-broker"), + "docs/specs must name the GitHub tap repo Homebrew actually clones", + ); + } + + assert.ok(syncScript.includes("--check-remote")); + assert.ok(syncScript.includes("cmp -s")); + assert.ok(syncScript.includes("--max-time")); + assert.ok(syncScript.includes("Formula/simbroker.rb")); +}); + +test("sync_homebrew_tap.sh copies Formula and Casks into a tap checkout", () => { + const tapDir = fs.mkdtempSync(path.join(os.tmpdir(), "simbroker-homebrew-tap-")); + const init = spawnSync("git", ["init", tapDir], { encoding: "utf8" }); + assert.equal(init.status, 0, init.stderr); + const sync = spawnSync( + "bash", + [path.join(repoRoot, "scripts/sync_homebrew_tap.sh"), "--tap-dir", tapDir], + { encoding: "utf8", cwd: repoRoot }, + ); + assert.equal(sync.status, 0, sync.stderr + sync.stdout); + assert.equal(fs.existsSync(path.join(tapDir, "Formula/simbroker.rb")), true); + assert.equal(fs.existsSync(path.join(tapDir, "Casks/simulator-broker.rb")), true); + const copied = fs.readFileSync(path.join(tapDir, "Formula/simbroker.rb"), "utf8"); + const source = readRepoFile("Formula/simbroker.rb"); + assert.equal(copied, source); +}); + +test("Homebrew formula points at the Alpha CLI tarball and the cask names a notarized app zip", () => { + const formula = readRepoFile("Formula/simbroker.rb"); + const cask = readRepoFile("Casks/simulator-broker.rb"); + const checksum = formula.match(/sha256 "([a-f0-9]{64})"/); + const url = formula.match(/url "([^"]+)"/); + + assert.ok(formula.includes("class Simbroker < Formula")); + assert.ok(url, "formula must have a url"); + assert.equal( + url[1], + "https://github.com/fiveonecode/simulator-broker/releases/download/v0.1.0-alpha.1/simulator-broker-0.1.0-alpha.1-cli.tar.gz", + ); + assert.ok(checksum, "formula must pin a sha256"); + assert.equal(checksum[1], "699695bc65a5bcb25b9c9b5d01494fcc3f25a40dcc90b8b5bf1ec61ea87a8522"); + assert.ok(formula.includes('shell_output("#{bin}/simbroker --help")')); + assert.equal(formula.includes("package:local"), false); + + assert.ok(cask.includes('cask "simulator-broker"')); + assert.ok(cask.includes("releases/download/v#{version}/Simulator-Broker-#{version}.zip")); + assert.ok(cask.includes('app "Simulator Broker.app"')); + assert.equal(cask.includes("package:local"), false); + assert.equal(cask.includes("package_local"), false); + assert.ok(cask.includes("package:distribution") || cask.includes("package_distribution")); +}); + +test("root package stays private and package_npm.sh packs a runnable simbroker bin", () => { + const rootPackage = JSON.parse(readRepoFile("package.json")); + const cliPackage = JSON.parse(readRepoFile("packages/simbroker/package.json")); + + assert.equal(rootPackage.private, true); + assert.equal(rootPackage.bin, undefined); + assert.equal(cliPackage.name, "simbroker"); + assert.equal(cliPackage.private, false); + assert.equal(cliPackage.bin.simbroker, "bin/simbroker.js"); + assert.equal(cliPackage.version, rootPackage.version); + assert.ok(rootPackage.scripts["package:npm"].includes("package_npm.sh")); + + const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "simbroker-package-npm-")); + const pack = spawnSync("bash", [path.join(repoRoot, "scripts/package_npm.sh"), "--output-dir", outputDir], { + encoding: "utf8", + cwd: repoRoot, + }); + assert.equal(pack.status, 0, pack.stderr + pack.stdout); + + const tarball = path.join(outputDir, "simbroker-0.1.0-alpha.1.tgz"); + assert.equal(fs.existsSync(tarball), true, pack.stdout); + + const installDir = path.join(outputDir, "prefix"); + fs.mkdirSync(installDir); + const install = spawnSync("npm", ["install", "--global", "--prefix", installDir, tarball], { + encoding: "utf8", + cwd: outputDir, + }); + assert.equal(install.status, 0, install.stderr + install.stdout); + + const installedBin = path.join(installDir, "bin/simbroker"); + const help = spawnSync(installedBin, ["--help"], { encoding: "utf8" }); + assert.equal(help.status, 0, help.stderr); + const helpText = `${help.stdout}${help.stderr}`; + assert.ok(helpText.includes("simbroker")); + assert.equal(helpText.trim().startsWith("{"), false, "help must not be JSON-only"); + assert.equal(fs.existsSync(path.join(installDir, "lib/node_modules/simbroker/broker-core/test")), false); + assert.equal(fs.existsSync(path.join(installDir, "lib/node_modules/simbroker/client/test")), false); +}); + test("package_cli.sh writes a runnable CLI tarball without tests or the app", () => { const outputDir = fs.mkdtempSync(path.join(os.tmpdir(), "simbroker-package-cli-")); const result = spawnSync("bash", [path.join(repoRoot, "scripts/package_cli.sh"), "--output-dir", outputDir], { diff --git a/package.json b/package.json index 907f031..d8a00a5 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "build:app": "bash ./scripts/generate_app_project.sh && xcodebuild -project ./app/SimulatorBrokerApp.xcodeproj -scheme SimulatorBrokerApp -derivedDataPath ./DerivedData/SimulatorBrokerApp -destination 'platform=macOS' build", "install:local": "bash ./scripts/install_local.sh", "package:cli": "bash ./scripts/package_cli.sh", + "package:npm": "bash ./scripts/package_npm.sh", "package:distribution": "bash ./scripts/package_distribution.sh", "package:local": "bash ./scripts/package_local.sh", "test": "npm run verify:public-surface && npm run test:broker-core && npm run test:client && npm run test:harness-adoption && npm run test:app", diff --git a/packages/simbroker/README.md b/packages/simbroker/README.md new file mode 100644 index 0000000..f805d15 --- /dev/null +++ b/packages/simbroker/README.md @@ -0,0 +1,12 @@ +# simbroker + +Alpha CLI for Simulator Broker. Node.js 20 or newer is required. Creating and +running iOS Simulators still requires macOS and Xcode. + +```bash +npm install -g https://github.com/fiveonecode/simulator-broker/releases/download/v0.1.0-alpha.1/simbroker-0.1.0-alpha.1.tgz +simbroker --help +``` + +From a clone, `npm run package:npm` writes the same tarball. The repo-root +package stays private and is not this CLI. diff --git a/packages/simbroker/bin/simbroker.js b/packages/simbroker/bin/simbroker.js new file mode 100755 index 0000000..ef04ecd --- /dev/null +++ b/packages/simbroker/bin/simbroker.js @@ -0,0 +1,20 @@ +#!/usr/bin/env node + +import { spawn } from "node:child_process"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const cli = path.join(root, "client/bin/simbroker.mjs"); +const child = spawn(process.execPath, [cli, ...process.argv.slice(2)], { + stdio: "inherit", +}); + +child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 1); +}); diff --git a/packages/simbroker/package.json b/packages/simbroker/package.json new file mode 100644 index 0000000..f09ec5f --- /dev/null +++ b/packages/simbroker/package.json @@ -0,0 +1,30 @@ +{ + "name": "simbroker", + "version": "0.1.0-alpha.1", + "private": false, + "description": "CLI for the local iOS Simulator control plane", + "license": "MIT", + "type": "module", + "bin": { + "simbroker": "bin/simbroker.js" + }, + "files": [ + "bin", + "broker-core", + "client", + "LICENSE", + "README.md" + ], + "engines": { + "node": ">=20" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fiveonecode/simulator-broker.git", + "directory": "packages/simbroker" + }, + "bugs": { + "url": "https://github.com/fiveonecode/simulator-broker/issues" + }, + "homepage": "https://github.com/fiveonecode/simulator-broker#readme" +} diff --git a/scripts/package_cli.sh b/scripts/package_cli.sh index 9e1b238..d75a415 100755 --- a/scripts/package_cli.sh +++ b/scripts/package_cli.sh @@ -72,8 +72,9 @@ Run: ./bin/simbroker --help -This archive does not include the macOS operator app, a Homebrew formula, or -an npm package. +This archive is the Node CLI only. Homebrew installs it through +Formula/simbroker.rb. The packable npm CLI is packages/simbroker +(`npm run package:npm`). The macOS operator app is separate. EOF cat > "$bundle/bin/simbroker" <<'EOF' diff --git a/scripts/package_npm.sh b/scripts/package_npm.sh new file mode 100755 index 0000000..06d899c --- /dev/null +++ b/scripts/package_npm.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" +output_dir="$repo_root/artifacts/npm" + +usage() { + cat <<'EOF' +Usage: bash scripts/package_npm.sh [options] + +Stage the packable `simbroker` npm CLI (runtime without tests) and write +a versioned tarball. The repo-root package stays private. + +Options: + --output-dir Destination for the npm tarball. Default: artifacts/npm + -h, --help Show this help text. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --output-dir) + output_dir="$2" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown package_npm.sh argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +pkg_src="$repo_root/packages/simbroker" +if [[ ! -f "$pkg_src/package.json" || ! -f "$pkg_src/bin/simbroker.js" ]]; then + echo "Missing packages/simbroker package.json or bin/simbroker.js" >&2 + exit 1 +fi + +version="$(node -e "const fs=require('node:fs'); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1],'utf8')).version)" "$repo_root/package.json")" +pkg_version="$(node -e "const fs=require('node:fs'); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1],'utf8')).version)" "$pkg_src/package.json")" +pkg_name="$(node -e "const fs=require('node:fs'); process.stdout.write(JSON.parse(fs.readFileSync(process.argv[1],'utf8')).name)" "$pkg_src/package.json")" +root_private="$(node -e "const fs=require('node:fs'); process.stdout.write(String(JSON.parse(fs.readFileSync(process.argv[1],'utf8')).private === true))" "$repo_root/package.json")" + +if [[ "$root_private" != "true" ]]; then + echo "Refusing to pack while the repo-root package.json is not private" >&2 + exit 1 +fi + +if [[ "$pkg_name" != "simbroker" ]]; then + echo "packages/simbroker package.json name must be simbroker" >&2 + exit 1 +fi + +if [[ "$version" != "$pkg_version" ]]; then + echo "packages/simbroker version ($pkg_version) must match root version ($version)" >&2 + exit 1 +fi + +if [[ -z "$version" || "$version" == "." || "$version" == ".." || "$version" == *"/"* || "$version" == *"\\"* ]]; then + echo "Refusing to package an invalid package.json version: ${version:-}" >&2 + exit 1 +fi + +stage="$(mktemp -d "${TMPDIR:-/tmp}/simbroker-package-npm.XXXXXX")" +cleanup() { + rm -rf "$stage" +} +trap cleanup EXIT + +mkdir -p "$stage/bin" "$stage/broker-core" "$stage/client" +cp "$pkg_src/package.json" "$stage/package.json" +cp "$pkg_src/bin/simbroker.js" "$stage/bin/simbroker.js" +chmod +x "$stage/bin/simbroker.js" +cp "$pkg_src/README.md" "$stage/README.md" +cp "$repo_root/LICENSE" "$stage/LICENSE" + +tar -C "$repo_root/broker-core" --exclude test --exclude '*.test.mjs' -cf - . \ + | tar -C "$stage/broker-core" -xf - +tar -C "$repo_root/client" --exclude test --exclude '*.test.mjs' -cf - . \ + | tar -C "$stage/client" -xf - + +mkdir -p "$output_dir" +output_dir="$(cd "$output_dir" && pwd -P)" + +( + cd "$stage" + npm pack --pack-destination "$output_dir" +) + +tarball="$output_dir/simbroker-${version}.tgz" +if [[ ! -f "$tarball" ]]; then + echo "npm pack did not write $tarball" >&2 + ls -la "$output_dir" >&2 || true + exit 1 +fi + +printf '%s\n' "$tarball" diff --git a/scripts/sync_homebrew_tap.sh b/scripts/sync_homebrew_tap.sh new file mode 100755 index 0000000..8ca8740 --- /dev/null +++ b/scripts/sync_homebrew_tap.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "$0")/.." && pwd)" + +usage() { + cat <<'EOF' +Usage: bash scripts/sync_homebrew_tap.sh --tap-dir + bash scripts/sync_homebrew_tap.sh --check-remote + +Copy Formula/ and Casks/ from this checkout into a clone of +fiveonecode/homebrew-simulator-broker. Refuses to delete tap files when +this tree has no Formula/simbroker.rb. + +--check-remote compares this checkout's Formula/simbroker.rb to the +published tap formula. It is optional and is not part of spec-only. + +Options: + --tap-dir Existing checkout of fiveonecode/homebrew-simulator-broker + --check-remote Compare local Formula/simbroker.rb to the live tap + -h, --help Show this help text. +EOF +} + +tap_dir="" +check_remote=0 +while [[ $# -gt 0 ]]; do + case "$1" in + --tap-dir) + tap_dir="$2" + shift 2 + ;; + --check-remote) + check_remote=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown sync_homebrew_tap.sh argument: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ "$check_remote" -eq 1 ]]; then + if [[ ! -f "$repo_root/Formula/simbroker.rb" ]]; then + echo "This tree has no Formula/simbroker.rb." >&2 + exit 1 + fi + remote_url="https://raw.githubusercontent.com/fiveonecode/homebrew-simulator-broker/main/Formula/simbroker.rb" + remote_tmp="$(mktemp "${TMPDIR:-/tmp}/simbroker-tap-formula.XXXXXX")" + cleanup_remote() { + rm -f "$remote_tmp" + } + trap cleanup_remote EXIT + if ! curl --fail --silent --show-error --max-time 15 -A "simulator-broker-sync-homebrew-tap" \ + -o "$remote_tmp" "$remote_url"; then + echo "Could not fetch live tap formula from $remote_url" >&2 + exit 1 + fi + if ! cmp -s "$repo_root/Formula/simbroker.rb" "$remote_tmp"; then + echo "Live tap Formula/simbroker.rb does not match this checkout. Run scripts/sync_homebrew_tap.sh --tap-dir and push the tap." >&2 + exit 1 + fi + echo "Live tap formula matches this checkout." + exit 0 +fi + +if [[ -z "$tap_dir" ]]; then + usage >&2 + exit 1 +fi + +if [[ ! -d "$tap_dir/.git" ]]; then + echo "Tap dir must be a git checkout: $tap_dir" >&2 + exit 1 +fi + +if [[ ! -f "$repo_root/Formula/simbroker.rb" ]]; then + echo "This tree has no Formula/simbroker.rb; leaving the tap unchanged." >&2 + exit 1 +fi + +mkdir -p "$tap_dir/Formula" "$tap_dir/Casks" +cp -R "$repo_root/Formula/." "$tap_dir/Formula/" +cp -R "$repo_root/Casks/." "$tap_dir/Casks/" +printf '%s\n' "$tap_dir" diff --git a/spec/README.md b/spec/README.md index 7d59b34..2b0e90e 100644 --- a/spec/README.md +++ b/spec/README.md @@ -57,6 +57,10 @@ This repo exists to develop a reusable local simulator broker: - tagged Alpha `0.1.0-alpha.1` publishes a CLI tarball from `scripts/package_cli.sh` and runs the Node test surface on GitHub-hosted Ubuntu CI; the macOS app suite is not on that job - GitHub issue forms cover install failure, bug, and feature; the pull-request template is a public-patch checklist and does not require a harness session +- Homebrew formula `Formula/simbroker.rb` and packable npm CLI + `packages/simbroker` install the Alpha CLI; cask `Casks/simulator-broker.rb` + names the signed, notarized GitHub Release app zip. The tap GitHub name + is `fiveonecode/homebrew-simulator-broker` - `host init --bootstrap-config` warns that it creates real Simulator devices before provisioning them - broker-aware sample consumer repo artifacts now cover manual human, interactive agent, unattended agent build-and-test, and CI patterns under `examples/harness-adoption/` - broker-aware build/test leases now support downstream process registration, memory ceiling containment, evidence bundles, and forced-abort cleanup for detached simulator-like processes diff --git a/spec/build-and-test.md b/spec/build-and-test.md index 4c88064..ba22fdc 100644 --- a/spec/build-and-test.md +++ b/spec/build-and-test.md @@ -37,14 +37,26 @@ A first extracted implementation slice now exists: - `host init --bootstrap-config` prints an honest warning that it creates real iOS Simulator devices before those devices are created - `scripts/package_cli.sh` packages the Node CLI runtime into a versioned tarball without XcodeGen or an app build +- `Formula/simbroker.rb` installs that GitHub Release tarball through Homebrew. + `brew install fiveonecode/simulator-broker/simbroker` clones + `fiveonecode/homebrew-simulator-broker`. `scripts/sync_homebrew_tap.sh` + copies `Formula/` and `Casks/` into a tap checkout only when + `Formula/simbroker.rb` exists. `--check-remote` compares this tree to + the published tap formula and is not part of `spec-only` +- `scripts/package_npm.sh` (`npm run package:npm`) packs `packages/simbroker` + with a `bin` field; the repo-root package stays `private` +- `Casks/simulator-broker.rb` installs `Simulator Broker.app` from + `Simulator-Broker-.zip` on GitHub Releases (signed/notarized app + shape from `package_distribution.sh`, not `package:local`) - public GitHub-hosted Ubuntu CI runs `verify:public-surface`, `test:broker-core`, `test:client`, and `test:harness-adoption`; it does not run `test:app`. The job budget is 30 minutes. Broker tests that build an app snapshot must inject the fixture `simctl` adapter. The default public-surface scan reads index blobs only for dirty or missing worktree files so a clean checkout does not spawn one `git cat-file` per file. -- tagged versions such as `v0.1.0-alpha.1` attach the CLI tarball to a GitHub - Release through `.github/workflows/release.yml` +- tagged versions such as `v0.1.0-alpha.1` attach the CLI tarball and the + packable `simbroker-.tgz` to a GitHub Release through + `.github/workflows/release.yml` - local-debug portable bundle support through a zip bundle plus package-smoke verification of the bundled install path and installed-app launch proof - a separate Release distribution packaging path that requires operator-supplied signing inputs, runs `codesign` plus `spctl`, optionally notarizes with `notarytool`, and writes a readiness summary JSON - executable `agent-harness/` changes now route through the implementation diff --git a/spec/project-structure.md b/spec/project-structure.md index 09b000b..5717bff 100644 --- a/spec/project-structure.md +++ b/spec/project-structure.md @@ -10,6 +10,14 @@ Related: `spec/README.md`, `spec/global-simulator-broker.md`, `references/README protected-path, and handoff contract - `CHANGELOG.md` — published version history for tagged releases - `.github/workflows/` — public Node test CI and tag-driven CLI release +- `Formula/` — Homebrew CLI formula for the tagged Alpha tarball. Source of + truth for the tap; Homebrew clones + `fiveonecode/homebrew-simulator-broker` when a stranger runs + `brew install fiveonecode/simulator-broker/simbroker` +- `Casks/` — Homebrew cask for the signed, notarized operator app zip. + Synced to the same tap. +- `packages/simbroker/` — packable npm CLI metadata and `bin` wrapper; the + repo-root package stays private - `.github/ISSUE_TEMPLATE/` — install-failure, bug, and feature issue forms - `.github/pull_request_template.md` — public-patch PR checklist; no harness session required @@ -25,7 +33,8 @@ Related: `spec/README.md`, `spec/global-simulator-broker.md`, `references/README - `scripts/` — repo-owned helper scripts including the canonical `validate.sh` full-repository gate, app generation, repo-local install, CLI-only install (`install_local.sh --cli-only`), CLI tarball packaging - (`package_cli.sh`), distribution install, portable package creation, smoke + (`package_cli.sh`), npm CLI packing (`package_npm.sh`), Homebrew tap + sync (`sync_homebrew_tap.sh`), distribution install, portable package creation, smoke verification, and harness bootstrap ## Important rule