shiply-cli 0.7.0 → 0.12.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/README.md +104 -47
- package/dist/claim.js +32 -0
- package/dist/data-init.js +33 -0
- package/dist/data.js +99 -0
- package/dist/db.js +225 -0
- package/dist/detect.js +35 -0
- package/dist/domain.js +56 -0
- package/dist/framework.js +110 -0
- package/dist/index.js +330 -57
- package/dist/login.js +60 -0
- package/dist/publish.js +2 -0
- package/package.json +38 -23
- package/skill/SKILL.md +359 -115
package/dist/login.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { hostname } from 'node:os';
|
|
2
|
+
import { api, ApiError } from './publish.js';
|
|
3
|
+
/** RFC 8628 client. The default for `shiply login`: no email round-trip,
|
|
4
|
+
* no manual code paste — just open a URL, click Allow, return to the
|
|
5
|
+
* terminal. The user can override the displayed agent name with --name
|
|
6
|
+
* (otherwise we synthesize from hostname). */
|
|
7
|
+
export async function loginViaDeviceFlow(base, agentNameOverride) {
|
|
8
|
+
const agentName = (agentNameOverride ?? `shiply-cli on ${hostname()}`).trim().slice(0, 80);
|
|
9
|
+
const start = await api(`${base}/api/v1/auth/device/start`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
headers: { 'content-type': 'application/json' },
|
|
12
|
+
body: JSON.stringify({ agent_name: agentName }),
|
|
13
|
+
});
|
|
14
|
+
// Print the URL prominently — the user must open it. We don't auto-launch
|
|
15
|
+
// a browser because the CLI runs in headless contexts (CI, SSH, containers)
|
|
16
|
+
// where opening a local browser would either fail silently or open in the
|
|
17
|
+
// wrong machine. Showing the URL is the predictable thing.
|
|
18
|
+
console.log('');
|
|
19
|
+
console.log(` → Open this URL to authorize ${agentName}:`);
|
|
20
|
+
console.log('');
|
|
21
|
+
console.log(` ${start.verification_url}`);
|
|
22
|
+
console.log('');
|
|
23
|
+
console.log(` Code shown there: ${start.user_code}`);
|
|
24
|
+
console.log(` Waiting up to ${Math.round(start.expires_in / 60)} minutes…`);
|
|
25
|
+
const deadline = Date.now() + start.expires_in * 1000;
|
|
26
|
+
const intervalMs = Math.max(start.interval, 1) * 1000;
|
|
27
|
+
while (Date.now() < deadline) {
|
|
28
|
+
await sleep(intervalMs);
|
|
29
|
+
let poll;
|
|
30
|
+
try {
|
|
31
|
+
poll = await api(`${base}/api/v1/auth/device/poll`, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ device_code: start.device_code }),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch (e) {
|
|
38
|
+
// Transient network blip — don't kill the whole flow, the user has time.
|
|
39
|
+
if (e instanceof ApiError && e.status >= 500)
|
|
40
|
+
continue;
|
|
41
|
+
throw e;
|
|
42
|
+
}
|
|
43
|
+
if (poll.status === 'approved' && poll.api_key) {
|
|
44
|
+
return { ok: true, apiKey: poll.api_key, slug: poll.slug_claimed ?? null };
|
|
45
|
+
}
|
|
46
|
+
if (poll.status === 'denied')
|
|
47
|
+
return { ok: false, reason: 'denied by you in the browser' };
|
|
48
|
+
if (poll.status === 'expired')
|
|
49
|
+
return { ok: false, reason: 'authorization request expired' };
|
|
50
|
+
if (poll.status === 'consumed') {
|
|
51
|
+
// Another process already collected the key for this device_code; we
|
|
52
|
+
// can't recover it. Tell the user to retry.
|
|
53
|
+
return { ok: false, reason: 'this authorization was already collected elsewhere — retry to mint a fresh one' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { ok: false, reason: 'authorization timed out — re-run `shiply login` to try again' };
|
|
57
|
+
}
|
|
58
|
+
function sleep(ms) {
|
|
59
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
60
|
+
}
|
package/dist/publish.js
CHANGED
|
@@ -40,6 +40,8 @@ export async function publish(dir, opts = {}) {
|
|
|
40
40
|
...(opts.spaMode ? { spaMode: true } : {}),
|
|
41
41
|
...(opts.claimToken ? { claimToken: opts.claimToken } : {}),
|
|
42
42
|
...(opts.slug ? { slug: opts.slug } : {}),
|
|
43
|
+
...(opts.attachDatabaseId ? { attachDatabaseId: opts.attachDatabaseId } : {}),
|
|
44
|
+
...(opts.previewBranchDbId ? { previewBranchDbId: opts.previewBranchDbId } : {}),
|
|
43
45
|
}),
|
|
44
46
|
});
|
|
45
47
|
await uploadAll(dir, created.upload.uploads);
|
package/package.json
CHANGED
|
@@ -1,23 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "shiply-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Publish static sites to shiply.now from the command line — instant web hosting for agents.",
|
|
5
|
-
"license": "MIT",
|
|
6
|
-
"type": "module",
|
|
7
|
-
"bin": {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "shiply-cli",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"description": "Publish static sites to shiply.now from the command line — 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
|
+
"typescript": "^5.8.0",
|
|
35
|
+
"vitest": "^3.1.0",
|
|
36
|
+
"@types/node": "^22.15.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
package/skill/SKILL.md
CHANGED
|
@@ -1,115 +1,359 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: shiply
|
|
3
|
-
description: Publish static sites to the web instantly with shiply.now and manage them (updates, SSL checks, custom domains, variables). Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report, or wants a live URL for generated files. Triggers - "publish this", "host this", "put this online", "give me a link", "update the site", shiply.
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# shiply — instant web hosting for agents
|
|
7
|
-
|
|
8
|
-
shiply.now puts files on the web in seconds. No account needed to start:
|
|
9
|
-
anonymous sites are live immediately, last 24 hours, and can be claimed into
|
|
10
|
-
an account to keep them. The cardinal rule:
|
|
11
|
-
|
|
12
|
-
**NEVER create a new site to update an existing one. Always re-publish to the
|
|
13
|
-
same site** — otherwise you litter subdomains and lose the user's URL.
|
|
14
|
-
|
|
15
|
-
##
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
- **
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
1
|
+
---
|
|
2
|
+
name: shiply
|
|
3
|
+
description: Publish static sites to the web instantly with shiply.now and manage them (updates, SSL checks, custom domains, variables). Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report, or wants a live URL for generated files. Triggers - "publish this", "host this", "put this online", "give me a link", "update the site", shiply.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# shiply — instant web hosting for agents
|
|
7
|
+
|
|
8
|
+
shiply.now puts files on the web in seconds. No account needed to start:
|
|
9
|
+
anonymous sites are live immediately, last 24 hours, and can be claimed into
|
|
10
|
+
an account to keep them. The cardinal rule:
|
|
11
|
+
|
|
12
|
+
**NEVER create a new site to update an existing one. Always re-publish to the
|
|
13
|
+
same site** — otherwise you litter subdomains and lose the user's URL.
|
|
14
|
+
|
|
15
|
+
## First publish on a new machine: ask for a permanent key in one step
|
|
16
|
+
|
|
17
|
+
On a no-Bearer publish, include `"agentName": "<your-tool-name>"` in the body
|
|
18
|
+
(e.g. `"Claude Code"`, `"Cursor"`). The response will include a `deviceAuth`
|
|
19
|
+
object alongside the usual `claimUrl`:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
"deviceAuth": {
|
|
23
|
+
"user_code": "ABCD-EFGH",
|
|
24
|
+
"device_code": "<long opaque token, keep private>",
|
|
25
|
+
"verification_url": "https://shiply.now/auth/ABCD-EFGH",
|
|
26
|
+
"poll_url": "https://shiply.now/api/v1/auth/device/poll",
|
|
27
|
+
"expires_in": 900,
|
|
28
|
+
"interval": 2
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Tell the user once, in plain language:
|
|
33
|
+
|
|
34
|
+
> Your site is live at <siteUrl>. To keep it forever AND let me publish more
|
|
35
|
+
> without re-pasting, open <verification_url> and click Allow.
|
|
36
|
+
|
|
37
|
+
Then immediately poll `verification_url`'s sibling endpoint until the user
|
|
38
|
+
acts:
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
POST https://shiply.now/api/v1/auth/device/poll
|
|
42
|
+
{"device_code": "<the device_code from the response>"}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The response is one of `{status:"pending"}` (keep polling at `interval`s),
|
|
46
|
+
`{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (save the key as
|
|
47
|
+
`SHIPLY_API_KEY`, the site is now permanent — Allow does both jobs in one
|
|
48
|
+
click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
|
|
49
|
+
the announced `expires_in` and respect `interval` — don't tight-loop.
|
|
50
|
+
|
|
51
|
+
After approval, every future publish uses `Authorization: Bearer <api_key>`
|
|
52
|
+
and the user's dashboard shows their sites without any further claiming.
|
|
53
|
+
|
|
54
|
+
## When the user pastes you something shiply-shaped, do this
|
|
55
|
+
|
|
56
|
+
These are the patterns you will see. Match the input, run the action — do not
|
|
57
|
+
ask the user to repeat themselves or explain.
|
|
58
|
+
|
|
59
|
+
- **Code like `SHIPLY-XXXXXXXX`** → it's a pairing code from
|
|
60
|
+
`shiply.now/claim/<slug>?pair=1`. The user wants to attach an anonymous
|
|
61
|
+
site to their account. Run `shiply claim verify SHIPLY-XXXXXXXX` from the
|
|
62
|
+
directory you originally published from (it reads `.shiply.json`). No CLI?
|
|
63
|
+
POST `https://shiply.now/api/v1/claim/pair/SHIPLY-XXXXXXXX/verify` with
|
|
64
|
+
`{"siteId":"...","claimToken":"..."}` taken from your earlier publish
|
|
65
|
+
response or `.shiply.json`. The user's browser auto-redirects on success;
|
|
66
|
+
tell them to switch back to it.
|
|
67
|
+
- **URL like `https://shiply.now/claim/<slug>?token=...`** → one-click claim
|
|
68
|
+
link. Tell the user to open it in the browser they are signed in to —
|
|
69
|
+
do NOT call any API yourself.
|
|
70
|
+
- **User says "claim my site" / "save it forever" with no code or URL** →
|
|
71
|
+
give them the `claimUrl` from your last publish response. If you no longer
|
|
72
|
+
have that response but `.shiply.json` is in the publish dir, reconstruct it
|
|
73
|
+
as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
|
|
74
|
+
Either way, also suggest `shiply login` so future publishes mint to their
|
|
75
|
+
account directly — that's the long-term fix and how the dashboard works.
|
|
76
|
+
- **User says "I have an API key now, use it from now on"** → save it as
|
|
77
|
+
`SHIPLY_API_KEY` in your environment / config and send
|
|
78
|
+
`Authorization: Bearer <key>` on every future publish. All future sites
|
|
79
|
+
are then permanent and appear in their dashboard automatically.
|
|
80
|
+
- **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** → no
|
|
81
|
+
action from you; they're on the device-flow consent screen. Keep polling
|
|
82
|
+
`poll_url` (see "First publish on a new machine" above). The poll response
|
|
83
|
+
will flip from `pending` to `approved` (with `api_key`) the moment they
|
|
84
|
+
click Allow.
|
|
85
|
+
|
|
86
|
+
## Pick your interface (best first)
|
|
87
|
+
|
|
88
|
+
### 1. MCP (native tools)
|
|
89
|
+
If the `shiply` MCP server is connected (https://shiply.now/mcp), use
|
|
90
|
+
`publish_site`. Every result includes a `toUpdate` field telling you the exact
|
|
91
|
+
call for updates — follow it, and a `shareSuggestion` you can relay to the user.
|
|
92
|
+
Other tools: `site_status`, `list_sites`, `get_site`, `set_handle`,
|
|
93
|
+
`duplicate_site`, `set_variable`, `get_analytics`, `delete_site`, `whoami`,
|
|
94
|
+
`add_custom_domain`, `connect_provider`, `add_subdomain`, `check_custom_domain`,
|
|
95
|
+
`sync_dns`, `list_custom_domains`, `remove_custom_domain`
|
|
96
|
+
(auto-DNS via OAuth provider connections — see **Custom domains** section below).
|
|
97
|
+
|
|
98
|
+
Newer capabilities (use when relevant):
|
|
99
|
+
- `set_site_access` — password / invite-only protect a site (paid).
|
|
100
|
+
- `set_link` — mount another owned **public** site at a path on a host
|
|
101
|
+
(host/docs → target).
|
|
102
|
+
- `set_profile` / `feature_site` — stand up the user's public portfolio at
|
|
103
|
+
<handle>.shiply.now and feature a public site on shiply.now/explore.
|
|
104
|
+
- **Drives** (private cloud storage for files/notes/context): `list_drives`,
|
|
105
|
+
`create_drive`, `drive_put_file` (driveId "default", utf8/base64, ≤2 MB),
|
|
106
|
+
`drive_list_files`, `drive_delete_file`, `publish_from_drive` (snapshot a
|
|
107
|
+
drive into a live site). Great for agent memory and assets you don't want on
|
|
108
|
+
a public site.
|
|
109
|
+
- `export_account` — JSON bundle of the user's data (no secrets).
|
|
110
|
+
|
|
111
|
+
### 2. CLI
|
|
112
|
+
```bash
|
|
113
|
+
npm i -g shiply-cli # or: curl -fsSL https://shiply.now/install.sh | bash
|
|
114
|
+
shiply publish ./dir # live URL + confetti
|
|
115
|
+
shiply publish ./dir # run AGAIN after edits → updates the SAME site
|
|
116
|
+
shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
|
|
117
|
+
shiply login # email code → API key → sites become permanent
|
|
118
|
+
shiply claim verify <code> # confirm a SHIPLY-XXXXXXXX pairing code from /claim/<slug>?pair=1
|
|
119
|
+
# (uses .shiply.json from CWD; pairs agent session to user's browser)
|
|
120
|
+
```
|
|
121
|
+
The CLI stores each directory's site in `.shiply.json` (slug + update token),
|
|
122
|
+
so repeat publishes reuse the URL automatically. `--new-site` opts out.
|
|
123
|
+
Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
|
|
124
|
+
lines for automation; exit code 0 = ready.
|
|
125
|
+
|
|
126
|
+
### 3. Raw HTTP (no installs)
|
|
127
|
+
```
|
|
128
|
+
1. POST https://shiply.now/api/v1/publish
|
|
129
|
+
{"files":[{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]}
|
|
130
|
+
(+ "Authorization: Bearer shp_…" for permanent owned sites)
|
|
131
|
+
2. PUT each file's bytes to response upload.uploads[].url
|
|
132
|
+
3. POST upload.finalizeUrl with {"versionId":"..."}
|
|
133
|
+
```
|
|
134
|
+
**Updates:** anonymous → include `"claimToken":"..."` (returned ONCE by the
|
|
135
|
+
first publish — save it); owned → include `"slug":"..."`. Hashes make updates
|
|
136
|
+
cheap: unchanged files are skipped server-side.
|
|
137
|
+
|
|
138
|
+
## The lifecycle to explain to users
|
|
139
|
+
- Anonymous site: live instantly, expires in 24 h. Give the user the
|
|
140
|
+
`claimUrl` — claiming keeps it forever on a free account.
|
|
141
|
+
- API key (`shiply login` or POST /api/auth/agent/request-code →
|
|
142
|
+
verify-code): publishes are permanent and manageable.
|
|
143
|
+
- Paid plans add vanity handles (<name>.shiply.now), more custom domains,
|
|
144
|
+
storage, analytics: https://shiply.now/dashboard/plan
|
|
145
|
+
|
|
146
|
+
## Custom domains
|
|
147
|
+
|
|
148
|
+
Serve a site at `www.yourdomain.com` instead of a `*.shiply.now` URL. Requires a paid plan and a Bearer API key.
|
|
149
|
+
|
|
150
|
+
### CLI commands
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
shiply domain add example.com # register a root domain (detects provider)
|
|
154
|
+
shiply domain connect example.com # one-click OAuth DNS setup (Cloudflare, GoDaddy, IONOS, etc.)
|
|
155
|
+
shiply domain sub add www.example.com --site my-site # map a subdomain → site
|
|
156
|
+
shiply domain ls # list all domains + subdomains + status
|
|
157
|
+
shiply domain sync example.com # re-sync DNS records with a connected provider
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
For one-click connect (`shiply domain connect`), the CLI prints an authorization URL — the user must open it in a browser and approve the OAuth grant before DNS is written.
|
|
161
|
+
|
|
162
|
+
For unsupported providers, `shiply domain add` returns the CNAME record to add manually:
|
|
163
|
+
`www CNAME cname.shiply.now`
|
|
164
|
+
|
|
165
|
+
Check status after connecting:
|
|
166
|
+
```bash
|
|
167
|
+
shiply status www.example.com --wait # polls until SSL_READY + SITE_READY; exit 0 when live
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### MCP tools (same operations via agent tools)
|
|
171
|
+
|
|
172
|
+
| Tool | Purpose |
|
|
173
|
+
|------|---------|
|
|
174
|
+
| `add_custom_domain` | Add a root domain and detect the provider |
|
|
175
|
+
| `connect_provider` | Start OAuth → returns `{ url }` (show to user to authorize) |
|
|
176
|
+
| `add_subdomain` | Map a subdomain to a site slug |
|
|
177
|
+
| `check_custom_domain` | Live CNAME + SSL + readiness probe |
|
|
178
|
+
| `sync_dns` | Re-push DNS records to a connected provider |
|
|
179
|
+
| `list_custom_domains` | List all domains and their subdomain mappings |
|
|
180
|
+
| `remove_custom_domain` | Remove a domain and all its mappings |
|
|
181
|
+
|
|
182
|
+
### REST endpoints (`/api/v1/custom-domains`)
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
GET /api/v1/custom-domains list domains
|
|
186
|
+
POST /api/v1/custom-domains add domain { domain }
|
|
187
|
+
DELETE /api/v1/custom-domains/{domain} remove domain
|
|
188
|
+
POST /api/v1/custom-domains/{domain}/subdomains add subdomain { subdomain, slug }
|
|
189
|
+
POST /api/v1/custom-domains/{domain}/connect start OAuth → { url }
|
|
190
|
+
POST /api/v1/custom-domains/{domain}/sync-dns re-sync DNS
|
|
191
|
+
POST /api/v1/custom-domains/{domain}/check live status
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
All require `Authorization: Bearer shp_…`.
|
|
195
|
+
|
|
196
|
+
## Power features (Bearer key)
|
|
197
|
+
- **Custom domains**: see the **Custom domains** section above.
|
|
198
|
+
REST: `POST /api/v1/custom-domains {"domain"}` → add domain + detect provider.
|
|
199
|
+
Then `POST /api/v1/custom-domains/{domain}/connect` → OAuth one-click (show URL to user)
|
|
200
|
+
or add the CNAME manually. Map subdomains: `POST /api/v1/custom-domains/{domain}/subdomains {"subdomain","slug"}`.
|
|
201
|
+
Poll `POST /api/v1/custom-domains/{domain}/check` until `ready:true`.
|
|
202
|
+
- **SSL/readiness checker**: `shiply status <slug-or-domain> --wait` or the
|
|
203
|
+
check endpoint — confirms certificate + serving before telling the user
|
|
204
|
+
it's done.
|
|
205
|
+
- **Variables**: encrypted per-user KV for API keys the user's sites need
|
|
206
|
+
(GET/PUT /api/v1/variables, DELETE /api/v1/variables/{NAME}); Supabase can
|
|
207
|
+
be connected from the dashboard and lands here as SUPABASE_URL +
|
|
208
|
+
SUPABASE_ANON_KEY.
|
|
209
|
+
- **Rollback**: POST /api/v1/publish/{slug}/rollback {"versionId"} flips any
|
|
210
|
+
finalized version live instantly.
|
|
211
|
+
- **SPA**: pass `"spaMode":true` so deep links serve index.html.
|
|
212
|
+
|
|
213
|
+
## Proxy routes — AI/API calls WITHOUT exposing keys
|
|
214
|
+
NEVER embed API keys in published HTML/JS. Instead: store the key as a
|
|
215
|
+
Variable (PUT /api/v1/variables), then include `.shiply/proxy.json` in the
|
|
216
|
+
published files declaring routes, e.g. {"proxies":{"/api/chat":{"upstream":
|
|
217
|
+
"https://openrouter.ai/api/v1/chat/completions","method":"POST","headers":
|
|
218
|
+
{"Authorization":"Bearer ${OPENROUTER_API_KEY}"}}}}. The page then calls the
|
|
219
|
+
relative path (fetch('/api/chat')) and shiply injects the secret server-side.
|
|
220
|
+
Prefix routes "/api/db/*" forward subpaths (great for Supabase REST).
|
|
221
|
+
Owned sites only; https public upstreams only. Docs: /docs/proxy-routes
|
|
222
|
+
|
|
223
|
+
## Site Data — forms/waitlists/guestbooks with ZERO backend
|
|
224
|
+
When the user wants to collect anything from visitors (waitlist, contact
|
|
225
|
+
form, RSVP, guestbook): include `.shiply/data.json` in the published files,
|
|
226
|
+
e.g. {"collections":{"signups":{"fields":{"email":{"type":"email",
|
|
227
|
+
"required":true}},"access":{"read":"owner","insert":"public"}}}}, then have
|
|
228
|
+
the page POST JSON to the relative path /.shiply/data/signups. Records are
|
|
229
|
+
validated server-side; the owner reads them in the dashboard (Data section,
|
|
230
|
+
CSV export) or GET /api/v1/publishes/<slug>/data/<collection> with a Bearer
|
|
231
|
+
key. Owned sites only. Docs: /docs/site-data
|
|
232
|
+
|
|
233
|
+
## SQL databases (Cloudflare D1 + Neon Postgres)
|
|
234
|
+
Two engines, same CLI/REST/MCP surface. **D1 (SQLite at the edge)** is
|
|
235
|
+
free on every plan and queryable from the browser via a built-in fetch
|
|
236
|
+
shim — no API key in the page. **Neon Postgres** is a developer-plan
|
|
237
|
+
add-on with copy-on-write branching; the connection URI is encrypted at
|
|
238
|
+
rest and surfaced to the site's Worker as `env.DATABASE_URL`.
|
|
239
|
+
|
|
240
|
+
### D1 (default)
|
|
241
|
+
```bash
|
|
242
|
+
shiply db create app # provision a D1 (binding APP_DB)
|
|
243
|
+
shiply db ls # list (name, provider, size, attached site)
|
|
244
|
+
shiply db sql app "SELECT 1" # one-shot query against your account
|
|
245
|
+
shiply db sql app "SELECT * FROM t WHERE id=?1" --params '[42]'
|
|
246
|
+
shiply db migrate app ./migrations # apply every *.sql in dir, sorted
|
|
247
|
+
shiply db attach app --site my-site # bind an existing DB to another site
|
|
248
|
+
shiply db delete app --yes # drop the DB and every row
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Run `shiply db create` inside a publish directory: it records the new
|
|
252
|
+
`databaseId` into `.shiply.json` so the next `shiply publish` auto-attaches
|
|
253
|
+
it. Site code then calls the shim:
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
fetch('/_shiply/db/APP_DB/query', {
|
|
257
|
+
method: 'POST',
|
|
258
|
+
headers: { 'content-type': 'application/json' },
|
|
259
|
+
body: JSON.stringify({ sql: 'SELECT * FROM posts', params: [] }),
|
|
260
|
+
}).then(r => r.json()) // → { results: [...], meta: {rows_read, rows_written, duration} }
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
**Public by default**: the shim has no visitor auth, so anyone who can fetch
|
|
264
|
+
the site can run any SQL. Until a private-mode gate ships, recommend it
|
|
265
|
+
strictly as a SELECT-only public data layer. Reads always work; writes hit
|
|
266
|
+
402 once the DB exceeds the plan storage cap (Founder Special 100 MB,
|
|
267
|
+
Hobby 1 GB, Developer 10 GB).
|
|
268
|
+
|
|
269
|
+
### Neon Postgres (developer plan)
|
|
270
|
+
Pass `--postgres` on create. One isolated Neon project per database;
|
|
271
|
+
binding defaults to `DATABASE_URL`. Free / Hobby get `402
|
|
272
|
+
payment_required` — point the user at https://shiply.now/dashboard/plan.
|
|
273
|
+
|
|
274
|
+
```bash
|
|
275
|
+
shiply db create app --postgres # provision Neon (~5–10s)
|
|
276
|
+
shiply db branch app dev # copy-on-write branch off main
|
|
277
|
+
shiply db branches app # list branches
|
|
278
|
+
shiply db delete-branch dev --yes # drop branch + endpoint
|
|
279
|
+
shiply db merge dev # NO-OP — prints pg_dump tip (see below)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
After the next `shiply publish` the serving Worker receives
|
|
283
|
+
`env.DATABASE_URL`. Minimal example:
|
|
284
|
+
|
|
285
|
+
```js
|
|
286
|
+
import { neon } from '@neondatabase/serverless'
|
|
287
|
+
const sql = neon(process.env.DATABASE_URL)
|
|
288
|
+
const rows = await sql`SELECT now()`
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Anything that speaks Postgres works: `pg`, `postgres`, drizzle, kysely,
|
|
292
|
+
prisma (with the Neon adapter).
|
|
293
|
+
|
|
294
|
+
**Branching is the headline feature.** Branches are cheap, instant,
|
|
295
|
+
copy-on-write forks of `main`. Bind a publish to a specific branch with
|
|
296
|
+
`shiply publish --preview-branch=<branchDbId>` (use the `site_databases`
|
|
297
|
+
row id printed by `shiply db branch`, NOT the raw Neon branch id) —
|
|
298
|
+
great for preview deploys and safe schema rollouts.
|
|
299
|
+
|
|
300
|
+
**`shiply db merge` is intentionally a no-op.** Neon has no server-side
|
|
301
|
+
merge. To promote branch data into the parent:
|
|
302
|
+
`pg_dump "$BRANCH_DATABASE_URL" | psql "$PARENT_DATABASE_URL"`.
|
|
303
|
+
Run schema migrations against both branches to keep them aligned.
|
|
304
|
+
|
|
305
|
+
**Not yet supported on Neon**: branch-of-branch (branches off `main`
|
|
306
|
+
only), auto preview-branch on every publish, cross-region projects.
|
|
307
|
+
|
|
308
|
+
### REST + MCP (provider-agnostic)
|
|
309
|
+
REST (Bearer `shp_…`): `POST /api/v1/databases` (pass
|
|
310
|
+
`"provider":"neon"` for Postgres) to create, `POST
|
|
311
|
+
/api/v1/databases/{id}/query` to query (works for D1 and Neon), `POST
|
|
312
|
+
.../attach` to bind, `DELETE` to drop. Neon-only:
|
|
313
|
+
`POST/GET/DELETE /api/v1/databases/{id}/branches[/{branchId}]` (400 on
|
|
314
|
+
D1 rows). Per-DB MCP server (so an agent can be scoped to one DB):
|
|
315
|
+
`https://shiply.now/api/mcp/db/<id>/sse` — tools: `db_query`,
|
|
316
|
+
`db_list_tables`, `db_schema`. The same three tools work against both
|
|
317
|
+
providers; `db_schema` introspects `sqlite_master` (D1) or
|
|
318
|
+
`information_schema` (Neon). Full docs:
|
|
319
|
+
https://shiply.now/docs/databases
|
|
320
|
+
|
|
321
|
+
## Make a site private (paid plans)
|
|
322
|
+
To password-protect or restrict a site: PATCH /api/v1/publishes/<slug>/access
|
|
323
|
+
with {"mode":"password","password":"..."} or {"mode":"restricted",
|
|
324
|
+
"allowedEmails":[...],"allowedDomains":[...]}, or set mode "public" to open it.
|
|
325
|
+
MCP tool set_site_access does the same. Enforced before any content is served;
|
|
326
|
+
changing settings signs current visitors out. Docs: /docs/access-control
|
|
327
|
+
|
|
328
|
+
## Any static site works — frameworks + SSGs auto-detected
|
|
329
|
+
shiply hosts any static site. Publish the BUILD OUTPUT, never the source:
|
|
330
|
+
run the build, then publish the output dir with SPA mode for client-routed
|
|
331
|
+
apps. The CLI auto-detects 16+ frameworks and tells you the right command.
|
|
332
|
+
Run `shiply detect` to preview what it found without uploading.
|
|
333
|
+
- Vite (React/Vue/Svelte/Solid/Qwik): `npm run build` then `shiply publish dist --spa`
|
|
334
|
+
- Create React App: `npm run build` then `shiply publish build --spa`
|
|
335
|
+
- Next.js: set `output: "export"` in next.config, `npm run build`, then
|
|
336
|
+
`shiply publish out` (no --spa; export emits real HTML per route)
|
|
337
|
+
- Astro: `npm run build` then `shiply publish dist`
|
|
338
|
+
- SvelteKit (adapter-static): `npm run build` then `shiply publish build --spa`
|
|
339
|
+
- Nuxt: `npx nuxt generate` then `shiply publish .output/public`
|
|
340
|
+
- Remix (client build): `npm run build` then `shiply publish build/client --spa`
|
|
341
|
+
- Docusaurus: `npm run build` then `shiply publish build`
|
|
342
|
+
- Hugo: `hugo` then `shiply publish public`
|
|
343
|
+
- Jekyll: `bundle exec jekyll build` then `shiply publish _site`
|
|
344
|
+
- Eleventy: `npx @11ty/eleventy` then `shiply publish _site`
|
|
345
|
+
- MkDocs: `mkdocs build` then `shiply publish site`
|
|
346
|
+
- Hexo: `npx hexo generate` then `shiply publish public`
|
|
347
|
+
- SolidStart (static): `npm run build` then `shiply publish dist/public`
|
|
348
|
+
- Qwik City (static adapter): `npm run build` then `shiply publish dist`
|
|
349
|
+
- Anything else with a built `dist/`/`build/`/`out/`/`_site/`/`public/`/
|
|
350
|
+
`.output/public/` folder: generic fallback picks it up.
|
|
351
|
+
- Plain HTML: `shiply publish .` — no build step.
|
|
352
|
+
Override auto-detection with `--framework=<name>`. Use the CLI (not inline
|
|
353
|
+
MCP files) for builds — hashed bundles often exceed the 50-file inline cap,
|
|
354
|
+
and the CLI hash-skips unchanged chunks on updates.
|
|
355
|
+
|
|
356
|
+
## Limits & references
|
|
357
|
+
≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
|
|
358
|
+
Machine guide: https://shiply.now/llms.txt · OpenAPI:
|
|
359
|
+
https://shiply.now/openapi.json · Docs: https://shiply.now/docs
|