aidevops 3.13.95 → 3.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/VERSION +1 -1
- package/aidevops.sh +44 -26
- package/package.json +1 -1
- package/setup.sh +25 -21
- package/aidevops-init-lib.sh +0 -1411
- package/aidevops-repos-lib.sh +0 -700
- package/aidevops-skills-plugin-lib.sh +0 -697
- package/aidevops-status-lib.sh +0 -141
- package/aidevops-update-lib.sh +0 -512
- package/aidevops-upgrade-planning-lib.sh +0 -370
- package/setup-modules/agent-deploy.sh +0 -1035
- package/setup-modules/agent-runtime.sh +0 -287
- package/setup-modules/config.sh +0 -478
- package/setup-modules/core.sh +0 -736
- package/setup-modules/mcp-setup.sh +0 -947
- package/setup-modules/migrations.sh +0 -1688
- package/setup-modules/plugins.sh +0 -728
- package/setup-modules/post-setup.sh +0 -301
- package/setup-modules/schedulers-linux.sh +0 -386
- package/setup-modules/schedulers-platform.sh +0 -1072
- package/setup-modules/schedulers-pulse.sh +0 -978
- package/setup-modules/schedulers.sh +0 -565
- package/setup-modules/shell-env.sh +0 -1240
- package/setup-modules/tool-beads.sh +0 -324
- package/setup-modules/tool-install.sh +0 -2134
|
@@ -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
|
-
}
|