aidevops 3.13.0 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/VERSION +1 -1
- package/aidevops.sh +162 -3
- package/package.json +1 -1
- package/setup-modules/schedulers-linux.sh +386 -0
- package/setup-modules/schedulers-platform.sh +1024 -0
- package/setup-modules/schedulers-pulse.sh +978 -0
- package/setup-modules/schedulers.sh +347 -2497
- package/setup-modules/tool-install.sh +89 -0
- package/setup.sh +131 -48
package/README.md
CHANGED
|
@@ -238,6 +238,23 @@ This creates:
|
|
|
238
238
|
|
|
239
239
|
**Available features:** `planning`, `git-workflow`, `code-quality`, `time-tracking`, `beads`
|
|
240
240
|
|
|
241
|
+
### Per-repo platform setup
|
|
242
|
+
|
|
243
|
+
After `aidevops init` registers a new repo, run `/setup-git` in your AI assistant
|
|
244
|
+
to apply per-repo platform secrets. Most notably, this sets `SYNC_PAT` — a
|
|
245
|
+
GitHub Actions secret that lets `issue-sync.yml` push TODO.md auto-completion
|
|
246
|
+
past branch protection.
|
|
247
|
+
|
|
248
|
+
This is distinct from `/onboarding` (per-account credentials like `gh auth login`):
|
|
249
|
+
GitHub Actions secrets are scoped per-repo, so each repo needs its own. You need
|
|
250
|
+
`gh auth login` to succeed before any per-repo helper can run, so `/onboarding`
|
|
251
|
+
comes first, `/setup-git` second.
|
|
252
|
+
|
|
253
|
+
Run `/setup-git` again whenever you register a new repo with `aidevops repos add`
|
|
254
|
+
or when a `SYNC_PAT` advisory appears in the session greeting toast. If you skip
|
|
255
|
+
this step, `issue-sync.yml` will post a remediation comment when it hits branch
|
|
256
|
+
protection — `/setup-git` walks through the fix.
|
|
257
|
+
|
|
241
258
|
### Upgrade Planning Files
|
|
242
259
|
|
|
243
260
|
When aidevops templates evolve, upgrade existing projects to the latest format:
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.13.
|
|
1
|
+
3.13.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.13.
|
|
8
|
+
# Version: 3.13.1
|
|
9
9
|
|
|
10
10
|
set -euo pipefail
|
|
11
11
|
|
|
@@ -557,6 +557,71 @@ _update_verify_signature() {
|
|
|
557
557
|
return 0
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
# One-shot, idempotent migration of supervisor.* → orchestration.* in settings.json (t2946).
|
|
561
|
+
# Safe: reads value from supervisor.* only when orchestration.* key is absent.
|
|
562
|
+
# Logs to ~/.aidevops/logs/settings-migration.log.
|
|
563
|
+
_migrate_settings_supervisor_to_orchestration() {
|
|
564
|
+
local _settings_file="${HOME}/.config/aidevops/settings.json"
|
|
565
|
+
local _log_file="${HOME}/.aidevops/logs/settings-migration.log"
|
|
566
|
+
|
|
567
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
568
|
+
return 0
|
|
569
|
+
fi
|
|
570
|
+
if [[ ! -f "$_settings_file" ]]; then
|
|
571
|
+
return 0
|
|
572
|
+
fi
|
|
573
|
+
if ! jq . "$_settings_file" >/dev/null 2>&1; then
|
|
574
|
+
return 0
|
|
575
|
+
fi
|
|
576
|
+
|
|
577
|
+
# Check if supervisor.pulse_interval_seconds exists and orchestration.pulse_interval_seconds is absent.
|
|
578
|
+
local _has_sv _has_orch
|
|
579
|
+
_has_sv=$(jq -r 'if .supervisor.pulse_interval_seconds != null then "yes" else "no" end' "$_settings_file" 2>/dev/null)
|
|
580
|
+
_has_orch=$(jq -r 'if .orchestration.pulse_interval_seconds != null then "yes" else "no" end' "$_settings_file" 2>/dev/null)
|
|
581
|
+
|
|
582
|
+
if [[ "$_has_sv" != "yes" ]]; then
|
|
583
|
+
return 0
|
|
584
|
+
fi
|
|
585
|
+
|
|
586
|
+
local _ts
|
|
587
|
+
_ts=$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date +%Y-%m-%dT%H:%M:%SZ)
|
|
588
|
+
mkdir -p "$(dirname "$_log_file")" 2>/dev/null || true
|
|
589
|
+
|
|
590
|
+
local _tmp
|
|
591
|
+
_tmp=$(mktemp 2>/dev/null) || return 0
|
|
592
|
+
|
|
593
|
+
if [[ "$_has_orch" == "no" ]]; then
|
|
594
|
+
# Migrate: copy supervisor.pulse_interval_seconds to orchestration.pulse_interval_seconds,
|
|
595
|
+
# then remove supervisor.pulse_interval_seconds.
|
|
596
|
+
local _sv_val
|
|
597
|
+
_sv_val=$(jq -r '.supervisor.pulse_interval_seconds' "$_settings_file" 2>/dev/null)
|
|
598
|
+
if jq --argjson v "$_sv_val" \
|
|
599
|
+
'(.orchestration.pulse_interval_seconds) = $v | del(.supervisor.pulse_interval_seconds)' \
|
|
600
|
+
"$_settings_file" >"$_tmp" 2>/dev/null && [[ -s "$_tmp" ]]; then
|
|
601
|
+
mv "$_tmp" "$_settings_file"
|
|
602
|
+
printf '[%s] migrated supervisor.pulse_interval_seconds=%s → orchestration.pulse_interval_seconds\n' \
|
|
603
|
+
"$_ts" "$_sv_val" >>"$_log_file" 2>/dev/null || true
|
|
604
|
+
print_info "Settings migrated: supervisor.pulse_interval_seconds → orchestration.pulse_interval_seconds ($_sv_val)"
|
|
605
|
+
else
|
|
606
|
+
rm -f "$_tmp"
|
|
607
|
+
fi
|
|
608
|
+
else
|
|
609
|
+
# Both present: orchestration wins, remove the stale supervisor key.
|
|
610
|
+
local _orch_val
|
|
611
|
+
_orch_val=$(jq -r '.orchestration.pulse_interval_seconds' "$_settings_file" 2>/dev/null)
|
|
612
|
+
if jq 'del(.supervisor.pulse_interval_seconds)' \
|
|
613
|
+
"$_settings_file" >"$_tmp" 2>/dev/null && [[ -s "$_tmp" ]]; then
|
|
614
|
+
mv "$_tmp" "$_settings_file"
|
|
615
|
+
printf '[%s] removed stale supervisor.pulse_interval_seconds (orchestration.pulse_interval_seconds=%s wins)\n' \
|
|
616
|
+
"$_ts" "$_orch_val" >>"$_log_file" 2>/dev/null || true
|
|
617
|
+
print_info "Settings cleaned: removed stale supervisor.pulse_interval_seconds (orchestration value $_orch_val kept)"
|
|
618
|
+
else
|
|
619
|
+
rm -f "$_tmp"
|
|
620
|
+
fi
|
|
621
|
+
fi
|
|
622
|
+
return 0
|
|
623
|
+
}
|
|
624
|
+
|
|
560
625
|
# Update/upgrade command
|
|
561
626
|
cmd_update() {
|
|
562
627
|
local skip_project_sync=false
|
|
@@ -693,6 +758,12 @@ cmd_update() {
|
|
|
693
758
|
_update_check_daemon_health
|
|
694
759
|
fi
|
|
695
760
|
|
|
761
|
+
# t2946: one-shot idempotent migration from legacy supervisor.* to canonical
|
|
762
|
+
# orchestration.* namespace in settings.json. Safe: no-op when orchestration.*
|
|
763
|
+
# is already set. Runs even on "already up to date" updates so users who
|
|
764
|
+
# install the fix without a new release still get migrated on next 'aidevops update'.
|
|
765
|
+
_migrate_settings_supervisor_to_orchestration
|
|
766
|
+
|
|
696
767
|
# t2914: ensure pulse is running after every update. The existing
|
|
697
768
|
# restart paths (setup.sh:1329, agent-deploy.sh:601) call
|
|
698
769
|
# pulse-lifecycle-helper.sh restart-if-running which is a silent no-op
|
|
@@ -708,7 +779,7 @@ cmd_update() {
|
|
|
708
779
|
# 'restart-if-running' do). Non-fatal: a pulse start failure should
|
|
709
780
|
# not fail the update.
|
|
710
781
|
if [[ "${AIDEVOPS_SKIP_PULSE_RESTART:-0}" != "1" ]]; then
|
|
711
|
-
local _pulse_helper="${
|
|
782
|
+
local _pulse_helper="${AGENTS_DIR}/scripts/pulse-lifecycle-helper.sh"
|
|
712
783
|
if [[ -x "$_pulse_helper" ]]; then
|
|
713
784
|
"$_pulse_helper" start >/dev/null 2>&1 || print_warning "Pulse start failed (non-fatal)"
|
|
714
785
|
fi
|
|
@@ -1496,6 +1567,7 @@ _help_commands() {
|
|
|
1496
1567
|
echo " client-format Client request format alignment (extract/check/canary/monitor)"
|
|
1497
1568
|
echo " opencode-sandbox Test OpenCode versions in isolation (install/run/check/clean)"
|
|
1498
1569
|
echo " approve <cmd> Cryptographic issue/PR approval (setup/issue/pr/verify/status)"
|
|
1570
|
+
echo " issue <cmd> Interactive issue ownership (claim/release/status/scan-stale)"
|
|
1499
1571
|
echo " security [cmd] Full security assessment (posture + hygiene + supply chain)"
|
|
1500
1572
|
echo " contributions External contributions inbox (bare: status | seed/scan/stop/restart/install/uninstall)"
|
|
1501
1573
|
echo " inbox [cmd] Capture transit zone (bare: status | provision/add/find/digest/help)"
|
|
@@ -1505,6 +1577,7 @@ _help_commands() {
|
|
|
1505
1577
|
echo " secret <cmd> Manage secrets (set/list/run/init/import/status)"
|
|
1506
1578
|
echo " config <cmd> Feature toggles (list/get/set/reset/path/help)"
|
|
1507
1579
|
echo " knowledge <cmd> Knowledge plane management (init/status/provision)"
|
|
1580
|
+
echo " campaign <cmd> Campaign plane P6: launch + promote results/learnings"
|
|
1508
1581
|
echo " stats <cmd> LLM usage analytics (summary/models/projects/costs/trend)"
|
|
1509
1582
|
echo " tabby <cmd> Manage Tabby terminal profiles (sync/status/zshrc/help)"
|
|
1510
1583
|
echo " parent-status <N> Show decomposition state of parent-task issue #N (alias: ps)"
|
|
@@ -1590,6 +1663,15 @@ _help_detailed_sections() {
|
|
|
1590
1663
|
echo " aidevops knowledge init off # Disable knowledge plane"
|
|
1591
1664
|
echo " aidevops knowledge status # Show provisioning state"
|
|
1592
1665
|
echo " aidevops knowledge provision [path] # Re-provision (idempotent)"
|
|
1666
|
+
echo " aidevops knowledge add <file|url> # Ingest file or URL into sources/"
|
|
1667
|
+
echo " aidevops knowledge list [--state s] [--kind k] # List all known sources"
|
|
1668
|
+
echo " aidevops knowledge search <query> # Search sources (grep fallback)"
|
|
1669
|
+
echo ""
|
|
1670
|
+
echo "Campaign Plane (P6 — performance integration + learnings promotion):"
|
|
1671
|
+
echo " aidevops campaign launch <id> # Move active/<id> → launched/, create result/learning templates"
|
|
1672
|
+
echo " aidevops campaign promote <id> --results # Push metrics to _performance/marketing/<id>.md"
|
|
1673
|
+
echo " aidevops campaign promote <id> --learnings # Promote insights to _knowledge/insights/marketing/"
|
|
1674
|
+
echo " aidevops campaign feedback [<id>] # Surface _feedback/ insights for campaign research"
|
|
1593
1675
|
echo ""
|
|
1594
1676
|
echo "LLM Stats:"
|
|
1595
1677
|
echo " aidevops stats # Show usage summary (last 30 days)"
|
|
@@ -1935,6 +2017,75 @@ main() {
|
|
|
1935
2017
|
pulse) _dispatch_helper "pulse-session-helper.sh" "pulse-session-helper.sh" "$@" ;;
|
|
1936
2018
|
check-workflows | workflows) _dispatch_helper "check-workflows-helper.sh" "check-workflows-helper.sh" "$@" ;;
|
|
1937
2019
|
sync-workflows) _dispatch_helper "sync-workflows-helper.sh" "sync-workflows-helper.sh" "$@" ;;
|
|
2020
|
+
badges)
|
|
2021
|
+
# Badge management: render | check | sync | install (t2975)
|
|
2022
|
+
# Bare 'aidevops badges' with no subcommand shows a usage summary.
|
|
2023
|
+
# Subcommands:
|
|
2024
|
+
# render <slug> — render canonical badge block for a repo
|
|
2025
|
+
# check [--repo SLUG] [--json] [--verbose] — cross-repo drift check
|
|
2026
|
+
# sync [--repo SLUG] [--apply] — inject badge block + install workflow
|
|
2027
|
+
# install [--repo SLUG] [--apply] — install loc-badge caller workflow only
|
|
2028
|
+
local _badges_sub="${1:-help}"
|
|
2029
|
+
local _badges_check_h="badges-check-helper.sh"
|
|
2030
|
+
local _badges_sync_h="badges-sync-helper.sh"
|
|
2031
|
+
case "$_badges_sub" in
|
|
2032
|
+
render)
|
|
2033
|
+
shift
|
|
2034
|
+
local _render_helper
|
|
2035
|
+
_render_helper=$(bash -c '
|
|
2036
|
+
d="$HOME/.aidevops/agents/scripts/readme-badges-helper.sh"
|
|
2037
|
+
l="'"$AGENTS_DIR"'/scripts/readme-badges-helper.sh"
|
|
2038
|
+
[[ -f "$d" ]] && echo "$d" || echo "$l"
|
|
2039
|
+
')
|
|
2040
|
+
if [[ -f "$_render_helper" ]]; then
|
|
2041
|
+
bash "$_render_helper" render "$@"
|
|
2042
|
+
else
|
|
2043
|
+
print_error "readme-badges-helper.sh not found. Run: aidevops update"
|
|
2044
|
+
exit 1
|
|
2045
|
+
fi
|
|
2046
|
+
;;
|
|
2047
|
+
check)
|
|
2048
|
+
shift
|
|
2049
|
+
_dispatch_helper "$_badges_check_h" "$_badges_check_h" "$@"
|
|
2050
|
+
;;
|
|
2051
|
+
sync)
|
|
2052
|
+
shift
|
|
2053
|
+
_dispatch_helper "$_badges_sync_h" "$_badges_sync_h" "$@"
|
|
2054
|
+
;;
|
|
2055
|
+
install)
|
|
2056
|
+
shift
|
|
2057
|
+
_dispatch_helper "$_badges_sync_h" "$_badges_sync_h" --workflow-only "$@"
|
|
2058
|
+
;;
|
|
2059
|
+
help | --help | -h | "")
|
|
2060
|
+
echo ""
|
|
2061
|
+
echo "aidevops badges — README badge block and LOC workflow management (t2975)"
|
|
2062
|
+
echo ""
|
|
2063
|
+
echo "Subcommands:"
|
|
2064
|
+
echo " render <slug> Print canonical badge block for a repo"
|
|
2065
|
+
echo " check [--repo SLUG] [--json] Detect badge drift across managed repos"
|
|
2066
|
+
echo " sync [--repo SLUG] [--apply] Inject badge block + install LOC workflow"
|
|
2067
|
+
echo " install [--repo SLUG] [--apply] Install loc-badge caller workflow only"
|
|
2068
|
+
echo ""
|
|
2069
|
+
echo "Options (check/sync/install):"
|
|
2070
|
+
echo " --repo SLUG Limit to a single repo"
|
|
2071
|
+
echo " --apply Actually perform the sync (default: dry-run)"
|
|
2072
|
+
echo " --json Machine-readable output"
|
|
2073
|
+
echo " --verbose Show diff summaries (check only)"
|
|
2074
|
+
echo ""
|
|
2075
|
+
echo "Examples:"
|
|
2076
|
+
echo " aidevops badges check # scan all repos for badge drift"
|
|
2077
|
+
echo " aidevops badges check --json | jq '.[]' # machine-readable output"
|
|
2078
|
+
echo " aidevops badges render owner/repo # print badge block"
|
|
2079
|
+
echo " aidevops badges sync # dry-run sync across all repos"
|
|
2080
|
+
echo " aidevops badges sync --repo owner/r --apply # apply to a single repo"
|
|
2081
|
+
echo ""
|
|
2082
|
+
;;
|
|
2083
|
+
*)
|
|
2084
|
+
print_error "Unknown badges subcommand: $_badges_sub (try render|check|sync|install|help)"
|
|
2085
|
+
exit 1
|
|
2086
|
+
;;
|
|
2087
|
+
esac
|
|
2088
|
+
;;
|
|
1938
2089
|
security) _cmd_security "$@" ;;
|
|
1939
2090
|
doctor | doc) _dispatch_helper "doctor-helper.sh" "doctor-helper.sh" "$@" ;;
|
|
1940
2091
|
detect | scan) cmd_detect ;;
|
|
@@ -1945,6 +2096,7 @@ main() {
|
|
|
1945
2096
|
review-gate | review_gate) _dispatch_helper "review-gate-config-helper.sh" "review-gate-config-helper.sh" "$@" ;;
|
|
1946
2097
|
secret | secrets) _dispatch_helper "secret-helper.sh" "secret-helper.sh" "$@" ;;
|
|
1947
2098
|
approve) _dispatch_helper "approval-helper.sh" "approval-helper.sh" "$@" ;;
|
|
2099
|
+
issue) _dispatch_helper "interactive-session-helper.sh" "interactive-session-helper.sh" "$@" ;;
|
|
1948
2100
|
signing) _dispatch_helper "signing-setup.sh" "signing-setup.sh" "$@" ;;
|
|
1949
2101
|
contributions | contrib)
|
|
1950
2102
|
# Bare `aidevops contributions` defaults to status (most common use).
|
|
@@ -1960,7 +2112,13 @@ main() {
|
|
|
1960
2112
|
case | cases)
|
|
1961
2113
|
# Bare `aidevops case` defaults to list (most common use).
|
|
1962
2114
|
[[ $# -eq 0 ]] && set -- list
|
|
1963
|
-
|
|
2115
|
+
# alarm-test subcommand routes to case-alarm-helper.sh
|
|
2116
|
+
if [[ "${1:-}" == "alarm-test" ]]; then
|
|
2117
|
+
shift
|
|
2118
|
+
_dispatch_helper "case-alarm-helper.sh" "case-alarm-helper.sh" alarm-test "$@"
|
|
2119
|
+
else
|
|
2120
|
+
_dispatch_helper "case-helper.sh" "case-helper.sh" "$@"
|
|
2121
|
+
fi
|
|
1964
2122
|
;;
|
|
1965
2123
|
email) _cmd_email "$@" ;;
|
|
1966
2124
|
stats | observability) _dispatch_helper "observability-helper.sh" "observability-helper.sh" "$@" ;;
|
|
@@ -1968,6 +2126,7 @@ main() {
|
|
|
1968
2126
|
init-routines) _dispatch_helper "init-routines-helper.sh" "init-routines-helper.sh" "$@" ;;
|
|
1969
2127
|
parent-status | ps) _dispatch_helper "parent-status-helper.sh" "parent-status-helper.sh" "$@" ;;
|
|
1970
2128
|
knowledge) _dispatch_helper "knowledge-helper.sh" "knowledge-helper.sh" "$@" ;;
|
|
2129
|
+
campaign | campaigns) _dispatch_helper "campaign-helper.sh" "campaign-helper.sh" "$@" ;;
|
|
1971
2130
|
config | configure) _dispatch_config "$@" ;;
|
|
1972
2131
|
uninstall | remove) cmd_uninstall ;;
|
|
1973
2132
|
version | v | -v | --version) cmd_version ;;
|
package/package.json
CHANGED
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
# SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
|
|
4
|
+
# =============================================================================
|
|
5
|
+
# Schedulers Linux Sub-Library -- systemd/cron scheduler installation and
|
|
6
|
+
# uninstall functions for Linux (and macOS uninstall path).
|
|
7
|
+
# =============================================================================
|
|
8
|
+
# This sub-library is sourced by setup-modules/schedulers.sh (the orchestrator).
|
|
9
|
+
# It covers:
|
|
10
|
+
# - systemd user service availability check
|
|
11
|
+
# - systemd value escaping
|
|
12
|
+
# - Building systemd Environment= and cron env prefix lines
|
|
13
|
+
# - Generic systemd timer installation
|
|
14
|
+
# - Generic cron entry installation
|
|
15
|
+
# - Linux dispatcher (systemd preferred, cron fallback)
|
|
16
|
+
# - Generic scheduler uninstall (launchd/systemd/cron)
|
|
17
|
+
# - Supervisor pulse uninstall
|
|
18
|
+
#
|
|
19
|
+
# Usage: source "${SCRIPT_DIR}/schedulers-linux.sh"
|
|
20
|
+
#
|
|
21
|
+
# Dependencies:
|
|
22
|
+
# - shared-constants.sh (print_info, print_warning)
|
|
23
|
+
#
|
|
24
|
+
# Part of aidevops framework: https://aidevops.sh
|
|
25
|
+
|
|
26
|
+
# Apply strict mode only when executed directly (not when sourced)
|
|
27
|
+
[[ "${BASH_SOURCE[0]}" == "${0}" ]] && set -euo pipefail
|
|
28
|
+
|
|
29
|
+
# Include guard
|
|
30
|
+
[[ -n "${_SCHEDULERS_LINUX_LIB_LOADED:-}" ]] && return 0
|
|
31
|
+
_SCHEDULERS_LINUX_LIB_LOADED=1
|
|
32
|
+
|
|
33
|
+
# SCRIPT_DIR fallback — needed when sourced from test harnesses that don't set it.
|
|
34
|
+
if [[ -z "${SCRIPT_DIR:-}" ]]; then
|
|
35
|
+
_sched_linux_lib_path="${BASH_SOURCE[0]%/*}"
|
|
36
|
+
[[ "$_sched_linux_lib_path" == "${BASH_SOURCE[0]}" ]] && _sched_linux_lib_path="."
|
|
37
|
+
SCRIPT_DIR="$(cd "$_sched_linux_lib_path" && pwd)"
|
|
38
|
+
unset _sched_linux_lib_path
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# --- Functions ---
|
|
42
|
+
|
|
43
|
+
# Check if systemd user services are available on this Linux system.
|
|
44
|
+
# Returns 0 if systemd --user is functional, 1 otherwise.
|
|
45
|
+
_systemd_user_available() {
|
|
46
|
+
command -v systemctl >/dev/null 2>&1 || return 1
|
|
47
|
+
systemctl --user status >/dev/null 2>&1 || return 1
|
|
48
|
+
return 0
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Escape a value for safe embedding in a systemd unit Environment= or ExecStart=
|
|
52
|
+
# directive. systemd interprets % as specifiers (%h, %n, %t, etc.) and spaces
|
|
53
|
+
# as key-value separators. This helper:
|
|
54
|
+
# 1. Escapes \ → \\ (must be first to avoid double-escaping)
|
|
55
|
+
# 2. Doubles % → %% (escape specifiers)
|
|
56
|
+
# 3. Escapes embedded " → \"
|
|
57
|
+
# 4. Wraps the result in "..." (handles spaces and other shell metacharacters)
|
|
58
|
+
# Usage: escaped=$(_systemd_escape "$value")
|
|
59
|
+
#
|
|
60
|
+
# WARNING: Do NOT use for StandardOutput= or StandardError= directives.
|
|
61
|
+
# systemd does not strip outer quotes from those values — "append:/path" is
|
|
62
|
+
# treated as a literal filename with quote characters, failing silently.
|
|
63
|
+
# Use bare values for StandardOutput=/StandardError=:
|
|
64
|
+
# StandardOutput=append:${log_file} ← correct
|
|
65
|
+
# StandardOutput=$(_systemd_escape "append:${log_file}") ← WRONG
|
|
66
|
+
_systemd_escape() {
|
|
67
|
+
local _val="$1"
|
|
68
|
+
# Step 1: escape backslashes
|
|
69
|
+
_val="${_val//\\/\\\\}"
|
|
70
|
+
# Step 2: escape % specifiers
|
|
71
|
+
_val="${_val//%/%%}"
|
|
72
|
+
# Step 3: escape embedded double-quotes
|
|
73
|
+
_val="${_val//\"/\\\"}"
|
|
74
|
+
# Step 4: wrap in double-quotes
|
|
75
|
+
printf '"%s"' "$_val"
|
|
76
|
+
return 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Build systemd Environment= lines from newline-separated KEY=VALUE pairs.
|
|
80
|
+
# Always appends HOME and PATH for parity with launchd and cron execution.
|
|
81
|
+
_scheduler_systemd_env_lines() {
|
|
82
|
+
local env_vars="$1"
|
|
83
|
+
local _env_lines=""
|
|
84
|
+
|
|
85
|
+
if [[ -n "$env_vars" ]]; then
|
|
86
|
+
while IFS= read -r _kv; do
|
|
87
|
+
[[ -z "$_kv" ]] && continue
|
|
88
|
+
local _key="${_kv%%=*}"
|
|
89
|
+
local _raw_val="${_kv#*=}"
|
|
90
|
+
local _escaped_val
|
|
91
|
+
_escaped_val=$(_systemd_escape "$_raw_val")
|
|
92
|
+
_env_lines+="Environment=${_key}=${_escaped_val}"$'\n'
|
|
93
|
+
done <<<"$env_vars"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
_env_lines+="Environment=HOME=$(_systemd_escape "$HOME")"$'\n'
|
|
97
|
+
_env_lines+="Environment=PATH=$(_systemd_escape "$PATH")"$'\n'
|
|
98
|
+
printf '%s' "$_env_lines"
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
# Build inline cron environment assignments from newline-separated KEY=VALUE pairs.
|
|
103
|
+
_scheduler_cron_env_prefix() {
|
|
104
|
+
local env_vars="$1"
|
|
105
|
+
local _env_prefix=""
|
|
106
|
+
|
|
107
|
+
if [[ -n "$env_vars" ]]; then
|
|
108
|
+
while IFS= read -r _kv; do
|
|
109
|
+
[[ -z "$_kv" ]] && continue
|
|
110
|
+
local _key="${_kv%%=*}"
|
|
111
|
+
local _raw_val="${_kv#*=}"
|
|
112
|
+
local _escaped_val
|
|
113
|
+
_escaped_val=$(_cron_escape "$_raw_val")
|
|
114
|
+
_env_prefix+="${_key}=${_escaped_val} "
|
|
115
|
+
done <<<"$env_vars"
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
printf '%s' "$_env_prefix"
|
|
119
|
+
return 0
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
# Install a generic scheduler via systemd user timer (Linux with systemd).
|
|
123
|
+
# Args:
|
|
124
|
+
# $1 = service_name (e.g. "aidevops-stats-wrapper")
|
|
125
|
+
# $2 = exec_command (shell command run via /bin/bash -lc)
|
|
126
|
+
# $3 = interval_sec (OnUnitActiveSec interval in seconds; may be empty for calendar-only)
|
|
127
|
+
# $4 = log_file (absolute path to log file)
|
|
128
|
+
# $5 = env_vars (newline-separated KEY=VALUE pairs, may be empty)
|
|
129
|
+
# $6 = run_at_load ("true" or "false")
|
|
130
|
+
# $7 = low_priority ("true" or "false")
|
|
131
|
+
# $8 = on_calendar (optional systemd OnCalendar spec)
|
|
132
|
+
# $9 = timeout_sec (optional TimeoutStartSec; defaults to interval_sec)
|
|
133
|
+
# Returns 0 on success, 1 if systemd enable fails (caller should fall back to cron).
|
|
134
|
+
_install_scheduler_systemd() {
|
|
135
|
+
local service_name="$1"
|
|
136
|
+
local exec_command="$2"
|
|
137
|
+
local interval_sec="$3"
|
|
138
|
+
local log_file="$4"
|
|
139
|
+
local env_vars="$5"
|
|
140
|
+
local run_at_load="$6"
|
|
141
|
+
local low_priority="$7"
|
|
142
|
+
local on_calendar="$8"
|
|
143
|
+
local timeout_sec="$9"
|
|
144
|
+
local service_dir="$HOME/.config/systemd/user"
|
|
145
|
+
local service_file="${service_dir}/${service_name}.service"
|
|
146
|
+
local timer_file="${service_dir}/${service_name}.timer"
|
|
147
|
+
|
|
148
|
+
mkdir -p "$service_dir"
|
|
149
|
+
|
|
150
|
+
# GH#18439 Bug 1: command substitution strips trailing newlines, which
|
|
151
|
+
# would run the final Environment=PATH=... into the following
|
|
152
|
+
# StandardOutput=... directive on the same line. Use a sentinel ('x')
|
|
153
|
+
# to preserve the trailing newline that _scheduler_systemd_env_lines
|
|
154
|
+
# always emits.
|
|
155
|
+
local _env_lines
|
|
156
|
+
_env_lines=$(
|
|
157
|
+
_scheduler_systemd_env_lines "$env_vars"
|
|
158
|
+
printf 'x'
|
|
159
|
+
)
|
|
160
|
+
_env_lines="${_env_lines%x}"
|
|
161
|
+
|
|
162
|
+
if [[ -z "$timeout_sec" ]]; then
|
|
163
|
+
timeout_sec="$interval_sec"
|
|
164
|
+
fi
|
|
165
|
+
if [[ -z "$timeout_sec" ]]; then
|
|
166
|
+
timeout_sec="3600"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
local _service_extra=""
|
|
170
|
+
if [[ "$low_priority" == "true" ]]; then
|
|
171
|
+
_service_extra+="Nice=10"$'\n'
|
|
172
|
+
_service_extra+="IOSchedulingClass=idle"$'\n'
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
printf '%s' "[Unit]
|
|
176
|
+
Description=aidevops ${service_name}
|
|
177
|
+
After=network.target
|
|
178
|
+
|
|
179
|
+
[Service]
|
|
180
|
+
Type=oneshot
|
|
181
|
+
KillMode=process
|
|
182
|
+
ExecStart=/bin/bash -lc $(_systemd_escape "$exec_command")
|
|
183
|
+
TimeoutStartSec=${timeout_sec}
|
|
184
|
+
${_service_extra}${_env_lines}StandardOutput=append:${log_file}
|
|
185
|
+
StandardError=append:${log_file}
|
|
186
|
+
" >"$service_file"
|
|
187
|
+
|
|
188
|
+
local _timer_lines=""
|
|
189
|
+
if [[ "$run_at_load" == "true" ]]; then
|
|
190
|
+
_timer_lines+="OnActiveSec=10s"$'\n'
|
|
191
|
+
fi
|
|
192
|
+
if [[ -n "$interval_sec" ]]; then
|
|
193
|
+
_timer_lines+="OnBootSec=${interval_sec}"$'\n'
|
|
194
|
+
_timer_lines+="OnUnitActiveSec=${interval_sec}"$'\n'
|
|
195
|
+
fi
|
|
196
|
+
if [[ -n "$on_calendar" ]]; then
|
|
197
|
+
_timer_lines+="OnCalendar=${on_calendar}"$'\n'
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
printf '%s' "[Unit]
|
|
201
|
+
Description=aidevops ${service_name} Timer
|
|
202
|
+
|
|
203
|
+
[Timer]
|
|
204
|
+
${_timer_lines}Persistent=true
|
|
205
|
+
|
|
206
|
+
[Install]
|
|
207
|
+
WantedBy=timers.target
|
|
208
|
+
" >"$timer_file"
|
|
209
|
+
|
|
210
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
211
|
+
if systemctl --user enable --now "${service_name}.timer" 2>/dev/null; then
|
|
212
|
+
return 0
|
|
213
|
+
fi
|
|
214
|
+
return 1
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# Install a generic cron entry.
|
|
218
|
+
# Args: $1=cron_tag, $2=cron_schedule, $3=exec_command, $4=log_file, $5=env_vars
|
|
219
|
+
_install_scheduler_cron() {
|
|
220
|
+
local cron_tag="$1"
|
|
221
|
+
local cron_schedule="$2"
|
|
222
|
+
local exec_command="$3"
|
|
223
|
+
local log_file="$4"
|
|
224
|
+
local env_vars="$5"
|
|
225
|
+
local _cron_exec
|
|
226
|
+
local _cron_log
|
|
227
|
+
local _env_prefix
|
|
228
|
+
|
|
229
|
+
_env_prefix=$(_scheduler_cron_env_prefix "$env_vars")
|
|
230
|
+
_cron_exec=$(_cron_escape "$exec_command")
|
|
231
|
+
_cron_log=$(_cron_escape "$log_file")
|
|
232
|
+
|
|
233
|
+
(
|
|
234
|
+
crontab -l 2>/dev/null | grep -vF "${cron_tag}" || true
|
|
235
|
+
echo "${cron_schedule} ${_env_prefix}/bin/bash -lc ${_cron_exec} >> ${_cron_log} 2>&1 # ${cron_tag}"
|
|
236
|
+
) | crontab - 2>/dev/null || true
|
|
237
|
+
return 0
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
# Dispatcher: install a scheduler on Linux, preferring systemd over cron.
|
|
241
|
+
# Args:
|
|
242
|
+
# $1 = service_name (systemd service name, e.g. "aidevops-stats-wrapper")
|
|
243
|
+
# $2 = cron_tag (comment tag for cron line, e.g. "aidevops: stats-wrapper")
|
|
244
|
+
# $3 = cron_schedule (cron schedule expression, e.g. "*/15 * * * *")
|
|
245
|
+
# $4 = exec_command (shell command run via /bin/bash -lc)
|
|
246
|
+
# $5 = interval_sec (systemd OnUnitActiveSec in seconds; may be empty for calendar-only)
|
|
247
|
+
# $6 = log_file (absolute path to log file)
|
|
248
|
+
# $7 = env_vars (newline-separated KEY=VALUE pairs for systemd/cron, may be empty)
|
|
249
|
+
# $8 = success_msg (message to print on success)
|
|
250
|
+
# $9 = fail_msg (message to print on failure)
|
|
251
|
+
# $10 = run_at_load ("true" or "false")
|
|
252
|
+
# $11 = low_priority ("true" or "false")
|
|
253
|
+
# $12 = on_calendar (optional systemd OnCalendar spec)
|
|
254
|
+
# $13 = timeout_sec (optional TimeoutStartSec)
|
|
255
|
+
# Returns 0 always (failures are warnings, not fatal).
|
|
256
|
+
_install_scheduler_linux() {
|
|
257
|
+
local service_name="$1"
|
|
258
|
+
local cron_tag="$2"
|
|
259
|
+
local cron_schedule="$3"
|
|
260
|
+
local exec_command="$4"
|
|
261
|
+
local interval_sec="$5"
|
|
262
|
+
local log_file="$6"
|
|
263
|
+
local env_vars="$7"
|
|
264
|
+
local success_msg="$8"
|
|
265
|
+
local fail_msg="$9"
|
|
266
|
+
local run_at_load="${10}"
|
|
267
|
+
local low_priority="${11}"
|
|
268
|
+
local on_calendar="${12:-}"
|
|
269
|
+
local timeout_sec="${13:-}"
|
|
270
|
+
|
|
271
|
+
if _systemd_user_available; then
|
|
272
|
+
if _install_scheduler_systemd \
|
|
273
|
+
"$service_name" \
|
|
274
|
+
"$exec_command" \
|
|
275
|
+
"$interval_sec" \
|
|
276
|
+
"$log_file" \
|
|
277
|
+
"$env_vars" \
|
|
278
|
+
"$run_at_load" \
|
|
279
|
+
"$low_priority" \
|
|
280
|
+
"$on_calendar" \
|
|
281
|
+
"$timeout_sec"; then
|
|
282
|
+
print_info "${success_msg} (systemd user timer)"
|
|
283
|
+
# After systemd install succeeds, remove any pre-existing cron entry
|
|
284
|
+
# to prevent dual-execution (GH#17695 Finding A)
|
|
285
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
286
|
+
local current_cron
|
|
287
|
+
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
288
|
+
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "$cron_tag"; then
|
|
289
|
+
echo "$current_cron" | grep -vF "$cron_tag" | crontab -
|
|
290
|
+
echo "[schedulers] Removed pre-existing cron entry for $cron_tag (migrated to systemd)"
|
|
291
|
+
fi
|
|
292
|
+
fi
|
|
293
|
+
else
|
|
294
|
+
print_warning "systemd enable failed for ${service_name} — falling back to cron"
|
|
295
|
+
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
296
|
+
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
297
|
+
print_info "${success_msg} (cron fallback)"
|
|
298
|
+
else
|
|
299
|
+
print_warning "${fail_msg}"
|
|
300
|
+
fi
|
|
301
|
+
fi
|
|
302
|
+
else
|
|
303
|
+
_install_scheduler_cron "$cron_tag" "$cron_schedule" "$exec_command" "$log_file" "$env_vars"
|
|
304
|
+
if crontab -l 2>/dev/null | grep -qF "${cron_tag}" 2>/dev/null; then
|
|
305
|
+
print_info "${success_msg} (cron)"
|
|
306
|
+
else
|
|
307
|
+
print_warning "${fail_msg}"
|
|
308
|
+
fi
|
|
309
|
+
fi
|
|
310
|
+
return 0
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
# Uninstall a scheduler across all backends (launchd/systemd/cron).
|
|
314
|
+
# Args:
|
|
315
|
+
# $1 = os (output of uname -s)
|
|
316
|
+
# $2 = launchd_label (e.g. "sh.aidevops.stats-wrapper")
|
|
317
|
+
# $3 = systemd_name (e.g. "aidevops-stats-wrapper")
|
|
318
|
+
# $4 = cron_tag (grep pattern for cron line, e.g. "aidevops: stats-wrapper")
|
|
319
|
+
# $5 = success_msg (message to print on removal)
|
|
320
|
+
# Returns 0 always.
|
|
321
|
+
_uninstall_scheduler() {
|
|
322
|
+
local _os="$1"
|
|
323
|
+
local launchd_label="$2"
|
|
324
|
+
local systemd_name="$3"
|
|
325
|
+
local cron_tag="$4"
|
|
326
|
+
local success_msg="$5"
|
|
327
|
+
|
|
328
|
+
if [[ "$_os" == "Darwin" ]]; then
|
|
329
|
+
local _plist="$HOME/Library/LaunchAgents/${launchd_label}.plist"
|
|
330
|
+
if _launchd_has_agent "$launchd_label"; then
|
|
331
|
+
launchctl unload "$_plist" 2>/dev/null || true
|
|
332
|
+
rm -f "$_plist"
|
|
333
|
+
print_info "${success_msg} (launchd agent removed)"
|
|
334
|
+
fi
|
|
335
|
+
else
|
|
336
|
+
# Check and remove from ALL backends sequentially, not just the first
|
|
337
|
+
# match. Prevents orphan entries when migrating between systemd and cron
|
|
338
|
+
# (GH#17695 Finding A).
|
|
339
|
+
if _systemd_user_available && systemctl --user is-enabled "${systemd_name}.timer" >/dev/null 2>&1; then
|
|
340
|
+
systemctl --user disable --now "${systemd_name}.timer" 2>/dev/null || true
|
|
341
|
+
rm -f "$HOME/.config/systemd/user/${systemd_name}.service"
|
|
342
|
+
rm -f "$HOME/.config/systemd/user/${systemd_name}.timer"
|
|
343
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
344
|
+
print_info "${success_msg} (systemd timer removed)"
|
|
345
|
+
fi
|
|
346
|
+
if command -v crontab >/dev/null 2>&1; then
|
|
347
|
+
local current_cron
|
|
348
|
+
current_cron=$(crontab -l 2>/dev/null) || current_cron=""
|
|
349
|
+
if [[ -n "$current_cron" ]] && echo "$current_cron" | grep -qF "${cron_tag}"; then
|
|
350
|
+
echo "$current_cron" | grep -vF "${cron_tag}" | crontab - 2>/dev/null || true
|
|
351
|
+
print_info "${success_msg} (cron entry removed)"
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
fi
|
|
355
|
+
return 0
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
# Uninstall supervisor pulse (user explicitly disabled)
|
|
359
|
+
_uninstall_pulse() {
|
|
360
|
+
local _os="$1"
|
|
361
|
+
local pulse_label="$2"
|
|
362
|
+
if [[ "$_os" == "Darwin" ]]; then
|
|
363
|
+
local pulse_plist="$HOME/Library/LaunchAgents/${pulse_label}.plist"
|
|
364
|
+
if _launchd_has_agent "$pulse_label"; then
|
|
365
|
+
launchctl unload "$pulse_plist" || true
|
|
366
|
+
rm -f "$pulse_plist"
|
|
367
|
+
pkill -f 'Supervisor Pulse' 2>/dev/null || true
|
|
368
|
+
print_info "Supervisor pulse disabled (launchd agent removed per config)"
|
|
369
|
+
fi
|
|
370
|
+
elif _systemd_user_available; then
|
|
371
|
+
local service_name="aidevops-supervisor-pulse"
|
|
372
|
+
if systemctl --user is-enabled "${service_name}.timer" >/dev/null 2>&1; then
|
|
373
|
+
systemctl --user disable --now "${service_name}.timer" 2>/dev/null || true
|
|
374
|
+
rm -f "$HOME/.config/systemd/user/${service_name}.service"
|
|
375
|
+
rm -f "$HOME/.config/systemd/user/${service_name}.timer"
|
|
376
|
+
systemctl --user daemon-reload 2>/dev/null || true
|
|
377
|
+
print_info "Supervisor pulse disabled (systemd timer removed per config)"
|
|
378
|
+
fi
|
|
379
|
+
else
|
|
380
|
+
if crontab -l 2>/dev/null | grep -qF "pulse-wrapper"; then
|
|
381
|
+
crontab -l 2>/dev/null | grep -v 'aidevops: supervisor-pulse' | crontab - || true
|
|
382
|
+
print_info "Supervisor pulse disabled (cron entry removed per config)"
|
|
383
|
+
fi
|
|
384
|
+
fi
|
|
385
|
+
return 0
|
|
386
|
+
}
|