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