gutterpress 0.11.6-alpha.2 → 0.11.6-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/api/index.d.ts +3 -3
  2. package/dist/api/index.js +10 -6
  3. package/dist/{audit-fdba6axw.js → audit-qd5q0k1m.js} +5 -5
  4. package/dist/{build-2gjr2rj6.js → build-38mz8d2q.js} +5 -5
  5. package/dist/cli-4tx0szk7.js +1251 -0
  6. package/dist/{cli-sekhwex4.js → cli-5vdgncmm.js} +2 -2
  7. package/dist/{cli-ersw5g7j.js → cli-a569p7v6.js} +1 -1
  8. package/dist/{cli-7k2v2sya.js → cli-q4e99n20.js} +409 -984
  9. package/dist/{cli-wmvvkwcc.js → cli-qt466d4e.js} +1 -1
  10. package/dist/cli.js +14 -14
  11. package/dist/{doctor-vgarhrsg.js → doctor-a98dkqnp.js} +2 -2
  12. package/dist/{engine-w5ed5hmb.js → engine-15dfcy85.js} +2 -2
  13. package/dist/{engine-cbc17ne4.js → engine-amxqsfka.js} +1 -1
  14. package/dist/{ext-hb5k5gga.js → ext-5wgrtygx.js} +7 -7
  15. package/dist/{gutterpress-viewer-k0xrt5hw.js → gutterpress-viewer-swz6bwfa.js} +25 -3
  16. package/dist/index-7mqst9tg.js +1302 -0
  17. package/dist/{index-nmykzwb4.js → index-ash0pvb9.js} +495 -1069
  18. package/dist/{index-6n15e1ze.js → index-gf1shk3y.js} +1 -1
  19. package/dist/{index-12pvhbyz.js → index-yeej2vhs.js} +2 -2
  20. package/dist/index.js +11 -7
  21. package/dist/lib/project-scaffold.d.ts +20 -0
  22. package/dist/lib/remote-auth/sync-messages.d.ts +7 -7
  23. package/dist/lib/source-provider.d.ts +24 -0
  24. package/dist/{lint-0pfqbs5r.js → lint-btmms4m9.js} +5 -5
  25. package/dist/{new-nxxx2mv2.js → new-xt38pdjk.js} +7 -7
  26. package/dist/{preflight-g7xjme17.js → preflight-fydrrjpz.js} +5 -5
  27. package/dist/{preview-j59b5hg3.js → preview-655bdb9g.js} +6 -6
  28. package/dist/{publish-mzct8ggw.js → publish-gz6re318.js} +6 -6
  29. package/dist/{source-provider-54a52bpd.js → source-provider-gy3cn7kw.js} +3 -1
  30. package/dist/{source-provider-a458mmec.js → source-provider-m5wes47h.js} +3 -1
  31. package/dist/{validate-z2drbha6.js → validate-e3vvngx0.js} +5 -5
  32. package/package.json +1 -1
  33. package/dist/cli-c9h2bk2k.js +0 -645
  34. package/dist/index-c5ev4r1y.js +0 -696
