shiply-cli 0.22.0 → 0.24.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/functions.js CHANGED
@@ -97,6 +97,25 @@ async function deployCmd(argv, inherited) {
97
97
  }
98
98
  const apiKey = await getApiKey(flags, 'function');
99
99
  const base = resolveBase(flags.base);
100
+ // Collect repeatable `--secret NAME=VALUE` — set on the worker as part of this
101
+ // deploy so first-time setup doesn't hit "deploy a function first".
102
+ const secrets = [];
103
+ for (let i = 0; i < argv.length; i++) {
104
+ let pair;
105
+ if (argv[i] === '--secret') {
106
+ pair = argv[i + 1];
107
+ i++;
108
+ }
109
+ else if (argv[i].startsWith('--secret=')) {
110
+ pair = argv[i].slice('--secret='.length);
111
+ }
112
+ if (pair) {
113
+ const eq = pair.indexOf('=');
114
+ if (eq < 1)
115
+ throw new Error(`--secret must be NAME=VALUE (got "${pair}")`);
116
+ secrets.push({ name: pair.slice(0, eq), value: pair.slice(eq + 1) });
117
+ }
118
+ }
100
119
  const filename = flags.ts ? 'worker.ts' : 'worker.js';
101
120
  const entryAbs = join(process.cwd(), filename);
102
121
  // Pre-read for a friendly "file not found" message (esbuild's is cryptic).
