aidevops 3.13.95 → 3.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,301 +0,0 @@
1
- #!/usr/bin/env bash
2
- # SPDX-License-Identifier: MIT
3
- # SPDX-FileCopyrightText: 2025-2026 Marcus Quinn
4
- # Post-setup functions: auto-update enablement, final instructions, onboarding prompt.
5
- # Part of aidevops setup.sh modularization (GH#5793)
6
-
7
- # Shell safety baseline
8
- set -Eeuo pipefail
9
- IFS=$'\n\t'
10
- # shellcheck disable=SC2154 # rc is assigned by $? in the trap string
11
- trap 'rc=$?; echo "[ERROR] ${BASH_SOURCE[0]}:${LINENO} exit $rc" >&2' ERR
12
- shopt -s inherit_errexit 2>/dev/null || true
13
-
14
- # Enable auto-update if not already enabled.
15
- # Check both launchd (macOS) and cron (Linux) for existing installation.
16
- # Respects config: aidevops config set updates.auto_update false
17
- setup_auto_update() {
18
- local auto_update_script="$HOME/.aidevops/agents/scripts/auto-update-helper.sh"
19
- if ! [[ -x "$auto_update_script" ]] || ! is_feature_enabled auto_update 2>/dev/null; then
20
- return 0
21
- fi
22
-
23
- local _auto_update_installed=false
24
- if _scheduler_detect_installed \
25
- "Auto-update" \
26
- "com.aidevops.aidevops-auto-update" \
27
- "com.aidevops.auto-update" \
28
- "aidevops-auto-update" \
29
- "$auto_update_script" \
30
- "enable" \
31
- "aidevops auto-update enable" \
32
- "aidevops-auto-update"; then
33
- _auto_update_installed=true
34
- fi
35
- # t2898: ALWAYS run idempotent re-install + health-check after the
36
- # detection above, so every release self-heals broken installs (daemon
37
- # unloaded by an OS update, scrubbed by a cron cleanup, etc.). The
38
- # detection helper already handled the "freshly install if missing"
39
- # interactive prompt; this loop ensures that even when the detection
40
- # said "yes installed" the daemon is actually loaded right now and
41
- # running on schedule.
42
- if [[ "$_auto_update_installed" == "true" ]]; then
43
- # Idempotent: no-op when daemon already loaded; re-installs only on drift.
44
- bash "$auto_update_script" enable --idempotent >/dev/null 2>&1 || true
45
- # Verify it's actually healthy. Surface any degradation so the
46
- # operator sees it on every release deploy.
47
- local _hc_rc=0
48
- bash "$auto_update_script" health-check --quiet >/dev/null 2>&1 || _hc_rc=$?
49
- if [[ "$_hc_rc" -eq 0 ]]; then
50
- :
51
- elif [[ "$_hc_rc" -eq 1 ]]; then
52
- print_warning "Auto-update daemon installed but stalled — run: aidevops auto-update check"
53
- elif [[ "$_hc_rc" -eq 2 ]]; then
54
- print_warning "Auto-update daemon not loaded — run: aidevops auto-update enable"
55
- fi
56
- fi
57
- if [[ "$_auto_update_installed" == "false" ]]; then
58
- if [[ "$NON_INTERACTIVE" == "true" ]]; then
59
- # Non-interactive: enable silently
60
- bash "$auto_update_script" enable >/dev/null 2>&1 || true
61
- print_info "Auto-update enabled (every 10 min). Disable: aidevops auto-update disable"
62
- # On Linux systemd, advise about linger without running sudo automatically.
63
- if [[ "$(uname -s)" == "Linux" ]] && [[ "${USER:-}" != "root" ]] \
64
- && command -v loginctl &>/dev/null; then
65
- local _linger_state
66
- _linger_state=$(loginctl show-user "$USER" -p Linger --value 2>/dev/null || true)
67
- if [[ "$_linger_state" != "yes" ]]; then
68
- echo "[INFO] Linux systemd: enable linger so auto-update runs when logged out:" >&2
69
- echo "[INFO] sudo loginctl enable-linger $USER" >&2
70
- fi
71
- fi
72
- else
73
- echo ""
74
- echo "Auto-update keeps aidevops current by checking every 10 minutes."
75
- echo "Safe to run while AI sessions are active."
76
- echo ""
77
- setup_prompt enable_auto "Enable auto-update? [Y/n]: " "Y"
78
- if [[ "$enable_auto" =~ ^[Yy]?$ ]]; then
79
- bash "$auto_update_script" enable
80
- # On Linux systemd hosts, offer to enable linger so the timer survives logout.
81
- # Skip for root (irrelevant), WSL/container (loginctl may be absent or stub),
82
- # and when the backend didn't resolve to systemd.
83
- if [[ "$(uname -s)" == "Linux" ]] \
84
- && [[ "${USER:-}" != "root" ]] \
85
- && command -v loginctl &>/dev/null \
86
- && systemctl --user is-enabled aidevops-auto-update.timer &>/dev/null 2>&1; then
87
- local _linger_state
88
- _linger_state=$(loginctl show-user "$USER" -p Linger --value 2>/dev/null || true)
89
- if [[ "$_linger_state" != "yes" ]]; then
90
- echo ""
91
- echo "Without linger, the auto-update timer stops when you log out."
92
- echo "On servers and headless hosts, linger is almost always required."
93
- local enable_linger=""
94
- setup_prompt enable_linger "Enable linger so auto-update runs when logged out? Requires sudo. [Y/n]: " "Y"
95
- if [[ "$enable_linger" =~ ^[Yy]?$ ]]; then
96
- sudo loginctl enable-linger "$USER"
97
- print_success "Linger enabled for $USER"
98
- else
99
- print_info "Skipped. Enable later: sudo loginctl enable-linger $USER"
100
- fi
101
- fi
102
- fi
103
- else
104
- print_info "Skipped. Enable later: aidevops auto-update enable"
105
- fi
106
- fi
107
- fi
108
- return 0
109
- }
110
-
111
- # Print final setup instructions and feature summary.
112
- print_final_instructions() {
113
- echo ""
114
- echo "CLI Command:"
115
- echo " aidevops init - Initialize aidevops in a project"
116
- echo " aidevops features - List available features"
117
- echo " aidevops status - Check installation status"
118
- echo " aidevops update - Update to latest version"
119
- echo " aidevops update-tools - Check for and update installed tools"
120
- echo " aidevops uninstall - Remove aidevops"
121
- echo ""
122
- echo "Deployed to:"
123
- echo " ~/.aidevops/agents/ - Agent files (main agents, subagents, scripts)"
124
- echo " ~/.aidevops/*-backups/ - Backups with rotation (keeps last $BACKUP_KEEP_COUNT)"
125
- echo ""
126
- echo "Next steps:"
127
- echo "1. Review config templates in configs/ (keep as placeholders — never store real credentials there)"
128
- echo "2. Setup Git CLI tools and authentication (shown during setup)"
129
- echo "3. Setup API keys: bash ~/.aidevops/agents/scripts/setup-local-api-keys.sh setup"
130
- echo "4. Test access: bash ~/.aidevops/agents/scripts/servers-helper.sh list"
131
- echo "5. Enable orchestration: see runners.md 'Pulse Scheduler Setup' (autonomous task dispatch)"
132
- echo "6. Read documentation: ~/.aidevops/agents/AGENTS.md"
133
- echo ""
134
- echo "For development on aidevops framework itself:"
135
- echo " See ~/Git/aidevops/AGENTS.md"
136
- echo ""
137
- echo "OpenCode Primary Agents (12 total, Tab to switch):"
138
- echo "• Plan+ - Enhanced planning with context tools (read-only)"
139
- echo "• Build+ - Enhanced build with context tools (full access)"
140
- echo "• Accounts, AI-DevOps, Content, Health, Legal, Marketing,"
141
- echo " Research, Sales, SEO, WordPress"
142
- echo ""
143
- echo "Agent Skills (SKILL.md):"
144
- echo "• 21 SKILL.md files generated in ~/.aidevops/agents/"
145
- echo "• Skills include: wordpress, seo, aidevops, build-mcp, and more"
146
- echo ""
147
- echo "MCP Integrations (OpenCode):"
148
- echo "• Context7 - Real-time library documentation"
149
- echo "• GSC - Google Search Console (MCP + OAuth2)"
150
- echo "• Google Analytics - Analytics data (shared GSC credentials)"
151
- echo ""
152
- echo "SEO Integrations (curl subagents - no MCP overhead):"
153
- echo "• DataForSEO - Comprehensive SEO data APIs"
154
- echo "• Serper - Google Search API"
155
- echo "• Ahrefs - Backlink and keyword data"
156
- echo ""
157
- echo "DSPy & DSPyGround Integration:"
158
- echo "• ./.agents/scripts/dspy-helper.sh - DSPy prompt optimization toolkit"
159
- echo "• ./.agents/scripts/dspyground-helper.sh - DSPyGround playground interface"
160
- echo "• python-env/dspy-env/ - Python virtual environment for DSPy"
161
- echo "• data/dspy/ - DSPy projects and datasets"
162
- echo "• data/dspyground/ - DSPyGround projects and configurations"
163
- echo ""
164
- echo "Task Management:"
165
- echo "• Beads CLI (bd) - Task graph visualization"
166
- echo "• beads-sync-helper.sh - Sync TODO.md/PLANS.md with Beads"
167
- echo "• todo-ready.sh - Show tasks with no open blockers"
168
- echo "• Run: aidevops init beads - Initialize Beads in a project"
169
- echo ""
170
- echo "Autonomous Orchestration:"
171
- echo "• Supervisor pulse - Dispatches workers, merges PRs, evaluates results"
172
- echo "• Auto-pickup - Workers claim #auto-dispatch tasks from TODO.md"
173
- echo "• Cross-repo visibility - Manages tasks across all repos in repos.json"
174
- echo "• Strategic review (opus) - 4-hourly queue health, root cause analysis"
175
- echo "• Model routing - Cost-aware: local>haiku>flash>sonnet>pro>opus"
176
- echo "• Budget tracking - Per-provider spend limits, subscription-aware"
177
- echo "• Session miner - Extracts learning from past sessions"
178
- echo "• Circuit breaker - Pauses dispatch on consecutive failures"
179
- echo ""
180
- echo " Supervisor pulse (autonomous orchestration) requires explicit consent."
181
- echo " Enable: aidevops config set orchestration.supervisor_pulse true && ./setup.sh"
182
- echo ""
183
- echo " Run /onboarding in your AI assistant to configure services interactively."
184
- echo ""
185
- echo "Security reminders:"
186
- echo "- Never commit configuration files with real credentials"
187
- echo "- Use strong passwords and enable MFA on all accounts"
188
- echo "- Regularly rotate API tokens and SSH keys"
189
- echo ""
190
- echo "Happy server managing! 🚀"
191
- echo ""
192
- return 0
193
- }
194
-
195
- # Setup Tabby terminal profiles from repos.json.
196
- # Creates a profile per registered repo with colour-matched themes and
197
- # the TABBY_AUTORUN hook for OpenCode TUI compatibility.
198
- # Skipped if Tabby is not installed.
199
- setup_tabby() {
200
- local tabby_helper="$HOME/.aidevops/agents/scripts/tabby-helper.sh"
201
- local tabby_config
202
-
203
- # Platform-aware config path
204
- if [[ "$(uname -s)" == "Darwin" ]]; then
205
- tabby_config="$HOME/Library/Application Support/tabby/config.yaml"
206
- else
207
- tabby_config="$HOME/.config/tabby-terminal/config.yaml"
208
- fi
209
-
210
- # Skip if Tabby not installed
211
- if [[ ! -f "$tabby_config" ]]; then
212
- return 0
213
- fi
214
-
215
- # Skip if helper not deployed yet
216
- if [[ ! -x "$tabby_helper" ]]; then
217
- return 0
218
- fi
219
-
220
- print_info "Tabby terminal detected"
221
-
222
- # Ensure default local profile uses /bin/zsh (macOS).
223
- # After macOS updates, Tabby can fall back to bash when this is unset.
224
- bash "$tabby_helper" fix-shell || true
225
-
226
- # Install zshrc hook (idempotent)
227
- if ! bash "$tabby_helper" zshrc; then
228
- print_warning "Failed to install Tabby zshrc hook — run manually: aidevops tabby zshrc"
229
- fi
230
-
231
- if [[ "$NON_INTERACTIVE" == "true" ]]; then
232
- # Non-interactive: sync silently, warn on failure
233
- if ! bash "$tabby_helper" sync; then
234
- print_warning "Tabby profile sync failed — run manually: aidevops tabby sync"
235
- fi
236
- return 0
237
- fi
238
-
239
- # Show status and offer to sync
240
- echo ""
241
- bash "$tabby_helper" status || true
242
- echo ""
243
- setup_prompt sync_tabby "Sync Tabby profiles from repos.json? [Y/n]: " "Y"
244
- if [[ "$sync_tabby" =~ ^[Yy]?$ ]]; then
245
- bash "$tabby_helper" sync
246
- else
247
- print_info "Skipped. Run later: aidevops tabby sync"
248
- fi
249
-
250
- return 0
251
- }
252
-
253
- # Offer to launch onboarding for new users (only if not running inside an AI
254
- # runtime session and not non-interactive). (t1665.5 — registry-driven)
255
- # Respects config: aidevops config set ui.onboarding_prompt false
256
- setup_onboarding_prompt() {
257
- # Skip if non-interactive or already inside a runtime session
258
- [[ "$NON_INTERACTIVE" == "true" ]] && return 0
259
- [[ -n "${OPENCODE_SESSION:-}" || -n "${CLAUDE_SESSION:-}" ]] && return 0
260
- is_feature_enabled onboarding_prompt 2>/dev/null || return 0
261
-
262
- # Find first available headless runtime for onboarding dispatch
263
- local _onb_bin="" _onb_name=""
264
- if type rt_list_headless &>/dev/null; then
265
- local _onb_rt_id
266
- while IFS= read -r _onb_rt_id; do
267
- _onb_bin=$(rt_binary "$_onb_rt_id") || continue
268
- if [[ -n "$_onb_bin" ]] && command -v "$_onb_bin" &>/dev/null; then
269
- _onb_name=$(rt_display_name "$_onb_rt_id") || _onb_name="$_onb_bin"
270
- break
271
- fi
272
- _onb_bin=""
273
- done < <(rt_list_headless)
274
- fi
275
- # Fallback
276
- if [[ -z "$_onb_bin" ]] && command -v opencode &>/dev/null; then
277
- _onb_bin="opencode"
278
- _onb_name="OpenCode"
279
- fi
280
- [[ -z "$_onb_bin" ]] && return 0
281
-
282
- echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
283
- echo ""
284
- echo "Ready to configure your services?"
285
- echo ""
286
- echo "Launch ${_onb_name} with the onboarding wizard to:"
287
- echo " - See which services are already configured"
288
- echo " - Get personalized recommendations based on your work"
289
- echo " - Set up API keys and credentials interactively"
290
- echo ""
291
- setup_prompt launch_onboarding "Launch ${_onb_name} with /onboarding now? [Y/n]: " "Y"
292
- if [[ "$launch_onboarding" =~ ^[Yy]?$ ]]; then
293
- echo ""
294
- echo "Starting ${_onb_name} with onboarding wizard..."
295
- "$_onb_bin" --prompt "/onboarding"
296
- else
297
- echo ""
298
- echo "You can run /onboarding anytime in ${_onb_name} to configure services."
299
- fi
300
- return 0
301
- }
@@ -1,386 +0,0 @@
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
- }