@agentify/cli 0.2.7 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,12 +18,12 @@ The hosted setup flow at `https://chat.agentify.sh` can also install the CLI for
18
18
  ```bash
19
19
  agentify chat pair --pairing-ticket <ticket> --workspace "$PWD" --label "$(basename "$PWD")"
20
20
  agentify chat daemon --runner codex
21
- agentify chat daemon --runner claude
22
- agentify chat daemon --runner gemini
21
+ agentify chat daemon --runner-command claude
22
+ agentify chat daemon --runner-command gemini
23
23
  agentify doctor
24
24
  ```
25
25
 
26
- `agentify chat pair` registers this machine as the local decrypting device for a workspace. `agentify chat daemon` receives encrypted chat jobs and runs the selected local CLI in the configured workspace.
26
+ `agentify chat pair` registers this machine as the local decrypting device for a workspace. `agentify chat daemon --runner codex` runs Codex through structured JSON events for reliable encrypted output streaming. `agentify chat daemon --runner-command ...` is available for other local CLIs and custom runner commands.
27
27
 
28
28
  ## Security Model
29
29
 
@@ -56,6 +56,60 @@ To verify release readiness:
56
56
  npm run release:check
57
57
  ```
58
58
 
59
+ ### Release automation
60
+
61
+ For one-command release prep across all supported platforms:
62
+
63
+ ```bash
64
+ npm run release:all
65
+ ```
66
+
67
+ From the workspace root, you can run the same flow with:
68
+
69
+ ```bash
70
+ bash ../scripts/release-agentify-cli.sh
71
+ ```
72
+
73
+ The release pipeline is non-publishing by default and runs:
74
+
75
+ - cross-platform Rust builds
76
+ - multi-target npm wrapper staging
77
+ - local version/release checks
78
+ - package verification
79
+ - release payload leak scan
80
+
81
+ Use explicit flags to publish:
82
+
83
+ ```bash
84
+ npm run release:all:publish -- --npm-tag latest
85
+ ```
86
+
87
+ or:
88
+
89
+ ```bash
90
+ bash ./scripts/release-all.sh --publish-npm --publish-git --auto-commit --create-tag --push-git
91
+ ```
92
+
93
+ Useful options:
94
+
95
+ ```bash
96
+ --targets darwin-arm64,linux-x64
97
+ --publish-npm
98
+ --publish-git
99
+ --auto-commit
100
+ --create-tag
101
+ --push-git
102
+ --skip-leak-scan
103
+ --skip-cargo-test
104
+ --skip-npm-tests
105
+ --dry-run
106
+ ```
107
+
108
+ Identity checks are enforced on publish:
109
+
110
+ - npm account must be `agentify`
111
+ - GitHub account must be `waml`
112
+
59
113
  During local incubation, when only some platform binaries are staged, use:
60
114
 
61
115
  ```bash
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentify/cli",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Agentify CLI for secure local Agentify Chat runners",
5
5
  "license": "MPL-2.0",
6
6
  "homepage": "https://agentify.sh",
@@ -30,6 +30,8 @@
30
30
  "postinstall": "node ./scripts/postinstall.js",
31
31
  "prepublishOnly": "npm run release:check",
32
32
  "release:check": "node ./scripts/release-check.js",
33
+ "release:all": "bash ./scripts/release-all.sh",
34
+ "release:all:publish": "bash ./scripts/release-all.sh --publish-npm --publish-git --auto-commit --create-tag --push-git",
33
35
  "release:manifest": "node ./scripts/build-release-manifest.js",
34
36
  "stage:bin": "node ./scripts/stage-binary.js",
35
37
  "test:npm": "node --test ./test/npm/*.test.js",