@@ -0,0 +1,1251 @@
1
+ import {
2
+ capabilitiesFor,
3
+ detectProjectSource,
4
+ findEnclosingRepoDir
5
+ } from "./cli-v5mp7a6q.js";
6
+
7
+ // src/lib/git-fs.ts
8
+ import * as nodeFs from "node:fs";
9
+ var MUTABLE_GIT_METADATA = /(?:^|[\\/])\.git[\\/](?:index|HEAD|packed-refs|refs[\\/].+)$/;
10
+ function needsAtomicWrite(file) {
11
+ return typeof file === "string" && MUTABLE_GIT_METADATA.test(file);
12
+ }
13
+ var tempCounter = 0;
14
+ function tempPathFor(file) {
15
+ return `${file}.gp${process.pid.toString(36)}-${(tempCounter++).toString(36)}.tmp`;
16
+ }
17
+ function writeFile2(file, data, options, callback) {
18
+ const cb = typeof options === "function" ? options : callback;
19
+ const opts = typeof options === "function" ? undefined : options;
20
+ if (!needsAtomicWrite(file)) {
21
+ nodeFs.writeFile(file, data, opts, cb);
22
+ return;
23
+ }
24
+ const temp = tempPathFor(file);
25
+ const failed = (err) => nodeFs.unlink(temp, () => cb(err));
26
+ nodeFs.writeFile(temp, data, opts, (writeErr) => {
27
+ if (writeErr)
28
+ return failed(writeErr);
29
+ nodeFs.rename(temp, file, (renameErr) => {
30
+ if (renameErr)
31
+ return failed(renameErr);
32
+ cb(null);
33
+ });
34
+ });
35
+ }
36
+ async function writeFilePromise(file, data, options) {
37
+ if (!needsAtomicWrite(file)) {
38
+ return nodeFs.promises.writeFile(file, data, options);
39
+ }
40
+ const temp = tempPathFor(file);
41
+ try {
42
+ await nodeFs.promises.writeFile(temp, data, options);
43
+ await nodeFs.promises.rename(temp, file);
44
+ } catch (err) {
45
+ await nodeFs.promises.unlink(temp).catch(() => {});
46
+ throw err;
47
+ }
48
+ }
49
+ var promises2 = {
50
+ ...nodeFs.promises,
51
+ writeFile: writeFilePromise
52
+ };
53
+ var gitFs = {
54
+ ...nodeFs,
55
+ writeFile: writeFile2,
56
+ promises: promises2
57
+ };
58
+
59
+ // src/lib/source-provider.ts
60
+ import path3 from "node:path";
61
+ import git from "isomorphic-git";
62
+
63
+ // src/lib/remote-auth/operation-log.ts
64
+ import { appendFileSync, mkdirSync } from "node:fs";
65
+ import path from "node:path";
66
+ function sanitizeLogText(value, secrets = []) {
67
+ let text = value;
68
+ for (const secret of secrets) {
69
+ if (!secret)
70
+ continue;
71
+ for (const form of [secret, encodeURIComponent(secret)]) {
72
+ text = text.split(form).join("[redacted]");
73
+ }
74
+ }
75
+ return text.replace(/\/\/[^/\s]*@/g, "//").replace(/([?&](?:access_token|token|password|secret|key|code)=)[^&#\s]*/gi, "$1[redacted]").replace(/\b(Bearer|Basic)\s+[A-Za-z0-9+/_.=~-]+/gi, "$1 [redacted]").replace(/\b(?:gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+)/g, "[redacted]").replace(/[\s\x00-\x1f\x7f]+/g, " ").trim().slice(0, 6000);
76
+ }
77
+ var noop = {
78
+ debug: () => {},
79
+ info: () => {},
80
+ warn: () => {},
81
+ error: () => {}
82
+ };
83
+ var LEVEL_ORDER = { debug: 0, info: 1, warn: 2, error: 3 };
84
+ function createFileLogger(logFile, operation, minLevel = "debug", options = {}) {
85
+ try {
86
+ mkdirSync(path.dirname(logFile), { recursive: true });
87
+ } catch {
88
+ return noop;
89
+ }
90
+ function write(level, step, message, data) {
91
+ if (LEVEL_ORDER[level] < LEVEL_ORDER[minLevel])
92
+ return;
93
+ const ts = new Date().toISOString();
94
+ const clean = (value) => sanitizeLogText(value, options.secrets);
95
+ const fields = formatData({ ...options.context, ...data }, clean);
96
+ const line = `[${ts}] ${level.toUpperCase().padEnd(5)} ${clean(operation)}: step=${clean(step)}${fields} | ${clean(message)}
97
+ `;
98
+ try {
99
+ appendFileSync(logFile, line, "utf8");
100
+ } catch {}
101
+ }
102
+ return {
103
+ debug: (step, message, data) => write("debug", step, message, data),
104
+ info: (step, message, data) => write("info", step, message, data),
105
+ warn: (step, message, data) => write("warn", step, message, data),
106
+ error: (step, message, data) => write("error", step, message, data)
107
+ };
108
+ }
109
+ function formatData(data, clean) {
110
+ if (!data)
111
+ return "";
112
+ const parts = [];
113
+ for (const [key, value] of Object.entries(data)) {
114
+ if (value === undefined)
115
+ continue;
116
+ if (Array.isArray(value)) {
117
+ parts.push(`${clean(key)}=${clean(value.join(","))}`);
118
+ } else {
119
+ parts.push(`${clean(key)}=${clean(String(value))}`);
120
+ }
121
+ }
122
+ return parts.length > 0 ? " " + parts.join(" ") : "";
123
+ }
124
+
125
+ // src/lib/plugin-vendor.ts
126
+ import { createHash } from "node:crypto";
127
+ import { createReadStream } from "node:fs";
128
+ import { lstat, readFile, readdir, realpath } from "node:fs/promises";
129
+ import path2 from "node:path";
130
+ import { exports as resolveExports } from "resolve.exports";
131
+ import { satisfies, validRange } from "semver";
132
+
133
+ // src/lib/npm-registry.ts
134
+ var DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org";
135
+ var ENV_OVERRIDE = "GUTTERPRESS_NPM_REGISTRY";
136
+ function npmRegistryUrl() {
137
+ const configured = process.env[ENV_OVERRIDE]?.trim();
138
+ if (!configured)
139
+ return DEFAULT_NPM_REGISTRY;
140
+ let scheme;
141
+ try {
142
+ scheme = new URL(configured).protocol;
143
+ } catch {
144
+ throw new Error(`Invalid ${ENV_OVERRIDE} URL "${configured}".`);
145
+ }
146
+ if (scheme !== "http:" && scheme !== "https:") {
147
+ throw new Error(`${ENV_OVERRIDE} must be an http(s) URL — got "${scheme}".`);
148
+ }
149
+ return configured.replace(/\/+$/, "");
150
+ }
151
+
152
+ // src/lib/plugin-vendor.ts
153
+ var PLUGINS_DIR = "plugins";
154
+ var VENDORED_NPM_DIR = "npm";
155
+ var VENDOR_RECEIPT_FILE = ".gutterpress-install.json";
156
+ var VENDOR_RECEIPT_VERSION = 2;
157
+ var NPM_SEGMENT = /^[a-z0-9][a-z0-9._~-]*$/;
158
+ var EXACT_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
159
+ var WINDOWS_RESERVED = /^(con|prn|aux|nul|clock\$|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\..*)?$/i;
160
+ var MAX_VERIFIED_FILES = 50000;
161
+ var MAX_VERIFIED_BYTES = 512 * 1024 * 1024;
162
+ function isValidNpmPackageName(name) {
163
+ if (!name || name.length > 214 || name !== name.toLowerCase())
164
+ return false;
165
+ if (name.startsWith("@")) {
166
+ const parts = name.slice(1).split("/");
167
+ return parts.length === 2 && parts.every((part) => NPM_SEGMENT.test(part));
168
+ }
169
+ return !name.includes("/") && NPM_SEGMENT.test(name);
170
+ }
171
+ function parseNpmPluginSpec(input) {
172
+ const spec = input.trim();
173
+ if (!spec)
174
+ throw new Error("An npm package name is required.");
175
+ let name = spec;
176
+ let selector;
177
+ if (spec.startsWith("@")) {
178
+ const slash = spec.indexOf("/");
179
+ const versionAt = spec.lastIndexOf("@");
180
+ if (slash > 1 && versionAt > slash) {
181
+ name = spec.slice(0, versionAt);
182
+ selector = spec.slice(versionAt + 1);
183
+ }
184
+ } else {
185
+ const versionAt = spec.lastIndexOf("@");
186
+ if (versionAt > 0) {
187
+ name = spec.slice(0, versionAt);
188
+ selector = spec.slice(versionAt + 1);
189
+ }
190
+ }
191
+ if (!isValidNpmPackageName(name)) {
192
+ throw new Error(`"${name}" is not a valid npm package name. Use a name like ` + "markdown-it-highlightjs or @scope/markdown-it-plugin.");
193
+ }
194
+ if (selector !== undefined && !selector) {
195
+ throw new Error(`The package spec "${spec}" is missing a selector after @.`);
196
+ }
197
+ return selector === undefined ? { name } : { name, selector };
198
+ }
199
+ function isExactNpmVersion(version) {
200
+ return EXACT_VERSION.test(version);
201
+ }
202
+ function encodePathPart(value) {
203
+ return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
204
+ }
205
+ function vendoredNpmPluginRoot(projectDir, name, version) {
206
+ return path2.join(projectDir, PLUGINS_DIR, VENDORED_NPM_DIR, encodePathPart(name), encodePathPart(version));
207
+ }
208
+ function vendoredNpmPluginPackageDir(installRoot, name) {
209
+ return path2.join(installRoot, "node_modules", ...name.split("/"));
210
+ }
211
+ function toPosixPath(value) {
212
+ return value.split(path2.sep).join("/");
213
+ }
214
+ function packageEntryFormat(manifest, relativeEntry) {
215
+ const extension = path2.posix.extname(relativeEntry);
216
+ return extension === ".cjs" || extension === ".json" || extension !== ".mjs" && manifest.type !== "module" ? "commonjs" : "module";
217
+ }
218
+ function assertWindowsSafeRelativePath(relative) {
219
+ if (!relative || relative.includes("\\") || relative.startsWith("/") || /^[a-zA-Z]:/.test(relative)) {
220
+ throw new Error(`Unsafe package path: ${relative || "(empty)"}`);
221
+ }
222
+ const normalized = relative.normalize("NFKC");
223
+ const parts = normalized.split("/");
224
+ if (parts.some((part) => !part || part === "." || part === "..")) {
225
+ throw new Error(`Unsafe package path: ${relative}`);
226
+ }
227
+ for (const part of parts) {
228
+ if (part.length > 255 || /[\u0000-\u001f<>:"|?*]/.test(part) || part.endsWith(".") || part.endsWith(" ") || WINDOWS_RESERVED.test(part)) {
229
+ throw new Error(`Package path is invalid on Windows: ${relative}`);
230
+ }
231
+ }
232
+ }
233
+ function windowsPathKey(relative) {
234
+ return relative.normalize("NFKC").toLowerCase();
235
+ }
236
+ function safeReceiptPath(relative, label) {
237
+ if (typeof relative !== "string")
238
+ throw new Error(`${label} must be a string.`);
239
+ assertWindowsSafeRelativePath(relative);
240
+ return relative;
241
+ }
242
+ function isContained(root, candidate) {
243
+ const relative = path2.relative(root, candidate);
244
+ return relative === "" || !relative.startsWith("..") && !path2.isAbsolute(relative);
245
+ }
246
+ function object(value) {
247
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
248
+ }
249
+ function stringMap(value) {
250
+ const map = object(value);
251
+ if (!map)
252
+ return {};
253
+ const result = {};
254
+ for (const [name, selector] of Object.entries(map)) {
255
+ if (typeof selector === "string" && selector.trim())
256
+ result[name] = selector.trim();
257
+ }
258
+ return result;
259
+ }
260
+ function dependencyDeclarations(manifest) {
261
+ const required = stringMap(manifest.dependencies);
262
+ const optional = stringMap(manifest.optionalDependencies);
263
+ for (const name of Object.keys(optional))
264
+ delete required[name];
265
+ const peers = stringMap(manifest.peerDependencies);
266
+ const peerMeta = object(manifest.peerDependenciesMeta) ?? {};
267
+ const optionalPeers = {};
268
+ for (const [name, selector] of Object.entries(peers)) {
269
+ if (name in required || name in optional)
270
+ continue;
271
+ if (object(peerMeta[name])?.optional === true)
272
+ optionalPeers[name] = selector;
273
+ else
274
+ required[name] = selector;
275
+ }
276
+ return { required, optional, optionalPeers };
277
+ }
278
+ function unsupportedRegistrySelector(selector) {
279
+ return /^(?:file:|git\+|https?:|ssh:|github:|workspace:|link:|npm:)/i.test(selector);
280
+ }
281
+ async function readPackageJson(packageDir) {
282
+ const packageJsonPath = path2.join(packageDir, "package.json");
283
+ const info = await lstat(packageJsonPath);
284
+ if (!info.isFile() || info.isSymbolicLink()) {
285
+ throw new Error(`Package metadata is not a regular file: ${packageJsonPath}`);
286
+ }
287
+ const parsed = object(JSON.parse(await readFile(packageJsonPath, "utf8")));
288
+ if (!parsed)
289
+ throw new Error(`Package metadata is not an object: ${packageJsonPath}`);
290
+ return parsed;
291
+ }
292
+ async function verifyReceiptEntry(installRoot, packagePath, entry, label) {
293
+ if (entry === undefined)
294
+ return;
295
+ const relative = safeReceiptPath(entry, label);
296
+ if (!relative.startsWith(`${packagePath}/`)) {
297
+ throw new Error(`${label} is outside its package.`);
298
+ }
299
+ const packageDir = path2.resolve(installRoot, ...packagePath.split("/"));
300
+ const entryPath = path2.resolve(installRoot, ...relative.split("/"));
301
+ const [realPackageDir, realEntry] = await Promise.all([
302
+ realpath(packageDir),
303
+ realpath(entryPath)
304
+ ]);
305
+ if (!isContained(realPackageDir, realEntry))
306
+ throw new Error(`${label} escapes its package.`);
307
+ const info = await lstat(realEntry);
308
+ if (!info.isFile() || info.isSymbolicLink())
309
+ throw new Error(`${label} is not a regular file.`);
310
+ return realEntry;
311
+ }
312
+ function packageResolutionTargets(manifest, subpath, condition) {
313
+ if (manifest.exports !== undefined) {
314
+ try {
315
+ const targets2 = resolveExports(manifest, subpath ? `./${subpath}` : ".", { require: condition === "require" }) ?? [];
316
+ return targets2.map((target) => ({ target, exact: true }));
317
+ } catch {
318
+ return [];
319
+ }
320
+ }
321
+ if (subpath)
322
+ return [{ target: subpath, exact: false }];
323
+ const targets = [
324
+ ...condition === "import" && typeof manifest.module === "string" ? [manifest.module] : [],
325
+ ...typeof manifest.main === "string" ? [manifest.main] : [],
326
+ "index.mjs",
327
+ "index.js",
328
+ "index.cjs"
329
+ ];
330
+ return [...new Set(targets)].map((target) => ({ target, exact: false }));
331
+ }
332
+ function safePackageTarget(target) {
333
+ if (!target || target.includes("\\") || target.includes("*") || target.includes("?") || target.includes("#")) {
334
+ return null;
335
+ }
336
+ const relative = target.startsWith("./") ? target.slice(2) : target;
337
+ if (!relative || path2.isAbsolute(relative))
338
+ return null;
339
+ assertWindowsSafeRelativePath(relative);
340
+ if (relative.normalize("NFKC").split("/").some((part) => part.toLowerCase() === "node_modules")) {
341
+ return null;
342
+ }
343
+ return relative;
344
+ }
345
+ async function resolveEntryCandidate(packageDir, resolution) {
346
+ const relative = safePackageTarget(resolution.target);
347
+ if (!relative)
348
+ return null;
349
+ const base = path2.resolve(packageDir, ...relative.split("/"));
350
+ if (!isContained(packageDir, base))
351
+ return null;
352
+ const candidates = resolution.exact ? [base] : [
353
+ base,
354
+ `${base}.mjs`,
355
+ `${base}.js`,
356
+ `${base}.cjs`,
357
+ `${base}.json`,
358
+ path2.join(base, "index.mjs"),
359
+ path2.join(base, "index.js"),
360
+ path2.join(base, "index.cjs"),
361
+ path2.join(base, "index.json")
362
+ ];
363
+ for (const candidate of candidates) {
364
+ try {
365
+ const info = await lstat(candidate);
366
+ if (!info.isFile() || info.isSymbolicLink())
367
+ continue;
368
+ const extension = path2.extname(candidate);
369
+ if (extension && ![".js", ".mjs", ".cjs", ".json"].includes(extension))
370
+ continue;
371
+ const [realRoot, realCandidate] = await Promise.all([realpath(packageDir), realpath(candidate)]);
372
+ if (!isContained(realRoot, realCandidate))
373
+ continue;
374
+ return toPosixPath(path2.relative(packageDir, realCandidate));
375
+ } catch {}
376
+ }
377
+ return null;
378
+ }
379
+ async function resolvePackageEntry(packageDir, packageJson, condition = "import") {
380
+ const manifest = packageJson ?? await readPackageJson(packageDir);
381
+ for (const target of packageResolutionTargets(manifest, "", condition)) {
382
+ const resolved = await resolveEntryCandidate(packageDir, target);
383
+ if (resolved)
384
+ return resolved;
385
+ }
386
+ throw new Error("Package has no contained JavaScript entry point for ESM import.");
387
+ }
388
+ async function listTreeEntries(root) {
389
+ const out = [];
390
+ async function visit(dir) {
391
+ const entries = await readdir(dir, { withFileTypes: true });
392
+ entries.sort((a, b) => a.name.localeCompare(b.name));
393
+ for (const entry of entries) {
394
+ const absolute = path2.join(dir, entry.name);
395
+ const relative = toPosixPath(path2.relative(root, absolute));
396
+ if (relative === VENDOR_RECEIPT_FILE)
397
+ continue;
398
+ if (entry.isSymbolicLink())
399
+ throw new Error(`Vendor tree contains a symbolic link: ${relative}`);
400
+ if (entry.isDirectory()) {
401
+ out.push({ relative, directory: true });
402
+ await visit(absolute);
403
+ } else if (entry.isFile())
404
+ out.push({ relative, directory: false });
405
+ else
406
+ throw new Error(`Vendor tree contains an unsupported filesystem entry: ${relative}`);
407
+ }
408
+ }
409
+ await visit(root);
410
+ return out;
411
+ }
412
+ async function computeVendorTreeDigest(root) {
413
+ const entries = await listTreeEntries(root);
414
+ const files = entries.filter((entry) => !entry.directory);
415
+ if (files.length > MAX_VERIFIED_FILES) {
416
+ throw new Error(`Vendor tree contains more than ${MAX_VERIFIED_FILES} files.`);
417
+ }
418
+ const seen = new Set;
419
+ const hash = createHash("sha256");
420
+ let bytes = 0;
421
+ for (const entry of entries) {
422
+ const { relative } = entry;
423
+ assertWindowsSafeRelativePath(relative);
424
+ const key = windowsPathKey(relative);
425
+ if (seen.has(key))
426
+ throw new Error(`Vendor tree has a Windows-colliding path: ${relative}`);
427
+ seen.add(key);
428
+ if (entry.directory)
429
+ continue;
430
+ const absolute = path2.join(root, ...relative.split("/"));
431
+ const info = await lstat(absolute);
432
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1) {
433
+ throw new Error(`Vendor tree file is not an independent regular file: ${relative}`);
434
+ }
435
+ bytes += info.size;
436
+ if (bytes > MAX_VERIFIED_BYTES) {
437
+ throw new Error(`Vendor tree exceeds ${Math.round(MAX_VERIFIED_BYTES / 1024 / 1024)}MB.`);
438
+ }
439
+ hash.update(`${Buffer.byteLength(relative)}:${relative}:${info.size}\x00`);
440
+ for await (const chunk of createReadStream(absolute))
441
+ hash.update(chunk);
442
+ }
443
+ return { digest: hash.digest("hex"), files: files.length, bytes };
444
+ }
445
+ async function assertVendorPackageOwnership(root, packagePaths) {
446
+ const packages = packagePaths.map((relative) => ({
447
+ relative,
448
+ key: windowsPathKey(relative),
449
+ parts: relative.split("/").length
450
+ })).sort((a, b) => b.key.length - a.key.length);
451
+ for (const entry of await listTreeEntries(root)) {
452
+ const actualParts = entry.relative.split("/");
453
+ for (const part of actualParts) {
454
+ if (part.normalize("NFKC").toLowerCase() === "node_modules" && part !== "node_modules") {
455
+ throw new Error(`Vendor tree contains a non-canonical node_modules path: ${entry.relative}`);
456
+ }
457
+ }
458
+ const key = windowsPathKey(entry.relative);
459
+ const structural = packages.some((pkg) => pkg.key === key || pkg.key.startsWith(`${key}/`));
460
+ if (entry.directory && structural)
461
+ continue;
462
+ const owner = packages.find((pkg) => key.startsWith(`${pkg.key}/`));
463
+ if (!owner)
464
+ throw new Error(`Vendor tree path is not owned by a receipt package: ${entry.relative}`);
465
+ const packageRelative = actualParts.slice(owner.parts);
466
+ const nodeModulesAt = packageRelative.findIndex((part) => part.normalize("NFKC").toLowerCase() === "node_modules");
467
+ if (nodeModulesAt < 0)
468
+ continue;
469
+ const generatedEmptyDirectory = entry.directory && nodeModulesAt === 0 && (packageRelative.length === 1 || packageRelative.length === 2 && packageRelative[1].startsWith("@"));
470
+ if (!generatedEmptyDirectory) {
471
+ throw new Error(`Package contains bundled node_modules content: ${entry.relative}`);
472
+ }
473
+ }
474
+ }
475
+ async function recomputeReceiptEntry(packageDir, packagePath, manifest, condition) {
476
+ try {
477
+ const entry = await resolvePackageEntry(packageDir, manifest, condition);
478
+ return `${packagePath}/${entry}`;
479
+ } catch (error) {
480
+ if (error instanceof Error && error.message.includes("no contained JavaScript entry point")) {
481
+ return;
482
+ }
483
+ throw error;
484
+ }
485
+ }
486
+ async function resolveVendoredPluginInstallRoot(projectDir, expectedName, expectedVersion) {
487
+ const requestedProjectRoot = path2.resolve(projectDir);
488
+ const requestedInstallRoot = vendoredNpmPluginRoot(requestedProjectRoot, expectedName, expectedVersion);
489
+ let installInfo;
490
+ try {
491
+ installInfo = await lstat(requestedInstallRoot);
492
+ } catch (error) {
493
+ if (error.code === "ENOENT")
494
+ return null;
495
+ throw error;
496
+ }
497
+ if (!installInfo.isDirectory() || installInfo.isSymbolicLink()) {
498
+ throw new Error("Vendor install root is not a normal directory.");
499
+ }
500
+ const [realProjectRoot, installRoot] = await Promise.all([
501
+ realpath(requestedProjectRoot),
502
+ realpath(requestedInstallRoot)
503
+ ]);
504
+ if (!isContained(realProjectRoot, installRoot)) {
505
+ throw new Error("Vendor install root resolves outside the book folder.");
506
+ }
507
+ return installRoot;
508
+ }
509
+ function reconcileReceiptGraph(receipt, packages, manifests) {
510
+ if (!Array.isArray(receipt.skipped))
511
+ throw new Error("Vendor receipt skipped list is invalid.");
512
+ const declarations = new Map;
513
+ for (const [packagePath, manifest] of manifests) {
514
+ declarations.set(packagePath, dependencyDeclarations(manifest));
515
+ }
516
+ const skippedByEdge = new Map;
517
+ for (const skipped of receipt.skipped) {
518
+ if (!packages.has(skipped.from) || !isValidNpmPackageName(skipped.name) || typeof skipped.selector !== "string" || !["optional", "optional-peer"].includes(skipped.kind) || typeof skipped.reason !== "string") {
519
+ throw new Error("Vendor receipt contains an invalid skipped dependency.");
520
+ }
521
+ const declared = declarations.get(skipped.from);
522
+ const expectedSelector = skipped.kind === "optional" ? declared.optional[skipped.name] : declared.optionalPeers[skipped.name];
523
+ if (expectedSelector !== skipped.selector) {
524
+ throw new Error(`Skipped dependency is not declared as ${skipped.kind}: ${skipped.name}`);
525
+ }
526
+ const key = `${skipped.from}\x00${skipped.name}`;
527
+ if (skippedByEdge.has(key))
528
+ throw new Error(`Duplicate skipped dependency: ${skipped.name}`);
529
+ skippedByEdge.set(key, skipped);
530
+ }
531
+ for (const pkg of receipt.packages) {
532
+ if (!object(pkg.dependencies))
533
+ throw new Error(`Invalid dependency map for ${pkg.name}.`);
534
+ const declared = declarations.get(pkg.path);
535
+ for (const [dependency, target] of Object.entries(pkg.dependencies)) {
536
+ const targetPackage = packages.get(target);
537
+ if (!isValidNpmPackageName(dependency) || !targetPackage || targetPackage.name !== dependency) {
538
+ throw new Error(`Broken dependency edge ${pkg.name} -> ${dependency}.`);
539
+ }
540
+ const selector = declared.required[dependency] ?? declared.optional[dependency];
541
+ if (!selector)
542
+ throw new Error(`Undeclared dependency edge ${pkg.name} -> ${dependency}.`);
543
+ if (unsupportedRegistrySelector(selector)) {
544
+ throw new Error(`Unsupported selector on dependency edge ${pkg.name} -> ${dependency}.`);
545
+ }
546
+ const range = validRange(selector);
547
+ if (range && !satisfies(targetPackage.version, range)) {
548
+ throw new Error(`Dependency edge ${pkg.name} -> ${dependency}@${targetPackage.version} does not satisfy ${selector}.`);
549
+ }
550
+ if (skippedByEdge.has(`${pkg.path}\x00${dependency}`)) {
551
+ throw new Error(`Dependency is both installed and skipped: ${pkg.name} -> ${dependency}.`);
552
+ }
553
+ }
554
+ for (const dependency of Object.keys(declared.required)) {
555
+ if (!(dependency in pkg.dependencies)) {
556
+ throw new Error(`Missing required dependency edge ${pkg.name} -> ${dependency}.`);
557
+ }
558
+ }
559
+ for (const dependency of Object.keys(declared.optional)) {
560
+ if (!(dependency in pkg.dependencies) && !skippedByEdge.has(`${pkg.path}\x00${dependency}`)) {
561
+ throw new Error(`Optional dependency is neither installed nor skipped: ${pkg.name} -> ${dependency}.`);
562
+ }
563
+ }
564
+ for (const dependency of Object.keys(declared.optionalPeers)) {
565
+ if (!skippedByEdge.has(`${pkg.path}\x00${dependency}`)) {
566
+ throw new Error(`Optional peer dependency is not recorded as skipped: ${pkg.name} -> ${dependency}.`);
567
+ }
568
+ }
569
+ }
570
+ }
571
+ async function verifyVendoredPlugin(projectDir, expectedName, expectedVersion) {
572
+ try {
573
+ const installRoot = await resolveVendoredPluginInstallRoot(projectDir, expectedName, expectedVersion);
574
+ if (!installRoot)
575
+ return null;
576
+ const receiptPath = path2.join(installRoot, VENDOR_RECEIPT_FILE);
577
+ let raw;
578
+ try {
579
+ const info = await lstat(receiptPath);
580
+ if (!info.isFile() || info.isSymbolicLink() || info.nlink !== 1 || info.size > 1024 * 1024) {
581
+ throw new Error("Vendor receipt is not a small regular file.");
582
+ }
583
+ raw = await readFile(receiptPath, "utf8");
584
+ } catch (error) {
585
+ if (error.code === "ENOENT")
586
+ return null;
587
+ throw error;
588
+ }
589
+ const receipt = JSON.parse(raw);
590
+ if (receipt.schemaVersion !== VENDOR_RECEIPT_VERSION) {
591
+ throw new Error(`Unsupported vendor receipt schema ${String(receipt.schemaVersion)}.`);
592
+ }
593
+ if (receipt.root?.name !== expectedName || receipt.root?.version !== expectedVersion) {
594
+ throw new Error("Vendor receipt identity does not match the manifest entry.");
595
+ }
596
+ if (!Array.isArray(receipt.packages) || receipt.packages.length === 0) {
597
+ throw new Error("Vendor receipt has no package graph.");
598
+ }
599
+ const tree = await computeVendorTreeDigest(installRoot);
600
+ if (receipt.tree?.algorithm !== "sha256" || receipt.tree.digest !== tree.digest || receipt.tree.files !== tree.files || receipt.tree.bytes !== tree.bytes) {
601
+ throw new Error("Vendor tree hash does not match its receipt.");
602
+ }
603
+ const packages = new Map;
604
+ const manifests = new Map;
605
+ const verifiedPackages = [];
606
+ const pathKeys = new Set;
607
+ for (const pkg of receipt.packages) {
608
+ const relative = safeReceiptPath(pkg.path, "Package receipt path");
609
+ const key = windowsPathKey(relative);
610
+ if (pathKeys.has(key))
611
+ throw new Error(`Duplicate package receipt path: ${relative}`);
612
+ pathKeys.add(key);
613
+ if (!isValidNpmPackageName(pkg.name) || !isExactNpmVersion(pkg.version)) {
614
+ throw new Error(`Invalid package identity in receipt: ${pkg.name}@${pkg.version}`);
615
+ }
616
+ const expectedSuffix = `node_modules/${pkg.name}`;
617
+ if (relative !== expectedSuffix && !relative.endsWith(`/${expectedSuffix}`)) {
618
+ throw new Error(`Package receipt path does not match ${pkg.name}: ${relative}`);
619
+ }
620
+ if (typeof pkg.integrity !== "string" || !/^(?:sha512|sha384|sha256|sha1)-(?:hex-)?[A-Za-z0-9+/=]+$/.test(pkg.integrity) || typeof pkg.legacySha1 !== "boolean" || pkg.legacySha1 !== pkg.integrity.startsWith("sha1-")) {
621
+ throw new Error(`Package receipt is missing provenance: ${pkg.name}@${pkg.version}`);
622
+ }
623
+ const tarball = new URL(pkg.tarball);
624
+ if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
625
+ throw new Error(`Package receipt has an invalid tarball URL: ${pkg.name}@${pkg.version}`);
626
+ }
627
+ const packageDir = path2.resolve(installRoot, ...relative.split("/"));
628
+ if (!isContained(installRoot, packageDir))
629
+ throw new Error(`Package escapes vendor root: ${relative}`);
630
+ const realPackageDir2 = await realpath(packageDir);
631
+ if (!isContained(installRoot, realPackageDir2)) {
632
+ throw new Error(`Package resolves outside the vendor root: ${relative}`);
633
+ }
634
+ const manifest = await readPackageJson(packageDir);
635
+ if (manifest.name !== pkg.name || manifest.version !== pkg.version) {
636
+ throw new Error(`Installed package identity changed: ${pkg.name}@${pkg.version}`);
637
+ }
638
+ const [expectedEntry, expectedRequireEntry] = await Promise.all([
639
+ recomputeReceiptEntry(packageDir, relative, manifest, "import"),
640
+ recomputeReceiptEntry(packageDir, relative, manifest, "require")
641
+ ]);
642
+ if (pkg.entry !== expectedEntry || pkg.requireEntry !== expectedRequireEntry) {
643
+ throw new Error(`Recorded package entries do not match package.json: ${pkg.name}@${pkg.version}`);
644
+ }
645
+ packages.set(relative, pkg);
646
+ manifests.set(relative, manifest);
647
+ verifiedPackages.push({
648
+ path: relative,
649
+ name: pkg.name,
650
+ packageDir: realPackageDir2,
651
+ manifest,
652
+ entryPath: await verifyReceiptEntry(installRoot, relative, pkg.entry, "Package import entry"),
653
+ requireEntryPath: await verifyReceiptEntry(installRoot, relative, pkg.requireEntry, "Package require entry"),
654
+ dependencies: pkg.dependencies
655
+ });
656
+ }
657
+ await assertVendorPackageOwnership(installRoot, receipt.packages.map((pkg) => pkg.path));
658
+ reconcileReceiptGraph(receipt, packages, manifests);
659
+ const rootPackagePath = safeReceiptPath(receipt.root.packagePath, "Root package path");
660
+ const rootPackage = packages.get(rootPackagePath);
661
+ if (!rootPackage || rootPackage.name !== expectedName || rootPackage.version !== expectedVersion) {
662
+ throw new Error("Vendor receipt root package is missing.");
663
+ }
664
+ const reachablePackages = new Set([rootPackagePath]);
665
+ const pendingPackages = [rootPackagePath];
666
+ while (pendingPackages.length > 0) {
667
+ const current = packages.get(pendingPackages.pop());
668
+ for (const target of Object.values(current.dependencies)) {
669
+ if (reachablePackages.has(target))
670
+ continue;
671
+ reachablePackages.add(target);
672
+ pendingPackages.push(target);
673
+ }
674
+ }
675
+ if (reachablePackages.size !== receipt.packages.length) {
676
+ throw new Error("Vendor receipt contains a package that is not reachable from its root.");
677
+ }
678
+ const rootManifest = manifests.get(rootPackagePath);
679
+ if (receipt.root.format !== "module" && receipt.root.format !== "commonjs") {
680
+ throw new Error("Vendor receipt root format is invalid.");
681
+ }
682
+ const entryRelative = safeReceiptPath(receipt.root.entry, "Plugin entry path");
683
+ if (!entryRelative.startsWith(`${rootPackagePath}/`)) {
684
+ throw new Error("Plugin entry is outside its package.");
685
+ }
686
+ const expectedRootEntry = receipt.root.format === "commonjs" ? rootPackage.requireEntry ?? rootPackage.entry : rootPackage.entry;
687
+ if (entryRelative !== expectedRootEntry) {
688
+ throw new Error("Plugin entry does not match the package graph entry.");
689
+ }
690
+ if (packageEntryFormat(rootManifest, entryRelative) !== receipt.root.format) {
691
+ throw new Error("Plugin entry format does not match its package metadata.");
692
+ }
693
+ const entryPath = path2.resolve(installRoot, ...entryRelative.split("/"));
694
+ const rootPackageDir = path2.resolve(installRoot, ...rootPackagePath.split("/"));
695
+ const [realPackageDir, realEntry] = await Promise.all([
696
+ realpath(rootPackageDir),
697
+ realpath(entryPath)
698
+ ]);
699
+ if (!isContained(installRoot, realEntry) || !isContained(realPackageDir, realEntry)) {
700
+ throw new Error("Plugin entry resolves outside the verified vendor tree.");
701
+ }
702
+ const entryInfo = await lstat(realEntry);
703
+ if (!entryInfo.isFile() || entryInfo.isSymbolicLink()) {
704
+ throw new Error("Plugin entry is not a regular file.");
705
+ }
706
+ return {
707
+ installRoot,
708
+ entryPath: realEntry,
709
+ format: receipt.root.format,
710
+ packages: verifiedPackages,
711
+ receipt
712
+ };
713
+ } catch (error) {
714
+ throw new Error(`Vendored plugin "${expectedName}@${expectedVersion}" failed verification: ` + `${error instanceof Error ? error.message : String(error)} Reinstall it before building.`, { cause: error });
715
+ }
716
+ }
717
+
718
+ // src/lib/source-provider.ts
719
+ var noopLogger = {
720
+ debug: () => {},
721
+ info: () => {},
722
+ warn: () => {},
723
+ error: () => {}
724
+ };
725
+ function gitScopeFor(source) {
726
+ return source.repoRoot || source.path;
727
+ }
728
+ var HISTORY_PAGE_LIMIT = 100;
729
+ var DEFAULT_AUTHOR = "gutterpress";
730
+ var DEFAULT_EMAIL = "noreply@gutterpress.local";
731
+ var DEFAULT_BRANCH = "main";
732
+ var SNAPSHOT_STAGING_MARKER = "gutterpress-snapshot-staging";
733
+ var repoQueues = new Map;
734
+ function repoLockKey(projectDir) {
735
+ const resolved = path3.resolve(projectDir);
736
+ try {
737
+ return gitFs.realpathSync(resolved);
738
+ } catch {
739
+ return resolved;
740
+ }
741
+ }
742
+ function withRepoLock(projectDir, fn) {
743
+ const key = repoLockKey(projectDir);
744
+ const prev = repoQueues.get(key) ?? Promise.resolve();
745
+ const run = prev.then(fn, fn);
746
+ const tail = run.then(() => {
747
+ return;
748
+ }, () => {
749
+ return;
750
+ });
751
+ repoQueues.set(key, tail);
752
+ tail.then(() => {
753
+ if (repoQueues.get(key) === tail)
754
+ repoQueues.delete(key);
755
+ });
756
+ return run;
757
+ }
758
+ function __repoLockQueueSizeForTests() {
759
+ return repoQueues.size;
760
+ }
761
+ function gitAuthor(name, email) {
762
+ const n = (name ?? "").trim();
763
+ const e = (email ?? "").trim();
764
+ return { name: n || DEFAULT_AUTHOR, email: e || DEFAULT_EMAIL };
765
+ }
766
+ async function readGitAuthor(dir) {
767
+ const [name, email] = await Promise.all([
768
+ git.getConfig({ fs: gitFs, dir, path: "user.name" }).catch(() => {
769
+ return;
770
+ }),
771
+ git.getConfig({ fs: gitFs, dir, path: "user.email" }).catch(() => {
772
+ return;
773
+ })
774
+ ]);
775
+ return {
776
+ name: typeof name === "string" ? name : undefined,
777
+ email: typeof email === "string" ? email : undefined
778
+ };
779
+ }
780
+ async function resolveGitAuthor(dir, name, email) {
781
+ const existing = await readGitAuthor(dir);
782
+ return gitAuthor(name || existing.name, email || existing.email);
783
+ }
784
+ async function listWorkdirChanges(dir, cache = {}) {
785
+ const adds = [];
786
+ const removes = [];
787
+ await git.walk({
788
+ fs: gitFs,
789
+ dir,
790
+ cache,
791
+ trees: [git.WORKDIR(), git.STAGE()],
792
+ map: async (filepath, [workdir, stage]) => {
793
+ if (filepath === ".")
794
+ return;
795
+ if (!stage && workdir && (filepath.startsWith(".git-damaged") || await git.isIgnored({ fs: gitFs, dir, filepath }))) {
796
+ return null;
797
+ }
798
+ const [wType, sType] = await Promise.all([
799
+ workdir ? workdir.type() : Promise.resolve(undefined),
800
+ stage ? stage.type() : Promise.resolve(undefined)
801
+ ]);
802
+ if (wType === "tree" || sType === "tree") {
803
+ if (sType === "blob")
804
+ removes.push(filepath);
805
+ else if (wType === "blob")
806
+ adds.push(filepath);
807
+ return;
808
+ }
809
+ if (wType === "blob" && !sType) {
810
+ adds.push(filepath);
811
+ } else if (!wType && sType === "blob") {
812
+ removes.push(filepath);
813
+ } else if (wType === "blob" && sType === "blob") {
814
+ const workdirOid = await workdir.oid();
815
+ const stageOid = await stage.oid();
816
+ if (workdirOid !== stageOid) {
817
+ adds.push(filepath);
818
+ } else {
819
+ const fileBytes = await gitFs.promises.readFile(path3.join(dir, filepath));
820
+ const { oid: actualOid } = await git.hashBlob({ object: fileBytes });
821
+ if (actualOid !== stageOid)
822
+ adds.push(filepath);
823
+ }
824
+ }
825
+ return;
826
+ }
827
+ });
828
+ return { adds, removes };
829
+ }
830
+ async function stageChanges(dir, changes, cache) {
831
+ if (changes.adds.length > 0) {
832
+ await git.add({ fs: gitFs, dir, cache, filepath: changes.adds });
833
+ }
834
+ for (const filepath of changes.removes) {
835
+ await git.remove({ fs: gitFs, dir, cache, filepath });
836
+ }
837
+ }
838
+ async function hasPendingChanges(dir, cache = {}) {
839
+ const { adds, removes } = await listWorkdirChanges(dir, cache);
840
+ return adds.length > 0 || removes.length > 0;
841
+ }
842
+ async function hasUncommittedChanges(dir, cache = {}) {
843
+ if (await hasPendingChanges(dir, cache))
844
+ return true;
845
+ return !await stageMatchesHead(dir, cache);
846
+ }
847
+ async function stageMatchesHead(dir, cache) {
848
+ let headOid;
849
+ try {
850
+ headOid = await git.resolveRef({ fs: gitFs, dir, ref: "HEAD" });
851
+ } catch {
852
+ return false;
853
+ }
854
+ let differs = false;
855
+ await git.walk({
856
+ fs: gitFs,
857
+ dir,
858
+ cache,
859
+ trees: [git.TREE({ ref: headOid }), git.STAGE()],
860
+ map: async (filepath, [tree, stage]) => {
861
+ if (differs || filepath === ".")
862
+ return differs ? null : undefined;
863
+ const [tType, sType] = await Promise.all([
864
+ tree ? tree.type() : Promise.resolve(undefined),
865
+ stage ? stage.type() : Promise.resolve(undefined)
866
+ ]);
867
+ if (tType === "tree" || sType === "tree")
868
+ return;
869
+ if (!tType !== !sType) {
870
+ differs = true;
871
+ return null;
872
+ }
873
+ if (!tType || !sType)
874
+ return;
875
+ const [tOid, sOid] = await Promise.all([tree.oid(), stage.oid()]);
876
+ if (tOid !== sOid)
877
+ differs = true;
878
+ return null;
879
+ }
880
+ });
881
+ return !differs;
882
+ }
883
+
884
+ class LocalFolderSourceProvider {
885
+ source;
886
+ capabilities;
887
+ constructor(source) {
888
+ this.source = source;
889
+ this.capabilities = capabilitiesFor(source);
890
+ }
891
+ async initVersionHistory(options) {
892
+ const dir = options.projectDir;
893
+ if (await findEnclosingRepoDir(dir) !== undefined) {
894
+ throw new Error("This folder is already inside a versioned book, so gutterpress " + "won't create a separate history here.");
895
+ }
896
+ return withRepoLock(dir, async () => {
897
+ const cache = {};
898
+ await git.init({ fs: gitFs, dir, defaultBranch: DEFAULT_BRANCH });
899
+ await stageChanges(dir, await listWorkdirChanges(dir, cache), cache);
900
+ await git.commit({
901
+ fs: gitFs,
902
+ dir,
903
+ cache,
904
+ message: options.initialMessage?.trim() || "Created project",
905
+ author: await resolveGitAuthor(dir, options.authorName, options.authorEmail)
906
+ });
907
+ return {
908
+ type: "local-git-folder",
909
+ path: dir,
910
+ repoRoot: dir,
911
+ subPath: "",
912
+ hasRemote: false,
913
+ branch: DEFAULT_BRANCH
914
+ };
915
+ });
916
+ }
917
+ snapshot() {
918
+ return Promise.reject(new Error("This book has no version history yet. Enable version history first."));
919
+ }
920
+ listHistory() {
921
+ return Promise.resolve([]);
922
+ }
923
+ listHistoryPage() {
924
+ return Promise.resolve({ entries: [], hasMore: false });
925
+ }
926
+ restore() {
927
+ return Promise.reject(new Error("This book has no version history yet. Enable version history first."));
928
+ }
929
+ }
930
+
931
+ class LocalGitSourceProvider {
932
+ source;
933
+ capabilities;
934
+ dir;
935
+ constructor(source) {
936
+ this.source = source;
937
+ this.capabilities = capabilitiesFor(source);
938
+ this.dir = gitScopeFor(source);
939
+ }
940
+ async initVersionHistory(options) {
941
+ try {
942
+ await this.snapshot({
943
+ projectDir: options.projectDir,
944
+ message: options.initialMessage?.trim() || "Created project",
945
+ authorName: options.authorName,
946
+ authorEmail: options.authorEmail
947
+ });
948
+ } catch (e) {
949
+ if (!isNoChangesError(e))
950
+ throw e;
951
+ }
952
+ return this.source;
953
+ }
954
+ snapshot(options) {
955
+ return withRepoLock(this.dir, () => this.snapshotUnlocked(options));
956
+ }
957
+ snapshotUnlocked(options) {
958
+ return snapshotWorkingTreeUnlocked({ ...options, repoRoot: this.dir });
959
+ }
960
+ async listHistory(projectDir) {
961
+ return (await this.listHistoryPage(projectDir)).entries;
962
+ }
963
+ async listHistoryPage(_projectDir, options = {}) {
964
+ const dir = this.dir;
965
+ const limit = Math.min(500, Math.max(1, options.limit ?? HISTORY_PAGE_LIMIT));
966
+ const before = options.before;
967
+ let commits;
968
+ try {
969
+ commits = await git.log({
970
+ fs: gitFs,
971
+ dir,
972
+ cache: {},
973
+ depth: limit + 2,
974
+ ...before ? { ref: before } : {}
975
+ });
976
+ } catch (e) {
977
+ if (before && e?.code === "NotFoundError") {
978
+ return { entries: [], hasMore: false };
979
+ }
980
+ throw e;
981
+ }
982
+ const seen = new Set;
983
+ let filtered = commits.filter((c) => {
984
+ if (c.oid === before || seen.has(c.oid))
985
+ return false;
986
+ seen.add(c.oid);
987
+ return true;
988
+ });
989
+ const hasMore = filtered.length > limit;
990
+ if (hasMore)
991
+ filtered = filtered.slice(0, limit);
992
+ return {
993
+ entries: filtered.map((c) => ({
994
+ id: c.oid,
995
+ message: c.commit.message.trim(),
996
+ timestamp: c.commit.author.timestamp * 1000,
997
+ author: c.commit.author.name
998
+ })),
999
+ hasMore
1000
+ };
1001
+ }
1002
+ restore(options) {
1003
+ return withRepoLock(this.dir, () => this.restoreUnlocked(options));
1004
+ }
1005
+ async restoreUnlocked(options) {
1006
+ await git.checkout({
1007
+ fs: gitFs,
1008
+ dir: this.dir,
1009
+ cache: {},
1010
+ ref: options.id,
1011
+ force: true,
1012
+ noUpdateHead: true
1013
+ });
1014
+ }
1015
+ }
1016
+ async function snapshotWorkingTreeUnlocked(options) {
1017
+ const dir = options.repoRoot ?? options.projectDir;
1018
+ const logger = options.logFile ? createFileLogger(options.logFile, "snapshot") : noopLogger;
1019
+ const cache = options.cache ?? {};
1020
+ const stagingMarker = snapshotStagingMarkerPath(dir);
1021
+ const staleStaging = gitFs.existsSync(stagingMarker);
1022
+ const changes = await listWorkdirChanges(dir, cache);
1023
+ const workdirClean = changes.adds.length === 0 && changes.removes.length === 0;
1024
+ if (workdirClean && !staleStaging) {
1025
+ logger.debug("snapshot", "no changes — skipping");
1026
+ throw new Error("No changes since the last snapshot — there is nothing new to save.");
1027
+ }
1028
+ if (workdirClean && staleStaging && await stageMatchesHead(dir, cache)) {
1029
+ logger.debug("snapshot", "stale marker outlived a completed commit — clearing, not recommitting");
1030
+ gitFs.rmSync(stagingMarker, { force: true });
1031
+ const [head2] = await git.log({ fs: gitFs, dir, cache, depth: 1 });
1032
+ return {
1033
+ id: head2.oid,
1034
+ message: head2.commit.message.trim(),
1035
+ timestamp: head2.commit.author.timestamp * 1000,
1036
+ author: head2.commit.author.name
1037
+ };
1038
+ }
1039
+ logger.info("snapshot", "committing", {
1040
+ adds: changes.adds.length,
1041
+ removes: changes.removes.length,
1042
+ ...staleStaging ? { recoveredStaleStaging: true } : {}
1043
+ });
1044
+ gitFs.writeFileSync(stagingMarker, "");
1045
+ await stageChanges(dir, changes, cache);
1046
+ const author = await resolveGitAuthor(dir, options.authorName, options.authorEmail);
1047
+ const id = await git.commit({
1048
+ fs: gitFs,
1049
+ dir,
1050
+ cache,
1051
+ message: options.message,
1052
+ author
1053
+ });
1054
+ gitFs.rmSync(stagingMarker, { force: true });
1055
+ const [head] = await git.log({ fs: gitFs, dir, cache, depth: 1 });
1056
+ return {
1057
+ id,
1058
+ message: options.message,
1059
+ timestamp: head ? head.commit.author.timestamp * 1000 : Date.now(),
1060
+ author: author.name
1061
+ };
1062
+ }
1063
+ function isNoChangesError(e) {
1064
+ return e instanceof Error && /no changes since the last snapshot/i.test(e.message);
1065
+ }
1066
+ async function countUnversionedChanges(projectDir) {
1067
+ const source = await detectProjectSource(projectDir);
1068
+ if (source.type !== "local-git-folder")
1069
+ return null;
1070
+ const dir = gitScopeFor(source);
1071
+ const bookPrefix = source.subPath ? `${source.subPath.replace(/\/+$/, "")}/` : "";
1072
+ const appWritten = `${bookPrefix}${PLUGINS_DIR}/${VENDORED_NPM_DIR}/`;
1073
+ return withRepoLock(dir, async () => {
1074
+ const { adds, removes } = await listWorkdirChanges(dir);
1075
+ const mine = new Set([...adds, ...removes].filter((f) => f.startsWith(bookPrefix) && !f.startsWith(appWritten) && path3.posix.basename(f) !== VENDOR_RECEIPT_FILE));
1076
+ return { changedFiles: mine.size, stale: gitFs.existsSync(snapshotStagingMarkerPath(dir)) };
1077
+ });
1078
+ }
1079
+ function providerFor(source) {
1080
+ switch (source.type) {
1081
+ case "local-folder":
1082
+ return new LocalFolderSourceProvider(source);
1083
+ case "local-git-folder":
1084
+ return new LocalGitSourceProvider(source);
1085
+ case "managed-github":
1086
+ throw new Error("Managed GitHub books are not supported yet (#15/#16).");
1087
+ }
1088
+ }
1089
+ function gitDirFor(projectDir) {
1090
+ return path3.join(projectDir, ".git");
1091
+ }
1092
+ function snapshotStagingMarkerPath(projectDir) {
1093
+ return path3.join(gitDirFor(projectDir), SNAPSHOT_STAGING_MARKER);
1094
+ }
1095
+ var RESTORE_BACKUP_MESSAGE = "Automatic backup before restoring an earlier version";
1096
+ var AUTO_SNAPSHOT_MESSAGE = "Automatic snapshot";
1097
+ async function restoreVersionWithBackup(options) {
1098
+ const { projectDir, id, authorName, authorEmail } = options;
1099
+ const source = await detectProjectSource(projectDir);
1100
+ if (source.type !== "local-git-folder") {
1101
+ throw new Error("This book has no version history yet. Enable version history first.");
1102
+ }
1103
+ const provider = new LocalGitSourceProvider(source);
1104
+ const repoDir = gitScopeFor(source);
1105
+ return withRepoLock(repoDir, async () => {
1106
+ let backupId;
1107
+ if (await hasPendingChanges(repoDir)) {
1108
+ const backup = await provider.snapshotUnlocked({
1109
+ projectDir,
1110
+ message: RESTORE_BACKUP_MESSAGE,
1111
+ authorName,
1112
+ authorEmail
1113
+ });
1114
+ backupId = backup.id;
1115
+ }
1116
+ try {
1117
+ await provider.restoreUnlocked({ projectDir, id });
1118
+ } catch (cause) {
1119
+ throw new Error(backupId ? "The restore could not be completed, but your work is safe — it was " + `automatically saved as a backup snapshot (${backupId.slice(0, 7)}) ` + "and appears in your version history." : "The restore could not be completed. Your book files were not changed.", { cause });
1120
+ }
1121
+ return { restoredId: id, backupId };
1122
+ });
1123
+ }
1124
+ var SWITCH_BRANCH_SNAPSHOT_MESSAGE = "Saved before switching copy";
1125
+ async function listLocalBranches(dir) {
1126
+ const source = await detectProjectSource(dir);
1127
+ if (source.type !== "local-git-folder")
1128
+ return null;
1129
+ const repoDir = gitScopeFor(source);
1130
+ const [local, current, remotes] = await Promise.all([
1131
+ git.listBranches({ fs: gitFs, dir: repoDir }),
1132
+ git.currentBranch({ fs: gitFs, dir: repoDir, fullname: false }).catch(() => {
1133
+ return;
1134
+ }),
1135
+ git.listRemotes({ fs: gitFs, dir: repoDir }).catch(() => [])
1136
+ ]);
1137
+ const localNames = new Set(local);
1138
+ const remoteOnly = new Set;
1139
+ for (const { remote } of remotes) {
1140
+ const names = await git.listBranches({ fs: gitFs, dir: repoDir, remote }).catch(() => []);
1141
+ for (const name of names) {
1142
+ if (name === "HEAD")
1143
+ continue;
1144
+ if (!localNames.has(name))
1145
+ remoteOnly.add(name);
1146
+ }
1147
+ }
1148
+ return {
1149
+ current: current ?? null,
1150
+ branches: [...localNames, ...remoteOnly].sort(),
1151
+ remoteOnly: [...remoteOnly].sort()
1152
+ };
1153
+ }
1154
+ async function resolveCopy(repoDir, branch) {
1155
+ const local = await git.resolveRef({ fs: gitFs, dir: repoDir, ref: `refs/heads/${branch}` }).catch(() => {
1156
+ return;
1157
+ });
1158
+ if (local)
1159
+ return { oid: local };
1160
+ const remotes = await git.listRemotes({ fs: gitFs, dir: repoDir }).catch(() => []);
1161
+ for (const { remote } of remotes) {
1162
+ const oid = await git.resolveRef({ fs: gitFs, dir: repoDir, ref: `refs/remotes/${remote}/${branch}` }).catch(() => {
1163
+ return;
1164
+ });
1165
+ if (oid)
1166
+ return { oid, remote };
1167
+ }
1168
+ throw new Error(`Couldn't find a copy named "${branch}" — it may have been renamed or deleted.`);
1169
+ }
1170
+ function isCheckoutConflictError(e) {
1171
+ return e?.code === "CheckoutConflictError";
1172
+ }
1173
+ async function diffCommitPaths(dir, fromOid, toOid, cache) {
1174
+ const changed = [];
1175
+ await git.walk({
1176
+ fs: gitFs,
1177
+ dir,
1178
+ cache,
1179
+ trees: [git.TREE({ ref: fromOid }), git.TREE({ ref: toOid })],
1180
+ map: async (filepath, [from, to]) => {
1181
+ if (filepath === ".")
1182
+ return;
1183
+ const [fType, tType] = await Promise.all([
1184
+ from ? from.type() : Promise.resolve(undefined),
1185
+ to ? to.type() : Promise.resolve(undefined)
1186
+ ]);
1187
+ if (fType === "tree" || tType === "tree")
1188
+ return;
1189
+ if (!fType && !tType)
1190
+ return;
1191
+ if (!fType || !tType) {
1192
+ changed.push(filepath);
1193
+ return;
1194
+ }
1195
+ const [fOid, tOid] = await Promise.all([from.oid(), to.oid()]);
1196
+ if (fOid !== tOid)
1197
+ changed.push(filepath);
1198
+ }
1199
+ });
1200
+ return changed;
1201
+ }
1202
+ async function switchBranch(options) {
1203
+ const { projectDir, branch, authorName, authorEmail } = options;
1204
+ const source = await detectProjectSource(projectDir);
1205
+ if (source.type !== "local-git-folder") {
1206
+ throw new Error("This book has no version history yet. Enable version history first.");
1207
+ }
1208
+ const repoDir = gitScopeFor(source);
1209
+ return withRepoLock(repoDir, async () => {
1210
+ const cache = {};
1211
+ try {
1212
+ await snapshotWorkingTreeUnlocked({
1213
+ projectDir,
1214
+ repoRoot: repoDir,
1215
+ message: SWITCH_BRANCH_SNAPSHOT_MESSAGE,
1216
+ authorName,
1217
+ authorEmail,
1218
+ cache
1219
+ });
1220
+ } catch (e) {
1221
+ if (!isNoChangesError(e))
1222
+ throw e;
1223
+ }
1224
+ const fromOid = await git.resolveRef({ fs: gitFs, dir: repoDir, ref: "HEAD" }).catch(() => {
1225
+ return;
1226
+ });
1227
+ const { oid: toOid, remote } = await resolveCopy(repoDir, branch);
1228
+ try {
1229
+ await git.checkout({
1230
+ fs: gitFs,
1231
+ dir: repoDir,
1232
+ cache,
1233
+ ref: branch,
1234
+ force: false,
1235
+ ...remote ? { remote } : {}
1236
+ });
1237
+ } catch (e) {
1238
+ if (isCheckoutConflictError(e)) {
1239
+ throw new Error("Couldn't switch copies — some files changed on disk right as the switch " + "started. Try again.", { cause: e });
1240
+ }
1241
+ throw e;
1242
+ }
1243
+ const changedPaths = fromOid && fromOid !== toOid ? await diffCommitPaths(repoDir, fromOid, toOid, cache) : [];
1244
+ return {
1245
+ current: branch,
1246
+ changedFiles: changedPaths.map((p) => path3.join(repoDir, p))
1247
+ };
1248
+ });
1249
+ }
1250
+
1251
+ export { npmRegistryUrl, PLUGINS_DIR, VENDORED_NPM_DIR, VENDOR_RECEIPT_FILE, VENDOR_RECEIPT_VERSION, isValidNpmPackageName, parseNpmPluginSpec, isExactNpmVersion, vendoredNpmPluginRoot, vendoredNpmPluginPackageDir, packageEntryFormat, assertWindowsSafeRelativePath, windowsPathKey, packageResolutionTargets, resolvePackageEntry, computeVendorTreeDigest, resolveVendoredPluginInstallRoot, verifyVendoredPlugin, gitScopeFor, HISTORY_PAGE_LIMIT, withRepoLock, __repoLockQueueSizeForTests, resolveGitAuthor, listWorkdirChanges, stageChanges, hasPendingChanges, hasUncommittedChanges, snapshotWorkingTreeUnlocked, isNoChangesError, countUnversionedChanges, providerFor, gitDirFor, snapshotStagingMarkerPath, RESTORE_BACKUP_MESSAGE, AUTO_SNAPSHOT_MESSAGE, restoreVersionWithBackup, SWITCH_BRANCH_SNAPSHOT_MESSAGE, listLocalBranches, switchBranch };