@m13v/s4l 1.6.197-rc.10

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 +1336 -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 +513 -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,513 @@
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
+ # Symmetric guard: --staging with a stable-shaped version would publish a
203
+ # "pre-release" that staging boxes rank ABOVE the real stable line while npm
204
+ # `next` points at a non-rc build. Always an operator mistake; refuse.
205
+ if [[ "$VERSION" != *-* ]]; then
206
+ die "--staging requires an -rc.N version but resolved $VERSION (stable-shaped). Drop --staging, or pin one with --version ${VERSION}-rc.1"
207
+ fi
208
+ GH_PRERELEASE_FLAG="--prerelease"
209
+ NPM_TAG_ARGS=(--tag next)
210
+ say "STAGING pre-release $TAG (invisible to stable boxes; promote later with --promote $TAG)"
211
+ fi
212
+
213
+ # Write the resolved version into the repo-root package.json + lockfile so the
214
+ # pipeline tarball, npm publish, and all satellites share one number.
215
+ if [[ "$VERSION" != "$PKG_VERSION" ]]; then
216
+ say "Bumping repo-root package.json $PKG_VERSION -> $VERSION"
217
+ node -e "
218
+ const fs=require('fs');
219
+ for (const p of ['$REPO_ROOT/package.json','$REPO_ROOT/package-lock.json']) {
220
+ if (!fs.existsSync(p)) continue;
221
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
222
+ j.version='$VERSION';
223
+ if (j.packages && j.packages['']) j.packages[''].version='$VERSION';
224
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
225
+ console.log(' '+p.replace('$REPO_ROOT/','')+' -> '+j.version);
226
+ }
227
+ "
228
+ else
229
+ say "Releasing $TAG (repo-root package.json already $VERSION)"
230
+ fi
231
+
232
+ # ---- 2. Stamp EVERY version satellite BEFORE packing, then build ------------
233
+ # ONE source of truth (repo-root package.json, bumped in step 1); every other
234
+ # file that carries a version is a SATELLITE stamped from it here. The ordering
235
+ # is load-bearing: the embedded pipeline.tgz is `npm pack` of the repo root
236
+ # (bundle-pipeline.mjs), so it captures mcp/package.json AND mcp/dist/version.json
237
+ # AS THEY ARE ON DISK at pack time. If a satellite is stamped AFTER the pack, the
238
+ # tarball ships a stale mcp/ subtree while its top-level package.json is current.
239
+ # The menu bar resolves its version from mcp/dist/version.json FIRST
240
+ # (scripts/snapshot.py::_resolve_version), so a late stamp shows the OLD version
241
+ # in the menu bar even though the install is current (the 1.6.181-menu-on-a-
242
+ # 1.6.182-box bug, 2026-07-01). So: stamp source-tree satellites (2a) -> build
243
+ # panel + server (2b) -> stamp dist/version.json now that tsc emitted dist/ (2c)
244
+ # -> pack pipeline.tgz, now capturing an all-$VERSION mcp/ subtree (2d).
245
+
246
+ # ---- 2a. Stamp manifest.json + mcp/package.json + lockfile (PRE-pack) --------
247
+ # manifest.json feeds Claude Desktop's extension "Details" panel; mcp/package.json
248
+ # + its lockfile are stamped in lockstep (npm errors if they disagree). All three
249
+ # are inside the repo `files` allowlist, so they land in pipeline.tgz — they MUST
250
+ # be current before 2d packs them.
251
+ say "Stamping mcp/manifest.json + mcp/package.json + mcp/package-lock.json -> $VERSION"
252
+ node -e "
253
+ const fs=require('fs');
254
+ const V='$VERSION';
255
+ for (const p of ['$MCP_DIR/manifest.json','$MCP_DIR/package.json']) {
256
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
257
+ j.version=V;
258
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
259
+ console.log(' '+p.replace('$MCP_DIR/','mcp/')+' -> '+j.version);
260
+ }
261
+ const lp='$MCP_DIR/package-lock.json';
262
+ if (fs.existsSync(lp)) {
263
+ const l=JSON.parse(fs.readFileSync(lp,'utf8'));
264
+ l.version=V;
265
+ if (l.packages && l.packages['']) l.packages[''].version=V;
266
+ fs.writeFileSync(lp, JSON.stringify(l,null,2)+'\n');
267
+ console.log(' mcp/package-lock.json -> '+l.version);
268
+ }
269
+ "
270
+
271
+ # ---- 2b. Build panel + server (emits dist/) ---------------------------------
272
+ # Split out of `build:bundle` so we can stamp dist/version.json (2c) AFTER tsc
273
+ # emits dist/ but BEFORE the pipeline pack (2d). tsc does not touch dist/version.json
274
+ # (it is JSON we author, not a TS output), so a 2c stamp survives to the pack.
275
+ say "Building MCP panel + server"
276
+ ( cd "$MCP_DIR" && npm run build:panel && npm run build:server )
277
+
278
+ # ---- 2c. Stamp mcp/dist/version.json (after tsc, before the pipeline pack) ---
279
+ say "Stamping mcp/dist/version.json -> $VERSION"
280
+ node -e "
281
+ const fs=require('fs'),p='$MCP_DIR/dist/version.json';
282
+ fs.writeFileSync(p, JSON.stringify({version:'$VERSION',installedAt:new Date().toISOString()},null,2)+'\n');
283
+ console.log(' '+fs.readFileSync(p,'utf8').trim());
284
+ "
285
+
286
+ # ---- 2d. Pack embedded pipeline.tgz (now captures an all-$VERSION mcp/) ------
287
+ say "Packing embedded pipeline.tgz"
288
+ ( cd "$MCP_DIR" && npm run bundle:pipeline )
289
+
290
+ # ---- 3c. Regenerate manifest.json `tools` from the SERVER's registrations ---
291
+ # Claude Desktop exposes a .mcpb extension's tools to agent chats from the
292
+ # manifest's `tools` array. It was hand-written and drifted: it listed 5 old
293
+ # tools while the server registers ~10 (queue_setup, run_draft_cycle, …), so the
294
+ # newer ones were INVISIBLE to the in-chat agent — which silently broke onboarding
295
+ # / re-arm on every .mcpb install (the agent couldn't call queue_setup). Derive
296
+ # the list from the source's tool()/appTool() registrations so it can never drift
297
+ # again. (name + title; the title is the human description Desktop shows.)
298
+ say "Regenerating mcp/manifest.json tools from src/index.ts registrations"
299
+ node -e "
300
+ const fs=require('fs');
301
+ const src=fs.readFileSync('$MCP_DIR/src/index.ts','utf8');
302
+ const re=/(?:^|\n)\s*(?:tool|appTool)\(\s*\n\s*\"([a-z0-9_]+)\"\s*,\s*\{\s*\n\s*title:\s*\"((?:[^\"\\\\]|\\\\.)*)\"/g;
303
+ const tools=[]; let m;
304
+ while((m=re.exec(src))!==null) tools.push({name:m[1], description:m[2]});
305
+ if(tools.length < 6) { console.error(' refusing: only '+tools.length+' tools parsed (regex drift?)'); process.exit(1); }
306
+ const p='$MCP_DIR/manifest.json';
307
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
308
+ j.tools=tools;
309
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
310
+ console.log(' manifest tools ('+tools.length+'): '+tools.map(t=>t.name).join(', '));
311
+ "
312
+
313
+ # ---- 4. Pack the .mcpb ------------------------------------------------------
314
+ say "Packing $BUNDLE"
315
+ rm -f "$BUNDLE"
316
+ mcpb pack "$MCP_DIR" "$BUNDLE"
317
+
318
+ # ---- 5. Verify --------------------------------------------------------------
319
+ say "Verifying bundle"
320
+ [[ -f "$BUNDLE" ]] || die "bundle was not produced"
321
+ BYTES=$(stat -f%z "$BUNDLE" 2>/dev/null || stat -c%s "$BUNDLE")
322
+ MB=$(( BYTES / 1024 / 1024 ))
323
+ echo " size: ${MB}MB (cap ${SIZE_CAP_MB}MB)"
324
+ (( MB <= SIZE_CAP_MB )) || die "bundle ${MB}MB exceeds ${SIZE_CAP_MB}MB cap"
325
+
326
+ # Capture the listing once (grep -q on a live pipe trips SIGPIPE under pipefail).
327
+ LISTING="$(unzip -l "$BUNDLE" 2>/dev/null || true)"
328
+
329
+ PIPELINE_COUNT=$(printf '%s\n' "$LISTING" | grep -c 'dist/pipeline.tgz' || true)
330
+ [[ "$PIPELINE_COUNT" == "1" ]] || die "expected exactly 1 embedded dist/pipeline.tgz, found $PIPELINE_COUNT"
331
+ echo " embedded pipeline.tgz: ok"
332
+
333
+ 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 "?")
334
+ [[ "$BUNDLE_VER" == "$VERSION" ]] || die "bundle version.json=$BUNDLE_VER != $VERSION"
335
+ echo " version.json: $BUNDLE_VER ok"
336
+
337
+ MANIFEST_VER=$(unzip -p "$BUNDLE" manifest.json 2>/dev/null | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).version" 2>/dev/null || echo "?")
338
+ [[ "$MANIFEST_VER" == "$VERSION" ]] || die "bundle manifest.json=$MANIFEST_VER != $VERSION (Desktop Details panel would show the wrong version)"
339
+ echo " manifest.json: $MANIFEST_VER ok"
340
+
341
+ # THE guard that was missing when 1.6.84 shipped a 1.6.83 pipeline: assert the
342
+ # version INSIDE the embedded pipeline.tgz matches the bundle. ensurePipelineCurrent()
343
+ # on the box trusts version.json to decide whether to re-extract; if the tarball's
344
+ # own package.json lags, the box materializes stale Python and never knows.
345
+ 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 "?")
346
+ [[ "$PIPELINE_VER" == "$VERSION" ]] || die "embedded pipeline.tgz version=$PIPELINE_VER != $VERSION (box would materialize a STALE pipeline; bump repo-root package.json BEFORE build)"
347
+ echo " pipeline.tgz internal version: $PIPELINE_VER ok"
348
+
349
+ # The menu bar reads package/mcp/dist/version.json FIRST, then package/mcp/package.json
350
+ # (scripts/snapshot.py::_resolve_version), both from the SAME pipeline.tgz the box
351
+ # extracts into S4L_REPO_DIR. Assert the whole mcp/ subtree matches so a satellite
352
+ # stamped after the pack can't ship a menu that shows the wrong version on an
353
+ # otherwise-current box (the 1.6.181-menu-on-a-1.6.182-box bug). Empty/missing =
354
+ # fail: _resolve_version would fall through and could read a stale file.
355
+ for sub in mcp/dist/version.json mcp/package.json; do
356
+ 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 "?")
357
+ [[ "$SUBV" == "$VERSION" ]] || die "embedded pipeline.tgz package/$sub=$SUBV != $VERSION (menu bar would show the wrong version; stamp satellites BEFORE the pipeline pack)"
358
+ echo " pipeline.tgz $sub: $SUBV ok"
359
+ done
360
+
361
+ for f in "dist/index.js" "dist/runtime.js" "manifest.json"; do
362
+ # grep -c reads all input (no SIGPIPE); anchor on the time column + 3-space
363
+ # gutter so node_modules/.../dist/index.js does not false-match the top-level.
364
+ n=$(printf '%s\n' "$LISTING" | grep -c "[0-9:] $f\$" || true)
365
+ [[ "$n" -ge 1 ]] || die "bundle missing $f"
366
+ done
367
+ echo " runtime + server + manifest: ok"
368
+
369
+ if [[ "$DO_RELEASE" == "0" ]]; then
370
+ say "Done (--no-release). Bundle ready at: $BUNDLE"
371
+ exit 0
372
+ fi
373
+
374
+ # ---- 6. npm publish (Story A: `npx social-autoposter@<v> init`) -------------
375
+ # Same VERSION as the bundle, from the SAME bumped repo-root package.json, so the
376
+ # npm install path and the .mcpb path can never disagree. Idempotent: a version
377
+ # already on the registry is skipped, not failed.
378
+ if [[ "$DO_NPM" == "1" ]]; then
379
+ command -v npm >/dev/null || die "npm not found on PATH"
380
+ NPM_HTTP=$(curl -s -o /dev/null -w "%{http_code}" "https://registry.npmjs.org/social-autoposter/$VERSION" || echo "000")
381
+ if [[ "$NPM_HTTP" == "200" ]]; then
382
+ say "npm: social-autoposter@$VERSION already published — skipping"
383
+ else
384
+ say "Publishing social-autoposter@$VERSION to npm${NPM_TAG_ARGS:+ (tag: ${NPM_TAG_ARGS[*]})}"
385
+ # Guarded array expansion: an EMPTY array under `set -u` trips "unbound
386
+ # variable" on macOS bash 3.2, so expand to nothing when no --tag is set.
387
+ ( cd "$REPO_ROOT" && npm publish ${NPM_TAG_ARGS[@]+"${NPM_TAG_ARGS[@]}"} ) || die "npm publish failed"
388
+ # Confirm it actually landed (granular-token whoami lies; a version fetch doesn't).
389
+ for _ in 1 2 3 4 5; do
390
+ sleep 2
391
+ [[ "$(curl -s -o /dev/null -w '%{http_code}' "https://registry.npmjs.org/social-autoposter/$VERSION")" == "200" ]] && break
392
+ done
393
+ echo " npm: social-autoposter@$VERSION live"
394
+ fi
395
+
396
+ # ---- 6b. Dual-publish the SAME content as "@m13v/s4l" (brand rename) -------
397
+ # `npx @m13v/s4l init` == `npx social-autoposter init`. Same version, same
398
+ # dist-tag logic (stable -> default `latest`; --staging -> `next`).
399
+ # npm REJECTS the bare name "s4l" (403: too similar to st/swr/sax/...), so the
400
+ # alias lives under the m13v scope, published --access=public.
401
+ # The repo package.json is NEVER mutated: we `npm pack` the repo root (the
402
+ # exact content step 6 shipped), extract into a temp dir, rewrite ONLY the
403
+ # temp copy's name field, and publish that copy. Idempotent like step 6.
404
+ # BEST-EFFORT BY DESIGN: every failure in 6b warns and falls through — it must
405
+ # NEVER die. On 2026-07-03 a die here aborted the run between the npm publish
406
+ # (step 6, done) and the GitHub release (step 7, never ran), leaving npm `next`
407
+ # on rc.6 with no matching GH release: exactly the diverged-lanes state this
408
+ # script exists to prevent. The alias package is a convenience; the
409
+ # social-autoposter npm lane + GH release lockstep is the contract.
410
+ S4L_ALIAS_PKG="@m13v/s4l"
411
+ S4L_ALIAS_URL="https://registry.npmjs.org/@m13v%2fs4l/$VERSION"
412
+ S4L_NPM_HTTP=$(curl -s -o /dev/null -w "%{http_code}" "$S4L_ALIAS_URL" || echo "000")
413
+ if [[ "$S4L_NPM_HTTP" == "200" ]]; then
414
+ say "npm: $S4L_ALIAS_PKG@$VERSION already published — skipping dual-publish"
415
+ else
416
+ say "Dual-publishing $S4L_ALIAS_PKG@$VERSION to npm${NPM_TAG_ARGS:+ (tag: ${NPM_TAG_ARGS[*]})}"
417
+ S4L_PUB_DIR="$(mktemp -d "${TMPDIR:-/tmp}/s4l-dual-publish.XXXXXX")"
418
+ if ( cd "$REPO_ROOT" && npm pack --pack-destination "$S4L_PUB_DIR" >/dev/null ) \
419
+ && S4L_TGZ="$(ls "$S4L_PUB_DIR"/social-autoposter-*.tgz 2>/dev/null | head -1)" \
420
+ && [[ -n "$S4L_TGZ" ]] \
421
+ && tar -xzf "$S4L_TGZ" -C "$S4L_PUB_DIR" \
422
+ && node -e "
423
+ const fs=require('fs');
424
+ const p='$S4L_PUB_DIR/package/package.json';
425
+ const j=JSON.parse(fs.readFileSync(p,'utf8'));
426
+ if (j.version!=='$VERSION') { console.error(' temp copy version '+j.version+' != $VERSION'); process.exit(1); }
427
+ j.name='$S4L_ALIAS_PKG';
428
+ fs.writeFileSync(p, JSON.stringify(j,null,2)+'\n');
429
+ console.log(' temp copy renamed to $S4L_ALIAS_PKG@'+j.version+' (repo package.json untouched)');
430
+ " \
431
+ && ( cd "$S4L_PUB_DIR/package" && npm publish --access=public ${NPM_TAG_ARGS[@]+"${NPM_TAG_ARGS[@]}"} ); then
432
+ for _ in 1 2 3 4 5; do
433
+ sleep 2
434
+ [[ "$(curl -s -o /dev/null -w '%{http_code}' "$S4L_ALIAS_URL")" == "200" ]] && break
435
+ done
436
+ echo " npm: $S4L_ALIAS_PKG@$VERSION live"
437
+ else
438
+ echo " WARNING: $S4L_ALIAS_PKG dual-publish failed (alias lane only; social-autoposter + GH release proceed)" >&2
439
+ fi
440
+ rm -rf "$S4L_PUB_DIR"
441
+ fi
442
+ else
443
+ say "npm publish skipped (--no-npm)"
444
+ fi
445
+
446
+ # ---- 7. GitHub release ------------------------------------------------------
447
+ command -v gh >/dev/null || die "gh CLI not found"
448
+ gh auth status >/dev/null 2>&1 || die "gh not authenticated (run: gh auth login)"
449
+
450
+ NOTES="social-autoposter ${TAG}
451
+
452
+ 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.
453
+
454
+ Power-user / CLI install: \`npx social-autoposter@${VERSION} init\`."
455
+
456
+ if gh release view "$TAG" -R "$GH_REPO" >/dev/null 2>&1; then
457
+ say "Release $TAG exists -> uploading asset (clobber)"
458
+ gh release upload "$TAG" "$BUNDLE" -R "$GH_REPO" --clobber
459
+ else
460
+ # HARD GUARD (2026-07-03): never CREATE a release for a prerelease-suffixed
461
+ # version through the stable path. Without --prerelease, gh marks it latest,
462
+ # releases/latest moves onto the rc, and every STABLE box updates to a
463
+ # prerelease. CI (release-mcpb.yml) used to run this script on any v* tag
464
+ # push with no --staging — an rc tag push was one npm-auth accident away from
465
+ # exactly that. The clobber branch above is deliberately unguarded: uploading
466
+ # assets onto an EXISTING (correctly-flagged) release is always safe.
467
+ if [[ "$VERSION" == *-* && "$DO_STAGING" != "1" ]]; then
468
+ die "refusing to CREATE a stable release for prerelease version $VERSION. Use --staging (new rc), --promote <tag> (ship a tested rc), or create the release locally first so this run only uploads assets."
469
+ fi
470
+ say "Creating release $TAG${GH_PRERELEASE_FLAG:+ (pre-release)}"
471
+ gh release create "$TAG" "$BUNDLE" \
472
+ -R "$GH_REPO" \
473
+ --title "social-autoposter $TAG" \
474
+ --notes "$NOTES" \
475
+ $DRAFT_FLAG $GH_PRERELEASE_FLAG
476
+ fi
477
+
478
+ URL=$(gh release view "$TAG" -R "$GH_REPO" --json url -q .url 2>/dev/null || echo "")
479
+ say "Released $TAG"
480
+ echo " asset: social-autoposter.mcpb (${MB}MB)"
481
+ [[ -n "$URL" ]] && echo " $URL"
482
+
483
+ # ---- 8. Verify the update banner will fire ---------------------------------
484
+ # The menu-bar "⬆ Update available" banner (mcp/src/version.ts::versionStatus)
485
+ # resolves "latest" from GitHub releases/latest, which is what .mcpb boxes (no
486
+ # npm) can actually read. A draft release is deliberately excluded by GitHub's
487
+ # releases/latest, so it also won't (and shouldn't) trigger the banner — skip
488
+ # the check then. For a normal release, poll releases/latest until it serves the
489
+ # new tag so we don't declare success while every box stays silent on the old
490
+ # version (the 1.6.177-vs-1.6.181 blind-banner bug this guards against).
491
+ if [[ -n "$DRAFT_FLAG" ]]; then
492
+ say "Draft release — skipping banner verification (releases/latest excludes drafts by design)"
493
+ elif [[ "$DO_STAGING" == "1" ]]; then
494
+ say "Staging pre-release — releases/latest deliberately EXCLUDES it, so stable boxes stay put."
495
+ echo " Only boxes on the staging channel pull $TAG (via the releases LIST endpoint)."
496
+ echo " To ship it to everyone once tested: bash scripts/release-mcpb.sh --promote $TAG"
497
+ else
498
+ say "Verifying releases/latest serves $TAG (drives the menu-bar update banner)"
499
+ LATEST_SEEN=""
500
+ for _ in 1 2 3 4 5 6 7 8 9 10; do
501
+ LATEST_SEEN="$(curl -fsSL -m 15 "https://api.github.com/repos/$GH_REPO/releases/latest" \
502
+ | node -p "JSON.parse(require('fs').readFileSync(0,'utf8')).tag_name || ''" 2>/dev/null || echo "")"
503
+ [[ "$LATEST_SEEN" == "$TAG" ]] && break
504
+ sleep 6
505
+ done
506
+ if [[ "$LATEST_SEEN" == "$TAG" ]]; then
507
+ 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)"
508
+ else
509
+ echo " WARNING: releases/latest still reports '${LATEST_SEEN:-<none>}', not $TAG." >&2
510
+ echo " The menu-bar update banner will NOT fire until this resolves. If it stays" >&2
511
+ echo " wrong, the release is likely a draft/prerelease or GitHub hasn't propagated." >&2
512
+ fi
513
+ fi