@slopperday/cli 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @slopperday/cli
2
2
 
3
- Throw slop at the wall from your terminal. `slopper` launches, updates and votes on
3
+ Too much slop? Not enough yet! Throw it at the wall from your terminal: `slopper` launches, updates and votes on
4
4
  [slopper.day](https://slopper.day) through its API, and prints JSON, so it's built for agents as much as people.
5
5
 
6
6
  ## Run it
package/dist/slopper.mjs CHANGED
@@ -33,7 +33,7 @@ function openInBrowser(url) {
33
33
  }
34
34
  //#endregion
35
35
  //#region package.json
36
- var version = "0.1.1";
36
+ var version = "0.1.2";
37
37
  //#endregion
38
38
  //#region src/client.ts
39
39
  /**
@@ -188,6 +188,7 @@ function createApi({ url, token, fetch, userAgent }) {
188
188
  get: (path) => request("GET", path),
189
189
  post: (path, body) => request("POST", path, JSON.stringify(body), "application/json"),
190
190
  patch: (path, body) => request("PATCH", path, JSON.stringify(body), "application/json"),
191
+ put: (path, body) => request("PUT", path, JSON.stringify(body), "application/json"),
191
192
  delete: (path) => request("DELETE", path),
192
193
  upload: (path, bytes, contentType) => request("POST", path, bytes, contentType)
193
194
  };
@@ -195,7 +196,7 @@ function createApi({ url, token, fetch, userAgent }) {
195
196
  //#endregion
196
197
  //#region src/skill.ts
197
198
  async function readSkill() {
198
- return "---\nname: slopper\ndescription: Ship to slopper.day from the terminal with the `slopper` CLI (npm @slopperday/cli). Use when the user wants to launch a project as today's slop or schedule launches for later days, post an update or field test, set a daily shipping goal, log costs or revenue, check today's slop of the day race, or vote on slops.\n---\n\n# Slopper\n\n[slopper.day](https://slopper.day) is where people throw slop at the wall every day: they ship something small\ndaily (a site, a TestFlight or Play beta, a repo), set a goal like 30 slops in 30 days, and keep slopping until one is\nslopcessful. Every UTC day the slops launched that day race on votes; the leader at midnight UTC wins slop of the day\nand goes slopcessful. Slopcessful is earned, never declared: nobody can mark their own slop slopcessful.\n\n## Setup\n\nRun the CLI without installing it, with the runner the project already uses:\n\n```sh\nnpx -y @slopperday/cli <command> # -y, or npx stops to ask \"Ok to proceed?\" and waits\npnpm dlx @slopperday/cli <command>\nbunx @slopperday/cli <command>\nyarn dlx -q @slopperday/cli <command> # -q, or yarn's install log lands in the JSON\ndeno run -A npm:@slopperday/cli <command>\n```\n\nOr install it once (`npm i -g @slopperday/cli`) and run `slopper`. The examples below say `slopper`: put the runner's\ncommand in its place.\n\nIt needs to be signed in as the user. Run `slopper login` with no token: it opens their browser on a one-time code and\nwaits (up to ten minutes) until they approve it, then saves a token of its own. Tell the user to check the code matches\nand click Approve. If no browser can open (a remote or sandboxed machine), use `--no-browser`, run it in the\nbackground and give the user the link it prints on stderr. If they hand you a token instead, use it:\n\n```sh\nslopper login # browser sign-in; saves a token readable by the user only\nslopper login --token slp_... # or check and save a token from /settings/api\nSLOPPER_TOKEN=slp_... slopper whoami # saves nothing: use this when you shouldn't write files\n```\n\n`--url` (or `SLOPPER_URL`) points it at another site, like a local one.\n\nEvery command prints JSON on stdout. Errors go to stderr with exit code 1 (2 for a usage mistake), and validation\nerrors list each field that needs fixing. `slopper help <command>` shows every flag. A value that starts with a dash,\nlike a Markdown list, needs `=`: `--description=\"- one\"`.\n\n## Start with whoami\n\n`slopper whoami` returns the user's username, time zone, today's date in that zone, the open goal and its progress, the\nstreak, and the votes left today. Goal days end at midnight in the user's time zone; the race runs on UTC days.\n\n## Launch a slop\n\n1. Read the project: README, package.json or pubspec.yaml, and any deployed URL, App Store, TestFlight or Play beta\n links.\n2. Take 1 to 3 screenshots of it running. The first is the cover.\n3. Pick a category from `slopper categories`.\n4. Launch it. Screenshot files are uploaded for you:\n\n```sh\nslopper launch \\\n --name \"TiltMaze\" \\\n --tagline \"A marble maze you play by tilting your phone\" \\\n --category games \\\n --description \"Built in a day. Tilt to roll, find the exit before the timer runs out.\" \\\n --link website=https://tiltmaze.app \\\n --link testflight=https://testflight.apple.com/join/abc123 \\\n --screenshot ./shots/cover.png --screenshot ./shots/level-2.png \\\n --made-with Flutter --made-with Claude \\\n --note \"Day 12: a maze you tilt. Try to beat 40 seconds.\" \\\n --autopost\n```\n\nLink kinds: `website`, `app_store`, `testflight`, `play_store`, `play_beta`, `source`, `other`. A bare URL is a\nwebsite. Store links must use their store's domain.\n\nThe result has the slop's `id`, `slug` and `url`, and the content check's verdict. Give the user the URL.\n\n`--autopost` posts the launch to the user's connected X, LinkedIn and Bluesky accounts. It's held (with the reason)\nunless the slop passed the automated content check and nobody has flagged it. Write the `--note` in the user's voice,\ntwo sentences at most.\n\nA new platform or a 2.0 is a new launch with its own shot at slop of the day:\n`slopper launch --relaunch-of <slop> --label \"iOS app\" ...`. A new feature is an update, not a launch.\n\n## Schedule launches\n\nWhen several things are ready at once, ask the user whether to put them all out now or line them up. Every launch\ncounts towards the goal on the day it goes out: three today are three slops today, and three lined up fill three days,\neach with its own shot at slop of the day.\n\n```sh\nslopper launch --at next ... # the next day with nothing out or scheduled, as its race starts\nslopper launch --at 2026-10-15T09:00 ... # the user's time zone, unless it has an offset\nslopper scheduled # what's waiting, soonest first\nslopper release <scheduled> # put it out now instead\nslopper unschedule <scheduled> # drop it\n```\n\nNobody else sees a scheduled launch until it goes out. `--autopost` posts it then.\n\n## Keep it alive\n\n```sh\nslopper post tiltmaze --kind feature --body \"Level editor: build a maze and share the link.\" --autopost\nslopper post tiltmaze --kind field --body \"Showed it to 5 people at the bus stop. 3 got hooked.\"\nslopper edit tiltmaze --tagline \"Now with a level editor\" --screenshot ./shots/editor.png\nslopper money tiltmaze cost 12.99 --category domain --note \"tiltmaze.app\"\n```\n\nUpdate kinds: `update` (fixes, news), `feature` (something new shipped), `field` (a real-world test: ask the user who\ntried it and what they said), `repromote` (bump it back up). `<slop>` is one of the user's slops, by id or slug (see\n`slopper slops`). `edit` changes only the flags passed; `--link` and `--screenshot` replace the whole list.\n\n## Goals\n\nAsk how many days first (7, 14, 30 or 100 are common), then how hard: one slop every five days is safe, every other day\nis getting there, one a day is hardcore, two a day is impossible.\n\n```sh\nslopper goal --days 30 --target 30\n```\n\nOnly one goal can be open at a time.\n\n## The race and votes\n\n`slopper today` ranks today's slops by votes, with each slop's `id`, whether it's the user's (`yours`), how they voted\n(`yourVote`), and `votesLeft`.\n\n```sh\nslopper vote <slop id> up # or down (private: lowers the ranking, nobody sees it), or clear\n```\n\nEveryone gets 5 votes per UTC day. Switching today's vote is free and clearing it gives it back. Nobody can vote on\ntheir own slops. Votes decide slop of the day, so only vote the way the user asks; never spend their votes on your own\njudgement.\n\n## Don't\n\n- Don't launch ideas, waitlists or anything that isn't live: a slop is something people can try today.\n- Don't launch features as new slops; post them as updates.\n- Don't put the token in files you commit. Use `slopper login` or `SLOPPER_TOKEN`.\n";
199
+ return "---\nname: slopper\ndescription: Ship to slopper.day from the terminal with the `slopper` CLI (npm @slopperday/cli). Use when the user wants to launch a project as today's slop or schedule launches for later days, post an update or field test, set a daily shipping goal, log costs or revenue, check today's slop of the day race, or vote on slops.\n---\n\n# Slopper\n\n[slopper.day](https://slopper.day) is where people throw slop at the wall every day: they ship something small\ndaily (a site, a TestFlight or Play beta, a repo), set a goal like 30 slops in 30 days, and keep slopping until one is\nslopcessful. Every UTC day the slops launched that day race on votes; the leader at midnight UTC wins slop of the day\nand goes slopcessful. Slopcessful is earned, never declared: nobody can mark their own slop slopcessful.\n\n## Setup\n\nRun the CLI without installing it, with the runner the project already uses:\n\n```sh\nnpx -y @slopperday/cli <command> # -y, or npx stops to ask \"Ok to proceed?\" and waits\npnpm dlx @slopperday/cli <command>\nbunx @slopperday/cli <command>\nyarn dlx -q @slopperday/cli <command> # -q, or yarn's install log lands in the JSON\ndeno run -A npm:@slopperday/cli <command>\n```\n\nOr install it once (`npm i -g @slopperday/cli`) and run `slopper`. The examples below say `slopper`: put the runner's\ncommand in its place.\n\nIt needs to be signed in as the user. Run `slopper login` with no token: it opens their browser on a one-time code and\nwaits (up to ten minutes) until they approve it, then saves a token of its own. Tell the user to check the code matches\nand click Approve. If no browser can open (a remote or sandboxed machine), use `--no-browser`, run it in the\nbackground and give the user the link it prints on stderr. If they hand you a token instead, use it:\n\n```sh\nslopper login # browser sign-in; saves a token readable by the user only\nslopper login --token slp_... # or check and save a token from /settings/api\nSLOPPER_TOKEN=slp_... slopper whoami # saves nothing: use this when you shouldn't write files\n```\n\n`--url` (or `SLOPPER_URL`) points it at another site, like a local one.\n\nEvery command prints JSON on stdout. Errors go to stderr with exit code 1 (2 for a usage mistake), and validation\nerrors list each field that needs fixing. `slopper help <command>` shows every flag. A value that starts with a dash,\nlike a Markdown list, needs `=`: `--description=\"- one\"`.\n\n## Start with whoami\n\n`slopper whoami` returns the user's username, time zone, today's date in that zone, the open goal and its progress, the\nstreak, and the votes left today. Goal days end at midnight in the user's time zone; the race runs on UTC days.\n\n## Launch a slop\n\n1. Read the project: README, package.json or pubspec.yaml, and any deployed URL, App Store, TestFlight or Play beta\n links.\n2. Take 1 to 3 screenshots of it running. The first is the cover.\n3. Pick a category from `slopper categories`.\n4. Launch it. Screenshot files are uploaded for you:\n\n```sh\nslopper launch \\\n --name \"TiltMaze\" \\\n --tagline \"A marble maze you play by tilting your phone\" \\\n --category games \\\n --description \"Built in a day. Tilt to roll, find the exit before the timer runs out.\" \\\n --link website=https://tiltmaze.app \\\n --link testflight=https://testflight.apple.com/join/abc123 \\\n --screenshot ./shots/cover.png --screenshot ./shots/level-2.png \\\n --made-with Flutter --made-with Claude \\\n --note \"Day 12: a maze you tilt. Try to beat 40 seconds.\" \\\n --autopost\n```\n\nLink kinds: `website`, `app_store`, `testflight`, `play_store`, `play_beta`, `source`, `other`. A bare URL is a\nwebsite. Store links must use their store's domain.\n\nThe result has the slop's `id`, `slug` and `url`, and the content check's verdict. Give the user the URL.\n\n`--autopost` posts the launch to the user's connected X, LinkedIn and Bluesky accounts. It's held (with the reason)\nunless the slop passed the automated content check and nobody has flagged it. Write the `--note` in the user's voice,\ntwo sentences at most.\n\nA new platform or a 2.0 is a new launch with its own shot at slop of the day:\n`slopper launch --relaunch-of <slop> --label \"iOS app\" ...`. A new feature is an update, not a launch.\n\n## Schedule launches\n\nWhen several things are ready at once, ask the user whether to put them all out now or line them up. Every launch\ncounts towards the goal on the day it goes out: three today are three slops today, and three lined up fill three days,\neach with its own shot at slop of the day.\n\n```sh\nslopper launch --at next ... # the next day with nothing out or scheduled, as its race starts\nslopper launch --at 2026-10-15T09:00 ... # the user's time zone, unless it has an offset\nslopper scheduled # what's waiting, soonest first\nslopper release <scheduled> # put it out now instead\nslopper unschedule <scheduled> # drop it\n```\n\nNobody else sees a scheduled launch until it goes out. `--autopost` posts it then.\n\n## Keep it alive\n\n```sh\nslopper post tiltmaze --kind feature --body \"Level editor: build a maze and share the link.\" --autopost\nslopper post tiltmaze --kind field --body \"Showed it to 5 people at the bus stop. 3 got hooked.\"\nslopper edit tiltmaze --tagline \"Now with a level editor\" --screenshot ./shots/editor.png\nslopper video tiltmaze ./promo.mp4\nslopper money tiltmaze cost 12.99 --category domain --note \"tiltmaze.app\"\n```\n\nUpdate kinds: `update` (fixes, news), `feature` (something new shipped), `field` (a real-world test: ask the user who\ntried it and what they said), `repromote` (bump it back up). `<slop>` is one of the user's slops, by id or slug (see\n`slopper slops`). `edit` changes only the flags passed; `--link` and `--screenshot` replace the whole list.\n`video` puts a promo (MP4 or WebM, up to 4 MB) on a slop, replacing its last; it says `pending` until a moderator\napproves it, because the content check can't watch video. `video <slop> --remove` takes it off.\n\n## Goals\n\nAsk how many days first (7, 14, 30 or 100 are common), then how hard: one slop every five days is safe, every other day\nis getting there, one a day is hardcore, two a day is impossible.\n\n```sh\nslopper goal --days 30 --target 30\n```\n\nOnly one goal can be open at a time.\n\n## The race and votes\n\n`slopper today` ranks today's slops by votes, with each slop's `id`, whether it's the user's (`yours`), how they voted\n(`yourVote`), and `votesLeft`.\n\n```sh\nslopper vote <slop id> up # or down (private: lowers the ranking, nobody sees it), or clear\n```\n\nEveryone gets 5 votes per UTC day. Switching today's vote is free and clearing it gives it back. Nobody can vote on\ntheir own slops. Votes decide slop of the day, so only vote the way the user asks; never spend their votes on your own\njudgement.\n\n## Don't\n\n- Don't launch ideas, waitlists or anything that isn't live: a slop is something people can try today.\n- Don't launch features as new slops; post them as updates.\n- Don't put the token in files you commit. Use `slopper login` or `SLOPPER_TOKEN`.\n";
199
200
  }
200
201
  //#endregion
201
202
  //#region src/commands.ts
@@ -204,6 +205,8 @@ async function readSkill() {
204
205
  * agree). Bigger screenshots are refused, so they aren't sent.
205
206
  */
206
207
  const MEDIA_BYTES = 4194304;
208
+ /** The site's LIMITS.videoBytes, for promo videos. */
209
+ const VIDEO_BYTES = 4194304;
207
210
  function text$1(values, key) {
208
211
  const value = values[key];
209
212
  return typeof value === "string" ? value : void 0;
@@ -266,6 +269,26 @@ async function uploadImage(api, file) {
266
269
  if (typeof uploaded !== "object" || uploaded === null || typeof uploaded.id !== "string") throw new SlopperError(`Uploading ${file} returned no media id.`);
267
270
  return uploaded;
268
271
  }
272
+ const VIDEO_TYPES = {
273
+ ".mp4": "video/mp4",
274
+ ".m4v": "video/mp4",
275
+ ".webm": "video/webm"
276
+ };
277
+ async function uploadVideoFile(api, file) {
278
+ const contentType = VIDEO_TYPES[extname(file).toLowerCase()];
279
+ if (!contentType) throw usageError(`${file} isn't an MP4 or WebM video.`);
280
+ let bytes;
281
+ try {
282
+ bytes = new Uint8Array(await readFile(resolve(file)));
283
+ } catch (error) {
284
+ if (!isSystemError(error)) throw error;
285
+ throw usageError(`Couldn't read ${file} (${error.code}).`);
286
+ }
287
+ if (bytes.byteLength > 4194304) throw usageError(`${file} is bigger than ${VIDEO_BYTES / 1024 / 1024} MB, the most a video can be.`);
288
+ const uploaded = await api.upload("/videos", bytes, contentType);
289
+ if (typeof uploaded !== "object" || uploaded === null || typeof uploaded.id !== "string") throw new SlopperError(`Uploading ${file} returned no media id.`);
290
+ return uploaded;
291
+ }
269
292
  /**
270
293
  * Screenshots are image files, uploaded first, or media ids from
271
294
  * `slopper upload`. Anything with an image extension or a path in it is a file.
@@ -511,6 +534,24 @@ const COMMANDS = [
511
534
  return context.api.patch(slopPath(slop), fields);
512
535
  }
513
536
  },
537
+ {
538
+ name: "video",
539
+ args: "<slop> [file|id]",
540
+ summary: "Put a promo video (MP4 or WebM, up to 4 MB) on one of your slops, replacing its last. It plays for everyone once a moderator approves it.",
541
+ flags: { "--remove": "Take the slop's video off instead." },
542
+ options: { remove: { type: "boolean" } },
543
+ async run(context) {
544
+ const { api, values, positionals } = context;
545
+ const slop = positional(context, 0, "slop");
546
+ if (values.remove === true) {
547
+ if (positionals[1] !== void 0) throw usageError("Pass a video or --remove, not both.");
548
+ return api.put(`${slopPath(slop)}/video`, { video: null });
549
+ }
550
+ const video = positional(context, 1, "file|id");
551
+ const id = video.includes("/") || video.includes("\\") || extname(video).toLowerCase() in VIDEO_TYPES ? (await uploadVideoFile(api, video)).id : video;
552
+ return api.put(`${slopPath(slop)}/video`, { video: id });
553
+ }
554
+ },
514
555
  {
515
556
  name: "post",
516
557
  args: "<slop>",
@@ -624,7 +665,7 @@ function usage(command) {
624
665
  function overview() {
625
666
  const width = Math.max(...COMMANDS.map((command) => command.name.length));
626
667
  return [
627
- `slopper ${version}: throw slop at the wall, from your terminal. Built for agents.`,
668
+ `slopper ${version}: too much slop? Not enough yet! Throw it at the wall from your terminal. Built for agents.`,
628
669
  "",
629
670
  "Usage: slopper <command> [flags]",
630
671
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@slopperday/cli",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Throw slop at the wall from your terminal: launch, update and vote on slopper.day. Built for agents.",
5
5
  "keywords": [
6
6
  "agent",