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
@@ -2,11 +2,9 @@ import {
2
2
  PACKAGE_META,
3
3
  PACKAGE_VERSION,
4
4
  convertToPdfxCmyk,
5
- credentialHostKey,
6
5
  debug,
7
6
  defaultConfigDir,
8
7
  error,
9
- extractUrlCredential,
10
8
  hasLiveTransparency,
11
9
  info,
12
10
  log,
@@ -15,12 +13,12 @@ import {
15
13
  stampCreator,
16
14
  stripAnnotations,
17
15
  warn
18
- } from "./cli-v935c2ws.js";
16
+ } from "./cli-mcb484g3.js";
19
17
  import {
20
18
  DEBOUNCE,
21
19
  UsageError,
22
20
  resolvePort
23
- } from "./cli-7e79d7se.js";
21
+ } from "./cli-dx6sxj2t.js";
24
22
  import {
25
23
  MARGIN_BOX_IGNORED_PROPERTIES,
26
24
  RENDER_TIMEOUT_MS,
@@ -29,7 +27,7 @@ import {
29
27
  getAssetPath,
30
28
  getBrowser,
31
29
  prewarmBrowser
32
- } from "./cli-afm8pa0h.js";
30
+ } from "./cli-ak1tagkr.js";
33
31
  import {
34
32
  INSTALL_HINTS,
35
33
  execCapture,
@@ -42,19 +40,10 @@ import {
42
40
  BuildError
43
41
  } from "./cli-46ycxe6r.js";
44
42
  import {
45
- gitDirFor,
46
- gitScopeFor,
47
- hasPendingChanges,
48
- hasUncommittedChanges,
49
- isNoChangesError,
50
- resolveGitAuthor,
51
- resolveLogger,
52
- snapshotWorkingTreeUnlocked,
53
- withRepoLock
54
- } from "./cli-k1065rkg.js";
43
+ hasUncommittedChanges
44
+ } from "./cli-w692g6dx.js";
55
45
  import {
56
- detectProjectSource,
57
- syncRemoteFor
46
+ detectProjectSource
58
47
  } from "./cli-v5mp7a6q.js";
