skillwiki 0.9.63 → 0.10.0

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.
Files changed (36) hide show
  1. package/dist/chunk-5TBIMLTZ.js +343 -0
  2. package/dist/chunk-C5OLZRRM.js +357 -0
  3. package/dist/{chunk-TUFQZ5K4.js → chunk-DR7KFHNH.js} +792 -1983
  4. package/dist/chunk-IZABIE44.js +647 -0
  5. package/dist/chunk-S5ABQCXQ.js +580 -0
  6. package/dist/cli.js +1540 -550
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-SILEUQEV.js +11 -0
  9. package/dist/skillwiki-mcp.js +4 -1
  10. package/dist/vault-sync/scripts/lib/conflict-markers.sh +69 -0
  11. package/dist/vault-sync/scripts/lib/delete-intent.sh +74 -0
  12. package/dist/vault-sync/scripts/lib/fleet.sh +103 -0
  13. package/dist/vault-sync/scripts/lib/git-case.sh +71 -0
  14. package/dist/vault-sync/scripts/lib/git-materialization.sh +264 -0
  15. package/dist/vault-sync/scripts/lib/git-operation-journal.sh +469 -0
  16. package/dist/vault-sync/scripts/lib/git-rebase-state.sh +180 -0
  17. package/dist/vault-sync/scripts/lib/lockfile.sh +70 -0
  18. package/dist/vault-sync/scripts/lib/managed-write-lock.sh +80 -0
  19. package/dist/vault-sync/scripts/lib/platform.sh +184 -0
  20. package/dist/vault-sync/scripts/lib/runtime-manifest.sh +223 -0
  21. package/dist/vault-sync/scripts/wiki-fetch-notify.sh +207 -0
  22. package/dist/vault-sync/scripts/wiki-fuse-refresh.sh +405 -0
  23. package/dist/vault-sync/scripts/wiki-pull-with-auto-resolve.sh +631 -0
  24. package/dist/vault-sync/scripts/wiki-push.sh +364 -0
  25. package/dist/vault-sync/scripts/wiki-snapshot.sh +587 -0
  26. package/package.json +2 -2
  27. package/skills/.claude-plugin/plugin.json +1 -1
  28. package/skills/.codex-plugin/plugin.json +1 -1
  29. package/skills/README.md +13 -0
  30. package/skills/package.json +1 -1
  31. package/skills/proj-work/SKILL.md +3 -0
  32. package/skills/skills/proj-work/SKILL.md +3 -0
  33. package/skills/skills/using-skillwiki/SKILL.md +13 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +13 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
