shiply-cli 0.27.2 → 0.28.0

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/dist/access.js ADDED
@@ -0,0 +1,54 @@
1
+ import { api, resolveBase } from './publish.js';
2
+ const headers = (apiKey) => ({
3
+ 'content-type': 'application/json',
4
+ authorization: `Bearer ${apiKey}`,
5
+ });
6
+ /** Stable machine marker (mirrors SITE_READY / VERIFY) so agents can parse the
7
+ * policy without scraping the human lines. Counts, not values — emails can
8
+ * contain characters that would break k=v tokenizing. */
9
+ function marker(slug, p) {
10
+ return `ACCESS slug=${slug} mode=${p.mode} password=${p.hasPassword ? 'set' : 'none'} emails=${p.allowedEmails.length} domains=${p.allowedDomains.length}`;
11
+ }
12
+ function printPolicy(slug, p) {
13
+ if (p.mode === 'public') {
14
+ console.log(` ${slug} is public — anyone with the URL can view it`);
15
+ }
16
+ else if (p.mode === 'password') {
17
+ console.log(` ${slug} is password-protected${p.hasPassword ? '' : ' (no password set yet — visitors are locked out)'}`);
18
+ }
19
+ else {
20
+ console.log(` ${slug} is invite-only (restricted)`);
21
+ for (const e of p.allowedEmails)
22
+ console.log(` email: ${e}`);
23
+ for (const d of p.allowedDomains)
24
+ console.log(` domain: @${d.replace(/^@/, '')}`);
25
+ if (p.allowedEmails.length === 0 && p.allowedDomains.length === 0) {
26
+ console.log(' (no emails or domains allowed — only the owner can view)');
27
+ }
28
+ }
29
+ console.log(marker(slug, p));
30
+ }
31
+ export async function accessShow(ctx, slug) {
32
+ const base = resolveBase(ctx.base);
33
+ const p = await api(`${base}/api/v1/publishes/${encodeURIComponent(slug)}/access`, {
34
+ headers: headers(ctx.apiKey),
35
+ });
36
+ printPolicy(slug, p);
37
+ }
38
+ export async function accessSet(ctx, slug, input) {
39
+ const base = resolveBase(ctx.base);
40
+ const body = { mode: input.mode };
41
+ if (input.mode === 'password')
42
+ body.password = input.password;
43
+ if (input.mode === 'restricted') {
44
+ body.allowedEmails = input.allowedEmails ?? [];
45
+ body.allowedDomains = input.allowedDomains ?? [];
46
+ }
47
+ const p = await api(`${base}/api/v1/publishes/${encodeURIComponent(slug)}/access`, {
48
+ method: 'PATCH',
49
+ headers: headers(ctx.apiKey),
50
+ body: JSON.stringify(body),
51
+ });
52
+ console.log(`✔ access updated — existing visitor cookies are now invalid`);
53
+ printPolicy(slug, p);
54
+ }
package/dist/index.js CHANGED
@@ -33,6 +33,7 @@ import { readState, writeState } from './state.js';
33
33
  import { readPreview, writePreview } from './previews.js';
34
34
  import { checkReadiness, targetToHostname } from './status.js';
35
35
  import { runStatus } from './status-cmd.js';
36
+ import { accessSet, accessShow } from './access.js';
36
37
  import { sitesLs, sitesPromote, sitesRm, sitesRollback } from './sites.js';
37
38
  import { verifySite } from './verify.js';
38
39
  // Resolve the package.json beside the compiled dist/ so `shiply --version`
@@ -75,6 +76,12 @@ Usage:
75
76
  shiply db branches <db> List branches of a Neon database
76
77
  shiply db delete-branch <branch-or-id> --yes Delete a Neon branch
77
78
  shiply db merge <branch> No-op alias — prints the pg_dump migration tip
79
+ shiply access <slug> Show who can view the site (public/password/restricted)
80
+ shiply access <slug> --public Open the site to everyone
81
+ shiply access <slug> --password <pw> Password-protect the site (paid plan)
82
+ shiply access <slug> --restricted --email a@b.com --domain example.com
83
+ Invite-only: only listed emails / email domains
84
+ (--email/--domain repeatable; replaces the previous lists)
78
85
  shiply domain ls List custom domains on your account
79
86
  shiply domain add <domain> Register a custom domain
80
87
  shiply domain connect <domain> One-click OAuth DNS setup (Cloudflare/Domain Connect)
