create-egregore 0.19.5 → 0.19.7

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 (130) hide show
  1. package/assets/egregore-launcher.js +411 -25
  2. package/bin/cli.js +34 -20
  3. package/lib/local.js +21 -5
  4. package/lib/pi-runtime.js +186 -0
  5. package/lib/setup.js +56 -11
  6. package/lib/ui.js +1 -1
  7. package/package.json +7 -5
  8. package/runtime/codex/.codex/spec-manifest.json +1 -1
  9. package/runtime/codex/AGENTS.md +7 -2
  10. package/runtime/codex/bin/codex-render-spec.mjs +6 -1
  11. package/runtime/codex/bin/codex-session-start.sh +5 -59
  12. package/runtime/codex/bin/codex-skill-render.mjs +7 -5
  13. package/runtime/codex/bin/lib/context.sh +8 -1
  14. package/runtime/codex/bin/lib/greeting.sh +12 -108
  15. package/runtime/codex/bin/lib/metrics.sh +374 -0
  16. package/runtime/codex/bin/publish-artifact.sh +14 -0
  17. package/runtime/codex/bin/settings.sh +274 -0
  18. package/runtime/codex/manifest.json +2 -0
  19. package/runtime/pi/.codex/config.toml +15 -0
  20. package/runtime/pi/.codex/hooks/branch-guard.js +242 -0
  21. package/runtime/pi/.codex/hooks/search-hint.js +65 -0
  22. package/runtime/pi/.codex/hooks.json +30 -0
  23. package/runtime/pi/.codex/skills/activity/SKILL.md +70 -0
  24. package/runtime/pi/.codex/skills/add/SKILL.md +25 -0
  25. package/runtime/pi/.codex/skills/announce/SKILL.md +54 -0
  26. package/runtime/pi/.codex/skills/answer/SKILL.md +25 -0
  27. package/runtime/pi/.codex/skills/archive/SKILL.md +41 -0
  28. package/runtime/pi/.codex/skills/ask/SKILL.md +59 -0
  29. package/runtime/pi/.codex/skills/branch/SKILL.md +25 -0
  30. package/runtime/pi/.codex/skills/checkup/SKILL.md +41 -0
  31. package/runtime/pi/.codex/skills/commit/SKILL.md +25 -0
  32. package/runtime/pi/.codex/skills/contribute/SKILL.md +25 -0
  33. package/runtime/pi/.codex/skills/dark-mode/SKILL.md +25 -0
  34. package/runtime/pi/.codex/skills/dashboard/SKILL.md +59 -0
  35. package/runtime/pi/.codex/skills/deep-reflect/SKILL.md +74 -0
  36. package/runtime/pi/.codex/skills/delete-user/SKILL.md +25 -0
  37. package/runtime/pi/.codex/skills/egregore-voice/SKILL.md +25 -0
  38. package/runtime/pi/.codex/skills/emissary/SKILL.md +41 -0
  39. package/runtime/pi/.codex/skills/env/SKILL.md +25 -0
  40. package/runtime/pi/.codex/skills/handoff/SKILL.md +67 -0
  41. package/runtime/pi/.codex/skills/harvest/SKILL.md +107 -0
  42. package/runtime/pi/.codex/skills/ingest/SKILL.md +25 -0
  43. package/runtime/pi/.codex/skills/ingest-google/SKILL.md +25 -0
  44. package/runtime/pi/.codex/skills/ingest-user-interview/SKILL.md +41 -0
  45. package/runtime/pi/.codex/skills/invite/SKILL.md +40 -0
  46. package/runtime/pi/.codex/skills/issue/SKILL.md +41 -0
  47. package/runtime/pi/.codex/skills/me/SKILL.md +25 -0
  48. package/runtime/pi/.codex/skills/meeting/SKILL.md +41 -0
  49. package/runtime/pi/.codex/skills/minion/SKILL.md +25 -0
  50. package/runtime/pi/.codex/skills/note/SKILL.md +25 -0
  51. package/runtime/pi/.codex/skills/onboarding/SKILL.md +41 -0
  52. package/runtime/pi/.codex/skills/pr/SKILL.md +25 -0
  53. package/runtime/pi/.codex/skills/project/SKILL.md +41 -0
  54. package/runtime/pi/.codex/skills/pull/SKILL.md +25 -0
  55. package/runtime/pi/.codex/skills/push/SKILL.md +25 -0
  56. package/runtime/pi/.codex/skills/quest/SKILL.md +77 -0
  57. package/runtime/pi/.codex/skills/quest-suggest/SKILL.md +25 -0
  58. package/runtime/pi/.codex/skills/reflect/SKILL.md +41 -0
  59. package/runtime/pi/.codex/skills/review-pr/SKILL.md +41 -0
  60. package/runtime/pi/.codex/skills/save/SKILL.md +62 -0
  61. package/runtime/pi/.codex/skills/scroll/SKILL.md +168 -0
  62. package/runtime/pi/.codex/skills/scroll/agents/openai.yaml +4 -0
  63. package/runtime/pi/.codex/skills/scroll/assets/shell.html +542 -0
  64. package/runtime/pi/.codex/skills/search/SKILL.md +56 -0
  65. package/runtime/pi/.codex/skills/session-view/SKILL.md +25 -0
  66. package/runtime/pi/.codex/skills/setup/SKILL.md +25 -0
  67. package/runtime/pi/.codex/skills/summon/SKILL.md +41 -0
  68. package/runtime/pi/.codex/skills/sync-repos/SKILL.md +25 -0
  69. package/runtime/pi/.codex/skills/telegram-connect/SKILL.md +25 -0
  70. package/runtime/pi/.codex/skills/telemetry/SKILL.md +25 -0
  71. package/runtime/pi/.codex/skills/test/SKILL.md +41 -0
  72. package/runtime/pi/.codex/skills/the-spiral/SKILL.md +84 -0
  73. package/runtime/pi/.codex/skills/thread/SKILL.md +25 -0
  74. package/runtime/pi/.codex/skills/todo/SKILL.md +41 -0
  75. package/runtime/pi/.codex/skills/tui-design/SKILL.md +41 -0
  76. package/runtime/pi/.codex/skills/tutorial/SKILL.md +41 -0
  77. package/runtime/pi/.codex/skills/update/SKILL.md +25 -0
  78. package/runtime/pi/.codex/skills/view/SKILL.md +156 -0
  79. package/runtime/pi/.codex/skills/view/agents/openai.yaml +4 -0
  80. package/runtime/pi/.codex/skills/view/assets/compose-scaffold.html +221 -0
  81. package/runtime/pi/.codex/skills/visual-explain/SKILL.md +41 -0
  82. package/runtime/pi/.codex/skills/wrap/SKILL.md +55 -0
  83. package/runtime/pi/.codex/spec-manifest.json +114 -0
  84. package/runtime/pi/.pi/APPEND_SYSTEM.md +255 -0
  85. package/runtime/pi/.pi/extensions/egregore.ts +324 -0
  86. package/runtime/pi/.pi/settings.json +6 -0
  87. package/runtime/pi/.pi/spec-manifest.json +12 -0
  88. package/runtime/pi/AGENTS.md +350 -0
  89. package/runtime/pi/bin/activity-data.sh +242 -0
  90. package/runtime/pi/bin/agent.sh +677 -0
  91. package/runtime/pi/bin/codex-install-skills.sh +78 -0
  92. package/runtime/pi/bin/codex-render-spec.mjs +394 -0
  93. package/runtime/pi/bin/codex-session-start.sh +188 -0
  94. package/runtime/pi/bin/codex-skill-render.mjs +323 -0
  95. package/runtime/pi/bin/codex-sync-skills.sh +227 -0
  96. package/runtime/pi/bin/dashboard-data.sh +178 -0
  97. package/runtime/pi/bin/graph-batch.sh +60 -0
  98. package/runtime/pi/bin/graph-op.sh +431 -0
  99. package/runtime/pi/bin/graph-wal.sh +228 -0
  100. package/runtime/pi/bin/graph.sh +177 -0
  101. package/runtime/pi/bin/handoff-pr-backfill.sh +86 -0
  102. package/runtime/pi/bin/handoff-run.sh +581 -0
  103. package/runtime/pi/bin/handoff-save-egregore.sh +136 -0
  104. package/runtime/pi/bin/index-handoff.sh +312 -0
  105. package/runtime/pi/bin/lib/artifact-id.sh +46 -0
  106. package/runtime/pi/bin/lib/config.sh +71 -0
  107. package/runtime/pi/bin/lib/context.sh +542 -0
  108. package/runtime/pi/bin/lib/dashboard-artifact.sh +142 -0
  109. package/runtime/pi/bin/lib/ensure-develop.sh +183 -0
  110. package/runtime/pi/bin/lib/git-sync.sh +288 -0
  111. package/runtime/pi/bin/lib/greeting.sh +340 -0
  112. package/runtime/pi/bin/lib/hash.sh +13 -0
  113. package/runtime/pi/bin/lib/identity.sh +174 -0
  114. package/runtime/pi/bin/lib/metrics.sh +374 -0
  115. package/runtime/pi/bin/lib/permissions.sh +106 -0
  116. package/runtime/pi/bin/lib/time.sh +56 -0
  117. package/runtime/pi/bin/lib/worktree-links.sh +54 -0
  118. package/runtime/pi/bin/notify.sh +207 -0
  119. package/runtime/pi/bin/pi-branch-policy.mjs +15 -0
  120. package/runtime/pi/bin/pi-product-workflows.mjs +324 -0
  121. package/runtime/pi/bin/pi-render-spec.mjs +114 -0
  122. package/runtime/pi/bin/pi-session-start.sh +24 -0
  123. package/runtime/pi/bin/publish-artifact.sh +201 -0
  124. package/runtime/pi/bin/publish-references.sh +144 -0
  125. package/runtime/pi/bin/repo-state.sh +99 -0
  126. package/runtime/pi/bin/telemetry.sh +391 -0
  127. package/runtime/pi/bin/worktree-create.sh +118 -0
  128. package/runtime/pi/bin/worktree-remove.sh +50 -0
  129. package/runtime/pi/bin/worktree.sh +198 -0
  130. package/runtime/pi/manifest.json +176 -0
