shiply-cli 0.26.1 → 0.27.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/index.js +72 -3
- package/dist/preflight.js +152 -0
- package/dist/publish.js +112 -8
- package/package.json +42 -42
- package/skill/SKILL.md +253 -253
- package/skill/references/ssr-frameworks.md +57 -47
package/dist/index.js
CHANGED
|
@@ -4,9 +4,10 @@ import { dirname, join } from 'node:path';
|
|
|
4
4
|
import { createInterface } from 'node:readline/promises';
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { parseArgs } from 'node:util';
|
|
7
|
-
import { rm } from 'node:fs/promises';
|
|
7
|
+
import { readdir, rm, stat } from 'node:fs/promises';
|
|
8
8
|
import { confetti, shouldCelebrate } from './confetti.js';
|
|
9
9
|
import { runDetect } from './detect.js';
|
|
10
|
+
import { printPreflight, runNextPreflight } from './preflight.js';
|
|
10
11
|
import { resolvePayloadDir } from './framework.js';
|
|
11
12
|
import { detectSsr } from './ssr-adapters.js';
|
|
12
13
|
import { prepareBundle } from './bundle.js';
|
|
@@ -290,6 +291,17 @@ async function main() {
|
|
|
290
291
|
if (!dir)
|
|
291
292
|
throw new Error(`usage: shiply ${cmd} <dir>`);
|
|
292
293
|
const apiKey = values.anonymous ? undefined : (values.key ?? (await loadApiKey()));
|
|
294
|
+
// Preflight (Next.js source dirs): catch wrong-shaped builds BEFORE they
|
|
295
|
+
// ship broken — Turbopack bundles, Vercel-shaped crons, publishing raw
|
|
296
|
+
// source with no worker build. Auto-fixes what's safe, instructs on the
|
|
297
|
+
// rest, and blocks only on fatals. Skipped with --no-ssr (static intent).
|
|
298
|
+
if (!values['no-ssr'] && !values.framework) {
|
|
299
|
+
const findings = await runNextPreflight(dir);
|
|
300
|
+
if (printPreflight(findings)) {
|
|
301
|
+
process.exitCode = 1;
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
293
305
|
// SSR / worker-bundle lane: a built Cloudflare-adapter project (SvelteKit,
|
|
294
306
|
// Astro) or a wrangler worker (Hono/raw fetch) ships as a worker bundle.
|
|
295
307
|
// Skipped when --no-ssr or --framework (static override) is passed.
|
|
@@ -311,6 +323,35 @@ async function main() {
|
|
|
311
323
|
publishDir = stagingDir;
|
|
312
324
|
spa = false; // SSR: an asset miss must 404 so the worker handles the route
|
|
313
325
|
stateDir = dir; // persist .shiply.json in the SOURCE dir, not the temp dir
|
|
326
|
+
// Bundle budget: warn while it's still cheap to fix. CF's cap is
|
|
327
|
+
// ~10MB gzipped; JS gzips ~4:1, so raw >30MB is the danger zone —
|
|
328
|
+
// name the biggest modules instead of describing them generically.
|
|
329
|
+
if (!bareJsonFlag) {
|
|
330
|
+
try {
|
|
331
|
+
const bundleDir = join(publishDir, '.shiply', 'bundle');
|
|
332
|
+
const sizes = [];
|
|
333
|
+
const walk = async (d, prefix) => {
|
|
334
|
+
for (const ent of await readdir(d, { withFileTypes: true })) {
|
|
335
|
+
const p = join(d, ent.name);
|
|
336
|
+
if (ent.isDirectory())
|
|
337
|
+
await walk(p, `${prefix}${ent.name}/`);
|
|
338
|
+
else
|
|
339
|
+
sizes.push({ name: `${prefix}${ent.name}`, bytes: (await stat(p)).size });
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
await walk(bundleDir, '');
|
|
343
|
+
const total = sizes.reduce((s, f) => s + f.bytes, 0);
|
|
344
|
+
if (total > 30_000_000) {
|
|
345
|
+
const top = sizes.sort((a, b) => b.bytes - a.bytes).slice(0, 3);
|
|
346
|
+
console.log(`⚠ worker bundle is ${(total / 1e6).toFixed(1)}MB raw — approaching Cloudflare's ~10MB gzipped cap.\n` +
|
|
347
|
+
` largest modules: ${top.map((t) => `${t.name} ${(t.bytes / 1e6).toFixed(1)}MB`).join(', ')}\n` +
|
|
348
|
+
` dev/test routes usually shouldn't ship; remove them or lazy-import heavy deps.`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
/* budget check is best-effort */
|
|
353
|
+
}
|
|
354
|
+
}
|
|
314
355
|
if (!bareJsonFlag) {
|
|
315
356
|
console.log(`✔ ${ssr.framework} (SSR) detected — deploying worker bundle`);
|
|
316
357
|
if (isr && isr.timeBased > 0) {
|
|
@@ -420,8 +461,21 @@ async function main() {
|
|
|
420
461
|
// its worker, so surface the server's errors instead of swallowing them.
|
|
421
462
|
if (res.function?.errors?.length) {
|
|
422
463
|
console.error(`\n✖ function deploy FAILED — the site's worker is NOT live:`);
|
|
423
|
-
|
|
424
|
-
|
|
464
|
+
// Prefer the structured what/why/fix when the server sent it — the
|
|
465
|
+
// fix line is imperative on purpose (agents execute it verbatim).
|
|
466
|
+
if (res.function.errorsDetail?.length) {
|
|
467
|
+
for (const d of res.function.errorsDetail) {
|
|
468
|
+
console.error(` ${d.what}${d.why ? ` — ${d.why}` : ''}`);
|
|
469
|
+
if (d.fix)
|
|
470
|
+
console.error(` fix: ${d.fix}`);
|
|
471
|
+
if (d.docs)
|
|
472
|
+
console.error(` docs: ${d.docs}`);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
else {
|
|
476
|
+
for (const err of res.function.errors)
|
|
477
|
+
console.error(` ${err}`);
|
|
478
|
+
}
|
|
425
479
|
// A worker source was present but did not deploy: the site is not what
|
|
426
480
|
// the user shipped, so exit non-zero. Static files DID go live, hence
|
|
427
481
|
// we still print the URL below — but automation/CI must see the failure.
|
|
@@ -433,6 +487,21 @@ async function main() {
|
|
|
433
487
|
}
|
|
434
488
|
for (const hint of res.function?.hints ?? [])
|
|
435
489
|
console.log(` ${hint}`);
|
|
490
|
+
// Postflight: the server probed the live site right after the deploy.
|
|
491
|
+
// Not-ok means "deployed but broken at runtime" — print the actual
|
|
492
|
+
// runtime exception it pulled from the worker's logs (the user can't
|
|
493
|
+
// see it any other way) and fail loudly for automation.
|
|
494
|
+
const smoke = res.function?.smoke;
|
|
495
|
+
if (smoke && !smoke.ok) {
|
|
496
|
+
console.error(`\n✖ deployed, but the site is NOT serving (HTTP ${smoke.status} at ${smoke.url})`);
|
|
497
|
+
if (smoke.note)
|
|
498
|
+
console.error(` ${smoke.note}`);
|
|
499
|
+
for (const line of smoke.runtimeErrors ?? [])
|
|
500
|
+
console.error(` runtime error: ${line}`);
|
|
501
|
+
for (const hint of smoke.hints ?? [])
|
|
502
|
+
console.error(` fix: ${hint}`);
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
}
|
|
436
505
|
console.log(`\n ${res.siteUrl}\n`);
|
|
437
506
|
// confirm the site actually serves, then celebrate
|
|
438
507
|
try {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
const exists = (p) => stat(p).then(() => true, () => false);
|
|
4
|
+
const mtimeMs = (p) => stat(p).then((s) => s.mtimeMs, () => null);
|
|
5
|
+
async function readJson(p) {
|
|
6
|
+
try {
|
|
7
|
+
return JSON.parse(await readFile(p, 'utf8'));
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async function readNextConfig(dir) {
|
|
14
|
+
for (const f of ['next.config.ts', 'next.config.js', 'next.config.mjs']) {
|
|
15
|
+
try {
|
|
16
|
+
return await readFile(join(dir, f), 'utf8');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
/* try next */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Major version out of a semver-ish range string ("^15.5.20" → 15). */
|
|
25
|
+
const majorOf = (range) => {
|
|
26
|
+
const m = /(\d+)/.exec(range);
|
|
27
|
+
return m ? Number(m[1]) : null;
|
|
28
|
+
};
|
|
29
|
+
/** Run the Next.js preflight over a SOURCE directory. Returns [] for
|
|
30
|
+
* non-Next dirs (cheap: one package.json read decides). */
|
|
31
|
+
export async function runNextPreflight(dir) {
|
|
32
|
+
const pkgPath = join(dir, 'package.json');
|
|
33
|
+
const pkg = await readJson(pkgPath);
|
|
34
|
+
if (!pkg)
|
|
35
|
+
return [];
|
|
36
|
+
const deps = {
|
|
37
|
+
...pkg.dependencies,
|
|
38
|
+
...pkg.devDependencies,
|
|
39
|
+
};
|
|
40
|
+
if (!deps.next)
|
|
41
|
+
return [];
|
|
42
|
+
const findings = [];
|
|
43
|
+
const nextMajor = majorOf(deps.next);
|
|
44
|
+
const scripts = pkg.scripts ?? {};
|
|
45
|
+
const nextConfig = await readNextConfig(dir);
|
|
46
|
+
const isStaticExport = !!nextConfig && /output\s*:\s*['"]export['"]/.test(nextConfig);
|
|
47
|
+
// -- turbopack-build: OpenNext requires a webpack build. Auto-fix the build
|
|
48
|
+
// script — this failure is otherwise silent-until-runtime.
|
|
49
|
+
const build = scripts.build ?? '';
|
|
50
|
+
if (!isStaticExport && /next\s+build/.test(build)) {
|
|
51
|
+
let fixedBuild = null;
|
|
52
|
+
if (/--turbopack\b/.test(build)) {
|
|
53
|
+
fixedBuild = build.replace(/\s*--turbopack\b/, nextMajor !== null && nextMajor >= 16 ? ' --webpack' : '');
|
|
54
|
+
}
|
|
55
|
+
else if (nextMajor !== null && nextMajor >= 16 && !/--webpack\b/.test(build)) {
|
|
56
|
+
fixedBuild = build.replace(/next\s+build/, 'next build --webpack');
|
|
57
|
+
}
|
|
58
|
+
if (fixedBuild !== null && fixedBuild !== build) {
|
|
59
|
+
const raw = await readFile(pkgPath, 'utf8');
|
|
60
|
+
// Surgical string replace preserves the user's package.json formatting.
|
|
61
|
+
const patched = raw.replace(JSON.stringify(build), JSON.stringify(fixedBuild));
|
|
62
|
+
if (patched !== raw) {
|
|
63
|
+
await writeFile(pkgPath, patched);
|
|
64
|
+
findings.push({
|
|
65
|
+
id: 'turbopack-build',
|
|
66
|
+
level: 'fixed',
|
|
67
|
+
message: `package.json "build" changed to ${JSON.stringify(fixedBuild)} — OpenNext (Next-on-Cloudflare) requires a webpack build; Turbopack output silently doesn't work. Rebuild before publishing.`,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
findings.push({
|
|
72
|
+
id: 'turbopack-build',
|
|
73
|
+
level: 'fix',
|
|
74
|
+
message: `your Next build must use webpack for Cloudflare — change the build script to ${JSON.stringify(fixedBuild)}, rebuild, then re-run shiply publish.`,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
// -- static-export: output:'export' apps are STATIC sites — publish the
|
|
80
|
+
// exported dir, don't take the worker-bundle lane.
|
|
81
|
+
if (isStaticExport) {
|
|
82
|
+
findings.push({
|
|
83
|
+
id: 'static-export',
|
|
84
|
+
level: 'note',
|
|
85
|
+
message: `next.config uses output:'export' — this is a static site. Run the build, then publish the exported out/ directory (shiply publish handles it as static; no worker needed).`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// -- vercel-crons: translate vercel.json crons to .shiply/crons.json (same
|
|
89
|
+
// {path,schedule} shape) so scheduled jobs keep firing after migration.
|
|
90
|
+
const vercel = await readJson(join(dir, 'vercel.json'));
|
|
91
|
+
const vercelCrons = vercel?.crons ?? [];
|
|
92
|
+
const shiplyCronsPath = join(dir, '.shiply', 'crons.json');
|
|
93
|
+
if (vercelCrons.length > 0 && !(await exists(shiplyCronsPath))) {
|
|
94
|
+
const crons = vercelCrons
|
|
95
|
+
.filter((c) => typeof c.path === 'string' && typeof c.schedule === 'string')
|
|
96
|
+
.map((c) => ({ path: c.path, schedule: c.schedule }));
|
|
97
|
+
if (crons.length > 0) {
|
|
98
|
+
await mkdir(join(dir, '.shiply'), { recursive: true });
|
|
99
|
+
await writeFile(shiplyCronsPath, `${JSON.stringify({ crons }, null, 2)}\n`);
|
|
100
|
+
findings.push({
|
|
101
|
+
id: 'vercel-crons',
|
|
102
|
+
level: 'fixed',
|
|
103
|
+
message: `translated ${crons.length} cron(s) from vercel.json to .shiply/crons.json — shiply reads the latter; without it your scheduled jobs silently stop after migrating.`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// -- missing-build / stale-build-state: never publish raw Next SOURCE as a
|
|
108
|
+
// static site (today's silent worst case), and warn when the app was
|
|
109
|
+
// rebuilt but the worker bundle wasn't.
|
|
110
|
+
const workerBuilt = await mtimeMs(join(dir, '.open-next', 'worker.js'));
|
|
111
|
+
if (!isStaticExport) {
|
|
112
|
+
if (workerBuilt === null) {
|
|
113
|
+
findings.push({
|
|
114
|
+
id: 'missing-build',
|
|
115
|
+
level: 'fix',
|
|
116
|
+
fatal: true,
|
|
117
|
+
message: 'no Cloudflare worker build found (.open-next/worker.js). Build first: npx opennextjs-cloudflare build ' +
|
|
118
|
+
'(requires a webpack next build — see any fixes above), then re-run shiply publish.',
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
const appBuilt = await mtimeMs(join(dir, '.next', 'BUILD_ID'));
|
|
123
|
+
if (appBuilt !== null && appBuilt > workerBuilt + 60_000) {
|
|
124
|
+
findings.push({
|
|
125
|
+
id: 'stale-build-state',
|
|
126
|
+
level: 'warn',
|
|
127
|
+
message: 'your .next build is newer than the worker bundle — you would publish STALE code. ' +
|
|
128
|
+
'Re-bundle first: npx opennextjs-cloudflare build --skipNextBuild',
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return findings;
|
|
134
|
+
}
|
|
135
|
+
/** Render the PREFLIGHT block to stderr (stable shape, greppable like
|
|
136
|
+
* SITE_READY). Returns true when a fatal finding should block the publish. */
|
|
137
|
+
export function printPreflight(findings) {
|
|
138
|
+
if (findings.length === 0)
|
|
139
|
+
return false;
|
|
140
|
+
const counts = { fixed: 0, fix: 0, warn: 0, note: 0 };
|
|
141
|
+
for (const f of findings)
|
|
142
|
+
counts[f.level]++;
|
|
143
|
+
const summary = Object.entries(counts)
|
|
144
|
+
.filter(([, n]) => n > 0)
|
|
145
|
+
.map(([k, n]) => `${n} ${k}`)
|
|
146
|
+
.join(', ');
|
|
147
|
+
process.stderr.write(`PREFLIGHT next.js — ${summary}\n`);
|
|
148
|
+
for (const f of findings) {
|
|
149
|
+
process.stderr.write(` ${f.level.toUpperCase()} ${f.id}: ${f.message}\n`);
|
|
150
|
+
}
|
|
151
|
+
return findings.some((f) => f.fatal);
|
|
152
|
+
}
|
package/dist/publish.js
CHANGED
|
@@ -92,21 +92,38 @@ export async function publish(dir, opts = {}) {
|
|
|
92
92
|
// whole publish with "already finalized" even though the deploy succeeded.
|
|
93
93
|
// Give it room, and treat that conflict as the success it is (an earlier
|
|
94
94
|
// attempt completed after the client gave up on it).
|
|
95
|
-
//
|
|
96
|
-
//
|
|
95
|
+
// The server streams NDJSON progress lines when asked (new servers), so a
|
|
96
|
+
// big-bundle finalize narrates its phases instead of being a silent
|
|
97
|
+
// multi-minute gap; old servers just return plain JSON and we print nothing.
|
|
97
98
|
process.stderr.write(' ⋯ finalizing — server is verifying files and deploying the worker…\n');
|
|
98
99
|
let finalized;
|
|
99
100
|
try {
|
|
100
|
-
finalized = await
|
|
101
|
-
method: 'POST',
|
|
102
|
-
headers: { 'content-type': 'application/json' },
|
|
103
|
-
body: JSON.stringify({ versionId: created.upload.versionId }),
|
|
104
|
-
}, { timeoutMs: 300_000 });
|
|
101
|
+
finalized = await finalizeWithProgress(created.upload.finalizeUrl, created.upload.versionId);
|
|
105
102
|
}
|
|
106
103
|
catch (e) {
|
|
107
104
|
if (e instanceof ApiError && e.code === 'conflict' && /already finalized/.test(e.message)) {
|
|
108
105
|
finalized = {}; // deployed by the earlier attempt; function info unavailable
|
|
109
106
|
}
|
|
107
|
+
else if (!(e instanceof ApiError)) {
|
|
108
|
+
// Transport died mid-stream. Finalize replays idempotently server-side —
|
|
109
|
+
// one plain retry either returns the completed result or the
|
|
110
|
+
// 'already finalized' conflict (success) handled above.
|
|
111
|
+
try {
|
|
112
|
+
finalized = await api(created.upload.finalizeUrl, {
|
|
113
|
+
method: 'POST',
|
|
114
|
+
headers: { 'content-type': 'application/json' },
|
|
115
|
+
body: JSON.stringify({ versionId: created.upload.versionId }),
|
|
116
|
+
}, { timeoutMs: 300_000 });
|
|
117
|
+
}
|
|
118
|
+
catch (e2) {
|
|
119
|
+
if (e2 instanceof ApiError && e2.code === 'conflict' && /already finalized/.test(e2.message)) {
|
|
120
|
+
finalized = {};
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
throw e2;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
110
127
|
else {
|
|
111
128
|
throw e;
|
|
112
129
|
}
|
|
@@ -124,7 +141,94 @@ export async function publish(dir, opts = {}) {
|
|
|
124
141
|
skipped: created.upload.skipped.length,
|
|
125
142
|
};
|
|
126
143
|
}
|
|
127
|
-
|
|
144
|
+
/** Server phase names → what the human/agent sees. `null` = internal detail,
|
|
145
|
+
* don't print. Unknown phases print as-is so new server phases surface
|
|
146
|
+
* without a CLI release. */
|
|
147
|
+
const FINALIZE_PHASE_LABELS = {
|
|
148
|
+
lookup: 'checking version',
|
|
149
|
+
fileSelects: null,
|
|
150
|
+
headCopy: 'verifying files',
|
|
151
|
+
goLive: 'going live',
|
|
152
|
+
workerHook: null, // its sub-phases (loading/uploading/probing) stream individually
|
|
153
|
+
};
|
|
154
|
+
/** Finalize with live progress: sends `x-shiply-progress: stream` and renders
|
|
155
|
+
* the server's NDJSON phase lines as they arrive; the final line carries the
|
|
156
|
+
* result (or the error, mapped to ApiError exactly like api() would). Old
|
|
157
|
+
* servers ignore the header and return plain JSON — handled transparently, so
|
|
158
|
+
* new CLI ↔ old server keeps working. */
|
|
159
|
+
async function finalizeWithProgress(url, versionId) {
|
|
160
|
+
const ac = new AbortController();
|
|
161
|
+
const timer = setTimeout(() => ac.abort(), 300_000);
|
|
162
|
+
try {
|
|
163
|
+
const res = await fetch(url, {
|
|
164
|
+
method: 'POST',
|
|
165
|
+
headers: { 'content-type': 'application/json', 'x-shiply-progress': 'stream' },
|
|
166
|
+
body: JSON.stringify({ versionId }),
|
|
167
|
+
signal: ac.signal,
|
|
168
|
+
});
|
|
169
|
+
const ctype = res.headers.get('content-type') ?? '';
|
|
170
|
+
if (!res.body || !ctype.includes('x-ndjson')) {
|
|
171
|
+
// Old server (or an error response): plain JSON, same as api().
|
|
172
|
+
const body = await res.json().catch(() => ({}));
|
|
173
|
+
if (!res.ok) {
|
|
174
|
+
const err = body.error;
|
|
175
|
+
throw new ApiError(err?.code ?? 'http_error', err?.message ?? `HTTP ${res.status}`, res.status);
|
|
176
|
+
}
|
|
177
|
+
return body;
|
|
178
|
+
}
|
|
179
|
+
const reader = res.body.getReader();
|
|
180
|
+
const decoder = new TextDecoder();
|
|
181
|
+
let buf = '';
|
|
182
|
+
let final = null;
|
|
183
|
+
const handleLine = (line) => {
|
|
184
|
+
if (!line.trim())
|
|
185
|
+
return;
|
|
186
|
+
let obj;
|
|
187
|
+
try {
|
|
188
|
+
obj = JSON.parse(line);
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return; // tolerate junk lines — the final {done} line is all we require
|
|
192
|
+
}
|
|
193
|
+
if (obj?.progress) {
|
|
194
|
+
const label = obj.progress in FINALIZE_PHASE_LABELS ? FINALIZE_PHASE_LABELS[obj.progress] : obj.progress;
|
|
195
|
+
if (label) {
|
|
196
|
+
const secs = ((obj.ms ?? 0) / 1000).toFixed(1);
|
|
197
|
+
process.stderr.write(` · ${label}${obj.detail ? ` (${obj.detail})` : ''} — ${secs}s\n`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
else if (obj?.done) {
|
|
201
|
+
final = obj;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
for (;;) {
|
|
205
|
+
const { done, value } = await reader.read();
|
|
206
|
+
if (done)
|
|
207
|
+
break;
|
|
208
|
+
buf += decoder.decode(value, { stream: true });
|
|
209
|
+
let nl;
|
|
210
|
+
while ((nl = buf.indexOf('\n')) >= 0) {
|
|
211
|
+
handleLine(buf.slice(0, nl));
|
|
212
|
+
buf = buf.slice(nl + 1);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
handleLine(buf);
|
|
216
|
+
if (!final)
|
|
217
|
+
throw new Error('finalize stream ended without a result');
|
|
218
|
+
const outcome = final;
|
|
219
|
+
if (outcome.error) {
|
|
220
|
+
throw new ApiError(outcome.error.code ?? 'http_error', outcome.error.message ?? 'finalize failed', 500);
|
|
221
|
+
}
|
|
222
|
+
return outcome.result ?? {};
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// 16-way: uploads are small-object PUTs to one R2 origin — latency-bound, not
|
|
229
|
+
// bandwidth-bound — and Node's fetch pool handles the fan-out fine. Roughly
|
|
230
|
+
// halves the upload phase of a ~60-changed-file publish vs 8.
|
|
231
|
+
const CONCURRENCY = 16;
|
|
128
232
|
async function uploadAll(dir, targets) {
|
|
129
233
|
let next = 0;
|
|
130
234
|
const worker = async () => {
|
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.27.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/SKILL.md
CHANGED
|
@@ -1,253 +1,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.
|
|
17
|
-
> To see what changed since your copy was written, fetch
|
|
18
|
-
> `https://shiply.now/changelog.md` (date-grouped, newest first) — if it lists
|
|
19
|
-
> capabilities newer than the date above, re-install with
|
|
20
|
-
> `npx -y shiply-cli@latest skill --force` OR read the live copy at
|
|
21
|
-
> `https://shiply.now/skill.md`.
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
# shiply — the production backend for anything an agent builds
|
|
26
|
-
|
|
27
|
-
**What it is.** Not static hosting. shiply takes whatever you build — a static page or a full-stack app in any framework — and gives it a live URL, a custom domain, a SQL database, server functions, email (send + receive), and a marketplace to sell it. One call. No account needed to start: anonymous sites are live immediately, last 24 hours, and can be claimed to keep.
|
|
28
|
-
|
|
29
|
-
**Why use it.** Ship a complete, working product without stitching together a host + database + email + registrar + payments — it's one agent-native platform (MCP / CLI / REST parity). Hard-ceiling pricing ($0/$8/$24, plus a $49 Studio tier for client delivery — no usage meter on any tier), so a spike or DDoS can never become a surprise bill for the owner.
|
|
30
|
-
|
|
31
|
-
**How (quickstart).** `shiply publish ./dir` (CLI) — or POST /api/v1/publish → PUT each file → POST finalize — returns a live URL + claimToken.
|
|
32
|
-
|
|
33
|
-
**NEVER create a new site to update an existing one. Always re-publish to the
|
|
34
|
-
same site** (same slug/claimToken) — otherwise you litter subdomains and lose
|
|
35
|
-
the user's URL.
|
|
36
|
-
|
|
37
|
-
**Re-publishing the same site is cheap — always prefer it over a new site.**
|
|
38
|
-
Unchanged files are hash-skipped, and an unchanged SSR worker bundle skips
|
|
39
|
-
redeploy entirely (fingerprinted against what's already live). A zero-change
|
|
40
|
-
`shiply publish` on the same site completes in seconds, even for large SSR
|
|
41
|
-
apps — so iterate freely by re-publishing, never by spinning up a fresh site.
|
|
42
|
-
|
|
43
|
-
This file covers the core path: publish → authorize → update → claim → verify.
|
|
44
|
-
Everything else (databases, domains, SSR, email, functions, client work) lives
|
|
45
|
-
in the **topic files indexed at the bottom** — read the one that matches the
|
|
46
|
-
task before improvising.
|
|
47
|
-
|
|
48
|
-
## First publish on a new machine: ask for a permanent key in one step
|
|
49
|
-
|
|
50
|
-
On a no-Bearer publish, include `"agentName": "<your-tool-name>"` in the body
|
|
51
|
-
(e.g. `"Claude Code"`, `"Cursor"`). The response will include a `deviceAuth`
|
|
52
|
-
object alongside the usual `claimUrl`:
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
"deviceAuth": {
|
|
56
|
-
"user_code": "ABCD-EFGH",
|
|
57
|
-
"device_code": "<long opaque token, keep private>",
|
|
58
|
-
"verification_url": "https://shiply.now/auth/ABCD-EFGH",
|
|
59
|
-
"poll_url": "https://shiply.now/api/v1/auth/device/poll",
|
|
60
|
-
"expires_in": 900,
|
|
61
|
-
"interval": 2
|
|
62
|
-
}
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Tell the user once, in plain language:
|
|
66
|
-
|
|
67
|
-
> Your site is live at <siteUrl>. To keep it forever AND let me publish more
|
|
68
|
-
> without re-pasting, open <verification_url> and click Allow.
|
|
69
|
-
|
|
70
|
-
Then immediately poll `verification_url`'s sibling endpoint until the user
|
|
71
|
-
acts:
|
|
72
|
-
|
|
73
|
-
```
|
|
74
|
-
POST https://shiply.now/api/v1/auth/device/poll
|
|
75
|
-
{"device_code": "<the device_code from the response>"}
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The response is one of `{status:"pending"}` (keep polling at `interval`s),
|
|
79
|
-
`{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (PERSIST the key —
|
|
80
|
-
see "Save the key" below — the site is now permanent; Allow does both jobs in
|
|
81
|
-
one click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
|
|
82
|
-
the announced `expires_in` and respect `interval` — don't tight-loop.
|
|
83
|
-
|
|
84
|
-
After approval, every future publish uses `Authorization: Bearer <api_key>`
|
|
85
|
-
and the user's dashboard shows their sites without any further claiming.
|
|
86
|
-
|
|
87
|
-
### Save the key — so EVERY future site is seamless (do this once)
|
|
88
|
-
|
|
89
|
-
Write the key to **`~/.shiply/credentials`** as `{"apiKey":"shp_..."}` (chmod
|
|
90
|
-
600) — the exact file the shiply CLI reads. This is what makes "one Allow, ever"
|
|
91
|
-
work: from then on every publish (any site, any project, ANY FUTURE SESSION)
|
|
92
|
-
sends `Authorization: Bearer <key>` automatically and lands owned in the user's
|
|
93
|
-
dashboard — no re-authorizing, no claim step, zero clicks. An env var alone
|
|
94
|
-
(`SHIPLY_API_KEY`) works for the current shell but does NOT survive a new
|
|
95
|
-
session — the credentials file is what makes it permanent. `shiply login` writes
|
|
96
|
-
this file for you; if you publish over raw HTTP, write it yourself.
|
|
97
|
-
|
|
98
|
-
## When the user pastes you something shiply-shaped, do this
|
|
99
|
-
|
|
100
|
-
These are the patterns you will see. Match the input, run the action — do not
|
|
101
|
-
ask the user to repeat themselves or explain.
|
|
102
|
-
|
|
103
|
-
- **Code like `SHIPLY-XXXXXXXX`** → it's a pairing code from
|
|
104
|
-
`shiply.now/claim/<slug>?pair=1`. The user wants to attach an anonymous
|
|
105
|
-
site to their account. Run `shiply claim verify SHIPLY-XXXXXXXX` from the
|
|
106
|
-
directory you originally published from (it reads `.shiply.json`). No CLI?
|
|
107
|
-
POST `https://shiply.now/api/v1/claim/pair/SHIPLY-XXXXXXXX/verify` with
|
|
108
|
-
`{"siteId":"...","claimToken":"..."}` taken from your earlier publish
|
|
109
|
-
response or `.shiply.json`. The user's browser auto-redirects on success;
|
|
110
|
-
tell them to switch back to it.
|
|
111
|
-
- **URL like `https://shiply.now/claim/<slug>?token=...`** → one-click claim
|
|
112
|
-
link. Tell the user to open it in the browser they are signed in to —
|
|
113
|
-
do NOT call any API yourself.
|
|
114
|
-
- **User says "claim my site" / "save it forever" with no code or URL** →
|
|
115
|
-
give them the `claimUrl` from your last publish response. If you no longer
|
|
116
|
-
have that response but `.shiply.json` is in the publish dir, reconstruct it
|
|
117
|
-
as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
|
|
118
|
-
Either way, also suggest `shiply login` so future publishes mint to their
|
|
119
|
-
account directly — that's the long-term fix and how the dashboard works.
|
|
120
|
-
- **User says "I have an API key now, use it from now on"** → persist it to
|
|
121
|
-
`~/.shiply/credentials` (`{"apiKey":"shp_..."}`, chmod 600 — see "Save the
|
|
122
|
-
key" above), then send `Authorization: Bearer <key>` on every future publish.
|
|
123
|
-
All future sites are then permanent and appear in their dashboard
|
|
124
|
-
automatically — across sessions, with no re-authorizing.
|
|
125
|
-
- **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** → no
|
|
126
|
-
action from you; they're on the device-flow consent screen. Keep polling
|
|
127
|
-
`poll_url` (see "First publish on a new machine" above). The poll response
|
|
128
|
-
will flip from `pending` to `approved` (with `api_key`) the moment they
|
|
129
|
-
click Allow.
|
|
130
|
-
|
|
131
|
-
## Pick your interface (best first)
|
|
132
|
-
|
|
133
|
-
### 1. MCP (native tools)
|
|
134
|
-
If the `shiply` MCP server is connected (https://shiply.now/mcp), use
|
|
135
|
-
`publish_site`. Every result includes a `toUpdate` field telling you the exact
|
|
136
|
-
call for updates — follow it, and a `shareSuggestion` you can relay to the user.
|
|
137
|
-
Core tools: `site_status`, `list_sites`, `get_site`, `delete_site`, `whoami`,
|
|
138
|
-
`duplicate_site`, `set_variable`, `get_analytics`, `set_handle`. There are
|
|
139
|
-
100+ tools in total — call `tools/list` for the authoritative set; the topic
|
|
140
|
-
files below name the tools for their area (domains, databases, email,
|
|
141
|
-
functions, projects, contracts, marketplace, drives).
|
|
142
|
-
|
|
143
|
-
### 2. CLI
|
|
144
|
-
```bash
|
|
145
|
-
# IMPORTANT: the npm package is `shiply-cli`, not `shiply` (different package).
|
|
146
|
-
npm i -g shiply-cli # or: npx -y shiply-cli@latest <command>
|
|
147
|
-
# or: curl -fsSL https://shiply.now/install.sh | bash
|
|
148
|
-
shiply publish ./dir # live URL + confetti
|
|
149
|
-
shiply publish ./dir # run AGAIN after edits → updates the SAME site
|
|
150
|
-
shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
|
|
151
|
-
shiply login # email code → API key → sites become permanent
|
|
152
|
-
shiply claim verify <code> # confirm a SHIPLY-XXXXXXXX pairing code from /claim/<slug>?pair=1
|
|
153
|
-
# (uses .shiply.json from CWD; pairs agent session to user's browser)
|
|
154
|
-
```
|
|
155
|
-
The CLI stores each directory's site in `.shiply.json` (slug + update token),
|
|
156
|
-
so repeat publishes reuse the URL automatically. `--new-site` opts out.
|
|
157
|
-
Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
|
|
158
|
-
lines for automation; exit code 0 = ready. For a static framework build
|
|
159
|
-
(Vite, Next export, Hugo, …) publish the BUILD OUTPUT, not the source — run
|
|
160
|
-
`shiply detect` to see what the CLI found; the full framework matrix is in
|
|
161
|
-
[references/publishing.md](references/publishing.md).
|
|
162
|
-
|
|
163
|
-
### 3. Raw HTTP (no installs)
|
|
164
|
-
```
|
|
165
|
-
1. POST https://shiply.now/api/v1/publish (Content-Type: application/json)
|
|
166
|
-
{
|
|
167
|
-
"agentName": "<your tool name, e.g. Claude Code, Cursor, Codex>",
|
|
168
|
-
"files": [{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256 lowercase hex, optional>"}]
|
|
169
|
-
}
|
|
170
|
-
(+ "Authorization: Bearer shp_…" for permanent owned sites)
|
|
171
|
-
2. PUT each file's bytes to response upload.uploads[].url
|
|
172
|
-
3. POST upload.finalizeUrl with {"versionId":"<upload.versionId from step 1>"}
|
|
173
|
-
→ both the publish and finalize responses include a "toUpdate" string: the
|
|
174
|
-
exact call to update THIS site next time. Follow it — never create a new one.
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
**ALWAYS include `agentName` on anonymous publishes.** The response then
|
|
178
|
-
carries `deviceAuth` ({user_code, verification_url, poll_url, device_code,
|
|
179
|
-
expires_in, interval}) alongside `claimUrl`. Show the user `verification_url`,
|
|
180
|
-
poll `poll_url` every `interval` seconds with the `device_code`, and on
|
|
181
|
-
`{status:"approved"}` you get a permanent `api_key` PLUS the linked site
|
|
182
|
-
is claimed in the same Allow click. Without `agentName` the user has to
|
|
183
|
-
do a separate manual claim step every time.
|
|
184
|
-
|
|
185
|
-
**Updates (the response tells you how):** every publish/finalize response
|
|
186
|
-
includes a `toUpdate` string with the exact call — follow it verbatim. In short:
|
|
187
|
-
anonymous → include `"claimToken":"..."` (returned ONCE by the first publish —
|
|
188
|
-
save it); owned → include `"slug":"..."`. Hashes make updates cheap: unchanged
|
|
189
|
-
files are skipped server-side.
|
|
190
|
-
|
|
191
|
-
## Verify before you say "done"
|
|
192
|
-
|
|
193
|
-
`shiply status <slug> --wait` polls until the site serves (exit 0, prints
|
|
194
|
-
`SITE_READY` / `SSL_READY`). `shiply verify <slug>` runs a deeper edge
|
|
195
|
-
SSL + HTTP + thumbnail check and prints a stable machine marker line
|
|
196
|
-
`VERIFY status=LIVE http=200 ssl=valid …` — parse that line in automation.
|
|
197
|
-
More manage commands (list, delete, rollback, promote, stable previews) are
|
|
198
|
-
in [references/publishing.md](references/publishing.md).
|
|
199
|
-
|
|
200
|
-
## The lifecycle to explain to users
|
|
201
|
-
- Anonymous site: live immediately after finalize (no claim needed), expires
|
|
202
|
-
in 24 h. Give the user the `claimUrl` — claiming keeps it forever on a free
|
|
203
|
-
account. Until claimed, anonymous pages are served with a small injected
|
|
204
|
-
claim banner + OG meta tags — expected, not corruption. If no human is
|
|
205
|
-
present right now, save the `claimToken` (it's also the update key); a
|
|
206
|
-
fresh device flow can be started later via
|
|
207
|
-
POST /api/v1/auth/device/start {"agent_name":"..."}.
|
|
208
|
-
- API key (`shiply login` or POST /api/auth/agent/request-code →
|
|
209
|
-
verify-code): publishes are permanent and manageable.
|
|
210
|
-
- Paid plans add vanity handles (<name>.shiply.now), more custom domains,
|
|
211
|
-
storage, analytics: https://shiply.now/dashboard/plan
|
|
212
|
-
|
|
213
|
-
## Everything else — read the topic file first
|
|
214
|
-
|
|
215
|
-
The files below sit next to this one in `references/` (installed with the
|
|
216
|
-
skill). Hosted copies: `https://shiply.now/skill/references/<file>`. Each is
|
|
217
|
-
self-contained — read the one matching the task before improvising; each
|
|
218
|
-
covers its CLI commands, MCP tools, AND REST endpoints.
|
|
219
|
-
|
|
220
|
-
* [Publishing & site management](references/publishing.md) — static framework
|
|
221
|
-
build matrix (Vite/Next/Hugo/…), `detect`, SPA mode, `.shiplyignore`,
|
|
222
|
-
list/delete/rollback/versions, `verify`, stable previews (`--as`),
|
|
223
|
-
`promote` preview→prod, `--json` output, form-data subcommands.
|
|
224
|
-
* [SSR frameworks](references/ssr-frameworks.md) — deploy SvelteKit, Astro,
|
|
225
|
-
Qwik, Nuxt/Nitro, React Router v7, Next.js (OpenNext), Hono/raw Workers
|
|
226
|
-
with server-side rendering.
|
|
227
|
-
* [Custom domains](references/custom-domains.md) — put the user's own domain
|
|
228
|
-
on a site: one-click OAuth DNS, manual CNAME, primary-subdomain SEO,
|
|
229
|
-
readiness polling.
|
|
230
|
-
* [Databases](references/databases.md) — per-site SQL: free D1 (SQLite at the
|
|
231
|
-
edge, browser shim) + Neon Postgres (branching), migrations, per-DB MCP
|
|
232
|
-
server.
|
|
233
|
-
* [Functions](references/functions.md) — `worker.js` server code on every
|
|
234
|
-
request: webhooks, cron triggers, secrets, runtime logs (Workers Lite,
|
|
235
|
-
Developer plan).
|
|
236
|
-
* [Email](references/email.md) — every owned site sends + receives: signup
|
|
237
|
-
capture, inbox, double-opt-in audiences, broadcasts, BYO sending domains.
|
|
238
|
-
* [Site features](references/site-features.md) — proxy routes (call AI APIs
|
|
239
|
-
without exposing keys), Site Data (forms/waitlists, zero backend),
|
|
240
|
-
password/invite-only access, Variables, Drives (private storage),
|
|
241
|
-
path-mounting, public profile.
|
|
242
|
-
* [Client work](references/client-work.md) — freelancer delivery: customer
|
|
243
|
-
intake projects + AI briefs, group work by client, e-sign contracts,
|
|
244
|
-
sell sites on the marketplace.
|
|
245
|
-
* [Authentication for the user's app](references/site-features.md) — shiply
|
|
246
|
-
does NOT host end-user auth; bring the user's own Clerk/Auth.js/Supabase
|
|
247
|
-
(pattern in Site features → "Bring your own auth").
|
|
248
|
-
|
|
249
|
-
## Limits & references
|
|
250
|
-
≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
|
|
251
|
-
Machine index: https://shiply.now/llms.txt (full single-file version:
|
|
252
|
-
https://shiply.now/llms-full.txt) · What's new: https://shiply.now/changelog.md
|
|
253
|
-
· OpenAPI: https://shiply.now/openapi.json · Docs: https://shiply.now/docs
|
|
1
|
+
---
|
|
2
|
+
name: shiply
|
|
3
|
+
description: "shiply is the production backend for anything an agent builds — publish any app (static or full-stack SSR, any framework) and give it a live URL, custom domain, SQL database, server functions, email, and a marketplace to sell it. Use when the user asks to publish, host, deploy, share, or update a website/page/demo/report/app, or wants a live URL for generated files. Triggers - \"publish this\", \"host this\", \"put this online\", \"give me a link\", \"update the site\", \"deploy my SvelteKit/Astro/Next.js app\", shiply."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
> **IMPORTANT — npm package name is `shiply-cli`, NOT `shiply`.** A different
|
|
9
|
+
> npm package named `shiply` (an auto-commit watcher) is published by someone
|
|
10
|
+
> else — installing it does NOT give you the shiply.now CLI. Always use
|
|
11
|
+
> `npx -y shiply-cli@latest …` or `npm install -g shiply-cli`. The CLI binary
|
|
12
|
+
> on PATH is still named `shiply` once installed; the warning is only about
|
|
13
|
+
> the npm package name.
|
|
14
|
+
|
|
15
|
+
> **Skill freshness check.** Shiply ships new capabilities weekly.
|
|
16
|
+
> **This skill: version 0.27.0, last updated 2026-07-11.**
|
|
17
|
+
> To see what changed since your copy was written, fetch
|
|
18
|
+
> `https://shiply.now/changelog.md` (date-grouped, newest first) — if it lists
|
|
19
|
+
> capabilities newer than the date above, re-install with
|
|
20
|
+
> `npx -y shiply-cli@latest skill --force` OR read the live copy at
|
|
21
|
+
> `https://shiply.now/skill.md`.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
# shiply — the production backend for anything an agent builds
|
|
26
|
+
|
|
27
|
+
**What it is.** Not static hosting. shiply takes whatever you build — a static page or a full-stack app in any framework — and gives it a live URL, a custom domain, a SQL database, server functions, email (send + receive), and a marketplace to sell it. One call. No account needed to start: anonymous sites are live immediately, last 24 hours, and can be claimed to keep.
|
|
28
|
+
|
|
29
|
+
**Why use it.** Ship a complete, working product without stitching together a host + database + email + registrar + payments — it's one agent-native platform (MCP / CLI / REST parity). Hard-ceiling pricing ($0/$8/$24, plus a $49 Studio tier for client delivery — no usage meter on any tier), so a spike or DDoS can never become a surprise bill for the owner.
|
|
30
|
+
|
|
31
|
+
**How (quickstart).** `shiply publish ./dir` (CLI) — or POST /api/v1/publish → PUT each file → POST finalize — returns a live URL + claimToken.
|
|
32
|
+
|
|
33
|
+
**NEVER create a new site to update an existing one. Always re-publish to the
|
|
34
|
+
same site** (same slug/claimToken) — otherwise you litter subdomains and lose
|
|
35
|
+
the user's URL.
|
|
36
|
+
|
|
37
|
+
**Re-publishing the same site is cheap — always prefer it over a new site.**
|
|
38
|
+
Unchanged files are hash-skipped, and an unchanged SSR worker bundle skips
|
|
39
|
+
redeploy entirely (fingerprinted against what's already live). A zero-change
|
|
40
|
+
`shiply publish` on the same site completes in seconds, even for large SSR
|
|
41
|
+
apps — so iterate freely by re-publishing, never by spinning up a fresh site.
|
|
42
|
+
|
|
43
|
+
This file covers the core path: publish → authorize → update → claim → verify.
|
|
44
|
+
Everything else (databases, domains, SSR, email, functions, client work) lives
|
|
45
|
+
in the **topic files indexed at the bottom** — read the one that matches the
|
|
46
|
+
task before improvising.
|
|
47
|
+
|
|
48
|
+
## First publish on a new machine: ask for a permanent key in one step
|
|
49
|
+
|
|
50
|
+
On a no-Bearer publish, include `"agentName": "<your-tool-name>"` in the body
|
|
51
|
+
(e.g. `"Claude Code"`, `"Cursor"`). The response will include a `deviceAuth`
|
|
52
|
+
object alongside the usual `claimUrl`:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
"deviceAuth": {
|
|
56
|
+
"user_code": "ABCD-EFGH",
|
|
57
|
+
"device_code": "<long opaque token, keep private>",
|
|
58
|
+
"verification_url": "https://shiply.now/auth/ABCD-EFGH",
|
|
59
|
+
"poll_url": "https://shiply.now/api/v1/auth/device/poll",
|
|
60
|
+
"expires_in": 900,
|
|
61
|
+
"interval": 2
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Tell the user once, in plain language:
|
|
66
|
+
|
|
67
|
+
> Your site is live at <siteUrl>. To keep it forever AND let me publish more
|
|
68
|
+
> without re-pasting, open <verification_url> and click Allow.
|
|
69
|
+
|
|
70
|
+
Then immediately poll `verification_url`'s sibling endpoint until the user
|
|
71
|
+
acts:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
POST https://shiply.now/api/v1/auth/device/poll
|
|
75
|
+
{"device_code": "<the device_code from the response>"}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The response is one of `{status:"pending"}` (keep polling at `interval`s),
|
|
79
|
+
`{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (PERSIST the key —
|
|
80
|
+
see "Save the key" below — the site is now permanent; Allow does both jobs in
|
|
81
|
+
one click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
|
|
82
|
+
the announced `expires_in` and respect `interval` — don't tight-loop.
|
|
83
|
+
|
|
84
|
+
After approval, every future publish uses `Authorization: Bearer <api_key>`
|
|
85
|
+
and the user's dashboard shows their sites without any further claiming.
|
|
86
|
+
|
|
87
|
+
### Save the key — so EVERY future site is seamless (do this once)
|
|
88
|
+
|
|
89
|
+
Write the key to **`~/.shiply/credentials`** as `{"apiKey":"shp_..."}` (chmod
|
|
90
|
+
600) — the exact file the shiply CLI reads. This is what makes "one Allow, ever"
|
|
91
|
+
work: from then on every publish (any site, any project, ANY FUTURE SESSION)
|
|
92
|
+
sends `Authorization: Bearer <key>` automatically and lands owned in the user's
|
|
93
|
+
dashboard — no re-authorizing, no claim step, zero clicks. An env var alone
|
|
94
|
+
(`SHIPLY_API_KEY`) works for the current shell but does NOT survive a new
|
|
95
|
+
session — the credentials file is what makes it permanent. `shiply login` writes
|
|
96
|
+
this file for you; if you publish over raw HTTP, write it yourself.
|
|
97
|
+
|
|
98
|
+
## When the user pastes you something shiply-shaped, do this
|
|
99
|
+
|
|
100
|
+
These are the patterns you will see. Match the input, run the action — do not
|
|
101
|
+
ask the user to repeat themselves or explain.
|
|
102
|
+
|
|
103
|
+
- **Code like `SHIPLY-XXXXXXXX`** → it's a pairing code from
|
|
104
|
+
`shiply.now/claim/<slug>?pair=1`. The user wants to attach an anonymous
|
|
105
|
+
site to their account. Run `shiply claim verify SHIPLY-XXXXXXXX` from the
|
|
106
|
+
directory you originally published from (it reads `.shiply.json`). No CLI?
|
|
107
|
+
POST `https://shiply.now/api/v1/claim/pair/SHIPLY-XXXXXXXX/verify` with
|
|
108
|
+
`{"siteId":"...","claimToken":"..."}` taken from your earlier publish
|
|
109
|
+
response or `.shiply.json`. The user's browser auto-redirects on success;
|
|
110
|
+
tell them to switch back to it.
|
|
111
|
+
- **URL like `https://shiply.now/claim/<slug>?token=...`** → one-click claim
|
|
112
|
+
link. Tell the user to open it in the browser they are signed in to —
|
|
113
|
+
do NOT call any API yourself.
|
|
114
|
+
- **User says "claim my site" / "save it forever" with no code or URL** →
|
|
115
|
+
give them the `claimUrl` from your last publish response. If you no longer
|
|
116
|
+
have that response but `.shiply.json` is in the publish dir, reconstruct it
|
|
117
|
+
as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
|
|
118
|
+
Either way, also suggest `shiply login` so future publishes mint to their
|
|
119
|
+
account directly — that's the long-term fix and how the dashboard works.
|
|
120
|
+
- **User says "I have an API key now, use it from now on"** → persist it to
|
|
121
|
+
`~/.shiply/credentials` (`{"apiKey":"shp_..."}`, chmod 600 — see "Save the
|
|
122
|
+
key" above), then send `Authorization: Bearer <key>` on every future publish.
|
|
123
|
+
All future sites are then permanent and appear in their dashboard
|
|
124
|
+
automatically — across sessions, with no re-authorizing.
|
|
125
|
+
- **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** → no
|
|
126
|
+
action from you; they're on the device-flow consent screen. Keep polling
|
|
127
|
+
`poll_url` (see "First publish on a new machine" above). The poll response
|
|
128
|
+
will flip from `pending` to `approved` (with `api_key`) the moment they
|
|
129
|
+
click Allow.
|
|
130
|
+
|
|
131
|
+
## Pick your interface (best first)
|
|
132
|
+
|
|
133
|
+
### 1. MCP (native tools)
|
|
134
|
+
If the `shiply` MCP server is connected (https://shiply.now/mcp), use
|
|
135
|
+
`publish_site`. Every result includes a `toUpdate` field telling you the exact
|
|
136
|
+
call for updates — follow it, and a `shareSuggestion` you can relay to the user.
|
|
137
|
+
Core tools: `site_status`, `list_sites`, `get_site`, `delete_site`, `whoami`,
|
|
138
|
+
`duplicate_site`, `set_variable`, `get_analytics`, `set_handle`. There are
|
|
139
|
+
100+ tools in total — call `tools/list` for the authoritative set; the topic
|
|
140
|
+
files below name the tools for their area (domains, databases, email,
|
|
141
|
+
functions, projects, contracts, marketplace, drives).
|
|
142
|
+
|
|
143
|
+
### 2. CLI
|
|
144
|
+
```bash
|
|
145
|
+
# IMPORTANT: the npm package is `shiply-cli`, not `shiply` (different package).
|
|
146
|
+
npm i -g shiply-cli # or: npx -y shiply-cli@latest <command>
|
|
147
|
+
# or: curl -fsSL https://shiply.now/install.sh | bash
|
|
148
|
+
shiply publish ./dir # live URL + confetti
|
|
149
|
+
shiply publish ./dir # run AGAIN after edits → updates the SAME site
|
|
150
|
+
shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
|
|
151
|
+
shiply login # email code → API key → sites become permanent
|
|
152
|
+
shiply claim verify <code> # confirm a SHIPLY-XXXXXXXX pairing code from /claim/<slug>?pair=1
|
|
153
|
+
# (uses .shiply.json from CWD; pairs agent session to user's browser)
|
|
154
|
+
```
|
|
155
|
+
The CLI stores each directory's site in `.shiply.json` (slug + update token),
|
|
156
|
+
so repeat publishes reuse the URL automatically. `--new-site` opts out.
|
|
157
|
+
Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
|
|
158
|
+
lines for automation; exit code 0 = ready. For a static framework build
|
|
159
|
+
(Vite, Next export, Hugo, …) publish the BUILD OUTPUT, not the source — run
|
|
160
|
+
`shiply detect` to see what the CLI found; the full framework matrix is in
|
|
161
|
+
[references/publishing.md](references/publishing.md).
|
|
162
|
+
|
|
163
|
+
### 3. Raw HTTP (no installs)
|
|
164
|
+
```
|
|
165
|
+
1. POST https://shiply.now/api/v1/publish (Content-Type: application/json)
|
|
166
|
+
{
|
|
167
|
+
"agentName": "<your tool name, e.g. Claude Code, Cursor, Codex>",
|
|
168
|
+
"files": [{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256 lowercase hex, optional>"}]
|
|
169
|
+
}
|
|
170
|
+
(+ "Authorization: Bearer shp_…" for permanent owned sites)
|
|
171
|
+
2. PUT each file's bytes to response upload.uploads[].url
|
|
172
|
+
3. POST upload.finalizeUrl with {"versionId":"<upload.versionId from step 1>"}
|
|
173
|
+
→ both the publish and finalize responses include a "toUpdate" string: the
|
|
174
|
+
exact call to update THIS site next time. Follow it — never create a new one.
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
**ALWAYS include `agentName` on anonymous publishes.** The response then
|
|
178
|
+
carries `deviceAuth` ({user_code, verification_url, poll_url, device_code,
|
|
179
|
+
expires_in, interval}) alongside `claimUrl`. Show the user `verification_url`,
|
|
180
|
+
poll `poll_url` every `interval` seconds with the `device_code`, and on
|
|
181
|
+
`{status:"approved"}` you get a permanent `api_key` PLUS the linked site
|
|
182
|
+
is claimed in the same Allow click. Without `agentName` the user has to
|
|
183
|
+
do a separate manual claim step every time.
|
|
184
|
+
|
|
185
|
+
**Updates (the response tells you how):** every publish/finalize response
|
|
186
|
+
includes a `toUpdate` string with the exact call — follow it verbatim. In short:
|
|
187
|
+
anonymous → include `"claimToken":"..."` (returned ONCE by the first publish —
|
|
188
|
+
save it); owned → include `"slug":"..."`. Hashes make updates cheap: unchanged
|
|
189
|
+
files are skipped server-side.
|
|
190
|
+
|
|
191
|
+
## Verify before you say "done"
|
|
192
|
+
|
|
193
|
+
`shiply status <slug> --wait` polls until the site serves (exit 0, prints
|
|
194
|
+
`SITE_READY` / `SSL_READY`). `shiply verify <slug>` runs a deeper edge
|
|
195
|
+
SSL + HTTP + thumbnail check and prints a stable machine marker line
|
|
196
|
+
`VERIFY status=LIVE http=200 ssl=valid …` — parse that line in automation.
|
|
197
|
+
More manage commands (list, delete, rollback, promote, stable previews) are
|
|
198
|
+
in [references/publishing.md](references/publishing.md).
|
|
199
|
+
|
|
200
|
+
## The lifecycle to explain to users
|
|
201
|
+
- Anonymous site: live immediately after finalize (no claim needed), expires
|
|
202
|
+
in 24 h. Give the user the `claimUrl` — claiming keeps it forever on a free
|
|
203
|
+
account. Until claimed, anonymous pages are served with a small injected
|
|
204
|
+
claim banner + OG meta tags — expected, not corruption. If no human is
|
|
205
|
+
present right now, save the `claimToken` (it's also the update key); a
|
|
206
|
+
fresh device flow can be started later via
|
|
207
|
+
POST /api/v1/auth/device/start {"agent_name":"..."}.
|
|
208
|
+
- API key (`shiply login` or POST /api/auth/agent/request-code →
|
|
209
|
+
verify-code): publishes are permanent and manageable.
|
|
210
|
+
- Paid plans add vanity handles (<name>.shiply.now), more custom domains,
|
|
211
|
+
storage, analytics: https://shiply.now/dashboard/plan
|
|
212
|
+
|
|
213
|
+
## Everything else — read the topic file first
|
|
214
|
+
|
|
215
|
+
The files below sit next to this one in `references/` (installed with the
|
|
216
|
+
skill). Hosted copies: `https://shiply.now/skill/references/<file>`. Each is
|
|
217
|
+
self-contained — read the one matching the task before improvising; each
|
|
218
|
+
covers its CLI commands, MCP tools, AND REST endpoints.
|
|
219
|
+
|
|
220
|
+
* [Publishing & site management](references/publishing.md) — static framework
|
|
221
|
+
build matrix (Vite/Next/Hugo/…), `detect`, SPA mode, `.shiplyignore`,
|
|
222
|
+
list/delete/rollback/versions, `verify`, stable previews (`--as`),
|
|
223
|
+
`promote` preview→prod, `--json` output, form-data subcommands.
|
|
224
|
+
* [SSR frameworks](references/ssr-frameworks.md) — deploy SvelteKit, Astro,
|
|
225
|
+
Qwik, Nuxt/Nitro, React Router v7, Next.js (OpenNext), Hono/raw Workers
|
|
226
|
+
with server-side rendering.
|
|
227
|
+
* [Custom domains](references/custom-domains.md) — put the user's own domain
|
|
228
|
+
on a site: one-click OAuth DNS, manual CNAME, primary-subdomain SEO,
|
|
229
|
+
readiness polling.
|
|
230
|
+
* [Databases](references/databases.md) — per-site SQL: free D1 (SQLite at the
|
|
231
|
+
edge, browser shim) + Neon Postgres (branching), migrations, per-DB MCP
|
|
232
|
+
server.
|
|
233
|
+
* [Functions](references/functions.md) — `worker.js` server code on every
|
|
234
|
+
request: webhooks, cron triggers, secrets, runtime logs (Workers Lite,
|
|
235
|
+
Developer plan).
|
|
236
|
+
* [Email](references/email.md) — every owned site sends + receives: signup
|
|
237
|
+
capture, inbox, double-opt-in audiences, broadcasts, BYO sending domains.
|
|
238
|
+
* [Site features](references/site-features.md) — proxy routes (call AI APIs
|
|
239
|
+
without exposing keys), Site Data (forms/waitlists, zero backend),
|
|
240
|
+
password/invite-only access, Variables, Drives (private storage),
|
|
241
|
+
path-mounting, public profile.
|
|
242
|
+
* [Client work](references/client-work.md) — freelancer delivery: customer
|
|
243
|
+
intake projects + AI briefs, group work by client, e-sign contracts,
|
|
244
|
+
sell sites on the marketplace.
|
|
245
|
+
* [Authentication for the user's app](references/site-features.md) — shiply
|
|
246
|
+
does NOT host end-user auth; bring the user's own Clerk/Auth.js/Supabase
|
|
247
|
+
(pattern in Site features → "Bring your own auth").
|
|
248
|
+
|
|
249
|
+
## Limits & references
|
|
250
|
+
≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
|
|
251
|
+
Machine index: https://shiply.now/llms.txt (full single-file version:
|
|
252
|
+
https://shiply.now/llms-full.txt) · What's new: https://shiply.now/changelog.md
|
|
253
|
+
· OpenAPI: https://shiply.now/openapi.json · Docs: https://shiply.now/docs
|
|
@@ -1,47 +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).
|
|
37
|
-
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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.
|