@@ -0,0 +1,207 @@
1
+ #!/bin/bash
2
+ # wiki-fetch-notify.sh — polls origin for remote wiki changes and notifies.
3
+ #
4
+ # Runs `git fetch` against origin and emits a notification only when
5
+ # NEW commits have arrived since the previous run. Replaces the old
6
+ # seaweedfs-bisync writer with a non-destructive poller.
7
+ #
8
+ # Opt-in WIKI_FETCH_PULL_ON_DELTA=1: on positive delta, also runs
9
+ # `git pull --rebase` (via wiki-pull-with-auto-resolve.sh) so the local
10
+ # working tree consumes sg01 Snapshot commits automatically. This is a
11
+ # mutating action — when enabled, this script is no longer a pure reader.
12
+ #
13
+ # When pull is disabled (default), this script is read-only: fetch only
14
+ # touches refs/objects and is safe to run concurrently with skillwiki sync.
15
+
16
+ set -u
17
+
18
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-$0}" )" && pwd )"
19
+ . "$SCRIPT_DIR/lib/platform.sh"
20
+ . "$SCRIPT_DIR/lib/lockfile.sh"
21
+ . "$SCRIPT_DIR/lib/git-operation-journal.sh"
22
+ platform_detect_os
23
+
24
+ WIKI_DIR="${WIKI_DIR:-$HOME/wiki}"
25
+ BRANCH="${WIKI_BRANCH:-main}"
26
+ STATE_DIR="$(platform_cache_dir)/wiki-fetch"
27
+ STATE_FILE="$STATE_DIR/last-behind"
28
+ STALE_STATE_FILE="$STATE_DIR/last-stale-notify"
29
+ STALE_NOTIFY_AFTER_SECONDS="${WIKI_FETCH_STALE_NOTIFY_AFTER_SECONDS:-1800}"
30
+ HANDOFF_STATE_FILE="$STATE_DIR/pending-handoff.env"
31
+ HANDOFF_NOTIFY_AFTER_SECONDS="${WIKI_FETCH_HANDOFF_NOTIFY_AFTER_SECONDS:-3600}"
32
+ case "$HANDOFF_NOTIFY_AFTER_SECONDS" in
33
+ ''|*[!0-9]*) HANDOFF_NOTIFY_AFTER_SECONDS=3600 ;;
34
+ esac
35
+ # Opt-in: when enabled, a positive delta triggers `git pull --rebase` so the
36
+ # local working tree consumes sg01 Snapshot commits automatically. This
37
+ # replaces the git pull that was previously bundled inside wiki-push.sh's
38
+ # removed git-push block. Default off — set WIKI_FETCH_PULL_ON_DELTA=1 to
39
+ # enable on leaf hosts that want automated pull.
40
+ PULL_ON_DELTA="${WIKI_FETCH_PULL_ON_DELTA:-0}"
41
+ PULL_HELPER="$SCRIPT_DIR/wiki-pull-with-auto-resolve.sh"
42
+ LOG_FILE="$(platform_log_dir)/wiki-fetch.log"
43
+
44
+ mkdir -p "$STATE_DIR" "$(dirname "$LOG_FILE")"
45
+
46
+ log() {
47
+ printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"
48
+ }
49
+
50
+ handle_existing_handoff() {
51
+ local blocker reason op identity previous_identity notified_at now
52
+ blocker="$(vault_sync_op_preflight_blocker "$WIKI_DIR" 2>/dev/null || true)"
53
+ [ -n "$blocker" ] || { rm -f "$HANDOFF_STATE_FILE" 2>/dev/null || true; return 1; }
54
+
55
+ reason="${blocker%% *}"
56
+ op="${blocker#* }"
57
+ [ "$op" = "$blocker" ] && op=""
58
+ identity="${reason}:${op:-none}"
59
+ previous_identity=""
60
+ notified_at=0
61
+ if [ -f "$HANDOFF_STATE_FILE" ]; then
62
+ previous_identity="$(awk -F= '$1=="identity"{print substr($0,index($0,"=")+1)}' "$HANDOFF_STATE_FILE")"
63
+ notified_at="$(awk -F= '$1=="notified_at"{print $2}' "$HANDOFF_STATE_FILE")"
64
+ fi
65
+ case "$notified_at" in ''|*[!0-9]*) notified_at=0 ;; esac
66
+ now="$(date +%s)"
67
+
68
+ if [ "$identity" != "$previous_identity" ] || [ $((now - notified_at)) -ge "$HANDOFF_NOTIFY_AFTER_SECONDS" ]; then
69
+ platform_notify "wiki" "review-required handoff ${op:-none} still blocks sync"
70
+ printf 'identity=%s\nnotified_at=%s\n' "$identity" "$now" > "$HANDOFF_STATE_FILE"
71
+ log "NOTIFY handoff identity=$identity"
72
+ else
73
+ log "SKIP PULL handoff identity=$identity reminder-backoff"
74
+ fi
75
+ return 0
76
+ }
77
+
78
+ attempt_pull_on_delta() {
79
+ # Retry on every poll while we remain behind. This avoids the failed-pull
80
+ # wedge where last-behind is advanced before the pull runs and DELTA stays 0
81
+ # forever until a fresh upstream commit arrives.
82
+ if [ "$PULL_ON_DELTA" != "1" ] || [ "$BEHIND" -le 0 ]; then
83
+ return 0
84
+ fi
85
+
86
+ if handle_existing_handoff; then
87
+ return 0
88
+ fi
89
+
90
+ if [ -f "$WIKI_DIR/.skillwiki/sync.lock" ]; then
91
+ log "SKIP PULL sync lock present — leave pull to skillwiki sync"
92
+ return 0
93
+ fi
94
+ if [ -f "$WIKI_DIR/.git/index.lock" ] || [ -d "$WIKI_DIR/.git/rebase-merge" ] || [ -d "$WIKI_DIR/.git/rebase-apply" ] || [ -f "$WIKI_DIR/.git/MERGE_HEAD" ]; then
95
+ log "SKIP PULL git operation already in progress"
96
+ return 0
97
+ fi
98
+
99
+ PULL_LOCK_FILE="$STATE_DIR/pull.lock"
100
+ PULL_LOCK_RC=0
101
+ lockfile_acquire "$PULL_LOCK_FILE" 600 || PULL_LOCK_RC=$?
102
+ if [ "$PULL_LOCK_RC" -eq 1 ]; then
103
+ log "SKIP PULL previous pull-on-delta still in flight"
104
+ return 0
105
+ elif [ "$PULL_LOCK_RC" -eq 2 ]; then
106
+ log "PULL stale lock reclaimed"
107
+ fi
108
+
109
+ if [ -x "$PULL_HELPER" ]; then
110
+ if "$PULL_HELPER" origin "$BRANCH" 2>>"$LOG_FILE"; then
111
+ log "PULL ok via wiki-pull-with-auto-resolve"
112
+ else
113
+ log "FAIL PULL via wiki-pull-with-auto-resolve — run skillwiki sync manually"
114
+ fi
115
+ elif [ -f "$PULL_HELPER" ]; then
116
+ if bash "$PULL_HELPER" origin "$BRANCH" 2>>"$LOG_FILE"; then
117
+ log "PULL ok via bash wiki-pull-with-auto-resolve"
118
+ else
119
+ log "FAIL PULL via bash wiki-pull-with-auto-resolve — run skillwiki sync manually"
120
+ fi
121
+ elif git pull --rebase origin "$BRANCH" 2>>"$LOG_FILE"; then
122
+ log "PULL ok via git pull --rebase"
123
+ else
124
+ log "FAIL PULL via git pull --rebase — run skillwiki sync manually"
125
+ fi
126
+ }
127
+
128
+ # Guard: working tree present.
129
+ if [ ! -d "$WIKI_DIR/.git" ]; then
130
+ log "ERROR: $WIKI_DIR is not a git repo"
131
+ exit 0
132
+ fi
133
+
134
+ cd "$WIKI_DIR" || { log "ERROR: cd $WIKI_DIR failed"; exit 0; }
135
+
136
+ # Fetch only the target branch. --quiet keeps log noise low.
137
+ # Failure (offline, auth, etc.) is silent on the notification side.
138
+ if ! git fetch --quiet origin "$BRANCH" 2>>"$LOG_FILE"; then
139
+ log "fetch failed (offline or auth) — skipping notify"
140
+ exit 0
141
+ fi
142
+
143
+ # Compute behind count. If branch is missing on either side, exit clean.
144
+ if ! BEHIND=$(git rev-list --count "HEAD..origin/$BRANCH" 2>>"$LOG_FILE"); then
145
+ log "rev-list failed — origin/$BRANCH may not exist"
146
+ exit 0
147
+ fi
148
+
149
+ LAST_BEHIND=0
150
+ if [ -f "$STATE_FILE" ]; then
151
+ LAST_BEHIND=$(cat "$STATE_FILE" 2>/dev/null || echo 0)
152
+ # Sanitize: must be a non-negative integer.
153
+ case "$LAST_BEHIND" in
154
+ ''|*[!0-9]*) LAST_BEHIND=0 ;;
155
+ esac
156
+ fi
157
+
158
+ printf '%s' "$BEHIND" >"$STATE_FILE"
159
+
160
+ DELTA=$(( BEHIND - LAST_BEHIND ))
161
+ NOW=$(date +%s)
162
+
163
+ LAST_STALE_NOTIFY=0
164
+ if [ -f "$STALE_STATE_FILE" ]; then
165
+ LAST_STALE_NOTIFY=$(cat "$STALE_STATE_FILE" 2>/dev/null || echo 0)
166
+ case "$LAST_STALE_NOTIFY" in
167
+ ''|*[!0-9]*) LAST_STALE_NOTIFY=0 ;;
168
+ esac
169
+ fi
170
+
171
+ case "$STALE_NOTIFY_AFTER_SECONDS" in
172
+ ''|*[!0-9]*) STALE_NOTIFY_AFTER_SECONDS=1800 ;;
173
+ esac
174
+
175
+ if [ "$DELTA" -gt 0 ]; then
176
+ # New commits arrived since last poll. Notify with delta and total.
177
+ TITLE="wiki"
178
+ if [ "$DELTA" -eq "$BEHIND" ]; then
179
+ MSG="$DELTA new commit(s) on origin/$BRANCH"
180
+ else
181
+ MSG="$DELTA new ($BEHIND total behind) on origin/$BRANCH"
182
+ fi
183
+ platform_notify "$TITLE" "$MSG — run skillwiki sync"
184
+ printf '%s' "$NOW" >"$STALE_STATE_FILE"
185
+ log "NOTIFY behind=$BEHIND delta=$DELTA"
186
+
187
+ elif [ "$BEHIND" -gt 0 ] && [ $(( NOW - LAST_STALE_NOTIFY )) -ge "$STALE_NOTIFY_AFTER_SECONDS" ]; then
188
+ TITLE="wiki"
189
+ MSG="still $BEHIND commit(s) behind origin/$BRANCH"
190
+ platform_notify "$TITLE" "$MSG — sync may be stuck"
191
+ printf '%s' "$NOW" >"$STALE_STATE_FILE"
192
+ log "NOTIFY stale behind=$BEHIND delta=$DELTA"
193
+ else
194
+ if [ "$BEHIND" -eq 0 ]; then
195
+ rm -f "$STALE_STATE_FILE" 2>/dev/null || true
196
+ fi
197
+ log "OK behind=$BEHIND delta=$DELTA (no notify)"
198
+ fi
199
+
200
+ # Opt-in auto-pull: consume sg01 Snapshot commits into the working tree.
201
+ # Uses wiki-pull-with-auto-resolve.sh for conflict-storm handling. On failure,
202
+ # leaves the working tree behind and logs — does NOT block. When the helper is
203
+ # present but not executable, invoke it via bash so we still get its dirty-tree
204
+ # and conflict-storm handling instead of falling back to a bare git pull.
205
+ attempt_pull_on_delta
206
+
207
+ exit 0
@@ -0,0 +1,405 @@
1
+ #!/bin/bash
2
+ # wiki-fuse-refresh.sh — keep rclone FUSE visibility within a bounded freshness SLA.
3
+ #
4
+ # Linux-focused helper that:
5
+ # 1) audits active rclone mount --dir-cache-time
6
+ # 2) optionally forgets stale VFS directory cache entries
7
+ # 3) optionally triggers `rclone rc vfs/refresh recursive=true dir=/`
8
+ #
9
+ # Intended to run under systemd timer (wiki-fuse-refresh.timer) and manually via
10
+ # the vault-fuse-freshness skill.
11
+
12
+ set -u
13
+
14
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]:-$0}" )" && pwd )"
15
+ . "$SCRIPT_DIR/lib/platform.sh"
16
+ platform_detect_os
17
+
18
+ LOG_FILE="$(platform_log_dir)/wiki-fuse-refresh.log"
19
+ mkdir -p "$(dirname "$LOG_FILE")"
20
+
21
+ log() {
22
+ printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >>"$LOG_FILE"
23
+ }
24
+
25
+ print() {
26
+ printf '[wiki-fuse-refresh] %s\n' "$*"
27
+ log "$*"
28
+ }
29
+
30
+ lower() {
31
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]'
32
+ }
33
+
34
+ is_true() {
35
+ case "$(lower "${1:-}")" in
36
+ 1|true|yes|on) return 0 ;;
37
+ *) return 1 ;;
38
+ esac
39
+ }
40
+
41
+ # Parse a duration string into seconds (supports: 10, 10m, 1h30m, 500ms).
42
+ duration_to_seconds() {
43
+ local input
44
+ input="$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
45
+ [ -n "$input" ] || return 1
46
+
47
+ if printf '%s' "$input" | grep -Eq '^[0-9]+([.][0-9]+)?$'; then
48
+ printf '%s\n' "$input"
49
+ return 0
50
+ fi
51
+
52
+ local rest="$input"
53
+ local total="0"
54
+
55
+ while [ -n "$rest" ]; do
56
+ if printf '%s' "$rest" | grep -Eq '^([0-9]+([.][0-9]+)?)(ms|s|m|h|d|w)'; then
57
+ local prefix
58
+ prefix="$(printf '%s' "$rest" | sed -E 's/^([0-9]+([.][0-9]+)?)(ms|s|m|h|d|w).*/\1 \3/')"
59
+ local value
60
+ value="$(printf '%s' "$prefix" | awk '{print $1}')"
61
+ local unit
62
+ unit="$(printf '%s' "$prefix" | awk '{print $2}')"
63
+
64
+ local factor
65
+ case "$unit" in
66
+ ms) factor="0.001" ;;
67
+ s) factor="1" ;;
68
+ m) factor="60" ;;
69
+ h) factor="3600" ;;
70
+ d) factor="86400" ;;
71
+ w) factor="604800" ;;
72
+ *) return 1 ;;
73
+ esac
74
+
75
+ total="$(awk -v a="$total" -v b="$value" -v f="$factor" 'BEGIN { printf "%.6f", a + (b * f) }')"
76
+
77
+ local seg_len
78
+ seg_len="$(printf '%s' "$rest" | sed -E 's/^([0-9]+([.][0-9]+)?)(ms|s|m|h|d|w).*/\1\3/' | awk '{print length($0)}')"
79
+ rest="${rest:$seg_len}"
80
+ else
81
+ return 1
82
+ fi
83
+ done
84
+
85
+ awk -v v="$total" 'BEGIN {
86
+ if (v + 0 == int(v + 0)) {
87
+ printf "%d\n", v
88
+ } else {
89
+ s = sprintf("%.6f", v)
90
+ sub(/0+$/, "", s)
91
+ sub(/[.]$/, "", s)
92
+ printf "%s\n", s
93
+ }
94
+ }'
95
+ }
96
+
97
+ seconds_gt() {
98
+ awk -v a="$1" -v b="$2" 'BEGIN { exit !(a > b) }'
99
+ }
100
+
101
+ find_rclone_mount_pid() {
102
+ local pid
103
+ pid="$(pgrep -f 'rclone.*mount' 2>/dev/null | head -n 1 || true)"
104
+ [ -n "$pid" ] && printf '%s\n' "$pid"
105
+ }
106
+
107
+ rclone_token_stream() {
108
+ local pid="$1"
109
+ if [ -r "/proc/$pid/cmdline" ]; then
110
+ tr '\0' '\n' < "/proc/$pid/cmdline"
111
+ return 0
112
+ fi
113
+ ps -p "$pid" -o args= 2>/dev/null | tr ' ' '\n'
114
+ }
115
+
116
+ extract_flag_value() {
117
+ local pid="$1"
118
+ local flag="$2"
119
+ local prev=""
120
+
121
+ while IFS= read -r token; do
122
+ [ -n "$token" ] || continue
123
+
124
+ if [ "$prev" = "$flag" ]; then
125
+ printf '%s\n' "$token"
126
+ return 0
127
+ fi
128
+
129
+ case "$token" in
130
+ "$flag="*)
131
+ printf '%s\n' "${token#*=}"
132
+ return 0
133
+ ;;
134
+ esac
135
+
136
+ prev="$token"
137
+ done < <(rclone_token_stream "$pid")
138
+
139
+ return 1
140
+ }
141
+
142
+ has_flag() {
143
+ local pid="$1"
144
+ local flag="$2"
145
+
146
+ while IFS= read -r token; do
147
+ [ -n "$token" ] || continue
148
+ if [ "$token" = "$flag" ]; then
149
+ return 0
150
+ fi
151
+ case "$token" in
152
+ "$flag="*) return 0 ;;
153
+ esac
154
+ done < <(rclone_token_stream "$pid")
155
+
156
+ return 1
157
+ }
158
+
159
+ extract_mount_remote() {
160
+ local pid="$1"
161
+ local saw_mount=0
162
+
163
+ while IFS= read -r token; do
164
+ [ -n "$token" ] || continue
165
+
166
+ if [ "$saw_mount" -eq 0 ]; then
167
+ [ "$token" = "mount" ] && saw_mount=1
168
+ continue
169
+ fi
170
+
171
+ case "$token" in
172
+ --*) continue ;;
173
+ /*) continue ;;
174
+ esac
175
+
176
+ case "$token" in
177
+ *:*)
178
+ printf '%s\n' "$token"
179
+ return 0
180
+ ;;
181
+ esac
182
+ done < <(rclone_token_stream "$pid")
183
+
184
+ return 1
185
+ }
186
+
187
+ DRY_RUN=0
188
+ CHECK_ONLY=0
189
+ MAX_DIR_CACHE_RAW="${VS_FUSE_MAX_DIR_CACHE:-15m}"
190
+ RC_ADDR_OVERRIDE="${VS_FUSE_RC_ADDR:-}"
191
+ RC_TIMEOUT_SECONDS="${VS_FUSE_RC_TIMEOUT_SECONDS:-60}"
192
+ FORGET_DIRS=()
193
+
194
+ if [ -n "${VS_FUSE_FORGET_DIRS:-}" ]; then
195
+ read -r -a FORGET_DIRS <<< "$VS_FUSE_FORGET_DIRS"
196
+ fi
197
+
198
+ usage() {
199
+ cat <<USAGE
200
+ Usage: bash wiki-fuse-refresh.sh [options]
201
+
202
+ Options:
203
+ --dry-run Print planned actions only
204
+ --check-only Audit dir-cache-time only (no rc refresh)
205
+ --max-dir-cache <dur> Freshness threshold (default: 15m)
206
+ --rc-addr <host:port> Override rc endpoint (default: from mount flags, else 127.0.0.1:5572)
207
+ --forget-dir <path> Forget a VFS directory cache path before refresh (repeatable)
208
+ --help Show this help
209
+
210
+ Environment overrides:
211
+ VS_DRY_RUN=1|0
212
+ VS_FUSE_CHECK_ONLY=1|0
213
+ VS_FUSE_MAX_DIR_CACHE=<duration>
214
+ VS_FUSE_RC_ADDR=<host:port>
215
+ VS_FUSE_RC_TIMEOUT_SECONDS=<seconds>
216
+ VS_FUSE_FORGET_DIRS="<path> [path...]"
217
+ USAGE
218
+ }
219
+
220
+ if is_true "${VS_DRY_RUN:-0}"; then
221
+ DRY_RUN=1
222
+ fi
223
+ if is_true "${VS_FUSE_CHECK_ONLY:-0}"; then
224
+ CHECK_ONLY=1
225
+ fi
226
+
227
+ while [ "$#" -gt 0 ]; do
228
+ case "$1" in
229
+ --dry-run)
230
+ DRY_RUN=1
231
+ shift
232
+ ;;
233
+ --check-only)
234
+ CHECK_ONLY=1
235
+ shift
236
+ ;;
237
+ --max-dir-cache)
238
+ [ "$#" -ge 2 ] || { print "FATAL: --max-dir-cache requires a value"; exit 2; }
239
+ MAX_DIR_CACHE_RAW="$2"
240
+ shift 2
241
+ ;;
242
+ --rc-addr)
243
+ [ "$#" -ge 2 ] || { print "FATAL: --rc-addr requires a value"; exit 2; }
244
+ RC_ADDR_OVERRIDE="$2"
245
+ shift 2
246
+ ;;
247
+ --forget-dir)
248
+ [ "$#" -ge 2 ] || { print "FATAL: --forget-dir requires a value"; exit 2; }
249
+ FORGET_DIRS+=("$2")
250
+ shift 2
251
+ ;;
252
+ --help|-h)
253
+ usage
254
+ exit 0
255
+ ;;
256
+ *)
257
+ print "FATAL: unknown argument: $1"
258
+ exit 2
259
+ ;;
260
+ esac
261
+ done
262
+
263
+ if [ "$VS_OS" != "linux" ]; then
264
+ print "skip: OS=$VS_OS (linux-only freshness workflow)"
265
+ exit 0
266
+ fi
267
+
268
+ if ! command -v rclone >/dev/null 2>&1; then
269
+ print "WARN: rclone not found in PATH"
270
+ exit 0
271
+ fi
272
+
273
+ case "$RC_TIMEOUT_SECONDS" in
274
+ ''|*[!0-9]*)
275
+ print "FATAL: VS_FUSE_RC_TIMEOUT_SECONDS must be an integer number of seconds"
276
+ exit 2
277
+ ;;
278
+ esac
279
+
280
+ MAX_DIR_CACHE_SECONDS="$(duration_to_seconds "$MAX_DIR_CACHE_RAW" 2>/dev/null || true)"
281
+ if [ -z "$MAX_DIR_CACHE_SECONDS" ]; then
282
+ print "FATAL: cannot parse --max-dir-cache=$MAX_DIR_CACHE_RAW"
283
+ exit 2
284
+ fi
285
+
286
+ RCLONE_PID="$(find_rclone_mount_pid || true)"
287
+ if [ -z "$RCLONE_PID" ]; then
288
+ print "WARN: no running rclone mount process found"
289
+ exit 0
290
+ fi
291
+
292
+ DIR_CACHE_RAW="$(extract_flag_value "$RCLONE_PID" --dir-cache-time 2>/dev/null || true)"
293
+ if [ -z "$DIR_CACHE_RAW" ]; then
294
+ DIR_CACHE_RAW="5m"
295
+ fi
296
+ DIR_CACHE_SECONDS="$(duration_to_seconds "$DIR_CACHE_RAW" 2>/dev/null || true)"
297
+ if [ -z "$DIR_CACHE_SECONDS" ]; then
298
+ print "WARN: cannot parse --dir-cache-time=$DIR_CACHE_RAW (pid=$RCLONE_PID)"
299
+ exit 1
300
+ fi
301
+
302
+ if seconds_gt "$DIR_CACHE_SECONDS" "$MAX_DIR_CACHE_SECONDS"; then
303
+ print "WARN: --dir-cache-time=$DIR_CACHE_RAW exceeds freshness threshold $MAX_DIR_CACHE_RAW (pid=$RCLONE_PID)"
304
+ # Keep going so refresh still runs when possible; return non-zero at end.
305
+ DIR_CACHE_STATUS=1
306
+ else
307
+ print "OK: --dir-cache-time=$DIR_CACHE_RAW within freshness threshold $MAX_DIR_CACHE_RAW (pid=$RCLONE_PID)"
308
+ DIR_CACHE_STATUS=0
309
+ fi
310
+
311
+ if [ "$CHECK_ONLY" -eq 1 ]; then
312
+ if [ "$DIR_CACHE_STATUS" -eq 0 ]; then
313
+ print "check-only: no refresh requested"
314
+ exit 0
315
+ fi
316
+ print "check-only: returning non-zero due to stale dir-cache-time"
317
+ exit 1
318
+ fi
319
+
320
+ if ! has_flag "$RCLONE_PID" --rc; then
321
+ print "WARN: rclone mount pid=$RCLONE_PID has no --rc flag; cannot run vfs/refresh"
322
+ [ "$DIR_CACHE_STATUS" -eq 0 ] && exit 0 || exit 1
323
+ fi
324
+
325
+ RC_ADDR="$RC_ADDR_OVERRIDE"
326
+ if [ -z "$RC_ADDR" ]; then
327
+ RC_ADDR="$(extract_flag_value "$RCLONE_PID" --rc-addr 2>/dev/null || true)"
328
+ fi
329
+ if [ -z "$RC_ADDR" ]; then
330
+ RC_ADDR="127.0.0.1:5572"
331
+ fi
332
+
333
+ RC_URL="$RC_ADDR"
334
+ case "$RC_URL" in
335
+ http://*|https://*) ;;
336
+ *) RC_URL="http://$RC_URL" ;;
337
+ esac
338
+
339
+ REMOTE="$(extract_mount_remote "$RCLONE_PID" 2>/dev/null || true)"
340
+
341
+ if ! command -v timeout >/dev/null 2>&1; then
342
+ print "WARN: timeout not found in PATH; cannot run bounded rclone rc operations"
343
+ [ "$DIR_CACHE_STATUS" -eq 0 ] && exit 1 || exit 1
344
+ fi
345
+
346
+ run_rclone_rc() {
347
+ timeout "$RC_TIMEOUT_SECONDS" rclone rc --url "$RC_URL" "$@" 2>&1
348
+ }
349
+
350
+ if [ "$DRY_RUN" -eq 1 ]; then
351
+ for forget_dir in "${FORGET_DIRS[@]}"; do
352
+ if [ -n "$REMOTE" ]; then
353
+ print "[dry-run] timeout $RC_TIMEOUT_SECONDS rclone rc --url $RC_URL vfs/forget dir=$forget_dir fs=$REMOTE"
354
+ else
355
+ print "[dry-run] timeout $RC_TIMEOUT_SECONDS rclone rc --url $RC_URL vfs/forget dir=$forget_dir"
356
+ fi
357
+ done
358
+ if [ -n "$REMOTE" ]; then
359
+ print "[dry-run] timeout $RC_TIMEOUT_SECONDS rclone rc --url $RC_URL vfs/refresh recursive=true dir=/ fs=$REMOTE"
360
+ else
361
+ print "[dry-run] timeout $RC_TIMEOUT_SECONDS rclone rc --url $RC_URL vfs/refresh recursive=true dir=/"
362
+ fi
363
+ [ "$DIR_CACHE_STATUS" -eq 0 ] && exit 0 || exit 1
364
+ fi
365
+
366
+ for forget_dir in "${FORGET_DIRS[@]}"; do
367
+ if [ -n "$REMOTE" ]; then
368
+ RC_OUT="$(run_rclone_rc vfs/forget "dir=$forget_dir" "fs=$REMOTE")"
369
+ else
370
+ RC_OUT="$(run_rclone_rc vfs/forget "dir=$forget_dir")"
371
+ fi
372
+ RC_CODE=$?
373
+
374
+ if [ "$RC_CODE" -ne 0 ]; then
375
+ print "FAIL: vfs/forget dir=$forget_dir rc=$RC_CODE url=$RC_URL"
376
+ log "$RC_OUT"
377
+ [ "$DIR_CACHE_STATUS" -eq 0 ] && exit "$RC_CODE" || exit 1
378
+ fi
379
+
380
+ print "OK: vfs/forget dir=$forget_dir completed via $RC_URL"
381
+ done
382
+
383
+ if [ -n "$REMOTE" ]; then
384
+ RC_OUT="$(run_rclone_rc vfs/refresh recursive=true dir=/ "fs=$REMOTE")"
385
+ else
386
+ RC_OUT="$(run_rclone_rc vfs/refresh recursive=true dir=/)"
387
+ fi
388
+ RC_CODE=$?
389
+
390
+ if [ "$RC_CODE" -ne 0 ]; then
391
+ if printf '%s\n' "$RC_OUT" | grep -qi 'file does not exist'; then
392
+ print "WARN: root vfs/refresh with dir=/ failed; retrying without dir=/"
393
+ RC_OUT="$(run_rclone_rc vfs/refresh recursive=true)"
394
+ RC_CODE=$?
395
+ fi
396
+ fi
397
+
398
+ if [ "$RC_CODE" -ne 0 ]; then
399
+ print "FAIL: vfs/refresh rc=$RC_CODE url=$RC_URL"
400
+ log "$RC_OUT"
401
+ [ "$DIR_CACHE_STATUS" -eq 0 ] && exit "$RC_CODE" || exit 1
402
+ fi
403
+
404
+ print "OK: vfs/refresh completed via $RC_URL"
405
+ [ "$DIR_CACHE_STATUS" -eq 0 ] && exit 0 || exit 1