gutterpress 0.10.0-beta.1 → 0.10.1-beta.1

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.
Files changed (56) hide show
  1. package/README.md +3 -16
  2. package/dist/api/index.d.ts +10 -19
  3. package/dist/api/index.js +8 -64
  4. package/dist/{audit-85zn35v8.js → audit-7a37g51p.js} +5 -5
  5. package/dist/{build-1faqmp0y.js → build-kdaet3pd.js} +5 -5
  6. package/dist/checks/source/index.d.ts +1 -0
  7. package/dist/checks/source/merge-markers.d.ts +3 -0
  8. package/dist/{cli-afm8pa0h.js → cli-ak1tagkr.js} +95 -58
  9. package/dist/{cli-5tffrg48.js → cli-df7gcyns.js} +510 -1375
  10. package/dist/{cli-7e79d7se.js → cli-dx6sxj2t.js} +1 -1
  11. package/dist/{cli-v935c2ws.js → cli-mcb484g3.js} +2 -47
  12. package/dist/{cli-k1065rkg.js → cli-w692g6dx.js} +75 -29
  13. package/dist/cli.js +14 -15
  14. package/dist/{doctor-69v5c9wy.js → doctor-vajd5h45.js} +2 -2
  15. package/dist/{engine-nbfdy0pp.js → engine-0nt35svz.js} +2 -2
  16. package/dist/{engine-hrtn0b28.js → engine-gkcqdye5.js} +1 -1
  17. package/dist/{gutterpress-viewer-fb6fhwsy.js → gutterpress-viewer-tma1qwz2.js} +111 -35
  18. package/dist/{index-052x5fef.js → index-6g72h2n1.js} +980 -1677
  19. package/dist/{index-c2b6gyeg.js → index-b2rqw41a.js} +2 -2
  20. package/dist/{index-mdefp0y5.js → index-dtrftayr.js} +75 -27
  21. package/dist/{index-wsydvp08.js → index-kvgq4q3q.js} +94 -57
  22. package/dist/index.js +9 -65
  23. package/dist/lib/build-staging.d.ts +51 -0
  24. package/dist/lib/git-fs.d.ts +43 -0
  25. package/dist/lib/host-policy.d.ts +14 -2
  26. package/dist/lib/remote-auth/clone.d.ts +0 -21
  27. package/dist/lib/remote-auth/converge-merge.d.ts +24 -7
  28. package/dist/lib/remote-auth/generic-auth.d.ts +0 -22
  29. package/dist/lib/remote-auth/git-http.d.ts +1 -1
  30. package/dist/lib/remote-auth/github-auth.d.ts +2 -22
  31. package/dist/lib/remote-auth/operation-log.d.ts +4 -9
  32. package/dist/lib/remote-auth/sync-messages.d.ts +35 -12
  33. package/dist/lib/remote-auth/sync-types.d.ts +30 -101
  34. package/dist/lib/remote-auth/sync.d.ts +19 -21
  35. package/dist/lib/remote-auth/token-store.d.ts +0 -10
  36. package/dist/lib/remote-auth/transport.d.ts +42 -40
  37. package/dist/lib/source-provider.d.ts +12 -0
  38. package/dist/{lint-17404bsx.js → lint-p6a4bkvh.js} +5 -5
  39. package/dist/{new-vsgpxzek.js → new-4cdzfbsj.js} +5 -5
  40. package/dist/{plugin-s4yfpx6p.js → plugin-a89tqspy.js} +5 -5
  41. package/dist/{preflight-1h3apvqh.js → preflight-r4y3f8f9.js} +5 -5
  42. package/dist/{preview-interface-hg5eqv88.js → preview-interface-0ssk8bmm.js} +4 -2
  43. package/dist/{preview-cqf383g4.js → preview-k8n2kew7.js} +5 -5
  44. package/dist/{publish-arvycera.js → publish-jnhjp2vd.js} +5 -5
  45. package/dist/{source-provider-3tcj6qg2.js → source-provider-2j5x2hyw.js} +1 -1
  46. package/dist/{source-provider-vanafrt9.js → source-provider-kn41jmbs.js} +1 -1
  47. package/dist/{validate-ztzq7p35.js → validate-prnybpkx.js} +5 -5
  48. package/package.json +1 -1
  49. package/dist/lib/app-heartbeat.d.ts +0 -102
  50. package/dist/lib/remote-auth/image-clash.d.ts +0 -17
  51. package/dist/lib/remote-auth/recovery/classify.d.ts +0 -97
  52. package/dist/lib/remote-auth/recovery/inspect.d.ts +0 -75
  53. package/dist/lib/remote-auth/recovery/locks.d.ts +0 -20
  54. package/dist/lib/remote-auth/recovery/repair.d.ts +0 -29
  55. package/dist/lib/remote-auth/recovery/types.d.ts +0 -39
  56. package/dist/repair-vmwh2bhe.js +0 -137
@@ -17,19 +17,18 @@ import {
17
17
  resolveChromiumExecutable,
18
18
  run,
19
19
  spawnCapture
20
- } from "./index-c2b6gyeg.js";
20
+ } from "./index-b2rqw41a.js";
21
21
  import {
22
- gitDirFor,
22
+ gitFs,
23
23
  gitScopeFor,
24
24
  hasPendingChanges,
25
25
  hasUncommittedChanges,
26
- isNoChangesError,
27
26
  resolveGitAuthor,
28
27
  resolveLogger,
29
28
  snapshotStagingMarkerPath,
30
29
  snapshotWorkingTreeUnlocked,
31
30
  withRepoLock
32
- } from "./index-mdefp0y5.js";
31
+ } from "./index-dtrftayr.js";
33
32
  import {
34
33
  detectProjectSource,
35
34
  syncRemoteFor
@@ -39,7 +38,7 @@ import {
39
38
  } from "./index-37x76zdn.js";
40
39
 
41
40
  // src/lib/build-runner.ts
42
- import path7 from "node:path";
41
+ import path8 from "node:path";
43
42
  import fs2 from "node:fs";
44
43
  import fsp2 from "node:fs/promises";
45
44
  import os2 from "node:os";
@@ -544,6 +543,37 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
544
543
 
545
544
  // src/lib/markdown/index.ts
546
545
  import { readdir as readdir2, readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
546
+
547
+ // src/lib/remote-auth/sync-messages.ts
548
+ var MSG_UP_TO_DATE = "Everything is in sync.";
549
+ var MSG_UP_TO_DATE_PULLED = "Everything is in sync. The latest online changes were downloaded to this computer.";
550
+ var MSG_SYNCED = "Your changes are online.";
551
+ var MSG_SYNCED_MERGED = "Your changes are online, combined with changes from the online copy.";
552
+ var MSG_OFFLINE = "Your changes are saved on this computer. gutterpress couldn't reach the online repository — try syncing again when you're back online.";
553
+ var MSG_AUTH = "The online repository didn't accept the saved connection. Reconnect and try again.";
554
+ var MSG_INSECURE_TRANSPORT = "This project's online address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Switch the address to a secure one (starting with https), or to a local loopback address for a server on this computer, to sync.";
555
+ var MSG_BUSY = "The online copy is changing very quickly right now. Your work is saved on this computer — try Sync again in a moment.";
556
+ var MSG_HISTORY_UNREADABLE = "This project's version history can't be read, so syncing can't continue. Your writing is safe — every file is still here on this computer. To get history and syncing working again, download a fresh copy of the project from online, then move any recent changes into it.";
557
+ var MSG_UNRELATED = "This project and the online copy have no history in common, so they can't be combined. Either the online address points at a different project, or this project's own history was lost and started over. Your writing is safe — every file is still here on this computer. Check the online address; if it is correct, download a fresh copy from online and move any recent changes into it.";
558
+ var MSG_NO_REMOTE = "This project isn't connected to an online repository yet.";
559
+ var MSG_SSH_REMOTE = "This project's online address uses SSH (git@…), which gutterpress can't sync to. Switch it to the web (HTTPS) address to sync from here.";
560
+ var MSG_NO_BRANCH = "This project's version history isn't on a named branch, so it can't be synced right now.";
561
+ var SYNC_SNAPSHOT_MESSAGE = "Automatic backup of your work";
562
+ var SYNC_LATE_EDIT_MESSAGE = "Saved the edit you made while syncing";
563
+ function onlineSiblingPath(filepath) {
564
+ const dot = filepath.lastIndexOf(".");
565
+ const slash = filepath.lastIndexOf("/");
566
+ const hasExt = dot > slash + 1;
567
+ return hasExt ? `${filepath.slice(0, dot)}.online${filepath.slice(dot)}` : `${filepath}.online`;
568
+ }
569
+ function isOnlineSibling(filepath) {
570
+ const dot = filepath.lastIndexOf(".");
571
+ const slash = filepath.lastIndexOf("/");
572
+ const stem = dot > slash + 1 ? filepath.slice(0, dot) : filepath;
573
+ return stem.endsWith(".online");
574
+ }
575
+
576
+ // src/lib/markdown/index.ts
547
577
  import { join } from "node:path";
548
578
 
549
579
  // src/lib/desktop.ts
@@ -2404,7 +2434,7 @@ async function inlineShapeUrls(html, baseDir) {
2404
2434
  async function resolveActiveMarkdownFiles(inputDir, configuredFiles) {
2405
2435
  if (configuredFiles && configuredFiles.length > 0)
2406
2436
  return configuredFiles;
2407
- return (await readdir2(inputDir)).filter((f) => f.endsWith(".md")).sort();
2437
+ return (await readdir2(inputDir)).filter((f) => f.endsWith(".md") && !isOnlineSibling(f)).sort();
2408
2438
  }
2409
2439
  async function renderChapters(inputDir, opts = {}) {
2410
2440
  const styles = await resolveActiveStyles(inputDir, opts.styles);
@@ -3950,7 +3980,7 @@ import git from "isomorphic-git";
3950
3980
  // package.json
3951
3981
  var package_default = {
3952
3982
  name: "gutterpress",
3953
- version: "0.10.0-beta.1",
3983
+ version: "0.10.1-beta.1",
3954
3984
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3955
3985
  author: "itlackey",
3956
3986
  license: "MPL-2.0",
@@ -4219,183 +4249,6 @@ async function writeBuildFingerprint(input) {
4219
4249
  return outPath;
4220
4250
  }
4221
4251
 
4222
- // src/lib/missing-asset-placeholder.ts
4223
- import { createHash as createHash5 } from "node:crypto";
4224
- import { deflateSync } from "node:zlib";
4225
- function placeholderOutputPath(missingOutputPath) {
4226
- const hash = createHash5("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
4227
- return `assets/gutterpress-missing/${hash}.png`;
4228
- }
4229
- function decodeHtmlAttribute(value) {
4230
- return value.replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
4231
- }
4232
- function rewriteMissingImageReferences(html, replacements) {
4233
- if (replacements.size === 0)
4234
- return html;
4235
- const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
4236
- const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|&quot;((?:(?!&quot;).)*)&quot;|([^'"()\s]*))\s*\)/giy;
4237
- const rewriteCssUrls = (css) => {
4238
- let out2 = "";
4239
- let copiedThrough = 0;
4240
- let index = 0;
4241
- while (index < css.length) {
4242
- if (css.startsWith("/*", index)) {
4243
- const end = css.indexOf("*/", index + 2);
4244
- index = end < 0 ? css.length : end + 2;
4245
- continue;
4246
- }
4247
- const char = css[index];
4248
- if (char === '"' || char === "'") {
4249
- const quote = char;
4250
- index++;
4251
- while (index < css.length) {
4252
- if (css[index] === "\\") {
4253
- index += 2;
4254
- continue;
4255
- }
4256
- const current = css[index++];
4257
- if (current === quote)
4258
- break;
4259
- }
4260
- continue;
4261
- }
4262
- const previous = index > 0 ? css[index - 1] : "";
4263
- if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
4264
- cssUrl.lastIndex = index;
4265
- const match = cssUrl.exec(css);
4266
- if (match) {
4267
- const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
4268
- const replacement = replacementFor(raw);
4269
- if (replacement) {
4270
- out2 += css.slice(copiedThrough, index);
4271
- out2 += match[3] !== undefined ? `url(&quot;${replacement}&quot;)` : `url("${replacement}")`;
4272
- index = cssUrl.lastIndex;
4273
- copiedThrough = index;
4274
- continue;
4275
- }
4276
- }
4277
- }
4278
- index++;
4279
- }
4280
- return copiedThrough === 0 ? css : out2 + css.slice(copiedThrough);
4281
- };
4282
- const rewriteTag = (tag) => {
4283
- let out2 = tag;
4284
- if (/^<img\b/i.test(tag)) {
4285
- out2 = out2.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
4286
- const raw = double ?? single ?? bare ?? "";
4287
- const replacement = replacementFor(raw);
4288
- if (!replacement)
4289
- return whole;
4290
- if (double !== undefined)
4291
- return `${prefix}"${replacement}"`;
4292
- if (single !== undefined)
4293
- return `${prefix}'${replacement}'`;
4294
- return `${prefix}${replacement}`;
4295
- });
4296
- }
4297
- if (/^<(?:img|source)\b/i.test(tag)) {
4298
- out2 = out2.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
4299
- const value = double ?? single ?? bare ?? "";
4300
- let rewritten = "";
4301
- let copiedThrough = 0;
4302
- let changed = false;
4303
- for (const candidate of parseSrcsetUrlCandidates(value)) {
4304
- const replacement = replacementFor(candidate.url);
4305
- if (!replacement)
4306
- continue;
4307
- rewritten += value.slice(copiedThrough, candidate.start) + replacement;
4308
- copiedThrough = candidate.end;
4309
- changed = true;
4310
- }
4311
- if (!changed)
4312
- return whole;
4313
- rewritten += value.slice(copiedThrough);
4314
- if (double !== undefined)
4315
- return `${prefix}"${rewritten}"`;
4316
- if (single !== undefined)
4317
- return `${prefix}'${rewritten}'`;
4318
- return `${prefix}${rewritten}`;
4319
- });
4320
- }
4321
- out2 = out2.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
4322
- const rewritten = rewriteCssUrls(value);
4323
- return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
4324
- });
4325
- return out2;
4326
- };
4327
- const rewriteActiveHtml = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, (tag) => rewriteTag(tag));
4328
- const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
4329
- let out = "";
4330
- let last = 0;
4331
- for (const match of html.matchAll(protectedRegion)) {
4332
- out += rewriteActiveHtml(html.slice(last, match.index));
4333
- if (match[1]?.toLowerCase() === "style") {
4334
- const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(match[0]);
4335
- out += style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : match[0];
4336
- } else {
4337
- out += match[0];
4338
- }
4339
- last = (match.index ?? 0) + match[0].length;
4340
- }
4341
- out += rewriteActiveHtml(html.slice(last));
4342
- return out;
4343
- }
4344
- function crc32(buf) {
4345
- let c = ~0;
4346
- for (let i = 0;i < buf.length; i++) {
4347
- c ^= buf[i];
4348
- for (let k = 0;k < 8; k++)
4349
- c = c >>> 1 ^ 3988292384 & -(c & 1);
4350
- }
4351
- return ~c >>> 0;
4352
- }
4353
- function chunk(type, data) {
4354
- const out = new Uint8Array(data.length + 12);
4355
- const view = new DataView(out.buffer);
4356
- view.setUint32(0, data.length);
4357
- for (let i = 0;i < 4; i++)
4358
- out[4 + i] = type.charCodeAt(i);
4359
- out.set(data, 8);
4360
- view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
4361
- return out;
4362
- }
4363
- function placeholderPng(width = 640, height = 480, cell = 32) {
4364
- const A = [217, 70, 239];
4365
- const B = [26, 26, 26];
4366
- const raw = new Uint8Array(height * (1 + width * 3));
4367
- let p = 0;
4368
- for (let y = 0;y < height; y++) {
4369
- raw[p++] = 0;
4370
- for (let x = 0;x < width; x++) {
4371
- const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
4372
- raw[p++] = c[0];
4373
- raw[p++] = c[1];
4374
- raw[p++] = c[2];
4375
- }
4376
- }
4377
- const ihdr = new Uint8Array(13);
4378
- const hv = new DataView(ihdr.buffer);
4379
- hv.setUint32(0, width);
4380
- hv.setUint32(4, height);
4381
- ihdr[8] = 8;
4382
- ihdr[9] = 2;
4383
- const parts = [
4384
- new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
4385
- chunk("IHDR", ihdr),
4386
- chunk("IDAT", new Uint8Array(deflateSync(raw))),
4387
- chunk("IEND", new Uint8Array(0))
4388
- ];
4389
- const total = parts.reduce((n, x) => n + x.length, 0);
4390
- const png = new Uint8Array(total);
4391
- let o = 0;
4392
- for (const part of parts) {
4393
- png.set(part, o);
4394
- o += part.length;
4395
- }
4396
- return png;
4397
- }
4398
-
4399
4252
  // src/lib/lint-runner.ts
4400
4253
  import { resolve as resolve4 } from "node:path";
4401
4254
  import { readFile as readFile8 } from "node:fs/promises";
@@ -6361,11 +6214,9 @@ var check22 = {
6361
6214
  };
6362
6215
  registerCheck(check22);
6363
6216
 
6364
- // src/checks/asset/image-file-size.ts
6365
- import { stat as stat3 } from "node:fs/promises";
6366
-
6367
- // src/lib/image-inspect.ts
6368
- import { stat as stat2, readFile as readFile18 } from "node:fs/promises";
6217
+ // src/checks/source/merge-markers.ts
6218
+ import { readFile as readFile18 } from "node:fs/promises";
6219
+ import path6 from "node:path";
6369
6220
 
6370
6221
  // src/checks/asset/extensions.ts
