aidevops 3.13.95 → 3.14.1

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.
@@ -1,700 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
3
- # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
- # =============================================================================
5
- # aidevops Repo Management Library
6
- # =============================================================================
7
- # Repository registration, discovery, and validation functions extracted from
8
- # aidevops.sh to keep the orchestrator under the 2000-line file-size threshold.
9
- #
10
- # Covers:
11
- # 1. init_repos_file / get_repo_slug / register_repo / get_registered_repos
12
- # 2. Repo defaults, scope, mission-control resolution
13
- # 3. Planning file checks, protected-branch validation
14
- #
15
- # Usage: source "${SCRIPT_DIR}/aidevops-repos-lib.sh"
16
- #
17
- # Dependencies:
18
- # - INSTALL_DIR, AGENTS_DIR, CONFIG_DIR, REPOS_FILE (set by aidevops.sh)
19
- # - print_* helpers and utility functions (defined in aidevops.sh before sourcing)
20
- #
21
- # Part of aidevops framework: https://aidevops.sh
22
-
23
- # Apply strict mode only when executed directly (not when sourced)
24
- [[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
25
-
26
- # Include guard
27
- [[ -n "${_AIDEVOPS_REPOS_LIB_LOADED:-}" ]] && return 0
28
- _AIDEVOPS_REPOS_LIB_LOADED=1
29
-
30
-
31
- # Initialize repos.json if it doesn't exist
32
- init_repos_file() {
33
- if [[ ! -f "$REPOS_FILE" ]]; then
34
- mkdir -p "$CONFIG_DIR"
35
- echo '{"initialized_repos": [], "git_parent_dirs": ["~/Git"]}' >"$REPOS_FILE"
36
- elif command -v jq &>/dev/null; then
37
- # Migrate: add git_parent_dirs if missing from existing repos.json
38
- if ! jq -e '.git_parent_dirs' "$REPOS_FILE" &>/dev/null; then
39
- local temp_file="${REPOS_FILE}.tmp"
40
- if jq '. + {"git_parent_dirs": ["~/Git"]}' "$REPOS_FILE" >"$temp_file"; then
41
- mv "$temp_file" "$REPOS_FILE"
42
- else
43
- rm -f "$temp_file"
44
- fi
45
- fi
46
- # Migrate: backfill slug for entries missing it (detect from git remote)
47
- local needs_slug
48
- needs_slug=$(jq '[.initialized_repos[] | select(.slug == null or .slug == "")] | length' "$REPOS_FILE" 2>/dev/null) || needs_slug="0"
49
- if [[ "$needs_slug" -gt 0 ]]; then
50
- local temp_file="${REPOS_FILE}.tmp"
51
- local repo_path slug
52
- # Build a map of path->slug for repos missing slugs
53
- while IFS= read -r repo_path; do
54
- # Expand ~ to $HOME for git operations
55
- local expanded_path="${repo_path/#\~/$HOME}"
56
- slug=$(get_repo_slug "$expanded_path" 2>/dev/null) || slug=""
57
- if [[ -n "$slug" ]]; then
58
- jq --arg path "$repo_path" --arg slug "$slug" \
59
- '(.initialized_repos[] | select(.path == $path and (.slug == null or .slug == ""))) |= . + {slug: $slug}' \
60
- "$REPOS_FILE" >"$temp_file" && mv "$temp_file" "$REPOS_FILE"
61
- fi
62
- done < <(jq -r '.initialized_repos[] | select(.slug == null or .slug == "") | .path' "$REPOS_FILE" 2>/dev/null)
63
- fi
64
- fi
65
- return 0
66
- }
67
-
68
- # Detect GitHub slug (owner/repo) from git remote origin
69
- # Usage: get_repo_slug <path>
70
- get_repo_slug() {
71
- local repo_path="$1"
72
- local remote_url
73
- remote_url=$(git -C "$repo_path" remote get-url origin 2>/dev/null) || return 1
74
- # Strip protocol/host prefix and .git suffix to get owner/repo
75
- local slug
76
- slug=$(echo "$remote_url" | sed 's|.*github\.com[:/]||;s|\.git$||')
77
- if [[ -n "$slug" && "$slug" == *"/"* ]]; then
78
- echo "$slug"
79
- return 0
80
- fi
81
- return 1
82
- }
83
-
84
- # Check whether a repo name follows mission-control naming.
85
- # Usage: _is_mission_control_repo_name <repo-name>
86
- _is_mission_control_repo_name() {
87
- local repo_name="$1"
88
- case "$repo_name" in
89
- mission-control | *-mission-control | mission-control-*)
90
- return 0
91
- ;;
92
- *)
93
- return 1
94
- ;;
95
- esac
96
- }
97
-
98
- # Resolve mission-control scope from slug and current actor.
99
- # Usage: _resolve_mission_control_scope <owner/repo> <current-login>
100
- # Prints: personal | org (or empty if not mission-control)
101
- _resolve_mission_control_scope() {
102
- local slug="$1"
103
- local current_login="$2"
104
-
105
- if [[ -z "$slug" ]] || [[ "$slug" != */* ]]; then
106
- echo ""
107
- return 1
108
- fi
109
-
110
- local owner repo
111
- owner="${slug%%/*}"
112
- repo="${slug##*/}"
113
-
114
- if ! _is_mission_control_repo_name "$repo"; then
115
- echo ""
116
- return 1
117
- fi
118
-
119
- if [[ -n "$current_login" && "$owner" == "$current_login" ]]; then
120
- echo "personal"
121
- return 0
122
- fi
123
-
124
- echo "org"
125
- return 0
126
- }
127
-
128
- # Compute default repos.json registration values.
129
- # Usage: _compute_repo_registration_defaults <path> <slug> <local_only> <maintainer>
130
- # Prints eval-safe key=value lines: DEFAULT_PULSE, DEFAULT_PRIORITY
131
- _compute_repo_registration_defaults() {
132
- local repo_path="$1"
133
- local slug="$2"
134
- local is_local_only="$3"
135
- local maintainer="$4"
136
-
137
- local default_pulse=false
138
- local default_priority=""
139
-
140
- if [[ "$is_local_only" == "true" ]]; then
141
- default_pulse=false
142
- else
143
- default_pulse=true
144
- fi
145
-
146
- if [[ "$slug" == */* ]]; then
147
- local owner repo
148
- owner="${slug%%/*}"
149
- repo="${slug##*/}"
150
-
151
- if [[ "$repo" == "$owner" ]] && [[ "$repo_path" == "$HOME/Git/$owner" ]]; then
152
- default_pulse=false
153
- default_priority="profile"
154
- elif _is_mission_control_repo_name "$repo"; then
155
- default_pulse=true
156
- if [[ "$owner" == "$maintainer" ]]; then
157
- default_priority="product"
158
- else
159
- default_priority="tooling"
160
- fi
161
- fi
162
- fi
163
-
164
- printf 'DEFAULT_PULSE=%q\n' "$default_pulse"
165
- printf 'DEFAULT_PRIORITY=%q\n' "$default_priority"
166
- return 0
167
- }
168
-
169
- # Infer the init_scope for a repo when not explicitly set.
170
- # Priority: .aidevops.json > repos.json entry > context inference.
171
- # Returns one of: minimal, standard, public
172
- # Usage: _infer_init_scope <project_root> [is_local_only]
173
- # Pass is_local_only="true" when the caller already has it to avoid redundant I/O.
174
- _infer_init_scope() {
175
- local project_root="$1"
176
- local is_local_only="${2:-}"
177
-
178
- # 1. Check .aidevops.json
179
- if [[ -f "$project_root/.aidevops.json" ]]; then
180
- local json_scope
181
- json_scope=$(jq -r '.init_scope // empty' "$project_root/.aidevops.json" 2>/dev/null || echo "")
182
- if [[ -n "$json_scope" ]]; then
183
- echo "$json_scope"
184
- return 0
185
- fi
186
- fi
187
-
188
- # 2. Check repos.json entry — single jq pass reads both init_scope and local_only
189
- if command -v jq &>/dev/null && [[ -f "${REPOS_FILE:-$HOME/.config/aidevops/repos.json}" ]]; then
190
- local repos_file="${REPOS_FILE:-$HOME/.config/aidevops/repos.json}"
191
- local canonical_path
192
- canonical_path=$(cd "$project_root" 2>/dev/null && pwd -P) || canonical_path="$project_root"
193
- local repo_data
194
- repo_data=$(jq -r --arg path "$canonical_path" \
195
- '.initialized_repos[] | select(.path == $path) | "\(.init_scope // "")|\(.local_only // "false")"' \
196
- "$repos_file" 2>/dev/null | head -n 1 || echo "")
197
- if [[ -n "$repo_data" ]]; then
198
- local repo_scope="${repo_data%|*}"
199
- local repo_local="${repo_data#*|}"
200
- if [[ -n "$repo_scope" ]]; then
201
- echo "$repo_scope"
202
- return 0
203
- fi
204
- # Repo found but no explicit scope — pick up local_only for context inference below
205
- [[ -z "$is_local_only" ]] && is_local_only="$repo_local"
206
- fi
207
- fi
208
-
209
- # 3. Context inference
210
- # Use pre-computed is_local_only when available; fall back to git remote check
211
- if [[ "$is_local_only" == "true" ]]; then
212
- echo "minimal"
213
- return 0
214
- fi
215
-
216
- if ! git -C "$project_root" remote get-url origin &>/dev/null 2>&1; then
217
- echo "minimal"
218
- return 0
219
- fi
220
-
221
- # Default: standard (backward compatible)
222
- echo "standard"
223
- return 0
224
- }
225
-
226
- # Check whether a given scope level includes a feature tier.
227
- # Scope hierarchy: minimal < standard < public
228
- # Usage: _scope_includes <current_scope> <required_level>
229
- # Returns 0 (true) if current_scope >= required_level, 1 (false) otherwise.
230
- _scope_includes() {
231
- local current="$1"
232
- local required="$2"
233
-
234
- # Map scope to numeric level
235
- local current_level=0 required_level=0
236
- case "$current" in
237
- minimal) current_level=0 ;;
238
- standard) current_level=1 ;;
239
- public) current_level=2 ;;
240
- *) current_level=1 ;; # unknown defaults to standard
241
- esac
242
- case "$required" in
243
- minimal) required_level=0 ;;
244
- standard) required_level=1 ;;
245
- public) required_level=2 ;;
246
- *) required_level=1 ;;
247
- esac
248
-
249
- if [[ $current_level -ge $required_level ]]; then
250
- return 0
251
- fi
252
- return 1
253
- }
254
-
255
- # Check whether a repo is marked as an agent source repo in local project
256
- # config or repos.json. Agent source repos use the same organization model as
257
- # the core `.agents/` tree and receive safe template seeding/updating.
258
- # Usage: is_agent_source_repo <project_root>
259
- is_agent_source_repo() {
260
- local project_root="$1"
261
-
262
- if command -v jq &>/dev/null && [[ -f "$project_root/.aidevops.json" ]]; then
263
- local project_flag
264
- project_flag=$(jq -r 'if .agent_source == true or .role == "agent-source" then "true" else "false" end' "$project_root/.aidevops.json" 2>/dev/null || echo "false")
265
- if [[ "$project_flag" == "true" ]]; then
266
- return 0
267
- fi
268
- fi
269
-
270
- if command -v jq &>/dev/null && [[ -f "${REPOS_FILE:-$HOME/.config/aidevops/repos.json}" ]]; then
271
- local repos_file="${REPOS_FILE:-$HOME/.config/aidevops/repos.json}"
272
- local canonical_path
273
- canonical_path=$(cd "$project_root" 2>/dev/null && pwd -P) || canonical_path="$project_root"
274
- local repo_flag
275
- repo_flag=$(jq -r --arg path "$canonical_path" --arg raw_path "$project_root" '
276
- .initialized_repos // []
277
- | map(select(.path == $path or .path == $raw_path))
278
- | if length > 0 and (.[0].agent_source == true or .[0].role == "agent-source") then "true" else "false" end
279
- ' "$repos_file" 2>/dev/null || echo "false")
280
- if [[ "$repo_flag" == "true" ]]; then
281
- return 0
282
- fi
283
- fi
284
-
285
- return 1
286
- }
287
-
288
- # Print registered repo paths marked as agent source repos.
289
- # Usage: get_agent_source_repos
290
- get_agent_source_repos() {
291
- init_repos_file
292
-
293
- if ! command -v jq &>/dev/null; then
294
- return 0
295
- fi
296
-
297
- jq -r '
298
- .initialized_repos // []
299
- | .[]
300
- | select(.agent_source == true or .role == "agent-source")
301
- | .path // empty
302
- ' "$REPOS_FILE" 2>/dev/null || true
303
- return 0
304
- }
305
-
306
- # Resolve a worktree path to its canonical main-worktree path, if applicable.
307
- # Usage: resolve_canonical_repo_path <path>
308
- # Prints the canonical path to stdout. If the input is already the main
309
- # worktree, a non-git path, or git is unavailable, prints the input unchanged.
310
- #
311
- # Why this exists: `find ~/Git -name .aidevops.json` in auto-discovery and
312
- # similar scans pick up .aidevops.json files that exist in linked worktrees
313
- # (because worktrees inherit the working tree contents), and without this
314
- # guard each worktree gets registered as a separate repo. That's what caused
315
- # tabby-profile-sync to emit a profile for a worktree directory.
316
- resolve_canonical_repo_path() {
317
- local input_path="$1"
318
- local common_dir
319
- common_dir=$(git -C "$input_path" rev-parse --git-common-dir 2>/dev/null) || {
320
- printf '%s\n' "$input_path"
321
- return 0
322
- }
323
- local own_git_dir
324
- own_git_dir=$(git -C "$input_path" rev-parse --git-dir 2>/dev/null) || {
325
- printf '%s\n' "$input_path"
326
- return 0
327
- }
328
-
329
- # Resolve both to absolute paths for a reliable comparison.
330
- # git -C <path> returns paths relative to <path> when they are relative.
331
- local common_abs own_abs
332
- if [[ "$common_dir" = /* ]]; then
333
- common_abs=$(cd "$common_dir" 2>/dev/null && pwd -P)
334
- else
335
- common_abs=$(cd "$input_path/$common_dir" 2>/dev/null && pwd -P)
336
- fi
337
- if [[ "$own_git_dir" = /* ]]; then
338
- own_abs=$(cd "$own_git_dir" 2>/dev/null && pwd -P)
339
- else
340
- own_abs=$(cd "$input_path/$own_git_dir" 2>/dev/null && pwd -P)
341
- fi
342
-
343
- if [[ -z "$common_abs" || -z "$own_abs" || "$common_abs" == "$own_abs" ]]; then
344
- # Main worktree or degraded resolution — pass through.
345
- printf '%s\n' "$input_path"
346
- return 0
347
- fi
348
-
349
- # Linked worktree — ask git for the main worktree's working tree path.
350
- local main_path
351
- main_path=$(git -C "$input_path" worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')
352
- if [[ -n "$main_path" && "$main_path" != "$input_path" && -d "$main_path" ]]; then
353
- printf '%s\n' "$main_path"
354
- return 0
355
- fi
356
-
357
- printf '%s\n' "$input_path"
358
- return 0
359
- }
360
-
361
- # Register a repo in repos.json
362
- # Usage: register_repo <path> <version> <features>
363
- register_repo() {
364
- local repo_path="$1"
365
- local version="$2"
366
- local features="$3"
367
-
368
- init_repos_file
369
-
370
- # Normalize path (resolve symlinks, remove trailing slash)
371
- if ! repo_path=$(cd "$repo_path" 2>/dev/null && pwd -P); then
372
- print_warning "Cannot access path: $repo_path"
373
- return 1
374
- fi
375
-
376
- # Resolve linked worktrees to their canonical main-worktree path.
377
- # Every registration path (cmd_init, auto-discovery, scan) runs through
378
- # register_repo, so the guard here catches all of them — not just the
379
- # cmd_init path that previously checked only when WORKTREE_PATH was set.
380
- local canonical_path
381
- canonical_path=$(resolve_canonical_repo_path "$repo_path")
382
- if [[ -n "$canonical_path" && "$canonical_path" != "$repo_path" ]]; then
383
- print_info "Resolved worktree to canonical repo: $repo_path → $canonical_path"
384
- if ! repo_path=$(cd "$canonical_path" 2>/dev/null && pwd -P); then
385
- print_warning "Cannot access canonical path: $canonical_path"
386
- return 1
387
- fi
388
- fi
389
-
390
- if ! command -v jq &>/dev/null; then
391
- print_warning "jq not installed - repo tracking disabled"
392
- return 0
393
- fi
394
-
395
- # Auto-detect GitHub slug from git remote
396
- local slug=""
397
- local is_local_only="false"
398
- if ! slug=$(get_repo_slug "$repo_path" 2>/dev/null); then
399
- slug=""
400
- # No remote origin — mark as local_only
401
- if ! git -C "$repo_path" remote get-url origin &>/dev/null; then
402
- is_local_only="true"
403
- fi
404
- fi
405
-
406
- # Auto-detect maintainer from gh API (current authenticated user)
407
- # Only runs once per registration — preserved on subsequent updates
408
- local maintainer=""
409
- if command -v gh &>/dev/null; then
410
- maintainer=$(gh api user --jq '.login' 2>/dev/null) || maintainer=""
411
- fi
412
-
413
- local DEFAULT_PULSE="false"
414
- local DEFAULT_PRIORITY=""
415
- eval "$(_compute_repo_registration_defaults "$repo_path" "$slug" "$is_local_only" "$maintainer")"
416
-
417
- # Infer default init_scope; pass is_local_only (already computed) to skip redundant I/O
418
- local default_init_scope
419
- default_init_scope=$(_infer_init_scope "$repo_path" "$is_local_only")
420
-
421
- # Check if repo already registered
422
- if jq -e --arg path "$repo_path" '.initialized_repos[] | select(.path == $path)' "$REPOS_FILE" &>/dev/null; then
423
- # Update existing entry, preserving pulse/priority/local_only/maintainer/init_scope if already set
424
- local temp_file="${REPOS_FILE}.tmp"
425
- jq --arg path "$repo_path" --arg version "$version" --arg features "$features" \
426
- --arg slug "$slug" --argjson local_only "$is_local_only" --arg maintainer "$maintainer" \
427
- --argjson pulse_default "$DEFAULT_PULSE" --arg priority_default "$DEFAULT_PRIORITY" \
428
- --arg init_scope_default "$default_init_scope" \
429
- '(.initialized_repos[] | select(.path == $path)) |= (
430
- . + {path: $path, version: $version, features: ($features | split(",")), updated: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))}
431
- | if $slug != "" then .slug = $slug else . end
432
- | if $local_only then .local_only = true else . end
433
- | if .pulse == null then .pulse = (if $local_only then false else $pulse_default end) else . end
434
- | if (.priority == null or .priority == "") and $priority_default != "" then .priority = $priority_default else . end
435
- | if (.maintainer == null or .maintainer == "") and $maintainer != "" then .maintainer = $maintainer else . end
436
- | if (.init_scope == null or .init_scope == "") then .init_scope = $init_scope_default else . end
437
- )' \
438
- "$REPOS_FILE" >"$temp_file" && mv "$temp_file" "$REPOS_FILE"
439
- else
440
- # Add new entry with slug, defaults, maintainer, and init_scope
441
- local temp_file="${REPOS_FILE}.tmp"
442
- jq --arg path "$repo_path" --arg version "$version" --arg features "$features" \
443
- --arg slug "$slug" --arg maintainer "$maintainer" \
444
- --argjson local_only "$is_local_only" --argjson pulse_default "$DEFAULT_PULSE" \
445
- --arg priority_default "$DEFAULT_PRIORITY" --arg init_scope "$default_init_scope" \
446
- '.initialized_repos += [(
447
- {
448
- path: $path,
449
- maintainer: $maintainer,
450
- version: $version,
451
- features: ($features | split(",")),
452
- pulse: $pulse_default,
453
- init_scope: $init_scope,
454
- initialized: (now | strftime("%Y-%m-%dT%H:%M:%SZ"))
455
- }
456
- | if $slug != "" then . + {slug: $slug} else . end
457
- | if $local_only then . + {local_only: true, pulse: false} else . end
458
- | if $priority_default != "" then . + {priority: $priority_default} else . end
459
- )]' \
460
- "$REPOS_FILE" >"$temp_file" && mv "$temp_file" "$REPOS_FILE"
461
- fi
462
- return 0
463
- }
464
-
465
- # Get list of registered repos
466
- get_registered_repos() {
467
- init_repos_file
468
-
469
- if ! command -v jq &>/dev/null; then
470
- echo "[]"
471
- return 0
472
- fi
473
-
474
- jq -r '.initialized_repos[] | .path' "$REPOS_FILE" 2>/dev/null || echo ""
475
- return 0
476
- }
477
-
478
- # Get the maintainer GitHub username for a repo
479
- # Fallback chain: maintainer field > slug owner > empty string
480
- # Usage: get_repo_maintainer <slug>
481
- get_repo_maintainer() {
482
- local slug="$1"
483
-
484
- if ! command -v jq &>/dev/null; then
485
- echo ""
486
- return 0
487
- fi
488
-
489
- local maintainer
490
- maintainer=$(jq -r --arg slug "$slug" \
491
- '.initialized_repos[] | select(.slug == $slug) | .maintainer // empty' \
492
- "$REPOS_FILE" 2>/dev/null) || maintainer=""
493
-
494
- if [[ -n "$maintainer" ]]; then
495
- echo "$maintainer"
496
- return 0
497
- fi
498
-
499
- # Fallback: extract owner from slug (owner/repo -> owner)
500
- if [[ -n "$slug" && "$slug" == *"/"* ]]; then
501
- echo "${slug%%/*}"
502
- return 0
503
- fi
504
-
505
- echo ""
506
- return 0
507
- }
508
-
509
- # Check if a repo needs upgrade (version behind current)
510
- check_repo_needs_upgrade() {
511
- local repo_path="$1"
512
- local current_version
513
- current_version=$(get_version)
514
-
515
- if ! command -v jq &>/dev/null; then
516
- return 1
517
- fi
518
-
519
- local repo_version
520
- repo_version=$(jq -r --arg path "$repo_path" '.initialized_repos[] | select(.path == $path) | .version' "$REPOS_FILE" 2>/dev/null)
521
-
522
- if [[ -z "$repo_version" || "$repo_version" == "null" ]]; then
523
- return 1
524
- fi
525
-
526
- # Compare versions (simple string comparison works for semver)
527
- if [[ "$repo_version" != "$current_version" ]]; then
528
- return 0 # needs upgrade
529
- fi
530
- return 1 # up to date
531
- }
532
-
533
- # Check if a planning file needs upgrading (version mismatch or missing TOON markers)
534
- # Usage: check_planning_file_version <file> <template>
535
- # Returns 0 if upgrade needed, 1 if up to date
536
- check_planning_file_version() {
537
- local file="$1" template="$2"
538
- if [[ -f "$file" ]]; then
539
- if ! grep -q "TOON:meta" "$file" 2>/dev/null; then
540
- return 0
541
- fi
542
- local current_ver template_ver
543
- current_ver=$(grep -A1 "TOON:meta" "$file" 2>/dev/null | tail -1 | cut -d',' -f1)
544
- template_ver=$(grep -A1 "TOON:meta" "$template" 2>/dev/null | tail -1 | cut -d',' -f1)
545
- if [[ -n "$template_ver" ]] && [[ "$current_ver" != "$template_ver" ]]; then
546
- return 0
547
- fi
548
- return 1
549
- else
550
- # No file = no upgrade needed (init would create it)
551
- return 1
552
- fi
553
- }
554
-
555
- # Check if a repo's planning templates need upgrading
556
- # Returns 0 if any planning file needs upgrade
557
- check_planning_needs_upgrade() {
558
- local repo_path="$1"
559
- local todo_file="$repo_path/TODO.md"
560
- local plans_file="$repo_path/todo/PLANS.md"
561
- local todo_template="$AGENTS_DIR/templates/todo-template.md"
562
- local plans_template="$AGENTS_DIR/templates/plans-template.md"
563
-
564
- [[ ! -f "$todo_template" ]] && return 1
565
-
566
- if check_planning_file_version "$todo_file" "$todo_template"; then
567
- return 0
568
- fi
569
- if [[ -f "$plans_template" ]] && check_planning_file_version "$plans_file" "$plans_template"; then
570
- return 0
571
- fi
572
- return 1
573
- }
574
-
575
- # Detect if current directory has aidevops but isn't registered
576
- detect_unregistered_repo() {
577
- local project_root
578
-
579
- # Check if in a git repo
580
- if ! git rev-parse --is-inside-work-tree &>/dev/null; then
581
- return 1
582
- fi
583
-
584
- project_root=$(git rev-parse --show-toplevel 2>/dev/null)
585
-
586
- # Check for .aidevops.json
587
- if [[ ! -f "$project_root/.aidevops.json" ]]; then
588
- return 1
589
- fi
590
-
591
- init_repos_file
592
-
593
- if ! command -v jq &>/dev/null; then
594
- return 1
595
- fi
596
-
597
- # Check if already registered
598
- if jq -e --arg path "$project_root" '.initialized_repos[] | select(.path == $path)' "$REPOS_FILE" &>/dev/null; then
599
- return 1 # already registered
600
- fi
601
-
602
- # Not registered - return the path
603
- echo "$project_root"
604
- return 0
605
- }
606
-
607
- # Check if on protected branch and offer worktree creation
608
- # Returns 0 if safe to proceed, 1 if user cancelled
609
- # Sets WORKTREE_PATH if worktree was created
610
- check_protected_branch() {
611
- local branch_type="${1:-chore}"
612
- local branch_suffix="${2:-aidevops-setup}"
613
-
614
- # Not in a git repo - skip check
615
- if ! git rev-parse --is-inside-work-tree &>/dev/null; then
616
- return 0
617
- fi
618
-
619
- local current_branch
620
- current_branch=$(git branch --show-current 2>/dev/null || echo "")
621
-
622
- # Not on a protected branch - safe to proceed
623
- if [[ ! "$current_branch" =~ ^(main|master)$ ]]; then
624
- return 0
625
- fi
626
-
627
- local project_root
628
- project_root=$(git rev-parse --show-toplevel)
629
- local repo_name
630
- repo_name=$(basename "$project_root")
631
- local suggested_branch="$branch_type/$branch_suffix"
632
-
633
- local choice
634
- # In non-interactive (non-TTY) contexts, auto-select option 1 (create worktree)
635
- # without prompting. This prevents read from blocking or getting EOF in CI/AI
636
- # assistant environments, which could cause silent script termination with set -e.
637
- if [[ -t 0 ]]; then
638
- echo ""
639
- print_warning "On protected branch '$current_branch'"
640
- echo ""
641
- echo "Options:"
642
- echo " 1. Create worktree: $suggested_branch (recommended)"
643
- echo " 2. Continue on $current_branch (commits directly to main)"
644
- echo " 3. Cancel"
645
- echo ""
646
- read -r -p "Choice [1]: " choice
647
- choice="${choice:-1}"
648
- else
649
- # Non-interactive: auto-create worktree (safest default)
650
- choice="1"
651
- print_info "Non-interactive mode: auto-selecting worktree creation for '$suggested_branch'"
652
- fi
653
-
654
- case "$choice" in
655
- 1)
656
- # Create worktree
657
- local worktree_dir
658
- worktree_dir="$(dirname "$project_root")/${repo_name}-${branch_type}-${branch_suffix}"
659
-
660
- print_info "Creating worktree at $worktree_dir..."
661
-
662
- local worktree_created=false
663
- if [[ -f "$AGENTS_DIR/scripts/worktree-helper.sh" ]]; then
664
- if bash "$AGENTS_DIR/scripts/worktree-helper.sh" add "$suggested_branch"; then
665
- worktree_created=true
666
- else
667
- print_error "Failed to create worktree via worktree-helper.sh"
668
- return 1
669
- fi
670
- else
671
- # Fallback without helper script
672
- if git worktree add -b "$suggested_branch" "$worktree_dir"; then
673
- worktree_created=true
674
- else
675
- print_error "Failed to create worktree"
676
- return 1
677
- fi
678
- fi
679
-
680
- if [[ "$worktree_created" == "true" ]]; then
681
- export WORKTREE_PATH="$worktree_dir"
682
- echo ""
683
- print_success "Worktree created at: $worktree_dir"
684
- print_info "Switching to: $worktree_dir"
685
- echo ""
686
- # Change to worktree directory for the remainder of this process
687
- cd "$worktree_dir" || return 1
688
- return 0
689
- fi
690
- ;;
691
- 2)
692
- print_warning "Continuing on $current_branch - changes will commit directly"
693
- return 0
694
- ;;
695
- 3 | *)
696
- print_info "Cancelled"
697
- return 1
698
- ;;
699
- esac
700
- }