aidevops 3.8.88 → 3.8.91
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/VERSION +1 -1
- package/aidevops.sh +57 -2531
- package/package.json +1 -1
- package/setup.sh +1 -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.
|
|
8
|
+
# Version: 3.8.91
|
|
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
|
-
#
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
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
|
-
|
|
2492
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
2518
|
-
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
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
|
|
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() {
|