@@ -20,3 +20,141 @@ run = "bun run typecheck"
[tasks.check]
description = "Format, lint, and type-check"
depends = ["fmt", "lint", "typecheck"]
+
+[tasks."release:bump"]
+description = "Bump one or more package versions"
+usage = '''
+arg "<version>" help="SemVer increment or exact version"
+arg "<packages>" help="Package selectors" var=#true var_min=1
+flag "--preid <preid>" help="Prerelease identifier for pre* increments"
+'''
+shell = "bash -c"
+run = '''
+set -euo pipefail
+
+version=${usage_version:?}
+preid=${usage_preid:-}
+eval "packages=(${usage_packages:?})"
+
+exact_version_re='^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$'
+is_exact_version=false
+if [[ "$version" =~ $exact_version_re ]]; then
+ is_exact_version=true
+fi
+
+case "$version" in
+ patch|minor|major|prepatch|preminor|premajor|prerelease) ;;
+ *)
+ if [[ "$is_exact_version" != true ]]; then
+ echo "Error: unsupported version '$version'" >&2
+ echo "Use patch, minor, major, prepatch, preminor, premajor, prerelease, or an exact SemVer." >&2
+ exit 1
+ fi
+ ;;
+esac
+
+if [[ -n "$preid" ]]; then
+ case "$version" in
+ prepatch|preminor|premajor|prerelease) ;;
+ *)
+ echo "Error: --preid only applies to prepatch, preminor, premajor, or prerelease" >&2
+ exit 1
+ ;;
+ esac
+fi
+
+resolved_dirs=$(bash scripts/resolve-release-packages.bash "${packages[@]}")
+mapfile -t dirs <<< "$resolved_dirs"
+
+if [[ "$is_exact_version" == true && -n "${dirs[1]:-}" ]]; then
+ echo "Error: exact versions may only be applied to one package at a time" >&2
+ exit 1
+fi
+
+for dir in "${dirs[@]}"; do
+ args=(pm version "$version" --no-git-tag-version)
+ if [[ -n "$preid" ]]; then
+ args+=(--preid "$preid")
+ fi
+ echo "Bumping $dir"
+ (
+ cd "$dir"
+ bun "${args[@]}"
+ )
+done
+'''
+
+[tasks."release:pack"]
+description = "Check and dry-run npm package packing"
+usage = '''
+arg "<packages>" help="Package selectors" var=#true var_min=1
+'''
+shell = "bash -c"
+run = '''
+set -euo pipefail
+
+eval "packages=(${usage_packages:?})"
+resolved_dirs=$(bash scripts/resolve-release-packages.bash "${packages[@]}")
+mapfile -t dirs <<< "$resolved_dirs"
+
+mise run check
+
+for dir in "${dirs[@]}"; do
+ echo "Packing $dir (dry run)"
+ bunx npm pack --dry-run "./$dir"
+done
+'''
+
+[tasks."release:publish"]
+description = "Check, dry-run, and publish npm packages"
+usage = '''
+arg "<packages>" help="Package selectors" var=#true var_min=1
+flag "--tag <tag>" help="npm dist-tag" default="latest"
+flag "--otp <otp>" help="One-time password for npm two-factor auth"
+'''
+shell = "bash -c"
+run = '''
+set -euo pipefail
+
+package_version() {
+ bun -e 'const fs = require("node:fs"); const path = require("node:path"); const pkg = JSON.parse(fs.readFileSync(path.join(process.argv[1], "package.json"), "utf8")); console.log(pkg.version);' "$1"
+}
+
+eval "packages=(${usage_packages:?})"
+tag=${usage_tag:?}
+otp=${usage_otp:-}
+resolved_dirs=$(bash scripts/resolve-release-packages.bash "${packages[@]}")
+mapfile -t dirs <<< "$resolved_dirs"
+
+if [[ -n "$otp" && -n "${dirs[1]:-}" ]]; then
+ echo "Error: --otp may only be used with one package at a time" >&2
+ echo "Use an npm automation token for multi-package publishing, or publish each package separately with a fresh OTP." >&2
+ exit 1
+fi
+
+for dir in "${dirs[@]}"; do
+ version=$(package_version "$dir")
+ if [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+- && "$tag" == latest ]]; then
+ echo "Error: refusing to publish prerelease $dir@$version with tag latest" >&2
+ echo "Use --tag beta or another prerelease dist-tag." >&2
+ exit 1
+ fi
+done
+
+mise run check
+
+publish_args=(--access public --tag "$tag")
+if [[ -n "$otp" ]]; then
+ publish_args+=(--otp "$otp")
+fi
+
+for dir in "${dirs[@]}"; do
+ echo "Publishing $dir (dry run)"
+ bunx npm publish --dry-run "${publish_args[@]}" "./$dir"
+done
+
+for dir in "${dirs[@]}"; do
+ echo "Publishing $dir"
+ bunx npm publish "${publish_args[@]}" "./$dir"
+done
+'''
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+
+# SPDX-FileCopyrightText: Amolith <amolith@secluded.site>
+#
+# SPDX-License-Identifier: CC0-1.0
+
+set -euo pipefail
+
+valid_packages="answer, handoff, personas, questionnaire"
+
+resolve_package_dir() {
+ case "$1" in
+ answer|pi-answer|@amolith/pi-answer) printf '%s\n' packages/answer ;;
+ handoff|pi-handoff|@amolith/pi-handoff) printf '%s\n' packages/handoff ;;
+ personas|pi-personas|@amolith/pi-personas) printf '%s\n' packages/personas ;;
+ questionnaire|pi-questionnaire|@amolith/pi-questionnaire) printf '%s\n' packages/questionnaire ;;
+ *)
+ echo "Error: unknown package '$1'" >&2
+ echo "Valid packages: $valid_packages" >&2
+ exit 1
+ ;;
+ esac
+}
+
+if [[ $# -eq 0 ]]; then
+ echo "Error: at least one package is required" >&2
+ echo "Valid packages: $valid_packages" >&2
+ exit 1
+fi
+
+results=()
+for package in "$@"; do
+ dir=$(resolve_package_dir "$package")
+ if [[ ${#results[@]} -gt 0 ]]; then
+ for existing in "${results[@]}"; do
+ if [[ "$existing" == "$dir" ]]; then
+ echo "Error: multiple selectors resolved to '$dir' (duplicate triggered by '$package')" >&2
+ exit 1
+ fi
+ done
+ fi
+ results+=("$dir")
+done
+
+printf '%s\n' "${results[@]}"