aidevops 3.13.76 → 3.13.78

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/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.13.76
8
+ # Version: 3.13.78
9
9
 
10
10
  set -euo pipefail
11
11
 
@@ -167,545 +167,15 @@ source "${INSTALL_DIR}/aidevops-init-lib.sh"
167
167
  # shellcheck source=./aidevops-skills-plugin-lib.sh
168
168
  # shellcheck disable=SC1091 # sub-library resolved at runtime via $INSTALL_DIR
169
169
  source "${INSTALL_DIR}/aidevops-skills-plugin-lib.sh"
170
-
171
- # Status helpers (extracted for complexity reduction)
172
- _status_recommended_tools() {
173
- print_header "Recommended Tools"
174
- if [[ "$(uname)" == "Darwin" ]]; then
175
- check_dir "/Applications/Tabby.app" && print_success "Tabby terminal" || print_warning "Tabby terminal - not installed"
176
- if check_dir "/Applications/Zed.app"; then
177
- print_success "Zed editor"
178
- check_dir "$HOME/Library/Application Support/Zed/extensions/installed/opencode" && print_success " └─ OpenCode extension" || print_warning " └─ OpenCode extension - not installed"
179
- else print_warning "Zed editor - not installed"; fi
180
- else
181
- check_cmd tabby && print_success "Tabby terminal" || print_warning "Tabby terminal - not installed"
182
- if check_cmd zed; then
183
- print_success "Zed editor"
184
- check_dir "$HOME/.local/share/zed/extensions/installed/opencode" && print_success " └─ OpenCode extension" || print_warning " └─ OpenCode extension - not installed"
185
- else print_warning "Zed editor - not installed"; fi
186
- fi
187
- echo ""
188
- return 0
189
- }
190
-
191
- _status_ai_tools() {
192
- print_header "AI Tools & MCPs"
193
- check_cmd opencode && print_success "OpenCode CLI" || print_warning "OpenCode CLI - not installed"
194
- if check_cmd auggie; then
195
- check_file "$HOME/.augment/session.json" && print_success "Augment Context Engine (authenticated)" || print_warning "Augment Context Engine (not authenticated)"
196
- else print_warning "Augment Context Engine - not installed"; fi
197
- check_cmd bd && print_success "Beads CLI (task graph)" || print_warning "Beads CLI (bd) - not installed"
198
- echo ""
199
- return 0
200
- }
201
-
202
- _status_dev_envs() {
203
- print_header "Development Environments"
204
- check_dir "$INSTALL_DIR/python-env/dspy-env" && print_success "DSPy Python environment" || print_warning "DSPy Python environment - not created"
205
- check_cmd dspyground && print_success "DSPyGround" || print_warning "DSPyGround - not installed"
206
- echo ""
207
- return 0
208
- }
209
-
210
- _status_ai_configs() {
211
- print_header "AI Assistant Configurations"
212
- local ai_configs=("$HOME/.config/opencode/opencode.json:OpenCode" "$HOME/.claude/commands:Claude Code CLI" "$HOME/CLAUDE.md:Claude Code memory")
213
- for config in "${ai_configs[@]}"; do
214
- local path="${config%%:*}" name="${config##*:}"
215
- [[ -e "$path" ]] && print_success "$name" || print_warning "$name - not configured"
216
- done
217
- echo ""
218
- return 0
219
- }
220
-
221
- # Status command
222
- cmd_status() {
223
- print_header "AI DevOps Framework Status"
224
- echo "=========================="
225
- echo ""
226
- local current_version
227
- current_version=$(get_version)
228
- local remote_version
229
- remote_version=$(get_remote_version)
230
- print_header "Version"
231
- echo " Installed: $current_version"
232
- echo " Latest: $remote_version"
233
- if [[ "$current_version" != "$remote_version" && "$remote_version" != "unknown" ]]; then
234
- print_warning "Update available! Run: aidevops update"
235
- elif [[ "$current_version" == "$remote_version" ]]; then print_success "Up to date"; fi
236
- echo ""
237
- print_header "Installation"
238
- check_dir "$INSTALL_DIR" && print_success "Repository: $INSTALL_DIR" || print_error "Repository: Not found at $INSTALL_DIR"
239
- if check_dir "$AGENTS_DIR"; then
240
- local agent_count
241
- agent_count=$(find "$AGENTS_DIR" -name "*.md" -type f 2>/dev/null | wc -l | tr -d ' ')
242
- print_success "Agents: $AGENTS_DIR ($agent_count files)"
243
- else print_error "Agents: Not deployed"; fi
244
- echo ""
245
- print_header "Required Dependencies"
246
- for cmd in git curl jq ssh; do check_cmd "$cmd" && print_success "$cmd" || print_error "$cmd - not installed"; done
247
- echo ""
248
- print_header "Optional Dependencies"
249
- check_cmd sshpass && print_success "sshpass" || print_warning "sshpass - not installed (needed for password SSH)"
250
- echo ""
251
- _status_recommended_tools
252
- print_header "Git CLI Tools"
253
- check_cmd gh && print_success "GitHub CLI (gh)" || print_warning "GitHub CLI (gh) - not installed"
254
- check_cmd glab && print_success "GitLab CLI (glab)" || print_warning "GitLab CLI (glab) - not installed"
255
- check_cmd tea && print_success "Gitea CLI (tea)" || print_warning "Gitea CLI (tea) - not installed"
256
- echo ""
257
- _status_ai_tools
258
- _status_dev_envs
259
- _status_ai_configs
260
- print_header "SSH Configuration"
261
- check_file "$HOME/.ssh/id_ed25519" && print_success "Ed25519 SSH key" || print_warning "Ed25519 SSH key - not found"
262
- echo ""
263
- print_header "Commit Signing"
264
- local signing_format signing_key signing_enabled
265
- signing_format=$(git config --global gpg.format 2>/dev/null || echo "")
266
- signing_key=$(git config --global user.signingkey 2>/dev/null || echo "")
267
- signing_enabled=$(git config --global commit.gpgsign 2>/dev/null || echo "")
268
- if [[ "$signing_format" == "ssh" && -n "$signing_key" && "$signing_enabled" == "true" ]]; then
269
- print_success "SSH commit signing enabled"
270
- if check_file "$HOME/.ssh/allowed_signers"; then
271
- print_success "Allowed signers file configured"
272
- else
273
- print_warning "No allowed_signers file — run: aidevops signing setup"
274
- fi
275
- else
276
- print_warning "Commit signing not configured — run: aidevops signing setup"
277
- fi
278
- echo ""
279
- # t2424/GH#20030: Pulse operational counters (pre-dispatch aborts, etc.)
280
- local stats_helper="$AGENTS_DIR/scripts/pulse-stats-helper.sh"
281
- if [[ -x "$stats_helper" ]]; then
282
- print_header "Pulse Stats"
283
- "$stats_helper" status 2>/dev/null || print_info " (no stats recorded yet)"
284
- echo ""
285
- fi
286
- }
287
-
288
- # Update helpers (extracted for complexity reduction)
289
-
290
- _update_fresh_install() {
291
- print_warning "Repository not found, performing fresh install..."
292
- local tmp_setup
293
- # t2997: drop .sh — XXXXXX must be at end for BSD mktemp.
294
- tmp_setup=$(mktemp "${TMPDIR:-/tmp}/aidevops-setup-XXXXXX") || {
295
- print_error "Failed to create temp file for setup script"
296
- return 1
297
- }
298
- trap 'rm -f "${tmp_setup:-}"' RETURN
299
- if curl -fsSL "https://raw.githubusercontent.com/marcusquinn/aidevops/main/setup.sh" -o "$tmp_setup" 2>/dev/null && [[ -s "$tmp_setup" ]]; then
300
- chmod +x "$tmp_setup"
301
- bash "$tmp_setup"
302
- local setup_exit=$?
303
- rm -f "$tmp_setup"
304
- [[ $setup_exit -ne 0 ]] && return 1
305
- else
306
- rm -f "$tmp_setup"
307
- print_error "Failed to download setup script"
308
- print_info "Try: git clone https://github.com/marcusquinn/aidevops.git $INSTALL_DIR && bash $INSTALL_DIR/setup.sh"
309
- return 1
310
- fi
311
- return 0
312
- }
313
-
314
- _update_sync_projects() {
315
- local skip="$1" current_ver="$2"
316
- echo ""
317
- print_header "Syncing Initialized Projects"
318
- if [[ "$skip" == "true" ]]; then
319
- print_info "Project sync skipped (--skip-project-sync)"
320
- return 0
321
- fi
322
- local repos_needing_upgrade=()
323
- while IFS= read -r repo_path; do
324
- [[ -z "$repo_path" ]] && continue
325
- [[ -d "$repo_path" ]] && check_repo_needs_upgrade "$repo_path" && repos_needing_upgrade+=("$repo_path")
326
- done < <(get_registered_repos)
327
- if [[ ${#repos_needing_upgrade[@]} -eq 0 ]]; then
328
- print_success "All registered projects are up to date"
329
- return 0
330
- fi
331
- local synced=0 skipped=0 failed=0
332
- for repo in "${repos_needing_upgrade[@]}"; do
333
- [[ ! -f "$repo/.aidevops.json" ]] && {
334
- skipped=$((skipped + 1))
335
- continue
336
- }
337
- local did_sync=false
338
- if command -v jq &>/dev/null; then
339
- local temp_file="${repo}/.aidevops.json.tmp"
340
- if jq --arg version "$current_ver" '.version = $version' "$repo/.aidevops.json" >"$temp_file" 2>/dev/null && [[ -s "$temp_file" ]]; then
341
- mv "$temp_file" "$repo/.aidevops.json"
342
- local features
343
- features=$(jq -r '[.features | to_entries[] | select(.value == true) | .key] | join(",")' "$repo/.aidevops.json" 2>/dev/null || echo "")
344
- register_repo "$repo" "$current_ver" "$features"
345
- did_sync=true
346
- else rm -f "$temp_file"; fi
347
- fi
348
- if [[ "$did_sync" != "true" ]]; then
349
- sed -i '' "s/\"version\": *\"[^\"]*\"/\"version\": \"$current_ver\"/" "$repo/.aidevops.json" 2>/dev/null && did_sync=true
350
- fi
351
- [[ "$did_sync" == "true" ]] && synced=$((synced + 1)) || failed=$((failed + 1))
352
- done
353
- [[ $synced -gt 0 ]] && print_success "Synced $synced project(s) to v$current_ver"
354
- [[ $skipped -gt 0 ]] && print_info "Skipped $skipped uninitialized project(s) (run 'aidevops init' in each to enable)"
355
- [[ $failed -gt 0 ]] && print_warning "$failed project(s) failed to sync (jq missing or write error)"
356
- return 0
357
- }
358
-
359
- _update_check_planning() {
360
- echo ""
361
- print_header "Checking Planning Templates"
362
- local repos_needing_planning=()
363
- while IFS= read -r repo_path; do
364
- [[ -z "$repo_path" || ! -d "$repo_path" ]] && continue
365
- if [[ -f "$repo_path/.aidevops.json" ]]; then
366
- local has_planning
367
- has_planning=$(grep -o '"planning": *true' "$repo_path/.aidevops.json" 2>/dev/null || true)
368
- [[ -n "$has_planning" ]] && check_planning_needs_upgrade "$repo_path" && repos_needing_planning+=("$repo_path")
369
- fi
370
- done < <(get_registered_repos)
371
- if [[ ${#repos_needing_planning[@]} -eq 0 ]]; then
372
- print_success "All planning templates are up to date"
373
- return 0
374
- fi
375
- echo ""
376
- print_warning "${#repos_needing_planning[@]} project(s) have outdated planning templates:"
377
- for repo in "${repos_needing_planning[@]}"; do
378
- local repo_name
379
- repo_name=$(basename "$repo")
380
- local todo_ver
381
- todo_ver=$(grep -A1 "TOON:meta" "$repo/TODO.md" 2>/dev/null | tail -1 | cut -d',' -f1)
382
- echo " - $repo_name (v${todo_ver:-none})"
383
- done
384
- local template_ver
385
- template_ver=$(grep -A1 "TOON:meta" "$AGENTS_DIR/templates/todo-template.md" 2>/dev/null | tail -1 | cut -d',' -f1)
386
- echo ""
387
- echo " Latest template: v${template_ver} (adds risk field, active session time estimates)"
388
- echo ""
389
- read -r -p "Upgrade planning templates in these projects? [y/N] " response
390
- if [[ "$response" =~ ^[Yy]$ ]]; then
391
- for repo in "${repos_needing_planning[@]}"; do
392
- print_info "Upgrading $(basename "$repo")..."
393
- (cd "$repo" && cmd_upgrade_planning --force) || print_warning "Failed to upgrade $(basename "$repo")"
394
- done
395
- else print_info "Run 'aidevops upgrade-planning' in each project to upgrade manually"; fi
396
- return 0
397
- }
398
-
399
- _update_check_tools() {
400
- echo ""
401
- print_header "Checking Key Tools"
402
- local tool_check_script="$AGENTS_DIR/scripts/tool-version-check.sh"
403
- if [[ ! -f "$tool_check_script" ]]; then
404
- print_info "Tool version check not available (run setup first)"
405
- return 0
406
- fi
407
- local stale_count=0 stale_tools=""
408
- local key_tool_cmds="opencode gh"
409
- local key_tool_pkgs="opencode-ai brew:gh"
410
- local idx=0
411
- for cmd_name in $key_tool_cmds; do
412
- local pkg_ref
413
- pkg_ref=$(echo "$key_tool_pkgs" | cut -d' ' -f$((idx + 1)))
414
- idx=$((idx + 1))
415
- local installed="" latest=""
416
- command -v "$cmd_name" &>/dev/null || continue
417
- installed=$("$cmd_name" --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
418
- [[ -z "$installed" ]] && continue
419
- if [[ "$pkg_ref" == brew:* ]]; then
420
- local brew_pkg="${pkg_ref#brew:}"
421
- local brew_bin=""
422
- brew_bin=$(command -v brew 2>/dev/null || true)
423
- if [[ -n "$brew_bin" && -x "$brew_bin" ]]; then
424
- latest=$(_timeout_cmd 30 "$brew_bin" info --json=v2 "$brew_pkg" | jq -r '.formulae[0].versions.stable // empty' || true)
425
- elif [[ "$brew_pkg" == "gh" ]] && command -v gh &>/dev/null; then latest=$(get_public_release_tag "cli/cli"); fi
426
- else latest=$(_timeout_cmd 30 npm view "$pkg_ref" version || true); fi
427
- [[ -z "$latest" ]] && continue
428
- [[ "$installed" != "$latest" ]] && {
429
- stale_tools="${stale_tools:+$stale_tools, }$cmd_name ($installed -> $latest)"
430
- ((++stale_count))
431
- }
432
- done
433
- if [[ "$stale_count" -eq 0 ]]; then
434
- print_success "Key tools are up to date"
435
- else
436
- print_warning "$stale_count tool(s) have updates: $stale_tools"
437
- echo ""
438
- read -r -p "Run full tool update check? [y/N] " response
439
- [[ "$response" =~ ^[Yy]$ ]] && bash "$tool_check_script" --update || print_info "Run 'aidevops update-tools --update' to update later"
440
- fi
441
- return 0
442
- }
443
-
444
- # Check for stale Homebrew-installed copy after git update (GH#11470)
445
- # Self-heal broken OpenCode runtime symlinks (t2172). A single dangling
446
- # symlink in ~/.config/opencode/{command,agent,skills,tool}/ blocks new
447
- # OpenCode sessions with "Failed to parse command ...". Running on every
448
- # update is cheap (find+rm on 4 small dirs) and catches orphans left
449
- # behind when users delete private agent source clones without going
450
- # through `agent-sources-helper.sh remove`. Fail-open — must never
451
- # break the update cron.
452
- _update_sweep_opencode_symlinks() {
453
- local sym_helper="${HOME}/.aidevops/agents/scripts/agent-sources-helper.sh"
454
- [[ -x "$sym_helper" ]] || return 0
455
- "$sym_helper" cleanup-broken-symlinks >/dev/null 2>&1 || true
456
- return 0
457
- }
458
-
459
- _update_check_homebrew() {
460
- command -v brew &>/dev/null || return 0
461
- brew list aidevops &>/dev/null 2>&1 || return 0
462
- local brew_version=""
463
- brew_version=$(brew info aidevops --json=v2 2>/dev/null | jq -r '.formulae[0].installed[0].version // empty' 2>/dev/null || true)
464
- [[ -z "$brew_version" ]] && return 0
465
- local current_version
466
- current_version=$(get_version)
467
- [[ -z "$current_version" ]] && return 0
468
- if [[ "$brew_version" != "$current_version" ]]; then
469
- echo ""
470
- print_warning "Homebrew-installed copy is outdated ($brew_version vs $current_version)"
471
- print_info "The Homebrew wrapper should prefer your git copy, but if your PATH"
472
- print_info "resolves the Homebrew libexec copy directly, you'll run the old version."
473
- echo ""
474
- read -r -p "Run 'brew upgrade aidevops' now? [y/N] " response
475
- if [[ "$response" =~ ^[Yy]$ ]]; then
476
- brew upgrade aidevops 2>&1 || print_warning "brew upgrade failed — run manually: brew upgrade aidevops"
477
- else
478
- print_info "Run 'brew upgrade aidevops' to sync the Homebrew copy"
479
- fi
480
- fi
481
- return 0
482
- }
483
-
484
- # t2926 / GH#21102: Re-check setsid on every 'aidevops update' run.
485
- # setsid (from util-linux) is required to detach pulse workers into their own
486
- # process group — without it, every pulse restart sends SIGHUP to its PGID,
487
- # killing in-flight workers. This check runs even when setup.sh is skipped
488
- # (already up-to-date path), so Homebrew drift doesn't silently break workers.
489
- _update_check_setsid() {
490
- command -v setsid >/dev/null 2>&1 && return 0
491
-
492
- # setsid is missing. On macOS with Homebrew, auto-install util-linux.
493
- # Use a boolean flag to avoid repeating the OS literal string.
494
- local _on_mac=false
495
- [[ "$(uname -s)" == Darwin* ]] && _on_mac=true
496
- if $_on_mac && command -v brew >/dev/null 2>&1; then
497
- print_info "setsid not found — installing util-linux for worker PGID isolation (GH#21102)"
498
- if brew install util-linux 2>&1 | tail -3; then
499
- local brew_prefix=""
500
- brew_prefix="$(brew --prefix 2>/dev/null || true)"
501
- local keg_setsid="${brew_prefix}/opt/util-linux/bin/setsid"
502
- local link_target="${brew_prefix}/bin/setsid"
503
- if [[ -x "$keg_setsid" && ! -e "$link_target" ]]; then
504
- ln -s "$keg_setsid" "$link_target" && \
505
- print_success "Symlinked setsid: $keg_setsid → $link_target"
506
- fi
507
- if command -v setsid >/dev/null 2>&1; then
508
- print_success "setsid installed at $(command -v setsid) (worker PGID isolation enabled)"
509
- else
510
- print_error "util-linux installed but setsid still not in PATH — check brew --prefix"
511
- fi
512
- else
513
- print_error "brew install util-linux failed — workers will share pulse PGID until resolved"
514
- fi
515
- elif $_on_mac; then
516
- print_error "setsid not found — worker isolation broken; install Homebrew then run: brew install util-linux"
517
- else
518
- print_error "setsid not found — worker isolation broken; install util-linux via your distro package manager"
519
- fi
520
-
521
- return 0
522
- }
523
-
524
- # GH#21735: Notify operator when framework workflow templates change.
525
- # When .agents/templates/workflows/*.yml or *-reusable.yml workflows change
526
- # in a framework update, downstream repos that use these as workflow_call
527
- # callers may have drifted from the new template. Detection and remediation
528
- # both already exist (`aidevops check-workflows`, `aidevops sync-workflows
529
- # --apply`); the gap was the notification surface — operators only learned
530
- # of drift when downstream CI failed (canonical incident: a managed
531
- # downstream repo's issue-sync.yml failed silently after the upstream
532
- # template added a new input).
533
- #
534
- # This check inspects the SHA-window diff for changes to workflow caller
535
- # templates and reusable workflows, prints a warning, and emits a daily
536
- # advisory so the next session greeting surfaces it if the operator
537
- # misses the inline output.
538
- #
539
- # Args: $1=old_sha, $2=new_sha
540
- # Returns: 0 (always — informational only, never breaks update)
541
- _update_check_workflow_drift() {
542
- local old_sha="$1"
543
- local new_sha="$2"
544
- [[ -z "$old_sha" || -z "$new_sha" || "$old_sha" == "$new_sha" ]] && return 0
545
- # `.git` is a directory in a regular repo and a file in a worktree;
546
- # `-e` covers both so the helper is testable from a worktree.
547
- [[ ! -e "$INSTALL_DIR/.git" ]] && return 0
548
-
549
- # Files that propagate to downstream caller workflows OR are themselves
550
- # reusable workflow definitions referenced by downstream callers.
551
- # Internal .github/workflows/*.yml hotfixes (e.g. self-test runs) are
552
- # intentionally skipped to avoid false-positive nags.
553
- local relevant_files
554
- relevant_files=$(git -C "$INSTALL_DIR" diff --name-only "$old_sha" "$new_sha" -- \
555
- '.agents/templates/workflows/' \
556
- '.github/workflows/' \
557
- 2>/dev/null \
558
- | grep -E '(\.agents/templates/workflows/.*\.ya?ml$|\.github/workflows/.*-reusable\.ya?ml$)' \
559
- || true)
560
- [[ -z "$relevant_files" ]] && return 0
561
-
562
- local file_count
563
- file_count=$(printf '%s\n' "$relevant_files" | wc -l | tr -d ' ')
564
- echo ""
565
- print_warning "Workflow templates updated ($file_count file(s)) — downstream callers may have drifted."
566
- print_info " Detect drift: aidevops check-workflows"
567
- print_info " Apply fix: aidevops sync-workflows --apply [--repo OWNER/REPO]"
568
-
569
- # Persist as advisory so the next session greeting surfaces it even if
570
- # the operator misses the inline warning. Day-stamped ID makes repeated
571
- # updates within the same day idempotent (one advisory per day);
572
- # 'aidevops security dismiss <id>' silences a specific day's advisory.
573
- _update_emit_workflow_drift_advisory "$relevant_files" || true
574
- return 0
575
- }
576
-
577
- # Companion to _update_check_workflow_drift — separated for testability.
578
- # Args: $1=relevant_files (newline-separated)
579
- # Returns: 0 (always — fail-open; advisory write must never break update)
580
- _update_emit_workflow_drift_advisory() {
581
- local relevant_files="$1"
582
- local advisories_dir="${HOME}/.aidevops/advisories"
583
- local adv_id
584
- adv_id="workflow-drift-$(date +%Y%m%d)"
585
- local dismissed_file="$advisories_dir/dismissed.txt"
586
-
587
- # Skip if today's advisory was already dismissed.
588
- if [[ -f "$dismissed_file" ]] && grep -qxF "$adv_id" "$dismissed_file" 2>/dev/null; then
589
- return 0
590
- fi
591
-
592
- mkdir -p "$advisories_dir" 2>/dev/null || return 0
593
- local adv_file="$advisories_dir/${adv_id}.advisory"
594
-
595
- {
596
- printf 'Workflow templates changed — downstream caller workflows may have drifted.\n'
597
- printf '\n'
598
- printf 'Files changed in this update:\n'
599
- printf '%s\n' "$relevant_files" | sed 's|^| |'
600
- printf '\n'
601
- printf 'Detect drift: aidevops check-workflows\n'
602
- printf 'Apply fix: aidevops sync-workflows --apply [--repo OWNER/REPO]\n'
603
- printf 'Background: reference/reusable-workflows.md\n'
604
- } >"$adv_file" 2>/dev/null || return 0
605
- return 0
606
- }
607
-
608
- # Verify supply chain signature after pulling framework updates.
609
- # Checks that the HEAD commit is signed by the trusted maintainer key.
610
- # Non-blocking: warns on failure, does not abort the update.
611
- _update_verify_signature() {
612
- local signing_helper="$AGENTS_DIR/scripts/signing-setup.sh"
613
-
614
- # Cannot verify if the helper script is not yet deployed
615
- if [[ ! -f "$signing_helper" ]]; then
616
- return 0
617
- fi
618
-
619
- local result
620
- result=$(bash "$signing_helper" verify-update "$INSTALL_DIR" 2>/dev/null || echo "UNKNOWN")
621
-
622
- case "$result" in
623
- VERIFIED)
624
- print_success "Supply chain verified: HEAD commit is signed by trusted maintainer"
625
- ;;
626
- UNSIGNED)
627
- print_warning "HEAD commit is not signed — cannot verify supply chain integrity"
628
- print_info "This is expected for older releases. Signed commits start from v3.6.21+"
629
- ;;
630
- UNTRUSTED)
631
- print_warning "HEAD commit is signed but by an untrusted key"
632
- print_info "Run 'aidevops signing setup' to configure signature verification"
633
- ;;
634
- BAD_SIGNATURE)
635
- print_error "HEAD commit has a BAD signature — update may be compromised"
636
- print_info "Verify manually: cd $INSTALL_DIR && git log --show-signature -1"
637
- ;;
638
- UNVERIFIABLE)
639
- # Signing not configured yet — silent, do not nag
640
- ;;
641
- esac
642
- return 0
643
- }
644
-
645
- # One-shot, idempotent migration of supervisor.* → orchestration.* in settings.json (t2946).
646
- # Safe: reads value from supervisor.* only when orchestration.* key is absent.
647
- # Logs to ~/.aidevops/logs/settings-migration.log.
648
- _migrate_settings_supervisor_to_orchestration() {
649
- local _settings_file="${HOME}/.config/aidevops/settings.json"
650
- local _log_file="${HOME}/.aidevops/logs/settings-migration.log"
651
-
652
- if ! command -v jq >/dev/null 2>&1; then
653
- return 0
654
- fi
655
- if [[ ! -f "$_settings_file" ]]; then
656
- return 0
657
- fi
658
- if ! jq . "$_settings_file" >/dev/null 2>&1; then
659
- return 0
660
- fi
661
-
662
- # Check if supervisor.pulse_interval_seconds exists and orchestration.pulse_interval_seconds is absent.
663
- local _has_sv _has_orch
664
- _has_sv=$(jq -r 'if .supervisor.pulse_interval_seconds != null then "yes" else "no" end' "$_settings_file" 2>/dev/null)
665
- _has_orch=$(jq -r 'if .orchestration.pulse_interval_seconds != null then "yes" else "no" end' "$_settings_file" 2>/dev/null)
666
-
667
- if [[ "$_has_sv" != "yes" ]]; then
668
- return 0
669
- fi
670
-
671
- local _ts
672
- _ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)
673
- mkdir -p "$(dirname "$_log_file")" 2>/dev/null || true
674
-
675
- local _tmp
676
- _tmp=$(mktemp 2>/dev/null) || return 0
677
-
678
- if [[ "$_has_orch" == "no" ]]; then
679
- # Migrate: copy supervisor.pulse_interval_seconds to orchestration.pulse_interval_seconds,
680
- # then remove supervisor.pulse_interval_seconds.
681
- local _sv_val
682
- _sv_val=$(jq -r '.supervisor.pulse_interval_seconds' "$_settings_file" 2>/dev/null)
683
- if jq --argjson v "$_sv_val" \
684
- '(.orchestration.pulse_interval_seconds) = $v | del(.supervisor.pulse_interval_seconds)' \
685
- "$_settings_file" >"$_tmp" 2>/dev/null && [[ -s "$_tmp" ]]; then
686
- mv "$_tmp" "$_settings_file"
687
- printf '[%s] migrated supervisor.pulse_interval_seconds=%s → orchestration.pulse_interval_seconds\n' \
688
- "$_ts" "$_sv_val" >>"$_log_file" 2>/dev/null || true
689
- print_info "Settings migrated: supervisor.pulse_interval_seconds → orchestration.pulse_interval_seconds ($_sv_val)"
690
- else
691
- rm -f "$_tmp"
692
- fi
693
- else
694
- # Both present: orchestration wins, remove the stale supervisor key.
695
- local _orch_val
696
- _orch_val=$(jq -r '.orchestration.pulse_interval_seconds' "$_settings_file" 2>/dev/null)
697
- if jq 'del(.supervisor.pulse_interval_seconds)' \
698
- "$_settings_file" >"$_tmp" 2>/dev/null && [[ -s "$_tmp" ]]; then
699
- mv "$_tmp" "$_settings_file"
700
- printf '[%s] removed stale supervisor.pulse_interval_seconds (orchestration.pulse_interval_seconds=%s wins)\n' \
701
- "$_ts" "$_orch_val" >>"$_log_file" 2>/dev/null || true
702
- print_info "Settings cleaned: removed stale supervisor.pulse_interval_seconds (orchestration value $_orch_val kept)"
703
- else
704
- rm -f "$_tmp"
705
- fi
706
- fi
707
- return 0
708
- }
170
+ # shellcheck source=./aidevops-status-lib.sh
171
+ # shellcheck disable=SC1091 # sub-library resolved at runtime via $INSTALL_DIR
172
+ source "${INSTALL_DIR}/aidevops-status-lib.sh"
173
+ # shellcheck source=./aidevops-update-lib.sh
174
+ # shellcheck disable=SC1091 # sub-library resolved at runtime via $INSTALL_DIR
175
+ source "${INSTALL_DIR}/aidevops-update-lib.sh"
176
+ # shellcheck source=./aidevops-upgrade-planning-lib.sh
177
+ # shellcheck disable=SC1091 # sub-library resolved at runtime via $INSTALL_DIR
178
+ source "${INSTALL_DIR}/aidevops-upgrade-planning-lib.sh"
709
179
 
710
180
  # Update/upgrade command
711
181
  cmd_update() {
@@ -885,41 +355,6 @@ cmd_update() {
885
355
  # Side effect: writes ~/.aidevops/advisories/daemon-disabled.advisory when
886
356
  # the daemon is unhealthy so the session greeting surfaces the warning.
887
357
  # Cleared (file removed) when the daemon recovers.
888
- _update_check_daemon_health() {
889
- local helper="$HOME/.aidevops/agents/scripts/auto-update-helper.sh"
890
- [[ -x "$helper" ]] || return 0
891
- local advisory_dir="$HOME/.aidevops/advisories"
892
- local advisory_file="$advisory_dir/daemon-disabled.advisory"
893
-
894
- local hc_rc=0
895
- "$helper" health-check --quiet >/dev/null 2>&1 || hc_rc=$?
896
-
897
- if [[ "$hc_rc" -eq 0 ]]; then
898
- # Healthy — clear any stale advisory.
899
- [[ -f "$advisory_file" ]] && rm -f "$advisory_file"
900
- return 0
901
- fi
902
-
903
- # Unhealthy — warn on stderr and write advisory.
904
- mkdir -p "$advisory_dir" 2>/dev/null || return 0
905
- local fix_cmd="aidevops auto-update enable"
906
- [[ "$hc_rc" -eq 1 ]] && fix_cmd="aidevops auto-update check"
907
- cat >"$advisory_file" <<EOF
908
- auto-update daemon is not running normally on this runner. Without it, this
909
- runner falls behind the fleet and may dispatch workers that fail because of
910
- bugs already fixed upstream. See cross-runner-coordination.md §4.4.
911
-
912
- Diagnose: aidevops auto-update health-check
913
- Fix: ${fix_cmd}
914
- EOF
915
-
916
- if [[ "$hc_rc" -eq 1 ]]; then
917
- print_warning "Auto-update daemon is stalled. Fix: ${fix_cmd}"
918
- else
919
- print_warning "Auto-update daemon is not running. Fix: ${fix_cmd}"
920
- fi
921
- return 0
922
- }
923
358
  # Uninstall helpers (extracted for complexity reduction)
924
359
  _uninstall_cleanup_refs() {
925
360
  print_info "Removing AI assistant configuration references..."
@@ -998,346 +433,6 @@ cmd_uninstall() {
998
433
  }
999
434
 
1000
435
 
1001
- # Upgrade planning helpers (extracted for complexity reduction)
1002
-
1003
- _upgrade_validate() {
1004
- local project_root="$1"
1005
- [[ ! -f "$project_root/.aidevops.json" ]] && {
1006
- print_error "aidevops not initialized in this project"
1007
- print_info "Run 'aidevops init' first"
1008
- return 1
1009
- }
1010
- if command -v jq &>/dev/null; then
1011
- jq -e '.features.planning == true' "$project_root/.aidevops.json" &>/dev/null || {
1012
- print_error "Planning feature not enabled"
1013
- print_info "Run 'aidevops init planning' to enable"
1014
- return 1
1015
- }
1016
- else
1017
- local pe
1018
- pe=$(grep -o '"planning": *true' "$project_root/.aidevops.json" 2>/dev/null || echo "")
1019
- [[ -z "$pe" ]] && {
1020
- print_error "Planning feature not enabled"
1021
- print_info "Run 'aidevops init planning' to enable"
1022
- return 1
1023
- }
1024
- fi
1025
- [[ ! -f "$AGENTS_DIR/templates/todo-template.md" ]] && {
1026
- print_error "TODO template not found: $AGENTS_DIR/templates/todo-template.md"
1027
- return 1
1028
- }
1029
- [[ ! -f "$AGENTS_DIR/templates/plans-template.md" ]] && {
1030
- print_error "PLANS template not found: $AGENTS_DIR/templates/plans-template.md"
1031
- return 1
1032
- }
1033
- return 0
1034
- }
1035
-
1036
- _upgrade_check_version() {
1037
- local file="$1" template="$2" label="$3"
1038
- if check_planning_file_version "$file" "$template"; then
1039
- if [[ -f "$file" ]]; then
1040
- if ! grep -q "TOON:meta" "$file" 2>/dev/null; then
1041
- print_warning "$label uses minimal template (missing TOON markers)"
1042
- else
1043
- local cv tv
1044
- cv=$(grep -A1 "TOON:meta" "$file" 2>/dev/null | tail -1 | cut -d',' -f1)
1045
- tv=$(grep -A1 "TOON:meta" "$template" 2>/dev/null | tail -1 | cut -d',' -f1)
1046
- print_warning "$label format version $cv -> $tv"
1047
- fi
1048
- else print_info "$label not found - will create from template"; fi
1049
- return 0
1050
- else
1051
- local cv
1052
- cv=$(grep -A1 "TOON:meta" "$file" 2>/dev/null | tail -1 | cut -d',' -f1)
1053
- print_success "$label already up to date (v${cv})"
1054
- return 1
1055
- fi
1056
- }
1057
-
1058
- # t2434: Extract lines under "## <section>" until the next "## " header.
1059
- # Skips ## Format block entirely (its content is documentation, not tasks).
1060
- # Skips fenced code blocks.
1061
- # Exact-match on the section header — no regex escaping concerns.
1062
- _extract_todo_section() {
1063
- local file="$1" section="$2"
1064
- awk -v target="## $section" '
1065
- /^## Format/ { in_format=1; next }
1066
- in_format && /^## / { in_format=0 }
1067
- in_format { next }
1068
- /^```/ { in_codeblock = !in_codeblock; next }
1069
- in_codeblock { next }
1070
- $0 == target { found=1; next }
1071
- found && /^## / { exit }
1072
- found
1073
- ' "$file" 2>/dev/null || echo ""
1074
- }
1075
-
1076
- # t2434: Filter stdin, removing only the literal Format-block placeholder IDs
1077
- # (tXXX, tYYY, tZZZ). Real-world repos have historic IDs that don't follow the
1078
- # strict t<digits> shape (e.g. "t059b", "t043-merge" from webapp) — we must
1079
- # preserve those. A blocklist is safer than an allowlist here: extraction
1080
- # already skips the Format section, so the filter is a secondary guard rather
1081
- # than primary validation.
1082
- _filter_todo_placeholders() {
1083
- awk '
1084
- !/^- \[[ x-]\] t/ { print; next }
1085
- {
1086
- id = $0
1087
- sub(/^- \[[ x-]\] /, "", id)
1088
- sub(/ .*/, "", id)
1089
- if (id == "tXXX" || id == "tYYY" || id == "tZZZ") next
1090
- print
1091
- }
1092
- '
1093
- }
1094
-
1095
- # t2434: Insert content_file into target_file immediately after the closing
1096
- # "-->" of the named TOON marker block (<!--TOON:<tag>...-->).
1097
- # Idempotent only in the sense that each call inserts once per marker; repeated
1098
- # calls would stack insertions. Intended to be called once per tag per upgrade.
1099
- _insert_after_toon_marker() {
1100
- local target_file="$1" toon_tag="$2" content_file="$3"
1101
- local temp_file="${target_file}.insert"
1102
- local marker_open="<!--TOON:${toon_tag}"
1103
- local in_marker=false
1104
- while IFS= read -r line || [[ -n "$line" ]]; do
1105
- [[ "$line" == *"$marker_open"* ]] && in_marker=true
1106
- if [[ "$in_marker" == true && "$line" == "-->" ]]; then
1107
- echo "$line"
1108
- echo ""
1109
- cat "$content_file"
1110
- in_marker=false
1111
- continue
1112
- fi
1113
- echo "$line"
1114
- done <"$target_file" >"$temp_file"
1115
- mv "$temp_file" "$target_file"
1116
- }
1117
-
1118
- # t2434: Preserve each of the 6 task sections into $workdir/<tag>.txt for
1119
- # later re-insertion after the template is applied. Placeholder filter runs
1120
- # per section so Format-block tXXX-style examples never reach the new file.
1121
- _upgrade_todo_preserve_sections() {
1122
- local todo_file="$1" workdir="$2"
1123
- local sections=("Ready" "Backlog" "In Progress" "In Review" "Done" "Declined")
1124
- local tags=("ready" "backlog" "in_progress" "in_review" "done" "declined")
1125
- local i=0
1126
- while [[ $i -lt ${#sections[@]} ]]; do
1127
- local section="${sections[$i]}" tag="${tags[$i]}"
1128
- local content
1129
- content=$(_extract_todo_section "$todo_file" "$section")
1130
- if [[ -n "$content" ]]; then
1131
- content=$(printf '%s\n' "$content" | _filter_todo_placeholders)
1132
- [[ -n "$content" ]] && printf '%s\n' "$content" >"$workdir/${tag}.txt"
1133
- fi
1134
- i=$((i + 1))
1135
- done
1136
- return 0
1137
- }
1138
-
1139
- # t2434: Re-insert preserved section content after its matching TOON marker
1140
- # in the freshly-applied new template. Caller is responsible for counting
1141
- # merged tasks from the final file — keeping count out of the hot loop avoids
1142
- # subshell/arithmetic edge cases under `set -u` when content contains `GH#`-
1143
- # style IDs that don't match a naive `t[0-9]` count pattern.
1144
- _upgrade_todo_reinsert_sections() {
1145
- local todo_file="$1" workdir="$2"
1146
- local tags=("ready" "backlog" "in_progress" "in_review" "done" "declined")
1147
- local tag content_file
1148
- for tag in "${tags[@]}"; do
1149
- content_file="$workdir/${tag}.txt"
1150
- [[ -f "$content_file" && -s "$content_file" ]] || continue
1151
- grep -q "<!--TOON:${tag}" "$todo_file" || continue
1152
- _insert_after_toon_marker "$todo_file" "$tag" "$content_file"
1153
- done
1154
- return 0
1155
- }
1156
-
1157
- # t2434: Upgrade TODO.md to the latest TOON-enhanced template, preserving
1158
- # tasks from all 6 sections (Ready, Backlog, In Progress, In Review, Done,
1159
- # Declined). Prior behaviour (GH#20077) only preserved Backlog and silently
1160
- # dropped the other 5 sections into TODO.md.bak, losing audit-trail data.
1161
- _upgrade_todo() {
1162
- local todo_file="$1" todo_template="$2" backup="$3"
1163
- print_info "Upgrading TODO.md..."
1164
- local workdir=""
1165
- workdir=$(mktemp -d)
1166
- # shellcheck disable=SC2064 # intentional $workdir expansion at trap-set time
1167
- trap "rm -rf \"${workdir}\"" RETURN
1168
- if [[ -f "$todo_file" ]]; then
1169
- _upgrade_todo_preserve_sections "$todo_file" "$workdir"
1170
- [[ "$backup" == "true" ]] && {
1171
- cp "$todo_file" "${todo_file}.bak"
1172
- print_success "Backup created: TODO.md.bak"
1173
- }
1174
- fi
1175
- local temp_todo="${todo_file}.new"
1176
- if awk '/^---$/ && !p {c++; if(c==2) p=1; next} p' "$todo_template" >"$temp_todo" 2>/dev/null && [[ -s "$temp_todo" ]]; then
1177
- mv "$temp_todo" "$todo_file"
1178
- else
1179
- rm -f "$temp_todo"
1180
- cp "$todo_template" "$todo_file"
1181
- fi
1182
- sed_inplace "s/{{DATE}}/$(date +%Y-%m-%d)/" "$todo_file" 2>/dev/null || true
1183
- _upgrade_todo_reinsert_sections "$todo_file" "$workdir"
1184
- local merged=0
1185
- merged=$(grep -cE '^- \[[ x-]\] (t[0-9]|GH#[0-9])' "$todo_file" 2>/dev/null || true)
1186
- merged="${merged:-0}"
1187
- [[ "$merged" -gt 0 ]] && print_success "Merged $merged existing task(s) across sections"
1188
- print_success "TODO.md upgraded to TOON-enhanced template"
1189
- return 0
1190
- }
1191
-
1192
- _upgrade_plans() {
1193
- local plans_file="$1" plans_template="$2" backup="$3" project_root="$4"
1194
- print_info "Upgrading todo/PLANS.md..."
1195
- mkdir -p "$project_root/todo/tasks"
1196
- local existing_plans=""
1197
- if [[ -f "$plans_file" ]]; then
1198
- existing_plans=$(awk '/^### /{found=1} found{print}' "$plans_file" 2>/dev/null || echo "")
1199
- [[ "$backup" == "true" ]] && {
1200
- cp "$plans_file" "${plans_file}.bak"
1201
- print_success "Backup created: todo/PLANS.md.bak"
1202
- }
1203
- fi
1204
- local temp_plans="${plans_file}.new"
1205
- if awk '/^---$/ && !p {c++; if(c==2) p=1; next} p' "$plans_template" >"$temp_plans" 2>/dev/null && [[ -s "$temp_plans" ]]; then
1206
- mv "$temp_plans" "$plans_file"
1207
- else
1208
- rm -f "$temp_plans"
1209
- cp "$plans_template" "$plans_file"
1210
- fi
1211
- sed_inplace "s/{{DATE}}/$(date +%Y-%m-%d)/" "$plans_file" 2>/dev/null || true
1212
- if [[ -n "$existing_plans" ]] && grep -q "<!--TOON:active_plans" "$plans_file"; then
1213
- local temp_file="${plans_file}.merge" pcf
1214
- pcf=$(mktemp)
1215
- trap 'rm -f "${pcf:-}"' RETURN
1216
- printf '%s\n' "$existing_plans" >"$pcf"
1217
- local in_active=false
1218
- while IFS= read -r line || [[ -n "$line" ]]; do
1219
- [[ "$line" == *"<!--TOON:active_plans"* ]] && in_active=true
1220
- if [[ "$in_active" == true && "$line" == "-->" ]]; then
1221
- echo "$line"
1222
- echo ""
1223
- cat "$pcf"
1224
- in_active=false
1225
- continue
1226
- fi
1227
- echo "$line"
1228
- done <"$plans_file" >"$temp_file"
1229
- rm -f "$pcf"
1230
- mv "$temp_file" "$plans_file"
1231
- print_success "Merged existing plans into Active Plans"
1232
- fi
1233
- print_success "todo/PLANS.md upgraded to TOON-enhanced template"
1234
- return 0
1235
- }
1236
-
1237
- _upgrade_config_version() {
1238
- local config_file="$1"
1239
- local av
1240
- av=$(get_version)
1241
- if command -v jq &>/dev/null; then
1242
- local tj="${config_file}.tmp"
1243
- jq --arg version "$av" '.templates_version = $version' "$config_file" >"$tj" && mv "$tj" "$config_file"
1244
- else
1245
- if ! grep -q '"templates_version"' "$config_file" 2>/dev/null; then
1246
- local tj="${config_file}.tmp"
1247
- awk -v ver="$av" '/"version":/ { sub(/"version": "[^"]*"/, "\"version\": \"" ver "\",\n \"templates_version\": \"" ver "\"") } { print }' "$config_file" >"$tj" && mv "$tj" "$config_file"
1248
- else sed_inplace "s/\"templates_version\": \"[^\"]*\"/\"templates_version\": \"$av\"/" "$config_file" 2>/dev/null || true; fi
1249
- fi
1250
- return 0
1251
- }
1252
-
1253
- # Upgrade planning files to latest templates
1254
- cmd_upgrade_planning() {
1255
- local force=false backup=true dry_run=false
1256
- while [[ $# -gt 0 ]]; do
1257
- case "$1" in --force | -f)
1258
- force=true
1259
- shift
1260
- ;;
1261
- --no-backup)
1262
- backup=false
1263
- shift
1264
- ;;
1265
- --dry-run | -n)
1266
- dry_run=true
1267
- shift
1268
- ;;
1269
- *) shift ;; esac
1270
- done
1271
- print_header "Upgrade Planning Files"
1272
- echo ""
1273
- git rev-parse --is-inside-work-tree &>/dev/null || {
1274
- print_error "Not in a git repository"
1275
- return 1
1276
- }
1277
- [[ "$dry_run" != "true" ]] && { check_protected_branch "chore" "upgrade-planning" || return 1; }
1278
- local project_root
1279
- project_root=$(git rev-parse --show-toplevel)
1280
- _upgrade_validate "$project_root" || return 1
1281
- local todo_file="$project_root/TODO.md" plans_file="$project_root/todo/PLANS.md"
1282
- local todo_template="$AGENTS_DIR/templates/todo-template.md" plans_template="$AGENTS_DIR/templates/plans-template.md"
1283
- local needs_upgrade=false todo_needs=false plans_needs=false
1284
- _upgrade_check_version "$todo_file" "$todo_template" "TODO.md" && {
1285
- todo_needs=true
1286
- needs_upgrade=true
1287
- }
1288
- _upgrade_check_version "$plans_file" "$plans_template" "todo/PLANS.md" && {
1289
- plans_needs=true
1290
- needs_upgrade=true
1291
- }
1292
- [[ "$needs_upgrade" == "false" ]] && {
1293
- echo ""
1294
- print_success "Planning files are up to date!"
1295
- return 0
1296
- }
1297
- echo ""
1298
- if [[ "$dry_run" == "true" ]]; then
1299
- print_info "Dry run - no changes will be made"
1300
- echo ""
1301
- [[ "$todo_needs" == "true" ]] && echo " Would upgrade: TODO.md"
1302
- [[ "$plans_needs" == "true" ]] && echo " Would upgrade: todo/PLANS.md"
1303
- return 0
1304
- fi
1305
- if [[ "$force" == "false" ]]; then
1306
- echo "Files to upgrade:"
1307
- [[ "$todo_needs" == "true" ]] && echo " - TODO.md"
1308
- [[ "$plans_needs" == "true" ]] && echo " - todo/PLANS.md"
1309
- echo ""
1310
- echo "This will:"
1311
- echo " 1. Extract existing tasks from current files"
1312
- echo " 2. Create backups (.bak files)"
1313
- echo " 3. Apply new TOON-enhanced templates"
1314
- echo " 4. Merge existing tasks into new structure"
1315
- echo ""
1316
- read -r -p "Continue? [y/N] " response
1317
- [[ ! "$response" =~ ^[Yy]$ ]] && {
1318
- print_info "Upgrade cancelled"
1319
- return 0
1320
- }
1321
- fi
1322
- echo ""
1323
- [[ "$todo_needs" == "true" ]] && _upgrade_todo "$todo_file" "$todo_template" "$backup"
1324
- [[ "$plans_needs" == "true" ]] && _upgrade_plans "$plans_file" "$plans_template" "$backup" "$project_root"
1325
- _upgrade_config_version "$project_root/.aidevops.json"
1326
- echo ""
1327
- print_success "Planning files upgraded!"
1328
- echo ""
1329
- echo "Next steps:"
1330
- echo " 1. Review the upgraded files"
1331
- echo " 2. Verify your tasks were preserved"
1332
- if [[ "$backup" == "true" ]]; then
1333
- echo " 3. Remove .bak files when satisfied"
1334
- echo ""
1335
- echo "If issues occurred, restore from backups:"
1336
- [[ "$todo_needs" == "true" ]] && echo " mv TODO.md.bak TODO.md"
1337
- [[ "$plans_needs" == "true" ]] && echo " mv todo/PLANS.md.bak todo/PLANS.md"
1338
- fi
1339
- return 0
1340
- }
1341
436
 
1342
437
  # Features command - list available features
1343
438
  cmd_features() {
@@ -1886,6 +981,7 @@ cmd_help() {
1886
981
  echo " aidevops uninstall # Remove aidevops"
1887
982
  echo ""
1888
983
  _help_detailed_sections
984
+ return 0
1889
985
  }
1890
986
 
1891
987
  # Version command