- Convert sync filesystem and DuckDB operations to async across API routes, workspace lib, and active-runs to prevent event loop blocking during tree discovery, object lookups, and database queries - Add "tags" field type for free-form string arrays with parse-tags utility, TagsBadges/TagsInput UI components, filter operators, and CRM skill docs - Preserve rich text formatting (bold, italic, code, @mentions) in user chat messages by sending HTML alongside plain text through the transport layer - Detect empty-stream errors, improve agent error emission, and add file mutation queues for concurrent write safety in active-runs - Add pre-publish standalone node_modules verification in deploy script checking serverExternalPackages are present - Extract syncManagedSkills and discoverWorkspaceDirs for multi-workspace skill syncing, add ensureSeedAssets for runtime app dir - Bump version 2.1.1 → 2.1.4
373 lines
12 KiB
Bash
Executable File
373 lines
12 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# deploy.sh — build and publish denchclaw to npm
|
|
#
|
|
# Versioning convention (standard semver):
|
|
# --bump <kind> Increment current package version.
|
|
# kind: major | minor | patch
|
|
# 2.0.0 --bump patch => 2.0.1
|
|
# --version <ver> 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"
|
|
ALIAS_PACKAGE_NAME="dench"
|
|
ALIAS_PACKAGE_DIR="packages/dench"
|
|
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 <x.y.z> or --bump <major|minor|patch>"
|
|
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 <x.y.z> or --bump <major|minor|patch>."
|
|
;;
|
|
--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 <major|minor|patch> or --version <x.y.z>."
|
|
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
|
|
|
|
# ── telemetry ────────────────────────────────────────────────────────────────
|
|
|
|
if [[ -z "${POSTHOG_KEY:-}" ]]; then
|
|
echo "warning: POSTHOG_KEY not set — telemetry will be disabled in this build"
|
|
fi
|
|
export POSTHOG_KEY="${POSTHOG_KEY:-}"
|
|
export NEXT_PUBLIC_POSTHOG_KEY="${POSTHOG_KEY:-}"
|
|
|
|
# ── build ────────────────────────────────────────────────────────────────────
|
|
|
|
# Run the full build chain here so we can verify the standalone output
|
|
# before publishing. The `prepack` hook in package.json re-runs the same
|
|
# steps during `npm publish` but that's harmless (idempotent).
|
|
if [[ "$SKIP_BUILD" != true ]]; then
|
|
echo "building..."
|
|
pnpm build
|
|
|
|
echo "building web app (standalone)..."
|
|
pnpm web:build
|
|
|
|
echo "flattening standalone deps..."
|
|
pnpm web:prepack
|
|
fi
|
|
|
|
# ── pre-publish: verify standalone node_modules ──────────────────────────────
|
|
|
|
STANDALONE_APP_NM="apps/web/.next/standalone/apps/web/node_modules"
|
|
|
|
# Auto-extract serverExternalPackages from next.config.ts — these are NOT
|
|
# bundled by webpack, so they must exist in standalone node_modules or the
|
|
# web runtime will crash with "fetch failed" for users.
|
|
# Also always verify next/react/react-dom which the standalone server needs.
|
|
#
|
|
# Optional native accelerators (bufferutil, utf-8-validate) are skipped —
|
|
# ws works without them.
|
|
OPTIONAL_NATIVE="bufferutil utf-8-validate"
|
|
|
|
SERVER_EXTERNAL="$(node -e "
|
|
import('file://${ROOT_DIR}/apps/web/next.config.ts')
|
|
.then(m => (m.default.serverExternalPackages || []).forEach(p => console.log(p)))
|
|
.catch(() => {})
|
|
" 2>/dev/null)"
|
|
|
|
STANDALONE_OK=true
|
|
CHECKED=""
|
|
|
|
for mod in next react react-dom $SERVER_EXTERNAL; do
|
|
[ -z "$mod" ] && continue
|
|
if [ ! -d "${STANDALONE_APP_NM}/${mod}" ]; then
|
|
case " $OPTIONAL_NATIVE " in
|
|
*" $mod "*) continue ;;
|
|
esac
|
|
echo "error: required module '${mod}' missing from standalone build (${STANDALONE_APP_NM}/${mod})"
|
|
STANDALONE_OK=false
|
|
fi
|
|
CHECKED="${CHECKED:+$CHECKED }$mod"
|
|
done
|
|
|
|
if [ "$STANDALONE_OK" != true ]; then
|
|
die "standalone build is missing required node_modules — web chat will crash at runtime.
|
|
Run 'pnpm web:build && pnpm web:prepack' and verify the output."
|
|
fi
|
|
|
|
# Quick sanity: try to resolve each server-external package from the standalone dir.
|
|
for mod in $SERVER_EXTERNAL; do
|
|
[ -z "$mod" ] && continue
|
|
case " $OPTIONAL_NATIVE " in
|
|
*" $mod "*) continue ;;
|
|
esac
|
|
if ! node -e "require.resolve('${mod}', { paths: ['${STANDALONE_APP_NM}'] })" 2>/dev/null; then
|
|
die "standalone '${mod}' module exists but cannot be resolved — check flatten-standalone-deps output"
|
|
fi
|
|
done
|
|
echo "standalone node_modules verified ($CHECKED)"
|
|
|
|
# ── 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[@]}"
|
|
|
|
# ── publish alias package (dench → denchclaw) ────────────────────────────────
|
|
|
|
ALIAS_PUBLISHED=false
|
|
ALIAS_DIR="${ROOT_DIR}/${ALIAS_PACKAGE_DIR}"
|
|
if [[ -d "$ALIAS_DIR" ]]; then
|
|
# Pin the alias package version and its denchclaw dependency to this release.
|
|
node -e "
|
|
const fs = require('fs');
|
|
const pkg = JSON.parse(fs.readFileSync('${ALIAS_DIR}/package.json', 'utf-8'));
|
|
pkg.version = '${VERSION}';
|
|
pkg.dependencies.denchclaw = '^${VERSION}';
|
|
fs.writeFileSync('${ALIAS_DIR}/package.json', JSON.stringify(pkg, null, 2) + '\n');
|
|
"
|
|
echo "publishing ${ALIAS_PACKAGE_NAME}@${VERSION}..."
|
|
if (cd "$ALIAS_DIR" && npm publish --access public --tag latest "${NPM_FLAGS[@]}" 2>/dev/null); then
|
|
ALIAS_PUBLISHED=true
|
|
echo "published ${ALIAS_PACKAGE_NAME}@${VERSION}"
|
|
else
|
|
echo "warning: failed to publish ${ALIAS_PACKAGE_NAME}@${VERSION} (non-fatal)"
|
|
echo " npx ${PACKAGE_NAME} still works; ${ALIAS_PACKAGE_NAME} alias is optional"
|
|
fi
|
|
fi
|
|
|
|
# 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
|
|
if [[ "$ALIAS_PUBLISHED" == true ]]; then
|
|
verify_npx_command "$VERSION" "npx dench (via dench package)" \
|
|
npx --yes "${ALIAS_PACKAGE_NAME}@${VERSION}" --version
|
|
fi
|
|
verify_npx_invocation "npx denchclaw update --help" \
|
|
npx --yes "${PACKAGE_NAME}@${VERSION}" update --help
|
|
verify_npx_invocation "npx denchclaw start --help" \
|
|
npx --yes "${PACKAGE_NAME}@${VERSION}" start --help
|
|
verify_npx_invocation "npx denchclaw stop --help" \
|
|
npx --yes "${PACKAGE_NAME}@${VERSION}" stop --help
|
|
fi
|
|
|
|
# Post-publish sanity: confirm the standalone server was published.
|
|
STANDALONE_SERVER="apps/web/.next/standalone/apps/web/server.js"
|
|
if [[ ! -f "$STANDALONE_SERVER" ]]; then
|
|
echo "warning: standalone web app server.js not found after publish ($STANDALONE_SERVER)"
|
|
echo " users may not get a working Web UI — check the prepack step"
|
|
fi
|
|
|
|
echo ""
|
|
if [[ "$ALIAS_PUBLISHED" == true ]]; then
|
|
echo "published ${PACKAGE_NAME}@${VERSION} + ${ALIAS_PACKAGE_NAME}@${VERSION}"
|
|
echo "install: npm i -g ${PACKAGE_NAME} (or: npm i -g ${ALIAS_PACKAGE_NAME})"
|
|
else
|
|
echo "published ${PACKAGE_NAME}@${VERSION}"
|
|
echo "install: npm i -g ${PACKAGE_NAME}"
|
|
fi
|