mnfst-publish 0.1.5 → 0.1.6

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.
@@ -202,6 +202,63 @@ export function isExcludedPath(rel) {
202
202
  return false;
203
203
  }
204
204
 
205
+ // Publish-time exclusions, declared per-project and DECOUPLED from git — so a file
206
+ // can stay versioned yet never ship. Sourced from a `.manifestignore` file
207
+ // (gitignore-style) and/or a `publishIgnore: []` array in manifest.json. Supports
208
+ // comments (#), directory patterns (`dir/`), path-anchored patterns (`a/b`), and
209
+ // `*` / `**` / `?` globs. Returns a matcher; matches nothing when there are no rules.
210
+ export function makePublishIgnore(rawPatterns) {
211
+ const rules = [];
212
+ for (let p of rawPatterns || []) {
213
+ if (typeof p !== 'string') continue;
214
+ p = p.trim();
215
+ if (!p || p.startsWith('#')) continue;
216
+ const dirOnly = p.endsWith('/');
217
+ if (dirOnly) p = p.slice(0, -1);
218
+ const anchored = p.startsWith('/');
219
+ if (anchored) p = p.replace(/^\/+/, '');
220
+ if (!p) continue;
221
+ const pathScoped = anchored || p.includes('/');
222
+ const body = p
223
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
224
+ .replace(/\*\*/g, '')
225
+ .replace(/\*/g, '[^/]*')
226
+ .replace(//g, '.*')
227
+ .replace(/\?/g, '[^/]');
228
+ rules.push({ dirOnly, pathScoped, exact: new RegExp('^' + body + '$'), prefix: new RegExp('^' + body + '/') });
229
+ }
230
+ if (!rules.length) return () => false;
231
+ return (rel) => {
232
+ for (const r of rules) {
233
+ if (r.pathScoped) {
234
+ if (!r.dirOnly && r.exact.test(rel)) return true; // exact file/path
235
+ if (r.prefix.test(rel)) return true; // anything under the dir/prefix
236
+ } else {
237
+ // Unanchored, no slash: match any path segment (a dir name, or a basename).
238
+ const segs = rel.split('/');
239
+ for (let i = 0; i < segs.length; i++) {
240
+ if (r.dirOnly && i === segs.length - 1) continue; // a `dir/` rule can't match the file itself
241
+ if (r.exact.test(segs[i])) return true;
242
+ }
243
+ }
244
+ }
245
+ return false;
246
+ };
247
+ }
248
+
249
+ export function loadPublishIgnore(root) {
250
+ const patterns = [];
251
+ const ignoreFile = join(root, '.manifestignore');
252
+ if (existsSync(ignoreFile)) {
253
+ try { patterns.push(...readFileSync(ignoreFile, 'utf8').split(/\r?\n/)); } catch { /* ignore */ }
254
+ }
255
+ try {
256
+ const mf = JSON.parse(readFileSync(join(root, 'manifest.json'), 'utf8'));
257
+ if (Array.isArray(mf.publishIgnore)) patterns.push(...mf.publishIgnore);
258
+ } catch { /* no / invalid manifest.json — nothing to add */ }
259
+ return makePublishIgnore(patterns);
260
+ }
261
+
205
262
  export function collectFiles(root) {
206
263
  const git = spawnSync('git', ['ls-files', '-co', '--exclude-standard'], { cwd: root, encoding: 'utf8' });
207
264
  let rels;
@@ -221,8 +278,11 @@ export function collectFiles(root) {
221
278
  };
222
279
  walk(root);
223
280
  }
224
- // Final guarddrops nested secret files the git/walk lists may include.
225
- return rels.filter((r) => !isExcludedPath(r));
281
+ // Final guardsdrop nested secret files, then project-declared publish exclusions.
282
+ // .manifestignore applies ON TOP of gitignore, so a versioned file can still be
283
+ // kept out of the published bundle.
284
+ const publishIgnored = loadPublishIgnore(root);
285
+ return rels.filter((r) => !isExcludedPath(r) && !publishIgnored(r));
226
286
  }
