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.
- package/manifest.publish.mjs +91 -10
- package/package.json +1 -1
package/manifest.publish.mjs
CHANGED
|
@@ -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
|
|
225
|
-
|
|
281
|
+
// Final guards — drop 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
|
-
//
|
|
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
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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.
|