gutterpress 0.10.0 → 0.10.1-beta.2

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 (57) 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-cvarpa72.js → audit-dce99css.js} +5 -5
  5. package/dist/{build-6r502chw.js → build-bj4x0727.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-cqtggsng.js → cli-0z6zf98d.js} +2 -47
  9. package/dist/{cli-eq5naw4m.js → cli-7n848af2.js} +370 -1249
  10. package/dist/{cli-zfcryxg8.js → cli-eewynen9.js} +97 -60
  11. package/dist/{cli-k1065rkg.js → cli-w692g6dx.js} +75 -29
  12. package/dist/{cli-149edp6b.js → cli-ydpf09d9.js} +1 -1
  13. package/dist/cli.js +14 -15
  14. package/dist/{doctor-akvxbtjb.js → doctor-7nd6n2aa.js} +2 -2
  15. package/dist/{engine-ft4cr3ep.js → engine-57vw8zpd.js} +1 -1
  16. package/dist/{engine-b159tbns.js → engine-8kdwy0jz.js} +2 -2
  17. package/dist/{gutterpress-viewer-te8g5grx.js → gutterpress-viewer-tma1qwz2.js} +57 -15
  18. package/dist/{index-ycpvr0am.js → index-7hrvksrb.js} +4 -4
  19. package/dist/{index-mdefp0y5.js → index-dtrftayr.js} +75 -27
  20. package/dist/{index-ge7q9xj3.js → index-h1x7krkg.js} +771 -1482
  21. package/dist/{index-wq3r5pj7.js → index-p0ebr7dh.js} +94 -57
  22. package/dist/index.js +9 -65
  23. package/dist/lib/git-fs.d.ts +43 -0
  24. package/dist/lib/host-policy.d.ts +14 -2
  25. package/dist/lib/remote-auth/clone.d.ts +0 -21
  26. package/dist/lib/remote-auth/converge-merge.d.ts +24 -7
  27. package/dist/lib/remote-auth/generic-auth.d.ts +0 -22
  28. package/dist/lib/remote-auth/git-http.d.ts +1 -1
  29. package/dist/lib/remote-auth/github-auth.d.ts +2 -22
  30. package/dist/lib/remote-auth/operation-log.d.ts +4 -9
  31. package/dist/lib/remote-auth/sync-messages.d.ts +35 -12
  32. package/dist/lib/remote-auth/sync-types.d.ts +30 -101
  33. package/dist/lib/remote-auth/sync.d.ts +19 -21
  34. package/dist/lib/remote-auth/token-store.d.ts +0 -10
  35. package/dist/lib/remote-auth/transport.d.ts +42 -40
  36. package/dist/lib/source-provider.d.ts +12 -0
  37. package/dist/{lint-p2sw53d9.js → lint-c8ze8sqw.js} +5 -5
  38. package/dist/{new-hvq0x91q.js → new-xy2qc300.js} +5 -5
  39. package/dist/{plugin-pssmk0dx.js → plugin-g9hwzjsm.js} +5 -5
  40. package/dist/{preflight-4j00yd0g.js → preflight-prcvdvmk.js} +5 -5
  41. package/dist/{preview-bridge-fz7vpk8m.js → preview-bridge-2nnrs2hy.js} +15 -0
  42. package/dist/{preview-d4s6gk6p.js → preview-h479apj9.js} +5 -5
  43. package/dist/{preview-interface-0ssk8bmm.js → preview-interface-2f5v96hd.js} +411 -88
  44. package/dist/{preview-shell-c5mfa3q0.js → preview-shell-kyjq59d9.js} +80 -25
  45. package/dist/{publish-tkc26en7.js → publish-emk644a8.js} +5 -5
  46. package/dist/{source-provider-3tcj6qg2.js → source-provider-2j5x2hyw.js} +1 -1
  47. package/dist/{source-provider-vanafrt9.js → source-provider-kn41jmbs.js} +1 -1
  48. package/dist/{validate-w9vfefrw.js → validate-be54bdc6.js} +5 -5
  49. package/package.json +1 -1
  50. package/dist/lib/app-heartbeat.d.ts +0 -102
  51. package/dist/lib/remote-auth/image-clash.d.ts +0 -17
  52. package/dist/lib/remote-auth/recovery/classify.d.ts +0 -97
  53. package/dist/lib/remote-auth/recovery/inspect.d.ts +0 -75
  54. package/dist/lib/remote-auth/recovery/locks.d.ts +0 -20
  55. package/dist/lib/remote-auth/recovery/repair.d.ts +0 -29
  56. package/dist/lib/remote-auth/recovery/types.d.ts +0 -39
  57. package/dist/repair-2va4w12t.js +0 -137
