Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
88667cc
fix(capabilities): export COMMAND_META, add rules explain entry, add …
Apr 25, 2026
a947cfc
test(devices): assert exit code 1 for API error 190, add happy-path e…
Apr 25, 2026
c94796e
docs: update test count to 1889
Apr 25, 2026
746f2ac
fix(policy): relax alias deviceId pattern to accept hex MAC and lower…
Apr 25, 2026
ace7ed9
docs: update test count to 1895
Apr 25, 2026
55a4875
feat(agent-bootstrap): add --sections flag to project top-level paylo…
Apr 25, 2026
8dd6ffc
docs: update test count to 1898
Apr 25, 2026
76d4384
perf: switch build:prod to esbuild bundler, inline pure-JS deps into …
Apr 25, 2026
e6a0aad
docs: update test count to 1900
Apr 25, 2026
68b8fe8
refactor(capabilities,schema): add schema export --capabilities, repl…
Apr 25, 2026
1ffb6b5
test(daemon): add stop, status, reload subcommand coverage
Apr 25, 2026
bcde7bc
test(rules): add conflicts, doctor, summary, last-fired coverage
Apr 25, 2026
bb7d4a6
test: add webhook token, suggest, health-check, scenes validate/simul…
Apr 25, 2026
0141ee5
test: address code review findings — timer cleanup, bundle guard, sta…
Apr 25, 2026
7e93979
chore: bump version to 3.2.0
Apr 25, 2026
787c92f
Merge pull request #33 from OpenWonderLabs/refactor/capabilities-cata…
chenliuyun Apr 25, 2026
10d094c
fix: remove duplicate shebang — src/index.ts had shebang AND bundle.m…
Apr 25, 2026
d79cf46
chore: bump version to 3.2.1
Apr 25, 2026
fadfa0f
test: add esbuild bundle validation tests (shebang, syntax, size, ver…
Apr 25, 2026
7bbbc44
fix(bundle): resolve CJS require shim and duplicate shebang in esbuil…
Apr 25, 2026
b92948a
Harden npm package publish and smoke verification
Apr 25, 2026
12dae35
chore(ci): align pack-install smoke with publish artifact, document r…
Apr 25, 2026
845237d
fix(ci): align publish.yml with prepublishOnly — single esbuild publi…
Apr 26, 2026
67a1a41
ci(bundle-smoke): make blocking + matrix across Node 18/20/22
Apr 26, 2026
7f59a0b
test(status-sync): replace vi.spyOn(process.kill) with direct assignment
Apr 26, 2026
8802f47
refactor(build): collapse build and build:prod into a single release …
Apr 26, 2026
10224b5
chore: bump version to 3.2.2
Apr 26, 2026
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
8 changes: 8 additions & 0 deletions .githooks/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh
set -eu

REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

echo "[pre-commit] packaging sanity checks"
npm run verify:pre-commit
8 changes: 8 additions & 0 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh
set -eu

REPO_ROOT="$(git rev-parse --show-toplevel)"
cd "$REPO_ROOT"

echo "[pre-push] tarball install smoke"
npm run verify:pre-push
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,44 @@ jobs:
fi
- run: npm test

bundle-smoke:
name: esbuild bundle smoke test (Node ${{ matrix.node-version }})
runs-on: ubuntu-latest
needs: test
strategy:
fail-fast: false
matrix:
node-version: [18.x, 20.x, 22.x]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: npm
- run: npm ci
- run: npm run build
- name: Shebang count must be exactly 1
run: |
COUNT=$(grep -c "#!/usr/bin/env node" dist/index.js)
if [ "$COUNT" -ne 1 ]; then
echo "FAIL: Expected 1 shebang, found $COUNT"
exit 1
fi
echo "OK: shebang count = $COUNT"
- name: Node.js syntax check
run: node --check dist/index.js
- name: --version smoke test (exits 0, outputs correct semver)
run: |
PKG=$(node -p "require('./package.json').version")
CLI=$(node dist/index.js --version)
echo "package.json=$PKG bundle=$CLI"
if [ "$PKG" != "$CLI" ]; then
echo "FAIL: version mismatch"
exit 1
fi
- name: Bundle size check
run: npm test -- tests/build/

offline-smoke:
name: Offline size budgets
runs-on: ubuntu-latest
Expand Down Expand Up @@ -89,6 +127,21 @@ jobs:
exit 1
fi

pack-install-smoke:
name: Packed install smoke (esbuild — matches publish)
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.x
cache: npm
- run: npm ci
- run: npm run build
- name: npm pack -> npm install tarball -> switchbot --version
run: npm run smoke:pack-install

policy-schema-sync:
name: Policy schema sync with skill repo
runs-on: ubuntu-latest
Expand Down
162 changes: 162 additions & 0 deletions .github/workflows/npm-published-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
name: npm published smoke

on:
workflow_run:
workflows: ['Publish to npm']
types: [completed]
workflow_dispatch:
inputs:
version:
description: 'Published npm version to verify (defaults to package.json from checked-out commit)'
required: false

jobs:
smoke:
if: >
github.event_name == 'workflow_dispatch' ||
(github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'release')
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}