227
287
 
228
288
  // --- Minimal ZIP writer (DEFLATE), pure Node, no deps ----------------------
@@ -311,19 +371,33 @@ export async function main() {
311
371
  }
312
372
 
313
373
  const root = opts.root ? opts.root : findRoot(process.cwd());
314
- const key = readApiKey(root, opts.key);
315
- if (!key) fail('no API key found. Expected MANIFEST_API_KEY in .env (this folder doesn’t look like a Manifest project, or it isn’t set up for publishing).');
316
374
  const url = readMcpUrl(root, opts.mcp);
317
375
  const source = detectSource(root, opts.source);
318
376
 
319
- // Promote a previously-staged build straight to production (no upload).
377
+ // A connector-driven publish passes a pre-authorised, one-time upload URL in
378
+ // the environment (minted by the manifest_publish tool for the signed-in
379
+ // user). When present, no API key is needed — the token IS the authorisation,
380
+ // so an invited teammate publishes without ever handling a project secret.
381
+ const injectedUpload = process.env.MNFST_PUBLISH_UPLOAD_URL || null;
382
+ const key = readApiKey(root, opts.key);
383
+
384
+ // Promote a previously-staged build straight to production (no upload). Headless
385
+ // convenience; interactive users promote via the connector's manifest_promote
386
+ // tool (no key). Needs a key.
320
387
  if (opts.promote) {
388
+ if (!key) fail('no API key found for --promote. Set MANIFEST_API_KEY in .env for headless use, or promote from Claude with the Manifest connector (no key needed).');
321
389
  log('Promoting the staged version to production…');
322
390
  const res = await callTool(url, key, 'manifest_promote', {});
323
391
  log('✓ Live: ' + (res.url || res._text || 'production updated'));
324
392
  return;
325
393
  }
326
394
 
395
+ // A normal publish needs either the injected one-time URL (connector) or a
396
+ // stored key (headless/CI). Without either, there's nothing to authorise with.
397
+ if (!injectedUpload && !key) {
398
+ fail('no API key found. Publish from Claude with the Manifest connector (no key needed), or set MANIFEST_API_KEY in .env for headless/CI use.');
399
+ }
400
+
327
401
  if (source === 'render' && opts.render !== false) {
328
402
  log('Rendering the site…');
329
403
  const r = spawnSync('npx', ['mnfst-render'], { cwd: root, stdio: 'inherit', shell: process.platform === 'win32' });
@@ -347,11 +421,18 @@ export async function main() {
347
421
  }
348
422
  }
349
423
 
350
- log(`Preparing ${opts.env} deploy…`);
351
- const handshake = await callTool(url, key, 'manifest_publish', { env: opts.env, source, via_cli: true });
352
- if (handshake.already_pro) { /* not applicable */ }
353
- const uploadUrl = handshake.upload_url;
354
- if (!uploadUrl) fail(handshake._text || 'could not start the publish (no upload URL returned).');
424
+ let uploadUrl;
425
+ if (injectedUpload) {
426
+ // Connector path: the manifest_publish tool already minted the deployment
427
+ // and a one-time upload URL for the signed-in user. No handshake, no key.
428
+ uploadUrl = injectedUpload;
429
+ } else {
430
+ // Headless/CI: authenticate the handshake with the key to get an upload URL.
431
+ log(`Preparing ${opts.env} deploy…`);
432
+ const handshake = await callTool(url, key, 'manifest_publish', { env: opts.env, source, via_cli: true });
433
+ uploadUrl = handshake.upload_url;
434
+ if (!uploadUrl) fail(handshake._text || 'could not start the publish (no upload URL returned).');
435
+ }
355
436
  // The upload carries the whole project. Only POST it to an HTTPS endpoint on
356
437
  // the SAME host as the MCP server — never to an arbitrary URL a tampered
357
438
  // response or misconfigured .mcp.json could inject.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mnfst-publish",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "One-command managed publishing for Manifest projects — render, zip, upload, live URL.",
5
5
  "type": "module",
6
6
  "bin": {