@@ -119,7 +138,7 @@ async function deployCmd(argv, inherited) {
119
138
  const row = await api(`${base}/api/v1/sites/${slug}/function`, {
120
139
  method: 'POST',
121
140
  headers: headers(apiKey),
122
- body: JSON.stringify({ source, lang: 'js' }),
141
+ body: JSON.stringify({ source, lang: 'js', ...(secrets.length ? { secrets } : {}) }),
123
142
  });
124
143
  if (flags.json) {
125
144
  console.log(JSON.stringify(row, null, 2));
@@ -129,6 +148,8 @@ async function deployCmd(argv, inherited) {
129
148
  console.log(` script: ${row.cfScriptName}`);
130
149
  console.log(` version: ${row.version.slice(0, 12)}`);
131
150
  console.log(` source: ${filename} (bundled → js)`);
151
+ if (secrets.length)
152
+ console.log(` secrets: set ${secrets.map((s) => s.name).join(', ')}`);
132
153
  console.log(` routes: requests to https://${slug}.shiply.now/* now hit your worker first`);
133
154
  }
134
155
  async function getFnCmd(argv, inherited) {
package/dist/index.js CHANGED
@@ -348,6 +348,10 @@ async function main() {
348
348
  throw new Error('nothing to update here — publish first (shiply remembers the site in .shiply.json), or pass --claim-token');
349
349
  }
350
350
  const updating = Boolean(claimToken || slug);
351
+ // Send the stable siteId so a publish self-heals after a handle rename
352
+ // left state.slug stale. Only for the normal state-based update path —
353
+ // never with --as (alias has its own slug) or --new-site.
354
+ const siteId = !aliasName && !values['new-site'] && state?.owned && apiKey ? state?.siteId : undefined;
351
355
  // Auto-attach a previously-created database on publish. Anonymous
352
356
  // publishes can't attach (no auth subject) — silently skipped server-side.
353
357
  const attachDatabaseId = state?.databaseId && apiKey ? state.databaseId : undefined;
@@ -361,6 +365,7 @@ async function main() {
361
365
  spaMode: spa,
362
366
  claimToken,
363
367
  slug,
368
+ siteId,
364
369
  attachDatabaseId,
365
370
  previewBranchDbId,
366
371
  // Group under a client (owned publishes only). --client "<name|email>"
package/dist/publish.js CHANGED
@@ -12,8 +12,48 @@ export class ApiError extends Error {
12
12
  this.name = 'ApiError';
13
13
  }
14
14
  }
15
+ // Transient statuses worth retrying: gateway/timeout/rate-limit. NOT 500 — a
16
+ // deterministic server bug shouldn't be hammered (and retrying a non-idempotent
17
+ // create on a 500 risks duplicate work; a 502/503/504 means the origin likely
18
+ // never processed the request, so a retry is safe).
19
+ const TRANSIENT_STATUS = new Set([408, 429, 502, 503, 504]);
20
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+ const backoffMs = (attempt) => Math.min(8000, 500 * 2 ** attempt) + Math.floor(Math.random() * 250);
22
+ /** fetch() with a per-attempt timeout + retry-with-backoff on transient failures
23
+ * (network errors, aborts/timeouts, and 5xx gateway statuses). Bodies here are
24
+ * strings/Buffers, so re-sending on retry is safe. */
25
+ export async function fetchWithRetry(url, init, opts = {}) {
26
+ const attempts = opts.attempts ?? 4;
27
+ const timeoutMs = opts.timeoutMs ?? 60_000;
28
+ const label = opts.label ?? url;
29
+ let lastErr;
30
+ for (let i = 0; i < attempts; i++) {
31
+ const ac = new AbortController();
32
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
33
+ try {
34
+ const res = await fetch(url, { ...init, signal: ac.signal });
35
+ clearTimeout(timer);
36
+ if (TRANSIENT_STATUS.has(res.status) && i < attempts - 1) {
37
+ process.stderr.write(` ↻ ${label} returned ${res.status} — retrying (${i + 2}/${attempts})…\n`);
38
+ await sleep(backoffMs(i));
39
+ continue;
40
+ }
41
+ return res;
42
+ }
43
+ catch (e) {
44
+ clearTimeout(timer);
45
+ lastErr = e;
46
+ if (i < attempts - 1) {
47
+ process.stderr.write(` ↻ ${label} failed (${e.message}) — retrying (${i + 2}/${attempts})…\n`);
48
+ await sleep(backoffMs(i));
49
+ continue;
50
+ }
51
+ }
52
+ }
53
+ throw new Error(`request to ${label} failed after ${attempts} attempts: ${lastErr?.message ?? 'unknown error'}`);
54
+ }
15
55
  export async function api(url, init) {
16
- const res = await fetch(url, init);
56
+ const res = await fetchWithRetry(url, init, { label: `${init.method ?? 'GET'} ${url}` });
17
57
  const body = await res.json().catch(() => ({}));
18
58
  if (!res.ok) {
19
59
  const err = body.error;
@@ -40,6 +80,7 @@ export async function publish(dir, opts = {}) {
40
80
  ...(opts.spaMode ? { spaMode: true } : {}),
41
81
  ...(opts.claimToken ? { claimToken: opts.claimToken } : {}),
42
82
  ...(opts.slug ? { slug: opts.slug } : {}),
83
+ ...(opts.siteId ? { siteId: opts.siteId } : {}),
43
84
  ...(opts.attachDatabaseId ? { attachDatabaseId: opts.attachDatabaseId } : {}),
44
85
  ...(opts.previewBranchDbId ? { previewBranchDbId: opts.previewBranchDbId } : {}),
45
86
  ...(opts.client ? { client: opts.client } : {}),
@@ -70,7 +111,7 @@ async function uploadAll(dir, targets) {
70
111
  while (next < targets.length) {
71
112
  const t = targets[next++];
72
113
  const body = await readFile(join(dir, t.path));
73
- const res = await fetch(t.url, { method: t.method, headers: t.headers, body });
114
+ const res = await fetchWithRetry(t.url, { method: t.method, headers: t.headers, body }, { label: `upload ${t.path}` });
74
115
  if (!res.ok)
75
116
  throw new Error(`upload failed for ${t.path}: HTTP ${res.status}`);
76
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shiply-cli",
3
- "version": "0.22.0",
3
+ "version": "0.24.0",
4
4
  "description": "Publish static sites to shiply.now from the command line \u00e2\u20ac\u201d instant web hosting for agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/skill/SKILL.md CHANGED
@@ -26,7 +26,7 @@ description: "shiply is the production backend for anything an agent builds —
26
26
 
27
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
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, no usage meter), so a spike or DDoS can never become a surprise bill for the owner.
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
30
 
31
31
  **How (quickstart).** `shiply publish ./dir` (CLI) — or POST /api/v1/publish → PUT each file → POST finalize — returns a live URL + claimToken.
32
32