@runchr/gstack-antigravity 0.1.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.

Potentially problematic release.


This version of @runchr/gstack-antigravity might be problematic. Click here for more details.

Files changed (297) hide show
  1. package/.agents/rules/ETHOS.md +129 -0
  2. package/.agents/rules/global-gstack.md +117 -0
  3. package/.agents/rules/persona-gstack-autoplan.md +14 -0
  4. package/.agents/rules/persona-gstack-benchmark.md +14 -0
  5. package/.agents/rules/persona-gstack-browse.md +14 -0
  6. package/.agents/rules/persona-gstack-canary.md +14 -0
  7. package/.agents/rules/persona-gstack-careful.md +14 -0
  8. package/.agents/rules/persona-gstack-codex.md +14 -0
  9. package/.agents/rules/persona-gstack-cso.md +14 -0
  10. package/.agents/rules/persona-gstack-design-consultation.md +14 -0
  11. package/.agents/rules/persona-gstack-design-review.md +14 -0
  12. package/.agents/rules/persona-gstack-document-release.md +14 -0
  13. package/.agents/rules/persona-gstack-freeze.md +14 -0
  14. package/.agents/rules/persona-gstack-gstack-upgrade.md +14 -0
  15. package/.agents/rules/persona-gstack-guard.md +14 -0
  16. package/.agents/rules/persona-gstack-investigate.md +14 -0
  17. package/.agents/rules/persona-gstack-land-and-deploy.md +14 -0
  18. package/.agents/rules/persona-gstack-office-hours.md +14 -0
  19. package/.agents/rules/persona-gstack-plan-ceo-review.md +14 -0
  20. package/.agents/rules/persona-gstack-plan-design-review.md +14 -0
  21. package/.agents/rules/persona-gstack-plan-eng-review.md +14 -0
  22. package/.agents/rules/persona-gstack-qa-only.md +14 -0
  23. package/.agents/rules/persona-gstack-qa.md +14 -0
  24. package/.agents/rules/persona-gstack-retro.md +14 -0
  25. package/.agents/rules/persona-gstack-review.md +14 -0
  26. package/.agents/rules/persona-gstack-setup-browser-cookies.md +14 -0
  27. package/.agents/rules/persona-gstack-setup-deploy.md +14 -0
  28. package/.agents/rules/persona-gstack-ship.md +14 -0
  29. package/.agents/rules/persona-gstack-unfreeze.md +14 -0
  30. package/.agents/rules/persona-gstack.md +40 -0
  31. package/.agents/rules/recursive-identities.md +22 -0
  32. package/.agents/workflows/autoplan.md +30 -0
  33. package/.agents/workflows/benchmark.md +31 -0
  34. package/.agents/workflows/browse.md +26 -0
  35. package/.agents/workflows/canary.md +33 -0
  36. package/.agents/workflows/careful.md +22 -0
  37. package/.agents/workflows/codex.md +36 -0
  38. package/.agents/workflows/cso.md +29 -0
  39. package/.agents/workflows/design-consultation.md +28 -0
  40. package/.agents/workflows/design-review.md +28 -0
  41. package/.agents/workflows/document-release.md +32 -0
  42. package/.agents/workflows/freeze.md +17 -0
  43. package/.agents/workflows/gstack-upgrade.md +54 -0
  44. package/.agents/workflows/gstack.md +56 -0
  45. package/.agents/workflows/guard.md +18 -0
  46. package/.agents/workflows/investigate.md +37 -0
  47. package/.agents/workflows/land-and-deploy.md +35 -0
  48. package/.agents/workflows/office-hours.md +27 -0
  49. package/.agents/workflows/plan-ceo-review.md +34 -0
  50. package/.agents/workflows/plan-design-review.md +31 -0
  51. package/.agents/workflows/plan-eng-review.md +28 -0
  52. package/.agents/workflows/qa-only.md +28 -0
  53. package/.agents/workflows/qa.md +73 -0
  54. package/.agents/workflows/retro.md +34 -0
  55. package/.agents/workflows/review.md +30 -0
  56. package/.agents/workflows/setup-browser-cookies.md +15 -0
  57. package/.agents/workflows/setup-cookies.md +8 -0
  58. package/.agents/workflows/setup-deploy.md +21 -0
  59. package/.agents/workflows/ship.md +93 -0
  60. package/.agents/workflows/unfreeze.md +12 -0
  61. package/LICENSE +22 -0
  62. package/README.md +189 -0
  63. package/README_KO.md +191 -0
  64. package/bin/install.js +105 -0
  65. package/gstack-origin/.agents/skills/gstack/SKILL.md +651 -0
  66. package/gstack-origin/.agents/skills/gstack-autoplan/SKILL.md +678 -0
  67. package/gstack-origin/.agents/skills/gstack-benchmark/SKILL.md +482 -0
  68. package/gstack-origin/.agents/skills/gstack-browse/SKILL.md +511 -0
  69. package/gstack-origin/.agents/skills/gstack-canary/SKILL.md +486 -0
  70. package/gstack-origin/.agents/skills/gstack-careful/SKILL.md +50 -0
  71. package/gstack-origin/.agents/skills/gstack-cso/SKILL.md +607 -0
  72. package/gstack-origin/.agents/skills/gstack-design-consultation/SKILL.md +615 -0
  73. package/gstack-origin/.agents/skills/gstack-design-review/SKILL.md +988 -0
  74. package/gstack-origin/.agents/skills/gstack-document-release/SKILL.md +604 -0
  75. package/gstack-origin/.agents/skills/gstack-freeze/SKILL.md +67 -0
  76. package/gstack-origin/.agents/skills/gstack-guard/SKILL.md +62 -0
  77. package/gstack-origin/.agents/skills/gstack-investigate/SKILL.md +415 -0
  78. package/gstack-origin/.agents/skills/gstack-land-and-deploy/SKILL.md +873 -0
  79. package/gstack-origin/.agents/skills/gstack-office-hours/SKILL.md +986 -0
  80. package/gstack-origin/.agents/skills/gstack-plan-ceo-review/SKILL.md +1268 -0
  81. package/gstack-origin/.agents/skills/gstack-plan-design-review/SKILL.md +668 -0
  82. package/gstack-origin/.agents/skills/gstack-plan-eng-review/SKILL.md +826 -0
  83. package/gstack-origin/.agents/skills/gstack-qa/SKILL.md +1006 -0
  84. package/gstack-origin/.agents/skills/gstack-qa-only/SKILL.md +626 -0
  85. package/gstack-origin/.agents/skills/gstack-retro/SKILL.md +1065 -0
  86. package/gstack-origin/.agents/skills/gstack-review/SKILL.md +704 -0
  87. package/gstack-origin/.agents/skills/gstack-setup-browser-cookies/SKILL.md +325 -0
  88. package/gstack-origin/.agents/skills/gstack-setup-deploy/SKILL.md +450 -0
  89. package/gstack-origin/.agents/skills/gstack-ship/SKILL.md +1312 -0
  90. package/gstack-origin/.agents/skills/gstack-unfreeze/SKILL.md +36 -0
  91. package/gstack-origin/.agents/skills/gstack-upgrade/SKILL.md +220 -0
  92. package/gstack-origin/.env.example +5 -0
  93. package/gstack-origin/.github/workflows/skill-docs.yml +17 -0
  94. package/gstack-origin/AGENTS.md +49 -0
  95. package/gstack-origin/ARCHITECTURE.md +359 -0
  96. package/gstack-origin/BROWSER.md +271 -0
  97. package/gstack-origin/CHANGELOG.md +800 -0
  98. package/gstack-origin/CLAUDE.md +284 -0
  99. package/gstack-origin/CONTRIBUTING.md +370 -0
  100. package/gstack-origin/ETHOS.md +129 -0
  101. package/gstack-origin/LICENSE +21 -0
  102. package/gstack-origin/README.md +228 -0
  103. package/gstack-origin/SKILL.md +657 -0
  104. package/gstack-origin/SKILL.md.tmpl +281 -0
  105. package/gstack-origin/TODOS.md +564 -0
  106. package/gstack-origin/VERSION +1 -0
  107. package/gstack-origin/autoplan/SKILL.md +689 -0
  108. package/gstack-origin/autoplan/SKILL.md.tmpl +416 -0
  109. package/gstack-origin/benchmark/SKILL.md +489 -0
  110. package/gstack-origin/benchmark/SKILL.md.tmpl +233 -0
  111. package/gstack-origin/bin/dev-setup +68 -0
  112. package/gstack-origin/bin/dev-teardown +56 -0
  113. package/gstack-origin/bin/gstack-analytics +191 -0
  114. package/gstack-origin/bin/gstack-community-dashboard +113 -0
  115. package/gstack-origin/bin/gstack-config +38 -0
  116. package/gstack-origin/bin/gstack-diff-scope +71 -0
  117. package/gstack-origin/bin/gstack-global-discover.ts +591 -0
  118. package/gstack-origin/bin/gstack-repo-mode +93 -0
  119. package/gstack-origin/bin/gstack-review-log +9 -0
  120. package/gstack-origin/bin/gstack-review-read +12 -0
  121. package/gstack-origin/bin/gstack-slug +15 -0
  122. package/gstack-origin/bin/gstack-telemetry-log +158 -0
  123. package/gstack-origin/bin/gstack-telemetry-sync +127 -0
  124. package/gstack-origin/bin/gstack-update-check +196 -0
  125. package/gstack-origin/browse/SKILL.md +517 -0
  126. package/gstack-origin/browse/SKILL.md.tmpl +141 -0
  127. package/gstack-origin/browse/bin/find-browse +21 -0
  128. package/gstack-origin/browse/bin/remote-slug +14 -0
  129. package/gstack-origin/browse/scripts/build-node-server.sh +48 -0
  130. package/gstack-origin/browse/src/browser-manager.ts +634 -0
  131. package/gstack-origin/browse/src/buffers.ts +137 -0
  132. package/gstack-origin/browse/src/bun-polyfill.cjs +109 -0
  133. package/gstack-origin/browse/src/cli.ts +420 -0
  134. package/gstack-origin/browse/src/commands.ts +111 -0
  135. package/gstack-origin/browse/src/config.ts +150 -0
  136. package/gstack-origin/browse/src/cookie-import-browser.ts +417 -0
  137. package/gstack-origin/browse/src/cookie-picker-routes.ts +207 -0
  138. package/gstack-origin/browse/src/cookie-picker-ui.ts +541 -0
  139. package/gstack-origin/browse/src/find-browse.ts +61 -0
  140. package/gstack-origin/browse/src/meta-commands.ts +269 -0
  141. package/gstack-origin/browse/src/platform.ts +17 -0
  142. package/gstack-origin/browse/src/read-commands.ts +335 -0
  143. package/gstack-origin/browse/src/server.ts +369 -0
  144. package/gstack-origin/browse/src/snapshot.ts +398 -0
  145. package/gstack-origin/browse/src/url-validation.ts +91 -0
  146. package/gstack-origin/browse/src/write-commands.ts +352 -0
  147. package/gstack-origin/browse/test/bun-polyfill.test.ts +72 -0
  148. package/gstack-origin/browse/test/commands.test.ts +1836 -0
  149. package/gstack-origin/browse/test/config.test.ts +250 -0
  150. package/gstack-origin/browse/test/cookie-import-browser.test.ts +397 -0
  151. package/gstack-origin/browse/test/cookie-picker-routes.test.ts +205 -0
  152. package/gstack-origin/browse/test/find-browse.test.ts +50 -0
  153. package/gstack-origin/browse/test/fixtures/basic.html +33 -0
  154. package/gstack-origin/browse/test/fixtures/cursor-interactive.html +22 -0
  155. package/gstack-origin/browse/test/fixtures/dialog.html +15 -0
  156. package/gstack-origin/browse/test/fixtures/empty.html +2 -0
  157. package/gstack-origin/browse/test/fixtures/forms.html +55 -0
  158. package/gstack-origin/browse/test/fixtures/qa-eval-checkout.html +108 -0
  159. package/gstack-origin/browse/test/fixtures/qa-eval-spa.html +98 -0
  160. package/gstack-origin/browse/test/fixtures/qa-eval.html +51 -0
  161. package/gstack-origin/browse/test/fixtures/responsive.html +49 -0
  162. package/gstack-origin/browse/test/fixtures/snapshot.html +55 -0
  163. package/gstack-origin/browse/test/fixtures/spa.html +24 -0
  164. package/gstack-origin/browse/test/fixtures/states.html +17 -0
  165. package/gstack-origin/browse/test/fixtures/upload.html +25 -0
  166. package/gstack-origin/browse/test/gstack-config.test.ts +125 -0
  167. package/gstack-origin/browse/test/gstack-update-check.test.ts +467 -0
  168. package/gstack-origin/browse/test/handoff.test.ts +235 -0
  169. package/gstack-origin/browse/test/path-validation.test.ts +63 -0
  170. package/gstack-origin/browse/test/platform.test.ts +37 -0
  171. package/gstack-origin/browse/test/snapshot.test.ts +467 -0
  172. package/gstack-origin/browse/test/test-server.ts +57 -0
  173. package/gstack-origin/browse/test/url-validation.test.ts +72 -0
  174. package/gstack-origin/canary/SKILL.md +493 -0
  175. package/gstack-origin/canary/SKILL.md.tmpl +220 -0
  176. package/gstack-origin/careful/SKILL.md +59 -0
  177. package/gstack-origin/careful/SKILL.md.tmpl +57 -0
  178. package/gstack-origin/careful/bin/check-careful.sh +112 -0
  179. package/gstack-origin/codex/SKILL.md +677 -0
  180. package/gstack-origin/codex/SKILL.md.tmpl +356 -0
  181. package/gstack-origin/conductor.json +6 -0
  182. package/gstack-origin/cso/SKILL.md +615 -0
  183. package/gstack-origin/cso/SKILL.md.tmpl +376 -0
  184. package/gstack-origin/design-consultation/SKILL.md +625 -0
  185. package/gstack-origin/design-consultation/SKILL.md.tmpl +369 -0
  186. package/gstack-origin/design-review/SKILL.md +998 -0
  187. package/gstack-origin/design-review/SKILL.md.tmpl +262 -0
  188. package/gstack-origin/docs/images/github-2013.png +0 -0
  189. package/gstack-origin/docs/images/github-2026.png +0 -0
  190. package/gstack-origin/docs/skills.md +877 -0
  191. package/gstack-origin/document-release/SKILL.md +613 -0
  192. package/gstack-origin/document-release/SKILL.md.tmpl +357 -0
  193. package/gstack-origin/freeze/SKILL.md +82 -0
  194. package/gstack-origin/freeze/SKILL.md.tmpl +80 -0
  195. package/gstack-origin/freeze/bin/check-freeze.sh +68 -0
  196. package/gstack-origin/gstack-upgrade/SKILL.md +226 -0
  197. package/gstack-origin/gstack-upgrade/SKILL.md.tmpl +224 -0
  198. package/gstack-origin/guard/SKILL.md +82 -0
  199. package/gstack-origin/guard/SKILL.md.tmpl +80 -0
  200. package/gstack-origin/investigate/SKILL.md +435 -0
  201. package/gstack-origin/investigate/SKILL.md.tmpl +196 -0
  202. package/gstack-origin/land-and-deploy/SKILL.md +880 -0
  203. package/gstack-origin/land-and-deploy/SKILL.md.tmpl +575 -0
  204. package/gstack-origin/office-hours/SKILL.md +996 -0
  205. package/gstack-origin/office-hours/SKILL.md.tmpl +624 -0
  206. package/gstack-origin/package.json +55 -0
  207. package/gstack-origin/plan-ceo-review/SKILL.md +1277 -0
  208. package/gstack-origin/plan-ceo-review/SKILL.md.tmpl +838 -0
  209. package/gstack-origin/plan-design-review/SKILL.md +676 -0
  210. package/gstack-origin/plan-design-review/SKILL.md.tmpl +314 -0
  211. package/gstack-origin/plan-eng-review/SKILL.md +836 -0
  212. package/gstack-origin/plan-eng-review/SKILL.md.tmpl +279 -0
  213. package/gstack-origin/qa/SKILL.md +1016 -0
  214. package/gstack-origin/qa/SKILL.md.tmpl +316 -0
  215. package/gstack-origin/qa/references/issue-taxonomy.md +85 -0
  216. package/gstack-origin/qa/templates/qa-report-template.md +126 -0
  217. package/gstack-origin/qa-only/SKILL.md +633 -0
  218. package/gstack-origin/qa-only/SKILL.md.tmpl +101 -0
  219. package/gstack-origin/retro/SKILL.md +1072 -0
  220. package/gstack-origin/retro/SKILL.md.tmpl +833 -0
  221. package/gstack-origin/review/SKILL.md +849 -0
  222. package/gstack-origin/review/SKILL.md.tmpl +259 -0
  223. package/gstack-origin/review/TODOS-format.md +62 -0
  224. package/gstack-origin/review/checklist.md +190 -0
  225. package/gstack-origin/review/design-checklist.md +132 -0
  226. package/gstack-origin/review/greptile-triage.md +220 -0
  227. package/gstack-origin/scripts/analytics.ts +190 -0
  228. package/gstack-origin/scripts/dev-skill.ts +82 -0
  229. package/gstack-origin/scripts/eval-compare.ts +96 -0
  230. package/gstack-origin/scripts/eval-list.ts +116 -0
  231. package/gstack-origin/scripts/eval-select.ts +86 -0
  232. package/gstack-origin/scripts/eval-summary.ts +187 -0
  233. package/gstack-origin/scripts/eval-watch.ts +172 -0
  234. package/gstack-origin/scripts/gen-skill-docs.ts +2414 -0
  235. package/gstack-origin/scripts/skill-check.ts +167 -0
  236. package/gstack-origin/setup +269 -0
  237. package/gstack-origin/setup-browser-cookies/SKILL.md +330 -0
  238. package/gstack-origin/setup-browser-cookies/SKILL.md.tmpl +74 -0
  239. package/gstack-origin/setup-deploy/SKILL.md +459 -0
  240. package/gstack-origin/setup-deploy/SKILL.md.tmpl +220 -0
  241. package/gstack-origin/ship/SKILL.md +1457 -0
  242. package/gstack-origin/ship/SKILL.md.tmpl +528 -0
  243. package/gstack-origin/supabase/config.sh +10 -0
  244. package/gstack-origin/supabase/functions/community-pulse/index.ts +59 -0
  245. package/gstack-origin/supabase/functions/telemetry-ingest/index.ts +135 -0
  246. package/gstack-origin/supabase/functions/update-check/index.ts +37 -0
  247. package/gstack-origin/supabase/migrations/001_telemetry.sql +89 -0
  248. package/gstack-origin/test/analytics.test.ts +277 -0
  249. package/gstack-origin/test/codex-e2e.test.ts +197 -0
  250. package/gstack-origin/test/fixtures/coverage-audit-fixture.ts +76 -0
  251. package/gstack-origin/test/fixtures/eval-baselines.json +7 -0
  252. package/gstack-origin/test/fixtures/qa-eval-checkout-ground-truth.json +43 -0
  253. package/gstack-origin/test/fixtures/qa-eval-ground-truth.json +43 -0
  254. package/gstack-origin/test/fixtures/qa-eval-spa-ground-truth.json +43 -0
  255. package/gstack-origin/test/fixtures/review-eval-design-slop.css +86 -0
  256. package/gstack-origin/test/fixtures/review-eval-design-slop.html +41 -0
  257. package/gstack-origin/test/fixtures/review-eval-enum-diff.rb +30 -0
  258. package/gstack-origin/test/fixtures/review-eval-enum.rb +27 -0
  259. package/gstack-origin/test/fixtures/review-eval-vuln.rb +14 -0
  260. package/gstack-origin/test/gemini-e2e.test.ts +173 -0
  261. package/gstack-origin/test/gen-skill-docs.test.ts +1049 -0
  262. package/gstack-origin/test/global-discover.test.ts +187 -0
  263. package/gstack-origin/test/helpers/codex-session-runner.ts +282 -0
  264. package/gstack-origin/test/helpers/e2e-helpers.ts +239 -0
  265. package/gstack-origin/test/helpers/eval-store.test.ts +548 -0
  266. package/gstack-origin/test/helpers/eval-store.ts +689 -0
  267. package/gstack-origin/test/helpers/gemini-session-runner.test.ts +104 -0
  268. package/gstack-origin/test/helpers/gemini-session-runner.ts +201 -0
  269. package/gstack-origin/test/helpers/llm-judge.ts +130 -0
  270. package/gstack-origin/test/helpers/observability.test.ts +283 -0
  271. package/gstack-origin/test/helpers/session-runner.test.ts +96 -0
  272. package/gstack-origin/test/helpers/session-runner.ts +357 -0
  273. package/gstack-origin/test/helpers/skill-parser.ts +206 -0
  274. package/gstack-origin/test/helpers/touchfiles.ts +260 -0
  275. package/gstack-origin/test/hook-scripts.test.ts +373 -0
  276. package/gstack-origin/test/skill-e2e-browse.test.ts +293 -0
  277. package/gstack-origin/test/skill-e2e-deploy.test.ts +279 -0
  278. package/gstack-origin/test/skill-e2e-design.test.ts +614 -0
  279. package/gstack-origin/test/skill-e2e-plan.test.ts +538 -0
  280. package/gstack-origin/test/skill-e2e-qa-bugs.test.ts +194 -0
  281. package/gstack-origin/test/skill-e2e-qa-workflow.test.ts +412 -0
  282. package/gstack-origin/test/skill-e2e-review.test.ts +535 -0
  283. package/gstack-origin/test/skill-e2e-workflow.test.ts +586 -0
  284. package/gstack-origin/test/skill-e2e.test.ts +3325 -0
  285. package/gstack-origin/test/skill-llm-eval.test.ts +787 -0
  286. package/gstack-origin/test/skill-parser.test.ts +179 -0
  287. package/gstack-origin/test/skill-routing-e2e.test.ts +605 -0
  288. package/gstack-origin/test/skill-validation.test.ts +1520 -0
  289. package/gstack-origin/test/telemetry.test.ts +278 -0
  290. package/gstack-origin/test/touchfiles.test.ts +262 -0
  291. package/gstack-origin/unfreeze/SKILL.md +40 -0
  292. package/gstack-origin/unfreeze/SKILL.md.tmpl +38 -0
  293. package/package.json +38 -0
  294. package/scripts/install-antigravity-skill.ps1 +33 -0
  295. package/scripts/install-antigravity-skill.sh +41 -0
  296. package/scripts/sync-gstack-origin.ps1 +37 -0
  297. package/scripts/sync-gstack-origin.sh +35 -0
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env bash
2
+ # gstack-repo-mode — detect solo vs collaborative repo mode
3
+ # Usage: source <(gstack-repo-mode) → sets REPO_MODE variable
4
+ # Or: gstack-repo-mode → prints REPO_MODE=... line
5
+ #
6
+ # Detection heuristic (90-day window):
7
+ # Solo: top author >= 80% of commits
8
+ # Collaborative: top author < 80%
9
+ #
10
+ # Override: gstack-config set repo_mode solo|collaborative
11
+ # Cache: ~/.gstack/projects/$SLUG/repo-mode.json (7-day TTL)
12
+ set -euo pipefail
13
+
14
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
15
+ # Compute SLUG directly (avoid eval of gstack-slug — branch names can contain shell metacharacters)
16
+ REMOTE_URL=$(git remote get-url origin 2>/dev/null || true)
17
+ if [ -z "$REMOTE_URL" ]; then
18
+ echo "REPO_MODE=unknown"
19
+ exit 0
20
+ fi
21
+ SLUG=$(echo "$REMOTE_URL" | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
22
+ [ -z "${SLUG:-}" ] && { echo "REPO_MODE=unknown"; exit 0; }
23
+
24
+ # Validate: only allow known values (prevent shell injection via source <(...))
25
+ validate_mode() {
26
+ case "$1" in solo|collaborative|unknown) echo "$1" ;; *) echo "unknown" ;; esac
27
+ }
28
+
29
+ # Config override takes precedence
30
+ OVERRIDE=$("$SCRIPT_DIR/gstack-config" get repo_mode 2>/dev/null || true)
31
+ if [ -n "$OVERRIDE" ] && [ "$OVERRIDE" != "null" ]; then
32
+ echo "REPO_MODE=$(validate_mode "$OVERRIDE")"
33
+ exit 0
34
+ fi
35
+
36
+ # Check cache (7-day TTL)
37
+ CACHE_DIR="$HOME/.gstack/projects/$SLUG"
38
+ CACHE_FILE="$CACHE_DIR/repo-mode.json"
39
+ if [ -f "$CACHE_FILE" ]; then
40
+ CACHE_AGE=$(( $(date +%s) - $(stat -f %m "$CACHE_FILE" 2>/dev/null || stat -c %Y "$CACHE_FILE" 2>/dev/null || echo 0) ))
41
+ if [ "$CACHE_AGE" -lt 604800 ]; then # 7 days in seconds
42
+ MODE=$(grep -o '"mode":"[^"]*"' "$CACHE_FILE" | head -1 | cut -d'"' -f4)
43
+ [ -n "$MODE" ] && echo "REPO_MODE=$(validate_mode "$MODE")" && exit 0
44
+ fi
45
+ fi
46
+
47
+ # Compute from git history (90-day window)
48
+ # Use default branch (not HEAD) to avoid feature-branch sampling bias
49
+ DEFAULT_BRANCH=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's|refs/remotes/||' || true)
50
+ # Fallback: try origin/main, then origin/master, then HEAD
51
+ if [ -z "$DEFAULT_BRANCH" ]; then
52
+ if git rev-parse --verify origin/main &>/dev/null; then
53
+ DEFAULT_BRANCH="origin/main"
54
+ elif git rev-parse --verify origin/master &>/dev/null; then
55
+ DEFAULT_BRANCH="origin/master"
56
+ else
57
+ DEFAULT_BRANCH="HEAD"
58
+ fi
59
+ fi
60
+ SHORTLOG=$(git shortlog -sn --since="90 days ago" --no-merges "$DEFAULT_BRANCH" 2>/dev/null)
61
+ if [ -z "$SHORTLOG" ]; then
62
+ echo "REPO_MODE=unknown"
63
+ exit 0
64
+ fi
65
+
66
+ # Compute TOTAL from ALL authors (not truncated) to avoid solo bias
67
+ TOTAL=$(echo "$SHORTLOG" | awk '{s+=$1} END {print s}')
68
+ TOP=$(echo "$SHORTLOG" | head -1 | awk '{print $1}')
69
+ AUTHORS=$(echo "$SHORTLOG" | wc -l | tr -d ' ')
70
+
71
+ # Minimum sample: need at least 5 commits to classify
72
+ if [ "$TOTAL" -lt 5 ]; then
73
+ echo "REPO_MODE=unknown"
74
+ exit 0
75
+ fi
76
+
77
+ TOP_PCT=$(( TOP * 100 / TOTAL ))
78
+
79
+ # Solo: top author >= 80% of commits (occasional outside PRs don't change mode)
80
+ if [ "$TOP_PCT" -ge 80 ]; then
81
+ MODE=solo
82
+ else
83
+ MODE=collaborative
84
+ fi
85
+
86
+ # Cache result atomically (fail silently if ~/.gstack is unwritable)
87
+ mkdir -p "$CACHE_DIR" 2>/dev/null || true
88
+ CACHE_TMP=$(mktemp "$CACHE_DIR/.repo-mode-XXXXXX" 2>/dev/null || true)
89
+ if [ -n "$CACHE_TMP" ]; then
90
+ echo "{\"mode\":\"$MODE\",\"top_pct\":$TOP_PCT,\"authors\":$AUTHORS,\"total\":$TOTAL,\"computed\":\"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$CACHE_TMP" 2>/dev/null && mv "$CACHE_TMP" "$CACHE_FILE" 2>/dev/null || rm -f "$CACHE_TMP" 2>/dev/null
91
+ fi
92
+
93
+ echo "REPO_MODE=$MODE"
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ # gstack-review-log — atomically log a review result
3
+ # Usage: gstack-review-log '{"skill":"...","timestamp":"...","status":"..."}'
4
+ set -euo pipefail
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source <("$SCRIPT_DIR/gstack-slug" 2>/dev/null)
7
+ GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
8
+ mkdir -p "$GSTACK_HOME/projects/$SLUG"
9
+ echo "$1" >> "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl"
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env bash
2
+ # gstack-review-read — read review log and config for dashboard
3
+ # Usage: gstack-review-read
4
+ set -euo pipefail
5
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
6
+ source <("$SCRIPT_DIR/gstack-slug" 2>/dev/null)
7
+ GSTACK_HOME="${GSTACK_HOME:-$HOME/.gstack}"
8
+ cat "$GSTACK_HOME/projects/$SLUG/$BRANCH-reviews.jsonl" 2>/dev/null || echo "NO_REVIEWS"
9
+ echo "---CONFIG---"
10
+ "$SCRIPT_DIR/gstack-config" get skip_eng_review 2>/dev/null || echo "false"
11
+ echo "---HEAD---"
12
+ git rev-parse --short HEAD 2>/dev/null || echo "unknown"
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env bash
2
+ # gstack-slug — output project slug and sanitized branch name
3
+ # Usage: source <(gstack-slug) → sets SLUG and BRANCH variables
4
+ # Or: gstack-slug → prints SLUG=... and BRANCH=... lines
5
+ #
6
+ # Security: output is sanitized to [a-zA-Z0-9._-] only, preventing
7
+ # shell injection when consumed via source or eval.
8
+ set -euo pipefail
9
+ RAW_SLUG=$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-')
10
+ RAW_BRANCH=$(git rev-parse --abbrev-ref HEAD 2>/dev/null | tr '/' '-')
11
+ # Strip any characters that aren't alphanumeric, dot, hyphen, or underscore
12
+ SLUG=$(printf '%s' "$RAW_SLUG" | tr -cd 'a-zA-Z0-9._-')
13
+ BRANCH=$(printf '%s' "$RAW_BRANCH" | tr -cd 'a-zA-Z0-9._-')
14
+ echo "SLUG=$SLUG"
15
+ echo "BRANCH=$BRANCH"
@@ -0,0 +1,158 @@
1
+ #!/usr/bin/env bash
2
+ # gstack-telemetry-log — append a telemetry event to local JSONL
3
+ #
4
+ # Data flow:
5
+ # preamble (start) ──▶ .pending marker
6
+ # preamble (epilogue) ──▶ gstack-telemetry-log ──▶ skill-usage.jsonl
7
+ # └──▶ gstack-telemetry-sync (bg)
8
+ #
9
+ # Usage:
10
+ # gstack-telemetry-log --skill qa --duration 142 --outcome success \
11
+ # --used-browse true --session-id "12345-1710756600"
12
+ #
13
+ # Env overrides (for testing):
14
+ # GSTACK_STATE_DIR — override ~/.gstack state directory
15
+ # GSTACK_DIR — override auto-detected gstack root
16
+ #
17
+ # NOTE: Uses set -uo pipefail (no -e) — telemetry must never exit non-zero
18
+ set -uo pipefail
19
+
20
+ GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
21
+ STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
22
+ ANALYTICS_DIR="$STATE_DIR/analytics"
23
+ JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl"
24
+ PENDING_DIR="$ANALYTICS_DIR" # .pending-* files live here
25
+ CONFIG_CMD="$GSTACK_DIR/bin/gstack-config"
26
+ VERSION_FILE="$GSTACK_DIR/VERSION"
27
+
28
+ # ─── Parse flags ─────────────────────────────────────────────
29
+ SKILL=""
30
+ DURATION=""
31
+ OUTCOME="unknown"
32
+ USED_BROWSE="false"
33
+ SESSION_ID=""
34
+ ERROR_CLASS=""
35
+ EVENT_TYPE="skill_run"
36
+
37
+ while [ $# -gt 0 ]; do
38
+ case "$1" in
39
+ --skill) SKILL="$2"; shift 2 ;;
40
+ --duration) DURATION="$2"; shift 2 ;;
41
+ --outcome) OUTCOME="$2"; shift 2 ;;
42
+ --used-browse) USED_BROWSE="$2"; shift 2 ;;
43
+ --session-id) SESSION_ID="$2"; shift 2 ;;
44
+ --error-class) ERROR_CLASS="$2"; shift 2 ;;
45
+ --event-type) EVENT_TYPE="$2"; shift 2 ;;
46
+ *) shift ;;
47
+ esac
48
+ done
49
+
50
+ # ─── Read telemetry tier ─────────────────────────────────────
51
+ TIER="$("$CONFIG_CMD" get telemetry 2>/dev/null || true)"
52
+ TIER="${TIER:-off}"
53
+
54
+ # Validate tier
55
+ case "$TIER" in
56
+ off|anonymous|community) ;;
57
+ *) TIER="off" ;; # invalid value → default to off
58
+ esac
59
+
60
+ if [ "$TIER" = "off" ]; then
61
+ # Still clear pending markers for this session even if telemetry is off
62
+ [ -n "$SESSION_ID" ] && rm -f "$PENDING_DIR/.pending-$SESSION_ID" 2>/dev/null || true
63
+ exit 0
64
+ fi
65
+
66
+ # ─── Finalize stale .pending markers ────────────────────────
67
+ # Each session gets its own .pending-$SESSION_ID file to avoid races
68
+ # between concurrent sessions. Finalize any that don't match our session.
69
+ for PFILE in "$PENDING_DIR"/.pending-*; do
70
+ [ -f "$PFILE" ] || continue
71
+ # Skip our own session's marker (it's still in-flight)
72
+ PFILE_BASE="$(basename "$PFILE")"
73
+ PFILE_SID="${PFILE_BASE#.pending-}"
74
+ [ "$PFILE_SID" = "$SESSION_ID" ] && continue
75
+
76
+ PENDING_DATA="$(cat "$PFILE" 2>/dev/null || true)"
77
+ rm -f "$PFILE" 2>/dev/null || true
78
+ if [ -n "$PENDING_DATA" ]; then
79
+ # Extract fields from pending marker using grep -o + awk
80
+ P_SKILL="$(echo "$PENDING_DATA" | grep -o '"skill":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
81
+ P_TS="$(echo "$PENDING_DATA" | grep -o '"ts":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
82
+ P_SID="$(echo "$PENDING_DATA" | grep -o '"session_id":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
83
+ P_VER="$(echo "$PENDING_DATA" | grep -o '"gstack_version":"[^"]*"' | head -1 | awk -F'"' '{print $4}')"
84
+ P_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
85
+ P_ARCH="$(uname -m)"
86
+
87
+ # Write the stale event as outcome: unknown
88
+ mkdir -p "$ANALYTICS_DIR"
89
+ printf '{"v":1,"ts":"%s","event_type":"skill_run","skill":"%s","session_id":"%s","gstack_version":"%s","os":"%s","arch":"%s","duration_s":null,"outcome":"unknown","error_class":null,"used_browse":false,"sessions":1}\n' \
90
+ "$P_TS" "$P_SKILL" "$P_SID" "$P_VER" "$P_OS" "$P_ARCH" >> "$JSONL_FILE" 2>/dev/null || true
91
+ fi
92
+ done
93
+
94
+ # Clear our own session's pending marker (we're about to log the real event)
95
+ [ -n "$SESSION_ID" ] && rm -f "$PENDING_DIR/.pending-$SESSION_ID" 2>/dev/null || true
96
+
97
+ # ─── Collect metadata ────────────────────────────────────────
98
+ TS="$(date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || date -u +%Y-%m-%dT%H:%M:%S 2>/dev/null || echo "")"
99
+ GSTACK_VERSION="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]' || echo "unknown")"
100
+ OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
101
+ ARCH="$(uname -m)"
102
+ SESSIONS="1"
103
+ if [ -d "$STATE_DIR/sessions" ]; then
104
+ _SC="$(find "$STATE_DIR/sessions" -mmin -120 -type f 2>/dev/null | wc -l | tr -d ' \n\r\t')"
105
+ [ -n "$_SC" ] && [ "$_SC" -gt 0 ] 2>/dev/null && SESSIONS="$_SC"
106
+ fi
107
+
108
+ # Generate installation_id for community tier
109
+ INSTALL_ID=""
110
+ if [ "$TIER" = "community" ]; then
111
+ HOST="$(hostname 2>/dev/null || echo "unknown")"
112
+ USER="$(whoami 2>/dev/null || echo "unknown")"
113
+ if command -v shasum >/dev/null 2>&1; then
114
+ INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | shasum -a 256 | awk '{print $1}')"
115
+ elif command -v sha256sum >/dev/null 2>&1; then
116
+ INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | sha256sum | awk '{print $1}')"
117
+ elif command -v openssl >/dev/null 2>&1; then
118
+ INSTALL_ID="$(printf '%s-%s' "$HOST" "$USER" | openssl dgst -sha256 | awk '{print $NF}')"
119
+ fi
120
+ # If no SHA-256 command available, install_id stays empty
121
+ fi
122
+
123
+ # Local-only fields (never sent remotely)
124
+ REPO_SLUG=""
125
+ BRANCH=""
126
+ if command -v git >/dev/null 2>&1; then
127
+ REPO_SLUG="$(git remote get-url origin 2>/dev/null | sed 's|.*[:/]\([^/]*/[^/]*\)\.git$|\1|;s|.*[:/]\([^/]*/[^/]*\)$|\1|' | tr '/' '-' 2>/dev/null || true)"
128
+ BRANCH="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)"
129
+ fi
130
+
131
+ # ─── Construct and append JSON ───────────────────────────────
132
+ mkdir -p "$ANALYTICS_DIR"
133
+
134
+ # Escape null fields
135
+ ERR_FIELD="null"
136
+ [ -n "$ERROR_CLASS" ] && ERR_FIELD="\"$ERROR_CLASS\""
137
+
138
+ DUR_FIELD="null"
139
+ [ -n "$DURATION" ] && DUR_FIELD="$DURATION"
140
+
141
+ INSTALL_FIELD="null"
142
+ [ -n "$INSTALL_ID" ] && INSTALL_FIELD="\"$INSTALL_ID\""
143
+
144
+ BROWSE_BOOL="false"
145
+ [ "$USED_BROWSE" = "true" ] && BROWSE_BOOL="true"
146
+
147
+ printf '{"v":1,"ts":"%s","event_type":"%s","skill":"%s","session_id":"%s","gstack_version":"%s","os":"%s","arch":"%s","duration_s":%s,"outcome":"%s","error_class":%s,"used_browse":%s,"sessions":%s,"installation_id":%s,"_repo_slug":"%s","_branch":"%s"}\n' \
148
+ "$TS" "$EVENT_TYPE" "$SKILL" "$SESSION_ID" "$GSTACK_VERSION" "$OS" "$ARCH" \
149
+ "$DUR_FIELD" "$OUTCOME" "$ERR_FIELD" "$BROWSE_BOOL" "${SESSIONS:-1}" \
150
+ "$INSTALL_FIELD" "$REPO_SLUG" "$BRANCH" >> "$JSONL_FILE" 2>/dev/null || true
151
+
152
+ # ─── Trigger sync if tier is not off ─────────────────────────
153
+ SYNC_CMD="$GSTACK_DIR/bin/gstack-telemetry-sync"
154
+ if [ -x "$SYNC_CMD" ]; then
155
+ "$SYNC_CMD" 2>/dev/null &
156
+ fi
157
+
158
+ exit 0
@@ -0,0 +1,127 @@
1
+ #!/usr/bin/env bash
2
+ # gstack-telemetry-sync — sync local JSONL events to Supabase
3
+ #
4
+ # Fire-and-forget, backgrounded, rate-limited to once per 5 minutes.
5
+ # Strips local-only fields before sending. Respects privacy tiers.
6
+ #
7
+ # Env overrides (for testing):
8
+ # GSTACK_STATE_DIR — override ~/.gstack state directory
9
+ # GSTACK_DIR — override auto-detected gstack root
10
+ # GSTACK_TELEMETRY_ENDPOINT — override Supabase endpoint URL
11
+ set -uo pipefail
12
+
13
+ GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
14
+ STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
15
+ ANALYTICS_DIR="$STATE_DIR/analytics"
16
+ JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl"
17
+ CURSOR_FILE="$ANALYTICS_DIR/.last-sync-line"
18
+ RATE_FILE="$ANALYTICS_DIR/.last-sync-time"
19
+ CONFIG_CMD="$GSTACK_DIR/bin/gstack-config"
20
+
21
+ # Source Supabase config if not overridden by env
22
+ if [ -z "${GSTACK_TELEMETRY_ENDPOINT:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
23
+ . "$GSTACK_DIR/supabase/config.sh"
24
+ fi
25
+ ENDPOINT="${GSTACK_TELEMETRY_ENDPOINT:-}"
26
+ ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
27
+
28
+ # ─── Pre-checks ──────────────────────────────────────────────
29
+ # No endpoint configured yet → exit silently
30
+ [ -z "$ENDPOINT" ] && exit 0
31
+
32
+ # No JSONL file → nothing to sync
33
+ [ -f "$JSONL_FILE" ] || exit 0
34
+
35
+ # Rate limit: once per 5 minutes
36
+ if [ -f "$RATE_FILE" ]; then
37
+ STALE=$(find "$RATE_FILE" -mmin +5 2>/dev/null || true)
38
+ [ -z "$STALE" ] && exit 0
39
+ fi
40
+
41
+ # ─── Read tier ───────────────────────────────────────────────
42
+ TIER="$("$CONFIG_CMD" get telemetry 2>/dev/null || true)"
43
+ TIER="${TIER:-off}"
44
+ [ "$TIER" = "off" ] && exit 0
45
+
46
+ # ─── Read cursor ─────────────────────────────────────────────
47
+ CURSOR=0
48
+ if [ -f "$CURSOR_FILE" ]; then
49
+ CURSOR="$(cat "$CURSOR_FILE" 2>/dev/null | tr -d ' \n\r\t')"
50
+ # Validate: must be a non-negative integer
51
+ case "$CURSOR" in *[!0-9]*) CURSOR=0 ;; esac
52
+ fi
53
+
54
+ # Safety: if cursor exceeds file length, reset
55
+ TOTAL_LINES="$(wc -l < "$JSONL_FILE" | tr -d ' \n\r\t')"
56
+ if [ "$CURSOR" -gt "$TOTAL_LINES" ] 2>/dev/null; then
57
+ CURSOR=0
58
+ fi
59
+
60
+ # Nothing new to sync
61
+ [ "$CURSOR" -ge "$TOTAL_LINES" ] 2>/dev/null && exit 0
62
+
63
+ # ─── Read unsent lines ───────────────────────────────────────
64
+ SKIP=$(( CURSOR + 1 ))
65
+ UNSENT="$(tail -n "+$SKIP" "$JSONL_FILE" 2>/dev/null || true)"
66
+ [ -z "$UNSENT" ] && exit 0
67
+
68
+ # ─── Strip local-only fields and build batch ─────────────────
69
+ BATCH="["
70
+ FIRST=true
71
+ COUNT=0
72
+
73
+ while IFS= read -r LINE; do
74
+ # Skip empty or malformed lines
75
+ [ -z "$LINE" ] && continue
76
+ echo "$LINE" | grep -q '^{' || continue
77
+
78
+ # Strip local-only fields + map JSONL field names to Postgres column names
79
+ CLEAN="$(echo "$LINE" | sed \
80
+ -e 's/,"_repo_slug":"[^"]*"//g' \
81
+ -e 's/,"_branch":"[^"]*"//g' \
82
+ -e 's/"v":/"schema_version":/g' \
83
+ -e 's/"ts":/"event_timestamp":/g' \
84
+ -e 's/"sessions":/"concurrent_sessions":/g' \
85
+ -e 's/,"repo":"[^"]*"//g')"
86
+
87
+ # If anonymous tier, strip installation_id
88
+ if [ "$TIER" = "anonymous" ]; then
89
+ CLEAN="$(echo "$CLEAN" | sed 's/,"installation_id":"[^"]*"//g; s/,"installation_id":null//g')"
90
+ fi
91
+
92
+ if [ "$FIRST" = "true" ]; then
93
+ FIRST=false
94
+ else
95
+ BATCH="$BATCH,"
96
+ fi
97
+ BATCH="$BATCH$CLEAN"
98
+ COUNT=$(( COUNT + 1 ))
99
+
100
+ # Batch size limit
101
+ [ "$COUNT" -ge 100 ] && break
102
+ done <<< "$UNSENT"
103
+
104
+ BATCH="$BATCH]"
105
+
106
+ # Nothing to send after filtering
107
+ [ "$COUNT" -eq 0 ] && exit 0
108
+
109
+ # ─── POST to Supabase ────────────────────────────────────────
110
+ HTTP_CODE="$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 \
111
+ -X POST "${ENDPOINT}/telemetry_events" \
112
+ -H "Content-Type: application/json" \
113
+ -H "apikey: ${ANON_KEY}" \
114
+ -H "Authorization: Bearer ${ANON_KEY}" \
115
+ -H "Prefer: return=minimal" \
116
+ -d "$BATCH" 2>/dev/null || echo "000")"
117
+
118
+ # ─── Update cursor on success (2xx) ─────────────────────────
119
+ case "$HTTP_CODE" in
120
+ 2*) NEW_CURSOR=$(( CURSOR + COUNT ))
121
+ echo "$NEW_CURSOR" > "$CURSOR_FILE" 2>/dev/null || true ;;
122
+ esac
123
+
124
+ # Update rate limit marker
125
+ touch "$RATE_FILE" 2>/dev/null || true
126
+
127
+ exit 0
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env bash
2
+ # gstack-update-check — periodic version check for all skills.
3
+ #
4
+ # Output (one line, or nothing):
5
+ # JUST_UPGRADED <old> <new> — marker found from recent upgrade
6
+ # UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
7
+ # (nothing) — up to date, snoozed, disabled, or check skipped
8
+ #
9
+ # Env overrides (for testing):
10
+ # GSTACK_DIR — override auto-detected gstack root
11
+ # GSTACK_REMOTE_URL — override remote VERSION URL
12
+ # GSTACK_STATE_DIR — override ~/.gstack state directory
13
+ set -euo pipefail
14
+
15
+ GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
16
+ STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
17
+ CACHE_FILE="$STATE_DIR/last-update-check"
18
+ MARKER_FILE="$STATE_DIR/just-upgraded-from"
19
+ SNOOZE_FILE="$STATE_DIR/update-snoozed"
20
+ VERSION_FILE="$GSTACK_DIR/VERSION"
21
+ REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
22
+
23
+ # ─── Force flag (busts cache for standalone /gstack-upgrade) ──
24
+ if [ "${1:-}" = "--force" ]; then
25
+ rm -f "$CACHE_FILE"
26
+ fi
27
+
28
+ # ─── Step 0: Check if updates are disabled ────────────────────
29
+ _UC=$("$GSTACK_DIR/bin/gstack-config" get update_check 2>/dev/null || true)
30
+ if [ "$_UC" = "false" ]; then
31
+ exit 0
32
+ fi
33
+
34
+ # ─── Snooze helper ──────────────────────────────────────────
35
+ # check_snooze <remote_version>
36
+ # Returns 0 if snoozed (should stay quiet), 1 if not snoozed (should output).
37
+ #
38
+ # Snooze file format: <version> <level> <epoch>
39
+ # Level durations: 1=24h, 2=48h, 3+=7d
40
+ # New version (version mismatch) resets snooze.
41
+ check_snooze() {
42
+ local remote_ver="$1"
43
+ if [ ! -f "$SNOOZE_FILE" ]; then
44
+ return 1 # no snooze file → not snoozed
45
+ fi
46
+ local snoozed_ver snoozed_level snoozed_epoch
47
+ snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
48
+ snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
49
+ snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
50
+
51
+ # Validate: all three fields must be non-empty
52
+ if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then
53
+ return 1 # corrupt file → not snoozed
54
+ fi
55
+
56
+ # Validate: level and epoch must be integers
57
+ case "$snoozed_level" in *[!0-9]*) return 1 ;; esac
58
+ case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac
59
+
60
+ # New version dropped? Ignore snooze.
61
+ if [ "$snoozed_ver" != "$remote_ver" ]; then
62
+ return 1
63
+ fi
64
+
65
+ # Compute snooze duration based on level
66
+ local duration
67
+ case "$snoozed_level" in
68
+ 1) duration=86400 ;; # 24 hours
69
+ 2) duration=172800 ;; # 48 hours
70
+ *) duration=604800 ;; # 7 days (level 3+)
71
+ esac
72
+
73
+ local now
74
+ now="$(date +%s)"
75
+ local expires=$(( snoozed_epoch + duration ))
76
+ if [ "$now" -lt "$expires" ]; then
77
+ return 0 # still snoozed
78
+ fi
79
+
80
+ return 1 # snooze expired
81
+ }
82
+
83
+ # ─── Step 1: Read local version ──────────────────────────────
84
+ LOCAL=""
85
+ if [ -f "$VERSION_FILE" ]; then
86
+ LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
87
+ fi
88
+ if [ -z "$LOCAL" ]; then
89
+ exit 0 # No VERSION file → skip check
90
+ fi
91
+
92
+ # ─── Step 2: Check "just upgraded" marker ─────────────────────
93
+ if [ -f "$MARKER_FILE" ]; then
94
+ OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
95
+ rm -f "$MARKER_FILE"
96
+ rm -f "$SNOOZE_FILE"
97
+ mkdir -p "$STATE_DIR"
98
+ echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
99
+ if [ -n "$OLD" ]; then
100
+ echo "JUST_UPGRADED $OLD $LOCAL"
101
+ fi
102
+ exit 0
103
+ fi
104
+
105
+ # ─── Step 3: Check cache freshness ──────────────────────────
106
+ # UP_TO_DATE: 60 min TTL (detect new releases quickly)
107
+ # UPGRADE_AVAILABLE: 720 min TTL (keep nagging)
108
+ if [ -f "$CACHE_FILE" ]; then
109
+ CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
110
+ case "$CACHED" in
111
+ UP_TO_DATE*) CACHE_TTL=60 ;;
112
+ UPGRADE_AVAILABLE*) CACHE_TTL=720 ;;
113
+ *) CACHE_TTL=0 ;; # corrupt → force re-fetch
114
+ esac
115
+
116
+ STALE=$(find "$CACHE_FILE" -mmin +$CACHE_TTL 2>/dev/null || true)
117
+ if [ -z "$STALE" ] && [ "$CACHE_TTL" -gt 0 ]; then
118
+ case "$CACHED" in
119
+ UP_TO_DATE*)
120
+ CACHED_VER="$(echo "$CACHED" | awk '{print $2}')"
121
+ if [ "$CACHED_VER" = "$LOCAL" ]; then
122
+ exit 0
123
+ fi
124
+ ;;
125
+ UPGRADE_AVAILABLE*)
126
+ CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
127
+ if [ "$CACHED_OLD" = "$LOCAL" ]; then
128
+ CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')"
129
+ if check_snooze "$CACHED_NEW"; then
130
+ exit 0 # snoozed — stay quiet
131
+ fi
132
+ echo "$CACHED"
133
+ exit 0
134
+ fi
135
+ ;;
136
+ esac
137
+ fi
138
+ fi
139
+
140
+ # ─── Step 4: Slow path — fetch remote version ────────────────
141
+ mkdir -p "$STATE_DIR"
142
+
143
+ # Fire Supabase install ping in background (parallel, non-blocking)
144
+ # This logs an update check event for community health metrics.
145
+ # If the endpoint isn't configured or Supabase is down, this is a no-op.
146
+ # Source Supabase config for install ping
147
+ if [ -z "${GSTACK_TELEMETRY_ENDPOINT:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
148
+ . "$GSTACK_DIR/supabase/config.sh"
149
+ fi
150
+ _SUPA_ENDPOINT="${GSTACK_TELEMETRY_ENDPOINT:-}"
151
+ _SUPA_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
152
+ # Respect telemetry opt-out — don't ping Supabase if user set telemetry: off
153
+ _TEL_TIER="$("$GSTACK_DIR/bin/gstack-config" get telemetry 2>/dev/null || true)"
154
+ if [ -n "$_SUPA_ENDPOINT" ] && [ -n "$_SUPA_KEY" ] && [ "${_TEL_TIER:-off}" != "off" ]; then
155
+ _OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
156
+ curl -sf --max-time 5 \
157
+ -X POST "${_SUPA_ENDPOINT}/update_checks" \
158
+ -H "Content-Type: application/json" \
159
+ -H "apikey: ${_SUPA_KEY}" \
160
+ -H "Authorization: Bearer ${_SUPA_KEY}" \
161
+ -H "Prefer: return=minimal" \
162
+ -d "{\"gstack_version\":\"$LOCAL\",\"os\":\"$_OS\"}" \
163
+ >/dev/null 2>&1 &
164
+ fi
165
+
166
+ # GitHub raw fetch (primary, always reliable)
167
+ REMOTE=""
168
+ REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
169
+ REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
170
+
171
+ # Validate: must look like a version number (reject HTML error pages)
172
+ if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then
173
+ # Invalid or empty response — assume up to date
174
+ echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
175
+ exit 0
176
+ fi
177
+
178
+ if [ "$LOCAL" = "$REMOTE" ]; then
179
+ echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
180
+ exit 0
181
+ fi
182
+
183
+ # Versions differ — upgrade available
184
+ echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
185
+ if check_snooze "$REMOTE"; then
186
+ exit 0 # snoozed — stay quiet
187
+ fi
188
+
189
+ # Log upgrade_prompted event (only on slow-path fetch, not cached replays)
190
+ TEL_CMD="$GSTACK_DIR/bin/gstack-telemetry-log"
191
+ if [ -x "$TEL_CMD" ]; then
192
+ "$TEL_CMD" --event-type upgrade_prompted --skill "" --duration 0 \
193
+ --outcome success --session-id "update-$$-$(date +%s)" 2>/dev/null &
194
+ fi
195
+
196
+ echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"