59
48
  import {
60
49
  __require
@@ -5148,11 +5137,9 @@ var check22 = {
5148
5137
  };
5149
5138
  registerCheck(check22);
5150
5139
 
5151
- // src/checks/asset/image-file-size.ts
5152
- import { stat as stat3 } from "node:fs/promises";
5153
-
5154
- // src/lib/image-inspect.ts
5155
- import { stat as stat2, readFile as readFile15 } from "node:fs/promises";
5140
+ // src/checks/source/merge-markers.ts
5141
+ import { readFile as readFile15 } from "node:fs/promises";
5142
+ import path4 from "node:path";
5156
5143
 
5157
5144
  // src/checks/asset/extensions.ts
5158
5145
  var RASTER_INSPECTABLE_EXTS = [
@@ -5175,27 +5162,108 @@ var ASSET_SCAN_IGNORE_GLOBS = [
5175
5162
  "**/dist/**"
5176
5163
  ];
5177
5164
 
5165
+ // src/checks/source/merge-markers.ts
5166
+ var OPEN_SENTINEL = "<<<<<<< your version";
5167
+ var CLOSE_SENTINEL = ">>>>>>> online version";
5168
+ function withoutOnlineTag(relPath) {
5169
+ return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
5170
+ }
5171
+ var check23 = {
5172
+ id: "source.sync.merge-markers",
5173
+ name: "Combined Versions",
5174
+ description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
5175
+ category: "source",
5176
+ phase: "pre-build",
5177
+ async run(ctx) {
5178
+ const results = [];
5179
+ const textFiles = [...ctx.markdownFiles ?? [], ...ctx.cssFiles ?? []].sort();
5180
+ for (const file of textFiles) {
5181
+ let content;
5182
+ try {
5183
+ content = await readFile15(file, "utf8");
5184
+ } catch {
5185
+ results.push(inspectionFailed(check23.id, `Could not read source file: ${file}`, { file }));
5186
+ continue;
5187
+ }
5188
+ if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
5189
+ continue;
5190
+ let inBlock = false;
5191
+ const lines = content.split(`
5192
+ `);
5193
+ for (let i = 0;i < lines.length; i++) {
5194
+ const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
5195
+ if (line === OPEN_SENTINEL) {
5196
+ inBlock = true;
5197
+ results.push(finding(check23.id, {
5198
+ severity: "error",
5199
+ code: "two-versions-passage",
5200
+ message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
5201
+ file,
5202
+ line: i + 1
5203
+ }));
5204
+ } else if (line === CLOSE_SENTINEL) {
5205
+ if (!inBlock) {
5206
+ results.push(finding(check23.id, {
5207
+ severity: "error",
5208
+ code: "leftover-version-marker",
5209
+ message: "This marker line is left over from combining two versions — delete it.",
5210
+ file,
5211
+ line: i + 1
5212
+ }));
5213
+ }
5214
+ inBlock = false;
5215
+ }
5216
+ }
5217
+ }
5218
+ try {
5219
+ const { glob } = await import("glob");
5220
+ const siblings = await glob(["**/*.online", "**/*.online.*"], {
5221
+ cwd: ctx.inputDir,
5222
+ absolute: true,
5223
+ ignore: [...ASSET_SCAN_IGNORE_GLOBS]
5224
+ });
5225
+ for (const sibling of siblings.sort()) {
5226
+ const rel = path4.relative(ctx.inputDir, sibling).split(path4.sep).join("/");
5227
+ results.push(finding(check23.id, {
5228
+ severity: "warning",
5229
+ code: "kept-both-versions",
5230
+ message: `Two versions of ${withoutOnlineTag(rel)} are in your project — keep the one you want, then delete the other.`,
5231
+ file: sibling
5232
+ }));
5233
+ }
5234
+ } catch (error2) {
5235
+ results.push(inspectionFailed(check23.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
5236
+ }
5237
+ return results;
5238
+ }
5239
+ };
5240
+ registerCheck(check23);
5241
+
5242
+ // src/checks/asset/image-file-size.ts
5243
+ import { stat as stat3 } from "node:fs/promises";
5244
+
5178
5245
  // src/lib/image-inspect.ts
5246
+ import { stat as stat2, readFile as readFile16 } from "node:fs/promises";
5179
5247
  var DEFAULT_DPI = 72;
5180
5248
  var cache = new Map;
5181
- async function inspectImage(path4) {
5249
+ async function inspectImage(path5) {
5182
5250
  let st;
5183
5251
  try {
5184
- st = await stat2(path4);
5252
+ st = await stat2(path5);
5185
5253
  } catch {
5186
5254
  return null;
5187
5255
  }
5188
- const hit = cache.get(path4);
5256
+ const hit = cache.get(path5);
5189
5257
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
5190
5258
  return hit.info;
5191
5259
  let info2 = null;
5192
5260
  try {
5193
- const buf = await readFile15(path4);
5261
+ const buf = await readFile16(path5);
5194
5262
  info2 = parseImage(buf);
5195
5263
  } catch {
5196
5264
  info2 = null;
5197
5265
  }
5198
- cache.set(path4, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
5266
+ cache.set(path5, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
5199
5267
  return info2;
5200
5268
  }
5201
5269
  function parseImage(b) {
@@ -5395,7 +5463,7 @@ async function collectImageFiles(dirs, exts, ignore = ASSET_SCAN_IGNORE_GLOBS) {
5395
5463
  }
5396
5464
 
5397
5465
  // src/checks/asset/image-file-size.ts
5398
- var check23 = {
5466
+ var check24 = {
5399
5467
  id: "asset.image.file-size",
5400
5468
  name: "Image File Size",
5401
5469
  description: "Checks that image files do not exceed the maximum size limit",
@@ -5416,14 +5484,14 @@ var check23 = {
5416
5484
  if (info2.size > maxSize) {
5417
5485
  const sizeMb = (info2.size / 1e6).toFixed(1);
5418
5486
  const maxMb = (maxSize / 1e6).toFixed(1);
5419
- results.push(finding(check23.id, {
5487
+ results.push(finding(check24.id, {
5420
5488
  severity: "warning",
5421
5489
  message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
5422
5490
  file
5423
5491
  }));
5424
5492
  }
5425
5493
  } catch {
5426
- results.push(inspectionFailed(check23.id, `Could not stat image file: ${file}`, {
5494
+ results.push(inspectionFailed(check24.id, `Could not stat image file: ${file}`, {
5427
5495
  file
5428
5496
  }));
5429
5497
  }
@@ -5431,10 +5499,10 @@ var check23 = {
5431
5499
  return results;
5432
5500
  }
5433
5501
  };
5434
- registerCheck(check23);
5502
+ registerCheck(check24);
5435
5503
 
5436
5504
  // src/checks/asset/image-resolution.ts
5437
- var check24 = {
5505
+ var check25 = {
5438
5506
  id: "asset.image.resolution",
5439
5507
  name: "Image Resolution",
5440
5508
  description: "Checks source image DPI from embedded density metadata",
@@ -5456,7 +5524,7 @@ var check24 = {
5456
5524
  const { xDpi, yDpi } = info2;
5457
5525
  if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
5458
5526
  results.push({
5459
- checkId: check24.id,
5527
+ checkId: check25.id,
5460
5528
  severity: "warning",
5461
5529
  message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
5462
5530
  file
@@ -5466,7 +5534,7 @@ var check24 = {
5466
5534
  return results;
5467
5535
  }
5468
5536
  };
5469
- registerCheck(check24);
5537
+ registerCheck(check25);
5470
5538
 
5471
5539
  // src/checks/asset/image-color-space.ts
5472
5540
  var LABEL = {
@@ -5474,7 +5542,7 @@ var LABEL = {
5474
5542
  gray: "Gray",
5475
5543
  cmyk: "CMYK"
5476
5544
  };
5477
- var check25 = {
5545
+ var check26 = {
5478
5546
  id: "asset.image.color-space",
5479
5547
  name: "Image Color Space",
5480
5548
  description: "Validates image color spaces against allowed list",
@@ -5495,7 +5563,7 @@ var check25 = {
5495
5563
  const cs = info2?.colorSpace;
5496
5564
  if (cs && !allowedLower.has(cs)) {
5497
5565
  results.push({
5498
- checkId: check25.id,
5566
+ checkId: check26.id,
5499
5567
  severity: "warning",
5500
5568
  message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
5501
5569
  file
@@ -5505,10 +5573,10 @@ var check25 = {
5505
5573
  return results;
5506
5574
  }
5507
5575
  };
5508
- registerCheck(check25);
5576
+ registerCheck(check26);
5509
5577
 
5510
5578
  // src/checks/asset/image-alpha.ts
5511
- var check26 = {
5579
+ var check27 = {
5512
5580
  id: "asset.image.alpha-channel",
5513
5581
  name: "Image Alpha Channel",
5514
5582
  description: "Checks for alpha channels in PNG/TIFF images",
@@ -5526,7 +5594,7 @@ var check26 = {
5526
5594
  const info2 = await inspectImage(file);
5527
5595
  if (info2?.hasAlpha) {
5528
5596
  results.push({
5529
- checkId: check26.id,
5597
+ checkId: check27.id,
5530
5598
  severity: "warning",
5531
5599
  message: "Image contains alpha channel, which may cause print issues",
5532
5600
  file
@@ -5536,10 +5604,10 @@ var check26 = {
5536
5604
  return results;
5537
5605
  }
5538
5606
  };
5539
- registerCheck(check26);
5607
+ registerCheck(check27);
5540
5608
 
5541
5609
  // src/checks/asset/image-tac.ts
5542
- var check27 = {
5610
+ var check28 = {
5543
5611
  id: "asset.image.tac-raster",
5544
5612
  name: "Image TAC (Raster)",
5545
5613
  description: "Rasterizes and checks TAC per image using Ghostscript",
@@ -5558,7 +5626,7 @@ var check27 = {
5558
5626
  const ghostscript = await resolveGhostscript();
5559
5627
  if (!ghostscript) {
5560
5628
  return [
5561
- inspectionFailed(check27.id, "Could not inspect image ink coverage: Ghostscript executable not found")
5629
+ inspectionFailed(check28.id, "Could not inspect image ink coverage: Ghostscript executable not found")
5562
5630
  ];
5563
5631
  }
5564
5632
  for (const file of files) {
@@ -5576,7 +5644,7 @@ var check27 = {
5576
5644
  if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
5577
5645
  const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
5578
5646
  if (tac > maxTac) {
5579
- results.push(finding(check27.id, {
5647
+ results.push(finding(check28.id, {
5580
5648
  severity: "warning",
5581
5649
  message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
5582
5650
  file,
@@ -5588,16 +5656,16 @@ var check27 = {
5588
5656
  }
5589
5657
  }
5590
5658
  } catch {
5591
- results.push(inspectionFailed(check27.id, `Could not inspect image ink coverage: ${file}`, { file }));
5659
+ results.push(inspectionFailed(check28.id, `Could not inspect image ink coverage: ${file}`, { file }));
5592
5660
  }
5593
5661
  }
5594
5662
  return results;
5595
5663
  }
5596
5664
  };
5597
- registerCheck(check27);
5665
+ registerCheck(check28);
5598
5666
 
5599
5667
  // src/checks/asset/approved-fonts.ts
5600
- var check28 = {
5668
+ var check29 = {
5601
5669
  id: "asset.font.approved-files",
5602
5670
  name: "Approved Font Files",
5603
5671
  description: "Checks font files against the approved file patterns",
@@ -5636,7 +5704,7 @@ var check28 = {
5636
5704
  for (const font of allFonts) {
5637
5705
  if (!approvedFonts.has(font)) {
5638
5706
  results.push({
5639
- checkId: check28.id,
5707
+ checkId: check29.id,
5640
5708
  severity: "warning",
5641
5709
  message: "Font file not in approved list",
5642
5710
  file: font
@@ -5646,7 +5714,7 @@ var check28 = {
5646
5714
  return results;
5647
5715
  }
5648
5716
  };
5649
- registerCheck(check28);
5717
+ registerCheck(check29);
5650
5718
 
5651
5719
  // src/checks/asset/font-license.ts
5652
5720
  import { existsSync as existsSync7 } from "node:fs";
@@ -5661,7 +5729,7 @@ var LICENSE_NAMES = [
5661
5729
  "OFL-1.1.txt",
5662
5730
  "COPYING"
5663
5731
  ];
5664
- var check29 = {
5732
+ var check30 = {
5665
5733
  id: "asset.font.license",
5666
5734
  name: "Font License",
5667
5735
  description: "Checks for license files in font directories",
@@ -5688,7 +5756,7 @@ var check29 = {
5688
5756
  const hasLicense = LICENSE_NAMES.some((name) => existsSync7(resolve6(fontDir, name)));
5689
5757
  if (!hasLicense) {
5690
5758
  results.push({
5691
- checkId: check29.id,
5759
+ checkId: check30.id,
5692
5760
  severity: "warning",
5693
5761
  message: `No font license file found in directory`,
5694
5762
  file: fontDir
@@ -5698,10 +5766,10 @@ var check29 = {
5698
5766
  return results;
5699
5767
  }
5700
5768
  };
5701
- registerCheck(check29);
5769
+ registerCheck(check30);
5702
5770
 
5703
5771
  // src/checks/heuristic/text-density.ts
5704
- var check30 = {
5772
+ var check31 = {
5705
5773
  id: "heuristic.whitespace.text-density",
5706
5774
  name: "Text Density",
5707
5775
  description: "Checks characters-per-page ratio",
@@ -5730,7 +5798,7 @@ var check30 = {
5730
5798
  });
5731
5799
  if (lowPages.length > 0) {
5732
5800
  results.push({
5733
- checkId: check30.id,
5801
+ checkId: check31.id,
5734
5802
  severity: "info",
5735
5803
  message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
5736
5804
  file: ctx.pdfPath
@@ -5738,7 +5806,7 @@ var check30 = {
5738
5806
  }
5739
5807
  if (highPages.length > 0) {
5740
5808
  results.push({
5741
- checkId: check30.id,
5809
+ checkId: check31.id,
5742
5810
  severity: "info",
5743
5811
  message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
5744
5812
  file: ctx.pdfPath
@@ -5747,11 +5815,11 @@ var check30 = {
5747
5815
  return results;
5748
5816
  }
5749
5817
  };
5750
- registerCheck(check30);
5818
+ registerCheck(check31);
5751
5819
 
5752
5820
  // src/checks/heuristic/section-density.ts
5753
- import { readFile as readFile16 } from "node:fs/promises";
5754
- var check31 = {
5821
+ import { readFile as readFile17 } from "node:fs/promises";
5822
+ var check32 = {
5755
5823
  id: "heuristic.chunking.section-density",
5756
5824
  name: "Section Density",
5757
5825
  description: "Checks heading/paragraph/callout density from source Markdown",
@@ -5767,7 +5835,7 @@ var check31 = {
5767
5835
  const results = [];
5768
5836
  for (const file of files) {
5769
5837
  try {
5770
- const content = await readFile16(file, "utf8");
5838
+ const content = await readFile17(file, "utf8");
5771
5839
  const lines = content.split(`
5772
5840
  `);
5773
5841
  let currentSectionStart = 0;
@@ -5777,7 +5845,7 @@ var check31 = {
5777
5845
  const line = lines[i];
5778
5846
  if (/^#{1,6}\s/.test(line)) {
5779
5847
  if (paragraphCount > maxParas) {
5780
- results.push(finding(check31.id, {
5848
+ results.push(finding(check32.id, {
5781
5849
  severity: "info",
5782
5850
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
5783
5851
  file,
@@ -5797,7 +5865,7 @@ var check31 = {
5797
5865
  }
5798
5866
  }
5799
5867
  if (paragraphCount > maxParas) {
5800
- results.push(finding(check31.id, {
5868
+ results.push(finding(check32.id, {
5801
5869
  severity: "info",
5802
5870
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
5803
5871
  file,
@@ -5805,7 +5873,7 @@ var check31 = {
5805
5873
  }));
5806
5874
  }
5807
5875
  } catch {
5808
- results.push(inspectionFailed(check31.id, `Could not read source file: ${file}`, {
5876
+ results.push(inspectionFailed(check32.id, `Could not read source file: ${file}`, {
5809
5877
  file
5810
5878
  }));
5811
5879
  }
@@ -5813,10 +5881,10 @@ var check31 = {
5813
5881
  return results;
5814
5882
  }
5815
5883
  };
5816
- registerCheck(check31);
5884
+ registerCheck(check32);
5817
5885
 
5818
5886
  // src/checks/heuristic/layer-count.ts
5819
- var check32 = {
5887
+ var check33 = {
5820
5888
  id: "heuristic.decoration.layer-count",
5821
5889
  name: "Layer Count",
5822
5890
  description: "Counts image objects per page",
@@ -5841,7 +5909,7 @@ var check32 = {
5841
5909
  heavyPages.sort((a, b) => a - b);
5842
5910
  return [
5843
5911
  {
5844
- checkId: check32.id,
5912
+ checkId: check33.id,
5845
5913
  severity: "info",
5846
5914
  message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
5847
5915
  file: ctx.pdfPath
@@ -5851,10 +5919,10 @@ var check32 = {
5851
5919
  return [];
5852
5920
  }
5853
5921
  };
5854
- registerCheck(check32);
5922
+ registerCheck(check33);
5855
5923
 
5856
5924
  // src/checks/heuristic/placement-variance.ts
5857
- var check33 = {
5925
+ var check34 = {
5858
5926
  id: "heuristic.layout.placement-variance",
5859
5927
  name: "Placement Variance",
5860
5928
  description: "Analyzes text baseline coordinates for layout consistency",
@@ -5872,7 +5940,7 @@ var check33 = {
5872
5940
  const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
5873
5941
  return [
5874
5942
  {
5875
- checkId: check33.id,
5943
+ checkId: check34.id,
5876
5944
  severity: "info",
5877
5945
  message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
5878
5946
  file: ctx.pdfPath
@@ -5880,21 +5948,21 @@ var check33 = {
5880
5948
  ];
5881
5949
  }
5882
5950
  };
5883
- registerCheck(check33);
5951
+ registerCheck(check34);
5884
5952
 
5885
5953
  // src/checks/tool-check.ts
5886
5954
  async function checkToolAvailability(config, opts = {}) {
5887
5955
  const { checks: checks2 } = selectChecks(opts, config);
5888
5956
  const toolToChecks = new Map;
5889
- for (const check34 of checks2) {
5890
- if (!check34.requiredTools?.length)
5957
+ for (const check35 of checks2) {
5958
+ if (!check35.requiredTools?.length)
5891
5959
  continue;
5892
- for (const tool of check34.requiredTools) {
5960
+ for (const tool of check35.requiredTools) {
5893
5961
  const existing = toolToChecks.get(tool);
5894
5962
  if (existing) {
5895
- existing.push(check34.id);
5963
+ existing.push(check35.id);
5896
5964
  } else {
5897
- toolToChecks.set(tool, [check34.id]);
5965
+ toolToChecks.set(tool, [check35.id]);
5898
5966
  }
5899
5967
  }
5900
5968
  }
@@ -5910,11 +5978,11 @@ async function checkToolAvailability(config, opts = {}) {
5910
5978
  const missing = results.filter((r) => !r.found).map((r) => r.tool);
5911
5979
  const missingSet = new Set(missing);
5912
5980
  const skippedChecks = [];
5913
- for (const check34 of checks2) {
5914
- if (!check34.requiredTools?.length)
5981
+ for (const check35 of checks2) {
5982
+ if (!check35.requiredTools?.length)
5915
5983
  continue;
5916
- if (check34.requiredTools.some((t) => missingSet.has(t))) {
5917
- skippedChecks.push(check34.id);
5984
+ if (check35.requiredTools.some((t) => missingSet.has(t))) {
5985
+ skippedChecks.push(check35.id);
5918
5986
  }
5919
5987
  }
5920
5988
  return { available, missing, skippedChecks, toolToChecks };
@@ -5936,7 +6004,7 @@ import { existsSync as existsSync8 } from "node:fs";
5936
6004
  import { dirname as dirname4, join as join3, resolve as resolve7 } from "node:path";
5937
6005
 
5938
6006
  // src/lib/output-paths.ts
5939
- import path4 from "node:path";
6007
+ import path5 from "node:path";
5940
6008
 
5941
6009
  // src/lib/slug.ts
5942
6010
  function slugify(name, fallback = "") {
@@ -5950,7 +6018,7 @@ function bookSlug(title) {
5950
6018
  return slugify(title ?? "", "book");
5951
6019
  }
5952
6020
  function resolveOutputDir(manifestDir, title) {
5953
- return path4.resolve(manifestDir, DIST_DIRNAME, bookSlug(title));
6021
+ return path5.resolve(manifestDir, DIST_DIRNAME, bookSlug(title));
5954
6022
  }
5955
6023
  function artifactName(title, format) {
5956
6024
  return `${bookSlug(title)}-${format}.pdf`;
@@ -5960,7 +6028,17 @@ function artifactName(title, format) {
5960
6028
  var BOOK_HTML_FILENAME = "book.html";
5961
6029
 
5962
6030
  // src/lib/markdown/index.ts
5963
- import { readdir as readdir3, readFile as readFile17, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
6031
+ import { readdir as readdir3, readFile as readFile18, writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
6032
+
6033
+ // src/lib/remote-auth/sync-messages.ts
6034
+ function isOnlineSibling(filepath) {
6035
+ const dot = filepath.lastIndexOf(".");
6036
+ const slash = filepath.lastIndexOf("/");
6037
+ const stem = dot > slash + 1 ? filepath.slice(0, dot) : filepath;
6038
+ return stem.endsWith(".online");
6039
+ }
6040
+
6041
+ // src/lib/markdown/index.ts
5964
6042
  import { join as join2 } from "node:path";
5965
6043
 
5966
6044
  // src/lib/markdown/chapter-id.ts
@@ -6229,7 +6307,7 @@ ${bodyContent}
6229
6307
  async function resolveActiveMarkdownFiles(inputDir, configuredFiles) {
6230
6308
  if (configuredFiles && configuredFiles.length > 0)
6231
6309
  return configuredFiles;
6232
- return (await readdir3(inputDir)).filter((f) => f.endsWith(".md")).sort();
6310
+ return (await readdir3(inputDir)).filter((f) => f.endsWith(".md") && !isOnlineSibling(f)).sort();
6233
6311
  }
6234
6312
  async function renderChapters(inputDir, opts = {}) {
6235
6313
  const styles = await resolveActiveStyles(inputDir, opts.styles);
@@ -6244,7 +6322,7 @@ async function renderChapters(inputDir, opts = {}) {
6244
6322
  }
6245
6323
  return assembleBookHtml({
6246
6324
  files,
6247
- readText: (relPath) => readFile17(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
6325
+ readText: (relPath) => readFile18(join2(inputDir, canonicalChapterId(relPath)), "utf-8"),
6248
6326
  projectCss: inlined.css,
6249
6327
  title: opts.title,
6250
6328
  plugins: opts.plugins,
@@ -6359,25 +6437,25 @@ async function runChecks(ctx, opts = {}) {
6359
6437
  }
6360
6438
  const release = retainPdfCache();
6361
6439
  try {
6362
- for (const check34 of checks2) {
6440
+ for (const check35 of checks2) {
6363
6441
  try {
6364
- const results = await check34.run(ctx);
6365
- const severityOverride = getCheckSeverityOverride(check34.id, ctx.config);
6442
+ const results = await check35.run(ctx);
6443
+ const severityOverride = getCheckSeverityOverride(check35.id, ctx.config);
6366
6444
  if (severityOverride) {
6367
6445
  for (const r of results) {
6368
6446
  r.severity = severityOverride;
6369
6447
  }
6370
6448
  }
6371
6449
  if (results.length === 0) {
6372
- passed.push(check34.id);
6450
+ passed.push(check35.id);
6373
6451
  } else {
6374
6452
  allResults.push(...results);
6375
6453
  }
6376
6454
  } catch (err) {
6377
6455
  allResults.push({
6378
- checkId: check34.id,
6456
+ checkId: check35.id,
6379
6457
  severity: "error",
6380
- message: `Check "${check34.id}" threw: ${err instanceof Error ? err.message : String(err)}`
6458
+ message: `Check "${check35.id}" threw: ${err instanceof Error ? err.message : String(err)}`
6381
6459
  });
6382
6460
  }
6383
6461
  }
@@ -6734,7 +6812,7 @@ function buildPdfSummaryLines(results) {
6734
6812
  }
6735
6813
 
6736
6814
  // src/lib/build-runner.ts
6737
- import path7 from "node:path";
6815
+ import path8 from "node:path";
6738
6816
  import fs2 from "node:fs";
6739
6817
  import fsp2 from "node:fs/promises";
6740
6818
  import os2 from "node:os";
@@ -6742,9 +6820,9 @@ import { randomBytes } from "node:crypto";
6742
6820
 
6743
6821
  // src/lib/build-fingerprint.ts
6744
6822
  import * as fs from "node:fs";
6745
- import { mkdir as mkdir3, readFile as readFile18, writeFile as writeFile3 } from "node:fs/promises";
6823
+ import { mkdir as mkdir3, readFile as readFile19, writeFile as writeFile3 } from "node:fs/promises";
6746
6824
  import { createHash as createHash4 } from "node:crypto";
6747
- import path5 from "node:path";
6825
+ import path6 from "node:path";
6748
6826
  import git from "isomorphic-git";
6749
6827
  var FINGERPRINT_FILENAME = "build-fingerprint.json";
6750
6828
  var VERSION_TIMEOUT_MS = 4000;
@@ -6793,7 +6871,7 @@ async function getGitRevision(sourceDir) {
6793
6871
  const candidateDirs = [sourceDir, process.cwd()].filter((v) => Boolean(v));
6794
6872
  const seen = new Set;
6795
6873
  for (const dir of candidateDirs) {
6796
- const abs = path5.resolve(dir);
6874
+ const abs = path6.resolve(dir);
6797
6875
  if (seen.has(abs))
6798
6876
  continue;
6799
6877
  seen.add(abs);
@@ -6829,8 +6907,8 @@ async function getEngineBundleHash() {
6829
6907
  getAssetPath("engine/gutterpress-agent.js")
6830
6908
  ]);
6831
6909
  const [viewerBytes, agentBytes] = await Promise.all([
6832
- readFile18(viewerPath),
6833
- readFile18(agentPath)
6910
+ readFile19(viewerPath),
6911
+ readFile19(agentPath)
6834
6912
  ]);
6835
6913
  const hash = createHash4("sha256");
6836
6914
  hash.update(viewerBytes);
@@ -6878,8 +6956,8 @@ function sanitizeArgs(args) {
6878
6956
  return out;
6879
6957
  }
6880
6958
  async function writeBuildFingerprint(input) {
6881
- const outputDir = path5.resolve(input.outputDir);
6882
- const outPath = path5.join(outputDir, FINGERPRINT_FILENAME);
6959
+ const outputDir = path6.resolve(input.outputDir);
6960
+ const outPath = path6.join(outputDir, FINGERPRINT_FILENAME);
6883
6961
  const [tools, sourceRevision] = await Promise.all([
6884
6962
  getToolVersions(),
6885
6963
  getGitRevision(input.sourceDir)
@@ -6895,7 +6973,7 @@ async function writeBuildFingerprint(input) {
6895
6973
  iccPath: input.pdfx.iccPath,
6896
6974
  stripAnnotations: input.pdfx.stripAnnotations
6897
6975
  },
6898
- outputDir: input.recordedOutputDir ? path5.resolve(input.recordedOutputDir) : outputDir
6976
+ outputDir: input.recordedOutputDir ? path6.resolve(input.recordedOutputDir) : outputDir
6899
6977
  },
6900
6978
  sourceRevision,
6901
6979
  tools
@@ -6905,6 +6983,76 @@ async function writeBuildFingerprint(input) {
6905
6983
  return outPath;
6906
6984
  }
6907
6985
 
6986
+ // src/lib/build-preflight.ts
6987
+ var INSTALL_HINTS2 = {
6988
+ gs: INSTALL_HINTS.gs.body,
6989
+ qpdf: INSTALL_HINTS.qpdf.body
6990
+ };
6991
+ function rendersInPooledChromium(format, opts) {
6992
+ return format !== "html" && !opts.engineBrowser;
6993
+ }
6994
+ async function preflightBuildTools(format, opts, config) {
6995
+ const missing = [];
6996
+ if (rendersInPooledChromium(format, opts) && !await resolveChromiumExecutable()) {
6997
+ await requireChromiumExecutable();
6998
+ }
6999
+ if (format === "pdfx" && !await resolveGhostscript()) {
7000
+ missing.push({ name: "gs (Ghostscript)", installHint: INSTALL_HINTS2.gs });
7001
+ }
7002
+ if (format === "pdfx") {
7003
+ const stripAnnotations2 = opts.stripAnnotations ?? config.pdfx.stripAnnotations;
7004
+ if (stripAnnotations2 && !await isToolAvailable("qpdf")) {
7005
+ missing.push({ name: "qpdf", installHint: INSTALL_HINTS2.qpdf });
7006
+ }
7007
+ }
7008
+ if (missing.length === 0)
7009
+ return;
7010
+ const list = missing.map((m) => ` • ${m.name}
7011
+ ${m.installHint}`).join(`
7012
+
7013
+ `);
7014
+ throw new BuildError(`Required system tools not found:
7015
+
7016
+ ${list}
7017
+
7018
+ Install the missing tools and re-run, or set GHOSTSCRIPT_PATH, CHROMIUM_PATH, or system PATH so gutterpress can find them. See the User Guide Chapter 7 (System Setup) at examples/gutterpress-user-guide/07-system-setup.md for the full per-feature matrix.`, 2);
7019
+ }
7020
+ async function verifyNativeChromiumMilestone() {
7021
+ let version;
7022
+ try {
7023
+ const browser = await getBrowser(RENDER_TIMEOUT_MS);
7024
+ version = await browser.version();
7025
+ } catch (err) {
7026
+ throw new BuildError(`Could not launch a Chromium browser for --engine native: ` + `${err instanceof Error ? err.message : String(err)}`, 2);
7027
+ }
7028
+ const milestone = Number(/Chrome\/(\d+)/.exec(version)?.[1] ?? 0);
7029
+ if (milestone < REQUIRED_MILESTONE) {
7030
+ await closeBrowser();
7031
+ throw new BuildError(`The Gutterpress native engine requires Chromium ${REQUIRED_MILESTONE}+; found ${version}.
7032
+
7033
+ ` + `Install a newer Chrome, Chromium, or Edge, or point CHROMIUM_PATH ` + `(or PUPPETEER_EXECUTABLE_PATH) at a ${REQUIRED_MILESTONE}+ binary.
7034
+
7035
+ ` + `Note: this applies to the CLI's own Chromium only — the desktop app ` + `renders --engine native with its own bundled browser and needs no ` + `separate install.`, 2);
7036
+ }
7037
+ }
7038
+ function computeGates(format, opts, config) {
7039
+ if (format === "html") {
7040
+ if (opts.skipLint || opts.skipPreValidate || opts.skipPostValidate) {
7041
+ log.info("Validation/lint flags ignored for --format html (no validation phases apply)");
7042
+ }
7043
+ return { lint: false, preValidate: false, postValidate: false };
7044
+ }
7045
+ const lint2 = !opts.skipLint && config.lint.enabled !== false;
7046
+ const preValidate = !opts.skipPreValidate && config.validate.enabled !== false;
7047
+ const postValidate = format === "pdfx" && !opts.skipPostValidate && config.validate.enabled !== false;
7048
+ return { lint: lint2, preValidate, postValidate };
7049
+ }
7050
+
7051
+ // src/lib/build-staging.ts
7052
+ import path7 from "node:path";
7053
+ import os from "node:os";
7054
+ import fsp from "node:fs/promises";
7055
+
6908
7056
  // src/lib/missing-asset-placeholder.ts
6909
7057
  import { createHash as createHash5 } from "node:crypto";
6910
7058
  import { deflateSync } from "node:zlib";
@@ -7082,104 +7230,80 @@ function placeholderPng(width = 640, height = 480, cell = 32) {
7082
7230
  return png;
7083
7231
  }
7084
7232
 
7085
- // src/lib/build-preflight.ts
7086
- var INSTALL_HINTS2 = {
7087
- gs: INSTALL_HINTS.gs.body,
7088
- qpdf: INSTALL_HINTS.qpdf.body
7089
- };
7090
- function rendersInPooledChromium(format, opts) {
7091
- return format !== "html" && !opts.engineBrowser;
7092
- }
7093
- async function preflightBuildTools(format, opts, config) {
7094
- const missing = [];
7095
- if (rendersInPooledChromium(format, opts) && !await resolveChromiumExecutable()) {
7096
- await requireChromiumExecutable();
7097
- }
7098
- if (format === "pdfx" && !await resolveGhostscript()) {
7099
- missing.push({ name: "gs (Ghostscript)", installHint: INSTALL_HINTS2.gs });
7100
- }
7101
- if (format === "pdfx") {
7102
- const stripAnnotations2 = opts.stripAnnotations ?? config.pdfx.stripAnnotations;
7103
- if (stripAnnotations2 && !await isToolAvailable("qpdf")) {
7104
- missing.push({ name: "qpdf", installHint: INSTALL_HINTS2.qpdf });
7105
- }
7106
- }
7107
- if (missing.length === 0)
7108
- return;
7109
- const list = missing.map((m) => ` • ${m.name}
7110
- ${m.installHint}`).join(`
7111
-
7112
- `);
7113
- throw new BuildError(`Required system tools not found:
7114
-
7115
- ${list}
7116
-
7117
- Install the missing tools and re-run, or set GHOSTSCRIPT_PATH, CHROMIUM_PATH, or system PATH so gutterpress can find them. See the User Guide Chapter 7 (System Setup) at examples/gutterpress-user-guide/07-system-setup.md for the full per-feature matrix.`, 2);
7118
- }
7119
- async function verifyNativeChromiumMilestone() {
7120
- let version;
7121
- try {
7122
- const browser = await getBrowser(RENDER_TIMEOUT_MS);
7123
- version = await browser.version();
7124
- } catch (err) {
7125
- throw new BuildError(`Could not launch a Chromium browser for --engine native: ` + `${err instanceof Error ? err.message : String(err)}`, 2);
7126
- }
7127
- const milestone = Number(/Chrome\/(\d+)/.exec(version)?.[1] ?? 0);
7128
- if (milestone < REQUIRED_MILESTONE) {
7129
- await closeBrowser();
7130
- throw new BuildError(`The Gutterpress native engine requires Chromium ${REQUIRED_MILESTONE}+; found ${version}.
7131
-
7132
- ` + `Install a newer Chrome, Chromium, or Edge, or point CHROMIUM_PATH ` + `(or PUPPETEER_EXECUTABLE_PATH) at a ${REQUIRED_MILESTONE}+ binary.
7133
-
7134
- ` + `Note: this applies to the CLI's own Chromium only — the desktop app ` + `renders --engine native with its own bundled browser and needs no ` + `separate install.`, 2);
7135
- }
7136
- }
7137
- function computeGates(format, opts, config) {
7138
- if (format === "html") {
7139
- if (opts.skipLint || opts.skipPreValidate || opts.skipPostValidate) {
7140
- log.info("Validation/lint flags ignored for --format html (no validation phases apply)");
7141
- }
7142
- return { lint: false, preValidate: false, postValidate: false };
7143
- }
7144
- const lint2 = !opts.skipLint && config.lint.enabled !== false;
7145
- const preValidate = !opts.skipPreValidate && config.validate.enabled !== false;
7146
- const postValidate = format === "pdfx" && !opts.skipPostValidate && config.validate.enabled !== false;
7147
- return { lint: lint2, preValidate, postValidate };
7148
- }
7149
-
7150
7233
  // src/lib/build-staging.ts
7151
- import path6 from "node:path";
7152
- import os from "node:os";
7153
- import fsp from "node:fs/promises";
7154
7234
  async function shipViewerHtml(htmlFile, outDir) {
7155
- await fsp.mkdir(path6.join(outDir, "engine"), { recursive: true });
7156
- await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path6.join(outDir, "engine/gutterpress-viewer.js"));
7235
+ await fsp.mkdir(path7.join(outDir, "engine"), { recursive: true });
7236
+ await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path7.join(outDir, "engine/gutterpress-viewer.js"));
7157
7237
  const tag = ` <script src="engine/gutterpress-viewer.js"></script>
7158
7238
  `;
7159
7239
  const html = await fsp.readFile(htmlFile, "utf-8");
7160
7240
  await fsp.writeFile(htmlFile, /<\/head>/i.test(html) ? html.replace(/<\/head>/i, tag + "</head>") : tag + html, "utf-8");
7161
7241
  }
7162
- async function createStageRoot() {
7163
- return fsp.mkdtemp(path6.join(os.tmpdir(), "gutterpress-stage-"));
7164
- }
7165
-
7166
- // src/lib/build-runner.ts
7167
- function splitOutPath(outArg, format) {
7168
- if (typeof outArg !== "string" || outArg.length === 0) {
7169
- return { outDir: undefined, pdfFileOverride: null };
7170
- }
7171
- const resolved = path7.resolve(outArg);
7172
- if ((format === "pdf" || format === "pdfx") && resolved.toLowerCase().endsWith(".pdf")) {
7173
- return { outDir: path7.dirname(resolved), pdfFileOverride: resolved };
7174
- }
7175
- return { outDir: resolved, pdfFileOverride: null };
7176
- }
7177
- function layoutWarningKey(file, line, message) {
7178
- return `${path7.resolve(file)}\x00${line ?? 0}\x00${message}`;
7242
+ async function stageBookAssets(options) {
7243
+ const { renderDir, outDir, htmlFile, imageRefs, cssAssets, onPlan } = options;
7244
+ const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
7245
+ const copies = [...cssAssets, ...imageCopies];
7246
+ onPlan?.({ unresolved: errors, copyCount: copies.length });
7247
+ const missingPlaceholders = copies.length ? await copyReferencedAssets(copies, outDir) : new Map;
7248
+ let staged = await fsp.readFile(htmlFile, "utf8");
7249
+ if (missingPlaceholders.size > 0) {
7250
+ const rewrites = new Map(missingPlaceholders);
7251
+ for (const [ref, dest] of destinations) {
7252
+ const placeholder = missingPlaceholders.get(dest);
7253
+ if (placeholder)
7254
+ rewrites.set(ref, placeholder);
7255
+ }
7256
+ staged = rewriteMissingImageReferences(staged, rewrites);
7257
+ await fsp.writeFile(htmlFile, staged, "utf8");
7258
+ }
7259
+ if (staged.includes("--gp-shape:")) {
7260
+ await fsp.writeFile(htmlFile, await inlineShapeUrls(staged, outDir), "utf8");
7261
+ }
7262
+ return { missing: [...missingPlaceholders.keys()].sort() };
7263
+ }
7264
+ async function copyReferencedAssets(copies, outDir) {
7265
+ const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
7266
+ await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
7267
+ const missing = new Map;
7268
+ await Promise.all(copies.map(async (c) => {
7269
+ const dest = path7.resolve(outDir, c.to);
7270
+ try {
7271
+ await fsp.copyFile(c.from, dest);
7272
+ } catch (err) {
7273
+ if (err?.code === "ENOENT") {
7274
+ const placeholder = placeholderOutputPath(c.to);
7275
+ const placeholderDest = path7.resolve(outDir, placeholder);
7276
+ await fsp.mkdir(path7.dirname(placeholderDest), { recursive: true });
7277
+ await fsp.writeFile(placeholderDest, placeholderPng());
7278
+ missing.set(c.to, placeholder);
7279
+ return;
7280
+ }
7281
+ throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
7282
+ }
7283
+ }));
7284
+ return missing;
7285
+ }
7286
+ async function createStageRoot() {
7287
+ return fsp.mkdtemp(path7.join(os.tmpdir(), "gutterpress-stage-"));
7288
+ }
7289
+
7290
+ // src/lib/build-runner.ts
7291
+ function splitOutPath(outArg, format) {
7292
+ if (typeof outArg !== "string" || outArg.length === 0) {
7293
+ return { outDir: undefined, pdfFileOverride: null };
7294
+ }
7295
+ const resolved = path8.resolve(outArg);
7296
+ if ((format === "pdf" || format === "pdfx") && resolved.toLowerCase().endsWith(".pdf")) {
7297
+ return { outDir: path8.dirname(resolved), pdfFileOverride: resolved };
7298
+ }
7299
+ return { outDir: resolved, pdfFileOverride: null };
7300
+ }
7301
+ function layoutWarningKey(file, line, message) {
7302
+ return `${path8.resolve(file)}\x00${line ?? 0}\x00${message}`;
7179
7303
  }
7180
7304
  async function resolveBuildContext(opts) {
7181
7305
  const { format } = opts;
7182
- const inputDir = path7.resolve(opts.inputDir);
7306
+ const inputDir = path8.resolve(opts.inputDir);
7183
7307
  const { manifest, manifestDir, manifestPath } = await loadManifestWithPath(opts.manifestPath ?? inputDir, { explicit: opts.manifestPath !== undefined });
7184
7308
  if (manifestPath === null) {
7185
7309
  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>`.");
@@ -7194,10 +7318,10 @@ async function resolveBuildContext(opts) {
7194
7318
  pdfx: pdfxConfigOverride,
7195
7319
  engine: opts.engine
7196
7320
  }, manifest);
7197
- 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) };
7198
- const outDir = target.kind === "file" ? path7.dirname(target.file) : target.dir;
7321
+ 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) };
7322
+ const outDir = target.kind === "file" ? path8.dirname(target.file) : target.dir;
7199
7323
  const gates = computeGates(format, opts, config);
7200
- 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")}`);
7324
+ 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")}`);
7201
7325
  return {
7202
7326
  opts,
7203
7327
  format,
@@ -7264,79 +7388,47 @@ async function renderBook(ctx) {
7264
7388
  pluginCss,
7265
7389
  onChapterWarnings: (file, warnings) => {
7266
7390
  for (const w of warnings) {
7267
- const key = layoutWarningKey(path7.resolve(renderDir, file), w.line, w.message);
7391
+ const key = layoutWarningKey(path8.resolve(renderDir, file), w.line, w.message);
7268
7392
  if (!ctx.prevalidatedLayoutWarningKeys.has(key)) {
7269
7393
  log.warn(` ${file}, line ${w.line}: ${w.message}`);
7270
7394
  }
7271
7395
  }
7272
7396
  },
7273
7397
  onImageRefs: (refs) => imageRefs.push(...refs),
7274
- onCssAssets: (copies2) => cssAssets.push(...copies2),
7398
+ onCssAssets: (copies) => cssAssets.push(...copies),
7275
7399
  onStyleWarnings: (warnings) => {
7276
7400
  for (const w of warnings)
7277
7401
  log.warn(` ${w}`);
7278
7402
  }
7279
7403
  });
7280
7404
  log.success(`Wrote ${htmlFile}`);
7281
- const { copies: imageCopies, errors, destinations } = await planImageCopies(renderDir, imageRefs);
7282
- if (errors.length > 0) {
7283
- throw new BuildError(`Cannot resolve ${errors.length} image reference(s):
7284
- ` + errors.map((e) => ` - ${e}`).join(`
7405
+ const { missing } = await stageBookAssets({
7406
+ renderDir,
7407
+ outDir: workDir,
7408
+ htmlFile,
7409
+ imageRefs,
7410
+ cssAssets,
7411
+ onPlan: ({ unresolved, copyCount }) => {
7412
+ if (unresolved.length > 0) {
7413
+ throw new BuildError(`Cannot resolve ${unresolved.length} image reference(s):
7414
+ ` + unresolved.map((e) => ` - ${e}`).join(`
7285
7415
  `), 1);
7286
- }
7287
- const copies = [...cssAssets, ...imageCopies];
7288
- let missingPlaceholders = new Map;
7289
- if (copies.length > 0) {
7290
- log.info(`Copying ${copies.length} referenced asset(s)`);
7291
- missingPlaceholders = await copyReferencedAssets(copies, workDir);
7292
- }
7293
- let staged = await fsp2.readFile(htmlFile, "utf8");
7294
- if (missingPlaceholders.size > 0) {
7295
- const rewrites = new Map(missingPlaceholders);
7296
- for (const [ref, dest] of destinations) {
7297
- const placeholder = missingPlaceholders.get(dest);
7298
- if (placeholder)
7299
- rewrites.set(ref, placeholder);
7300
- }
7301
- staged = rewriteMissingImageReferences(staged, rewrites);
7302
- await fsp2.writeFile(htmlFile, staged, "utf8");
7303
- }
7304
- if (staged.includes("--gp-shape:")) {
7305
- await fsp2.writeFile(htmlFile, await inlineShapeUrls(staged, workDir), "utf8");
7306
- }
7307
- return htmlFile;
7308
- }
7309
- async function copyReferencedAssets(copies, outDir) {
7310
- const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
7311
- await Promise.all([...dirs].map((d) => fsp2.mkdir(d, { recursive: true })));
7312
- const missing = new Map;
7313
- await Promise.all(copies.map(async (c) => {
7314
- const dest = path7.resolve(outDir, c.to);
7315
- try {
7316
- await fsp2.copyFile(c.from, dest);
7317
- } catch (err) {
7318
- if (err?.code === "ENOENT") {
7319
- const placeholder = placeholderOutputPath(c.to);
7320
- const placeholderDest = path7.resolve(outDir, placeholder);
7321
- await fsp2.mkdir(path7.dirname(placeholderDest), { recursive: true });
7322
- await fsp2.writeFile(placeholderDest, placeholderPng());
7323
- missing.set(c.to, placeholder);
7324
- return;
7325
7416
  }
7326
- throw new BuildError(`Could not copy asset ${c.from} → ${c.to}: ` + (err instanceof Error ? err.message : String(err)), 1);
7417
+ if (copyCount > 0)
7418
+ log.info(`Copying ${copyCount} referenced asset(s)`);
7327
7419
  }
7328
- }));
7329
- if (missing.size > 0) {
7330
- 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:`);
7331
- for (const m of [...missing.keys()].sort())
7420
+ });
7421
+ if (missing.length > 0) {
7422
+ 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:`);
7423
+ for (const m of missing)
7332
7424
  log.warn(` missing: ${m}`);
7333
7425
  }
7334
- return missing;
7426
+ return htmlFile;
7335
7427
  }
7336
7428
  async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
7337
- const iccCandidates = path7.isAbsolute(icc) ? [icc] : [path7.resolve(manifestDir, icc), path7.resolve(icc)];
7429
+ const iccCandidates = path8.isAbsolute(icc) ? [icc] : [path8.resolve(manifestDir, icc), path8.resolve(icc)];
7338
7430
  let effectiveIccPath = iccCandidates.find((candidate) => fs2.existsSync(candidate)) ?? null;
7339
- if (!effectiveIccPath && !explicitIccPath && path7.basename(icc) === "CGATS21_CRPC1.icc") {
7431
+ if (!effectiveIccPath && !explicitIccPath && path8.basename(icc) === "CGATS21_CRPC1.icc") {
7340
7432
  effectiveIccPath = await getAssetPath("profiles/CGATS21_CRPC1.icc");
7341
7433
  }
7342
7434
  if (!effectiveIccPath) {
@@ -7352,13 +7444,13 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
7352
7444
  });
7353
7445
  await publishBuild(ctx, artifactName2);
7354
7446
  const delivered = ctx.target.kind !== "file";
7355
- const fingerprintPath = delivered ? path7.join(ctx.outDir, path7.basename(workFingerprint)) : null;
7447
+ const fingerprintPath = delivered ? path8.join(ctx.outDir, path8.basename(workFingerprint)) : null;
7356
7448
  log.success(wroteMessage);
7357
7449
  if (fingerprintPath)
7358
7450
  log.info(`Fingerprint: ${fingerprintPath}`);
7359
7451
  return {
7360
7452
  outDir: ctx.outDir,
7361
- htmlPath: delivered ? path7.join(ctx.outDir, path7.relative(ctx.workDir, paths.htmlPath)) : null,
7453
+ htmlPath: delivered ? path8.join(ctx.outDir, path8.relative(ctx.workDir, paths.htmlPath)) : null,
7362
7454
  pdfPath: paths.pdfPath,
7363
7455
  fingerprintPath,
7364
7456
  diagnostics
@@ -7369,8 +7461,8 @@ async function publishBuild(ctx, artifactName2) {
7369
7461
  if (target.kind === "file") {
7370
7462
  if (!artifactName2)
7371
7463
  throw new BuildError("No artifact to write for this format", 1);
7372
- await fsp2.mkdir(path7.dirname(target.file), { recursive: true });
7373
- await fsp2.copyFile(path7.join(workDir, artifactName2), target.file);
7464
+ await fsp2.mkdir(path8.dirname(target.file), { recursive: true });
7465
+ await fsp2.copyFile(path8.join(workDir, artifactName2), target.file);
7374
7466
  return;
7375
7467
  }
7376
7468
  if (target.kind === "directory") {
@@ -7379,12 +7471,12 @@ async function publishBuild(ctx, artifactName2) {
7379
7471
  return;
7380
7472
  }
7381
7473
  for (const name of await listPdfs(target.dir)) {
7382
- if (!fs2.existsSync(path7.join(workDir, name))) {
7383
- await fsp2.rename(path7.join(target.dir, name), path7.join(workDir, name));
7474
+ if (!fs2.existsSync(path8.join(workDir, name))) {
7475
+ await fsp2.rename(path8.join(target.dir, name), path8.join(workDir, name));
7384
7476
  }
7385
7477
  }
7386
7478
  await fsp2.rm(target.dir, { recursive: true, force: true });
7387
- await fsp2.mkdir(path7.dirname(target.dir), { recursive: true });
7479
+ await fsp2.mkdir(path8.dirname(target.dir), { recursive: true });
7388
7480
  await fsp2.rename(workDir, target.dir);
7389
7481
  }
7390
7482
  async function listPdfs(dir) {
@@ -7400,7 +7492,7 @@ class HtmlOutput {
7400
7492
  const { workDir, outDir, inputDir, config, opts } = ctx;
7401
7493
  log.info("Shipping self-contained HTML + viewer bundle (native engine)");
7402
7494
  await shipViewerHtml(htmlFile, workDir);
7403
- 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>
7495
+ 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>
7404
7496
  `, "utf-8");
7405
7497
  return finalizeBuild(ctx, {
7406
7498
  command: "build",
@@ -7413,7 +7505,7 @@ class HtmlOutput {
7413
7505
  iccPath: null,
7414
7506
  stripAnnotations: null
7415
7507
  }
7416
- }, `Wrote: ${path7.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
7508
+ }, `Wrote: ${path8.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
7417
7509
  }
7418
7510
  }
7419
7511
 
@@ -7422,14 +7514,14 @@ class PdfOutput {
7422
7514
  const { workDir, outDir, inputDir, config, opts, format, manifestDir, gates } = ctx;
7423
7515
  const pdfxMode = format === "pdfx" ? opts.pdfxFlavor ?? config.pdfx.flavor : undefined;
7424
7516
  const pdfName = artifactName(config.title, pdfxMode ? "pdfx" : "pdf");
7425
- const pdfFile = path7.join(workDir, pdfName);
7426
- const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path7.join(outDir, pdfName);
7517
+ const pdfFile = path8.join(workDir, pdfName);
7518
+ const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path8.join(outDir, pdfName);
7427
7519
  const stage = await createStageRoot();
7428
7520
  try {
7429
- const rawPdf = pdfxMode ? path7.join(stage, "raw.pdf") : path7.resolve(pdfFile);
7430
- await fsp2.mkdir(path7.dirname(path7.resolve(pdfFile)), { recursive: true });
7521
+ const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
7522
+ await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
7431
7523
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
7432
- const { buildNativePdf } = await import("./engine-hrtn0b28.js");
7524
+ const { buildNativePdf } = await import("./engine-gkcqdye5.js");
7433
7525
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
7434
7526
  title: config.title,
7435
7527
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -7460,7 +7552,7 @@ class PdfOutput {
7460
7552
  await stripAnnotations(rawPdf, stage);
7461
7553
  }
7462
7554
  log.info(`Converting to CMYK PDF/X (${pdfxMode})`);
7463
- await convertToPdfxCmyk(rawPdf, path7.resolve(pdfFile), {
7555
+ await convertToPdfxCmyk(rawPdf, path8.resolve(pdfFile), {
7464
7556
  iccPath: effectiveIccPath,
7465
7557
  pdfx: pdfxMode,
7466
7558
  title: config.title,
@@ -7549,10 +7641,10 @@ function openPath(filePath) {
7549
7641
  }
7550
7642
 
7551
7643
  // src/preview/lifecycle.ts
7552
- import path9 from "path";
7644
+ import path10 from "path";
7553
7645
  import { tmpdir as tmpdir2 } from "os";
7554
7646
  import { randomBytes as randomBytes2 } from "crypto";
7555
- import { readdir as readdir4, readFile as readFile19, writeFile as writeFile4 } from "node:fs/promises";
7647
+ import { readdir as readdir4, readFile as readFile20, writeFile as writeFile4 } from "node:fs/promises";
7556
7648
 
7557
7649
  // src/utils/file-utils.ts
7558
7650
  import { promises as fs3 } from "fs";
@@ -7574,7 +7666,7 @@ async function mkdir4(dirPath) {
7574
7666
  // src/preview/file-watcher.ts
7575
7667
  import { watch } from "chokidar";
7576
7668
  import fsp3 from "node:fs/promises";
7577
- import path8 from "path";
7669
+ import path9 from "path";
7578
7670
  var EMPTY_BOOK_HTML = `<!doctype html>
7579
7671
  <html lang="en">
7580
7672
  <head>
@@ -7627,7 +7719,7 @@ function injectPreviewScripts(html, pageIsolateChapters) {
7627
7719
  async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
7628
7720
  if (!inputPath) {
7629
7721
  cssAssets.clear();
7630
- await fsp3.writeFile(path8.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
7722
+ await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
7631
7723
  return;
7632
7724
  }
7633
7725
  const nextAssets = new Map;
@@ -7642,7 +7734,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
7642
7734
  cssAssets.clear();
7643
7735
  for (const [to, from] of nextAssets)
7644
7736
  cssAssets.set(to, from);
7645
- await fsp3.writeFile(path8.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
7737
+ await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
7646
7738
  }
7647
7739
  async function renderChapterPreviewHtml(inputPath, file, config) {
7648
7740
  const html = await renderPreviewBook(inputPath, config, {
@@ -7654,12 +7746,12 @@ async function renderChapterPreviewHtml(inputPath, file, config) {
7654
7746
  function describeChanges(changes, inputResolved) {
7655
7747
  const files = [];
7656
7748
  for (const [changedPath, event] of changes) {
7657
- const relative2 = path8.relative(inputResolved, path8.resolve(changedPath));
7658
- if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path8.sep}`) || path8.isAbsolute(relative2))
7749
+ const relative2 = path9.relative(inputResolved, path9.resolve(changedPath));
7750
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path9.sep}`) || path9.isAbsolute(relative2))
7659
7751
  continue;
7660
7752
  files.push({
7661
7753
  relativePath: canonicalChapterId(relative2),
7662
- ext: path8.extname(changedPath).toLowerCase(),
7754
+ ext: path9.extname(changedPath).toLowerCase(),
7663
7755
  event
7664
7756
  });
7665
7757
  }
@@ -7711,23 +7803,23 @@ function isIgnoredWatchPath(candidatePath, projectRoot) {
7711
7803
  return isDotPathUnderRoot(candidatePath, projectRoot) || isGeneratedProjectPath(relative2);
7712
7804
  }
7713
7805
  function isExternalWatchCandidate(candidatePath, targets) {
7714
- const candidate = path8.resolve(candidatePath);
7806
+ const candidate = path9.resolve(candidatePath);
7715
7807
  for (const rawTarget of targets) {
7716
- const target = path8.resolve(rawTarget);
7717
- const rel = path8.relative(candidate, target);
7718
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel))
7808
+ const target = path9.resolve(rawTarget);
7809
+ const rel = path9.relative(candidate, target);
7810
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
7719
7811
  return true;
7720
7812
  }
7721
7813
  return false;
7722
7814
  }
7723
7815
  async function nearestExistingDirectory(start) {
7724
- let current = path8.resolve(start);
7816
+ let current = path9.resolve(start);
7725
7817
  while (true) {
7726
7818
  try {
7727
7819
  if ((await fsp3.stat(current)).isDirectory())
7728
7820
  return current;
7729
7821
  } catch {}
7730
- const parent = path8.dirname(current);
7822
+ const parent = path9.dirname(current);
7731
7823
  if (parent === current)
7732
7824
  return current;
7733
7825
  current = parent;
@@ -7736,12 +7828,12 @@ async function nearestExistingDirectory(start) {
7736
7828
  async function externalWatchRoots(targets) {
7737
7829
  const roots = new Set;
7738
7830
  for (const target of targets) {
7739
- roots.add(await nearestExistingDirectory(path8.dirname(path8.resolve(target))));
7831
+ roots.add(await nearestExistingDirectory(path9.dirname(path9.resolve(target))));
7740
7832
  }
7741
7833
  return [...roots];
7742
7834
  }
7743
7835
  async function externalWatchTargets(projectDir, config) {
7744
- const root = path8.resolve(projectDir);
7836
+ const root = path9.resolve(projectDir);
7745
7837
  let canonicalRoot = root;
7746
7838
  try {
7747
7839
  canonicalRoot = await fsp3.realpath(root);
@@ -7749,21 +7841,21 @@ async function externalWatchTargets(projectDir, config) {
7749
7841
  const styles = await resolveActiveStyles(root, config.styles);
7750
7842
  const candidates = [
7751
7843
  ...await collectStyleDependencies(root, styles),
7752
- ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path8.resolve(root, p))
7844
+ ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path9.resolve(root, p))
7753
7845
  ];
7754
7846
  const external = new Set;
7755
7847
  for (const abs of candidates) {
7756
- const authoredPath = path8.resolve(abs);
7757
- const authoredRel = path8.relative(root, authoredPath);
7758
- if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path8.sep}`) || path8.isAbsolute(authoredRel))) {
7848
+ const authoredPath = path9.resolve(abs);
7849
+ const authoredRel = path9.relative(root, authoredPath);
7850
+ if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path9.sep}`) || path9.isAbsolute(authoredRel))) {
7759
7851
  external.add(authoredPath);
7760
7852
  }
7761
7853
  let canonicalPath = authoredPath;
7762
7854
  try {
7763
7855
  canonicalPath = await fsp3.realpath(canonicalPath);
7764
7856
  } catch {}
7765
- const rel = path8.relative(canonicalRoot, canonicalPath);
7766
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel))
7857
+ const rel = path9.relative(canonicalRoot, canonicalPath);
7858
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
7767
7859
  continue;
7768
7860
  external.add(canonicalPath);
7769
7861
  }
@@ -7790,7 +7882,7 @@ async function addAndAwaitWatch(watcher, roots) {
7790
7882
  }
7791
7883
  }
7792
7884
  function createFileWatcher(state) {
7793
- const inputResolved = path8.resolve(state.currentInputPath);
7885
+ const inputResolved = path9.resolve(state.currentInputPath);
7794
7886
  const watcher = watch(inputResolved, {
7795
7887
  persistent: true,
7796
7888
  ignoreInitial: true,
@@ -7830,7 +7922,7 @@ function createFileWatcher(state) {
7830
7922
  async function recordChange(event, filePath) {
7831
7923
  if (closed)
7832
7924
  return;
7833
- const target = path8.resolve(filePath);
7925
+ const target = path9.resolve(filePath);
7834
7926
  const settled = settledWrites.get(target);
7835
7927
  if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
7836
7928
  const diskContent = await fsp3.readFile(target, "utf8").catch(() => null);
@@ -7851,7 +7943,7 @@ function createFileWatcher(state) {
7851
7943
  recordChange(event, filePath);
7852
7944
  });
7853
7945
  externalWatcher.on("all", (event, filePath) => {
7854
- const target = path8.resolve(filePath);
7946
+ const target = path9.resolve(filePath);
7855
7947
  if (!desiredExternals.has(target))
7856
7948
  return;
7857
7949
  if (event === "add" && suppressInitialExternalAdds.delete(target))
@@ -7859,8 +7951,8 @@ function createFileWatcher(state) {
7859
7951
  recordChange(event, target);
7860
7952
  });
7861
7953
  function isWatchedSource(target) {
7862
- const rel = path8.relative(inputResolved, target);
7863
- const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel);
7954
+ const rel = path9.relative(inputResolved, target);
7955
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
7864
7956
  if (inProject)
7865
7957
  return !isIgnoredWatchPath(target, inputResolved);
7866
7958
  return desiredExternals.has(target);
@@ -7885,7 +7977,7 @@ function createFileWatcher(state) {
7885
7977
  state.notifySettledWrite = (filePath, writtenContent) => {
7886
7978
  if (closed)
7887
7979
  return;
7888
- const target = path8.resolve(filePath);
7980
+ const target = path9.resolve(filePath);
7889
7981
  if (isWatchedSource(target)) {
7890
7982
  acceptSettledWrite(target, writtenContent);
7891
7983
  return;
@@ -7906,7 +7998,7 @@ function createFileWatcher(state) {
7906
7998
  return;
7907
7999
  let nextTargets;
7908
8000
  try {
7909
- nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path8.resolve(target)));
8001
+ nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path9.resolve(target)));
7910
8002
  } catch (err) {
7911
8003
  debug(`Could not resolve external watch targets: ${err}`);
7912
8004
  return;
@@ -8038,7 +8130,7 @@ async function stopFileWatcher(state) {
8038
8130
  }
8039
8131
 
8040
8132
  // src/preview/lifecycle.ts
8041
- var TEMP_DIR_BASE = path9.join(tmpdir2(), "gutterpress-preview");
8133
+ var TEMP_DIR_BASE = path10.join(tmpdir2(), "gutterpress-preview");
8042
8134
  var PID_FILE_NAME = ".gutterpress.pid";
8043
8135
  function isProcessAlive(pid) {
8044
8136
  if (!Number.isFinite(pid) || pid <= 0)
@@ -8058,10 +8150,10 @@ async function cleanupOrphanTempDirs() {
8058
8150
  return;
8059
8151
  }
8060
8152
  for (const entry of entries) {
8061
- const dir = path9.join(TEMP_DIR_BASE, entry);
8062
- const pidFile = path9.join(dir, PID_FILE_NAME);
8153
+ const dir = path10.join(TEMP_DIR_BASE, entry);
8154
+ const pidFile = path10.join(dir, PID_FILE_NAME);
8063
8155
  try {
8064
- const raw = await readFile19(pidFile, "utf8");
8156
+ const raw = await readFile20(pidFile, "utf8");
8065
8157
  const pid = parseInt(raw.trim(), 10);
8066
8158
  if (!isProcessAlive(pid)) {
8067
8159
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -8073,10 +8165,10 @@ async function cleanupOrphanTempDirs() {
8073
8165
  async function initializePreviewDirectories() {
8074
8166
  await cleanupOrphanTempDirs();
8075
8167
  const tempDirSuffix = randomBytes2(8).toString("hex");
8076
- const tempDir = path9.join(TEMP_DIR_BASE, tempDirSuffix);
8168
+ const tempDir = path10.join(TEMP_DIR_BASE, tempDirSuffix);
8077
8169
  await mkdir4(tempDir);
8078
8170
  debug(`Created temporary directory: ${tempDir}`);
8079
- await writeFile4(path9.join(tempDir, PID_FILE_NAME), `${process.pid}
8171
+ await writeFile4(path10.join(tempDir, PID_FILE_NAME), `${process.pid}
8080
8172
  `, "utf8");
8081
8173
  return tempDir;
8082
8174
  }
@@ -8167,12 +8259,12 @@ function createServerState(inputPath, tempDir, config, options) {
8167
8259
  import http from "node:http";
8168
8260
  import net from "node:net";
8169
8261
  import { createHash as createHash6, randomUUID } from "node:crypto";
8170
- import { readFile as readFile20, stat as stat4 } from "node:fs/promises";
8171
- import path11 from "path";
8262
+ import { readFile as readFile21, stat as stat4 } from "node:fs/promises";
8263
+ import path12 from "path";
8172
8264
  import { WebSocket, WebSocketServer } from "ws";
8173
8265
 
8174
8266
  // src/lib/static-serve.ts
8175
- import path10 from "node:path";
8267
+ import path11 from "node:path";
8176
8268
  var STATIC_MIME = {
8177
8269
  ".html": "text/html; charset=utf-8",
8178
8270
  ".htm": "text/html; charset=utf-8",
@@ -8194,9 +8286,9 @@ var STATIC_MIME = {
8194
8286
  ".otf": "font/otf"
8195
8287
  };
8196
8288
  function resolveWithinRoot(relPath, root) {
8197
- const resolvedRoot = path10.resolve(root);
8198
- const candidate = path10.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
8199
- if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path10.sep)) {
8289
+ const resolvedRoot = path11.resolve(root);
8290
+ const candidate = path11.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
8291
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path11.sep)) {
8200
8292
  return null;
8201
8293
  }
8202
8294
  return candidate;
@@ -8449,7 +8541,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8449
8541
  try {
8450
8542
  const s = await stat4(filePath);
8451
8543
  if (s.isDirectory())
8452
- filePath = path11.join(filePath, "index.html");
8544
+ filePath = path12.join(filePath, "index.html");
8453
8545
  } catch {
8454
8546
  res.writeHead(404);
8455
8547
  res.end("Not Found");
@@ -8457,7 +8549,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8457
8549
  }
8458
8550
  let data;
8459
8551
  try {
8460
- data = await readFile20(filePath);
8552
+ data = await readFile21(filePath);
8461
8553
  } catch {
8462
8554
  res.writeHead(404);
8463
8555
  res.end("Not Found");
@@ -8470,7 +8562,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8470
8562
  res.end(withHmr);
8471
8563
  return;
8472
8564
  }
8473
- const ct = STATIC_MIME[path11.extname(filePath).toLowerCase()] ?? "application/octet-stream";
8565
+ const ct = STATIC_MIME[path12.extname(filePath).toLowerCase()] ?? "application/octet-stream";
8474
8566
  res.writeHead(200, { "Content-Type": ct, "Cache-Control": cacheControl, ...extraHeaders });
8475
8567
  res.end(data);
8476
8568
  }
@@ -8480,7 +8572,7 @@ var EMBEDDED_CACHE_CONTROL = "public, no-cache";
8480
8572
  var EMBEDDED_ETAG = `"gutterpress-${PACKAGE_VERSION}"`;
8481
8573
  var PROJECT_ASSET_CACHE_CONTROL = "private, no-cache";
8482
8574
  async function serveRevalidatedStatic(req, absPath, res, validators) {
8483
- const extension = path11.extname(absPath).toLowerCase();
8575
+ const extension = path12.extname(absPath).toLowerCase();
8484
8576
  if (extension === ".html" || extension === ".htm") {
8485
8577
  await serveStatic(absPath, res);
8486
8578
  return;
@@ -8507,7 +8599,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
8507
8599
  let data;
8508
8600
  if (!validator || validator.stamp !== stamp) {
8509
8601
  try {
8510
- data = await readFile20(absPath);
8602
+ data = await readFile21(absPath);
8511
8603
  } catch {
8512
8604
  await serveStatic(absPath, res);
8513
8605
  return;
@@ -8646,7 +8738,7 @@ async function createPreviewServer(state, port) {
8646
8738
  const file = rawFile ? canonicalChapterId(rawFile) : "";
8647
8739
  const revision = Number(url.searchParams.get("revision"));
8648
8740
  const configuredFiles = state.config.source?.files;
8649
- 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)) {
8741
+ 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)) {
8650
8742
  res.writeHead(400);
8651
8743
  res.end("Bad Request");
8652
8744
  return;
@@ -8846,25 +8938,25 @@ async function startPreviewServer(options) {
8846
8938
  }
8847
8939
 
8848
8940
  // src/lib/project-scaffold.ts
8849
- import { access, copyFile, cp as cp2, mkdir as mkdir6, readdir as readdir5, readFile as readFile22, stat as stat5, writeFile as writeFile5 } from "node:fs/promises";
8941
+ import { access, copyFile, cp as cp2, mkdir as mkdir6, readdir as readdir5, readFile as readFile23, stat as stat5, writeFile as writeFile5 } from "node:fs/promises";
8850
8942
  import { constants as FS, existsSync as existsSync10 } from "node:fs";
8851
- import path13 from "node:path";
8943
+ import path14 from "node:path";
8852
8944
 
8853
8945
  // src/lib/manifest-doc.ts
8854
8946
  import { existsSync as existsSync9 } from "node:fs";
8855
8947
  import { randomUUID as randomUUID2 } from "node:crypto";
8856
- import { mkdir as mkdir5, open, readFile as readFile21, rename, rm as rm2 } from "node:fs/promises";
8857
- import path12 from "node:path";
8948
+ import { mkdir as mkdir5, open, readFile as readFile22, rename, rm as rm2 } from "node:fs/promises";
8949
+ import path13 from "node:path";
8858
8950
  import { parseDocument, isSeq, YAMLSeq } from "yaml";
8859
8951
  function resolveManifestPath(projectDir) {
8860
- const existing = MANIFEST_FILENAMES.find((name) => existsSync9(path12.join(projectDir, name)));
8861
- return path12.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
8952
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync9(path13.join(projectDir, name)));
8953
+ return path13.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
8862
8954
  }
8863
8955
  async function loadManifestDoc(projectDir) {
8864
8956
  const file = resolveManifestPath(projectDir);
8865
8957
  let text = "";
8866
8958
  try {
8867
- text = await readFile21(file, "utf8");
8959
+ text = await readFile22(file, "utf8");
8868
8960
  } catch (error2) {
8869
8961
  if (error2.code !== "ENOENT")
8870
8962
  throw error2;
@@ -8872,9 +8964,9 @@ async function loadManifestDoc(projectDir) {
8872
8964
  return { doc: parseDocument(text), file };
8873
8965
  }
8874
8966
  async function writeManifestDoc(file, doc) {
8875
- const dir = path12.dirname(file);
8967
+ const dir = path13.dirname(file);
8876
8968
  await mkdir5(dir, { recursive: true });
8877
- const temporary = path12.join(dir, `.${path12.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
8969
+ const temporary = path13.join(dir, `.${path13.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
8878
8970
  try {
8879
8971
  const handle = await open(temporary, "wx", 438);
8880
8972
  try {
@@ -8969,7 +9061,7 @@ async function scaffoldProject(options) {
8969
9061
  throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
8970
9062
  }
8971
9063
  const parentDir = options.parentDir;
8972
- if (!parentDir || !path13.isAbsolute(parentDir)) {
9064
+ if (!parentDir || !path14.isAbsolute(parentDir)) {
8973
9065
  throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
8974
9066
  }
8975
9067
  try {
@@ -8977,7 +9069,7 @@ async function scaffoldProject(options) {
8977
9069
  } catch {
8978
9070
  throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
8979
9071
  }
8980
- const projectDir = path13.join(parentDir, folderName);
9072
+ const projectDir = path14.join(parentDir, folderName);
8981
9073
  let targetExists = true;
8982
9074
  try {
8983
9075
  await access(projectDir, FS.F_OK);
@@ -8998,19 +9090,19 @@ async function scaffoldProject(options) {
8998
9090
  for (const entry of entries) {
8999
9091
  if (entry.name === ".gutterpress-template.json")
9000
9092
  continue;
9001
- await cp2(path13.join(customTemplateDir, entry.name), path13.join(projectDir, entry.name), { recursive: true });
9093
+ await cp2(path14.join(customTemplateDir, entry.name), path14.join(projectDir, entry.name), { recursive: true });
9002
9094
  }
9003
- await mkdir6(path13.join(projectDir, "assets"), { recursive: true });
9095
+ await mkdir6(path14.join(projectDir, "assets"), { recursive: true });
9004
9096
  } else {
9005
9097
  const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
9006
9098
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
9007
- await mkdir6(path13.join(projectDir, "assets"), { recursive: true });
9008
- await copyFile(tplManifest, path13.join(projectDir, MANIFEST_FILENAMES[0]));
9009
- await copyFile(tplChapter, path13.join(projectDir, "chapter-01.md"));
9099
+ await mkdir6(path14.join(projectDir, "assets"), { recursive: true });
9100
+ await copyFile(tplManifest, path14.join(projectDir, MANIFEST_FILENAMES[0]));
9101
+ await copyFile(tplChapter, path14.join(projectDir, "chapter-01.md"));
9010
9102
  const starterThemeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
9011
9103
  const starterCss = await getAssetPath(`themes/${starterThemeId}/theme.css`);
9012
- await mkdir6(path13.join(projectDir, "styles"), { recursive: true });
9013
- await copyFile(starterCss, path13.join(projectDir, "styles", "book.css"));
9104
+ await mkdir6(path14.join(projectDir, "styles"), { recursive: true });
9105
+ await copyFile(starterCss, path14.join(projectDir, "styles", "book.css"));
9014
9106
  }
9015
9107
  } catch (e) {
9016
9108
  throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -9020,13 +9112,13 @@ async function scaffoldProject(options) {
9020
9112
  } catch (e) {
9021
9113
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
9022
9114
  }
9023
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path13.join(projectDir, name2)).find(existsSync10) ?? path13.join(projectDir, MANIFEST_FILENAMES[0]);
9115
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path14.join(projectDir, name2)).find(existsSync10) ?? path14.join(projectDir, MANIFEST_FILENAMES[0]);
9024
9116
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
9025
9117
  const substitutions = {
9026
9118
  "{{TITLE}}": escapeYamlScalar(name),
9027
9119
  "{{AUTHOR}}": escapeYamlScalar(author)
9028
9120
  };
9029
- let openFile = path13.join(projectDir, "chapter-01.md");
9121
+ let openFile = path14.join(projectDir, "chapter-01.md");
9030
9122
  try {
9031
9123
  await fillTemplateFile(manifestPath, substitutions);
9032
9124
  if (preset) {
@@ -9047,7 +9139,7 @@ async function scaffoldProject(options) {
9047
9139
  }
9048
9140
  const firstSource = await firstSourceFile(manifestPath);
9049
9141
  if (firstSource)
9050
- openFile = path13.join(projectDir, firstSource);
9142
+ openFile = path14.join(projectDir, firstSource);
9051
9143
  await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
9052
9144
  } catch (e) {
9053
9145
  throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -9057,7 +9149,7 @@ async function scaffoldProject(options) {
9057
9149
  let versionHistoryError;
9058
9150
  if (requested === "local-git") {
9059
9151
  try {
9060
- const { providerFor } = await import("./source-provider-3tcj6qg2.js");
9152
+ const { providerFor } = await import("./source-provider-2j5x2hyw.js");
9061
9153
  const provider = providerFor({ type: "local-folder", path: projectDir });
9062
9154
  await provider.initVersionHistory({
9063
9155
  projectDir,
@@ -9069,7 +9161,7 @@ async function scaffoldProject(options) {
9069
9161
  versionHistory = "none";
9070
9162
  const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
9071
9163
  const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
9072
- versionHistoryError = enclosingRepoDir ? `This book was added to ${path13.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
9164
+ 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);
9073
9165
  }
9074
9166
  }
9075
9167
  const result = {
@@ -9084,10 +9176,10 @@ async function scaffoldProject(options) {
9084
9176
  return result;
9085
9177
  }
9086
9178
  async function ensureGitignoreHasDist(projectDir) {
9087
- const gitignorePath = path13.join(projectDir, ".gitignore");
9179
+ const gitignorePath = path14.join(projectDir, ".gitignore");
9088
9180
  let existing = null;
9089
9181
  try {
9090
- existing = await readFile22(gitignorePath, "utf8");
9182
+ existing = await readFile23(gitignorePath, "utf8");
9091
9183
  } catch {}
9092
9184
  if (existing === null) {
9093
9185
  await writeFile5(gitignorePath, `dist/
@@ -9107,7 +9199,7 @@ async function ensureGitignoreHasDist(projectDir) {
9107
9199
  async function firstSourceFile(manifestPath) {
9108
9200
  let text;
9109
9201
  try {
9110
- text = await readFile22(manifestPath, "utf8");
9202
+ text = await readFile23(manifestPath, "utf8");
9111
9203
  } catch {
9112
9204
  return;
9113
9205
  }
@@ -9119,7 +9211,7 @@ async function firstSourceFile(manifestPath) {
9119
9211
  return m ? m[1].trim() : undefined;
9120
9212
  }
9121
9213
  async function fillTemplateFile(filePath, substitutions) {
9122
- let text = await readFile22(filePath, "utf8");
9214
+ let text = await readFile23(filePath, "utf8");
9123
9215
  for (const [token, value] of Object.entries(substitutions)) {
9124
9216
  text = text.split(token).join(value);
9125
9217
  }
@@ -9144,7 +9236,7 @@ var SKIP_ENTRIES = new Set([
9144
9236
  ]);
9145
9237
 
9146
9238
  // src/lib/plugin-manager.ts
9147
- import path15 from "node:path";
9239
+ import path16 from "node:path";
9148
9240
  import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
9149
9241
 
9150
9242
  // src/lib/npm-plugin-installer.ts
@@ -9155,12 +9247,12 @@ import {
9155
9247
  mkdir as mkdir7,
9156
9248
  mkdtemp as mkdtemp2,
9157
9249
  open as open2,
9158
- readFile as readFile23,
9250
+ readFile as readFile24,
9159
9251
  realpath as realpath2,
9160
9252
  rename as rename2,
9161
9253
  rm as rm3
9162
9254
  } from "node:fs/promises";
9163
- import path14 from "node:path";
9255
+ import path15 from "node:path";
9164
9256
  import { Transform } from "node:stream";
9165
9257
  import { pipeline } from "node:stream/promises";
9166
9258
  import { createGunzip } from "node:zlib";
@@ -9240,8 +9332,8 @@ function mb(bytes) {
9240
9332
  return `${Math.ceil(bytes / 1024 / 1024)}MB`;
9241
9333
  }
9242
9334
  function isContained2(root, candidate) {
9243
- const relative2 = path14.relative(root, candidate);
9244
- return relative2 === "" || !relative2.startsWith("..") && !path14.isAbsolute(relative2);
9335
+ const relative2 = path15.relative(root, candidate);
9336
+ return relative2 === "" || !relative2.startsWith("..") && !path15.isAbsolute(relative2);
9245
9337
  }
9246
9338
  function mergeLimits(overrides) {
9247
9339
  const limits = { ...DEFAULT_LIMITS, ...overrides };
@@ -9548,13 +9640,13 @@ async function extractTarball(ctx, archivePath, packageDir, packageName) {
9548
9640
  throw rejected;
9549
9641
  }
9550
9642
  async function readInstalledManifest(packageDir, selected) {
9551
- const packageJsonPath = path14.join(packageDir, "package.json");
9643
+ const packageJsonPath = path15.join(packageDir, "package.json");
9552
9644
  let manifest = null;
9553
9645
  try {
9554
9646
  const info2 = await lstat2(packageJsonPath);
9555
9647
  if (!info2.isFile() || info2.isSymbolicLink())
9556
9648
  throw new Error("not a regular file");
9557
- manifest = object2(JSON.parse(await readFile23(packageJsonPath, "utf8")));
9649
+ manifest = object2(JSON.parse(await readFile24(packageJsonPath, "utf8")));
9558
9650
  } catch (error2) {
9559
9651
  throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
9560
9652
  }
@@ -9624,10 +9716,10 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
9624
9716
  throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
9625
9717
  }
9626
9718
  const relativePath = relativePackagePath(parentPackagePath, name);
9627
- const packageDir = path14.join(ctx.stageRoot, ...relativePath.split("/"));
9628
- const archivePath = path14.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
9719
+ const packageDir = path15.join(ctx.stageRoot, ...relativePath.split("/"));
9720
+ const archivePath = path15.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
9629
9721
  try {
9630
- await mkdir7(path14.dirname(packageDir), { recursive: true });
9722
+ await mkdir7(path15.dirname(packageDir), { recursive: true });
9631
9723
  await downloadTarball(ctx, selected, archivePath);
9632
9724
  await extractTarball(ctx, archivePath, packageDir, selected.name);
9633
9725
  const manifest = await readInstalledManifest(packageDir, selected);
@@ -9726,9 +9818,9 @@ async function ensureDirectory(pathname) {
9726
9818
  }
9727
9819
  }
9728
9820
  async function prepareVendorParent(projectDir, name) {
9729
- const plugins = path14.join(projectDir, PLUGINS_DIR);
9730
- const npm = path14.join(plugins, VENDORED_NPM_DIR);
9731
- const packageParent = path14.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
9821
+ const plugins = path15.join(projectDir, PLUGINS_DIR);
9822
+ const npm = path15.join(plugins, VENDORED_NPM_DIR);
9823
+ const packageParent = path15.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
9732
9824
  await mkdir7(projectDir, { recursive: true });
9733
9825
  await ensureDirectory(plugins);
9734
9826
  await ensureDirectory(npm);
@@ -9756,14 +9848,14 @@ function publicReceipt(record) {
9756
9848
  };
9757
9849
  }
9758
9850
  async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
9759
- const requestedProjectDir = path14.resolve(projectDirInput);
9851
+ const requestedProjectDir = path15.resolve(projectDirInput);
9760
9852
  await mkdir7(requestedProjectDir, { recursive: true });
9761
9853
  const projectDir = await realpath2(requestedProjectDir);
9762
9854
  const requested = parseNpmPluginSpec(packageSpec);
9763
9855
  const npmRoot = await prepareVendorParent(projectDir, requested.name);
9764
- const container = await mkdtemp2(path14.join(npmRoot, ".install-"));
9765
- const stageRoot = path14.join(container, "root");
9766
- const downloadsDir = path14.join(container, "downloads");
9856
+ const container = await mkdtemp2(path15.join(npmRoot, ".install-"));
9857
+ const stageRoot = path15.join(container, "root");
9858
+ const downloadsDir = path15.join(container, "downloads");
9767
9859
  await Promise.all([mkdir7(stageRoot), mkdir7(downloadsDir)]);
9768
9860
  const ctx = {
9769
9861
  fetch: options.fetch ?? globalThis.fetch,
@@ -9809,7 +9901,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
9809
9901
  skipped: ctx.skipped,
9810
9902
  tree: { algorithm: "sha256", ...tree }
9811
9903
  };
9812
- const receiptFile = await open2(path14.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
9904
+ const receiptFile = await open2(path15.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
9813
9905
  try {
9814
9906
  await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
9815
9907
  `, "utf8");
@@ -9818,7 +9910,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
9818
9910
  await receiptFile.close();
9819
9911
  }
9820
9912
  finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
9821
- const realFinalParent = await realpath2(path14.dirname(finalRoot));
9913
+ const realFinalParent = await realpath2(path15.dirname(finalRoot));
9822
9914
  if (!isContained2(projectDir, realFinalParent)) {
9823
9915
  throw new Error("Plugin install destination resolves outside the project.");
9824
9916
  }
@@ -9867,7 +9959,7 @@ async function rollbackNpmPluginInstall(installed) {
9867
9959
  // src/lib/plugin-manager.ts
9868
9960
  var pluginMutationQueues = new Map;
9869
9961
  function withPluginMutationLock(projectDir, mutation) {
9870
- const resolved = path15.resolve(projectDir);
9962
+ const resolved = path16.resolve(projectDir);
9871
9963
  const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
9872
9964
  const previous = pluginMutationQueues.get(key) ?? Promise.resolve();
9873
9965
  const run = previous.then(mutation, mutation);
@@ -10060,221 +10152,6 @@ async function addNpmPluginUnlocked(projectDir, packageSpec, options) {
10060
10152
  };
10061
10153
  }
10062
10154
 
10063
- // src/lib/app-heartbeat.ts
10064
- import { readFile as readFile24, unlink as unlink2, writeFile as writeFile6 } from "node:fs/promises";
10065
- import path16 from "node:path";
10066
- var HEARTBEAT_FILENAME = "gutterpress-app-heartbeat";
10067
- var APP_HEARTBEAT_FRESH_MS = 2 * 60000;
10068
- function appHeartbeatPath(repoDir) {
10069
- return path16.join(gitDirFor(repoDir), HEARTBEAT_FILENAME);
10070
- }
10071
- async function readAppHeartbeat(repoDir) {
10072
- try {
10073
- const raw = await readFile24(appHeartbeatPath(repoDir), "utf8");
10074
- const parsed = JSON.parse(raw);
10075
- if (parsed && typeof parsed === "object" && typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
10076
- const candidate = parsed;
10077
- const ttlMs = typeof candidate.ttlMs === "number" ? candidate.ttlMs : undefined;
10078
- return { pid: candidate.pid, timestamp: candidate.timestamp, ...ttlMs !== undefined ? { ttlMs } : {} };
10079
- }
10080
- return null;
10081
- } catch {
10082
- return null;
10083
- }
10084
- }
10085
- async function isAppHeartbeatFresh(repoDir, now = Date.now(), maxAgeMs = APP_HEARTBEAT_FRESH_MS) {
10086
- const heartbeat = await readAppHeartbeat(repoDir);
10087
- if (!heartbeat)
10088
- return false;
10089
- return now - heartbeat.timestamp < (heartbeat.ttlMs ?? maxAgeMs);
10090
- }
10091
-
10092
- // src/lib/remote-auth/recovery/classify.ts
10093
- var STALE_LOCK_MIN_AGE_MS = 2 * 60 * 1000;
10094
- class InsecureTransportError extends Error {
10095
- code = "InsecureTransport";
10096
- constructor() {
10097
- 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.");
10098
- this.name = "InsecureTransportError";
10099
- }
10100
- }
10101
- function isInsecureTransportError(e) {
10102
- return e?.code === "InsecureTransport";
10103
- }
10104
- function isMergeConflictError(e) {
10105
- return e?.code === "MergeConflictError";
10106
- }
10107
- function classifyFromHealth(health, opts = {}) {
10108
- const minLockAgeMs = opts.minLockAgeMs ?? STALE_LOCK_MIN_AGE_MS;
10109
- if (!health.hasGitDir)
10110
- return "needs_repair";
10111
- if (health.hasInterruptedRebase)
10112
- return "needs_repair";
10113
- if (health.hasInterruptedCherryPick)
10114
- return "needs_repair";
10115
- if (health.hasInterruptedMerge)
10116
- return "needs_repair";
10117
- if (health.headUnreadable)
10118
- return "needs_repair";
10119
- if (health.isDetachedHead)
10120
- return "needs_repair";
10121
- if (health.hasStaleLock && (health.lockAgeMs ?? 0) >= minLockAgeMs) {
10122
- return "stale_lock";
10123
- }
10124
- return null;
10125
- }
10126
-
10127
- // src/lib/remote-auth/recovery/inspect.ts
10128
- import * as fs4 from "node:fs";
10129
- import path18 from "node:path";
10130
- import git2 from "isomorphic-git";
10131
-
10132
- // src/lib/remote-auth/recovery/locks.ts
10133
- import { readdir as readdir6, rm as rm4, stat as stat6 } from "node:fs/promises";
10134
- import path17 from "node:path";
10135
- var TOP_LEVEL_LOCK_NAMES = [
10136
- "index.lock",
10137
- "HEAD.lock",
10138
- "config.lock",
10139
- "packed-refs.lock"
10140
- ];
10141
- async function collectRefLockPaths(dir, out) {
10142
- let entries;
10143
- try {
10144
- entries = await readdir6(dir, { withFileTypes: true });
10145
- } catch {
10146
- return;
10147
- }
10148
- for (const entry of entries) {
10149
- const abs = path17.join(dir, entry.name);
10150
- if (entry.isDirectory()) {
10151
- await collectRefLockPaths(abs, out);
10152
- } else if (entry.isFile() && entry.name.endsWith(".lock")) {
10153
- out.push(abs);
10154
- }
10155
- }
10156
- }
10157
- async function findLockCandidates(gitDir, now) {
10158
- const paths = TOP_LEVEL_LOCK_NAMES.map((name) => path17.join(gitDir, name));
10159
- await collectRefLockPaths(path17.join(gitDir, "refs"), paths);
10160
- const candidates = [];
10161
- for (const p of paths) {
10162
- try {
10163
- const s = await stat6(p);
10164
- if (s.isFile())
10165
- candidates.push({ path: p, ageMs: now - s.mtimeMs });
10166
- } catch {}
10167
- }
10168
- return candidates;
10169
- }
10170
- async function sweepStaleLocks(gitDir, now = Date.now(), minAgeMs = STALE_LOCK_MIN_AGE_MS) {
10171
- const candidates = await findLockCandidates(gitDir, now);
10172
- if (candidates.length === 0)
10173
- return "clean";
10174
- if (candidates.some((c) => c.ageMs < minAgeMs))
10175
- return "fresh";
10176
- for (const c of candidates) {
10177
- await rm4(c.path, { force: true });
10178
- }
10179
- return "swept";
10180
- }
10181
-
10182
- // src/lib/remote-auth/recovery/inspect.ts
10183
- async function inspectRepo(ctx, opts = {}) {
10184
- let repoDir = ctx.repoDir;
10185
- try {
10186
- const source = ctx.source ?? await detectProjectSource(ctx.repoDir);
10187
- if (source.type === "local-git-folder")
10188
- repoDir = gitScopeFor(source);
10189
- } catch {}
10190
- const gitDir = gitDirFor(repoDir);
10191
- const hasGitDir = fs4.existsSync(gitDir);
10192
- if (!hasGitDir) {
10193
- return {
10194
- hasGitDir: false,
10195
- isDetachedHead: false,
10196
- headUnreadable: false,
10197
- hasStaleLock: false,
10198
- hasInterruptedMerge: false,
10199
- hasInterruptedRebase: false,
10200
- hasInterruptedCherryPick: false,
10201
- hasLocalChanges: false
10202
- };
10203
- }
10204
- let currentBranch;
10205
- let isDetachedHead = false;
10206
- let headUnreadable = false;
10207
- try {
10208
- const branch = await git2.currentBranch({ fs: fs4, dir: repoDir });
10209
- if (branch == null) {
10210
- isDetachedHead = true;
10211
- } else {
10212
- currentBranch = branch;
10213
- }
10214
- } catch {
10215
- headUnreadable = true;
10216
- }
10217
- const lockCandidates = await findLockCandidates(gitDir, Date.now());
10218
- const hasStaleLock = lockCandidates.length > 0;
10219
- const lockAgeMs = hasStaleLock ? Math.min(...lockCandidates.map((c) => c.ageMs)) : undefined;
10220
- const hasInterruptedMerge = fs4.existsSync(path18.join(gitDir, "MERGE_HEAD"));
10221
- const hasInterruptedRebase = fs4.existsSync(path18.join(gitDir, "rebase-merge")) || fs4.existsSync(path18.join(gitDir, "rebase-apply"));
10222
- const hasInterruptedCherryPick = fs4.existsSync(path18.join(gitDir, "CHERRY_PICK_HEAD"));
10223
- let hasLocalChanges = false;
10224
- if (opts.checkLocalChanges !== false) {
10225
- try {
10226
- hasLocalChanges = await hasPendingChanges(repoDir);
10227
- } catch {
10228
- hasLocalChanges = true;
10229
- }
10230
- }
10231
- return {
10232
- hasGitDir,
10233
- currentBranch,
10234
- isDetachedHead,
10235
- headUnreadable,
10236
- hasStaleLock,
10237
- lockAgeMs,
10238
- hasInterruptedMerge,
10239
- hasInterruptedRebase,
10240
- hasInterruptedCherryPick,
10241
- hasLocalChanges
10242
- };
10243
- }
10244
- async function verifyRepoReadable(dir) {
10245
- const headOid = await git2.resolveRef({ fs: fs4, dir, ref: "HEAD" });
10246
- const { commit } = await git2.readCommit({ fs: fs4, dir, oid: headOid });
10247
- await git2.readTree({ fs: fs4, dir, oid: commit.tree });
10248
- }
10249
- function isUnbornRepo(repoDir) {
10250
- const objectsDir = path18.join(gitDirFor(repoDir), "objects");
10251
- let entries;
10252
- try {
10253
- entries = fs4.readdirSync(objectsDir);
10254
- } catch {
10255
- return false;
10256
- }
10257
- for (const entry of entries) {
10258
- if (/^[0-9a-f]{2}$/.test(entry)) {
10259
- try {
10260
- if (fs4.readdirSync(path18.join(objectsDir, entry)).length > 0)
10261
- return false;
10262
- } catch {
10263
- return false;
10264
- }
10265
- } else if (entry === "pack") {
10266
- try {
10267
- if (fs4.readdirSync(path18.join(objectsDir, entry)).some((f) => f.endsWith(".pack"))) {
10268
- return false;
10269
- }
10270
- } catch {
10271
- return false;
10272
- }
10273
- }
10274
- }
10275
- return true;
10276
- }
10277
-
10278
10155
  // src/lib/publish/types.ts
10279
10156
  function publishCredentialKey(host, account) {
10280
10157
  const a = (account ?? "").trim();
@@ -10460,25 +10337,25 @@ ${tail}` : ""}`);
10460
10337
  };
10461
10338
 
10462
10339
  // src/lib/publish/providers/guided.ts
10463
- import { copyFile as copyFile2, mkdir as mkdir8, writeFile as writeFile7 } from "node:fs/promises";
10464
- import path19 from "node:path";
10340
+ import { copyFile as copyFile2, mkdir as mkdir8, writeFile as writeFile6 } from "node:fs/promises";
10341
+ import path17 from "node:path";
10465
10342
  async function stageGuidedPackage(req, providerId, listingLines) {
10466
- const packageDir = path19.join(path19.dirname(req.artifact.path), "publish", providerId);
10343
+ const packageDir = path17.join(path17.dirname(req.artifact.path), "publish", providerId);
10467
10344
  await mkdir8(packageDir, { recursive: true });
10468
- await copyFile2(req.artifact.path, path19.join(packageDir, path19.basename(req.artifact.path)));
10345
+ await copyFile2(req.artifact.path, path17.join(packageDir, path17.basename(req.artifact.path)));
10469
10346
  const { title, authors } = req.project;
10470
10347
  const sheet = [
10471
10348
  `# ${title || "Untitled"}`,
10472
10349
  "",
10473
10350
  `- **Authors:** ${authors.length ? authors.join(", ") : "(not set)"}`,
10474
- `- **File:** ${path19.basename(req.artifact.path)}`,
10351
+ `- **File:** ${path17.basename(req.artifact.path)}`,
10475
10352
  ...listingLines,
10476
10353
  "",
10477
10354
  "Copy these details into the platform's listing form when uploading.",
10478
10355
  ""
10479
10356
  ].join(`
10480
10357
  `);
10481
- await writeFile7(path19.join(packageDir, "LISTING.md"), sheet, "utf8");
10358
+ await writeFile6(path17.join(packageDir, "LISTING.md"), sheet, "utf8");
10482
10359
  return packageDir;
10483
10360
  }
10484
10361
 
@@ -10541,8 +10418,8 @@ var drivethrurpgProvider = {
10541
10418
  };
10542
10419
 
10543
10420
  // src/lib/publish/butler.ts
10544
- import { chmod, mkdir as mkdir9, stat as stat7, writeFile as writeFile8 } from "node:fs/promises";
10545
- import path20 from "node:path";
10421
+ import { chmod, mkdir as mkdir9, stat as stat6, writeFile as writeFile7 } from "node:fs/promises";
10422
+ import path18 from "node:path";
10546
10423
  import { unzipSync } from "fflate";
10547
10424
  var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
10548
10425
  function butlerBrothChannel(platform = process.platform, arch = process.arch) {
@@ -10562,7 +10439,7 @@ function butlerBinaryName(platform = process.platform) {
10562
10439
  }
10563
10440
  async function fileExists2(p) {
10564
10441
  try {
10565
- return (await stat7(p)).isFile();
10442
+ return (await stat6(p)).isFile();
10566
10443
  } catch {
10567
10444
  return false;
10568
10445
  }
@@ -10579,8 +10456,8 @@ async function ensureButler(deps) {
10579
10456
  }
10580
10457
  if (await commandExists("butler", runCommand, deps.env))
10581
10458
  return "butler";
10582
- const cacheDir = path20.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
10583
- const cached = path20.join(cacheDir, butlerBinaryName());
10459
+ const cacheDir = path18.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
10460
+ const cached = path18.join(cacheDir, butlerBinaryName());
10584
10461
  if (await fileExists2(cached))
10585
10462
  return cached;
10586
10463
  const channel = butlerBrothChannel();
@@ -10607,9 +10484,9 @@ async function ensureButler(deps) {
10607
10484
  for (const [name, data] of Object.entries(files)) {
10608
10485
  if (name.endsWith("/"))
10609
10486
  continue;
10610
- const target = path20.join(cacheDir, path20.basename(name));
10611
- await writeFile8(target, data);
10612
- if (path20.basename(name) === butlerBinaryName()) {
10487
+ const target = path18.join(cacheDir, path18.basename(name));
10488
+ await writeFile7(target, data);
10489
+ if (path18.basename(name) === butlerBinaryName()) {
10613
10490
  await chmod(target, 493).catch(() => {});
10614
10491
  foundBinary = true;
10615
10492
  }
@@ -11037,8 +10914,8 @@ function publishProviderFor(id) {
11037
10914
  }
11038
10915
 
11039
10916
  // src/lib/publish/run-publish.ts
11040
- import { readdir as readdir7, stat as stat8 } from "node:fs/promises";
11041
- import path21 from "node:path";
10917
+ import { readdir as readdir6, stat as stat7 } from "node:fs/promises";
10918
+ import path19 from "node:path";
11042
10919
  async function resolvePublishRequest(options, deps) {
11043
10920
  const provider = publishProviderFor(options.providerId);
11044
10921
  const { manifest, manifestDir } = await loadManifestWithPath(options.manifestPath ?? options.projectDir, { explicit: options.manifestPath !== undefined });
@@ -11049,9 +10926,9 @@ async function resolvePublishRequest(options, deps) {
11049
10926
  const effectiveAccount = bookAccount || deps.credentialAccount;
11050
10927
  const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
11051
10928
  const outDir = resolveOutputDir(manifestDir, config.title);
11052
- const defaultArtifact = provider.info.format === "pdf" ? path21.join(outDir, artifactName(config.title, "pdf")) : outDir;
10929
+ const defaultArtifact = provider.info.format === "pdf" ? path19.join(outDir, artifactName(config.title, "pdf")) : outDir;
11053
10930
  const artifact = {
11054
- path: options.artifactPath ? path21.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
10931
+ path: options.artifactPath ? path19.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
11055
10932
  format: provider.info.format
11056
10933
  };
11057
10934
  return {
@@ -11069,7 +10946,7 @@ var PDF_HINT = "Build the PDF first (gutterpress build, or export it from the ap
11069
10946
  var HTML_HINT = "Build the website export first (gutterpress build --format html).";
11070
10947
  async function artifactIssues(artifact) {
11071
10948
  try {
11072
- const s = await stat8(artifact.path);
10949
+ const s = await stat7(artifact.path);
11073
10950
  if (artifact.format === "pdf" && !s.isFile()) {
11074
10951
  return [
11075
10952
  {
@@ -11115,7 +10992,7 @@ async function artifactIssues(artifact) {
11115
10992
  var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
11116
10993
  async function htmlDirIssues(dir) {
11117
10994
  const issues = [];
11118
- const isThere = async (rel) => stat8(path21.join(dir, rel)).then(() => true, () => false);
10995
+ const isThere = async (rel) => stat7(path19.join(dir, rel)).then(() => true, () => false);
11119
10996
  if (!await isThere(BOOK_HTML)) {
11120
10997
  issues.push({
11121
10998
  severity: "error",
@@ -11123,7 +11000,7 @@ async function htmlDirIssues(dir) {
11123
11000
  message: `${dir} has no ${BOOK_HTML} — it isn't an HTML export. ${HTML_HINT}`
11124
11001
  });
11125
11002
  }
11126
- const entries = await readdir7(dir).catch(() => []);
11003
+ const entries = await readdir6(dir).catch(() => []);
11127
11004
  const extras = entries.filter((name) => name.toLowerCase().endsWith(".pdf") || name === BUILD_FINGERPRINT_FILENAME);
11128
11005
  if (extras.length > 0) {
11129
11006
  issues.push({
@@ -11222,285 +11099,39 @@ async function connectPublishProvider(options, deps) {
11222
11099
  await deps.tokenStore.set(key, candidate);
11223
11100
  return { connected: true, providerId: info7.id };
11224
11101
  }
11225
-
11226
- // src/lib/remote-auth/recovery/repair.ts
11227
- import * as fs7 from "node:fs";
11228
- import { cp as cp3, mkdtemp as mkdtemp3, rename as rename3, rm as rm6 } from "node:fs/promises";
11229
- import os3 from "node:os";
11230
- import path24 from "node:path";
11231
- import git6 from "isomorphic-git";
11232
-
11233
- // src/lib/remote-auth/converge-merge.ts
11234
- import * as fs5 from "node:fs";
11235
- import { mkdir as mkdir10, unlink as unlink3, writeFile as writeFile9 } from "node:fs/promises";
11236
- import path22 from "node:path";
11237
- import git3 from "isomorphic-git";
11238
- import diff3Merge from "diff3";
11239
- var CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
11240
- var CONVERGE_RESTORE_MESSAGE = "Kept the newest version of files that can't be combined";
11241
- var CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
11242
- var IMAGE_EXT = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)$/i;
11243
- var OUR_LABEL = "your version";
11244
- var THEIR_LABEL = "online version";
11245
- function stringLooksBinary(s) {
11246
- return s.includes("\x00");
11247
- }
11248
- var LINEBREAKS = /^.*(\r?\n|$)/gm;
11249
- function mergeWithMarkers(baseContent, ourContent, theirContent) {
11250
- const result = diff3Merge(ourContent.match(LINEBREAKS) ?? [], baseContent.match(LINEBREAKS) ?? [], theirContent.match(LINEBREAKS) ?? []);
11251
- let merged = "";
11252
- for (const item of result) {
11253
- if (item.ok)
11254
- merged += item.ok.join("");
11255
- if (item.conflict) {
11256
- merged += `<<<<<<< ${OUR_LABEL}
11257
- `;
11258
- merged += item.conflict.a.join("");
11259
- merged += `=======
11260
- `;
11261
- merged += item.conflict.b.join("");
11262
- merged += `>>>>>>> ${THEIR_LABEL}
11263
- `;
11264
- }
11265
- }
11266
- return merged;
11267
- }
11268
- async function readBlobOrNull(dir, cache2, oid, filepath) {
11269
- try {
11270
- const r = await git3.readBlob({ fs: fs5, dir, cache: cache2, oid, filepath });
11271
- return { oid: r.oid, blob: r.blob };
11272
- } catch {
11273
- return null;
11274
- }
11275
- }
11276
- async function writeTreeFile(dir, filepath, content) {
11277
- const abs = path22.join(dir, filepath);
11278
- await mkdir10(path22.dirname(abs), { recursive: true });
11279
- await writeFile9(abs, content);
11280
- }
11281
- async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
11282
- const bases = await git3.findMergeBase({ fs: fs5, dir, cache: cache2, oids: [ourTip, theirTip] });
11283
- const base = bases[0];
11284
- if (!base && !allowUnrelated)
11285
- return null;
11286
- const out = [];
11287
- await git3.walk({
11288
- fs: fs5,
11289
- dir,
11290
- cache: cache2,
11291
- trees: [
11292
- git3.TREE({ ref: ourTip }),
11293
- git3.TREE({ ref: theirTip }),
11294
- ...base ? [git3.TREE({ ref: base })] : []
11295
- ],
11296
- map: async (filepath, entries) => {
11297
- if (filepath === ".")
11298
- return true;
11299
- const [ours, theirs, inBase] = entries;
11300
- const types = await Promise.all([ours, theirs, inBase].map((e) => e ? e.type() : Promise.resolve(undefined)));
11301
- if (types.some((t) => t === "tree"))
11302
- return true;
11303
- if (!ours || !theirs || inBase)
11304
- return null;
11305
- const [a, b] = await Promise.all([ours.oid(), theirs.oid()]);
11306
- if (a !== b)
11307
- out.push(filepath);
11308
- return null;
11309
- }
11310
- });
11311
- return out;
11312
- }
11313
- async function convergeMerge(params) {
11314
- const { dir, cache: cache2, branch, theirs, author } = params;
11315
- const ourTip = await git3.resolveRef({ fs: fs5, dir, ref: branch });
11316
- const [ourCommit, theirCommit] = await Promise.all([
11317
- git3.readCommit({ fs: fs5, dir, cache: cache2, oid: ourTip }),
11318
- git3.readCommit({ fs: fs5, dir, cache: cache2, oid: theirs })
11319
- ]);
11320
- const localNewer = ourCommit.commit.committer.timestamp >= theirCommit.commit.committer.timestamp;
11321
- const combined = new Set;
11322
- const driverBinary = new Set;
11323
- const imageClashes = [];
11324
- const attempt = () => git3.merge({
11325
- fs: fs5,
11326
- dir,
11327
- cache: cache2,
11328
- ours: branch,
11329
- theirs,
11330
- author,
11331
- message: CONVERGE_MERGE_MESSAGE,
11332
- allowUnrelatedHistories: params.allowUnrelatedHistories ?? false,
11333
- mergeDriver: ({ contents, path: filepath }) => {
11334
- const [base, ours, theirsContent] = contents;
11335
- if (IMAGE_EXT.test(filepath) || stringLooksBinary(ours) || stringLooksBinary(theirsContent) || stringLooksBinary(base)) {
11336
- driverBinary.add(filepath);
11337
- return { cleanMerge: false, mergedText: ours };
11338
- }
11339
- combined.add(filepath);
11340
- return {
11341
- cleanMerge: true,
11342
- mergedText: mergeWithMarkers(base, ours, theirsContent)
11343
- };
11344
- }
11345
- });
11346
- const snapshot = (message) => snapshotWorkingTreeUnlocked({
11347
- projectDir: dir,
11348
- repoRoot: dir,
11349
- message,
11350
- authorName: params.authorName,
11351
- authorEmail: params.authorEmail
11352
- });
11353
- const postRestore = [];
11354
- const equalize = async (paths) => {
11355
- for (const p of paths.binary) {
11356
- const [ourBlob, theirBlob] = await Promise.all([
11357
- readBlobOrNull(dir, cache2, ourTip, p),
11358
- readBlobOrNull(dir, cache2, theirs, p)
11359
- ]);
11360
- if (!ourBlob || !theirBlob)
11361
- continue;
11362
- await writeTreeFile(dir, p, theirBlob.blob);
11363
- if (localNewer)
11364
- postRestore.push({ path: p, bytes: ourBlob.blob });
11365
- if (IMAGE_EXT.test(p)) {
11366
- imageClashes.push({
11367
- path: p,
11368
- localOid: ourBlob.oid,
11369
- remoteOid: theirBlob.oid,
11370
- kept: localNewer ? "local" : "online"
11371
- });
11372
- }
11373
- }
11374
- for (const p of paths.deleteByUs) {
11375
- const theirBlob = await readBlobOrNull(dir, cache2, theirs, p);
11376
- if (theirBlob)
11377
- await writeTreeFile(dir, p, theirBlob.blob);
11378
- }
11379
- for (const p of paths.deleteByTheirs) {
11380
- const ourBlob = await readBlobOrNull(dir, cache2, ourTip, p);
11381
- await unlink3(path22.join(dir, p)).catch((e) => {
11382
- if (e?.code !== "ENOENT")
11383
- throw e;
11384
- });
11385
- if (ourBlob)
11386
- postRestore.push({ path: p, bytes: ourBlob.blob });
11387
- }
11388
- if (await hasPendingChanges(dir))
11389
- await snapshot(CONVERGE_PREPARE_MESSAGE);
11390
- };
11391
- try {
11392
- await attempt();
11393
- } catch (e) {
11394
- if (isMergeConflictError(e)) {
11395
- await equalize({
11396
- binary: (e.data.bothModified ?? []).filter((p) => driverBinary.has(p)),
11397
- deleteByUs: e.data.deleteByUs ?? [],
11398
- deleteByTheirs: e.data.deleteByTheirs ?? []
11399
- });
11400
- await attempt();
11401
- } else if (e?.code === "MergeNotSupportedError") {
11402
- const added = await bothAddedPaths(dir, cache2, ourTip, theirs, params.allowUnrelatedHistories ?? false);
11403
- if (added === null || added.length === 0)
11404
- throw e;
11405
- await equalize({ binary: added, deleteByUs: [], deleteByTheirs: [] });
11406
- await attempt();
11407
- } else {
11408
- throw e;
11409
- }
11410
- }
11411
- await git3.checkout({ fs: fs5, dir, cache: cache2, ref: branch, force: true });
11412
- if (postRestore.length > 0) {
11413
- for (const r of postRestore)
11414
- await writeTreeFile(dir, r.path, r.bytes);
11415
- if (await hasPendingChanges(dir))
11416
- await snapshot(CONVERGE_RESTORE_MESSAGE);
11417
- }
11418
- return {
11419
- oid: await git3.resolveRef({ fs: fs5, dir, ref: branch }),
11420
- combinedFiles: [...combined].sort(),
11421
- imageClashes
11422
- };
11423
- }
11424
-
11425
- // src/lib/remote-auth/transport.ts
11426
- import git4 from "isomorphic-git";
11427
-
11428
- // src/lib/remote-auth/sync-messages.ts
11429
- var MSG_NO_REMOTE = "This project isn't connected to an online repository yet.";
11430
- 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.";
11431
-
11432
- // src/lib/remote-auth/transport.ts
11433
- async function repoDirFor(projectDir) {
11434
- const source = await detectProjectSource(projectDir);
11435
- if (source.type === "local-git-folder")
11436
- return gitScopeFor(source);
11437
- return projectDir;
11438
- }
11439
- function isCredentialTransmissionSafe(url) {
11440
- try {
11441
- const u = new URL(url);
11442
- if (u.protocol === "https:")
11443
- return true;
11444
- if (u.protocol === "http:") {
11445
- const host = u.hostname.toLowerCase();
11446
- return host === "127.0.0.1" || host === "[::1]" || host === "localhost";
11447
- }
11448
- return false;
11449
- } catch {
11450
- return false;
11451
- }
11452
- }
11453
- function onAuthFor(credential) {
11454
- if (!credential)
11455
- return {};
11456
- return {
11457
- onAuth: (url) => {
11458
- if (!isCredentialTransmissionSafe(url))
11459
- throw new InsecureTransportError;
11460
- return {
11461
- username: credential.kind === "github-oauth" ? "x-access-token" : credential.username || credential.token,
11462
- password: credential.token
11463
- };
11464
- }
11465
- };
11466
- }
11467
- async function resolveTransport(dir, options) {
11468
- const origin = await syncRemoteFor(dir);
11469
- if (!origin?.url)
11470
- throw new Error(MSG_NO_REMOTE);
11471
- const { cleanUrl, credential: urlCredential } = extractUrlCredential(origin.url);
11472
- let parsed;
11473
- try {
11474
- parsed = new URL(cleanUrl);
11475
- } catch {
11476
- throw new Error(MSG_SSH_REMOTE);
11477
- }
11478
- if (!/^https?:$/.test(parsed.protocol))
11479
- throw new Error(MSG_SSH_REMOTE);
11480
- const host = credentialHostKey(cleanUrl);
11481
- let credential = options.credential ?? undefined;
11482
- if (!credential && options.tokenStore) {
11483
- try {
11484
- credential = await options.tokenStore.get(host) ?? undefined;
11485
- } catch {
11486
- credential = undefined;
11487
- }
11488
- }
11489
- if (!credential && urlCredential)
11490
- credential = urlCredential;
11491
- return {
11492
- remote: origin.remote,
11493
- url: cleanUrl,
11494
- host,
11495
- ...credential ? { credential } : {}
11496
- };
11497
- }
11498
-
11102
+ // src/lib/theme-manager.ts
11103
+ import { isSeq as isSeq4, Scalar as Scalar2 } from "yaml";
11104
+ var THEMES_DIR = "themes";
11105
+ var THEME_HREF_RE = new RegExp(`^${THEMES_DIR}/([^/]+)/theme\\.css$`);
11106
+ // src/lib/theme-import.ts
11107
+ import { unzipSync as unzipSync2 } from "fflate";
11108
+ var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
11109
+ var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
11110
+ var KNOWN_THEME_FILES = new Set(["theme.css", "theme.json"]);
11111
+ var ALLOWED_ASSET_EXTS = new Set([
11112
+ ".woff",
11113
+ ".woff2",
11114
+ ".ttf",
11115
+ ".otf",
11116
+ ".eot",
11117
+ ".png",
11118
+ ".jpg",
11119
+ ".jpeg",
11120
+ ".gif",
11121
+ ".svg",
11122
+ ".webp",
11123
+ ".avif",
11124
+ ".css"
11125
+ ]);
11126
+ // src/lib/manifest-config.ts
11127
+ import { isSeq as isSeq5, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
11128
+ // src/lib/host-policy.ts
11129
+ var AUTO_SNAPSHOT_MAX_MINUTES = 24 * 60;
11130
+ var AUTO_SYNC_MAX_MINUTES = 24 * 60;
11131
+ // src/lib/remote-auth/github-repos.ts
11132
+ var MANIFEST_NAMES = new Set(MANIFEST_FILENAMES);
11499
11133
  // src/lib/remote-auth/clone.ts
11500
- import * as fs6 from "node:fs";
11501
- import { mkdir as mkdir11, readdir as readdir8, readFile as readFile25, rm as rm5, writeFile as writeFile10 } from "node:fs/promises";
11502
- import path23 from "node:path";
11503
- import git5 from "isomorphic-git";
11134
+ import git3 from "isomorphic-git";
11504
11135
 
11505
11136
  // src/lib/remote-auth/git-http.ts
11506
11137
  import httpNode from "isomorphic-git/http/node";
@@ -11593,525 +11224,29 @@ function withIdleTimeout(http2, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
11593
11224
  }
11594
11225
  var defaultGitHttp = withIdleTimeout(httpNode);
11595
11226
 
11596
- // src/lib/remote-auth/github-auth.ts
11597
- var OFFLINE_MESSAGE = "Couldn't reach GitHub. Check your connection and try again.";
11598
-
11599
- // src/lib/remote-auth/clone.ts
11600
- var PROVENANCE_FILE = "gutterpress-remote.json";
11601
- function provenancePath(projectDir) {
11602
- return path23.join(projectDir, ".git", PROVENANCE_FILE);
11603
- }
11604
- async function assertCloneTarget(dir) {
11605
- try {
11606
- const entries = await readdir8(dir);
11607
- if (entries.length > 0) {
11608
- throw new Error("That folder already has files in it. Choose an empty folder for the project.");
11609
- }
11610
- } catch (e) {
11611
- if (e.code === "ENOENT")
11612
- return;
11613
- throw e;
11614
- }
11615
- }
11616
- function friendlyCloneError(e) {
11617
- if (isInsecureTransportError(e)) {
11618
- return new Error("That repository address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Use a secure address (starting with https), or a local loopback address for a server on this computer.", { cause: e });
11619
- }
11620
- const msg = e instanceof Error ? e.message : String(e);
11621
- if (/401|403|auth|credential/i.test(msg)) {
11622
- return new Error("GitHub didn't accept the connection for this repository. Reconnect GitHub and try again.", { cause: e });
11623
- }
11624
- if (/404|not found/i.test(msg)) {
11625
- return new Error("That repository couldn't be found. It may have been renamed or you may no longer have access.", { cause: e });
11626
- }
11627
- if (/ENOTFOUND|ECONNREFUSED|ETIMEDOUT|EAI_AGAIN|network|fetch failed/i.test(msg)) {
11628
- return new Error(OFFLINE_MESSAGE, { cause: e });
11629
- }
11630
- if (e instanceof Error && /already has files/i.test(e.message)) {
11631
- return e;
11632
- }
11633
- return new Error("The project couldn't be downloaded from GitHub. Please try again.", { cause: e });
11634
- }
11635
- async function cloneRepository(options) {
11636
- const { dir, onProgress, tokenStore, provenance } = options;
11637
- if (!path23.isAbsolute(dir)) {
11638
- throw new Error("cloneRepository requires an absolute destination path.");
11639
- }
11640
- let parsed;
11641
- try {
11642
- parsed = new URL(options.url);
11643
- } catch {
11644
- throw new Error("That repository address is not a valid web URL.");
11645
- }
11646
- if (!/^https?:$/.test(parsed.protocol)) {
11647
- throw new Error("Only HTTPS repository addresses are supported. SSH addresses (git@…) can't be used here.");
11648
- }
11649
- const { cleanUrl, credential: urlCredential } = extractUrlCredential(options.url);
11650
- const credential = options.credential ?? urlCredential;
11651
- if (urlCredential && tokenStore && !await tokenStore.get(urlCredential.host)) {
11652
- await tokenStore.set(urlCredential.host, urlCredential);
11653
- }
11654
- return withRepoLock(dir, async () => {
11655
- await assertCloneTarget(dir);
11656
- const dirExistedBefore = fs6.existsSync(dir);
11657
- await mkdir11(dir, { recursive: true });
11658
- try {
11659
- await git5.clone({
11660
- fs: fs6,
11661
- http: options.httpClient ?? defaultGitHttp,
11662
- dir,
11663
- url: cleanUrl,
11664
- singleBranch: true,
11665
- ...options.branch ? { ref: options.branch } : {},
11666
- ...options.depth ? { depth: options.depth } : {},
11667
- ...onAuthFor(credential),
11668
- ...onProgress ? {
11669
- onProgress: (p) => onProgress({
11670
- phase: p.phase,
11671
- loaded: p.loaded,
11672
- ...p.total ? { total: p.total } : {}
11673
- })
11674
- } : {}
11675
- });
11676
- } catch (e) {
11677
- const friendly = friendlyCloneError(e);
11678
- if (!dirExistedBefore) {
11679
- try {
11680
- await rm5(dir, { recursive: true, force: true });
11681
- } catch {
11682
- throw new Error(`${friendly.message} A partial download was left at ${dir} — delete that folder before trying again.`, { cause: e });
11683
- }
11684
- }
11685
- throw friendly;
11686
- }
11687
- if (provenance) {
11688
- try {
11689
- await writeFile10(provenancePath(dir), JSON.stringify(provenance, null, 2), "utf8");
11690
- } catch {}
11691
- }
11692
- let branch;
11693
- try {
11694
- branch = await git5.currentBranch({ fs: fs6, dir }) ?? undefined;
11695
- } catch {
11696
- branch = undefined;
11697
- }
11698
- return { projectDir: dir, ...branch ? { branch } : {} };
11699
- });
11700
- }
11701
-
11702
- // src/lib/remote-auth/recovery/repair.ts
11703
- var INTERRUPTED_STATE = [
11704
- "MERGE_HEAD",
11705
- "MERGE_MSG",
11706
- "MERGE_MODE",
11707
- "CHERRY_PICK_HEAD",
11708
- "REVERT_HEAD",
11709
- "sequencer",
11710
- "rebase-merge",
11711
- "rebase-apply"
11712
- ];
11713
- var MSG_REPAIRED = "Your project's version history is working again. Your files were not changed.";
11714
- var MSG_RETRY = "Another program may be using this project's history right now — try again in a moment.";
11715
- var MSG_FAILED = "Your project's history couldn't be repaired automatically, but your files are safe and untouched.";
11716
- function stamp() {
11717
- return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
11718
- }
11719
- async function indexReadable(dir) {
11720
- try {
11721
- await git6.listFiles({ fs: fs7, dir });
11722
- return true;
11723
- } catch {
11724
- return false;
11725
- }
11726
- }
11727
- async function rebuildIndexFromHead(dir) {
11728
- await rm6(path24.join(gitDirFor(dir), "index"), { force: true });
11729
- const cache2 = {};
11730
- let files;
11731
- try {
11732
- files = await git6.listFiles({ fs: fs7, dir, ref: "HEAD" });
11733
- } catch {
11734
- return;
11735
- }
11736
- for (const filepath of files) {
11737
- await git6.resetIndex({ fs: fs7, dir, cache: cache2, filepath, ref: "HEAD" });
11738
- }
11739
- }
11740
- function readDamagedBranchTips(damagedGitDir) {
11741
- const tips = new Map;
11742
- const OID = /^[0-9a-f]{40}$/;
11743
- try {
11744
- const packed = fs7.readFileSync(path24.join(damagedGitDir, "packed-refs"), "utf8");
11745
- for (const line of packed.split(`
11746
- `)) {
11747
- const m = /^([0-9a-f]{40}) refs\/heads\/(.+)$/.exec(line.trim());
11748
- if (m)
11749
- tips.set(m[2], m[1]);
11750
- }
11751
- } catch {}
11752
- const headsDir = path24.join(damagedGitDir, "refs", "heads");
11753
- const walk = (dir, prefix) => {
11754
- let entries;
11755
- try {
11756
- entries = fs7.readdirSync(dir, { withFileTypes: true });
11757
- } catch {
11758
- return;
11759
- }
11760
- for (const e of entries) {
11761
- const name = prefix ? `${prefix}/${e.name}` : e.name;
11762
- if (e.isDirectory())
11763
- walk(path24.join(dir, e.name), name);
11764
- else if (e.isFile()) {
11765
- try {
11766
- const oid = fs7.readFileSync(path24.join(dir, e.name), "utf8").trim();
11767
- if (OID.test(oid))
11768
- tips.set(name, oid);
11769
- } catch {}
11770
- }
11771
- }
11772
- };
11773
- walk(headsDir, "");
11774
- return [...tips].map(([name, oid]) => ({ name, oid }));
11775
- }
11776
- async function looseCommitTips(dir, cache2) {
11777
- const objectsDir = path24.join(gitDirFor(dir), "objects");
11778
- const oids = [];
11779
- let fanouts;
11780
- try {
11781
- fanouts = fs7.readdirSync(objectsDir).filter((e) => /^[0-9a-f]{2}$/.test(e));
11782
- } catch {
11783
- return [];
11784
- }
11785
- for (const fan of fanouts) {
11786
- let files;
11787
- try {
11788
- files = fs7.readdirSync(path24.join(objectsDir, fan));
11789
- } catch {
11790
- continue;
11791
- }
11792
- for (const f of files) {
11793
- if (/^[0-9a-f]{38}$/.test(f))
11794
- oids.push(fan + f);
11795
- }
11796
- }
11797
- const commits = new Map;
11798
- for (const oid of oids) {
11799
- try {
11800
- const obj = await git6.readObject({ fs: fs7, dir, cache: cache2, oid, format: "parsed" });
11801
- if (obj.type === "commit") {
11802
- commits.set(oid, obj.object.parent ?? []);
11803
- }
11804
- } catch {}
11805
- }
11806
- const isParent = new Set;
11807
- for (const parents of commits.values())
11808
- for (const p of parents)
11809
- isParent.add(p);
11810
- return [...commits.keys()].filter((oid) => !isParent.has(oid));
11811
- }
11812
- async function targetBranch(dir) {
11813
- try {
11814
- const branches = await git6.listBranches({ fs: fs7, dir });
11815
- if (branches.includes("main"))
11816
- return "main";
11817
- if (branches.includes("master"))
11818
- return "master";
11819
- if (branches.length > 0)
11820
- return branches[0];
11821
- } catch {}
11822
- return "main";
11823
- }
11824
- async function repairRepo(options) {
11825
- const dir = await repoDirFor(options.projectDir);
11826
- return withRepoLock(dir, () => repairLocked(dir, options));
11827
- }
11828
- async function repairLocked(dir, options) {
11829
- const logger = resolveLogger(options.logFile, "repair");
11830
- const gitDir = gitDirFor(dir);
11831
- const actions = [];
11832
- const snapshotFiles = async (message) => {
11833
- try {
11834
- if (await hasPendingChanges(dir)) {
11835
- await snapshotWorkingTreeUnlocked({
11836
- projectDir: dir,
11837
- repoRoot: dir,
11838
- message,
11839
- authorName: options.authorName,
11840
- authorEmail: options.authorEmail
11841
- });
11842
- }
11843
- } catch (e) {
11844
- if (!isNoChangesError(e))
11845
- throw e;
11846
- }
11847
- };
11848
- try {
11849
- if (fs7.existsSync(gitDir)) {
11850
- const sweep = await sweepStaleLocks(gitDir);
11851
- if (sweep === "fresh") {
11852
- logger.info("repair", "fresh lock present — deferring");
11853
- return { status: "retry_later", message: MSG_RETRY, actions, retryAfterMs: 30000 };
11854
- }
11855
- if (sweep === "swept") {
11856
- actions.push("Removed leftover lock files from an interrupted operation.");
11857
- logger.info("repair", "swept stale locks");
11858
- }
11859
- }
11860
- if (fs7.existsSync(gitDir)) {
11861
- let removedState = false;
11862
- for (const name of INTERRUPTED_STATE) {
11863
- const p = path24.join(gitDir, name);
11864
- if (fs7.existsSync(p)) {
11865
- await rm6(p, { recursive: true, force: true });
11866
- removedState = true;
11867
- }
11868
- }
11869
- if (removedState) {
11870
- actions.push("Cleared an unfinished operation another program left behind.");
11871
- logger.info("repair", "removed interrupted-operation state");
11872
- await rebuildIndexFromHead(dir);
11873
- } else if (!await indexReadable(dir)) {
11874
- await rebuildIndexFromHead(dir);
11875
- actions.push("Rebuilt the project's tracking information.");
11876
- logger.info("repair", "rebuilt corrupt index");
11877
- }
11878
- }
11879
- let health = await inspectRepo({ repoDir: dir }, { checkLocalChanges: false });
11880
- if (health.hasGitDir && !health.headUnreadable && health.isDetachedHead) {
11881
- await snapshotFiles("Saved your latest changes");
11882
- const headOid = await git6.resolveRef({ fs: fs7, dir, ref: "HEAD" });
11883
- const rescueRef = `refs/heads/recovered-${stamp()}`;
11884
- await git6.writeRef({ fs: fs7, dir, ref: rescueRef, value: headOid, force: true });
11885
- const branch = await targetBranch(dir);
11886
- const branchExists = (await git6.listBranches({ fs: fs7, dir })).includes(branch);
11887
- if (!branchExists) {
11888
- await git6.writeRef({ fs: fs7, dir, ref: `refs/heads/${branch}`, value: headOid, force: true });
11889
- }
11890
- await git6.writeRef({
11891
- fs: fs7,
11892
- dir,
11893
- ref: "HEAD",
11894
- value: `refs/heads/${branch}`,
11895
- symbolic: true,
11896
- force: true
11897
- });
11898
- const cache2 = {};
11899
- await git6.checkout({ fs: fs7, dir, cache: cache2, ref: branch, force: true });
11900
- const branchTip = await git6.resolveRef({ fs: fs7, dir, ref: branch });
11901
- if (branchTip !== headOid) {
11902
- await convergeMerge({
11903
- dir,
11904
- cache: cache2,
11905
- branch,
11906
- theirs: headOid,
11907
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
11908
- authorName: options.authorName,
11909
- authorEmail: options.authorEmail,
11910
- allowUnrelatedHistories: true
11911
- });
11912
- }
11913
- actions.push("Reconnected your work to the project's version line.");
11914
- logger.info("repair", "reattached detached HEAD", { rescueRef });
11915
- }
11916
- health = await inspectRepo({ repoDir: dir }, { checkLocalChanges: false });
11917
- if (classifyFromHealth(health, { minLockAgeMs: 0 }) === null) {
11918
- await snapshotFiles("Saved your latest changes");
11919
- logger.info("repair", "repaired in place", { actions: actions.length });
11920
- return { status: "repaired", message: MSG_REPAIRED, actions };
11921
- }
11922
- return await recloneWithSalvage(dir, options, actions, logger, snapshotFiles);
11923
- } catch (e) {
11924
- logger.error("repair", "repair failed", { error: e instanceof Error ? e.message : String(e) });
11925
- return { status: "failed", message: MSG_FAILED, actions };
11926
- }
11927
- }
11928
- async function recloneWithSalvage(dir, options, actions, logger, snapshotFiles) {
11929
- const gitDir = gitDirFor(dir);
11930
- let remoteUrl;
11931
- let credential;
11932
- try {
11933
- const transport = await resolveTransport(dir, {
11934
- credential: options.credential,
11935
- tokenStore: options.tokenStore
11936
- });
11937
- remoteUrl = transport.url;
11938
- credential = transport.credential;
11939
- } catch {
11940
- remoteUrl = undefined;
11941
- }
11942
- let freshGitDir;
11943
- let tempDir;
11944
- if (remoteUrl) {
11945
- tempDir = await mkdtemp3(path24.join(os3.tmpdir(), "gutterpress-repair-"));
11946
- const cloneDir = path24.join(tempDir, "clone");
11947
- try {
11948
- await cloneRepository({
11949
- url: remoteUrl,
11950
- dir: cloneDir,
11951
- ...credential ? { credential } : {}
11952
- });
11953
- } catch (e) {
11954
- await rm6(tempDir, { recursive: true, force: true });
11955
- logger.warn("repair", "re-clone failed — deferring", {
11956
- error: e instanceof Error ? e.message : String(e)
11957
- });
11958
- return {
11959
- status: "retry_later",
11960
- message: "The online copy couldn't be reached to finish the repair. Your files are safe — it will be tried again.",
11961
- actions,
11962
- retryAfterMs: 60000
11963
- };
11964
- }
11965
- freshGitDir = path24.join(cloneDir, ".git");
11966
- } else {
11967
- tempDir = await mkdtemp3(path24.join(os3.tmpdir(), "gutterpress-repair-"));
11968
- const initDir = path24.join(tempDir, "init");
11969
- fs7.mkdirSync(initDir, { recursive: true });
11970
- await git6.init({ fs: fs7, dir: initDir, defaultBranch: "main" });
11971
- freshGitDir = path24.join(initDir, ".git");
11972
- }
11973
- let damagedBackup;
11974
- if (fs7.existsSync(gitDir)) {
11975
- damagedBackup = path24.join(dir, `.git-damaged-${stamp()}`);
11976
- await rename3(gitDir, damagedBackup);
11977
- actions.push("Kept the old history folder on disk as a backup.");
11978
- }
11979
- await rename3(freshGitDir, gitDir);
11980
- if (tempDir)
11981
- await rm6(tempDir, { recursive: true, force: true });
11982
- actions.push(remoteUrl ? "Rebuilt the project's history from the online copy." : "Started a fresh history for the project (no online copy is connected).");
11983
- logger.info("repair", "replaced .git", { fromRemote: Boolean(remoteUrl), damagedBackup });
11984
- if (damagedBackup) {
11985
- const from = path24.join(damagedBackup, "objects");
11986
- if (fs7.existsSync(from)) {
11987
- await cp3(from, path24.join(gitDir, "objects"), {
11988
- recursive: true,
11989
- force: false,
11990
- errorOnExist: false
11991
- }).catch(() => {});
11992
- }
11993
- const branch = await targetBranch(dir);
11994
- let branchExists = (await git6.listBranches({ fs: fs7, dir })).includes(branch);
11995
- const scanCache = {};
11996
- const tips = new Map;
11997
- for (const tip of readDamagedBranchTips(damagedBackup)) {
11998
- tips.set(tip.oid, tip.name);
11999
- }
12000
- for (const oid of await looseCommitTips(dir, scanCache)) {
12001
- if (!tips.has(oid))
12002
- tips.set(oid, "recovered-work");
12003
- }
12004
- for (const [oid, name] of tips) {
12005
- const cache2 = {};
12006
- try {
12007
- await git6.readCommit({ fs: fs7, dir, cache: cache2, oid });
12008
- } catch {
12009
- logger.warn("repair", "old tip unreadable — left in the backup", { tip: name });
12010
- continue;
12011
- }
12012
- if (!branchExists) {
12013
- await git6.writeRef({ fs: fs7, dir, ref: `refs/heads/${branch}`, value: oid, force: true });
12014
- await git6.writeRef({
12015
- fs: fs7,
12016
- dir,
12017
- ref: "HEAD",
12018
- value: `refs/heads/${branch}`,
12019
- symbolic: true,
12020
- force: true
12021
- });
12022
- await rebuildIndexFromHead(dir);
12023
- branchExists = true;
12024
- actions.push("Restored your saved history from the backup.");
12025
- continue;
12026
- }
12027
- try {
12028
- const branchTip = await git6.resolveRef({ fs: fs7, dir, ref: branch });
12029
- if (branchTip === oid)
12030
- continue;
12031
- await convergeMerge({
12032
- dir,
12033
- cache: cache2,
12034
- branch,
12035
- theirs: oid,
12036
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
12037
- authorName: options.authorName,
12038
- authorEmail: options.authorEmail,
12039
- allowUnrelatedHistories: true
12040
- });
12041
- actions.push("Brought your earlier saved versions back into the history.");
12042
- } catch (e) {
12043
- logger.warn("repair", "old tip could not be merged — pinned as a recovery branch", {
12044
- tip: name,
12045
- error: e instanceof Error ? e.message : String(e)
12046
- });
12047
- await git6.writeRef({
12048
- fs: fs7,
12049
- dir,
12050
- ref: `refs/heads/recovered-${stamp()}-${name.replace(/[^A-Za-z0-9_-]/g, "_")}`,
12051
- value: oid,
12052
- force: true
12053
- }).catch(() => {});
12054
- }
12055
- }
12056
- }
12057
- await snapshotFiles("Saved your files after repairing the project");
12058
- return {
12059
- status: "repaired",
12060
- message: MSG_REPAIRED,
12061
- actions,
12062
- ...damagedBackup ? { damagedGitBackupPath: damagedBackup } : {}
12063
- };
12064
- }
12065
- // src/lib/theme-manager.ts
12066
- import { isSeq as isSeq4, Scalar as Scalar2 } from "yaml";
12067
- var THEMES_DIR = "themes";
12068
- var THEME_HREF_RE = new RegExp(`^${THEMES_DIR}/([^/]+)/theme\\.css$`);
12069
- // src/lib/theme-import.ts
12070
- import { unzipSync as unzipSync2 } from "fflate";
12071
- var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
12072
- var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
12073
- var KNOWN_THEME_FILES = new Set(["theme.css", "theme.json"]);
12074
- var ALLOWED_ASSET_EXTS = new Set([
12075
- ".woff",
12076
- ".woff2",
12077
- ".ttf",
12078
- ".otf",
12079
- ".eot",
12080
- ".png",
12081
- ".jpg",
12082
- ".jpeg",
12083
- ".gif",
12084
- ".svg",
12085
- ".webp",
12086
- ".avif",
12087
- ".css"
12088
- ]);
12089
- // src/lib/manifest-config.ts
12090
- import { isSeq as isSeq5, isMap as isMap3, YAMLSeq as YAMLSeq3, Scalar as Scalar3 } from "yaml";
12091
- // src/lib/host-policy.ts
12092
- var AUTO_SNAPSHOT_MAX_MINUTES = 24 * 60;
12093
- var AUTO_SYNC_MAX_MINUTES = 24 * 60;
12094
- // src/lib/remote-auth/github-repos.ts
12095
- var MANIFEST_NAMES = new Set(MANIFEST_FILENAMES);
11227
+ // src/lib/remote-auth/transport.ts
11228
+ import git2 from "isomorphic-git";
12096
11229
  // src/lib/remote-auth/test-access.ts
12097
- import git7 from "isomorphic-git";
11230
+ import git4 from "isomorphic-git";
12098
11231
  import httpNode2 from "isomorphic-git/http/node";
12099
11232
  // src/lib/remote-auth/sync.ts
12100
- import git8 from "isomorphic-git";
12101
- // src/lib/remote-auth/image-clash.ts
12102
- import git9 from "isomorphic-git";
11233
+ import git6 from "isomorphic-git";
11234
+
11235
+ // src/lib/remote-auth/converge-merge.ts
11236
+ import git5 from "isomorphic-git";
11237
+ import diff3Merge from "diff3";
12103
11238
  // src/lib/publish/selections.ts
12104
- import { chmod as chmod2, mkdir as mkdir12, readFile as readFile26, writeFile as writeFile11 } from "node:fs/promises";
12105
- import path25 from "node:path";
11239
+ import { chmod as chmod2, mkdir as mkdir10, readFile as readFile25, writeFile as writeFile8 } from "node:fs/promises";
11240
+ import path20 from "node:path";
12106
11241
  class PublishSelectionsStore {
12107
11242
  filePath;
12108
11243
  queue = Promise.resolve();
12109
11244
  constructor(filePath) {
12110
- this.filePath = filePath ?? path25.join(defaultConfigDir(), "publish-selections.json");
11245
+ this.filePath = filePath ?? path20.join(defaultConfigDir(), "publish-selections.json");
12111
11246
  }
12112
11247
  async read() {
12113
11248
  try {
12114
- const raw = await readFile26(this.filePath, "utf8");
11249
+ const raw = await readFile25(this.filePath, "utf8");
12115
11250
  const parsed = JSON.parse(raw);
12116
11251
  if (parsed && typeof parsed === "object") {
12117
11252
  return {
@@ -12124,8 +11259,8 @@ class PublishSelectionsStore {
12124
11259
  return { version: 1, global: {}, projects: {} };
12125
11260
  }
12126
11261
  async write(data) {
12127
- await mkdir12(path25.dirname(this.filePath), { recursive: true });
12128
- await writeFile11(this.filePath, JSON.stringify(data, null, 2), {
11262
+ await mkdir10(path20.dirname(this.filePath), { recursive: true });
11263
+ await writeFile8(this.filePath, JSON.stringify(data, null, 2), {
12129
11264
  encoding: "utf8",
12130
11265
  mode: 384
12131
11266
  });
@@ -12190,4 +11325,4 @@ function normalizeProjectKey(projectDir) {
12190
11325
  }
12191
11326
  // src/lib/publish/manifest-publish.ts
12192
11327
  import { isMap as isMap4 } from "yaml";
12193
- export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, parseNpmPluginSpec, runLint, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, scaffoldProject, BUILT_IN_TEMPLATE_IDS, addNpmPlugin, isAppHeartbeatFresh, classifyFromHealth, inspectRepo, verifyRepoReadable, isUnbornRepo, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, runPublish, connectPublishProvider, repairRepo };
11328
+ export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, parseNpmPluginSpec, runLint, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, scaffoldProject, BUILT_IN_TEMPLATE_IDS, addNpmPlugin, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, runPublish, connectPublishProvider };