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