shiply-cli 0.27.1 → 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 +54 -0
- package/dist/index.js +57 -3
- package/dist/publish.js +12 -3
- package/package.json +42 -42
- package/skill/CHANGELOG.md +10 -0
- package/skill/SKILL.md +254 -253
- package/skill/references/site-features.md +6 -1
- package/skill/references/ssr-frameworks.md +57 -57
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
|
|
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/dist/publish.js
CHANGED
|
@@ -21,11 +21,17 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
21
21
|
const backoffMs = (attempt) => Math.min(8000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
|
|
22
22
|
/** fetch() with a per-attempt timeout + retry-with-backoff on transient failures
|
|
23
23
|
* (network errors, aborts/timeouts, and 5xx gateway statuses). Bodies here are
|
|
24
|
-
* strings/Buffers, so re-sending on retry is safe.
|
|
24
|
+
* strings/Buffers, so re-sending on retry is safe.
|
|
25
|
+
*
|
|
26
|
+
* `alsoRetry`: extra statuses the CALLER knows are safe to retry. The global
|
|
27
|
+
* set excludes 500 to protect non-idempotent creates — but an idempotent
|
|
28
|
+
* presigned PUT (upload) SHOULD retry a 500 (observed: R2 throws transient
|
|
29
|
+
* 500s mid-publish; without this, one blip failed the whole publish). */
|
|
25
30
|
export async function fetchWithRetry(url, init, opts = {}) {
|
|
26
31
|
const attempts = opts.attempts ?? 4;
|
|
27
32
|
const timeoutMs = opts.timeoutMs ?? 60_000;
|
|
28
33
|
const label = opts.label ?? url;
|
|
34
|
+
const alsoRetry = new Set(opts.alsoRetry ?? []);
|
|
29
35
|
let lastErr;
|
|
30
36
|
for (let i = 0; i < attempts; i++) {
|
|
31
37
|
const ac = new AbortController();
|
|
@@ -33,7 +39,7 @@ export async function fetchWithRetry(url, init, opts = {}) {
|
|
|
33
39
|
try {
|
|
34
40
|
const res = await fetch(url, { ...init, signal: ac.signal });
|
|
35
41
|
clearTimeout(timer);
|
|
36
|
-
if (TRANSIENT_STATUS.has(res.status) && i < attempts - 1) {
|
|
42
|
+
if ((TRANSIENT_STATUS.has(res.status) || alsoRetry.has(res.status)) && i < attempts - 1) {
|
|
37
43
|
process.stderr.write(` ↻ ${label} returned ${res.status} — retrying (${i + 2}/${attempts})…\n`);
|
|
38
44
|
await sleep(backoffMs(i));
|
|
39
45
|
continue;
|
|
@@ -235,7 +241,10 @@ async function uploadAll(dir, targets) {
|
|
|
235
241
|
while (next < targets.length) {
|
|
236
242
|
const t = targets[next++];
|
|
237
243
|
const body = await readFile(join(dir, t.path));
|
|
238
|
-
const res = await fetchWithRetry(t.url, { method: t.method, headers: t.headers, body },
|
|
244
|
+
const res = await fetchWithRetry(t.url, { method: t.method, headers: t.headers, body },
|
|
245
|
+
// Presigned object PUT = idempotent → a transient 500 (R2 does throw
|
|
246
|
+
// them) must be retried, not fail the whole publish.
|
|
247
|
+
{ label: `upload ${t.path}`, alsoRetry: [500] });
|
|
239
248
|
if (!res.ok) {
|
|
240
249
|
if (res.status === 403) {
|
|
241
250
|
throw new Error(`upload failed for ${t.path}: HTTP 403 — presigned URLs likely expired ` +
|
package/package.json
CHANGED
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "shiply-cli",
|
|
3
|
-
"version": "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
|
-
}
|
|
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
|
+
}
|
package/skill/CHANGELOG.md
CHANGED
|
@@ -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
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
https://shiply.now/llms
|
|
253
|
-
|
|
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
|
-
|
|
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.
|