mnfst-publish 0.1.1 → 0.1.4

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.
@@ -109,6 +109,35 @@ function prerenderOutputDir(root) {
109
109
 
110
110
  // --- MCP JSON-RPC over Streamable HTTP -------------------------------------
111
111
 
112
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
113
+
114
+ // Retry transient failures — a thrown fetch (network blip, worker cold-start /
115
+ // redeploy window) or a 5xx — with a short backoff, so a single hiccup doesn't
116
+ // surface as a raw "fetch failed". Publish/promote/upload are idempotent enough
117
+ // that a repeat is harmless. 4xx are NOT retried (they're real client errors).
118
+ async function fetchRetry(url, init, { tries = 4, label = 'the server' } = {}) {
119
+ let lastErr;
120
+ for (let attempt = 1; attempt <= tries; attempt++) {
121
+ try {
122
+ const res = await fetch(url, init);
123
+ if (res.status >= 500 && attempt < tries) {
124
+ log(` ${label} had a hiccup (HTTP ${res.status}) — retrying (${attempt}/${tries - 1})…`);
125
+ await sleep(500 * attempt);
126
+ continue;
127
+ }
128
+ return res;
129
+ } catch (e) {
130
+ lastErr = e;
131
+ if (attempt < tries) {
132
+ log(` couldn't reach ${label} — retrying (${attempt}/${tries - 1})…`);
133
+ await sleep(500 * attempt);
134
+ continue;
135
+ }
136
+ }
137
+ }
138
+ throw lastErr;
139
+ }
140
+
112
141
  async function mcp(url, key, method, params, sessionId) {
113
142
  const headers = {
114
143
  'content-type': 'application/json',
@@ -116,7 +145,11 @@ async function mcp(url, key, method, params, sessionId) {
116
145
  'x-api-key': key,
117
146
  };
118
147
  if (sessionId) headers['mcp-session-id'] = sessionId;
119
- const res = await fetch(url, { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) });
148
+ const res = await fetchRetry(
149
+ url,
150
+ { method: 'POST', headers, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }) },
151
+ { label: 'Manifest' },
152
+ );
120
153
  const sid = res.headers.get('mcp-session-id') || sessionId;
121
154
  const text = await res.text();
122
155
  // Response may be a JSON object or an SSE "data: {...}" line.
@@ -165,7 +198,6 @@ export function isExcludedPath(rel) {
165
198
  const base = parts[parts.length - 1];
166
199
  if (base === '.env' || base.startsWith('.env.')) return true; // .env, .env.local, .env.prod …
167
200
  if (base === '.npmrc' || base === '.dev.vars' || base === 'id_rsa' || base === '.DS_Store') return true;
168
- if (base === '.mnfst-render-complete') return true; // internal render sentinel — not part of the site
169
201
  if (/\.(pem|key|p12|pfx)$/i.test(base)) return true;
170
202
  return false;
171
203
  }
@@ -298,16 +330,19 @@ export async function main() {
298
330
  if (r.status !== 0) fail('render failed — fix the errors above and try again.');
299
331
  }
300
332
 
301
- // For a render project, confirm the output is a COMPLETE render before shipping
302
- // it. mnfst-render writes a .mnfst-render-complete sentinel as its final step
303
- // (atomic swap), so its absence means the dir is stale or half-built — refuse
304
- // rather than publish a broken/empty site.
333
+ // For a render project, sanity-check there's actually a built site to ship —
334
+ // an output folder with an index.html. (When we just ran the render above, its
335
+ // exit code was already checked; this also covers --no-render and hand-built
336
+ // output.) Deliberately forgiving: no hard dependency on any marker file, so a
337
+ // good build always publishes regardless of which mnfst-render produced it.
305
338
  if (source === 'render') {
306
339
  const outDir = prerenderOutputDir(root);
307
- if (!existsSync(join(root, outDir, '.mnfst-render-complete'))) {
340
+ if (!existsSync(join(root, outDir, 'index.html'))) {
308
341
  fail(
309
- `the "${outDir}" directory isn't a complete render` +
310
- (opts.render === false ? ' — re-run without --no-render.' : ' — run `npx mnfst-render` and try again.'),
342
+ `the "${outDir}" folder doesn't have a built site yet (no index.html). ` +
343
+ (opts.render === false
344
+ ? 'Remove --no-render so it builds first, or run `npx mnfst-render`, then publish again.'
345
+ : 'Run `npx mnfst-render` first, then publish again.'),
311
346
  );
312
347
  }
313
348
  }
@@ -335,11 +370,11 @@ export async function main() {
335
370
  const zip = buildZip(root, rels);
336
371
  log(`Uploading ${rels.length} files (${(zip.length / 1048576).toFixed(1)} MB)…`);
337
372
 
338
- const up = await fetch(uploadUrl, {
339
- method: 'POST',
340
- headers: { 'content-type': 'application/zip' },
341
- body: zip,
342
- });
373
+ const up = await fetchRetry(
374
+ uploadUrl,
375
+ { method: 'POST', headers: { 'content-type': 'application/zip' }, body: zip },
376
+ { label: 'the upload server' },
377
+ );
343
378
  const upText = await up.text();
344
379
  let upJson = null;
345
380
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-publish",
3
- "version": "0.1.1",
3
+ "version": "0.1.4",
4
4
  "description": "One-command managed publishing for Manifest projects — render, zip, upload, live URL.",
5
5
  "type": "module",
6
6
  "bin": {