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