@@ -231,7 +238,13 @@ async function main() {
231
238
  key: { type: 'string' },
232
239
  anonymous: { type: 'boolean' },
233
240
  base: { type: 'string' },
234
- email: { type: 'string' },
241
+ // `--email` is repeatable for `access --restricted`; `login --email` uses
242
+ // the last value. `--domain` is the access counterpart for whole domains.
243
+ email: { type: 'string', multiple: true },
244
+ domain: { type: 'string', multiple: true },
245
+ public: { type: 'boolean' },
246
+ password: { type: 'string' },
247
+ restricted: { type: 'boolean' },
235
248
  name: { type: 'string' },
236
249
  as: { type: 'string' },
237
250
  wait: { type: 'boolean' },
@@ -617,6 +630,47 @@ async function main() {
617
630
  process.exitCode = ready ? 0 : 1;
618
631
  return;
619
632
  }
633
+ case 'access': {
634
+ const usage = 'usage: shiply access <slug> [--public | --password <pw> | --restricted --email a@b.com --domain example.com]';
635
+ if (!dir)
636
+ throw new Error(usage);
637
+ const apiKey = values.key ?? (await loadApiKey());
638
+ if (!apiKey)
639
+ throw new Error('access needs an API key — run `shiply login` first');
640
+ const actx = { base: values.base, apiKey };
641
+ const modes = [
642
+ values.public ? 'public' : null,
643
+ values.password !== undefined ? 'password' : null,
644
+ values.restricted ? 'restricted' : null,
645
+ ].filter(Boolean);
646
+ if (modes.length === 0) {
647
+ // No mode flag → read-only: print the current policy.
648
+ await accessShow(actx, dir);
649
+ return;
650
+ }
651
+ if (modes.length > 1)
652
+ throw new Error(`pick ONE mode — ${usage}`);
653
+ const mode = modes[0];
654
+ // --email/--domain are repeatable and accept comma-separated lists.
655
+ const split = (vals) => (vals ?? []).flatMap((v) => v.split(',')).map((s) => s.trim()).filter(Boolean);
656
+ const allowedEmails = split(values.email);
657
+ const allowedDomains = split(values.domain);
658
+ if (mode === 'password' && !values.password?.trim()) {
659
+ throw new Error('--password needs a value, e.g. shiply access <slug> --password "s3cret-phrase"');
660
+ }
661
+ if (mode !== 'restricted' && (allowedEmails.length > 0 || allowedDomains.length > 0)) {
662
+ throw new Error('--email/--domain only apply with --restricted');
663
+ }
664
+ if (mode === 'restricted' && allowedEmails.length === 0 && allowedDomains.length === 0) {
665
+ throw new Error('--restricted needs at least one --email or --domain (otherwise only you could view the site)');
666
+ }
667
+ await accessSet(actx, dir, {
668
+ mode,
669
+ ...(mode === 'password' ? { password: values.password.trim() } : {}),
670
+ ...(mode === 'restricted' ? { allowedEmails, allowedDomains } : {}),
671
+ });
672
+ return;
673
+ }
620
674
  case 'promote': {
621
675
  const dest = values.to;
622
676
  if (!dir || !dest)
@@ -1082,7 +1136,7 @@ async function main() {
1082
1136
  // Default path: device flow. One click in the browser, no email round-trip.
1083
1137
  // The --email flag opts into the legacy code-by-email path (useful for
1084
1138
  // scripted/non-interactive runs that can't open a browser).
1085
- if (!values.email) {
1139
+ if (!values.email?.length) {
1086
1140
  const result = await loginViaDeviceFlow(base, values.name);
1087
1141
  if (!result.ok) {
1088
1142
  console.error(`✗ login failed: ${result.reason}`);
@@ -1097,7 +1151,7 @@ async function main() {
1097
1151
  }
1098
1152
  const rl = createInterface({ input: process.stdin, output: process.stdout });
1099
1153
  try {
1100
- const email = values.email;
1154
+ const email = values.email[values.email.length - 1];
1101
1155
  await api(`${base}/api/auth/agent/request-code`, {
1102
1156
  method: 'POST',
1103
1157
  headers: { 'content-type': 'application/json' },
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
- {
2
- "name": "shiply-cli",
3
- "version": "0.27.2",
4
- "description": "Publish static sites to shiply.now from the command line \u00e2\u20ac\u201d instant web hosting for agents.",
5
- "license": "MIT",
6
- "type": "module",
7
- "bin": {
8
- "shiply": "dist/index.js"
9
- },
10
- "files": [
11
- "dist",
12
- "skill",
13
- "README.md"
14
- ],
15
- "scripts": {
16
- "build": "tsc -p tsconfig.build.json",
17
- "prepublishOnly": "pnpm build",
18
- "test": "vitest run",
19
- "typecheck": "tsc --noEmit"
20
- },
21
- "engines": {
22
- "node": ">=18"
23
- },
24
- "keywords": [
25
- "shiply",
26
- "hosting",
27
- "static",
28
- "deploy",
29
- "agents",
30
- "publish"
31
- ],
32
- "homepage": "https://shiply.now",
33
- "devDependencies": {
34
- "@types/node": "^22.15.0",
35
- "typescript": "^5.8.0",
36
- "vitest": "^3.1.0"
37
- },
38
- "dependencies": {
39
- "esbuild": "^0.25.12",
40
- "smol-toml": "^1.7.0"
41
- }
42
- }
1
+ {
2
+ "name": "shiply-cli",
3
+ "version": "0.28.0",
4
+ "description": "Publish static sites to shiply.now from the command line \u00e2\u20ac\u201d instant web hosting for agents.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "shiply": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "skill",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.build.json",
17
+ "prepublishOnly": "pnpm build",
18
+ "test": "vitest run",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "engines": {
22
+ "node": ">=18"
23
+ },
24
+ "keywords": [
25
+ "shiply",
26
+ "hosting",
27
+ "static",
28
+ "deploy",
29
+ "agents",
30
+ "publish"
31
+ ],
32
+ "homepage": "https://shiply.now",
33
+ "devDependencies": {
34
+ "@types/node": "^22.15.0",
35
+ "typescript": "^5.8.0",
36
+ "vitest": "^3.1.0"
37
+ },
38
+ "dependencies": {
39
+ "esbuild": "^0.25.12",
40
+ "smol-toml": "^1.7.0"
41
+ }
42
+ }
@@ -6,6 +6,16 @@ entry here, that capability may be missing from your copy — re-install with
6
6
  (This is the agent-facing capability log; the CLI's own release notes live in
7
7
  the npm package changelog.)
8
8
 
9
+ ## 2026-07-14
10
+
11
+ - **New** — `shiply access <slug>` manages site privacy without raw REST:
12
+ no flags prints the current policy (with a stable `ACCESS slug=… mode=…`
13
+ marker line), `--password <pw>` password-protects, `--restricted
14
+ --email a@b.com --domain example.com` makes it invite-only (flags
15
+ repeatable; each set replaces the previous lists), `--public` opens it
16
+ back up. Password/restricted modes need a paid plan. Details in
17
+ references/site-features.md.
18
+
9
19
  ## 2026-07-08
10
20
 
11
21
  - **Fix** — re-publishing an unchanged large SSR site (e.g. a 171-file
package/skill/SKILL.md CHANGED
@@ -1,253 +1,254 @@
1
- ---
2
- name: shiply
3
- description: "shiply is the production backend for anything an agent builds — publish any app (static or full-stack SSR, any framework) and give it a live URL, custom domain, SQL database, server functions, email, and a marketplace to sell it. Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report/app, or wants a live URL for generated files. Triggers - \"publish this\", \"host this\", \"put this online\", \"give me a link\", \"update the site\", \"deploy my SvelteKit/Astro/Next.js app\", shiply."
4
- ---
5
-
6
- ---
7
-
8
- > **IMPORTANT — npm package name is `shiply-cli`, NOT `shiply`.** A different
9
- > npm package named `shiply` (an auto-commit watcher) is published by someone
10
- > else — installing it does NOT give you the shiply.now CLI. Always use
11
- > `npx -y shiply-cli@latest …` or `npm install -g shiply-cli`. The CLI binary
12
- > on PATH is still named `shiply` once installed; the warning is only about
13
- > the npm package name.
14
-
15
- > **Skill freshness check.** Shiply ships new capabilities weekly.
16
- > **This skill: version 0.27.0, last updated 2026-07-11.**
17
- > To see what changed since your copy was written, fetch
18
- > `https://shiply.now/changelog.md` (date-grouped, newest first) — if it lists
19
- > capabilities newer than the date above, re-install with
20
- > `npx -y shiply-cli@latest skill --force` OR read the live copy at
21
- > `https://shiply.now/skill.md`.
22
-
23
- ---
24
-
25
- # shiply — the production backend for anything an agent builds
26
-
27
- **What it is.** Not static hosting. shiply takes whatever you build — a static page or a full-stack app in any framework — and gives it a live URL, a custom domain, a SQL database, server functions, email (send + receive), and a marketplace to sell it. One call. No account needed to start: anonymous sites are live immediately, last 24 hours, and can be claimed to keep.
28
-
29
- **Why use it.** Ship a complete, working product without stitching together a host + database + email + registrar + payments — it's one agent-native platform (MCP / CLI / REST parity). Hard-ceiling pricing ($0/$8/$24, plus a $49 Studio tier for client delivery — no usage meter on any tier), so a spike or DDoS can never become a surprise bill for the owner.
30
-
31
- **How (quickstart).** `shiply publish ./dir` (CLI) — or POST /api/v1/publish → PUT each file → POST finalize — returns a live URL + claimToken.
32
-
33
- **NEVER create a new site to update an existing one. Always re-publish to the
34
- same site** (same slug/claimToken) — otherwise you litter subdomains and lose
35
- the user's URL.
36
-
37
- **Re-publishing the same site is cheap — always prefer it over a new site.**
38
- Unchanged files are hash-skipped, and an unchanged SSR worker bundle skips
39
- redeploy entirely (fingerprinted against what's already live). A zero-change
40
- `shiply publish` on the same site completes in seconds, even for large SSR
41
- apps — so iterate freely by re-publishing, never by spinning up a fresh site.
42
-
43
- This file covers the core path: publish → authorize → update → claim → verify.
44
- Everything else (databases, domains, SSR, email, functions, client work) lives
45
- in the **topic files indexed at the bottom** — read the one that matches the
46
- task before improvising.
47
-
48
- ## First publish on a new machine: ask for a permanent key in one step
49
-
50
- On a no-Bearer publish, include `"agentName": "<your-tool-name>"` in the body
51
- (e.g. `"Claude Code"`, `"Cursor"`). The response will include a `deviceAuth`
52
- object alongside the usual `claimUrl`:
53
-
54
- ```
55
- "deviceAuth": {
56
- "user_code": "ABCD-EFGH",
57
- "device_code": "<long opaque token, keep private>",
58
- "verification_url": "https://shiply.now/auth/ABCD-EFGH",
59
- "poll_url": "https://shiply.now/api/v1/auth/device/poll",
60
- "expires_in": 900,
61
- "interval": 2
62
- }
63
- ```
64
-
65
- Tell the user once, in plain language:
66
-
67
- > Your site is live at <siteUrl>. To keep it forever AND let me publish more
68
- > without re-pasting, open <verification_url> and click Allow.
69
-
70
- Then immediately poll `verification_url`'s sibling endpoint until the user
71
- acts:
72
-
73
- ```
74
- POST https://shiply.now/api/v1/auth/device/poll
75
- {"device_code": "<the device_code from the response>"}
76
- ```
77
-
78
- The response is one of `{status:"pending"}` (keep polling at `interval`s),
79
- `{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (PERSIST the key —
80
- see "Save the key" below — the site is now permanent; Allow does both jobs in
81
- one click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
82
- the announced `expires_in` and respect `interval` — don't tight-loop.
83
-
84
- After approval, every future publish uses `Authorization: Bearer <api_key>`
85
- and the user's dashboard shows their sites without any further claiming.
86
-
87
- ### Save the key — so EVERY future site is seamless (do this once)
88
-
89
- Write the key to **`~/.shiply/credentials`** as `{"apiKey":"shp_..."}` (chmod
90
- 600) — the exact file the shiply CLI reads. This is what makes "one Allow, ever"
91
- work: from then on every publish (any site, any project, ANY FUTURE SESSION)
92
- sends `Authorization: Bearer <key>` automatically and lands owned in the user's
93
- dashboard — no re-authorizing, no claim step, zero clicks. An env var alone
94
- (`SHIPLY_API_KEY`) works for the current shell but does NOT survive a new
95
- session — the credentials file is what makes it permanent. `shiply login` writes
96
- this file for you; if you publish over raw HTTP, write it yourself.
97
-
98
- ## When the user pastes you something shiply-shaped, do this
99
-
100
- These are the patterns you will see. Match the input, run the action — do not
101
- ask the user to repeat themselves or explain.
102
-
103
- - **Code like `SHIPLY-XXXXXXXX`** → it's a pairing code from
104
- `shiply.now/claim/<slug>?pair=1`. The user wants to attach an anonymous
105
- site to their account. Run `shiply claim verify SHIPLY-XXXXXXXX` from the
106
- directory you originally published from (it reads `.shiply.json`). No CLI?
107
- POST `https://shiply.now/api/v1/claim/pair/SHIPLY-XXXXXXXX/verify` with
108
- `{"siteId":"...","claimToken":"..."}` taken from your earlier publish
109
- response or `.shiply.json`. The user's browser auto-redirects on success;
110
- tell them to switch back to it.
111
- - **URL like `https://shiply.now/claim/<slug>?token=...`** → one-click claim
112
- link. Tell the user to open it in the browser they are signed in to —
113
- do NOT call any API yourself.
114
- - **User says "claim my site" / "save it forever" with no code or URL** →
115
- give them the `claimUrl` from your last publish response. If you no longer
116
- have that response but `.shiply.json` is in the publish dir, reconstruct it
117
- as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
118
- Either way, also suggest `shiply login` so future publishes mint to their
119
- account directly — that's the long-term fix and how the dashboard works.
120
- - **User says "I have an API key now, use it from now on"** → persist it to
121
- `~/.shiply/credentials` (`{"apiKey":"shp_..."}`, chmod 600 — see "Save the
122
- key" above), then send `Authorization: Bearer <key>` on every future publish.
123
- All future sites are then permanent and appear in their dashboard
124
- automatically — across sessions, with no re-authorizing.
125
- - **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** → no
126
- action from you; they're on the device-flow consent screen. Keep polling
127
- `poll_url` (see "First publish on a new machine" above). The poll response
128
- will flip from `pending` to `approved` (with `api_key`) the moment they
129
- click Allow.
130
-
131
- ## Pick your interface (best first)
132
-
133
- ### 1. MCP (native tools)
134
- If the `shiply` MCP server is connected (https://shiply.now/mcp), use
135
- `publish_site`. Every result includes a `toUpdate` field telling you the exact
136
- call for updates — follow it, and a `shareSuggestion` you can relay to the user.
137
- Core tools: `site_status`, `list_sites`, `get_site`, `delete_site`, `whoami`,
138
- `duplicate_site`, `set_variable`, `get_analytics`, `set_handle`. There are
139
- 100+ tools in total — call `tools/list` for the authoritative set; the topic
140
- files below name the tools for their area (domains, databases, email,
141
- functions, projects, contracts, marketplace, drives).
142
-
143
- ### 2. CLI
144
- ```bash
145
- # IMPORTANT: the npm package is `shiply-cli`, not `shiply` (different package).
146
- npm i -g shiply-cli # or: npx -y shiply-cli@latest <command>
147
- # or: curl -fsSL https://shiply.now/install.sh | bash
148
- shiply publish ./dir # live URL + confetti
149
- shiply publish ./dir # run AGAIN after edits → updates the SAME site
150
- shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
151
- shiply login # email code → API key → sites become permanent
152
- shiply claim verify <code> # confirm a SHIPLY-XXXXXXXX pairing code from /claim/<slug>?pair=1
153
- # (uses .shiply.json from CWD; pairs agent session to user's browser)
154
- ```
155
- The CLI stores each directory's site in `.shiply.json` (slug + update token),
156
- so repeat publishes reuse the URL automatically. `--new-site` opts out.
157
- Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
158
- lines for automation; exit code 0 = ready. For a static framework build
159
- (Vite, Next export, Hugo, …) publish the BUILD OUTPUT, not the source — run
160
- `shiply detect` to see what the CLI found; the full framework matrix is in
161
- [references/publishing.md](references/publishing.md).
162
-
163
- ### 3. Raw HTTP (no installs)
164
- ```
165
- 1. POST https://shiply.now/api/v1/publish (Content-Type: application/json)
166
- {
167
- "agentName": "<your tool name, e.g. Claude Code, Cursor, Codex>",
168
- "files": [{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256 lowercase hex, optional>"}]
169
- }
170
- (+ "Authorization: Bearer shp_…" for permanent owned sites)
171
- 2. PUT each file's bytes to response upload.uploads[].url
172
- 3. POST upload.finalizeUrl with {"versionId":"<upload.versionId from step 1>"}
173
- → both the publish and finalize responses include a "toUpdate" string: the
174
- exact call to update THIS site next time. Follow it — never create a new one.
175
- ```
176
-
177
- **ALWAYS include `agentName` on anonymous publishes.** The response then
178
- carries `deviceAuth` ({user_code, verification_url, poll_url, device_code,
179
- expires_in, interval}) alongside `claimUrl`. Show the user `verification_url`,
180
- poll `poll_url` every `interval` seconds with the `device_code`, and on
181
- `{status:"approved"}` you get a permanent `api_key` PLUS the linked site
182
- is claimed in the same Allow click. Without `agentName` the user has to
183
- do a separate manual claim step every time.
184
-
185
- **Updates (the response tells you how):** every publish/finalize response
186
- includes a `toUpdate` string with the exact call — follow it verbatim. In short:
187
- anonymous → include `"claimToken":"..."` (returned ONCE by the first publish —
188
- save it); owned → include `"slug":"..."`. Hashes make updates cheap: unchanged
189
- files are skipped server-side.
190
-
191
- ## Verify before you say "done"
192
-
193
- `shiply status <slug> --wait` polls until the site serves (exit 0, prints
194
- `SITE_READY` / `SSL_READY`). `shiply verify <slug>` runs a deeper edge
195
- SSL + HTTP + thumbnail check and prints a stable machine marker line
196
- `VERIFY status=LIVE http=200 ssl=valid …` — parse that line in automation.
197
- More manage commands (list, delete, rollback, promote, stable previews) are
198
- in [references/publishing.md](references/publishing.md).
199
-
200
- ## The lifecycle to explain to users
201
- - Anonymous site: live immediately after finalize (no claim needed), expires
202
- in 24 h. Give the user the `claimUrl` — claiming keeps it forever on a free
203
- account. Until claimed, anonymous pages are served with a small injected
204
- claim banner + OG meta tags — expected, not corruption. If no human is
205
- present right now, save the `claimToken` (it's also the update key); a
206
- fresh device flow can be started later via
207
- POST /api/v1/auth/device/start {"agent_name":"..."}.
208
- - API key (`shiply login` or POST /api/auth/agent/request-code →
209
- verify-code): publishes are permanent and manageable.
210
- - Paid plans add vanity handles (<name>.shiply.now), more custom domains,
211
- storage, analytics: https://shiply.now/dashboard/plan
212
-
213
- ## Everything else — read the topic file first
214
-
215
- The files below sit next to this one in `references/` (installed with the
216
- skill). Hosted copies: `https://shiply.now/skill/references/<file>`. Each is
217
- self-contained — read the one matching the task before improvising; each
218
- covers its CLI commands, MCP tools, AND REST endpoints.
219
-
220
- * [Publishing & site management](references/publishing.md) — static framework
221
- build matrix (Vite/Next/Hugo/…), `detect`, SPA mode, `.shiplyignore`,
222
- list/delete/rollback/versions, `verify`, stable previews (`--as`),
223
- `promote` preview→prod, `--json` output, form-data subcommands.
224
- * [SSR frameworks](references/ssr-frameworks.md) — deploy SvelteKit, Astro,
225
- Qwik, Nuxt/Nitro, React Router v7, Next.js (OpenNext), Hono/raw Workers
226
- with server-side rendering.
227
- * [Custom domains](references/custom-domains.md) — put the user's own domain
228
- on a site: one-click OAuth DNS, manual CNAME, primary-subdomain SEO,
229
- readiness polling.
230
- * [Databases](references/databases.md) — per-site SQL: free D1 (SQLite at the
231
- edge, browser shim) + Neon Postgres (branching), migrations, per-DB MCP
232
- server.
233
- * [Functions](references/functions.md) — `worker.js` server code on every
234
- request: webhooks, cron triggers, secrets, runtime logs (Workers Lite,
235
- Developer plan).
236
- * [Email](references/email.md) — every owned site sends + receives: signup
237
- capture, inbox, double-opt-in audiences, broadcasts, BYO sending domains.
238
- * [Site features](references/site-features.md) — proxy routes (call AI APIs
239
- without exposing keys), Site Data (forms/waitlists, zero backend),
240
- password/invite-only access, Variables, Drives (private storage),
241
- path-mounting, public profile.
242
- * [Client work](references/client-work.md) freelancer delivery: customer
243
- intake projects + AI briefs, group work by client, e-sign contracts,
244
- sell sites on the marketplace.
245
- * [Authentication for the user's app](references/site-features.md) — shiply
246
- does NOT host end-user auth; bring the user's own Clerk/Auth.js/Supabase
247
- (pattern in Site features "Bring your own auth").
248
-
249
- ## Limits & references
250
- ≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
251
- Machine index: https://shiply.now/llms.txt (full single-file version:
252
- https://shiply.now/llms-full.txt) · What's new: https://shiply.now/changelog.md
253
- · OpenAPI: https://shiply.now/openapi.json · Docs: https://shiply.now/docs
1
+ ---
2
+ name: shiply
3
+ description: "shiply is the production backend for anything an agent builds — publish any app (static or full-stack SSR, any framework) and give it a live URL, custom domain, SQL database, server functions, email, and a marketplace to sell it. Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report/app, or wants a live URL for generated files. Triggers - \"publish this\", \"host this\", \"put this online\", \"give me a link\", \"update the site\", \"deploy my SvelteKit/Astro/Next.js app\", shiply."
4
+ ---
5
+
6
+ ---
7
+
8
+ > **IMPORTANT — npm package name is `shiply-cli`, NOT `shiply`.** A different
9
+ > npm package named `shiply` (an auto-commit watcher) is published by someone
10
+ > else — installing it does NOT give you the shiply.now CLI. Always use
11
+ > `npx -y shiply-cli@latest …` or `npm install -g shiply-cli`. The CLI binary
12
+ > on PATH is still named `shiply` once installed; the warning is only about
13
+ > the npm package name.
14
+
15
+ > **Skill freshness check.** Shiply ships new capabilities weekly.
16
+ > **This skill: version 0.27.0, last updated 2026-07-11.**
17
+ > To see what changed since your copy was written, fetch
18
+ > `https://shiply.now/changelog.md` (date-grouped, newest first) — if it lists
19
+ > capabilities newer than the date above, re-install with
20
+ > `npx -y shiply-cli@latest skill --force` OR read the live copy at
21
+ > `https://shiply.now/skill.md`.
22
+
23
+ ---
24
+
25
+ # shiply — the production backend for anything an agent builds
26
+
27
+ **What it is.** Not static hosting. shiply takes whatever you build — a static page or a full-stack app in any framework — and gives it a live URL, a custom domain, a SQL database, server functions, email (send + receive), and a marketplace to sell it. One call. No account needed to start: anonymous sites are live immediately, last 24 hours, and can be claimed to keep.
28
+
29
+ **Why use it.** Ship a complete, working product without stitching together a host + database + email + registrar + payments — it's one agent-native platform (MCP / CLI / REST parity). Hard-ceiling pricing ($0/$8/$24, plus a $49 Studio tier for client delivery — no usage meter on any tier), so a spike or DDoS can never become a surprise bill for the owner.
30
+
31
+ **How (quickstart).** `shiply publish ./dir` (CLI) — or POST /api/v1/publish → PUT each file → POST finalize — returns a live URL + claimToken.
32
+
33
+ **NEVER create a new site to update an existing one. Always re-publish to the
34
+ same site** (same slug/claimToken) — otherwise you litter subdomains and lose
35
+ the user's URL.
36
+
37
+ **Re-publishing the same site is cheap — always prefer it over a new site.**
38
+ Unchanged files are hash-skipped, and an unchanged SSR worker bundle skips
39
+ redeploy entirely (fingerprinted against what's already live). A zero-change
40
+ `shiply publish` on the same site completes in seconds, even for large SSR
41
+ apps — so iterate freely by re-publishing, never by spinning up a fresh site.
42
+
43
+ This file covers the core path: publish → authorize → update → claim → verify.
44
+ Everything else (databases, domains, SSR, email, functions, client work) lives
45
+ in the **topic files indexed at the bottom** — read the one that matches the
46
+ task before improvising.
47
+
48
+ ## First publish on a new machine: ask for a permanent key in one step
49
+
50
+ On a no-Bearer publish, include `"agentName": "<your-tool-name>"` in the body
51
+ (e.g. `"Claude Code"`, `"Cursor"`). The response will include a `deviceAuth`
52
+ object alongside the usual `claimUrl`:
53
+
54
+ ```
55
+ "deviceAuth": {
56
+ "user_code": "ABCD-EFGH",
57
+ "device_code": "<long opaque token, keep private>",
58
+ "verification_url": "https://shiply.now/auth/ABCD-EFGH",
59
+ "poll_url": "https://shiply.now/api/v1/auth/device/poll",
60
+ "expires_in": 900,
61
+ "interval": 2
62
+ }
63
+ ```
64
+
65
+ Tell the user once, in plain language:
66
+
67
+ > Your site is live at <siteUrl>. To keep it forever AND let me publish more
68
+ > without re-pasting, open <verification_url> and click Allow.
69
+
70
+ Then immediately poll `verification_url`'s sibling endpoint until the user
71
+ acts:
72
+
73
+ ```
74
+ POST https://shiply.now/api/v1/auth/device/poll
75
+ {"device_code": "<the device_code from the response>"}
76
+ ```
77
+
78
+ The response is one of `{status:"pending"}` (keep polling at `interval`s),
79
+ `{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (PERSIST the key —
80
+ see "Save the key" below — the site is now permanent; Allow does both jobs in
81
+ one click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
82
+ the announced `expires_in` and respect `interval` — don't tight-loop.
83
+
84
+ After approval, every future publish uses `Authorization: Bearer <api_key>`
85
+ and the user's dashboard shows their sites without any further claiming.
86
+
87
+ ### Save the key — so EVERY future site is seamless (do this once)
88
+
89
+ Write the key to **`~/.shiply/credentials`** as `{"apiKey":"shp_..."}` (chmod
90
+ 600) — the exact file the shiply CLI reads. This is what makes "one Allow, ever"
91
+ work: from then on every publish (any site, any project, ANY FUTURE SESSION)
92
+ sends `Authorization: Bearer <key>` automatically and lands owned in the user's
93
+ dashboard — no re-authorizing, no claim step, zero clicks. An env var alone
94
+ (`SHIPLY_API_KEY`) works for the current shell but does NOT survive a new
95
+ session — the credentials file is what makes it permanent. `shiply login` writes
96
+ this file for you; if you publish over raw HTTP, write it yourself.
97
+
98
+ ## When the user pastes you something shiply-shaped, do this
99
+
100
+ These are the patterns you will see. Match the input, run the action — do not
101
+ ask the user to repeat themselves or explain.
102
+
103
+ - **Code like `SHIPLY-XXXXXXXX`** → it's a pairing code from
104
+ `shiply.now/claim/<slug>?pair=1`. The user wants to attach an anonymous
105
+ site to their account. Run `shiply claim verify SHIPLY-XXXXXXXX` from the
106
+ directory you originally published from (it reads `.shiply.json`). No CLI?
107
+ POST `https://shiply.now/api/v1/claim/pair/SHIPLY-XXXXXXXX/verify` with
108
+ `{"siteId":"...","claimToken":"..."}` taken from your earlier publish
109
+ response or `.shiply.json`. The user's browser auto-redirects on success;
110
+ tell them to switch back to it.
111
+ - **URL like `https://shiply.now/claim/<slug>?token=...`** → one-click claim
112
+ link. Tell the user to open it in the browser they are signed in to —
113
+ do NOT call any API yourself.
114
+ - **User says "claim my site" / "save it forever" with no code or URL** →
115
+ give them the `claimUrl` from your last publish response. If you no longer
116
+ have that response but `.shiply.json` is in the publish dir, reconstruct it
117
+ as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
118
+ Either way, also suggest `shiply login` so future publishes mint to their
119
+ account directly — that's the long-term fix and how the dashboard works.
120
+ - **User says "I have an API key now, use it from now on"** → persist it to
121
+ `~/.shiply/credentials` (`{"apiKey":"shp_..."}`, chmod 600 — see "Save the
122
+ key" above), then send `Authorization: Bearer <key>` on every future publish.
123
+ All future sites are then permanent and appear in their dashboard
124
+ automatically — across sessions, with no re-authorizing.
125
+ - **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** → no
126
+ action from you; they're on the device-flow consent screen. Keep polling
127
+ `poll_url` (see "First publish on a new machine" above). The poll response
128
+ will flip from `pending` to `approved` (with `api_key`) the moment they
129
+ click Allow.
130
+
131
+ ## Pick your interface (best first)
132
+
133
+ ### 1. MCP (native tools)
134
+ If the `shiply` MCP server is connected (https://shiply.now/mcp), use
135
+ `publish_site`. Every result includes a `toUpdate` field telling you the exact
136
+ call for updates — follow it, and a `shareSuggestion` you can relay to the user.
137
+ Core tools: `site_status`, `list_sites`, `get_site`, `delete_site`, `whoami`,
138
+ `duplicate_site`, `set_variable`, `get_analytics`, `set_handle`. There are
139
+ 100+ tools in total — call `tools/list` for the authoritative set; the topic
140
+ files below name the tools for their area (domains, databases, email,
141
+ functions, projects, contracts, marketplace, drives).
142
+
143
+ ### 2. CLI
144
+ ```bash
145
+ # IMPORTANT: the npm package is `shiply-cli`, not `shiply` (different package).
146
+ npm i -g shiply-cli # or: npx -y shiply-cli@latest <command>
147
+ # or: curl -fsSL https://shiply.now/install.sh | bash
148
+ shiply publish ./dir # live URL + confetti
149
+ shiply publish ./dir # run AGAIN after edits → updates the SAME site
150
+ shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
151
+ shiply login # email code → API key → sites become permanent
152
+ shiply claim verify <code> # confirm a SHIPLY-XXXXXXXX pairing code from /claim/<slug>?pair=1
153
+ # (uses .shiply.json from CWD; pairs agent session to user's browser)
154
+ ```
155
+ The CLI stores each directory's site in `.shiply.json` (slug + update token),
156
+ so repeat publishes reuse the URL automatically. `--new-site` opts out.
157
+ Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
158
+ lines for automation; exit code 0 = ready. For a static framework build
159
+ (Vite, Next export, Hugo, …) publish the BUILD OUTPUT, not the source — run
160
+ `shiply detect` to see what the CLI found; the full framework matrix is in
161
+ [references/publishing.md](references/publishing.md).
162
+
163
+ ### 3. Raw HTTP (no installs)
164
+ ```
165
+ 1. POST https://shiply.now/api/v1/publish (Content-Type: application/json)
166
+ {
167
+ "agentName": "<your tool name, e.g. Claude Code, Cursor, Codex>",
168
+ "files": [{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256 lowercase hex, optional>"}]
169
+ }
170
+ (+ "Authorization: Bearer shp_…" for permanent owned sites)
171
+ 2. PUT each file's bytes to response upload.uploads[].url
172
+ 3. POST upload.finalizeUrl with {"versionId":"<upload.versionId from step 1>"}
173
+ → both the publish and finalize responses include a "toUpdate" string: the
174
+ exact call to update THIS site next time. Follow it — never create a new one.
175
+ ```
176
+
177
+ **ALWAYS include `agentName` on anonymous publishes.** The response then
178
+ carries `deviceAuth` ({user_code, verification_url, poll_url, device_code,
179
+ expires_in, interval}) alongside `claimUrl`. Show the user `verification_url`,
180
+ poll `poll_url` every `interval` seconds with the `device_code`, and on
181
+ `{status:"approved"}` you get a permanent `api_key` PLUS the linked site
182
+ is claimed in the same Allow click. Without `agentName` the user has to
183
+ do a separate manual claim step every time.
184
+
185
+ **Updates (the response tells you how):** every publish/finalize response
186
+ includes a `toUpdate` string with the exact call — follow it verbatim. In short:
187
+ anonymous → include `"claimToken":"..."` (returned ONCE by the first publish —
188
+ save it); owned → include `"slug":"..."`. Hashes make updates cheap: unchanged
189
+ files are skipped server-side.
190
+
191
+ ## Verify before you say "done"
192
+
193
+ `shiply status <slug> --wait` polls until the site serves (exit 0, prints
194
+ `SITE_READY` / `SSL_READY`). `shiply verify <slug>` runs a deeper edge
195
+ SSL + HTTP + thumbnail check and prints a stable machine marker line
196
+ `VERIFY status=LIVE http=200 ssl=valid …` — parse that line in automation.
197
+ More manage commands (list, delete, rollback, promote, stable previews) are
198
+ in [references/publishing.md](references/publishing.md).
199
+
200
+ ## The lifecycle to explain to users
201
+ - Anonymous site: live immediately after finalize (no claim needed), expires
202
+ in 24 h. Give the user the `claimUrl` — claiming keeps it forever on a free
203
+ account. Until claimed, anonymous pages are served with a small injected
204
+ claim banner + OG meta tags — expected, not corruption. If no human is
205
+ present right now, save the `claimToken` (it's also the update key); a
206
+ fresh device flow can be started later via
207
+ POST /api/v1/auth/device/start {"agent_name":"..."}.
208
+ - API key (`shiply login` or POST /api/auth/agent/request-code →
209
+ verify-code): publishes are permanent and manageable.
210
+ - Paid plans add vanity handles (<name>.shiply.now), more custom domains,
211
+ storage, analytics: https://shiply.now/dashboard/plan
212
+
213
+ ## Everything else — read the topic file first
214
+
215
+ The files below sit next to this one in `references/` (installed with the
216
+ skill). Hosted copies: `https://shiply.now/skill/references/<file>`. Each is
217
+ self-contained — read the one matching the task before improvising; each
218
+ covers its CLI commands, MCP tools, AND REST endpoints.
219
+
220
+ * [Publishing & site management](references/publishing.md) — static framework
221
+ build matrix (Vite/Next/Hugo/…), `detect`, SPA mode, `.shiplyignore`,
222
+ list/delete/rollback/versions, `verify`, stable previews (`--as`),
223
+ `promote` preview→prod, `--json` output, form-data subcommands.
224
+ * [SSR frameworks](references/ssr-frameworks.md) — deploy SvelteKit, Astro,
225
+ Qwik, Nuxt/Nitro, React Router v7, Next.js (OpenNext), Hono/raw Workers
226
+ with server-side rendering.
227
+ * [Custom domains](references/custom-domains.md) — put the user's own domain
228
+ on a site: one-click OAuth DNS, manual CNAME, primary-subdomain SEO,
229
+ readiness polling.
230
+ * [Databases](references/databases.md) — per-site SQL: free D1 (SQLite at the
231
+ edge, browser shim) + Neon Postgres (branching), migrations, per-DB MCP
232
+ server.
233
+ * [Functions](references/functions.md) — `worker.js` server code on every
234
+ request: webhooks, cron triggers, secrets, runtime logs (Workers Lite,
235
+ Developer plan).
236
+ * [Email](references/email.md) — every owned site sends + receives: signup
237
+ capture, inbox, double-opt-in audiences, broadcasts, BYO sending domains.
238
+ * [Site features](references/site-features.md) — proxy routes (call AI APIs
239
+ without exposing keys), Site Data (forms/waitlists, zero backend),
240
+ password/invite-only access (`shiply access <slug> --password <pw>` /
241
+ `--restricted --email a@b.com` / `--public`), Variables, Drives (private
242
+ storage), path-mounting, public profile.
243
+ * [Client work](references/client-work.md) freelancer delivery: customer
244
+ intake projects + AI briefs, group work by client, e-sign contracts,
245
+ sell sites on the marketplace.
246
+ * [Authentication for the user's app](references/site-features.md) — shiply
247
+ does NOT host end-user auth; bring the user's own Clerk/Auth.js/Supabase
248
+ (pattern in Site features → "Bring your own auth").
249
+
250
+ ## Limits & references
251
+ ≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
252
+ Machine index: https://shiply.now/llms.txt (full single-file version:
253
+ https://shiply.now/llms-full.txt) · What's new: https://shiply.now/changelog.md
254
+ · OpenAPI: https://shiply.now/openapi.json · Docs: https://shiply.now/docs
@@ -44,7 +44,12 @@ see [email.md](email.md).
44
44
 
45
45
  ## Access control — password or invite-only sites (paid)
46
46
 
47
- To password-protect or restrict a site: PATCH /api/v1/publishes/<slug>/access
47
+ CLI (preferred): `shiply access <slug>` prints the current policy (plus a
48
+ stable `ACCESS slug=… mode=…` marker line); set it with
49
+ `shiply access <slug> --password "..."`, `--restricted --email a@b.com
50
+ --domain example.com` (both flags repeatable — each set REPLACES the previous
51
+ lists), or `--public` to open it up.
52
+ REST: PATCH /api/v1/publishes/<slug>/access
48
53
  with {"mode":"password","password":"..."} or {"mode":"restricted",
49
54
  "allowedEmails":[...],"allowedDomains":[...]}, or set mode "public" to open it.
50
55
  GET returns the policy (never the hash). MCP tool: `set_site_access`. Enforced
@@ -1,57 +1,57 @@
1
- ---
2
- type: reference
3
- title: SSR frameworks
4
- description: Deploy full server-side-rendered apps — SvelteKit, Astro, Qwik, the Nitro family (Nuxt, SolidStart, Analog, TanStack Start), React Router v7, Next.js via OpenNext, and Hono/raw Workers.
5
- timestamp: 2026-07-04
6
- ---
7
-
8
- # SSR / server frameworks (live: every major JS framework)
9
-
10
- Part of the shiply skill (see SKILL.md for publish basics). Requires sign-in +
11
- Developer plan.
12
-
13
- `shiply publish` auto-detects an SSR build and deploys the worker bundle with
14
- `nodejs_compat`, serving static assets from the edge. Live and verified on prod:
15
- SvelteKit (`@sveltejs/adapter-cloudflare`), Astro (`@astrojs/cloudflare`), Qwik
16
- City, the Nitro family (Nuxt, SolidStart, Analog, TanStack Start), React Router
17
- v7, and Next.js (via OpenNext) — plus wrangler workers (`wrangler.toml` with
18
- `main` — Hono, itty-router, raw `fetch`). **Build first** (`npm run build`),
19
- then `shiply publish .`. Force the static path with `--framework=<name>`, or
20
- skip SSR detection entirely with `--no-ssr`.
21
-
22
- Per-framework build setup:
23
-
24
- - **SvelteKit** — `@sveltejs/adapter-cloudflare`, then `npm run build`,
25
- `shiply publish .`
26
- - **Astro** — `@astrojs/cloudflare` adapter, `npm run build`, `shiply publish .`
27
- - **Qwik City** — `@builder.io/qwik-city` cloudflare-pages adapter,
28
- `npm run build`, `shiply publish .`
29
- - **Nitro family (Nuxt, SolidStart, Analog, TanStack Start)** — build with the
30
- `cloudflare`(-module) Nitro preset, then `shiply publish .`
31
- - **React Router v7** — framework mode (`@react-router/dev`), `react-router
32
- build`, then `shiply publish .`
33
- - **Next.js** — build via `@opennextjs/cloudflare` (OpenNext), then
34
- `shiply publish .`. Plain SSR works today; **ISR / on-demand revalidate is
35
- not live yet**, and Next 16 must build with `next build --webpack` (not
36
- Turbopack). `shiply publish` runs a **PREFLIGHT** on Next source dirs: it
37
- auto-fixes Turbopack build scripts, translates `vercel.json` crons to
38
- `.shiply/crons.json`, and refuses to publish raw source with no worker
39
- build — read its `FIX`/`WARN` lines, do what they say, re-run.
40
-
41
- Publish output is self-describing — trust it over memory: finalize streams
42
- per-phase progress lines, a failed worker deploy prints a structured `fix:`
43
- line to execute verbatim, and after every worker deploy the server probes
44
- the live site — a non-zero exit with a `runtime error:` line means
45
- DEPLOYED BUT BROKEN (the actual worker exception + likely fix are printed,
46
- e.g. a missing `shiply secret set`); fix the printed cause and republish.
47
- - **Wrangler workers** — any repo with `wrangler.toml` declaring `main`
48
- (Hono, itty-router, raw `fetch` handler) deploys as-is.
49
-
50
- How serving works: the deployed worker receives every request; static assets
51
- are tried first (the worker calls `env.ASSETS`, 200 = asset served from the
52
- edge / 404 = your SSR handler runs). The worker gets the same bindings,
53
- secrets, and crons as Functions — see [functions.md](functions.md). Bundle
54
- cap 8 MB.
55
-
56
- Plan gate: Free/Hobby get `402 payment_required` — upgrade at
57
- https://shiply.now/dashboard/plan.
1
+ ---
2
+ type: reference
3
+ title: SSR frameworks
4
+ description: Deploy full server-side-rendered apps — SvelteKit, Astro, Qwik, the Nitro family (Nuxt, SolidStart, Analog, TanStack Start), React Router v7, Next.js via OpenNext, and Hono/raw Workers.
5
+ timestamp: 2026-07-04
6
+ ---
7
+
8
+ # SSR / server frameworks (live: every major JS framework)
9
+
10
+ Part of the shiply skill (see SKILL.md for publish basics). Requires sign-in +
11
+ Developer plan.
12
+
13
+ `shiply publish` auto-detects an SSR build and deploys the worker bundle with
14
+ `nodejs_compat`, serving static assets from the edge. Live and verified on prod:
15
+ SvelteKit (`@sveltejs/adapter-cloudflare`), Astro (`@astrojs/cloudflare`), Qwik
16
+ City, the Nitro family (Nuxt, SolidStart, Analog, TanStack Start), React Router
17
+ v7, and Next.js (via OpenNext) — plus wrangler workers (`wrangler.toml` with
18
+ `main` — Hono, itty-router, raw `fetch`). **Build first** (`npm run build`),
19
+ then `shiply publish .`. Force the static path with `--framework=<name>`, or
20
+ skip SSR detection entirely with `--no-ssr`.
21
+
22
+ Per-framework build setup:
23
+
24
+ - **SvelteKit** — `@sveltejs/adapter-cloudflare`, then `npm run build`,
25
+ `shiply publish .`
26
+ - **Astro** — `@astrojs/cloudflare` adapter, `npm run build`, `shiply publish .`
27
+ - **Qwik City** — `@builder.io/qwik-city` cloudflare-pages adapter,
28
+ `npm run build`, `shiply publish .`
29
+ - **Nitro family (Nuxt, SolidStart, Analog, TanStack Start)** — build with the
30
+ `cloudflare`(-module) Nitro preset, then `shiply publish .`
31
+ - **React Router v7** — framework mode (`@react-router/dev`), `react-router
32
+ build`, then `shiply publish .`
33
+ - **Next.js** — build via `@opennextjs/cloudflare` (OpenNext), then
34
+ `shiply publish .`. Plain SSR works today; **ISR / on-demand revalidate is
35
+ not live yet**, and Next 16 must build with `next build --webpack` (not
36
+ Turbopack). `shiply publish` runs a **PREFLIGHT** on Next source dirs: it
37
+ auto-fixes Turbopack build scripts, translates `vercel.json` crons to
38
+ `.shiply/crons.json`, and refuses to publish raw source with no worker
39
+ build — read its `FIX`/`WARN` lines, do what they say, re-run.
40
+
41
+ Publish output is self-describing — trust it over memory: finalize streams
42
+ per-phase progress lines, a failed worker deploy prints a structured `fix:`
43
+ line to execute verbatim, and after every worker deploy the server probes
44
+ the live site — a non-zero exit with a `runtime error:` line means
45
+ DEPLOYED BUT BROKEN (the actual worker exception + likely fix are printed,
46
+ e.g. a missing `shiply secret set`); fix the printed cause and republish.
47
+ - **Wrangler workers** — any repo with `wrangler.toml` declaring `main`
48
+ (Hono, itty-router, raw `fetch` handler) deploys as-is.
49
+
50
+ How serving works: the deployed worker receives every request; static assets
51
+ are tried first (the worker calls `env.ASSETS`, 200 = asset served from the
52
+ edge / 404 = your SSR handler runs). The worker gets the same bindings,
53
+ secrets, and crons as Functions — see [functions.md](functions.md). Bundle
54
+ cap 8 MB.
55
+
56
+ Plan gate: Free/Hobby get `402 payment_required` — upgrade at
57
+ https://shiply.now/dashboard/plan.