- uses: actions/setup-node@v4
with:
node-version: 20.x
registry-url: https://registry.npmjs.org

- name: Verify credentials present
env:
TOKEN: ${{ secrets.SWITCHBOT_TOKEN }}
SECRET: ${{ secrets.SWITCHBOT_SECRET }}
run: |
if [ -z "$TOKEN" ] || [ -z "$SECRET" ]; then
echo "SWITCHBOT_TOKEN / SWITCHBOT_SECRET not set in repo secrets"
exit 1
fi

- name: Verify npm token present
env:
TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
if [ -z "$TOKEN" ]; then
echo "NPM_TOKEN not set in repo secrets"
exit 1
fi

- name: Resolve target version
id: version
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
else
VERSION=$(node -p "require('./package.json').version")
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "target_version=$VERSION"

- name: Resolve current latest dist-tag
id: latest
run: |
LATEST=$(npm view @switchbot/openapi-cli dist-tags.latest)
echo "version=$LATEST" >> "$GITHUB_OUTPUT"
echo "current_latest=$LATEST"

- name: Wait for npm package to become available
id: wait_package
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
for i in $(seq 1 24); do
if [ "${{ github.event_name }}" = "workflow_run" ]; then
FOUND=$(npm view "@switchbot/openapi-cli@next" version 2>/dev/null || true)
if [ "$FOUND" = "$VERSION" ]; then
echo "npm package is available on next: $FOUND"
exit 0
fi
echo "waiting for @switchbot/openapi-cli@$VERSION to appear on npm dist-tag next ($i/24); current next=$FOUND"
else
FOUND=$(npm view "@switchbot/openapi-cli@$VERSION" version 2>/dev/null || true)
if [ "$FOUND" = "$VERSION" ]; then
echo "npm package version is available: $FOUND"
exit 0
fi
echo "waiting for @switchbot/openapi-cli@$VERSION to appear on npm ($i/24)"
fi
sleep 10
done
echo "Timed out waiting for @switchbot/openapi-cli@$VERSION on npm"
exit 1

- name: Install published package in a clean temp project
id: install_package
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
TMPDIR=$(mktemp -d)
echo "TMPDIR=$TMPDIR" >> "$GITHUB_ENV"
cd "$TMPDIR"
npm init -y >/dev/null 2>&1
npm install "@switchbot/openapi-cli@$VERSION"

- name: Binary and offline smoke
id: offline_smoke
env:
TMPDIR: ${{ env.TMPDIR }}
VERSION: ${{ steps.version.outputs.version }}
run: |
cd "$TMPDIR"
ACTUAL=$(npx --no-install switchbot --version)
test "$ACTUAL" = "$VERSION"
npx --no-install switchbot --help >/dev/null
npx --no-install switchbot schema export --compact >/dev/null
npx --no-install switchbot capabilities --json | jq -e '.data.commandMeta != null' >/dev/null

