#!/usr/bin/env bash # deploy.sh — build and publish denchclaw to npm # # Versioning convention (standard semver): # --bump Increment current package version. # kind: major | minor | patch # 2.0.0 --bump patch => 2.0.1 # --version Publish an explicit semver version (x.y.z). # (no flag) Publish whatever version is already in package.json. # # Flags: # --skip-tests Skip running tests before build/publish. # --skip-npx-smoke Skip post-publish npx binary verification. # # Environment: # NPM_TOKEN Required. npm auth token for publishing. set -euo pipefail PACKAGE_NAME="denchclaw" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" cd "$ROOT_DIR" # ── helpers ────────────────────────────────────────────────────────────────── die() { echo "error: $*" >&2; exit 1; } current_version() { node -p "require('./package.json').version" } # Check whether a specific version is already on the npm registry. npm_version_exists() { local v="$1" npm view "${PACKAGE_NAME}@${v}" version 2>/dev/null | grep -q "${v}" 2>/dev/null } is_plain_semver() { local v="$1" [[ "$v" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] } bump_semver() { local current="$1" local kind="$2" if ! is_plain_semver "$current"; then die "current version must be plain semver (x.y.z) for --bump, got: $current" fi local major minor patch IFS='.' read -r major minor patch <<<"$current" case "$kind" in major) echo "$((major + 1)).0.0" ;; minor) echo "${major}.$((minor + 1)).0" ;; patch) echo "${major}.${minor}.$((patch + 1))" ;; *) die "--bump requires one of: major, minor, patch" ;; esac } verify_npx_command() { local version="$1" local label="$2" shift 2 local attempts=15 local delay_seconds=2 local output="" local temp_dir temp_dir="$(mktemp -d)" for ((i = 1; i <= attempts; i++)); do if output="$(cd "$temp_dir" && "$@" 2>/dev/null)"; then if [[ "$output" == *"$version"* ]]; then echo "verified ${label}: ${output}" rm -rf "$temp_dir" return 0 fi fi sleep "$delay_seconds" done rm -rf "$temp_dir" echo "error: failed to verify ${label} for ${PACKAGE_NAME}@${version}" >&2 return 1 } verify_npx_invocation() { local label="$1" shift local attempts=15 local delay_seconds=2 local temp_dir temp_dir="$(mktemp -d)" for ((i = 1; i <= attempts; i++)); do if (cd "$temp_dir" && "$@" >/dev/null 2>&1); then echo "verified ${label}" rm -rf "$temp_dir" return 0 fi sleep "$delay_seconds" done rm -rf "$temp_dir" echo "error: failed to verify ${label}" >&2 return 1 } # ── parse args ─────────────────────────────────────────────────────────────── MODE="" BUMP_KIND="" EXPLICIT_VERSION="" DRY_RUN=false SKIP_BUILD=false SKIP_TESTS=false SKIP_NPX_SMOKE=false set_mode() { local next="$1" if [[ -n "$MODE" && "$MODE" != "$next" ]]; then die "choose only one version mode: --version or --bump " fi MODE="$next" } while [[ $# -gt 0 ]]; do case $1 in --version) set_mode "version" EXPLICIT_VERSION="${2:?--version requires a semver argument (x.y.z)}" shift 2 ;; --bump) set_mode "bump" BUMP_KIND="${2:?--bump requires one of: major, minor, patch}" shift 2 ;; --upstream) die "--upstream has been removed. Use --version or --bump ." ;; --dry-run) DRY_RUN=true shift ;; --skip-build) SKIP_BUILD=true shift ;; --skip-tests) SKIP_TESTS=true shift ;; --skip-npx-smoke) SKIP_NPX_SMOKE=true shift ;; --help|-h) sed -n '2,/^[^#]/{ /^#/s/^# \{0,1\}//p; }' "$0" exit 0 ;; *) die "unknown argument: $1 (see --help)" ;; esac done # ── auth ───────────────────────────────────────────────────────────────────── if [[ -z "${NPM_TOKEN:-}" ]]; then die "NPM_TOKEN environment variable is required" fi # Write a temporary .npmrc for auth (npm_config_ env vars can't encode # registry-scoped keys because they contain slashes and colons). NPMRC_TEMP="${ROOT_DIR}/.npmrc.deploy" trap 'rm -f "$NPMRC_TEMP"' EXIT echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > "$NPMRC_TEMP" NPM_FLAGS=(--userconfig "$NPMRC_TEMP") # ── compute version ───────────────────────────────────────────────────────── CURRENT="$(current_version)" case "$MODE" in version) if ! is_plain_semver "$EXPLICIT_VERSION"; then die "--version must be plain semver (x.y.z), got: $EXPLICIT_VERSION" fi VERSION="$EXPLICIT_VERSION" echo "explicit version: $CURRENT → $VERSION" ;; bump) VERSION="$(bump_semver "$CURRENT" "$BUMP_KIND")" echo "semver bump (${BUMP_KIND}): $CURRENT → $VERSION" ;; *) VERSION="$CURRENT" echo "publishing current version: $VERSION" ;; esac if npm_version_exists "$VERSION"; then die "version $VERSION already exists on npm. Use --bump or --version ." fi if [[ "$DRY_RUN" == true ]]; then echo "[dry-run] would publish ${PACKAGE_NAME}@${VERSION}" exit 0 fi # ── set version ────────────────────────────────────────────────────────────── npm version "$VERSION" --no-git-tag-version --allow-same-version "${NPM_FLAGS[@]}" # ── pre-flight: tests ──────────────────────────────────────────────────────── if [[ "$SKIP_TESTS" != true ]] && [[ "$SKIP_BUILD" != true ]]; then echo "running tests..." pnpm test fi # ── build ──────────────────────────────────────────────────────────────────── # The `prepack` script (triggered by `npm publish`) runs the DenchClaw build chain: # pnpm build && pnpm web:build && pnpm web:prepack # Running `pnpm build` here is a redundant fail-fast: catch CLI build errors # before committing to a publish attempt. if [[ "$SKIP_BUILD" != true ]]; then echo "building..." pnpm build echo "building web app (standalone verification)..." pnpm web:build fi # ── publish ────────────────────────────────────────────────────────────────── # Always tag as "latest" — npm skips the latest tag for prerelease versions # by default, but we want `npm i -g denchclaw` to always resolve to # the most recently published version. echo "publishing ${PACKAGE_NAME}@${VERSION}..." npm publish --access public --tag latest "${NPM_FLAGS[@]}" # Verify published npx flows for both CLI aliases. if [[ "$SKIP_NPX_SMOKE" != true ]]; then echo "verifying npx binaries..." verify_npx_command "$VERSION" "npx denchclaw" \ npx --yes "${PACKAGE_NAME}@${VERSION}" --version verify_npx_command "$VERSION" "npx dench" \ npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench --version verify_npx_invocation "npx dench update --help" \ npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench update --help verify_npx_invocation "npx dench start --help" \ npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench start --help verify_npx_invocation "npx dench stop --help" \ npx --yes --package "${PACKAGE_NAME}@${VERSION}" dench stop --help fi # Verify the standalone web app was included in the published package. # `prepack` should have built it; if this file is missing, the web UI # won't work for users who install globally. STANDALONE_SERVER="apps/web/.next/standalone/apps/web/server.js" if [[ ! -f "$STANDALONE_SERVER" ]]; then echo "warning: standalone web app build not found after publish ($STANDALONE_SERVER)" echo " users may not get a working Web UI — check the prepack step" fi echo "" echo "published ${PACKAGE_NAME}@${VERSION}" echo "install: npm i -g ${PACKAGE_NAME}"