@@ -0,0 +1,374 @@
1
+ # shellcheck shell=bash
2
+ # metrics.sh — Momentum metrics gathering and rendering for startup greetings.
3
+ #
4
+ # Function-only library. Safe to source from bash or zsh.
5
+
6
+ _metrics_zero_json() {
7
+ printf '%s\n' '{"source":"local","sessions":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"all":0},"commits":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"authors":0,"all":0},"handoffs":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"avg_resolution_days":null,"all":0},"knowledge":{"buckets":[0,0,0,0,0],"week":0,"prev":0,"all":0},"members":{"active":0,"total":0}}'
8
+ }
9
+
10
+ _num() {
11
+ awk '{n=$1+0; if (n < 0) n=0; printf "%d\n", n}' 2>/dev/null <<EOF
12
+ $1
13
+ EOF
14
+ }
15
+
16
+ _num_float() {
17
+ awk '{n=$1+0; if (n < 0) n=0; printf "%.1f\n", n}' 2>/dev/null <<EOF
18
+ $1
19
+ EOF
20
+ }
21
+
22
+ _normalize_buckets() {
23
+ set -- $(printf '%s' "$1")
24
+ printf '%d %d %d %d %d\n' "$(_num "${1:-0}")" "$(_num "${2:-0}")" "$(_num "${3:-0}")" "$(_num "${4:-0}")" "$(_num "${5:-0}")"
25
+ }
26
+
27
+ _buckets_json() {
28
+ set -- $(printf '%s' "$(_normalize_buckets "$1")")
29
+ jq -n --argjson a "$(_num "${1:-0}")" --argjson b "$(_num "${2:-0}")" \
30
+ --argjson c "$(_num "${3:-0}")" --argjson d "$(_num "${4:-0}")" \
31
+ --argjson e "$(_num "${5:-0}")" '[$a,$b,$c,$d,$e]' 2>/dev/null || echo '[0,0,0,0,0]'
32
+ }
33
+
34
+ _bucket_week() {
35
+ set -- $(printf '%s' "$(_normalize_buckets "$1")")
36
+ _num "${5:-0}"
37
+ }
38
+
39
+ _bucket_prev() {
40
+ set -- $(printf '%s' "$(_normalize_buckets "$1")")
41
+ _num "${4:-0}"
42
+ }
43
+
44
+ _spark() {
45
+ local values max out v idx glyph
46
+ values="${1:-0 0 0 0 0}"
47
+ max=0
48
+ set -- $(printf '%s' "$values")
49
+ [ "$#" -eq 0 ] && set -- 0 0 0 0 0
50
+ for v in "$@"; do
51
+ v=$(_num "$v")
52
+ [ "$v" -gt "$max" ] 2>/dev/null && max="$v"
53
+ done
54
+ out=""
55
+ for v in "$@"; do
56
+ v=$(_num "$v")
57
+ if [ "$max" -le 0 ] 2>/dev/null; then
58
+ idx=0
59
+ else
60
+ idx=$(( (v * 7) / max ))
61
+ fi
62
+ # Select glyphs explicitly: substring slicing can split UTF-8 characters
63
+ # under Bash 3.2 or a C locale.
64
+ case "$idx" in
65
+ 0) glyph="▁" ;;
66
+ 1) glyph="▂" ;;
67
+ 2) glyph="▃" ;;
68
+ 3) glyph="▄" ;;
69
+ 4) glyph="▅" ;;
70
+ 5) glyph="▆" ;;
71
+ 6) glyph="▇" ;;
72
+ *) glyph="█" ;;
73
+ esac
74
+ out="${out}${glyph}"
75
+ done
76
+ printf '%s\n' "$out"
77
+ }
78
+
79
+ _pct_delta() {
80
+ local cur prev pct
81
+ cur=$(_num "${1:-0}")
82
+ prev=$(_num "${2:-0}")
83
+ [ "$prev" -le 0 ] 2>/dev/null && return 0
84
+ pct=$(( ((cur - prev) * 100) / prev ))
85
+ if [ "$pct" -gt 0 ] 2>/dev/null; then
86
+ printf '↑ %d%%\n' "$pct"
87
+ elif [ "$pct" -lt 0 ] 2>/dev/null; then
88
+ printf '↓ %d%%\n' "$(( -pct ))"
89
+ else
90
+ printf '→ 0%%\n'
91
+ fi
92
+ }
93
+
94
+ _commafy() {
95
+ awk '
96
+ function comma(s, out, sign) {
97
+ s = int(s + 0)
98
+ if (s < 0) { sign = "-"; s = -s } else { sign = "" }
99
+ s = sprintf("%d", s)
100
+ out = ""
101
+ while (length(s) > 3) {
102
+ out = "," substr(s, length(s) - 2, 3) out
103
+ s = substr(s, 1, length(s) - 3)
104
+ }
105
+ return sign s out
106
+ }
107
+ { print comma($1) }
108
+ ' 2>/dev/null <<EOF
109
+ ${1:-0}
110
+ EOF
111
+ }
112
+
113
+ _git_added_buckets() {
114
+ local repo now paths
115
+ repo="$1"
116
+ shift
117
+ now=$(date +%s)
118
+ if [ -d "$repo" ] || [ -f "$repo" ]; then
119
+ git -C "$repo" log --diff-filter=A --since="35 days ago" --format='%ct' -- "$@" 2>/dev/null | \
120
+ awk -v now="$now" '
121
+ BEGIN { for (i=0;i<5;i++) b[i]=0 }
122
+ /^[0-9]+$/ {
123
+ days = int((now - $1) / 86400)
124
+ idx = 4 - int(days / 7)
125
+ if (idx >= 0 && idx < 5) b[idx]++
126
+ }
127
+ END { printf "%d %d %d %d %d\n", b[0], b[1], b[2], b[3], b[4] }
128
+ ' 2>/dev/null || echo "0 0 0 0 0"
129
+ else
130
+ echo "0 0 0 0 0"
131
+ fi
132
+ }
133
+
134
+ _find_count_md() {
135
+ find -L "$@" -name '*.md' ! -name 'index*' 2>/dev/null | wc -l | awk '{print $1+0}' 2>/dev/null || echo 0
136
+ }
137
+
138
+ _metrics_gather() {
139
+ local _root _mem _scratch _API_URL _API_KEY _connected
140
+ _root="${SCRIPT_DIR:-$PWD}"
141
+ _mem="$_root/memory"
142
+ _scratch=$(mktemp -d 2>/dev/null || mktemp -d -t egregore-metrics)
143
+ _API_URL=$(jq -r '.api_url // empty' "$_root/egregore.json" 2>/dev/null || echo "")
144
+ _API_KEY=$(grep '^EGREGORE_API_KEY=' "${ENV_FILE:-$_root/.env}" 2>/dev/null | cut -d'=' -f2- || true)
145
+ _connected=false
146
+ [ -n "$_API_URL" ] && [ -n "$_API_KEY" ] && _connected=true
147
+
148
+ (
149
+ if [ "$_connected" = true ]; then
150
+ raw=$(bash "$_root/bin/graph.sh" query "MATCH (s:Session) WHERE date(s.date) >= date() - duration('P35D')
151
+ OPTIONAL MATCH (s)-[:BY]->(p:Person)
152
+ WITH duration.inDays(date(s.date), date()).days / 7 AS weeksAgo,
153
+ count(s) AS sessions, count(DISTINCT p) AS people
154
+ RETURN weeksAgo, sessions, people ORDER BY weeksAgo" 2>/dev/null || echo "")
155
+ printf '%s' "$raw" | jq -r '
156
+ (.values // []) as $v
157
+ | if ($v|length) == 0 then empty else
158
+ reduce $v[] as $r ({b:[0,0,0,0,0], a:0, ok:0};
159
+ ($r[0] // 99 | tonumber? // 99) as $w
160
+ | if $w >= 0 and $w <= 4 then
161
+ .b[4 - $w] = (($r[1] // 0 | tonumber? // 0) | floor)
162
+ | .ok = 1
163
+ | if $w == 0 then .a = (($r[2] // 0 | tonumber? // 0) | floor) else . end
164
+ else . end)
165
+ | "\(.b|join(" "))|\(.a)|\(.ok)"
166
+ end
167
+ ' 2>/dev/null || echo "0 0 0 0 0|0|0"
168
+ else
169
+ echo "0 0 0 0 0|0|0"
170
+ fi
171
+ ) > "$_scratch/graph_sessions" 2>/dev/null &
172
+
173
+ (
174
+ if [ "$_connected" = true ]; then
175
+ raw=$(bash "$_root/bin/graph.sh" query "MATCH (s:Session) WITH count(s) AS totalSessions MATCH (p:Person)-[:MEMBER_OF]->(:Org) RETURN totalSessions, count(DISTINCT p) AS members" 2>/dev/null || echo "")
176
+ total=$(printf '%s' "$raw" | jq -r '.values[0][0] // 0' 2>/dev/null || echo 0)
177
+ members=$(printf '%s' "$raw" | jq -r '.values[0][1] // 0' 2>/dev/null || echo 0)
178
+ if [ "$(_num "$members")" -eq 0 ] 2>/dev/null; then
179
+ raw2=$(bash "$_root/bin/graph.sh" query "MATCH (p:Person) RETURN count(p)" 2>/dev/null || echo "")
180
+ members=$(printf '%s' "$raw2" | jq -r '.values[0][0] // 0' 2>/dev/null || echo 0)
181
+ fi
182
+ printf '%s|%s\n' "$(_num "$total")" "$(_num "$members")"
183
+ else
184
+ echo "0|0"
185
+ fi
186
+ ) > "$_scratch/graph_totals" 2>/dev/null &
187
+
188
+ (
189
+ if [ "$_connected" = true ]; then
190
+ raw=$(bash "$_root/bin/graph.sh" query "MATCH (s:Session)-[:HANDED_TO]->(:Person)
191
+ WHERE s.handoffStatus = 'done' AND date(s.date) >= date() - duration('P30D')
192
+ AND s.handoffReadDate IS NOT NULL
193
+ WITH duration.inDays(date(s.date), date(s.handoffReadDate)).days AS d
194
+ RETURN avg(d) AS avgDays, count(*) AS n" 2>/dev/null || echo "")
195
+ avg=$(printf '%s' "$raw" | jq -r '.values[0][0] // empty' 2>/dev/null || echo "")
196
+ n=$(printf '%s' "$raw" | jq -r '.values[0][1] // 0' 2>/dev/null || echo 0)
197
+ if [ "$(_num "$n")" -gt 0 ] 2>/dev/null && [ -n "$avg" ]; then _num_float "$avg"; else echo ""; fi
198
+ else
199
+ echo ""
200
+ fi
201
+ ) > "$_scratch/handoff_resolution" 2>/dev/null &
202
+
203
+ (
204
+ buckets=$(_git_added_buckets "$_mem" sessions wraps)
205
+ all=$(_find_count_md "$_root/memory/sessions" "$_root/memory/wraps")
206
+ printf '%s|%s\n' "$buckets" "$all"
207
+ ) > "$_scratch/local_sessions" 2>/dev/null &
208
+
209
+ (
210
+ now=$(date +%s)
211
+ if [ -d "$_mem" ] || [ -f "$_mem" ]; then
212
+ git -C "$_mem" log --since="35 days ago" --format='%ct|%ae' 2>/dev/null | \
213
+ awk -F'|' -v now="$now" '
214
+ BEGIN { for (i=0;i<5;i++) b[i]=0 }
215
+ {
216
+ days = int((now - $1) / 86400)
217
+ idx = 4 - int(days / 7)
218
+ if (idx >= 0 && idx < 5) {
219
+ b[idx]++
220
+ if (idx == 4 && $2 != "") a[$2]=1
221
+ }
222
+ }
223
+ END {
224
+ n=0; for (k in a) n++
225
+ printf "%d %d %d %d %d|%d\n", b[0], b[1], b[2], b[3], b[4], n
226
+ }
227
+ ' 2>/dev/null || echo "0 0 0 0 0|0"
228
+ else
229
+ echo "0 0 0 0 0|0"
230
+ fi
231
+ ) > "$_scratch/commits" 2>/dev/null &
232
+
233
+ (
234
+ if [ -d "$_mem" ] || [ -f "$_mem" ]; then git -C "$_mem" rev-list --count HEAD 2>/dev/null || echo 0; else echo 0; fi
235
+ ) > "$_scratch/commits_all" 2>/dev/null &
236
+
237
+ (
238
+ buckets=$(_git_added_buckets "$_mem" handoffs)
239
+ all=$(_find_count_md "$_root/memory/handoffs")
240
+ printf '%s|%s\n' "$buckets" "$all"
241
+ ) > "$_scratch/handoffs" 2>/dev/null &
242
+
243
+ (
244
+ buckets=$(_git_added_buckets "$_mem" knowledge/decisions knowledge/findings knowledge/patterns)
245
+ all=$(_find_count_md "$_root/memory/knowledge/decisions" "$_root/memory/knowledge/findings" "$_root/memory/knowledge/patterns")
246
+ printf '%s|%s\n' "$buckets" "$all"
247
+ ) > "$_scratch/knowledge" 2>/dev/null &
248
+
249
+ (
250
+ _find_count_md "$_root/memory/people"
251
+ ) > "$_scratch/members_total_local" 2>/dev/null &
252
+
253
+ wait
254
+
255
+ gs=$(cat "$_scratch/graph_sessions" 2>/dev/null || echo "0 0 0 0 0|0|0")
256
+ gt=$(cat "$_scratch/graph_totals" 2>/dev/null || echo "0|0")
257
+ lsess=$(cat "$_scratch/local_sessions" 2>/dev/null || echo "0 0 0 0 0|0")
258
+ commits=$(cat "$_scratch/commits" 2>/dev/null || echo "0 0 0 0 0|0")
259
+ handoffs=$(cat "$_scratch/handoffs" 2>/dev/null || echo "0 0 0 0 0|0")
260
+ knowledge=$(cat "$_scratch/knowledge" 2>/dev/null || echo "0 0 0 0 0|0")
261
+
262
+ graph_valid=$(_num "$(printf '%s' "$gs" | awk -F'|' '{print $3}')")
263
+ if [ "$graph_valid" -gt 0 ] 2>/dev/null; then
264
+ source_name="graph"
265
+ session_buckets=$(_normalize_buckets "$(printf '%s' "$gs" | awk -F'|' '{print $1}')")
266
+ session_all=$(_num "$(printf '%s' "$gt" | awk -F'|' '{print $1}')")
267
+ members_active=$(_num "$(printf '%s' "$gs" | awk -F'|' '{print $2}')")
268
+ members_total=$(_num "$(printf '%s' "$gt" | awk -F'|' '{print $2}')")
269
+ if [ "$session_all" -le 0 ] 2>/dev/null; then
270
+ session_all=$(_num "$(printf '%s' "$lsess" | awk -F'|' '{print $2}')")
271
+ fi
272
+ if [ "$members_total" -le 0 ] 2>/dev/null; then
273
+ members_total=$(_num "$(cat "$_scratch/members_total_local" 2>/dev/null || echo 0)")
274
+ fi
275
+ else
276
+ source_name="local"
277
+ session_buckets=$(_normalize_buckets "$(printf '%s' "$lsess" | awk -F'|' '{print $1}')")
278
+ session_all=$(_num "$(printf '%s' "$lsess" | awk -F'|' '{print $2}')")
279
+ members_active=$(_num "$(printf '%s' "$commits" | awk -F'|' '{print $2}')")
280
+ members_total=$(_num "$(cat "$_scratch/members_total_local" 2>/dev/null || echo 0)")
281
+ fi
282
+
283
+ commit_buckets=$(_normalize_buckets "$(printf '%s' "$commits" | awk -F'|' '{print $1}')")
284
+ handoff_buckets=$(_normalize_buckets "$(printf '%s' "$handoffs" | awk -F'|' '{print $1}')")
285
+ knowledge_buckets=$(_normalize_buckets "$(printf '%s' "$knowledge" | awk -F'|' '{print $1}')")
286
+ avg_res=$(cat "$_scratch/handoff_resolution" 2>/dev/null || echo "")
287
+
288
+ session_buckets_json=$(_buckets_json "$session_buckets")
289
+ commit_buckets_json=$(_buckets_json "$commit_buckets")
290
+ handoff_buckets_json=$(_buckets_json "$handoff_buckets")
291
+ knowledge_buckets_json=$(_buckets_json "$knowledge_buckets")
292
+
293
+ if [ -n "$avg_res" ]; then
294
+ jq -n --arg source "$source_name" \
295
+ --argjson sb "$session_buckets_json" --argjson sw "$(_bucket_week "$session_buckets")" --argjson sp "$(_bucket_prev "$session_buckets")" --argjson sall "$session_all" \
296
+ --argjson cb "$commit_buckets_json" --argjson cw "$(_bucket_week "$commit_buckets")" --argjson cp "$(_bucket_prev "$commit_buckets")" --argjson ca "$(_num "$(printf '%s' "$commits" | awk -F'|' '{print $2}')")" --argjson call "$(_num "$(cat "$_scratch/commits_all" 2>/dev/null || echo 0)")" \
297
+ --argjson hb "$handoff_buckets_json" --argjson hw "$(_bucket_week "$handoff_buckets")" --argjson hp "$(_bucket_prev "$handoff_buckets")" --argjson havg "$avg_res" --argjson hall "$(_num "$(printf '%s' "$handoffs" | awk -F'|' '{print $2}')")" \
298
+ --argjson kb "$knowledge_buckets_json" --argjson kw "$(_bucket_week "$knowledge_buckets")" --argjson kp "$(_bucket_prev "$knowledge_buckets")" --argjson kall "$(_num "$(printf '%s' "$knowledge" | awk -F'|' '{print $2}')")" \
299
+ --argjson ma "$members_active" --argjson mt "$members_total" \
300
+ '{source:$source,sessions:{buckets:$sb,week:$sw,prev:$sp,all:$sall},commits:{buckets:$cb,week:$cw,prev:$cp,authors:$ca,all:$call},handoffs:{buckets:$hb,week:$hw,prev:$hp,avg_resolution_days:$havg,all:$hall},knowledge:{buckets:$kb,week:$kw,prev:$kp,all:$kall},members:{active:$ma,total:$mt}}' 2>/dev/null || _metrics_zero_json
301
+ else
302
+ jq -n --arg source "$source_name" \
303
+ --argjson sb "$session_buckets_json" --argjson sw "$(_bucket_week "$session_buckets")" --argjson sp "$(_bucket_prev "$session_buckets")" --argjson sall "$session_all" \
304
+ --argjson cb "$commit_buckets_json" --argjson cw "$(_bucket_week "$commit_buckets")" --argjson cp "$(_bucket_prev "$commit_buckets")" --argjson ca "$(_num "$(printf '%s' "$commits" | awk -F'|' '{print $2}')")" --argjson call "$(_num "$(cat "$_scratch/commits_all" 2>/dev/null || echo 0)")" \
305
+ --argjson hb "$handoff_buckets_json" --argjson hw "$(_bucket_week "$handoff_buckets")" --argjson hp "$(_bucket_prev "$handoff_buckets")" --argjson hall "$(_num "$(printf '%s' "$handoffs" | awk -F'|' '{print $2}')")" \
306
+ --argjson kb "$knowledge_buckets_json" --argjson kw "$(_bucket_week "$knowledge_buckets")" --argjson kp "$(_bucket_prev "$knowledge_buckets")" --argjson kall "$(_num "$(printf '%s' "$knowledge" | awk -F'|' '{print $2}')")" \
307
+ --argjson ma "$members_active" --argjson mt "$members_total" \
308
+ '{source:$source,sessions:{buckets:$sb,week:$sw,prev:$sp,all:$sall},commits:{buckets:$cb,week:$cw,prev:$cp,authors:$ca,all:$call},handoffs:{buckets:$hb,week:$hw,prev:$hp,avg_resolution_days:null,all:$hall},knowledge:{buckets:$kb,week:$kw,prev:$kp,all:$kall},members:{active:$ma,total:$mt}}' 2>/dev/null || _metrics_zero_json
309
+ fi
310
+ rm -rf "$_scratch" 2>/dev/null || true
311
+ }
312
+
313
+ _metrics_read() {
314
+ local file filter fallback
315
+ file="${CTX_DIR:-}/metrics"
316
+ filter="$1"
317
+ fallback="$2"
318
+ if [ -f "$file" ]; then
319
+ jq -r "$filter" "$file" 2>/dev/null || echo "$fallback"
320
+ else
321
+ echo "$fallback"
322
+ fi
323
+ }
324
+
325
+ _render_momentum_board() {
326
+ local separator line_width header_left header_right pad
327
+ local sb sw sp sall cb cw ca call hb hw havg hall kb kw kall active total
328
+ local sdelta sann cann hann kann know_week
329
+ separator="${SEPARATOR:- ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄}"
330
+ line_width="${LINE_WIDTH:-67}"
331
+
332
+ sb=$(_metrics_read '(.sessions.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
333
+ sw=$(_num "$(_metrics_read '.sessions.week // 0' "0")")
334
+ sp=$(_num "$(_metrics_read '.sessions.prev // 0' "0")")
335
+ sall=$(_num "$(_metrics_read '.sessions.all // 0' "0")")
336
+ cb=$(_metrics_read '(.commits.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
337
+ cw=$(_num "$(_metrics_read '.commits.week // 0' "0")")
338
+ ca=$(_num "$(_metrics_read '.commits.authors // 0' "0")")
339
+ call=$(_num "$(_metrics_read '.commits.all // 0' "0")")
340
+ hb=$(_metrics_read '(.handoffs.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
341
+ hw=$(_num "$(_metrics_read '.handoffs.week // 0' "0")")
342
+ havg=$(_metrics_read '.handoffs.avg_resolution_days // empty' "")
343
+ hall=$(_num "$(_metrics_read '.handoffs.all // 0' "0")")
344
+ kb=$(_metrics_read '(.knowledge.buckets // [0,0,0,0,0]) | .[0:5] | map(. // 0) | join(" ")' "0 0 0 0 0")
345
+ kw=$(_num "$(_metrics_read '.knowledge.week // 0' "0")")
346
+ kall=$(_num "$(_metrics_read '.knowledge.all // 0' "0")")
347
+ active=$(_num "$(_metrics_read '.members.active // 0' "0")")
348
+ total=$(_num "$(_metrics_read '.members.total // 0' "0")")
349
+
350
+ header_left=" ◦ momentum · this week"
351
+ header_right="${active} of ${total} active"
352
+ pad=$((line_width - ${#header_left} - ${#header_right}))
353
+ [ "$pad" -lt 1 ] && pad=1
354
+ printf "%s%*s%s\n" "$header_left" "$pad" "" "$header_right"
355
+
356
+ sdelta=$(_pct_delta "$sw" "$sp")
357
+ sann=""
358
+ [ -n "$sdelta" ] && sann="${sdelta} vs last week"
359
+ cann=""
360
+ if [ "$ca" -gt 0 ] 2>/dev/null; then
361
+ if [ "$ca" -eq 1 ] 2>/dev/null; then cann="1 author"; else cann="${ca} authors"; fi
362
+ fi
363
+ hann=""
364
+ if [ -n "$havg" ]; then hann="$(_num_float "$havg")d avg resolution"; fi
365
+ kann="$(_commafy "$kall") artifacts total"
366
+ know_week="+${kw}"
367
+
368
+ printf " %-11s%5s 5-week %s %s\n\n" "sessions" "$sw" "$(_spark "$sb")" "$sann"
369
+ printf " %-11s%5s 5-week %s %s\n\n" "commits" "$cw" "$(_spark "$cb")" "$cann"
370
+ printf " %-11s%5s 5-week %s %s\n\n" "handoffs" "$hw" "$(_spark "$hb")" "$hann"
371
+ printf " %-11s%5s 5-week %s %s\n" "knowledge" "$know_week" "$(_spark "$kb")" "$kann"
372
+ echo "$separator"
373
+ printf " %s sessions · %s commits · %s handoffs all-time\n" "$(_commafy "$sall")" "$(_commafy "$call")" "$(_commafy "$hall")"
374
+ }
@@ -95,6 +95,20 @@ fi
95
95
  # Public relay for OSS share endpoint
96
96
  RELAY_URL="https://egregore-production-55f2.up.railway.app"
97
97
 
98
+ # --- Artifact hosting off-switch (compliance / privacy) ---
99
+ # An instance can disable ALL egregore.xyz publishing via `bin/settings.sh hosting off`
100
+ # (sets features.publishing=false). Absent/true means ON, so existing instances are
101
+ # unaffected. We bail BEFORE any render or upload so "off" truly does nothing and costs
102
+ # nothing. Exit 3 is distinct from usage errors; fire-and-forget callers ignore it.
103
+ # NOTE: test `!= false`, not jq's `// "true"` — `//` treats an explicit `false` as empty
104
+ # and would return the default, silently defeating the off-switch.
105
+ PUBLISHING_ENABLED="$(jq -r 'if .features.publishing == false then "false" else "true" end' "$CONFIG" 2>/dev/null || echo "true")"
106
+ if [ "$PUBLISHING_ENABLED" = "false" ]; then
107
+ echo "Artifact hosting is off for this egregore (features.publishing=false); nothing uploaded." >&2
108
+ echo "Turn it on with: bin/settings.sh hosting on (or: egregore settings → Hosting)." >&2
109
+ exit 3
110
+ fi
111
+
98
112
  # --- Prepare HTML ---
99
113
  TMP_HTML="/tmp/egregore-artifacts/publish-$$.html"
100
114
  mkdir -p /tmp/egregore-artifacts
@@ -0,0 +1,274 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # settings.sh — deterministic config verbs for an Egregore instance.
5
+ #
6
+ # Edits egregore.json atomically. No agent, no network, no session. Idempotent:
7
+ # re-adding an existing item is a no-op success; removing a missing one is too.
8
+ # This is the deterministic core the launcher settings screen (and slash
9
+ # commands) call — the reliability lives here, the surfaces are thin callers.
10
+ #
11
+ # Usage:
12
+ # settings.sh hosting status|on|off
13
+ # settings.sh repo list | add <name> [description] | remove <name>
14
+ # settings.sh admin list | add <github-handle> | remove <github-handle>
15
+ # settings.sh dump # full settings snapshot as JSON (for the launcher)
16
+ #
17
+ # Add --json to a read verb (status/list) for machine-readable output.
18
+ #
19
+ # Exit codes: 0 ok · 1 usage/validation error · 2 config not found.
20
+
21
+ SCRIPT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
22
+ CONFIG="$SCRIPT_DIR/egregore.json"
23
+
24
+ # ── helpers ──────────────────────────────────────────────────────────────
25
+
26
+ _die() { echo "$1" >&2; exit "${2:-1}"; }
27
+
28
+ _valid_token() {
29
+ # non-empty, GitHub-handle / repo-name safe charset
30
+ [ -n "${1:-}" ] && printf '%s' "$1" | grep -qE '^[A-Za-z0-9._-]+$'
31
+ }
32
+
33
+ _save() {
34
+ # _save '<jq-filter>' [jq-args...] — apply filter to CONFIG, write atomically.
35
+ local filter="$1"; shift
36
+ local tmp
37
+ tmp="$(mktemp "$CONFIG.XXXXXX")" || _die "cannot create temp file" 1
38
+ if jq "$@" "$filter" "$CONFIG" > "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
39
+ mv "$tmp" "$CONFIG"
40
+ else
41
+ rm -f "$tmp"
42
+ _die "failed to update $CONFIG (invalid jq filter or unwritable file)" 1
43
+ fi
44
+ }
45
+
46
+ _has_repo() { jq -e --arg n "$1" 'any(.repos[]?; (if type=="object" then .name else . end) == $n)' "$CONFIG" >/dev/null; }
47
+ _has_admin() { jq -e --arg h "$1" 'any(.admins[]?; . == $h)' "$CONFIG" >/dev/null; }
48
+
49
+ usage() {
50
+ cat >&2 <<'EOF'
51
+ settings.sh — Egregore instance settings
52
+
53
+ settings.sh hosting status|on|off
54
+ settings.sh repo list | add <name> [description] | remove <name>
55
+ settings.sh admin list | add <github-handle> | remove <github-handle>
56
+ settings.sh people list | add <github-handle> | remove <github-handle>
57
+ settings.sh dump full snapshot (JSON)
58
+
59
+ --json machine-readable output for status/list
60
+ EOF
61
+ exit "${1:-1}"
62
+ }
63
+
64
+ # ── verbs ────────────────────────────────────────────────────────────────
65
+
66
+ cmd_hosting() {
67
+ local action="${1:-status}"
68
+ # NEVER use jq's `// "true"` here: `//` treats an explicit `false` as empty and
69
+ # would return the default, silently defeating the off-switch. Test `!= false`
70
+ # so absent/true => enabled, explicit false => disabled. `enabled` is the
71
+ # resolved boolean string ("true"/"false").
72
+ local enabled; enabled="$(jq -r '.features.publishing != false' "$CONFIG" 2>/dev/null || echo "true")"
73
+ case "$action" in
74
+ status)
75
+ if [ "$JSON" = 1 ]; then
76
+ jq -n --argjson on "$enabled" '{domain:"hosting", publishing:$on}'
77
+ elif [ "$enabled" = "false" ]; then echo "Hosting (egregore.xyz): OFF"
78
+ else echo "Hosting (egregore.xyz): ON"; fi ;;
79
+ on)
80
+ _save '.features.publishing = true'
81
+ echo "Hosting (egregore.xyz): ON" ;;
82
+ off)
83
+ _save '.features.publishing = false'
84
+ echo "Hosting (egregore.xyz): OFF — artifacts will no longer upload to egregore.xyz" ;;
85
+ *) _die "usage: settings.sh hosting status|on|off" 1 ;;
86
+ esac
87
+ }
88
+
89
+ cmd_repo() {
90
+ local action="${1:-list}"; shift || true
91
+ case "$action" in
92
+ list)
93
+ if [ "$JSON" = 1 ]; then
94
+ jq '[.repos[]? | if type=="object" then . else {name:.} end]' "$CONFIG"
95
+ else
96
+ jq -r '.repos[]? | if type=="object" then " " + .name + (if .description then " — " + .description else "" end) else " " + . end' "$CONFIG"
97
+ fi ;;
98
+ add)
99
+ local name="${1:-}"; shift || true
100
+ local desc="${*:-}"
101
+ _valid_token "$name" || _die "usage: settings.sh repo add <name> [description]" 1
102
+ if _has_repo "$name"; then echo "repo '$name' already managed"; return 0; fi
103
+ if [ -n "$desc" ]; then
104
+ _save '.repos = ((.repos // []) + [{name:$n, description:$d}])' --arg n "$name" --arg d "$desc"
105
+ else
106
+ _save '.repos = ((.repos // []) + [{name:$n}])' --arg n "$name"
107
+ fi
108
+ echo "added repo '$name'"
109
+ echo " clone it with: /sync-repos (or reopen the egregore)" ;;
110
+ remove)
111
+ local name="${1:-}"
112
+ _valid_token "$name" || _die "usage: settings.sh repo remove <name>" 1
113
+ if ! _has_repo "$name"; then echo "repo '$name' is not managed"; return 0; fi
114
+ _save '.repos = [.repos[]? | select((if type=="object" then .name else . end) != $n)]' --arg n "$name"
115
+ echo "removed repo '$name' from config"
116
+ echo " note: the local ../$name checkout is left untouched" ;;
117
+ *) _die "usage: settings.sh repo list|add|remove" 1 ;;
118
+ esac
119
+ }
120
+
121
+ cmd_admin() {
122
+ local action="${1:-list}"; shift || true
123
+ case "$action" in
124
+ list)
125
+ if [ "$JSON" = 1 ]; then jq '{admins:(.admins // [])}' "$CONFIG"
126
+ else jq -r '.admins[]? | " " + .' "$CONFIG"; fi ;;
127
+ add)
128
+ local h="${1:-}"
129
+ _valid_token "$h" || _die "usage: settings.sh admin add <github-handle>" 1
130
+ if _has_admin "$h"; then echo "'$h' is already an admin"; return 0; fi
131
+ _save '.admins = ((.admins // []) + [$h])' --arg h "$h"
132
+ echo "added admin '$h'" ;;
133
+ remove)
134
+ local h="${1:-}"
135
+ _valid_token "$h" || _die "usage: settings.sh admin remove <github-handle>" 1
136
+ if ! _has_admin "$h"; then echo "'$h' is not an admin"; return 0; fi
137
+ local count; count="$(jq '(.admins // []) | length' "$CONFIG")"
138
+ [ "$count" -le 1 ] && _die "refusing to remove the last admin ('$h') — add another admin first" 1
139
+ _save '.admins = [.admins[]? | select(. != $h)]' --arg h "$h"
140
+ echo "removed admin '$h'" ;;
141
+ *) _die "usage: settings.sh admin list|add|remove" 1 ;;
142
+ esac
143
+ }
144
+
145
+ # ── people ───────────────────────────────────────────────────────────────
146
+ # People live as files in memory/people/. add/remove manage that directory AND
147
+ # GitHub repo access (the meaningful local grant). Connected-mode extras — the
148
+ # invite link + Telegram (/invite) and Supabase/Neo4j teardown (/delete-user) —
149
+ # are pointed to, not duplicated here.
150
+
151
+ _people_dir() { echo "$SCRIPT_DIR/memory/people"; }
152
+
153
+ _people_names() {
154
+ local pdir; pdir="$(_people_dir)"
155
+ [ -d "$pdir" ] || return 0
156
+ ls "$pdir"/*.md 2>/dev/null | while IFS= read -r f; do basename "$f" .md; done \
157
+ | grep -viE '^(index|readme)$' || true
158
+ }
159
+
160
+ _people_json() {
161
+ local names; names="$(_people_names)"
162
+ if [ -z "$names" ]; then echo '[]'; return; fi
163
+ printf '%s\n' "$names" | jq -R . | jq -sc 'map(select(length>0))'
164
+ }
165
+
166
+ _has_person() { [ -f "$(_people_dir)/$1.md" ]; }
167
+
168
+ # add|remove a GitHub push-collaborator across the instance's repos. Best-effort:
169
+ # no token/org => skip with a note; per-repo failures never abort.
170
+ _people_github() {
171
+ local user="$1" op="$2" token org repos r
172
+ token="$(grep '^GITHUB_TOKEN=' "$SCRIPT_DIR/.env" 2>/dev/null | cut -d= -f2- || true)"
173
+ org="$(jq -r '.github_org // empty' "$CONFIG" 2>/dev/null)"
174
+ if [ -z "$token" ] || [ -z "$org" ]; then
175
+ echo " (no GitHub token/org — skipped repo access; run bin/github-auth.sh)" >&2
176
+ return 0
177
+ fi
178
+ repos="$(jq -r '[.repo_name, (.memory_repo // "" | split("/") | last | sub("\\.git$";"")), (.repos[]? | if type=="object" then .name else . end)] | map(select(. != null and . != "")) | unique | .[]' "$CONFIG" 2>/dev/null)"
179
+ for r in $repos; do
180
+ if [ "$op" = "add" ]; then
181
+ curl -s -o /dev/null -X PUT -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" \
182
+ "https://api.github.com/repos/$org/$r/collaborators/$user" -d '{"permission":"push"}' 2>/dev/null || true
183
+ else
184
+ curl -s -o /dev/null -X DELETE -H "Authorization: Bearer $token" -H "Accept: application/vnd.github+json" \
185
+ "https://api.github.com/repos/$org/$r/collaborators/$user" 2>/dev/null || true
186
+ fi
187
+ done
188
+ }
189
+
190
+ # create|remove memory/people/<user>.md, committing ONLY the people dir.
191
+ _people_file() {
192
+ local user="$1" op="$2" pdir f inviter today
193
+ pdir="$(_people_dir)"
194
+ [ -d "$SCRIPT_DIR/memory" ] || { echo " (no memory dir — skipped person file)" >&2; return 0; }
195
+ mkdir -p "$pdir"
196
+ f="$pdir/$user.md"
197
+ if [ "$op" = "add" ]; then
198
+ if [ ! -f "$f" ]; then
199
+ inviter="$(jq -r '.display_name // .github_username // "admin"' "$SCRIPT_DIR/.egregore-state.json" 2>/dev/null || echo admin)"
200
+ today="$(date -u +%Y-%m-%d)"
201
+ printf -- '---\nname: %s\ngithub: %s\ninvited_by: %s\njoined: %s\n---\n' "$user" "$user" "$inviter" "$today" > "$f"
202
+ fi
203
+ else
204
+ rm -f "$f"
205
+ fi
206
+ ( cd "$SCRIPT_DIR/memory" 2>/dev/null && git add -A people/ 2>/dev/null \
207
+ && git commit -q -m "settings: people $op $user" 2>/dev/null \
208
+ && git push -q 2>/dev/null ) || true
209
+ }
210
+
211
+ cmd_people() {
212
+ local action="${1:-list}"; shift || true
213
+ case "$action" in
214
+ list)
215
+ if [ "$JSON" = 1 ]; then _people_json
216
+ else
217
+ local names; names="$(_people_names)"
218
+ if [ -n "$names" ]; then printf ' %s\n' $names; else echo " (none)"; fi
219
+ fi ;;
220
+ add)
221
+ local h="${1:-}"
222
+ _valid_token "$h" || _die "usage: settings.sh people add <github-handle>" 1
223
+ if _has_person "$h"; then echo "'$h' is already in people"; return 0; fi
224
+ _people_github "$h" "add"
225
+ _people_file "$h" "add"
226
+ echo "added '$h' (repo access + person file)"
227
+ [ -n "$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)" ] && echo " connected org: for the invite link + Telegram, run /invite $h" ;;
228
+ remove)
229
+ local h="${1:-}"
230
+ _valid_token "$h" || _die "usage: settings.sh people remove <github-handle>" 1
231
+ if ! _has_person "$h"; then echo "'$h' is not in people"; return 0; fi
232
+ _people_github "$h" "remove"
233
+ _people_file "$h" "remove"
234
+ echo "removed '$h' (repo access + person file)"
235
+ [ -n "$(jq -r '.api_url // empty' "$CONFIG" 2>/dev/null)" ] && echo " connected org: for full deprovision (Supabase/Neo4j), run /delete-user $h" ;;
236
+ *) _die "usage: settings.sh people list|add|remove" 1 ;;
237
+ esac
238
+ }
239
+
240
+ cmd_dump() {
241
+ # Always JSON — the launcher calls this once to render the whole settings screen.
242
+ local people; people="$(_people_json)"
243
+ jq --argjson people "$people" '{
244
+ org: .org_name,
245
+ slug: .slug,
246
+ hosting: (.features.publishing != false),
247
+ repos: [.repos[]? | if type=="object" then . else {name:.} end],
248
+ admins: (.admins // []),
249
+ people: $people
250
+ }' "$CONFIG"
251
+ }
252
+
253
+ # ── dispatch ─────────────────────────────────────────────────────────────
254
+
255
+ [ -f "$CONFIG" ] || _die "egregore.json not found at $CONFIG" 2
256
+
257
+ # Pull out --json (bash 3.2-safe array handling), leave the rest as positionals.
258
+ JSON=0
259
+ _args=()
260
+ for _a in "$@"; do
261
+ if [ "$_a" = "--json" ]; then JSON=1; else _args+=("$_a"); fi
262
+ done
263
+ set -- "${_args[@]+"${_args[@]}"}"
264
+
265
+ DOMAIN="${1:-}"; shift || true
266
+ case "$DOMAIN" in
267
+ hosting) cmd_hosting "$@" ;;
268
+ repo) cmd_repo "$@" ;;
269
+ admin) cmd_admin "$@" ;;
270
+ people) cmd_people "$@" ;;
271
+ dump) cmd_dump ;;
272
+ ""|-h|--help|help) usage 0 ;;
273
+ *) _die "unknown settings domain: '$DOMAIN' (try: hosting, repo, admin, people, dump)" 1 ;;
274
+ esac