@matt82198/aesop 0.1.0-beta.3 → 0.1.0-beta.5
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/CHANGELOG.md +79 -95
- package/CLAUDE-TEMPLATE.md +115 -0
- package/README.md +126 -245
- package/aesop.config.example.json +34 -26
- package/bin/CLAUDE.md +67 -0
- package/bin/cli.js +593 -36
- package/daemons/CLAUDE.md +36 -0
- package/daemons/backup-fleet.sh +92 -21
- package/daemons/run-watchdog.sh +95 -12
- package/dash/CLAUDE.md +32 -0
- package/dash/dash-extra.mjs +194 -21
- package/dash/watchdog-gui.sh +23 -6
- package/docs/BEHAVIORAL-PR-REVIEW.md +86 -0
- package/docs/CHECKPOINTING.md +100 -0
- package/docs/DISPATCH-MODEL.md +11 -7
- package/docs/FORENSICS.md +204 -0
- package/docs/GOVERNANCE.md +4 -2
- package/docs/HOOK-INSTALL.md +307 -0
- package/docs/HOW-THE-LOOP-WORKS.md +175 -0
- package/docs/MEMORY-TEMPLATE.md +61 -0
- package/docs/README.md +44 -0
- package/docs/RELEASING.md +159 -0
- package/docs/RELIABILITY.md +61 -0
- package/docs/RESTORE.md +397 -0
- package/docs/SCRIPTS-POLICY.md +97 -0
- package/docs/archive/README.md +3 -0
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +125 -0
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +287 -0
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +113 -0
- package/docs/archive/spikes/tiered-cognition/README.md +27 -0
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +32 -0
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +673 -0
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +434 -0
- package/docs/case-study-portfolio.md +61 -0
- package/docs/self-stats-data.json +11 -0
- package/docs/templates/FLEET-OPS-ANALYSIS.example.md +27 -0
- package/docs/templates/FLEET-OPS-RECOMMENDATIONS.example.md +24 -0
- package/docs/templates/PROPOSALS-LOG.example.md +64 -0
- package/hooks/CLAUDE.md +61 -0
- package/hooks/claude/force-model-policy.mjs +159 -0
- package/hooks/pre-push-policy.sh +753 -0
- package/mcp/CLAUDE.md +213 -0
- package/mcp/package.json +26 -0
- package/mcp/server.mjs +543 -0
- package/monitor/.signal-state.json +3 -0
- package/monitor/ACTIONS.log +1 -0
- package/monitor/BRIEF.md +24 -0
- package/monitor/CHARTER.md +75 -88
- package/monitor/CLAUDE.md +40 -0
- package/monitor/SIGNALS.json +54 -0
- package/monitor/collect-signals.mjs +613 -61
- package/package.json +15 -3
- package/scan/fleet-scan.example.mjs +292 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/healthcheck/SKILL.md +44 -0
- package/skills/power/SKILL.md +161 -0
- package/state_store/CLAUDE.md +39 -0
- package/state_store/__init__.py +24 -0
- package/state_store/__pycache__/__init__.cpython-314.pyc +0 -0
- package/state_store/__pycache__/api.cpython-314.pyc +0 -0
- package/state_store/__pycache__/export.cpython-314.pyc +0 -0
- package/state_store/__pycache__/ingest.cpython-314.pyc +0 -0
- package/state_store/__pycache__/projections.cpython-314.pyc +0 -0
- package/state_store/__pycache__/store.cpython-314.pyc +0 -0
- package/state_store/api.py +35 -0
- package/state_store/export.py +19 -0
- package/state_store/ingest.py +24 -0
- package/state_store/projections.py +52 -0
- package/state_store/store.py +102 -0
- package/tools/CLAUDE.md +72 -0
- package/tools/__pycache__/alert_bridge.cpython-314.pyc +0 -0
- package/tools/__pycache__/buildlog.cpython-314.pyc +0 -0
- package/tools/__pycache__/ci_merge_wait.cpython-314.pyc +0 -0
- package/tools/__pycache__/ensure_state.cpython-314.pyc +0 -0
- package/tools/__pycache__/eod_sweep.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_ledger.cpython-314.pyc +0 -0
- package/tools/__pycache__/fleet_prompt_extractor.cpython-314.pyc +0 -0
- package/tools/__pycache__/healthcheck.cpython-314.pyc +0 -0
- package/tools/__pycache__/heartbeat.cpython-314.pyc +0 -0
- package/tools/__pycache__/inbox_drain.cpython-314.pyc +0 -0
- package/tools/__pycache__/launch_tui.cpython-314.pyc +0 -0
- package/tools/__pycache__/metrics_gate.cpython-314.pyc +0 -0
- package/tools/__pycache__/orchestrator_status.cpython-314.pyc +0 -0
- package/tools/__pycache__/power_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/prepublish_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/rotate_logs.cpython-314.pyc +0 -0
- package/tools/__pycache__/scanner_selftest.cpython-314.pyc +0 -0
- package/tools/__pycache__/secret_scan.cpython-314.pyc +0 -0
- package/tools/__pycache__/self_stats.cpython-314.pyc +0 -0
- package/tools/__pycache__/session_usage_summary.cpython-314.pyc +0 -0
- package/tools/__pycache__/stall_check.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_replay.cpython-314.pyc +0 -0
- package/tools/__pycache__/transcript_timeline.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_dash.cpython-314.pyc +0 -0
- package/tools/__pycache__/verify_submit_encoding.cpython-314.pyc +0 -0
- package/tools/agent-forensics.sh +186 -0
- package/tools/alert_bridge.py +449 -0
- package/tools/buildlog.py +182 -0
- package/tools/ci_merge_wait.py +317 -0
- package/tools/ensure_state.py +94 -0
- package/tools/eod_sweep.py +239 -0
- package/tools/fleet_ledger.py +272 -0
- package/tools/fleet_prompt_extractor.py +134 -0
- package/tools/healthcheck.py +296 -0
- package/tools/heartbeat.py +131 -0
- package/tools/inbox_drain.py +249 -0
- package/tools/launch_tui.py +51 -3
- package/tools/lock.mjs +211 -0
- package/tools/metrics_gate.py +205 -0
- package/tools/orchestrator_status.py +99 -0
- package/tools/power_selftest.py +386 -0
- package/tools/prepublish_scan.py +84 -0
- package/tools/proposals.mjs +248 -0
- package/tools/reconstitute.sh +467 -0
- package/tools/rotate_logs.py +228 -0
- package/tools/scanner_selftest.py +303 -0
- package/tools/secret_scan.py +138 -31
- package/tools/self_stats.py +509 -0
- package/tools/session_usage_summary.py +198 -0
- package/tools/stall_check.py +172 -0
- package/tools/svg_to_png.mjs +50 -0
- package/tools/transcript_replay.py +236 -0
- package/tools/transcript_timeline.py +184 -0
- package/tools/verify_dash.py +513 -0
- package/tools/verify_submit_encoding.py +202 -0
- package/ui/CLAUDE.md +125 -0
- package/ui/__pycache__/agents.cpython-314.pyc +0 -0
- package/ui/__pycache__/collectors.cpython-314.pyc +0 -0
- package/ui/__pycache__/config.cpython-314.pyc +0 -0
- package/ui/__pycache__/cost.cpython-314.pyc +0 -0
- package/ui/__pycache__/csrf.cpython-314.pyc +0 -0
- package/ui/__pycache__/handler.cpython-314.pyc +0 -0
- package/ui/__pycache__/render.cpython-314.pyc +0 -0
- package/ui/__pycache__/serve.cpython-314.pyc +0 -0
- package/ui/__pycache__/sse.cpython-314.pyc +0 -0
- package/ui/agents.py +183 -0
- package/ui/api/__init__.py +84 -0
- package/ui/api/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/submit.cpython-314.pyc +0 -0
- package/ui/api/__pycache__/tracker.cpython-314.pyc +0 -0
- package/ui/api/submit.py +97 -0
- package/ui/api/tracker.py +116 -0
- package/ui/collectors.py +735 -0
- package/ui/config.py +123 -0
- package/ui/cost.py +260 -0
- package/ui/csrf.py +144 -0
- package/ui/handler.py +676 -0
- package/ui/render.py +49 -0
- package/ui/serve.py +68 -796
- package/ui/sse.py +183 -0
- package/ui/web/.gitattributes +13 -0
- package/ui/web/dist/assets/index-2LZDQirC.js +9 -0
- package/ui/web/dist/assets/index-D4M1qyOv.css +1 -0
- package/ui/web/dist/index.html +14 -0
- package/ui/web/index.html +13 -0
- package/ui/web/package-lock.json +2225 -0
- package/ui/web/package.json +26 -0
- package/ui/web/src/App.test.tsx +74 -0
- package/ui/web/src/App.tsx +142 -0
- package/ui/web/src/CONTRIBUTING-UI.md +49 -0
- package/ui/web/src/components/AgentRow.css +187 -0
- package/ui/web/src/components/AgentRow.test.tsx +209 -0
- package/ui/web/src/components/AgentRow.tsx +207 -0
- package/ui/web/src/components/AgentsPanel.css +108 -0
- package/ui/web/src/components/AgentsPanel.test.tsx +41 -0
- package/ui/web/src/components/AgentsPanel.tsx +58 -0
- package/ui/web/src/components/AlertsPanel.css +88 -0
- package/ui/web/src/components/AlertsPanel.test.tsx +51 -0
- package/ui/web/src/components/AlertsPanel.tsx +67 -0
- package/ui/web/src/components/BacklogPanel.test.tsx +126 -0
- package/ui/web/src/components/BacklogPanel.tsx +122 -0
- package/ui/web/src/components/CostChart.css +110 -0
- package/ui/web/src/components/CostChart.test.tsx +144 -0
- package/ui/web/src/components/CostChart.tsx +152 -0
- package/ui/web/src/components/CostTable.css +93 -0
- package/ui/web/src/components/CostTable.test.tsx +165 -0
- package/ui/web/src/components/CostTable.tsx +94 -0
- package/ui/web/src/components/EventsFeed.css +68 -0
- package/ui/web/src/components/EventsFeed.test.tsx +36 -0
- package/ui/web/src/components/EventsFeed.tsx +31 -0
- package/ui/web/src/components/HealthHeader.css +137 -0
- package/ui/web/src/components/HealthHeader.test.tsx +278 -0
- package/ui/web/src/components/HealthHeader.tsx +281 -0
- package/ui/web/src/components/InboxForm.css +135 -0
- package/ui/web/src/components/InboxForm.test.tsx +208 -0
- package/ui/web/src/components/InboxForm.tsx +116 -0
- package/ui/web/src/components/MessagesTail.module.css +144 -0
- package/ui/web/src/components/MessagesTail.test.tsx +176 -0
- package/ui/web/src/components/MessagesTail.tsx +94 -0
- package/ui/web/src/components/ReposPanel.css +90 -0
- package/ui/web/src/components/ReposPanel.test.tsx +45 -0
- package/ui/web/src/components/ReposPanel.tsx +67 -0
- package/ui/web/src/components/Scorecard.css +106 -0
- package/ui/web/src/components/Scorecard.test.tsx +117 -0
- package/ui/web/src/components/Scorecard.tsx +85 -0
- package/ui/web/src/components/Timeline.module.css +151 -0
- package/ui/web/src/components/Timeline.test.tsx +215 -0
- package/ui/web/src/components/Timeline.tsx +99 -0
- package/ui/web/src/components/TrackerBoard.test.tsx +121 -0
- package/ui/web/src/components/TrackerBoard.tsx +107 -0
- package/ui/web/src/components/TrackerCard.test.tsx +180 -0
- package/ui/web/src/components/TrackerCard.tsx +160 -0
- package/ui/web/src/components/TrackerForm.test.tsx +189 -0
- package/ui/web/src/components/TrackerForm.tsx +144 -0
- package/ui/web/src/lib/api.ts +218 -0
- package/ui/web/src/lib/format.test.ts +89 -0
- package/ui/web/src/lib/format.ts +103 -0
- package/ui/web/src/lib/sanitizeUrl.test.ts +84 -0
- package/ui/web/src/lib/sanitizeUrl.ts +38 -0
- package/ui/web/src/lib/types.ts +230 -0
- package/ui/web/src/lib/useHashRoute.test.ts +60 -0
- package/ui/web/src/lib/useHashRoute.ts +23 -0
- package/ui/web/src/lib/useSSE.ts +175 -0
- package/ui/web/src/main.tsx +10 -0
- package/ui/web/src/styles/global.css +179 -0
- package/ui/web/src/styles/theme.css +184 -0
- package/ui/web/src/styles/work.css +572 -0
- package/ui/web/src/test/fixtures.ts +385 -0
- package/ui/web/src/test/setup.ts +49 -0
- package/ui/web/src/views/Activity.module.css +43 -0
- package/ui/web/src/views/Activity.test.tsx +89 -0
- package/ui/web/src/views/Activity.tsx +31 -0
- package/ui/web/src/views/Cost.css +87 -0
- package/ui/web/src/views/Cost.test.tsx +142 -0
- package/ui/web/src/views/Cost.tsx +54 -0
- package/ui/web/src/views/Overview.css +51 -0
- package/ui/web/src/views/Overview.test.tsx +76 -0
- package/ui/web/src/views/Overview.tsx +46 -0
- package/ui/web/src/views/Work.test.tsx +82 -0
- package/ui/web/src/views/Work.tsx +79 -0
- package/ui/web/src/vite-env.d.ts +10 -0
- package/ui/web/tsconfig.json +22 -0
- package/ui/web/vite.config.ts +25 -0
- package/ui/web/vitest.config.ts +12 -0
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Reconstitute.sh: Clone or fetch repos from config/file
|
|
5
|
+
# Parses tab-delimited (url\ttarget) or space-delimited (url target) repos
|
|
6
|
+
# Tab-delimited recommended: preserves paths with spaces
|
|
7
|
+
# Legacy space-delimited supported but discouraged for multi-word paths
|
|
8
|
+
# ISSUE 1 FIX: Validates clone targets against fleet root (HIGH, security)
|
|
9
|
+
# ISSUE 2 FIX: Tests drive real script via TEST_MODE=1 (P1, bash)
|
|
10
|
+
# ISSUE 3 FIX: Uses read -r semantics for space-delimited parsing (P2, arch)
|
|
11
|
+
# ISSUE 4 FIX: validate_target resolves target/fleet_root PHYSICALLY
|
|
12
|
+
# (realpath -m style; cd -P/pwd -P or realpath/readlink -f, with a manual
|
|
13
|
+
# ancestor-walk fallback) so a symlink/junction planted inside the fleet
|
|
14
|
+
# root pointing outside it can no longer defeat containment (HIGH,
|
|
15
|
+
# security), while a legit nested target whose parent dir doesn't exist
|
|
16
|
+
# yet is still accepted (MEDIUM, false-reject fix)
|
|
17
|
+
|
|
18
|
+
DRY_RUN=0
|
|
19
|
+
TEST_MODE="${TEST_MODE:-0}"
|
|
20
|
+
REPOS_FILE=""
|
|
21
|
+
REPOS_CONFIG=""
|
|
22
|
+
AESOP_CONFIG="aesop.config.json"
|
|
23
|
+
AESOP_FLEET_ROOT="${AESOP_FLEET_ROOT:-}"
|
|
24
|
+
|
|
25
|
+
parse_args() {
|
|
26
|
+
while [ $# -gt 0 ]; do
|
|
27
|
+
case "$1" in
|
|
28
|
+
--dry-run)
|
|
29
|
+
DRY_RUN=1
|
|
30
|
+
shift
|
|
31
|
+
;;
|
|
32
|
+
--test)
|
|
33
|
+
TEST_MODE=1
|
|
34
|
+
shift
|
|
35
|
+
;;
|
|
36
|
+
--repos-file)
|
|
37
|
+
REPOS_FILE="$2"
|
|
38
|
+
shift 2
|
|
39
|
+
;;
|
|
40
|
+
*)
|
|
41
|
+
echo "Unknown option: $1"
|
|
42
|
+
exit 1
|
|
43
|
+
;;
|
|
44
|
+
esac
|
|
45
|
+
done
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
validate_url() {
|
|
49
|
+
local url="$1"
|
|
50
|
+
# Allow: https://, git@host:, ssh://, file:// (test mode only)
|
|
51
|
+
# Reject: leading dash, ext::, or other dangerous patterns
|
|
52
|
+
if [[ "$url" =~ ^(https://|git@[a-zA-Z0-9.-]+:|ssh://) ]]; then
|
|
53
|
+
return 0
|
|
54
|
+
fi
|
|
55
|
+
# Allow file:// and absolute paths only in test mode
|
|
56
|
+
if [ $TEST_MODE -eq 1 ]; then
|
|
57
|
+
if [[ "$url" =~ ^(file://|/) ]]; then
|
|
58
|
+
return 0
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
echo "Invalid URL: $url" >&2
|
|
62
|
+
return 1
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
get_fleet_root() {
|
|
66
|
+
# Priority: env var > config file > default home
|
|
67
|
+
if [ -n "$AESOP_FLEET_ROOT" ]; then
|
|
68
|
+
echo "$AESOP_FLEET_ROOT"
|
|
69
|
+
return 0
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
if [ -f "$AESOP_CONFIG" ]; then
|
|
73
|
+
local config_fleet_root
|
|
74
|
+
config_fleet_root=$(python3 -c "
|
|
75
|
+
import json
|
|
76
|
+
try:
|
|
77
|
+
with open('$AESOP_CONFIG') as f:
|
|
78
|
+
cfg = json.load(f)
|
|
79
|
+
fleet_root = cfg.get('fleet_root', '')
|
|
80
|
+
if fleet_root:
|
|
81
|
+
print(fleet_root)
|
|
82
|
+
except:
|
|
83
|
+
pass
|
|
84
|
+
" 2>/dev/null || true)
|
|
85
|
+
if [ -n "$config_fleet_root" ]; then
|
|
86
|
+
echo "$config_fleet_root"
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Default to home directory
|
|
92
|
+
echo "$HOME"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# resolve_physical_path: realpath -m style canonicalization.
|
|
96
|
+
#
|
|
97
|
+
# Resolves PATH to its physical (symlink/junction-free) absolute form:
|
|
98
|
+
# - Existing path components are resolved through the filesystem, so any
|
|
99
|
+
# directory symlink or Windows junction along the way is followed to
|
|
100
|
+
# its real physical target (never a "logical" cd result).
|
|
101
|
+
# - Trailing components that do not exist yet are appended verbatim,
|
|
102
|
+
# without requiring the full path to exist (like GNU `realpath -m`).
|
|
103
|
+
#
|
|
104
|
+
# This function is the single normalization point used by validate_target
|
|
105
|
+
# for both the clone target and the fleet root, so that a symlink/junction
|
|
106
|
+
# planted inside the fleet root pointing outside it cannot defeat the
|
|
107
|
+
# containment check (HIGH security fix), while a legitimate nested target
|
|
108
|
+
# whose parent directory doesn't exist yet is still accepted (MEDIUM
|
|
109
|
+
# false-reject fix).
|
|
110
|
+
resolve_physical_path() {
|
|
111
|
+
local input="$1"
|
|
112
|
+
local abs_path
|
|
113
|
+
|
|
114
|
+
if [ -z "$input" ]; then
|
|
115
|
+
return 1
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
if [[ "$input" = /* ]]; then
|
|
119
|
+
abs_path="$input"
|
|
120
|
+
else
|
|
121
|
+
abs_path="$PWD/$input"
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Preferred: GNU realpath -m. Resolves existing components physically
|
|
125
|
+
# and tolerates missing trailing components natively.
|
|
126
|
+
if command -v realpath > /dev/null 2>&1; then
|
|
127
|
+
local resolved
|
|
128
|
+
if resolved=$(realpath -m -- "$abs_path" 2>/dev/null) && [ -n "$resolved" ]; then
|
|
129
|
+
printf '%s\n' "$resolved"
|
|
130
|
+
return 0
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
|
|
134
|
+
# Fallback: GNU readlink -f, same missing-component tolerance.
|
|
135
|
+
if command -v readlink > /dev/null 2>&1; then
|
|
136
|
+
local resolved
|
|
137
|
+
if resolved=$(readlink -f -- "$abs_path" 2>/dev/null) && [ -n "$resolved" ]; then
|
|
138
|
+
printf '%s\n' "$resolved"
|
|
139
|
+
return 0
|
|
140
|
+
fi
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# Manual ancestor-walk fallback (portable, no external dependency):
|
|
144
|
+
# walk up from the target until we hit the nearest EXISTING ancestor,
|
|
145
|
+
# resolve that ancestor physically with `cd -P && pwd -P`, then
|
|
146
|
+
# re-append the missing trailing components unresolved (they don't
|
|
147
|
+
# exist yet, so there is nothing on disk to resolve).
|
|
148
|
+
local suffix=""
|
|
149
|
+
local current="$abs_path"
|
|
150
|
+
while [ -n "$current" ] && [ "$current" != "/" ] && [ ! -d "$current" ]; do
|
|
151
|
+
local base
|
|
152
|
+
base=$(basename "$current")
|
|
153
|
+
if [ -z "$suffix" ]; then
|
|
154
|
+
suffix="$base"
|
|
155
|
+
else
|
|
156
|
+
suffix="$base/$suffix"
|
|
157
|
+
fi
|
|
158
|
+
current=$(dirname "$current")
|
|
159
|
+
done
|
|
160
|
+
|
|
161
|
+
local physical_root
|
|
162
|
+
if [ -z "$current" ] || [ "$current" = "/" ]; then
|
|
163
|
+
physical_root="/"
|
|
164
|
+
else
|
|
165
|
+
physical_root=$(cd -P -- "$current" 2>/dev/null && pwd -P) || physical_root="$current"
|
|
166
|
+
fi
|
|
167
|
+
physical_root="${physical_root%/}"
|
|
168
|
+
[ -z "$physical_root" ] && physical_root="/"
|
|
169
|
+
|
|
170
|
+
if [ -n "$suffix" ]; then
|
|
171
|
+
printf '%s/%s\n' "$physical_root" "$suffix"
|
|
172
|
+
else
|
|
173
|
+
printf '%s\n' "$physical_root"
|
|
174
|
+
fi
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
validate_target() {
|
|
178
|
+
local target="$1"
|
|
179
|
+
local fleet_root="$2"
|
|
180
|
+
|
|
181
|
+
# Empty target
|
|
182
|
+
if [ -z "$target" ]; then
|
|
183
|
+
echo "Error: target path cannot be empty" >&2
|
|
184
|
+
return 1
|
|
185
|
+
fi
|
|
186
|
+
|
|
187
|
+
# Physically resolve both target and fleet_root (see resolve_physical_path
|
|
188
|
+
# above): this walks up to the nearest existing ancestor and resolves it
|
|
189
|
+
# with cd -P/pwd -P (falling back to realpath -m/readlink -f when
|
|
190
|
+
# available), so symlinks/junctions are followed to their real physical
|
|
191
|
+
# location instead of being trusted at face value.
|
|
192
|
+
local normalized
|
|
193
|
+
normalized=$(resolve_physical_path "$target") || {
|
|
194
|
+
echo "Error: could not resolve target path '$target'" >&2
|
|
195
|
+
return 1
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
local normalized_fleet_root
|
|
199
|
+
normalized_fleet_root=$(resolve_physical_path "$fleet_root") || {
|
|
200
|
+
echo "Error: could not resolve fleet root '$fleet_root'" >&2
|
|
201
|
+
return 1
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
# Ensure fleet root ends without trailing slash for comparison
|
|
205
|
+
normalized_fleet_root="${normalized_fleet_root%/}"
|
|
206
|
+
|
|
207
|
+
# Check if target is under fleet root
|
|
208
|
+
# Using pattern matching: if normalized path starts with fleet_root/, it's valid
|
|
209
|
+
if [[ "$normalized" = "$normalized_fleet_root" ]] || [[ "$normalized" = "$normalized_fleet_root"/* ]]; then
|
|
210
|
+
return 0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# Target escapes fleet root
|
|
214
|
+
echo "Error: target path '$target' (resolves to: $normalized) is outside fleet root '$fleet_root' (resolves to: $normalized_fleet_root)" >&2
|
|
215
|
+
return 1
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
load_repos_from_config() {
|
|
219
|
+
if [ -f "$AESOP_CONFIG" ]; then
|
|
220
|
+
# Also load fleet_root from config if not already set
|
|
221
|
+
if [ -z "$AESOP_FLEET_ROOT" ]; then
|
|
222
|
+
AESOP_FLEET_ROOT=$(python3 -c "
|
|
223
|
+
import json
|
|
224
|
+
try:
|
|
225
|
+
with open('$AESOP_CONFIG') as f:
|
|
226
|
+
cfg = json.load(f)
|
|
227
|
+
fleet_root = cfg.get('fleet_root', '')
|
|
228
|
+
if fleet_root:
|
|
229
|
+
print(fleet_root)
|
|
230
|
+
except:
|
|
231
|
+
pass
|
|
232
|
+
" 2>/dev/null || true)
|
|
233
|
+
fi
|
|
234
|
+
|
|
235
|
+
REPOS_CONFIG=$(python3 -c "
|
|
236
|
+
import json
|
|
237
|
+
try:
|
|
238
|
+
with open('$AESOP_CONFIG') as f:
|
|
239
|
+
cfg = json.load(f)
|
|
240
|
+
repos = cfg.get('repos', [])
|
|
241
|
+
for repo in repos:
|
|
242
|
+
path = repo.get('path', '')
|
|
243
|
+
url = repo.get('url', '')
|
|
244
|
+
if path:
|
|
245
|
+
if url:
|
|
246
|
+
print(url + '\t' + path)
|
|
247
|
+
else:
|
|
248
|
+
print(path)
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print('ERROR: Failed to parse config:', e, file=__import__('sys').stderr)
|
|
251
|
+
exit(1)
|
|
252
|
+
")
|
|
253
|
+
fi
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
run_test_suite() {
|
|
257
|
+
echo "Running self-test..."
|
|
258
|
+
local temp_root
|
|
259
|
+
temp_root=$(mktemp -d)
|
|
260
|
+
trap "rm -rf $temp_root" EXIT
|
|
261
|
+
|
|
262
|
+
# Set fleet root to temp_root for test isolation
|
|
263
|
+
AESOP_FLEET_ROOT="$temp_root"
|
|
264
|
+
|
|
265
|
+
local origin_bare="$temp_root/origin.bare"
|
|
266
|
+
local cloned_repo="$temp_root/cloned"
|
|
267
|
+
local repos_file="$temp_root/repos.txt"
|
|
268
|
+
|
|
269
|
+
echo "Setting up test fixtures..."
|
|
270
|
+
git init --bare "$origin_bare" > /dev/null 2>&1
|
|
271
|
+
git clone "$origin_bare" "$temp_root/workdir" > /dev/null 2>&1
|
|
272
|
+
(
|
|
273
|
+
cd "$temp_root/workdir"
|
|
274
|
+
# Local, repo-scoped identity: CI runners have no global git user.name/
|
|
275
|
+
# user.email configured, and "git commit" fails loudly (exit 128) without
|
|
276
|
+
# one, which kills this self-test under set -e.
|
|
277
|
+
git config user.email "test@example.com"
|
|
278
|
+
git config user.name "Test User"
|
|
279
|
+
echo "test content" > README.md
|
|
280
|
+
git add README.md
|
|
281
|
+
git commit -m "initial commit" > /dev/null 2>&1
|
|
282
|
+
git push origin main 2>/dev/null || true
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
echo "repos.txt test format:"
|
|
286
|
+
printf "%s\t%s\n" "$origin_bare" "$cloned_repo" > "$repos_file"
|
|
287
|
+
cat "$repos_file"
|
|
288
|
+
|
|
289
|
+
echo ""
|
|
290
|
+
echo "TEST 1: Clone missing repo (using real reconstruct_fleet)..."
|
|
291
|
+
(
|
|
292
|
+
cd "$temp_root"
|
|
293
|
+
REPOS_FILE="$repos_file" reconstruct_fleet
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
if [ -d "$cloned_repo/.git" ]; then
|
|
297
|
+
echo "PASS: clone succeeded"
|
|
298
|
+
else
|
|
299
|
+
echo "FAIL: clone failed"
|
|
300
|
+
return 1
|
|
301
|
+
fi
|
|
302
|
+
|
|
303
|
+
echo ""
|
|
304
|
+
echo "TEST 2: URL validation - reject ext:: prefix..."
|
|
305
|
+
cat > "$repos_file" << 'EOF'
|
|
306
|
+
ext::sh -c 'echo bad' /tmp/target
|
|
307
|
+
EOF
|
|
308
|
+
|
|
309
|
+
(
|
|
310
|
+
cd "$temp_root"
|
|
311
|
+
REPOS_FILE="$repos_file" reconstruct_fleet 2>&1 || true
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
echo "PASS: ext:: URL rejected gracefully"
|
|
315
|
+
|
|
316
|
+
echo ""
|
|
317
|
+
echo "TEST 3: URL validation - reject leading dash..."
|
|
318
|
+
cat > "$repos_file" << 'EOF'
|
|
319
|
+
-c /tmp/target
|
|
320
|
+
EOF
|
|
321
|
+
|
|
322
|
+
(
|
|
323
|
+
cd "$temp_root"
|
|
324
|
+
REPOS_FILE="$repos_file" reconstruct_fleet 2>&1 || true
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
echo "PASS: leading dash URL rejected gracefully"
|
|
328
|
+
|
|
329
|
+
echo ""
|
|
330
|
+
echo "Self-test completed."
|
|
331
|
+
return 0
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
reconstruct_fleet() {
|
|
335
|
+
local repos_to_process=""
|
|
336
|
+
local cloned_count=0
|
|
337
|
+
local fetched_count=0
|
|
338
|
+
local failed_count=0
|
|
339
|
+
|
|
340
|
+
if [ -n "$REPOS_FILE" ]; then
|
|
341
|
+
if [ ! -f "$REPOS_FILE" ]; then
|
|
342
|
+
echo "Error: repos file not found: $REPOS_FILE"
|
|
343
|
+
exit 1
|
|
344
|
+
fi
|
|
345
|
+
repos_to_process=$(<"$REPOS_FILE")
|
|
346
|
+
else
|
|
347
|
+
load_repos_from_config
|
|
348
|
+
repos_to_process="$REPOS_CONFIG"
|
|
349
|
+
fi
|
|
350
|
+
|
|
351
|
+
if [ -z "$repos_to_process" ]; then
|
|
352
|
+
echo "No repos to process."
|
|
353
|
+
exit 0
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
# Get the fleet root for target validation
|
|
357
|
+
local fleet_root
|
|
358
|
+
fleet_root=$(get_fleet_root)
|
|
359
|
+
|
|
360
|
+
echo "Processing repos..."
|
|
361
|
+
while IFS= read -r line; do
|
|
362
|
+
[ -z "$line" ] && continue
|
|
363
|
+
|
|
364
|
+
url=""
|
|
365
|
+
target=""
|
|
366
|
+
|
|
367
|
+
# Try to parse as tab-delimited first (url\ttarget)
|
|
368
|
+
if echo "$line" | grep -q $'\t'; then
|
|
369
|
+
url=$(echo "$line" | cut -f1)
|
|
370
|
+
target=$(echo "$line" | cut -f2-)
|
|
371
|
+
else
|
|
372
|
+
# Fall back to space-delimited (url target) for backward compatibility
|
|
373
|
+
# ISSUE 3 FIX: Use read -r to preserve spaces in the remainder
|
|
374
|
+
# Split on first space only
|
|
375
|
+
read -r url target <<< "$line"
|
|
376
|
+
fi
|
|
377
|
+
|
|
378
|
+
if [ -z "$target" ]; then
|
|
379
|
+
# If still no target, this line is config-only (just a path)
|
|
380
|
+
target="$line"
|
|
381
|
+
url=""
|
|
382
|
+
fi
|
|
383
|
+
|
|
384
|
+
if [ -z "$target" ]; then
|
|
385
|
+
continue
|
|
386
|
+
fi
|
|
387
|
+
|
|
388
|
+
# Validate URL if present
|
|
389
|
+
if [ -n "$url" ]; then
|
|
390
|
+
if ! validate_url "$url"; then
|
|
391
|
+
failed_count=$((failed_count + 1))
|
|
392
|
+
continue
|
|
393
|
+
fi
|
|
394
|
+
fi
|
|
395
|
+
|
|
396
|
+
# ISSUE 1 FIX: Validate target path against fleet root before any git operations
|
|
397
|
+
if ! validate_target "$target" "$fleet_root"; then
|
|
398
|
+
failed_count=$((failed_count + 1))
|
|
399
|
+
continue
|
|
400
|
+
fi
|
|
401
|
+
|
|
402
|
+
if [ ! -d "$target" ]; then
|
|
403
|
+
if [ $DRY_RUN -eq 1 ]; then
|
|
404
|
+
if [ -n "$url" ]; then
|
|
405
|
+
echo "[DRY-RUN] Would clone $url to $target"
|
|
406
|
+
fi
|
|
407
|
+
else
|
|
408
|
+
if [ -n "$url" ]; then
|
|
409
|
+
echo "Cloning $url to $target..."
|
|
410
|
+
git clone -- "$url" "$target" 2>&1 | tail -1
|
|
411
|
+
if [ -d "$target/.git" ]; then
|
|
412
|
+
cloned_count=$((cloned_count + 1))
|
|
413
|
+
else
|
|
414
|
+
failed_count=$((failed_count + 1))
|
|
415
|
+
fi
|
|
416
|
+
fi
|
|
417
|
+
fi
|
|
418
|
+
else
|
|
419
|
+
if [ $DRY_RUN -eq 1 ]; then
|
|
420
|
+
echo "[DRY-RUN] Would fetch $target"
|
|
421
|
+
else
|
|
422
|
+
echo "Fetching $target..."
|
|
423
|
+
git -C "$target" fetch --all --quiet 2>&1 && fetched_count=$((fetched_count + 1)) || failed_count=$((failed_count + 1))
|
|
424
|
+
fi
|
|
425
|
+
fi
|
|
426
|
+
done <<< "$repos_to_process"
|
|
427
|
+
|
|
428
|
+
echo ""
|
|
429
|
+
echo "=== Summary ==="
|
|
430
|
+
echo "CLONED: $cloned_count"
|
|
431
|
+
echo "FETCHED: $fetched_count"
|
|
432
|
+
echo "FAILED: $failed_count"
|
|
433
|
+
|
|
434
|
+
if [ $failed_count -gt 0 ]; then
|
|
435
|
+
# In test mode, return failure but don't exit
|
|
436
|
+
if [ $TEST_MODE -eq 1 ]; then
|
|
437
|
+
return 1
|
|
438
|
+
else
|
|
439
|
+
exit 1
|
|
440
|
+
fi
|
|
441
|
+
fi
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
main() {
|
|
445
|
+
parse_args "$@"
|
|
446
|
+
|
|
447
|
+
# If --test flag was explicitly passed, run the built-in test suite
|
|
448
|
+
# Otherwise, use TEST_MODE from environment (if set) for external testing
|
|
449
|
+
# This allows external tests to use TEST_MODE=1 without triggering the built-in suite
|
|
450
|
+
if [ "${TEST_SUITE:-0}" -eq 1 ]; then
|
|
451
|
+
run_test_suite
|
|
452
|
+
exit $?
|
|
453
|
+
fi
|
|
454
|
+
|
|
455
|
+
reconstruct_fleet
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
# Check if --test was explicitly passed (before parse_args overwrites it)
|
|
459
|
+
TEST_SUITE=0
|
|
460
|
+
for arg in "$@"; do
|
|
461
|
+
if [ "$arg" = "--test" ]; then
|
|
462
|
+
TEST_SUITE=1
|
|
463
|
+
break
|
|
464
|
+
fi
|
|
465
|
+
done
|
|
466
|
+
|
|
467
|
+
main "$@"
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Log rotation utility: archive oldest log lines when exceeding size/line thresholds.
|
|
3
|
+
|
|
4
|
+
Rotates log files by moving the oldest content to an archive file when the original
|
|
5
|
+
exceeds configured thresholds (--max-lines or --max-bytes). Preserves newest lines
|
|
6
|
+
in the original, ensures no data loss (archive + original == original content).
|
|
7
|
+
|
|
8
|
+
Exit codes:
|
|
9
|
+
0: Success (no rotation needed or rotation completed)
|
|
10
|
+
1: Error (invalid args, I/O failure, etc.)
|
|
11
|
+
2: Usage error
|
|
12
|
+
3: Rotation needed (--check mode only)
|
|
13
|
+
"""
|
|
14
|
+
import argparse
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def count_lines(filepath):
|
|
22
|
+
"""Count lines in a file."""
|
|
23
|
+
try:
|
|
24
|
+
with open(filepath, 'r') as f:
|
|
25
|
+
return sum(1 for _ in f)
|
|
26
|
+
except IOError:
|
|
27
|
+
return 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_file_size(filepath):
|
|
31
|
+
"""Get file size in bytes."""
|
|
32
|
+
try:
|
|
33
|
+
return os.path.getsize(filepath)
|
|
34
|
+
except OSError:
|
|
35
|
+
return 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def read_lines(filepath):
|
|
39
|
+
"""Read all lines from file, preserving newlines."""
|
|
40
|
+
try:
|
|
41
|
+
with open(filepath, 'r') as f:
|
|
42
|
+
return f.readlines()
|
|
43
|
+
except IOError as e:
|
|
44
|
+
raise IOError(f"Failed to read {filepath}: {e}")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_lines(filepath, lines):
|
|
48
|
+
"""Write lines to file."""
|
|
49
|
+
try:
|
|
50
|
+
with open(filepath, 'w') as f:
|
|
51
|
+
f.writelines(lines)
|
|
52
|
+
except IOError as e:
|
|
53
|
+
raise IOError(f"Failed to write {filepath}: {e}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def needs_rotation(filepath, max_lines, max_bytes):
|
|
57
|
+
"""Check if file exceeds any threshold."""
|
|
58
|
+
current_lines = count_lines(filepath)
|
|
59
|
+
current_bytes = get_file_size(filepath)
|
|
60
|
+
|
|
61
|
+
if max_lines and current_lines > max_lines:
|
|
62
|
+
return True
|
|
63
|
+
if max_bytes and current_bytes > max_bytes:
|
|
64
|
+
return True
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def rotate_log(logfile, max_lines, max_bytes, archive_dir, check_only=False):
|
|
69
|
+
"""Rotate log file by archiving oldest lines.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
logfile: Path to log file to rotate.
|
|
73
|
+
max_lines: Max lines before rotation (None to skip this check).
|
|
74
|
+
max_bytes: Max bytes before rotation (None to skip this check).
|
|
75
|
+
archive_dir: Directory to store archives (default: logfile dir/archive).
|
|
76
|
+
check_only: If True, only check if rotation needed (no writes).
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
0 if successful or no rotation needed,
|
|
80
|
+
3 if rotation needed (in check_only mode),
|
|
81
|
+
1 on error.
|
|
82
|
+
"""
|
|
83
|
+
logfile = Path(logfile)
|
|
84
|
+
|
|
85
|
+
# Check if rotation is needed
|
|
86
|
+
if not needs_rotation(str(logfile), max_lines, max_bytes):
|
|
87
|
+
return 0
|
|
88
|
+
|
|
89
|
+
# If check-only mode, report that rotation is needed
|
|
90
|
+
if check_only:
|
|
91
|
+
return 3
|
|
92
|
+
|
|
93
|
+
# Determine archive directory
|
|
94
|
+
if archive_dir:
|
|
95
|
+
archive_path = Path(archive_dir)
|
|
96
|
+
else:
|
|
97
|
+
archive_path = logfile.parent / "archive"
|
|
98
|
+
|
|
99
|
+
# Create archive directory if needed
|
|
100
|
+
try:
|
|
101
|
+
archive_path.mkdir(parents=True, exist_ok=True)
|
|
102
|
+
except OSError as e:
|
|
103
|
+
print(f"ERROR: Failed to create archive directory {archive_path}: {e}",
|
|
104
|
+
file=sys.stderr)
|
|
105
|
+
return 1
|
|
106
|
+
|
|
107
|
+
# Read all lines
|
|
108
|
+
try:
|
|
109
|
+
lines = read_lines(str(logfile))
|
|
110
|
+
except IOError as e:
|
|
111
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
112
|
+
return 1
|
|
113
|
+
|
|
114
|
+
if not lines:
|
|
115
|
+
return 0
|
|
116
|
+
|
|
117
|
+
# Determine split point based on which threshold was exceeded
|
|
118
|
+
current_lines = len(lines)
|
|
119
|
+
current_bytes = sum(len(line.encode('utf-8')) for line in lines)
|
|
120
|
+
|
|
121
|
+
keep_count = current_lines // 2 # Default: keep ~half
|
|
122
|
+
|
|
123
|
+
# If max_lines exceeded, keep lines just under threshold
|
|
124
|
+
if max_lines and current_lines > max_lines:
|
|
125
|
+
keep_count = min(keep_count, max_lines)
|
|
126
|
+
|
|
127
|
+
# If max_bytes exceeded, calculate how many lines fit under threshold
|
|
128
|
+
if max_bytes and current_bytes > max_bytes:
|
|
129
|
+
cumulative_bytes = 0
|
|
130
|
+
bytes_keep_count = 0
|
|
131
|
+
# Count from newest (end) backward
|
|
132
|
+
for i in range(len(lines) - 1, -1, -1):
|
|
133
|
+
line_bytes = len(lines[i].encode('utf-8'))
|
|
134
|
+
if cumulative_bytes + line_bytes <= max_bytes:
|
|
135
|
+
cumulative_bytes += line_bytes
|
|
136
|
+
bytes_keep_count += 1
|
|
137
|
+
else:
|
|
138
|
+
break
|
|
139
|
+
# Use the more conservative count
|
|
140
|
+
keep_count = min(keep_count, bytes_keep_count)
|
|
141
|
+
|
|
142
|
+
# Guard: ensure we keep at least 1 line, archive the rest
|
|
143
|
+
if keep_count <= 0:
|
|
144
|
+
# If calculated keep_count is 0 or negative, keep at least 1 line
|
|
145
|
+
keep_count = 1
|
|
146
|
+
|
|
147
|
+
# Split lines
|
|
148
|
+
archive_lines = lines[:-keep_count] if keep_count < len(lines) else []
|
|
149
|
+
remaining_lines = lines[-keep_count:] if keep_count > 0 else []
|
|
150
|
+
|
|
151
|
+
if not archive_lines:
|
|
152
|
+
# Nothing to archive
|
|
153
|
+
return 0
|
|
154
|
+
|
|
155
|
+
# Generate archive filename with UTC timestamp
|
|
156
|
+
utc_now = datetime.now(timezone.utc).strftime("%Y%m%d%H%M%S")
|
|
157
|
+
archive_filename = f"{logfile.stem}.{utc_now}.log"
|
|
158
|
+
archive_file = archive_path / archive_filename
|
|
159
|
+
|
|
160
|
+
# Write archive file
|
|
161
|
+
try:
|
|
162
|
+
write_lines(str(archive_file), archive_lines)
|
|
163
|
+
except IOError as e:
|
|
164
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
165
|
+
return 1
|
|
166
|
+
|
|
167
|
+
# Write remaining lines back to original
|
|
168
|
+
try:
|
|
169
|
+
write_lines(str(logfile), remaining_lines)
|
|
170
|
+
except IOError as e:
|
|
171
|
+
print(f"ERROR: {e}", file=sys.stderr)
|
|
172
|
+
return 1
|
|
173
|
+
|
|
174
|
+
return 0
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def main():
|
|
178
|
+
"""Entry point."""
|
|
179
|
+
parser = argparse.ArgumentParser(
|
|
180
|
+
description=__doc__,
|
|
181
|
+
formatter_class=argparse.RawDescriptionHelpFormatter
|
|
182
|
+
)
|
|
183
|
+
parser.add_argument(
|
|
184
|
+
"logfile",
|
|
185
|
+
help="Path to log file to rotate"
|
|
186
|
+
)
|
|
187
|
+
parser.add_argument(
|
|
188
|
+
"--max-lines",
|
|
189
|
+
type=int,
|
|
190
|
+
default=200,
|
|
191
|
+
help="Max lines before rotation (default: 200)"
|
|
192
|
+
)
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
"--max-bytes",
|
|
195
|
+
type=int,
|
|
196
|
+
default=20480,
|
|
197
|
+
help="Max bytes before rotation (default: 20480)"
|
|
198
|
+
)
|
|
199
|
+
parser.add_argument(
|
|
200
|
+
"--archive-dir",
|
|
201
|
+
default=None,
|
|
202
|
+
help="Directory for archived logs (default: <logfile-dir>/archive)"
|
|
203
|
+
)
|
|
204
|
+
parser.add_argument(
|
|
205
|
+
"--check",
|
|
206
|
+
action="store_true",
|
|
207
|
+
help="Check if rotation needed (exit 3 if yes, 0 if no; no writes)"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
args = parser.parse_args()
|
|
211
|
+
|
|
212
|
+
# Verify logfile exists
|
|
213
|
+
if not os.path.exists(args.logfile):
|
|
214
|
+
print(f"ERROR: Log file not found: {args.logfile}", file=sys.stderr)
|
|
215
|
+
return 1
|
|
216
|
+
|
|
217
|
+
# Run rotation
|
|
218
|
+
return rotate_log(
|
|
219
|
+
args.logfile,
|
|
220
|
+
args.max_lines,
|
|
221
|
+
args.max_bytes,
|
|
222
|
+
args.archive_dir,
|
|
223
|
+
check_only=args.check
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
if __name__ == "__main__":
|
|
228
|
+
sys.exit(main())
|