@@ -0,0 +1,614 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
+
6
+ NPM_PUBLISH=false
7
+ GIT_PUBLISH=false
8
+ AUTO_COMMIT=false
9
+ CREATE_TAG=false
10
+ PUSH_GIT=false
11
+ ALLOW_MISSING_TARGETS=false
12
+ ALLOW_MISSING_TOOLCHAIN=false
13
+ AUTO_INSTALL_TARGETS=false
14
+ SKIP_LEAK_SCAN=false
15
+ SKIP_CARGO_TEST=false
16
+ SKIP_NPM_TESTS=false
17
+ SKIP_RELEASE_CHECK=false
18
+ SKIP_VERIFY_PACKAGE=false
19
+ SKIP_GIT=false
20
+ DRY_RUN=false
21
+ CHECK_IDENTITY=false
22
+ REQUIRE_CLEAN_WORKTREE=false
23
+
24
+ NPM_TAG="latest"
25
+ GIT_REMOTE="origin"
26
+ GIT_BRANCH=""
27
+ COMMIT_MESSAGE=""
28
+ TAG_NAME=""
29
+
30
+ REQUIRED_NPM_ACCOUNT="agentify"
31
+ REQUIRED_GH_ACCOUNT="waml"
32
+ ALL_TARGETS="darwin-arm64,darwin-x64,linux-arm64,linux-x64,win32-arm64,win32-x64"
33
+ FILTER_TARGETS=""
34
+
35
+ declare -A TARGET_INFO=(
36
+ ["darwin-arm64"]="aarch64-apple-darwin:agentify"
37
+ ["darwin-x64"]="x86_64-apple-darwin:agentify"
38
+ ["linux-arm64"]="aarch64-unknown-linux-gnu:agentify"
39
+ ["linux-x64"]="x86_64-unknown-linux-gnu:agentify"
40
+ ["win32-arm64"]="aarch64-pc-windows-gnullvm:agentify.exe"
41
+ ["win32-x64"]="x86_64-pc-windows-gnu:agentify.exe"
42
+ )
43
+
44
+ log() {
45
+ echo "[$(date -u +'%Y-%m-%dT%H:%M:%SZ')] $*"
46
+ }
47
+
48
+ abort() {
49
+ log "ERROR: $*"
50
+ exit 1
51
+ }
52
+
53
+ usage() {
54
+ cat <<'EOF'
55
+ Usage:
56
+ bash scripts/release-all.sh [options]
57
+
58
+ Release automation (safe by default: no publish unless explicitly enabled):
59
+ --publish-npm Publish npm package (explicit opt-in)
60
+ --publish-git Push release git changes/tags (explicit opt-in)
61
+ --auto-commit Commit staged release artifacts before git push
62
+ --create-tag Create an annotated git tag
63
+ --push-git Push branch/tags (requires --publish-git)
64
+ --publish-all Publish npm + git together (implies --publish-npm --publish-git)
65
+ --skip-missing-targets Continue if a requested target cannot be built (strict by default)
66
+ --allow-missing-toolchain Continue if requested target is blocked by missing toolchain
67
+ --auto-install-targets Install missing rust targets via rustup
68
+
69
+ Build / checks:
70
+ --targets <list> Comma-separated targets (default: all supported)
71
+ --skip-leak-scan Skip leak scan
72
+ --skip-cargo-test Skip cargo test
73
+ --skip-npm-tests Skip npm wrapper tests
74
+ --skip-release-check Skip npm run release:check -- --skip-cargo
75
+ --skip-verify-package Skip npm run verify:package
76
+ --dry-run Print actions only
77
+ --require-clean-worktree Require clean worktree for git publish
78
+
79
+ Git:
80
+ --git-remote <name> Git remote (default: origin)
81
+ --git-branch <name> Git branch for push (default: current branch)
82
+ --commit-message <text> Commit message for auto commit
83
+ --tag <name> Tag name (default: v<version>)
84
+ --skip-git Skip all git publish checks
85
+
86
+ Publish:
87
+ --npm-tag <tag> npm tag (default: latest)
88
+ --check-identity Verify npm + GitHub identities before publish
89
+ --help
90
+
91
+ Examples:
92
+ bash scripts/release-all.sh
93
+ bash scripts/release-all.sh --targets darwin-arm64,linux-x64
94
+ bash scripts/release-all.sh --publish-npm --publish-git --auto-commit --create-tag --push-git
95
+ EOF
96
+ }
97
+
98
+ run_cmd() {
99
+ if [[ "$DRY_RUN" == "true" ]]; then
100
+ log "DRY RUN: $*" >&2
101
+ return 0
102
+ fi
103
+ log "RUN: $*" >&2
104
+ "$@"
105
+ }
106
+
107
+ run_in_root() {
108
+ (cd "$ROOT_DIR" && "$@")
109
+ }
110
+
111
+ log_target_status() {
112
+ local target=$1
113
+ local status=$2
114
+ if [[ "$status" == "ok" ]]; then
115
+ log "Target ${target} built and staged."
116
+ elif [[ "$status" == "skipped" ]]; then
117
+ log "Target ${target} skipped."
118
+ fi
119
+ }
120
+
121
+ trim() {
122
+ local value=$1
123
+ value="${value#"${value%%[![:space:]]*}"}"
124
+ value="${value%"${value##*[![:space:]]}"}"
125
+ if [[ -z "$value" ]]; then
126
+ printf ''
127
+ return
128
+ fi
129
+ printf '%s' "$value"
130
+ }
131
+
132
+ is_installed_target() {
133
+ local rust_target=$1
134
+ if ! command -v rustup >/dev/null 2>&1; then
135
+ return 1
136
+ fi
137
+ rustup target list --installed | awk '{print $1}' | grep -Fxq "$rust_target"
138
+ }
139
+
140
+ ensure_target_toolchain() {
141
+ local rust_target=$1
142
+ if is_installed_target "$rust_target"; then
143
+ return 0
144
+ fi
145
+
146
+ if [[ "$AUTO_INSTALL_TARGETS" == "true" ]]; then
147
+ if ! command -v rustup >/dev/null 2>&1; then
148
+ log "WARN: rustup is required to install missing target ${rust_target}, but it is unavailable."
149
+ return 1
150
+ fi
151
+ log "Installing rust target ${rust_target}"
152
+ run_cmd rustup target add "$rust_target"
153
+ if is_installed_target "$rust_target"; then
154
+ return 0
155
+ fi
156
+ return 1
157
+ fi
158
+
159
+ if [[ "$ALLOW_MISSING_TOOLCHAIN" == "true" ]]; then
160
+ log "WARN: missing rust target ${rust_target}; skipping (allow-missing-toolchain=true)"
161
+ return 1
162
+ fi
163
+
164
+ return 1
165
+ }
166
+
167
+ cleanup_temp_dir() {
168
+ local dir=$1
169
+ if [[ -d "$dir" ]]; then
170
+ python3 - "$dir" <<'PY'
171
+ import shutil
172
+ import sys
173
+ shutil.rmtree(sys.argv[1], ignore_errors=True)
174
+ PY
175
+ fi
176
+ }
177
+
178
+ require_cmd() {
179
+ local cmd_name=$1
180
+ command -v "$cmd_name" >/dev/null 2>&1 || abort "${cmd_name} is required"
181
+ }
182
+
183
+ while [[ "${#}" -gt 0 ]]; do
184
+ case "$1" in
185
+ --publish-npm)
186
+ NPM_PUBLISH=true
187
+ ;;
188
+ --publish-git)
189
+ GIT_PUBLISH=true
190
+ ;;
191
+ --publish-all)
192
+ NPM_PUBLISH=true
193
+ GIT_PUBLISH=true
194
+ ;;
195
+ --auto-commit)
196
+ AUTO_COMMIT=true
197
+ ;;
198
+ --create-tag)
199
+ CREATE_TAG=true
200
+ ;;
201
+ --push-git)
202
+ PUSH_GIT=true
203
+ ;;
204
+ --skip-leak-scan)
205
+ SKIP_LEAK_SCAN=true
206
+ ;;
207
+ --allow-missing-toolchain)
208
+ ALLOW_MISSING_TOOLCHAIN=true
209
+ ;;
210
+ --auto-install-targets)
211
+ AUTO_INSTALL_TARGETS=true
212
+ ;;
213
+ --skip-cargo-test)
214
+ SKIP_CARGO_TEST=true
215
+ ;;
216
+ --skip-npm-tests)
217
+ SKIP_NPM_TESTS=true
218
+ ;;
219
+ --skip-missing-targets)
220
+ ALLOW_MISSING_TARGETS=true
221
+ ;;
222
+ --skip-release-check)
223
+ SKIP_RELEASE_CHECK=true
224
+ ;;
225
+ --skip-verify-package)
226
+ SKIP_VERIFY_PACKAGE=true
227
+ ;;
228
+ --skip-git)
229
+ SKIP_GIT=true
230
+ ;;
231
+ --require-clean-worktree)
232
+ REQUIRE_CLEAN_WORKTREE=true
233
+ ;;
234
+ --check-identity)
235
+ CHECK_IDENTITY=true
236
+ ;;
237
+ --dry-run)
238
+ DRY_RUN=true
239
+ ;;
240
+ --git-remote)
241
+ shift
242
+ GIT_REMOTE=${1:-}
243
+ [[ -n "$GIT_REMOTE" ]] || abort "--git-remote requires a value"
244
+ ;;
245
+ --git-branch)
246
+ shift
247
+ GIT_BRANCH=${1:-}
248
+ [[ -n "$GIT_BRANCH" ]] || abort "--git-branch requires a value"
249
+ ;;
250
+ --commit-message)
251
+ shift
252
+ COMMIT_MESSAGE=${1:-}
253
+ [[ -n "$COMMIT_MESSAGE" ]] || abort "--commit-message requires a value"
254
+ ;;
255
+ --tag)
256
+ shift
257
+ TAG_NAME=${1:-}
258
+ [[ -n "$TAG_NAME" ]] || abort "--tag requires a value"
259
+ ;;
260
+ --targets)
261
+ shift
262
+ FILTER_TARGETS=${1:-}
263
+ [[ -n "$FILTER_TARGETS" ]] || abort "--targets requires a value"
264
+ ;;
265
+ --npm-tag)
266
+ shift
267
+ NPM_TAG=${1:-}
268
+ [[ -n "$NPM_TAG" ]] || abort "--npm-tag requires a value"
269
+ ;;
270
+ -h|--help)
271
+ usage
272
+ exit 0
273
+ ;;
274
+ *)
275
+ abort "Unknown argument: $1"
276
+ ;;
277
+ esac
278
+ shift
279
+ done
280
+
281
+ if [[ "$NPM_PUBLISH" == "true" && "$SKIP_GIT" == "true" ]]; then
282
+ log "WARN: --skip-git only disables git publish checks, not npm publish"
283
+ fi
284
+
285
+ if [[ "$GIT_PUBLISH" == "true" && "$SKIP_GIT" == "true" ]]; then
286
+ abort "--publish-git requires git publish actions; remove --skip-git."
287
+ fi
288
+
289
+ require_cmd node
290
+ require_cmd npm
291
+ require_cmd cargo
292
+
293
+ get_workspace_version() {
294
+ node -p "require('./package.json').version"
295
+ }
296
+
297
+ VERSION="$(cd "$ROOT_DIR" && get_workspace_version)"
298
+ VERSION="${VERSION:-0.2.9}"
299
+ COMMIT_MESSAGE="${COMMIT_MESSAGE:-chore(release): release Agentify CLI ${VERSION}}"
300
+ TAG_NAME="${TAG_NAME:-v${VERSION}}"
301
+
302
+ if [[ -z "$FILTER_TARGETS" ]]; then
303
+ FILTER_TARGETS="$ALL_TARGETS"
304
+ fi
305
+
306
+ declare -a TARGETS=()
307
+ declare -a RAW_TARGETS=()
308
+ declare -A SEEN_TARGETS=()
309
+ IFS=',' read -r -a RAW_TARGETS <<< "$FILTER_TARGETS"
310
+
311
+ for raw_target in "${RAW_TARGETS[@]}"; do
312
+ target="$(trim "$raw_target")"
313
+ [[ -z "$target" ]] && continue
314
+ if [[ -n "${SEEN_TARGETS[$target]+x}" ]]; then
315
+ continue
316
+ fi
317
+ SEEN_TARGETS["$target"]=1
318
+ TARGETS+=("$target")
319
+ done
320
+
321
+ if [[ "${#TARGETS[@]}" -eq 0 ]]; then
322
+ abort "No valid targets provided."
323
+ fi
324
+
325
+ validate_target() {
326
+ local target=$1
327
+ if [[ -z "${TARGET_INFO[$target]+x}" ]]; then
328
+ abort "Unsupported target: ${target}"
329
+ fi
330
+ }
331
+
332
+ for target in "${TARGETS[@]}"; do
333
+ validate_target "$target"
334
+ done
335
+
336
+ cargo_build_target() {
337
+ local rust_target=$1
338
+ if [[ "$rust_target" != *apple-darwin && -x "${HOME}/.cargo/bin/cargo-zigbuild" ]]; then
339
+ run_in_root run_cmd cargo zigbuild --locked --release --target "$rust_target"
340
+ return
341
+ fi
342
+ run_in_root run_cmd cargo build --locked --release --target "$rust_target"
343
+ }
344
+
345
+ run_build_and_stage() {
346
+ local target=$1
347
+ local info=${TARGET_INFO[$target]}
348
+ IFS=':' read -r rust_target binary_name <<< "$info"
349
+ local platform="${target%%-*}"
350
+ local arch="${target#*-}"
351
+ local source_path="${ROOT_DIR}/target/${rust_target}/release/${binary_name}"
352
+
353
+ log "Building ${target} -> ${rust_target}"
354
+
355
+ if [[ "$DRY_RUN" == "true" ]]; then
356
+ log "DRY RUN: skipping build + stage for ${target}"
357
+ return 0
358
+ fi
359
+
360
+ if ! ensure_target_toolchain "$rust_target"; then
361
+ return 1
362
+ fi
363
+
364
+ if ! cargo_build_target "$rust_target"; then
365
+ if [[ "$ALLOW_MISSING_TARGETS" == "true" ]]; then
366
+ log "WARN: failed to build ${target}; likely missing cross toolchain."
367
+ return 1
368
+ fi
369
+ return 1
370
+ fi
371
+
372
+ [[ -f "$source_path" ]] || abort "build output missing: ${source_path}"
373
+
374
+ log "Staging ${target} from ${source_path}"
375
+ (cd "$ROOT_DIR" && run_cmd npm run stage:bin -- --platform "$platform" --arch "$arch" --source "$source_path" --force)
376
+ }
377
+
378
+ parse_pack_name() {
379
+ local pack_json=$1
380
+ node -e "const input=require('fs').readFileSync(process.argv[1], 'utf8'); const data=JSON.parse(input); const first=Array.isArray(data) ? data[0] : data; process.stdout.write(first && first.filename ? first.filename : '');" "$pack_json"
381
+ }
382
+
383
+ run_leak_scan() {
384
+ if [[ "$DRY_RUN" == "true" ]]; then
385
+ log "Leak scan skipped in dry run."
386
+ return 0
387
+ fi
388
+
389
+ if [[ "$SKIP_LEAK_SCAN" == "true" ]]; then
390
+ log "Leak scan skipped."
391
+ return 0
392
+ fi
393
+
394
+ log "Running release leak scan"
395
+ local temp_dir
396
+ temp_dir="$(mktemp -d)"
397
+ local pack_json="${temp_dir}/pack.json"
398
+ local pack_name
399
+ local gitleaks_log="${temp_dir}/gitleaks.log"
400
+
401
+ (cd "$ROOT_DIR" &&
402
+ run_cmd npm pack --silent --json --pack-destination "$temp_dir" > "$pack_json")
403
+
404
+ pack_name="$(parse_pack_name "$pack_json")"
405
+ if [[ -z "$pack_name" ]]; then
406
+ cleanup_temp_dir "$temp_dir"
407
+ abort "Could not determine npm pack filename"
408
+ fi
409
+
410
+ local archive="${temp_dir}/${pack_name}"
411
+ if [[ ! -f "$archive" ]]; then
412
+ cleanup_temp_dir "$temp_dir"
413
+ abort "npm pack artifact not found: ${archive}"
414
+ fi
415
+
416
+ log "Unpacking ${archive}"
417
+ tar -xzf "$archive" -C "$temp_dir"
418
+ local package_payload="${temp_dir}/package"
419
+ if [[ ! -d "$package_payload" ]]; then
420
+ cleanup_temp_dir "$temp_dir"
421
+ abort "Expected package payload directory missing: ${package_payload}"
422
+ fi
423
+
424
+ if command -v gitleaks >/dev/null 2>&1; then
425
+ log "Running gitleaks on packed package"
426
+ local gitleaks_report="${temp_dir}/gitleaks-report.json"
427
+ set +e
428
+ gitleaks detect --no-git --redact --source "$package_payload" --report-format json --report-path "$gitleaks_report" >"$gitleaks_log" 2>&1
429
+ local gitleaks_code=$?
430
+ set -e
431
+ if [[ "$gitleaks_code" -ne 0 ]]; then
432
+ if [[ -s "$gitleaks_report" ]]; then
433
+ cleanup_temp_dir "$temp_dir"
434
+ abort "Gitleaks detected possible secrets in release payload. Report: $gitleaks_report"
435
+ fi
436
+ if rg -q "Found" "$gitleaks_log"; then
437
+ cleanup_temp_dir "$temp_dir"
438
+ abort "Gitleaks failed to complete for release payload"
439
+ fi
440
+ fi
441
+ cleanup_temp_dir "$temp_dir"
442
+ return 0
443
+ fi
444
+
445
+ log "Gitleaks unavailable; running fallback regex scan"
446
+ local pattern='(?i)-----BEGIN [A-Z ]+ PRIVATE KEY-----|(?i)AKIA[0-9A-Z]{16}|(?i)(?:ghp|gho|ghu|ghr)_[A-Za-z0-9]{36,}|(?i)github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}|(?i)BEGIN OPENSSH PRIVATE KEY|(?i)secret\\s*[:=]|(?i)api[_-]?key\\s*[:=]|(?i)password\\s*[:=]'
447
+ if rg -n --hidden --pcre2 -S "$pattern" "$package_payload"; then
448
+ cleanup_temp_dir "$temp_dir"
449
+ abort "Fallback leak scan found suspicious strings in packed package."
450
+ fi
451
+
452
+ log "Leak scan passed"
453
+ cleanup_temp_dir "$temp_dir"
454
+ }
455
+
456
+ maybe_check_npm_identity() {
457
+ local actual
458
+ actual="$(npm whoami 2>/dev/null || true)"
459
+ if [[ -z "$actual" ]]; then
460
+ abort "npm auth missing. Run npm login as ${REQUIRED_NPM_ACCOUNT}."
461
+ fi
462
+ if [[ "$actual" != "$REQUIRED_NPM_ACCOUNT" ]]; then
463
+ abort "npm identity mismatch. Expected ${REQUIRED_NPM_ACCOUNT}, got ${actual}."
464
+ fi
465
+ log "npm identity: ${actual}"
466
+ }
467
+
468
+ maybe_check_github_identity() {
469
+ if ! command -v gh >/dev/null 2>&1; then
470
+ abort "gh CLI required for GitHub identity checks."
471
+ fi
472
+ local user
473
+ user="$(gh api user --jq .login 2>/dev/null || true)"
474
+ if [[ -z "$user" ]]; then
475
+ abort "GitHub auth missing or gh not authorized."
476
+ fi
477
+ if [[ "$user" != "$REQUIRED_GH_ACCOUNT" ]]; then
478
+ abort "GitHub identity mismatch. Expected ${REQUIRED_GH_ACCOUNT}, got ${user}."
479
+ fi
480
+ log "GitHub identity: ${user}"
481
+ }
482
+
483
+ maybe_publish_npm() {
484
+ [[ "$NPM_PUBLISH" == "true" ]] || return 0
485
+
486
+ maybe_check_npm_identity
487
+ log "Publishing npm package @agentify/cli@${VERSION} (${NPM_TAG})"
488
+ (cd "$ROOT_DIR" && run_cmd npm publish --access public --tag "$NPM_TAG")
489
+ }
490
+
491
+ current_branch() {
492
+ (cd "$ROOT_DIR" && git rev-parse --abbrev-ref HEAD)
493
+ }
494
+
495
+ maybe_publish_git() {
496
+ [[ "$GIT_PUBLISH" == "true" ]] || return 0
497
+
498
+ if [[ "$REQUIRE_CLEAN_WORKTREE" == "true" ]]; then
499
+ if [[ -n "$(cd "$ROOT_DIR" && git status --porcelain)" ]]; then
500
+ abort "git publish requested with --require-clean-worktree, but workspace has changes."
501
+ fi
502
+ fi
503
+
504
+ maybe_check_github_identity
505
+ local branch
506
+ branch="$(current_branch)"
507
+ if [[ -z "$GIT_BRANCH" ]]; then
508
+ GIT_BRANCH="$branch"
509
+ fi
510
+ if [[ "$GIT_BRANCH" != "$branch" ]]; then
511
+ log "WARN: target push branch is ${GIT_BRANCH}; current branch is ${branch}"
512
+ fi
513
+
514
+ (cd "$ROOT_DIR" && git status --short)
515
+
516
+ if [[ "$AUTO_COMMIT" == "true" ]]; then
517
+ local commit_paths=(".")
518
+ local repo_root
519
+ repo_root="$(cd "$ROOT_DIR" && git rev-parse --show-toplevel)"
520
+ local wrapper_path="${repo_root}/scripts/release-agentify-cli.sh"
521
+ if [[ -f "$wrapper_path" ]]; then
522
+ commit_paths+=("$wrapper_path")
523
+ fi
524
+ (cd "$ROOT_DIR" && run_cmd git add "${commit_paths[@]}")
525
+ if (cd "$ROOT_DIR" && git diff --cached --quiet); then
526
+ log "No release artifacts changed; skipping commit."
527
+ else
528
+ (cd "$ROOT_DIR" && run_cmd git commit -m "$COMMIT_MESSAGE")
529
+ fi
530
+ fi
531
+
532
+ if [[ "$CREATE_TAG" == "true" ]]; then
533
+ if (cd "$ROOT_DIR" && git rev-parse "$TAG_NAME" >/dev/null 2>&1); then
534
+ log "Tag exists already: ${TAG_NAME}"
535
+ else
536
+ (cd "$ROOT_DIR" && run_cmd git tag -a "${TAG_NAME}" -m "${COMMIT_MESSAGE}")
537
+ fi
538
+ fi
539
+
540
+ if [[ "$PUSH_GIT" == "true" ]]; then
541
+ (cd "$ROOT_DIR" && run_cmd git push "$GIT_REMOTE" "$GIT_BRANCH")
542
+ if [[ "$CREATE_TAG" == "true" ]]; then
543
+ (cd "$ROOT_DIR" && run_cmd git push "$GIT_REMOTE" "${TAG_NAME}")
544
+ fi
545
+ fi
546
+ }
547
+
548
+ run_checks() {
549
+ if [[ "$SKIP_RELEASE_CHECK" != "true" ]]; then
550
+ log "Running release checks"
551
+ run_in_root run_cmd npm run check:versions
552
+
553
+ if [[ "$SKIP_CARGO_TEST" != "true" ]]; then
554
+ run_in_root run_cmd cargo test --locked
555
+ fi
556
+ if [[ "$SKIP_NPM_TESTS" != "true" ]]; then
557
+ run_in_root run_cmd npm run test:npm
558
+ fi
559
+ if [[ "$ALLOW_MISSING_TARGETS" == "true" ]]; then
560
+ run_in_root run_cmd npm run release:check -- --skip-cargo --allow-missing-targets
561
+ else
562
+ run_in_root run_cmd npm run release:check -- --skip-cargo
563
+ fi
564
+ fi
565
+
566
+ if [[ "$SKIP_VERIFY_PACKAGE" != "true" ]]; then
567
+ run_in_root run_cmd npm run verify:package
568
+ fi
569
+ }
570
+
571
+ if [[ "$CHECK_IDENTITY" == "true" ]]; then
572
+ maybe_check_npm_identity
573
+ maybe_check_github_identity
574
+ fi
575
+
576
+ log "Starting release pipeline"
577
+ log "Version: ${VERSION}"
578
+ log "Targets: ${TARGETS[*]}"
579
+ log "Dry run: ${DRY_RUN}"
580
+
581
+ declare -a COMPLETED_TARGETS=()
582
+ declare -a FAILED_TARGETS=()
583
+
584
+ for target in "${TARGETS[@]}"; do
585
+ if run_build_and_stage "$target"; then
586
+ COMPLETED_TARGETS+=("$target")
587
+ log_target_status "$target" "ok"
588
+ else
589
+ FAILED_TARGETS+=("$target")
590
+ log_target_status "$target" "skipped"
591
+ if [[ "$ALLOW_MISSING_TARGETS" != "true" ]]; then
592
+ abort "Build failed for requested target: ${target}"
593
+ fi
594
+ fi
595
+ done
596
+
597
+ if [[ "${#COMPLETED_TARGETS[@]}" -eq 0 ]]; then
598
+ if [[ "$DRY_RUN" == "true" ]]; then
599
+ log "No targets would be built in dry-run (check requested target list and allow-missing settings)."
600
+ else
601
+ abort "No targets were built successfully."
602
+ fi
603
+ fi
604
+
605
+ run_checks
606
+ run_leak_scan
607
+ maybe_publish_npm
608
+ maybe_publish_git
609
+
610
+ if [[ "${#FAILED_TARGETS[@]}" -gt 0 ]]; then
611
+ log "Skipped targets: ${FAILED_TARGETS[*]}"
612
+ fi
613
+
614
+ log "Release pipeline complete"