@sprid/cli 1.2.2 → 1.2.4
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/CHANGELOG.md +8 -0
- package/package.json +3 -3
- package/src/post/cli.mjs +100 -10
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,14 @@
|
|
|
3
3
|
sprid follows semver: a breaking change to a command's arguments, its output
|
|
4
4
|
shape or its exit codes is a major release.
|
|
5
5
|
|
|
6
|
+
## 1.2.4 - 2026-10-11
|
|
7
|
+
|
|
8
|
+
- `sprid post sync` no longer reports a post as synced when its new media never reached Sprid. A file rebuilt since `register`, or a post whose checks are not passing, is now listed as not synced with the step that fixes it, and the command exits 1. `draft` also reads each slide back and says so when Sprid is still holding the old file.
|
|
9
|
+
|
|
10
|
+
## 1.2.3 - 2026-10-08
|
|
11
|
+
|
|
12
|
+
- `sprid screenshots` carries @sprid/shots 0.2.2: bounded device commands, retries for transient capture errors, and Android cold-boot recovery for capture hooks.
|
|
13
|
+
|
|
6
14
|
## 1.2.2 - 2026-10-08
|
|
7
15
|
|
|
8
16
|
- `sprid screenshots` carries @sprid/shots 0.2.1: native capture saves a verified
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sprid/cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.4",
|
|
4
4
|
"description": "The Sprid command line: connect your app, review results, prepare posts and store screenshots, and release mobile apps.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE",
|
|
6
6
|
"type": "module",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"node": ">=18"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@sprid/shots": "0.2.
|
|
42
|
-
"@sprid/release": "1.2.
|
|
41
|
+
"@sprid/shots": "0.2.2",
|
|
42
|
+
"@sprid/release": "1.2.1"
|
|
43
43
|
},
|
|
44
44
|
"publishConfig": {
|
|
45
45
|
"access": "public"
|
package/src/post/cli.mjs
CHANGED
|
@@ -384,6 +384,14 @@ const fill = (tpl, slug) => tpl.replaceAll("<slug>", slug);
|
|
|
384
384
|
const kindOf = (lane) => lane.kind ?? (lane.slides ? "carousel" : "reel");
|
|
385
385
|
const isComposed = (x) => (x?.kind ?? (x?.slides ? "carousel" : "reel")) === "composed";
|
|
386
386
|
|
|
387
|
+
/** What the media files hash to right now, in the manifest's sha shape. Null if any is missing. */
|
|
388
|
+
function diskSha(cfg, m) {
|
|
389
|
+
const files = (m.kind ?? "reel") === "carousel" ? (m.slides ?? []).map((sl) => sl.file) : [m.source];
|
|
390
|
+
const paths = files.map((f) => (f ? resolve(cfg.root, f) : null));
|
|
391
|
+
if (paths.some((f) => !f || !existsSync(f))) return null;
|
|
392
|
+
return paths.map(sha256).join(",");
|
|
393
|
+
}
|
|
394
|
+
|
|
387
395
|
/** The path template a lane's media lives at, whichever kind it is. */
|
|
388
396
|
const mediaTemplate = (lane) =>
|
|
389
397
|
kindOf(lane) === "composed" ? (lane.spec ?? "social/specs/<slug>.json") : kindOf(lane) === "carousel" ? lane.slides : lane.media;
|
|
@@ -1561,9 +1569,14 @@ async function cmdPush(cfg, argv) {
|
|
|
1561
1569
|
);
|
|
1562
1570
|
if (!targets.length) return console.log("\n nothing to push (everything is already in Sprid)\n");
|
|
1563
1571
|
|
|
1572
|
+
// **What was refused is returned, not only printed.** `sync` calls this and
|
|
1573
|
+
// used to count a slug as synced whatever happened here, so a refused upload
|
|
1574
|
+
// reported "1 synced" while the draft kept the old file.
|
|
1575
|
+
const pushed = [], refused = [];
|
|
1564
1576
|
for (const m of targets) {
|
|
1565
1577
|
if (!checksPass(m)) {
|
|
1566
1578
|
console.log(` ${C.warn("skip")} ${m.slug} ${C.dim("- checks not passing, run `sprid post check`")}`);
|
|
1579
|
+
refused.push({ slug: m.slug, why: "checks not passing - run `sprid post check`" });
|
|
1567
1580
|
continue;
|
|
1568
1581
|
}
|
|
1569
1582
|
// **A composed post has nothing here to upload.** Its pixels are the
|
|
@@ -1574,18 +1587,21 @@ async function cmdPush(cfg, argv) {
|
|
|
1574
1587
|
continue;
|
|
1575
1588
|
}
|
|
1576
1589
|
if ((m.kind ?? "reel") === "carousel") {
|
|
1577
|
-
await pushCarousel(cfg, m);
|
|
1590
|
+
const why = await pushCarousel(cfg, m);
|
|
1591
|
+
(why ? refused : pushed).push(why ? { slug: m.slug, why } : m.slug);
|
|
1578
1592
|
continue;
|
|
1579
1593
|
}
|
|
1580
1594
|
const file = resolve(cfg.root, m.source);
|
|
1581
1595
|
if (!existsSync(file)) {
|
|
1582
1596
|
console.log(` ${C.bad("gone")} ${m.slug} ${C.dim(`- ${m.source} is not on disk; rebuild it`)}`);
|
|
1597
|
+
refused.push({ slug: m.slug, why: `${m.source} is not on disk` });
|
|
1583
1598
|
continue;
|
|
1584
1599
|
}
|
|
1585
1600
|
// Guard the record against a silent rebuild: the manifest is only true of
|
|
1586
1601
|
// the bytes it measured.
|
|
1587
1602
|
if (sha256(file) !== m.media.sha256) {
|
|
1588
1603
|
console.log(` ${C.bad("stale")} ${m.slug} ${C.dim("- the file changed since it was registered; re-register it")}`);
|
|
1604
|
+
refused.push({ slug: m.slug, why: "the file changed since `register` - run `register`, then `check`" });
|
|
1589
1605
|
continue;
|
|
1590
1606
|
}
|
|
1591
1607
|
|
|
@@ -1633,9 +1649,11 @@ async function cmdPush(cfg, argv) {
|
|
|
1633
1649
|
// scheduled post looking finished. Record the exact bytes uploaded.
|
|
1634
1650
|
m.sprid = { ...m.sprid, account: cfg.account, videoId, status: "uploaded", mediaShaAtPush: m.media.sha256 };
|
|
1635
1651
|
writeManifest(cfg, m);
|
|
1652
|
+
pushed.push(m.slug);
|
|
1636
1653
|
console.log(` ${C.ok("pushed")} ${m.slug.padEnd(40)} video ${videoId}`);
|
|
1637
1654
|
}
|
|
1638
1655
|
console.log();
|
|
1656
|
+
return { pushed, refused };
|
|
1639
1657
|
}
|
|
1640
1658
|
|
|
1641
1659
|
/**
|
|
@@ -1651,12 +1669,12 @@ async function pushCarousel(cfg, m) {
|
|
|
1651
1669
|
const missing = files.filter((f) => !existsSync(f));
|
|
1652
1670
|
if (missing.length) {
|
|
1653
1671
|
console.log(` ${C.bad("gone")} ${m.slug} ${C.dim(`- ${missing.length} slide(s) not on disk; rebuild it`)}`);
|
|
1654
|
-
return
|
|
1672
|
+
return `${missing.length} slide(s) not on disk`;
|
|
1655
1673
|
}
|
|
1656
1674
|
const changed = files.filter((f, i) => sha256(f) !== m.slides[i].sha256);
|
|
1657
1675
|
if (changed.length) {
|
|
1658
1676
|
console.log(` ${C.bad("stale")} ${m.slug} ${C.dim(`- ${changed.length} slide(s) changed since registering; re-register it`)}`);
|
|
1659
|
-
return
|
|
1677
|
+
return `${changed.length} slide(s) changed since \`register\` - run \`register\`, then \`check\``;
|
|
1660
1678
|
}
|
|
1661
1679
|
|
|
1662
1680
|
const imageIds = [];
|
|
@@ -1699,6 +1717,7 @@ async function pushCarousel(cfg, m) {
|
|
|
1699
1717
|
};
|
|
1700
1718
|
writeManifest(cfg, m);
|
|
1701
1719
|
console.log(` ${C.ok("pushed")} ${m.slug.padEnd(40)} ${imageIds.length} images`);
|
|
1720
|
+
return null;
|
|
1702
1721
|
}
|
|
1703
1722
|
|
|
1704
1723
|
/** A 400px-wide copy, or null if ffmpeg will not make one. */
|
|
@@ -1725,6 +1744,7 @@ async function cmdDraft(cfg, argv) {
|
|
|
1725
1744
|
);
|
|
1726
1745
|
if (!targets.length) return console.log("\n nothing to draft (push first, or everything is drafted)\n");
|
|
1727
1746
|
|
|
1747
|
+
const drafted = [], refused = [];
|
|
1728
1748
|
for (const m of targets) {
|
|
1729
1749
|
// **`push` refused a failing post and `draft` did not, which left the gate
|
|
1730
1750
|
// ungated.** For a composed post there is nothing to push at all, so
|
|
@@ -1734,6 +1754,7 @@ async function cmdDraft(cfg, argv) {
|
|
|
1734
1754
|
// preference.
|
|
1735
1755
|
if (!checksPass(m)) {
|
|
1736
1756
|
console.log(` ${C.warn("skip")} ${m.slug} ${C.dim("- checks not passing, run `sprid post check`")}`);
|
|
1757
|
+
refused.push({ slug: m.slug, why: "checks not passing - run `sprid post check`" });
|
|
1737
1758
|
continue;
|
|
1738
1759
|
}
|
|
1739
1760
|
const caption = existsSync(captionPath(cfg, m.slug))
|
|
@@ -1858,8 +1879,16 @@ async function cmdDraft(cfg, argv) {
|
|
|
1858
1879
|
// with a second music bed laid over its own - which is the exact failure
|
|
1859
1880
|
// the field exists to prevent. The consuming repo has no way to know that
|
|
1860
1881
|
// from the output, so the record is read back and compared.
|
|
1861
|
-
const
|
|
1882
|
+
const back = await api(cfg, "GET", `/posts/${post.id}`);
|
|
1883
|
+
const dropped = verifyDraft(back, wanted);
|
|
1862
1884
|
for (const [field, why] of dropped) console.log(` ${C.bad("dropped")} ${m.slug.padEnd(40)} ${field} - ${why}`);
|
|
1885
|
+
// **The media is read back too.** Same reason as the fields above: the
|
|
1886
|
+
// slide PATCH answering 200 is not proof the post now shows the new file.
|
|
1887
|
+
const mediaDrift = composed ? null : slideMediaDrift(back, carousel ? m.sprid.imageIds ?? [] : [m.sprid.videoId], carousel);
|
|
1888
|
+
if (mediaDrift) {
|
|
1889
|
+
console.log(` ${C.bad("media")} ${m.slug.padEnd(40)} ${mediaDrift}`);
|
|
1890
|
+
refused.push({ slug: m.slug, why: mediaDrift });
|
|
1891
|
+
}
|
|
1863
1892
|
|
|
1864
1893
|
m.sprid = {
|
|
1865
1894
|
...m.sprid,
|
|
@@ -1872,12 +1901,28 @@ async function cmdDraft(cfg, argv) {
|
|
|
1872
1901
|
dropped: dropped.length ? dropped.map(([f]) => f) : null,
|
|
1873
1902
|
};
|
|
1874
1903
|
writeManifest(cfg, m);
|
|
1904
|
+
if (mediaDrift) continue;
|
|
1905
|
+
drafted.push(m.slug);
|
|
1875
1906
|
console.log(
|
|
1876
1907
|
` ${C.ok(existingPost ? "updated" : "drafted")} ${m.slug.padEnd(40)} post ${post.id}` +
|
|
1877
1908
|
(composed ? C.dim(` ${seats.length} cards`) : carousel ? C.dim(` ${m.sprid.imageIds.length} slides`) : ""),
|
|
1878
1909
|
);
|
|
1879
1910
|
}
|
|
1880
1911
|
console.log(`\n ${C.dim("review in Sprid, then move each to `ready` yourself.")}\n`);
|
|
1912
|
+
return { drafted, refused };
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
/**
|
|
1916
|
+
* Whether the post's slides carry the files the registry pushed, in order.
|
|
1917
|
+
* Null when they do; otherwise what a person needs to read.
|
|
1918
|
+
*/
|
|
1919
|
+
export function slideMediaDrift(back, ids, carousel) {
|
|
1920
|
+
const slides = [...(back.slides ?? [])].sort((a, b) => a.position - b.position);
|
|
1921
|
+
const held = slides.map((sl) => (carousel ? sl.imageId : sl.videoId) ?? null);
|
|
1922
|
+
if (held.length === ids.length && held.every((id, i) => id === ids[i])) return null;
|
|
1923
|
+
return carousel
|
|
1924
|
+
? `Sprid's slides hold images ${held.join(",") || "none"}, not the pushed ${ids.join(",")}`
|
|
1925
|
+
: `Sprid's slide holds video ${held[0] ?? "none"}, not the pushed ${ids[0]}`;
|
|
1881
1926
|
}
|
|
1882
1927
|
|
|
1883
1928
|
/**
|
|
@@ -2007,7 +2052,18 @@ async function cmdSync(cfg, argv) {
|
|
|
2007
2052
|
: (m.kind ?? "reel") === "carousel"
|
|
2008
2053
|
? (m.slides ?? []).map((sl) => sl.sha256).join(",")
|
|
2009
2054
|
: m.media?.sha256 ?? null;
|
|
2010
|
-
|
|
2055
|
+
// **The registry's sha is not the file.** A rebuild that was never
|
|
2056
|
+
// re-registered leaves the manifest agreeing with what was pushed, and sync
|
|
2057
|
+
// answered "in sync" while the drafts held the old video. So the bytes on
|
|
2058
|
+
// disk are hashed too, and a mismatch is reported as the step it needs.
|
|
2059
|
+
// A file no longer on disk proves nothing either way, so it falls back to
|
|
2060
|
+
// the record: scratch builds get cleaned up after a push.
|
|
2061
|
+
const onDisk = isComposed(m) ? null : diskSha(cfg, m);
|
|
2062
|
+
const media = isComposed(m)
|
|
2063
|
+
? false
|
|
2064
|
+
: onDisk != null && onDisk !== localSha
|
|
2065
|
+
? "unregistered"
|
|
2066
|
+
: pushedSha == null ? "unknown" : pushedSha !== localSha;
|
|
2011
2067
|
const caption = (m.post?.captionSha ?? null) !== (m.sprid.captionShaAtDraft ?? null);
|
|
2012
2068
|
// The Pin's board, link and words. Unrecorded counts as changed, same as
|
|
2013
2069
|
// media: a record that cannot say what was sent cannot say it is current.
|
|
@@ -2024,7 +2080,10 @@ async function cmdSync(cfg, argv) {
|
|
|
2024
2080
|
console.log(`\n ${C.bold(execute ? "syncing" : "would sync")} ${plan.length} of ${all.length}\n`);
|
|
2025
2081
|
for (const { m, media, caption, title, pin, unreadable } of plan) {
|
|
2026
2082
|
const what = [
|
|
2027
|
-
media === "
|
|
2083
|
+
media === "unregistered"
|
|
2084
|
+
? C.bad("media (file on disk changed since `register`)")
|
|
2085
|
+
: media === "unknown" ? C.warn("media (never recorded, assumed stale)") : media ? C.warn("media") : null,
|
|
2086
|
+
!checksPass(m) ? C.bad("checks not passing") : null,
|
|
2028
2087
|
caption ? C.warn("caption") : null,
|
|
2029
2088
|
title ? C.warn(unreadable ? "title (could not read Sprid's, assumed stale)" : "title") : null,
|
|
2030
2089
|
pin ? C.warn("pin") : null,
|
|
@@ -2038,7 +2097,20 @@ async function cmdSync(cfg, argv) {
|
|
|
2038
2097
|
|
|
2039
2098
|
console.log();
|
|
2040
2099
|
let done = 0, skipped = 0;
|
|
2100
|
+
const failed = [];
|
|
2041
2101
|
for (const { m, media } of plan) {
|
|
2102
|
+
// Refused before touching Sprid: pushing or drafting would either do
|
|
2103
|
+
// nothing or re-send the old file, and both used to be counted as synced.
|
|
2104
|
+
if (media === "unregistered") {
|
|
2105
|
+
console.log(` ${C.bad("not synced")} ${m.slug.padEnd(36)} ${C.dim("file changed since `register` - run `register`, then `check`")}`);
|
|
2106
|
+
failed.push({ slug: m.slug, why: "file changed since `register`" });
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
if (!checksPass(m)) {
|
|
2110
|
+
console.log(` ${C.bad("not synced")} ${m.slug.padEnd(36)} ${C.dim("checks not passing - run `check`")}`);
|
|
2111
|
+
failed.push({ slug: m.slug, why: "checks not passing" });
|
|
2112
|
+
continue;
|
|
2113
|
+
}
|
|
2042
2114
|
// The live status decides whether this one may be touched at all. Read it
|
|
2043
2115
|
// here rather than trusting the manifest: the manifest's `status` is what
|
|
2044
2116
|
// this machine last wrote, and the queue moves without us.
|
|
@@ -2059,13 +2131,31 @@ async function cmdSync(cfg, argv) {
|
|
|
2059
2131
|
skipped++;
|
|
2060
2132
|
continue;
|
|
2061
2133
|
}
|
|
2062
|
-
if (media)
|
|
2134
|
+
if (media) {
|
|
2135
|
+
const pushed = await cmdPush(cfg, [m.slug, "--force"]);
|
|
2136
|
+
if (!pushed?.pushed.includes(m.slug)) {
|
|
2137
|
+
const why = pushed?.refused.find((r) => r.slug === m.slug)?.why ?? "nothing was uploaded";
|
|
2138
|
+
console.log(` ${C.bad("not synced")} ${m.slug.padEnd(36)} ${C.dim(why)}`);
|
|
2139
|
+
failed.push({ slug: m.slug, why });
|
|
2140
|
+
continue;
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2063
2143
|
// Re-read: push rewrote the manifest, and draft has to send the new videoId.
|
|
2064
|
-
await cmdDraft(cfg, [readManifest(cfg, m.slug).slug, "--force"]);
|
|
2144
|
+
const drafted = await cmdDraft(cfg, [readManifest(cfg, m.slug).slug, "--force"]);
|
|
2145
|
+
if (!drafted?.drafted.includes(m.slug)) {
|
|
2146
|
+
const why = drafted?.refused.find((r) => r.slug === m.slug)?.why ?? "the draft was not updated";
|
|
2147
|
+
console.log(` ${C.bad("not synced")} ${m.slug.padEnd(36)} ${C.dim(why)}`);
|
|
2148
|
+
failed.push({ slug: m.slug, why });
|
|
2149
|
+
continue;
|
|
2150
|
+
}
|
|
2065
2151
|
done++;
|
|
2066
2152
|
}
|
|
2067
|
-
console.log(` ${done} synced, ${skipped} left alone\n`);
|
|
2068
|
-
return {
|
|
2153
|
+
console.log(` ${done} synced, ${skipped} left alone${failed.length ? `, ${C.bad(`${failed.length} not synced`)}` : ""}\n`);
|
|
2154
|
+
return {
|
|
2155
|
+
exitCode: failed.length ? 1 : 0,
|
|
2156
|
+
execute, synced: done, skipped, failed,
|
|
2157
|
+
plan: plan.map(({ m, ...change }) => ({ slug: m.slug, ...change })),
|
|
2158
|
+
};
|
|
2069
2159
|
}
|
|
2070
2160
|
|
|
2071
2161
|
/**
|