- name: Live smoke with configured credentials
id: live_smoke
env:
TMPDIR: ${{ env.TMPDIR }}
SWITCHBOT_TOKEN: ${{ secrets.SWITCHBOT_TOKEN }}
SWITCHBOT_SECRET: ${{ secrets.SWITCHBOT_SECRET }}
run: |
cd "$TMPDIR"
npx --no-install switchbot doctor --json | jq -e '.data.summary != null' >/dev/null
npx --no-install switchbot devices list --json | jq -e '.data.deviceList != null or .data.infraredRemoteList != null' >/dev/null

- name: Promote verified version to latest
if: success()
env:
VERSION: ${{ steps.version.outputs.version }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm dist-tag add "@switchbot/openapi-cli@$VERSION" latest
echo "Promoted @switchbot/openapi-cli@$VERSION to dist-tag latest"

- name: Deprecate failed version
if: >
failure() &&
steps.wait_package.outcome == 'success' &&
(
steps.install_package.outcome == 'failure' ||
steps.offline_smoke.outcome == 'failure'
)
env:
VERSION: ${{ steps.version.outputs.version }}
PREVIOUS_LATEST: ${{ steps.latest.outputs.version }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
npm deprecate "@switchbot/openapi-cli@$VERSION" "Published to dist-tag next but failed package smoke tests. Install @switchbot/openapi-cli@${PREVIOUS_LATEST} or use dist-tag latest."
echo "Deprecated @switchbot/openapi-cli@$VERSION after package smoke failure"

- name: Cleanup temp project
if: always()
env:
TMPDIR: ${{ env.TMPDIR }}
run: |
if [ -n "$TMPDIR" ] && [ -d "$TMPDIR" ]; then
rm -rf "$TMPDIR"
fi
5 changes: 4 additions & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ jobs:
echo "Tag $TAG_VERSION does not match package.json version $PKG_VERSION"
exit 1
fi
- run: npm publish --provenance --access public
- name: Smoke test packed npm artifact
run: npm run smoke:pack-install
- name: Publish package to npm dist-tag next
run: npm publish --tag next --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
49 changes: 49 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,35 @@ All notable changes to `@switchbot/openapi-cli` are documented in this file.
The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [3.2.2] - 2026-04-26

### Changed — release pipeline

- Release pipeline unified: `npm run build` is now the single source for the
published tarball. It runs a 5-stage `scripts/build.mjs` orchestrator
(clean → typecheck → bundle → copy-assets → ensure-binary). `prepublishOnly`,
`verify:pre-commit`, `verify:pre-push`, `publish.yml`, and the `bundle-smoke`
/ `pack-install-smoke` CI jobs all call `npm run build` by name — no job
re-implements the build steps and no other script writes to `dist/`.
- Removed `npm run build:prod` and `npm run clean` — both are folded into
`scripts/build.mjs`.
- Added `npm run typecheck` (`tsc --noEmit`) as the local "does it still
compile?" escape hatch.
- Split `scripts/copy-assets.mjs` responsibility into two scripts with one
failure mode each: `copy-assets.mjs` only copies policy assets, and the
new `scripts/ensure-binary.mjs` asserts the shebang is present on
`dist/index.js` and `chmod 0755`s it. `ensure-binary.mjs` is a regression
guard — it fails loudly if the esbuild banner drops the shebang, rather
than silently repairing it the way `copy-assets.mjs` used to.

## [3.2.1] - 2026-04-25

> **Deprecated on npm.** The initial `3.2.1` publish shipped a broken bin
> (missing shebang / exec bit after `npm pack`). It has been rolled back
> from `main` and relanded in `3.2.2`; install `@switchbot/openapi-cli@3.2.2`
> or later. The feature list below is retained as the historical record of
> what `3.2.1` intended to deliver and what `3.2.2` now ships.

### Added — plan resource model, MCP risk profiles, rules safety primitives

- `switchbot plan save [file]` — persist a validated plan to `~/.switchbot/plans/<planId>.json`
Expand All @@ -32,6 +59,28 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `rules lint` now validates `hysteresis` / `requires_stable_for` duration syntax and warns
when `hysteresis` and `requires_stable_for` are both set.

### Changed — release pipeline

- **Single publish source**: `publish.yml` now runs `npm run build:prod`
(esbuild) to match what `prepublishOnly` produces at `npm publish` time. The
tarball validated by `smoke:pack-install` is now byte-identical to the
tarball that actually ships to the registry — no artifact swap during
publish.
- Pre-publish `smoke:pack-install` runs in `publish.yml` before `npm publish`,
and the same smoke runs locally via `pre-push` hook (`verify:pre-push`) and
on every PR in CI (`pack-install-smoke`).
- `scripts/copy-assets.mjs` now injects the `#!/usr/bin/env node` shebang into
`dist/index.js` and chmods it to `0755` after every build, so the npm bin
entry is always executable.
- New `npm-published-smoke.yml` workflow verifies published tarballs on the
npm registry, auto-promotes `next → latest` on success, and auto-deprecates
on package-install/offline smoke failures only (never on live API flakes).
- `bundle-smoke` CI job is now a blocking matrix across Node 18/20/22 (was
single-node Node 20, advisory), so the esbuild bundle must start cleanly
on every supported Node version before a PR can merge.
- See [`docs/release-pipeline.md`](./docs/release-pipeline.md) for the full
gate sequence and invariants.

## [3.2.0] - 2026-04-25

### Added — daemon, upgrade-check, scenes validate/simulate, rules summary
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ Under the hood every surface shares the same catalog, cache, and HMAC client —
- 🎨 **Dual output modes** — colorized tables by default; `--json` passthrough for `jq` and scripting
- 🔐 **Secure credentials** — HMAC-SHA256 signed requests; config file written with `0600`; env-var override for CI
- 🔍 **Dry-run mode** — preview every mutating request before it hits the API
- 🧪 **Fully tested** — 1882 Vitest tests, mocked axios, zero network in CI
- 🧪 **Fully tested** — 1959 Vitest tests, mocked axios, zero network in CI
- ⚡ **Shell completion** — Bash / Zsh / Fish / PowerShell

## Requirements
Expand Down Expand Up @@ -894,7 +894,7 @@ Queries the npm registry for the latest published version and compares it agains

```json
{
"current": "3.2.1",
"current": "3.2.2",
"latest": "4.0.0",
"upToDate": false,
"updateAvailable": true,
Expand Down Expand Up @@ -1123,7 +1123,7 @@ npm install

npm run dev -- <args> # Run from TypeScript sources via tsx
npm run build # Compile to dist/
npm test # Run the Vitest suite (1882 tests)
npm test # Run the Vitest suite (1959 tests)
npm run test:watch # Watch mode
npm run test:coverage # Coverage report (v8, HTML + text)
```
Expand Down Expand Up @@ -1205,7 +1205,7 @@ src/
├── format.ts # renderRows / filterFields / output-format dispatch
├── audit.ts # JSONL audit log writer
└── quota.ts # Local daily-quota counter
tests/ # Vitest suite (1882 tests, mocked axios, no network)
tests/ # Vitest suite (1959 tests, mocked axios, no network)
```

### Release flow
Expand All @@ -1219,6 +1219,8 @@ git push --follow-tags

Then on GitHub → **Releases → Draft a new release → select tag → Publish**. The `publish.yml` workflow runs tests, verifies the tag matches `package.json`, and publishes `@switchbot/openapi-cli` to npm with [provenance](https://docs.npmjs.com/generating-provenance-statements).

See [`docs/release-pipeline.md`](./docs/release-pipeline.md) for the full pre-publish and post-publish verification flow (local hooks → CI → `publish.yml` → `npm-published-smoke.yml`).

## License

[MIT](./LICENSE) © chenliuyun
Expand Down
Loading
Loading