shiply-cli 0.26.0 → 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/framework.js CHANGED
@@ -8,9 +8,14 @@ const SSG_SIGNATURES = [
8
8
  { files: ['mkdocs.yml', 'mkdocs.yaml'], framework: 'MkDocs', outputDir: 'site', buildCommand: 'mkdocs build' },
9
9
  ];
10
10
  async function detectStaticSiteGenerator(dir) {
11
+ // Check every signature file in parallel, then pick the first match in the
12
+ // original priority order — same result as the serial version, faster I/O.
13
+ const flatFiles = SSG_SIGNATURES.flatMap((sig) => sig.files);
14
+ const hits = await Promise.all(flatFiles.map((f) => exists(join(dir, f))));
15
+ let i = 0;
11
16
  for (const sig of SSG_SIGNATURES) {
12
17
  for (const f of sig.files) {
13
- if (await exists(join(dir, f))) {
18
+ if (hits[i++]) {
14
19
  return { framework: sig.framework, outputDir: sig.outputDir, spa: false, buildCommand: sig.buildCommand };
15
20
  }
16
21
  }
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
- for (const err of res.function.errors)
424
- console.error(` ${err}`);
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
- // Say what's happening finalize is one server-side call (file verification
96
- // + worker deploy) and used to be a silent multi-minute gap for big bundles.
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 api(created.upload.finalizeUrl, {
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
- const CONCURRENCY = 8;
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 () => {
@@ -132,8 +236,14 @@ async function uploadAll(dir, targets) {
132
236
  const t = targets[next++];
133
237
  const body = await readFile(join(dir, t.path));
134
238
  const res = await fetchWithRetry(t.url, { method: t.method, headers: t.headers, body }, { label: `upload ${t.path}` });
135
- if (!res.ok)
239
+ if (!res.ok) {
240
+ if (res.status === 403) {
241
+ throw new Error(`upload failed for ${t.path}: HTTP 403 — presigned URLs likely expired ` +
242
+ `(they're short-lived). Re-run \`shiply publish\` to mint fresh ones — ` +
243
+ `unchanged files are hash-skipped, so the retry is cheap.`);
244
+ }
136
245
  throw new Error(`upload failed for ${t.path}: HTTP ${res.status}`);
246
+ }
137
247
  }
138
248
  };
139
249
  await Promise.all(Array.from({ length: Math.min(CONCURRENCY, targets.length) }, worker));
package/package.json CHANGED
@@ -1,42 +1,42 @@
1
- {
2
- "name": "shiply-cli",
3
- "version": "0.26.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
+ }
@@ -6,6 +6,17 @@ entry here, that capability may be missing from your copy — re-install with
6
6
  (This is the agent-facing capability log; the CLI's own release notes live in
7
7
  the npm package changelog.)
8
8
 
9
+ ## 2026-07-08
10
+
11
+ - **Fix** — re-publishing an unchanged large SSR site (e.g. a 171-file
12
+ Next.js build) used to take 6+ minutes (serial hash-skip HEAD checks, a
13
+ serial worker-bundle re-read/re-upload even with no changes, plus a
14
+ finalize retry after a client-side timeout). Finalize is now: concurrent
15
+ hash-skip checks, a fingerprinted worker-bundle deploy that's skipped
16
+ entirely when nothing changed, and an idempotent replay on retry. A
17
+ zero-change publish now finishes in seconds. No action needed — always
18
+ re-publish to the same site (slug/claimToken); it's cheap.
19
+
9
20
  ## 2026-07-07
10
21
 
11
22
  - **Fix** — custom domains attached to SSR/function sites no longer 404: the
package/skill/SKILL.md CHANGED
@@ -1,246 +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.25.0, last updated 2026-07-04.**
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** — otherwise you litter subdomains and lose the user's URL.
35
-
36
- This file covers the core path: publish → authorize → update → claim → verify.
37
- Everything else (databases, domains, SSR, email, functions, client work) lives
38
- in the **topic files indexed at the bottom** read the one that matches the
39
- task before improvising.
40
-
41
- ## First publish on a new machine: ask for a permanent key in one step
42
-
43
- On a no-Bearer publish, include `"agentName": "<your-tool-name>"` in the body
44
- (e.g. `"Claude Code"`, `"Cursor"`). The response will include a `deviceAuth`
45
- object alongside the usual `claimUrl`:
46
-
47
- ```
48
- "deviceAuth": {
49
- "user_code": "ABCD-EFGH",
50
- "device_code": "<long opaque token, keep private>",
51
- "verification_url": "https://shiply.now/auth/ABCD-EFGH",
52
- "poll_url": "https://shiply.now/api/v1/auth/device/poll",
53
- "expires_in": 900,
54
- "interval": 2
55
- }
56
- ```
57
-
58
- Tell the user once, in plain language:
59
-
60
- > Your site is live at <siteUrl>. To keep it forever AND let me publish more
61
- > without re-pasting, open <verification_url> and click Allow.
62
-
63
- Then immediately poll `verification_url`'s sibling endpoint until the user
64
- acts:
65
-
66
- ```
67
- POST https://shiply.now/api/v1/auth/device/poll
68
- {"device_code": "<the device_code from the response>"}
69
- ```
70
-
71
- The response is one of `{status:"pending"}` (keep polling at `interval`s),
72
- `{status:"approved", api_key:"shp_...", slug_claimed:"..."}` (PERSIST the key —
73
- see "Save the key" below — the site is now permanent; Allow does both jobs in
74
- one click), `{status:"expired"|"denied"|"consumed"}` (stop). Cap your polling at
75
- the announced `expires_in` and respect `interval` — don't tight-loop.
76
-
77
- After approval, every future publish uses `Authorization: Bearer <api_key>`
78
- and the user's dashboard shows their sites without any further claiming.
79
-
80
- ### Save the key — so EVERY future site is seamless (do this once)
81
-
82
- Write the key to **`~/.shiply/credentials`** as `{"apiKey":"shp_..."}` (chmod
83
- 600) — the exact file the shiply CLI reads. This is what makes "one Allow, ever"
84
- work: from then on every publish (any site, any project, ANY FUTURE SESSION)
85
- sends `Authorization: Bearer <key>` automatically and lands owned in the user's
86
- dashboard — no re-authorizing, no claim step, zero clicks. An env var alone
87
- (`SHIPLY_API_KEY`) works for the current shell but does NOT survive a new
88
- session — the credentials file is what makes it permanent. `shiply login` writes
89
- this file for you; if you publish over raw HTTP, write it yourself.
90
-
91
- ## When the user pastes you something shiply-shaped, do this
92
-
93
- These are the patterns you will see. Match the input, run the action do not
94
- ask the user to repeat themselves or explain.
95
-
96
- - **Code like `SHIPLY-XXXXXXXX`** it's a pairing code from
97
- `shiply.now/claim/<slug>?pair=1`. The user wants to attach an anonymous
98
- site to their account. Run `shiply claim verify SHIPLY-XXXXXXXX` from the
99
- directory you originally published from (it reads `.shiply.json`). No CLI?
100
- POST `https://shiply.now/api/v1/claim/pair/SHIPLY-XXXXXXXX/verify` with
101
- `{"siteId":"...","claimToken":"..."}` taken from your earlier publish
102
- response or `.shiply.json`. The user's browser auto-redirects on success;
103
- tell them to switch back to it.
104
- - **URL like `https://shiply.now/claim/<slug>?token=...`** one-click claim
105
- link. Tell the user to open it in the browser they are signed in to —
106
- do NOT call any API yourself.
107
- - **User says "claim my site" / "save it forever" with no code or URL** →
108
- give them the `claimUrl` from your last publish response. If you no longer
109
- have that response but `.shiply.json` is in the publish dir, reconstruct it
110
- as `https://shiply.now/claim/<slug>?token=<claimToken>` from those fields.
111
- Either way, also suggest `shiply login` so future publishes mint to their
112
- account directly that's the long-term fix and how the dashboard works.
113
- - **User says "I have an API key now, use it from now on"** → persist it to
114
- `~/.shiply/credentials` (`{"apiKey":"shp_..."}`, chmod 600 see "Save the
115
- key" above), then send `Authorization: Bearer <key>` on every future publish.
116
- All future sites are then permanent and appear in their dashboard
117
- automatically across sessions, with no re-authorizing.
118
- - **User opens an `https://shiply.now/auth/XXXX-YYYY` URL you printed** no
119
- action from you; they're on the device-flow consent screen. Keep polling
120
- `poll_url` (see "First publish on a new machine" above). The poll response
121
- will flip from `pending` to `approved` (with `api_key`) the moment they
122
- click Allow.
123
-
124
- ## Pick your interface (best first)
125
-
126
- ### 1. MCP (native tools)
127
- If the `shiply` MCP server is connected (https://shiply.now/mcp), use
128
- `publish_site`. Every result includes a `toUpdate` field telling you the exact
129
- call for updates — follow it, and a `shareSuggestion` you can relay to the user.
130
- Core tools: `site_status`, `list_sites`, `get_site`, `delete_site`, `whoami`,
131
- `duplicate_site`, `set_variable`, `get_analytics`, `set_handle`. There are
132
- 100+ tools in total — call `tools/list` for the authoritative set; the topic
133
- files below name the tools for their area (domains, databases, email,
134
- functions, projects, contracts, marketplace, drives).
135
-
136
- ### 2. CLI
137
- ```bash
138
- # IMPORTANT: the npm package is `shiply-cli`, not `shiply` (different package).
139
- npm i -g shiply-cli # or: npx -y shiply-cli@latest <command>
140
- # or: curl -fsSL https://shiply.now/install.sh | bash
141
- shiply publish ./dir # live URL + confetti
142
- shiply publish ./dir # run AGAIN after edits → updates the SAME site
143
- shiply status <slug> --wait # SSL + readiness; prints SSL_READY / SITE_READY
144
- shiply login # email code → API key → sites become permanent
145
- shiply claim verify <code> # confirm a SHIPLY-XXXXXXXX pairing code from /claim/<slug>?pair=1
146
- # (uses .shiply.json from CWD; pairs agent session to user's browser)
147
- ```
148
- The CLI stores each directory's site in `.shiply.json` (slug + update token),
149
- so repeat publishes reuse the URL automatically. `--new-site` opts out.
150
- Gitignore `.shiply.json` in public repos. Parse `SITE_READY` / `SSL_READY`
151
- lines for automation; exit code 0 = ready. For a static framework build
152
- (Vite, Next export, Hugo, …) publish the BUILD OUTPUT, not the source — run
153
- `shiply detect` to see what the CLI found; the full framework matrix is in
154
- [references/publishing.md](references/publishing.md).
155
-
156
- ### 3. Raw HTTP (no installs)
157
- ```
158
- 1. POST https://shiply.now/api/v1/publish (Content-Type: application/json)
159
- {
160
- "agentName": "<your tool name, e.g. Claude Code, Cursor, Codex>",
161
- "files": [{"path":"index.html","size":<bytes>,"contentType":"text/html","hash":"<sha256 lowercase hex, optional>"}]
162
- }
163
- (+ "Authorization: Bearer shp_…" for permanent owned sites)
164
- 2. PUT each file's bytes to response upload.uploads[].url
165
- 3. POST upload.finalizeUrl with {"versionId":"<upload.versionId from step 1>"}
166
- → both the publish and finalize responses include a "toUpdate" string: the
167
- exact call to update THIS site next time. Follow it never create a new one.
168
- ```
169
-
170
- **ALWAYS include `agentName` on anonymous publishes.** The response then
171
- carries `deviceAuth` ({user_code, verification_url, poll_url, device_code,
172
- expires_in, interval}) alongside `claimUrl`. Show the user `verification_url`,
173
- poll `poll_url` every `interval` seconds with the `device_code`, and on
174
- `{status:"approved"}` you get a permanent `api_key` PLUS the linked site
175
- is claimed in the same Allow click. Without `agentName` the user has to
176
- do a separate manual claim step every time.
177
-
178
- **Updates (the response tells you how):** every publish/finalize response
179
- includes a `toUpdate` string with the exact call — follow it verbatim. In short:
180
- anonymous include `"claimToken":"..."` (returned ONCE by the first publish
181
- save it); owned include `"slug":"..."`. Hashes make updates cheap: unchanged
182
- files are skipped server-side.
183
-
184
- ## Verify before you say "done"
185
-
186
- `shiply status <slug> --wait` polls until the site serves (exit 0, prints
187
- `SITE_READY` / `SSL_READY`). `shiply verify <slug>` runs a deeper edge
188
- SSL + HTTP + thumbnail check and prints a stable machine marker line
189
- `VERIFY status=LIVE http=200 ssl=valid …` — parse that line in automation.
190
- More manage commands (list, delete, rollback, promote, stable previews) are
191
- in [references/publishing.md](references/publishing.md).
192
-
193
- ## The lifecycle to explain to users
194
- - Anonymous site: live immediately after finalize (no claim needed), expires
195
- in 24 h. Give the user the `claimUrl` claiming keeps it forever on a free
196
- account. Until claimed, anonymous pages are served with a small injected
197
- claim banner + OG meta tags — expected, not corruption. If no human is
198
- present right now, save the `claimToken` (it's also the update key); a
199
- fresh device flow can be started later via
200
- POST /api/v1/auth/device/start {"agent_name":"..."}.
201
- - API key (`shiply login` or POST /api/auth/agent/request-code
202
- verify-code): publishes are permanent and manageable.
203
- - Paid plans add vanity handles (<name>.shiply.now), more custom domains,
204
- storage, analytics: https://shiply.now/dashboard/plan
205
-
206
- ## Everything else read the topic file first
207
-
208
- The files below sit next to this one in `references/` (installed with the
209
- skill). Hosted copies: `https://shiply.now/skill/references/<file>`. Each is
210
- self-contained read the one matching the task before improvising; each
211
- covers its CLI commands, MCP tools, AND REST endpoints.
212
-
213
- * [Publishing & site management](references/publishing.md) static framework
214
- build matrix (Vite/Next/Hugo/…), `detect`, SPA mode, `.shiplyignore`,
215
- list/delete/rollback/versions, `verify`, stable previews (`--as`),
216
- `promote` preview→prod, `--json` output, form-data subcommands.
217
- * [SSR frameworks](references/ssr-frameworks.md)deploy SvelteKit, Astro,
218
- Qwik, Nuxt/Nitro, React Router v7, Next.js (OpenNext), Hono/raw Workers
219
- with server-side rendering.
220
- * [Custom domains](references/custom-domains.md) — put the user's own domain
221
- on a site: one-click OAuth DNS, manual CNAME, primary-subdomain SEO,
222
- readiness polling.
223
- * [Databases](references/databases.md) per-site SQL: free D1 (SQLite at the
224
- edge, browser shim) + Neon Postgres (branching), migrations, per-DB MCP
225
- server.
226
- * [Functions](references/functions.md) — `worker.js` server code on every
227
- request: webhooks, cron triggers, secrets, runtime logs (Workers Lite,
228
- Developer plan).
229
- * [Email](references/email.md) — every owned site sends + receives: signup
230
- capture, inbox, double-opt-in audiences, broadcasts, BYO sending domains.
231
- * [Site features](references/site-features.md) proxy routes (call AI APIs
232
- without exposing keys), Site Data (forms/waitlists, zero backend),
233
- password/invite-only access, Variables, Drives (private storage),
234
- path-mounting, public profile.
235
- * [Client work](references/client-work.md) — freelancer delivery: customer
236
- intake projects + AI briefs, group work by client, e-sign contracts,
237
- sell sites on the marketplace.
238
- * [Authentication for the user's app](references/site-features.md) — shiply
239
- does NOT host end-user auth; bring the user's own Clerk/Auth.js/Supabase
240
- (pattern in Site features "Bring your own auth").
241
-
242
- ## Limits & references
243
- ≤1000 files/site (≤50 inline via MCP), ≤100 MiB/file, 1 GiB total.
244
- Machine index: https://shiply.now/llms.txt (full single-file version:
245
- https://shiply.now/llms-full.txt) · What's new: https://shiply.now/changelog.md
246
- · 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-containedread 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
- - **Wrangler workers** any repo with `wrangler.toml` declaring `main`
38
- (Hono, itty-router, raw `fetch` handler) deploys as-is.
39
-
40
- How serving works: the deployed worker receives every request; static assets
41
- are tried first (the worker calls `env.ASSETS`, 200 = asset served from the
42
- edge / 404 = your SSR handler runs). The worker gets the same bindings,
43
- secrets, and crons as Functions see [functions.md](functions.md). Bundle
44
- cap 8 MB.
45
-
46
- Plan gate: Free/Hobby get `402 payment_required` upgrade at
47
- https://shiply.now/dashboard/plan.
1
+ ---
2
+ type: reference
3
+ title: SSR frameworks
4
+ description: Deploy full server-side-rendered apps — SvelteKit, Astro, Qwik, the Nitro family (Nuxt, SolidStart, Analog, TanStack Start), React Router v7, Next.js via OpenNext, and Hono/raw Workers.
5
+ timestamp: 2026-07-04
6
+ ---
7
+
8
+ # SSR / server frameworks (live: every major JS framework)
9
+
10
+ Part of the shiply skill (see SKILL.md for publish basics). Requires sign-in +
11
+ Developer plan.
12
+
13
+ `shiply publish` auto-detects an SSR build and deploys the worker bundle with
14
+ `nodejs_compat`, serving static assets from the edge. Live and verified on prod:
15
+ SvelteKit (`@sveltejs/adapter-cloudflare`), Astro (`@astrojs/cloudflare`), Qwik
16
+ City, the Nitro family (Nuxt, SolidStart, Analog, TanStack Start), React Router
17
+ v7, and Next.js (via OpenNext) — plus wrangler workers (`wrangler.toml` with
18
+ `main` — Hono, itty-router, raw `fetch`). **Build first** (`npm run build`),
19
+ then `shiply publish .`. Force the static path with `--framework=<name>`, or
20
+ skip SSR detection entirely with `--no-ssr`.
21
+
22
+ Per-framework build setup:
23
+
24
+ - **SvelteKit** — `@sveltejs/adapter-cloudflare`, then `npm run build`,
25
+ `shiply publish .`
26
+ - **Astro** — `@astrojs/cloudflare` adapter, `npm run build`, `shiply publish .`
27
+ - **Qwik City** — `@builder.io/qwik-city` cloudflare-pages adapter,
28
+ `npm run build`, `shiply publish .`
29
+ - **Nitro family (Nuxt, SolidStart, Analog, TanStack Start)** — build with the
30
+ `cloudflare`(-module) Nitro preset, then `shiply publish .`
31
+ - **React Router v7** — framework mode (`@react-router/dev`), `react-router
32
+ build`, then `shiply publish .`
33
+ - **Next.js** — build via `@opennextjs/cloudflare` (OpenNext), then
34
+ `shiply publish .`. Plain SSR works today; **ISR / on-demand revalidate is
35
+ not live yet**, and Next 16 must build with `next build --webpack` (not
36
+ Turbopack). `shiply publish` runs a **PREFLIGHT** on Next source dirs: it
37
+ auto-fixes Turbopack build scripts, translates `vercel.json` crons to
38
+ `.shiply/crons.json`, and refuses to publish raw source with no worker
39
+ build — read its `FIX`/`WARN` lines, do what they say, re-run.
40
+
41
+ Publish output is self-describing trust it over memory: finalize streams
42
+ per-phase progress lines, a failed worker deploy prints a structured `fix:`
43
+ line to execute verbatim, and after every worker deploy the server probes
44
+ the live site — a non-zero exit with a `runtime error:` line means
45
+ DEPLOYED BUT BROKEN (the actual worker exception + likely fix are printed,
46
+ e.g. a missing `shiply secret set`); fix the printed cause and republish.
47
+ - **Wrangler workers** — any repo with `wrangler.toml` declaring `main`
48
+ (Hono, itty-router, raw `fetch` handler) deploys as-is.
49
+
50
+ How serving works: the deployed worker receives every request; static assets
51
+ are tried first (the worker calls `env.ASSETS`, 200 = asset served from the
52
+ edge / 404 = your SSR handler runs). The worker gets the same bindings,
53
+ secrets, and crons as Functions — see [functions.md](functions.md). Bundle
54
+ cap 8 MB.
55
+
56
+ Plan gate: Free/Hobby get `402 payment_required` — upgrade at
57
+ https://shiply.now/dashboard/plan.