@@ -17,19 +17,18 @@ import {
17
17
  resolveChromiumExecutable,
18
18
  run,
19
19
  spawnCapture
20
- } from "./index-ycpvr0am.js";
20
+ } from "./index-7hrvksrb.js";
21
21
  import {
22
- gitDirFor,
22
+ gitFs,
23
23
  gitScopeFor,
24
24
  hasPendingChanges,
25
25
  hasUncommittedChanges,
26
- isNoChangesError,
27
26
  resolveGitAuthor,
28
27
  resolveLogger,
29
28
  snapshotStagingMarkerPath,
30
29
  snapshotWorkingTreeUnlocked,
31
30
  withRepoLock
32
- } from "./index-mdefp0y5.js";
31
+ } from "./index-dtrftayr.js";
33
32
  import {
34
33
  detectProjectSource,
35
34
  syncRemoteFor
@@ -39,7 +38,7 @@ import {
39
38
  } from "./index-37x76zdn.js";
40
39
 
41
40
  // src/lib/build-runner.ts
42
- import path7 from "node:path";
41
+ import path8 from "node:path";
43
42
  import fs2 from "node:fs";
44
43
  import fsp2 from "node:fs/promises";
45
44
  import os2 from "node:os";
@@ -544,6 +543,37 @@ function resolveWithPreset(cliOverrides, manifest, preset) {
544
543
 
545
544
  // src/lib/markdown/index.ts
546
545
  import { readdir as readdir2, readFile as readFile3, writeFile, mkdir } from "node:fs/promises";
546
+
547
+ // src/lib/remote-auth/sync-messages.ts
548
+ var MSG_UP_TO_DATE = "Everything is in sync.";
549
+ var MSG_UP_TO_DATE_PULLED = "Everything is in sync. The latest online changes were downloaded to this computer.";
550
+ var MSG_SYNCED = "Your changes are online.";
551
+ var MSG_SYNCED_MERGED = "Your changes are online, combined with changes from the online copy.";
552
+ var MSG_OFFLINE = "Your changes are saved on this computer. gutterpress couldn't reach the online repository — try syncing again when you're back online.";
553
+ var MSG_AUTH = "The online repository didn't accept the saved connection. Reconnect and try again.";
554
+ var MSG_INSECURE_TRANSPORT = "This project's online address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Switch the address to a secure one (starting with https), or to a local loopback address for a server on this computer, to sync.";
555
+ var MSG_BUSY = "The online copy is changing very quickly right now. Your work is saved on this computer — try Sync again in a moment.";
556
+ var MSG_HISTORY_UNREADABLE = "This project's version history can't be read, so syncing can't continue. Your writing is safe — every file is still here on this computer. To get history and syncing working again, download a fresh copy of the project from online, then move any recent changes into it.";
557
+ var MSG_UNRELATED = "This project and the online copy have no history in common, so they can't be combined. Either the online address points at a different project, or this project's own history was lost and started over. Your writing is safe — every file is still here on this computer. Check the online address; if it is correct, download a fresh copy from online and move any recent changes into it.";
558
+ var MSG_NO_REMOTE = "This project isn't connected to an online repository yet.";
559
+ var MSG_SSH_REMOTE = "This project's online address uses SSH (git@…), which gutterpress can't sync to. Switch it to the web (HTTPS) address to sync from here.";
560
+ var MSG_NO_BRANCH = "This project's version history isn't on a named branch, so it can't be synced right now.";
561
+ var SYNC_SNAPSHOT_MESSAGE = "Automatic backup of your work";
562
+ var SYNC_LATE_EDIT_MESSAGE = "Saved the edit you made while syncing";
563
+ function onlineSiblingPath(filepath) {
564
+ const dot = filepath.lastIndexOf(".");
565
+ const slash = filepath.lastIndexOf("/");
566
+ const hasExt = dot > slash + 1;
567
+ return hasExt ? `${filepath.slice(0, dot)}.online${filepath.slice(dot)}` : `${filepath}.online`;
568
+ }
569
+ function isOnlineSibling(filepath) {
570
+ const dot = filepath.lastIndexOf(".");
571
+ const slash = filepath.lastIndexOf("/");
572
+ const stem = dot > slash + 1 ? filepath.slice(0, dot) : filepath;
573
+ return stem.endsWith(".online");
574
+ }
575
+
576
+ // src/lib/markdown/index.ts
547
577
  import { join } from "node:path";
548
578
 
549
579
  // src/lib/desktop.ts
@@ -2404,7 +2434,7 @@ async function inlineShapeUrls(html, baseDir) {
2404
2434
  async function resolveActiveMarkdownFiles(inputDir, configuredFiles) {
2405
2435
  if (configuredFiles && configuredFiles.length > 0)
2406
2436
  return configuredFiles;
2407
- return (await readdir2(inputDir)).filter((f) => f.endsWith(".md")).sort();
2437
+ return (await readdir2(inputDir)).filter((f) => f.endsWith(".md") && !isOnlineSibling(f)).sort();
2408
2438
  }
2409
2439
  async function renderChapters(inputDir, opts = {}) {
2410
2440
  const styles = await resolveActiveStyles(inputDir, opts.styles);
@@ -3950,7 +3980,7 @@ import git from "isomorphic-git";
3950
3980
  // package.json
3951
3981
  var package_default = {
3952
3982
  name: "gutterpress",
3953
- version: "0.10.0",
3983
+ version: "0.10.1-beta.2",
3954
3984
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
3955
3985
  author: "itlackey",
3956
3986
  license: "MPL-2.0",
@@ -6184,11 +6214,9 @@ var check22 = {
6184
6214
  };
6185
6215
  registerCheck(check22);
6186
6216
 
6187
- // src/checks/asset/image-file-size.ts
6188
- import { stat as stat3 } from "node:fs/promises";
6189
-
6190
- // src/lib/image-inspect.ts
6191
- import { stat as stat2, readFile as readFile18 } from "node:fs/promises";
6217
+ // src/checks/source/merge-markers.ts
6218
+ import { readFile as readFile18 } from "node:fs/promises";
6219
+ import path6 from "node:path";
6192
6220
 
6193
6221
  // src/checks/asset/extensions.ts
6194
6222
  var RASTER_INSPECTABLE_EXTS = [
@@ -6211,27 +6239,108 @@ var ASSET_SCAN_IGNORE_GLOBS = [
6211
6239
  "**/dist/**"
6212
6240
  ];
6213
6241
 
6242
+ // src/checks/source/merge-markers.ts
6243
+ var OPEN_SENTINEL = "<<<<<<< your version";
6244
+ var CLOSE_SENTINEL = ">>>>>>> online version";
6245
+ function withoutOnlineTag(relPath) {
6246
+ return relPath.replace(/\.online(?=\.[^./]*$|$)/, "");
6247
+ }
6248
+ var check23 = {
6249
+ id: "source.sync.merge-markers",
6250
+ name: "Combined Versions",
6251
+ description: "Finds passages and files still holding two versions (yours and the online copy) after a sync",
6252
+ category: "source",
6253
+ phase: "pre-build",
6254
+ async run(ctx) {
6255
+ const results = [];
6256
+ const textFiles = [...ctx.markdownFiles ?? [], ...ctx.cssFiles ?? []].sort();
6257
+ for (const file of textFiles) {
6258
+ let content;
6259
+ try {
6260
+ content = await readFile18(file, "utf8");
6261
+ } catch {
6262
+ results.push(inspectionFailed(check23.id, `Could not read source file: ${file}`, { file }));
6263
+ continue;
6264
+ }
6265
+ if (!content.includes(OPEN_SENTINEL) && !content.includes(CLOSE_SENTINEL))
6266
+ continue;
6267
+ let inBlock = false;
6268
+ const lines = content.split(`
6269
+ `);
6270
+ for (let i = 0;i < lines.length; i++) {
6271
+ const line = lines[i].endsWith("\r") ? lines[i].slice(0, -1) : lines[i];
6272
+ if (line === OPEN_SENTINEL) {
6273
+ inBlock = true;
6274
+ results.push(finding(check23.id, {
6275
+ severity: "error",
6276
+ code: "two-versions-passage",
6277
+ message: "This passage has two versions (yours and the online copy) — keep what you want, then delete the marker lines.",
6278
+ file,
6279
+ line: i + 1
6280
+ }));
6281
+ } else if (line === CLOSE_SENTINEL) {
6282
+ if (!inBlock) {
6283
+ results.push(finding(check23.id, {
6284
+ severity: "error",
6285
+ code: "leftover-version-marker",
6286
+ message: "This marker line is left over from combining two versions — delete it.",
6287
+ file,
6288
+ line: i + 1
6289
+ }));
6290
+ }
6291
+ inBlock = false;
6292
+ }
6293
+ }
6294
+ }
6295
+ try {
6296
+ const { glob } = await import("glob");
6297
+ const siblings = await glob(["**/*.online", "**/*.online.*"], {
6298
+ cwd: ctx.inputDir,
6299
+ absolute: true,
6300
+ ignore: [...ASSET_SCAN_IGNORE_GLOBS]
6301
+ });
6302
+ for (const sibling of siblings.sort()) {
6303
+ const rel = path6.relative(ctx.inputDir, sibling).split(path6.sep).join("/");
6304
+ results.push(finding(check23.id, {
6305
+ severity: "warning",
6306
+ code: "kept-both-versions",
6307
+ message: `Two versions of ${withoutOnlineTag(rel)} are in your project — keep the one you want, then delete the other.`,
6308
+ file: sibling
6309
+ }));
6310
+ }
6311
+ } catch (error2) {
6312
+ results.push(inspectionFailed(check23.id, `Could not scan for kept-both files: ${error2 instanceof Error ? error2.message : String(error2)}`));
6313
+ }
6314
+ return results;
6315
+ }
6316
+ };
6317
+ registerCheck(check23);
6318
+
6319
+ // src/checks/asset/image-file-size.ts
6320
+ import { stat as stat3 } from "node:fs/promises";
6321
+
6214
6322
  // src/lib/image-inspect.ts
6323
+ import { stat as stat2, readFile as readFile19 } from "node:fs/promises";
6215
6324
  var DEFAULT_DPI = 72;
6216
6325
  var cache = new Map;
6217
- async function inspectImage(path6) {
6326
+ async function inspectImage(path7) {
6218
6327
  let st;
6219
6328
  try {
6220
- st = await stat2(path6);
6329
+ st = await stat2(path7);
6221
6330
  } catch {
6222
6331
  return null;
6223
6332
  }
6224
- const hit = cache.get(path6);
6333
+ const hit = cache.get(path7);
6225
6334
  if (hit && hit.mtimeMs === st.mtimeMs && hit.size === st.size)
6226
6335
  return hit.info;
6227
6336
  let info2 = null;
6228
6337
  try {
6229
- const buf = await readFile18(path6);
6338
+ const buf = await readFile19(path7);
6230
6339
  info2 = parseImage(buf);
6231
6340
  } catch {
6232
6341
  info2 = null;
6233
6342
  }
6234
- cache.set(path6, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
6343
+ cache.set(path7, { mtimeMs: st.mtimeMs, size: st.size, info: info2 });
6235
6344
  return info2;
6236
6345
  }
6237
6346
  function parseImage(b) {
@@ -6431,7 +6540,7 @@ async function collectImageFiles(dirs, exts, ignore = ASSET_SCAN_IGNORE_GLOBS) {
6431
6540
  }
6432
6541
 
6433
6542
  // src/checks/asset/image-file-size.ts
6434
- var check23 = {
6543
+ var check24 = {
6435
6544
  id: "asset.image.file-size",
6436
6545
  name: "Image File Size",
6437
6546
  description: "Checks that image files do not exceed the maximum size limit",
@@ -6452,14 +6561,14 @@ var check23 = {
6452
6561
  if (info2.size > maxSize) {
6453
6562
  const sizeMb = (info2.size / 1e6).toFixed(1);
6454
6563
  const maxMb = (maxSize / 1e6).toFixed(1);
6455
- results.push(finding(check23.id, {
6564
+ results.push(finding(check24.id, {
6456
6565
  severity: "warning",
6457
6566
  message: `Image file too large: ${sizeMb}MB (max ${maxMb}MB)`,
6458
6567
  file
6459
6568
  }));
6460
6569
  }
6461
6570
  } catch {
6462
- results.push(inspectionFailed(check23.id, `Could not stat image file: ${file}`, {
6571
+ results.push(inspectionFailed(check24.id, `Could not stat image file: ${file}`, {
6463
6572
  file
6464
6573
  }));
6465
6574
  }
@@ -6467,10 +6576,10 @@ var check23 = {
6467
6576
  return results;
6468
6577
  }
6469
6578
  };
6470
- registerCheck(check23);
6579
+ registerCheck(check24);
6471
6580
 
6472
6581
  // src/checks/asset/image-resolution.ts
6473
- var check24 = {
6582
+ var check25 = {
6474
6583
  id: "asset.image.resolution",
6475
6584
  name: "Image Resolution",
6476
6585
  description: "Checks source image DPI from embedded density metadata",
@@ -6492,7 +6601,7 @@ var check24 = {
6492
6601
  const { xDpi, yDpi } = info2;
6493
6602
  if (xDpi > 0 && yDpi > 0 && (xDpi < minDpi || yDpi < minDpi)) {
6494
6603
  results.push({
6495
- checkId: check24.id,
6604
+ checkId: check25.id,
6496
6605
  severity: "warning",
6497
6606
  message: `Image resolution too low: ${xDpi}x${yDpi} DPI (minimum ${minDpi} DPI)`,
6498
6607
  file
@@ -6502,7 +6611,7 @@ var check24 = {
6502
6611
  return results;
6503
6612
  }
6504
6613
  };
6505
- registerCheck(check24);
6614
+ registerCheck(check25);
6506
6615
 
6507
6616
  // src/checks/asset/image-color-space.ts
6508
6617
  var LABEL = {
@@ -6510,7 +6619,7 @@ var LABEL = {
6510
6619
  gray: "Gray",
6511
6620
  cmyk: "CMYK"
6512
6621
  };
6513
- var check25 = {
6622
+ var check26 = {
6514
6623
  id: "asset.image.color-space",
6515
6624
  name: "Image Color Space",
6516
6625
  description: "Validates image color spaces against allowed list",
@@ -6531,7 +6640,7 @@ var check25 = {
6531
6640
  const cs = info2?.colorSpace;
6532
6641
  if (cs && !allowedLower.has(cs)) {
6533
6642
  results.push({
6534
- checkId: check25.id,
6643
+ checkId: check26.id,
6535
6644
  severity: "warning",
6536
6645
  message: `Image uses ${LABEL[cs] ?? cs} color space (allowed: ${allowed.join(", ")})`,
6537
6646
  file
@@ -6541,10 +6650,10 @@ var check25 = {
6541
6650
  return results;
6542
6651
  }
6543
6652
  };
6544
- registerCheck(check25);
6653
+ registerCheck(check26);
6545
6654
 
6546
6655
  // src/checks/asset/image-alpha.ts
6547
- var check26 = {
6656
+ var check27 = {
6548
6657
  id: "asset.image.alpha-channel",
6549
6658
  name: "Image Alpha Channel",
6550
6659
  description: "Checks for alpha channels in PNG/TIFF images",
@@ -6562,7 +6671,7 @@ var check26 = {
6562
6671
  const info2 = await inspectImage(file);
6563
6672
  if (info2?.hasAlpha) {
6564
6673
  results.push({
6565
- checkId: check26.id,
6674
+ checkId: check27.id,
6566
6675
  severity: "warning",
6567
6676
  message: "Image contains alpha channel, which may cause print issues",
6568
6677
  file
@@ -6572,10 +6681,10 @@ var check26 = {
6572
6681
  return results;
6573
6682
  }
6574
6683
  };
6575
- registerCheck(check26);
6684
+ registerCheck(check27);
6576
6685
 
6577
6686
  // src/checks/asset/image-tac.ts
6578
- var check27 = {
6687
+ var check28 = {
6579
6688
  id: "asset.image.tac-raster",
6580
6689
  name: "Image TAC (Raster)",
6581
6690
  description: "Rasterizes and checks TAC per image using Ghostscript",
@@ -6594,7 +6703,7 @@ var check27 = {
6594
6703
  const ghostscript = await resolveGhostscript();
6595
6704
  if (!ghostscript) {
6596
6705
  return [
6597
- inspectionFailed(check27.id, "Could not inspect image ink coverage: Ghostscript executable not found")
6706
+ inspectionFailed(check28.id, "Could not inspect image ink coverage: Ghostscript executable not found")
6598
6707
  ];
6599
6708
  }
6600
6709
  for (const file of files) {
@@ -6612,7 +6721,7 @@ var check27 = {
6612
6721
  if (nums.length === 4 && nums.every((n) => Number.isFinite(n))) {
6613
6722
  const tac = (nums[0] + nums[1] + nums[2] + nums[3]) * 100;
6614
6723
  if (tac > maxTac) {
6615
- results.push(finding(check27.id, {
6724
+ results.push(finding(check28.id, {
6616
6725
  severity: "warning",
6617
6726
  message: `Image TAC exceeds limit: ${tac.toFixed(1)}% (max ${maxTac}%)`,
6618
6727
  file,
@@ -6624,16 +6733,16 @@ var check27 = {
6624
6733
  }
6625
6734
  }
6626
6735
  } catch {
6627
- results.push(inspectionFailed(check27.id, `Could not inspect image ink coverage: ${file}`, { file }));
6736
+ results.push(inspectionFailed(check28.id, `Could not inspect image ink coverage: ${file}`, { file }));
6628
6737
  }
6629
6738
  }
6630
6739
  return results;
6631
6740
  }
6632
6741
  };
6633
- registerCheck(check27);
6742
+ registerCheck(check28);
6634
6743
 
6635
6744
  // src/checks/asset/approved-fonts.ts
6636
- var check28 = {
6745
+ var check29 = {
6637
6746
  id: "asset.font.approved-files",
6638
6747
  name: "Approved Font Files",
6639
6748
  description: "Checks font files against the approved file patterns",
@@ -6672,7 +6781,7 @@ var check28 = {
6672
6781
  for (const font of allFonts) {
6673
6782
  if (!approvedFonts.has(font)) {
6674
6783
  results.push({
6675
- checkId: check28.id,
6784
+ checkId: check29.id,
6676
6785
  severity: "warning",
6677
6786
  message: "Font file not in approved list",
6678
6787
  file: font
@@ -6682,7 +6791,7 @@ var check28 = {
6682
6791
  return results;
6683
6792
  }
6684
6793
  };
6685
- registerCheck(check28);
6794
+ registerCheck(check29);
6686
6795
 
6687
6796
  // src/checks/asset/font-license.ts
6688
6797
  import { existsSync as existsSync8 } from "node:fs";
@@ -6697,7 +6806,7 @@ var LICENSE_NAMES = [
6697
6806
  "OFL-1.1.txt",
6698
6807
  "COPYING"
6699
6808
  ];
6700
- var check29 = {
6809
+ var check30 = {
6701
6810
  id: "asset.font.license",
6702
6811
  name: "Font License",
6703
6812
  description: "Checks for license files in font directories",
@@ -6724,7 +6833,7 @@ var check29 = {
6724
6833
  const hasLicense = LICENSE_NAMES.some((name) => existsSync8(resolve7(fontDir, name)));
6725
6834
  if (!hasLicense) {
6726
6835
  results.push({
6727
- checkId: check29.id,
6836
+ checkId: check30.id,
6728
6837
  severity: "warning",
6729
6838
  message: `No font license file found in directory`,
6730
6839
  file: fontDir
@@ -6734,10 +6843,10 @@ var check29 = {
6734
6843
  return results;
6735
6844
  }
6736
6845
  };
6737
- registerCheck(check29);
6846
+ registerCheck(check30);
6738
6847
 
6739
6848
  // src/checks/heuristic/text-density.ts
6740
- var check30 = {
6849
+ var check31 = {
6741
6850
  id: "heuristic.whitespace.text-density",
6742
6851
  name: "Text Density",
6743
6852
  description: "Checks characters-per-page ratio",
@@ -6766,7 +6875,7 @@ var check30 = {
6766
6875
  });
6767
6876
  if (lowPages.length > 0) {
6768
6877
  results.push({
6769
- checkId: check30.id,
6878
+ checkId: check31.id,
6770
6879
  severity: "info",
6771
6880
  message: `Low text density on pages: ${lowPages.join(", ")} (below ${range.min} chars)`,
6772
6881
  file: ctx.pdfPath
@@ -6774,7 +6883,7 @@ var check30 = {
6774
6883
  }
6775
6884
  if (highPages.length > 0) {
6776
6885
  results.push({
6777
- checkId: check30.id,
6886
+ checkId: check31.id,
6778
6887
  severity: "info",
6779
6888
  message: `High text density on pages: ${highPages.join(", ")} (above ${range.max} chars)`,
6780
6889
  file: ctx.pdfPath
@@ -6783,11 +6892,11 @@ var check30 = {
6783
6892
  return results;
6784
6893
  }
6785
6894
  };
6786
- registerCheck(check30);
6895
+ registerCheck(check31);
6787
6896
 
6788
6897
  // src/checks/heuristic/section-density.ts
6789
- import { readFile as readFile19 } from "node:fs/promises";
6790
- var check31 = {
6898
+ import { readFile as readFile20 } from "node:fs/promises";
6899
+ var check32 = {
6791
6900
  id: "heuristic.chunking.section-density",
6792
6901
  name: "Section Density",
6793
6902
  description: "Checks heading/paragraph/callout density from source Markdown",
@@ -6803,7 +6912,7 @@ var check31 = {
6803
6912
  const results = [];
6804
6913
  for (const file of files) {
6805
6914
  try {
6806
- const content = await readFile19(file, "utf8");
6915
+ const content = await readFile20(file, "utf8");
6807
6916
  const lines = content.split(`
6808
6917
  `);
6809
6918
  let currentSectionStart = 0;
@@ -6813,7 +6922,7 @@ var check31 = {
6813
6922
  const line = lines[i];
6814
6923
  if (/^#{1,6}\s/.test(line)) {
6815
6924
  if (paragraphCount > maxParas) {
6816
- results.push(finding(check31.id, {
6925
+ results.push(finding(check32.id, {
6817
6926
  severity: "info",
6818
6927
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
6819
6928
  file,
@@ -6833,7 +6942,7 @@ var check31 = {
6833
6942
  }
6834
6943
  }
6835
6944
  if (paragraphCount > maxParas) {
6836
- results.push(finding(check31.id, {
6945
+ results.push(finding(check32.id, {
6837
6946
  severity: "info",
6838
6947
  message: `Section has ${paragraphCount} paragraphs (max recommended: ${maxParas})`,
6839
6948
  file,
@@ -6841,7 +6950,7 @@ var check31 = {
6841
6950
  }));
6842
6951
  }
6843
6952
  } catch {
6844
- results.push(inspectionFailed(check31.id, `Could not read source file: ${file}`, {
6953
+ results.push(inspectionFailed(check32.id, `Could not read source file: ${file}`, {
6845
6954
  file
6846
6955
  }));
6847
6956
  }
@@ -6849,10 +6958,10 @@ var check31 = {
6849
6958
  return results;
6850
6959
  }
6851
6960
  };
6852
- registerCheck(check31);
6961
+ registerCheck(check32);
6853
6962
 
6854
6963
  // src/checks/heuristic/layer-count.ts
6855
- var check32 = {
6964
+ var check33 = {
6856
6965
  id: "heuristic.decoration.layer-count",
6857
6966
  name: "Layer Count",
6858
6967
  description: "Counts image objects per page",
@@ -6877,7 +6986,7 @@ var check32 = {
6877
6986
  heavyPages.sort((a, b) => a - b);
6878
6987
  return [
6879
6988
  {
6880
- checkId: check32.id,
6989
+ checkId: check33.id,
6881
6990
  severity: "info",
6882
6991
  message: `Pages with many image layers (>${maxLayers}): ${heavyPages.join(", ")}`,
6883
6992
  file: ctx.pdfPath
@@ -6887,10 +6996,10 @@ var check32 = {
6887
6996
  return [];
6888
6997
  }
6889
6998
  };
6890
- registerCheck(check32);
6999
+ registerCheck(check33);
6891
7000
 
6892
7001
  // src/checks/heuristic/placement-variance.ts
6893
- var check33 = {
7002
+ var check34 = {
6894
7003
  id: "heuristic.layout.placement-variance",
6895
7004
  name: "Placement Variance",
6896
7005
  description: "Analyzes text baseline coordinates for layout consistency",
@@ -6908,7 +7017,7 @@ var check33 = {
6908
7017
  const uniqueX = new Set(positions.map((p) => Math.round(p.x)));
6909
7018
  return [
6910
7019
  {
6911
- checkId: check33.id,
7020
+ checkId: check34.id,
6912
7021
  severity: "info",
6913
7022
  message: `Layout analysis: ${uniqueX.size} unique horizontal text positions across ${positions.length} text blocks.`,
6914
7023
  file: ctx.pdfPath
@@ -6916,7 +7025,7 @@ var check33 = {
6916
7025
  ];
6917
7026
  }
6918
7027
  };
6919
- registerCheck(check33);
7028
+ registerCheck(check34);
6920
7029
 
6921
7030
  // src/checks/runner.ts
6922
7031
  function getCheckSeverityOverride(checkId, config) {
@@ -6946,25 +7055,25 @@ async function runChecks(ctx, opts = {}) {
6946
7055
  }
6947
7056
  const release = retainPdfCache();
6948
7057
  try {
6949
- for (const check34 of checks2) {
7058
+ for (const check35 of checks2) {
6950
7059
  try {
6951
- const results = await check34.run(ctx);
6952
- const severityOverride = getCheckSeverityOverride(check34.id, ctx.config);
7060
+ const results = await check35.run(ctx);
7061
+ const severityOverride = getCheckSeverityOverride(check35.id, ctx.config);
6953
7062
  if (severityOverride) {
6954
7063
  for (const r of results) {
6955
7064
  r.severity = severityOverride;
6956
7065
  }
6957
7066
  }
6958
7067
  if (results.length === 0) {
6959
- passed.push(check34.id);
7068
+ passed.push(check35.id);
6960
7069
  } else {
6961
7070
  allResults.push(...results);
6962
7071
  }
6963
7072
  } catch (err) {
6964
7073
  allResults.push({
6965
- checkId: check34.id,
7074
+ checkId: check35.id,
6966
7075
  severity: "error",
6967
- message: `Check "${check34.id}" threw: ${err instanceof Error ? err.message : String(err)}`
7076
+ message: `Check "${check35.id}" threw: ${err instanceof Error ? err.message : String(err)}`
6968
7077
  });
6969
7078
  }
6970
7079
  }
@@ -7004,15 +7113,15 @@ function emptyReport() {
7004
7113
  async function checkToolAvailability(config, opts = {}) {
7005
7114
  const { checks: checks2 } = selectChecks(opts, config);
7006
7115
  const toolToChecks = new Map;
7007
- for (const check34 of checks2) {
7008
- if (!check34.requiredTools?.length)
7116
+ for (const check35 of checks2) {
7117
+ if (!check35.requiredTools?.length)
7009
7118
  continue;
7010
- for (const tool of check34.requiredTools) {
7119
+ for (const tool of check35.requiredTools) {
7011
7120
  const existing = toolToChecks.get(tool);
7012
7121
  if (existing) {
7013
- existing.push(check34.id);
7122
+ existing.push(check35.id);
7014
7123
  } else {
7015
- toolToChecks.set(tool, [check34.id]);
7124
+ toolToChecks.set(tool, [check35.id]);
7016
7125
  }
7017
7126
  }
7018
7127
  }
@@ -7028,11 +7137,11 @@ async function checkToolAvailability(config, opts = {}) {
7028
7137
  const missing = results.filter((r) => !r.found).map((r) => r.tool);
7029
7138
  const missingSet = new Set(missing);
7030
7139
  const skippedChecks = [];
7031
- for (const check34 of checks2) {
7032
- if (!check34.requiredTools?.length)
7140
+ for (const check35 of checks2) {
7141
+ if (!check35.requiredTools?.length)
7033
7142
  continue;
7034
- if (check34.requiredTools.some((t) => missingSet.has(t))) {
7035
- skippedChecks.push(check34.id);
7143
+ if (check35.requiredTools.some((t) => missingSet.has(t))) {
7144
+ skippedChecks.push(check35.id);
7036
7145
  }
7037
7146
  }
7038
7147
  return { available, missing, skippedChecks, toolToChecks };
@@ -7435,7 +7544,7 @@ function computeGates(format, opts, config) {
7435
7544
  }
7436
7545
 
7437
7546
  // src/lib/build-staging.ts
7438
- import path6 from "node:path";
7547
+ import path7 from "node:path";
7439
7548
  import os from "node:os";
7440
7549
  import fsp from "node:fs/promises";
7441
7550
 
@@ -7618,8 +7727,8 @@ function placeholderPng(width = 640, height = 480, cell = 32) {
7618
7727
 
7619
7728
  // src/lib/build-staging.ts
7620
7729
  async function shipViewerHtml(htmlFile, outDir) {
7621
- await fsp.mkdir(path6.join(outDir, "engine"), { recursive: true });
7622
- await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path6.join(outDir, "engine/gutterpress-viewer.js"));
7730
+ await fsp.mkdir(path7.join(outDir, "engine"), { recursive: true });
7731
+ await fsp.copyFile(await getAssetPath("engine/gutterpress-viewer.js"), path7.join(outDir, "engine/gutterpress-viewer.js"));
7623
7732
  const tag = ` <script src="engine/gutterpress-viewer.js"></script>
7624
7733
  `;
7625
7734
  const html = await fsp.readFile(htmlFile, "utf-8");
@@ -7648,18 +7757,18 @@ async function stageBookAssets(options) {
7648
7757
  return { missing: [...missingPlaceholders.keys()].sort() };
7649
7758
  }
7650
7759
  async function copyReferencedAssets(copies, outDir) {
7651
- const dirs = new Set(copies.map((c) => path6.dirname(path6.resolve(outDir, c.to))));
7760
+ const dirs = new Set(copies.map((c) => path7.dirname(path7.resolve(outDir, c.to))));
7652
7761
  await Promise.all([...dirs].map((d) => fsp.mkdir(d, { recursive: true })));
7653
7762
  const missing = new Map;
7654
7763
  await Promise.all(copies.map(async (c) => {
7655
- const dest = path6.resolve(outDir, c.to);
7764
+ const dest = path7.resolve(outDir, c.to);
7656
7765
  try {
7657
7766
  await fsp.copyFile(c.from, dest);
7658
7767
  } catch (err) {
7659
7768
  if (err?.code === "ENOENT") {
7660
7769
  const placeholder = placeholderOutputPath(c.to);
7661
- const placeholderDest = path6.resolve(outDir, placeholder);
7662
- await fsp.mkdir(path6.dirname(placeholderDest), { recursive: true });
7770
+ const placeholderDest = path7.resolve(outDir, placeholder);
7771
+ await fsp.mkdir(path7.dirname(placeholderDest), { recursive: true });
7663
7772
  await fsp.writeFile(placeholderDest, placeholderPng());
7664
7773
  missing.set(c.to, placeholder);
7665
7774
  return;
@@ -7670,7 +7779,7 @@ async function copyReferencedAssets(copies, outDir) {
7670
7779
  return missing;
7671
7780
  }
7672
7781
  async function createStageRoot() {
7673
- return fsp.mkdtemp(path6.join(os.tmpdir(), "gutterpress-stage-"));
7782
+ return fsp.mkdtemp(path7.join(os.tmpdir(), "gutterpress-stage-"));
7674
7783
  }
7675
7784
 
7676
7785
  // src/lib/build-runner.ts
@@ -7678,18 +7787,18 @@ function splitOutPath(outArg, format) {
7678
7787
  if (typeof outArg !== "string" || outArg.length === 0) {
7679
7788
  return { outDir: undefined, pdfFileOverride: null };
7680
7789
  }
7681
- const resolved = path7.resolve(outArg);
7790
+ const resolved = path8.resolve(outArg);
7682
7791
  if ((format === "pdf" || format === "pdfx") && resolved.toLowerCase().endsWith(".pdf")) {
7683
- return { outDir: path7.dirname(resolved), pdfFileOverride: resolved };
7792
+ return { outDir: path8.dirname(resolved), pdfFileOverride: resolved };
7684
7793
  }
7685
7794
  return { outDir: resolved, pdfFileOverride: null };
7686
7795
  }
7687
7796
  function layoutWarningKey(file, line, message) {
7688
- return `${path7.resolve(file)}\x00${line ?? 0}\x00${message}`;
7797
+ return `${path8.resolve(file)}\x00${line ?? 0}\x00${message}`;
7689
7798
  }
7690
7799
  async function resolveBuildContext(opts) {
7691
7800
  const { format } = opts;
7692
- const inputDir = path7.resolve(opts.inputDir);
7801
+ const inputDir = path8.resolve(opts.inputDir);
7693
7802
  const { manifest, manifestDir, manifestPath } = await loadManifestWithPath(opts.manifestPath ?? inputDir, { explicit: opts.manifestPath !== undefined });
7694
7803
  if (manifestPath === null) {
7695
7804
  throw new UsageError(`No project manifest found in ${inputDir}. Looked for ${MANIFEST_FILENAMES.join(" or ")}. ` + "Run from your project folder or pass that folder with `gutterpress build <project-dir>`. " + "For a custom manifest filename, pass `--manifest <path>`.");
@@ -7704,10 +7813,10 @@ async function resolveBuildContext(opts) {
7704
7813
  pdfx: pdfxConfigOverride,
7705
7814
  engine: opts.engine
7706
7815
  }, manifest);
7707
- 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) };
7708
- const outDir = target.kind === "file" ? path7.dirname(target.file) : target.dir;
7816
+ const target = opts.pdfFileOverride ? { kind: "file", file: path8.resolve(opts.pdfFileOverride) } : opts.outDir ? { kind: "directory", dir: opts.outDir } : { kind: "project", dir: resolveOutputDir(manifestDir, config.title) };
7817
+ const outDir = target.kind === "file" ? path8.dirname(target.file) : target.dir;
7709
7818
  const gates = computeGates(format, opts, config);
7710
- const workDir = target.kind === "project" ? path7.join(path7.dirname(target.dir), `.${path7.basename(target.dir)}-build-${randomBytes(6).toString("hex")}`) : path7.join(os2.tmpdir(), `gutterpress-build-${randomBytes(6).toString("hex")}`);
7819
+ const workDir = target.kind === "project" ? path8.join(path8.dirname(target.dir), `.${path8.basename(target.dir)}-build-${randomBytes(6).toString("hex")}`) : path8.join(os2.tmpdir(), `gutterpress-build-${randomBytes(6).toString("hex")}`);
7711
7820
  return {
7712
7821
  opts,
7713
7822
  format,
@@ -7774,7 +7883,7 @@ async function renderBook(ctx) {
7774
7883
  pluginCss,
7775
7884
  onChapterWarnings: (file, warnings) => {
7776
7885
  for (const w of warnings) {
7777
- const key = layoutWarningKey(path7.resolve(renderDir, file), w.line, w.message);
7886
+ const key = layoutWarningKey(path8.resolve(renderDir, file), w.line, w.message);
7778
7887
  if (!ctx.prevalidatedLayoutWarningKeys.has(key)) {
7779
7888
  log.warn(` ${file}, line ${w.line}: ${w.message}`);
7780
7889
  }
@@ -7812,9 +7921,9 @@ async function renderBook(ctx) {
7812
7921
  return htmlFile;
7813
7922
  }
7814
7923
  async function resolveIccProfile(icc, manifestDir, explicitIccPath) {
7815
- const iccCandidates = path7.isAbsolute(icc) ? [icc] : [path7.resolve(manifestDir, icc), path7.resolve(icc)];
7924
+ const iccCandidates = path8.isAbsolute(icc) ? [icc] : [path8.resolve(manifestDir, icc), path8.resolve(icc)];
7816
7925
  let effectiveIccPath = iccCandidates.find((candidate) => fs2.existsSync(candidate)) ?? null;
7817
- if (!effectiveIccPath && !explicitIccPath && path7.basename(icc) === "CGATS21_CRPC1.icc") {
7926
+ if (!effectiveIccPath && !explicitIccPath && path8.basename(icc) === "CGATS21_CRPC1.icc") {
7818
7927
  effectiveIccPath = await getAssetPath("profiles/CGATS21_CRPC1.icc");
7819
7928
  }
7820
7929
  if (!effectiveIccPath) {
@@ -7830,13 +7939,13 @@ async function finalizeBuild(ctx, fingerprint, wroteMessage, paths, artifactName
7830
7939
  });
7831
7940
  await publishBuild(ctx, artifactName2);
7832
7941
  const delivered = ctx.target.kind !== "file";
7833
- const fingerprintPath = delivered ? path7.join(ctx.outDir, path7.basename(workFingerprint)) : null;
7942
+ const fingerprintPath = delivered ? path8.join(ctx.outDir, path8.basename(workFingerprint)) : null;
7834
7943
  log.success(wroteMessage);
7835
7944
  if (fingerprintPath)
7836
7945
  log.info(`Fingerprint: ${fingerprintPath}`);
7837
7946
  return {
7838
7947
  outDir: ctx.outDir,
7839
- htmlPath: delivered ? path7.join(ctx.outDir, path7.relative(ctx.workDir, paths.htmlPath)) : null,
7948
+ htmlPath: delivered ? path8.join(ctx.outDir, path8.relative(ctx.workDir, paths.htmlPath)) : null,
7840
7949
  pdfPath: paths.pdfPath,
7841
7950
  fingerprintPath,
7842
7951
  diagnostics
@@ -7847,8 +7956,8 @@ async function publishBuild(ctx, artifactName2) {
7847
7956
  if (target.kind === "file") {
7848
7957
  if (!artifactName2)
7849
7958
  throw new BuildError("No artifact to write for this format", 1);
7850
- await fsp2.mkdir(path7.dirname(target.file), { recursive: true });
7851
- await fsp2.copyFile(path7.join(workDir, artifactName2), target.file);
7959
+ await fsp2.mkdir(path8.dirname(target.file), { recursive: true });
7960
+ await fsp2.copyFile(path8.join(workDir, artifactName2), target.file);
7852
7961
  return;
7853
7962
  }
7854
7963
  if (target.kind === "directory") {
@@ -7857,12 +7966,12 @@ async function publishBuild(ctx, artifactName2) {
7857
7966
  return;
7858
7967
  }
7859
7968
  for (const name of await listPdfs(target.dir)) {
7860
- if (!fs2.existsSync(path7.join(workDir, name))) {
7861
- await fsp2.rename(path7.join(target.dir, name), path7.join(workDir, name));
7969
+ if (!fs2.existsSync(path8.join(workDir, name))) {
7970
+ await fsp2.rename(path8.join(target.dir, name), path8.join(workDir, name));
7862
7971
  }
7863
7972
  }
7864
7973
  await fsp2.rm(target.dir, { recursive: true, force: true });
7865
- await fsp2.mkdir(path7.dirname(target.dir), { recursive: true });
7974
+ await fsp2.mkdir(path8.dirname(target.dir), { recursive: true });
7866
7975
  await fsp2.rename(workDir, target.dir);
7867
7976
  }
7868
7977
  async function listPdfs(dir) {
@@ -7878,7 +7987,7 @@ class HtmlOutput {
7878
7987
  const { workDir, outDir, inputDir, config, opts } = ctx;
7879
7988
  log.info("Shipping self-contained HTML + viewer bundle (native engine)");
7880
7989
  await shipViewerHtml(htmlFile, workDir);
7881
- await fsp2.writeFile(path7.join(workDir, "index.html"), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=book.html"><title>Gutterpress</title></head><body></body></html>
7990
+ await fsp2.writeFile(path8.join(workDir, "index.html"), `<!DOCTYPE html><html><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=book.html"><title>Gutterpress</title></head><body></body></html>
7882
7991
  `, "utf-8");
7883
7992
  return finalizeBuild(ctx, {
7884
7993
  command: "build",
@@ -7891,7 +8000,7 @@ class HtmlOutput {
7891
8000
  iccPath: null,
7892
8001
  stripAnnotations: null
7893
8002
  }
7894
- }, `Wrote: ${path7.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
8003
+ }, `Wrote: ${path8.join(outDir, BOOK_HTML)}`, { htmlPath: htmlFile, pdfPath: null });
7895
8004
  }
7896
8005
  }
7897
8006
 
@@ -7900,14 +8009,14 @@ class PdfOutput {
7900
8009
  const { workDir, outDir, inputDir, config, opts, format, manifestDir, gates } = ctx;
7901
8010
  const pdfxMode = format === "pdfx" ? opts.pdfxFlavor ?? config.pdfx.flavor : undefined;
7902
8011
  const pdfName = artifactName(config.title, pdfxMode ? "pdfx" : "pdf");
7903
- const pdfFile = path7.join(workDir, pdfName);
7904
- const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path7.join(outDir, pdfName);
8012
+ const pdfFile = path8.join(workDir, pdfName);
8013
+ const reportedPdf = ctx.target.kind === "file" ? ctx.target.file : path8.join(outDir, pdfName);
7905
8014
  const stage = await createStageRoot();
7906
8015
  try {
7907
- const rawPdf = pdfxMode ? path7.join(stage, "raw.pdf") : path7.resolve(pdfFile);
7908
- await fsp2.mkdir(path7.dirname(path7.resolve(pdfFile)), { recursive: true });
8016
+ const rawPdf = pdfxMode ? path8.join(stage, "raw.pdf") : path8.resolve(pdfFile);
8017
+ await fsp2.mkdir(path8.dirname(path8.resolve(pdfFile)), { recursive: true });
7909
8018
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
7910
- const { buildNativePdf } = await import("./engine-b159tbns.js");
8019
+ const { buildNativePdf } = await import("./engine-8kdwy0jz.js");
7911
8020
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
7912
8021
  title: config.title,
7913
8022
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -7938,7 +8047,7 @@ class PdfOutput {
7938
8047
  await stripAnnotations(rawPdf, stage);
7939
8048
  }
7940
8049
  log.info(`Converting to CMYK PDF/X (${pdfxMode})`);
7941
- await convertToPdfxCmyk(rawPdf, path7.resolve(pdfFile), {
8050
+ await convertToPdfxCmyk(rawPdf, path8.resolve(pdfFile), {
7942
8051
  iccPath: effectiveIccPath,
7943
8052
  pdfx: pdfxMode,
7944
8053
  title: config.title,
@@ -7998,10 +8107,10 @@ async function runBuild(opts) {
7998
8107
  }
7999
8108
  }
8000
8109
  // src/preview/lifecycle.ts
8001
- import path9 from "path";
8110
+ import path10 from "path";
8002
8111
  import { tmpdir as tmpdir2 } from "os";
8003
8112
  import { randomBytes as randomBytes2 } from "crypto";
8004
- import { readdir as readdir5, readFile as readFile20, writeFile as writeFile5 } from "node:fs/promises";
8113
+ import { readdir as readdir5, readFile as readFile21, writeFile as writeFile5 } from "node:fs/promises";
8005
8114
 
8006
8115
  // src/utils/file-utils.ts
8007
8116
  import { promises as fs3 } from "fs";
@@ -8023,7 +8132,7 @@ async function mkdir4(dirPath) {
8023
8132
  // src/preview/file-watcher.ts
8024
8133
  import { watch } from "chokidar";
8025
8134
  import fsp3 from "node:fs/promises";
8026
- import path8 from "path";
8135
+ import path9 from "path";
8027
8136
  var EMPTY_BOOK_HTML = `<!doctype html>
8028
8137
  <html lang="en">
8029
8138
  <head>
@@ -8076,7 +8185,7 @@ function injectPreviewScripts(html, pageIsolateChapters) {
8076
8185
  async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8077
8186
  if (!inputPath) {
8078
8187
  cssAssets.clear();
8079
- await fsp3.writeFile(path8.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
8188
+ await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), EMPTY_BOOK_HTML, "utf-8");
8080
8189
  return;
8081
8190
  }
8082
8191
  const nextAssets = new Map;
@@ -8091,7 +8200,7 @@ async function generateAndWriteHtml(inputPath, tempDir, config, cssAssets) {
8091
8200
  cssAssets.clear();
8092
8201
  for (const [to, from] of nextAssets)
8093
8202
  cssAssets.set(to, from);
8094
- await fsp3.writeFile(path8.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
8203
+ await fsp3.writeFile(path9.join(tempDir, BOOK_HTML_FILENAME), injectPreviewScripts(html, false), "utf-8");
8095
8204
  }
8096
8205
  async function renderChapterPreviewHtml(inputPath, file, config) {
8097
8206
  const html = await renderPreviewBook(inputPath, config, {
@@ -8103,12 +8212,12 @@ async function renderChapterPreviewHtml(inputPath, file, config) {
8103
8212
  function describeChanges(changes, inputResolved) {
8104
8213
  const files = [];
8105
8214
  for (const [changedPath, event] of changes) {
8106
- const relative2 = path8.relative(inputResolved, path8.resolve(changedPath));
8107
- if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path8.sep}`) || path8.isAbsolute(relative2))
8215
+ const relative2 = path9.relative(inputResolved, path9.resolve(changedPath));
8216
+ if (relative2 === "" || relative2 === ".." || relative2.startsWith(`..${path9.sep}`) || path9.isAbsolute(relative2))
8108
8217
  continue;
8109
8218
  files.push({
8110
8219
  relativePath: canonicalChapterId(relative2),
8111
- ext: path8.extname(changedPath).toLowerCase(),
8220
+ ext: path9.extname(changedPath).toLowerCase(),
8112
8221
  event
8113
8222
  });
8114
8223
  }
@@ -8160,23 +8269,23 @@ function isIgnoredWatchPath(candidatePath, projectRoot) {
8160
8269
  return isDotPathUnderRoot(candidatePath, projectRoot) || isGeneratedProjectPath(relative2);
8161
8270
  }
8162
8271
  function isExternalWatchCandidate(candidatePath, targets) {
8163
- const candidate = path8.resolve(candidatePath);
8272
+ const candidate = path9.resolve(candidatePath);
8164
8273
  for (const rawTarget of targets) {
8165
- const target = path8.resolve(rawTarget);
8166
- const rel = path8.relative(candidate, target);
8167
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel))
8274
+ const target = path9.resolve(rawTarget);
8275
+ const rel = path9.relative(candidate, target);
8276
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
8168
8277
  return true;
8169
8278
  }
8170
8279
  return false;
8171
8280
  }
8172
8281
  async function nearestExistingDirectory(start) {
8173
- let current = path8.resolve(start);
8282
+ let current = path9.resolve(start);
8174
8283
  while (true) {
8175
8284
  try {
8176
8285
  if ((await fsp3.stat(current)).isDirectory())
8177
8286
  return current;
8178
8287
  } catch {}
8179
- const parent = path8.dirname(current);
8288
+ const parent = path9.dirname(current);
8180
8289
  if (parent === current)
8181
8290
  return current;
8182
8291
  current = parent;
@@ -8185,12 +8294,12 @@ async function nearestExistingDirectory(start) {
8185
8294
  async function externalWatchRoots(targets) {
8186
8295
  const roots = new Set;
8187
8296
  for (const target of targets) {
8188
- roots.add(await nearestExistingDirectory(path8.dirname(path8.resolve(target))));
8297
+ roots.add(await nearestExistingDirectory(path9.dirname(path9.resolve(target))));
8189
8298
  }
8190
8299
  return [...roots];
8191
8300
  }
8192
8301
  async function externalWatchTargets(projectDir, config) {
8193
- const root = path8.resolve(projectDir);
8302
+ const root = path9.resolve(projectDir);
8194
8303
  let canonicalRoot = root;
8195
8304
  try {
8196
8305
  canonicalRoot = await fsp3.realpath(root);
@@ -8198,21 +8307,21 @@ async function externalWatchTargets(projectDir, config) {
8198
8307
  const styles = await resolveActiveStyles(root, config.styles);
8199
8308
  const candidates = [
8200
8309
  ...await collectStyleDependencies(root, styles),
8201
- ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path8.resolve(root, p))
8310
+ ...(config.plugins ?? []).map((p) => p.path).filter((p) => !!p).map((p) => path9.resolve(root, p))
8202
8311
  ];
8203
8312
  const external = new Set;
8204
8313
  for (const abs of candidates) {
8205
- const authoredPath = path8.resolve(abs);
8206
- const authoredRel = path8.relative(root, authoredPath);
8207
- if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path8.sep}`) || path8.isAbsolute(authoredRel))) {
8314
+ const authoredPath = path9.resolve(abs);
8315
+ const authoredRel = path9.relative(root, authoredPath);
8316
+ if (authoredRel !== "" && (authoredRel === ".." || authoredRel.startsWith(`..${path9.sep}`) || path9.isAbsolute(authoredRel))) {
8208
8317
  external.add(authoredPath);
8209
8318
  }
8210
8319
  let canonicalPath = authoredPath;
8211
8320
  try {
8212
8321
  canonicalPath = await fsp3.realpath(canonicalPath);
8213
8322
  } catch {}
8214
- const rel = path8.relative(canonicalRoot, canonicalPath);
8215
- if (rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel))
8323
+ const rel = path9.relative(canonicalRoot, canonicalPath);
8324
+ if (rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel))
8216
8325
  continue;
8217
8326
  external.add(canonicalPath);
8218
8327
  }
@@ -8239,7 +8348,7 @@ async function addAndAwaitWatch(watcher, roots) {
8239
8348
  }
8240
8349
  }
8241
8350
  function createFileWatcher(state) {
8242
- const inputResolved = path8.resolve(state.currentInputPath);
8351
+ const inputResolved = path9.resolve(state.currentInputPath);
8243
8352
  const watcher = watch(inputResolved, {
8244
8353
  persistent: true,
8245
8354
  ignoreInitial: true,
@@ -8279,7 +8388,7 @@ function createFileWatcher(state) {
8279
8388
  async function recordChange(event, filePath) {
8280
8389
  if (closed)
8281
8390
  return;
8282
- const target = path8.resolve(filePath);
8391
+ const target = path9.resolve(filePath);
8283
8392
  const settled = settledWrites.get(target);
8284
8393
  if (settled && event !== "unlink" && Date.now() <= settled.expiresAt) {
8285
8394
  const diskContent = await fsp3.readFile(target, "utf8").catch(() => null);
@@ -8300,7 +8409,7 @@ function createFileWatcher(state) {
8300
8409
  recordChange(event, filePath);
8301
8410
  });
8302
8411
  externalWatcher.on("all", (event, filePath) => {
8303
- const target = path8.resolve(filePath);
8412
+ const target = path9.resolve(filePath);
8304
8413
  if (!desiredExternals.has(target))
8305
8414
  return;
8306
8415
  if (event === "add" && suppressInitialExternalAdds.delete(target))
@@ -8308,8 +8417,8 @@ function createFileWatcher(state) {
8308
8417
  recordChange(event, target);
8309
8418
  });
8310
8419
  function isWatchedSource(target) {
8311
- const rel = path8.relative(inputResolved, target);
8312
- const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path8.sep}`) && !path8.isAbsolute(rel);
8420
+ const rel = path9.relative(inputResolved, target);
8421
+ const inProject = rel === "" || rel !== ".." && !rel.startsWith(`..${path9.sep}`) && !path9.isAbsolute(rel);
8313
8422
  if (inProject)
8314
8423
  return !isIgnoredWatchPath(target, inputResolved);
8315
8424
  return desiredExternals.has(target);
@@ -8334,7 +8443,7 @@ function createFileWatcher(state) {
8334
8443
  state.notifySettledWrite = (filePath, writtenContent) => {
8335
8444
  if (closed)
8336
8445
  return;
8337
- const target = path8.resolve(filePath);
8446
+ const target = path9.resolve(filePath);
8338
8447
  if (isWatchedSource(target)) {
8339
8448
  acceptSettledWrite(target, writtenContent);
8340
8449
  return;
@@ -8355,7 +8464,7 @@ function createFileWatcher(state) {
8355
8464
  return;
8356
8465
  let nextTargets;
8357
8466
  try {
8358
- nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path8.resolve(target)));
8467
+ nextTargets = new Set((await externalWatchTargets(inputResolved, state.config)).map((target) => path9.resolve(target)));
8359
8468
  } catch (err) {
8360
8469
  debug(`Could not resolve external watch targets: ${err}`);
8361
8470
  return;
@@ -8487,7 +8596,7 @@ async function stopFileWatcher(state) {
8487
8596
  }
8488
8597
 
8489
8598
  // src/preview/lifecycle.ts
8490
- var TEMP_DIR_BASE = path9.join(tmpdir2(), "gutterpress-preview");
8599
+ var TEMP_DIR_BASE = path10.join(tmpdir2(), "gutterpress-preview");
8491
8600
  var PID_FILE_NAME = ".gutterpress.pid";
8492
8601
  function isProcessAlive(pid) {
8493
8602
  if (!Number.isFinite(pid) || pid <= 0)
@@ -8507,10 +8616,10 @@ async function cleanupOrphanTempDirs() {
8507
8616
  return;
8508
8617
  }
8509
8618
  for (const entry of entries) {
8510
- const dir = path9.join(TEMP_DIR_BASE, entry);
8511
- const pidFile = path9.join(dir, PID_FILE_NAME);
8619
+ const dir = path10.join(TEMP_DIR_BASE, entry);
8620
+ const pidFile = path10.join(dir, PID_FILE_NAME);
8512
8621
  try {
8513
- const raw = await readFile20(pidFile, "utf8");
8622
+ const raw = await readFile21(pidFile, "utf8");
8514
8623
  const pid = parseInt(raw.trim(), 10);
8515
8624
  if (!isProcessAlive(pid)) {
8516
8625
  debug(`Removing orphan temp dir ${dir} (pid ${pid} not alive)`);
@@ -8522,10 +8631,10 @@ async function cleanupOrphanTempDirs() {
8522
8631
  async function initializePreviewDirectories() {
8523
8632
  await cleanupOrphanTempDirs();
8524
8633
  const tempDirSuffix = randomBytes2(8).toString("hex");
8525
- const tempDir = path9.join(TEMP_DIR_BASE, tempDirSuffix);
8634
+ const tempDir = path10.join(TEMP_DIR_BASE, tempDirSuffix);
8526
8635
  await mkdir4(tempDir);
8527
8636
  debug(`Created temporary directory: ${tempDir}`);
8528
- await writeFile5(path9.join(tempDir, PID_FILE_NAME), `${process.pid}
8637
+ await writeFile5(path10.join(tempDir, PID_FILE_NAME), `${process.pid}
8529
8638
  `, "utf8");
8530
8639
  return tempDir;
8531
8640
  }
@@ -8616,8 +8725,8 @@ function createServerState(inputPath, tempDir, config, options) {
8616
8725
  import http from "node:http";
8617
8726
  import net from "node:net";
8618
8727
  import { createHash as createHash6, randomUUID } from "node:crypto";
8619
- import { readFile as readFile21, stat as stat4 } from "node:fs/promises";
8620
- import path11 from "path";
8728
+ import { readFile as readFile22, stat as stat4 } from "node:fs/promises";
8729
+ import path12 from "path";
8621
8730
  import { WebSocket, WebSocketServer } from "ws";
8622
8731
 
8623
8732
  // src/lib/open-path.ts
@@ -8649,7 +8758,7 @@ function openPath(filePath) {
8649
8758
  }
8650
8759
 
8651
8760
  // src/lib/static-serve.ts
8652
- import path10 from "node:path";
8761
+ import path11 from "node:path";
8653
8762
  var STATIC_MIME = {
8654
8763
  ".html": "text/html; charset=utf-8",
8655
8764
  ".htm": "text/html; charset=utf-8",
@@ -8671,9 +8780,9 @@ var STATIC_MIME = {
8671
8780
  ".otf": "font/otf"
8672
8781
  };
8673
8782
  function resolveWithinRoot(relPath, root) {
8674
- const resolvedRoot = path10.resolve(root);
8675
- const candidate = path10.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
8676
- if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path10.sep)) {
8783
+ const resolvedRoot = path11.resolve(root);
8784
+ const candidate = path11.resolve(resolvedRoot, "." + (relPath.startsWith("/") ? relPath : "/" + relPath));
8785
+ if (candidate !== resolvedRoot && !candidate.startsWith(resolvedRoot + path11.sep)) {
8677
8786
  return null;
8678
8787
  }
8679
8788
  return candidate;
@@ -8926,7 +9035,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8926
9035
  try {
8927
9036
  const s = await stat4(filePath);
8928
9037
  if (s.isDirectory())
8929
- filePath = path11.join(filePath, "index.html");
9038
+ filePath = path12.join(filePath, "index.html");
8930
9039
  } catch {
8931
9040
  res.writeHead(404);
8932
9041
  res.end("Not Found");
@@ -8934,7 +9043,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8934
9043
  }
8935
9044
  let data;
8936
9045
  try {
8937
- data = await readFile21(filePath);
9046
+ data = await readFile22(filePath);
8938
9047
  } catch {
8939
9048
  res.writeHead(404);
8940
9049
  res.end("Not Found");
@@ -8947,7 +9056,7 @@ async function serveStatic(absPath, res, cacheControl = "no-store", extraHeaders
8947
9056
  res.end(withHmr);
8948
9057
  return;
8949
9058
  }
8950
- const ct = STATIC_MIME[path11.extname(filePath).toLowerCase()] ?? "application/octet-stream";
9059
+ const ct = STATIC_MIME[path12.extname(filePath).toLowerCase()] ?? "application/octet-stream";
8951
9060
  res.writeHead(200, { "Content-Type": ct, "Cache-Control": cacheControl, ...extraHeaders });
8952
9061
  res.end(data);
8953
9062
  }
@@ -8957,7 +9066,7 @@ var EMBEDDED_CACHE_CONTROL = "public, no-cache";
8957
9066
  var EMBEDDED_ETAG = `"gutterpress-${PACKAGE_VERSION}"`;
8958
9067
  var PROJECT_ASSET_CACHE_CONTROL = "private, no-cache";
8959
9068
  async function serveRevalidatedStatic(req, absPath, res, validators) {
8960
- const extension = path11.extname(absPath).toLowerCase();
9069
+ const extension = path12.extname(absPath).toLowerCase();
8961
9070
  if (extension === ".html" || extension === ".htm") {
8962
9071
  await serveStatic(absPath, res);
8963
9072
  return;
@@ -8984,7 +9093,7 @@ async function serveRevalidatedStatic(req, absPath, res, validators) {
8984
9093
  let data;
8985
9094
  if (!validator || validator.stamp !== stamp) {
8986
9095
  try {
8987
- data = await readFile21(absPath);
9096
+ data = await readFile22(absPath);
8988
9097
  } catch {
8989
9098
  await serveStatic(absPath, res);
8990
9099
  return;
@@ -9123,7 +9232,7 @@ async function createPreviewServer(state, port) {
9123
9232
  const file = rawFile ? canonicalChapterId(rawFile) : "";
9124
9233
  const revision = Number(url.searchParams.get("revision"));
9125
9234
  const configuredFiles = state.config.source?.files;
9126
- if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path11.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
9235
+ if (!file || !state.currentInputPath || !incrementalPreviewEnabled() || !Number.isSafeInteger(revision) || revision !== reloadRevision || path12.extname(file).toLowerCase() !== ".md" || hasDotSegment(file) || !resolveWithinRoot(file, state.currentInputPath) || Array.isArray(configuredFiles) && configuredFiles.length > 0 && !configuredFiles.some((configured) => canonicalChapterId(configured) === file)) {
9127
9236
  res.writeHead(400);
9128
9237
  res.end("Bad Request");
9129
9238
  return;
@@ -9325,12 +9434,9 @@ async function startPreviewServer(options) {
9325
9434
  import { platform, arch, release } from "node:os";
9326
9435
 
9327
9436
  // src/lib/remote-auth/token-store.ts
9328
- import { chmod, mkdir as mkdir5, readFile as readFile22, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
9437
+ import { chmod, mkdir as mkdir5, readFile as readFile23, rename as rename2, writeFile as writeFile6 } from "node:fs/promises";
9329
9438
  import os3 from "node:os";
9330
- import path12 from "node:path";
9331
- function redactCredential(cred) {
9332
- return { ...cred, token: "•••redacted•••" };
9333
- }
9439
+ import path13 from "node:path";
9334
9440
  function credentialHostKey(hostOrUrl) {
9335
9441
  const trimmed = String(hostOrUrl ?? "").trim();
9336
9442
  if (!trimmed)
@@ -9357,22 +9463,22 @@ function defaultConfigDir() {
9357
9463
  if (override)
9358
9464
  return override;
9359
9465
  if (process.platform === "win32" && process.env.APPDATA) {
9360
- return path12.join(process.env.APPDATA, "gutterpress");
9466
+ return path13.join(process.env.APPDATA, "gutterpress");
9361
9467
  }
9362
9468
  const xdg = process.env.XDG_CONFIG_HOME?.trim();
9363
- return path12.join(xdg || path12.join(os3.homedir(), ".config"), "gutterpress");
9469
+ return path13.join(xdg || path13.join(os3.homedir(), ".config"), "gutterpress");
9364
9470
  }
9365
9471
 
9366
9472
  class FileTokenStore {
9367
9473
  filePath;
9368
9474
  queue = Promise.resolve();
9369
9475
  constructor(filePath) {
9370
- this.filePath = filePath ?? path12.join(defaultConfigDir(), "credentials.json");
9476
+ this.filePath = filePath ?? path13.join(defaultConfigDir(), "credentials.json");
9371
9477
  }
9372
9478
  async read() {
9373
9479
  let raw;
9374
9480
  try {
9375
- raw = await readFile22(this.filePath, "utf8");
9481
+ raw = await readFile23(this.filePath, "utf8");
9376
9482
  } catch (e) {
9377
9483
  if (e?.code === "ENOENT") {
9378
9484
  return { version: 1, credentials: {} };
@@ -9392,7 +9498,7 @@ class FileTokenStore {
9392
9498
  return { version: 1, credentials: {} };
9393
9499
  }
9394
9500
  async write(data) {
9395
- await mkdir5(path12.dirname(this.filePath), { recursive: true });
9501
+ await mkdir5(path13.dirname(this.filePath), { recursive: true });
9396
9502
  const tmpPath = `${this.filePath}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
9397
9503
  await writeFile6(tmpPath, JSON.stringify(data, null, 2), {
9398
9504
  encoding: "utf8",
@@ -9470,13 +9576,6 @@ function extractUrlCredential(url) {
9470
9576
  };
9471
9577
  return { cleanUrl: parsed.toString(), credential };
9472
9578
  }
9473
- async function migrateUrlCredential(url, store) {
9474
- const { cleanUrl, credential } = extractUrlCredential(url);
9475
- if (credential && !await store.get(credential.host)) {
9476
- await store.set(credential.host, credential);
9477
- }
9478
- return cleanUrl;
9479
- }
9480
9579
 
9481
9580
  // src/lib/diagnostics.ts
9482
9581
  var INSTALL_HINTS3 = {
@@ -9548,18 +9647,18 @@ async function getSystemDiagnostics() {
9548
9647
  installHint: INSTALL_HINTS3[t.hintKey]
9549
9648
  };
9550
9649
  }
9551
- const [path13, available] = await Promise.all([
9650
+ const [path14, available] = await Promise.all([
9552
9651
  findTool(t.bin),
9553
9652
  isToolAvailable(t.bin)
9554
9653
  ]);
9555
- const found = available || !!path13;
9654
+ const found = available || !!path14;
9556
9655
  const version = found ? await getVersion(t.bin) : undefined;
9557
9656
  return {
9558
9657
  id: t.id,
9559
9658
  name: t.name,
9560
9659
  bin: t.bin,
9561
9660
  found,
9562
- path: path13,
9661
+ path: path14,
9563
9662
  version,
9564
9663
  usedBy: t.usedBy,
9565
9664
  installHint: INSTALL_HINTS3[t.hintKey]
@@ -9579,25 +9678,25 @@ async function getSystemDiagnostics() {
9579
9678
  };
9580
9679
  }
9581
9680
  // src/lib/project-scaffold.ts
9582
- import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile24, stat as stat5, writeFile as writeFile7 } from "node:fs/promises";
9681
+ import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile25, stat as stat5, writeFile as writeFile7 } from "node:fs/promises";
9583
9682
  import { constants as FS, existsSync as existsSync11 } from "node:fs";
9584
- import path14 from "node:path";
9683
+ import path15 from "node:path";
9585
9684
 
9586
9685
  // src/lib/manifest-doc.ts
9587
9686
  import { existsSync as existsSync10 } from "node:fs";
9588
9687
  import { randomUUID as randomUUID2 } from "node:crypto";
9589
- import { mkdir as mkdir6, open, readFile as readFile23, rename as rename3, rm as rm2 } from "node:fs/promises";
9590
- import path13 from "node:path";
9688
+ import { mkdir as mkdir6, open, readFile as readFile24, rename as rename3, rm as rm2 } from "node:fs/promises";
9689
+ import path14 from "node:path";
9591
9690
  import { parseDocument, isSeq, YAMLSeq } from "yaml";
9592
9691
  function resolveManifestPath(projectDir) {
9593
- const existing = MANIFEST_FILENAMES.find((name) => existsSync10(path13.join(projectDir, name)));
9594
- return path13.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
9692
+ const existing = MANIFEST_FILENAMES.find((name) => existsSync10(path14.join(projectDir, name)));
9693
+ return path14.join(projectDir, existing ?? MANIFEST_FILENAMES[0]);
9595
9694
  }
9596
9695
  async function loadManifestDoc(projectDir) {
9597
9696
  const file = resolveManifestPath(projectDir);
9598
9697
  let text = "";
9599
9698
  try {
9600
- text = await readFile23(file, "utf8");
9699
+ text = await readFile24(file, "utf8");
9601
9700
  } catch (error2) {
9602
9701
  if (error2.code !== "ENOENT")
9603
9702
  throw error2;
@@ -9605,9 +9704,9 @@ async function loadManifestDoc(projectDir) {
9605
9704
  return { doc: parseDocument(text), file };
9606
9705
  }
9607
9706
  async function writeManifestDoc(file, doc) {
9608
- const dir = path13.dirname(file);
9707
+ const dir = path14.dirname(file);
9609
9708
  await mkdir6(dir, { recursive: true });
9610
- const temporary = path13.join(dir, `.${path13.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
9709
+ const temporary = path14.join(dir, `.${path14.basename(file)}.tmp-${process.pid}-${randomUUID2()}`);
9611
9710
  try {
9612
9711
  const handle = await open(temporary, "wx", 438);
9613
9712
  try {
@@ -9709,7 +9808,7 @@ async function scaffoldProject(options) {
9709
9808
  throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
9710
9809
  }
9711
9810
  const parentDir = options.parentDir;
9712
- if (!parentDir || !path14.isAbsolute(parentDir)) {
9811
+ if (!parentDir || !path15.isAbsolute(parentDir)) {
9713
9812
  throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
9714
9813
  }
9715
9814
  try {
@@ -9717,7 +9816,7 @@ async function scaffoldProject(options) {
9717
9816
  } catch {
9718
9817
  throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
9719
9818
  }
9720
- const projectDir = path14.join(parentDir, folderName);
9819
+ const projectDir = path15.join(parentDir, folderName);
9721
9820
  let targetExists = true;
9722
9821
  try {
9723
9822
  await access(projectDir, FS.F_OK);
@@ -9738,19 +9837,19 @@ async function scaffoldProject(options) {
9738
9837
  for (const entry of entries) {
9739
9838
  if (entry.name === ".gutterpress-template.json")
9740
9839
  continue;
9741
- await cp2(path14.join(customTemplateDir, entry.name), path14.join(projectDir, entry.name), { recursive: true });
9840
+ await cp2(path15.join(customTemplateDir, entry.name), path15.join(projectDir, entry.name), { recursive: true });
9742
9841
  }
9743
- await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
9842
+ await mkdir7(path15.join(projectDir, "assets"), { recursive: true });
9744
9843
  } else {
9745
9844
  const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
9746
9845
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
9747
- await mkdir7(path14.join(projectDir, "assets"), { recursive: true });
9748
- await copyFile(tplManifest, path14.join(projectDir, MANIFEST_FILENAMES[0]));
9749
- await copyFile(tplChapter, path14.join(projectDir, "chapter-01.md"));
9846
+ await mkdir7(path15.join(projectDir, "assets"), { recursive: true });
9847
+ await copyFile(tplManifest, path15.join(projectDir, MANIFEST_FILENAMES[0]));
9848
+ await copyFile(tplChapter, path15.join(projectDir, "chapter-01.md"));
9750
9849
  const starterThemeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
9751
9850
  const starterCss = await getAssetPath(`themes/${starterThemeId}/theme.css`);
9752
- await mkdir7(path14.join(projectDir, "styles"), { recursive: true });
9753
- await copyFile(starterCss, path14.join(projectDir, "styles", "book.css"));
9851
+ await mkdir7(path15.join(projectDir, "styles"), { recursive: true });
9852
+ await copyFile(starterCss, path15.join(projectDir, "styles", "book.css"));
9754
9853
  }
9755
9854
  } catch (e) {
9756
9855
  throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -9760,13 +9859,13 @@ async function scaffoldProject(options) {
9760
9859
  } catch (e) {
9761
9860
  throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
9762
9861
  }
9763
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path14.join(projectDir, name2)).find(existsSync11) ?? path14.join(projectDir, MANIFEST_FILENAMES[0]);
9862
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path15.join(projectDir, name2)).find(existsSync11) ?? path15.join(projectDir, MANIFEST_FILENAMES[0]);
9764
9863
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
9765
9864
  const substitutions = {
9766
9865
  "{{TITLE}}": escapeYamlScalar(name),
9767
9866
  "{{AUTHOR}}": escapeYamlScalar(author)
9768
9867
  };
9769
- let openFile = path14.join(projectDir, "chapter-01.md");
9868
+ let openFile = path15.join(projectDir, "chapter-01.md");
9770
9869
  try {
9771
9870
  await fillTemplateFile(manifestPath, substitutions);
9772
9871
  if (preset) {
@@ -9787,7 +9886,7 @@ async function scaffoldProject(options) {
9787
9886
  }
9788
9887
  const firstSource = await firstSourceFile(manifestPath);
9789
9888
  if (firstSource)
9790
- openFile = path14.join(projectDir, firstSource);
9889
+ openFile = path15.join(projectDir, firstSource);
9791
9890
  await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
9792
9891
  } catch (e) {
9793
9892
  throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
@@ -9797,7 +9896,7 @@ async function scaffoldProject(options) {
9797
9896
  let versionHistoryError;
9798
9897
  if (requested === "local-git") {
9799
9898
  try {
9800
- const { providerFor } = await import("./source-provider-vanafrt9.js");
9899
+ const { providerFor } = await import("./source-provider-kn41jmbs.js");
9801
9900
  const provider = providerFor({ type: "local-folder", path: projectDir });
9802
9901
  await provider.initVersionHistory({
9803
9902
  projectDir,
@@ -9809,7 +9908,7 @@ async function scaffoldProject(options) {
9809
9908
  versionHistory = "none";
9810
9909
  const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
9811
9910
  const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
9812
- versionHistoryError = enclosingRepoDir ? `This book was added to ${path14.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
9911
+ versionHistoryError = enclosingRepoDir ? `This book was added to ${path15.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
9813
9912
  }
9814
9913
  }
9815
9914
  const result = {
@@ -9829,7 +9928,7 @@ function prettifyFolderName(base) {
9829
9928
  }
9830
9929
  async function adoptFolder(options) {
9831
9930
  const dir = options.dir;
9832
- if (typeof dir !== "string" || !path14.isAbsolute(dir)) {
9931
+ if (typeof dir !== "string" || !path15.isAbsolute(dir)) {
9833
9932
  throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
9834
9933
  }
9835
9934
  let st;
@@ -9841,11 +9940,11 @@ async function adoptFolder(options) {
9841
9940
  if (!st.isDirectory()) {
9842
9941
  throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
9843
9942
  }
9844
- if (MANIFEST_FILENAMES.some((name) => existsSync11(path14.join(dir, name)))) {
9943
+ if (MANIFEST_FILENAMES.some((name) => existsSync11(path15.join(dir, name)))) {
9845
9944
  throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
9846
9945
  }
9847
9946
  const template = options.template ?? "book";
9848
- const title = (options.title ?? "").trim() || prettifyFolderName(path14.basename(dir));
9947
+ const title = (options.title ?? "").trim() || prettifyFolderName(path15.basename(dir));
9849
9948
  const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
9850
9949
  let mdFiles;
9851
9950
  try {
@@ -9853,13 +9952,13 @@ async function adoptFolder(options) {
9853
9952
  mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
9854
9953
  if (mdFiles.length === 0) {
9855
9954
  const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
9856
- await copyFile(tplChapter, path14.join(dir, "chapter-01.md"));
9857
- await fillTemplateFile(path14.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
9955
+ await copyFile(tplChapter, path15.join(dir, "chapter-01.md"));
9956
+ await fillTemplateFile(path15.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
9858
9957
  mdFiles = ["chapter-01.md"];
9859
9958
  }
9860
9959
  const themeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
9861
- await mkdir7(path14.join(dir, "styles"), { recursive: true });
9862
- const bookCssPath = path14.join(dir, "styles", "book.css");
9960
+ await mkdir7(path15.join(dir, "styles"), { recursive: true });
9961
+ const bookCssPath = path15.join(dir, "styles", "book.css");
9863
9962
  if (!existsSync11(bookCssPath)) {
9864
9963
  await copyFile(await getAssetPath(`themes/${themeId}/theme.css`), bookCssPath);
9865
9964
  }
@@ -9877,8 +9976,8 @@ ${filesYaml}
9877
9976
  styles:
9878
9977
  - styles/book.css
9879
9978
  `;
9880
- await writeFile7(path14.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
9881
- await mkdir7(path14.join(dir, "assets"), { recursive: true });
9979
+ await writeFile7(path15.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
9980
+ await mkdir7(path15.join(dir, "assets"), { recursive: true });
9882
9981
  await ensureGitignoreHasDist(dir);
9883
9982
  } catch (e) {
9884
9983
  if (e instanceof CreateProjectErrorImpl)
@@ -9890,7 +9989,7 @@ styles:
9890
9989
  let versionHistoryError;
9891
9990
  if (requested === "local-git") {
9892
9991
  try {
9893
- const { providerFor } = await import("./source-provider-vanafrt9.js");
9992
+ const { providerFor } = await import("./source-provider-kn41jmbs.js");
9894
9993
  const provider = providerFor({ type: "local-folder", path: dir });
9895
9994
  await provider.initVersionHistory({
9896
9995
  projectDir: dir,
@@ -9905,8 +10004,8 @@ styles:
9905
10004
  }
9906
10005
  const result = {
9907
10006
  projectDir: dir,
9908
- manifestPath: path14.join(dir, MANIFEST_FILENAMES[0]),
9909
- openFile: path14.join(dir, mdFiles[0]),
10007
+ manifestPath: path15.join(dir, MANIFEST_FILENAMES[0]),
10008
+ openFile: path15.join(dir, mdFiles[0]),
9910
10009
  versionHistory
9911
10010
  };
9912
10011
  if (versionHistoryError !== undefined)
@@ -9914,10 +10013,10 @@ styles:
9914
10013
  return result;
9915
10014
  }
9916
10015
  async function ensureGitignoreHasDist(projectDir) {
9917
- const gitignorePath = path14.join(projectDir, ".gitignore");
10016
+ const gitignorePath = path15.join(projectDir, ".gitignore");
9918
10017
  let existing = null;
9919
10018
  try {
9920
- existing = await readFile24(gitignorePath, "utf8");
10019
+ existing = await readFile25(gitignorePath, "utf8");
9921
10020
  } catch {}
9922
10021
  if (existing === null) {
9923
10022
  await writeFile7(gitignorePath, `dist/
@@ -9937,7 +10036,7 @@ async function ensureGitignoreHasDist(projectDir) {
9937
10036
  async function firstSourceFile(manifestPath) {
9938
10037
  let text;
9939
10038
  try {
9940
- text = await readFile24(manifestPath, "utf8");
10039
+ text = await readFile25(manifestPath, "utf8");
9941
10040
  } catch {
9942
10041
  return;
9943
10042
  }
@@ -9949,17 +10048,17 @@ async function firstSourceFile(manifestPath) {
9949
10048
  return m ? m[1].trim() : undefined;
9950
10049
  }
9951
10050
  async function fillTemplateFile(filePath, substitutions) {
9952
- let text = await readFile24(filePath, "utf8");
10051
+ let text = await readFile25(filePath, "utf8");
9953
10052
  for (const [token, value] of Object.entries(substitutions)) {
9954
10053
  text = text.split(token).join(value);
9955
10054
  }
9956
10055
  await writeFile7(filePath, text, "utf8");
9957
10056
  }
9958
10057
  // src/lib/project-templates.ts
9959
- import { cp as cp3, mkdir as mkdir8, readFile as readFile25, readdir as readdir7, stat as stat6, writeFile as writeFile8 } from "node:fs/promises";
10058
+ import { cp as cp3, mkdir as mkdir8, readFile as readFile26, readdir as readdir7, stat as stat6, writeFile as writeFile8 } from "node:fs/promises";
9960
10059
  import { constants as FS2, existsSync as existsSync12 } from "node:fs";
9961
10060
  import { access as access2 } from "node:fs/promises";
9962
- import path15 from "node:path";
10061
+ import path16 from "node:path";
9963
10062
  import { isMap, isScalar, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
9964
10063
  var BUILT_IN_TEMPLATE_IDS = [
9965
10064
  "book",
@@ -9968,7 +10067,7 @@ var BUILT_IN_TEMPLATE_IDS = [
9968
10067
  ];
9969
10068
  async function readTemplateChoices(manifestPath) {
9970
10069
  try {
9971
- const text = await readFile25(manifestPath, "utf8");
10070
+ const text = await readFile26(manifestPath, "utf8");
9972
10071
  const doc = parseDocument2(text);
9973
10072
  const out = {};
9974
10073
  const preset = doc.get("preset");
@@ -10043,7 +10142,7 @@ function rewriteEntry(node, newPath) {
10043
10142
  node.value = newPath;
10044
10143
  }
10045
10144
  function commonAncestorDir(absFiles) {
10046
- const dirSegs = absFiles.map((f) => path15.resolve(f).split(path15.sep).slice(0, -1));
10145
+ const dirSegs = absFiles.map((f) => path16.resolve(f).split(path16.sep).slice(0, -1));
10047
10146
  if (dirSegs.length === 0)
10048
10147
  return "";
10049
10148
  let common = dirSegs[0];
@@ -10053,22 +10152,22 @@ function commonAncestorDir(absFiles) {
10053
10152
  i++;
10054
10153
  common = common.slice(0, i);
10055
10154
  }
10056
- return common.join(path15.sep) || path15.sep;
10155
+ return common.join(path16.sep) || path16.sep;
10057
10156
  }
10058
10157
  function pickVendorDir(templateDir, base) {
10059
- const preferred = path15.basename(base) || VENDOR_DIR;
10060
- if (!existsSync12(path15.join(templateDir, preferred)))
10158
+ const preferred = path16.basename(base) || VENDOR_DIR;
10159
+ if (!existsSync12(path16.join(templateDir, preferred)))
10061
10160
  return preferred;
10062
10161
  for (let n = 1;; n++) {
10063
10162
  const candidate = `${preferred}-${n}`;
10064
- if (!existsSync12(path15.join(templateDir, candidate)))
10163
+ if (!existsSync12(path16.join(templateDir, candidate)))
10065
10164
  return candidate;
10066
10165
  }
10067
10166
  }
10068
10167
  var TEMPLATE_META_FILE = ".gutterpress-template.json";
10069
10168
  async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
10070
10169
  const { doc, file } = await loadManifestDoc(templateDir);
10071
- const escapes = (p) => escapesProjectRoot(sourceProjectDir, path15.resolve(sourceProjectDir, p));
10170
+ const escapes = (p) => escapesProjectRoot(sourceProjectDir, path16.resolve(sourceProjectDir, p));
10072
10171
  const stylesSeq = doc.get("styles", true);
10073
10172
  const pluginsSeq = doc.get("plugins", true);
10074
10173
  const escapingStyles = collectEscaping(isSeq2(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
@@ -10084,16 +10183,16 @@ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
10084
10183
  return { vendoredRefs: [], excludedRefs };
10085
10184
  }
10086
10185
  const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
10087
- const pluginFiles = escapingPlugins.map((p) => path15.resolve(sourceProjectDir, p.value));
10186
+ const pluginFiles = escapingPlugins.map((p) => path16.resolve(sourceProjectDir, p.value));
10088
10187
  const externalFiles = [...new Set([...styleClosure, ...pluginFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
10089
10188
  const base = commonAncestorDir(externalFiles);
10090
10189
  const vendorDir = pickVendorDir(templateDir, base);
10091
10190
  await Promise.all(externalFiles.filter((abs) => existsSync12(abs)).map(async (abs) => {
10092
- const dest = path15.join(templateDir, vendorDir, path15.relative(base, abs));
10093
- await mkdir8(path15.dirname(dest), { recursive: true });
10191
+ const dest = path16.join(templateDir, vendorDir, path16.relative(base, abs));
10192
+ await mkdir8(path16.dirname(dest), { recursive: true });
10094
10193
  await cp3(abs, dest);
10095
10194
  }));
10096
- const bookLocal = (value) => toPosixPath(path15.join(vendorDir, path15.relative(base, path15.resolve(sourceProjectDir, value))));
10195
+ const bookLocal = (value) => toPosixPath(path16.join(vendorDir, path16.relative(base, path16.resolve(sourceProjectDir, value))));
10097
10196
  const vendoredRefs = [];
10098
10197
  for (const { node, value } of [...escapingStyles, ...escapingPlugins]) {
10099
10198
  const rewritten = bookLocal(value);
@@ -10108,7 +10207,7 @@ async function saveProjectAsTemplate(options) {
10108
10207
  const id = slugify(name);
10109
10208
  if (!id)
10110
10209
  throw new Error(`Could not derive a template id from "${name}".`);
10111
- const dir = path15.join(templatesRoot, id);
10210
+ const dir = path16.join(templatesRoot, id);
10112
10211
  let exists = true;
10113
10212
  try {
10114
10213
  await access2(dir, FS2.F_OK);
@@ -10125,13 +10224,13 @@ async function saveProjectAsTemplate(options) {
10125
10224
  continue;
10126
10225
  if (entry.name === TEMPLATE_META_FILE)
10127
10226
  continue;
10128
- await cp3(path15.join(projectDir, entry.name), path15.join(dir, entry.name), { recursive: true });
10227
+ await cp3(path16.join(projectDir, entry.name), path16.join(dir, entry.name), { recursive: true });
10129
10228
  }
10130
10229
  const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
10131
10230
  const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
10132
10231
  if (manifestName)
10133
- await retokeniseManifest(path15.join(dir, manifestName));
10134
- await writeFile8(path15.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
10232
+ await retokeniseManifest(path16.join(dir, manifestName));
10233
+ await writeFile8(path16.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
10135
10234
  return {
10136
10235
  id,
10137
10236
  label: name,
@@ -10144,7 +10243,7 @@ async function saveProjectAsTemplate(options) {
10144
10243
  async function retokeniseManifest(manifestPath) {
10145
10244
  let text;
10146
10245
  try {
10147
- text = await readFile25(manifestPath, "utf8");
10246
+ text = await readFile26(manifestPath, "utf8");
10148
10247
  } catch {
10149
10248
  return;
10150
10249
  }
@@ -10162,10 +10261,10 @@ async function listCustomTemplates(templatesRoot) {
10162
10261
  for (const entry of entries) {
10163
10262
  if (!entry.isDirectory())
10164
10263
  continue;
10165
- const dir = path15.join(templatesRoot, entry.name);
10264
+ const dir = path16.join(templatesRoot, entry.name);
10166
10265
  let label = prettify(entry.name);
10167
10266
  try {
10168
- const meta = JSON.parse(await readFile25(path15.join(dir, TEMPLATE_META_FILE), "utf8"));
10267
+ const meta = JSON.parse(await readFile26(path16.join(dir, TEMPLATE_META_FILE), "utf8"));
10169
10268
  if (typeof meta.label === "string" && meta.label)
10170
10269
  label = meta.label;
10171
10270
  } catch {}
@@ -10175,7 +10274,7 @@ async function listCustomTemplates(templatesRoot) {
10175
10274
  description: "Your saved template.",
10176
10275
  kind: "custom",
10177
10276
  dir,
10178
- ...await readTemplateChoices(path15.join(dir, MANIFEST_FILENAMES[0]))
10277
+ ...await readTemplateChoices(path16.join(dir, MANIFEST_FILENAMES[0]))
10179
10278
  });
10180
10279
  }
10181
10280
  out.sort((a, b) => a.label.localeCompare(b.label));
@@ -10187,12 +10286,12 @@ async function importTemplateFromFolder(options) {
10187
10286
  if (!srcStat.isDirectory()) {
10188
10287
  throw new Error("The chosen path is not a folder.");
10189
10288
  }
10190
- const name = options.name?.trim() || prettify(path15.basename(sourceDir));
10289
+ const name = options.name?.trim() || prettify(path16.basename(sourceDir));
10191
10290
  return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
10192
10291
  }
10193
10292
  // src/lib/snippets.ts
10194
- import { readdir as readdir8, readFile as readFile26, writeFile as writeFile9, mkdir as mkdir9, rm as rm3 } from "node:fs/promises";
10195
- import path16 from "node:path";
10293
+ import { readdir as readdir8, readFile as readFile27, writeFile as writeFile9, mkdir as mkdir9, rm as rm3 } from "node:fs/promises";
10294
+ import path17 from "node:path";
10196
10295
  var SNIPPETS_DIR = "snippets";
10197
10296
  var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
10198
10297
  function extractVariables(template) {
@@ -10211,15 +10310,15 @@ function substituteVariables(template, values) {
10211
10310
  return template.replace(PLACEHOLDER_RE, (_full, name) => Object.prototype.hasOwnProperty.call(values, name) ? values[name] : "");
10212
10311
  }
10213
10312
  function resolveSnippetPath(projectDir, fileName) {
10214
- const dir = path16.resolve(projectDir, SNIPPETS_DIR);
10215
- const full = path16.resolve(dir, fileName);
10216
- if (full !== path16.join(dir, path16.basename(fileName)) || path16.dirname(full) !== dir) {
10313
+ const dir = path17.resolve(projectDir, SNIPPETS_DIR);
10314
+ const full = path17.resolve(dir, fileName);
10315
+ if (full !== path17.join(dir, path17.basename(fileName)) || path17.dirname(full) !== dir) {
10217
10316
  throw new Error(`Unsafe snippet filename: ${fileName}`);
10218
10317
  }
10219
10318
  return full;
10220
10319
  }
10221
10320
  async function listSnippets(projectDir) {
10222
- const dir = path16.join(projectDir, SNIPPETS_DIR);
10321
+ const dir = path17.join(projectDir, SNIPPETS_DIR);
10223
10322
  let names;
10224
10323
  try {
10225
10324
  names = await readdir8(dir);
@@ -10232,7 +10331,7 @@ async function listSnippets(projectDir) {
10232
10331
  continue;
10233
10332
  let body = "";
10234
10333
  try {
10235
- body = await readFile26(path16.join(dir, fileName), "utf8");
10334
+ body = await readFile27(path17.join(dir, fileName), "utf8");
10236
10335
  } catch {
10237
10336
  continue;
10238
10337
  }
@@ -10246,14 +10345,14 @@ async function listSnippets(projectDir) {
10246
10345
  return entries;
10247
10346
  }
10248
10347
  async function readSnippet(projectDir, fileName) {
10249
- return readFile26(resolveSnippetPath(projectDir, fileName), "utf8");
10348
+ return readFile27(resolveSnippetPath(projectDir, fileName), "utf8");
10250
10349
  }
10251
10350
  async function saveSnippet(projectDir, name, body) {
10252
10351
  const stem = slugify(name);
10253
10352
  if (!stem)
10254
10353
  throw new Error(`Could not derive a filename from "${name}".`);
10255
10354
  const fileName = `${stem}.md`;
10256
- const dir = path16.join(projectDir, SNIPPETS_DIR);
10355
+ const dir = path17.join(projectDir, SNIPPETS_DIR);
10257
10356
  await mkdir9(dir, { recursive: true });
10258
10357
  await writeFile9(resolveSnippetPath(projectDir, fileName), body, "utf8");
10259
10358
  return { name, fileName, variables: extractVariables(body) };
@@ -10264,7 +10363,7 @@ async function deleteSnippet(projectDir, fileName) {
10264
10363
  // src/lib/plugin-manager.ts
10265
10364
  import { randomUUID as randomUUID4 } from "node:crypto";
10266
10365
  import { cp as cp4, lstat as lstat3, mkdir as mkdir11, rename as rename5, rm as rm5, stat as stat7 } from "node:fs/promises";
10267
- import path18 from "node:path";
10366
+ import path19 from "node:path";
10268
10367
  import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
10269
10368
 
10270
10369
  // src/lib/npm-plugin-installer.ts
@@ -10275,12 +10374,12 @@ import {
10275
10374
  mkdir as mkdir10,
10276
10375
  mkdtemp as mkdtemp2,
10277
10376
  open as open2,
10278
- readFile as readFile27,
10377
+ readFile as readFile28,
10279
10378
  realpath as realpath2,
10280
10379
  rename as rename4,
10281
10380
  rm as rm4
10282
10381
  } from "node:fs/promises";
10283
- import path17 from "node:path";
10382
+ import path18 from "node:path";
10284
10383
  import { Transform } from "node:stream";
10285
10384
  import { pipeline } from "node:stream/promises";
10286
10385
  import { createGunzip } from "node:zlib";
@@ -10360,8 +10459,8 @@ function mb(bytes) {
10360
10459
  return `${Math.ceil(bytes / 1024 / 1024)}MB`;
10361
10460
  }
10362
10461
  function isContained2(root, candidate) {
10363
- const relative2 = path17.relative(root, candidate);
10364
- return relative2 === "" || !relative2.startsWith("..") && !path17.isAbsolute(relative2);
10462
+ const relative2 = path18.relative(root, candidate);
10463
+ return relative2 === "" || !relative2.startsWith("..") && !path18.isAbsolute(relative2);
10365
10464
  }
10366
10465
  function mergeLimits(overrides) {
10367
10466
  const limits = { ...DEFAULT_LIMITS, ...overrides };
@@ -10668,13 +10767,13 @@ async function extractTarball(ctx, archivePath, packageDir, packageName) {
10668
10767
  throw rejected;
10669
10768
  }
10670
10769
  async function readInstalledManifest(packageDir, selected) {
10671
- const packageJsonPath = path17.join(packageDir, "package.json");
10770
+ const packageJsonPath = path18.join(packageDir, "package.json");
10672
10771
  let manifest = null;
10673
10772
  try {
10674
10773
  const info2 = await lstat2(packageJsonPath);
10675
10774
  if (!info2.isFile() || info2.isSymbolicLink())
10676
10775
  throw new Error("not a regular file");
10677
- manifest = object2(JSON.parse(await readFile27(packageJsonPath, "utf8")));
10776
+ manifest = object2(JSON.parse(await readFile28(packageJsonPath, "utf8")));
10678
10777
  } catch (error2) {
10679
10778
  throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
10680
10779
  }
@@ -10744,10 +10843,10 @@ async function installPackage(ctx, name, selector, parentPackagePath, ancestors,
10744
10843
  throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
10745
10844
  }
10746
10845
  const relativePath = relativePackagePath(parentPackagePath, name);
10747
- const packageDir = path17.join(ctx.stageRoot, ...relativePath.split("/"));
10748
- const archivePath = path17.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
10846
+ const packageDir = path18.join(ctx.stageRoot, ...relativePath.split("/"));
10847
+ const archivePath = path18.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
10749
10848
  try {
10750
- await mkdir10(path17.dirname(packageDir), { recursive: true });
10849
+ await mkdir10(path18.dirname(packageDir), { recursive: true });
10751
10850
  await downloadTarball(ctx, selected, archivePath);
10752
10851
  await extractTarball(ctx, archivePath, packageDir, selected.name);
10753
10852
  const manifest = await readInstalledManifest(packageDir, selected);
@@ -10846,9 +10945,9 @@ async function ensureDirectory(pathname) {
10846
10945
  }
10847
10946
  }
10848
10947
  async function prepareVendorParent(projectDir, name) {
10849
- const plugins = path17.join(projectDir, PLUGINS_DIR);
10850
- const npm = path17.join(plugins, VENDORED_NPM_DIR);
10851
- const packageParent = path17.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
10948
+ const plugins = path18.join(projectDir, PLUGINS_DIR);
10949
+ const npm = path18.join(plugins, VENDORED_NPM_DIR);
10950
+ const packageParent = path18.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
10852
10951
  await mkdir10(projectDir, { recursive: true });
10853
10952
  await ensureDirectory(plugins);
10854
10953
  await ensureDirectory(npm);
@@ -10876,14 +10975,14 @@ function publicReceipt(record) {
10876
10975
  };
10877
10976
  }
10878
10977
  async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10879
- const requestedProjectDir = path17.resolve(projectDirInput);
10978
+ const requestedProjectDir = path18.resolve(projectDirInput);
10880
10979
  await mkdir10(requestedProjectDir, { recursive: true });
10881
10980
  const projectDir = await realpath2(requestedProjectDir);
10882
10981
  const requested = parseNpmPluginSpec(packageSpec);
10883
10982
  const npmRoot = await prepareVendorParent(projectDir, requested.name);
10884
- const container = await mkdtemp2(path17.join(npmRoot, ".install-"));
10885
- const stageRoot = path17.join(container, "root");
10886
- const downloadsDir = path17.join(container, "downloads");
10983
+ const container = await mkdtemp2(path18.join(npmRoot, ".install-"));
10984
+ const stageRoot = path18.join(container, "root");
10985
+ const downloadsDir = path18.join(container, "downloads");
10887
10986
  await Promise.all([mkdir10(stageRoot), mkdir10(downloadsDir)]);
10888
10987
  const ctx = {
10889
10988
  fetch: options.fetch ?? globalThis.fetch,
@@ -10929,7 +11028,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10929
11028
  skipped: ctx.skipped,
10930
11029
  tree: { algorithm: "sha256", ...tree }
10931
11030
  };
10932
- const receiptFile = await open2(path17.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
11031
+ const receiptFile = await open2(path18.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
10933
11032
  try {
10934
11033
  await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
10935
11034
  `, "utf8");
@@ -10938,7 +11037,7 @@ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
10938
11037
  await receiptFile.close();
10939
11038
  }
10940
11039
  finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
10941
- const realFinalParent = await realpath2(path17.dirname(finalRoot));
11040
+ const realFinalParent = await realpath2(path18.dirname(finalRoot));
10942
11041
  if (!isContained2(projectDir, realFinalParent)) {
10943
11042
  throw new Error("Plugin install destination resolves outside the project.");
10944
11043
  }
@@ -10987,7 +11086,7 @@ async function rollbackNpmPluginInstall(installed) {
10987
11086
  // src/lib/plugin-manager.ts
10988
11087
  var pluginMutationQueues = new Map;
10989
11088
  function withPluginMutationLock(projectDir, mutation) {
10990
- const resolved = path18.resolve(projectDir);
11089
+ const resolved = path19.resolve(projectDir);
10991
11090
  const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
10992
11091
  const previous = pluginMutationQueues.get(key) ?? Promise.resolve();
10993
11092
  const run2 = previous.then(mutation, mutation);
@@ -11101,7 +11200,7 @@ async function setPluginEnabledUnlocked(projectDir, ref, enabled) {
11101
11200
  const { doc, file } = await loadManifestDoc(projectDir);
11102
11201
  const seq = doc.get("plugins", true);
11103
11202
  if (!isSeq3(seq)) {
11104
- throw new Error(`No plugins are configured in ${path18.basename(file)}.`);
11203
+ throw new Error(`No plugins are configured in ${path19.basename(file)}.`);
11105
11204
  }
11106
11205
  const idx = indexOfRef(seq, ref);
11107
11206
  if (idx < 0) {
@@ -11219,12 +11318,12 @@ async function addLocalPluginUnlocked(projectDir, sourcePath) {
11219
11318
  } catch {
11220
11319
  throw new Error(`Plugin source not found: ${sourcePath}`);
11221
11320
  }
11222
- const base = path18.basename(sourcePath);
11223
- const destDir = path18.join(projectDir, PLUGINS_DIR);
11321
+ const base = path19.basename(sourcePath);
11322
+ const destDir = path19.join(projectDir, PLUGINS_DIR);
11224
11323
  await mkdir11(destDir, { recursive: true });
11225
- const dest = path18.join(destDir, base);
11226
- const staging = path18.join(destDir, `.import-${randomUUID4()}`);
11227
- const backup = path18.join(destDir, `.backup-${randomUUID4()}`);
11324
+ const dest = path19.join(destDir, base);
11325
+ const staging = path19.join(destDir, `.import-${randomUUID4()}`);
11326
+ const backup = path19.join(destDir, `.backup-${randomUUID4()}`);
11228
11327
  let hasBackup = false;
11229
11328
  try {
11230
11329
  if (info2.isDirectory()) {
@@ -11299,9 +11398,9 @@ async function validateProjectPlugins(projectDir) {
11299
11398
  return out;
11300
11399
  }
11301
11400
  // src/lib/theme-manager.ts
11302
- import { cp as cp5, mkdir as mkdir12, readFile as readFile28, readdir as readdir9, stat as stat8, writeFile as writeFile10, rm as rm6 } from "node:fs/promises";
11401
+ import { cp as cp5, mkdir as mkdir12, readFile as readFile29, readdir as readdir9, stat as stat8, writeFile as writeFile10, rm as rm6 } from "node:fs/promises";
11303
11402
  import { existsSync as existsSync13 } from "node:fs";
11304
- import path19 from "node:path";
11403
+ import path20 from "node:path";
11305
11404
  import { isSeq as isSeq4, Scalar as Scalar2 } from "yaml";
11306
11405
  var THEMES_DIR = "themes";
11307
11406
  var BUILT_IN_THEME_IDS = [
@@ -11325,7 +11424,7 @@ var BUILT_IN_FALLBACK_META = {
11325
11424
  };
11326
11425
  async function readThemeMeta(jsonPath) {
11327
11426
  try {
11328
- const text = await readFile28(jsonPath, "utf8");
11427
+ const text = await readFile29(jsonPath, "utf8");
11329
11428
  const parsed = JSON.parse(text);
11330
11429
  return parsed && typeof parsed === "object" ? parsed : {};
11331
11430
  } catch {
@@ -11349,7 +11448,7 @@ function themeDirFor(projectDir, id) {
11349
11448
  if (typeof id !== "string" || id.length === 0 || themeSlug(id) !== id) {
11350
11449
  throw new Error(`Invalid theme id "${id}".`);
11351
11450
  }
11352
- return path19.join(projectDir, THEMES_DIR, id);
11451
+ return path20.join(projectDir, THEMES_DIR, id);
11353
11452
  }
11354
11453
  async function listBuiltInThemes() {
11355
11454
  const out = [];
@@ -11384,10 +11483,10 @@ async function resolveBuiltInTheme(id) {
11384
11483
  if (!meta.description) {
11385
11484
  meta.description = BUILT_IN_FALLBACK_META[id]?.description;
11386
11485
  }
11387
- return { info: themeInfo(id, "builtin", meta), cssPath, dir: path19.dirname(cssPath) };
11486
+ return { info: themeInfo(id, "builtin", meta), cssPath, dir: path20.dirname(cssPath) };
11388
11487
  }
11389
11488
  async function listProjectThemes(projectDir) {
11390
- const root = path19.join(projectDir, THEMES_DIR);
11489
+ const root = path20.join(projectDir, THEMES_DIR);
11391
11490
  let entries;
11392
11491
  try {
11393
11492
  entries = await readdir9(root, { withFileTypes: true });
@@ -11398,10 +11497,10 @@ async function listProjectThemes(projectDir) {
11398
11497
  for (const entry of entries) {
11399
11498
  if (!entry.isDirectory())
11400
11499
  continue;
11401
- const dir = path19.join(root, entry.name);
11402
- if (!existsSync13(path19.join(dir, "theme.css")))
11500
+ const dir = path20.join(root, entry.name);
11501
+ if (!existsSync13(path20.join(dir, "theme.css")))
11403
11502
  continue;
11404
- const meta = await readThemeMeta(path19.join(dir, "theme.json"));
11503
+ const meta = await readThemeMeta(path20.join(dir, "theme.json"));
11405
11504
  out.push(themeInfo(entry.name, "project", meta));
11406
11505
  }
11407
11506
  out.sort((a, b) => a.name.localeCompare(b.name));
@@ -11434,10 +11533,10 @@ async function getActiveTheme(projectDir) {
11434
11533
  if (!m)
11435
11534
  continue;
11436
11535
  const id = m[1];
11437
- const dir = path19.join(projectDir, THEMES_DIR, id);
11438
- if (!existsSync13(path19.join(dir, "theme.css")))
11536
+ const dir = path20.join(projectDir, THEMES_DIR, id);
11537
+ if (!existsSync13(path20.join(dir, "theme.css")))
11439
11538
  continue;
11440
- const meta = await readThemeMeta(path19.join(dir, "theme.json"));
11539
+ const meta = await readThemeMeta(path20.join(dir, "theme.json"));
11441
11540
  return themeInfo(id, "project", meta);
11442
11541
  }
11443
11542
  return null;
@@ -11472,12 +11571,12 @@ async function getPreviousTheme(projectDir) {
11472
11571
  if (!id)
11473
11572
  return null;
11474
11573
  const dir = themeDirFor(projectDir, id);
11475
- if (!existsSync13(path19.join(dir, "theme.css")))
11574
+ if (!existsSync13(path20.join(dir, "theme.css")))
11476
11575
  return null;
11477
11576
  const active = await getActiveTheme(projectDir);
11478
11577
  if (active && active.id === id)
11479
11578
  return null;
11480
- const meta = await readThemeMeta(path19.join(dir, "theme.json"));
11579
+ const meta = await readThemeMeta(path20.join(dir, "theme.json"));
11481
11580
  return themeInfo(id, "project", meta);
11482
11581
  }
11483
11582
  async function revertTheme(projectDir) {
@@ -11492,28 +11591,28 @@ async function applyTheme(projectDir, target) {
11492
11591
  if (target.kind === "builtin") {
11493
11592
  const resolved = await resolveBuiltInTheme(target.id);
11494
11593
  let destId = target.id;
11495
- if (existsSync13(path19.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
11594
+ if (existsSync13(path20.join(projectDir, THEMES_DIR, destId, "theme.css"))) {
11496
11595
  destId = await uniqueThemeId(projectDir, target.id);
11497
11596
  }
11498
- const destDir = path19.join(projectDir, THEMES_DIR, destId);
11597
+ const destDir = path20.join(projectDir, THEMES_DIR, destId);
11499
11598
  await mkdir12(destDir, { recursive: true });
11500
11599
  await cp5(resolved.dir, destDir, { recursive: true });
11501
- info2 = themeInfo(destId, "project", await readThemeMeta(path19.join(destDir, "theme.json")));
11600
+ info2 = themeInfo(destId, "project", await readThemeMeta(path20.join(destDir, "theme.json")));
11502
11601
  } else {
11503
11602
  const dir = themeDirFor(projectDir, target.id);
11504
- if (!existsSync13(path19.join(dir, "theme.css"))) {
11603
+ if (!existsSync13(path20.join(dir, "theme.css"))) {
11505
11604
  throw new Error(`Theme "${target.id}" is not present in this project.`);
11506
11605
  }
11507
- info2 = themeInfo(target.id, "project", await readThemeMeta(path19.join(dir, "theme.json")));
11606
+ info2 = themeInfo(target.id, "project", await readThemeMeta(path20.join(dir, "theme.json")));
11508
11607
  }
11509
11608
  await setActiveThemeStyle(projectDir, info2.id);
11510
11609
  return info2;
11511
11610
  }
11512
11611
  async function uniqueThemeId(projectDir, base) {
11513
- const root = path19.join(projectDir, THEMES_DIR);
11612
+ const root = path20.join(projectDir, THEMES_DIR);
11514
11613
  let id = themeSlug(base);
11515
11614
  let n = 2;
11516
- while (existsSync13(path19.join(root, id))) {
11615
+ while (existsSync13(path20.join(root, id))) {
11517
11616
  id = `${themeSlug(base)}-${n++}`;
11518
11617
  }
11519
11618
  return id;
@@ -11528,16 +11627,16 @@ async function importThemeFromFolder(projectDir, sourceDir) {
11528
11627
  if (!info2.isDirectory()) {
11529
11628
  throw new Error("The chosen path is not a folder.");
11530
11629
  }
11531
- if (!existsSync13(path19.join(sourceDir, "theme.css"))) {
11630
+ if (!existsSync13(path20.join(sourceDir, "theme.css"))) {
11532
11631
  throw new Error("A theme folder must contain a theme.css file.");
11533
11632
  }
11534
- const meta = await readThemeMeta(path19.join(sourceDir, "theme.json"));
11535
- const base = meta.name || path19.basename(sourceDir);
11633
+ const meta = await readThemeMeta(path20.join(sourceDir, "theme.json"));
11634
+ const base = meta.name || path20.basename(sourceDir);
11536
11635
  const id = await uniqueThemeId(projectDir, base);
11537
- const destDir = path19.join(projectDir, THEMES_DIR, id);
11538
- await mkdir12(path19.dirname(destDir), { recursive: true });
11636
+ const destDir = path20.join(projectDir, THEMES_DIR, id);
11637
+ await mkdir12(path20.dirname(destDir), { recursive: true });
11539
11638
  await cp5(sourceDir, destDir, { recursive: true });
11540
- return themeInfo(id, "project", await readThemeMeta(path19.join(destDir, "theme.json")));
11639
+ return themeInfo(id, "project", await readThemeMeta(path20.join(destDir, "theme.json")));
11541
11640
  }
11542
11641
  function looksLikeCssUrl(url) {
11543
11642
  try {
@@ -11584,7 +11683,7 @@ async function importThemeFromUrl(projectDir, url) {
11584
11683
  let baseName;
11585
11684
  if (looksLikeCssUrl(trimmed)) {
11586
11685
  css = await fetchText(trimmed);
11587
- baseName = path19.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
11686
+ baseName = path20.basename(new URL(trimmed, "https://x/").pathname, ".css") || "theme";
11588
11687
  } else {
11589
11688
  const base = trimmed.endsWith("/") ? trimmed : `${trimmed}/`;
11590
11689
  css = await fetchText(`${base}theme.css`);
@@ -11594,37 +11693,37 @@ async function importThemeFromUrl(projectDir, url) {
11594
11693
  if (parsed && typeof parsed === "object")
11595
11694
  meta = parsed;
11596
11695
  } catch {}
11597
- baseName = meta.name || path19.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
11696
+ baseName = meta.name || path20.basename(new URL(base, "https://x/").pathname.replace(/\/$/, "")) || "theme";
11598
11697
  }
11599
11698
  if (!css.trim()) {
11600
11699
  throw new Error(`Fetched theme CSS from ${trimmed} was empty.`);
11601
11700
  }
11602
11701
  assertLooksLikeCss(css, trimmed);
11603
11702
  const id = await uniqueThemeId(projectDir, meta.name || baseName);
11604
- const destDir = path19.join(projectDir, THEMES_DIR, id);
11703
+ const destDir = path20.join(projectDir, THEMES_DIR, id);
11605
11704
  await mkdir12(destDir, { recursive: true });
11606
- await writeFile10(path19.join(destDir, "theme.css"), css, "utf8");
11705
+ await writeFile10(path20.join(destDir, "theme.css"), css, "utf8");
11607
11706
  const finalMeta = {
11608
11707
  name: meta.name || prettify(id),
11609
11708
  author: meta.author,
11610
11709
  description: meta.description,
11611
11710
  preview: meta.preview ?? null
11612
11711
  };
11613
- await writeFile10(path19.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
11712
+ await writeFile10(path20.join(destDir, "theme.json"), JSON.stringify(finalMeta, null, 2), "utf8");
11614
11713
  return themeInfo(id, "project", finalMeta);
11615
11714
  }
11616
11715
  async function readThemeCss(projectDir, source) {
11617
11716
  if (source.kind === "builtin") {
11618
11717
  const resolved = await resolveBuiltInTheme(source.id);
11619
- return readFile28(resolved.cssPath, "utf8");
11718
+ return readFile29(resolved.cssPath, "utf8");
11620
11719
  }
11621
11720
  if (!projectDir)
11622
11721
  throw new Error("A project is required to read a project theme.");
11623
- const cssPath = path19.join(themeDirFor(projectDir, source.id), "theme.css");
11722
+ const cssPath = path20.join(themeDirFor(projectDir, source.id), "theme.css");
11624
11723
  if (!existsSync13(cssPath)) {
11625
11724
  throw new Error(`Theme "${source.id}" has no theme.css in this project.`);
11626
11725
  }
11627
- return readFile28(cssPath, "utf8");
11726
+ return readFile29(cssPath, "utf8");
11628
11727
  }
11629
11728
  async function removeProjectTheme(projectDir, id) {
11630
11729
  const dir = themeDirFor(projectDir, id);
@@ -11651,9 +11750,9 @@ async function removeProjectTheme(projectDir, id) {
11651
11750
  }
11652
11751
  // src/lib/theme-import.ts
11653
11752
  import { existsSync as existsSync14 } from "node:fs";
11654
- import { mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile29, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
11753
+ import { mkdir as mkdir13, mkdtemp as mkdtemp3, readFile as readFile30, rm as rm7, writeFile as writeFile11 } from "node:fs/promises";
11655
11754
  import { tmpdir as tmpdir3 } from "node:os";
11656
- import path20 from "node:path";
11755
+ import path21 from "node:path";
11657
11756
  import { unzipSync } from "fflate";
11658
11757
  var MAX_THEME_ARCHIVE_BYTES = 25 * 1024 * 1024;
11659
11758
  var MAX_THEME_UNZIPPED_BYTES = 25 * 1024 * 1024;
@@ -11717,18 +11816,18 @@ function mb2(n) {
11717
11816
  }
11718
11817
  async function readThemeJson(p) {
11719
11818
  try {
11720
- const parsed = JSON.parse(await readFile29(p, "utf8"));
11819
+ const parsed = JSON.parse(await readFile30(p, "utf8"));
11721
11820
  return parsed && typeof parsed === "object" ? parsed : {};
11722
11821
  } catch {
11723
11822
  return null;
11724
11823
  }
11725
11824
  }
11726
11825
  async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
11727
- const cssPath = path20.join(sourceDir, "theme.css");
11826
+ const cssPath = path21.join(sourceDir, "theme.css");
11728
11827
  if (!existsSync14(cssPath)) {
11729
11828
  throw new Error("A theme must contain a theme.css file.");
11730
11829
  }
11731
- const css = await readFile29(cssPath, "utf8");
11830
+ const css = await readFile30(cssPath, "utf8");
11732
11831
  const { reject, warnings: printFindings } = classifyThemeCssFindings(checkCss(css, "theme.css"));
11733
11832
  if (reject) {
11734
11833
  throw new Error(`theme.css could not be parsed — ${reject.message}`);
@@ -11737,7 +11836,7 @@ async function finalizeThemeImport(projectDir, sourceDir, relPaths) {
11737
11836
  for (const w of printFindings) {
11738
11837
  warnings.push({ code: "print-safety", message: w.message });
11739
11838
  }
11740
- const jsonPath = path20.join(sourceDir, "theme.json");
11839
+ const jsonPath = path21.join(sourceDir, "theme.json");
11741
11840
  if (!existsSync14(jsonPath)) {
11742
11841
  warnings.push({
11743
11842
  code: "no-theme-json",
@@ -11804,7 +11903,7 @@ async function importThemeFromZip(projectDir, archive) {
11804
11903
  throw new Error("No theme.css found in the package. A theme needs a theme.css at the package root or inside a single folder.");
11805
11904
  }
11806
11905
  const prefix = root === "" ? "" : `${root}/`;
11807
- const tmp = await mkdtemp3(path20.join(tmpdir3(), "gutterpress-theme-"));
11906
+ const tmp = await mkdtemp3(path21.join(tmpdir3(), "gutterpress-theme-"));
11808
11907
  try {
11809
11908
  const relPaths = [];
11810
11909
  for (const [name, data] of Object.entries(files)) {
@@ -11816,12 +11915,12 @@ async function importThemeFromZip(projectDir, archive) {
11816
11915
  const rel = prefix ? norm.slice(prefix.length) : norm;
11817
11916
  if (!rel || rel.split("/").some((seg) => seg === ".."))
11818
11917
  continue;
11819
- const dest = path20.join(tmp, rel);
11820
- if (dest !== tmp && !dest.startsWith(tmp + path20.sep)) {
11918
+ const dest = path21.join(tmp, rel);
11919
+ if (dest !== tmp && !dest.startsWith(tmp + path21.sep)) {
11821
11920
  throw new Error(`The package contains an unsafe path and was rejected: ${name}`);
11822
11921
  }
11823
11922
  relPaths.push(rel);
11824
- await mkdir13(path20.dirname(dest), { recursive: true });
11923
+ await mkdir13(path21.dirname(dest), { recursive: true });
11825
11924
  await writeFile11(dest, data);
11826
11925
  }
11827
11926
  return await finalizeThemeImport(projectDir, tmp, relPaths);
@@ -11835,10 +11934,10 @@ async function importThemeFromCssText(projectDir, css, name) {
11835
11934
  throw new Error(`The CSS could not be parsed — ${reject.message}`);
11836
11935
  }
11837
11936
  const displayName = name.trim() || "Imported theme";
11838
- const tmp = await mkdtemp3(path20.join(tmpdir3(), "gutterpress-theme-"));
11937
+ const tmp = await mkdtemp3(path21.join(tmpdir3(), "gutterpress-theme-"));
11839
11938
  try {
11840
- await writeFile11(path20.join(tmp, "theme.css"), css, "utf8");
11841
- await writeFile11(path20.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
11939
+ await writeFile11(path21.join(tmp, "theme.css"), css, "utf8");
11940
+ await writeFile11(path21.join(tmp, "theme.json"), JSON.stringify({ name: displayName }, null, 2), "utf8");
11842
11941
  const result = await finalizeThemeImport(projectDir, tmp, ["theme.css", "theme.json"]);
11843
11942
  result.warnings.unshift({
11844
11943
  code: "no-theme-json",
@@ -11850,14 +11949,14 @@ async function importThemeFromCssText(projectDir, css, name) {
11850
11949
  }
11851
11950
  }
11852
11951
  async function importThemeFromFile(projectDir, filePath) {
11853
- const ext = path20.extname(filePath).toLowerCase();
11952
+ const ext = path21.extname(filePath).toLowerCase();
11854
11953
  if (ext === ".zip") {
11855
- const buf = await readFile29(filePath);
11954
+ const buf = await readFile30(filePath);
11856
11955
  return importThemeFromZip(projectDir, new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength));
11857
11956
  }
11858
11957
  if (ext === ".css") {
11859
- const css = await readFile29(filePath, "utf8");
11860
- return importThemeFromCssText(projectDir, css, prettify(path20.basename(filePath, ".css")));
11958
+ const css = await readFile30(filePath, "utf8");
11959
+ return importThemeFromCssText(projectDir, css, prettify(path21.basename(filePath, ".css")));
11861
11960
  }
11862
11961
  throw new Error("Choose a .zip theme package or a .css stylesheet.");
11863
11962
  }
@@ -11951,52 +12050,6 @@ async function setActiveStyles(projectDir, paths) {
11951
12050
  await writeManifestDoc(file, doc);
11952
12051
  return paths;
11953
12052
  }
11954
- // src/lib/app-heartbeat.ts
11955
- import { readFile as readFile30, unlink as unlink3, writeFile as writeFile12 } from "node:fs/promises";
11956
- import path21 from "node:path";
11957
- var HEARTBEAT_FILENAME = "gutterpress-app-heartbeat";
11958
- var APP_HEARTBEAT_FRESH_MS = 2 * 60000;
11959
- var HEARTBEAT_TTL_MULTIPLIER = 2;
11960
- var HEARTBEAT_TTL_BUFFER_MS = 30000;
11961
- function heartbeatTtlMs(periodicMs) {
11962
- if (periodicMs === null)
11963
- return APP_HEARTBEAT_FRESH_MS;
11964
- return periodicMs * HEARTBEAT_TTL_MULTIPLIER + HEARTBEAT_TTL_BUFFER_MS;
11965
- }
11966
- function appHeartbeatPath(repoDir) {
11967
- return path21.join(gitDirFor(repoDir), HEARTBEAT_FILENAME);
11968
- }
11969
- async function writeAppHeartbeat(repoDir, now = Date.now(), pid = process.pid, ttlMs) {
11970
- try {
11971
- const heartbeat = { pid, timestamp: now, ...ttlMs !== undefined ? { ttlMs } : {} };
11972
- await writeFile12(appHeartbeatPath(repoDir), JSON.stringify(heartbeat));
11973
- } catch {}
11974
- }
11975
- async function removeAppHeartbeat(repoDir) {
11976
- try {
11977
- await unlink3(appHeartbeatPath(repoDir));
11978
- } catch {}
11979
- }
11980
- async function readAppHeartbeat(repoDir) {
11981
- try {
11982
- const raw = await readFile30(appHeartbeatPath(repoDir), "utf8");
11983
- const parsed = JSON.parse(raw);
11984
- if (parsed && typeof parsed === "object" && typeof parsed.pid === "number" && typeof parsed.timestamp === "number") {
11985
- const candidate = parsed;
11986
- const ttlMs = typeof candidate.ttlMs === "number" ? candidate.ttlMs : undefined;
11987
- return { pid: candidate.pid, timestamp: candidate.timestamp, ...ttlMs !== undefined ? { ttlMs } : {} };
11988
- }
11989
- return null;
11990
- } catch {
11991
- return null;
11992
- }
11993
- }
11994
- async function isAppHeartbeatFresh(repoDir, now = Date.now(), maxAgeMs = APP_HEARTBEAT_FRESH_MS) {
11995
- const heartbeat = await readAppHeartbeat(repoDir);
11996
- if (!heartbeat)
11997
- return false;
11998
- return now - heartbeat.timestamp < (heartbeat.ttlMs ?? maxAgeMs);
11999
- }
12000
12053
  // src/lib/host-policy.ts
12001
12054
  var AUTO_SNAPSHOT_MIN_MINUTES = 5;
12002
12055
  var AUTO_SNAPSHOT_MAX_MINUTES = 24 * 60;
@@ -12004,6 +12057,7 @@ var AUTO_SNAPSHOT_DEFAULT_MINUTES = 10;
12004
12057
  var AUTO_SYNC_MIN_MINUTES = 1;
12005
12058
  var AUTO_SYNC_MAX_MINUTES = 24 * 60;
12006
12059
  var AUTO_SYNC_DEFAULT_MINUTES = 2;
12060
+ var AUTO_SYNC_PUSH_INTERVAL_MINUTES = 15;
12007
12061
  function clampedDelayMs(enabled, minutes, bounds) {
12008
12062
  const isEnabled = enabled ?? bounds.enabledDefault;
12009
12063
  if (!isEnabled)
@@ -12065,9 +12119,6 @@ class GitHubAuthProvider {
12065
12119
  this.fetchImpl = options.fetchImpl ?? fetch;
12066
12120
  this.sleep = options.sleepImpl ?? defaultSleep;
12067
12121
  }
12068
- matches(origin) {
12069
- return origin.hostname.toLowerCase() === GITHUB_HOST;
12070
- }
12071
12122
  async connect(callbacks) {
12072
12123
  const { signal } = callbacks;
12073
12124
  const codeRes = await safeFetch(this.fetchImpl, DEVICE_CODE_URL, {
@@ -12131,17 +12182,6 @@ class GitHubAuthProvider {
12131
12182
  }
12132
12183
  }
12133
12184
  }
12134
- async validate(credential) {
12135
- try {
12136
- const res = await safeFetch(this.fetchImpl, `${API_BASE}/user`, {
12137
- method: "GET",
12138
- headers: githubApiHeaders(credential.token)
12139
- });
12140
- return res.status === 200;
12141
- } catch {
12142
- return true;
12143
- }
12144
- }
12145
12185
  async fetchUsername(token) {
12146
12186
  try {
12147
12187
  const res = await safeFetch(this.fetchImpl, `${API_BASE}/user`, {
@@ -12264,10 +12304,9 @@ async function listGitHubBranches(credential, owner, repo, options = {}) {
12264
12304
  return branches;
12265
12305
  }
12266
12306
  // src/lib/remote-auth/clone.ts
12267
- import * as fs6 from "node:fs";
12268
- import { mkdir as mkdir14, readdir as readdir11, readFile as readFile31, rm as rm9, writeFile as writeFile13 } from "node:fs/promises";
12269
- import path24 from "node:path";
12270
- import git4 from "isomorphic-git";
12307
+ import { mkdir as mkdir14, readdir as readdir10, rm as rm8 } from "node:fs/promises";
12308
+ import path22 from "node:path";
12309
+ import git3 from "isomorphic-git";
12271
12310
 
12272
12311
  // src/lib/remote-auth/git-http.ts
12273
12312
  import httpNode from "isomorphic-git/http/node";
@@ -12360,20 +12399,27 @@ function withIdleTimeout(http2, idleMs = GIT_HTTP_IDLE_TIMEOUT_MS, uploadMs = GI
12360
12399
  }
12361
12400
  var defaultGitHttp = withIdleTimeout(httpNode);
12362
12401
 
12363
- // src/lib/remote-auth/recovery/classify.ts
12364
- var STALE_LOCK_MIN_AGE_MS = 2 * 60 * 1000;
12365
-
12366
- class RepoNeedsRecoveryError extends Error {
12367
- kind;
12368
- code = "RepoNeedsRecovery";
12369
- constructor(kind) {
12370
- super(`The project needs repair before it can sync (${kind}).`);
12371
- this.kind = kind;
12372
- this.name = "RepoNeedsRecoveryError";
12373
- }
12402
+ // src/lib/remote-auth/transport.ts
12403
+ import git2 from "isomorphic-git";
12404
+ async function repoDirFor(projectDir) {
12405
+ const source = await detectProjectSource(projectDir);
12406
+ if (source.type === "local-git-folder")
12407
+ return gitScopeFor(source);
12408
+ return projectDir;
12374
12409
  }
12375
- function isRepoNeedsRecoveryError(e) {
12376
- return e?.code === "RepoNeedsRecovery";
12410
+ function isCredentialTransmissionSafe(url) {
12411
+ try {
12412
+ const u = new URL(url);
12413
+ if (u.protocol === "https:")
12414
+ return true;
12415
+ if (u.protocol === "http:") {
12416
+ const host = u.hostname.toLowerCase();
12417
+ return host === "127.0.0.1" || host === "[::1]" || host === "localhost";
12418
+ }
12419
+ return false;
12420
+ } catch {
12421
+ return false;
12422
+ }
12377
12423
  }
12378
12424
 
12379
12425
  class InsecureTransportError extends Error {
@@ -12386,25 +12432,50 @@ class InsecureTransportError extends Error {
12386
12432
  function isInsecureTransportError(e) {
12387
12433
  return e?.code === "InsecureTransport";
12388
12434
  }
12389
- function isPushRejected(e) {
12390
- const code = e?.code;
12391
- if (code === "PushRejectedError") {
12392
- const reason = e?.data?.reason;
12393
- return reason === undefined || reason === "not-fast-forward";
12435
+ function onAuthFor(credential) {
12436
+ if (!credential)
12437
+ return {};
12438
+ return {
12439
+ onAuth: (url) => {
12440
+ if (!isCredentialTransmissionSafe(url))
12441
+ throw new InsecureTransportError;
12442
+ return {
12443
+ username: credential.kind === "github-oauth" ? "x-access-token" : credential.username || credential.token,
12444
+ password: credential.token
12445
+ };
12446
+ }
12447
+ };
12448
+ }
12449
+ async function resolveTransport(dir, options) {
12450
+ const origin = await syncRemoteFor(dir);
12451
+ if (!origin?.url)
12452
+ throw new Error(MSG_NO_REMOTE);
12453
+ const { cleanUrl, credential: urlCredential } = extractUrlCredential(origin.url);
12454
+ let parsed;
12455
+ try {
12456
+ parsed = new URL(cleanUrl);
12457
+ } catch {
12458
+ throw new Error(MSG_SSH_REMOTE);
12394
12459
  }
12395
- if (code === "GitPushError") {
12396
- const msg = (e?.data?.prettyDetails ?? "") + " " + (e?.message ?? "");
12397
- return /non-fast-forward|would not be a fast-forward|not a simple fast-forward/i.test(msg);
12460
+ if (!/^https?:$/.test(parsed.protocol))
12461
+ throw new Error(MSG_SSH_REMOTE);
12462
+ const host = credentialHostKey(cleanUrl);
12463
+ let credential = options.credential ?? undefined;
12464
+ if (!credential && options.tokenStore) {
12465
+ try {
12466
+ credential = await options.tokenStore.get(host) ?? undefined;
12467
+ } catch {
12468
+ credential = undefined;
12469
+ }
12398
12470
  }
12399
- return false;
12400
- }
12401
- function isMergeConflictError(e) {
12402
- return e?.code === "MergeConflictError";
12403
- }
12404
- function isUnrelatedHistories(e) {
12405
- const code = e?.code;
12406
- const msg = e?.message ?? String(e);
12407
- return code === "MergeNotSupportedError" || /unrelated histories|no common commits|refusing to merge unrelated/i.test(msg);
12471
+ if (!credential && urlCredential)
12472
+ credential = urlCredential;
12473
+ return {
12474
+ remote: origin.remote,
12475
+ url: cleanUrl,
12476
+ host,
12477
+ ...credential ? { credential } : {}
12478
+ };
12408
12479
  }
12409
12480
  function classifyTransportFailure(e) {
12410
12481
  if (isInsecureTransportError(e))
@@ -12424,300 +12495,6 @@ function classifyTransportFailure(e) {
12424
12495
  }
12425
12496
  return null;
12426
12497
  }
12427
- function isLikelyRepoCorruption(e) {
12428
- if (classifyTransportFailure(e))
12429
- return false;
12430
- const code = e?.code;
12431
- const msg = e?.message ?? String(e);
12432
- return code === "ReadObjectFail" || code === "ObjectTypeError" || code === "NotFoundError" && /could not find/i.test(msg) || /object not found|missing object|pack.*corrupt|bad object|corrupt.*index|index.*corrupt|invalid index|could not find head|packed-refs/i.test(msg);
12433
- }
12434
- function classifyFromHealth(health, opts = {}) {
12435
- const minLockAgeMs = opts.minLockAgeMs ?? STALE_LOCK_MIN_AGE_MS;
12436
- if (!health.hasGitDir)
12437
- return "needs_repair";
12438
- if (health.hasInterruptedRebase)
12439
- return "needs_repair";
12440
- if (health.hasInterruptedCherryPick)
12441
- return "needs_repair";
12442
- if (health.hasInterruptedMerge)
12443
- return "needs_repair";
12444
- if (health.headUnreadable)
12445
- return "needs_repair";
12446
- if (health.isDetachedHead)
12447
- return "needs_repair";
12448
- if (health.hasStaleLock && (health.lockAgeMs ?? 0) >= minLockAgeMs) {
12449
- return "stale_lock";
12450
- }
12451
- return null;
12452
- }
12453
-
12454
- // src/lib/remote-auth/transport.ts
12455
- import * as fs5 from "node:fs";
12456
- import git3 from "isomorphic-git";
12457
-
12458
- // src/lib/remote-auth/recovery/inspect.ts
12459
- import * as fs4 from "node:fs";
12460
- import path23 from "node:path";
12461
- import git2 from "isomorphic-git";
12462
-
12463
- // src/lib/remote-auth/recovery/locks.ts
12464
- import { readdir as readdir10, rm as rm8, stat as stat9 } from "node:fs/promises";
12465
- import path22 from "node:path";
12466
- var TOP_LEVEL_LOCK_NAMES = [
12467
- "index.lock",
12468
- "HEAD.lock",
12469
- "config.lock",
12470
- "packed-refs.lock"
12471
- ];
12472
- async function collectRefLockPaths(dir, out) {
12473
- let entries;
12474
- try {
12475
- entries = await readdir10(dir, { withFileTypes: true });
12476
- } catch {
12477
- return;
12478
- }
12479
- for (const entry of entries) {
12480
- const abs = path22.join(dir, entry.name);
12481
- if (entry.isDirectory()) {
12482
- await collectRefLockPaths(abs, out);
12483
- } else if (entry.isFile() && entry.name.endsWith(".lock")) {
12484
- out.push(abs);
12485
- }
12486
- }
12487
- }
12488
- async function findLockCandidates(gitDir, now) {
12489
- const paths = TOP_LEVEL_LOCK_NAMES.map((name) => path22.join(gitDir, name));
12490
- await collectRefLockPaths(path22.join(gitDir, "refs"), paths);
12491
- const candidates = [];
12492
- for (const p of paths) {
12493
- try {
12494
- const s = await stat9(p);
12495
- if (s.isFile())
12496
- candidates.push({ path: p, ageMs: now - s.mtimeMs });
12497
- } catch {}
12498
- }
12499
- return candidates;
12500
- }
12501
- async function sweepStaleLocks(gitDir, now = Date.now(), minAgeMs = STALE_LOCK_MIN_AGE_MS) {
12502
- const candidates = await findLockCandidates(gitDir, now);
12503
- if (candidates.length === 0)
12504
- return "clean";
12505
- if (candidates.some((c) => c.ageMs < minAgeMs))
12506
- return "fresh";
12507
- for (const c of candidates) {
12508
- await rm8(c.path, { force: true });
12509
- }
12510
- return "swept";
12511
- }
12512
-
12513
- // src/lib/remote-auth/recovery/inspect.ts
12514
- async function inspectRepo(ctx, opts = {}) {
12515
- let repoDir = ctx.repoDir;
12516
- try {
12517
- const source = ctx.source ?? await detectProjectSource(ctx.repoDir);
12518
- if (source.type === "local-git-folder")
12519
- repoDir = gitScopeFor(source);
12520
- } catch {}
12521
- const gitDir = gitDirFor(repoDir);
12522
- const hasGitDir = fs4.existsSync(gitDir);
12523
- if (!hasGitDir) {
12524
- return {
12525
- hasGitDir: false,
12526
- isDetachedHead: false,
12527
- headUnreadable: false,
12528
- hasStaleLock: false,
12529
- hasInterruptedMerge: false,
12530
- hasInterruptedRebase: false,
12531
- hasInterruptedCherryPick: false,
12532
- hasLocalChanges: false
12533
- };
12534
- }
12535
- let currentBranch;
12536
- let isDetachedHead = false;
12537
- let headUnreadable = false;
12538
- try {
12539
- const branch = await git2.currentBranch({ fs: fs4, dir: repoDir });
12540
- if (branch == null) {
12541
- isDetachedHead = true;
12542
- } else {
12543
- currentBranch = branch;
12544
- }
12545
- } catch {
12546
- headUnreadable = true;
12547
- }
12548
- const lockCandidates = await findLockCandidates(gitDir, Date.now());
12549
- const hasStaleLock = lockCandidates.length > 0;
12550
- const lockAgeMs = hasStaleLock ? Math.min(...lockCandidates.map((c) => c.ageMs)) : undefined;
12551
- const hasInterruptedMerge = fs4.existsSync(path23.join(gitDir, "MERGE_HEAD"));
12552
- const hasInterruptedRebase = fs4.existsSync(path23.join(gitDir, "rebase-merge")) || fs4.existsSync(path23.join(gitDir, "rebase-apply"));
12553
- const hasInterruptedCherryPick = fs4.existsSync(path23.join(gitDir, "CHERRY_PICK_HEAD"));
12554
- let hasLocalChanges = false;
12555
- if (opts.checkLocalChanges !== false) {
12556
- try {
12557
- hasLocalChanges = await hasPendingChanges(repoDir);
12558
- } catch {
12559
- hasLocalChanges = true;
12560
- }
12561
- }
12562
- return {
12563
- hasGitDir,
12564
- currentBranch,
12565
- isDetachedHead,
12566
- headUnreadable,
12567
- hasStaleLock,
12568
- lockAgeMs,
12569
- hasInterruptedMerge,
12570
- hasInterruptedRebase,
12571
- hasInterruptedCherryPick,
12572
- hasLocalChanges
12573
- };
12574
- }
12575
- async function verifyRepoReadable(dir) {
12576
- const headOid = await git2.resolveRef({ fs: fs4, dir, ref: "HEAD" });
12577
- const { commit } = await git2.readCommit({ fs: fs4, dir, oid: headOid });
12578
- await git2.readTree({ fs: fs4, dir, oid: commit.tree });
12579
- }
12580
- function isUnbornRepo(repoDir) {
12581
- const objectsDir = path23.join(gitDirFor(repoDir), "objects");
12582
- let entries;
12583
- try {
12584
- entries = fs4.readdirSync(objectsDir);
12585
- } catch {
12586
- return false;
12587
- }
12588
- for (const entry of entries) {
12589
- if (/^[0-9a-f]{2}$/.test(entry)) {
12590
- try {
12591
- if (fs4.readdirSync(path23.join(objectsDir, entry)).length > 0)
12592
- return false;
12593
- } catch {
12594
- return false;
12595
- }
12596
- } else if (entry === "pack") {
12597
- try {
12598
- if (fs4.readdirSync(path23.join(objectsDir, entry)).some((f) => f.endsWith(".pack"))) {
12599
- return false;
12600
- }
12601
- } catch {
12602
- return false;
12603
- }
12604
- }
12605
- }
12606
- return true;
12607
- }
12608
- function preflightStructuralReason(kind) {
12609
- switch (kind) {
12610
- case "stale_lock":
12611
- return "health.hasStaleLock";
12612
- case "needs_repair":
12613
- return "health.structural";
12614
- case null:
12615
- return "none";
12616
- }
12617
- }
12618
- function buildPreflightDiagnostics(openedDir, repoDir, health, kind) {
12619
- return {
12620
- openedDir,
12621
- repoDir,
12622
- repoRootDiffers: repoDir !== openedDir,
12623
- kind: kind ?? "none",
12624
- reason: preflightStructuralReason(kind),
12625
- hasGitDir: health.hasGitDir,
12626
- hasInterruptedMerge: health.hasInterruptedMerge,
12627
- hasInterruptedRebase: health.hasInterruptedRebase,
12628
- hasInterruptedCherryPick: health.hasInterruptedCherryPick,
12629
- hasStaleLock: health.hasStaleLock,
12630
- isDetachedHead: health.isDetachedHead,
12631
- hasLocalChanges: health.hasLocalChanges
12632
- };
12633
- }
12634
-
12635
- // src/lib/remote-auth/sync-messages.ts
12636
- var MSG_UP_TO_DATE = "Everything is in sync.";
12637
- var MSG_UP_TO_DATE_PULLED = "Everything is in sync. The latest online changes were downloaded to this computer.";
12638
- var MSG_SYNCED = "Your changes are online.";
12639
- var MSG_SYNCED_MERGED = "Your changes are online, combined with changes from the online copy.";
12640
- var MSG_OFFLINE = "Your changes are saved on this computer. gutterpress couldn't reach the online repository — try syncing again when you're back online.";
12641
- var MSG_AUTH = "The online repository didn't accept the saved connection. Reconnect and try again.";
12642
- var MSG_INSECURE_TRANSPORT = "This project's online address isn't secure, so the saved connection wasn't sent — connections are never sent over an insecure address. Switch the address to a secure one (starting with https), or to a local loopback address for a server on this computer, to sync.";
12643
- var MSG_BUSY = "The online copy is changing very quickly right now. Your work is saved on this computer — try Sync again in a moment.";
12644
- var MSG_UNRELATED = "The online address points at a different project's files, so the two can't be combined. Check the project's online address.";
12645
- var MSG_NO_REMOTE = "This project isn't connected to an online repository yet.";
12646
- 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.";
12647
- var MSG_NO_BRANCH = "This project's version history isn't on a named branch, so it can't be synced right now.";
12648
- var MSG_PULLED = "The latest online changes were downloaded to this computer.";
12649
- var MSG_PULLED_MERGED = "The latest online changes were combined with your changes on this computer.";
12650
- var MSG_PULL_UP_TO_DATE = "You already have the latest online changes.";
12651
- var MSG_PUSH_UP_TO_DATE = "There's nothing new to send — everything is already online.";
12652
- var MSG_PULL_FIRST = "The online copy has changes you don't have yet. Get the latest changes first, then send yours.";
12653
- var SYNC_SNAPSHOT_MESSAGE = "Snapshot before syncing";
12654
-
12655
- // src/lib/remote-auth/transport.ts
12656
- async function repoDirFor(projectDir) {
12657
- const source = await detectProjectSource(projectDir);
12658
- if (source.type === "local-git-folder")
12659
- return gitScopeFor(source);
12660
- return projectDir;
12661
- }
12662
- function isCredentialTransmissionSafe(url) {
12663
- try {
12664
- const u = new URL(url);
12665
- if (u.protocol === "https:")
12666
- return true;
12667
- if (u.protocol === "http:") {
12668
- const host = u.hostname.toLowerCase();
12669
- return host === "127.0.0.1" || host === "[::1]" || host === "localhost";
12670
- }
12671
- return false;
12672
- } catch {
12673
- return false;
12674
- }
12675
- }
12676
- function onAuthFor(credential) {
12677
- if (!credential)
12678
- return {};
12679
- return {
12680
- onAuth: (url) => {
12681
- if (!isCredentialTransmissionSafe(url))
12682
- throw new InsecureTransportError;
12683
- return {
12684
- username: credential.kind === "github-oauth" ? "x-access-token" : credential.username || credential.token,
12685
- password: credential.token
12686
- };
12687
- }
12688
- };
12689
- }
12690
- async function resolveTransport(dir, options) {
12691
- const origin = await syncRemoteFor(dir);
12692
- if (!origin?.url)
12693
- throw new Error(MSG_NO_REMOTE);
12694
- const { cleanUrl, credential: urlCredential } = extractUrlCredential(origin.url);
12695
- let parsed;
12696
- try {
12697
- parsed = new URL(cleanUrl);
12698
- } catch {
12699
- throw new Error(MSG_SSH_REMOTE);
12700
- }
12701
- if (!/^https?:$/.test(parsed.protocol))
12702
- throw new Error(MSG_SSH_REMOTE);
12703
- const host = credentialHostKey(cleanUrl);
12704
- let credential = options.credential ?? undefined;
12705
- if (!credential && options.tokenStore) {
12706
- try {
12707
- credential = await options.tokenStore.get(host) ?? undefined;
12708
- } catch {
12709
- credential = undefined;
12710
- }
12711
- }
12712
- if (!credential && urlCredential)
12713
- credential = urlCredential;
12714
- return {
12715
- remote: origin.remote,
12716
- url: cleanUrl,
12717
- host,
12718
- ...credential ? { credential } : {}
12719
- };
12720
- }
12721
12498
  function failureOutcome(e, snapshotId) {
12722
12499
  const kind = classifyTransportFailure(e);
12723
12500
  const base = snapshotId ? { snapshotId } : {};
@@ -12743,7 +12520,7 @@ function setupErrorMessage(e) {
12743
12520
  async function snapshotBeforeAction(args) {
12744
12521
  const { projectDir, dir, cache: cache2 } = args;
12745
12522
  const hasChanges = await hasPendingChanges(dir, cache2);
12746
- const staleStaging = fs5.existsSync(snapshotStagingMarkerPath(dir));
12523
+ const staleStaging = gitFs.existsSync(snapshotStagingMarkerPath(dir));
12747
12524
  if (!hasChanges && !staleStaging)
12748
12525
  return;
12749
12526
  const snap = await snapshotWorkingTreeUnlocked({
@@ -12751,70 +12528,49 @@ async function snapshotBeforeAction(args) {
12751
12528
  repoRoot: dir,
12752
12529
  message: args.message?.trim() || SYNC_SNAPSHOT_MESSAGE,
12753
12530
  authorName: args.authorName,
12754
- authorEmail: args.authorEmail
12531
+ authorEmail: args.authorEmail,
12532
+ cache: cache2
12755
12533
  });
12756
12534
  return snap.id;
12757
12535
  }
12758
- async function assertNoStructuralDamage(projectDir, logger) {
12759
- const health = await inspectRepo({ repoDir: projectDir }, { checkLocalChanges: false });
12760
- const structural = classifyFromHealth(health);
12761
- if (structural) {
12762
- logger.warn("sync", "structural preflight blocked sync", { kind: structural });
12763
- throw new RepoNeedsRecoveryError(structural);
12764
- }
12765
- }
12766
12536
  async function currentBranchOrThrow(dir) {
12767
- const branch = await git3.currentBranch({ fs: fs5, dir });
12537
+ const branch = await git2.currentBranch({ fs: gitFs, dir });
12768
12538
  if (!branch)
12769
12539
  throw new Error(MSG_NO_BRANCH);
12770
12540
  return branch;
12771
12541
  }
12772
12542
  async function resolveRefOrNull(dir, ref) {
12773
12543
  try {
12774
- return await git3.resolveRef({ fs: fs5, dir, ref });
12544
+ return await git2.resolveRef({ fs: gitFs, dir, ref });
12775
12545
  } catch {
12776
12546
  return null;
12777
12547
  }
12778
12548
  }
12779
- async function guardRefs(dir, listRefs, cache2, fn) {
12780
- const before = new Map;
12781
- for (const ref of await listRefs().catch(() => [])) {
12782
- before.set(ref, await resolveRefOrNull(dir, ref));
12783
- }
12549
+ async function guardTrackingRef(dir, ref, cache2, fn) {
12550
+ const before = await resolveRefOrNull(dir, ref);
12784
12551
  try {
12785
12552
  return await fn();
12786
12553
  } catch (e) {
12787
12554
  try {
12788
- const relisted = await listRefs().catch(() => []);
12789
- const refs = new Set([...before.keys(), ...relisted]);
12790
- for (const ref of refs) {
12791
- try {
12792
- const prev = before.get(ref) ?? null;
12793
- const after = await resolveRefOrNull(dir, ref);
12794
- if (!after || after === prev)
12795
- continue;
12796
- const landed = await git3.readObject({ fs: fs5, dir, oid: after, cache: cache2 }).then(() => true, () => false);
12797
- if (landed)
12798
- continue;
12799
- if (prev) {
12800
- await git3.writeRef({ fs: fs5, dir, ref, value: prev, force: true });
12801
- } else {
12802
- await git3.deleteRef({ fs: fs5, dir, ref });
12803
- }
12804
- } catch {}
12555
+ const after = await resolveRefOrNull(dir, ref);
12556
+ if (after && after !== before) {
12557
+ const landed = await git2.readObject({ fs: gitFs, dir, oid: after, cache: cache2 }).then(() => true, () => false);
12558
+ if (!landed) {
12559
+ if (before)
12560
+ await git2.writeRef({ fs: gitFs, dir, ref, value: before, force: true });
12561
+ else
12562
+ await git2.deleteRef({ fs: gitFs, dir, ref });
12563
+ }
12805
12564
  }
12806
12565
  } catch {}
12807
12566
  throw e;
12808
12567
  }
12809
12568
  }
12810
- async function guardTrackingRef(dir, ref, cache2, fn) {
12811
- return guardRefs(dir, async () => [ref], cache2, fn);
12812
- }
12813
12569
  async function fetchRemoteTip(dir, branch, transport, http2, cache2) {
12814
12570
  try {
12815
12571
  const trackingRef = `refs/remotes/${transport.remote}/${branch}`;
12816
- const result = await guardTrackingRef(dir, trackingRef, cache2, () => git3.fetch({
12817
- fs: fs5,
12572
+ const result = await guardTrackingRef(dir, trackingRef, cache2, () => git2.fetch({
12573
+ fs: gitFs,
12818
12574
  http: http2,
12819
12575
  dir,
12820
12576
  cache: cache2,
@@ -12844,25 +12600,12 @@ function short(oid) {
12844
12600
  }
12845
12601
 
12846
12602
  // src/lib/remote-auth/clone.ts
12847
- var PROVENANCE_FILE = "gutterpress-remote.json";
12848
- function provenancePath(projectDir) {
12849
- return path24.join(projectDir, ".git", PROVENANCE_FILE);
12850
- }
12851
- async function readProjectProvenance(projectDir) {
12852
- try {
12853
- const raw = await readFile31(provenancePath(projectDir), "utf8");
12854
- const parsed = JSON.parse(raw);
12855
- return parsed && parsed.provider ? parsed : null;
12856
- } catch {
12857
- return null;
12858
- }
12859
- }
12860
12603
  function sanitizeCloneFolderName(name) {
12861
12604
  return String(name ?? "").replace(/[\\/]/g, "-").replace(/^\.+/, "").trim();
12862
12605
  }
12863
12606
  async function assertCloneTarget(dir) {
12864
12607
  try {
12865
- const entries = await readdir11(dir);
12608
+ const entries = await readdir10(dir);
12866
12609
  if (entries.length > 0) {
12867
12610
  throw new Error("That folder already has files in it. Choose an empty folder for the project.");
12868
12611
  }
@@ -12892,8 +12635,8 @@ function friendlyCloneError(e) {
12892
12635
  return new Error("The project couldn't be downloaded from GitHub. Please try again.", { cause: e });
12893
12636
  }
12894
12637
  async function cloneRepository(options) {
12895
- const { dir, onProgress, tokenStore, provenance } = options;
12896
- if (!path24.isAbsolute(dir)) {
12638
+ const { dir, onProgress, tokenStore } = options;
12639
+ if (!path22.isAbsolute(dir)) {
12897
12640
  throw new Error("cloneRepository requires an absolute destination path.");
12898
12641
  }
12899
12642
  let parsed;
@@ -12912,11 +12655,11 @@ async function cloneRepository(options) {
12912
12655
  }
12913
12656
  return withRepoLock(dir, async () => {
12914
12657
  await assertCloneTarget(dir);
12915
- const dirExistedBefore = fs6.existsSync(dir);
12658
+ const dirExistedBefore = gitFs.existsSync(dir);
12916
12659
  await mkdir14(dir, { recursive: true });
12917
12660
  try {
12918
- await git4.clone({
12919
- fs: fs6,
12661
+ await git3.clone({
12662
+ fs: gitFs,
12920
12663
  http: options.httpClient ?? defaultGitHttp,
12921
12664
  dir,
12922
12665
  url: cleanUrl,
@@ -12936,21 +12679,16 @@ async function cloneRepository(options) {
12936
12679
  const friendly = friendlyCloneError(e);
12937
12680
  if (!dirExistedBefore) {
12938
12681
  try {
12939
- await rm9(dir, { recursive: true, force: true });
12682
+ await rm8(dir, { recursive: true, force: true });
12940
12683
  } catch {
12941
12684
  throw new Error(`${friendly.message} A partial download was left at ${dir} — delete that folder before trying again.`, { cause: e });
12942
12685
  }
12943
12686
  }
12944
12687
  throw friendly;
12945
12688
  }
12946
- if (provenance) {
12947
- try {
12948
- await writeFile13(provenancePath(dir), JSON.stringify(provenance, null, 2), "utf8");
12949
- } catch {}
12950
- }
12951
12689
  let branch;
12952
12690
  try {
12953
- branch = await git4.currentBranch({ fs: fs6, dir }) ?? undefined;
12691
+ branch = await git3.currentBranch({ fs: gitFs, dir }) ?? undefined;
12954
12692
  } catch {
12955
12693
  branch = undefined;
12956
12694
  }
@@ -12958,7 +12696,7 @@ async function cloneRepository(options) {
12958
12696
  });
12959
12697
  }
12960
12698
  // src/lib/remote-auth/test-access.ts
12961
- import git5 from "isomorphic-git";
12699
+ import git4 from "isomorphic-git";
12962
12700
  import httpNode2 from "isomorphic-git/http/node";
12963
12701
  function isSshRemoteUrl(url) {
12964
12702
  const trimmed = url.trim();
@@ -13014,7 +12752,7 @@ async function testRemoteAccess(options) {
13014
12752
  });
13015
12753
  const probe = (async () => {
13016
12754
  try {
13017
- const refs = await git5.listServerRefs({
12755
+ const refs = await git4.listServerRefs({
13018
12756
  http: options.httpClient ?? httpNode2,
13019
12757
  url: cleanUrl,
13020
12758
  protocolVersion: 1,
@@ -13113,28 +12851,6 @@ async function connectGenericHost(input, options = {}) {
13113
12851
  }
13114
12852
  return candidate;
13115
12853
  }
13116
-
13117
- class GenericTokenAuthProvider {
13118
- options;
13119
- constructor(options = {}) {
13120
- this.options = options;
13121
- }
13122
- matches(origin) {
13123
- return /^https?:$/.test(origin.protocol) && origin.hostname.toLowerCase() !== GITHUB_HOST;
13124
- }
13125
- connect(callbacks) {
13126
- return connectGenericHost(callbacks, this.options);
13127
- }
13128
- async validate(credential) {
13129
- const result = await testRemoteAccess({
13130
- url: `https://${credential.host}/`,
13131
- credential,
13132
- ...this.options.httpClient ? { httpClient: this.options.httpClient } : {},
13133
- ...this.options.timeoutMs ? { timeoutMs: this.options.timeoutMs } : {}
13134
- });
13135
- return result.ok || result.reason !== "auth";
13136
- }
13137
- }
13138
12854
  // src/lib/remote-auth/diagnose.ts
13139
12855
  function parseRemoteOrigin(url) {
13140
12856
  const trimmed = url.trim();
@@ -13229,19 +12945,23 @@ async function diagnoseProjectRemote(projectDir, options = {}) {
13229
12945
  };
13230
12946
  }
13231
12947
  // src/lib/remote-auth/sync.ts
13232
- import * as fs8 from "node:fs";
13233
- import git7 from "isomorphic-git";
12948
+ import git6 from "isomorphic-git";
13234
12949
 
13235
12950
  // src/lib/remote-auth/converge-merge.ts
13236
- import * as fs7 from "node:fs";
13237
- import { mkdir as mkdir15, unlink as unlink4, writeFile as writeFile14 } from "node:fs/promises";
13238
- import path25 from "node:path";
13239
- import git6 from "isomorphic-git";
12951
+ import { mkdir as mkdir15, unlink as unlink3, writeFile as writeFile12 } from "node:fs/promises";
12952
+ import path23 from "node:path";
12953
+ import git5 from "isomorphic-git";
13240
12954
  import diff3Merge from "diff3";
12955
+ function isMergeConflictError(e) {
12956
+ return e?.code === "MergeConflictError";
12957
+ }
12958
+ function isCheckoutConflict(e) {
12959
+ return e?.code === "CheckoutConflictError";
12960
+ }
13241
12961
  var CONVERGE_PREPARE_MESSAGE = "Getting your changes ready to combine with the online version";
13242
- var CONVERGE_RESTORE_MESSAGE = "Kept the newest version of files that can't be combined";
12962
+ var CONVERGE_RESTORE_MESSAGE = "Kept both versions of the files that can't be combined";
13243
12963
  var CONVERGE_MERGE_MESSAGE = "Combined your changes with the online version";
13244
- var IMAGE_EXT = /\.(png|jpe?g|gif|webp|svg|avif|bmp|ico)$/i;
12964
+ var MARKER_UNSAFE_EXT = /\.svg$/i;
13245
12965
  var OUR_LABEL = "your version";
13246
12966
  var THEIR_LABEL = "online version";
13247
12967
  function stringLooksBinary(s) {
@@ -13269,31 +12989,31 @@ function mergeWithMarkers(baseContent, ourContent, theirContent) {
13269
12989
  }
13270
12990
  async function readBlobOrNull(dir, cache2, oid, filepath) {
13271
12991
  try {
13272
- const r = await git6.readBlob({ fs: fs7, dir, cache: cache2, oid, filepath });
12992
+ const r = await git5.readBlob({ fs: gitFs, dir, cache: cache2, oid, filepath });
13273
12993
  return { oid: r.oid, blob: r.blob };
13274
12994
  } catch {
13275
12995
  return null;
13276
12996
  }
13277
12997
  }
13278
12998
  async function writeTreeFile(dir, filepath, content) {
13279
- const abs = path25.join(dir, filepath);
13280
- await mkdir15(path25.dirname(abs), { recursive: true });
13281
- await writeFile14(abs, content);
12999
+ const abs = path23.join(dir, filepath);
13000
+ await mkdir15(path23.dirname(abs), { recursive: true });
13001
+ await writeFile12(abs, content);
13282
13002
  }
13283
13003
  async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
13284
- const bases = await git6.findMergeBase({ fs: fs7, dir, cache: cache2, oids: [ourTip, theirTip] });
13004
+ const bases = await git5.findMergeBase({ fs: gitFs, dir, cache: cache2, oids: [ourTip, theirTip] });
13285
13005
  const base = bases[0];
13286
13006
  if (!base && !allowUnrelated)
13287
13007
  return null;
13288
13008
  const out = [];
13289
- await git6.walk({
13290
- fs: fs7,
13009
+ await git5.walk({
13010
+ fs: gitFs,
13291
13011
  dir,
13292
13012
  cache: cache2,
13293
13013
  trees: [
13294
- git6.TREE({ ref: ourTip }),
13295
- git6.TREE({ ref: theirTip }),
13296
- ...base ? [git6.TREE({ ref: base })] : []
13014
+ git5.TREE({ ref: ourTip }),
13015
+ git5.TREE({ ref: theirTip }),
13016
+ ...base ? [git5.TREE({ ref: base })] : []
13297
13017
  ],
13298
13018
  map: async (filepath, entries) => {
13299
13019
  if (filepath === ".")
@@ -13314,44 +13034,44 @@ async function bothAddedPaths(dir, cache2, ourTip, theirTip, allowUnrelated) {
13314
13034
  }
13315
13035
  async function convergeMerge(params) {
13316
13036
  const { dir, cache: cache2, branch, theirs, author } = params;
13317
- const ourTip = await git6.resolveRef({ fs: fs7, dir, ref: branch });
13318
- const [ourCommit, theirCommit] = await Promise.all([
13319
- git6.readCommit({ fs: fs7, dir, cache: cache2, oid: ourTip }),
13320
- git6.readCommit({ fs: fs7, dir, cache: cache2, oid: theirs })
13321
- ]);
13322
- const localNewer = ourCommit.commit.committer.timestamp >= theirCommit.commit.committer.timestamp;
13323
- const combined = new Set;
13324
- const driverBinary = new Set;
13325
- const imageClashes = [];
13326
- const attempt = () => git6.merge({
13327
- fs: fs7,
13328
- dir,
13329
- cache: cache2,
13330
- ours: branch,
13331
- theirs,
13332
- author,
13333
- message: CONVERGE_MERGE_MESSAGE,
13334
- allowUnrelatedHistories: params.allowUnrelatedHistories ?? false,
13335
- mergeDriver: ({ contents, path: filepath }) => {
13336
- const [base, ours, theirsContent] = contents;
13337
- if (IMAGE_EXT.test(filepath) || stringLooksBinary(ours) || stringLooksBinary(theirsContent) || stringLooksBinary(base)) {
13338
- driverBinary.add(filepath);
13339
- return { cleanMerge: false, mergedText: ours };
13340
- }
13341
- combined.add(filepath);
13342
- return {
13343
- cleanMerge: true,
13344
- mergedText: mergeWithMarkers(base, ours, theirsContent)
13345
- };
13346
- }
13347
- });
13348
13037
  const snapshot = (message) => snapshotWorkingTreeUnlocked({
13349
13038
  projectDir: dir,
13350
13039
  repoRoot: dir,
13351
13040
  message,
13352
13041
  authorName: params.authorName,
13353
- authorEmail: params.authorEmail
13042
+ authorEmail: params.authorEmail,
13043
+ cache: cache2
13354
13044
  });
13045
+ const ourTip = await git5.resolveRef({ fs: gitFs, dir, ref: branch });
13046
+ const combined = new Set;
13047
+ const driverBinary = new Set;
13048
+ const keptBothFiles = [];
13049
+ let tipBeforeMerge = ourTip;
13050
+ const attempt = async () => {
13051
+ tipBeforeMerge = await git5.resolveRef({ fs: gitFs, dir, ref: branch });
13052
+ return git5.merge({
13053
+ fs: gitFs,
13054
+ dir,
13055
+ cache: cache2,
13056
+ ours: branch,
13057
+ theirs,
13058
+ author,
13059
+ message: CONVERGE_MERGE_MESSAGE,
13060
+ allowUnrelatedHistories: false,
13061
+ mergeDriver: ({ contents, path: filepath }) => {
13062
+ const [base, ours, theirsContent] = contents;
13063
+ if (MARKER_UNSAFE_EXT.test(filepath) || stringLooksBinary(ours) || stringLooksBinary(theirsContent) || stringLooksBinary(base)) {
13064
+ driverBinary.add(filepath);
13065
+ return { cleanMerge: false, mergedText: ours };
13066
+ }
13067
+ combined.add(filepath);
13068
+ return {
13069
+ cleanMerge: true,
13070
+ mergedText: mergeWithMarkers(base, ours, theirsContent)
13071
+ };
13072
+ }
13073
+ });
13074
+ };
13355
13075
  const postRestore = [];
13356
13076
  const equalize = async (paths) => {
13357
13077
  for (const p of paths.binary) {
@@ -13362,16 +13082,10 @@ async function convergeMerge(params) {
13362
13082
  if (!ourBlob || !theirBlob)
13363
13083
  continue;
13364
13084
  await writeTreeFile(dir, p, theirBlob.blob);
13365
- if (localNewer)
13366
- postRestore.push({ path: p, bytes: ourBlob.blob });
13367
- if (IMAGE_EXT.test(p)) {
13368
- imageClashes.push({
13369
- path: p,
13370
- localOid: ourBlob.oid,
13371
- remoteOid: theirBlob.oid,
13372
- kept: localNewer ? "local" : "online"
13373
- });
13374
- }
13085
+ const onlinePath = onlineSiblingPath(p);
13086
+ postRestore.push({ path: p, bytes: ourBlob.blob });
13087
+ postRestore.push({ path: onlinePath, bytes: theirBlob.blob });
13088
+ keptBothFiles.push({ path: p, onlinePath });
13375
13089
  }
13376
13090
  for (const p of paths.deleteByUs) {
13377
13091
  const theirBlob = await readBlobOrNull(dir, cache2, theirs, p);
@@ -13380,7 +13094,7 @@ async function convergeMerge(params) {
13380
13094
  }
13381
13095
  for (const p of paths.deleteByTheirs) {
13382
13096
  const ourBlob = await readBlobOrNull(dir, cache2, ourTip, p);
13383
- await unlink4(path25.join(dir, p)).catch((e) => {
13097
+ await unlink3(path23.join(dir, p)).catch((e) => {
13384
13098
  if (e?.code !== "ENOENT")
13385
13099
  throw e;
13386
13100
  });
@@ -13401,7 +13115,7 @@ async function convergeMerge(params) {
13401
13115
  });
13402
13116
  await attempt();
13403
13117
  } else if (e?.code === "MergeNotSupportedError") {
13404
- const added = await bothAddedPaths(dir, cache2, ourTip, theirs, params.allowUnrelatedHistories ?? false);
13118
+ const added = await bothAddedPaths(dir, cache2, ourTip, theirs, false);
13405
13119
  if (added === null || added.length === 0)
13406
13120
  throw e;
13407
13121
  await equalize({ binary: added, deleteByUs: [], deleteByTheirs: [] });
@@ -13410,7 +13124,20 @@ async function convergeMerge(params) {
13410
13124
  throw e;
13411
13125
  }
13412
13126
  }
13413
- await git6.checkout({ fs: fs7, dir, cache: cache2, ref: branch, force: true });
13127
+ try {
13128
+ await git5.checkout({ fs: gitFs, dir, cache: cache2, ref: branch, force: false });
13129
+ } catch (e) {
13130
+ if (!isCheckoutConflict(e))
13131
+ throw e;
13132
+ await git5.writeRef({
13133
+ fs: gitFs,
13134
+ dir,
13135
+ ref: await git5.expandRef({ fs: gitFs, dir, ref: branch }),
13136
+ value: tipBeforeMerge,
13137
+ force: true
13138
+ });
13139
+ throw e;
13140
+ }
13414
13141
  if (postRestore.length > 0) {
13415
13142
  for (const r of postRestore)
13416
13143
  await writeTreeFile(dir, r.path, r.bytes);
@@ -13418,12 +13145,39 @@ async function convergeMerge(params) {
13418
13145
  await snapshot(CONVERGE_RESTORE_MESSAGE);
13419
13146
  }
13420
13147
  return {
13421
- oid: await git6.resolveRef({ fs: fs7, dir, ref: branch }),
13148
+ oid: await git5.resolveRef({ fs: gitFs, dir, ref: branch }),
13422
13149
  combinedFiles: [...combined].sort(),
13423
- imageClashes
13150
+ keptBothFiles
13424
13151
  };
13425
13152
  }
13426
13153
  // src/lib/remote-auth/sync.ts
13154
+ function isPushRejected(e) {
13155
+ const code = e?.code;
13156
+ if (code === "PushRejectedError") {
13157
+ const reason = e?.data?.reason;
13158
+ return reason === undefined || reason === "not-fast-forward";
13159
+ }
13160
+ if (code === "GitPushError") {
13161
+ const msg = (e?.data?.prettyDetails ?? "") + " " + (e?.message ?? "");
13162
+ return /non-fast-forward|would not be a fast-forward|not a simple fast-forward/i.test(msg);
13163
+ }
13164
+ return false;
13165
+ }
13166
+ function isUnrelatedHistories(e) {
13167
+ const code = e?.code;
13168
+ const msg = e?.message ?? String(e);
13169
+ return code === "MergeNotSupportedError" || /unrelated histories|no common commits|refusing to merge unrelated/i.test(msg);
13170
+ }
13171
+ async function historyUnreadable(dir) {
13172
+ try {
13173
+ const oid = await git6.resolveRef({ fs: gitFs, dir, ref: "HEAD" });
13174
+ await git6.readCommit({ fs: gitFs, dir, oid });
13175
+ await git6.listFiles({ fs: gitFs, dir });
13176
+ return false;
13177
+ } catch {
13178
+ return true;
13179
+ }
13180
+ }
13427
13181
  var DEFAULT_SYNC_RETRY = {
13428
13182
  attempts: 3,
13429
13183
  backoffMs: 150
@@ -13432,254 +13186,157 @@ function defaultSleep2(ms) {
13432
13186
  return new Promise((resolve9) => setTimeout(resolve9, ms));
13433
13187
  }
13434
13188
  async function syncProject(options) {
13435
- const logger = resolveLogger(options.logFile, "sync");
13189
+ const http2 = options.httpClient ?? defaultGitHttp;
13190
+ const push = options.push ?? true;
13436
13191
  const attempts = Math.max(1, options.retry?.attempts ?? DEFAULT_SYNC_RETRY.attempts);
13437
13192
  const backoffMs = Math.max(0, options.retry?.backoffMs ?? DEFAULT_SYNC_RETRY.backoffMs);
13438
13193
  const sleep = options.retry?.sleep ?? defaultSleep2;
13439
- let snapshotId;
13440
- let pulled = false;
13441
- let filesChanged = false;
13442
- const combinedFiles = new Set;
13443
- const imageClashes = [];
13444
- const convergeExtras = () => ({
13445
- ...combinedFiles.size > 0 ? { combinedFiles: [...combinedFiles].sort() } : {},
13446
- ...imageClashes.length > 0 ? { imageClashes } : {}
13447
- });
13448
- for (let attempt = 0;attempt < attempts; attempt++) {
13449
- logger.info("sync", `sync pass ${attempt + 1}/${attempts}`);
13450
- const pull = await pullChanges(options);
13451
- snapshotId = snapshotId ?? pull.snapshotId;
13452
- const base = snapshotId ? { snapshotId } : {};
13453
- if (pull.status !== "pulled" && pull.status !== "up-to-date") {
13454
- logger.warn("sync", `pull non-success`, { status: pull.status });
13455
- return { ...pull, ...base };
13456
- }
13457
- pulled = pulled || pull.status === "pulled";
13458
- filesChanged = filesChanged || pull.status === "pulled" && pull.filesChanged;
13459
- if (pull.status === "pulled") {
13460
- for (const f of pull.combinedFiles ?? [])
13461
- combinedFiles.add(f);
13462
- imageClashes.push(...pull.imageClashes ?? []);
13463
- }
13464
- const push = await pushChanges(options);
13465
- snapshotId = snapshotId ?? push.snapshotId;
13466
- switch (push.status) {
13467
- case "pushed":
13468
- logger.info("sync", `synced`, { pulled });
13469
- return {
13470
- status: "synced",
13471
- message: pulled ? MSG_SYNCED_MERGED : MSG_SYNCED,
13472
- mergedRemoteChanges: pulled,
13473
- ...filesChanged ? { filesChanged: true } : {},
13474
- ...convergeExtras(),
13475
- ...snapshotId ? { snapshotId } : {}
13476
- };
13477
- case "up-to-date":
13478
- logger.info("sync", `up-to-date`, { pulled });
13479
- return {
13480
- status: "up-to-date",
13481
- message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
13482
- ...filesChanged ? { filesChanged: true } : {},
13483
- ...snapshotId ? { snapshotId } : {}
13484
- };
13485
- case "pull-first":
13486
- logger.info("sync", `push rejected (non-fast-forward) — retrying`);
13487
- if (attempt < attempts - 1 && backoffMs > 0)
13488
- await sleep(backoffMs);
13489
- continue;
13490
- default:
13491
- logger.warn("sync", `push non-success`, { status: push.status });
13492
- return {
13493
- ...push,
13494
- ...filesChanged ? { filesChanged: true } : {},
13495
- ...snapshotId ? { snapshotId } : {}
13496
- };
13497
- }
13498
- }
13499
- logger.error("sync", `exhausted ${attempts} retry attempts (race)`);
13500
- return {
13501
- status: "error",
13502
- message: MSG_BUSY,
13503
- ...filesChanged ? { filesChanged: true } : {},
13504
- ...convergeExtras(),
13505
- ...snapshotId ? { snapshotId } : {}
13506
- };
13507
- }
13508
- async function pullChanges(options) {
13509
- const http2 = options.httpClient ?? defaultGitHttp;
13510
13194
  const dir = await repoDirFor(options.projectDir);
13511
13195
  return withRepoLock(dir, async () => {
13512
13196
  const logger = resolveLogger(options.logFile, "sync");
13513
- await assertNoStructuralDamage(options.projectDir, logger);
13514
13197
  const cache2 = {};
13515
13198
  let snapshotId;
13199
+ let pulled = false;
13200
+ let filesChanged = false;
13201
+ const combinedFiles = new Set;
13202
+ const keptBothFiles = [];
13203
+ const base = () => ({
13204
+ ...filesChanged ? { filesChanged: true } : {},
13205
+ ...snapshotId ? { snapshotId } : {}
13206
+ });
13207
+ const convergeExtras = () => ({
13208
+ ...combinedFiles.size > 0 ? { combinedFiles: [...combinedFiles].sort() } : {},
13209
+ ...keptBothFiles.length > 0 ? { keptBothFiles } : {}
13210
+ });
13516
13211
  try {
13517
13212
  const branch = await currentBranchOrThrow(dir);
13518
13213
  const transport = await resolveTransport(dir, options);
13519
- snapshotId = await snapshotBeforeAction({
13520
- projectDir: options.projectDir,
13521
- dir,
13522
- message: options.message,
13523
- authorName: options.authorName,
13524
- authorEmail: options.authorEmail,
13525
- cache: cache2
13526
- });
13527
- const remoteTip = await fetchRemoteTip(dir, branch, transport, http2, cache2);
13528
- const localTip = await git7.resolveRef({ fs: fs8, dir, ref: branch });
13529
- const base = snapshotId ? { snapshotId } : {};
13530
- logger.info("pull", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)}`);
13531
- if (!remoteTip || remoteTip === localTip) {
13532
- return { status: "up-to-date", message: MSG_PULL_UP_TO_DATE, ...base };
13533
- }
13534
- let converge;
13535
- try {
13536
- converge = await convergeMerge({
13214
+ if (push) {
13215
+ snapshotId = await snapshotBeforeAction({
13216
+ projectDir: options.projectDir,
13537
13217
  dir,
13538
- cache: cache2,
13539
- branch,
13540
- theirs: remoteTip,
13541
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
13218
+ message: options.message,
13542
13219
  authorName: options.authorName,
13543
- authorEmail: options.authorEmail
13220
+ authorEmail: options.authorEmail,
13221
+ cache: cache2
13544
13222
  });
13545
- } catch (e) {
13546
- if (isUnrelatedHistories(e)) {
13547
- logger.warn("pull", "unrelated histories — refusing to combine");
13548
- return { status: "error", message: MSG_UNRELATED, ...base };
13549
- }
13550
- throw e;
13551
13223
  }
13552
- const newTip = converge.oid;
13553
- if (newTip === localTip) {
13554
- return { status: "up-to-date", message: MSG_PULL_UP_TO_DATE, ...base };
13555
- }
13556
- if (converge.combinedFiles.length > 0) {
13557
- logger.info("pull", `combined with markers`, { files: converge.combinedFiles });
13558
- }
13559
- const merged = newTip !== remoteTip;
13560
- const treeBefore = (await git7.readCommit({ fs: fs8, dir, cache: cache2, oid: localTip })).commit.tree;
13561
- const treeAfter = (await git7.readCommit({ fs: fs8, dir, cache: cache2, oid: newTip })).commit.tree;
13562
- return {
13563
- status: "pulled",
13564
- message: merged ? MSG_PULLED_MERGED : MSG_PULLED,
13565
- merged,
13566
- filesChanged: treeBefore !== treeAfter,
13567
- ...converge.combinedFiles.length > 0 ? { combinedFiles: converge.combinedFiles } : {},
13568
- ...converge.imageClashes.length > 0 ? { imageClashes: converge.imageClashes } : {},
13569
- ...base
13570
- };
13571
- } catch (e) {
13572
- const setupMsg = setupErrorMessage(e);
13573
- if (setupMsg) {
13224
+ for (let attempt = 0;attempt < attempts; attempt++) {
13225
+ logger.info("sync", `sync pass ${attempt + 1}/${attempts}`);
13226
+ const remoteTip = await fetchRemoteTip(dir, branch, transport, http2, cache2);
13227
+ let localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
13228
+ if (push || remoteTip !== null && remoteTip !== localTip) {
13229
+ const lateSnapshot = await snapshotBeforeAction({
13230
+ projectDir: options.projectDir,
13231
+ dir,
13232
+ message: SYNC_LATE_EDIT_MESSAGE,
13233
+ authorName: options.authorName,
13234
+ authorEmail: options.authorEmail,
13235
+ cache: cache2
13236
+ });
13237
+ if (lateSnapshot) {
13238
+ snapshotId = lateSnapshot;
13239
+ localTip = await git6.resolveRef({ fs: gitFs, dir, ref: branch });
13240
+ }
13241
+ }
13242
+ logger.info("sync", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
13243
+ let tip = localTip;
13244
+ if (remoteTip && remoteTip !== localTip) {
13245
+ let converge;
13246
+ try {
13247
+ converge = await convergeMerge({
13248
+ dir,
13249
+ cache: cache2,
13250
+ branch,
13251
+ theirs: remoteTip,
13252
+ author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
13253
+ authorName: options.authorName,
13254
+ authorEmail: options.authorEmail
13255
+ });
13256
+ } catch (e) {
13257
+ if (isUnrelatedHistories(e)) {
13258
+ logger.warn("sync", "unrelated histories — refusing to combine");
13259
+ return { status: "error", message: MSG_UNRELATED, ...base() };
13260
+ }
13261
+ throw e;
13262
+ }
13263
+ tip = converge.oid;
13264
+ if (tip !== localTip) {
13265
+ pulled = true;
13266
+ const [before, after] = await Promise.all([
13267
+ git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: localTip }),
13268
+ git6.readCommit({ fs: gitFs, dir, cache: cache2, oid: tip })
13269
+ ]);
13270
+ filesChanged = filesChanged || before.commit.tree !== after.commit.tree;
13271
+ for (const f of converge.combinedFiles)
13272
+ combinedFiles.add(f);
13273
+ keptBothFiles.push(...converge.keptBothFiles);
13274
+ if (converge.combinedFiles.length > 0) {
13275
+ logger.info("sync", `combined with markers`, { files: converge.combinedFiles });
13276
+ }
13277
+ }
13278
+ }
13279
+ if (tip === remoteTip) {
13280
+ logger.info("sync", `up-to-date`, { pulled });
13281
+ return {
13282
+ status: "up-to-date",
13283
+ message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
13284
+ ...base()
13285
+ };
13286
+ }
13287
+ if (!push) {
13288
+ logger.info("sync", `pull-only pass complete — push deferred`, { pulled });
13289
+ return {
13290
+ status: "up-to-date",
13291
+ message: pulled ? MSG_UP_TO_DATE_PULLED : MSG_UP_TO_DATE,
13292
+ ...convergeExtras(),
13293
+ ...base()
13294
+ };
13295
+ }
13296
+ try {
13297
+ await git6.push({
13298
+ fs: gitFs,
13299
+ http: http2,
13300
+ dir,
13301
+ cache: cache2,
13302
+ remote: transport.remote,
13303
+ ref: branch,
13304
+ ...onAuthFor(transport.credential)
13305
+ });
13306
+ } catch (e) {
13307
+ if (!isPushRejected(e))
13308
+ throw e;
13309
+ logger.info("sync", `push rejected (non-fast-forward) — retrying`);
13310
+ if (attempt < attempts - 1 && backoffMs > 0)
13311
+ await sleep(backoffMs);
13312
+ continue;
13313
+ }
13314
+ logger.info("sync", `synced`, { pulled });
13574
13315
  return {
13575
- status: "error",
13576
- message: setupMsg,
13577
- ...snapshotId ? { snapshotId } : {}
13316
+ status: "synced",
13317
+ message: pulled ? MSG_SYNCED_MERGED : MSG_SYNCED,
13318
+ mergedRemoteChanges: pulled,
13319
+ ...convergeExtras(),
13320
+ ...base()
13578
13321
  };
13579
13322
  }
13580
- return failureOutcome(e, snapshotId);
13581
- }
13582
- });
13583
- }
13584
- async function pushChanges(options) {
13585
- const http2 = options.httpClient ?? defaultGitHttp;
13586
- const dir = await repoDirFor(options.projectDir);
13587
- return withRepoLock(dir, async () => {
13588
- const logger = resolveLogger(options.logFile, "sync");
13589
- await assertNoStructuralDamage(options.projectDir, logger);
13590
- const cache2 = {};
13591
- let snapshotId;
13592
- try {
13593
- const branch = await currentBranchOrThrow(dir);
13594
- const transport = await resolveTransport(dir, options);
13595
- snapshotId = await snapshotBeforeAction({
13596
- projectDir: options.projectDir,
13597
- dir,
13598
- message: options.message,
13599
- authorName: options.authorName,
13600
- authorEmail: options.authorEmail,
13601
- cache: cache2
13602
- });
13603
- const remoteTip = await fetchRemoteTip(dir, branch, transport, http2, cache2);
13604
- const localTip = await git7.resolveRef({ fs: fs8, dir, ref: branch });
13605
- const base = snapshotId ? { snapshotId } : {};
13606
- logger.info("push", `branch=${branch} local=${short(localTip)} fetched=${short(remoteTip)} snapshot=${snapshotId ? short(snapshotId) : "none"}`);
13607
- if (remoteTip === localTip) {
13608
- return { status: "up-to-date", message: MSG_PUSH_UP_TO_DATE, ...base };
13609
- }
13610
- try {
13611
- await git7.push({
13612
- fs: fs8,
13613
- http: http2,
13614
- dir,
13615
- cache: cache2,
13616
- remote: transport.remote,
13617
- ref: branch,
13618
- ...onAuthFor(transport.credential)
13619
- });
13620
- } catch (e) {
13621
- if (isPushRejected(e)) {
13622
- return { status: "pull-first", message: MSG_PULL_FIRST, ...base };
13623
- }
13624
- throw e;
13625
- }
13626
- return { status: "pushed", message: MSG_SYNCED, ...base };
13323
+ logger.error("sync", `exhausted ${attempts} retry attempts (race)`);
13324
+ return { status: "error", message: MSG_BUSY, ...convergeExtras(), ...base() };
13627
13325
  } catch (e) {
13628
13326
  const setupMsg = setupErrorMessage(e);
13629
- if (setupMsg) {
13630
- return {
13631
- status: "error",
13632
- message: setupMsg,
13633
- ...snapshotId ? { snapshotId } : {}
13634
- };
13327
+ if (setupMsg)
13328
+ return { status: "error", message: setupMsg, ...base() };
13329
+ if (await historyUnreadable(dir)) {
13330
+ logger.error("sync", "the project's history could not be read");
13331
+ return { status: "error", message: MSG_HISTORY_UNREADABLE, ...base() };
13635
13332
  }
13636
- return failureOutcome(e, snapshotId);
13333
+ return { ...failureOutcome(e, snapshotId), ...filesChanged ? { filesChanged: true } : {} };
13637
13334
  }
13638
13335
  });
13639
13336
  }
13640
- // src/lib/remote-auth/image-clash.ts
13641
- import * as fs9 from "node:fs";
13642
- import { mkdir as mkdir16, writeFile as writeFile15 } from "node:fs/promises";
13643
- import path26 from "node:path";
13644
- import git8 from "isomorphic-git";
13645
- var OID = /^[0-9a-f]{40}$/;
13646
- function assertSafeRelativePath(filepath) {
13647
- if (path26.isAbsolute(filepath) || filepath.split(/[\\/]/).some((seg) => seg === "..")) {
13648
- throw new Error("Invalid file path.");
13649
- }
13650
- }
13651
- async function readImageVersion(options) {
13652
- if (!OID.test(options.oid))
13653
- throw new Error("Invalid version id.");
13654
- const dir = await repoDirFor(options.projectDir);
13655
- const { blob } = await git8.readBlob({ fs: fs9, dir, oid: options.oid });
13656
- return blob;
13657
- }
13658
- async function keepImageVersion(options) {
13659
- if (!OID.test(options.oid))
13660
- throw new Error("Invalid version id.");
13661
- assertSafeRelativePath(options.path);
13662
- const dir = await repoDirFor(options.projectDir);
13663
- await withRepoLock(dir, async () => {
13664
- const { blob } = await git8.readBlob({ fs: fs9, dir, oid: options.oid });
13665
- const abs = path26.join(dir, options.path);
13666
- await mkdir16(path26.dirname(abs), { recursive: true });
13667
- await writeFile15(abs, blob);
13668
- await snapshotWorkingTreeUnlocked({
13669
- projectDir: dir,
13670
- repoRoot: dir,
13671
- message: `Kept your chosen version of ${path26.posix.basename(options.path)}`,
13672
- authorName: options.authorName,
13673
- authorEmail: options.authorEmail
13674
- }).catch((e) => {
13675
- if (!(e instanceof Error && /nothing new to save/i.test(e.message)))
13676
- throw e;
13677
- });
13678
- });
13679
- }
13680
13337
  // src/lib/publish/run-publish.ts
13681
- import { readdir as readdir12, stat as stat11 } from "node:fs/promises";
13682
- import path29 from "node:path";
13338
+ import { readdir as readdir11, stat as stat10 } from "node:fs/promises";
13339
+ import path26 from "node:path";
13683
13340
 
13684
13341
  // src/lib/publish/command-runner.ts
13685
13342
  var CAPTURE_LIMIT = 64 * 1024;
@@ -13866,25 +13523,25 @@ ${tail}` : ""}`);
13866
13523
  };
13867
13524
 
13868
13525
  // src/lib/publish/providers/guided.ts
13869
- import { copyFile as copyFile2, mkdir as mkdir17, writeFile as writeFile16 } from "node:fs/promises";
13870
- import path27 from "node:path";
13526
+ import { copyFile as copyFile2, mkdir as mkdir16, writeFile as writeFile13 } from "node:fs/promises";
13527
+ import path24 from "node:path";
13871
13528
  async function stageGuidedPackage(req, providerId, listingLines) {
13872
- const packageDir = path27.join(path27.dirname(req.artifact.path), "publish", providerId);
13873
- await mkdir17(packageDir, { recursive: true });
13874
- await copyFile2(req.artifact.path, path27.join(packageDir, path27.basename(req.artifact.path)));
13529
+ const packageDir = path24.join(path24.dirname(req.artifact.path), "publish", providerId);
13530
+ await mkdir16(packageDir, { recursive: true });
13531
+ await copyFile2(req.artifact.path, path24.join(packageDir, path24.basename(req.artifact.path)));
13875
13532
  const { title, authors } = req.project;
13876
13533
  const sheet = [
13877
13534
  `# ${title || "Untitled"}`,
13878
13535
  "",
13879
13536
  `- **Authors:** ${authors.length ? authors.join(", ") : "(not set)"}`,
13880
- `- **File:** ${path27.basename(req.artifact.path)}`,
13537
+ `- **File:** ${path24.basename(req.artifact.path)}`,
13881
13538
  ...listingLines,
13882
13539
  "",
13883
13540
  "Copy these details into the platform's listing form when uploading.",
13884
13541
  ""
13885
13542
  ].join(`
13886
13543
  `);
13887
- await writeFile16(path27.join(packageDir, "LISTING.md"), sheet, "utf8");
13544
+ await writeFile13(path24.join(packageDir, "LISTING.md"), sheet, "utf8");
13888
13545
  return packageDir;
13889
13546
  }
13890
13547
 
@@ -13947,8 +13604,8 @@ var drivethrurpgProvider = {
13947
13604
  };
13948
13605
 
13949
13606
  // src/lib/publish/butler.ts
13950
- import { chmod as chmod2, mkdir as mkdir18, stat as stat10, writeFile as writeFile17 } from "node:fs/promises";
13951
- import path28 from "node:path";
13607
+ import { chmod as chmod2, mkdir as mkdir17, stat as stat9, writeFile as writeFile14 } from "node:fs/promises";
13608
+ import path25 from "node:path";
13952
13609
  import { unzipSync as unzipSync2 } from "fflate";
13953
13610
  var BUTLER_DOWNLOAD_TIMEOUT_MS = 300000;
13954
13611
  function butlerBrothChannel(platform2 = process.platform, arch2 = process.arch) {
@@ -13968,7 +13625,7 @@ function butlerBinaryName(platform2 = process.platform) {
13968
13625
  }
13969
13626
  async function fileExists2(p) {
13970
13627
  try {
13971
- return (await stat10(p)).isFile();
13628
+ return (await stat9(p)).isFile();
13972
13629
  } catch {
13973
13630
  return false;
13974
13631
  }
@@ -13985,8 +13642,8 @@ async function ensureButler(deps) {
13985
13642
  }
13986
13643
  if (await commandExists("butler", runCommand, deps.env))
13987
13644
  return "butler";
13988
- const cacheDir = path28.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
13989
- const cached = path28.join(cacheDir, butlerBinaryName());
13645
+ const cacheDir = path25.join(deps.configDir ?? defaultConfigDir(), "tools", "butler");
13646
+ const cached = path25.join(cacheDir, butlerBinaryName());
13990
13647
  if (await fileExists2(cached))
13991
13648
  return cached;
13992
13649
  const channel = butlerBrothChannel();
@@ -14008,14 +13665,14 @@ async function ensureButler(deps) {
14008
13665
  return new Uint8Array(await response.arrayBuffer());
14009
13666
  });
14010
13667
  const files = unzipSync2(archive);
14011
- await mkdir18(cacheDir, { recursive: true });
13668
+ await mkdir17(cacheDir, { recursive: true });
14012
13669
  let foundBinary = false;
14013
13670
  for (const [name, data] of Object.entries(files)) {
14014
13671
  if (name.endsWith("/"))
14015
13672
  continue;
14016
- const target = path28.join(cacheDir, path28.basename(name));
14017
- await writeFile17(target, data);
14018
- if (path28.basename(name) === butlerBinaryName()) {
13673
+ const target = path25.join(cacheDir, path25.basename(name));
13674
+ await writeFile14(target, data);
13675
+ if (path25.basename(name) === butlerBinaryName()) {
14019
13676
  await chmod2(target, 493).catch(() => {});
14020
13677
  foundBinary = true;
14021
13678
  }
@@ -14453,9 +14110,9 @@ async function resolvePublishRequest(options, deps) {
14453
14110
  const effectiveAccount = bookAccount || deps.credentialAccount;
14454
14111
  const effectiveDeps = effectiveAccount ? { ...deps, credentialAccount: effectiveAccount } : deps;
14455
14112
  const outDir = resolveOutputDir(manifestDir, config.title);
14456
- const defaultArtifact = provider.info.format === "pdf" ? path29.join(outDir, artifactName(config.title, "pdf")) : outDir;
14113
+ const defaultArtifact = provider.info.format === "pdf" ? path26.join(outDir, artifactName(config.title, "pdf")) : outDir;
14457
14114
  const artifact = {
14458
- path: options.artifactPath ? path29.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
14115
+ path: options.artifactPath ? path26.resolve(options.projectDir, options.artifactPath) : defaultArtifact,
14459
14116
  format: provider.info.format
14460
14117
  };
14461
14118
  return {
@@ -14473,7 +14130,7 @@ var PDF_HINT = "Build the PDF first (gutterpress build, or export it from the ap
14473
14130
  var HTML_HINT = "Build the website export first (gutterpress build --format html).";
14474
14131
  async function artifactIssues(artifact) {
14475
14132
  try {
14476
- const s = await stat11(artifact.path);
14133
+ const s = await stat10(artifact.path);
14477
14134
  if (artifact.format === "pdf" && !s.isFile()) {
14478
14135
  return [
14479
14136
  {
@@ -14519,7 +14176,7 @@ async function artifactIssues(artifact) {
14519
14176
  var BUILD_FINGERPRINT_FILENAME = "build-fingerprint.json";
14520
14177
  async function htmlDirIssues(dir) {
14521
14178
  const issues = [];
14522
- const isThere = async (rel) => stat11(path29.join(dir, rel)).then(() => true, () => false);
14179
+ const isThere = async (rel) => stat10(path26.join(dir, rel)).then(() => true, () => false);
14523
14180
  if (!await isThere(BOOK_HTML)) {
14524
14181
  issues.push({
14525
14182
  severity: "error",
@@ -14527,7 +14184,7 @@ async function htmlDirIssues(dir) {
14527
14184
  message: `${dir} has no ${BOOK_HTML} — it isn't an HTML export. ${HTML_HINT}`
14528
14185
  });
14529
14186
  }
14530
- const entries = await readdir12(dir).catch(() => []);
14187
+ const entries = await readdir11(dir).catch(() => []);
14531
14188
  const extras = entries.filter((name) => name.toLowerCase().endsWith(".pdf") || name === BUILD_FINGERPRINT_FILENAME);
14532
14189
  if (extras.length > 0) {
14533
14190
  issues.push({
@@ -14626,17 +14283,17 @@ async function connectPublishProvider(options, deps) {
14626
14283
  return { connected: true, providerId: info7.id };
14627
14284
  }
14628
14285
  // src/lib/publish/selections.ts
14629
- import { chmod as chmod3, mkdir as mkdir19, readFile as readFile32, writeFile as writeFile18 } from "node:fs/promises";
14630
- import path30 from "node:path";
14286
+ import { chmod as chmod3, mkdir as mkdir18, readFile as readFile31, writeFile as writeFile15 } from "node:fs/promises";
14287
+ import path27 from "node:path";
14631
14288
  class PublishSelectionsStore {
14632
14289
  filePath;
14633
14290
  queue = Promise.resolve();
14634
14291
  constructor(filePath) {
14635
- this.filePath = filePath ?? path30.join(defaultConfigDir(), "publish-selections.json");
14292
+ this.filePath = filePath ?? path27.join(defaultConfigDir(), "publish-selections.json");
14636
14293
  }
14637
14294
  async read() {
14638
14295
  try {
14639
- const raw = await readFile32(this.filePath, "utf8");
14296
+ const raw = await readFile31(this.filePath, "utf8");
14640
14297
  const parsed = JSON.parse(raw);
14641
14298
  if (parsed && typeof parsed === "object") {
14642
14299
  return {
@@ -14649,8 +14306,8 @@ class PublishSelectionsStore {
14649
14306
  return { version: 1, global: {}, projects: {} };
14650
14307
  }
14651
14308
  async write(data) {
14652
- await mkdir19(path30.dirname(this.filePath), { recursive: true });
14653
- await writeFile18(this.filePath, JSON.stringify(data, null, 2), {
14309
+ await mkdir18(path27.dirname(this.filePath), { recursive: true });
14310
+ await writeFile15(this.filePath, JSON.stringify(data, null, 2), {
14654
14311
  encoding: "utf8",
14655
14312
  mode: 384
14656
14313
  });
@@ -14752,372 +14409,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
14752
14409
  await writeManifestDoc(file, doc);
14753
14410
  return settingsFromDoc(doc);
14754
14411
  }
14755
- // src/lib/remote-auth/recovery/repair.ts
14756
- import * as fs10 from "node:fs";
14757
- import { cp as cp6, mkdtemp as mkdtemp4, rename as rename6, rm as rm10 } from "node:fs/promises";
14758
- import os4 from "node:os";
14759
- import path31 from "node:path";
14760
- import git9 from "isomorphic-git";
14761
- var INTERRUPTED_STATE = [
14762
- "MERGE_HEAD",
14763
- "MERGE_MSG",
14764
- "MERGE_MODE",
14765
- "CHERRY_PICK_HEAD",
14766
- "REVERT_HEAD",
14767
- "sequencer",
14768
- "rebase-merge",
14769
- "rebase-apply"
14770
- ];
14771
- var MSG_REPAIRED = "Your project's version history is working again. Your files were not changed.";
14772
- var MSG_RETRY = "Another program may be using this project's history right now — try again in a moment.";
14773
- var MSG_FAILED = "Your project's history couldn't be repaired automatically, but your files are safe and untouched.";
14774
- function stamp() {
14775
- return new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19);
14776
- }
14777
- async function indexReadable(dir) {
14778
- try {
14779
- await git9.listFiles({ fs: fs10, dir });
14780
- return true;
14781
- } catch {
14782
- return false;
14783
- }
14784
- }
14785
- async function rebuildIndexFromHead(dir) {
14786
- await rm10(path31.join(gitDirFor(dir), "index"), { force: true });
14787
- const cache2 = {};
14788
- let files;
14789
- try {
14790
- files = await git9.listFiles({ fs: fs10, dir, ref: "HEAD" });
14791
- } catch {
14792
- return;
14793
- }
14794
- for (const filepath of files) {
14795
- await git9.resetIndex({ fs: fs10, dir, cache: cache2, filepath, ref: "HEAD" });
14796
- }
14797
- }
14798
- function readDamagedBranchTips(damagedGitDir) {
14799
- const tips = new Map;
14800
- const OID2 = /^[0-9a-f]{40}$/;
14801
- try {
14802
- const packed = fs10.readFileSync(path31.join(damagedGitDir, "packed-refs"), "utf8");
14803
- for (const line of packed.split(`
14804
- `)) {
14805
- const m = /^([0-9a-f]{40}) refs\/heads\/(.+)$/.exec(line.trim());
14806
- if (m)
14807
- tips.set(m[2], m[1]);
14808
- }
14809
- } catch {}
14810
- const headsDir = path31.join(damagedGitDir, "refs", "heads");
14811
- const walk = (dir, prefix) => {
14812
- let entries;
14813
- try {
14814
- entries = fs10.readdirSync(dir, { withFileTypes: true });
14815
- } catch {
14816
- return;
14817
- }
14818
- for (const e of entries) {
14819
- const name = prefix ? `${prefix}/${e.name}` : e.name;
14820
- if (e.isDirectory())
14821
- walk(path31.join(dir, e.name), name);
14822
- else if (e.isFile()) {
14823
- try {
14824
- const oid = fs10.readFileSync(path31.join(dir, e.name), "utf8").trim();
14825
- if (OID2.test(oid))
14826
- tips.set(name, oid);
14827
- } catch {}
14828
- }
14829
- }
14830
- };
14831
- walk(headsDir, "");
14832
- return [...tips].map(([name, oid]) => ({ name, oid }));
14833
- }
14834
- async function looseCommitTips(dir, cache2) {
14835
- const objectsDir = path31.join(gitDirFor(dir), "objects");
14836
- const oids = [];
14837
- let fanouts;
14838
- try {
14839
- fanouts = fs10.readdirSync(objectsDir).filter((e) => /^[0-9a-f]{2}$/.test(e));
14840
- } catch {
14841
- return [];
14842
- }
14843
- for (const fan of fanouts) {
14844
- let files;
14845
- try {
14846
- files = fs10.readdirSync(path31.join(objectsDir, fan));
14847
- } catch {
14848
- continue;
14849
- }
14850
- for (const f of files) {
14851
- if (/^[0-9a-f]{38}$/.test(f))
14852
- oids.push(fan + f);
14853
- }
14854
- }
14855
- const commits = new Map;
14856
- for (const oid of oids) {
14857
- try {
14858
- const obj = await git9.readObject({ fs: fs10, dir, cache: cache2, oid, format: "parsed" });
14859
- if (obj.type === "commit") {
14860
- commits.set(oid, obj.object.parent ?? []);
14861
- }
14862
- } catch {}
14863
- }
14864
- const isParent = new Set;
14865
- for (const parents of commits.values())
14866
- for (const p of parents)
14867
- isParent.add(p);
14868
- return [...commits.keys()].filter((oid) => !isParent.has(oid));
14869
- }
14870
- async function targetBranch(dir) {
14871
- try {
14872
- const branches = await git9.listBranches({ fs: fs10, dir });
14873
- if (branches.includes("main"))
14874
- return "main";
14875
- if (branches.includes("master"))
14876
- return "master";
14877
- if (branches.length > 0)
14878
- return branches[0];
14879
- } catch {}
14880
- return "main";
14881
- }
14882
- async function repairRepo(options) {
14883
- const dir = await repoDirFor(options.projectDir);
14884
- return withRepoLock(dir, () => repairLocked(dir, options));
14885
- }
14886
- async function repairLocked(dir, options) {
14887
- const logger = resolveLogger(options.logFile, "repair");
14888
- const gitDir = gitDirFor(dir);
14889
- const actions = [];
14890
- const snapshotFiles = async (message) => {
14891
- try {
14892
- if (await hasPendingChanges(dir)) {
14893
- await snapshotWorkingTreeUnlocked({
14894
- projectDir: dir,
14895
- repoRoot: dir,
14896
- message,
14897
- authorName: options.authorName,
14898
- authorEmail: options.authorEmail
14899
- });
14900
- }
14901
- } catch (e) {
14902
- if (!isNoChangesError(e))
14903
- throw e;
14904
- }
14905
- };
14906
- try {
14907
- if (fs10.existsSync(gitDir)) {
14908
- const sweep = await sweepStaleLocks(gitDir);
14909
- if (sweep === "fresh") {
14910
- logger.info("repair", "fresh lock present — deferring");
14911
- return { status: "retry_later", message: MSG_RETRY, actions, retryAfterMs: 30000 };
14912
- }
14913
- if (sweep === "swept") {
14914
- actions.push("Removed leftover lock files from an interrupted operation.");
14915
- logger.info("repair", "swept stale locks");
14916
- }
14917
- }
14918
- if (fs10.existsSync(gitDir)) {
14919
- let removedState = false;
14920
- for (const name of INTERRUPTED_STATE) {
14921
- const p = path31.join(gitDir, name);
14922
- if (fs10.existsSync(p)) {
14923
- await rm10(p, { recursive: true, force: true });
14924
- removedState = true;
14925
- }
14926
- }
14927
- if (removedState) {
14928
- actions.push("Cleared an unfinished operation another program left behind.");
14929
- logger.info("repair", "removed interrupted-operation state");
14930
- await rebuildIndexFromHead(dir);
14931
- } else if (!await indexReadable(dir)) {
14932
- await rebuildIndexFromHead(dir);
14933
- actions.push("Rebuilt the project's tracking information.");
14934
- logger.info("repair", "rebuilt corrupt index");
14935
- }
14936
- }
14937
- let health = await inspectRepo({ repoDir: dir }, { checkLocalChanges: false });
14938
- if (health.hasGitDir && !health.headUnreadable && health.isDetachedHead) {
14939
- await snapshotFiles("Saved your latest changes");
14940
- const headOid = await git9.resolveRef({ fs: fs10, dir, ref: "HEAD" });
14941
- const rescueRef = `refs/heads/recovered-${stamp()}`;
14942
- await git9.writeRef({ fs: fs10, dir, ref: rescueRef, value: headOid, force: true });
14943
- const branch = await targetBranch(dir);
14944
- const branchExists = (await git9.listBranches({ fs: fs10, dir })).includes(branch);
14945
- if (!branchExists) {
14946
- await git9.writeRef({ fs: fs10, dir, ref: `refs/heads/${branch}`, value: headOid, force: true });
14947
- }
14948
- await git9.writeRef({
14949
- fs: fs10,
14950
- dir,
14951
- ref: "HEAD",
14952
- value: `refs/heads/${branch}`,
14953
- symbolic: true,
14954
- force: true
14955
- });
14956
- const cache2 = {};
14957
- await git9.checkout({ fs: fs10, dir, cache: cache2, ref: branch, force: true });
14958
- const branchTip = await git9.resolveRef({ fs: fs10, dir, ref: branch });
14959
- if (branchTip !== headOid) {
14960
- await convergeMerge({
14961
- dir,
14962
- cache: cache2,
14963
- branch,
14964
- theirs: headOid,
14965
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
14966
- authorName: options.authorName,
14967
- authorEmail: options.authorEmail,
14968
- allowUnrelatedHistories: true
14969
- });
14970
- }
14971
- actions.push("Reconnected your work to the project's version line.");
14972
- logger.info("repair", "reattached detached HEAD", { rescueRef });
14973
- }
14974
- health = await inspectRepo({ repoDir: dir }, { checkLocalChanges: false });
14975
- if (classifyFromHealth(health, { minLockAgeMs: 0 }) === null) {
14976
- await snapshotFiles("Saved your latest changes");
14977
- logger.info("repair", "repaired in place", { actions: actions.length });
14978
- return { status: "repaired", message: MSG_REPAIRED, actions };
14979
- }
14980
- return await recloneWithSalvage(dir, options, actions, logger, snapshotFiles);
14981
- } catch (e) {
14982
- logger.error("repair", "repair failed", { error: e instanceof Error ? e.message : String(e) });
14983
- return { status: "failed", message: MSG_FAILED, actions };
14984
- }
14985
- }
14986
- async function recloneWithSalvage(dir, options, actions, logger, snapshotFiles) {
14987
- const gitDir = gitDirFor(dir);
14988
- let remoteUrl;
14989
- let credential;
14990
- try {
14991
- const transport = await resolveTransport(dir, {
14992
- credential: options.credential,
14993
- tokenStore: options.tokenStore
14994
- });
14995
- remoteUrl = transport.url;
14996
- credential = transport.credential;
14997
- } catch {
14998
- remoteUrl = undefined;
14999
- }
15000
- let freshGitDir;
15001
- let tempDir;
15002
- if (remoteUrl) {
15003
- tempDir = await mkdtemp4(path31.join(os4.tmpdir(), "gutterpress-repair-"));
15004
- const cloneDir = path31.join(tempDir, "clone");
15005
- try {
15006
- await cloneRepository({
15007
- url: remoteUrl,
15008
- dir: cloneDir,
15009
- ...credential ? { credential } : {}
15010
- });
15011
- } catch (e) {
15012
- await rm10(tempDir, { recursive: true, force: true });
15013
- logger.warn("repair", "re-clone failed — deferring", {
15014
- error: e instanceof Error ? e.message : String(e)
15015
- });
15016
- return {
15017
- status: "retry_later",
15018
- message: "The online copy couldn't be reached to finish the repair. Your files are safe — it will be tried again.",
15019
- actions,
15020
- retryAfterMs: 60000
15021
- };
15022
- }
15023
- freshGitDir = path31.join(cloneDir, ".git");
15024
- } else {
15025
- tempDir = await mkdtemp4(path31.join(os4.tmpdir(), "gutterpress-repair-"));
15026
- const initDir = path31.join(tempDir, "init");
15027
- fs10.mkdirSync(initDir, { recursive: true });
15028
- await git9.init({ fs: fs10, dir: initDir, defaultBranch: "main" });
15029
- freshGitDir = path31.join(initDir, ".git");
15030
- }
15031
- let damagedBackup;
15032
- if (fs10.existsSync(gitDir)) {
15033
- damagedBackup = path31.join(dir, `.git-damaged-${stamp()}`);
15034
- await rename6(gitDir, damagedBackup);
15035
- actions.push("Kept the old history folder on disk as a backup.");
15036
- }
15037
- await rename6(freshGitDir, gitDir);
15038
- if (tempDir)
15039
- await rm10(tempDir, { recursive: true, force: true });
15040
- actions.push(remoteUrl ? "Rebuilt the project's history from the online copy." : "Started a fresh history for the project (no online copy is connected).");
15041
- logger.info("repair", "replaced .git", { fromRemote: Boolean(remoteUrl), damagedBackup });
15042
- if (damagedBackup) {
15043
- const from = path31.join(damagedBackup, "objects");
15044
- if (fs10.existsSync(from)) {
15045
- await cp6(from, path31.join(gitDir, "objects"), {
15046
- recursive: true,
15047
- force: false,
15048
- errorOnExist: false
15049
- }).catch(() => {});
15050
- }
15051
- const branch = await targetBranch(dir);
15052
- let branchExists = (await git9.listBranches({ fs: fs10, dir })).includes(branch);
15053
- const scanCache = {};
15054
- const tips = new Map;
15055
- for (const tip of readDamagedBranchTips(damagedBackup)) {
15056
- tips.set(tip.oid, tip.name);
15057
- }
15058
- for (const oid of await looseCommitTips(dir, scanCache)) {
15059
- if (!tips.has(oid))
15060
- tips.set(oid, "recovered-work");
15061
- }
15062
- for (const [oid, name] of tips) {
15063
- const cache2 = {};
15064
- try {
15065
- await git9.readCommit({ fs: fs10, dir, cache: cache2, oid });
15066
- } catch {
15067
- logger.warn("repair", "old tip unreadable — left in the backup", { tip: name });
15068
- continue;
15069
- }
15070
- if (!branchExists) {
15071
- await git9.writeRef({ fs: fs10, dir, ref: `refs/heads/${branch}`, value: oid, force: true });
15072
- await git9.writeRef({
15073
- fs: fs10,
15074
- dir,
15075
- ref: "HEAD",
15076
- value: `refs/heads/${branch}`,
15077
- symbolic: true,
15078
- force: true
15079
- });
15080
- await rebuildIndexFromHead(dir);
15081
- branchExists = true;
15082
- actions.push("Restored your saved history from the backup.");
15083
- continue;
15084
- }
15085
- try {
15086
- const branchTip = await git9.resolveRef({ fs: fs10, dir, ref: branch });
15087
- if (branchTip === oid)
15088
- continue;
15089
- await convergeMerge({
15090
- dir,
15091
- cache: cache2,
15092
- branch,
15093
- theirs: oid,
15094
- author: await resolveGitAuthor(dir, options.authorName, options.authorEmail),
15095
- authorName: options.authorName,
15096
- authorEmail: options.authorEmail,
15097
- allowUnrelatedHistories: true
15098
- });
15099
- actions.push("Brought your earlier saved versions back into the history.");
15100
- } catch (e) {
15101
- logger.warn("repair", "old tip could not be merged — pinned as a recovery branch", {
15102
- tip: name,
15103
- error: e instanceof Error ? e.message : String(e)
15104
- });
15105
- await git9.writeRef({
15106
- fs: fs10,
15107
- dir,
15108
- ref: `refs/heads/recovered-${stamp()}-${name.replace(/[^A-Za-z0-9_-]/g, "_")}`,
15109
- value: oid,
15110
- force: true
15111
- }).catch(() => {});
15112
- }
15113
- }
15114
- }
15115
- await snapshotFiles("Saved your files after repairing the project");
15116
- return {
15117
- status: "repaired",
15118
- message: MSG_REPAIRED,
15119
- actions,
15120
- ...damagedBackup ? { damagedGitBackupPath: damagedBackup } : {}
15121
- };
15122
- }
15123
- export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, redactCredential, defaultConfigDir, FileTokenStore, extractUrlCredential, migrateUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, APP_HEARTBEAT_FRESH_MS, heartbeatTtlMs, appHeartbeatPath, writeAppHeartbeat, removeAppHeartbeat, readAppHeartbeat, isAppHeartbeatFresh, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, listGitHubRepositories, listRepoBooks, listGitHubBranches, STALE_LOCK_MIN_AGE_MS, RepoNeedsRecoveryError, isRepoNeedsRecoveryError, isLikelyRepoCorruption, classifyFromHealth, inspectRepo, verifyRepoReadable, isUnbornRepo, preflightStructuralReason, buildPreflightDiagnostics, SYNC_SNAPSHOT_MESSAGE, provenancePath, readProjectProvenance, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, normalizeForgeHost, knownForgeTokenUrl, connectGenericHost, GenericTokenAuthProvider, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, pullChanges, pushChanges, readImageVersion, keepImageVersion, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, resolvePublishRequest, runPublish, connectPublishProvider, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig, repairRepo };
14412
+ export { PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, PLUGINS_DIR, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, checkCss, runLint, formatReport, getChecks, getCheckById, resolveCheckSelectors, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, RECOMMENDED_PLUGINS, listProjectPlugins, setPluginEnabled, addNpmPlugin, addLocalPlugin, validateProjectPlugins, THEMES_DIR, BUILT_IN_THEME_IDS, listBuiltInThemes, resolveBuiltInTheme, listProjectThemes, getActiveTheme, getPreviousTheme, revertTheme, applyTheme, importThemeFromFolder, importThemeFromUrl, readThemeCss, removeProjectTheme, importThemeFromFile, setManifestFields, readManifestFields, setActiveStyles, AUTO_SNAPSHOT_MIN_MINUTES, AUTO_SNAPSHOT_MAX_MINUTES, AUTO_SNAPSHOT_DEFAULT_MINUTES, AUTO_SYNC_MIN_MINUTES, AUTO_SYNC_MAX_MINUTES, AUTO_SYNC_DEFAULT_MINUTES, AUTO_SYNC_PUSH_INTERVAL_MINUTES, autoSnapshotDelayMs, autoSyncDelayMs, isGitInternalPath, resolveGitHubClientId, GITHUB_HOST, GitHubAuthProvider, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, publishCredentialKey, publishConnectionStatus, listPublishAccounts, listPublishProviders, publishProviderFor, resolvePublishRequest, runPublish, connectPublishProvider, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };