shiply-cli 0.4.1 → 0.6.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/framework.js +52 -0
- package/dist/index.js +44 -4
- package/dist/manifest.js +6 -1
- package/package.json +1 -1
- package/skill/SKILL.md +115 -75
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { readFile, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const exists = async (p) => stat(p).then((s) => s.isFile() || s.isDirectory(), () => false);
|
|
4
|
+
/** Detect a framework *source* directory (package.json, no root index.html)
|
|
5
|
+
* so `shiply publish .` on a React/Vue/etc project can do the right thing:
|
|
6
|
+
* publish the build output with SPA mode instead of raw source files. */
|
|
7
|
+
export async function detectFramework(dir) {
|
|
8
|
+
if (await exists(join(dir, 'index.html'))) {
|
|
9
|
+
// Vite keeps index.html at the root of the *source* too — only treat it as
|
|
10
|
+
// a source dir when a vite config is present alongside it.
|
|
11
|
+
const viteConfig = (await Promise.all(['vite.config.ts', 'vite.config.js', 'vite.config.mjs'].map((f) => exists(join(dir, f))))).some(Boolean);
|
|
12
|
+
if (!viteConfig)
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
let pkgRaw;
|
|
16
|
+
try {
|
|
17
|
+
pkgRaw = await readFile(join(dir, 'package.json'), 'utf8');
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
let deps;
|
|
23
|
+
try {
|
|
24
|
+
const pkg = JSON.parse(pkgRaw);
|
|
25
|
+
deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const has = (name) => name in deps;
|
|
31
|
+
if (has('next')) {
|
|
32
|
+
return { framework: 'Next.js', outputDir: 'out', spa: false, buildCommand: 'npm run build (needs `output: "export"` in next.config)' };
|
|
33
|
+
}
|
|
34
|
+
if (has('astro'))
|
|
35
|
+
return { framework: 'Astro', outputDir: 'dist', spa: false, buildCommand: 'npm run build' };
|
|
36
|
+
if (has('@sveltejs/kit')) {
|
|
37
|
+
return { framework: 'SvelteKit', outputDir: 'build', spa: true, buildCommand: 'npm run build (use adapter-static)' };
|
|
38
|
+
}
|
|
39
|
+
if (has('vite'))
|
|
40
|
+
return { framework: 'Vite', outputDir: 'dist', spa: true, buildCommand: 'npm run build' };
|
|
41
|
+
if (has('react-scripts'))
|
|
42
|
+
return { framework: 'Create React App', outputDir: 'build', spa: true, buildCommand: 'npm run build' };
|
|
43
|
+
if (has('react') || has('vue') || has('svelte')) {
|
|
44
|
+
return { framework: 'JavaScript app', outputDir: 'dist', spa: true, buildCommand: 'npm run build' };
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** If the detected output dir exists and has an index.html, return it. */
|
|
49
|
+
export async function findBuildOutput(dir, hint) {
|
|
50
|
+
const out = join(dir, hint.outputDir);
|
|
51
|
+
return (await exists(join(out, 'index.html'))) ? out : null;
|
|
52
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
3
4
|
import { parseArgs } from 'node:util';
|
|
4
5
|
import { confetti } from './confetti.js';
|
|
6
|
+
import { detectFramework, findBuildOutput } from './framework.js';
|
|
5
7
|
import { loadApiKey, saveApiKey } from './config.js';
|
|
6
8
|
import { api, DEFAULT_BASE, publish, resolveBase } from './publish.js';
|
|
7
9
|
import { installSkill } from './skill.js';
|
|
@@ -14,6 +16,7 @@ Usage:
|
|
|
14
16
|
Re-running UPDATES the same site (state in .shiply.json)
|
|
15
17
|
shiply update <dir> Same as publish when .shiply.json exists
|
|
16
18
|
shiply status <slug-or-domain> [--wait] SSL + readiness check (agent-friendly output)
|
|
19
|
+
shiply duplicate <slug> Server-side copy of an owned site → new URL
|
|
17
20
|
shiply skill [--project] Install the shiply skill for your AI agent
|
|
18
21
|
(global ~/.claude/skills, or ./.claude/skills with --project)
|
|
19
22
|
shiply login [--email <address>] Email a 6-digit code, mint + save an API key
|
|
@@ -89,8 +92,29 @@ async function main() {
|
|
|
89
92
|
if (!dir)
|
|
90
93
|
throw new Error(`usage: shiply ${cmd} <dir>`);
|
|
91
94
|
const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
|
|
95
|
+
// Full React/Vue/etc apps work great — but you publish the BUILD OUTPUT,
|
|
96
|
+
// not the source. Detect source dirs and do the right thing.
|
|
97
|
+
let publishDir = dir;
|
|
98
|
+
let spa = values.spa;
|
|
99
|
+
const hint = await detectFramework(dir);
|
|
100
|
+
if (hint) {
|
|
101
|
+
const out = await findBuildOutput(dir, hint);
|
|
102
|
+
if (out) {
|
|
103
|
+
publishDir = out;
|
|
104
|
+
if (hint.spa && spa === undefined)
|
|
105
|
+
spa = true;
|
|
106
|
+
console.log(`✔ ${hint.framework} project detected — publishing ${out}${hint.spa ? ' with SPA mode' : ''}`);
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
throw new Error(`this looks like a ${hint.framework} project (source code, not a built site).\n` +
|
|
110
|
+
` Build it first, then publish the output:\n` +
|
|
111
|
+
` ${hint.buildCommand}\n` +
|
|
112
|
+
` shiply ${cmd} ${join(dir, hint.outputDir)}${hint.spa ? ' --spa' : ''}\n` +
|
|
113
|
+
` (or pass the built folder directly if it lives elsewhere)`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
92
116
|
// same command, same URL: reuse this directory's site automatically
|
|
93
|
-
const state = await readState(
|
|
117
|
+
const state = await readState(publishDir);
|
|
94
118
|
let claimToken = values['claim-token'] ?? state?.claimToken;
|
|
95
119
|
let slug = state?.owned && apiKey ? state.slug : undefined;
|
|
96
120
|
if (values['new-site']) {
|
|
@@ -101,14 +125,14 @@ async function main() {
|
|
|
101
125
|
throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
|
|
102
126
|
}
|
|
103
127
|
const updating = Boolean(claimToken || slug);
|
|
104
|
-
const res = await publish(
|
|
128
|
+
const res = await publish(publishDir, {
|
|
105
129
|
apiKey,
|
|
106
130
|
base: values.base,
|
|
107
|
-
spaMode:
|
|
131
|
+
spaMode: spa,
|
|
108
132
|
claimToken,
|
|
109
133
|
slug,
|
|
110
134
|
});
|
|
111
|
-
await writeState(
|
|
135
|
+
await writeState(publishDir, {
|
|
112
136
|
slug: res.slug,
|
|
113
137
|
siteUrl: res.siteUrl,
|
|
114
138
|
...(res.claimToken ? { claimToken: res.claimToken } : state?.claimToken && updating ? { claimToken: state.claimToken } : {}),
|
|
@@ -169,6 +193,22 @@ async function main() {
|
|
|
169
193
|
await new Promise((r) => setTimeout(r, 5000));
|
|
170
194
|
}
|
|
171
195
|
}
|
|
196
|
+
case 'duplicate': {
|
|
197
|
+
if (!dir)
|
|
198
|
+
throw new Error('usage: shiply duplicate <slug>');
|
|
199
|
+
const apiKey = values.key ?? (await loadApiKey());
|
|
200
|
+
if (!apiKey)
|
|
201
|
+
throw new Error('duplicate needs an API key — run `shiply login` first');
|
|
202
|
+
const base = resolveBase(values.base);
|
|
203
|
+
const out = await api(`${base}/api/v1/publishes/${dir}/duplicate`, {
|
|
204
|
+
method: 'POST',
|
|
205
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${apiKey}` },
|
|
206
|
+
body: JSON.stringify({}),
|
|
207
|
+
});
|
|
208
|
+
console.log(`✔ duplicated ${dir} → ${out.slug} (${out.filesCount} files)`);
|
|
209
|
+
console.log(`\n ${out.siteUrl}\n`);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
172
212
|
case 'skill': {
|
|
173
213
|
const written = await installSkill(Boolean(values.project));
|
|
174
214
|
for (const w of written)
|
package/dist/manifest.js
CHANGED
|
@@ -36,7 +36,8 @@ export const contentTypeFor = (path) => MIME[extname(path).toLowerCase()] ?? 'ap
|
|
|
36
36
|
const SKIP_DIRS = new Set(['node_modules']);
|
|
37
37
|
/** Walk a directory into the publish manifest: posix-relative paths, byte
|
|
38
38
|
* sizes, sha256 hex hashes (server hash-skips unchanged files on update).
|
|
39
|
-
* Dot entries and node_modules are never published.
|
|
39
|
+
* Dot entries and node_modules are never published — except the .shiply/
|
|
40
|
+
* config directory (proxy.json, data.json), which the server consumes. */
|
|
40
41
|
export async function buildManifest(dir) {
|
|
41
42
|
const out = [];
|
|
42
43
|
await walk(dir, '', out);
|
|
@@ -45,6 +46,10 @@ export async function buildManifest(dir) {
|
|
|
45
46
|
async function walk(abs, rel, out) {
|
|
46
47
|
const entries = await readdir(abs, { withFileTypes: true });
|
|
47
48
|
for (const e of entries) {
|
|
49
|
+
if (e.name === '.shiply' && e.isDirectory() && rel === '') {
|
|
50
|
+
await walk(join(abs, e.name), '.shiply', out);
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
48
53
|
if (e.name.startsWith('.') || SKIP_DIRS.has(e.name))
|
|
49
54
|
continue;
|
|
50
55
|
const childAbs = join(abs, e.name);
|
package/package.json
CHANGED
package/skill/SKILL.md
CHANGED
|
@@ -1,75 +1,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
|
-
## Pick your interface (best first)
|
|
16
|
-
|
|
17
|
-
### 1. MCP (native tools)
|
|
18
|
-
If the `shiply` MCP server is connected (https://shiply.now/mcp), use
|
|
19
|
-
`publish_site`. Every result includes a `toUpdate` field telling you the exact
|
|
20
|
-
call for updates — follow it. Other tools: `site_status`, `list_sites`,
|
|
21
|
-
`set_handle`, `set_variable`, `add_domain`, `check_domain`, `get_analytics`,
|
|
22
|
-
`delete_site`, `rollback` via dashboard.
|
|
23
|
-
|
|
24
|
-
### 2. CLI
|
|
25
|
-
```bash
|
|
26
|
-
npm i -g shiply-cli # or: curl -fsSL https://shiply.now/install.sh | bash
|
|
27
|
-
shiply publish ./dir # live URL + confetti
|
|
28
|
-
shiply publish ./dir # run AGAIN after edits → updates the SAME site
|
|
29
|
-
shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
|
|
30
|
-
shiply login # email code → API key → sites become permanent
|
|
31
|
-
```
|
|
32
|
-
The CLI stores each directory's site in `.shiply.json` (slug + update token),
|
|
33
|
-
so repeat publishes reuse the URL automatically. `--new-site` opts out.
|
|
34
|
-
Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
|
|
35
|
-
lines for automation; exit code 0 = ready.
|
|
36
|
-
|
|
37
|
-
### 3. Raw HTTP (no installs)
|
|
38
|
-
```
|
|
39
|
-
1. POST https://shiply.now/api/v1/publish
|
|
40
|
-
{"files":[{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]}
|
|
41
|
-
(+ "Authorization: Bearer shp_…" for permanent owned sites)
|
|
42
|
-
2. PUT each file's bytes to response upload.uploads[].url
|
|
43
|
-
3. POST upload.finalizeUrl with {"versionId":"..."}
|
|
44
|
-
```
|
|
45
|
-
**Updates:** anonymous → include `"claimToken":"..."` (returned ONCE by the
|
|
46
|
-
first publish — save it); owned → include `"slug":"..."`. Hashes make updates
|
|
47
|
-
cheap: unchanged files are skipped server-side.
|
|
48
|
-
|
|
49
|
-
## The lifecycle to explain to users
|
|
50
|
-
- Anonymous site: live instantly, expires in 24 h. Give the user the
|
|
51
|
-
`claimUrl` — claiming keeps it forever on a free account.
|
|
52
|
-
- API key (`shiply login` or POST /api/auth/agent/request-code →
|
|
53
|
-
verify-code): publishes are permanent and manageable.
|
|
54
|
-
- Paid plans add vanity handles (<name>.shiply.now), more custom domains,
|
|
55
|
-
storage, analytics: https://shiply.now/dashboard/plan
|
|
56
|
-
|
|
57
|
-
## Power features (Bearer key)
|
|
58
|
-
- **Custom domains**: POST /api/v1/domains {"hostname","slug"} → tell the
|
|
59
|
-
user to CNAME the hostname to `cname.shiply.now` → cert auto-issues; poll
|
|
60
|
-
GET /api/v1/domains/{id}/check until `ready:true`.
|
|
61
|
-
- **SSL/readiness checker**: `shiply status <slug-or-domain> --wait` or the
|
|
62
|
-
check endpoint — confirms certificate + serving before telling the user
|
|
63
|
-
it's done.
|
|
64
|
-
- **Variables**: encrypted per-user KV for API keys the user's sites need
|
|
65
|
-
(GET/PUT /api/v1/variables, DELETE /api/v1/variables/{NAME}); Supabase can
|
|
66
|
-
be connected from the dashboard and lands here as SUPABASE_URL +
|
|
67
|
-
SUPABASE_ANON_KEY.
|
|
68
|
-
- **Rollback**: POST /api/v1/publish/{slug}/rollback {"versionId"} flips any
|
|
69
|
-
finalized version live instantly.
|
|
70
|
-
- **SPA**: pass `"spaMode":true` so deep links serve index.html.
|
|
71
|
-
|
|
72
|
-
##
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
+
## Pick your interface (best first)
|
|
16
|
+
|
|
17
|
+
### 1. MCP (native tools)
|
|
18
|
+
If the `shiply` MCP server is connected (https://shiply.now/mcp), use
|
|
19
|
+
`publish_site`. Every result includes a `toUpdate` field telling you the exact
|
|
20
|
+
call for updates — follow it. Other tools: `site_status`, `list_sites`,
|
|
21
|
+
`set_handle`, `set_variable`, `add_domain`, `check_domain`, `get_analytics`,
|
|
22
|
+
`delete_site`, `rollback` via dashboard.
|
|
23
|
+
|
|
24
|
+
### 2. CLI
|
|
25
|
+
```bash
|
|
26
|
+
npm i -g shiply-cli # or: curl -fsSL https://shiply.now/install.sh | bash
|
|
27
|
+
shiply publish ./dir # live URL + confetti
|
|
28
|
+
shiply publish ./dir # run AGAIN after edits → updates the SAME site
|
|
29
|
+
shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
|
|
30
|
+
shiply login # email code → API key → sites become permanent
|
|
31
|
+
```
|
|
32
|
+
The CLI stores each directory's site in `.shiply.json` (slug + update token),
|
|
33
|
+
so repeat publishes reuse the URL automatically. `--new-site` opts out.
|
|
34
|
+
Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
|
|
35
|
+
lines for automation; exit code 0 = ready.
|
|
36
|
+
|
|
37
|
+
### 3. Raw HTTP (no installs)
|
|
38
|
+
```
|
|
39
|
+
1. POST https://shiply.now/api/v1/publish
|
|
40
|
+
{"files":[{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256, optional>"}]}
|
|
41
|
+
(+ "Authorization: Bearer shp_…" for permanent owned sites)
|
|
42
|
+
2. PUT each file's bytes to response upload.uploads[].url
|
|
43
|
+
3. POST upload.finalizeUrl with {"versionId":"..."}
|
|
44
|
+
```
|
|
45
|
+
**Updates:** anonymous → include `"claimToken":"..."` (returned ONCE by the
|
|
46
|
+
first publish — save it); owned → include `"slug":"..."`. Hashes make updates
|
|
47
|
+
cheap: unchanged files are skipped server-side.
|
|
48
|
+
|
|
49
|
+
## The lifecycle to explain to users
|
|
50
|
+
- Anonymous site: live instantly, expires in 24 h. Give the user the
|
|
51
|
+
`claimUrl` — claiming keeps it forever on a free account.
|
|
52
|
+
- API key (`shiply login` or POST /api/auth/agent/request-code →
|
|
53
|
+
verify-code): publishes are permanent and manageable.
|
|
54
|
+
- Paid plans add vanity handles (<name>.shiply.now), more custom domains,
|
|
55
|
+
storage, analytics: https://shiply.now/dashboard/plan
|
|
56
|
+
|
|
57
|
+
## Power features (Bearer key)
|
|
58
|
+
- **Custom domains**: POST /api/v1/domains {"hostname","slug"} → tell the
|
|
59
|
+
user to CNAME the hostname to `cname.shiply.now` → cert auto-issues; poll
|
|
60
|
+
GET /api/v1/domains/{id}/check until `ready:true`.
|
|
61
|
+
- **SSL/readiness checker**: `shiply status <slug-or-domain> --wait` or the
|
|
62
|
+
check endpoint — confirms certificate + serving before telling the user
|
|
63
|
+
it's done.
|
|
64
|
+
- **Variables**: encrypted per-user KV for API keys the user's sites need
|
|
65
|
+
(GET/PUT /api/v1/variables, DELETE /api/v1/variables/{NAME}); Supabase can
|
|
66
|
+
be connected from the dashboard and lands here as SUPABASE_URL +
|
|
67
|
+
SUPABASE_ANON_KEY.
|
|
68
|
+
- **Rollback**: POST /api/v1/publish/{slug}/rollback {"versionId"} flips any
|
|
69
|
+
finalized version live instantly.
|
|
70
|
+
- **SPA**: pass `"spaMode":true` so deep links serve index.html.
|
|
71
|
+
|
|
72
|
+
## Proxy routes — AI/API calls WITHOUT exposing keys
|
|
73
|
+
NEVER embed API keys in published HTML/JS. Instead: store the key as a
|
|
74
|
+
Variable (PUT /api/v1/variables), then include `.shiply/proxy.json` in the
|
|
75
|
+
published files declaring routes, e.g. {"proxies":{"/api/chat":{"upstream":
|
|
76
|
+
"https://openrouter.ai/api/v1/chat/completions","method":"POST","headers":
|
|
77
|
+
{"Authorization":"Bearer ${OPENROUTER_API_KEY}"}}}}. The page then calls the
|
|
78
|
+
relative path (fetch('/api/chat')) and shiply injects the secret server-side.
|
|
79
|
+
Prefix routes "/api/db/*" forward subpaths (great for Supabase REST).
|
|
80
|
+
Owned sites only; https public upstreams only. Docs: /docs/proxy-routes
|
|
81
|
+
|
|
82
|
+
## Site Data — forms/waitlists/guestbooks with ZERO backend
|
|
83
|
+
When the user wants to collect anything from visitors (waitlist, contact
|
|
84
|
+
form, RSVP, guestbook): include `.shiply/data.json` in the published files,
|
|
85
|
+
e.g. {"collections":{"signups":{"fields":{"email":{"type":"email",
|
|
86
|
+
"required":true}},"access":{"read":"owner","insert":"public"}}}}, then have
|
|
87
|
+
the page POST JSON to the relative path /.shiply/data/signups. Records are
|
|
88
|
+
validated server-side; the owner reads them in the dashboard (Data section,
|
|
89
|
+
CSV export) or GET /api/v1/publishes/<slug>/data/<collection> with a Bearer
|
|
90
|
+
key. Owned sites only. Docs: /docs/site-data
|
|
91
|
+
|
|
92
|
+
## Make a site private (paid plans)
|
|
93
|
+
To password-protect or restrict a site: PATCH /api/v1/publishes/<slug>/access
|
|
94
|
+
with {"mode":"password","password":"..."} or {"mode":"restricted",
|
|
95
|
+
"allowedEmails":[...],"allowedDomains":[...]}, or set mode "public" to open it.
|
|
96
|
+
MCP tool set_site_access does the same. Enforced before any content is served;
|
|
97
|
+
changing settings signs current visitors out. Docs: /docs/access-control
|
|
98
|
+
|
|
99
|
+
## Full React / Vue / Svelte / Astro apps — YES, they work
|
|
100
|
+
shiply hosts complete framework apps. Publish the BUILD OUTPUT, never the
|
|
101
|
+
source: run the build, then publish the output dir with SPA mode for
|
|
102
|
+
client-routed apps. The CLI auto-detects source dirs and tells you the
|
|
103
|
+
right command.
|
|
104
|
+
- Vite/React/Vue/Svelte: `npm run build` then `shiply publish dist --spa`
|
|
105
|
+
- Create React App: `npm run build` then `shiply publish build --spa`
|
|
106
|
+
- Next.js: set `output: "export"` in next.config, `npm run build`, then
|
|
107
|
+
`shiply publish out` (no --spa; export emits real HTML per route)
|
|
108
|
+
- Astro/static generators: `shiply publish dist`
|
|
109
|
+
Use the CLI (not inline MCP files) for builds — hashed bundles often exceed
|
|
110
|
+
the 50-file inline cap, and the CLI hash-skips unchanged chunks on updates.
|
|
111
|
+
|
|
112
|
+
## Limits & references
|
|
113
|
+
≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
|
|
114
|
+
Machine guide: https://shiply.now/llms.txt · OpenAPI:
|
|
115
|
+
https://shiply.now/openapi.json · Docs: https://shiply.now/docs
|