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.
- package/README.md +0 -1
- package/VERSION +1 -1
- package/aidevops.sh +44 -26
- package/package.json +1 -1
- package/setup.sh +25 -21
- package/aidevops-init-lib.sh +0 -1411
- package/aidevops-repos-lib.sh +0 -700
- package/aidevops-skills-plugin-lib.sh +0 -697
- package/aidevops-status-lib.sh +0 -141
- package/aidevops-update-lib.sh +0 -512
- package/aidevops-upgrade-planning-lib.sh +0 -370
- package/setup-modules/agent-deploy.sh +0 -1035
- package/setup-modules/agent-runtime.sh +0 -287
- package/setup-modules/config.sh +0 -478
- package/setup-modules/core.sh +0 -736
- package/setup-modules/mcp-setup.sh +0 -947
- package/setup-modules/migrations.sh +0 -1688
- package/setup-modules/plugins.sh +0 -728
- package/setup-modules/post-setup.sh +0 -301
- package/setup-modules/schedulers-linux.sh +0 -386
- package/setup-modules/schedulers-platform.sh +0 -1072
- package/setup-modules/schedulers-pulse.sh +0 -978
- package/setup-modules/schedulers.sh +0 -565
- package/setup-modules/shell-env.sh +0 -1240
- package/setup-modules/tool-beads.sh +0 -324
- package/setup-modules/tool-install.sh +0 -2134
package/aidevops-repos-lib.sh
DELETED
|
@@ -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
|
-
}
|