6371
6222
  var RASTER_INSPECTABLE_EXTS = [
@@ -6388,27 +6239,108 @@ var ASSET_SCAN_IGNORE_GLOBS = [
6388
6239
  "**/dist/**"
6389
6240
  ];
6390
6241
 
6242
+ // src/checks/source/merge-markers.ts
6243
+ var OPEN_SENTINEL = "<<<<<<< your version";
6244
+ var CLOSE_SENTINEL = ">>>>>>> online version";
6245
+ function withoutOnlineTag(relPath) {
6246
+ return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
6247
+ }
6248
+ var check23 = {
6249
+ id: "source.sync.merge-markers",
6250
+ name: "Combined Versions",
6251
+ description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
6252
+ category: "source",
6253
+ phase: "pre-build",
6254
+ async run(ctx) {
6255
+ const results = [];
6256
+ const textFiles = [...ctx.markdownFiles ?? [], ...ctx.cssFiles ?? []].sort();
6257
+ for (const file of textFiles) {
6258
+ let content;
6259
+ try {
6260
+ content = await readFile18(file, "utf8");
6261
+ } catch {
6262
+ results.push(inspectionFailed(check23.id, `Could not read source file: ${file}`, { file }));
6263
+ continue;
6264
+ }
6265
+ if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
6266
+ continue;
6267
+ let inBlock = false;
6268
+ const lines = content.split(`
6269
+ `);
6270
+ for (let i = 0;i < lines.length; i++) {
6271
+ const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
6272
+ if (line === OPEN_SENTINEL) {
6273
+ inBlock = true;
6274
+ results.push(finding(check23.id, {
6275
+ severity: "error",
6276
+ code: "two-versions-passage",
6277
+ message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
6278
+ file,
6279
+ line: i + 1
6280
+ }));
6281
+ } else if (line === CLOSE_SENTINEL) {
6282
+ if (!inBlock) {
6283
+ results.push(finding(check23.id, {
6284
+ severity: "error",
6285
+ code: "leftover-version-marker",
6286
+ message: "This marker line is left over from combining two versions — delete it.",
6287
+ file,
6288
+ line: i + 1
6289
+ }));
6290
+ }
6291
+ inBlock = false;
6292
+ }
6293
+ }
6294
+ }
6295
+ try {
6296
+ const { glob } = await import("glob");
6297
+ const siblings = await glob(["**/*.online", "**/*.online.*"], {
6298
+ cwd: ctx.inputDir,
6299
+ absolute: true,
6300
+ ignore: [...ASSET_SCAN_IGNORE_GLOBS]
6301
+ });
6302
+ for (const sibling of siblings.sort()) {
6303
+ const rel = path6.relative(ctx.inputDir, sibling).split(path6.sep).join("/");
6304
+ results.push(finding(check23.id, {
6305
+ severity: "warning",
6306
+ code: "kept-both-versions",
6307
+ message: `Two versions of ${withoutOnlineTag(rel)} are in your project — keep the one you want, then delete the other.`,
6308
+ file: sibling
6309
+ }));
6310
+ }
6311
+ } catch (error2) {
6312
+ results.push(inspectionFailed(check23.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
6313
+ }
6314
+ return results;
6315
+ }
6316
+ };
6317
+ registerCheck(check23);
6318
+
6319
+ // src/checks/asset/image-file-size.ts
6320
+ import { stat as stat3 } from "node:fs/promises";
6321
+
6391
6322
  // src/lib/image-inspect.ts
6323
+ import { stat as stat2, readFile as readFile19 } from "node:fs/promises";
6392
6324
  var DEFAULT_DPI = 72;
6393
6325
  var cache = new Map;
6394
- async function inspectImage(path6) {
6326
+ async function inspectImage(path7) {
6395
6327
  let st;
6396
6328
  try {
6397
- st = await stat2(path6);
6329
+ st = await stat2(path7);
6398
6330
  } catch {
6399
6331
  return null;
6400
6332
  }
6401
- const hit = cache.get(path6);
6333
+ const hit = cache.get(path7);
6402
6334
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
6403
6335
  return hit.info;
6404
6336
  let info2 = null;
6405
6337
  try {
6406
- const buf = await readFile18(path6);
6338
+ const buf = await readFile19(path7);
6407
6339
  info2 = parseImage(buf);
6408
6340
  } catch {
6409
6341
  info2 = null;
6410
6342
  }
6411
- cache.set(path6, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
6343
+ cache.set(path7, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
6412
6344
  return info2;
6413
6345
  }
6414
6346
  function parseImage(b) {
@@ -6608,7 +6540,7 @@ async function collectImageFiles(dirs, exts, ignore = ASSET_SCAN_IGNORE_GLOBS) {
6608
6540
  }
6609
6541
 
6610
6542
  // src/checks/asset/image-file-size.ts
6611
- var check23 = {
6543
+ var check24 = {
6612
6544
  id: "asset.image.file-size",
6613
6545
  name: "Image File Size",
6614
6546
  description: "Checks that image files do not exceed the maximum size limit",
@@ -6629,14 +6561,14 @@ var check23 = {
6629
6561
  if (info2.size > maxSize) {
6630
6562
  const sizeMb = (info2.size / 1e6).toFixed(1);
6631
6563
  const maxMb = (maxSize / 1e6).toFixed(1);
6632
- results.push(finding(check23.id, {
6564
+ results.push(finding(check24.id, {
6633
6565
  severity: "warning",
6634
6566
  message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
6635
6567
  file
6636
6568
  }));
6637
6569
  }
6638
6570
  } catch {
6639
- results.push(inspectionFailed(check23.id, `Could not stat image file: ${file}`, {
6571
+ results.push(inspectionFailed(check24.id, `Could not stat image file: ${file}`, {
6640
6572
  file
6641
6573
  }));
6642
6574
  }
@@ -6644,10 +6576,10 @@ var check23 = {
6644
6576
  return results;
6645
6577
  }
6646
6578
  };
6647
- registerCheck(check23);
6579
+ registerCheck(check24);
6648
6580
 
6649
6581
  // src/checks/asset/image-resolution.ts
6650
- var check24 = {
6582
+ var check25 = {
6651
6583
  id: "asset.image.resolution",
6652
6584
  name: "Image Resolution",
6653
6585
  description: "Checks source image DPI from embedded density metadata",
@@ -6669,7 +6601,7 @@ var check24 = {
6669
6601
  const { xDpi, yDpi } = info2;
6670
6602
  if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
6671
6603
  results.push({
6672
- checkId: check24.id,
6604
+ checkId: check25.id,
6673
6605
  severity: "warning",
6674
6606
  message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
6675
6607
  file
@@ -6679,7 +6611,7 @@ var check24 = {
6679
6611
  return results;
6680
6612
  }
6681
6613
  };
6682
- registerCheck(check24);
6614
+ registerCheck(check25);
6683
6615
 
6684
6616
  // src/checks/asset/image-color-space.ts
6685
6617
  var LABEL = {
@@ -6687,7 +6619,7 @@ var LABEL = {
6687
6619
  gray: "Gray",
6688
6620
  cmyk: "CMYK"
6689
6621
  };
6690
- var check25 = {
6622
+ var check26 = {
6691
6623
  id: "asset.image.color-space",
6692
6624
  name: "Image Color Space",
6693
6625
  description: "Validates image color spaces against allowed list",
@@ -6708,7 +6640,7 @@ var check25 = {
6708
6640
  const cs = info2?.colorSpace;
6709
6641
  if (cs && !allowedLower.has(cs)) {
6710
6642
  results.push({
6711
- checkId: check25.id,
6643
+ checkId: check26.id,
6712
6644
  severity: "warning",
6713
6645
  message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
6714
6646
  file
@@ -6718,10 +6650,10 @@ var check25 = {
6718
6650
  return results;
6719
6651
  }
6720
6652
  };
6721
- registerCheck(check25);
6653
+ registerCheck(check26);
6722
6654
 
6723
6655
  // src/checks/asset/image-alpha.ts
6724
- var check26 = {
6656
+ var check27 = {
6725
6657
  id: "asset.image.alpha-channel",
6726
6658
  name: "Image Alpha Channel",
6727
6659
  description: "Checks for alpha channels in PNG/TIFF images",
@@ -6739,7 +6671,7 @@ var check26 = {
6739
6671
  const info2 = await inspectImage(file);
6740
6672
  if (info2?.hasAlpha) {
6741
6673
  results.push({
6742
- checkId: check26.id,
6674
+ checkId: check27.id,
6743
6675
  severity: "warning",
6744
6676
  message: "Image contains alpha channel, which may cause print issues",
6745
6677
  file
@@ -6749,10 +6681,10 @@ var check26 = {
6749
6681
  return results;
6750
6682
  }
6751
6683
  };
6752
- registerCheck(check26);
6684
+ registerCheck(check27);
6753
6685
 
6754
6686
  // src/checks/asset/image-tac.ts
6755
- var check27 = {
6687
+ var check28 = {
6756
6688
  id: "asset.image.tac-raster",
6757
6689
  name: "Image TAC (Raster)",
6758
6690
  description: "Rasterizes and checks TAC per image using Ghostscript",
@@ -6771,7 +6703,7 @@ var check27 = {
6771
6703
  const ghostscript = await resolveGhostscript();
6772
6704
  if (!ghostscript) {
6773
6705
  return [
6774
- inspectionFailed(check27.id, "Could not inspect image ink coverage: Ghostscript executable not found")
6706
+ inspectionFailed(check28.id, "Could not inspect image ink coverage: Ghostscript executable not found")
6775
6707
  ];
6776
6708
  }
6777
6709
  for (const file of files) {
@@ -6789,7 +6721,7 @@ var check27 = {
6789
6721
  if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
6790
6722
  const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
6791
6723
  if (tac > maxTac) {
6792
- results.push(finding(check27.id, {
6724
+ results.push(finding(check28.id, {
6793
6725
  severity: "warning",
6794
6726
  message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
6795
6727
  file,
@@ -6801,16 +6733,16 @@ var check27 = {
6801
6733
  }
6802
6734
  }
6803
6735
  } catch {
6804
- results.push(inspectionFailed(check27.id, `Could not inspect image ink coverage: ${file}`, { file }));
6736
+ results.push(inspectionFailed(check28.id, `Could not inspect image ink coverage: ${file}`, { file }));
6805
6737
  }
6806
6738
  }
6807
6739
  return results;
6808
6740
  }
6809
6741
  };
6810
- registerCheck(check27);
6742
+ registerCheck(check28);
6811
6743
 
6812
6744
  // src/checks/asset/approved-fonts.ts
6813
- var check28 = {
6745
+ var check29 = {
6814
6746
  id: "asset.font.approved-files",
6815
6747
  name: "Approved Font Files",
6816
6748
  description: "Checks font files against the approved file patterns",
@@ -6849,7 +6781,7 @@ var check28 = {
6849
6781
  for (const font of allFonts) {
6850
6782
  if (!approvedFonts.has(font)) {
6851
6783
  results.push({
6852
- checkId: check28.id,
6784
+ checkId: check29.id,
6853
6785
  severity: "warning",
6854
6786
  message: "Font file not in approved list",
6855
6787
  file: font
@@ -6859,7 +6791,7 @@ var check28 = {
6859
6791
  return results;
6860
6792
  }
6861
6793
  };
6862
- registerCheck(check28);
6794
+ registerCheck(check29);
6863
6795
 
6864
6796
  // src/checks/asset/font-license.ts
6865
6797
  import { existsSync as existsSync8 } from "node:fs";
@@ -6874,7 +6806,7 @@ var LICENSE_NAMES = [
6874
6806
  "OFL-1.1.txt",
6875
6807
  "COPYING"
6876
6808
  ];
6877
- var check29 = {
6809
+ var check30 = {
6878
6810
  id: "asset.font.license",
6879
6811
  name: "Font License",
6880
6812
  description: "Checks for license files in font directories",
@@ -6901,7 +6833,7 @@ var check29 = {
6901
6833
  const hasLicense = LICENSE_NAMES.some((name) => existsSync8(resolve7(fontDir, name)));
6902
6834
  if (!hasLicense) {
6903
6835
  results.push({
6904
- checkId: check29.id,
6836
+ checkId: check30.id,
6905
6837
  severity: "warning",
6906
6838
  message: `No font license file found in directory`,
6907
6839
  file: fontDir
@@ -6911,10 +6843,10 @@ var check29 = {
6911
6843
  return results;
6912
6844
  }
6913
6845
  };
6914
- registerCheck(check29);
6846
+ registerCheck(check30);
6915
6847
 
6916
6848
  // src/checks/heuristic/text-density.ts
6917
- var check30 = {
6849
+ var check31 = {
6918
6850
  id: "heuristic.whitespace.text-density",
6919
6851
  name: "Text Density",
6920
6852
  description: "Checks characters-per-page ratio",
@@ -6943,7 +6875,7 @@ var check30 = {
6943
6875
  });
6944
6876
  if (lowPages.length > 0) {
6945
6877
  results.push({
6946
- checkId: check30.id,
6878
+ checkId: check31.id,
6947
6879
  severity: "info",
6948
6880
  message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
6949
6881
  file: ctx.pdfPath
@@ -6951,7 +6883,7 @@ var check30 = {
6951
6883
  }
6952
6884
  if (highPages.length > 0) {
6953
6885
  results.push({
6954
- checkId: check30.id,
6886
+ checkId: check31.id,
6955
6887
  severity: "info",
6956
6888
  message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
6957
6889
  file: ctx.pdfPath
@@ -6960,11 +6892,11 @@ var check30 = {
6960
6892
  return results;
6961
6893
  }
6962
6894
  };
6963
- registerCheck(check30);
6895
+ registerCheck(check31);
6964
6896
 
6965
6897
  // src/checks/heuristic/section-density.ts
6966
- import { readFile as readFile19 } from "node:fs/promises";
6967
- var check31 = {
6898
+ import { readFile as readFile20 } from "node:fs/promises";
6899
+ var check32 = {
6968
6900
  id: "heuristic.chunking.section-density",
6969
6901
  name: "Section Density",
6970
6902
  description: "Checks heading/paragraph/callout density from source Markdown",
@@ -6980,7 +6912,7 @@ var check31 = {
6980
6912
  const results = [];
6981
6913
  for (const file of files) {
6982
6914
  try {
6983
- const content = await readFile19(file, "utf8");
6915
+ const content = await readFile20(file, "utf8");
6984
6916
  const lines = content.split(`
6985
6917
  `);
6986
6918
  let currentSectionStart = 0;
@@ -6990,7 +6922,7 @@ var check31 = {
6990
6922
  const line = lines[i];
6991
6923
  if (/^#{1,6}\s/.test(line)) {
6992
6924
  if (paragraphCount > maxParas) {
6993
- results.push(finding(check31.id, {
6925
+ results.push(finding(check32.id, {
6994
6926
  severity: "info",
6995
6927
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
6996
6928
  file,
@@ -7010,7 +6942,7 @@ var check31 = {
7010
6942
  }
7011
6943
  }
7012
6944
  if (paragraphCount > maxParas) {
7013
- results.push(finding(check31.id, {
6945
+ results.push(finding(check32.id, {
7014
6946
  severity: "info",
7015
6947
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
7016
6948
  file,
@@ -7018,7 +6950,7 @@ var check31 = {
7018
6950
  }));
7019
6951
  }
7020
6952
  } catch {
7021
- results.push(inspectionFailed(check31.id, `Could not read source file: ${file}`, {
6953
+ results.push(inspectionFailed(check32.id, `Could not read source file: ${file}`, {
7022
6954
  file
7023
6955
  }));
7024
6956
  }
@@ -7026,10 +6958,10 @@ var check31 = {
7026
6958
  return results;
7027
6959
  }
7028
6960
  };
7029
- registerCheck(check31);
6961
+ registerCheck(check32);
7030
6962
 
7031
6963
  // src/checks/heuristic/layer-count.ts
7032
- var check32 = {
6964
+ var check33 = {
7033
6965
  id: "heuristic.decoration.layer-count",
7034
6966
  name: "Layer Count",
7035
6967
  description: "Counts image objects per page",
@@ -7054,7 +6986,7 @@ var check32 = {
7054
6986
  heavyPages.sort((a, b) => a - b);
7055
6987
  return [
7056
6988
  {
7057
- checkId: check32.id,
6989
+ checkId: check33.id,
7058
6990
  severity: "info",
7059
6991
  message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
7060
6992
  file: ctx.pdfPath
@@ -7064,10 +6996,10 @@ var check32 = {
7064
6996
  return [];
7065
6997
  }
7066
6998
  };
7067
- registerCheck(check32);
6999
+ registerCheck(check33);
7068
7000
 
7069
7001
  // src/checks/heuristic/placement-variance.ts
7070
- var check33 = {
7002
+ var check34 = {
7071
7003
  id: "heuristic.layout.placement-variance",
7072
7004
  name: "Placement Variance",
7073
7005
  description: "Analyzes text baseline coordinates for layout consistency",
@@ -7085,7 +7017,7 @@ var check33 = {
7085
7017
  const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
7086
7018
  return [
7087
7019
  {
7088
- checkId: check33.id,
7020
+ checkId: check34.id,
7089
7021
  severity: "info",
7090
7022
  message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
7091
7023
  file: ctx.pdfPath
@@ -7093,7 +7025,7 @@ var check33 = {
7093
7025
  ];
7094
7026
  }
7095
7027
  };
7096
- registerCheck(check33);
7028
+ registerCheck(check34);
7097
7029
 
7098
7030
  // src/checks/runner.ts
7099
7031
  function getCheckSeverityOverride(checkId, config) {
@@ -7123,25 +7055,25 @@ async function runChecks(ctx, opts = {}) {
7123
7055
  }
7124
7056
  const release = retainPdfCache();
7125
7057
  try {
7126
- for (const check34 of checks2) {
7058
+ for (const check35 of checks2) {
7127
7059
  try {
7128
- const results = await check34.run(ctx);
7129
- const severityOverride = getCheckSeverityOverride(check34.id, ctx.config);
7060
+ const results = await check35.run(ctx);
7061
+ const severityOverride = getCheckSeverityOverride(check35.id, ctx.config);
7130
7062
  if (severityOverride) {
7131
7063
  for (const r of results) {
7132
7064
  r.severity = severityOverride;
7133
7065
  }
7134
7066
  }
7135
7067
  if (results.length === 0) {
7136
- passed.push(check34.id);
7068
+ passed.push(check35.id);
7137
7069
  } else {
7138
7070
  allResults.push(...results);
7139
7071
  }
7140
7072
  } catch (err) {
7141
7073
  allResults.push({
7142
- checkId: check34.id,
7074
+ checkId: check35.id,
7143
7075
  severity: "error",
7144
- message: `Check "${check34.id}" threw: ${err instanceof Error ? err.message : String(err)}`
7076
+ message: `Check "${check35.id}" threw: ${err instanceof Error ? err.message : String(err)}`
7145
7077
  });
7146
7078
  }
7147
7079
  }
@@ -7181,15 +7113,15 @@ function emptyReport() {
7181
7113
  async function checkToolAvailability(config, opts = {}) {
7182
7114
  const { checks: checks2 } = selectChecks(opts, config);
7183
7115
  const toolToChecks = new Map;
7184
- for (const check34 of checks2) {
7185
- if (!check34.requiredTools?.length)
7116
+ for (const check35 of checks2) {
7117
+ if (!check35.requiredTools?.length)
7186
7118
  continue;
7187
- for (const tool of check34.requiredTools) {
7119
+ for (const tool of check35.requiredTools) {
7188
7120
  const existing = toolToChecks.get(tool);
7189
7121
  if (existing) {
7190
- existing.push(check34.id);
7122
+ existing.push(check35.id);
7191
7123
  } else {
7192
- toolToChecks.set(tool, [check34.id]);
7124
+ toolToChecks.set(tool, [check35.id]);
7193
7125
  }
7194
7126
  }
7195
7127
  }
@@ -7205,11 +7137,11 @@ async function checkToolAvailability(config, opts = {}) {
7205
7137
  const missing = results.filter((r) => !r.found).map((r) => r.tool);
7206
7138
  const missingSet = new Set(missing);
7207
7139
  const skippedChecks = [];
7208
- for (const check34 of checks2) {
7209
- if (!check34.requiredTools?.length)
7140
+ for (const check35 of checks2) {
7141
+ if (!check35.requiredTools?.length)
7210
7142
  continue;
7211
- if (check34.requiredTools.some((t) => missingSet.has(t))) {
7212
- skippedChecks.push(check34.id);
7143
+ if (check35.requiredTools.some((t) => missingSet.has(t))) {
7144
+ skippedChecks.push(check35.id);
7213
7145
  }
7214
7146
  }
7215
7147
  return { available, missing, skippedChecks, toolToChecks };
@@ -7612,19 +7544,242 @@ function computeGates(format, opts, config) {
7612
7544
  }
7613
7545
 
7614
7546
  // src/lib/build-staging.ts
7615
- import path6 from "node:path";
7547
+ import path7 from "node:path";
7616
7548
  import os from "node:os";
7617
7549
  import fsp from "node:fs/promises";
7550
+
7551
+ // src/lib/missing-asset-placeholder.ts
7552
+ import { createHash as createHash5 } from "node:crypto";
7553
+ import { deflateSync } from "node:zlib";
7554
+ function placeholderOutputPath(missingOutputPath) {
7555
+ const hash = createHash5("sha256").update(missingOutputPath).digest("hex").slice(0, 16);
7556
+ return `assets/gutterpress-missing/${hash}.png`;
7557
+ }
7558
+ function decodeHtmlAttribute(value) {
7559
+ return value.replace(/&quot;/g, '"').replace(/&#39;|&apos;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&");
7560
+ }
7561
+ function rewriteMissingImageReferences(html, replacements) {
7562
+ if (replacements.size === 0)
7563
+ return html;
7564
+ const replacementFor = (raw) => replacements.get(raw) ?? replacements.get(decodeHtmlAttribute(raw));
7565
+ const cssUrl = /url\(\s*(?:"([^"]*)"|'([^']*)'|&quot;((?:(?!&quot;).)*)&quot;|([^'"()\s]*))\s*\)/giy;
7566
+ const rewriteCssUrls = (css) => {
7567
+ let out2 = "";
7568
+ let copiedThrough = 0;
7569
+ let index = 0;
7570
+ while (index < css.length) {
7571
+ if (css.startsWith("/*", index)) {
7572
+ const end = css.indexOf("*/", index + 2);
7573
+ index = end < 0 ? css.length : end + 2;
7574
+ continue;
7575
+ }
7576
+ const char = css[index];
7577
+ if (char === '"' || char === "'") {
7578
+ const quote = char;
7579
+ index++;
7580
+ while (index < css.length) {
7581
+ if (css[index] === "\\") {
7582
+ index += 2;
7583
+ continue;
7584
+ }
7585
+ const current = css[index++];
7586
+ if (current === quote)
7587
+ break;
7588
+ }
7589
+ continue;
7590
+ }
7591
+ const previous = index > 0 ? css[index - 1] : "";
7592
+ if (!/[a-z0-9_-]/i.test(previous) && css.slice(index, index + 3).toLowerCase() === "url") {
7593
+ cssUrl.lastIndex = index;
7594
+ const match = cssUrl.exec(css);
7595
+ if (match) {
7596
+ const raw = match[1] ?? match[2] ?? match[3] ?? match[4] ?? "";
7597
+ const replacement = replacementFor(raw);
7598
+ if (replacement) {
7599
+ out2 += css.slice(copiedThrough, index);
7600
+ out2 += match[3] !== undefined ? `url(&quot;${replacement}&quot;)` : `url("${replacement}")`;
7601
+ index = cssUrl.lastIndex;
7602
+ copiedThrough = index;
7603
+ continue;
7604
+ }
7605
+ }
7606
+ }
7607
+ index++;
7608
+ }
7609
+ return copiedThrough === 0 ? css : out2 + css.slice(copiedThrough);
7610
+ };
7611
+ const rewriteTag = (tag) => {
7612
+ let out2 = tag;
7613
+ if (/^<img\b/i.test(tag)) {
7614
+ out2 = out2.replace(/(\s+src\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
7615
+ const raw = double ?? single ?? bare ?? "";
7616
+ const replacement = replacementFor(raw);
7617
+ if (!replacement)
7618
+ return whole;
7619
+ if (double !== undefined)
7620
+ return `${prefix}"${replacement}"`;
7621
+ if (single !== undefined)
7622
+ return `${prefix}'${replacement}'`;
7623
+ return `${prefix}${replacement}`;
7624
+ });
7625
+ }
7626
+ if (/^<(?:img|source)\b/i.test(tag)) {
7627
+ out2 = out2.replace(/(\s+srcset\s*=\s*)(?:"([^"]*)"|'([^']*)'|([^\s>]+))/gi, (whole, prefix, double, single, bare) => {
7628
+ const value = double ?? single ?? bare ?? "";
7629
+ let rewritten = "";
7630
+ let copiedThrough = 0;
7631
+ let changed = false;
7632
+ for (const candidate of parseSrcsetUrlCandidates(value)) {
7633
+ const replacement = replacementFor(candidate.url);
7634
+ if (!replacement)
7635
+ continue;
7636
+ rewritten += value.slice(copiedThrough, candidate.start) + replacement;
7637
+ copiedThrough = candidate.end;
7638
+ changed = true;
7639
+ }
7640
+ if (!changed)
7641
+ return whole;
7642
+ rewritten += value.slice(copiedThrough);
7643
+ if (double !== undefined)
7644
+ return `${prefix}"${rewritten}"`;
7645
+ if (single !== undefined)
7646
+ return `${prefix}'${rewritten}'`;
7647
+ return `${prefix}${rewritten}`;
7648
+ });
7649
+ }
7650
+ out2 = out2.replace(/(\s+style\s*=\s*)(["'])(.*?)\2/gi, (whole, prefix, quote, value) => {
7651
+ const rewritten = rewriteCssUrls(value);
7652
+ return rewritten === value ? whole : `${prefix}${quote}${rewritten}${quote}`;
7653
+ });
7654
+ return out2;
7655
+ };
7656
+ const rewriteActiveHtml = (active) => active.replace(/<(?:"[^"]*"|'[^']*'|[^'">])*>/g, (tag) => rewriteTag(tag));
7657
+ const protectedRegion = /<!--[\s\S]*?-->|<(script|style|pre|code|textarea)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
7658
+ let out = "";
7659
+ let last = 0;
7660
+ for (const match of html.matchAll(protectedRegion)) {
7661
+ out += rewriteActiveHtml(html.slice(last, match.index));
7662
+ if (match[1]?.toLowerCase() === "style") {
7663
+ const style = /^(<style\b[^>]*>)([\s\S]*)(<\/style\s*>)$/i.exec(match[0]);
7664
+ out += style ? `${rewriteTag(style[1])}${rewriteCssUrls(style[2])}${style[3]}` : match[0];
7665
+ } else {
7666
+ out += match[0];
7667
+ }
7668
+ last = (match.index ?? 0) + match[0].length;
7669
+ }
7670
+ out += rewriteActiveHtml(html.slice(last));
7671
+ return out;
7672
+ }
7673
+ function crc32(buf) {
7674
+ let c = ~0;
7675
+ for (let i = 0;i < buf.length; i++) {
7676
+ c ^= buf[i];
7677
+ for (let k = 0;k < 8; k++)
7678
+ c = c >>> 1 ^ 3988292384 & -(c & 1);
7679
+ }
7680
+ return ~c >>> 0;
7681
+ }
7682
+ function chunk(type, data) {
7683
+ const out = new Uint8Array(data.length + 12);
7684
+ const view = new DataView(out.buffer);
7685
+ view.setUint32(0, data.length);
7686
+ for (let i = 0;i < 4; i++)
7687
+ out[4 + i] = type.charCodeAt(i);
7688
+ out.set(data, 8);
7689
+ view.setUint32(data.length + 8, crc32(out.subarray(4, data.length + 8)));
7690
+ return out;
7691
+ }
7692
+ function placeholderPng(width = 640, height = 480, cell = 32) {
7693
+ const A = [217, 70, 239];
7694
+ const B = [26, 26, 26];
7695
+ const raw = new Uint8Array(height * (1 + width * 3));
7696
+ let p = 0;
7697
+ for (let y = 0;y < height; y++) {
7698
+ raw[p++] = 0;
7699
+ for (let x = 0;x < width; x++) {
7700
+ const c = ((x / cell | 0) + (y / cell | 0)) % 2 === 0 ? A : B;
7701
+ raw[p++] = c[0];
7702
+ raw[p++] = c[1];
7703
+ raw[p++] = c[2];
7704
+ }
7705
+ }
7706
+ const ihdr = new Uint8Array(13);
7707
+ const hv = new DataView(ihdr.buffer);
7708
+ hv.setUint32(0, width);
7709
+ hv.setUint32(4, height);
7710
+ ihdr[8] = 8;
7711
+ ihdr[9] = 2;
7712
+ const parts = [
7713
+ new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
7714
+ chunk("IHDR", ihdr),
7715
+ chunk("IDAT", new Uint8Array(deflateSync(raw))),
7716
+ chunk("IEND", new Uint8Array(0))
7717
+ ];
7718
+ const total = parts.reduce((n, x) => n + x.length, 0);
7719
+ const png = new Uint8Array(total);
7720
+ let o = 0;
7721
+ for (const part of parts) {
7722
+ png.set(part, o);
7723
+ o += part.length;
7724
+ }
7725
+ return png;
7726
+ }
7727
+
7728
+ // src/lib/build-staging.ts
7618
7729
  async function shipViewerHtml(htmlFile, outDir) {
7619
- await fsp.mkdir(path6.join(outDir, "engine"), { recursive: true });
7620
- await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path6.join(outDir, "engine/gutterpress-viewer.js"));
7730
+ await fsp.mkdir(path7.join(outDir, "engine"), { recursive: true });
7731
+ await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path7.join(outDir, "engine/gutterpress-viewer.js"));
7621
7732
  const tag = ` <script src="engine/gutterpress-viewer.js"></script>
7622
7733
  `;
7623
7734
  const html = await fsp.readFile(htmlFile, "utf-8");
7624
7735
  await fsp.writeFile(htmlFile, /<\/head>/i.test(html) ? html.replace(/<\/head>/i, tag + "</head>") : tag + html, "utf-8");
7625
7736
  }
7737
+ async function stageBookAssets(options) {
7738
+ const { renderDir, outDir, htmlFile, imageRefs, cssAssets, onPlan } = options;
7739
+ const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
7740
+ const copies = [...cssAssets, ...imageCopies];
7741
+ onPlan?.({ unresolved: errors, copyCount: copies.length });
7742
+ const missingPlaceholders = copies.length ? await copyReferencedAssets(copies, outDir) : new Map;
7743
+ let staged = await fsp.readFile(htmlFile, "utf8");
7744
+ if (missingPlaceholders.size > 0) {
7745
+ const rewrites = new Map(missingPlaceholders);
7746
+ for (const [ref, dest] of destinations) {
7747
+ const placeholder = missingPlaceholders.get(dest);
7748
+ if (placeholder)
7749
+ rewrites.set(ref, placeholder);
7750
+ }
7751
+ staged = rewriteMissingImageReferences(staged, rewrites);
7752
+ await fsp.writeFile(htmlFile, staged, "utf8");
7753
+ }
7754
+ if (staged.includes("--gp-shape:")) {
7755
+ await fsp.writeFile(htmlFile, await inlineShapeUrls(staged, outDir), "utf8");
7756
+ }
7757
+ return { missing: [...missingPlaceholders.keys()].sort() };
7758
+ }
7759
+ async function copyReferencedAssets(copies, outDir) {
7760
+ const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
7761
+ await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
7762
+ const missing = new Map;
7763
+ await Promise.all(copies.map(async (c) => {
7764
+ const dest = path7.resolve(outDir, c.to);
7765
+ try {
7766
+ await fsp.copyFile(c.from, dest);
7767
+ } catch (err) {
7768
+ if (err?.code === "ENOENT") {
7769
+ const placeholder = placeholderOutputPath(c.to);
7770
+ const placeholderDest = path7.resolve(outDir, placeholder);
7771
+ await fsp.mkdir(path7.dirname(placeholderDest), { recursive: true });
7772
+ await fsp.writeFile(placeholderDest, placeholderPng());
7773
+ missing.set(c.to, placeholder);
7774
+ return;
7775
+ }
7776
+ throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
7777
+ }
7778
+ }));
7779
+ return missing;
7780
+ }
7626
7781
  async function createStageRoot() {
7627
- return fsp.mkdtemp(path6.join(os.tmpdir(), "gutterpress-stage-"));
7782
+ return fsp.mkdtemp(path7.join(os.tmpdir(), "gutterpress-stage-"));
7628
7783
  }
7629
7784
 
7630
7785
  // src/lib/build-runner.ts
@@ -7632,18 +7787,18 @@ function splitOutPath(outArg, format) {
7632
7787
  if (typeof outArg !== "string" || outArg.length === 0) {
7633
7788
  return { outDir: undefined, pdfFileOverride: null };
7634
7789
  }
7635
- const resolved = path7.resolve(outArg);
7790
+ const resolved = path8.resolve(outArg);
7636
7791
  if ((format === "pdf" || format === "pdfx") && resolved.toLowerCase().endsWith(".pdf")) {
7637
- return { outDir: path7.dirname(resolved), pdfFileOverride: resolved };
7792
+ return { outDir: path8.dirname(resolved), pdfFileOverride: resolved };
7638
7793
  }
7639
7794
  return { outDir: resolved, pdfFileOverride: null };
7640
7795
  }
7641
7796
  function layoutWarningKey(file, line, message) {
7642
- return `${path7.resolve(file)}\x00${line ?? 0}\x00${message}`;
7797
+ return `${path8.resolve(file)}\x00${line ?? 0}\x00${message}`;
7643
7798
  }
7644
7799
  async function resolveBuildContext(opts) {
7645
7800
  const { format } = opts;
7646
- const inputDir = path7.resolve(opts.inputDir);
7801
+ const inputDir = path8.resolve(opts.inputDir);
7647
7802
  const { manifest, manifestDir, manifestPath } = await loadManifestWithPath(opts.manifestPath ?? inputDir, { explicit: opts.manifestPath !== undefined });
7648
7803
  if (manifestPath === null) {
7649
7804
  throw new UsageError(`No project manifest found in ${inputDir}. Looked for ${MANIFEST_FILENAMES.join(" or ")}. ` + "Run from your project folder or pass that folder with `gutterpress build <project-dir>`. " + "For a custom manifest filename, pass `--manifest <path>`.");
@@ -7658,10 +7813,10 @@ async function resolveBuildContext(opts) {
7658
7813
  pdfx: pdfxConfigOverride,
7659
7814
  engine: opts.engine
7660
7815
  }, manifest);
7661
- const target = opts.pdfFileOverride ? { kind: "file", file: path7.resolve(opts.pdfFileOverride) } : opts.outDir ? { kind: "directory", dir: opts.outDir } : { kind: "project", dir: resolveOutputDir(manifestDir, config.title) };
7662
- const outDir = target.kind === "file" ? path7.dirname(target.file) : target.dir;
7816
+ const target = opts.pdfFileOverride ? { kind: "file", file: path8.resolve(opts.pdfFileOverride) } : opts.outDir ? { kind: "directory", dir: opts.outDir } : { kind: "project", dir: resolveOutputDir(manifestDir, config.title) };
7817
+ const outDir = target.kind === "file" ? path8.dirname(target.file) : target.dir;
7663
7818
  const gates = computeGates(format, opts, config);
7664
- const workDir = target.kind === "project" ? path7.join(path7.dirname(target.dir), `.${path7.basename(target.dir)}-build-${randomBytes(6).toString("hex")}`) : path7.join(os2.tmpdir(), `gutterpress-build-${randomBytes(6).toString("hex")}`);
7819
+ const workDir = target.kind === "project" ? path8.join(path8.dirname(target.dir), `.${path8.basename(target.dir)}-build-${randomBytes(6).toString("hex")}`) : path8.join(os2.tmpdir(), `gutterpress-build-${randomBytes(6).toString("hex")}`);
7665
7820
  return {
7666
7821
  opts,
7667
7822
  format,
@@ -7728,79 +7883,47 @@ async function renderBook(ctx) {
7728
7883
  pluginCss,
7729
7884
  onChapterWarnings: (file, warnings) => {
7730
7885
  for (const w of warnings) {
7731
- const key = layoutWarningKey(path7.resolve(renderDir, file), w.line, w.message);
7886
+ const key = layoutWarningKey(path8.resolve(renderDir, file), w.line, w.message);
7732
7887
  if (!ctx.prevalidatedLayoutWarningKeys.has(key)) {
7733
7888
  log.warn(` ${file}, line ${w.line}: ${w.message}`);
7734
7889
  }
7735
7890
  }
7736
7891
  },
7737
7892
  onImageRefs: (refs) => imageRefs.push(...refs),
7738
- onCssAssets: (copies2) => cssAssets.push(...copies2),
7893
+ onCssAssets: (copies) => cssAssets.push(...copies),
7739
7894
  onStyleWarnings: (warnings) => {
7740
7895
  for (const w of warnings)
7741
7896
  log.warn(` ${w}`);
7742
7897
  }
7743
7898
  });
7744
7899
  log.success(`Wrote ${htmlFile}`);
7745
- const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
7746
- if (errors.length > 0) {
7747
- throw new BuildError(`Cannot resolve ${errors.length} image reference(s):
7748
- ` + errors.map((e) => ` - ${e}`).join(`
7900
+ const { missing } = await stageBookAssets({
7901
+ renderDir,
7902
+ outDir: workDir,
7903
+ htmlFile,
7904
+ imageRefs,
7905
+ cssAssets,
7906
+ onPlan: ({ unresolved, copyCount }) => {
7907
+ if (unresolved.length > 0) {
7908
+ throw new BuildError(`Cannot resolve ${unresolved.length} image reference(s):
7909
+ ` + unresolved.map((e) => ` - ${e}`).join(`
7749
7910
  `), 1);
7750
- }
7751
- const copies = [...cssAssets, ...imageCopies];
7752
- let missingPlaceholders = new Map;
7753
- if (copies.length > 0) {
7754
- log.info(`Copying ${copies.length} referenced asset(s)`);
7755
- missingPlaceholders = await copyReferencedAssets(copies, workDir);
7756
- }
7757
- let staged = await fsp2.readFile(htmlFile, "utf8");
7758
- if (missingPlaceholders.size > 0) {
7759
- const rewrites = new Map(missingPlaceholders);
7760
- for (const [ref, dest] of destinations) {
7761
- const placeholder = missingPlaceholders.get(dest);
7762
- if (placeholder)
7763
- rewrites.set(ref, placeholder);
7764
- }
7765
- staged = rewriteMissingImageReferences(staged, rewrites);
7766
- await fsp2.writeFile(htmlFile, staged, "utf8");
7767
- }
7768
- if (staged.includes("--gp-shape:")) {
7769
- await fsp2.writeFile(htmlFile, await inlineShapeUrls(staged, workDir), "utf8");
7770
- }
7771
- return htmlFile;
7772
- }
7773
- async function copyReferencedAssets(copies, outDir) {
7774
- const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
7775
- await Promise.all([...dirs].map((d) => fsp2.mkdir(d, { recursive: true })));
7776
- const missing = new Map;
7777
- await Promise.all(copies.map(async (c) => {
7778
- const dest = path7.resolve(outDir, c.to);
7779
- try {
7780
- await fsp2.copyFile(c.from, dest);
7781
- } catch (err) {
7782
- if (err?.code === "ENOENT") {
7783
- const placeholder = placeholderOutputPath(c.to);
7784
- const placeholderDest = path7.resolve(outDir, placeholder);
7785
- await fsp2.mkdir(path7.dirname(placeholderDest), { recursive: true });
7786
- await fsp2.writeFile(placeholderDest, placeholderPng());
7787
- missing.set(c.to, placeholder);
7788
- return;
7789
7911
  }
7790
- throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
7912
+ if (copyCount > 0)
7913
+ log.info(`Copying ${copyCount} referenced asset(s)`);
7791
7914
  }
7792
- }));
7793
- if (missing.size > 0) {
7794
- log.warn(`${missing.size} referenced image(s) do not exist — a magenta placeholder ` + `was substituted so the build could finish. Each one is a visible hole ` + `in the PDF:`);
7795
- for (const m of [...missing.keys()].sort())
7915
+ });
7916
+ if (missing.length > 0) {
7917
+ log.warn(`${missing.length} referenced image(s) do not exist — a magenta placeholder ` + `was substituted so the build could finish. Each one is a visible hole ` + `in the PDF:`);
7918
+ for (const m of missing)
7796
7919
  log.warn(` missing: ${m}`);
7797
7920
  }
7798
- return missing;
7921
+ return htmlFile;
7799
7922
  }
7800
7923
  async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
7801
- const iccCandidates = path7.isAbsolute(icc) ? [icc] : [path7.resolve(manifestDir, icc), path7.resolve(icc)];
7924
+ const iccCandidates = path8.isAbsolute(icc) ? [icc] : [path8.resolve(manifestDir, icc), path8.resolve(icc)];
7802
7925
  let effectiveIccPath = iccCandidates.find((candidate) => fs2.existsSync(candidate)) ?? null;
7803
- if (!effectiveIccPath && !explicitIccPath && path7.basename(icc) === "CGATS21_CRPC1.icc") {
7926
+ if (!effectiveIccPath && !explicitIccPath && path8.basename(icc) === "CGATS21_CRPC1.icc") {
7804
7927
  effectiveIccPath = await getAssetPath("profiles/CGATS21_CRPC1.icc");
7805
7928
  }
7806
7929
  if (!effectiveIccPath) {
@@ -7816,13 +7939,13 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
7816
7939
  });
7817
7940
  await publishBuild(ctx, artifactName2);
7818
7941
  const delivered = ctx.target.kind !== "file";
7819
- const fingerprintPath = delivered ? path7.join(ctx.outDir, path7.basename(workFingerprint)) : null;
7942
+ const fingerprintPath = delivered ? path8.join(ctx.outDir, path8.basename(workFingerprint)) : null;
7820
7943
  log.success(wroteMessage);
7821
7944
  if (fingerprintPath)
7822
7945
  log.info(`Fingerprint: ${fingerprintPath}`);
7823
7946
  return {
7824
7947
  outDir: ctx.outDir,
7825
- htmlPath: delivered ? path7.join(ctx.outDir, path7.relative(ctx.workDir, paths.htmlPath)) : null,
7948
+ htmlPath: delivered ? path8.join(ctx.outDir, path8.relative(ctx.workDir, paths.htmlPath)) : null,
7826
7949
  pdfPath: paths.pdfPath,
7827
7950
  fingerprintPath,
7828
7951
  diagnostics
@@ -7833,8 +7956,8 @@ async function publishBuild(ctx, artifactName2) {
7833
7956
  if (target.kind === "file") {
7834
7957
  if (!artifactName2)
7835
7958
  throw new BuildError("No artifact to write for this format", 1);
7836
- await fsp2.mkdir(path7.dirname(target.file), { recursive: true });
7837
- await fsp2.copyFile(path7.join(workDir, artifactName2), target.file);
7959
+ await fsp2.mkdir(path8.dirname(target.file), { recursive: true });
7960
+ await fsp2.copyFile(path8.join(workDir, artifactName2), target.file);
7838
7961
  return;
7839
7962
  }
7840
7963
  if (target.kind === "directory") {
@@ -7843,12 +7966,12 @@ async function publishBuild(ctx, artifactName2) {
7843
7966
  return;
7844
7967
  }
7845
7968
  for (const name of await listPdfs(target.dir)) {
7846
- if (!fs2.existsSync(path7.join(workDir, name))) {
7847
- await fsp2.rename(path7.join(target.dir, name), path7.join(workDir, name));
7969
+ if (!fs2.existsSync(path8.join(workDir, name))) {
7970
+ await fsp2.rename(path8.join(target.dir, name), path8.join(workDir, name));
7848
7971
  }
7849
7972
  }
7850
7973
  await fsp2.rm(target.dir, { recursive: true, force: true });
7851
- await fsp2.mkdir(path7.dirname(target.dir), { recursive: true });
7974
+ await fsp2.mkdir(path8.dirname(target.dir), { recursive: true });
7852
7975
  await fsp2.rename(workDir, target.dir);
7853
7976
  }
7854
7977
  async function listPdfs(dir) {
@@ -7864,7 +7987,7 @@ class HtmlOutput {
7864
7987
  const { workDir, outDir, inputDir, config, opts } = ctx;
7865
7988
  log.info("Shipping self-contained HTML + viewer bundle (native engine)");
7866
7989
  await shipViewerHtml(htmlFile, workDir);
7867
- await fsp2.writeFile(path7.join(workDir, "index.html"), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=book.html"><title>Gutterpress</title></head><body></body></html>
7990
+ await fsp2.writeFile(path8.join(workDir, "index.html"), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=book.html"><title>Gutterpress</title></head><body></body></html>
7868
7991
  `, "utf-8");
7869
7992
  return finalizeBuild(ctx, {
7870
7993
  command: "build",
@@ -7877,7 +8000,7 @@ class HtmlOutput {
7877
8000
  iccPath: null,
7878
8001
  stripAnnotations: null
7879
8002
  }
7880
- }, `Wrote: ${path7.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
8003
+ }, `Wrote: ${path8.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
7881
8004
  }
7882
8005
  }
7883
8006
 
@@ -7886,14 +8009,14 @@ class PdfOutput {
7886
8009
  const { workDir, outDir, inputDir, config, opts, format, manifestDir, gates } = ctx;
7887
8010
  const pdfxMode = format === "pdfx" ? opts.pdfxFlavor ?? config.pdfx.flavor : undefined;
7888
8011
  const pdfName = artifactName(config.title, pdfxMode ? "pdfx" : "pdf");
7889
- const pdfFile = path7.join(workDir, pdfName);
7890
- const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path7.join(outDir, pdfName);
8012
+ const pdfFile = path8.join(workDir, pdfName);
8013
+ const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path8.join(outDir, pdfName);
7891
8014
  const stage = await createStageRoot();
7892
8015
  try {
7893
- const rawPdf = pdfxMode ? path7.join(stage, "raw.pdf") : path7.resolve(pdfFile);
7894
- await fsp2.mkdir(path7.dirname(path7.resolve(pdfFile)), { recursive: true });
8016
+ const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8017
+ await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
7895
8018
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
7896
- const { buildNativePdf } = await import("./engine-nbfdy0pp.js");
8019
+ const { buildNativePdf } = await import("./engine-0nt35svz.js");
7897
8020
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
7898
8021
  title: config.title,
7899
8022
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -7924,7 +8047,7 @@ class PdfOutput {
7924
8047
  await stripAnnotations(rawPdf, stage);
7925
8048
  }
7926
8049
  log.info(`Converting to CMYK PDF/X (${pdfxMode})`);
7927
- await convertToPdfxCmyk(rawPdf, path7.resolve(pdfFile), {
8050
+ await convertToPdfxCmyk(rawPdf, path8.resolve(pdfFile), {
7928
8051
  iccPath: effectiveIccPath,
7929
8052
  pdfx: pdfxMode,
7930
8053
  title: config.title,
@@ -7984,10 +8107,10 @@ async function runBuild(opts) {
7984
8107
  }
7985
8108
  }
7986
8109
  // src/preview/lifecycle.ts
7987
- import path9 from "path";
8110
+ import path10 from "path";
7988
8111
  import { tmpdir as tmpdir2 } from "os";
7989
8112
  import { randomBytes as randomBytes2 } from "crypto";
7990
- import { readdir as readdir5, readFile as readFile20, writeFile as writeFile5 } from "node:fs/promises";
8113
+ import { readdir as readdir5, readFile as readFile21, writeFile as writeFile5 } from "node:fs/promises";
7991
8114
 
7992
8115
  // src/utils/file-utils.ts
7993
8116
  import { promises as fs3 } from "fs";
@@ -8009,7 +8132,7 @@ async function mkdir4(dirPath) {
8009
8132
  // src/preview/file-watcher.ts
8010
8133
  import { watch } from "chokidar";
8011
8134
  import fsp3 from "node:fs/promises";
8012
- import path8 from "path";
8135
+ import path9 from "path";
8013
8136
  var EMPTY_BOOK_HTML = `<!doctype html>
8014
8137
  <html lang="en">
8015
8138
  <head>
@@ -8062,7 +8185,7 @@ function injectPreviewScripts(html, pageIsolateChapters) {
8062
8185
  async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8063
8186
  if (!inputPath) {
8064
8187
  cssAssets.clear();
8065
- await fsp3.writeFile(path8.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
8188
+ await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
8066
8189
  return;
8067
8190
  }
8068
8191
  const nextAssets = new Map;
@@ -8077,7 +8200,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8077
8200
  cssAssets.clear();
8078
8201
  for (const [to, from] of nextAssets)
8079
8202
  cssAssets.set(to, from);
8080
- await fsp3.writeFile(path8.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
8203
+ await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
8081
8204
  }
8082
8205
  async function renderChapterPreviewHtml(inputPath, file, config) {
8083
8206
  const html = await renderPreviewBook(inputPath, config, {
@@ -8089,12 +8212,12 @@ async function renderChapterPreviewHtml(inputPath, file, config) {
8089
8212
  function describeChanges(changes, inputResolved) {
8090
8213
  const files = [];
8091
8214
  for (const [changedPath, event] of changes) {
8092
- const relative2 = path8.relative(inputResolved, path8.resolve(changedPath));
8093
- if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path8.sep}`) || path8.isAbsolute(relative2))
8215
+ const relative2 = path9.relative(inputResolved, path9.resolve(changedPath));
8216
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path9.sep}`) || path9.isAbsolute(relative2))
8094
8217
  continue;
8095
8218
  files.push({
8096
8219
  relativePath: canonicalChapterId(relative2),
8097
- ext: path8.extname(changedPath).toLowerCase(),
8220
+ ext: path9.extname(changedPath).toLowerCase(),
8098
8221
  event
8099
8222
  });
8100
8223
  }
@@ -8146,23 +8269,23 @@ function isIgnoredWatchPath(candidatePath, projectRoot) {
8146
8269
  return isDotPathUnderRoot(candidatePath, projectRoot) || isGeneratedProjectPath(relative2);
8147
8270
  }
8148
8271
  function isExternalWatchCandidate(candidatePath, targets) {
8149
- const candidate = path8.resolve(candidatePath);
8272
+ const candidate = path9.resolve(candidatePath);
8150
8273
  for (const rawTarget of targets) {
8151
- const target = path8.resolve(rawTarget);
8152
- const rel = path8.relative(candidate, target);
8153
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel))
8274
+ const target = path9.resolve(rawTarget);
8275
+ const rel = path9.relative(candidate, target);
8276
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
8154
8277
  return true;
8155
8278
  }
8156
8279
  return false;
8157
8280
  }
8158
8281
  async function nearestExistingDirectory(start) {
8159
- let current = path8.resolve(start);
8282
+ let current = path9.resolve(start);
8160
8283
  while (true) {
8161
8284
  try {
8162
8285
  if ((await fsp3.stat(current)).isDirectory())
8163
8286
  return current;
8164
8287
  } catch {}
8165
- const parent = path8.dirname(current);
8288
+ const parent = path9.dirname(current);
8166
8289
  if (parent === current)
8167
8290
  return current;
8168
8291
  current = parent;
@@ -8171,12 +8294,12 @@ async function nearestExistingDirectory(start) {
8171
8294
  async function externalWatchRoots(targets) {
8172
8295
  const roots = new Set;
8173
8296
  for (const target of targets) {
8174
- roots.add(await nearestExistingDirectory(path8.dirname(path8.resolve(target))));
8297
+ roots.add(await nearestExistingDirectory(path9.dirname(path9.resolve(target))));
8175
8298
  }
8176
8299
  return [...roots];
8177
8300
  }
8178
8301
  async function externalWatchTargets(projectDir, config) {
8179
- const root = path8.resolve(projectDir);
8302
+ const root = path9.resolve(projectDir);
8180
8303
  let canonicalRoot = root;
8181
8304
  try {
8182
8305
  canonicalRoot = await fsp3.realpath(root);
@@ -8184,21 +8307,21 @@ async function externalWatchTargets(projectDir, config) {
8184
8307
  const styles = await resolveActiveStyles(root, config.styles);
8185
8308
  const candidates = [
8186
8309
  ...await collectStyleDependencies(root, styles),
8187
- ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path8.resolve(root, p))
8310
+ ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path9.resolve(root, p))
8188
8311
  ];
8189
8312
  const external = new Set;
8190
8313
  for (const abs of candidates) {
8191
- const authoredPath = path8.resolve(abs);
8192
- const authoredRel = path8.relative(root, authoredPath);
8193
- if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path8.sep}`) || path8.isAbsolute(authoredRel))) {
8314
+ const authoredPath = path9.resolve(abs);
8315
+ const authoredRel = path9.relative(root, authoredPath);
8316
+ if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path9.sep}`) || path9.isAbsolute(authoredRel))) {
8194
8317
  external.add(authoredPath);
8195
8318
  }
8196
8319
  let canonicalPath = authoredPath;
8197
8320
  try {
8198
8321
  canonicalPath = await fsp3.realpath(canonicalPath);
8199
8322
  } catch {}
8200
- const rel = path8.relative(canonicalRoot, canonicalPath);
8201
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel))
8323
+ const rel = path9.relative(canonicalRoot, canonicalPath);
8324
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
8202
8325
  continue;
8203
8326
  external.add(canonicalPath);
8204
8327
  }
@@ -8225,7 +8348,7 @@ async function addAndAwaitWatch(watcher, roots) {
8225
8348
  }
8226
8349
  }
8227
8350
  function createFileWatcher(state) {
8228
- const inputResolved = path8.resolve(state.currentInputPath);
8351
+ const inputResolved = path9.resolve(state.currentInputPath);
8229
8352
  const watcher = watch(inputResolved, {
8230
8353
  persistent: true,
8231
8354
  ignoreInitial: true,
@@ -8265,7 +8388,7 @@ function createFileWatcher(state) {
8265
8388
  async function recordChange(event, filePath) {
8266
8389
  if (closed)
8267
8390
  return;
8268
- const target = path8.resolve(filePath);
8391
+ const target = path9.resolve(filePath);
8269
8392
  const settled = settledWrites.get(target);
8270
8393
  if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
8271
8394
  const diskContent = await fsp3.readFile(target, "utf8").catch(() => null);
@@ -8286,7 +8409,7 @@ function createFileWatcher(state) {
8286
8409
  recordChange(event, filePath);
8287
8410
  });
8288
8411
  externalWatcher.on("all", (event, filePath) => {
8289
- const target = path8.resolve(filePath);
8412
+ const target = path9.resolve(filePath);
8290
8413
  if (!desiredExternals.has(target))
8291
8414
  return;
8292
8415
  if (event === "add" && suppressInitialExternalAdds.delete(target))
@@ -8294,8 +8417,8 @@ function createFileWatcher(state) {
8294
8417
  recordChange(event, target);
8295
8418
  });
8296
8419
  function isWatchedSource(target) {
8297
- const rel = path8.relative(inputResolved, target);
8298
- const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel);
8420
+ const rel = path9.relative(inputResolved, target);
8421
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
8299
8422
  if (inProject)
8300
8423
  return !isIgnoredWatchPath(target, inputResolved);
8301
8424
  return desiredExternals.has(target);
@@ -8320,7 +8443,7 @@ function createFileWatcher(state) {
8320
8443
  state.notifySettledWrite = (filePath, writtenContent) => {
8321
8444
  if (closed)
8322
8445
  return;
8323
- const target = path8.resolve(filePath);
8446
+ const target = path9.resolve(filePath);
8324
8447
  if (isWatchedSource(target)) {
8325
8448
  acceptSettledWrite(target, writtenContent);
8326
8449
  return;
@@ -8341,7 +8464,7 @@ function createFileWatcher(state) {
8341
8464
  return;
8342
8465
  let nextTargets;
8343
8466
  try {
8344
- nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path8.resolve(target)));
8467
+ nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path9.resolve(target)));
8345
8468
  } catch (err) {
8346
8469
  debug(`Could not resolve external watch targets: ${err}`);
8347
8470
  return;
@@ -8473,7 +8596,7 @@ async function stopFileWatcher(state) {
8473
8596
  }
8474
8597
 
8475
8598
  // src/preview/lifecycle.ts
8476
- var TEMP_DIR_BASE = path9.join(tmpdir2(), "gutterpress-preview");
8599
+ var TEMP_DIR_BASE = path10.join(tmpdir2(), "gutterpress-preview");
8477
8600
  var PID_FILE_NAME = ".gutterpress.pid";
8478
8601
  function isProcessAlive(pid) {
8479
8602
  if (!Number.isFinite(pid) || pid <= 0)
@@ -8493,10 +8616,10 @@ async function cleanupOrphanTempDirs() {
8493
8616
  return;
8494
8617
  }
8495
8618
  for (const entry of entries) {
8496
- const dir = path9.join(TEMP_DIR_BASE, entry);
8497
- const pidFile = path9.join(dir, PID_FILE_NAME);
8619
+ const dir = path10.join(TEMP_DIR_BASE, entry);
8620
+ const pidFile = path10.join(dir, PID_FILE_NAME);
8498
8621
  try {
8499
- const raw = await readFile20(pidFile, "utf8");
8622
+ const raw = await readFile21(pidFile, "utf8");
8500
8623
  const pid = parseInt(raw.trim(), 10);
8501
8624
  if (!isProcessAlive(pid)) {
8502
8625
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -8508,10 +8631,10 @@ async function cleanupOrphanTempDirs() {
8508
8631
  async function initializePreviewDirectories() {
8509
8632
  await cleanupOrphanTempDirs();
8510
8633
  const tempDirSuffix = randomBytes2(8).toString("hex");
8511
- const tempDir = path9.join(TEMP_DIR_BASE, tempDirSuffix);
8634
+ const tempDir = path10.join(TEMP_DIR_BASE, tempDirSuffix);
8512
8635
  await mkdir4(tempDir);
8513
8636
  debug(`Created temporary directory: ${tempDir}`);
8514
- await writeFile5(path9.join(tempDir, PID_FILE_NAME), `${process.pid}
8637
+ await writeFile5(path10.join(tempDir, PID_FILE_NAME), `${process.pid}
8515
8638
  `, "utf8");
8516
8639
  return tempDir;
8517
8640
  }
@@ -8602,8 +8725,8 @@ function createServerState(inputPath, tempDir, config, options) {
8602
8725
  import http from "node:http";
8603
8726
  import net from "node:net";
8604
8727
  import { createHash as createHash6, randomUUID } from "node:crypto";
8605
- import { readFile as readFile21, stat as stat4 } from "node:fs/promises";
8606
- import path11 from "path";
8728
+ import { readFile as readFile22, stat as stat4 } from "node:fs/promises";
8729
+ import path12 from "path";
8607
8730
  import { WebSocket, WebSocketServer } from "ws";
8608
8731
 
8609
8732
  // src/lib/open-path.ts
@@ -8635,7 +8758,7 @@ function openPath(filePath) {
8635
8758
  }
8636
8759
 
8637
8760
  // src/lib/static-serve.ts
8638
- import path10 from "node:path";
8761
+ import path11 from "node:path";
8639
8762
  var STATIC_MIME = {
8640
8763
  ".html": "text/html; charset=utf-8",
8641
8764
  ".htm": "text/html; charset=utf-8",
@@ -8657,9 +8780,9 @@ var STATIC_MIME = {
8657
8780
  ".otf": "font/otf"
8658
8781
  };
8659
8782
  function resolveWithinRoot(relPath, root) {
8660
- const resolvedRoot = path10.resolve(root);
8661
- const candidate = path10.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
8662
- if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path10.sep)) {
8783
+ const resolvedRoot = path11.resolve(root);
8784
+ const candidate = path11.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
8785
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path11.sep)) {
8663
8786
  return null;
8664
8787
  }
8665
8788
  return candidate;
@@ -8912,7 +9035,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8912
9035
  try {
8913
9036
  const s = await stat4(filePath);
8914
9037
  if (s.isDirectory())
8915
- filePath = path11.join(filePath, "index.html");
9038
+ filePath = path12.join(filePath, "index.html");
8916
9039
  } catch {
8917
9040
  res.writeHead(404);
8918
9041
  res.end("Not Found");
@@ -8920,7 +9043,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8920
9043
  }
8921
9044
  let data;
8922
9045
  try {
8923
- data = await readFile21(filePath);
9046
+ data = await readFile22(filePath);
8924
9047
  } catch {
8925
9048
  res.writeHead(404);
8926
9049
  res.end("Not Found");
@@ -8933,7 +9056,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8933
9056
  res.end(withHmr);
8934
9057
  return;
8935
9058
  }
8936
- const ct = STATIC_MIME[path11.extname(filePath).toLowerCase()] ?? "application/octet-stream";
9059
+ const ct = STATIC_MIME[path12.extname(filePath).toLowerCase()] ?? "application/octet-stream";
8937
9060
  res.writeHead(200, { "Content-Type": ct, "Cache-Control": cacheControl, ...extraHeaders });
8938
9061
  res.end(data);
8939
9062
  }
@@ -8943,7 +9066,7 @@ var EMBEDDED_CACHE_CONTROL = "public, no-cache";
8943
9066
  var EMBEDDED_ETAG = `"gutterpress-${PACKAGE_VERSION}"`;
8944
9067
  var PROJECT_ASSET_CACHE_CONTROL = "private, no-cache";
8945
9068
  async function serveRevalidatedStatic(req, absPath, res, validators) {
8946
- const extension = path11.extname(absPath).toLowerCase();
9069
+ const extension = path12.extname(absPath).toLowerCase();
8947
9070
  if (extension === ".html" || extension === ".htm") {
8948
9071
  await serveStatic(absPath, res);
8949
9072
  return;
@@ -8970,7 +9093,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
8970
9093
  let data;
8971
9094
  if (!validator || validator.stamp !== stamp) {
8972
9095
  try {
8973
- data = await readFile21(absPath);
9096
+ data = await readFile22(absPath);
8974
9097
  } catch {
8975
9098
  await serveStatic(absPath, res);
8976
9099
  return;
@@ -9109,7 +9232,7 @@ async function createPreviewServer(state, port) {
9109
9232
  const file = rawFile ? canonicalChapterId(rawFile) : "";
9110
9233
  const revision = Number(url.searchParams.get("revision"));
9111
9234
  const configuredFiles = state.config.source?.files;
9112
- if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path11.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
9235
+ if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path12.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
9113
9236
  res.writeHead(400);
9114
9237
  res.end("Bad Request");
9115
9238
  return;
@@ -9311,12 +9434,9 @@ async function startPreviewServer(options) {
9311
9434
  import { platform, arch, release } from "node:os";
9312
9435
 
9313
9436
  // src/lib/remote-auth/token-store.ts
9314
- import { chmod, mkdir as mkdir5, readFile as readFile22, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
9437
+ import { chmod, mkdir as mkdir5, readFile as readFile23, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
9315
9438
  import os3 from "node:os";
9316
- import path12 from "node:path";
9317
- function redactCredential(cred) {
9318
- return { ...cred, token: "•••redacted•••" };
9319
- }
9439
+ import path13 from "node:path";
9320
9440
  function credentialHostKey(hostOrUrl) {
9321
9441
  const trimmed = String(hostOrUrl ?? "").trim();
9322
9442
  if (!trimmed)
@@ -9343,22 +9463,22 @@ function defaultConfigDir() {
9343
9463
  if (override)
9344
9464
  return override;
9345
9465
  if (process.platform === "win32" && process.env.APPDATA) {
9346
- return path12.join(process.env.APPDATA, "gutterpress");
9466
+ return path13.join(process.env.APPDATA, "gutterpress");
9347
9467
  }
9348
9468
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
9349
- return path12.join(xdg || path12.join(os3.homedir(), ".config"), "gutterpress");
9469
+ return path13.join(xdg || path13.join(os3.homedir(), ".config"), "gutterpress");
9350
9470
  }
9351
9471
 
9352
9472
  class FileTokenStore {
9353
9473
  filePath;
9354
9474
  queue = Promise.resolve();
9355
9475
  constructor(filePath) {
9356
- this.filePath = filePath ?? path12.join(defaultConfigDir(), "credentials.json");
9476
+ this.filePath = filePath ?? path13.join(defaultConfigDir(), "credentials.json");
9357
9477
  }
9358
9478
  async read() {
9359
9479
  let raw;
9360
9480
  try {
9361
- raw = await readFile22(this.filePath, "utf8");
9481
+ raw = await readFile23(this.filePath, "utf8");
9362
9482
  } catch (e) {
9363
9483
  if (e?.code === "ENOENT") {
9364
9484
  return { version: 1, credentials: {} };
@@ -9378,7 +9498,7 @@ class FileTokenStore {
9378
9498
  return { version: 1, credentials: {} };
9379
9499
  }
9380
9500
  async write(data) {
9381
- await mkdir5(path12.dirname(this.filePath), { recursive: true });
9501
+ await mkdir5(path13.dirname(this.filePath), { recursive: true });
9382
9502
  const tmpPath = `${this.filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
9383
9503
  await writeFile6(tmpPath, JSON.stringify(data, null, 2), {
9384
9504
  encoding: "utf8",
@@ -9456,13 +9576,6 @@ function extractUrlCredential(url) {
9456
9576
  };
9457
9577
  return { cleanUrl: parsed.toString(), credential };
9458
9578
  }
9459
- async function migrateUrlCredential(url, store) {
9460
- const { cleanUrl, credential } = extractUrlCredential(url);
9461
- if (credential && !await store.get(credential.host)) {
9462
- await store.set(credential.host, credential);
9463
- }
9464
- return cleanUrl;
9465
- }
9466
9579
 
9467
9580
  // src/lib/diagnostics.ts
9468
9581
  var INSTALL_HINTS3 = {
@@ -9534,18 +9647,18 @@ async function getSystemDiagnostics() {
9534
9647
  installHint: INSTALL_HINTS3[t.hintKey]
9535
9648
  };
9536
9649
  }
9537
- const [path13, available] = await Promise.all([
9650
+ const [path14, available] = await Promise.all([
9538
9651
  findTool(t.bin),
9539
9652
  isToolAvailable(t.bin)
9540
9653
  ]);
9541
- const found = available || !!path13;
9654
+ const found = available || !!path14;
9542
9655
  const version = found ? await getVersion(t.bin) : undefined;
9543
9656
  return {
9544
9657
  id: t.id,
9545
9658
  name: t.name,
9546
9659
  bin: t.bin,
9547
9660
  found,
9548
- path: path13,
9661
+ path: path14,
9549
9662
  version,
9550
9663
  usedBy: t.usedBy,
9551
9664
  installHint: INSTALL_HINTS3[t.hintKey]
@@ -9565,25 +9678,25 @@ async function getSystemDiagnostics() {
9565
9678
  };
9566
9679
  }
9567
9680
  // src/lib/project-scaffold.ts
9568
- import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile24, stat as stat5, writeFile as writeFile7 } from "node:fs/promises";
9681
+ import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile25, stat as stat5, writeFile as writeFile7 } from "node:fs/promises";
9569
9682
  import { constants as FS, existsSync as existsSync11 } from "node:fs";
9570
- import path14 from "node:path";
9683
+ import path15 from "node:path";
9571
9684
 
9572
9685
  // src/lib/manifest-doc.ts
9573
9686
  import { existsSync as existsSync10 } from "node:fs";
9574
9687
  import { randomUUID as randomUUID2 } from "node:crypto";
9575
- import { mkdir as mkdir6, open, readFile as readFile23, rename as rename3, rm as rm2 } from "node:fs/promises";
9576
- import path13 from "node:path";
9688
+ import { mkdir as mkdir6, open, readFile as readFile24, rename as rename3, rm as rm2 } from "node:fs/promises";
9689
+ import path14 from "node:path";
9577
9690
  import { parseDocument, isSeq, YAMLSeq } from "yaml";
9578
9691
  function resolveManifestPath(projectDir) {
9579
- const existing = MANIFEST_FILENAMES.find((name) => existsSync10(path13.join(projectDir, name)));
9580
- return path13.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
9692
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync10(path14.join(projectDir, name)));
9693
+ return path14.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
9581
9694
  }
9582
9695
  async function loadManifestDoc(projectDir) {
9583
9696
  const file = resolveManifestPath(projectDir);
9584
9697
  let text = "";
9585
9698
  try {
9586
- text = await readFile23(file, "utf8");
9699
+ text = await readFile24(file, "utf8");
9587
9700
  } catch (error2) {
9588
9701
  if (error2.code !== "ENOENT")
9589
9702
  throw error2;
@@ -9591,9 +9704,9 @@ async function loadManifestDoc(projectDir) {
9591
9704
  return { doc: parseDocument(text), file };
9592
9705
  }
9593
9706
  async function writeManifestDoc(file, doc) {
9594
- const dir = path13.dirname(file);
9707
+ const dir = path14.dirname(file);
9595
9708
  await mkdir6(dir, { recursive: true });
9596
- const temporary = path13.join(dir, `.${path13.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
9709
+ const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
9597
9710
  try {
9598
9711
  const handle = await open(temporary, "wx", 438);
9599
9712
  try {
@@ -9695,7 +9808,7 @@ async function scaffoldProject(options) {
9695
9808
  throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
9696
9809
  }
9697
9810
  const parentDir = options.parentDir;
9698
- if (!parentDir || !path14.isAbsolute(parentDir)) {
9811
+ if (!parentDir || !path15.isAbsolute(parentDir)) {
9699
9812
  throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
9700
9813
  }
9701
9814
  try {
@@ -9703,7 +9816,7 @@ async function scaffoldProject(options) {
9703
9816
  } catch {
9704
9817
  throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
9705
9818
  }
9706
- const projectDir = path14.join(parentDir, folderName);
9819
+ const projectDir = path15.join(parentDir, folderName);
9707
9820
  let targetExists = true;
9708
9821
  try {
9709
9822
  await access(projectDir, FS.F_OK);
@@ -9724,19 +9837,19 @@ async function scaffoldProject(options) {
9724
9837
  for (const entry of entries) {
9725
9838
  if (entry.name === ".gutterpress-template.json")
9726
9839
  continue;
9727
- await cp2(path14.join(customTemplateDir, entry.name), path14.join(projectDir, entry.name), { recursive: true });
9840
+ await cp2(path15.join(customTemplateDir, entry.name), path15.join(projectDir, entry.name), { recursive: true });
9728
9841
  }
9729
- await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
9842
+ await mkdir7(path15.join(projectDir, "assets"), { recursive: true });
9730
9843
  } else {
9731
9844
  const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
9732
9845
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
9733
- await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
9734
- await copyFile(tplManifest, path14.join(projectDir, MANIFEST_FILENAMES[0]));
9735
- await copyFile(tplChapter, path14.join(projectDir, "chapter-01.md"));
9846
+ await mkdir7(path15.join(projectDir, "assets"), { recursive: true });
9847
+ await copyFile(tplManifest, path15.join(projectDir, MANIFEST_FILENAMES[0]));
9848
+ await copyFile(tplChapter, path15.join(projectDir, "chapter-01.md"));
9736
9849
  const starterThemeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
9737
9850
  const starterCss = await getAssetPath(`themes/${starterThemeId}/theme.css`);
9738
- await mkdir7(path14.join(projectDir, "styles"), { recursive: true });
9739
- await copyFile(starterCss, path14.join(projectDir, "styles", "book.css"));
9851
+ await mkdir7(path15.join(projectDir, "styles"), { recursive: true });
9852
+ await copyFile(starterCss, path15.join(projectDir, "styles", "book.css"));
9740
9853
  }
9741
9854
  } catch (e) {
9742
9855
  throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -9746,13 +9859,13 @@ async function scaffoldProject(options) {
9746
9859
  } catch (e) {
9747
9860
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
9748
9861
  }
9749
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path14.join(projectDir, name2)).find(existsSync11) ?? path14.join(projectDir, MANIFEST_FILENAMES[0]);
9862
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path15.join(projectDir, name2)).find(existsSync11) ?? path15.join(projectDir, MANIFEST_FILENAMES[0]);
9750
9863
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
9751
9864
  const substitutions = {
9752
9865
  "{{TITLE}}": escapeYamlScalar(name),
9753
9866
  "{{AUTHOR}}": escapeYamlScalar(author)
9754
9867
  };
9755
- let openFile = path14.join(projectDir, "chapter-01.md");
9868
+ let openFile = path15.join(projectDir, "chapter-01.md");
9756
9869
  try {
9757
9870
  await fillTemplateFile(manifestPath, substitutions);
9758
9871
  if (preset) {
@@ -9773,7 +9886,7 @@ async function scaffoldProject(options) {
9773
9886
  }
9774
9887
  const firstSource = await firstSourceFile(manifestPath);
9775
9888
  if (firstSource)
9776
- openFile = path14.join(projectDir, firstSource);
9889
+ openFile = path15.join(projectDir, firstSource);
9777
9890
  await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
9778
9891
  } catch (e) {
9779
9892
  throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -9783,7 +9896,7 @@ async function scaffoldProject(options) {
9783
9896
  let versionHistoryError;
9784
9897
  if (requested === "local-git") {
9785
9898
  try {
9786
- const { providerFor } = await import("./source-provider-vanafrt9.js");
9899
+ const { providerFor } = await import("./source-provider-kn41jmbs.js");
9787
9900
  const provider = providerFor({ type: "local-folder", path: projectDir });
9788
9901
  await provider.initVersionHistory({
9789
9902
  projectDir,
@@ -9795,7 +9908,7 @@ async function scaffoldProject(options) {
9795
9908
  versionHistory = "none";
9796
9909
  const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
9797
9910
  const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
9798
- versionHistoryError = enclosingRepoDir ? `This book was added to ${path14.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
9911
+ versionHistoryError = enclosingRepoDir ? `This book was added to ${path15.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
9799
9912
  }
9800
9913
  }
9801
9914
  const result = {
@@ -9815,7 +9928,7 @@ function prettifyFolderName(base) {
9815
9928
  }
9816
9929
  async function adoptFolder(options) {
9817
9930
  const dir = options.dir;
9818
- if (typeof dir !== "string" || !path14.isAbsolute(dir)) {
9931
+ if (typeof dir !== "string" || !path15.isAbsolute(dir)) {
9819
9932
  throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
9820
9933
  }
9821
9934
  let st;
@@ -9827,11 +9940,11 @@ async function adoptFolder(options) {
9827
9940
  if (!st.isDirectory()) {
9828
9941
  throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
9829
9942
  }
9830
- if (MANIFEST_FILENAMES.some((name) => existsSync11(path14.join(dir, name)))) {
9943
+ if (MANIFEST_FILENAMES.some((name) => existsSync11(path15.join(dir, name)))) {
9831
9944
  throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
9832
9945
  }
9833
9946
  const template = options.template ?? "book";
9834
- const title = (options.title ?? "").trim() || prettifyFolderName(path14.basename(dir));
9947
+ const title = (options.title ?? "").trim() || prettifyFolderName(path15.basename(dir));
9835
9948
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
9836
9949
  let mdFiles;
9837
9950
  try {
@@ -9839,13 +9952,13 @@ async function adoptFolder(options) {
9839
9952
  mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
9840
9953
  if (mdFiles.length === 0) {
9841
9954
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
9842
- await copyFile(tplChapter, path14.join(dir, "chapter-01.md"));
9843
- await fillTemplateFile(path14.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
9955
+ await copyFile(tplChapter, path15.join(dir, "chapter-01.md"));
9956
+ await fillTemplateFile(path15.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
9844
9957
  mdFiles = ["chapter-01.md"];
9845
9958
  }
9846
9959
  const themeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
9847
- await mkdir7(path14.join(dir, "styles"), { recursive: true });
9848
- const bookCssPath = path14.join(dir, "styles", "book.css");
9960
+ await mkdir7(path15.join(dir, "styles"), { recursive: true });
9961
+ const bookCssPath = path15.join(dir, "styles", "book.css");
9849
9962
  if (!existsSync11(bookCssPath)) {
9850
9963
  await copyFile(await getAssetPath(`themes/${themeId}/theme.css`), bookCssPath);
9851
9964
  }
@@ -9863,8 +9976,8 @@ ${filesYaml}
9863
9976
  styles:
9864
9977
  - styles/book.css
9865
9978
  `;
9866
- await writeFile7(path14.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
9867
- await mkdir7(path14.join(dir, "assets"), { recursive: true });
9979
+ await writeFile7(path15.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
9980
+ await mkdir7(path15.join(dir, "assets"), { recursive: true });
9868
9981
  await ensureGitignoreHasDist(dir);
9869
9982
  } catch (e) {
9870
9983
  if (e instanceof CreateProjectErrorImpl)
@@ -9876,7 +9989,7 @@ styles:
9876
9989
  let versionHistoryError;
9877
9990
  if (requested === "local-git") {
9878
9991
  try {
9879
- const { providerFor } = await import("./source-provider-vanafrt9.js");
9992
+ const { providerFor } = await import("./source-provider-kn41jmbs.js");
9880
9993
  const provider = providerFor({ type: "local-folder", path: dir });
9881
9994
  await provider.initVersionHistory({
9882
9995
  projectDir: dir,
@@ -9891,8 +10004,8 @@ styles:
9891
10004
  }
9892
10005
  const result = {
9893
10006
  projectDir: dir,
9894
- manifestPath: path14.join(dir, MANIFEST_FILENAMES[0]),
9895
- openFile: path14.join(dir, mdFiles[0]),
10007
+ manifestPath: path15.join(dir, MANIFEST_FILENAMES[0]),
10008
+ openFile: path15.join(dir, mdFiles[0]),
9896
10009
  versionHistory
9897
10010
  };
9898
10011
  if (versionHistoryError !== undefined)
@@ -9900,10 +10013,10 @@ styles:
9900
10013
  return result;
9901
10014
  }
9902
10015
  async function ensureGitignoreHasDist(projectDir) {
9903
- const gitignorePath = path14.join(projectDir, ".gitignore");
10016
+ const gitignorePath = path15.join(projectDir, ".gitignore");
9904
10017
  let existing = null;
9905
10018
  try {
9906
- existing = await readFile24(gitignorePath, "utf8");
10019
+ existing = await readFile25(gitignorePath, "utf8");
9907
10020
  } catch {}
9908
10021
  if (existing === null) {
9909
10022
  await writeFile7(gitignorePath, `dist/
@@ -9923,7 +10036,7 @@ async function ensureGitignoreHasDist(projectDir) {
9923
10036
  async function firstSourceFile(manifestPath) {
9924
10037
  let text;
9925
10038
  try {
9926
- text = await readFile24(manifestPath, "utf8");
10039
+ text = await readFile25(manifestPath, "utf8");
9927
10040
  } catch {
9928
10041
  return;
9929
10042
  }
@@ -9935,17 +10048,17 @@ async function firstSourceFile(manifestPath) {
9935
10048
  return m ? m[1].trim() : undefined;
9936
10049
  }
9937
10050
  async function fillTemplateFile(filePath, substitutions) {
9938
- let text = await readFile24(filePath, "utf8");
10051
+ let text = await readFile25(filePath, "utf8");
9939
10052
  for (const [token, value] of Object.entries(substitutions)) {
9940
10053
  text = text.split(token).join(value);
9941
10054
  }
9942
10055
  await writeFile7(filePath, text, "utf8");
9943
10056
  }
9944
10057
  // src/lib/project-templates.ts
9945
- import { cp as cp3, mkdir as mkdir8, readFile as readFile25, readdir as readdir7, stat as stat6, writeFile as writeFile8 } from "node:fs/promises";
10058
+ import { cp as cp3, mkdir as mkdir8, readFile as readFile26, readdir as readdir7, stat as stat6, writeFile as writeFile8 } from "node:fs/promises";
9946
10059
  import { constants as FS2, existsSync as existsSync12 } from "node:fs";
9947
10060
  import { access as access2 } from "node:fs/promises";
9948
- import path15 from "node:path";
10061
+ import path16 from "node:path";
9949
10062
  import { isMap, isScalar, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
9950
10063
  var BUILT_IN_TEMPLATE_IDS = [
9951
10064
  "book",
@@ -9954,7 +10067,7 @@ var BUILT_IN_TEMPLATE_IDS = [
9954
10067
  ];
9955
10068
  async function readTemplateChoices(manifestPath) {
9956
10069
  try {
9957
- const text = await readFile25(manifestPath, "utf8");
10070
+ const text = await readFile26(manifestPath, "utf8");
9958
10071
  const doc = parseDocument2(text);
9959
10072
  const out = {};
9960
10073
  const preset = doc.get("preset");
@@ -10029,7 +10142,7 @@ function rewriteEntry(node, newPath) {
10029
10142
  node.value = newPath;
10030
10143
  }
10031
10144
  function commonAncestorDir(absFiles) {
10032
- const dirSegs = absFiles.map((f) => path15.resolve(f).split(path15.sep).slice(0, -1));
10145
+ const dirSegs = absFiles.map((f) => path16.resolve(f).split(path16.sep).slice(0, -1));
10033
10146
  if (dirSegs.length === 0)
10034
10147
  return "";
10035
10148
  let common = dirSegs[0];
@@ -10039,22 +10152,22 @@ function commonAncestorDir(absFiles) {
10039
10152
  i++;
10040
10153
  common = common.slice(0, i);
10041
10154
  }
10042
- return common.join(path15.sep) || path15.sep;
10155
+ return common.join(path16.sep) || path16.sep;
10043
10156
  }
10044
10157
  function pickVendorDir(templateDir, base) {
10045
- const preferred = path15.basename(base) || VENDOR_DIR;
10046
- if (!existsSync12(path15.join(templateDir, preferred)))
10158
+ const preferred = path16.basename(base) || VENDOR_DIR;
10159
+ if (!existsSync12(path16.join(templateDir, preferred)))
10047
10160
  return preferred;
10048
10161
  for (let n = 1;; n++) {
10049
10162
  const candidate = `${preferred}-${n}`;
10050
- if (!existsSync12(path15.join(templateDir, candidate)))
10163
+ if (!existsSync12(path16.join(templateDir, candidate)))
10051
10164
  return candidate;
10052
10165
  }
10053
10166
  }
10054
10167
  var TEMPLATE_META_FILE = ".gutterpress-template.json";
10055
10168
  async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
10056
10169
  const { doc, file } = await loadManifestDoc(templateDir);
10057
- const escapes = (p) => escapesProjectRoot(sourceProjectDir, path15.resolve(sourceProjectDir, p));
10170
+ const escapes = (p) => escapesProjectRoot(sourceProjectDir, path16.resolve(sourceProjectDir, p));
10058
10171
  const stylesSeq = doc.get("styles", true);
10059
10172
  const pluginsSeq = doc.get("plugins", true);
10060
10173
  const escapingStyles = collectEscaping(isSeq2(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
@@ -10070,16 +10183,16 @@ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
10070
10183
  return { vendoredRefs: [], excludedRefs };
10071
10184
  }
10072
10185
  const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
10073
- const pluginFiles = escapingPlugins.map((p) => path15.resolve(sourceProjectDir, p.value));
10186
+ const pluginFiles = escapingPlugins.map((p) => path16.resolve(sourceProjectDir, p.value));
10074
10187
  const externalFiles = [...new Set([...styleClosure, ...pluginFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
10075
10188
  const base = commonAncestorDir(externalFiles);
10076
10189
  const vendorDir = pickVendorDir(templateDir, base);
10077
10190
  await Promise.all(externalFiles.filter((abs) => existsSync12(abs)).map(async (abs) => {
10078
- const dest = path15.join(templateDir, vendorDir, path15.relative(base, abs));
10079
- await mkdir8(path15.dirname(dest), { recursive: true });
10191
+ const dest = path16.join(templateDir, vendorDir, path16.relative(base, abs));
10192
+ await mkdir8(path16.dirname(dest), { recursive: true });
10080
10193
  await cp3(abs, dest);
10081
10194
  }));
10082
- const bookLocal = (value) => toPosixPath(path15.join(vendorDir, path15.relative(base, path15.resolve(sourceProjectDir, value))));
10195
+ const bookLocal = (value) => toPosixPath(path16.join(vendorDir, path16.relative(base, path16.resolve(sourceProjectDir, value))));
10083
10196
  const vendoredRefs = [];
10084
10197
  for (const { node, value } of [...escapingStyles, ...escapingPlugins]) {
10085
10198
  const rewritten = bookLocal(value);
@@ -10094,7 +10207,7 @@ async function saveProjectAsTemplate(options) {
10094
10207
  const id = slugify(name);
10095
10208
  if (!id)
10096
10209
  throw new Error(`Could not derive a template id from "${name}".`);
10097
- const dir = path15.join(templatesRoot, id);
10210
+ const dir = path16.join(templatesRoot, id);
10098
10211
  let exists = true;
10099
10212
  try {
10100
10213
  await access2(dir, FS2.F_OK);
@@ -10111,13 +10224,13 @@ async function saveProjectAsTemplate(options) {
10111
10224
  continue;
10112
10225
  if (entry.name === TEMPLATE_META_FILE)
10113
10226
  continue;
10114
- await cp3(path15.join(projectDir, entry.name), path15.join(dir, entry.name), { recursive: true });
10227
+ await cp3(path16.join(projectDir, entry.name), path16.join(dir, entry.name), { recursive: true });
10115
10228
  }
10116
10229
  const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
10117
10230
  const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
10118
10231
  if (manifestName)
10119
- await retokeniseManifest(path15.join(dir, manifestName));
10120
- await writeFile8(path15.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
10232
+ await retokeniseManifest(path16.join(dir, manifestName));
10233
+ await writeFile8(path16.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
10121
10234
  return {
10122
10235
  id,
10123
10236
  label: name,
@@ -10130,7 +10243,7 @@ async function saveProjectAsTemplate(options) {
10130
10243
  async function retokeniseManifest(manifestPath) {
10131
10244
  let text;
10132
10245
  try {
10133
- text = await readFile25(manifestPath, "utf8");
10246
+ text = await readFile26(manifestPath, "utf8");
10134
10247
  } catch {
10135
10248
  return;
10136
10249
  }
@@ -10148,10 +10261,10 @@ async function listCustomTemplates(templatesRoot) {
10148
10261
  for (const entry of entries) {
10149
10262
  if (!entry.isDirectory())
10150
10263
  continue;
10151
- const dir = path15.join(templatesRoot, entry.name);
10264
+ const dir = path16.join(templatesRoot, entry.name);
10152
10265
  let label = prettify(entry.name);
10153
10266
  try {
10154
- const meta = JSON.parse(await readFile25(path15.join(dir, TEMPLATE_META_FILE), "utf8"));
10267
+ const meta = JSON.parse(await readFile26(path16.join(dir, TEMPLATE_META_FILE), "utf8"));
10155
10268
  if (typeof meta.label === "string" && meta.label)
10156
10269
  label = meta.label;
10157
10270
  } catch {}
@@ -10161,7 +10274,7 @@ async function listCustomTemplates(templatesRoot) {
10161
10274
  description: "Your saved template.",
10162
10275
  kind: "custom",
10163
10276
  dir,
10164
- ...await readTemplateChoices(path15.join(dir, MANIFEST_FILENAMES[0]))
10277
+ ...await readTemplateChoices(path16.join(dir, MANIFEST_FILENAMES[0]))
10165
10278
  });
10166
10279
  }
10167
10280
  out.sort((a, b) => a.label.localeCompare(b.label));
@@ -10173,12 +10286,12 @@ async function importTemplateFromFolder(options) {
10173
10286
  if (!srcStat.isDirectory()) {
10174
10287
  throw new Error("The chosen path is not a folder.");
10175
10288
  }
10176
- const name = options.name?.trim() || prettify(path15.basename(sourceDir));
10289
+ const name = options.name?.trim() || prettify(path16.basename(sourceDir));
10177
10290
  return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
10178
10291
  }
10179
10292
  // src/lib/snippets.ts
10180
- import { readdir as readdir8, readFile as readFile26, writeFile as writeFile9, mkdir as mkdir9, rm as rm3 } from "node:fs/promises";
10181
- import path16 from "node:path";
10293
+ import { readdir as readdir8, readFile as readFile27, writeFile as writeFile9, mkdir as mkdir9, rm as rm3 } from "node:fs/promises";
10294
+ import path17 from "node:path";
10182
10295
  var SNIPPETS_DIR = "snippets";
10183
10296
  var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
10184
10297
  function extractVariables(template) {
@@ -10197,15 +10310,15 @@ function substituteVariables(template, values) {
10197
10310
  return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
10198
10311
  }
10199
10312
  function resolveSnippetPath(projectDir, fileName) {
10200
- const dir = path16.resolve(projectDir, SNIPPETS_DIR);
10201
- const full = path16.resolve(dir, fileName);
10202
- if (full !== path16.join(dir, path16.basename(fileName)) || path16.dirname(full) !== dir) {
10313
+ const dir = path17.resolve(projectDir, SNIPPETS_DIR);
10314
+ const full = path17.resolve(dir, fileName);
10315
+ if (full !== path17.join(dir, path17.basename(fileName)) || path17.dirname(full) !== dir) {
10203
10316
  throw new Error(`Unsafe snippet filename: ${fileName}`);
10204
10317
  }
10205
10318
  return full;
10206
10319
  }
10207
10320
  async function listSnippets(projectDir) {
10208
- const dir = path16.join(projectDir, SNIPPETS_DIR);
10321
+ const dir = path17.join(projectDir, SNIPPETS_DIR);
10209
10322
  let names;
10210
10323
  try {
10211
10324
  names = await readdir8(dir);
@@ -10218,7 +10331,7 @@ async function listSnippets(projectDir) {
10218
10331
  continue;
10219
10332
  let body = "";
10220
10333
  try {
10221
- body = await readFile26(path16.join(dir, fileName), "utf8");
10334
+ body = await readFile27(path17.join(dir, fileName), "utf8");
10222
10335
  } catch {
10223
10336
  continue;
10224
10337
  }
@@ -10232,14 +10345,14 @@ async function listSnippets(projectDir) {
10232
10345
  return entries;
10233
10346
  }
10234
10347
  async function readSnippet(projectDir, fileName) {
10235
- return readFile26(resolveSnippetPath(projectDir, fileName), "utf8");
10348
+ return readFile27(resolveSnippetPath(projectDir, fileName), "utf8");
10236
10349
  }
10237
10350
  async function saveSnippet(projectDir, name, body) {
10238
10351
  const stem = slugify(name);
10239
10352
  if (!stem)
10240
10353
  throw new Error(`Could not derive a filename from "${name}".`);
10241
10354
  const fileName = `${stem}.md`;
10242
- const dir = path16.join(projectDir, SNIPPETS_DIR);
10355
+ const dir = path17.join(projectDir, SNIPPETS_DIR);
10243
10356
  await mkdir9(dir, { recursive: true });
10244
10357
  await writeFile9(resolveSnippetPath(projectDir, fileName), body, "utf8");
10245
10358
  return { name, fileName, variables: extractVariables(body) };
@@ -10250,7 +10363,7 @@ async function deleteSnippet(projectDir, fileName) {
10250
10363
  // src/lib/plugin-manager.ts
10251
10364
  import { randomUUID as randomUUID4 } from "node:crypto";
10252
10365
  import { cp as cp4, lstat as lstat3, mkdir as mkdir11, rename as rename5, rm as rm5, stat as stat7 } from "node:fs/promises";
10253
- import path18 from "node:path";
10366
+ import path19 from "node:path";
10254
10367
  import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
10255
10368
 
10256
10369
  // src/lib/npm-plugin-installer.ts
@@ -10261,12 +10374,12 @@ import {
10261
10374
  mkdir as mkdir10,
10262
10375
  mkdtemp as mkdtemp2,
10263
10376
  open as open2,
10264
- readFile as readFile27,
10377
+ readFile as readFile28,
10265
10378
  realpath as realpath2,
10266
10379
  rename as rename4,
10267
10380
  rm as rm4
10268
10381
  } from "node:fs/promises";
10269
- import path17 from "node:path";
10382
+ import path18 from "node:path";
10270
10383
  import { Transform } from "node:stream";
10271
10384
  import { pipeline } from "node:stream/promises";
10272
10385
  import { createGunzip } from "node:zlib";
@@ -10346,8 +10459,8 @@ function mb(bytes) {
10346
10459
  return `${Math.ceil(bytes / 1024 / 1024)}MB`;
10347
10460
  }
10348
10461
  function isContained2(root, candidate) {
10349
- const relative2 = path17.relative(root, candidate);
10350
- return relative2 === "" || !relative2.startsWith("..") && !path17.isAbsolute(relative2);
10462
+ const relative2 = path18.relative(root, candidate);
10463
+ return relative2 === "" || !relative2.startsWith("..") && !path18.isAbsolute(relative2);
10351
10464
  }
10352
10465
  function mergeLimits(overrides) {
10353
10466
  const limits = { ...DEFAULT_LIMITS, ...overrides };
@@ -10654,13 +10767,13 @@ async function extractTarball(ctx, archivePath, packageDir, packageName) {
10654
10767
  throw rejected;
10655
10768
  }
10656
10769
  async function readInstalledManifest(packageDir, selected) {
10657
- const packageJsonPath = path17.join(packageDir, "package.json");
10770
+ const packageJsonPath = path18.join(packageDir, "package.json");
10658
10771
  let manifest = null;
10659
10772
  try {
10660
10773
  const info2 = await lstat2(packageJsonPath);
10661
10774
  if (!info2.isFile() || info2.isSymbolicLink())
10662
10775
  throw new Error("not a regular file");
10663
- manifest = object2(JSON.parse(await readFile27(packageJsonPath, "utf8")));
10776
+ manifest = object2(JSON.parse(await readFile28(packageJsonPath, "utf8")));
10664
10777
  } catch (error2) {
10665
10778
  throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
10666
10779
  }
@@ -10730,10 +10843,10 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
10730
10843
  throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
10731
10844
  }
10732
10845
  const relativePath = relativePackagePath(parentPackagePath, name);
10733
- const packageDir = path17.join(ctx.stageRoot, ...relativePath.split("/"));
10734
- const archivePath = path17.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
10846
+ const packageDir = path18.join(ctx.stageRoot, ...relativePath.split("/"));
10847
+ const archivePath = path18.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
10735
10848
  try {
10736
- await mkdir10(path17.dirname(packageDir), { recursive: true });
10849
+ await mkdir10(path18.dirname(packageDir), { recursive: true });
10737
10850
  await downloadTarball(ctx, selected, archivePath);
10738
10851
  await extractTarball(ctx, archivePath, packageDir, selected.name);
10739
10852
  const manifest = await readInstalledManifest(packageDir, selected);
@@ -10832,9 +10945,9 @@ async function ensureDirectory(pathname) {
10832
10945
  }
10833
10946
  }
10834
10947
  async function prepareVendorParent(projectDir, name) {
10835
- const plugins = path17.join(projectDir, PLUGINS_DIR);
10836
- const npm = path17.join(plugins, VENDORED_NPM_DIR);
10837
- const packageParent = path17.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
10948
+ const plugins = path18.join(projectDir, PLUGINS_DIR);
10949
+ const npm = path18.join(plugins, VENDORED_NPM_DIR);
10950
+ const packageParent = path18.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
10838
10951
  await mkdir10(projectDir, { recursive: true });
10839
10952
  await ensureDirectory(plugins);
10840
10953
  await ensureDirectory(npm);
@@ -10862,14 +10975,14 @@ function publicReceipt(record) {
10862
10975
  };
10863
10976
  }
10864
10977
  async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10865
- const requestedProjectDir = path17.resolve(projectDirInput);
10978
+ const requestedProjectDir = path18.resolve(projectDirInput);
10866
10979
  await mkdir10(requestedProjectDir, { recursive: true });
10867
10980
  const projectDir = await realpath2(requestedProjectDir);
10868
10981
  const requested = parseNpmPluginSpec(packageSpec);
10869
10982
  const npmRoot = await prepareVendorParent(projectDir, requested.name);
10870
- const container = await mkdtemp2(path17.join(npmRoot, ".install-"));
10871
- const stageRoot = path17.join(container, "root");
10872
- const downloadsDir = path17.join(container, "downloads");
10983
+ const container = await mkdtemp2(path18.join(npmRoot, ".install-"));
10984
+ const stageRoot = path18.join(container, "root");
10985
+ const downloadsDir = path18.join(container, "downloads");
10873
10986
  await Promise.all([mkdir10(stageRoot), mkdir10(downloadsDir)]);
10874
10987
  const ctx = {
10875
10988
  fetch: options.fetch ?? globalThis.fetch,
@@ -10915,7 +11028,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10915
11028
  skipped: ctx.skipped,
10916
11029
  tree: { algorithm: "sha256", ...tree }
10917
11030
  };
10918
- const receiptFile = await open2(path17.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
11031
+ const receiptFile = await open2(path18.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
10919
11032
  try {
10920
11033
  await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
10921
11034
  `, "utf8");
@@ -10924,7 +11037,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10924
11037
  await receiptFile.close();
10925
11038
  }
10926
11039
  finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
10927
- const realFinalParent = await realpath2(path17.dirname(finalRoot));
11040
+ const realFinalParent = await realpath2(path18.dirname(finalRoot));
10928
11041
  if (!isContained2(projectDir, realFinalParent)) {
10929
11042
  throw new Error("Plugin install destination resolves outside the project.");
10930
11043
  }
@@ -10973,7 +11086,7 @@ async function rollbackNpmPluginInstall(installed) {
10973
11086
  // src/lib/plugin-manager.ts
10974
11087
  var pluginMutationQueues = new Map;
10975
11088
  function withPluginMutationLock(projectDir, mutation) {
10976
- const resolved = path18.resolve(projectDir);
11089
+ const resolved = path19.resolve(projectDir);
10977
11090
  const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
10978
11091
  const previous = pluginMutationQueues.get(key) ?? Promise.resolve();
10979
11092
  const run2 = previous.then(mutation, mutation);
@@ -11087,7 +11200,7 @@ async function setPluginEnabledUnlocked(projectDir, ref, enabled) {
11087
11200
  const { doc, file } = await loadManifestDoc(projectDir);
11088
11201
  const seq = doc.get("plugins", true);
11089
11202
  if (!isSeq3(seq)) {
11090
- throw new Error(`No plugins are configured in ${path18.basename(file)}.`);
11203
+ throw new Error(`No plugins are configured in ${path19.basename(file)}.`);
11091
11204
  }
11092
11205
  const idx = indexOfRef(seq, ref);
11093
11206
  if (idx < 0) {
@@ -11205,12 +11318,12 @@ async function addLocalPluginUnlocked(projectDir, sourcePath) {
11205
11318
  } catch {
11206
11319
  throw new Error(`Plugin source not found: ${sourcePath}`);
11207
11320
  }
11208
- const base = path18.basename(sourcePath);
11209
- const destDir = path18.join(projectDir, PLUGINS_DIR);
11321
+ const base = path19.basename(sourcePath);
11322
+ const destDir = path19.join(projectDir, PLUGINS_DIR);
11210
11323
  await mkdir11(destDir, { recursive: true });
11211
- const dest = path18.join(destDir, base);
11212
- const staging = path18.join(destDir, `.import-${randomUUID4()}`);
11213
- const backup = path18.join(destDir, `.backup-${randomUUID4()}`);
11324
+ const dest = path19.join(destDir, base);
11325
+ const staging = path19.join(destDir, `.import-${randomUUID4()}`);
11326
+ const backup = path19.join(destDir, `.backup-${randomUUID4()}`);
11214
11327
  let hasBackup = false;
11215
11328
  try {
11216
11329
  if (info2.isDirectory()) {
@@ -11285,9 +11398,9 @@ async function validateProjectPlugins(projectDir) {
11285
11398
  return out;
11286
11399
  }
11287
11400
  // src/lib/theme-manager.ts
11288
- import { cp as cp5, mkdir as mkdir12, readFile as readFile28, readdir as readdir9, stat as stat8, writeFile as writeFile10, rm as rm6 } from "node:fs/promises";
11401
+ import { cp as cp5, mkdir as mkdir12, readFile as readFile29, readdir as readdir9, stat as stat8, writeFile as writeFile10, rm as rm6 } from "node:fs/promises";
11289
11402
  import { existsSync as existsSync13 } from "node:fs";
11290
- import path19 from "node:path";
11403
+ import path20 from "node:path";
11291
11404
  import { isSeq as isSeq4, Scalar as Scalar2 } from "yaml";
11292
11405
  var THEMES_DIR = "themes";
11293
11406
  var BUILT_IN_THEME_IDS = [
@@ -11311,7 +11424,7 @@ var BUILT_IN_FALLBACK_META = {
11311
11424
  };
11312
11425
  async function readThemeMeta(jsonPath) {
11313
11426
  try {
11314
- const text = await readFile28(jsonPath, "utf8");
11427
+ const text = await readFile29(jsonPath, "utf8");
11315
11428
  const parsed = JSON.parse(text);
11316
11429
  return parsed && typeof parsed === "object" ? parsed : {};
11317
11430
  } catch {
@@ -11335,7 +11448,7 @@ function themeDirFor(projectDir, id) {
11335
11448
  if (typeof id !== "string" || id.length === 0 || themeSlug(id) !== id) {
11336
11449
  throw new Error(`Invalid theme id "${id}".`);
11337
11450
  }
11338
- return path19.join(projectDir, THEMES_DIR, id);
11451
+ return path20.join(projectDir, THEMES_DIR, id);
11339
11452
  }
11340
11453
  async function listBuiltInThemes() {
11341
11454
  const out = [];
@@ -11370,10 +11483,10 @@ async function resolveBuiltInTheme(id) {
11370
11483
  if (!meta.description) {
11371
11484
  meta.description = BUILT_IN_FALLBACK_META[id]?.description;
11372
11485
  }
11373
- return { info: themeInfo(id, "builtin", meta), cssPath, dir: path19.dirname(cssPath) };
11486
+ return { info: themeInfo(id, "builtin", meta), cssPath, dir: path20.dirname(cssPath) };
11374
11487
  }
11375
11488
  async function listProjectThemes(projectDir) {
11376
- const root = path19.join(projectDir, THEMES_DIR);
11489
+ const root = path20.join(projectDir, THEMES_DIR);
11377
11490
  let entries;
11378
11491
  try {
11379
11492
  entries = await readdir9(root, { withFileTypes: true });
@@ -11384,10 +11497,10 @@ async function listProjectThemes(projectDir) {
11384
11497
  for (const entry of entries) {
11385
11498
  if (!entry.isDirectory())
11386
11499
  continue;
11387
- const dir = path19.join(root, entry.name);
11388
- if (!existsSync13(path19.join(dir, "theme.css")))
11500
+ const dir = path20.join(root, entry.name);
11501
+ if (!existsSync13(path20.join(dir, "theme.css")))
11389
11502
  continue;
11390
- const meta = await readThemeMeta(path19.join(dir, "theme.json"));
11503
+ const meta = await readThemeMeta(path20.join(dir, "theme.json"));
11391
11504
  out.push(themeInfo(entry.name, "project", meta));
11392
11505
  }
11393
11506
  out.sort((a, b) => a.name.localeCompare(b.name));
@@ -11420,10 +11533,10 @@ async function getActiveTheme(projectDir) {
11420
11533
  if (!m)
11421
11534
  continue;
11422
11535
  const id = m[1];
11423
- const dir = path19.join(projectDir, THEMES_DIR, id);
11424
- if (!existsSync13(path19.join(dir, "theme.css")))
11536
+ const dir = path20.join(projectDir, THEMES_DIR, id);
11537
+ if (!existsSync13(path20.join(dir, "theme.css")))
11425
11538
  continue;
11426
- const meta = await readThemeMeta(path19.join(dir, "theme.json"));
11539
+ const meta = await readThemeMeta(path20.join(dir, "theme.json"));
11427
11540
  return themeInfo(id, "project", meta);
11428
11541
  }
11429
11542
  return null;
@@ -11458,12 +11571,12 @@ async function getPreviousTheme(projectDir) {
11458
11571
  if (!id)
11459
11572
  return null;
11460
11573
  const dir = themeDirFor(projectDir, id);
11461
- if (!existsSync13(path19.join(dir, "theme.css")))
11574
+ if (!existsSync13(path20.join(dir, "theme.css")))
11462
11575
  return null;
11463
11576
  const active = await getActiveTheme(projectDir);
11464
11577
  if (active && active.id === id)
11465
11578
  return null;
11466
- const meta = await readThemeMeta(path19.join(dir, "theme.json"));
11579
+ const meta = await readThemeMeta(path20.join(dir, "theme.json"));
11467
11580
  return themeInfo(id, "project", meta);
11468
11581
  }
11469
11582
  async function revertTheme(projectDir) {
@@ -11478,28 +11591,28 @@ async function applyTheme(projectDir, target) {
11478
11591
  if (target.kind === "builtin") {
11479
11592
  const resolved = await resolveBuiltInTheme(target.id);
11480
11593
  let destId = target.id;
11481
- if (existsSync13(path19.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
11594
+ if (existsSync13(path20.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
11482
11595
  destId = await uniqueThemeId(projectDir, target.id);
11483
11596
  }
11484
- const destDir = path19.join(projectDir, THEMES_DIR, destId);
11597
+ const destDir = path20.join(projectDir, THEMES_DIR, destId);
11485
11598
  await mkdir12(destDir, { recursive: true });
11486
11599
  await cp5(resolved.dir, destDir, { recursive: true });
11487
- info2 = themeInfo(destId, "project", await readThemeMeta(path19.join(destDir, "theme.json")));
11600
+ info2 = themeInfo(destId, "project", await readThemeMeta(path20.join(destDir, "theme.json")));
11488
11601
  } else {
11489
11602
  const dir = themeDirFor(projectDir, target.id);
11490
- if (!existsSync13(path19.join(dir, "theme.css"))) {
11603
+ if (!existsSync13(path20.join(dir, "theme.css"))) {
11491
11604
  throw new Error(`Theme "${target.id}" is not present in this project.`);
11492
11605
  }
11493
- info2 = themeInfo(target.id, "project", await readThemeMeta(path19.join(dir, "theme.json")));
11606
+ info2 = themeInfo(target.id, "project", await readThemeMeta(path20.join(dir, "theme.json")));
11494
11607
  }
11495
11608
  await setActiveThemeStyle(projectDir, info2.id);
11496
11609
  return info2;
11497
11610
  }
11498
11611
  async function uniqueThemeId(projectDir, base) {
11499
- const root = path19.join(projectDir, THEMES_DIR);
11612
+ const root = path20.join(projectDir, THEMES_DIR);
11500
11613
  let id = themeSlug(base);
11501
11614
  let n = 2;
11502
- while (existsSync13(path19.join(root, id))) {
11615
+ while (existsSync13(path20.join(root, id))) {
11503
11616
  id = `${themeSlug(base)}-${n++}`;
11504
11617
  }
11505
11618
  return id;
@@ -11514,16 +11627,16 @@ async function importThemeFromFolder(projectDir, sourceDir) {
11514
11627
  if (!info2.isDirectory()) {
11515
11628
  throw new Error("The chosen path is not a folder.");
11516
11629
  }
11517
- if (!existsSync13(path19.join(sourceDir, "theme.css"))) {
11630
+ if (!existsSync13(path20.join(sourceDir, "theme.css"))) {
11518
11631
  throw new Error("A theme folder must contain a theme.css file.");
11519
11632
  }
11520
- const meta = await readThemeMeta(path19.join(sourceDir, "theme.json"));
11521
- const base = meta.name || path19.basename(sourceDir);
11633
+ const meta = await readThemeMeta(path20.join(sourceDir, "theme.json"));
11634
+ const base = meta.name || path20.basename(sourceDir);
11522
11635
  const id = await uniqueThemeId(projectDir, base);
11523
- const destDir = path19.join(projectDir, THEMES_DIR, id);
11524
- await mkdir12(path19.dirname(destDir), { recursive: true });
11636
+ const destDir = path20.join(projectDir, THEMES_DIR, id);
11637
+ await mkdir12(path20.dirname(destDir), { recursive: true });
11525
11638
  await cp5(sourceDir, destDir, { recursive: true });
11526
- return themeInfo(id, "project", await readThemeMeta(path19.join(destDir, "theme.json")));
11639
+ return themeInfo(id, "project", await readThemeMeta(path20.join(destDir, "theme.json")));
11527
11640
  }
11528
11641
  function looksLikeCssUrl(url) {
11529
11642
  try {
@@ -11570,7 +11683,7 @@ async function importThemeFromUrl(projectDir, url) {
11570
11683
  let baseName;
11571
11684
  if (looksLikeCssUrl(trimmed)) {
11572
11685
  css = await fetchText(trimmed);
11573
- baseName = path19.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
11686
+ baseName = path20.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
11574
11687
  } else {
11575
11688
  const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
11576
11689
  css = await fetchText(`${base}theme.css`);
@@ -11580,37 +11693,37 @@ async function importThemeFromUrl(projectDir, url) {
11580
11693
  if (parsed && typeof parsed === "object")
11581
11694
  meta = parsed;
11582
11695
  } catch {}
11583
- baseName = meta.name || path19.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
11696
+ baseName = meta.name || path20.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
11584
11697
  }
11585
11698
  if (!css.trim()) {
11586
11699
  throw new Error(`Fetched theme CSS from ${trimmed} was empty.`);
11587
11700
  }
11588
11701
  assertLooksLikeCss(css, trimmed);
11589
11702
  const id = await uniqueThemeId(projectDir, meta.name || baseName);
11590
- const destDir = path19.join(projectDir, THEMES_DIR, id);
11703
+ const destDir = path20.join(projectDir, THEMES_DIR, id);
11591
11704
  await mkdir12(destDir, { recursive: true });
11592
- await writeFile10(path19.join(destDir, "theme.css"), css, "utf8");
11705
+ await writeFile10(path20.join(destDir, "theme.css"), css, "utf8");
11593
11706
  const finalMeta = {
11594
11707
  name: meta.name || prettify(id),
11595
11708
  author: meta.author,
11596
11709
  description: meta.description,
11597
11710
  preview: meta.preview ?? null
11598
11711
  };
11599
- await writeFile10(path19.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
11712
+ await writeFile10(path20.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
11600
11713
  return themeInfo(id, "project", finalMeta);
11601
11714
  }
11602
11715
  async function readThemeCss(projectDir, source) {
11603
11716
  if (source.kind === "builtin") {
11604
11717
  const resolved = await resolveBuiltInTheme(source.id);
11605
- return readFile28(resolved.cssPath, "utf8");
11718
+ return readFile29(resolved.cssPath, "utf8");
11606
11719
  }
11607
11720
  if (!projectDir)
11608
11721
  throw new Error("A project is required to read a project theme.");
11609
- const cssPath = path19.join(themeDirFor(projectDir, source.id), "theme.css");
11722
+ const cssPath = path20.join(themeDirFor(projectDir, source.id), "theme.css");
11610
11723
  if (!existsSync13(cssPath)) {
11611
11724
  throw new Error(`Theme "${source.id}" has no theme.css in this project.`);
11612
11725
  }
11613
- return readFile28(cssPath, "utf8");
11726
+ return readFile29(cssPath, "utf8");
11614
11727
  }
11615
11728
  async function removeProjectTheme(projectDir, id) {
11616
11729
  const dir = themeDirFor(projectDir, id);
@@ -11637,9 +11750,9 @@ async function removeProjectTheme(projectDir, id) {
11637
11750
  }
11638
11751
  // src/lib/theme-import.ts
11639
11752
  import { existsSync as existsSync14 } from "node:fs";
11640
- import { mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile29, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
11753
+ import { mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile30, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
11641
11754
  import { tmpdir as tmpdir3 } from "node:os";
11642
- import path20 from "node:path";
11755
+ import path21 from "node:path";
11643
11756
  import { unzipSync } from "fflate";
11644
11757
  var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
11645
11758
  var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
@@ -11703,18 +11816,18 @@ function mb2(n) {
11703
11816
  }
11704
11817
  async function readThemeJson(p) {
11705
11818
  try {
11706
- const parsed = JSON.parse(await readFile29(p, "utf8"));
11819
+ const parsed = JSON.parse(await readFile30(p, "utf8"));
11707
11820
  return parsed && typeof parsed === "object" ? parsed : {};
11708
11821
  } catch {
11709
11822
  return null;
11710
11823
  }
11711
11824
  }
11712
11825
  async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
11713
- const cssPath = path20.join(sourceDir, "theme.css");
11826
+ const cssPath = path21.join(sourceDir, "theme.css");
11714
11827
  if (!existsSync14(cssPath)) {
11715
11828
  throw new Error("A theme must contain a theme.css file.");
11716
11829
  }
11717
- const css = await readFile29(cssPath, "utf8");
11830
+ const css = await readFile30(cssPath, "utf8");
11718
11831
  const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
11719
11832
  if (reject) {
11720
11833
  throw new Error(`theme.css could not be parsed — ${reject.message}`);
@@ -11723,7 +11836,7 @@ async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
11723
11836
  for (const w of printFindings) {
11724
11837
  warnings.push({ code: "print-safety", message: w.message });
11725
11838
  }
11726
- const jsonPath = path20.join(sourceDir, "theme.json");
11839
+ const jsonPath = path21.join(sourceDir, "theme.json");
11727
11840
  if (!existsSync14(jsonPath)) {
11728
11841
  warnings.push({
11729
11842
  code: "no-theme-json",
@@ -11790,7 +11903,7 @@ async function importThemeFromZip(projectDir, archive) {
11790
11903
  throw new Error("No theme.css found in the package. A theme needs a theme.css at the package root or inside a single folder.");
11791
11904
  }
11792
11905
  const prefix = root === "" ? "" : `${root}/`;
11793
- const tmp = await mkdtemp3(path20.join(tmpdir3(), "gutterpress-theme-"));
11906
+ const tmp = await mkdtemp3(path21.join(tmpdir3(), "gutterpress-theme-"));
11794
11907
  try {
11795
11908
  const relPaths = [];
11796
11909
  for (const [name, data] of Object.entries(files)) {
@@ -11802,12 +11915,12 @@ async function importThemeFromZip(projectDir, archive) {
11802
11915
  const rel = prefix ? norm.slice(prefix.length) : norm;
11803
11916
  if (!rel || rel.split("/").some((seg) => seg === ".."))
11804
11917
  continue;
11805
- const dest = path20.join(tmp, rel);
11806
- if (dest !== tmp && !dest.startsWith(tmp + path20.sep)) {
11918
+ const dest = path21.join(tmp, rel);
11919
+ if (dest !== tmp && !dest.startsWith(tmp + path21.sep)) {
11807
11920
  throw new Error(`The package contains an unsafe path and was rejected: ${name}`);
11808
11921
  }
11809
11922
  relPaths.push(rel);
11810
- await mkdir13(path20.dirname(dest), { recursive: true });
11923
+ await mkdir13(path21.dirname(dest), { recursive: true });
11811
11924
  await writeFile11(dest, data);
11812
11925
  }
11813
11926
  return await finalizeThemeImport(projectDir, tmp, relPaths);
@@ -11821,10 +11934,10 @@ async function importThemeFromCssText(projectDir, css, name) {
11821
11934
  throw new Error(`The CSS could not be parsed — ${reject.message}`);
11822
11935
  }
11823
11936
  const displayName = name.trim() || "Imported theme";
11824
- const tmp = await mkdtemp3(path20.join(tmpdir3(), "gutterpress-theme-"));
11937
+ const tmp = await mkdtemp3(path21.join(tmpdir3(), "gutterpress-theme-"));
11825
11938
  try {
11826
- await writeFile11(path20.join(tmp, "theme.css"), css, "utf8");
11827
- await writeFile11(path20.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
11939
+ await writeFile11(path21.join(tmp, "theme.css"), css, "utf8");
11940
+ await writeFile11(path21.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
11828
11941
  const result = await finalizeThemeImport(projectDir, tmp, ["theme.css", "theme.json"]);
11829
11942
  result.warnings.unshift({
11830
11943
  code: "no-theme-json",
@@ -11836,14 +11949,14 @@ async function importThemeFromCssText(projectDir, css, name) {
11836
11949
  }
11837
11950
  }
11838
11951
  async function importThemeFromFile(projectDir, filePath) {
11839
- const ext = path20.extname(filePath).toLowerCase();
11952
+ const ext = path21.extname(filePath).toLowerCase();
11840
11953
  if (ext === ".zip") {
11841
- const buf = await readFile29(filePath);
11954
+ const buf = await readFile30(filePath);
11842
11955
  return importThemeFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
11843
11956
  }
11844
11957
  if (ext === ".css") {
11845
- const css = await readFile29(filePath, "utf8");
11846
- return importThemeFromCssText(projectDir, css, prettify(path20.basename(filePath, ".css")));
11958
+ const css = await readFile30(filePath, "utf8");
11959
+ return importThemeFromCssText(projectDir, css, prettify(path21.basename(filePath, ".css")));
11847
11960
  }
11848
11961
  throw new Error("Choose a .zip theme package or a .css stylesheet.");
11849
11962
  }
@@ -11937,52 +12050,6 @@ async function setActiveStyles(projectDir, paths) {
11937
12050
  await writeManifestDoc(file, doc);
11938
12051
  return paths;
11939
12052
  }
11940
- // src/lib/app-heartbeat.ts
11941
- import { readFile as readFile30, unlink as unlink3, writeFile as writeFile12 } from "node:fs/promises";
11942
- import path21 from "node:path";
11943
- var HEARTBEAT_FILENAME = "gutterpress-app-heartbeat";
11944
- var APP_HEARTBEAT_FRESH_MS = 2 * 60000;
11945
- var HEARTBEAT_TTL_MULTIPLIER = 2;
11946
- var HEARTBEAT_TTL_BUFFER_MS = 30000;
11947
- function heartbeatTtlMs(periodicMs) {
11948
- if (periodicMs === null)
11949
- return APP_HEARTBEAT_FRESH_MS;
11950
- return periodicMs * HEARTBEAT_TTL_MULTIPLIER + HEARTBEAT_TTL_BUFFER_MS;
11951
- }
11952
- function appHeartbeatPath(repoDir) {
11953
- return path21.join(gitDirFor(repoDir), HEARTBEAT_FILENAME);
11954
- }
11955
- async function writeAppHeartbeat(repoDir, now = Date.now(), pid = process.pid, ttlMs) {
11956
- try {
11957
- const heartbeat = { pid, timestamp: now, ...ttlMs !== undefined ? { ttlMs } : {} };
11958
- await writeFile12(appHeartbeatPath(repoDir), JSON.stringify(heartbeat));
11959
- } catch {}
11960
- }
11961
- async function removeAppHeartbeat(repoDir) {
11962
- try {
11963
- await unlink3(appHeartbeatPath(repoDir));
11964
- } catch {}
11965
- }
11966
- async function readAppHeartbeat(repoDir) {
11967
- try {
11968
- const raw = await readFile30(appHeartbeatPath(repoDir), "utf8");
11969
- const parsed = JSON.parse(raw);
11970
- if (parsed && typeof parsed === "object" && typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
11971
- const candidate = parsed;
11972
- const ttlMs = typeof candidate.ttlMs === "number" ? candidate.ttlMs : undefined;
11973
- return { pid: candidate.pid, timestamp: candidate.timestamp, ...ttlMs !== undefined ? { ttlMs } : {} };
11974
- }
11975
- return null;
11976
- } catch {
11977
- return null;
11978
- }
11979
- }
11980
- async function isAppHeartbeatFresh(repoDir, now = Date.now(), maxAgeMs = APP_HEARTBEAT_FRESH_MS) {
11981
- const heartbeat = await readAppHeartbeat(repoDir);
11982
- if (!heartbeat)
11983
- return false;
11984
- return now - heartbeat.timestamp < (heartbeat.ttlMs ?? maxAgeMs);
11985
- }
11986
12053
  // src/lib/host-policy.ts
11987
12054
  var AUTO_SNAPSHOT_MIN_MINUTES = 5;
11988
12055
  var AUTO_SNAPSHOT_MAX_MINUTES = 24 * 60;
@@ -11990,6 +12057,7 @@ var AUTO_SNAPSHOT_DEFAULT_MINUTES = 10;
11990
12057
  var AUTO_SYNC_MIN_MINUTES = 1;
11991
12058
  var AUTO_SYNC_MAX_MINUTES = 24 * 60;
11992
12059
  var AUTO_SYNC_DEFAULT_MINUTES = 2;
12060
+ var AUTO_SYNC_PUSH_INTERVAL_MINUTES = 15;
11993
12061
  function clampedDelayMs(enabled, minutes, bounds) {
11994
12062
  const isEnabled = enabled ?? bounds.enabledDefault;
11995
12063
  if (!isEnabled)
@@ -12051,9 +12119,6 @@ class GitHubAuthProvider {
12051
12119
  this.fetchImpl = options.fetchImpl ?? fetch;
12052
12120
  this.sleep = options.sleepImpl ?? defaultSleep;
12053
12121
  }
12054
- matches(origin) {
12055
- return origin.hostname.toLowerCase() === GITHUB_HOST;
12056
- }
12057
12122
  async connect(callbacks) {
12058
12123
  const { signal } = callbacks;
12059
12124
  const codeRes = await safeFetch(this.fetchImpl, DEVICE_CODE_URL, {
@@ -12117,17 +12182,6 @@ class GitHubAuthProvider {
12117
12182
  }
12118
12183
  }
12119
12184
  }
12120
- async validate(credential) {
12121
- try {
12122
- const res = await safeFetch(this.fetchImpl, `${API_BASE}/user`, {
12123
- method: "GET",
12124
- headers: githubApiHeaders(credential.token)
12125
- });
12126
- return res.status === 200;
12127
- } catch {
12128
- return true;
12129
- }
12130
- }
12131
12185
  async fetchUsername(token) {
12132
12186
  try {
12133
12187
  const res = await safeFetch(this.fetchImpl, `${API_BASE}/user`, {
@@ -12250,10 +12304,9 @@ async function listGitHubBranches(credential, owner, repo, options = {}) {
12250
12304
  return branches;
12251
12305
  }
12252
12306
  // src/lib/remote-auth/clone.ts
12253
- import * as fs6 from "node:fs";
12254
- import { mkdir as mkdir14, readdir as readdir11, readFile as readFile31, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
12255
- import path24 from "node:path";
12256
- import git4 from "isomorphic-git";
12307
+ import { mkdir as mkdir14, readdir as readdir10, rm as rm8 } from "node:fs/promises";
12308
+ import path22 from "node:path";
12309
+ import git3 from "isomorphic-git";
12257
12310
 
12258
12311
  // src/lib/remote-auth/git-http.ts
12259
12312
  import httpNode from "isomorphic-git/http/node";
@@ -12346,299 +12399,8 @@ function withIdleTimeout(http2, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
12346
12399
  }
12347
12400
  var defaultGitHttp = withIdleTimeout(httpNode);
12348
12401
 
12349
- // src/lib/remote-auth/recovery/classify.ts
12350
- var STALE_LOCK_MIN_AGE_MS = 2 * 60 * 1000;
12351
-
12352
- class RepoNeedsRecoveryError extends Error {
12353
- kind;
12354
- code = "RepoNeedsRecovery";
12355
- constructor(kind) {
12356
- super(`The project needs repair before it can sync (${kind}).`);
12357
- this.kind = kind;
12358
- this.name = "RepoNeedsRecoveryError";
12359
- }
12360
- }
12361
- function isRepoNeedsRecoveryError(e) {
12362
- return e?.code === "RepoNeedsRecovery";
12363
- }
12364
-
12365
- class InsecureTransportError extends Error {
12366
- code = "InsecureTransport";
12367
- constructor() {
12368
- super("This online address isn't secure, so the saved connection wasn't sent — " + "connections are never sent over an insecure address. Switch the address " + "to a secure one (starting with https) to sync with a saved connection.");
12369
- this.name = "InsecureTransportError";
12370
- }
12371
- }
12372
- function isInsecureTransportError(e) {
12373
- return e?.code === "InsecureTransport";
12374
- }
12375
- function isPushRejected(e) {
12376
- const code = e?.code;
12377
- if (code === "PushRejectedError") {
12378
- const reason = e?.data?.reason;
12379
- return reason === undefined || reason === "not-fast-forward";
12380
- }
12381
- if (code === "GitPushError") {
12382
- const msg = (e?.data?.prettyDetails ?? "") + " " + (e?.message ?? "");
12383
- return /non-fast-forward|would not be a fast-forward|not a simple fast-forward/i.test(msg);
12384
- }
12385
- return false;
12386
- }
12387
- function isMergeConflictError(e) {
12388
- return e?.code === "MergeConflictError";
12389
- }
12390
- function isUnrelatedHistories(e) {
12391
- const code = e?.code;
12392
- const msg = e?.message ?? String(e);
12393
- return code === "MergeNotSupportedError" || /unrelated histories|no common commits|refusing to merge unrelated/i.test(msg);
12394
- }
12395
- function classifyTransportFailure(e) {
12396
- if (isInsecureTransportError(e))
12397
- return "insecure_transport";
12398
- const err = e;
12399
- if (err?.code === "HttpError") {
12400
- const status = err.data?.statusCode;
12401
- if (status === 401 || status === 403 || status === 404)
12402
- return "auth_required";
12403
- }
12404
- const msg = `${err?.message ?? String(e)} ${err?.data?.prettyDetails ?? ""}`;
12405
- if (/\b401\b|\b403\b|\b404\b|unauthorized|authentication|not authorized|permission denied|forbidden|access denied|not allowed to push|pre-receive hook declined|hook declined/i.test(msg)) {
12406
- return "auth_required";
12407
- }
12408
- if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network|fetch failed|couldn't reach|socket hang ?up/i.test(msg)) {
12409
- return "network_unavailable";
12410
- }
12411
- return null;
12412
- }
12413
- function isLikelyRepoCorruption(e) {
12414
- if (classifyTransportFailure(e))
12415
- return false;
12416
- const code = e?.code;
12417
- const msg = e?.message ?? String(e);
12418
- return code === "ReadObjectFail" || code === "ObjectTypeError" || code === "NotFoundError" && /could not find/i.test(msg) || /object not found|missing object|pack.*corrupt|bad object|corrupt.*index|index.*corrupt|invalid index|could not find head|packed-refs/i.test(msg);
12419
- }
12420
- function classifyFromHealth(health, opts = {}) {
12421
- const minLockAgeMs = opts.minLockAgeMs ?? STALE_LOCK_MIN_AGE_MS;
12422
- if (!health.hasGitDir)
12423
- return "needs_repair";
12424
- if (health.hasInterruptedRebase)
12425
- return "needs_repair";
12426
- if (health.hasInterruptedCherryPick)
12427
- return "needs_repair";
12428
- if (health.hasInterruptedMerge)
12429
- return "needs_repair";
12430
- if (health.headUnreadable)
12431
- return "needs_repair";
12432
- if (health.isDetachedHead)
12433
- return "needs_repair";
12434
- if (health.hasStaleLock && (health.lockAgeMs ?? 0) >= minLockAgeMs) {
12435
- return "stale_lock";
12436
- }
12437
- return null;
12438
- }
12439
-
12440
12402
  // src/lib/remote-auth/transport.ts
12441
- import * as fs5 from "node:fs";
12442
- import git3 from "isomorphic-git";
12443
-
12444
- // src/lib/remote-auth/recovery/inspect.ts
12445
- import * as fs4 from "node:fs";
12446
- import path23 from "node:path";
12447
12403
  import git2 from "isomorphic-git";
12448
-
12449
- // src/lib/remote-auth/recovery/locks.ts
12450
- import { readdir as readdir10, rm as rm8, stat as stat9 } from "node:fs/promises";
12451
- import path22 from "node:path";
12452
- var TOP_LEVEL_LOCK_NAMES = [
12453
- "index.lock",
12454
- "HEAD.lock",
12455
- "config.lock",
12456
- "packed-refs.lock"
12457
- ];
12458
- async function collectRefLockPaths(dir, out) {
12459
- let entries;
12460
- try {
12461
- entries = await readdir10(dir, { withFileTypes: true });
12462
- } catch {
12463
- return;
12464
- }
12465
- for (const entry of entries) {
12466
- const abs = path22.join(dir, entry.name);
12467
- if (entry.isDirectory()) {
12468
- await collectRefLockPaths(abs, out);
12469
- } else if (entry.isFile() && entry.name.endsWith(".lock")) {
12470
- out.push(abs);
12471
- }
12472
- }
12473
- }
12474
- async function findLockCandidates(gitDir, now) {
12475
- const paths = TOP_LEVEL_LOCK_NAMES.map((name) => path22.join(gitDir, name));
12476
- await collectRefLockPaths(path22.join(gitDir, "refs"), paths);
12477
- const candidates = [];
12478
- for (const p of paths) {
12479
- try {
12480
- const s = await stat9(p);
12481
- if (s.isFile())
12482
- candidates.push({ path: p, ageMs: now - s.mtimeMs });
12483
- } catch {}
12484
- }
12485
- return candidates;
12486
- }
12487
- async function sweepStaleLocks(gitDir, now = Date.now(), minAgeMs = STALE_LOCK_MIN_AGE_MS) {
12488
- const candidates = await findLockCandidates(gitDir, now);
12489
- if (candidates.length === 0)
12490
- return "clean";
12491
- if (candidates.some((c) => c.ageMs < minAgeMs))
12492
- return "fresh";
12493
- for (const c of candidates) {
12494
- await rm8(c.path, { force: true });
12495
- }
12496
- return "swept";
12497
- }
12498
-
12499
- // src/lib/remote-auth/recovery/inspect.ts
12500
- async function inspectRepo(ctx, opts = {}) {
12501
- let repoDir = ctx.repoDir;
12502
- try {
12503
- const source = ctx.source ?? await detectProjectSource(ctx.repoDir);
12504
- if (source.type === "local-git-folder")
12505
- repoDir = gitScopeFor(source);
12506
- } catch {}
12507
- const gitDir = gitDirFor(repoDir);
12508
- const hasGitDir = fs4.existsSync(gitDir);
12509
- if (!hasGitDir) {
12510
- return {
12511
- hasGitDir: false,
12512
- isDetachedHead: false,
12513
- headUnreadable: false,
12514
- hasStaleLock: false,
12515
- hasInterruptedMerge: false,
12516
- hasInterruptedRebase: false,
12517
- hasInterruptedCherryPick: false,
12518
- hasLocalChanges: false
12519
- };
12520
- }
12521
- let currentBranch;
12522
- let isDetachedHead = false;
12523
- let headUnreadable = false;
12524
- try {
12525
- const branch = await git2.currentBranch({ fs: fs4, dir: repoDir });
12526
- if (branch == null) {
12527
- isDetachedHead = true;
12528
- } else {
12529
- currentBranch = branch;
12530
- }
12531
- } catch {
12532
- headUnreadable = true;
12533
- }
12534
- const lockCandidates = await findLockCandidates(gitDir, Date.now());
12535
- const hasStaleLock = lockCandidates.length > 0;
12536
- const lockAgeMs = hasStaleLock ? Math.min(...lockCandidates.map((c) => c.ageMs)) : undefined;
12537
- const hasInterruptedMerge = fs4.existsSync(path23.join(gitDir, "MERGE_HEAD"));
12538
- const hasInterruptedRebase = fs4.existsSync(path23.join(gitDir, "rebase-merge")) || fs4.existsSync(path23.join(gitDir, "rebase-apply"));
12539
- const hasInterruptedCherryPick = fs4.existsSync(path23.join(gitDir, "CHERRY_PICK_HEAD"));
12540
- let hasLocalChanges = false;
12541
- if (opts.checkLocalChanges !== false) {
12542
- try {
12543
- hasLocalChanges = await hasPendingChanges(repoDir);
12544
- } catch {
12545
- hasLocalChanges = true;
12546
- }
12547
- }
12548
- return {
12549
- hasGitDir,
12550
- currentBranch,
12551
- isDetachedHead,
12552
- headUnreadable,
12553
- hasStaleLock,
12554
- lockAgeMs,
12555
- hasInterruptedMerge,
12556
- hasInterruptedRebase,
12557
- hasInterruptedCherryPick,
12558
- hasLocalChanges
12559
- };
12560
- }
12561
- async function verifyRepoReadable(dir) {
12562
- const headOid = await git2.resolveRef({ fs: fs4, dir, ref: "HEAD" });
12563
- const { commit } = await git2.readCommit({ fs: fs4, dir, oid: headOid });
12564
- await git2.readTree({ fs: fs4, dir, oid: commit.tree });
12565
- }
12566
- function isUnbornRepo(repoDir) {
12567
- const objectsDir = path23.join(gitDirFor(repoDir), "objects");
12568
- let entries;
12569
- try {
12570
- entries = fs4.readdirSync(objectsDir);
12571
- } catch {
12572
- return false;
12573
- }
12574
- for (const entry of entries) {
12575
- if (/^[0-9a-f]{2}$/.test(entry)) {
12576
- try {
12577
- if (fs4.readdirSync(path23.join(objectsDir, entry)).length > 0)
12578
- return false;
12579
- } catch {
12580
- return false;
12581
- }
12582
- } else if (entry === "pack") {
12583
- try {
12584
- if (fs4.readdirSync(path23.join(objectsDir, entry)).some((f) => f.endsWith(".pack"))) {
12585
- return false;
12586
- }
12587
- } catch {
12588
- return false;
12589
- }
12590
- }
12591
- }
12592
- return true;
12593
- }
12594
- function preflightStructuralReason(kind) {
12595
- switch (kind) {
12596
- case "stale_lock":
12597
- return "health.hasStaleLock";
12598
- case "needs_repair":
12599
- return "health.structural";
12600
- case null:
12601
- return "none";
12602
- }
12603
- }
12604
- function buildPreflightDiagnostics(openedDir, repoDir, health, kind) {
12605
- return {
12606
- openedDir,
12607
- repoDir,
12608
- repoRootDiffers: repoDir !== openedDir,
12609
- kind: kind ?? "none",
12610
- reason: preflightStructuralReason(kind),
12611
- hasGitDir: health.hasGitDir,
12612
- hasInterruptedMerge: health.hasInterruptedMerge,
12613
- hasInterruptedRebase: health.hasInterruptedRebase,
12614
- hasInterruptedCherryPick: health.hasInterruptedCherryPick,
12615
- hasStaleLock: health.hasStaleLock,
12616
- isDetachedHead: health.isDetachedHead,
12617
- hasLocalChanges: health.hasLocalChanges
12618
- };
12619
- }
12620
-
12621
- // src/lib/remote-auth/sync-messages.ts
12622
- var MSG_UP_TO_DATE = "Everything is in sync.";
12623
- var MSG_UP_TO_DATE_PULLED = "Everything is in sync. The latest online changes were downloaded to this computer.";
12624
- var MSG_SYNCED = "Your changes are online.";
12625
- var MSG_SYNCED_MERGED = "Your changes are online, combined with changes from the online copy.";
12626
- var MSG_OFFLINE = "Your changes are saved on this computer. gutterpress couldn't reach the online repository — try syncing again when you're back online.";
12627
- var MSG_AUTH = "The online repository didn't accept the saved connection. Reconnect and try again.";
12628
- var MSG_INSECURE_TRANSPORT = "This project's online address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Switch the address to a secure one (starting with https), or to a local loopback address for a server on this computer, to sync.";
12629
- var MSG_BUSY = "The online copy is changing very quickly right now. Your work is saved on this computer — try Sync again in a moment.";
12630
- var MSG_UNRELATED = "The online address points at a different project's files, so the two can't be combined. Check the project's online address.";
12631
- var MSG_NO_REMOTE = "This project isn't connected to an online repository yet.";
12632
- var MSG_SSH_REMOTE = "This project's online address uses SSH (git@…), which gutterpress can't sync to. Switch it to the web (HTTPS) address to sync from here.";
12633
- var MSG_NO_BRANCH = "This project's version history isn't on a named branch, so it can't be synced right now.";
12634
- var MSG_PULLED = "The latest online changes were downloaded to this computer.";
12635
- var MSG_PULLED_MERGED = "The latest online changes were combined with your changes on this computer.";
12636
- var MSG_PULL_UP_TO_DATE = "You already have the latest online changes.";
12637
- var MSG_PUSH_UP_TO_DATE = "There's nothing new to send — everything is already online.";
12638
- var MSG_PULL_FIRST = "The online copy has changes you don't have yet. Get the latest changes first, then send yours.";
12639
- var SYNC_SNAPSHOT_MESSAGE = "Snapshot before syncing";
12640
-
12641
- // src/lib/remote-auth/transport.ts
12642
12404
  async function repoDirFor(projectDir) {
12643
12405
  const source = await detectProjectSource(projectDir);
12644
12406
  if (source.type === "local-git-folder")
@@ -12659,6 +12421,17 @@ function isCredentialTransmissionSafe(url) {
12659
12421
  return false;
12660
12422
  }
12661
12423
  }
12424
+
12425
+ class InsecureTransportError extends Error {
12426
+ code = "InsecureTransport";
12427
+ constructor() {
12428
+ super("This online address isn't secure, so the saved connection wasn't sent — " + "connections are never sent over an insecure address. Switch the address " + "to a secure one (starting with https) to sync with a saved connection.");
12429
+ this.name = "InsecureTransportError";
12430
+ }
12431
+ }
12432
+ function isInsecureTransportError(e) {
12433
+ return e?.code === "InsecureTransport";
12434
+ }
12662
12435
  function onAuthFor(credential) {
12663
12436
  if (!credential)
12664
12437
  return {};
@@ -12704,6 +12477,24 @@ async function resolveTransport(dir, options) {
12704
12477
  ...credential ? { credential } : {}
12705
12478
  };
12706
12479
  }
12480
+ function classifyTransportFailure(e) {
12481
+ if (isInsecureTransportError(e))
12482
+ return "insecure_transport";
12483
+ const err = e;
12484
+ if (err?.code === "HttpError") {
12485
+ const status = err.data?.statusCode;
12486
+ if (status === 401 || status === 403 || status === 404)
12487
+ return "auth_required";
12488
+ }
12489
+ const msg = `${err?.message ?? String(e)} ${err?.data?.prettyDetails ?? ""}`;
12490
+ if (/\b401\b|\b403\b|\b404\b|unauthorized|authentication|not authorized|permission denied|forbidden|access denied|not allowed to push|pre-receive hook declined|hook declined/i.test(msg)) {
12491
+ return "auth_required";
12492
+ }
12493
+ if (/ENOTFOUND|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EAI_AGAIN|network|fetch failed|couldn't reach|socket hang ?up/i.test(msg)) {
12494
+ return "network_unavailable";
12495
+ }
12496
+ return null;
12497
+ }
12707
12498
  function failureOutcome(e, snapshotId) {
12708
12499
  const kind = classifyTransportFailure(e);
12709
12500
  const base = snapshotId ? { snapshotId } : {};
@@ -12729,7 +12520,7 @@ function setupErrorMessage(e) {
12729
12520
  async function snapshotBeforeAction(args) {
12730
12521
  const { projectDir, dir, cache: cache2 } = args;
12731
12522
  const hasChanges = await hasPendingChanges(dir, cache2);
12732
- const staleStaging = fs5.existsSync(snapshotStagingMarkerPath(dir));
12523
+ const staleStaging = gitFs.existsSync(snapshotStagingMarkerPath(dir));
12733
12524
  if (!hasChanges && !staleStaging)
12734
12525
  return;
12735
12526
  const snap = await snapshotWorkingTreeUnlocked({
@@ -12737,70 +12528,49 @@ async function snapshotBeforeAction(args) {
12737
12528
  repoRoot: dir,
12738
12529
  message: args.message?.trim() || SYNC_SNAPSHOT_MESSAGE,
12739
12530
  authorName: args.authorName,
12740
- authorEmail: args.authorEmail
12531
+ authorEmail: args.authorEmail,
12532
+ cache: cache2
12741
12533
  });
12742
12534
  return snap.id;
12743
12535
  }
12744
- async function assertNoStructuralDamage(projectDir, logger) {
12745
- const health = await inspectRepo({ repoDir: projectDir }, { checkLocalChanges: false });
12746
- const structural = classifyFromHealth(health);
12747
- if (structural) {
12748
- logger.warn("sync", "structural preflight blocked sync", { kind: structural });
12749
- throw new RepoNeedsRecoveryError(structural);
12750
- }
12751
- }
12752
12536
  async function currentBranchOrThrow(dir) {
12753
- const branch = await git3.currentBranch({ fs: fs5, dir });
12537
+ const branch = await git2.currentBranch({ fs: gitFs, dir });
12754
12538
  if (!branch)
12755
12539
  throw new Error(MSG_NO_BRANCH);
12756
12540
  return branch;
12757
12541
  }
12758
12542
  async function resolveRefOrNull(dir, ref) {
12759
12543
  try {
12760
- return await git3.resolveRef({ fs: fs5, dir, ref });
12544
+ return await git2.resolveRef({ fs: gitFs, dir, ref });
12761
12545
  } catch {
12762
12546
  return null;
12763
12547
  }
12764
12548
  }
12765
- async function guardRefs(dir, listRefs, cache2, fn) {
12766
- const before = new Map;
12767
- for (const ref of await listRefs().catch(() => [])) {
12768
- before.set(ref, await resolveRefOrNull(dir, ref));
12769
- }
12549
+ async function guardTrackingRef(dir, ref, cache2, fn) {
12550
+ const before = await resolveRefOrNull(dir, ref);
12770
12551
  try {
12771
12552
  return await fn();
12772
12553
  } catch (e) {
12773
12554
  try {
12774
- const relisted = await listRefs().catch(() => []);
12775
- const refs = new Set([...before.keys(), ...relisted]);
12776
- for (const ref of refs) {
12777
- try {
12778
- const prev = before.get(ref) ?? null;
12779
- const after = await resolveRefOrNull(dir, ref);
12780
- if (!after || after === prev)
12781
- continue;
12782
- const landed = await git3.readObject({ fs: fs5, dir, oid: after, cache: cache2 }).then(() => true, () => false);
12783
- if (landed)
12784
- continue;
12785
- if (prev) {
12786
- await git3.writeRef({ fs: fs5, dir, ref, value: prev, force: true });
12787
- } else {
12788
- await git3.deleteRef({ fs: fs5, dir, ref });
12789
- }
12790
- } catch {}
12555
+ const after = await resolveRefOrNull(dir, ref);
12556
+ if (after && after !== before) {
12557
+ const landed = await git2.readObject({ fs: gitFs, dir, oid: after, cache: cache2 }).then(() => true, () => false);
12558
+ if (!landed) {
12559
+ if (before)
12560
+ await git2.writeRef({ fs: gitFs, dir, ref, value: before, force: true });
12561
+ else
12562
+ await git2.deleteRef({ fs: gitFs, dir, ref });
12563
+ }
12791
12564
  }
12792
12565
  } catch {}
12793
12566
  throw e;
12794
12567
  }
12795
12568
  }
12796
- async function guardTrackingRef(dir, ref, cache2, fn) {
12797
- return guardRefs(dir, async () => [ref], cache2, fn);
12798
- }
12799
12569
  async function fetchRemoteTip(dir, branch, transport, http2, cache2) {
12800
12570
  try {
12801
12571
  const trackingRef = `refs/remotes/${transport.remote}/${branch}`;
12802
- const result = await guardTrackingRef(dir, trackingRef, cache2, () => git3.fetch({
12803
- fs: fs5,
12572
+ const result = await guardTrackingRef(dir, trackingRef, cache2, () => git2.fetch({
12573
+ fs: gitFs,
12804
12574
  http: http2,
12805
12575
  dir,
12806
12576
  cache: cache2,
@@ -12830,25 +12600,12 @@ function short(oid) {
12830
12600
  }
12831
12601
 
12832
12602
  // src/lib/remote-auth/clone.ts
12833
- var PROVENANCE_FILE = "gutterpress-remote.json";
12834
- function provenancePath(projectDir) {
12835
- return path24.join(projectDir, ".git", PROVENANCE_FILE);
12836
- }
12837
- async function readProjectProvenance(projectDir) {
12838
- try {
12839
- const raw = await readFile31(provenancePath(projectDir), "utf8");
12840
- const parsed = JSON.parse(raw);
12841
- return parsed && parsed.provider ? parsed : null;
12842
- } catch {
12843
- return null;
12844
- }
12845
- }
12846
12603
  function sanitizeCloneFolderName(name) {
12847
12604
  return String(name ?? "").replace(/[\\/]/g, "-").replace(/^\.+/, "").trim();
12848
12605
  }
12849
12606
  async function assertCloneTarget(dir) {
12850
12607
  try {
12851
- const entries = await readdir11(dir);
12608
+ const entries = await readdir10(dir);
12852
12609
  if (entries.length > 0) {
12853
12610
  throw new Error("That folder already has files in it. Choose an empty folder for the project.");
12854
12611
  }
@@ -12878,8 +12635,8 @@ function friendlyCloneError(e) {
12878
12635
  return new Error("The project couldn't be downloaded from GitHub. Please try again.", { cause: e });
12879
12636
  }
12880
12637
  async function cloneRepository(options) {
12881
- const { dir, onProgress, tokenStore, provenance } = options;
12882
- if (!path24.isAbsolute(dir)) {
12638
+ const { dir, onProgress, tokenStore } = options;
12639
+ if (!path22.isAbsolute(dir)) {
12883
12640
  throw new Error("cloneRepository requires an absolute destination path.");
12884
12641
  }
12885
12642
  let parsed;
@@ -12898,11 +12655,11 @@ async function cloneRepository(options) {
12898
12655
  }
12899
12656
  return withRepoLock(dir, async () => {
12900
12657
  await assertCloneTarget(dir);
12901
- const dirExistedBefore = fs6.existsSync(dir);
12658
+ const dirExistedBefore = gitFs.existsSync(dir);
12902
12659
  await mkdir14(dir, { recursive: true });
12903
12660
  try {
12904
- await git4.clone({
12905
- fs: fs6,
12661
+ await git3.clone({
12662
+ fs: gitFs,
12906
12663
  http: options.httpClient ?? defaultGitHttp,
12907
12664
  dir,
12908
12665
  url: cleanUrl,
@@ -12922,21 +12679,16 @@ async function cloneRepository(options) {
12922
12679
  const friendly = friendlyCloneError(e);
12923
12680
  if (!dirExistedBefore) {
12924
12681
  try {
12925
- await rm9(dir, { recursive: true, force: true });
12682
+ await rm8(dir, { recursive: true, force: true });
12926
12683
  } catch {
12927
12684
  throw new Error(`${friendly.message} A partial download was left at ${dir} — delete that folder before trying again.`, { cause: e });
12928
12685
  }
12929
12686
  }
12930
12687
  throw friendly;
12931
12688
  }
12932
- if (provenance) {
12933
- try {
12934
- await writeFile13(provenancePath(dir), JSON.stringify(provenance, null, 2), "utf8");
12935
- } catch {}
12936
- }
12937
12689
  let branch;
12938
12690
  try {
12939
- branch = await git4.currentBranch({ fs: fs6, dir }) ?? undefined;
12691
+ branch = await git3.currentBranch({ fs: gitFs, dir }) ?? undefined;
12940
12692
  } catch {
12941
12693
  branch = undefined;
12942
12694
  }
@@ -12944,7 +12696,7 @@ async function cloneRepository(options) {
12944
12696
  });
12945
12697
  }
12946
12698
  // src/lib/remote-auth/test-access.ts
12947
- import git5 from "isomorphic-git";
12699
+ import git4 from "isomorphic-git";
12948
12700
  import httpNode2 from "isomorphic-git/http/node";
12949
12701
  function isSshRemoteUrl(url) {
12950
12702
  const trimmed = url.trim();
@@ -13000,7 +12752,7 @@ async function testRemoteAccess(options) {
13000
12752
  });
13001
12753
  const probe = (async () => {
13002
12754
  try {
13003
- const refs = await git5.listServerRefs({
12755
+ const refs = await git4.listServerRefs({
13004
12756
  http: options.httpClient ?? httpNode2,
13005
12757
  url: cleanUrl,
13006
12758
  protocolVersion: 1,
@@ -13099,28 +12851,6 @@ async function connectGenericHost(input, options = {}) {
13099
12851
  }
13100
12852
  return candidate;
13101
12853
  }
13102
-
13103
- class GenericTokenAuthProvider {
13104
- options;
13105
- constructor(options = {}) {
13106
- this.options = options;
13107
- }
13108
- matches(origin) {
13109
- return /^https?:$/.test(origin.protocol) && origin.hostname.toLowerCase() !== GITHUB_HOST;
13110
- }
13111
- connect(callbacks) {
13112
- return connectGenericHost(callbacks, this.options);
13113
- }
13114
- async validate(credential) {
13115
- const result = await testRemoteAccess({
13116
- url: `https://${credential.host}/`,
13117
- credential,
13118
- ...this.options.httpClient ? { httpClient: this.options.httpClient } : {},
13119
- ...this.options.timeoutMs ? { timeoutMs: this.options.timeoutMs } : {}
13120
- });
13121
- return result.ok || result.reason !== "auth";
13122
- }
13123
- }
13124
12854
  // src/lib/remote-auth/diagnose.ts
13125
12855
  function parseRemoteOrigin(url) {
13126
12856
  const trimmed = url.trim();
@@ -13215,19 +12945,23 @@ async function diagnoseProjectRemote(projectDir, options = {}) {
13215
12945
  };
13216
12946
  }
13217
12947
  // src/lib/remote-auth/sync.ts
13218
- import * as fs8 from "node:fs";
13219
- import git7 from "isomorphic-git";
12948
+ import git6 from "isomorphic-git";
13220
12949
 
13221
12950
  // src/lib/remote-auth/converge-merge.ts
13222
- import * as fs7 from "node:fs";
13223
- import { mkdir as mkdir15, unlink as unlink4, writeFile as writeFile14 } from "node:fs/promises";
13224
- import path25 from "node:path";
13225
- import git6 from "isomorphic-git";
12951
+ import { mkdir as mkdir15, unlink as unlink3, writeFile as writeFile12 } from "node:fs/promises";
12952
+ import path23 from "node:path";
12953
+ import git5 from "isomorphic-git";
13226
12954
  import diff3Merge from "diff3";
12955
+ function isMergeConflictError(e) {
12956
+ return e?.code === "MergeConflictError";
12957
+ }
12958
+ function isCheckoutConflict(e) {
12959
+ return e?.code === "CheckoutConflictError";
12960
+ }
13227
12961
  var CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
13228
- var CONVERGE_RESTORE_MESSAGE = "Kept the newest version of files that can't be combined";
12962
+ var CONVERGE_RESTORE_MESSAGE = "Kept both versions of the files that can't be combined";
13229
12963
  var CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
13230
- var IMAGE_EXT = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)$/i;
12964
+ var MARKER_UNSAFE_EXT = /\.svg$/i;
13231
12965
  var OUR_LABEL = "your version";
13232
12966
  var THEIR_LABEL = "online version";
13233
12967
  function stringLooksBinary(s) {
@@ -13255,31 +12989,31 @@ function mergeWithMarkers(baseContent, ourContent, theirContent) {
13255
12989
  }
13256
12990
  async function readBlobOrNull(dir, cache2, oid, filepath) {
13257
12991
  try {
13258
- const r = await git6.readBlob({ fs: fs7, dir, cache: cache2, oid, filepath });
12992
+ const r = await git5.readBlob({ fs: gitFs, dir, cache: cache2, oid, filepath });
13259
12993
  return { oid: r.oid, blob: r.blob };
13260
12994
  } catch {
13261
12995
  return null;
13262
12996
  }
13263
12997
  }
13264
12998
  async function writeTreeFile(dir, filepath, content) {
13265
- const abs = path25.join(dir, filepath);
13266
- await mkdir15(path25.dirname(abs), { recursive: true });
13267
- await writeFile14(abs, content);
12999
+ const abs = path23.join(dir, filepath);
13000
+ await mkdir15(path23.dirname(abs), { recursive: true });
13001
+ await writeFile12(abs, content);
13268
13002
  }
13269
13003
  async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
13270
- const bases = await git6.findMergeBase({ fs: fs7, dir, cache: cache2, oids: [ourTip, theirTip] });
13004
+ const bases = await git5.findMergeBase({ fs: gitFs, dir, cache: cache2, oids: [ourTip, theirTip] });
13271
13005
  const base = bases[0];
13272
13006
  if (!base && !allowUnrelated)
13273
13007
  return null;
13274
13008
  const out = [];
13275
- await git6.walk({
13276
- fs: fs7,
13009
+ await git5.walk({
13010
+ fs: gitFs,
13277
13011
  dir,
13278
13012
  cache: cache2,
13279
13013
  trees: [
13280
- git6.TREE({ ref: ourTip }),
13281
- git6.TREE({ ref: theirTip }),
13282
- ...base ? [git6.TREE({ ref: base })] : []
13014
+ git5.TREE({ ref: ourTip }),
13015
+ git5.TREE({ ref: theirTip }),
13016
+ ...base ? [git5.TREE({ ref: base })] : []
13283
13017
  ],
13284
13018
  map: async (filepath, entries) => {
13285
13019
  if (filepath === ".")
@@ -13293,51 +13027,51 @@ async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
13293
13027
  const [a, b] = await Promise.all([ours.oid(), theirs.oid()]);
13294
13028
  if (a !== b)
13295
13029
  out.push(filepath);
13296
- return null;
13297
- }
13298
- });
13299
- return out;
13300
- }
13301
- async function convergeMerge(params) {
13302
- const { dir, cache: cache2, branch, theirs, author } = params;
13303
- const ourTip = await git6.resolveRef({ fs: fs7, dir, ref: branch });
13304
- const [ourCommit, theirCommit] = await Promise.all([
13305
- git6.readCommit({ fs: fs7, dir, cache: cache2, oid: ourTip }),
13306
- git6.readCommit({ fs: fs7, dir, cache: cache2, oid: theirs })
13307
- ]);
13308
- const localNewer = ourCommit.commit.committer.timestamp >= theirCommit.commit.committer.timestamp;
13309
- const combined = new Set;
13310
- const driverBinary = new Set;
13311
- const imageClashes = [];
13312
- const attempt = () => git6.merge({
13313
- fs: fs7,
13314
- dir,
13315
- cache: cache2,
13316
- ours: branch,
13317
- theirs,
13318
- author,
13319
- message: CONVERGE_MERGE_MESSAGE,
13320
- allowUnrelatedHistories: params.allowUnrelatedHistories ?? false,
13321
- mergeDriver: ({ contents, path: filepath }) => {
13322
- const [base, ours, theirsContent] = contents;
13323
- if (IMAGE_EXT.test(filepath) || stringLooksBinary(ours) || stringLooksBinary(theirsContent) || stringLooksBinary(base)) {
13324
- driverBinary.add(filepath);
13325
- return { cleanMerge: false, mergedText: ours };
13326
- }
13327
- combined.add(filepath);
13328
- return {
13329
- cleanMerge: true,
13330
- mergedText: mergeWithMarkers(base, ours, theirsContent)
13331
- };
13030
+ return null;
13332
13031
  }
13333
13032
  });
13033
+ return out;
13034
+ }
13035
+ async function convergeMerge(params) {
13036
+ const { dir, cache: cache2, branch, theirs, author } = params;
13334
13037
  const snapshot = (message) => snapshotWorkingTreeUnlocked({
13335
13038
  projectDir: dir,
13336
13039
  repoRoot: dir,
13337
13040
  message,
13338
13041
  authorName: params.authorName,
13339
- authorEmail: params.authorEmail
13042
+ authorEmail: params.authorEmail,
13043
+ cache: cache2
13340
13044
  });
13045
+ const ourTip = await git5.resolveRef({ fs: gitFs, dir, ref: branch });
13046
+ const combined = new Set;
13047
+ const driverBinary = new Set;
13048
+ const keptBothFiles = [];
13049
+ let tipBeforeMerge = ourTip;
13050
+ const attempt = async () => {
13051
+ tipBeforeMerge = await git5.resolveRef({ fs: gitFs, dir, ref: branch });
13052
+ return git5.merge({
13053
+ fs: gitFs,
13054
+ dir,
13055
+ cache: cache2,
13056
+ ours: branch,
13057
+ theirs,
13058
+ author,
13059
+ message: CONVERGE_MERGE_MESSAGE,
13060
+ allowUnrelatedHistories: false,
13061
+ mergeDriver: ({ contents, path: filepath }) => {
13062
+ const [base, ours, theirsContent] = contents;
13063
+ if (MARKER_UNSAFE_EXT.test(filepath) || stringLooksBinary(ours) || stringLooksBinary(theirsContent) || stringLooksBinary(base)) {
13064
+ driverBinary.add(filepath);
13065
+ return { cleanMerge: false, mergedText: ours };
13066
+ }
13067
+ combined.add(filepath);
13068
+ return {
13069
+ cleanMerge: true,
13070
+ mergedText: mergeWithMarkers(base, ours, theirsContent)
13071
+ };
13072
+ }
13073
+ });
13074
+ };
13341
13075
  const postRestore = [];
13342
13076
  const equalize = async (paths) => {
13343
13077
  for (const p of paths.binary) {
@@ -13348,16 +13082,10 @@ async function convergeMerge(params) {
13348
13082
  if (!ourBlob || !theirBlob)
13349
13083
  continue;
13350
13084
  await writeTreeFile(dir, p, theirBlob.blob);
13351
- if (localNewer)
13352
- postRestore.push({ path: p, bytes: ourBlob.blob });
13353
- if (IMAGE_EXT.test(p)) {
13354
- imageClashes.push({
13355
- path: p,
13356
- localOid: ourBlob.oid,
13357
- remoteOid: theirBlob.oid,
13358
- kept: localNewer ? "local" : "online"
13359
- });
13360
- }
13085
+ const onlinePath = onlineSiblingPath(p);
13086
+ postRestore.push({ path: p, bytes: ourBlob.blob });
13087
+ postRestore.push({ path: onlinePath, bytes: theirBlob.blob });
13088
+ keptBothFiles.push({ path: p, onlinePath });
13361
13089
  }
13362
13090
  for (const p of paths.deleteByUs) {
13363
13091
  const theirBlob = await readBlobOrNull(dir, cache2, theirs, p);
@@ -13366,7 +13094,7 @@ async function convergeMerge(params) {
13366
13094
  }
13367
13095
  for (const p of paths.deleteByTheirs) {
13368
13096
  const ourBlob = await readBlobOrNull(dir, cache2, ourTip, p);
13369
- await unlink4(path25.join(dir, p)).catch((e) => {
13097
+ await unlink3(path23.join(dir, p)).catch((e) => {
13370
13098
  if (e?.code !== "ENOENT")
13371
13099
  throw e;
13372
13100
  });
@@ -13387,7 +13115,7 @@ async function convergeMerge(params) {
13387
13115
  });
13388
13116
  await attempt();
13389
13117
  } else if (e?.code === "MergeNotSupportedError") {
13390
- const added = await bothAddedPaths(dir, cache2, ourTip, theirs, params.allowUnrelatedHistories ?? false);
13118
+ const added = await bothAddedPaths(dir, cache2, ourTip, theirs, false);
13391
13119
  if (added === null || added.length === 0)
13392
13120
  throw e;
13393
13121
  await equalize({ binary: added, deleteByUs: [], deleteByTheirs: [] });
@@ -13396,7 +13124,20 @@ async function convergeMerge(params) {
13396
13124
  throw e;
13397
13125
  }
13398
13126
  }
13399
- await git6.checkout({ fs: fs7, dir, cache: cache2, ref: branch, force: true });
13127
+ try {
13128
+ await git5.checkout({ fs: gitFs, dir, cache: cache2, ref: branch, force: false });
13129
+ } catch (e) {
13130
+ if (!isCheckoutConflict(e))
13131
+ throw e;
13132
+ await git5.writeRef({
13133
+ fs: gitFs,
13134
+ dir,
13135
+ ref: await git5.expandRef({ fs: gitFs, dir, ref: branch }),
13136
+ value: tipBeforeMerge,
13137
+ force: true
13138
+ });
13139
+ throw e;
13140
+ }
13400
13141
  if (postRestore.length > 0) {
13401
13142
  for (const r of postRestore)
13402
13143
  await writeTreeFile(dir, r.path, r.bytes);
@@ -13404,12 +13145,39 @@ async function convergeMerge(params) {
13404
13145
  await snapshot(CONVERGE_RESTORE_MESSAGE);
13405
13146
  }
13406
13147
  return {
13407
- oid: await git6.resolveRef({ fs: fs7, dir, ref: branch }),
13148
+ oid: await git5.resolveRef({ fs: gitFs, dir, ref: branch }),
13408
13149
  combinedFiles: [...combined].sort(),
13409
- imageClashes
13150
+ keptBothFiles
13410
13151
  };
13411
13152
  }
13412
13153
  // src/lib/remote-auth/sync.ts
13154
+ function isPushRejected(e) {
13155
+ const code = e?.code;
13156
+ if (code === "PushRejectedError") {
13157
+ const reason = e?.data?.reason;
13158
+ return reason === undefined || reason === "not-fast-forward";
13159
+ }
13160
+ if (code === "GitPushError") {
13161
+ const msg = (e?.data?.prettyDetails ?? "") + " " + (e?.message ?? "");
13162
+ return /non-fast-forward|would not be a fast-forward|not a simple fast-forward/i.test(msg);
13163
+ }
13164
+ return false;
13165
+ }
13166
+ function isUnrelatedHistories(e) {
13167
+ const code = e?.code;
13168
+ const msg = e?.message ?? String(e);
13169
+ return code === "MergeNotSupportedError" || /unrelated histories|no common commits|refusing to merge unrelated/i.test(msg);
13170
+ }
13171
+ async function historyUnreadable(dir) {
13172
+ try {
13173
+ const oid = await git6.resolveRef({ fs: gitFs, dir, ref: "HEAD" });
13174
+ await git6.readCommit({ fs: gitFs, dir, oid });
13175
+ await git6.listFiles({ fs: gitFs, dir });
13176
+ return false;
13177
+ } catch {
13178
+ return true;
13179
+ }
13180
+ }
13413
13181
  var DEFAULT_SYNC_RETRY = {
13414
13182
  attempts: 3,
13415
13183
  backoffMs: 150
@@ -13418,254 +13186,157 @@ function defaultSleep2(ms) {
13418
13186
  return new Promise((resolve9) => setTimeout(resolve9, ms));
13419
13187
  }
13420
13188
  async function syncProject(options) {
13421
- const logger = resolveLogger(options.logFile, "sync");
13189
+ const http2 = options.httpClient ?? defaultGitHttp;
13190
+ const push = options.push ?? true;
13422
13191
  const attempts = Math.max(1, options.retry?.attempts ?? DEFAULT_SYNC_RETRY.attempts);
13423
13192
  const backoffMs = Math.max(0, options.retry?.backoffMs ?? DEFAULT_SYNC_RETRY.backoffMs);
13424
13193
  const sleep = options.retry?.sleep ?? defaultSleep2;
13425
- let snapshotId;
13426
- let pulled = false;
13427
- let filesChanged = false;
13428
- const combinedFiles = new Set;
13429
- const imageClashes = [];
13430
- const convergeExtras = () => ({
13431
- ...combinedFiles.size > 0 ? { combinedFiles: [...combinedFiles].sort() } : {},
13432
- ...imageClashes.length > 0 ? { imageClashes } : {}
13433
- });
13434
- for (let attempt = 0;attempt < attempts; attempt++) {
13435
- logger.info("sync", `sync pass ${attempt + 1}/${attempts}`);
13436
- const pull = await pullChanges(options);
13437
- snapshotId = snapshotId ?? pull.snapshotId;
13438
- const base = snapshotId ? { snapshotId } : {};
13439
- if (pull.status !== "pulled" && pull.status !== "up-to-date") {
13440
- logger.warn("sync", `pull non-success`, { status: pull.status });
13441
- return { ...pull, ...base };
13442
- }
13443
- pulled = pulled || pull.status === "pulled";
13444
- filesChanged = filesChanged || pull.status === "pulled" && pull.filesChanged;
13445
- if (pull.status === "pulled") {
13446
- for (const f of pull.combinedFiles ?? [])
13447
- combinedFiles.add(f);
13448
- imageClashes.push(...pull.imageClashes ?? []);
13449
- }
13450
- const push = await pushChanges(options);
13451
- snapshotId = snapshotId ?? push.snapshotId;
13452
- switch (push.status) {
13453
- case "pushed":
13454
- logger.info("sync", `synced`, { pulled });
13455
- return {
13456
- status: "synced",
13457
- message: pulled ? MSG_SYNCED_MERGED : MSG_SYNCED,
13458
- mergedRemoteChanges: pulled,
13459
- ...filesChanged ? { filesChanged: true } : {},
13460
- ...convergeExtras(),
13461
- ...snapshotId ? { snapshotId } : {}
13462
- };
13463
- case "up-to-date":
13464
- logger.info("sync", `up-to-date`, { pulled });
13465
- return {
13466
- status: "up-to-date",
13467
- message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
13468
- ...filesChanged ? { filesChanged: true } : {},
13469
- ...snapshotId ? { snapshotId } : {}
13470
- };
13471
- case "pull-first":
13472
- logger.info("sync", `push rejected (non-fast-forward) — retrying`);
13473
- if (attempt < attempts - 1 && backoffMs > 0)
13474
- await sleep(backoffMs);
13475
- continue;
13476
- default:
13477
- logger.warn("sync", `push non-success`, { status: push.status });
13478
- return {
13479
- ...push,
13480
- ...filesChanged ? { filesChanged: true } : {},
13481
- ...snapshotId ? { snapshotId } : {}
13482
- };
13483
- }
13484
- }
13485
- logger.error("sync", `exhausted ${attempts} retry attempts (race)`);
13486
- return {
13487
- status: "error",
13488
- message: MSG_BUSY,
13489
- ...filesChanged ? { filesChanged: true } : {},
13490
- ...convergeExtras(),
13491
- ...snapshotId ? { snapshotId } : {}
13492
- };
13493
- }
13494
- async function pullChanges(options) {
13495
- const http2 = options.httpClient ?? defaultGitHttp;
13496
13194
  const dir = await repoDirFor(options.projectDir);
13497
13195
  return withRepoLock(dir, async () => {
13498
13196
  const logger = resolveLogger(options.logFile, "sync");
13499
- await assertNoStructuralDamage(options.projectDir, logger);
13500
13197
  const cache2 = {};
13501
13198
  let snapshotId;
13199
+ let pulled = false;
13200
+ let filesChanged = false;
13201
+ const combinedFiles = new Set;
13202
+ const keptBothFiles = [];
13203
+ const base = () => ({
13204
+ ...filesChanged ? { filesChanged: true } : {},
13205
+ ...snapshotId ? { snapshotId } : {}
13206
+ });
13207
+ const convergeExtras = () => ({
13208
+ ...combinedFiles.size > 0 ? { combinedFiles: [...combinedFiles].sort() } : {},
13209
+ ...keptBothFiles.length > 0 ? { keptBothFiles } : {}
13210
+ });
13502
13211
  try {
13503
13212
  const branch = await currentBranchOrThrow(dir);
13504
13213
  const transport = await resolveTransport(dir, options);
13505
- snapshotId = await snapshotBeforeAction({
13506
- projectDir: options.projectDir,
13507
- dir,
13508
- message: options.message,
13509
- authorName: options.authorName,
13510
- authorEmail: options.authorEmail,
13511
- cache: cache2
13512
- });
13513
- const remoteTip = await fetchRemoteTip(dir, branch, transport, http2, cache2);
13514
- const localTip = await git7.resolveRef({ fs: fs8, dir, ref: branch });
13515
- const base = snapshotId ? { snapshotId } : {};
13516
- logger.info("pull", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)}`);
13517
- if (!remoteTip || remoteTip === localTip) {
13518
- return { status: "up-to-date", message: MSG_PULL_UP_TO_DATE, ...base };
13519
- }
13520
- let converge;
13521
- try {
13522
- converge = await convergeMerge({
13214
+ if (push) {
13215
+ snapshotId = await snapshotBeforeAction({
13216
+ projectDir: options.projectDir,
13523
13217
  dir,
13524
- cache: cache2,
13525
- branch,
13526
- theirs: remoteTip,
13527
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
13218
+ message: options.message,
13528
13219
  authorName: options.authorName,
13529
- authorEmail: options.authorEmail
13220
+ authorEmail: options.authorEmail,
13221
+ cache: cache2
13530
13222
  });
13531
- } catch (e) {
13532
- if (isUnrelatedHistories(e)) {
13533
- logger.warn("pull", "unrelated histories — refusing to combine");
13534
- return { status: "error", message: MSG_UNRELATED, ...base };
13535
- }
13536
- throw e;
13537
- }
13538
- const newTip = converge.oid;
13539
- if (newTip === localTip) {
13540
- return { status: "up-to-date", message: MSG_PULL_UP_TO_DATE, ...base };
13541
- }
13542
- if (converge.combinedFiles.length > 0) {
13543
- logger.info("pull", `combined with markers`, { files: converge.combinedFiles });
13544
13223
  }
13545
- const merged = newTip !== remoteTip;
13546
- const treeBefore = (await git7.readCommit({ fs: fs8, dir, cache: cache2, oid: localTip })).commit.tree;
13547
- const treeAfter = (await git7.readCommit({ fs: fs8, dir, cache: cache2, oid: newTip })).commit.tree;
13548
- return {
13549
- status: "pulled",
13550
- message: merged ? MSG_PULLED_MERGED : MSG_PULLED,
13551
- merged,
13552
- filesChanged: treeBefore !== treeAfter,
13553
- ...converge.combinedFiles.length > 0 ? { combinedFiles: converge.combinedFiles } : {},
13554
- ...converge.imageClashes.length > 0 ? { imageClashes: converge.imageClashes } : {},
13555
- ...base
13556
- };
13557
- } catch (e) {
13558
- const setupMsg = setupErrorMessage(e);
13559
- if (setupMsg) {
13224
+ for (let attempt = 0;attempt < attempts; attempt++) {
13225
+ logger.info("sync", `sync pass ${attempt + 1}/${attempts}`);
13226
+ const remoteTip = await fetchRemoteTip(dir, branch, transport, http2, cache2);
13227
+ let localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
13228
+ if (push || remoteTip !== null && remoteTip !== localTip) {
13229
+ const lateSnapshot = await snapshotBeforeAction({
13230
+ projectDir: options.projectDir,
13231
+ dir,
13232
+ message: SYNC_LATE_EDIT_MESSAGE,
13233
+ authorName: options.authorName,
13234
+ authorEmail: options.authorEmail,
13235
+ cache: cache2
13236
+ });
13237
+ if (lateSnapshot) {
13238
+ snapshotId = lateSnapshot;
13239
+ localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
13240
+ }
13241
+ }
13242
+ logger.info("sync", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
13243
+ let tip = localTip;
13244
+ if (remoteTip && remoteTip !== localTip) {
13245
+ let converge;
13246
+ try {
13247
+ converge = await convergeMerge({
13248
+ dir,
13249
+ cache: cache2,
13250
+ branch,
13251
+ theirs: remoteTip,
13252
+ author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
13253
+ authorName: options.authorName,
13254
+ authorEmail: options.authorEmail
13255
+ });
13256
+ } catch (e) {
13257
+ if (isUnrelatedHistories(e)) {
13258
+ logger.warn("sync", "unrelated histories — refusing to combine");
13259
+ return { status: "error", message: MSG_UNRELATED, ...base() };
13260
+ }
13261
+ throw e;
13262
+ }
13263
+ tip = converge.oid;
13264
+ if (tip !== localTip) {
13265
+ pulled = true;
13266
+ const [before, after] = await Promise.all([
13267
+ git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: localTip }),
13268
+ git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: tip })
13269
+ ]);
13270
+ filesChanged = filesChanged || before.commit.tree !== after.commit.tree;
13271
+ for (const f of converge.combinedFiles)
13272
+ combinedFiles.add(f);
13273
+ keptBothFiles.push(...converge.keptBothFiles);
13274
+ if (converge.combinedFiles.length > 0) {
13275
+ logger.info("sync", `combined with markers`, { files: converge.combinedFiles });
13276
+ }
13277
+ }
13278
+ }
13279
+ if (tip === remoteTip) {
13280
+ logger.info("sync", `up-to-date`, { pulled });
13281
+ return {
13282
+ status: "up-to-date",
13283
+ message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
13284
+ ...base()
13285
+ };
13286
+ }
13287
+ if (!push) {
13288
+ logger.info("sync", `pull-only pass complete — push deferred`, { pulled });
13289
+ return {
13290
+ status: "up-to-date",
13291
+ message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
13292
+ ...convergeExtras(),
13293
+ ...base()
13294
+ };
13295
+ }
13296
+ try {
13297
+ await git6.push({
13298
+ fs: gitFs,
13299
+ http: http2,
13300
+ dir,
13301
+ cache: cache2,
13302
+ remote: transport.remote,
13303
+ ref: branch,
13304
+ ...onAuthFor(transport.credential)
13305
+ });
13306
+ } catch (e) {
13307
+ if (!isPushRejected(e))
13308
+ throw e;
13309
+ logger.info("sync", `push rejected (non-fast-forward) — retrying`);
13310
+ if (attempt < attempts - 1 && backoffMs > 0)
13311
+ await sleep(backoffMs);
13312
+ continue;
13313
+ }
13314
+ logger.info("sync", `synced`, { pulled });
13560
13315
  return {
13561
- status: "error",
13562
- message: setupMsg,
13563
- ...snapshotId ? { snapshotId } : {}
13316
+ status: "synced",
13317
+ message: pulled ? MSG_SYNCED_MERGED : MSG_SYNCED,
13318
+ mergedRemoteChanges: pulled,
13319
+ ...convergeExtras(),
13320
+ ...base()
13564
13321
  };
13565
13322
  }
13566
- return failureOutcome(e, snapshotId);
13567
- }
13568
- });
13569
- }
13570
- async function pushChanges(options) {
13571
- const http2 = options.httpClient ?? defaultGitHttp;
13572
- const dir = await repoDirFor(options.projectDir);
13573
- return withRepoLock(dir, async () => {
13574
- const logger = resolveLogger(options.logFile, "sync");
13575
- await assertNoStructuralDamage(options.projectDir, logger);
13576
- const cache2 = {};
13577
- let snapshotId;
13578
- try {
13579
- const branch = await currentBranchOrThrow(dir);
13580
- const transport = await resolveTransport(dir, options);
13581
- snapshotId = await snapshotBeforeAction({
13582
- projectDir: options.projectDir,
13583
- dir,
13584
- message: options.message,
13585
- authorName: options.authorName,
13586
- authorEmail: options.authorEmail,
13587
- cache: cache2
13588
- });
13589
- const remoteTip = await fetchRemoteTip(dir, branch, transport, http2, cache2);
13590
- const localTip = await git7.resolveRef({ fs: fs8, dir, ref: branch });
13591
- const base = snapshotId ? { snapshotId } : {};
13592
- logger.info("push", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
13593
- if (remoteTip === localTip) {
13594
- return { status: "up-to-date", message: MSG_PUSH_UP_TO_DATE, ...base };
13595
- }
13596
- try {
13597
- await git7.push({
13598
- fs: fs8,
13599
- http: http2,
13600
- dir,
13601
- cache: cache2,
13602
- remote: transport.remote,
13603
- ref: branch,
13604
- ...onAuthFor(transport.credential)
13605
- });
13606
- } catch (e) {
13607
- if (isPushRejected(e)) {
13608
- return { status: "pull-first", message: MSG_PULL_FIRST, ...base };
13609
- }
13610
- throw e;
13611
- }
13612
- return { status: "pushed", message: MSG_SYNCED, ...base };
13323
+ logger.error("sync", `exhausted ${attempts} retry attempts (race)`);
13324
+ return { status: "error", message: MSG_BUSY, ...convergeExtras(), ...base() };
13613
13325
  } catch (e) {
13614
13326
  const setupMsg = setupErrorMessage(e);
13615
- if (setupMsg) {
13616
- return {
13617
- status: "error",
13618
- message: setupMsg,
13619
- ...snapshotId ? { snapshotId } : {}
13620
- };
13327
+ if (setupMsg)
13328
+ return { status: "error", message: setupMsg, ...base() };
13329
+ if (await historyUnreadable(dir)) {
13330
+ logger.error("sync", "the project's history could not be read");
13331
+ return { status: "error", message: MSG_HISTORY_UNREADABLE, ...base() };
13621
13332
  }
13622
- return failureOutcome(e, snapshotId);
13333
+ return { ...failureOutcome(e, snapshotId), ...filesChanged ? { filesChanged: true } : {} };
13623
13334
  }
13624
13335
  });
13625
13336
  }
13626
- // src/lib/remote-auth/image-clash.ts
13627
- import * as fs9 from "node:fs";
13628
- import { mkdir as mkdir16, writeFile as writeFile15 } from "node:fs/promises";
13629
- import path26 from "node:path";
13630
- import git8 from "isomorphic-git";
13631
- var OID = /^[0-9a-f]{40}$/;
13632
- function assertSafeRelativePath(filepath) {
13633
- if (path26.isAbsolute(filepath) || filepath.split(/[\\/]/).some((seg) => seg === "..")) {
13634
- throw new Error("Invalid file path.");
13635
- }
13636
- }
13637
- async function readImageVersion(options) {
13638
- if (!OID.test(options.oid))
13639
- throw new Error("Invalid version id.");
13640
- const dir = await repoDirFor(options.projectDir);
13641
- const { blob } = await git8.readBlob({ fs: fs9, dir, oid: options.oid });
13642
- return blob;
13643
- }
13644
- async function keepImageVersion(options) {
13645
- if (!OID.test(options.oid))
13646
- throw new Error("Invalid version id.");
13647
- assertSafeRelativePath(options.path);
13648
- const dir = await repoDirFor(options.projectDir);
13649
- await withRepoLock(dir, async () => {
13650
- const { blob } = await git8.readBlob({ fs: fs9, dir, oid: options.oid });
13651
- const abs = path26.join(dir, options.path);
13652
- await mkdir16(path26.dirname(abs), { recursive: true });
13653
- await writeFile15(abs, blob);
13654
- await snapshotWorkingTreeUnlocked({
13655
- projectDir: dir,
13656
- repoRoot: dir,
13657
- message: `Kept your chosen version of ${path26.posix.basename(options.path)}`,
13658
- authorName: options.authorName,
13659
- authorEmail: options.authorEmail
13660
- }).catch((e) => {
13661
- if (!(e instanceof Error && /nothing new to save/i.test(e.message)))
13662
- throw e;
13663
- });
13664
- });
13665
- }
13666
13337
  // src/lib/publish/run-publish.ts
13667
- import { readdir as readdir12, stat as stat11 } from "node:fs/promises";
13668
- import path29 from "node:path";
13338
+ import { readdir as readdir11, stat as stat10 } from "node:fs/promises";
13339
+ import path26 from "node:path";
13669
13340
 
13670
13341
  // src/lib/publish/command-runner.ts
13671
13342
  var CAPTURE_LIMIT = 64 * 1024;
@@ -13852,25 +13523,25 @@ ${tail}` : ""}`);
13852
13523
  };
13853
13524
 
13854
13525
  // src/lib/publish/providers/guided.ts
13855
- import { copyFile as copyFile2, mkdir as mkdir17, writeFile as writeFile16 } from "node:fs/promises";
13856
- import path27 from "node:path";
13526
+ import { copyFile as copyFile2, mkdir as mkdir16, writeFile as writeFile13 } from "node:fs/promises";
13527
+ import path24 from "node:path";
13857
13528
  async function stageGuidedPackage(req, providerId, listingLines) {
13858
- const packageDir = path27.join(path27.dirname(req.artifact.path), "publish", providerId);
13859
- await mkdir17(packageDir, { recursive: true });
13860
- await copyFile2(req.artifact.path, path27.join(packageDir, path27.basename(req.artifact.path)));
13529
+ const packageDir = path24.join(path24.dirname(req.artifact.path), "publish", providerId);
13530
+ await mkdir16(packageDir, { recursive: true });
13531
+ await copyFile2(req.artifact.path, path24.join(packageDir, path24.basename(req.artifact.path)));
13861
13532
  const { title, authors } = req.project;
13862
13533
  const sheet = [
13863
13534
  `# ${title || "Untitled"}`,
13864
13535
  "",
13865
13536
  `- **Authors:** ${authors.length ? authors.join(", ") : "(not set)"}`,
13866
- `- **File:** ${path27.basename(req.artifact.path)}`,
13537
+ `- **File:** ${path24.basename(req.artifact.path)}`,
13867
13538
  ...listingLines,
13868
13539
  "",
13869
13540
  "Copy these details into the platform's listing form when uploading.",
13870
13541
  ""
13871
13542
  ].join(`
13872
13543
  `);
13873
- await writeFile16(path27.join(packageDir, "LISTING.md"), sheet, "utf8");
13544
+ await writeFile13(path24.join(packageDir, "LISTING.md"), sheet, "utf8");
13874
13545
  return packageDir;
13875
13546
  }
13876
13547
 
@@ -13933,8 +13604,8 @@ var drivethrurpgProvider = {
13933
13604
  };
13934
13605
 
13935
13606
  // src/lib/publish/butler.ts
13936
- import { chmod as chmod2, mkdir as mkdir18, stat as stat10, writeFile as writeFile17 } from "node:fs/promises";
13937
- import path28 from "node:path";
13607
+ import { chmod as chmod2, mkdir as mkdir17, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
13608
+ import path25 from "node:path";
13938
13609
  import { unzipSync as unzipSync2 } from "fflate";
13939
13610
  var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
13940
13611
  function butlerBrothChannel(platform2 = process.platform, arch2 = process.arch) {
@@ -13954,7 +13625,7 @@ function butlerBinaryName(platform2 = process.platform) {
13954
13625
  }
13955
13626
  async function fileExists2(p) {
13956
13627
  try {
13957
- return (await stat10(p)).isFile();
13628
+ return (await stat9(p)).isFile();
13958
13629
  } catch {
13959
13630
  return false;
13960
13631
  }
@@ -13971,8 +13642,8 @@ async function ensureButler(deps) {
13971
13642
  }
13972
13643
  if (await commandExists("butler", runCommand, deps.env))
13973
13644
  return "butler";
13974
- const cacheDir = path28.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
13975
- const cached = path28.join(cacheDir, butlerBinaryName());
13645
+ const cacheDir = path25.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
13646
+ const cached = path25.join(cacheDir, butlerBinaryName());
13976
13647
  if (await fileExists2(cached))
13977
13648
  return cached;
13978
13649
  const channel = butlerBrothChannel();
@@ -13994,14 +13665,14 @@ async function ensureButler(deps) {
13994
13665
  return new Uint8Array(await response.arrayBuffer());
13995
13666
  });
13996
13667
  const files = unzipSync2(archive);
13997
- await mkdir18(cacheDir, { recursive: true });
13668
+ await mkdir17(cacheDir, { recursive: true });
13998
13669
  let foundBinary = false;
13999
13670
  for (const [name, data] of Object.entries(files)) {
14000
13671
  if (name.endsWith("/"))
14001
13672
  continue;
14002
- const target = path28.join(cacheDir, path28.basename(name));
14003
- await writeFile17(target, data);
14004
- if (path28.basename(name) === butlerBinaryName()) {
13673
+ const target = path25.join(cacheDir, path25.basename(name));
13674
+ await writeFile14(target, data);
13675
+ if (path25.basename(name) === butlerBinaryName()) {
14005
13676
  await chmod2(target, 493).catch(() => {});
14006
13677
  foundBinary = true;
14007
13678
  }
@@ -14439,9 +14110,9 @@ async function resolvePublishRequest(options, deps) {
14439
14110
  const effectiveAccount = bookAccount || deps.credentialAccount;
14440
14111
  const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
14441
14112
  const outDir = resolveOutputDir(manifestDir, config.title);
14442
- const defaultArtifact = provider.info.format === "pdf" ? path29.join(outDir, artifactName(config.title, "pdf")) : outDir;
14113
+ const defaultArtifact = provider.info.format === "pdf" ? path26.join(outDir, artifactName(config.title, "pdf")) : outDir;
14443
14114
  const artifact = {
14444
- path: options.artifactPath ? path29.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
14115
+ path: options.artifactPath ? path26.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
14445
14116
  format: provider.info.format
14446
14117
  };
14447
14118
  return {
@@ -14459,7 +14130,7 @@ var PDF_HINT = "Build the PDF first (gutterpress build, or export it from the ap
14459
14130
  var HTML_HINT = "Build the website export first (gutterpress build --format html).";
14460
14131
  async function artifactIssues(artifact) {
14461
14132
  try {
14462
- const s = await stat11(artifact.path);
14133
+ const s = await stat10(artifact.path);
14463
14134
  if (artifact.format === "pdf" && !s.isFile()) {
14464
14135
  return [
14465
14136
  {
@@ -14505,7 +14176,7 @@ async function artifactIssues(artifact) {
14505
14176
  var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
14506
14177
  async function htmlDirIssues(dir) {
14507
14178
  const issues = [];
14508
- const isThere = async (rel) => stat11(path29.join(dir, rel)).then(() => true, () => false);
14179
+ const isThere = async (rel) => stat10(path26.join(dir, rel)).then(() => true, () => false);
14509
14180
  if (!await isThere(BOOK_HTML)) {
14510
14181
  issues.push({
14511
14182
  severity: "error",
@@ -14513,7 +14184,7 @@ async function htmlDirIssues(dir) {
14513
14184
  message: `${dir} has no ${BOOK_HTML} — it isn't an HTML export. ${HTML_HINT}`
14514
14185
  });
14515
14186
  }
14516
- const entries = await readdir12(dir).catch(() => []);
14187
+ const entries = await readdir11(dir).catch(() => []);
14517
14188
  const extras = entries.filter((name) => name.toLowerCase().endsWith(".pdf") || name === BUILD_FINGERPRINT_FILENAME);
14518
14189
  if (extras.length > 0) {
14519
14190
  issues.push({
@@ -14612,17 +14283,17 @@ async function connectPublishProvider(options, deps) {
14612
14283
  return { connected: true, providerId: info7.id };
14613
14284
  }
14614
14285
  // src/lib/publish/selections.ts
14615
- import { chmod as chmod3, mkdir as mkdir19, readFile as readFile32, writeFile as writeFile18 } from "node:fs/promises";
14616
- import path30 from "node:path";
14286
+ import { chmod as chmod3, mkdir as mkdir18, readFile as readFile31, writeFile as writeFile15 } from "node:fs/promises";
14287
+ import path27 from "node:path";
14617
14288
  class PublishSelectionsStore {
14618
14289
  filePath;
14619
14290
  queue = Promise.resolve();
14620
14291
  constructor(filePath) {
14621
- this.filePath = filePath ?? path30.join(defaultConfigDir(), "publish-selections.json");
14292
+ this.filePath = filePath ?? path27.join(defaultConfigDir(), "publish-selections.json");
14622
14293
  }
14623
14294
  async read() {
14624
14295
  try {
14625
- const raw = await readFile32(this.filePath, "utf8");
14296
+ const raw = await readFile31(this.filePath, "utf8");
14626
14297
  const parsed = JSON.parse(raw);
14627
14298
  if (parsed && typeof parsed === "object") {
14628
14299
  return {
@@ -14635,8 +14306,8 @@ class PublishSelectionsStore {
14635
14306
  return { version: 1, global: {}, projects: {} };
14636
14307
  }
14637
14308
  async write(data) {
14638
- await mkdir19(path30.dirname(this.filePath), { recursive: true });
14639
- await writeFile18(this.filePath, JSON.stringify(data, null, 2), {
14309
+ await mkdir18(path27.dirname(this.filePath), { recursive: true });
14310
+ await writeFile15(this.filePath, JSON.stringify(data, null, 2), {
14640
14311
  encoding: "utf8",
14641
14312
  mode: 384
14642
14313
  });
@@ -14738,372 +14409,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
14738
14409
  await writeManifestDoc(file, doc);
14739
14410
  return settingsFromDoc(doc);
14740
14411
  }
14741
- // src/lib/remote-auth/recovery/repair.ts
14742
- import * as fs10 from "node:fs";
14743
- import { cp as cp6, mkdtemp as mkdtemp4, rename as rename6, rm as rm10 } from "node:fs/promises";
14744
- import os4 from "node:os";
14745
- import path31 from "node:path";
14746
- import git9 from "isomorphic-git";
14747
- var INTERRUPTED_STATE = [
14748
- "MERGE_HEAD",
14749
- "MERGE_MSG",
14750
- "MERGE_MODE",
14751
- "CHERRY_PICK_HEAD",
14752
- "REVERT_HEAD",
14753
- "sequencer",
14754
- "rebase-merge",
14755
- "rebase-apply"
14756
- ];
14757
- var MSG_REPAIRED = "Your project's version history is working again. Your files were not changed.";
14758
- var MSG_RETRY = "Another program may be using this project's history right now — try again in a moment.";
14759
- var MSG_FAILED = "Your project's history couldn't be repaired automatically, but your files are safe and untouched.";
14760
- function stamp() {
14761
- return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
14762
- }
14763
- async function indexReadable(dir) {
14764
- try {
14765
- await git9.listFiles({ fs: fs10, dir });
14766
- return true;
14767
- } catch {
14768
- return false;
14769
- }
14770
- }
14771
- async function rebuildIndexFromHead(dir) {
14772
- await rm10(path31.join(gitDirFor(dir), "index"), { force: true });
14773
- const cache2 = {};
14774
- let files;
14775
- try {
14776
- files = await git9.listFiles({ fs: fs10, dir, ref: "HEAD" });
14777
- } catch {
14778
- return;
14779
- }
14780
- for (const filepath of files) {
14781
- await git9.resetIndex({ fs: fs10, dir, cache: cache2, filepath, ref: "HEAD" });
14782
- }
14783
- }
14784
- function readDamagedBranchTips(damagedGitDir) {
14785
- const tips = new Map;
14786
- const OID2 = /^[0-9a-f]{40}$/;
14787
- try {
14788
- const packed = fs10.readFileSync(path31.join(damagedGitDir, "packed-refs"), "utf8");
14789
- for (const line of packed.split(`
14790
- `)) {
14791
- const m = /^([0-9a-f]{40}) refs\/heads\/(.+)$/.exec(line.trim());
14792
- if (m)
14793
- tips.set(m[2], m[1]);
14794
- }
14795
- } catch {}
14796
- const headsDir = path31.join(damagedGitDir, "refs", "heads");
14797
- const walk = (dir, prefix) => {
14798
- let entries;
14799
- try {
14800
- entries = fs10.readdirSync(dir, { withFileTypes: true });
14801
- } catch {
14802
- return;
14803
- }
14804
- for (const e of entries) {
14805
- const name = prefix ? `${prefix}/${e.name}` : e.name;
14806
- if (e.isDirectory())
14807
- walk(path31.join(dir, e.name), name);
14808
- else if (e.isFile()) {
14809
- try {
14810
- const oid = fs10.readFileSync(path31.join(dir, e.name), "utf8").trim();
14811
- if (OID2.test(oid))
14812
- tips.set(name, oid);
14813
- } catch {}
14814
- }
14815
- }
14816
- };
14817
- walk(headsDir, "");
14818
- return [...tips].map(([name, oid]) => ({ name, oid }));
14819
- }
14820
- async function looseCommitTips(dir, cache2) {
14821
- const objectsDir = path31.join(gitDirFor(dir), "objects");
14822
- const oids = [];
14823
- let fanouts;
14824
- try {
14825
- fanouts = fs10.readdirSync(objectsDir).filter((e) => /^[0-9a-f]{2}$/.test(e));
14826
- } catch {
14827
- return [];
14828
- }
14829
- for (const fan of fanouts) {
14830
- let files;
14831
- try {
14832
- files = fs10.readdirSync(path31.join(objectsDir, fan));
14833
- } catch {
14834
- continue;
14835
- }
14836
- for (const f of files) {
14837
- if (/^[0-9a-f]{38}$/.test(f))
14838
- oids.push(fan + f);
14839
- }
14840
- }
14841
- const commits = new Map;
14842
- for (const oid of oids) {
14843
- try {
14844
- const obj = await git9.readObject({ fs: fs10, dir, cache: cache2, oid, format: "parsed" });
14845
- if (obj.type === "commit") {
14846
- commits.set(oid, obj.object.parent ?? []);
14847
- }
14848
- } catch {}
14849
- }
14850
- const isParent = new Set;
14851
- for (const parents of commits.values())
14852
- for (const p of parents)
14853
- isParent.add(p);
14854
- return [...commits.keys()].filter((oid) => !isParent.has(oid));
14855
- }
14856
- async function targetBranch(dir) {
14857
- try {
14858
- const branches = await git9.listBranches({ fs: fs10, dir });
14859
- if (branches.includes("main"))
14860
- return "main";
14861
- if (branches.includes("master"))
14862
- return "master";
14863
- if (branches.length > 0)
14864
- return branches[0];
14865
- } catch {}
14866
- return "main";
14867
- }
14868
- async function repairRepo(options) {
14869
- const dir = await repoDirFor(options.projectDir);
14870
- return withRepoLock(dir, () => repairLocked(dir, options));
14871
- }
14872
- async function repairLocked(dir, options) {
14873
- const logger = resolveLogger(options.logFile, "repair");
14874
- const gitDir = gitDirFor(dir);
14875
- const actions = [];
14876
- const snapshotFiles = async (message) => {
14877
- try {
14878
- if (await hasPendingChanges(dir)) {
14879
- await snapshotWorkingTreeUnlocked({
14880
- projectDir: dir,
14881
- repoRoot: dir,
14882
- message,
14883
- authorName: options.authorName,
14884
- authorEmail: options.authorEmail
14885
- });
14886
- }
14887
- } catch (e) {
14888
- if (!isNoChangesError(e))
14889
- throw e;
14890
- }
14891
- };
14892
- try {
14893
- if (fs10.existsSync(gitDir)) {
14894
- const sweep = await sweepStaleLocks(gitDir);
14895
- if (sweep === "fresh") {
14896
- logger.info("repair", "fresh lock present — deferring");
14897
- return { status: "retry_later", message: MSG_RETRY, actions, retryAfterMs: 30000 };
14898
- }
14899
- if (sweep === "swept") {
14900
- actions.push("Removed leftover lock files from an interrupted operation.");
14901
- logger.info("repair", "swept stale locks");
14902
- }
14903
- }
14904
- if (fs10.existsSync(gitDir)) {
14905
- let removedState = false;
14906
- for (const name of INTERRUPTED_STATE) {
14907
- const p = path31.join(gitDir, name);
14908
- if (fs10.existsSync(p)) {
14909
- await rm10(p, { recursive: true, force: true });
14910
- removedState = true;
14911
- }
14912
- }
14913
- if (removedState) {
14914
- actions.push("Cleared an unfinished operation another program left behind.");
14915
- logger.info("repair", "removed interrupted-operation state");
14916
- await rebuildIndexFromHead(dir);
14917
- } else if (!await indexReadable(dir)) {
14918
- await rebuildIndexFromHead(dir);
14919
- actions.push("Rebuilt the project's tracking information.");
14920
- logger.info("repair", "rebuilt corrupt index");
14921
- }
14922
- }
14923
- let health = await inspectRepo({ repoDir: dir }, { checkLocalChanges: false });
14924
- if (health.hasGitDir && !health.headUnreadable && health.isDetachedHead) {
14925
- await snapshotFiles("Saved your latest changes");
14926
- const headOid = await git9.resolveRef({ fs: fs10, dir, ref: "HEAD" });
14927
- const rescueRef = `refs/heads/recovered-${stamp()}`;
14928
- await git9.writeRef({ fs: fs10, dir, ref: rescueRef, value: headOid, force: true });
14929
- const branch = await targetBranch(dir);
14930
- const branchExists = (await git9.listBranches({ fs: fs10, dir })).includes(branch);
14931
- if (!branchExists) {
14932
- await git9.writeRef({ fs: fs10, dir, ref: `refs/heads/${branch}`, value: headOid, force: true });
14933
- }
14934
- await git9.writeRef({
14935
- fs: fs10,
14936
- dir,
14937
- ref: "HEAD",
14938
- value: `refs/heads/${branch}`,
14939
- symbolic: true,
14940
- force: true
14941
- });
14942
- const cache2 = {};
14943
- await git9.checkout({ fs: fs10, dir, cache: cache2, ref: branch, force: true });
14944
- const branchTip = await git9.resolveRef({ fs: fs10, dir, ref: branch });
14945
- if (branchTip !== headOid) {
14946
- await convergeMerge({
14947
- dir,
14948
- cache: cache2,
14949
- branch,
14950
- theirs: headOid,
14951
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
14952
- authorName: options.authorName,
14953
- authorEmail: options.authorEmail,
14954
- allowUnrelatedHistories: true
14955
- });
14956
- }
14957
- actions.push("Reconnected your work to the project's version line.");
14958
- logger.info("repair", "reattached detached HEAD", { rescueRef });
14959
- }
14960
- health = await inspectRepo({ repoDir: dir }, { checkLocalChanges: false });
14961
- if (classifyFromHealth(health, { minLockAgeMs: 0 }) === null) {
14962
- await snapshotFiles("Saved your latest changes");
14963
- logger.info("repair", "repaired in place", { actions: actions.length });
14964
- return { status: "repaired", message: MSG_REPAIRED, actions };
14965
- }
14966
- return await recloneWithSalvage(dir, options, actions, logger, snapshotFiles);
14967
- } catch (e) {
14968
- logger.error("repair", "repair failed", { error: e instanceof Error ? e.message : String(e) });
14969
- return { status: "failed", message: MSG_FAILED, actions };
14970
- }
14971
- }
14972
- async function recloneWithSalvage(dir, options, actions, logger, snapshotFiles) {
14973
- const gitDir = gitDirFor(dir);
14974
- let remoteUrl;
14975
- let credential;
14976
- try {
14977
- const transport = await resolveTransport(dir, {
14978
- credential: options.credential,
14979
- tokenStore: options.tokenStore
14980
- });
14981
- remoteUrl = transport.url;
14982
- credential = transport.credential;
14983
- } catch {
14984
- remoteUrl = undefined;
14985
- }
14986
- let freshGitDir;
14987
- let tempDir;
14988
- if (remoteUrl) {
14989
- tempDir = await mkdtemp4(path31.join(os4.tmpdir(), "gutterpress-repair-"));
14990
- const cloneDir = path31.join(tempDir, "clone");
14991
- try {
14992
- await cloneRepository({
14993
- url: remoteUrl,
14994
- dir: cloneDir,
14995
- ...credential ? { credential } : {}
14996
- });
14997
- } catch (e) {
14998
- await rm10(tempDir, { recursive: true, force: true });
14999
- logger.warn("repair", "re-clone failed — deferring", {
15000
- error: e instanceof Error ? e.message : String(e)
15001
- });
15002
- return {
15003
- status: "retry_later",
15004
- message: "The online copy couldn't be reached to finish the repair. Your files are safe — it will be tried again.",
15005
- actions,
15006
- retryAfterMs: 60000
15007
- };
15008
- }
15009
- freshGitDir = path31.join(cloneDir, ".git");
15010
- } else {
15011
- tempDir = await mkdtemp4(path31.join(os4.tmpdir(), "gutterpress-repair-"));
15012
- const initDir = path31.join(tempDir, "init");
15013
- fs10.mkdirSync(initDir, { recursive: true });
15014
- await git9.init({ fs: fs10, dir: initDir, defaultBranch: "main" });
15015
- freshGitDir = path31.join(initDir, ".git");
15016
- }
15017
- let damagedBackup;
15018
- if (fs10.existsSync(gitDir)) {
15019
- damagedBackup = path31.join(dir, `.git-damaged-${stamp()}`);
15020
- await rename6(gitDir, damagedBackup);
15021
- actions.push("Kept the old history folder on disk as a backup.");
15022
- }
15023
- await rename6(freshGitDir, gitDir);
15024
- if (tempDir)
15025
- await rm10(tempDir, { recursive: true, force: true });
15026
- actions.push(remoteUrl ? "Rebuilt the project's history from the online copy." : "Started a fresh history for the project (no online copy is connected).");
15027
- logger.info("repair", "replaced .git", { fromRemote: Boolean(remoteUrl), damagedBackup });
15028
- if (damagedBackup) {
15029
- const from = path31.join(damagedBackup, "objects");
15030
- if (fs10.existsSync(from)) {
15031
- await cp6(from, path31.join(gitDir, "objects"), {
15032
- recursive: true,
15033
- force: false,
15034
- errorOnExist: false
15035
- }).catch(() => {});
15036
- }
15037
- const branch = await targetBranch(dir);
15038
- let branchExists = (await git9.listBranches({ fs: fs10, dir })).includes(branch);
15039
- const scanCache = {};
15040
- const tips = new Map;
15041
- for (const tip of readDamagedBranchTips(damagedBackup)) {
15042
- tips.set(tip.oid, tip.name);
15043
- }
15044
- for (const oid of await looseCommitTips(dir, scanCache)) {
15045
- if (!tips.has(oid))
15046
- tips.set(oid, "recovered-work");
15047
- }
15048
- for (const [oid, name] of tips) {
15049
- const cache2 = {};
15050
- try {
15051
- await git9.readCommit({ fs: fs10, dir, cache: cache2, oid });
15052
- } catch {
15053
- logger.warn("repair", "old tip unreadable — left in the backup", { tip: name });
15054
- continue;
15055
- }
15056
- if (!branchExists) {
15057
- await git9.writeRef({ fs: fs10, dir, ref: `refs/heads/${branch}`, value: oid, force: true });
15058
- await git9.writeRef({
15059
- fs: fs10,
15060
- dir,
15061
- ref: "HEAD",
15062
- value: `refs/heads/${branch}`,
15063
- symbolic: true,
15064
- force: true
15065
- });
15066
- await rebuildIndexFromHead(dir);
15067
- branchExists = true;
15068
- actions.push("Restored your saved history from the backup.");
15069
- continue;
15070
- }
15071
- try {
15072
- const branchTip = await git9.resolveRef({ fs: fs10, dir, ref: branch });
15073
- if (branchTip === oid)
15074
- continue;
15075
- await convergeMerge({
15076
- dir,
15077
- cache: cache2,
15078
- branch,
15079
- theirs: oid,
15080
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
15081
- authorName: options.authorName,
15082
- authorEmail: options.authorEmail,
15083
- allowUnrelatedHistories: true
15084
- });
15085
- actions.push("Brought your earlier saved versions back into the history.");
15086
- } catch (e) {
15087
- logger.warn("repair", "old tip could not be merged — pinned as a recovery branch", {
15088
- tip: name,
15089
- error: e instanceof Error ? e.message : String(e)
15090
- });
15091
- await git9.writeRef({
15092
- fs: fs10,
15093
- dir,
15094
- ref: `refs/heads/recovered-${stamp()}-${name.replace(/[^A-Za-z0-9_-]/g, "_")}`,
15095
- value: oid,
15096
- force: true
15097
- }).catch(() => {});
15098
- }
15099
- }
15100
- }
15101
- await snapshotFiles("Saved your files after repairing the project");
15102
- return {
15103
- status: "repaired",
15104
- message: MSG_REPAIRED,
15105
- actions,
15106
- ...damagedBackup ? { damagedGitBackupPath: damagedBackup } : {}
15107
- };
15108
- }
15109
- export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, redactCredential, defaultConfigDir, FileTokenStore, extractUrlCredential, migrateUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, APP_HEARTBEAT_FRESH_MS, heartbeatTtlMs, appHeartbeatPath, writeAppHeartbeat, removeAppHeartbeat, readAppHeartbeat, isAppHeartbeatFresh, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, listGitHubRepositories, listRepoBooks, listGitHubBranches, STALE_LOCK_MIN_AGE_MS, RepoNeedsRecoveryError, isRepoNeedsRecoveryError, isLikelyRepoCorruption, classifyFromHealth, inspectRepo, verifyRepoReadable, isUnbornRepo, preflightStructuralReason, buildPreflightDiagnostics, SYNC_SNAPSHOT_MESSAGE, provenancePath, readProjectProvenance, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, normalizeForgeHost, knownForgeTokenUrl, connectGenericHost, GenericTokenAuthProvider, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, pullChanges, pushChanges, readImageVersion, keepImageVersion, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, resolvePublishRequest, runPublish, connectPublishProvider, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig, repairRepo };
14412
+ export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, resolvePublishRequest, runPublish, connectPublishProvider, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };