@m13v/s4l 1.6.197-rc.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 (326) hide show
  1. package/README.md +143 -0
  2. package/SKILL.md +342 -0
  3. package/bin/cli.js +980 -0
  4. package/bin/cookie-helper.js +315 -0
  5. package/bin/platform.js +59 -0
  6. package/bin/scheduler/index.js +12 -0
  7. package/bin/scheduler/launchd.js +518 -0
  8. package/browser-agent-configs/all-agents-mcp.json +68 -0
  9. package/browser-agent-configs/linkedin-agent-mcp.json +16 -0
  10. package/browser-agent-configs/linkedin-agent.json +17 -0
  11. package/browser-agent-configs/linkedin-harness-mcp.json +21 -0
  12. package/browser-agent-configs/reddit-agent-mcp.json +16 -0
  13. package/browser-agent-configs/reddit-agent.json +17 -0
  14. package/browser-agent-configs/twitter-harness-mcp.json +18 -0
  15. package/config.example.json +45 -0
  16. package/mcp/dist/index.js +4212 -0
  17. package/mcp/dist/onboarding.js +200 -0
  18. package/mcp/dist/panel.html +176 -0
  19. package/mcp/dist/product-link.html +102 -0
  20. package/mcp/dist/repo.js +222 -0
  21. package/mcp/dist/runtime.js +1079 -0
  22. package/mcp/dist/screencast.js +323 -0
  23. package/mcp/dist/setup.js +545 -0
  24. package/mcp/dist/telemetry.js +306 -0
  25. package/mcp/dist/twitterAuth.js +138 -0
  26. package/mcp/dist/version.js +271 -0
  27. package/mcp/dist/version.json +4 -0
  28. package/mcp/install-runtime.mjs +70 -0
  29. package/mcp/install.mjs +169 -0
  30. package/mcp/manifest.json +80 -0
  31. package/mcp/menubar/dashboard_server.py +213 -0
  32. package/mcp/menubar/s4l_card.py +1314 -0
  33. package/mcp/menubar/s4l_log_relay.py +179 -0
  34. package/mcp/menubar/s4l_menubar.py +2439 -0
  35. package/mcp/menubar/s4l_state.py +891 -0
  36. package/mcp/package.json +34 -0
  37. package/mcp/shared/doctor.cjs +437 -0
  38. package/mcp/shared/onboarding-ledger.cjs +324 -0
  39. package/mcp-servers/browser-harness/server.py +968 -0
  40. package/package.json +160 -0
  41. package/requirements.txt +20 -0
  42. package/scripts/_compute_allowlist.py +58 -0
  43. package/scripts/_db_update.py +20 -0
  44. package/scripts/_filt.py +9 -0
  45. package/scripts/_li_notif_match.py +76 -0
  46. package/scripts/_li_notif_orchestrate.py +126 -0
  47. package/scripts/_lock_preempt_test.py +60 -0
  48. package/scripts/_run_icp_precheck.py +57 -0
  49. package/scripts/a16z_pearx_calendar_reminders.py +99 -0
  50. package/scripts/account_resolver.py +141 -0
  51. package/scripts/active_campaigns.py +114 -0
  52. package/scripts/active_users.py +190 -0
  53. package/scripts/amplitude_24h_signups.py +468 -0
  54. package/scripts/amplitude_signups.py +177 -0
  55. package/scripts/apply_onboarding_selections.py +131 -0
  56. package/scripts/audience_pages.py +243 -0
  57. package/scripts/audit_helper.py +120 -0
  58. package/scripts/author_history_block.py +353 -0
  59. package/scripts/autopilot_stall_watch.py +284 -0
  60. package/scripts/backfill_twitter_attempts_topic.py +81 -0
  61. package/scripts/backfill_twitter_log_post_no_id.py +322 -0
  62. package/scripts/bench_dashboard.sh +138 -0
  63. package/scripts/bh_send.py +39 -0
  64. package/scripts/build_persona.py +409 -0
  65. package/scripts/bulk_icp.py +18 -0
  66. package/scripts/campaign_bump.py +51 -0
  67. package/scripts/capture_thread_media.py +288 -0
  68. package/scripts/check_browser_lock_health.sh +81 -0
  69. package/scripts/check_external_pool_depth.py +253 -0
  70. package/scripts/check_unread_web_chats.py +28 -0
  71. package/scripts/claim_web_chat.py +47 -0
  72. package/scripts/classify_run_error.py +158 -0
  73. package/scripts/claude_job.py +988 -0
  74. package/scripts/clean_stale_singleton.sh +56 -0
  75. package/scripts/cleanup_harness_tabs.py +68 -0
  76. package/scripts/copy_browser_cookies.py +454 -0
  77. package/scripts/counterparty_history.py +350 -0
  78. package/scripts/db.py +57 -0
  79. package/scripts/discover_claude_profiles.py +120 -0
  80. package/scripts/discover_linkedin_candidates.py +984 -0
  81. package/scripts/dm_conversation.py +682 -0
  82. package/scripts/dm_db_update.py +69 -0
  83. package/scripts/dm_engage_helper.py +161 -0
  84. package/scripts/dm_outreach_helper.py +147 -0
  85. package/scripts/dm_outreach_twitter_helper.py +129 -0
  86. package/scripts/dm_send_log.py +106 -0
  87. package/scripts/dm_short_links.py +1084 -0
  88. package/scripts/dump_web_chat_history.py +47 -0
  89. package/scripts/engage_github.py +640 -0
  90. package/scripts/engage_reddit.py +1235 -0
  91. package/scripts/engage_twitter_helper.py +301 -0
  92. package/scripts/engagement_styles.py +1787 -0
  93. package/scripts/enrich_twitter_candidates.py +82 -0
  94. package/scripts/feedback_digest.py +448 -0
  95. package/scripts/fetch_prospect_profile.py +312 -0
  96. package/scripts/fetch_twitter_t1.py +134 -0
  97. package/scripts/find_threads.py +530 -0
  98. package/scripts/follow_gate_log.py +59 -0
  99. package/scripts/funnel_per_day.py +194 -0
  100. package/scripts/generate_daily_human_style.py +494 -0
  101. package/scripts/generation_trace.py +173 -0
  102. package/scripts/get_run_cost.py +107 -0
  103. package/scripts/github_engage_helper.py +93 -0
  104. package/scripts/github_tools.py +509 -0
  105. package/scripts/harness_overlay.py +556 -0
  106. package/scripts/harvest_twitter_following.py +243 -0
  107. package/scripts/heartbeat.sh +70 -0
  108. package/scripts/history_context.py +284 -0
  109. package/scripts/http_api.py +206 -0
  110. package/scripts/human_dm_replies_helper.py +169 -0
  111. package/scripts/identity.py +302 -0
  112. package/scripts/ig_batch_creator.sh +93 -0
  113. package/scripts/ig_post_type_picker.py +243 -0
  114. package/scripts/ig_scrape_transcribe.sh +91 -0
  115. package/scripts/ingest_human_dm_replies.py +271 -0
  116. package/scripts/ingest_web_chat_replies.py +229 -0
  117. package/scripts/install_fleet.py +187 -0
  118. package/scripts/invent_mcp_server.py +350 -0
  119. package/scripts/invent_topics.py +1462 -0
  120. package/scripts/learned_preferences.py +263 -0
  121. package/scripts/li_discovery.py +161 -0
  122. package/scripts/link_edit_helper.py +142 -0
  123. package/scripts/link_tail.py +592 -0
  124. package/scripts/linkedin_api.py +561 -0
  125. package/scripts/linkedin_browser.py +730 -0
  126. package/scripts/linkedin_cooldown.py +128 -0
  127. package/scripts/linkedin_exclusions.py +234 -0
  128. package/scripts/linkedin_killswitch.py +1333 -0
  129. package/scripts/linkedin_search_topic_schema.py +49 -0
  130. package/scripts/linkedin_unipile.py +658 -0
  131. package/scripts/linkedin_url.py +228 -0
  132. package/scripts/log_claude_session.py +636 -0
  133. package/scripts/log_draft.py +143 -0
  134. package/scripts/log_linkedin_search_attempts.py +126 -0
  135. package/scripts/log_post.py +651 -0
  136. package/scripts/log_run.py +364 -0
  137. package/scripts/log_thread_media.py +108 -0
  138. package/scripts/log_twitter_search_attempts.py +150 -0
  139. package/scripts/log_twitter_skips.py +211 -0
  140. package/scripts/lookup_post.py +78 -0
  141. package/scripts/mark_web_chat_processed.py +32 -0
  142. package/scripts/mcp_lock_proxy.py +370 -0
  143. package/scripts/memory_snapshot.py +972 -0
  144. package/scripts/merge_review_queue.py +215 -0
  145. package/scripts/mint_external_pool.py +182 -0
  146. package/scripts/mint_kent_pool.py +249 -0
  147. package/scripts/moltbook_post.py +320 -0
  148. package/scripts/moltbook_tools.py +159 -0
  149. package/scripts/pending_threads.py +188 -0
  150. package/scripts/pick_ig_account.py +177 -0
  151. package/scripts/pick_project.py +208 -0
  152. package/scripts/pick_search_topic.py +771 -0
  153. package/scripts/pick_thread_target.py +279 -0
  154. package/scripts/pick_twitter_thread_target.py +202 -0
  155. package/scripts/podlog_fetch_batch.sh +32 -0
  156. package/scripts/post_github.py +1311 -0
  157. package/scripts/post_reddit.py +2668 -0
  158. package/scripts/precompute_dashboard_stats.py +204 -0
  159. package/scripts/preflight.sh +297 -0
  160. package/scripts/progress.py +88 -0
  161. package/scripts/project_excludes.py +353 -0
  162. package/scripts/project_slugs.py +91 -0
  163. package/scripts/project_stats.py +241 -0
  164. package/scripts/project_stats_json.py +1563 -0
  165. package/scripts/project_topics.py +192 -0
  166. package/scripts/qualified_query_bank.py +436 -0
  167. package/scripts/reap_stale_claude_sessions.py +867 -0
  168. package/scripts/reddit_browser.py +2549 -0
  169. package/scripts/reddit_browser_fetch.py +141 -0
  170. package/scripts/reddit_browser_lock.py +593 -0
  171. package/scripts/reddit_chat_sync.py +710 -0
  172. package/scripts/reddit_query_bank.py +200 -0
  173. package/scripts/reddit_threads_helper.py +151 -0
  174. package/scripts/reddit_tools.py +956 -0
  175. package/scripts/refresh_instagram_tokens.py +280 -0
  176. package/scripts/release-mcpb.sh +497 -0
  177. package/scripts/reply_db.py +334 -0
  178. package/scripts/reply_insert.py +98 -0
  179. package/scripts/reply_risk_digest.py +761 -0
  180. package/scripts/reset-test-machine.sh +602 -0
  181. package/scripts/restore_twitter_session.py +177 -0
  182. package/scripts/ripen_reddit_plan.py +478 -0
  183. package/scripts/run_claude.sh +433 -0
  184. package/scripts/run_moltbook_cycle.py +555 -0
  185. package/scripts/s4l_box_update.sh +226 -0
  186. package/scripts/s4l_channel.py +103 -0
  187. package/scripts/s4l_ctl.sh +75 -0
  188. package/scripts/s4l_env.py +47 -0
  189. package/scripts/saps_activity.py +126 -0
  190. package/scripts/saps_mode.py +328 -0
  191. package/scripts/scan_dm_candidates.py +580 -0
  192. package/scripts/scan_github_replies.py +168 -0
  193. package/scripts/scan_instagram_comments.py +481 -0
  194. package/scripts/scan_moltbook_replies.py +252 -0
  195. package/scripts/scan_pii.py +190 -0
  196. package/scripts/scan_reddit_replies.py +377 -0
  197. package/scripts/scan_twitter_mentions_browser.py +327 -0
  198. package/scripts/scan_twitter_thread_followups.py +299 -0
  199. package/scripts/scan_x_profile.py +384 -0
  200. package/scripts/schedule_state.py +202 -0
  201. package/scripts/scheduled_tasks_snapshot.py +123 -0
  202. package/scripts/score_linkedin_candidates.py +419 -0
  203. package/scripts/score_twitter_candidates.py +718 -0
  204. package/scripts/scrape_linkedin_comment_stats.py +1755 -0
  205. package/scripts/scrape_linkedin_stats_browser.py +52 -0
  206. package/scripts/scrape_reddit_views.py +365 -0
  207. package/scripts/seed_search_queries.py +453 -0
  208. package/scripts/seed_search_topics.py +127 -0
  209. package/scripts/send_web_chat_reply.py +130 -0
  210. package/scripts/sentry_init.py +128 -0
  211. package/scripts/setup_twitter_auth.py +1320 -0
  212. package/scripts/snapshot.py +583 -0
  213. package/scripts/stats.py +2702 -0
  214. package/scripts/stats_helper.py +52 -0
  215. package/scripts/strike_alert.py +783 -0
  216. package/scripts/sweep_post_link_clicks.py +107 -0
  217. package/scripts/sync_ig_to_posts.py +147 -0
  218. package/scripts/test_browser_lock.py +189 -0
  219. package/scripts/test_installation_api.sh +52 -0
  220. package/scripts/test_percard_posting.py +142 -0
  221. package/scripts/top_dud_linkedin_queries.py +71 -0
  222. package/scripts/top_dud_reddit_queries.py +67 -0
  223. package/scripts/top_dud_twitter_queries.py +71 -0
  224. package/scripts/top_dud_twitter_topics.py +102 -0
  225. package/scripts/top_linkedin_queries.py +55 -0
  226. package/scripts/top_omitted_reddit_topics.py +91 -0
  227. package/scripts/top_performers.py +588 -0
  228. package/scripts/top_search_topics.py +180 -0
  229. package/scripts/top_twitter_queries.py +190 -0
  230. package/scripts/twitter_access_check.py +382 -0
  231. package/scripts/twitter_account.py +41 -0
  232. package/scripts/twitter_batch_phase.py +126 -0
  233. package/scripts/twitter_browser.py +2804 -0
  234. package/scripts/twitter_cookie_mirror.py +130 -0
  235. package/scripts/twitter_cycle_helper.py +310 -0
  236. package/scripts/twitter_gen_links.py +287 -0
  237. package/scripts/twitter_post_plan.py +1188 -0
  238. package/scripts/twitter_scan.py +324 -0
  239. package/scripts/twitter_supply_signal.py +57 -0
  240. package/scripts/twitter_threads_helper.py +152 -0
  241. package/scripts/unclaim_web_chat.py +29 -0
  242. package/scripts/update_instagram_stats.py +261 -0
  243. package/scripts/update_linkedin_stats_from_feed.py +328 -0
  244. package/scripts/version.py +72 -0
  245. package/scripts/watchdog_hung_runs.py +343 -0
  246. package/scripts/write_generation_trace.py +73 -0
  247. package/setup/SKILL.md +277 -0
  248. package/skill/amplitude-24h-signups.sh +38 -0
  249. package/skill/archive-old-logs.sh +40 -0
  250. package/skill/audit-dm-staleness.sh +42 -0
  251. package/skill/audit-linkedin.sh +14 -0
  252. package/skill/audit-moltbook.sh +4 -0
  253. package/skill/audit-reddit-resurrect.sh +67 -0
  254. package/skill/audit-reddit.sh +4 -0
  255. package/skill/audit-twitter.sh +4 -0
  256. package/skill/audit.sh +287 -0
  257. package/skill/backfill-twitter-attempts-topic.sh +19 -0
  258. package/skill/backfill-twitter-ghost-posts.sh +24 -0
  259. package/skill/check-external-pool-depth.sh +7 -0
  260. package/skill/check-web-chats.sh +203 -0
  261. package/skill/dm-outreach-linkedin.sh +250 -0
  262. package/skill/dm-outreach-reddit.sh +274 -0
  263. package/skill/dm-outreach-twitter.sh +265 -0
  264. package/skill/engage-dm-replies-linkedin.sh +4 -0
  265. package/skill/engage-dm-replies-reddit.sh +4 -0
  266. package/skill/engage-dm-replies-twitter.sh +4 -0
  267. package/skill/engage-dm-replies.sh +1597 -0
  268. package/skill/engage-linkedin.sh +581 -0
  269. package/skill/engage-moltbook.sh +36 -0
  270. package/skill/engage-reddit.sh +146 -0
  271. package/skill/engage-twitter.sh +467 -0
  272. package/skill/github-engage.sh +176 -0
  273. package/skill/ingest-web-chat-replies.sh +38 -0
  274. package/skill/invent-supply-test.sh +100 -0
  275. package/skill/invent-topics.sh +50 -0
  276. package/skill/lib/linkedin-backend.sh +364 -0
  277. package/skill/lib/platform.sh +48 -0
  278. package/skill/lib/reddit-backend.sh +234 -0
  279. package/skill/lib/twitter-backend.sh +314 -0
  280. package/skill/link-edit-github.sh +136 -0
  281. package/skill/link-edit-moltbook.sh +117 -0
  282. package/skill/link-edit-reddit.sh +201 -0
  283. package/skill/linkedin-presence.sh +182 -0
  284. package/skill/linkedin-recovery.sh +282 -0
  285. package/skill/lock.sh +647 -0
  286. package/skill/memory-snapshot.sh +39 -0
  287. package/skill/precompute-stats.sh +35 -0
  288. package/skill/prewarm-funnel.sh +104 -0
  289. package/skill/refresh-instagram-tokens.sh +57 -0
  290. package/skill/refresh-twitter-following.sh +52 -0
  291. package/skill/reply-risk-digest.sh +31 -0
  292. package/skill/run-cycle-update-guard.sh +44 -0
  293. package/skill/run-draft-and-publish.sh +123 -0
  294. package/skill/run-generate-daily-style.sh +50 -0
  295. package/skill/run-github-launchd.sh +62 -0
  296. package/skill/run-github.sh +102 -0
  297. package/skill/run-instagram-daily.sh +149 -0
  298. package/skill/run-instagram-render.sh +875 -0
  299. package/skill/run-linkedin-launchd.sh +81 -0
  300. package/skill/run-linkedin-unipile.sh +130 -0
  301. package/skill/run-linkedin.sh +1593 -0
  302. package/skill/run-moltbook-launchd.sh +61 -0
  303. package/skill/run-moltbook.sh +38 -0
  304. package/skill/run-overlay-watch.sh +100 -0
  305. package/skill/run-reddit-search-launchd.sh +64 -0
  306. package/skill/run-reddit-search.sh +505 -0
  307. package/skill/run-reddit-threads-double.sh +32 -0
  308. package/skill/run-reddit-threads.sh +847 -0
  309. package/skill/run-scan-moltbook-replies.sh +57 -0
  310. package/skill/run-twitter-cycle-launchd.sh +63 -0
  311. package/skill/run-twitter-cycle-singleton.sh +62 -0
  312. package/skill/run-twitter-cycle.sh +2408 -0
  313. package/skill/run-twitter-threads.sh +592 -0
  314. package/skill/scan-instagram-replies.sh +61 -0
  315. package/skill/scan-twitter-followups.sh +57 -0
  316. package/skill/social-autoposter-update.sh +66 -0
  317. package/skill/stats-instagram.sh +72 -0
  318. package/skill/stats-linkedin.sh +271 -0
  319. package/skill/stats-moltbook.sh +4 -0
  320. package/skill/stats-reddit.sh +4 -0
  321. package/skill/stats-twitter.sh +4 -0
  322. package/skill/stats.sh +521 -0
  323. package/skill/strike-alert.sh +18 -0
  324. package/skill/styles.sh +87 -0
  325. package/skill/sweep-link-clicks.sh +40 -0
  326. package/skill/topics.sh +51 -0
