skillwiki 0.9.63 → 0.10.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.
Files changed (36) hide show
  1. package/dist/chunk-C5OLZRRM.js +357 -0
  2. package/dist/chunk-IZABIE44.js +647 -0
  3. package/dist/chunk-R6BKJWVC.js +890 -0
  4. package/dist/chunk-SCGC7YNM.js +213 -0
  5. package/dist/{chunk-TUFQZ5K4.js → chunk-XSTXPA34.js} +834 -1981
  6. package/dist/cli.js +1780 -697
  7. package/dist/index-projection-ERFX76U5.js +10 -0
  8. package/dist/managed-write-preflight-PW4OOOMV.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 +24 -0
  34. package/skills/skills/wiki-crystallize/SKILL.md +3 -0
  35. package/skills/using-skillwiki/SKILL.md +24 -0
  36. package/skills/wiki-crystallize/SKILL.md +3 -0
@@ -0,0 +1,70 @@
1
+ #!/bin/sh
2
+ # lockfile.sh — Advisory locking for vault-sync scripts.
3
+ #
4
+ # Uses flock when available (Linux primary); falls back to mkdir mutex (macOS).
5
+ # Reclaims locks older than max_age seconds (default 600 = 10 min).
6
+ #
7
+ # Usage:
8
+ # lockfile_acquire <path> [max_age_seconds] → returns 0 on success, 1 on contention, 2 on stale-reclaim
9
+ # lockfile_release <path>
10
+
11
+ # Acquire advisory lock.
12
+ # Returns: 0 = acquired, 1 = contended, 2 = stale-reclaim
13
+ lockfile_acquire() {
14
+ _lock_path="$1"
15
+ _max_age="${2:-600}"
16
+
17
+ # Try flock first (Linux primary, macOS opportunistic)
18
+ # shellcheck disable=SC2034
19
+ if command -v flock >/dev/null 2>&1; then
20
+ # Use fd 9 for the lock
21
+ eval "exec 9>\"$_lock_path\""
22
+ if flock -n 9 2>/dev/null; then
23
+ return 0
24
+ fi
25
+ # flock failed — fall through to mkdir mutex
26
+ fi
27
+
28
+ # mkdir mutex fallback
29
+ _lock_dir="${_lock_path}.d"
30
+ if mkdir "$_lock_dir" 2>/dev/null; then
31
+ _VS_LOCK_DIR="$_lock_dir"
32
+ export _VS_LOCK_DIR
33
+ trap 'lockfile_release "$_lock_path"' EXIT
34
+ return 0
35
+ fi
36
+
37
+ # Directory exists — check if stale
38
+ if [ -d "$_lock_dir" ]; then
39
+ _now=$(date +%s)
40
+ _ctime=$(platform_stat_ctime "$_lock_dir")
41
+ _age=$(( _now - _ctime ))
42
+ if [ "$_age" -gt "$_max_age" ]; then
43
+ # Stale lock — reclaim
44
+ rmdir "$_lock_dir" 2>/dev/null || true
45
+ if mkdir "$_lock_dir" 2>/dev/null; then
46
+ _VS_LOCK_DIR="$_lock_dir"
47
+ export _VS_LOCK_DIR
48
+ trap 'lockfile_release "$_lock_path"' EXIT
49
+ return 2
50
+ fi
51
+ fi
52
+ fi
53
+
54
+ return 1
55
+ }
56
+
57
+ # Release advisory lock.
58
+ lockfile_release() {
59
+ _lock_path="$1"
60
+ _lock_dir="${_lock_path}.d"
61
+
62
+ # Release mkdir mutex if we hold it
63
+ if [ -n "${_VS_LOCK_DIR:-}" ] && [ "$_VS_LOCK_DIR" = "$_lock_dir" ]; then
64
+ rmdir "$_lock_dir" 2>/dev/null || true
65
+ unset _VS_LOCK_DIR
66
+ fi
67
+
68
+ # flock is released when fd 9 closes (process exit)
69
+ return 0
70
+ }
@@ -0,0 +1,80 @@
1
+ #!/bin/bash
2
+ # managed-write-lock.sh — shared CLI/shell managed-write lock (Bash 3.2).
3
+ # Source from vault-sync scripts. Lock path: git --git-path vault-sync/managed-write.lock
4
+
5
+ VAULT_SYNC_MANAGED_LOCK_PATH=""
6
+ VAULT_SYNC_MANAGED_LOCK_TOKEN_OWNED=""
7
+ VAULT_SYNC_MANAGED_LOCK_ACQUIRED=""
8
+ VAULT_SYNC_MANAGED_LOCK_OWNS_RELEASE=0
9
+
10
+ vault_sync_managed_lock_path() {
11
+ local repo="${1:-.}"
12
+ local path
13
+ path="$(git -C "$repo" rev-parse --git-path vault-sync/managed-write.lock 2>/dev/null)" || return 1
14
+ case "$path" in
15
+ /*) printf '%s\n' "$path" ;;
16
+ *) printf '%s\n' "$repo/$path" ;;
17
+ esac
18
+ }
19
+
20
+ vault_sync_managed_lock_read_token() {
21
+ local path="$1"
22
+ [ -f "$path" ] || return 1
23
+ sed -n 's/.*"owner_token"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$path" | head -1
24
+ }
25
+
26
+ # Acquire or adopt managed-write lock. Returns 0 on success, 1 on contention/mismatch.
27
+ vault_sync_managed_lock_acquire() {
28
+ local repo="${1:-.}"
29
+ local command="${2:-wiki-pull}"
30
+ local path token now inherited
31
+ path="$(vault_sync_managed_lock_path "$repo")" || return 1
32
+ VAULT_SYNC_MANAGED_LOCK_PATH="$path"
33
+ mkdir -p "$(dirname "$path")" || return 1
34
+
35
+ inherited="${VAULT_SYNC_MANAGED_LOCK_TOKEN:-}"
36
+ if [ -n "$inherited" ]; then
37
+ if [ -f "$path" ]; then
38
+ token="$(vault_sync_managed_lock_read_token "$path" || true)"
39
+ if [ "$token" = "$inherited" ]; then
40
+ VAULT_SYNC_MANAGED_LOCK_TOKEN_OWNED="$inherited"
41
+ VAULT_SYNC_MANAGED_LOCK_OWNS_RELEASE=0
42
+ return 0
43
+ fi
44
+ return 1
45
+ fi
46
+ return 1
47
+ fi
48
+
49
+ token="$(od -An -N16 -tx1 /dev/urandom 2>/dev/null | tr -d ' \n')"
50
+ [ -n "$token" ] || token="$$-$(date +%s)"
51
+ now="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
52
+ if ( set -o noclobber; printf '{"pid":%s,"owner_token":"%s","acquired":"%s","command":"%s"}\n' \
53
+ "$$" "$token" "$now" "$command" >"$path" ) 2>/dev/null; then
54
+ VAULT_SYNC_MANAGED_LOCK_TOKEN_OWNED="$token"
55
+ VAULT_SYNC_MANAGED_LOCK_ACQUIRED="$now"
56
+ VAULT_SYNC_MANAGED_LOCK_OWNS_RELEASE=1
57
+ return 0
58
+ fi
59
+ return 1
60
+ }
61
+
62
+ vault_sync_managed_lock_release() {
63
+ local path token
64
+ path="${VAULT_SYNC_MANAGED_LOCK_PATH:-}"
65
+ [ -n "$path" ] || return 0
66
+ if [ "${VAULT_SYNC_MANAGED_LOCK_OWNS_RELEASE:-0}" != "1" ]; then
67
+ return 0
68
+ fi
69
+ if [ ! -f "$path" ]; then
70
+ VAULT_SYNC_MANAGED_LOCK_OWNS_RELEASE=0
71
+ return 0
72
+ fi
73
+ token="$(vault_sync_managed_lock_read_token "$path" || true)"
74
+ if [ "$token" != "${VAULT_SYNC_MANAGED_LOCK_TOKEN_OWNED:-}" ]; then
75
+ return 1
76
+ fi
77
+ rm -f -- "$path"
78
+ VAULT_SYNC_MANAGED_LOCK_OWNS_RELEASE=0
79
+ return 0
80
+ }
@@ -0,0 +1,184 @@
1
+ #!/bin/sh
2
+ # platform.sh — Cross-platform abstraction for vault-sync scripts.
3
+ #
4
+ # Sourced by all vault-sync scripts. Provides OS detection, normalized
5
+ # paths, stat wrappers, notification shim, scheduler abstraction, and
6
+ # feature prerequisites.
7
+ #
8
+ # Works in bash and /bin/sh (dash on Debian). No external deps beyond
9
+ # what vault-sync itself requires (rclone, git).
10
+
11
+ # Detect OS. Sets VS_OS to "macos" | "linux" | "unsupported".
12
+ platform_detect_os() {
13
+ case "$(uname -s)" in
14
+ Darwin) VS_OS=macos ;;
15
+ Linux) VS_OS=linux ;;
16
+ *) VS_OS=unsupported ;;
17
+ esac
18
+ export VS_OS
19
+ }
20
+
21
+ # Normalized paths (XDG on Linux, ~/Library on macOS):
22
+
23
+ platform_log_dir() {
24
+ case "${VS_OS:-}" in
25
+ macos) echo "$HOME/Library/Logs" ;;
26
+ linux) echo "$HOME/.local/state/vault-sync/log" ;;
27
+ *) echo "$HOME/.local/state/vault-sync/log" ;;
28
+ esac
29
+ }
30
+
31
+ platform_cache_dir() {
32
+ case "${VS_OS:-}" in
33
+ macos) echo "$HOME/Library/Caches/vault-sync" ;;
34
+ linux) echo "$HOME/.cache/vault-sync" ;;
35
+ *) echo "$HOME/.cache/vault-sync" ;;
36
+ esac
37
+ }
38
+
39
+ platform_share_dir() {
40
+ case "${VS_OS:-}" in
41
+ macos) echo "$HOME/Library/Application Support/vault-sync" ;;
42
+ linux) echo "$HOME/.local/share/vault-sync" ;;
43
+ *) echo "$HOME/.local/share/vault-sync" ;;
44
+ esac
45
+ }
46
+
47
+ platform_rclone_config_dir() {
48
+ echo "$HOME/.config/rclone"
49
+ }
50
+
51
+ # Stat wrappers (BSD -f vs GNU -c):
52
+
53
+ platform_stat_size() {
54
+ # echo bytes
55
+ case "${VS_OS:-}" in
56
+ macos) stat -f%z "$1" 2>/dev/null || echo 0 ;;
57
+ linux) stat -c%s "$1" 2>/dev/null || echo 0 ;;
58
+ *) stat -c%s "$1" 2>/dev/null || stat -f%z "$1" 2>/dev/null || echo 0 ;;
59
+ esac
60
+ }
61
+
62
+ platform_stat_ctime() {
63
+ # echo unix epoch
64
+ case "${VS_OS:-}" in
65
+ macos) stat -f%c "$1" 2>/dev/null || echo 0 ;;
66
+ linux) stat -c%Z "$1" 2>/dev/null || echo 0 ;;
67
+ *) stat -c%Z "$1" 2>/dev/null || stat -f%c "$1" 2>/dev/null || echo 0 ;;
68
+ esac
69
+ }
70
+
71
+ # Notification (graceful degrade):
72
+ # macos: osascript display notification
73
+ # linux: notify-send if available, else log only
74
+ # headless: no-op (return 0)
75
+ platform_notify() {
76
+ _title="$1"
77
+ _msg="$2"
78
+ case "${VS_OS:-}" in
79
+ macos)
80
+ osascript -e "display notification \"$_msg\" with title \"$_title\"" 2>/dev/null || true
81
+ ;;
82
+ linux)
83
+ if command -v notify-send >/dev/null 2>&1; then
84
+ notify-send "$_title" "$_msg" 2>/dev/null || true
85
+ fi
86
+ # Headless Linux: no-op, return 0
87
+ ;;
88
+ *) ;;
89
+ esac
90
+ }
91
+
92
+ # Scheduler abstraction:
93
+
94
+ platform_scheduler() {
95
+ # echo: launchd | systemd | none
96
+ case "${VS_OS:-}" in
97
+ macos)
98
+ if command -v launchctl >/dev/null 2>&1; then
99
+ echo "launchd"
100
+ else
101
+ echo "none"
102
+ fi
103
+ ;;
104
+ linux)
105
+ if command -v systemctl >/dev/null 2>&1 && systemctl --user >/dev/null 2>&1; then
106
+ echo "systemd"
107
+ else
108
+ echo "none"
109
+ fi
110
+ ;;
111
+ *) echo "none" ;;
112
+ esac
113
+ }
114
+
115
+ platform_job_status() {
116
+ # Returns JSON: {"enabled": bool, "running": bool, "last_exit": int}
117
+ _name="$1"
118
+ _enabled=false
119
+ _running=false
120
+ _last_exit=-1
121
+
122
+ case "${VS_OS:-}" in
123
+ macos)
124
+ # launchd: check via launchctl print
125
+ if launchctl print "gui/$(id -u)/${_name}" >/dev/null 2>&1; then
126
+ _enabled=true
127
+ _running=true # launchd prints exit status if job ran
128
+ _last_exit=0 # simplified; full parsing is complex
129
+ fi
130
+ ;;
131
+ linux)
132
+ # systemd --user
133
+ if _is_enabled="$(systemctl --user is-enabled "${_name}.timer" 2>/dev/null)"; then
134
+ if [ "$_is_enabled" = "enabled" ]; then
135
+ _enabled=true
136
+ fi
137
+ fi
138
+ if _is_active="$(systemctl --user is-active "${_name}.timer" 2>/dev/null)"; then
139
+ if [ "$_is_active" = "active" ]; then
140
+ _running=true
141
+ fi
142
+ fi
143
+ _last_exit=0 # simplified
144
+ ;;
145
+ esac
146
+
147
+ printf '{"enabled": %s, "running": %s, "last_exit": %d}\n' "$_enabled" "$_running" "$_last_exit"
148
+ }
149
+
150
+ # Feature prerequisite check:
151
+ # exit 1 with message if not available
152
+ platform_require() {
153
+ _feature="$1"
154
+ case "$_feature" in
155
+ rclone)
156
+ if ! command -v rclone >/dev/null 2>&1; then
157
+ echo "FATAL: rclone not found in PATH" >&2
158
+ return 1
159
+ fi
160
+ ;;
161
+ git)
162
+ if ! command -v git >/dev/null 2>&1; then
163
+ echo "FATAL: git not found in PATH" >&2
164
+ return 1
165
+ fi
166
+ ;;
167
+ linux)
168
+ if [ "${VS_OS:-}" != "linux" ]; then
169
+ echo "FATAL: this operation requires Linux" >&2
170
+ return 1
171
+ fi
172
+ ;;
173
+ macos)
174
+ if [ "${VS_OS:-}" != "macos" ]; then
175
+ echo "FATAL: this operation requires macOS" >&2
176
+ return 1
177
+ fi
178
+ ;;
179
+ *)
180
+ echo "FATAL: unknown prerequisite: $_feature" >&2
181
+ return 1
182
+ ;;
183
+ esac
184
+ }
@@ -0,0 +1,223 @@
1
+ #!/bin/sh
2
+ # runtime-manifest.sh — SHA-256 inventory of installed vault-sync artifacts.
3
+ #
4
+ # Sourced by vault-sync-install (and later vault-sync-status). Works in bash
5
+ # and /bin/sh. Prefers python3 for JSON emission; falls back to a minimal
6
+ # hand-built JSON object when python3 is unavailable.
7
+ #
8
+ # Public API:
9
+ # vault_sync_sha256 <file> → hex digest or empty
10
+ # vault_sync_package_version <root> → version string
11
+ # vault_sync_package_commit <root> → git HEAD or empty
12
+ # vault_sync_write_runtime_manifest \
13
+ # <out_path> <share_dir> <launch_agents_dir> \
14
+ # <package_version> <package_commit> <installer_version> \
15
+ # <installed_at> <role> <host_id>
16
+
17
+ # Resolve once per process when possible.
18
+ _vault_sync_sha256_resolved=0
19
+ _vault_sync_sha256_tool=""
20
+
21
+ _vault_sync_sha256_resolve() {
22
+ if [ "$_vault_sync_sha256_resolved" = "1" ]; then
23
+ return 0
24
+ fi
25
+ _vault_sync_sha256_resolved=1
26
+ if command -v shasum >/dev/null 2>&1; then
27
+ _vault_sync_sha256_tool=shasum
28
+ elif command -v sha256sum >/dev/null 2>&1; then
29
+ _vault_sync_sha256_tool=sha256sum
30
+ elif command -v openssl >/dev/null 2>&1; then
31
+ _vault_sync_sha256_tool=openssl
32
+ elif command -v python3 >/dev/null 2>&1; then
33
+ _vault_sync_sha256_tool=python3
34
+ else
35
+ _vault_sync_sha256_tool=""
36
+ fi
37
+ }
38
+
39
+ vault_sync_sha256() {
40
+ _f="$1"
41
+ if [ ! -f "$_f" ]; then
42
+ echo ""
43
+ return 0
44
+ fi
45
+ _vault_sync_sha256_resolve
46
+ case "$_vault_sync_sha256_tool" in
47
+ shasum)
48
+ shasum -a 256 "$_f" 2>/dev/null | awk '{print $1}'
49
+ ;;
50
+ sha256sum)
51
+ sha256sum "$_f" 2>/dev/null | awk '{print $1}'
52
+ ;;
53
+ openssl)
54
+ openssl dgst -sha256 "$_f" 2>/dev/null | awk '{print $NF}'
55
+ ;;
56
+ python3)
57
+ python3 -c 'import hashlib,sys; print(hashlib.sha256(open(sys.argv[1],"rb").read()).hexdigest())' "$_f" 2>/dev/null
58
+ ;;
59
+ *)
60
+ echo ""
61
+ ;;
62
+ esac
63
+ }
64
+
65
+ # Trim leading/trailing whitespace (portable; no bashisms required).
66
+ _vault_sync_trim() {
67
+ printf '%s' "$1" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//'
68
+ }
69
+
70
+ vault_sync_package_version() {
71
+ _root="$1"
72
+ # Deploy provenance override (rsync/staged install). Metadata only.
73
+ _override="$(_vault_sync_trim "${VS_PACKAGE_VERSION:-}")"
74
+ if [ -n "$_override" ]; then
75
+ printf '%s\n' "$_override"
76
+ return 0
77
+ fi
78
+ # Prefer monorepo package.json two levels up from packages/vault-sync, else root.
79
+ if [ -f "$_root/../../package.json" ]; then
80
+ if command -v python3 >/dev/null 2>&1; then
81
+ python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version",""))' "$_root/../../package.json" 2>/dev/null && return 0
82
+ fi
83
+ sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$_root/../../package.json" 2>/dev/null | head -n 1
84
+ return 0
85
+ fi
86
+ if [ -f "$_root/package.json" ]; then
87
+ if command -v python3 >/dev/null 2>&1; then
88
+ python3 -c 'import json,sys; print(json.load(open(sys.argv[1])).get("version",""))' "$_root/package.json" 2>/dev/null && return 0
89
+ fi
90
+ fi
91
+ echo "0.0.0"
92
+ }
93
+
94
+ vault_sync_package_commit() {
95
+ _root="$1"
96
+ # Deploy provenance override (rsync/staged install). Metadata only.
97
+ _override="$(_vault_sync_trim "${VS_PACKAGE_COMMIT:-}")"
98
+ if [ -n "$_override" ]; then
99
+ printf '%s\n' "$_override"
100
+ return 0
101
+ fi
102
+ if command -v git >/dev/null 2>&1 && [ -d "$_root/../../.git" -o -f "$_root/../../.git" ] 2>/dev/null; then
103
+ git -C "$_root/../.." rev-parse HEAD 2>/dev/null || echo ""
104
+ return 0
105
+ fi
106
+ if command -v git >/dev/null 2>&1; then
107
+ git -C "$_root" rev-parse HEAD 2>/dev/null || echo ""
108
+ return 0
109
+ fi
110
+ echo ""
111
+ }
112
+
113
+ # Collect relative_path=sha256 pairs for files under share_dir/bin and
114
+ # LaunchAgents plists. Writes newline-separated "relpath\thash" to stdout.
115
+ vault_sync_collect_file_hashes() {
116
+ _share="$1"
117
+ _agents="$2"
118
+ _bin="$_share/bin"
119
+
120
+ if [ -d "$_bin" ]; then
121
+ # shell scripts at bin root
122
+ for _f in "$_bin"/*; do
123
+ [ -f "$_f" ] || continue
124
+ _base="$(basename "$_f")"
125
+ _h="$(vault_sync_sha256 "$_f")"
126
+ [ -n "$_h" ] && printf 'bin/%s\t%s\n' "$_base" "$_h"
127
+ done
128
+ # lib/*.sh
129
+ if [ -d "$_bin/lib" ]; then
130
+ for _f in "$_bin/lib"/*; do
131
+ [ -f "$_f" ] || continue
132
+ _base="$(basename "$_f")"
133
+ _h="$(vault_sync_sha256 "$_f")"
134
+ [ -n "$_h" ] && printf 'bin/lib/%s\t%s\n' "$_base" "$_h"
135
+ done
136
+ fi
137
+ fi
138
+
139
+ if [ -d "$_agents" ]; then
140
+ for _f in "$_agents"/com.karlchow.wiki-*.plist; do
141
+ [ -f "$_f" ] || continue
142
+ _base="$(basename "$_f")"
143
+ _h="$(vault_sync_sha256 "$_f")"
144
+ [ -n "$_h" ] && printf 'LaunchAgents/%s\t%s\n' "$_base" "$_h"
145
+ done
146
+ fi
147
+ }
148
+
149
+ vault_sync_write_runtime_manifest() {
150
+ _out="$1"
151
+ _share="$2"
152
+ _agents="$3"
153
+ _pkg_ver="$4"
154
+ _pkg_commit="$5"
155
+ _installer_ver="$6"
156
+ _installed_at="$7"
157
+ _role="$8"
158
+ _host_id="$9"
159
+
160
+ _pairs="$(mktemp)"
161
+ vault_sync_collect_file_hashes "$_share" "$_agents" >"$_pairs"
162
+
163
+ mkdir -p "$(dirname "$_out")"
164
+
165
+ if command -v python3 >/dev/null 2>&1; then
166
+ python3 - "$_out" "$_pkg_ver" "$_pkg_commit" "$_installer_ver" "$_installed_at" "$_role" "$_host_id" "$_pairs" <<'PY'
167
+ import json, sys
168
+ out, pkg_ver, pkg_commit, installer_ver, installed_at, role, host_id, pairs_path = sys.argv[1:9]
169
+ files = {}
170
+ with open(pairs_path, "r", encoding="utf-8") as fh:
171
+ for line in fh:
172
+ line = line.rstrip("\n")
173
+ if not line:
174
+ continue
175
+ if "\t" not in line:
176
+ continue
177
+ rel, digest = line.split("\t", 1)
178
+ files[rel] = digest
179
+ manifest = {
180
+ "schema_version": 1,
181
+ "package_commit": pkg_commit,
182
+ "package_version": pkg_ver,
183
+ "installer_version": installer_ver,
184
+ "installed_at": installed_at,
185
+ "role": role,
186
+ "host_id": host_id,
187
+ "files": files,
188
+ }
189
+ with open(out, "w", encoding="utf-8") as fh:
190
+ json.dump(manifest, fh, indent=2, sort_keys=False)
191
+ fh.write("\n")
192
+ PY
193
+ _rc=$?
194
+ rm -f "$_pairs"
195
+ return $_rc
196
+ fi
197
+
198
+ # Minimal fallback without python3
199
+ {
200
+ printf '{\n'
201
+ printf ' "schema_version": 1,\n'
202
+ printf ' "package_commit": "%s",\n' "$_pkg_commit"
203
+ printf ' "package_version": "%s",\n' "$_pkg_ver"
204
+ printf ' "installer_version": "%s",\n' "$_installer_ver"
205
+ printf ' "installed_at": "%s",\n' "$_installed_at"
206
+ printf ' "role": "%s",\n' "$_role"
207
+ printf ' "host_id": "%s",\n' "$_host_id"
208
+ printf ' "files": {\n'
209
+ _first=1
210
+ while IFS="$(printf '\t')" read -r _rel _hash; do
211
+ [ -n "$_rel" ] || continue
212
+ if [ "$_first" -eq 1 ]; then
213
+ _first=0
214
+ else
215
+ printf ',\n'
216
+ fi
217
+ printf ' "%s": "%s"' "$_rel" "$_hash"
218
+ done <"$_pairs"
219
+ printf '\n }\n}\n'
220
+ } >"$_out"
221
+ rm -f "$_pairs"
222
+ return 0
223
+ }