@m13v/s4l 1.7.1-rc.7 → 1.7.1-rc.9
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.
- package/bin/cli.js +23 -0
- package/mcp/dist/runtime.js +23 -0
- package/mcp/dist/version.json +2 -2
- package/mcp/install.mjs +0 -15
- package/mcp/manifest.json +1 -1
- package/mcp/menubar/s4l_menubar.py +62 -22
- package/mcp/package.json +1 -1
- package/mcp/shared/doctor.cjs +8 -0
- package/package.json +1 -1
- package/scripts/get-latest-staging-mcpb.sh +28 -45
- package/scripts/release-mcpb.sh +48 -0
- package/scripts/set-channel.sh +0 -29
package/bin/cli.js
CHANGED
|
@@ -823,6 +823,29 @@ function installMcp() {
|
|
|
823
823
|
} catch (e) {
|
|
824
824
|
console.warn(' WARNING: could not stamp MCP version:', e && e.message);
|
|
825
825
|
}
|
|
826
|
+
// Auto-opt a fresh install into the staging channel when the version being
|
|
827
|
+
// installed is itself a pre-release (-rc.N) — e.g. `npx social-autoposter@
|
|
828
|
+
// X.Y.Z-rc.N init`. Without this, channel.json stays absent, which
|
|
829
|
+
// scripts/s4l_channel.py's fail-safe default reads as "stable", so the box
|
|
830
|
+
// would install this one rc and then silently stop tracking staging (never
|
|
831
|
+
// pick up the next rc). Mirrors the same one-time write in
|
|
832
|
+
// mcp/src/runtime.ts's provision() for the .mcpb (Desktop) install path.
|
|
833
|
+
// Only writes when no channel marker exists yet — never overrides an
|
|
834
|
+
// existing preference (e.g. a user who already opted back out).
|
|
835
|
+
try {
|
|
836
|
+
const pkgVersion = require('../package.json').version;
|
|
837
|
+
if (pkgVersion.includes('-rc.')) {
|
|
838
|
+
const stateDir = process.env.S4L_STATE_DIR || path.join(os.homedir(), '.social-autoposter-mcp');
|
|
839
|
+
const channelPath = path.join(stateDir, 'channel.json');
|
|
840
|
+
if (!fs.existsSync(channelPath)) {
|
|
841
|
+
fs.mkdirSync(stateDir, { recursive: true });
|
|
842
|
+
fs.writeFileSync(channelPath, JSON.stringify({ channel: 'staging' }, null, 2) + '\n');
|
|
843
|
+
console.log(' pre-release install detected -> opted into the staging channel');
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
} catch (e) {
|
|
847
|
+
console.warn(' WARNING: could not set staging channel:', e && e.message);
|
|
848
|
+
}
|
|
826
849
|
console.log(' installing MCP runtime deps (npm install --omit=dev in mcp/)');
|
|
827
850
|
const npmRes = spawnSync('npm', ['install', '--omit=dev', '--no-audit', '--no-fund'], {
|
|
828
851
|
cwd: mcpDest,
|
package/mcp/dist/runtime.js
CHANGED
|
@@ -676,6 +676,29 @@ async function provision(progress) {
|
|
|
676
676
|
/* best effort; S4L_REPO_DIR + the run-*.sh fallback also resolve the repo */
|
|
677
677
|
}
|
|
678
678
|
setStep("repo", "done", `unpacked to ${resolvedRepo}`);
|
|
679
|
+
// Auto-opt a bare .mcpb FIRST install into the staging channel when the
|
|
680
|
+
// shipped build is itself a pre-release (-rc.N) — e.g. downloaded from
|
|
681
|
+
// the staging-latest alias link (scripts/release-mcpb.sh step 7b).
|
|
682
|
+
// Without this, channel.json stays absent, which releaseChannel()'s
|
|
683
|
+
// fail-safe default reads as "stable" (mcp/src/version.ts), so the box
|
|
684
|
+
// would install this one rc and then silently stop tracking staging
|
|
685
|
+
// (never pick up the next rc). Mirrors the same one-time write in
|
|
686
|
+
// bin/cli.js's installMcp() for the npx install path. Gated to this
|
|
687
|
+
// `else` branch (genuine bare-.mcpb unpack, not envClone) so a
|
|
688
|
+
// developer's own npm/git clone is never auto-enrolled. Only writes when
|
|
689
|
+
// no channel marker exists yet — never overrides an existing preference.
|
|
690
|
+
try {
|
|
691
|
+
const bv = bundledVersion();
|
|
692
|
+
const channelPath = path.join(STATE_DIR, "channel.json");
|
|
693
|
+
if (bv && bv.includes("-rc.") && !fs.existsSync(channelPath)) {
|
|
694
|
+
fs.mkdirSync(STATE_DIR, { recursive: true });
|
|
695
|
+
fs.writeFileSync(channelPath, JSON.stringify({ channel: "staging" }, null, 2) + "\n", "utf-8");
|
|
696
|
+
console.error(`[runtime] first install of a staging build (${bv}) — opted into the staging channel`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
catch {
|
|
700
|
+
/* best effort; worst case the box just tracks stable */
|
|
701
|
+
}
|
|
679
702
|
}
|
|
680
703
|
// --- Step 1: uv -----------------------------------------------------------
|
|
681
704
|
setStep("uv", "running");
|
package/mcp/dist/version.json
CHANGED
package/mcp/install.mjs
CHANGED
|
@@ -14,8 +14,6 @@ import path from "node:path";
|
|
|
14
14
|
|
|
15
15
|
const SERVER_KEY = "social-autoposter";
|
|
16
16
|
const UNINSTALL = process.argv.includes("--uninstall");
|
|
17
|
-
const CHANNEL_STAGING = process.argv.includes("--channel-staging");
|
|
18
|
-
const CHANNEL_STABLE = process.argv.includes("--channel-stable");
|
|
19
17
|
|
|
20
18
|
// ---- resolve the absolute paths we want pinned into the spawn env ----------
|
|
21
19
|
const here = path.dirname(new URL(import.meta.url).pathname);
|
|
@@ -137,19 +135,6 @@ for (const t of targets) {
|
|
|
137
135
|
}
|
|
138
136
|
}
|
|
139
137
|
|
|
140
|
-
// Set release channel if requested
|
|
141
|
-
if (!UNINSTALL && (CHANNEL_STAGING || CHANNEL_STABLE)) {
|
|
142
|
-
const stateDir = path.join(home, ".social-autoposter-mcp");
|
|
143
|
-
const channel = CHANNEL_STAGING ? "staging" : "stable";
|
|
144
|
-
try {
|
|
145
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
146
|
-
writeJson(path.join(stateDir, "channel.json"), { channel });
|
|
147
|
-
console.log(`✓ Release channel set to: ${channel}`);
|
|
148
|
-
} catch (err) {
|
|
149
|
-
console.error(`✗ Failed to set channel: ${err.message}`);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
138
|
console.log("");
|
|
154
139
|
if (ok === targets.length) {
|
|
155
140
|
if (UNINSTALL) {
|
package/mcp/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"dxt_version": "0.1",
|
|
3
3
|
"name": "social-autoposter",
|
|
4
4
|
"display_name": "S4L",
|
|
5
|
-
"version": "1.7.1-rc.
|
|
5
|
+
"version": "1.7.1-rc.9",
|
|
6
6
|
"description": "Draft, review, approve, and autopilot X/Twitter posts.",
|
|
7
7
|
"long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
|
|
8
8
|
"author": {
|
|
@@ -132,20 +132,24 @@ except Exception as _import_err:
|
|
|
132
132
|
import s4l_state as st # noqa: E402
|
|
133
133
|
|
|
134
134
|
# AppKit is available in the owned venv (PyObjC is a rumps dependency). We use it
|
|
135
|
-
#
|
|
136
|
-
#
|
|
137
|
-
#
|
|
138
|
-
#
|
|
139
|
-
# menu bar — the alert still shows, just possibly not front-most.
|
|
135
|
+
# to pull the accessory (LSUIElement) app to the front AND to force the alert
|
|
136
|
+
# window itself above whatever else is on screen. Guarded so a missing AppKit
|
|
137
|
+
# never breaks the menu bar — the alert still shows, just possibly not
|
|
138
|
+
# front-most.
|
|
140
139
|
try:
|
|
141
|
-
from AppKit import NSApplication # noqa: E402
|
|
140
|
+
from AppKit import NSApplication, NSAlert, NSFloatingWindowLevel # noqa: E402
|
|
142
141
|
except Exception:
|
|
143
142
|
NSApplication = None
|
|
143
|
+
NSAlert = None
|
|
144
|
+
NSFloatingWindowLevel = None
|
|
144
145
|
|
|
145
146
|
|
|
146
147
|
def _activate_front():
|
|
147
|
-
"""Bring this accessory app to the front so the next
|
|
148
|
-
|
|
148
|
+
"""Bring this accessory app to the front so the next alert opens on top of
|
|
149
|
+
whatever was frontmost, instead of behind it. Best-effort; kept as a
|
|
150
|
+
belt-and-suspenders call alongside _show_alert's own window-level fix
|
|
151
|
+
below, since activation also affects which app is "key" for keyboard
|
|
152
|
+
input on the alert's buttons."""
|
|
149
153
|
try:
|
|
150
154
|
if NSApplication is not None:
|
|
151
155
|
NSApplication.sharedApplication().activateIgnoringOtherApps_(True)
|
|
@@ -153,6 +157,44 @@ def _activate_front():
|
|
|
153
157
|
pass
|
|
154
158
|
|
|
155
159
|
|
|
160
|
+
def _show_alert(title=None, message='', ok=None, cancel=None, other=None):
|
|
161
|
+
"""Like rumps.alert(), but actually shows up on top.
|
|
162
|
+
|
|
163
|
+
rumps.alert() builds an NSAlert and immediately calls runModal() with no
|
|
164
|
+
window-level or ordering call at all — it just assumes the app is already
|
|
165
|
+
active. activateIgnoringOtherApps_ (_activate_front, above) is async, so
|
|
166
|
+
it can lose the race against runModal()'s own (assumed-active) window
|
|
167
|
+
presentation: the alert window is created at the default level and never
|
|
168
|
+
explicitly raised, so when this accessory app ISN'T active (e.g. Claude
|
|
169
|
+
Desktop is frontmost), it renders BEHIND Claude's window instead of on
|
|
170
|
+
top of it. Confirmed recurring 2026-07-07 (uninstall modal hidden behind
|
|
171
|
+
Claude Desktop) despite the activation call landing 2026-06-30.
|
|
172
|
+
|
|
173
|
+
Fix: build the NSAlert ourselves (same call rumps.alert uses) and force
|
|
174
|
+
its window to NSFloatingWindowLevel + orderFrontRegardless() BEFORE
|
|
175
|
+
runModal(), mirroring the pattern s4l_card.py's review panel already uses
|
|
176
|
+
successfully (see _ReviewPanel._build: setLevel_(NSFloatingWindowLevel) +
|
|
177
|
+
orderFrontRegardless()). Floating level sits above normal app windows in
|
|
178
|
+
the window server's global z-order regardless of app-activation state,
|
|
179
|
+
so it doesn't depend on the activation race resolving in time."""
|
|
180
|
+
_activate_front()
|
|
181
|
+
if NSAlert is None:
|
|
182
|
+
# AppKit unavailable in this venv: fall back to rumps' plain alert.
|
|
183
|
+
return rumps.alert(title=title, message=message, ok=ok, cancel=cancel, other=other)
|
|
184
|
+
msg = str(message).replace('%', '%%')
|
|
185
|
+
if not isinstance(cancel, str):
|
|
186
|
+
cancel = 'Cancel' if cancel else None
|
|
187
|
+
alert = NSAlert.alertWithMessageText_defaultButton_alternateButton_otherButton_informativeTextWithFormat_(
|
|
188
|
+
title, ok, cancel, other, msg)
|
|
189
|
+
alert.setAlertStyle_(0) # same style rumps.alert() uses
|
|
190
|
+
try:
|
|
191
|
+
alert.window().setLevel_(NSFloatingWindowLevel)
|
|
192
|
+
alert.window().orderFrontRegardless()
|
|
193
|
+
except Exception:
|
|
194
|
+
pass
|
|
195
|
+
return alert.runModal()
|
|
196
|
+
|
|
197
|
+
|
|
156
198
|
CLAUDE_APP = "Claude"
|
|
157
199
|
|
|
158
200
|
|
|
@@ -595,7 +637,7 @@ class S4LMenuBar(rumps.App):
|
|
|
595
637
|
msg = "Click OK, then open Claude and ask it to " + action_desc + "."
|
|
596
638
|
try:
|
|
597
639
|
_activate_front()
|
|
598
|
-
|
|
640
|
+
_show_alert(title=title, message=msg, ok="OK")
|
|
599
641
|
except Exception:
|
|
600
642
|
self._notify("S4L · prompt copied" if copied else "S4L",
|
|
601
643
|
"Paste the prompt into Claude (Cmd+V) and press Enter.")
|
|
@@ -666,7 +708,7 @@ class S4LMenuBar(rumps.App):
|
|
|
666
708
|
also run the registry self-heal, which additionally repairs the
|
|
667
709
|
account-switch orphan case, so this one click covers both known causes."""
|
|
668
710
|
_activate_front()
|
|
669
|
-
choice =
|
|
711
|
+
choice = _show_alert(
|
|
670
712
|
title="Restart Claude Desktop?",
|
|
671
713
|
message=(
|
|
672
714
|
"Claude’s scheduler stopped running the draft tasks (a known "
|
|
@@ -994,7 +1036,7 @@ class S4LMenuBar(rumps.App):
|
|
|
994
1036
|
# ok=1 (plugin reset, keeps X login + browser layer), other=-1 (deep wipe),
|
|
995
1037
|
# cancel=0. See rumps.alert: default=1, alternate=0, other=-1.
|
|
996
1038
|
_activate_front()
|
|
997
|
-
choice =
|
|
1039
|
+
choice = _show_alert(
|
|
998
1040
|
title="Uninstall S4L?",
|
|
999
1041
|
message=(
|
|
1000
1042
|
"This quits Claude Desktop, removes the S4L extension + its "
|
|
@@ -1037,7 +1079,7 @@ class S4LMenuBar(rumps.App):
|
|
|
1037
1079
|
def _alert(self, title, message):
|
|
1038
1080
|
try:
|
|
1039
1081
|
_activate_front()
|
|
1040
|
-
|
|
1082
|
+
_show_alert(title=title, message=message, ok="OK")
|
|
1041
1083
|
except Exception:
|
|
1042
1084
|
pass
|
|
1043
1085
|
|
|
@@ -1072,7 +1114,7 @@ class S4LMenuBar(rumps.App):
|
|
|
1072
1114
|
modal FIRST that Claude Desktop will restart, since the app window will
|
|
1073
1115
|
close and reopen under them."""
|
|
1074
1116
|
_activate_front()
|
|
1075
|
-
choice =
|
|
1117
|
+
choice = _show_alert(
|
|
1076
1118
|
title="Quit the S4L autoposter?",
|
|
1077
1119
|
message=(
|
|
1078
1120
|
"Quitting stops the autoposter completely: the draft + query "
|
|
@@ -1758,16 +1800,14 @@ class S4LMenuBar(rumps.App):
|
|
|
1758
1800
|
self._cwd_healed = True
|
|
1759
1801
|
return
|
|
1760
1802
|
_activate_front()
|
|
1761
|
-
choice =
|
|
1762
|
-
title="Tidy
|
|
1803
|
+
choice = _show_alert(
|
|
1804
|
+
title="Tidy S4L background tasks?",
|
|
1763
1805
|
message=(
|
|
1764
|
-
"
|
|
1765
|
-
"
|
|
1766
|
-
"
|
|
1767
|
-
"
|
|
1768
|
-
"
|
|
1769
|
-
"close and reopen in a moment. Your X login, drafts, and config "
|
|
1770
|
-
"all stay."
|
|
1806
|
+
"Move the background worker to a dedicated folder so its "
|
|
1807
|
+
"once-a-minute runs stop cluttering your `claude --resume` "
|
|
1808
|
+
"history.\n\n"
|
|
1809
|
+
"Claude Desktop will restart once to apply. Your X login, "
|
|
1810
|
+
"drafts, and config all stay."
|
|
1771
1811
|
),
|
|
1772
1812
|
ok="Tidy & restart Claude", cancel="Later",
|
|
1773
1813
|
)
|
package/mcp/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@m13v/s4l-mcp",
|
|
3
|
-
"version": "1.7.1-rc.
|
|
3
|
+
"version": "1.7.1-rc.9",
|
|
4
4
|
"private": true,
|
|
5
5
|
"description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
|
|
6
6
|
"license": "MIT",
|
package/mcp/shared/doctor.cjs
CHANGED
|
@@ -149,6 +149,14 @@ function runDoctorSync(options = {}) {
|
|
|
149
149
|
"deferred until X connection to avoid an unexpected keychain prompt"
|
|
150
150
|
);
|
|
151
151
|
}
|
|
152
|
+
// Once the durable cookie mirror is populated, the X session no longer
|
|
153
|
+
// depends on this keychain item (see twitter_cookie_mirror.py — it exists
|
|
154
|
+
// precisely so a locked/missing Safe Storage key isn't fatal). Skip the
|
|
155
|
+
// probe instead of re-triggering + re-failing it on every doctor run;
|
|
156
|
+
// same mirrorCount() gate keychain_autolock already uses below.
|
|
157
|
+
if (mirrorCount() > 0) {
|
|
158
|
+
return result("pass", "skipped — durable cookie mirror already covers this");
|
|
159
|
+
}
|
|
152
160
|
const r = spawnSync(
|
|
153
161
|
"security",
|
|
154
162
|
[
|
package/package.json
CHANGED
|
@@ -1,67 +1,50 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
2
|
+
# Print (or download) the permanent download link for the latest S4L staging
|
|
3
|
+
# release (.mcpb).
|
|
4
|
+
#
|
|
3
5
|
# Usage:
|
|
4
|
-
# bash scripts/get-latest-staging-mcpb.sh [--download] [--channel]
|
|
6
|
+
# bash scripts/get-latest-staging-mcpb.sh [--download] [--channel]
|
|
5
7
|
#
|
|
6
8
|
# Options:
|
|
7
|
-
# --download Download the .mcpb to current directory (instead of
|
|
8
|
-
#
|
|
9
|
-
# --
|
|
9
|
+
# --download Download the .mcpb to the current directory (instead of
|
|
10
|
+
# just printing the URL)
|
|
11
|
+
# --channel After downloading, opt this machine into the staging
|
|
12
|
+
# channel (delegates to scripts/s4l_channel.py, the single
|
|
13
|
+
# source of truth for the channel marker)
|
|
14
|
+
#
|
|
15
|
+
# The link below is a FIXED URL, not a live GitHub API lookup: every
|
|
16
|
+
# `bash scripts/release-mcpb.sh --staging` run mirrors its .mcpb onto a
|
|
17
|
+
# separate, non-version-tagged release ("staging-latest") so this exact URL
|
|
18
|
+
# always serves whatever's newest — see scripts/release-mcpb.sh step 7b. That
|
|
19
|
+
# means this script needs zero network calls to resolve anything; it only
|
|
20
|
+
# hits the network when actually downloading.
|
|
10
21
|
|
|
11
22
|
set -euo pipefail
|
|
12
23
|
|
|
24
|
+
STAGING_MCPB_URL="https://github.com/m13v/s4l/releases/download/staging-latest/social-autoposter.mcpb"
|
|
25
|
+
|
|
13
26
|
DO_DOWNLOAD=0
|
|
14
27
|
DO_CHANNEL=0
|
|
15
|
-
DO_PATH=0
|
|
16
28
|
|
|
17
29
|
while [[ $# -gt 0 ]]; do
|
|
18
30
|
case "$1" in
|
|
19
31
|
--download) DO_DOWNLOAD=1; shift ;;
|
|
20
32
|
--channel) DO_CHANNEL=1; shift ;;
|
|
21
|
-
|
|
22
|
-
*) echo "Unknown option: $1"; exit 1 ;;
|
|
33
|
+
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
|
23
34
|
esac
|
|
24
35
|
done
|
|
25
36
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
RELEASE_DATA=$(curl -fsS "https://api.github.com/repos/m13v/s4l/releases?per_page=1&pre_release=true" 2>/dev/null | python3 -c "import sys, json; data=json.load(sys.stdin); r=data[0] if data else {}; print(json.dumps(r))")
|
|
29
|
-
|
|
30
|
-
# Extract version and download URL using python for reliable JSON parsing
|
|
31
|
-
LATEST_TAG=$(echo "$RELEASE_DATA" | python3 -c "import sys, json; data=json.load(sys.stdin); print(data.get('tag_name', ''))" 2>/dev/null)
|
|
32
|
-
MCPB_URL=$(echo "$RELEASE_DATA" | python3 -c "
|
|
33
|
-
import sys, json
|
|
34
|
-
data = json.load(sys.stdin)
|
|
35
|
-
for asset in data.get('assets', []):
|
|
36
|
-
if 'social-autoposter.mcpb' in asset.get('name', ''):
|
|
37
|
-
print(asset.get('browser_download_url', ''))
|
|
38
|
-
break
|
|
39
|
-
" 2>/dev/null)
|
|
40
|
-
|
|
41
|
-
if [ -z "$LATEST_TAG" ] || [ -z "$MCPB_URL" ]; then
|
|
42
|
-
echo "Error: Could not find latest staging release" >&2
|
|
43
|
-
exit 1
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
if [ "$DO_PATH" -eq 1 ]; then
|
|
47
|
-
# Return the tag only (for scripting)
|
|
48
|
-
echo "$LATEST_TAG"
|
|
37
|
+
if [ "$DO_DOWNLOAD" -eq 0 ]; then
|
|
38
|
+
echo "$STAGING_MCPB_URL"
|
|
49
39
|
exit 0
|
|
50
40
|
fi
|
|
51
41
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
echo "$FILENAME"
|
|
42
|
+
FILENAME="social-autoposter-staging.mcpb"
|
|
43
|
+
echo "Downloading $FILENAME from $STAGING_MCPB_URL" >&2
|
|
44
|
+
curl -fL "$STAGING_MCPB_URL" -o "$FILENAME"
|
|
45
|
+
echo "$FILENAME"
|
|
57
46
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
echo '{"channel":"staging"}' > "$STATE_DIR/channel.json"
|
|
62
|
-
echo "✓ Channel set to staging" >&2
|
|
63
|
-
fi
|
|
64
|
-
else
|
|
65
|
-
# Just print the URL
|
|
66
|
-
echo "$MCPB_URL"
|
|
47
|
+
if [ "$DO_CHANNEL" -eq 1 ]; then
|
|
48
|
+
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
49
|
+
python3 "$REPO_ROOT/scripts/s4l_channel.py" set staging >&2
|
|
67
50
|
fi
|
package/scripts/release-mcpb.sh
CHANGED
|
@@ -45,6 +45,16 @@
|
|
|
45
45
|
# the EXACT tested artifact ships to everyone. Nothing can drift between test and
|
|
46
46
|
# ship because there is no repack.
|
|
47
47
|
#
|
|
48
|
+
# FIXED STAGING DOWNLOAD LINK (2026-07-07, step 7b): stable has a permanent
|
|
49
|
+
# "latest" URL (releases/latest/download/social-autoposter.mcpb); staging did
|
|
50
|
+
# not, because GitHub's releases/latest hard-excludes prereleases with no
|
|
51
|
+
# override. Every --staging run now also mirrors the .mcpb onto a separate,
|
|
52
|
+
# non-version-tagged release ("staging-latest") so this URL never changes:
|
|
53
|
+
# https://github.com/m13v/s4l/releases/download/staging-latest/social-autoposter.mcpb
|
|
54
|
+
# The tag deliberately isn't semver-shaped, so version.ts/snapshot.py's real
|
|
55
|
+
# "newest rc" resolution (which requires a ^\d+\.\d+\.\d+ tag) ignores it —
|
|
56
|
+
# it is a convenience download link only, never a candidate update source.
|
|
57
|
+
#
|
|
48
58
|
# Usage:
|
|
49
59
|
# bash scripts/release-mcpb.sh # patch bump, npm + .mcpb + GitHub (STABLE)
|
|
50
60
|
# bash scripts/release-mcpb.sh --bump minor
|
|
@@ -68,6 +78,12 @@ MCP_DIR="$REPO_ROOT/mcp"
|
|
|
68
78
|
BUNDLE="$MCP_DIR/social-autoposter.mcpb"
|
|
69
79
|
GH_REPO="m13v/s4l"
|
|
70
80
|
SIZE_CAP_MB=180
|
|
81
|
+
# Floating alias release for a fixed "always the newest staging build" download
|
|
82
|
+
# link, mirroring what releases/latest/download/<asset> already gives stable.
|
|
83
|
+
# GitHub's own releases/latest excludes prereleases with no override, so there is
|
|
84
|
+
# no built-in equivalent for the rc channel — see step 7b below for why a
|
|
85
|
+
# non-semver tag here is safe (never confuses the real update-detection code).
|
|
86
|
+
STAGING_ALIAS_TAG="staging-latest"
|
|
71
87
|
|
|
72
88
|
TAG_OVERRIDE=""
|
|
73
89
|
VERSION_OVERRIDE=""
|
|
@@ -496,6 +512,38 @@ say "Released $TAG"
|
|
|
496
512
|
echo " asset: social-autoposter.mcpb (${MB}MB)"
|
|
497
513
|
[[ -n "$URL" ]] && echo " $URL"
|
|
498
514
|
|
|
515
|
+
# ---- 7b. Fixed "always latest staging" download link ------------------------
|
|
516
|
+
# Mirrors the asset just uploaded above onto a SEPARATE release tagged
|
|
517
|
+
# "$STAGING_ALIAS_TAG" (not a version), so the download URL never changes:
|
|
518
|
+
# https://github.com/$GH_REPO/releases/download/$STAGING_ALIAS_TAG/social-autoposter.mcpb
|
|
519
|
+
# Safe by construction: latestFromGithubListStaging() (mcp/src/version.ts) and
|
|
520
|
+
# _latest_from_github_list_staging() (scripts/snapshot.py) both require
|
|
521
|
+
# tag_name to match ^\d+\.\d+\.\d+ before treating a release as a candidate
|
|
522
|
+
# "newest" build; a tag literally named "staging-latest" fails that check and
|
|
523
|
+
# is silently skipped by both, so this alias can never get mistaken for a real
|
|
524
|
+
# rc and can never confuse which build a box installs. It exists ONLY to give
|
|
525
|
+
# humans (and scripts) one fixed bookmark — actual boxes still resolve "newest"
|
|
526
|
+
# from the real per-release rc tags via the releases LIST endpoint, unchanged.
|
|
527
|
+
if [[ "$DO_STAGING" == "1" ]]; then
|
|
528
|
+
say "Mirroring asset onto the fixed staging-latest alias"
|
|
529
|
+
ALIAS_NOTES="Always mirrors the newest staging pre-release's .mcpb asset (currently $TAG).
|
|
530
|
+
|
|
531
|
+
Fixed download link (never changes): https://github.com/$GH_REPO/releases/download/$STAGING_ALIAS_TAG/social-autoposter.mcpb
|
|
532
|
+
|
|
533
|
+
Not a real release — do not install from the tag page; use the asset link above. Actual per-build releases are tagged by version (e.g. $TAG)."
|
|
534
|
+
if gh release view "$STAGING_ALIAS_TAG" -R "$GH_REPO" >/dev/null 2>&1; then
|
|
535
|
+
gh release upload "$STAGING_ALIAS_TAG" "$BUNDLE" -R "$GH_REPO" --clobber
|
|
536
|
+
gh release edit "$STAGING_ALIAS_TAG" -R "$GH_REPO" --notes "$ALIAS_NOTES" >/dev/null
|
|
537
|
+
else
|
|
538
|
+
gh release create "$STAGING_ALIAS_TAG" "$BUNDLE" \
|
|
539
|
+
-R "$GH_REPO" \
|
|
540
|
+
--title "social-autoposter (staging — always latest)" \
|
|
541
|
+
--notes "$ALIAS_NOTES" \
|
|
542
|
+
--prerelease
|
|
543
|
+
fi
|
|
544
|
+
echo " https://github.com/$GH_REPO/releases/download/$STAGING_ALIAS_TAG/social-autoposter.mcpb"
|
|
545
|
+
fi
|
|
546
|
+
|
|
499
547
|
# ---- 8. Verify the update banner will fire ---------------------------------
|
|
500
548
|
# The menu-bar "⬆ Update available" banner (mcp/src/version.ts::versionStatus)
|
|
501
549
|
# resolves "latest" from GitHub releases/latest, which is what .mcpb boxes (no
|
package/scripts/set-channel.sh
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Set the S4L release channel (stable or staging).
|
|
3
|
-
# Usage:
|
|
4
|
-
# bash scripts/set-channel.sh stable # track stable releases only (default)
|
|
5
|
-
# bash scripts/set-channel.sh staging # track staging releases (pre-releases)
|
|
6
|
-
|
|
7
|
-
set -euo pipefail
|
|
8
|
-
|
|
9
|
-
if [ $# -lt 1 ]; then
|
|
10
|
-
echo "Usage: bash scripts/set-channel.sh <stable|staging>" >&2
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
CHANNEL="$1"
|
|
15
|
-
if [ "$CHANNEL" != "stable" ] && [ "$CHANNEL" != "staging" ]; then
|
|
16
|
-
echo "Error: channel must be 'stable' or 'staging'" >&2
|
|
17
|
-
exit 1
|
|
18
|
-
fi
|
|
19
|
-
|
|
20
|
-
STATE_DIR="${S4L_STATE_DIR:-$HOME/.social-autoposter-mcp}"
|
|
21
|
-
mkdir -p "$STATE_DIR"
|
|
22
|
-
echo "{\"channel\":\"$CHANNEL\"}" > "$STATE_DIR/channel.json"
|
|
23
|
-
|
|
24
|
-
echo "✓ Release channel set to: $CHANNEL"
|
|
25
|
-
if [ "$CHANNEL" = "staging" ]; then
|
|
26
|
-
echo " Will now track pre-releases (rc.N builds). Next update check pulls the latest staging release."
|
|
27
|
-
else
|
|
28
|
-
echo " Will now track stable releases only. Next update check pulls the latest stable release."
|
|
29
|
-
fi
|