@@ -0,0 +1,497 @@
1
+ #!/usr/bin/env bash
2
+ # THE single release flow for social-autoposter. One command does EVERYTHING and
3
+ # keeps the npm path (Story A) and the .mcpb double-click path (Story B) on one
4
+ # version, because both derive from one bumped repo-root package.json.
5
+ #
6
+ # What it does, end to end:
7
+ # 1. Bump the repo-root package.json (the SINGLE source of truth) and lockfile.
8
+ # Default: patch bump. --bump minor|major, or pin with --version / --tag,
9
+ # or --no-bump to re-release the current version as-is.
10
+ # 2. Stamp EVERY version satellite from that one source, THEN build, so the
11
+ # embedded pipeline.tgz (an `npm pack` of the repo) captures an all-current
12
+ # mcp/ subtree. Order is load-bearing: satellites (manifest.json,
13
+ # mcp/package.json+lock, dist/version.json) are stamped BEFORE the pack, not
14
+ # after, or the tarball ships a stale mcp/ subtree (the 1.6.181-menu bug).
15
+ # Sub-steps: 2a stamp source satellites -> 2b build panel+server -> 2c stamp
16
+ # dist/version.json -> 2d pack pipeline.tgz.
17
+ # 3. (Regenerate manifest.json `tools` from the server's registrations.)
18
+ # 4. Pack mcp/ into mcp/social-autoposter.mcpb via the mcpb CLI.
19
+ # 5. Verify: size cap, embedded pipeline.tgz present, version.json + manifest +
20
+ # the pipeline.tgz's OWN internal version AND its mcp/ subtree (dist/version.json,
21
+ # mcp/package.json) all == VERSION (guards the 1.6.84 stale-pipeline and the
22
+ # 1.6.181 stale-menu bugs), install tools present.
23
+ # 6. npm publish social-autoposter@VERSION (idempotent; skipped if already live).
24
+ # 7. Create/update GitHub release vX.Y.Z and upload the .mcpb (--clobber).
25
+ #
26
+ # Boxes self-update from the GitHub release: the menu-bar updater polls
27
+ # releases/latest, pulls the new .mcpb, and on next server boot
28
+ # ensurePipelineCurrent() re-extracts pipeline.tgz. No manual box step needed.
29
+ #
30
+ # WHERE THE "⬆ Update available" BANNER COMES FROM (read this before touching the
31
+ # release/detection path): the menu-bar banner is driven by versionStatus() in
32
+ # mcp/src/version.ts, which resolves "latest" from GitHub releases/latest (via
33
+ # curl), NOT from npm. .mcpb boxes have no npm/npx on PATH, so an npm-based probe
34
+ # is permanently blind there. Consequence for releasing: the banner fires only
35
+ # after the GitHub release step (7) lands and releases/latest serves the new tag,
36
+ # NOT after npm publish (6). Step 8 below verifies that so a release can't
37
+ # "succeed" while every box stays silent on the old version.
38
+ #
39
+ # CHANNELS (2026-07-02): a box can opt into pre-release builds by setting its
40
+ # channel to `staging` (scripts/s4l_channel.py). Staging releases are GitHub
41
+ # PRE-releases with an -rc.N version, so releases/latest and npm `latest` do NOT
42
+ # move — only staging boxes pull them (they resolve the newest release from the
43
+ # releases LIST endpoint). `--promote <tag>` flips a tested pre-release to stable
44
+ # IN PLACE (no rebuild): it clears the prerelease flag + moves npm `latest`, so
45
+ # the EXACT tested artifact ships to everyone. Nothing can drift between test and
46
+ # ship because there is no repack.
47
+ #
48
+ # Usage:
49
+ # bash scripts/release-mcpb.sh # patch bump, npm + .mcpb + GitHub (STABLE)
50
+ # bash scripts/release-mcpb.sh --bump minor
51
+ # bash scripts/release-mcpb.sh --version 1.7.0 # pin an exact version
52
+ # bash scripts/release-mcpb.sh --no-bump # re-release current package.json version
53
+ # bash scripts/release-mcpb.sh --no-npm # skip npm publish (only .mcpb + GitHub)
54
+ # bash scripts/release-mcpb.sh --no-release # build + pack + verify only (no npm, no GitHub)
55
+ # bash scripts/release-mcpb.sh --draft # GitHub release as a draft
56
+ # bash scripts/release-mcpb.sh --staging # PRE-release -rc.N (staging channel only)
57
+ # bash scripts/release-mcpb.sh --promote v1.6.193-rc.2 # ship a tested pre-release to stable
58
+
59
+ set -euo pipefail
60
+
61
+ # Homebrew node/gh/mcpb are not on the default Fazm/launchd PATH.
62
+ export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"
63
+
64
+ REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
65
+ MCP_DIR="$REPO_ROOT/mcp"
66
+ BUNDLE="$MCP_DIR/social-autoposter.mcpb"
67
+ GH_REPO="m13v/s4l"
68
+ SIZE_CAP_MB=180
69
+
70
+ TAG_OVERRIDE=""
71
+ VERSION_OVERRIDE=""
72
+ DO_RELEASE=1
73
+ DO_NPM=1
74
+ DO_BUMP=1
75
+ BUMP_LEVEL="patch"
76
+ DRAFT_FLAG=""
77
+ # CHANNEL (2026-07-02): staging releases are GitHub PRE-releases carrying an
78
+ # -rc.N version, so they are invisible to releases/latest (stable boxes) and to
79
+ # npm's `latest` dist-tag — only a box on the `staging` channel pulls them. See
80
+ # scripts/s4l_channel.py. `--promote <tag>` flips a tested pre-release to stable
81
+ # IN PLACE (no rebuild, the exact tested artifact) by clearing its prerelease
82
+ # flag + moving npm `latest`.
83
+ DO_STAGING=0
84
+ PROMOTE_TAG=""
85
+
86
+ while [[ $# -gt 0 ]]; do
87
+ case "$1" in
88
+ --tag) TAG_OVERRIDE="$2"; shift 2 ;;
89
+ --tag=*) TAG_OVERRIDE="${1#*=}"; shift ;;
90
+ --version) VERSION_OVERRIDE="$2"; shift 2 ;;
91
+ --version=*) VERSION_OVERRIDE="${1#*=}"; shift ;;
92
+ --bump) BUMP_LEVEL="$2"; shift 2 ;;
93
+ --bump=*) BUMP_LEVEL="${1#*=}"; shift ;;
94
+ --no-bump) DO_BUMP=0; shift ;;
95
+ --no-npm) DO_NPM=0; shift ;;
96
+ --no-release) DO_RELEASE=0; shift ;;
97
+ --draft) DRAFT_FLAG="--draft"; shift ;;
98
+ --staging) DO_STAGING=1; shift ;;
99
+ --promote) PROMOTE_TAG="$2"; shift 2 ;;
100
+ --promote=*) PROMOTE_TAG="${1#*=}"; shift ;;
101
+ -h|--help) sed -n '2,46p' "$0"; exit 0 ;;
102
+ *) echo "unknown arg: $1" >&2; exit 2 ;;
103
+ esac
104
+ done
105
+
106
+ case "$BUMP_LEVEL" in
107
+ patch|minor|major) ;;
108
+ *) echo "invalid --bump level: $BUMP_LEVEL (want patch|minor|major)" >&2; exit 2 ;;
109
+ esac
110
+
111
+ say() { printf '\n\033[1m==> %s\033[0m\n' "$*"; }
112
+ die() { printf '\033[1mERROR:\033[0m %s\n' "$*" >&2; exit 1; }
113
+
114
+ command -v node >/dev/null || die "node not found on PATH"
115
+
116
+ # ---- 0. Promote a tested pre-release to stable (no rebuild) ------------------
117
+ # Flip the SAME artifact the staging box tested: clear GitHub's prerelease flag
118
+ # and mark it latest (so releases/latest + the stable boxes pick it up), and move
119
+ # npm's `latest` dist-tag onto it. Byte-for-byte identical to what was tested;
120
+ # there is no repack, so nothing can drift between test and ship. The version
121
+ # keeps its -rc.N label on purpose (that IS the tested build); cut a fresh stable
122
+ # patch later if you want a clean number.
123
+ if [[ -n "$PROMOTE_TAG" ]]; then
124
+ command -v gh >/dev/null || die "gh CLI not found"
125
+ gh auth status >/dev/null 2>&1 || die "gh not authenticated (run: gh auth login)"
126
+ PTAG="$PROMOTE_TAG"; [[ "$PTAG" == v* ]] || PTAG="v$PTAG"
127
+ PVER="${PTAG#v}"
128
+ gh release view "$PTAG" -R "$GH_REPO" >/dev/null 2>&1 || die "no release $PTAG to promote"
129
+ say "Promoting $PTAG to stable (in place; same tested artifact, no rebuild)"
130
+ gh release edit "$PTAG" -R "$GH_REPO" --prerelease=false --latest
131
+ if [[ "$DO_NPM" == "1" ]]; then
132
+ command -v npm >/dev/null || die "npm not found on PATH"
133
+ say "Moving npm 'latest' dist-tag -> $PVER"
134
+ npm dist-tag add "social-autoposter@$PVER" latest || die "npm dist-tag add failed"
135
+ # Keep the dual-published "s4l" package's dist-tag in lockstep. Best-effort:
136
+ # releases cut before the 2026-07-03 dual-publish have no s4l@$PVER, so a
137
+ # miss here warns instead of failing the promote.
138
+ npm dist-tag add "@m13v/s4l@$PVER" latest \
139
+ || echo " WARNING: npm dist-tag add @m13v/s4l@$PVER latest failed (version may predate the dual-publish)" >&2
140
+ fi
141
+ say "Verifying releases/latest serves $PTAG (drives stable boxes' update banner)"
142
+ LATEST_SEEN=""
143
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
144
+ LATEST_SEEN="$(curl -fsSL -m 15 "https://api.github.com/repos/$GH_REPO/releases/latest" \
145
+ | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).tag_name || ''" 2>/dev/null || echo "")"
146
+ [[ "$LATEST_SEEN" == "$PTAG" ]] && break
147
+ sleep 6
148
+ done
149
+ [[ "$LATEST_SEEN" == "$PTAG" ]] \
150
+ && echo " releases/latest -> $LATEST_SEEN; stable boxes detect within ~1 min" \
151
+ || echo " WARNING: releases/latest still reports '${LATEST_SEEN:-<none>}', not $PTAG (GitHub may still be propagating)." >&2
152
+ say "Promoted $PTAG"
153
+ exit 0
154
+ fi
155
+
156
+ command -v mcpb >/dev/null || die "mcpb CLI not found (npm i -g @anthropic-ai/mcpb)"
157
+
158
+ # ---- 1. Resolve + WRITE version into the repo-root package.json -------------
159
+ # The repo-root package.json is the SINGLE source of truth: `npm pack` reads it
160
+ # to build the embedded pipeline.tgz, so the bundle shell and the bundled
161
+ # pipeline cannot diverge as long as we bump it BEFORE building (step 2). This
162
+ # closes the 1.6.84-class bug where the shell said X but pipeline.tgz carried
163
+ # the prior version because only the satellites were stamped.
164
+ PKG_VERSION="$(node -p "require('$REPO_ROOT/package.json').version")"
165
+ if [[ -n "$VERSION_OVERRIDE" ]]; then
166
+ VERSION="${VERSION_OVERRIDE#v}"
167
+ elif [[ -n "$TAG_OVERRIDE" ]]; then
168
+ VERSION="${TAG_OVERRIDE#v}"
169
+ elif [[ "$DO_STAGING" == "1" ]]; then
170
+ # Staging = the next -rc.N. If package.json already carries an -rc.N for an
171
+ # unreleased patch, bump the rc; otherwise start rc.1 on the next patch of the
172
+ # current full release. The -rc.N rides through EVERY satellite (manifest,
173
+ # pipeline.tgz, dist/version.json) so a staging box's installed version string
174
+ # distinguishes rc.1 from rc.2 (the rc-aware compare in version.ts/snapshot.py
175
+ # depends on that).
176
+ VERSION="$(node -e "
177
+ const v='$PKG_VERSION';
178
+ const m=v.match(/^(\d+)\.(\d+)\.(\d+)(?:-rc\.(\d+))?/);
179
+ let [_,a,b,c,rc]=m.map((x)=>x);
180
+ a=+a;b=+b;c=+c;
181
+ if(rc!==undefined){ rc=+rc+1; } else { c=c+1; rc=1; }
182
+ console.log(a+'.'+b+'.'+c+'-rc.'+rc);
183
+ ")"
184
+ elif [[ "$DO_BUMP" == "1" ]]; then
185
+ # Compute the next version without writing a git tag; we own the write below.
186
+ VERSION="$(node -e "
187
+ let [a,b,c]='$PKG_VERSION'.split('.').map(Number);
188
+ const lvl='$BUMP_LEVEL';
189
+ if(lvl==='major'){a++;b=0;c=0;} else if(lvl==='minor'){b++;c=0;} else {c++;}
190
+ console.log(a+'.'+b+'.'+c);
191
+ ")"
192
+ else
193
+ VERSION="$PKG_VERSION"
194
+ fi
195
+ TAG="v$VERSION"
196
+
197
+ # Staging publishes as a GitHub pre-release + npm `next` dist-tag, so neither
198
+ # releases/latest nor npm `latest` moves — only staging-channel boxes see it.
199
+ GH_PRERELEASE_FLAG=""
200
+ NPM_TAG_ARGS=()
201
+ if [[ "$DO_STAGING" == "1" ]]; then
202
+ GH_PRERELEASE_FLAG="--prerelease"
203
+ NPM_TAG_ARGS=(--tag next)
204
+ say "STAGING pre-release $TAG (invisible to stable boxes; promote later with --promote $TAG)"
205
+ fi
206
+
207
+ # Write the resolved version into the repo-root package.json + lockfile so the
208
+ # pipeline tarball, npm publish, and all satellites share one number.
209
+ if [[ "$VERSION" != "$PKG_VERSION" ]]; then
210
+ say "Bumping repo-root package.json $PKG_VERSION -> $VERSION"
211
+ node -e "
212
+ const fs=require('fs');
213
+ for (const p of ['$REPO_ROOT/package.json','$REPO_ROOT/package-lock.json']) {
214
+ if (!fs.existsSync(p)) continue;
215
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
216
+ j.version='$VERSION';
217
+ if (j.packages && j.packages['']) j.packages[''].version='$VERSION';
218
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
219
+ console.log(' '+p.replace('$REPO_ROOT/','')+' -> '+j.version);
220
+ }
221
+ "
222
+ else
223
+ say "Releasing $TAG (repo-root package.json already $VERSION)"
224
+ fi
225
+
226
+ # ---- 2. Stamp EVERY version satellite BEFORE packing, then build ------------
227
+ # ONE source of truth (repo-root package.json, bumped in step 1); every other
228
+ # file that carries a version is a SATELLITE stamped from it here. The ordering
229
+ # is load-bearing: the embedded pipeline.tgz is `npm pack` of the repo root
230
+ # (bundle-pipeline.mjs), so it captures mcp/package.json AND mcp/dist/version.json
231
+ # AS THEY ARE ON DISK at pack time. If a satellite is stamped AFTER the pack, the
232
+ # tarball ships a stale mcp/ subtree while its top-level package.json is current.
233
+ # The menu bar resolves its version from mcp/dist/version.json FIRST
234
+ # (scripts/snapshot.py::_resolve_version), so a late stamp shows the OLD version
235
+ # in the menu bar even though the install is current (the 1.6.181-menu-on-a-
236
+ # 1.6.182-box bug, 2026-07-01). So: stamp source-tree satellites (2a) -> build
237
+ # panel + server (2b) -> stamp dist/version.json now that tsc emitted dist/ (2c)
238
+ # -> pack pipeline.tgz, now capturing an all-$VERSION mcp/ subtree (2d).
239
+
240
+ # ---- 2a. Stamp manifest.json + mcp/package.json + lockfile (PRE-pack) --------
241
+ # manifest.json feeds Claude Desktop's extension "Details" panel; mcp/package.json
242
+ # + its lockfile are stamped in lockstep (npm errors if they disagree). All three
243
+ # are inside the repo `files` allowlist, so they land in pipeline.tgz — they MUST
244
+ # be current before 2d packs them.
245
+ say "Stamping mcp/manifest.json + mcp/package.json + mcp/package-lock.json -> $VERSION"
246
+ node -e "
247
+ const fs=require('fs');
248
+ const V='$VERSION';
249
+ for (const p of ['$MCP_DIR/manifest.json','$MCP_DIR/package.json']) {
250
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
251
+ j.version=V;
252
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
253
+ console.log(' '+p.replace('$MCP_DIR/','mcp/')+' -> '+j.version);
254
+ }
255
+ const lp='$MCP_DIR/package-lock.json';
256
+ if (fs.existsSync(lp)) {
257
+ const l=JSON.parse(fs.readFileSync(lp,'utf8'));
258
+ l.version=V;
259
+ if (l.packages && l.packages['']) l.packages[''].version=V;
260
+ fs.writeFileSync(lp, JSON.stringify(l,null,2)+'\n');
261
+ console.log(' mcp/package-lock.json -> '+l.version);
262
+ }
263
+ "
264
+
265
+ # ---- 2b. Build panel + server (emits dist/) ---------------------------------
266
+ # Split out of `build:bundle` so we can stamp dist/version.json (2c) AFTER tsc
267
+ # emits dist/ but BEFORE the pipeline pack (2d). tsc does not touch dist/version.json
268
+ # (it is JSON we author, not a TS output), so a 2c stamp survives to the pack.
269
+ say "Building MCP panel + server"
270
+ ( cd "$MCP_DIR" && npm run build:panel && npm run build:server )
271
+
272
+ # ---- 2c. Stamp mcp/dist/version.json (after tsc, before the pipeline pack) ---
273
+ say "Stamping mcp/dist/version.json -> $VERSION"
274
+ node -e "
275
+ const fs=require('fs'),p='$MCP_DIR/dist/version.json';
276
+ fs.writeFileSync(p, JSON.stringify({version:'$VERSION',installedAt:new Date().toISOString()},null,2)+'\n');
277
+ console.log(' '+fs.readFileSync(p,'utf8').trim());
278
+ "
279
+
280
+ # ---- 2d. Pack embedded pipeline.tgz (now captures an all-$VERSION mcp/) ------
281
+ say "Packing embedded pipeline.tgz"
282
+ ( cd "$MCP_DIR" && npm run bundle:pipeline )
283
+
284
+ # ---- 3c. Regenerate manifest.json `tools` from the SERVER's registrations ---
285
+ # Claude Desktop exposes a .mcpb extension's tools to agent chats from the
286
+ # manifest's `tools` array. It was hand-written and drifted: it listed 5 old
287
+ # tools while the server registers ~10 (queue_setup, run_draft_cycle, …), so the
288
+ # newer ones were INVISIBLE to the in-chat agent — which silently broke onboarding
289
+ # / re-arm on every .mcpb install (the agent couldn't call queue_setup). Derive
290
+ # the list from the source's tool()/appTool() registrations so it can never drift
291
+ # again. (name + title; the title is the human description Desktop shows.)
292
+ say "Regenerating mcp/manifest.json tools from src/index.ts registrations"
293
+ node -e "
294
+ const fs=require('fs');
295
+ const src=fs.readFileSync('$MCP_DIR/src/index.ts','utf8');
296
+ const re=/(?:^|\n)\s*(?:tool|appTool)\(\s*\n\s*\"([a-z0-9_]+)\"\s*,\s*\{\s*\n\s*title:\s*\"((?:[^\"\\\\]|\\\\.)*)\"/g;
297
+ const tools=[]; let m;
298
+ while((m=re.exec(src))!==null) tools.push({name:m[1], description:m[2]});
299
+ if(tools.length < 6) { console.error(' refusing: only '+tools.length+' tools parsed (regex drift?)'); process.exit(1); }
300
+ const p='$MCP_DIR/manifest.json';
301
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
302
+ j.tools=tools;
303
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
304
+ console.log(' manifest tools ('+tools.length+'): '+tools.map(t=>t.name).join(', '));
305
+ "
306
+
307
+ # ---- 4. Pack the .mcpb ------------------------------------------------------
308
+ say "Packing $BUNDLE"
309
+ rm -f "$BUNDLE"
310
+ mcpb pack "$MCP_DIR" "$BUNDLE"
311
+
312
+ # ---- 5. Verify --------------------------------------------------------------
313
+ say "Verifying bundle"
314
+ [[ -f "$BUNDLE" ]] || die "bundle was not produced"
315
+ BYTES=$(stat -f%z "$BUNDLE" 2>/dev/null || stat -c%s "$BUNDLE")
316
+ MB=$(( BYTES / 1024 / 1024 ))
317
+ echo " size: ${MB}MB (cap ${SIZE_CAP_MB}MB)"
318
+ (( MB <= SIZE_CAP_MB )) || die "bundle ${MB}MB exceeds ${SIZE_CAP_MB}MB cap"
319
+
320
+ # Capture the listing once (grep -q on a live pipe trips SIGPIPE under pipefail).
321
+ LISTING="$(unzip -l "$BUNDLE" 2>/dev/null || true)"
322
+
323
+ PIPELINE_COUNT=$(printf '%s\n' "$LISTING" | grep -c 'dist/pipeline.tgz' || true)
324
+ [[ "$PIPELINE_COUNT" == "1" ]] || die "expected exactly 1 embedded dist/pipeline.tgz, found $PIPELINE_COUNT"
325
+ echo " embedded pipeline.tgz: ok"
326
+
327
+ BUNDLE_VER=$(unzip -p "$BUNDLE" dist/version.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "?")
328
+ [[ "$BUNDLE_VER" == "$VERSION" ]] || die "bundle version.json=$BUNDLE_VER != $VERSION"
329
+ echo " version.json: $BUNDLE_VER ok"
330
+
331
+ MANIFEST_VER=$(unzip -p "$BUNDLE" manifest.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "?")
332
+ [[ "$MANIFEST_VER" == "$VERSION" ]] || die "bundle manifest.json=$MANIFEST_VER != $VERSION (Desktop Details panel would show the wrong version)"
333
+ echo " manifest.json: $MANIFEST_VER ok"
334
+
335
+ # THE guard that was missing when 1.6.84 shipped a 1.6.83 pipeline: assert the
336
+ # version INSIDE the embedded pipeline.tgz matches the bundle. ensurePipelineCurrent()
337
+ # on the box trusts version.json to decide whether to re-extract; if the tarball's
338
+ # own package.json lags, the box materializes stale Python and never knows.
339
+ PIPELINE_VER=$(unzip -p "$BUNDLE" dist/pipeline.tgz 2>/dev/null | tar -xzO package/package.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "?")
340
+ [[ "$PIPELINE_VER" == "$VERSION" ]] || die "embedded pipeline.tgz version=$PIPELINE_VER != $VERSION (box would materialize a STALE pipeline; bump repo-root package.json BEFORE build)"
341
+ echo " pipeline.tgz internal version: $PIPELINE_VER ok"
342
+
343
+ # The menu bar reads package/mcp/dist/version.json FIRST, then package/mcp/package.json
344
+ # (scripts/snapshot.py::_resolve_version), both from the SAME pipeline.tgz the box
345
+ # extracts into S4L_REPO_DIR. Assert the whole mcp/ subtree matches so a satellite
346
+ # stamped after the pack can't ship a menu that shows the wrong version on an
347
+ # otherwise-current box (the 1.6.181-menu-on-a-1.6.182-box bug). Empty/missing =
348
+ # fail: _resolve_version would fall through and could read a stale file.
349
+ for sub in mcp/dist/version.json mcp/package.json; do
350
+ SUBV=$(unzip -p "$BUNDLE" dist/pipeline.tgz 2>/dev/null | tar -xzO "package/$sub" 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "?")
351
+ [[ "$SUBV" == "$VERSION" ]] || die "embedded pipeline.tgz package/$sub=$SUBV != $VERSION (menu bar would show the wrong version; stamp satellites BEFORE the pipeline pack)"
352
+ echo " pipeline.tgz $sub: $SUBV ok"
353
+ done
354
+
355
+ for f in "dist/index.js" "dist/runtime.js" "manifest.json"; do
356
+ # grep -c reads all input (no SIGPIPE); anchor on the time column + 3-space
357
+ # gutter so node_modules/.../dist/index.js does not false-match the top-level.
358
+ n=$(printf '%s\n' "$LISTING" | grep -c "[0-9:] $f\$" || true)
359
+ [[ "$n" -ge 1 ]] || die "bundle missing $f"
360
+ done
361
+ echo " runtime + server + manifest: ok"
362
+
363
+ if [[ "$DO_RELEASE" == "0" ]]; then
364
+ say "Done (--no-release). Bundle ready at: $BUNDLE"
365
+ exit 0
366
+ fi
367
+
368
+ # ---- 6. npm publish (Story A: `npx social-autoposter@<v> init`) -------------
369
+ # Same VERSION as the bundle, from the SAME bumped repo-root package.json, so the
370
+ # npm install path and the .mcpb path can never disagree. Idempotent: a version
371
+ # already on the registry is skipped, not failed.
372
+ if [[ "$DO_NPM" == "1" ]]; then
373
+ command -v npm >/dev/null || die "npm not found on PATH"
374
+ NPM_HTTP=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/social-autoposter/$VERSION" || echo "000")
375
+ if [[ "$NPM_HTTP" == "200" ]]; then
376
+ say "npm: social-autoposter@$VERSION already published — skipping"
377
+ else
378
+ say "Publishing social-autoposter@$VERSION to npm${NPM_TAG_ARGS:+ (tag: ${NPM_TAG_ARGS[*]})}"
379
+ # Guarded array expansion: an EMPTY array under `set -u` trips "unbound
380
+ # variable" on macOS bash 3.2, so expand to nothing when no --tag is set.
381
+ ( cd "$REPO_ROOT" && npm publish ${NPM_TAG_ARGS[@]+"${NPM_TAG_ARGS[@]}"} ) || die "npm publish failed"
382
+ # Confirm it actually landed (granular-token whoami lies; a version fetch doesn't).
383
+ for _ in 1 2 3 4 5; do
384
+ sleep 2
385
+ [[ "$(curl -s -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/social-autoposter/$VERSION")" == "200" ]] && break
386
+ done
387
+ echo " npm: social-autoposter@$VERSION live"
388
+ fi
389
+
390
+ # ---- 6b. Dual-publish the SAME content as "@m13v/s4l" (brand rename) -------
391
+ # `npx @m13v/s4l init` == `npx social-autoposter init`. Same version, same
392
+ # dist-tag logic (stable -> default `latest`; --staging -> `next`).
393
+ # npm REJECTS the bare name "s4l" (403: too similar to st/swr/sax/...), so the
394
+ # alias lives under the m13v scope, published --access=public.
395
+ # The repo package.json is NEVER mutated: we `npm pack` the repo root (the
396
+ # exact content step 6 shipped), extract into a temp dir, rewrite ONLY the
397
+ # temp copy's name field, and publish that copy. Idempotent like step 6.
398
+ # BEST-EFFORT BY DESIGN: every failure in 6b warns and falls through — it must
399
+ # NEVER die. On 2026-07-03 a die here aborted the run between the npm publish
400
+ # (step 6, done) and the GitHub release (step 7, never ran), leaving npm `next`
401
+ # on rc.6 with no matching GH release: exactly the diverged-lanes state this
402
+ # script exists to prevent. The alias package is a convenience; the
403
+ # social-autoposter npm lane + GH release lockstep is the contract.
404
+ S4L_ALIAS_PKG="@m13v/s4l"
405
+ S4L_ALIAS_URL="https://registry.npmjs.org/@m13v%2fs4l/$VERSION"
406
+ S4L_NPM_HTTP=$(curl -s -o /dev/null -w "%{http_code}" "$S4L_ALIAS_URL" || echo "000")
407
+ if [[ "$S4L_NPM_HTTP" == "200" ]]; then
408
+ say "npm: $S4L_ALIAS_PKG@$VERSION already published — skipping dual-publish"
409
+ else
410
+ say "Dual-publishing $S4L_ALIAS_PKG@$VERSION to npm${NPM_TAG_ARGS:+ (tag: ${NPM_TAG_ARGS[*]})}"
411
+ S4L_PUB_DIR="$(mktemp -d "${TMPDIR:-/tmp}/s4l-dual-publish.XXXXXX")"
412
+ if ( cd "$REPO_ROOT" && npm pack --pack-destination "$S4L_PUB_DIR" >/dev/null ) \
413
+ && S4L_TGZ="$(ls "$S4L_PUB_DIR"/social-autoposter-*.tgz 2>/dev/null | head -1)" \
414
+ && [[ -n "$S4L_TGZ" ]] \
415
+ && tar -xzf "$S4L_TGZ" -C "$S4L_PUB_DIR" \
416
+ && node -e "
417
+ const fs=require('fs');
418
+ const p='$S4L_PUB_DIR/package/package.json';
419
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
420
+ if (j.version!=='$VERSION') { console.error(' temp copy version '+j.version+' != $VERSION'); process.exit(1); }
421
+ j.name='$S4L_ALIAS_PKG';
422
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
423
+ console.log(' temp copy renamed to $S4L_ALIAS_PKG@'+j.version+' (repo package.json untouched)');
424
+ " \
425
+ && ( cd "$S4L_PUB_DIR/package" && npm publish --access=public ${NPM_TAG_ARGS[@]+"${NPM_TAG_ARGS[@]}"} ); then
426
+ for _ in 1 2 3 4 5; do
427
+ sleep 2
428
+ [[ "$(curl -s -o /dev/null -w '%{http_code}' "$S4L_ALIAS_URL")" == "200" ]] && break
429
+ done
430
+ echo " npm: $S4L_ALIAS_PKG@$VERSION live"
431
+ else
432
+ echo " WARNING: $S4L_ALIAS_PKG dual-publish failed (alias lane only; social-autoposter + GH release proceed)" >&2
433
+ fi
434
+ rm -rf "$S4L_PUB_DIR"
435
+ fi
436
+ else
437
+ say "npm publish skipped (--no-npm)"
438
+ fi
439
+
440
+ # ---- 7. GitHub release ------------------------------------------------------
441
+ command -v gh >/dev/null || die "gh CLI not found"
442
+ gh auth status >/dev/null 2>&1 || die "gh not authenticated (run: gh auth login)"
443
+
444
+ NOTES="social-autoposter ${TAG}
445
+
446
+ Double-click install for Claude Desktop. Drag \`social-autoposter.mcpb\` into Settings > Extensions, enable it, then open the panel in a Chat tab and click Install runtime (provisions uv + Python 3.12 + Chromium on first run). The pipeline source is bundled, so no separate clone or config is needed.
447
+
448
+ Power-user / CLI install: \`npx social-autoposter@${VERSION} init\`."
449
+
450
+ if gh release view "$TAG" -R "$GH_REPO" >/dev/null 2>&1; then
451
+ say "Release $TAG exists -> uploading asset (clobber)"
452
+ gh release upload "$TAG" "$BUNDLE" -R "$GH_REPO" --clobber
453
+ else
454
+ say "Creating release $TAG${GH_PRERELEASE_FLAG:+ (pre-release)}"
455
+ gh release create "$TAG" "$BUNDLE" \
456
+ -R "$GH_REPO" \
457
+ --title "social-autoposter $TAG" \
458
+ --notes "$NOTES" \
459
+ $DRAFT_FLAG $GH_PRERELEASE_FLAG
460
+ fi
461
+
462
+ URL=$(gh release view "$TAG" -R "$GH_REPO" --json url -q .url 2>/dev/null || echo "")
463
+ say "Released $TAG"
464
+ echo " asset: social-autoposter.mcpb (${MB}MB)"
465
+ [[ -n "$URL" ]] && echo " $URL"
466
+
467
+ # ---- 8. Verify the update banner will fire ---------------------------------
468
+ # The menu-bar "⬆ Update available" banner (mcp/src/version.ts::versionStatus)
469
+ # resolves "latest" from GitHub releases/latest, which is what .mcpb boxes (no
470
+ # npm) can actually read. A draft release is deliberately excluded by GitHub's
471
+ # releases/latest, so it also won't (and shouldn't) trigger the banner — skip
472
+ # the check then. For a normal release, poll releases/latest until it serves the
473
+ # new tag so we don't declare success while every box stays silent on the old
474
+ # version (the 1.6.177-vs-1.6.181 blind-banner bug this guards against).
475
+ if [[ -n "$DRAFT_FLAG" ]]; then
476
+ say "Draft release — skipping banner verification (releases/latest excludes drafts by design)"
477
+ elif [[ "$DO_STAGING" == "1" ]]; then
478
+ say "Staging pre-release — releases/latest deliberately EXCLUDES it, so stable boxes stay put."
479
+ echo " Only boxes on the staging channel pull $TAG (via the releases LIST endpoint)."
480
+ echo " To ship it to everyone once tested: bash scripts/release-mcpb.sh --promote $TAG"
481
+ else
482
+ say "Verifying releases/latest serves $TAG (drives the menu-bar update banner)"
483
+ LATEST_SEEN=""
484
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
485
+ LATEST_SEEN="$(curl -fsSL -m 15 "https://api.github.com/repos/$GH_REPO/releases/latest" \
486
+ | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).tag_name || ''" 2>/dev/null || echo "")"
487
+ [[ "$LATEST_SEEN" == "$TAG" ]] && break
488
+ sleep 6
489
+ done
490
+ if [[ "$LATEST_SEEN" == "$TAG" ]]; then
491
+ echo " releases/latest -> $LATEST_SEEN; boxes detect within version.ts's ~1-min TTL (55s, ETag-conditional; boxes older than 1.6.188 poll every 10 min)"
492
+ else
493
+ echo " WARNING: releases/latest still reports '${LATEST_SEEN:-<none>}', not $TAG." >&2
494
+ echo " The menu-bar update banner will NOT fire until this resolves. If it stays" >&2
495
+ echo " wrong, the release is likely a draft/prerelease or GitHub hasn't propagated." >&2
496
+ fi
497
+ fi