aidevops 3.8.87 → 3.8.90

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.
Files changed (4) hide show
  1. package/VERSION +1 -1
  2. package/aidevops.sh +57 -2531
  3. package/package.json +1 -1
  4. package/setup.sh +10 -1
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.8.87
8
+ # Version: 3.8.90
9
9
 
10
10
  set -euo pipefail
11
11
 
@@ -153,622 +153,20 @@ ensure_trailing_newline() {
153
153
  [[ -s "$file" ]] && [[ "$last" != $'\n'x ]] && printf '\n' >>"$file"
154
154
  }
155
155
 
156
- # Initialize repos.json if it doesn't exist
157
- init_repos_file() {
158
- if [[ ! -f "$REPOS_FILE" ]]; then
159
- mkdir -p "$CONFIG_DIR"
160
- echo '{"initialized_repos": [], "git_parent_dirs": ["~/Git"]}' >"$REPOS_FILE"
161
- elif command -v jq &>/dev/null; then
162
- # Migrate: add git_parent_dirs if missing from existing repos.json
163
- if ! jq -e '.git_parent_dirs' "$REPOS_FILE" &>/dev/null; then
164
- local temp_file="${REPOS_FILE}.tmp"
165
- if jq '. + {"git_parent_dirs": ["~/Git"]}' "$REPOS_FILE" >"$temp_file"; then
166
- mv "$temp_file" "$REPOS_FILE"
167
- else
168
- rm -f "$temp_file"
169
- fi
170
- fi
171
- # Migrate: backfill slug for entries missing it (detect from git remote)
172
- local needs_slug
173
- needs_slug=$(jq '[.initialized_repos[] | select(.slug == null or .slug == "")] | length' "$REPOS_FILE" 2>/dev/null) || needs_slug="0"
174
- if [[ "$needs_slug" -gt 0 ]]; then
175
- local temp_file="${REPOS_FILE}.tmp"
176
- local repo_path slug
177
- # Build a map of path->slug for repos missing slugs
178
- while IFS= read -r repo_path; do
179
- # Expand ~ to $HOME for git operations
180
- local expanded_path="${repo_path/#\~/$HOME}"
181
- slug=$(get_repo_slug "$expanded_path" 2>/dev/null) || slug=""
182
- if [[ -n "$slug" ]]; then
183
- jq --arg path "$repo_path" --arg slug "$slug" \
184
- '(.initialized_repos[] | select(.path == $path and (.slug == null or .slug == ""))) |= . + {slug: $slug}' \
185
- "$REPOS_FILE" >"$temp_file" && mv "$temp_file" "$REPOS_FILE"
186
- fi
187
- done < <(jq -r '.initialized_repos[] | select(.slug == null or .slug == "") | .path' "$REPOS_FILE" 2>/dev/null)
188
- fi
189
- fi
190
- return 0
191
- }
192
-
193
- # Detect GitHub slug (owner/repo) from git remote origin
194
- # Usage: get_repo_slug <path>
195
- get_repo_slug() {
196
- local repo_path="$1"
197
- local remote_url
198
- remote_url=$(git -C "$repo_path" remote get-url origin 2>/dev/null) || return 1
199
- # Strip protocol/host prefix and .git suffix to get owner/repo
200
- local slug
201
- slug=$(echo "$remote_url" | sed 's|.*github\.com[:/]||;s|\.git$||')
202
- if [[ -n "$slug" && "$slug" == *"/"* ]]; then
203
- echo "$slug"
204
- return 0
205
- fi
206
- return 1
207
- }
208
-
209
- # Check whether a repo name follows mission-control naming.
210
- # Usage: _is_mission_control_repo_name <repo-name>
211
- _is_mission_control_repo_name() {
212
- local repo_name="$1"
213
- case "$repo_name" in
214
- mission-control | *-mission-control | mission-control-*)
215
- return 0
216
- ;;
217
- *)
218
- return 1
219
- ;;
220
- esac
221
- }
222
-
223
- # Resolve mission-control scope from slug and current actor.
224
- # Usage: _resolve_mission_control_scope <owner/repo> <current-login>
225
- # Prints: personal | org (or empty if not mission-control)
226
- _resolve_mission_control_scope() {
227
- local slug="$1"
228
- local current_login="$2"
229
-
230
- if [[ -z "$slug" ]] || [[ "$slug" != */* ]]; then
231
- echo ""
232
- return 1
233
- fi
234
-
235
- local owner repo
236
- owner="${slug%%/*}"
237
- repo="${slug##*/}"
238
-
239
- if ! _is_mission_control_repo_name "$repo"; then
240
- echo ""
241
- return 1
242
- fi
243
-
244
- if [[ -n "$current_login" && "$owner" == "$current_login" ]]; then
245
- echo "personal"
246
- return 0
247
- fi
248
-
249
- echo "org"
250
- return 0
251
- }
252
-
253
- # Compute default repos.json registration values.
254
- # Usage: _compute_repo_registration_defaults <path> <slug> <local_only> <maintainer>
255
- # Prints eval-safe key=value lines: DEFAULT_PULSE, DEFAULT_PRIORITY
256
- _compute_repo_registration_defaults() {
257
- local repo_path="$1"
258
- local slug="$2"
259
- local is_local_only="$3"
260
- local maintainer="$4"
261
-
262
- local default_pulse=false
263
- local default_priority=""
264
-
265
- if [[ "$is_local_only" == "true" ]]; then
266
- default_pulse=false
267
- else
268
- default_pulse=true
269
- fi
270
-
271
- if [[ "$slug" == */* ]]; then
272
- local owner repo
273
- owner="${slug%%/*}"
274
- repo="${slug##*/}"
275
-
276
- if [[ "$repo" == "$owner" ]] && [[ "$repo_path" == "$HOME/Git/$owner" ]]; then
277
- default_pulse=false
278
- default_priority="profile"
279
- elif _is_mission_control_repo_name "$repo"; then
280
- default_pulse=true
281
- if [[ "$owner" == "$maintainer" ]]; then
282
- default_priority="product"
283
- else
284
- default_priority="tooling"
285
- fi
286
- fi
287
- fi
288
-
289
- printf 'DEFAULT_PULSE=%q\n' "$default_pulse"
290
- printf 'DEFAULT_PRIORITY=%q\n' "$default_priority"
291
- return 0
292
- }
293
-
294
- # Infer the init_scope for a repo when not explicitly set.
295
- # Priority: .aidevops.json > repos.json entry > context inference.
296
- # Returns one of: minimal, standard, public
297
- # Usage: _infer_init_scope <project_root> [is_local_only]
298
- # Pass is_local_only="true" when the caller already has it to avoid redundant I/O.
299
- _infer_init_scope() {
300
- local project_root="$1"
301
- local is_local_only="${2:-}"
302
-
303
- # 1. Check .aidevops.json
304
- if [[ -f "$project_root/.aidevops.json" ]]; then
305
- local json_scope
306
- json_scope=$(jq -r '.init_scope // empty' "$project_root/.aidevops.json" 2>/dev/null || echo "")
307
- if [[ -n "$json_scope" ]]; then
308
- echo "$json_scope"
309
- return 0
310
- fi
311
- fi
312
-
313
- # 2. Check repos.json entry — single jq pass reads both init_scope and local_only
314
- if command -v jq &>/dev/null && [[ -f "${REPOS_FILE:-$HOME/.config/aidevops/repos.json}" ]]; then
315
- local repos_file="${REPOS_FILE:-$HOME/.config/aidevops/repos.json}"
316
- local canonical_path
317
- canonical_path=$(cd "$project_root" 2>/dev/null && pwd -P) || canonical_path="$project_root"
318
- local repo_data
319
- repo_data=$(jq -r --arg path "$canonical_path" \
320
- '.initialized_repos[] | select(.path == $path) | "\(.init_scope // "")|\(.local_only // "false")"' \
321
- "$repos_file" 2>/dev/null | head -n 1 || echo "")
322
- if [[ -n "$repo_data" ]]; then
323
- local repo_scope="${repo_data%|*}"
324
- local repo_local="${repo_data#*|}"
325
- if [[ -n "$repo_scope" ]]; then
326
- echo "$repo_scope"
327
- return 0
328
- fi
329
- # Repo found but no explicit scope — pick up local_only for context inference below
330
- [[ -z "$is_local_only" ]] && is_local_only="$repo_local"
331
- fi
332
- fi
333
-
334
- # 3. Context inference
335
- # Use pre-computed is_local_only when available; fall back to git remote check
336
- if [[ "$is_local_only" == "true" ]]; then
337
- echo "minimal"
338
- return 0
339
- fi
340
-
341
- if ! git -C "$project_root" remote get-url origin &>/dev/null 2>&1; then
342
- echo "minimal"
343
- return 0
344
- fi
345
-
346
- # Default: standard (backward compatible)
347
- echo "standard"
348
- return 0
349
- }
350
-
351
- # Check whether a given scope level includes a feature tier.
352
- # Scope hierarchy: minimal < standard < public
353
- # Usage: _scope_includes <current_scope> <required_level>
354
- # Returns 0 (true) if current_scope >= required_level, 1 (false) otherwise.
355
- _scope_includes() {
356
- local current="$1"
357
- local required="$2"
358
-
359
- # Map scope to numeric level
360
- local current_level=0 required_level=0
361
- case "$current" in
362
- minimal) current_level=0 ;;
363
- standard) current_level=1 ;;
364
- public) current_level=2 ;;
365
- *) current_level=1 ;; # unknown defaults to standard
366
- esac
367
- case "$required" in
368
- minimal) required_level=0 ;;
369
- standard) required_level=1 ;;
370
- public) required_level=2 ;;
371
- *) required_level=1 ;;
372
- esac
373
-
374
- [[ $current_level -ge $required_level ]]
375
- }
376
-
377
- # Resolve a worktree path to its canonical main-worktree path, if applicable.
378
- # Usage: resolve_canonical_repo_path <path>
379
- # Prints the canonical path to stdout. If the input is already the main
380
- # worktree, a non-git path, or git is unavailable, prints the input unchanged.
381
- #
382
- # Why this exists: `find ~/Git -name .aidevops.json` in auto-discovery and
383
- # similar scans pick up .aidevops.json files that exist in linked worktrees
384
- # (because worktrees inherit the working tree contents), and without this
385
- # guard each worktree gets registered as a separate repo. That's what caused
386
- # tabby-profile-sync to emit a profile for a worktree directory.
387
- resolve_canonical_repo_path() {
388
- local input_path="$1"
389
- local common_dir
390
- common_dir=$(git -C "$input_path" rev-parse --git-common-dir 2>/dev/null) || {
391
- printf '%s\n' "$input_path"
392
- return 0
393
- }
394
- local own_git_dir
395
- own_git_dir=$(git -C "$input_path" rev-parse --git-dir 2>/dev/null) || {
396
- printf '%s\n' "$input_path"
397
- return 0
398
- }
399
-
400
- # Resolve both to absolute paths for a reliable comparison.
401
- # git -C <path> returns paths relative to <path> when they are relative.
402
- local common_abs own_abs
403
- if [[ "$common_dir" = /* ]]; then
404
- common_abs=$(cd "$common_dir" 2>/dev/null && pwd -P)
405
- else
406
- common_abs=$(cd "$input_path/$common_dir" 2>/dev/null && pwd -P)
407
- fi
408
- if [[ "$own_git_dir" = /* ]]; then
409
- own_abs=$(cd "$own_git_dir" 2>/dev/null && pwd -P)
410
- else
411
- own_abs=$(cd "$input_path/$own_git_dir" 2>/dev/null && pwd -P)
412
- fi
413
-
414
- if [[ -z "$common_abs" || -z "$own_abs" || "$common_abs" == "$own_abs" ]]; then
415
- # Main worktree or degraded resolution — pass through.
416
- printf '%s\n' "$input_path"
417
- return 0
418
- fi
419
-
420
- # Linked worktree — ask git for the main worktree's working tree path.
421
- local main_path
422
- main_path=$(git -C "$input_path" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')
423
- if [[ -n "$main_path" && "$main_path" != "$input_path" && -d "$main_path" ]]; then
424
- printf '%s\n' "$main_path"
425
- return 0
426
- fi
427
-
428
- printf '%s\n' "$input_path"
429
- return 0
430
- }
431
-
432
- # Register a repo in repos.json
433
- # Usage: register_repo <path> <version> <features>
434
- register_repo() {
435
- local repo_path="$1"
436
- local version="$2"
437
- local features="$3"
438
-
439
- init_repos_file
440
-
441
- # Normalize path (resolve symlinks, remove trailing slash)
442
- if ! repo_path=$(cd "$repo_path" 2>/dev/null && pwd -P); then
443
- print_warning "Cannot access path: $repo_path"
444
- return 1
445
- fi
446
-
447
- # Resolve linked worktrees to their canonical main-worktree path.
448
- # Every registration path (cmd_init, auto-discovery, scan) runs through
449
- # register_repo, so the guard here catches all of them — not just the
450
- # cmd_init path that previously checked only when WORKTREE_PATH was set.
451
- local canonical_path
452
- canonical_path=$(resolve_canonical_repo_path "$repo_path")
453
- if [[ -n "$canonical_path" && "$canonical_path" != "$repo_path" ]]; then
454
- print_info "Resolved worktree to canonical repo: $repo_path → $canonical_path"
455
- if ! repo_path=$(cd "$canonical_path" 2>/dev/null && pwd -P); then
456
- print_warning "Cannot access canonical path: $canonical_path"
457
- return 1
458
- fi
459
- fi
460
-
461
- if ! command -v jq &>/dev/null; then
462
- print_warning "jq not installed - repo tracking disabled"
463
- return 0
464
- fi
465
-
466
- # Auto-detect GitHub slug from git remote
467
- local slug=""
468
- local is_local_only="false"
469
- if ! slug=$(get_repo_slug "$repo_path" 2>/dev/null); then
470
- slug=""
471
- # No remote origin — mark as local_only
472
- if ! git -C "$repo_path" remote get-url origin &>/dev/null; then
473
- is_local_only="true"
474
- fi
475
- fi
476
-
477
- # Auto-detect maintainer from gh API (current authenticated user)
478
- # Only runs once per registration — preserved on subsequent updates
479
- local maintainer=""
480
- if command -v gh &>/dev/null; then
481
- maintainer=$(gh api user --jq '.login' 2>/dev/null) || maintainer=""
482
- fi
483
-
484
- local DEFAULT_PULSE="false"
485
- local DEFAULT_PRIORITY=""
486
- eval "$(_compute_repo_registration_defaults "$repo_path" "$slug" "$is_local_only" "$maintainer")"
487
-
488
- # Infer default init_scope; pass is_local_only (already computed) to skip redundant I/O
489
- local default_init_scope
490
- default_init_scope=$(_infer_init_scope "$repo_path" "$is_local_only")
491
-
492
- # Check if repo already registered
493
- if jq -e --arg path "$repo_path" '.initialized_repos[] | select(.path == $path)' "$REPOS_FILE" &>/dev/null; then
494
- # Update existing entry, preserving pulse/priority/local_only/maintainer/init_scope if already set
495
- local temp_file="${REPOS_FILE}.tmp"
496
- jq --arg path "$repo_path" --arg version "$version" --arg features "$features" \
497
- --arg slug "$slug" --argjson local_only "$is_local_only" --arg maintainer "$maintainer" \
498
- --argjson pulse_default "$DEFAULT_PULSE" --arg priority_default "$DEFAULT_PRIORITY" \
499
- --arg init_scope_default "$default_init_scope" \
500
- '(.initialized_repos[] | select(.path == $path)) |= (
501
- . + {path: $path, version: $version, features: ($features | split(",")), updated: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))}
502
- | if $slug != "" then .slug = $slug else . end
503
- | if $local_only then .local_only = true else . end
504
- | if .pulse == null then .pulse = (if $local_only then false else $pulse_default end) else . end
505
- | if (.priority == null or .priority == "") and $priority_default != "" then .priority = $priority_default else . end
506
- | if (.maintainer == null or .maintainer == "") and $maintainer != "" then .maintainer = $maintainer else . end
507
- | if (.init_scope == null or .init_scope == "") then .init_scope = $init_scope_default else . end
508
- )' \
509
- "$REPOS_FILE" >"$temp_file" && mv "$temp_file" "$REPOS_FILE"
510
- else
511
- # Add new entry with slug, defaults, maintainer, and init_scope
512
- local temp_file="${REPOS_FILE}.tmp"
513
- jq --arg path "$repo_path" --arg version "$version" --arg features "$features" \
514
- --arg slug "$slug" --arg maintainer "$maintainer" \
515
- --argjson local_only "$is_local_only" --argjson pulse_default "$DEFAULT_PULSE" \
516
- --arg priority_default "$DEFAULT_PRIORITY" --arg init_scope "$default_init_scope" \
517
- '.initialized_repos += [(
518
- {
519
- path: $path,
520
- maintainer: $maintainer,
521
- version: $version,
522
- features: ($features | split(",")),
523
- pulse: $pulse_default,
524
- init_scope: $init_scope,
525
- initialized: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
526
- }
527
- | if $slug != "" then . + {slug: $slug} else . end
528
- | if $local_only then . + {local_only: true, pulse: false} else . end
529
- | if $priority_default != "" then . + {priority: $priority_default} else . end
530
- )]' \
531
- "$REPOS_FILE" >"$temp_file" && mv "$temp_file" "$REPOS_FILE"
532
- fi
533
- return 0
534
- }
535
-
536
- # Get list of registered repos
537
- get_registered_repos() {
538
- init_repos_file
539
-
540
- if ! command -v jq &>/dev/null; then
541
- echo "[]"
542
- return 0
543
- fi
544
-
545
- jq -r '.initialized_repos[] | .path' "$REPOS_FILE" 2>/dev/null || echo ""
546
- return 0
547
- }
548
-
549
- # Get the maintainer GitHub username for a repo
550
- # Fallback chain: maintainer field > slug owner > empty string
551
- # Usage: get_repo_maintainer <slug>
552
- get_repo_maintainer() {
553
- local slug="$1"
554
-
555
- if ! command -v jq &>/dev/null; then
556
- echo ""
557
- return 0
558
- fi
559
-
560
- local maintainer
561
- maintainer=$(jq -r --arg slug "$slug" \
562
- '.initialized_repos[] | select(.slug == $slug) | .maintainer // empty' \
563
- "$REPOS_FILE" 2>/dev/null) || maintainer=""
564
-
565
- if [[ -n "$maintainer" ]]; then
566
- echo "$maintainer"
567
- return 0
568
- fi
569
-
570
- # Fallback: extract owner from slug (owner/repo -> owner)
571
- if [[ -n "$slug" && "$slug" == *"/"* ]]; then
572
- echo "${slug%%/*}"
573
- return 0
574
- fi
575
-
576
- echo ""
577
- return 0
578
- }
579
-
580
- # Check if a repo needs upgrade (version behind current)
581
- check_repo_needs_upgrade() {
582
- local repo_path="$1"
583
- local current_version
584
- current_version=$(get_version)
585
-
586
- if ! command -v jq &>/dev/null; then
587
- return 1
588
- fi
589
-
590
- local repo_version
591
- repo_version=$(jq -r --arg path "$repo_path" '.initialized_repos[] | select(.path == $path) | .version' "$REPOS_FILE" 2>/dev/null)
592
-
593
- if [[ -z "$repo_version" || "$repo_version" == "null" ]]; then
594
- return 1
595
- fi
596
-
597
- # Compare versions (simple string comparison works for semver)
598
- if [[ "$repo_version" != "$current_version" ]]; then
599
- return 0 # needs upgrade
600
- fi
601
- return 1 # up to date
602
- }
603
-
604
- # Check if a planning file needs upgrading (version mismatch or missing TOON markers)
605
- # Usage: check_planning_file_version <file> <template>
606
- # Returns 0 if upgrade needed, 1 if up to date
607
- check_planning_file_version() {
608
- local file="$1" template="$2"
609
- if [[ -f "$file" ]]; then
610
- if ! grep -q "TOON:meta" "$file" 2>/dev/null; then
611
- return 0
612
- fi
613
- local current_ver template_ver
614
- current_ver=$(grep -A1 "TOON:meta" "$file" 2>/dev/null | tail -1 | cut -d',' -f1)
615
- template_ver=$(grep -A1 "TOON:meta" "$template" 2>/dev/null | tail -1 | cut -d',' -f1)
616
- if [[ -n "$template_ver" ]] && [[ "$current_ver" != "$template_ver" ]]; then
617
- return 0
618
- fi
619
- return 1
620
- else
621
- # No file = no upgrade needed (init would create it)
622
- return 1
623
- fi
624
- }
625
-
626
- # Check if a repo's planning templates need upgrading
627
- # Returns 0 if any planning file needs upgrade
628
- check_planning_needs_upgrade() {
629
- local repo_path="$1"
630
- local todo_file="$repo_path/TODO.md"
631
- local plans_file="$repo_path/todo/PLANS.md"
632
- local todo_template="$AGENTS_DIR/templates/todo-template.md"
633
- local plans_template="$AGENTS_DIR/templates/plans-template.md"
634
-
635
- [[ ! -f "$todo_template" ]] && return 1
636
-
637
- if check_planning_file_version "$todo_file" "$todo_template"; then
638
- return 0
639
- fi
640
- if [[ -f "$plans_template" ]] && check_planning_file_version "$plans_file" "$plans_template"; then
641
- return 0
642
- fi
643
- return 1
644
- }
645
-
646
- # Detect if current directory has aidevops but isn't registered
647
- detect_unregistered_repo() {
648
- local project_root
649
-
650
- # Check if in a git repo
651
- if ! git rev-parse --is-inside-work-tree &>/dev/null; then
652
- return 1
653
- fi
654
-
655
- project_root=$(git rev-parse --show-toplevel 2>/dev/null)
656
-
657
- # Check for .aidevops.json
658
- if [[ ! -f "$project_root/.aidevops.json" ]]; then
659
- return 1
660
- fi
661
-
662
- init_repos_file
663
-
664
- if ! command -v jq &>/dev/null; then
665
- return 1
666
- fi
667
-
668
- # Check if already registered
669
- if jq -e --arg path "$project_root" '.initialized_repos[] | select(.path == $path)' "$REPOS_FILE" &>/dev/null; then
670
- return 1 # already registered
671
- fi
672
-
673
- # Not registered - return the path
674
- echo "$project_root"
675
- return 0
676
- }
677
-
678
- # Check if on protected branch and offer worktree creation
679
- # Returns 0 if safe to proceed, 1 if user cancelled
680
- # Sets WORKTREE_PATH if worktree was created
681
- check_protected_branch() {
682
- local branch_type="${1:-chore}"
683
- local branch_suffix="${2:-aidevops-setup}"
684
-
685
- # Not in a git repo - skip check
686
- if ! git rev-parse --is-inside-work-tree &>/dev/null; then
687
- return 0
688
- fi
689
-
690
- local current_branch
691
- current_branch=$(git branch --show-current 2>/dev/null || echo "")
692
-
693
- # Not on a protected branch - safe to proceed
694
- if [[ ! "$current_branch" =~ ^(main|master)$ ]]; then
695
- return 0
696
- fi
697
-
698
- local project_root
699
- project_root=$(git rev-parse --show-toplevel)
700
- local repo_name
701
- repo_name=$(basename "$project_root")
702
- local suggested_branch="$branch_type/$branch_suffix"
703
-
704
- local choice
705
- # In non-interactive (non-TTY) contexts, auto-select option 1 (create worktree)
706
- # without prompting. This prevents read from blocking or getting EOF in CI/AI
707
- # assistant environments, which could cause silent script termination with set -e.
708
- if [[ -t 0 ]]; then
709
- echo ""
710
- print_warning "On protected branch '$current_branch'"
711
- echo ""
712
- echo "Options:"
713
- echo " 1. Create worktree: $suggested_branch (recommended)"
714
- echo " 2. Continue on $current_branch (commits directly to main)"
715
- echo " 3. Cancel"
716
- echo ""
717
- read -r -p "Choice [1]: " choice
718
- choice="${choice:-1}"
719
- else
720
- # Non-interactive: auto-create worktree (safest default)
721
- choice="1"
722
- print_info "Non-interactive mode: auto-selecting worktree creation for '$suggested_branch'"
723
- fi
724
-
725
- case "$choice" in
726
- 1)
727
- # Create worktree
728
- local worktree_dir
729
- worktree_dir="$(dirname "$project_root")/${repo_name}-${branch_type}-${branch_suffix}"
730
-
731
- print_info "Creating worktree at $worktree_dir..."
732
-
733
- local worktree_created=false
734
- if [[ -f "$AGENTS_DIR/scripts/worktree-helper.sh" ]]; then
735
- if bash "$AGENTS_DIR/scripts/worktree-helper.sh" add "$suggested_branch"; then
736
- worktree_created=true
737
- else
738
- print_error "Failed to create worktree via worktree-helper.sh"
739
- return 1
740
- fi
741
- else
742
- # Fallback without helper script
743
- if git worktree add -b "$suggested_branch" "$worktree_dir"; then
744
- worktree_created=true
745
- else
746
- print_error "Failed to create worktree"
747
- return 1
748
- fi
749
- fi
750
-
751
- if [[ "$worktree_created" == "true" ]]; then
752
- export WORKTREE_PATH="$worktree_dir"
753
- echo ""
754
- print_success "Worktree created at: $worktree_dir"
755
- print_info "Switching to: $worktree_dir"
756
- echo ""
757
- # Change to worktree directory for the remainder of this process
758
- cd "$worktree_dir" || return 1
759
- return 0
760
- fi
761
- ;;
762
- 2)
763
- print_warning "Continuing on $current_branch - changes will commit directly"
764
- return 0
765
- ;;
766
- 3 | *)
767
- print_info "Cancelled"
768
- return 1
769
- ;;
770
- esac
771
- }
156
+ # Source sub-libraries (repo management, init/scaffold, skills/plugins).
157
+ # INSTALL_DIR is the canonical location of aidevops.sh (set above).
158
+ # Using INSTALL_DIR rather than BASH_SOURCE[0] because aidevops is installed
159
+ # as a symlink at /usr/local/bin/aidevops → $INSTALL_DIR/aidevops.sh;
160
+ # dirname(BASH_SOURCE[0]) resolves to /usr/local/bin, not $INSTALL_DIR.
161
+ # shellcheck source=./aidevops-repos-lib.sh
162
+ # shellcheck disable=SC1091 # sub-library resolved at runtime via $INSTALL_DIR
163
+ source "${INSTALL_DIR}/aidevops-repos-lib.sh"
164
+ # shellcheck source=./aidevops-init-lib.sh
165
+ # shellcheck disable=SC1091 # sub-library resolved at runtime via $INSTALL_DIR
166
+ source "${INSTALL_DIR}/aidevops-init-lib.sh"
167
+ # shellcheck source=./aidevops-skills-plugin-lib.sh
168
+ # shellcheck disable=SC1091 # sub-library resolved at runtime via $INSTALL_DIR
169
+ source "${INSTALL_DIR}/aidevops-skills-plugin-lib.sh"
772
170
 
773
171
  # Status helpers (extracted for complexity reduction)
774
172
  _status_recommended_tools() {
@@ -1249,1292 +647,51 @@ _uninstall_cleanup_refs() {
1249
647
  cmd_uninstall() {
1250
648
  print_header "Uninstall AI DevOps Framework"
1251
649
  echo ""
1252
- print_warning "This will remove:"
1253
- echo " - $AGENTS_DIR (deployed agents)"
1254
- echo " - $INSTALL_DIR (repository)"
1255
- echo " - AI assistant configuration references"
1256
- echo " - Shell aliases (if added)"
1257
- echo ""
1258
- print_warning "This will NOT remove:"
1259
- echo " - Installed tools (Tabby, Zed, gh, glab, etc.)"
1260
- echo " - SSH keys"
1261
- echo " - Python/Node environments"
1262
- echo ""
1263
- read -r -p "Are you sure you want to uninstall? (yes/no): " confirm
1264
- [[ "$confirm" != "yes" ]] && {
1265
- print_info "Uninstall cancelled"
1266
- return 0
1267
- }
1268
- echo ""
1269
- check_dir "$AGENTS_DIR" && {
1270
- print_info "Removing $AGENTS_DIR..."
1271
- rm -rf "$AGENTS_DIR"
1272
- print_success "Removed agents directory"
1273
- }
1274
- check_dir "$HOME/.aidevops" && {
1275
- print_info "Removing $HOME/.aidevops..."
1276
- rm -rf "$HOME/.aidevops"
1277
- print_success "Removed aidevops config directory"
1278
- }
1279
- _uninstall_cleanup_refs
1280
- echo ""
1281
- read -r -p "Also remove the repository at $INSTALL_DIR? (yes/no): " remove_repo
1282
- if [[ "$remove_repo" == "yes" ]]; then
1283
- check_dir "$INSTALL_DIR" && {
1284
- print_info "Removing $INSTALL_DIR..."
1285
- rm -rf "$INSTALL_DIR"
1286
- print_success "Removed repository"
1287
- }
1288
- else print_info "Keeping repository at $INSTALL_DIR"; fi
1289
- echo ""
1290
- print_success "Uninstall complete!"
1291
- print_info "To reinstall, run:"
1292
- echo " npm install -g aidevops && aidevops update"
1293
- echo " OR: brew install marcusquinn/tap/aidevops && aidevops update"
1294
- }
1295
-
1296
- # Scaffold standard repo courtesy files if they don't exist
1297
- # Scaffold helpers (extracted for complexity reduction)
1298
- _scaffold_contributing() {
1299
- local project_root="$1" repo_name="$2"
1300
- [[ -f "$project_root/CONTRIBUTING.md" ]] && return 1
1301
- local c="# Contributing to $repo_name"
1302
- c="$c"$'\n\n'"Thanks for your interest in contributing!"
1303
- c="$c"$'\n\n'"## Quick Start"$'\n\n'"1. Fork the repository"
1304
- c="$c"$'\n'"2. Create a branch: \`git checkout -b feature/your-feature\`"
1305
- c="$c"$'\n'"3. Make your changes"
1306
- c="$c"$'\n'"4. Commit with conventional commits: \`git commit -m \"feat: add new feature\"\`"
1307
- c="$c"$'\n'"5. Push and open a PR"
1308
- c="$c"$'\n\n'"## Commit Messages"$'\n\n'"We use [Conventional Commits](https://www.conventionalcommits.org/):"
1309
- c="$c"$'\n\n'"- \`feat:\` - New feature"$'\n'"- \`fix:\` - Bug fix"$'\n'"- \`docs:\` - Documentation only"
1310
- c="$c"$'\n'"- \`refactor:\` - Code change that neither fixes a bug nor adds a feature"$'\n'"- \`chore:\` - Maintenance tasks"
1311
- printf '%s\n' "$c" >"$project_root/CONTRIBUTING.md"
1312
- return 0
1313
- }
1314
-
1315
- _scaffold_security() {
1316
- local project_root="$1"
1317
- [[ -f "$project_root/SECURITY.md" ]] && return 1
1318
- local se="" ge
1319
- ge=$(git -C "$project_root" config user.email 2>/dev/null || echo "")
1320
- [[ -n "$ge" ]] && se="$ge"
1321
- cat >"$project_root/SECURITY.md" <<SECEOF
1322
- # Security Policy
1323
-
1324
- ## Reporting a Vulnerability
1325
-
1326
- If you discover a security vulnerability, please report it privately.
1327
- SECEOF
1328
- [[ -n "$se" ]] && cat >>"$project_root/SECURITY.md" <<SECEOF
1329
-
1330
- **Email:** $se
1331
-
1332
- Please do not open public issues for security vulnerabilities.
1333
- SECEOF
1334
- return 0
1335
- }
1336
-
1337
- _scaffold_coc() {
1338
- local project_root="$1"
1339
- [[ -f "$project_root/CODE_OF_CONDUCT.md" ]] && return 1
1340
- cat >"$project_root/CODE_OF_CONDUCT.md" <<'COCEOF'
1341
- # Contributor Covenant Code of Conduct
1342
-
1343
- ## Our Pledge
1344
-
1345
- We as members, contributors, and leaders pledge to make participation in our
1346
- community a harassment-free experience for everyone.
1347
-
1348
- ## Our Standards
1349
-
1350
- Examples of behavior that contributes to a positive environment:
1351
-
1352
- - Using welcoming and inclusive language
1353
- - Being respectful of differing viewpoints and experiences
1354
- - Gracefully accepting constructive criticism
1355
- - Focusing on what is best for the community
1356
-
1357
- ## Attribution
1358
-
1359
- This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org),
1360
- version 2.1.
1361
- COCEOF
1362
- return 0
1363
- }
1364
-
1365
- # Scaffold standard repo courtesy files if they don't exist
1366
- # Creates: README.md, LICENCE, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md
1367
- scaffold_repo_courtesy_files() {
1368
- local project_root="$1"
1369
- local scope="${2:-standard}" # Default to standard for backward compatibility
1370
- local created=0
1371
- local repo_name
1372
- repo_name=$(basename "$project_root")
1373
- local author_name
1374
- author_name=$(git -C "$project_root" config user.name 2>/dev/null || echo "")
1375
- local current_year
1376
- current_year=$(date +%Y)
1377
- print_info "Checking repo courtesy files (scope: $scope)..."
1378
-
1379
- # README.md: requires "standard" scope
1380
- if _scope_includes "$scope" "standard"; then
1381
- if [[ ! -f "$project_root/README.md" ]]; then
1382
- local rc="# $repo_name"
1383
- if [[ -f "$project_root/.aidevops.json" ]]; then
1384
- local desc
1385
- desc=$(jq -r '.description // empty' "$project_root/.aidevops.json" 2>/dev/null || echo "")
1386
- [[ -n "$desc" ]] && rc="$rc"$'\n\n'"$desc"
1387
- fi
1388
- { [[ -f "$project_root/LICENCE" ]] || [[ -f "$project_root/LICENSE" ]]; } && rc="$rc"$'\n\n'"## Licence"$'\n\n'"See [LICENCE](LICENCE) for details."
1389
- printf '%s\n' "$rc" >"$project_root/README.md"
1390
- ((++created))
1391
- fi
1392
- fi
1393
-
1394
- # LICENCE: requires "public" scope
1395
- if _scope_includes "$scope" "public"; then
1396
- if [[ ! -f "$project_root/LICENCE" ]] && [[ ! -f "$project_root/LICENSE" ]]; then
1397
- local lh="${author_name:-$(whoami)}"
1398
- cat >"$project_root/LICENCE" <<LICEOF
1399
- MIT License
1400
-
1401
- Copyright (c) $current_year $lh
1402
-
1403
- Permission is hereby granted, free of charge, to any person obtaining a copy
1404
- of this software and associated documentation files (the "Software"), to deal
1405
- in the Software without restriction, including without limitation the rights
1406
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
1407
- copies of the Software, and to permit persons to whom the Software is
1408
- furnished to do so, subject to the following conditions:
1409
-
1410
- The above copyright notice and this permission notice shall be included in all
1411
- copies or substantial portions of the Software.
1412
-
1413
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
1414
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
1415
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
1416
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
1417
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
1418
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
1419
- SOFTWARE.
1420
- LICEOF
1421
- ((++created))
1422
- fi
1423
- fi
1424
-
1425
- # CHANGELOG.md: requires "public" scope
1426
- if _scope_includes "$scope" "public"; then
1427
- if [[ ! -f "$project_root/CHANGELOG.md" ]]; then
1428
- cat >"$project_root/CHANGELOG.md" <<'CHEOF'
1429
- # Changelog
1430
-
1431
- All notable changes to this project will be documented in this file.
1432
-
1433
- The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
1434
- and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1435
-
1436
- ## [Unreleased]
1437
- CHEOF
1438
- ((++created))
1439
- fi
1440
- fi
1441
-
1442
- # CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md: require "public" scope
1443
- if _scope_includes "$scope" "public"; then
1444
- _scaffold_contributing "$project_root" "$repo_name" && ((++created))
1445
- _scaffold_security "$project_root" && ((++created))
1446
- _scaffold_coc "$project_root" && ((++created))
1447
- fi
1448
-
1449
- [[ $created -gt 0 ]] && print_success "Created $created repo courtesy file(s) (README, LICENCE, CHANGELOG, etc.)" || print_info "Repo courtesy files already exist"
1450
- return 0
1451
- }
1452
-
1453
- # Generate the Security section content based on project type (t1412.11)
1454
- # Returns the content via stdout so callers can use it for create or update.
1455
- _generate_security_section() {
1456
- local project_root="$1"
1457
-
1458
- # Detect project type for security recommendations
1459
- local is_js_ts=false
1460
- if [[ -f "$project_root/package.json" ]]; then
1461
- is_js_ts=true
1462
- fi
1463
-
1464
- cat <<'SECHEADER'
1465
- ## Security
1466
-
1467
- SECHEADER
1468
-
1469
- if [[ "$is_js_ts" == "true" ]]; then
1470
- cat <<'SECEOF'
1471
- ### Prompt Injection Defense
1472
-
1473
- Any feature that processes untrusted content (tool outputs, user input, webhook
1474
- payloads) and passes it to an LLM must defend against prompt injection. This is
1475
- a JS/TS project — use `@stackone/defender` to sanitize untrusted tool results
1476
- before they enter the LLM context:
1477
-
1478
- ```bash
1479
- npm install @stackone/defender
1480
- ```
1481
-
1482
- ```typescript
1483
- import { createPromptDefense } from '@stackone/defender';
1484
-
1485
- const defense = createPromptDefense({ enableTier2: true, blockHighRisk: true });
1486
-
1487
- // Sanitize untrusted tool output before adding to LLM context
1488
- const result = await defense.defendToolResult(toolResponse, 'tool_name');
1489
- if (!result.allowed) {
1490
- throw new Error('Blocked untrusted content');
1491
- }
1492
- const safeContent = result.sanitized;
1493
- ```
1494
-
1495
- For features that don't use LLMs but process untrusted text (webhooks, form
1496
- submissions, API endpoints), validate and sanitize inputs at the boundary.
1497
-
1498
- ### General Security Rules
1499
-
1500
- - Never log or expose API keys, tokens, or credentials in output
1501
- - Store secrets via `aidevops secret set <NAME>` (gopass-encrypted) or
1502
- environment variables — never hardcode them in source
1503
- - Use `<PLACEHOLDER>` values in code examples; note the secure storage location
1504
- - Validate all external input (user input, webhook payloads, API responses)
1505
- - Pin third-party GitHub Actions to SHA hashes, not branch tags
1506
- - Run `aidevops security audit` periodically to check security posture
1507
- - See `~/.aidevops/agents/tools/security/prompt-injection-defender.md` for
1508
- the framework's prompt injection defense patterns
1509
- SECEOF
1510
- else
1511
- cat <<'SECEOF'
1512
- ### Prompt Injection Defense
1513
-
1514
- Any feature that passes untrusted content to an LLM — user input, tool outputs,
1515
- retrieved documents, emails, tickets, or webhook payloads — must defend against
1516
- prompt injection. Sanitize and validate that content before including it in
1517
- prompts:
1518
-
1519
- - Strip or escape control characters and instruction-like patterns
1520
- - Use structured prompt templates with clear system/user boundaries
1521
- - Never concatenate raw external content directly into system prompts
1522
- - Validate all externally sourced content (tool results, API responses, database
1523
- records) before inclusion in prompts
1524
- - Consider allowlist-based input validation where possible
1525
-
1526
- ### General Security Rules
1527
-
1528
- - Never log or expose API keys, tokens, or credentials in output
1529
- - Store secrets via `aidevops secret set <NAME>` (gopass-encrypted) or
1530
- environment variables — never hardcode them in source
1531
- - Use `<PLACEHOLDER>` values in code examples; note the secure storage location
1532
- - Validate all external input (user input, webhook payloads, API responses)
1533
- - Pin third-party GitHub Actions to SHA hashes, not branch tags
1534
- - Run `aidevops security audit` periodically to check security posture
1535
- - See `~/.aidevops/agents/tools/security/prompt-injection-defender.md` for
1536
- the framework's prompt injection defense patterns
1537
- SECEOF
1538
- fi
1539
-
1540
- return 0
1541
- }
1542
-
1543
- # Scaffold .agents/AGENTS.md with context-aware Security section (t1412.11)
1544
- # Idempotent: creates the file if missing, or updates the Security section
1545
- # in an existing file (preserving all other custom content).
1546
- scaffold_agents_md() {
1547
- local project_root="$1"
1548
- local agents_md="$project_root/.agents/AGENTS.md"
1549
-
1550
- mkdir -p "$(dirname "$agents_md")"
1551
-
1552
- if [[ -f "$agents_md" ]]; then
1553
- # File exists — update the Security section idempotently
1554
- _update_agents_md_security "$project_root"
1555
- return $?
1556
- fi
1557
-
1558
- # File missing — create from scratch with base template + security
1559
- local security_content
1560
- security_content=$(_generate_security_section "$project_root")
1561
-
1562
- cat >"$agents_md" <<'AGENTSEOF'
1563
- # Agent Instructions
1564
-
1565
- This directory contains project-specific agent context. The [aidevops](https://aidevops.sh)
1566
- framework is loaded separately via the global config (`~/.aidevops/agents/`).
1567
-
1568
- ## Purpose
1569
-
1570
- Files in `.agents/` provide project-specific instructions that AI assistants
1571
- read when working in this repository. Use this for:
1572
-
1573
- - Domain-specific conventions not covered by the framework
1574
- - Project architecture decisions and patterns
1575
- - API design rules, data models, naming conventions
1576
- - Integration details (third-party services, deployment targets)
1577
-
1578
- ## Adding Agents
1579
-
1580
- Create `.md` files in this directory for domain-specific context:
1581
-
1582
- ```text
1583
- .agents/
1584
- AGENTS.md # This file - overview and index
1585
- api-patterns.md # API design conventions
1586
- deployment.md # Deployment procedures
1587
- data-model.md # Database schema and relationships
1588
- ```
1589
-
1590
- Each file is read on demand by AI assistants when relevant to the task.
1591
-
1592
- AGENTSEOF
1593
-
1594
- # Append the generated security section
1595
- printf '%s\n' "$security_content" >>"$agents_md"
1596
-
1597
- return 0
1598
- }
1599
-
1600
- # Update the Security section in an existing .agents/AGENTS.md (t1412.11)
1601
- # Replaces everything from "## Security" to the next "## " heading (or EOF)
1602
- # with the latest security guidance. Preserves all other content.
1603
- _update_agents_md_security() {
1604
- local project_root="$1"
1605
- local agents_md="$project_root/.agents/AGENTS.md"
1606
- local tmp_file="${agents_md}.tmp.$$"
1607
-
1608
- local security_content
1609
- security_content=$(_generate_security_section "$project_root")
1610
-
1611
- local in_security=false
1612
- local has_security_section=false
1613
-
1614
- # Process line by line: skip old Security section, insert new one
1615
- while IFS= read -r line || [[ -n "$line" ]]; do
1616
- # Match "## Security" exactly, with optional trailing whitespace
1617
- if [[ "$line" =~ ^'## Security'[[:space:]]*$ ]]; then
1618
- # Found the Security heading — replace it
1619
- in_security=true
1620
- has_security_section=true
1621
- printf '%s\n' "$security_content" >>"$tmp_file"
1622
- continue
1623
- fi
1624
-
1625
- if [[ "$in_security" == "true" ]]; then
1626
- # Check if we've hit the next ## heading (end of Security section)
1627
- if [[ "$line" == "## "* ]]; then
1628
- in_security=false
1629
- printf '%s\n' "$line" >>"$tmp_file"
1630
- fi
1631
- # Skip lines within the old Security section
1632
- continue
1633
- fi
1634
-
1635
- printf '%s\n' "$line" >>"$tmp_file"
1636
- done <"$agents_md"
1637
-
1638
- if [[ "$has_security_section" == "false" ]]; then
1639
- # No existing Security section — append it
1640
- printf '\n%s\n' "$security_content" >>"$tmp_file"
1641
- fi
1642
-
1643
- mv "$tmp_file" "$agents_md"
1644
-
1645
- return 0
1646
- }
1647
-
1648
- # Init helpers (extracted for complexity reduction)
1649
- _init_parse_features() {
1650
- local features="$1"
1651
- case "$features" in
1652
- all) echo "planning git_workflow code_quality time_tracking database beads security" ;;
1653
- planning) echo "planning" ;; git-workflow) echo "git_workflow" ;; code-quality) echo "code_quality" ;;
1654
- time-tracking) echo "time_tracking planning" ;; database) echo "database" ;;
1655
- beads) echo "beads planning" ;; sops) echo "sops" ;; security) echo "security" ;;
1656
- *)
1657
- local result=""
1658
- IFS=',' read -ra FL <<<"$features"
1659
- for f in "${FL[@]}"; do
1660
- case "$f" in
1661
- planning) result="$result planning" ;; git-workflow) result="$result git_workflow" ;;
1662
- code-quality) result="$result code_quality" ;; time-tracking) result="$result time_tracking planning" ;;
1663
- database) result="$result database" ;; beads) result="$result beads planning" ;;
1664
- sops) result="$result sops" ;; security) result="$result security" ;;
1665
- esac
1666
- done
1667
- echo "$result"
1668
- ;;
1669
- esac
1670
- return 0
1671
- }
1672
-
1673
- # Seed mission-control onboarding template when initializing a mission-control repo.
1674
- # Usage: _seed_mission_control_template <project_root> <personal|org>
1675
- _seed_mission_control_template() {
1676
- local project_root="$1"
1677
- local scope="$2"
1678
- local seed_file="$project_root/todo/mission-control-seed.md"
1679
-
1680
- if [[ -z "$scope" ]]; then
1681
- return 0
1682
- fi
1683
-
1684
- if [[ -f "$seed_file" ]]; then
1685
- return 0
1686
- fi
1687
-
1688
- mkdir -p "$project_root/todo"
1689
-
1690
- if [[ "$scope" == "personal" ]]; then
1691
- cat >"$seed_file" <<'EOF'
1692
- # Mission Control Seed (Personal)
1693
-
1694
- Starter checklist for a personal mission-control repo initialized with aidevops.
1695
-
1696
- ## First-Day Setup
1697
-
1698
- - [ ] Confirm `~/.config/aidevops/repos.json` has all active repos registered with correct `slug` and `path`
1699
- - [ ] Set `pulse: true` only for repos you want actively supervised
1700
- - [ ] Add `pulse_hours` windows to avoid dispatch during daytime manual development
1701
- - [ ] Verify profile and archive repos are `pulse: false` and `priority: "profile"` where applicable
1702
-
1703
- ## Operating Rhythm
1704
-
1705
- - [ ] Define weekly review cadence for `aidevops pulse` health and backlog aging
1706
- - [ ] Add hygiene tasks for stale branches, stale worktrees, and stale queued issues
1707
- - [ ] Track cross-repo blockers in TODO with clear `blocked-by:` links
1708
- EOF
1709
- else
1710
- cat >"$seed_file" <<'EOF'
1711
- # Mission Control Seed (Organization)
1712
-
1713
- Starter checklist for an organization mission-control repo initialized with aidevops.
1714
-
1715
- ## First-Day Setup
1716
-
1717
- - [ ] Register all managed org repos in `~/.config/aidevops/repos.json` with `slug`, `path`, `priority`, and `maintainer`
1718
- - [ ] Set `pulse: true` only for repos approved for autonomous dispatch
1719
- - [ ] Configure `pulse_hours` and optional `pulse_expires` windows for sprint-based focus
1720
- - [ ] Keep sensitive/internal-only repos `pulse: false` until policy checks are complete
1721
-
1722
- ## Governance
1723
-
1724
- - [ ] Define maintainer response SLA for `needs-maintainer-review` triage
1725
- - [ ] Document worker guardrails (release, merge, and security boundaries)
1726
- - [ ] Add a weekly audit task for repo registration drift and label hygiene
1727
- EOF
1728
- fi
1729
-
1730
- print_success "Seeded mission-control template: todo/mission-control-seed.md (${scope})"
1731
- return 0
1732
- }
1733
-
1734
- # Scaffold .agents/commands/ and .windsurf/workflows/ symlinks so that clients
1735
- # which read repo-local command directories (Amp reads .agents/commands/ natively;
1736
- # Windsurf reads .windsurf/workflows/) see the aidevops main-agent slash commands.
1737
- #
1738
- # Behavior is idempotent:
1739
- # - If .agents/commands/ already contains the expected aidevops-*.md symlinks
1740
- # (this repo IS the aidevops source), do nothing.
1741
- # - Otherwise link .agents/commands/ → ~/.aidevops/agents/commands/
1742
- # - Always link .windsurf/workflows/ → ../.agents/commands/ (relative)
1743
- _init_scaffold_commands_symlinks() {
1744
- local project_root="$1"
1745
- local source_dir="$HOME/.aidevops/agents/commands"
1746
- local commands_dir="$project_root/.agents/commands"
1747
- local windsurf_dir="$project_root/.windsurf"
1748
- local workflows_link="$windsurf_dir/workflows"
1749
-
1750
- # If .agents/commands/ already contains main-agent symlinks, this repo
1751
- # manages them directly (e.g. the aidevops source repo itself) — leave
1752
- # it alone so we never overwrite authoritative content.
1753
- if [[ -e "$commands_dir/aidevops-build-plus.md" ]]; then
1754
- print_info ".agents/commands/ already contains main-agent symlinks — preserving"
1755
- elif [[ ! -d "$source_dir" ]]; then
1756
- print_warning "Framework commands dir not found at $source_dir — run setup.sh first to deploy main-agent symlinks"
1757
- elif [[ -L "$commands_dir" ]]; then
1758
- # Existing symlink — point at the canonical source
1759
- local current_target
1760
- current_target=$(readlink "$commands_dir")
1761
- if [[ "$current_target" != "$source_dir" ]]; then
1762
- rm "$commands_dir"
1763
- ln -s "$source_dir" "$commands_dir"
1764
- print_success "Re-linked .agents/commands/ → $source_dir"
1765
- else
1766
- print_info ".agents/commands/ already linked correctly"
1767
- fi
1768
- elif [[ -d "$commands_dir" ]]; then
1769
- print_warning ".agents/commands/ exists as a real directory — not overwriting"
1770
- else
1771
- ln -s "$source_dir" "$commands_dir"
1772
- print_success "Linked .agents/commands/ → $source_dir (Amp reads this natively)"
1773
- fi
1774
-
1775
- # .windsurf/workflows/ → ../.agents/commands/ (relative, so the link
1776
- # resolves inside the repo regardless of checkout path).
1777
- mkdir -p "$windsurf_dir"
1778
- if [[ -L "$workflows_link" ]]; then
1779
- print_info ".windsurf/workflows/ already linked"
1780
- elif [[ -d "$workflows_link" ]]; then
1781
- print_warning ".windsurf/workflows/ exists as a real directory — not overwriting"
1782
- else
1783
- (cd "$windsurf_dir" && ln -s "../.agents/commands" workflows)
1784
- print_success "Linked .windsurf/workflows/ → ../.agents/commands (Windsurf slash commands)"
1785
- fi
1786
- return 0
1787
- }
1788
-
1789
- # Scaffold optional files gated by init_scope (t2265).
1790
- # Extracted from cmd_init to reduce nesting depth and function length.
1791
- # Usage: _init_scaffold_scope_gated_files <project_root> <init_scope> <repo_name>
1792
- _init_scaffold_scope_gated_files() {
1793
- local project_root="$1"
1794
- local init_scope="$2"
1795
- local repo_name="$3"
1796
-
1797
- # Collaborator pointer files — require standard scope
1798
- if _scope_includes "$init_scope" "standard"; then
1799
- local pointer_content="Read AGENTS.md for all project context and instructions."
1800
- local pointer_files=(".cursorrules" ".windsurfrules" ".clinerules" ".github/copilot-instructions.md")
1801
- local pointer_created=0
1802
- local pf
1803
- for pf in "${pointer_files[@]}"; do
1804
- local pf_path="$project_root/$pf"
1805
- if [[ ! -f "$pf_path" ]]; then
1806
- mkdir -p "$(dirname "$pf_path")"
1807
- echo "$pointer_content" >"$pf_path"
1808
- ((++pointer_created))
1809
- fi
1810
- done
1811
- if [[ $pointer_created -gt 0 ]]; then
1812
- print_success "Created $pointer_created collaborator pointer file(s) (.cursorrules, etc.)"
1813
- else
1814
- print_info "Collaborator pointer files already exist"
1815
- fi
1816
- else
1817
- print_info "Collaborator pointer files skipped (init_scope: $init_scope)"
1818
- fi
1819
-
1820
- # DESIGN.md — requires standard scope
1821
- if _scope_includes "$init_scope" "standard"; then
1822
- if [[ ! -f "$project_root/DESIGN.md" ]]; then
1823
- local design_template="$AGENTS_DIR/templates/DESIGN.md.template"
1824
- if [[ -f "$design_template" ]]; then
1825
- sed "s/{Project Name}/$repo_name/g" "$design_template" >"$project_root/DESIGN.md"
1826
- print_success "Created DESIGN.md (design system skeleton — populate with tools/design/design-md.md)"
1827
- fi
1828
- else
1829
- print_info "DESIGN.md already exists, skipping"
1830
- fi
1831
- else
1832
- print_info "DESIGN.md skipped (init_scope: $init_scope)"
1833
- fi
1834
-
1835
- # Courtesy files (README, LICENCE, CHANGELOG, etc.) — scope handled internally
1836
- scaffold_repo_courtesy_files "$project_root" "$init_scope"
1837
-
1838
- # MODELS.md — requires standard scope
1839
- if _scope_includes "$init_scope" "standard"; then
1840
- local generate_models_script="$AGENTS_DIR/scripts/generate-models-md.sh"
1841
- if [[ -x "$generate_models_script" ]] && command -v sqlite3 &>/dev/null; then
1842
- print_info "Generating MODELS.md (model performance leaderboard)..."
1843
- if "$generate_models_script" --output "$project_root/MODELS.md" --repo-path "$project_root" --quiet 2>/dev/null; then
1844
- print_success "Created MODELS.md (per-repo model leaderboard)"
1845
- else
1846
- print_warning "MODELS.md generation failed (will be populated as tasks run)"
1847
- fi
1848
- else
1849
- print_info "MODELS.md skipped (sqlite3 or generate script not available)"
1850
- fi
1851
- else
1852
- print_info "MODELS.md skipped (init_scope: $init_scope)"
1853
- fi
1854
-
1855
- return 0
1856
- }
1857
-
1858
- # Init command - initialize aidevops in a project
1859
- cmd_init() {
1860
- local features="${1:-all}"
1861
-
1862
- print_header "Initialize AI DevOps in Project"
1863
- echo ""
1864
-
1865
- # Check if we're in a git repo
1866
- if ! git rev-parse --is-inside-work-tree &>/dev/null; then
1867
- print_error "Not in a git repository"
1868
- print_info "Run 'git init' first or navigate to a git repository"
1869
- return 1
1870
- fi
1871
-
1872
- # Check for protected branch and offer worktree
1873
- if ! check_protected_branch "chore" "aidevops-init"; then
1874
- return 1
1875
- fi
1876
-
1877
- local project_root
1878
- project_root=$(git rev-parse --show-toplevel)
1879
- print_info "Project root: $project_root"
1880
- echo ""
1881
-
1882
- # Parse features using helper
1883
- local parsed
1884
- parsed=$(_init_parse_features "$features")
1885
- local enable_planning=false enable_git_workflow=false enable_code_quality=false
1886
- local enable_time_tracking=false enable_database=false enable_beads=false
1887
- local enable_sops=false enable_security=false
1888
- local _f
1889
- for _f in $parsed; do
1890
- case "$_f" in
1891
- planning) enable_planning=true ;; git_workflow) enable_git_workflow=true ;;
1892
- code_quality) enable_code_quality=true ;; time_tracking) enable_time_tracking=true ;;
1893
- database) enable_database=true ;; beads) enable_beads=true ;;
1894
- sops) enable_sops=true ;; security) enable_security=true ;;
1895
- esac
1896
- done
1897
-
1898
- # Determine init_scope: minimal | standard | public
1899
- # Infer from context when not set; user can override via repos.json or .aidevops.json
1900
- local init_scope
1901
- init_scope=$(_infer_init_scope "$project_root")
1902
- print_info "Init scope: $init_scope (controls which scaffolding files are created)"
1903
-
1904
- # Create .aidevops.json config
1905
- local config_file="$project_root/.aidevops.json"
1906
- local aidevops_version
1907
- aidevops_version=$(get_version)
1908
-
1909
- print_info "Creating .aidevops.json..."
1910
- cat >"$config_file" <<EOF
1911
- {
1912
- "version": "$aidevops_version",
1913
- "initialized": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
1914
- "init_scope": "$init_scope",
1915
- "features": {
1916
- "planning": $enable_planning,
1917
- "git_workflow": $enable_git_workflow,
1918
- "code_quality": $enable_code_quality,
1919
- "time_tracking": $enable_time_tracking,
1920
- "database": $enable_database,
1921
- "beads": $enable_beads,
1922
- "sops": $enable_sops,
1923
- "security": $enable_security
1924
- },
1925
- "time_tracking": {
1926
- "enabled": $enable_time_tracking,
1927
- "prompt_on_commit": true,
1928
- "auto_record_branch_start": true
1929
- },
1930
- "database": {
1931
- "enabled": $enable_database,
1932
- "schema_path": "schemas",
1933
- "migrations_path": "migrations",
1934
- "seeds_path": "seeds",
1935
- "auto_generate_migration": true
1936
- },
1937
- "beads": {
1938
- "enabled": $enable_beads,
1939
- "sync_on_commit": false,
1940
- "auto_ready_check": true
1941
- },
1942
- "sops": {
1943
- "enabled": $enable_sops,
1944
- "backend": "age",
1945
- "patterns": ["*.secret.yaml", "*.secret.json", "configs/*.enc.json", "configs/*.enc.yaml"]
1946
- },
1947
- "plugins": []
1948
- }
1949
- EOF
1950
- # Note: plugins array is always present but empty by default.
1951
- # Users add plugins via: aidevops plugin add <repo-url> [--namespace <name>]
1952
- # Schema per plugin entry:
1953
- # {
1954
- # "name": "pro",
1955
- # "repo": "https://github.com/user/aidevops-pro.git",
1956
- # "branch": "main",
1957
- # "namespace": "pro",
1958
- # "enabled": true
1959
- # }
1960
- # Plugins deploy to ~/.aidevops/agents/<namespace>/ (namespaced, no collisions)
1961
- print_success "Created .aidevops.json"
1962
-
1963
- # Derive repo name for scaffolding
1964
- # In worktrees, basename gives the worktree dir name (e.g., "repo-chore-foo"),
1965
- # not the actual repo name. Prefer: git remote URL > main worktree basename > cwd basename.
1966
- local repo_name
1967
- local remote_url
1968
- remote_url=$(git -C "$project_root" remote get-url origin 2>/dev/null || true)
1969
- local repo_slug=""
1970
- if [[ -n "$remote_url" ]]; then
1971
- repo_slug=$(echo "$remote_url" | sed 's|.*github\.com[:/]||;s|\.git$||')
1972
- fi
1973
- if [[ -n "$remote_url" ]]; then
1974
- repo_name=$(basename "$remote_url" .git)
1975
- else
1976
- # No remote — try main worktree path (first line of `git worktree list`)
1977
- local main_wt
1978
- main_wt=$(git -C "$project_root" worktree list --porcelain 2>/dev/null | head -1 | sed 's/^worktree //')
1979
- if [[ -n "$main_wt" ]]; then
1980
- repo_name=$(basename "$main_wt")
1981
- else
1982
- repo_name=$(basename "$project_root")
1983
- fi
1984
- fi
1985
-
1986
- # Create .agents/ directory for project-specific agent context
1987
- # (The aidevops framework is loaded globally via ~/.aidevops/agents/ — this
1988
- # directory is for project-specific agents, conventions, and architecture docs)
1989
- if [[ -L "$project_root/.agents" ]]; then
1990
- # Migrate legacy symlink to real directory
1991
- rm -f "$project_root/.agents"
1992
- print_info "Removed legacy .agents symlink (framework is loaded globally now)"
1993
- fi
1994
- # Also clean up legacy .agent symlink/directory
1995
- if [[ -L "$project_root/.agent" ]]; then
1996
- rm -f "$project_root/.agent"
1997
- print_info "Removed legacy .agent symlink"
1998
- elif [[ -d "$project_root/.agent" && ! -d "$project_root/.agents" ]]; then
1999
- mv "$project_root/.agent" "$project_root/.agents"
2000
- print_success "Migrated .agent/ -> .agents/ directory"
2001
- fi
2002
-
2003
- if [[ ! -d "$project_root/.agents" ]]; then
2004
- mkdir -p "$project_root/.agents"
2005
- print_success "Created .agents/ directory"
2006
- fi
2007
-
2008
- # Link .agents/commands/ and .windsurf/workflows/ so Amp (native) and Windsurf
2009
- # (symlinked) can see the aidevops main-agent slash commands.
2010
- _init_scaffold_commands_symlinks "$project_root"
2011
-
2012
- # Scaffold or update .agents/AGENTS.md (idempotent — creates if missing,
2013
- # updates Security section if file already exists)
2014
- local _agents_md_existed=false
2015
- [[ -f "$project_root/.agents/AGENTS.md" ]] && _agents_md_existed=true
2016
- scaffold_agents_md "$project_root"
2017
- if [[ "$_agents_md_existed" == "true" ]]; then
2018
- print_success "Updated Security section in .agents/AGENTS.md"
2019
- else
2020
- print_success "Created .agents/AGENTS.md"
2021
- fi
2022
-
2023
- # Scaffold root AGENTS.md if missing
2024
- if [[ ! -f "$project_root/AGENTS.md" ]]; then
2025
- cat >"$project_root/AGENTS.md" <<ROOTAGENTSEOF
2026
- # $repo_name
2027
-
2028
- <!-- AI-CONTEXT-START -->
2029
-
2030
- ## Quick Reference
2031
-
2032
- - **Build**: \`# TODO: add build command\`
2033
- - **Test**: \`# TODO: add test command\`
2034
- - **Deploy**: \`# TODO: add deploy command\`
2035
-
2036
- ## Project Overview
2037
-
2038
- <!-- Brief description of what this project does and why it exists. -->
2039
-
2040
- ## Architecture
2041
-
2042
- <!-- Key architectural decisions, tech stack, directory structure. -->
2043
-
2044
- ## Conventions
2045
-
2046
- - Commits: [Conventional Commits](https://www.conventionalcommits.org/)
2047
- - Branches: \`feature/\`, \`bugfix/\`, \`hotfix/\`, \`refactor/\`, \`chore/\`
2048
-
2049
- ## Key Files
2050
-
2051
- | File | Purpose |
2052
- |------|---------|
2053
- | \`.agents/AGENTS.md\` | Project-specific agent instructions |
2054
- | \`TODO.md\` | Task tracking |
2055
- | \`CHANGELOG.md\` | Version history |
2056
-
2057
- <!-- AI-CONTEXT-END -->
2058
- ROOTAGENTSEOF
2059
- print_success "Created AGENTS.md"
2060
- fi
2061
-
2062
- # Create planning files if enabled
2063
- if [[ "$enable_planning" == "true" ]]; then
2064
- print_info "Setting up planning files..."
2065
-
2066
- # Create TODO.md from template
2067
- if [[ ! -f "$project_root/TODO.md" ]]; then
2068
- if [[ -f "$AGENTS_DIR/templates/todo-template.md" ]]; then
2069
- cp "$AGENTS_DIR/templates/todo-template.md" "$project_root/TODO.md"
2070
- print_success "Created TODO.md"
2071
- else
2072
- # Fallback minimal template
2073
- cat >"$project_root/TODO.md" <<'EOF'
2074
- # TODO
2075
-
2076
- ## In Progress
2077
-
2078
- <!-- Tasks currently being worked on -->
2079
-
2080
- ## Backlog
2081
-
2082
- <!-- Prioritized list of upcoming tasks -->
2083
-
2084
- ---
2085
-
2086
- *Format: `- [ ] Task description @owner #tag ~estimate`*
2087
- *Time tracking: `started:`, `completed:`, `actual:`*
2088
- EOF
2089
- print_success "Created TODO.md (minimal template)"
2090
- fi
2091
- else
2092
- print_warning "TODO.md already exists, skipping"
2093
- fi
2094
-
2095
- # Create todo/ directory and PLANS.md
2096
- mkdir -p "$project_root/todo/tasks"
2097
-
2098
- if [[ ! -f "$project_root/todo/PLANS.md" ]]; then
2099
- if [[ -f "$AGENTS_DIR/templates/plans-template.md" ]]; then
2100
- cp "$AGENTS_DIR/templates/plans-template.md" "$project_root/todo/PLANS.md"
2101
- print_success "Created todo/PLANS.md"
2102
- else
2103
- # Fallback minimal template
2104
- cat >"$project_root/todo/PLANS.md" <<'EOF'
2105
- # Execution Plans
2106
-
2107
- Complex, multi-session work that requires detailed planning.
2108
-
2109
- ## Active Plans
2110
-
2111
- <!-- Plans currently in progress -->
2112
-
2113
- ## Completed Plans
2114
-
2115
- <!-- Archived completed plans -->
2116
-
2117
- ---
2118
-
2119
- *See `.agents/workflows/plans.md` for planning workflow*
2120
- EOF
2121
- print_success "Created todo/PLANS.md (minimal template)"
2122
- fi
2123
- else
2124
- print_warning "todo/PLANS.md already exists, skipping"
2125
- fi
2126
-
2127
- # Create .gitkeep in tasks
2128
- touch "$project_root/todo/tasks/.gitkeep"
2129
-
2130
- # Seed mission-control starter template for personal/org control repos
2131
- local init_actor=""
2132
- if command -v gh &>/dev/null; then
2133
- init_actor=$(gh api user --jq '.login' 2>/dev/null || echo "")
2134
- fi
2135
- local mission_scope=""
2136
- mission_scope=$(_resolve_mission_control_scope "$repo_slug" "$init_actor" 2>/dev/null || echo "")
2137
- _seed_mission_control_template "$project_root" "$mission_scope"
2138
- fi
2139
-
2140
- # Create database directories if enabled
2141
- if [[ "$enable_database" == "true" ]]; then
2142
- print_info "Setting up database schema directories..."
2143
-
2144
- # Create schemas directory with AGENTS.md
2145
- if [[ ! -d "$project_root/schemas" ]]; then
2146
- mkdir -p "$project_root/schemas"
2147
- cat >"$project_root/schemas/AGENTS.md" <<'EOF'
2148
- # Database Schemas
2149
-
2150
- Declarative schema files - source of truth for database structure.
2151
-
2152
- See: `@sql-migrations` or `.agents/workflows/sql-migrations.md`
2153
- EOF
2154
- print_success "Created schemas/ directory"
2155
- else
2156
- print_warning "schemas/ already exists, skipping"
2157
- fi
2158
-
2159
- # Create migrations directory with AGENTS.md
2160
- if [[ ! -d "$project_root/migrations" ]]; then
2161
- mkdir -p "$project_root/migrations"
2162
- cat >"$project_root/migrations/AGENTS.md" <<'EOF'
2163
- # Database Migrations
2164
-
2165
- Auto-generated versioned migration files. Do not edit manually.
2166
-
2167
- See: `@sql-migrations` or `.agents/workflows/sql-migrations.md`
2168
- EOF
2169
- print_success "Created migrations/ directory"
2170
- else
2171
- print_warning "migrations/ already exists, skipping"
2172
- fi
2173
-
2174
- # Create seeds directory with AGENTS.md
2175
- if [[ ! -d "$project_root/seeds" ]]; then
2176
- mkdir -p "$project_root/seeds"
2177
- cat >"$project_root/seeds/AGENTS.md" <<'EOF'
2178
- # Database Seeds
2179
-
2180
- Initial and reference data (roles, statuses, test accounts).
2181
-
2182
- See: `@sql-migrations` or `.agents/workflows/sql-migrations.md`
2183
- EOF
2184
- print_success "Created seeds/ directory"
2185
- else
2186
- print_warning "seeds/ already exists, skipping"
2187
- fi
2188
- fi
2189
-
2190
- # Initialize Beads if enabled
2191
- if [[ "$enable_beads" == "true" ]]; then
2192
- print_info "Setting up Beads task graph..."
2193
-
2194
- # Check if Beads CLI is installed
2195
- if ! command -v bd &>/dev/null; then
2196
- print_warning "Beads CLI (bd) not installed"
2197
- echo " Install with: brew install steveyegge/beads/bd"
2198
- echo " Or download: https://github.com/steveyegge/beads/releases"
2199
- echo " Or via Go: go install github.com/steveyegge/beads/cmd/bd@latest"
2200
- else
2201
- # Initialize Beads in the project
2202
- if [[ ! -d "$project_root/.beads" ]]; then
2203
- print_info "Initializing Beads database..."
2204
- if (cd "$project_root" && bd init 2>/dev/null); then
2205
- print_success "Beads initialized"
2206
- else
2207
- print_warning "Beads init failed - run manually: bd init"
2208
- fi
2209
- else
2210
- print_info "Beads already initialized"
2211
- fi
2212
-
2213
- # Run initial sync from TODO.md/PLANS.md
2214
- if [[ -f "$AGENTS_DIR/scripts/beads-sync-helper.sh" ]]; then
2215
- print_info "Syncing tasks to Beads..."
2216
- if bash "$AGENTS_DIR/scripts/beads-sync-helper.sh" push "$project_root" 2>/dev/null; then
2217
- print_success "Tasks synced to Beads"
2218
- else
2219
- print_warning "Beads sync failed - run manually: beads-sync-helper.sh push"
2220
- fi
2221
- fi
2222
- fi
2223
- fi
2224
-
2225
- # Initialize SOPS if enabled
2226
- if [[ "$enable_sops" == "true" ]]; then
2227
- print_info "Setting up SOPS encrypted config support..."
2228
-
2229
- # Check for sops and age
2230
- local sops_ready=true
2231
- if ! command -v sops &>/dev/null; then
2232
- print_warning "SOPS not installed"
2233
- echo " Install with: brew install sops"
2234
- sops_ready=false
2235
- fi
2236
- if ! command -v age-keygen &>/dev/null; then
2237
- print_warning "age not installed (default SOPS backend)"
2238
- echo " Install with: brew install age"
2239
- sops_ready=false
2240
- fi
2241
-
2242
- # Generate age key if none exists
2243
- local age_key_file="$HOME/.config/sops/age/keys.txt"
2244
- if [[ "$sops_ready" == "true" ]] && [[ ! -f "$age_key_file" ]]; then
2245
- print_info "Generating age key for SOPS..."
2246
- mkdir -p "$(dirname "$age_key_file")"
2247
- age-keygen -o "$age_key_file" 2>/dev/null
2248
- chmod 600 "$age_key_file"
2249
- print_success "Age key generated at $age_key_file"
2250
- fi
2251
-
2252
- # Create .sops.yaml if it doesn't exist
2253
- if [[ ! -f "$project_root/.sops.yaml" ]]; then
2254
- local age_pubkey=""
2255
- if [[ -f "$age_key_file" ]]; then
2256
- age_pubkey=$(grep -o 'age1[a-z0-9]*' "$age_key_file" | head -1)
2257
- fi
2258
-
2259
- if [[ -n "$age_pubkey" ]]; then
2260
- cat >"$project_root/.sops.yaml" <<SOPSEOF
2261
- # SOPS configuration - encrypts values in config files while keeping keys visible
2262
- # See: .agents/tools/credentials/sops.md
2263
- creation_rules:
2264
- - path_regex: '\.secret\.(yaml|yml|json)$'
2265
- age: >-
2266
- $age_pubkey
2267
- - path_regex: 'configs/.*\.enc\.(yaml|yml|json)$'
2268
- age: >-
2269
- $age_pubkey
2270
- SOPSEOF
2271
- print_success "Created .sops.yaml with age key"
2272
- else
2273
- cat >"$project_root/.sops.yaml" <<'SOPSEOF'
2274
- # SOPS configuration - encrypts values in config files while keeping keys visible
2275
- # See: .agents/tools/credentials/sops.md
2276
- #
2277
- # Generate an age key first:
2278
- # age-keygen -o ~/.config/sops/age/keys.txt
2279
- #
2280
- # Then replace AGE_PUBLIC_KEY below with your public key:
2281
- creation_rules:
2282
- - path_regex: '\.secret\.(yaml|yml|json)$'
2283
- age: >-
2284
- AGE_PUBLIC_KEY
2285
- - path_regex: 'configs/.*\.enc\.(yaml|yml|json)$'
2286
- age: >-
2287
- AGE_PUBLIC_KEY
2288
- SOPSEOF
2289
- print_warning "Created .sops.yaml template (replace AGE_PUBLIC_KEY with your key)"
2290
- fi
2291
- else
2292
- print_info ".sops.yaml already exists"
2293
- fi
2294
- fi
2295
-
2296
- # Ensure .gitattributes has ai-training=false (opt out of AI model training)
2297
- # GitHub and other platforms respect this attribute to exclude repo content
2298
- # from AI/ML training datasets. Idempotent — only adds if not already present.
2299
- local gitattributes="$project_root/.gitattributes"
2300
- if [[ -f "$gitattributes" ]]; then
2301
- if ! grep -qE '^\*[[:space:]]+ai-training=false' "$gitattributes" 2>/dev/null; then
2302
- ensure_trailing_newline "$gitattributes"
2303
- {
2304
- echo ""
2305
- echo "# Opt out of AI model training"
2306
- echo "* ai-training=false"
2307
- } >>"$gitattributes"
2308
- print_success "Added ai-training=false to .gitattributes"
2309
- else
2310
- print_info ".gitattributes already has ai-training=false"
2311
- fi
2312
- else
2313
- cat >"$gitattributes" <<'GITATTRSEOF'
2314
- # Opt out of AI model training
2315
- * ai-training=false
2316
- GITATTRSEOF
2317
- print_success "Created .gitattributes with ai-training=false"
2318
- fi
2319
-
2320
- # Add aidevops runtime artifacts to .gitignore
2321
- # Note: .agents/ itself is NOT ignored — it contains committed project-specific agents.
2322
- # Only runtime artifacts (loop state, tmp, memory) are ignored.
2323
- local gitignore="$project_root/.gitignore"
2324
- if [[ -f "$gitignore" ]]; then
2325
- local gitignore_updated=false
2326
-
2327
- # Remove legacy bare ".agents" entry if present (was added by older versions)
2328
- if grep -q "^\.agents$" "$gitignore" 2>/dev/null; then
2329
- sed -i '' '/^\.agents$/d' "$gitignore" 2>/dev/null ||
2330
- sed -i '/^\.agents$/d' "$gitignore" 2>/dev/null || true
2331
- # Also remove the "# aidevops" comment if it's now orphaned
2332
- sed -i '' '/^# aidevops$/{ N; /^# aidevops\n$/d; }' "$gitignore" 2>/dev/null || true
2333
- print_info "Removed legacy bare .agents from .gitignore (now tracked)"
2334
- gitignore_updated=true
2335
- fi
2336
-
2337
- # Remove legacy bare ".agent" entry if present
2338
- if grep -q "^\.agent$" "$gitignore" 2>/dev/null; then
2339
- sed -i '' '/^\.agent$/d' "$gitignore" 2>/dev/null ||
2340
- sed -i '/^\.agent$/d' "$gitignore" 2>/dev/null || true
2341
- gitignore_updated=true
2342
- fi
2343
-
2344
- # Add runtime artifact ignores
2345
- if ! grep -q "^\.agents/loop-state/" "$gitignore" 2>/dev/null; then
2346
- # Ensure trailing newline before appending (prevents malformed entries like *.zip.agents/loop-state/)
2347
- ensure_trailing_newline "$gitignore"
2348
- {
2349
- echo ""
2350
- echo "# aidevops runtime artifacts"
2351
- echo ".agents/loop-state/"
2352
- echo ".agents/tmp/"
2353
- echo ".agents/memory/"
2354
- } >>"$gitignore"
2355
- print_success "Added .agents/ runtime artifact ignores to .gitignore"
2356
- gitignore_updated=true
2357
- fi
2358
-
2359
- # Add .aidevops.json to gitignore (local config, not committed).
2360
- # If .aidevops.json is already tracked by git (committed by older framework
2361
- # versions), untrack it first — adding a tracked file to .gitignore is a
2362
- # no-op and the file keeps showing in git diff on every re-init (#2570 bug 3).
2363
- if ! grep -q "^\.aidevops\.json$" "$gitignore" 2>/dev/null; then
2364
- if git -C "$project_root" ls-files --error-unmatch .aidevops.json &>/dev/null; then
2365
- git -C "$project_root" rm --cached .aidevops.json &>/dev/null || true
2366
- print_info "Untracked .aidevops.json from git (was committed by older version)"
2367
- fi
2368
- # Ensure trailing newline before appending
2369
- ensure_trailing_newline "$gitignore"
2370
- echo ".aidevops.json" >>"$gitignore"
2371
- gitignore_updated=true
2372
- fi
2373
-
2374
- # Add .beads if beads is enabled
2375
- if [[ "$enable_beads" == "true" ]]; then
2376
- if ! grep -q "^\.beads$" "$gitignore" 2>/dev/null; then
2377
- # Ensure trailing newline before appending
2378
- ensure_trailing_newline "$gitignore"
2379
- echo ".beads" >>"$gitignore"
2380
- print_success "Added .beads to .gitignore"
2381
- gitignore_updated=true
2382
- fi
2383
- fi
2384
-
2385
- if [[ "$gitignore_updated" == "true" ]]; then
2386
- print_info "Updated .gitignore"
2387
- fi
2388
- fi
2389
-
2390
- # Scaffold optional files gated by init_scope (collaborator pointers,
2391
- # DESIGN.md, courtesy files, MODELS.md). Extracted to reduce cmd_init
2392
- # nesting depth and function length (t2265).
2393
- _init_scaffold_scope_gated_files "$project_root" "$init_scope" "$repo_name"
2394
-
2395
- # Run security posture assessment if enabled (t1412.11)
2396
- if [[ "$enable_security" == "true" ]]; then
2397
- local security_posture_script="$AGENTS_DIR/scripts/security-posture-helper.sh"
2398
- if [[ -f "$security_posture_script" ]]; then
2399
- print_info "Running security posture assessment..."
2400
- if bash "$security_posture_script" store "$project_root"; then
2401
- print_success "Security posture assessed and stored in .aidevops.json"
2402
- else
2403
- print_warning "Security posture assessment found issues (review with: aidevops security audit)"
2404
- fi
2405
- else
2406
- print_info "Security posture check skipped (security-posture-helper.sh not available)"
2407
- fi
2408
- fi
2409
-
2410
- # Build features string for registration
2411
- local features_list=""
2412
- [[ "$enable_planning" == "true" ]] && features_list="${features_list}planning,"
2413
- [[ "$enable_git_workflow" == "true" ]] && features_list="${features_list}git-workflow,"
2414
- [[ "$enable_code_quality" == "true" ]] && features_list="${features_list}code-quality,"
2415
- [[ "$enable_time_tracking" == "true" ]] && features_list="${features_list}time-tracking,"
2416
- [[ "$enable_database" == "true" ]] && features_list="${features_list}database,"
2417
- [[ "$enable_beads" == "true" ]] && features_list="${features_list}beads,"
2418
- [[ "$enable_sops" == "true" ]] && features_list="${features_list}sops,"
2419
- [[ "$enable_security" == "true" ]] && features_list="${features_list}security,"
2420
- features_list="${features_list%,}" # Remove trailing comma
2421
-
2422
- # Register the *main* repo path (not the worktree path) in repos.json.
2423
- # When check_protected_branch creates a worktree and cd's into it,
2424
- # $project_root (resolved via git rev-parse --show-toplevel) points to the
2425
- # worktree directory. We must register the canonical main worktree path so
2426
- # that pulse and cleanup processes don't treat the worktree as a standalone repo.
2427
- local register_path="$project_root"
2428
- if [[ -n "${WORKTREE_PATH:-}" ]]; then
2429
- # We're inside a worktree — resolve the main worktree path from git metadata
2430
- local main_wt_path
2431
- main_wt_path=$(git -C "$project_root" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')
2432
- if [[ -n "$main_wt_path" ]] && [[ "$main_wt_path" != "$project_root" ]]; then
2433
- register_path="$main_wt_path"
2434
- fi
2435
- fi
2436
- register_repo "$register_path" "$aidevops_version" "$features_list"
2437
-
2438
- # Auto-commit initialized files so they don't linger as mystery unstaged
2439
- # changes (#2570 bug 2). Collect all files that cmd_init creates/modifies.
2440
- local init_files=()
2441
- [[ -f "$project_root/.gitattributes" ]] && init_files+=(".gitattributes")
2442
- [[ -f "$project_root/.gitignore" ]] && init_files+=(".gitignore")
2443
- [[ -d "$project_root/.agents" ]] && init_files+=(".agents/")
2444
- [[ -f "$project_root/AGENTS.md" ]] && init_files+=("AGENTS.md")
2445
- [[ -f "$project_root/DESIGN.md" ]] && init_files+=("DESIGN.md")
2446
- [[ -f "$project_root/TODO.md" ]] && init_files+=("TODO.md")
2447
- [[ -d "$project_root/todo" ]] && init_files+=("todo/")
2448
- [[ -f "$project_root/MODELS.md" ]] && init_files+=("MODELS.md")
2449
- [[ -f "$project_root/LICENCE" ]] && init_files+=("LICENCE")
2450
- [[ -f "$project_root/CHANGELOG.md" ]] && init_files+=("CHANGELOG.md")
2451
- [[ -f "$project_root/README.md" ]] && init_files+=("README.md")
2452
- [[ -f "$project_root/.cursorrules" ]] && init_files+=(".cursorrules")
2453
- [[ -f "$project_root/.windsurfrules" ]] && init_files+=(".windsurfrules")
2454
- [[ -f "$project_root/.clinerules" ]] && init_files+=(".clinerules")
2455
- [[ -d "$project_root/.github" ]] && init_files+=(".github/")
2456
- [[ -f "$project_root/.sops.yaml" ]] && init_files+=(".sops.yaml")
2457
- [[ -d "$project_root/schemas" ]] && init_files+=("schemas/")
2458
- [[ -d "$project_root/migrations" ]] && init_files+=("migrations/")
2459
- [[ -d "$project_root/seeds" ]] && init_files+=("seeds/")
2460
-
2461
- local committed=false
2462
- if [[ ${#init_files[@]} -gt 0 ]]; then
2463
- # Stage all init files (--force not needed; .aidevops.json is gitignored above)
2464
- if git -C "$project_root" add -- "${init_files[@]}" 2>/dev/null; then
2465
- # Only commit if there are staged changes
2466
- if ! git -C "$project_root" diff --cached --quiet 2>/dev/null; then
2467
- if git -C "$project_root" commit -m "chore: initialize aidevops v${aidevops_version}" 2>/dev/null; then
2468
- committed=true
2469
- print_success "Committed initialized files"
2470
- else
2471
- print_warning "Auto-commit failed (pre-commit hook rejected?)"
2472
- fi
2473
- fi
2474
- fi
2475
- fi
2476
-
2477
- echo ""
2478
- print_success "AI DevOps initialized! (scope: $init_scope)"
650
+ print_warning "This will remove:"
651
+ echo " - $AGENTS_DIR (deployed agents)"
652
+ echo " - $INSTALL_DIR (repository)"
653
+ echo " - AI assistant configuration references"
654
+ echo " - Shell aliases (if added)"
2479
655
  echo ""
2480
- echo "Enabled features:"
2481
- [[ "$enable_planning" == "true" ]] && echo " ✓ Planning (TODO.md, PLANS.md)"
2482
- [[ "$enable_git_workflow" == "true" ]] && echo " Git workflow (branch management)"
2483
- [[ "$enable_code_quality" == "true" ]] && echo " Code quality (linting, auditing)"
2484
- [[ "$enable_time_tracking" == "true" ]] && echo " ✓ Time tracking (estimates, actuals)"
2485
- [[ "$enable_database" == "true" ]] && echo " ✓ Database (schemas/, migrations/, seeds/)"
2486
- [[ "$enable_beads" == "true" ]] && echo " ✓ Beads (task graph visualization)"
2487
- [[ "$enable_sops" == "true" ]] && echo " ✓ SOPS (encrypted config files with age backend)"
2488
- [[ "$enable_security" == "true" ]] && echo " ✓ Security (per-repo posture assessment)"
2489
- [[ -f "$project_root/MODELS.md" ]] && echo " ✓ MODELS.md (per-repo model performance leaderboard)"
656
+ print_warning "This will NOT remove:"
657
+ echo " - Installed tools (Tabby, Zed, gh, glab, etc.)"
658
+ echo " - SSH keys"
659
+ echo " - Python/Node environments"
2490
660
  echo ""
2491
- # When init ran inside a worktree (check_protected_branch created one),
2492
- # print explicit instructions so the user knows where to find their work.
2493
- # Without this, the user's shell is back in the main repo after aidevops exits
2494
- # and the worktree appears to have "disappeared".
2495
- if [[ -n "${WORKTREE_PATH:-}" ]]; then
2496
- local worktree_branch
2497
- worktree_branch=$(git branch --show-current 2>/dev/null || echo "chore/aidevops-init")
2498
- echo "Worktree location:"
2499
- echo " $WORKTREE_PATH"
2500
- echo ""
2501
- echo "Your init commit is in the worktree above. To continue:"
2502
- echo " cd $WORKTREE_PATH"
2503
- echo " git push -u origin ${worktree_branch}"
2504
- echo " gh pr create --fill"
2505
- echo ""
2506
- fi
2507
- echo "Next steps:"
2508
- local step=1
2509
- if [[ "$committed" != "true" ]]; then
2510
- echo " ${step}. Commit the initialized files: git add -A && git commit -m 'chore: initialize aidevops'"
2511
- ((++step))
2512
- fi
2513
- if [[ "$enable_beads" == "true" ]]; then
2514
- echo " ${step}. Add tasks to TODO.md with dependencies (blocked-by:t001)"
2515
- ((++step))
2516
- echo " ${step}. Run /ready to see unblocked tasks"
2517
- ((++step))
2518
- echo " ${step}. Run /sync-beads to sync with Beads graph"
2519
- ((++step))
2520
- echo " ${step}. Use 'bd' CLI for graph visualization"
2521
- elif [[ "$enable_database" == "true" ]]; then
2522
- echo " ${step}. Add schema files to schemas/"
2523
- ((++step))
2524
- echo " ${step}. Run diff to generate migrations"
2525
- ((++step))
2526
- echo " ${step}. See .agents/workflows/sql-migrations.md"
2527
- else
2528
- echo " ${step}. Add tasks to TODO.md"
2529
- ((++step))
2530
- echo " ${step}. Use /create-prd for complex features"
2531
- ((++step))
2532
- echo " ${step}. Use /feature to start development"
2533
- fi
2534
-
2535
- return 0
661
+ read -r -p "Are you sure you want to uninstall? (yes/no): " confirm
662
+ [[ "$confirm" != "yes" ]] && {
663
+ print_info "Uninstall cancelled"
664
+ return 0
665
+ }
666
+ echo ""
667
+ check_dir "$AGENTS_DIR" && {
668
+ print_info "Removing $AGENTS_DIR..."
669
+ rm -rf "$AGENTS_DIR"
670
+ print_success "Removed agents directory"
671
+ }
672
+ check_dir "$HOME/.aidevops" && {
673
+ print_info "Removing $HOME/.aidevops..."
674
+ rm -rf "$HOME/.aidevops"
675
+ print_success "Removed aidevops config directory"
676
+ }
677
+ _uninstall_cleanup_refs
678
+ echo ""
679
+ read -r -p "Also remove the repository at $INSTALL_DIR? (yes/no): " remove_repo
680
+ if [[ "$remove_repo" == "yes" ]]; then
681
+ check_dir "$INSTALL_DIR" && {
682
+ print_info "Removing $INSTALL_DIR..."
683
+ rm -rf "$INSTALL_DIR"
684
+ print_success "Removed repository"
685
+ }
686
+ else print_info "Keeping repository at $INSTALL_DIR"; fi
687
+ echo ""
688
+ print_success "Uninstall complete!"
689
+ print_info "To reinstall, run:"
690
+ echo " npm install -g aidevops && aidevops update"
691
+ echo " OR: brew install marcusquinn/tap/aidevops && aidevops update"
2536
692
  }
2537
693
 
694
+
2538
695
  # Upgrade planning helpers (extracted for complexity reduction)
2539
696
 
2540
697
  _upgrade_validate() {
@@ -2612,7 +769,7 @@ _extract_todo_section() {
2612
769
 
2613
770
  # t2434: Filter stdin, removing only the literal Format-block placeholder IDs
2614
771
  # (tXXX, tYYY, tZZZ). Real-world repos have historic IDs that don't follow the
2615
- # strict t<digits> shape (e.g. "t059b", "t043-merge" from awardsapp) — we must
772
+ # strict t<digits> shape (e.g. "t059b", "t043-merge" from webapp) — we must
2616
773
  # preserve those. A blocklist is safer than an allowlist here: extraction
2617
774
  # already skips the Format section, so the filter is a secondary guard rather
2618
775
  # than primary validation.
@@ -3173,637 +1330,6 @@ cmd_detect() {
3173
1330
  return 0
3174
1331
  }
3175
1332
 
3176
- # Skill help text (extracted for complexity reduction)
3177
- _skill_help() {
3178
- print_header "Agent Skills Management"
3179
- echo ""
3180
- echo "Import and manage reusable AI agent skills from the community."
3181
- echo "Skills are converted to aidevops format with upstream tracking."
3182
- echo "Telemetry is disabled - no data sent to third parties."
3183
- echo ""
3184
- echo "Usage: aidevops skill <command> [options]"
3185
- echo ""
3186
- echo "Commands:"
3187
- echo " add <source> Import a skill from GitHub (saved as *-skill.md)"
3188
- echo " list List all imported skills"
3189
- echo " check Check for upstream updates"
3190
- echo " update [name] Update specific or all skills"
3191
- echo " remove <name> Remove an imported skill"
3192
- echo " scan [name] Security scan imported skills (Cisco Skill Scanner)"
3193
- echo " status Show detailed skill status"
3194
- echo " generate Generate SKILL.md stubs for cross-tool discovery"
3195
- echo " clean Remove generated SKILL.md stubs"
3196
- echo ""
3197
- echo "Source formats:"
3198
- echo " owner/repo GitHub shorthand"
3199
- echo " owner/repo/path/to/skill Specific skill in multi-skill repo"
3200
- echo " https://github.com/owner/repo Full URL"
3201
- echo ""
3202
- echo "Examples:"
3203
- echo " aidevops skill add vercel-labs/agent-skills"
3204
- echo " aidevops skill add anthropics/skills/pdf"
3205
- echo " aidevops skill add expo/skills --name expo-dev"
3206
- echo " aidevops skill check"
3207
- echo " aidevops skill update"
3208
- echo " aidevops skill scan"
3209
- echo " aidevops skill scan cloudflare-platform"
3210
- echo " aidevops skill generate --dry-run"
3211
- echo ""
3212
- echo "Imported skills are saved with a -skill suffix to distinguish"
3213
- echo "from native aidevops subagents (e.g., playwright-skill.md vs playwright.md)."
3214
- echo ""
3215
- echo "Browse community skills: https://skills.sh"
3216
- echo "Agent Skills specification: https://agentskills.io"
3217
- return 0
3218
- }
3219
-
3220
- _skill_add_usage() {
3221
- print_error "Source required (owner/repo or URL)"
3222
- echo ""
3223
- echo "Usage: aidevops skill add <source> [options]"
3224
- echo ""
3225
- echo "Examples:"
3226
- echo " aidevops skill add vercel-labs/agent-skills"
3227
- echo " aidevops skill add anthropics/skills/pdf"
3228
- echo " aidevops skill add https://github.com/owner/repo"
3229
- echo ""
3230
- echo "Options:"
3231
- echo " --name <name> Override the skill name"
3232
- echo " --force Overwrite existing skill"
3233
- echo " --dry-run Preview without making changes"
3234
- echo ""
3235
- echo "Browse skills: https://skills.sh"
3236
- return 0
3237
- }
3238
-
3239
- # Skill management command
3240
- cmd_skill() {
3241
- local action="${1:-help}"
3242
- shift || true
3243
- export DISABLE_TELEMETRY=1 DO_NOT_TRACK=1 SKILLS_NO_TELEMETRY=1
3244
- local add_skill_script="$AGENTS_DIR/scripts/add-skill-helper.sh"
3245
- local update_skill_script="$AGENTS_DIR/scripts/skill-update-helper.sh"
3246
- case "$action" in
3247
- add | a)
3248
- if [[ $# -lt 1 ]]; then
3249
- _skill_add_usage
3250
- return 1
3251
- fi
3252
- [[ ! -f "$add_skill_script" ]] && {
3253
- print_error "add-skill-helper.sh not found"
3254
- print_info "Run 'aidevops update' to get the latest scripts"
3255
- return 1
3256
- }
3257
- bash "$add_skill_script" add "$@"
3258
- ;;
3259
- list | ls | l)
3260
- [[ ! -f "$add_skill_script" ]] && {
3261
- print_error "add-skill-helper.sh not found"
3262
- return 1
3263
- }
3264
- bash "$add_skill_script" list
3265
- ;;
3266
- check | c)
3267
- [[ ! -f "$update_skill_script" ]] && {
3268
- print_error "skill-update-helper.sh not found"
3269
- return 1
3270
- }
3271
- bash "$update_skill_script" check "$@"
3272
- ;;
3273
- update | u)
3274
- [[ ! -f "$update_skill_script" ]] && {
3275
- print_error "skill-update-helper.sh not found"
3276
- return 1
3277
- }
3278
- bash "$update_skill_script" update "$@"
3279
- ;;
3280
- remove | rm)
3281
- [[ $# -lt 1 ]] && {
3282
- print_error "Skill name required"
3283
- echo "Usage: aidevops skill remove <name>"
3284
- return 1
3285
- }
3286
- [[ ! -f "$add_skill_script" ]] && {
3287
- print_error "add-skill-helper.sh not found"
3288
- return 1
3289
- }
3290
- bash "$add_skill_script" remove "$@"
3291
- ;;
3292
- status | s)
3293
- [[ ! -f "$update_skill_script" ]] && {
3294
- print_error "skill-update-helper.sh not found"
3295
- return 1
3296
- }
3297
- bash "$update_skill_script" status "$@"
3298
- ;;
3299
- generate | gen | g)
3300
- local gs="$AGENTS_DIR/scripts/generate-skills.sh"
3301
- [[ ! -f "$gs" ]] && {
3302
- print_error "generate-skills.sh not found"
3303
- print_info "Run 'aidevops update' to get the latest scripts"
3304
- return 1
3305
- }
3306
- print_info "Generating SKILL.md stubs for cross-tool discovery..."
3307
- bash "$gs" "$@"
3308
- ;;
3309
- scan)
3310
- local ss="$AGENTS_DIR/scripts/security-helper.sh"
3311
- [[ ! -f "$ss" ]] && {
3312
- print_error "security-helper.sh not found"
3313
- print_info "Run 'aidevops update' to get the latest scripts"
3314
- return 1
3315
- }
3316
- bash "$ss" skill-scan "$@"
3317
- ;;
3318
- clean)
3319
- local gs="$AGENTS_DIR/scripts/generate-skills.sh"
3320
- [[ ! -f "$gs" ]] && {
3321
- print_error "generate-skills.sh not found"
3322
- return 1
3323
- }
3324
- bash "$gs" --clean "$@"
3325
- ;;
3326
- help | --help | -h) _skill_help ;;
3327
- *)
3328
- print_error "Unknown skill command: $action"
3329
- echo "Run 'aidevops skill help' for usage information."
3330
- return 1
3331
- ;;
3332
- esac
3333
- }
3334
-
3335
- # Plugin management helpers (extracted for complexity reduction)
3336
- _PLUGIN_RESERVED="custom draft scripts tools services workflows templates memory plugins seo wordpress aidevops"
3337
-
3338
- _plugin_validate_ns() {
3339
- local ns="$1"
3340
- if [[ ! "$ns" =~ ^[a-z][a-z0-9-]*$ ]]; then
3341
- print_error "Invalid namespace '$ns': must be lowercase alphanumeric with hyphens, starting with a letter"
3342
- return 1
3343
- fi
3344
- local r
3345
- for r in $_PLUGIN_RESERVED; do [[ "$ns" == "$r" ]] && {
3346
- print_error "Namespace '$ns' is reserved."
3347
- return 1
3348
- }; done
3349
- return 0
3350
- }
3351
-
3352
- _plugin_field() {
3353
- local pf="$1" n="$2" f="$3"
3354
- jq -r --arg n "$n" --arg f "$f" '.plugins[] | select(.name == $n) | .[$f] // empty' "$pf" 2>/dev/null || echo ""
3355
- }
3356
-
3357
- _plugin_add() {
3358
- local pf="$1" ad="$2"
3359
- shift 2
3360
- if [[ $# -lt 1 ]]; then
3361
- print_error "Repository URL required"
3362
- echo ""
3363
- echo "Usage: aidevops plugin add <repo-url> [options]"
3364
- echo ""
3365
- echo "Options:"
3366
- echo " --namespace <name> Namespace directory (default: derived from repo name)"
3367
- echo " --branch <branch> Branch to track (default: main)"
3368
- echo " --name <name> Human-readable name (default: derived from repo)"
3369
- echo ""
3370
- echo "Examples:"
3371
- echo " aidevops plugin add https://github.com/marcusquinn/aidevops-pro.git --namespace pro"
3372
- echo " aidevops plugin add https://github.com/marcusquinn/aidevops-anon.git --namespace anon"
3373
- return 1
3374
- fi
3375
- local repo_url="$1"
3376
- shift
3377
- local namespace="" branch="main" plugin_name=""
3378
- while [[ $# -gt 0 ]]; do
3379
- case "$1" in
3380
- --namespace | --ns)
3381
- namespace="$2"
3382
- shift 2
3383
- ;;
3384
- --branch | -b)
3385
- branch="$2"
3386
- shift 2
3387
- ;;
3388
- --name | -n)
3389
- plugin_name="$2"
3390
- shift 2
3391
- ;;
3392
- *)
3393
- print_error "Unknown option: $1"
3394
- return 1
3395
- ;;
3396
- esac
3397
- done
3398
- [[ -z "$namespace" ]] && {
3399
- namespace=$(basename "$repo_url" .git | sed 's/^aidevops-//')
3400
- namespace=$(echo "$namespace" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9-]/-/g')
3401
- }
3402
- [[ -z "$plugin_name" ]] && plugin_name="$namespace"
3403
- _plugin_validate_ns "$namespace" || return 1
3404
- local existing
3405
- existing=$(jq -r --arg n "$plugin_name" '.plugins[] | select(.name == $n) | .name' "$pf" 2>/dev/null || echo "")
3406
- [[ -n "$existing" ]] && {
3407
- print_error "Plugin '$plugin_name' already exists. Use 'aidevops plugin update $plugin_name' to update."
3408
- return 1
3409
- }
3410
- if [[ -d "$ad/$namespace" ]]; then
3411
- local ns_owner
3412
- ns_owner=$(jq -r --arg ns "$namespace" '.plugins[] | select(.namespace == $ns) | .name' "$pf" 2>/dev/null || echo "")
3413
- [[ -n "$ns_owner" ]] && print_error "Namespace '$namespace' is already used by plugin '$ns_owner'" || {
3414
- print_error "Directory '$ad/$namespace/' already exists"
3415
- echo " Choose a different namespace with --namespace <name>"
3416
- }
3417
- return 1
3418
- fi
3419
- print_info "Adding plugin '$plugin_name' from $repo_url..."
3420
- print_info " Namespace: $namespace"
3421
- print_info " Branch: $branch"
3422
- local clone_dir="$ad/$namespace"
3423
- if ! git clone --branch "$branch" --depth 1 "$repo_url" "$clone_dir" 2>&1; then
3424
- print_error "Failed to clone repository"
3425
- rm -rf "$clone_dir" 2>/dev/null || true
3426
- return 1
3427
- fi
3428
- rm -rf "$clone_dir/.git"
3429
- local tmp="${pf}.tmp"
3430
- jq --arg name "$plugin_name" --arg repo "$repo_url" --arg branch "$branch" --arg ns "$namespace" \
3431
- '.plugins += [{"name": $name, "repo": $repo, "branch": $branch, "namespace": $ns, "enabled": true}]' "$pf" >"$tmp" && mv "$tmp" "$pf"
3432
- local loader="$ad/scripts/plugin-loader-helper.sh"
3433
- [[ -f "$loader" ]] && bash "$loader" hooks "$namespace" init 2>/dev/null || true
3434
- print_success "Plugin '$plugin_name' installed to $clone_dir"
3435
- echo ""
3436
- echo " Agents available at: ~/.aidevops/agents/$namespace/"
3437
- echo " Update: aidevops plugin update $plugin_name"
3438
- echo " Remove: aidevops plugin remove $plugin_name"
3439
- return 0
3440
- }
3441
-
3442
- _plugin_list() {
3443
- local pf="$1"
3444
- local count
3445
- count=$(jq '.plugins | length' "$pf" 2>/dev/null || echo "0")
3446
- if [[ "$count" == "0" ]]; then
3447
- echo "No plugins installed."
3448
- echo ""
3449
- echo "Add a plugin: aidevops plugin add <repo-url> --namespace <name>"
3450
- return 0
3451
- fi
3452
- echo "Installed plugins ($count):"
3453
- echo ""
3454
- printf " %-15s %-10s %-8s %s\n" "NAME" "NAMESPACE" "ENABLED" "REPO"
3455
- printf " %-15s %-10s %-8s %s\n" "----" "---------" "-------" "----"
3456
- jq -r '.plugins[] | " \(.name)\t\(.namespace)\t\(.enabled // true)\t\(.repo)"' "$pf" 2>/dev/null |
3457
- while IFS=$'\t' read -r name ns enabled repo; do
3458
- local si="yes"
3459
- [[ "$enabled" == "false" ]] && si="no"
3460
- printf " %-15s %-10s %-8s %s\n" "$name" "$ns" "$si" "$repo"
3461
- done
3462
- return 0
3463
- }
3464
-
3465
- _plugin_update() {
3466
- local pf="$1" ad="$2" target="${3:-}"
3467
- if [[ -n "$target" ]]; then
3468
- local repo ns bn
3469
- repo=$(_plugin_field "$pf" "$target" "repo")
3470
- ns=$(_plugin_field "$pf" "$target" "namespace")
3471
- bn=$(_plugin_field "$pf" "$target" "branch")
3472
- bn="${bn:-main}"
3473
- [[ -z "$repo" ]] && {
3474
- print_error "Plugin '$target' not found"
3475
- return 1
3476
- }
3477
- print_info "Updating plugin '$target'..."
3478
- local cd2="$ad/$ns"
3479
- rm -rf "$cd2"
3480
- if git clone --branch "$bn" --depth 1 "$repo" "$cd2" 2>&1; then
3481
- rm -rf "$cd2/.git"
3482
- print_success "Plugin '$target' updated"
3483
- else
3484
- print_error "Failed to update plugin '$target'"
3485
- return 1
3486
- fi
3487
- else
3488
- local names
3489
- names=$(jq -r '.plugins[] | select(.enabled != false) | .name' "$pf" 2>/dev/null || echo "")
3490
- [[ -z "$names" ]] && {
3491
- echo "No enabled plugins to update."
3492
- return 0
3493
- }
3494
- local failed=0
3495
- while IFS= read -r pn; do
3496
- [[ -z "$pn" ]] && continue
3497
- local pr pns pb
3498
- pr=$(_plugin_field "$pf" "$pn" "repo")
3499
- pns=$(_plugin_field "$pf" "$pn" "namespace")
3500
- pb=$(_plugin_field "$pf" "$pn" "branch")
3501
- pb="${pb:-main}"
3502
- print_info "Updating '$pn'..."
3503
- local pd="$ad/$pns"
3504
- rm -rf "$pd"
3505
- if git clone --branch "$pb" --depth 1 "$pr" "$pd" 2>/dev/null; then
3506
- rm -rf "$pd/.git"
3507
- print_success " '$pn' updated"
3508
- else
3509
- print_error " '$pn' failed to update"
3510
- failed=$((failed + 1))
3511
- fi
3512
- done <<<"$names"
3513
- [[ "$failed" -gt 0 ]] && {
3514
- print_warning "$failed plugin(s) failed to update"
3515
- return 1
3516
- }
3517
- print_success "All plugins updated"
3518
- fi
3519
- return 0
3520
- }
3521
-
3522
- _plugin_toggle() {
3523
- local pf="$1" ad="$2" tn="$3" action="$4"
3524
- if [[ "$action" == "enable" ]]; then
3525
- local tr
3526
- tr=$(_plugin_field "$pf" "$tn" "repo")
3527
- [[ -z "$tr" ]] && {
3528
- print_error "Plugin '$tn' not found"
3529
- return 1
3530
- }
3531
- local tns
3532
- tns=$(_plugin_field "$pf" "$tn" "namespace")
3533
- local tb
3534
- tb=$(_plugin_field "$pf" "$tn" "branch")
3535
- tb="${tb:-main}"
3536
- local tmp="${pf}.tmp"
3537
- jq --arg n "$tn" '(.plugins[] | select(.name == $n)).enabled = true' "$pf" >"$tmp" && mv "$tmp" "$pf"
3538
- [[ ! -d "$ad/$tns" ]] && {
3539
- print_info "Deploying plugin '$tn'..."
3540
- git clone --branch "$tb" --depth 1 "$tr" "$ad/$tns" 2>/dev/null && rm -rf "$ad/$tns/.git"
3541
- }
3542
- local loader="$ad/scripts/plugin-loader-helper.sh"
3543
- [[ -f "$loader" ]] && bash "$loader" hooks "$tns" init 2>/dev/null || true
3544
- print_success "Plugin '$tn' enabled"
3545
- else
3546
- local tns
3547
- tns=$(_plugin_field "$pf" "$tn" "namespace")
3548
- [[ -z "$tns" ]] && {
3549
- print_error "Plugin '$tn' not found"
3550
- return 1
3551
- }
3552
- local loader="$ad/scripts/plugin-loader-helper.sh"
3553
- [[ -f "$loader" && -d "$ad/$tns" ]] && bash "$loader" hooks "$tns" unload 2>/dev/null || true
3554
- local tmp="${pf}.tmp"
3555
- jq --arg n "$tn" '(.plugins[] | select(.name == $n)).enabled = false' "$pf" >"$tmp" && mv "$tmp" "$pf"
3556
- [[ -d "$ad/${tns:?}" ]] && rm -rf "$ad/${tns:?}"
3557
- print_success "Plugin '$tn' disabled (config preserved)"
3558
- fi
3559
- return 0
3560
- }
3561
-
3562
- _plugin_remove() {
3563
- local pf="$1" ad="$2" tn="$3"
3564
- local tns
3565
- tns=$(_plugin_field "$pf" "$tn" "namespace")
3566
- [[ -z "$tns" ]] && {
3567
- print_error "Plugin '$tn' not found"
3568
- return 1
3569
- }
3570
- local loader="$ad/scripts/plugin-loader-helper.sh"
3571
- [[ -f "$loader" && -d "$ad/$tns" ]] && bash "$loader" hooks "$tns" unload 2>/dev/null || true
3572
- [[ -d "$ad/${tns:?}" ]] && {
3573
- rm -rf "$ad/${tns:?}"
3574
- print_info "Removed $ad/$tns/"
3575
- }
3576
- local tmp="${pf}.tmp"
3577
- jq --arg n "$tn" '.plugins = [.plugins[] | select(.name != $n)]' "$pf" >"$tmp" && mv "$tmp" "$pf"
3578
- print_success "Plugin '$tn' removed"
3579
- return 0
3580
- }
3581
-
3582
- _plugin_scaffold() {
3583
- local ad="$1" td="${2:-.}" pn="${3:-my-plugin}"
3584
- local ns="${4:-$pn}"
3585
- if [[ "$td" != "." && -d "$td" ]]; then
3586
- local ec
3587
- ec=$(find "$td" -maxdepth 1 -type f | wc -l | tr -d ' ')
3588
- [[ "$ec" -gt 0 ]] && {
3589
- print_error "Directory '$td' already has files. Use an empty directory."
3590
- return 1
3591
- }
3592
- fi
3593
- mkdir -p "$td"
3594
- local tpl="$ad/templates/plugin-template"
3595
- [[ ! -d "$tpl" ]] && {
3596
- print_error "Plugin template not found at $tpl"
3597
- print_info "Run 'aidevops update' to get the latest templates."
3598
- return 1
3599
- }
3600
- local pnu
3601
- pnu=$(echo "$pn" | tr '[:lower:]' '[:upper:]' | tr '-' '_')
3602
- sed -e "s|{{PLUGIN_NAME}}|$pn|g" -e "s|{{PLUGIN_NAME_UPPER}}|$pnu|g" -e "s|{{NAMESPACE}}|$ns|g" -e "s|{{REPO_URL}}|https://github.com/user/aidevops-$ns.git|g" "$tpl/AGENTS.md" >"$td/AGENTS.md"
3603
- sed -e "s|{{PLUGIN_NAME}}|$pn|g" -e "s|{{PLUGIN_DESCRIPTION}}|$pn plugin for aidevops|g" -e "s|{{NAMESPACE}}|$ns|g" "$tpl/main-agent.md" >"$td/$ns.md"
3604
- mkdir -p "$td/$ns"
3605
- sed -e "s|{{PLUGIN_NAME}}|$pn|g" -e "s|{{NAMESPACE}}|$ns|g" "$tpl/example-subagent.md" >"$td/$ns/example.md"
3606
- mkdir -p "$td/scripts"
3607
- if [[ -d "$tpl/scripts" ]]; then
3608
- for hf in "$tpl/scripts"/on-*.sh; do
3609
- [[ -f "$hf" ]] || continue
3610
- local hb
3611
- hb=$(basename "$hf")
3612
- sed -e "s|{{PLUGIN_NAME}}|$pn|g" -e "s|{{NAMESPACE}}|$ns|g" "$hf" >"$td/scripts/$hb"
3613
- chmod +x "$td/scripts/$hb"
3614
- done
3615
- fi
3616
- [[ -f "$tpl/plugin.json" ]] && sed -e "s|{{PLUGIN_NAME}}|$pn|g" -e "s|{{PLUGIN_DESCRIPTION}}|$pn plugin for aidevops|g" -e "s|{{NAMESPACE}}|$ns|g" "$tpl/plugin.json" >"$td/plugin.json"
3617
- print_success "Plugin scaffolded in $td/"
3618
- echo ""
3619
- echo "Structure:"
3620
- echo " $td/"
3621
- echo " ├── AGENTS.md # Plugin documentation"
3622
- echo " ├── plugin.json # Plugin manifest"
3623
- echo " ├── $ns.md # Main agent"
3624
- echo " ├── $ns/"
3625
- echo " │ └── example.md # Example subagent"
3626
- echo " └── scripts/"
3627
- echo " ├── on-init.sh # Init lifecycle hook"
3628
- echo " ├── on-load.sh # Load lifecycle hook"
3629
- echo " └── on-unload.sh # Unload lifecycle hook"
3630
- echo ""
3631
- echo "Next steps:"
3632
- echo " 1. Edit plugin.json with your plugin metadata"
3633
- echo " 2. Edit $ns.md with your agent instructions"
3634
- echo " 3. Add subagents to $ns/"
3635
- echo " 4. Push to a git repo"
3636
- echo " 5. Install: aidevops plugin add <repo-url> --namespace $ns"
3637
- return 0
3638
- }
3639
-
3640
- _plugin_help() {
3641
- print_header "Plugin Management"
3642
- echo ""
3643
- echo "Manage third-party agent plugins that extend aidevops."
3644
- echo "Plugins deploy to ~/.aidevops/agents/<namespace>/ (isolated from core)."
3645
- echo ""
3646
- echo "Usage: aidevops plugin <command> [options]"
3647
- echo ""
3648
- echo "Commands:"
3649
- echo " add <repo-url> Install a plugin from a git repository"
3650
- echo " list List installed plugins"
3651
- echo " update [name] Update specific or all plugins"
3652
- echo " enable <name> Enable a disabled plugin (redeploys files)"
3653
- echo " disable <name> Disable a plugin (removes files, keeps config)"
3654
- echo " remove <name> Remove a plugin entirely"
3655
- echo " init [dir] [name] [namespace] Scaffold a new plugin from template"
3656
- echo ""
3657
- echo "Options for 'add':"
3658
- echo " --namespace <name> Directory name under ~/.aidevops/agents/"
3659
- echo " --branch <branch> Branch to track (default: main)"
3660
- echo " --name <name> Human-readable plugin name"
3661
- echo ""
3662
- echo "Examples:"
3663
- echo " aidevops plugin add https://github.com/marcusquinn/aidevops-pro.git --namespace pro"
3664
- echo " aidevops plugin add https://github.com/marcusquinn/aidevops-anon.git --namespace anon"
3665
- echo " aidevops plugin list"
3666
- echo " aidevops plugin update"
3667
- echo " aidevops plugin update pro"
3668
- echo " aidevops plugin disable pro"
3669
- echo " aidevops plugin enable pro"
3670
- echo " aidevops plugin remove pro"
3671
- echo " aidevops plugin init ./my-plugin my-plugin my-plugin"
3672
- echo ""
3673
- echo "Plugin docs: ~/.aidevops/agents/aidevops/plugins.md"
3674
- return 0
3675
- }
3676
-
3677
- # Plugin management command
3678
- cmd_plugin() {
3679
- local action="${1:-help}"
3680
- shift || true
3681
- local pf="$CONFIG_DIR/plugins.json" ad="$AGENTS_DIR"
3682
- mkdir -p "$CONFIG_DIR"
3683
- [[ ! -f "$pf" ]] && echo '{"plugins":[]}' >"$pf"
3684
- case "$action" in
3685
- add | a) _plugin_add "$pf" "$ad" "$@" ;;
3686
- list | ls | l) _plugin_list "$pf" ;;
3687
- update | u) _plugin_update "$pf" "$ad" "$@" ;;
3688
- enable)
3689
- [[ $# -lt 1 ]] && {
3690
- print_error "Plugin name required"
3691
- echo "Usage: aidevops plugin enable <name>"
3692
- return 1
3693
- }
3694
- _plugin_toggle "$pf" "$ad" "$1" enable
3695
- ;;
3696
- disable)
3697
- [[ $# -lt 1 ]] && {
3698
- print_error "Plugin name required"
3699
- echo "Usage: aidevops plugin disable <name>"
3700
- return 1
3701
- }
3702
- _plugin_toggle "$pf" "$ad" "$1" disable
3703
- ;;
3704
- remove | rm)
3705
- [[ $# -lt 1 ]] && {
3706
- print_error "Plugin name required"
3707
- echo "Usage: aidevops plugin remove <name>"
3708
- return 1
3709
- }
3710
- _plugin_remove "$pf" "$ad" "$1"
3711
- ;;
3712
- init) _plugin_scaffold "$ad" "$@" ;;
3713
- help | --help | -h) _plugin_help ;;
3714
- *)
3715
- print_error "Unknown plugin command: $action"
3716
- echo "Run 'aidevops plugin help' for usage information."
3717
- return 1
3718
- ;;
3719
- esac
3720
- return 0
3721
- }
3722
-
3723
- # Skills discovery command - search, browse, describe installed skills
3724
- cmd_skills() {
3725
- local action="${1:-help}"
3726
- shift || true
3727
-
3728
- local skills_helper="$AGENTS_DIR/scripts/skills-helper.sh"
3729
-
3730
- if [[ ! -f "$skills_helper" ]]; then
3731
- print_error "skills-helper.sh not found"
3732
- print_info "Run 'aidevops update' to get the latest scripts"
3733
- return 1
3734
- fi
3735
-
3736
- case "$action" in
3737
- search | s | find | f)
3738
- bash "$skills_helper" search "$@"
3739
- ;;
3740
- browse | b)
3741
- bash "$skills_helper" browse "$@"
3742
- ;;
3743
- describe | desc | d | show)
3744
- bash "$skills_helper" describe "$@"
3745
- ;;
3746
- info | i | meta)
3747
- bash "$skills_helper" info "$@"
3748
- ;;
3749
- list | ls | l)
3750
- bash "$skills_helper" list "$@"
3751
- ;;
3752
- categories | cats | cat)
3753
- bash "$skills_helper" categories "$@"
3754
- ;;
3755
- recommend | rec | suggest)
3756
- bash "$skills_helper" recommend "$@"
3757
- ;;
3758
- install | add)
3759
- bash "$skills_helper" install "$@"
3760
- ;;
3761
- registry | online)
3762
- bash "$skills_helper" registry "$@"
3763
- ;;
3764
- help | --help | -h)
3765
- print_header "Skill Discovery & Exploration"
3766
- echo ""
3767
- echo "Discover, explore, and get recommendations for installed skills."
3768
- echo "For importing/managing skills, use: aidevops skill <cmd>"
3769
- echo ""
3770
- echo "Usage: aidevops skills <command> [options]"
3771
- echo ""
3772
- echo "Commands:"
3773
- echo " search <query> Search installed skills by keyword"
3774
- echo " search --registry <q> Search the public skills.sh registry (online)"
3775
- echo " browse [category] Browse skills by category"
3776
- echo " describe <name> Show detailed skill description"
3777
- echo " info <name> Show skill metadata (path, source, model tier)"
3778
- echo " list [filter] List skills (--imported, --native, --all)"
3779
- echo " categories List all categories with skill counts"
3780
- echo " recommend <task> Suggest skills for a task description"
3781
- echo " install <owner/repo@s> Install a skill from the public registry"
3782
- echo ""
3783
- echo "Options:"
3784
- echo " --json Output in JSON format (for scripting)"
3785
- echo " --registry, --online Search the public skills.sh registry"
3786
- echo ""
3787
- echo "Examples:"
3788
- echo " aidevops skills search \"browser automation\""
3789
- echo " aidevops skills search --registry \"seo\""
3790
- echo " aidevops skills browse tools"
3791
- echo " aidevops skills browse tools/browser"
3792
- echo " aidevops skills describe playwright"
3793
- echo " aidevops skills info seo-audit-skill"
3794
- echo " aidevops skills list --imported"
3795
- echo " aidevops skills categories"
3796
- echo " aidevops skills recommend \"deploy a Next.js app\""
3797
- echo " aidevops skills install vercel-labs/agent-browser@agent-browser"
3798
- echo ""
3799
- echo "See also: aidevops skill help (import/manage skills)"
3800
- ;;
3801
- *)
3802
- # Treat unknown action as a search query
3803
- bash "$skills_helper" search "$action $*"
3804
- ;;
3805
- esac
3806
- }
3807
1333
 
3808
1334
  # Help text helpers (extracted for complexity reduction)
3809
1335
  _help_commands() {