gutterpress 0.10.11 → 0.11.0

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 (35) hide show
  1. package/README.md +1 -1
  2. package/dist/{README-7x5akcns.md → README-3nj919rm.md} +10 -8
  3. package/dist/{README-3zk4tgnj.md → README-8b33qbjw.md} +14 -21
  4. package/dist/api/index.js +2 -2
  5. package/dist/{audit-ebbbhpx7.js → audit-xcreaxvj.js} +4 -4
  6. package/dist/{build-zrajk3s2.js → build-j433exnc.js} +4 -4
  7. package/dist/{cli-gxq7yz5f.js → cli-9a60c9wn.js} +1 -1
  8. package/dist/{cli-36r50yr3.js → cli-r0ycvdr9.js} +1 -1
  9. package/dist/{cli-k8f9eb12.js → cli-sekhwex4.js} +5 -5
  10. package/dist/{cli-hz44te1m.js → cli-xa9y4np2.js} +1426 -1405
  11. package/dist/cli.js +14 -14
  12. package/dist/{doctor-78qft9g7.js → doctor-mmgg0p64.js} +2 -2
  13. package/dist/{engine-qtmvaqt7.js → engine-cbc17ne4.js} +1 -1
  14. package/dist/{engine-pt2acjb5.js → engine-w5ed5hmb.js} +2 -2
  15. package/dist/{ext-3g1mk8mb.js → ext-2vjqcmsh.js} +4 -4
  16. package/dist/{index-a0e2253t.js → index-12pvhbyz.js} +1 -1
  17. package/dist/{index-5dgxeevv.js → index-6n15e1ze.js} +5 -5
  18. package/dist/{index-03cqyyyk.js → index-g6t4qdzm.js} +1764 -1748
  19. package/dist/index.js +3 -3
  20. package/dist/lib/markdown/index.d.ts +8 -16
  21. package/dist/lib/markdown/plugins.d.ts +9 -4
  22. package/dist/lib/markdown/renderer.d.ts +18 -0
  23. package/dist/lib/project-scaffold.d.ts +1 -1
  24. package/dist/{lint-va95zh6j.js → lint-485wgyn0.js} +4 -4
  25. package/dist/{new-3ex11eyq.js → new-nayk48pr.js} +4 -4
  26. package/dist/plugin-spbcem4j.css +103 -0
  27. package/dist/{plugin.test.js-k2r87k04.tpl → plugin.test.js-nw7c84s6.tpl} +5 -6
  28. package/dist/{preflight-q8w6dnjh.js → preflight-wf4j4vs6.js} +4 -4
  29. package/dist/{preview-azatsrtp.js → preview-ke5fyhtt.js} +4 -4
  30. package/dist/{publish-5tqzepkb.js → publish-prnv6hf1.js} +4 -4
  31. package/dist/render.js +1 -1
  32. package/dist/{tokens-2sracx1w.css → tokens-7984461t.css} +8 -8
  33. package/dist/{validate-rp613ejg.js → validate-98fk1yqg.js} +4 -4
  34. package/package.json +1 -1
  35. package/dist/plugin-m2w7g47v.css +0 -114
@@ -18,7 +18,7 @@ import {
18
18
  resolveChromiumExecutable,
19
19
  run,
20
20
  spawnCapture
21
- } from "./index-5dgxeevv.js";
21
+ } from "./index-6n15e1ze.js";
22
22
  import {
23
23
  gitFs,
24
24
  gitScopeFor,
@@ -3115,14 +3115,30 @@ function applyPlugins(md, plugins) {
3115
3115
  }
3116
3116
  }
3117
3117
  }
3118
- function collectPluginCss(plugins) {
3119
- return plugins.map((p) => p.css).filter((css) => typeof css === "string" && css.length > 0).join(`
3120
-
3121
- `);
3122
- }
3123
3118
  function collectPluginStylePaths(plugins) {
3124
3119
  return plugins.flatMap((p) => p.styles ?? []);
3125
3120
  }
3121
+ function collectPluginStyleGroups(plugins) {
3122
+ const taken = new Set;
3123
+ const groups = [];
3124
+ for (const p of plugins) {
3125
+ const paths = p.styles ?? [];
3126
+ const css = typeof p.css === "string" && p.css.length > 0 ? p.css : undefined;
3127
+ if (paths.length === 0 && !css)
3128
+ continue;
3129
+ const base = `ext.${layerSlug(p.name)}`;
3130
+ let layer = base;
3131
+ for (let n = 2;taken.has(layer); n++)
3132
+ layer = `${base}-${n}`;
3133
+ taken.add(layer);
3134
+ groups.push({ name: p.name, layer, paths, ...css ? { css } : {} });
3135
+ }
3136
+ return groups;
3137
+ }
3138
+ function layerSlug(name) {
3139
+ const slug = name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
3140
+ return /^[a-z]/.test(slug) ? slug : `x${slug}`;
3141
+ }
3126
3142
 
3127
3143
  // src/lib/markdown/assemble.ts
3128
3144
  async function assembleBookHtml(opts) {
@@ -3180,7 +3196,7 @@ ${MARKER_CSS.trim()}
3180
3196
  @layer gp.vocab {
3181
3197
  ${GUTTERPRESS_CSS.trim()}
3182
3198
  }`,
3183
- pluginCss ? `/* user plugin css */
3199
+ pluginCss ? `/* extension css */
3184
3200
  ${pluginCss.trim()}` : null,
3185
3201
  projectCss ? `/* project css */
3186
3202
  ${projectCss.trim()}` : null
@@ -3611,16 +3627,34 @@ async function resolveActiveMarkdownFiles(inputDir, configuredFiles) {
3611
3627
  async function renderChapters(inputDir, opts = {}) {
3612
3628
  const styles = await resolveActiveStyles(inputDir, opts.styles);
3613
3629
  const inlined = await inlineStyles(inputDir, styles);
3614
- const pluginInlined = await inlineStyles(inputDir, opts.pluginStylePaths ?? []);
3615
- const styleWarnings = [...pluginInlined.warnings, ...inlined.warnings];
3630
+ const blocks = [];
3631
+ const layers = [];
3632
+ const styleWarnings = [];
3633
+ const cssAssetCopies = [];
3634
+ for (const group of opts.pluginStyles ?? []) {
3635
+ const groupInlined = await inlineStyles(inputDir, group.paths);
3636
+ styleWarnings.push(...groupInlined.warnings);
3637
+ cssAssetCopies.push(...groupInlined.copies);
3638
+ const css = [groupInlined.css, group.css].filter((s) => !!s && s.trim().length > 0).join(`
3639
+
3640
+ `);
3641
+ if (!css)
3642
+ continue;
3643
+ layers.push(group.layer);
3644
+ blocks.push(`/* ${group.name} */
3645
+ @layer ${group.layer} {
3646
+ ${css.trim()}
3647
+ }`);
3648
+ }
3649
+ styleWarnings.push(...inlined.warnings);
3616
3650
  if (styleWarnings.length > 0)
3617
3651
  opts.onStyleWarnings?.(styleWarnings);
3618
- const cssAssetCopies = [...pluginInlined.copies, ...inlined.copies];
3652
+ cssAssetCopies.push(...inlined.copies);
3619
3653
  if (cssAssetCopies.length > 0)
3620
3654
  opts.onCssAssets?.(cssAssetCopies);
3621
- const pluginCss = [pluginInlined.css, opts.pluginCss].filter((s) => !!s && s.trim().length > 0).join(`
3655
+ const pluginCss = blocks.length > 0 ? [`@layer ${layers.join(", ")};`, ...blocks].join(`
3622
3656
 
3623
- `);
3657
+ `) : "";
3624
3658
  const files = await resolveActiveMarkdownFiles(inputDir, opts.files);
3625
3659
  if (files.length === 0) {
3626
3660
  throw new Error(`No markdown files found in ${inputDir}`);
@@ -3629,7 +3663,7 @@ async function renderChapters(inputDir, opts = {}) {
3629
3663
  files,
3630
3664
  readText: (relPath) => readFile4(join(inputDir, canonicalChapterId(relPath)), "utf-8"),
3631
3665
  projectCss: inlined.css,
3632
- preloadImages: [...pluginInlined.copies.map((c) => c.to), ...inlined.copies.map((c) => c.to)],
3666
+ preloadImages: cssAssetCopies.map((c) => c.to),
3633
3667
  title: opts.title,
3634
3668
  plugins: opts.plugins,
3635
3669
  pluginCss,
@@ -3646,8 +3680,7 @@ async function renderChaptersToFile(inputDir, outDir, opts = {}) {
3646
3680
  styles: opts.styles,
3647
3681
  files: opts.files,
3648
3682
  plugins: opts.plugins,
3649
- pluginCss: opts.pluginCss,
3650
- pluginStylePaths: opts.pluginStylePaths,
3683
+ pluginStyles: opts.pluginStyles,
3651
3684
  onChapterWarnings: opts.onChapterWarnings,
3652
3685
  onImageRefs: opts.onImageRefs,
3653
3686
  onCssAssets: opts.onCssAssets,
@@ -4569,12 +4602,12 @@ async function loadPlugins(configs, baseDir, onError) {
4569
4602
  }
4570
4603
  async function loadPluginsWithCss(configs, baseDir, onError) {
4571
4604
  if (!configs || configs.length === 0) {
4572
- return { plugins: undefined, pluginCss: "", pluginStylePaths: [] };
4605
+ return { plugins: undefined, pluginStyles: [], pluginStylePaths: [] };
4573
4606
  }
4574
4607
  const plugins = await loadPlugins(configs, baseDir, onError);
4575
4608
  return {
4576
4609
  plugins,
4577
- pluginCss: collectPluginCss(plugins),
4610
+ pluginStyles: collectPluginStyleGroups(plugins),
4578
4611
  pluginStylePaths: collectPluginStylePaths(plugins)
4579
4612
  };
4580
4613
  }
@@ -4841,7 +4874,7 @@ import git from "isomorphic-git";
4841
4874
  // package.json
4842
4875
  var package_default = {
4843
4876
  name: "gutterpress",
4844
- version: "0.10.11",
4877
+ version: "0.11.0",
4845
4878
  description: "Markdown-to-PDF converter for professional print layout using a native Chromium print engine and Ghostscript.",
4846
4879
  author: "itlackey",
4847
4880
  license: "MPL-2.0",
@@ -9070,7 +9103,7 @@ async function renderBook(ctx) {
9070
9103
  } else {
9071
9104
  log.info("Using all .md files in alphabetical order");
9072
9105
  }
9073
- const { plugins, pluginCss, pluginStylePaths } = await loadBuildPlugins(ctx);
9106
+ const { plugins, pluginStyles } = await loadBuildPlugins(ctx);
9074
9107
  const imageRefs = [];
9075
9108
  const cssAssets = [];
9076
9109
  const htmlFile = await renderChaptersToFile(renderDir, workDir, {
@@ -9078,8 +9111,7 @@ async function renderBook(ctx) {
9078
9111
  styles: config.styles,
9079
9112
  files: config.source.files,
9080
9113
  plugins,
9081
- pluginCss,
9082
- pluginStylePaths,
9114
+ pluginStyles,
9083
9115
  onChapterWarnings: (file, warnings) => {
9084
9116
  for (const w of warnings) {
9085
9117
  const key = layoutWarningKey(path9.resolve(renderDir, file), w.line, w.message);
@@ -9225,7 +9257,7 @@ class PdfOutput {
9225
9257
  const rawPdf = pdfxMode ? path9.join(stage, "raw.pdf") : path9.resolve(pdfFile);
9226
9258
  await fsp2.mkdir(path9.dirname(path9.resolve(pdfFile)), { recursive: true });
9227
9259
  log.info("Rendering HTML to PDF via the Gutterpress engine (native Chromium pagination)");
9228
- const { buildNativePdf } = await import("./engine-pt2acjb5.js");
9260
+ const { buildNativePdf } = await import("./engine-w5ed5hmb.js");
9229
9261
  const engineDiagnostics = await buildNativePdf(htmlFile, rawPdf, {
9230
9262
  title: config.title,
9231
9263
  author: config.authors.length > 0 ? config.authors.join(", ") : undefined,
@@ -9362,14 +9394,13 @@ function incrementalPreviewEnabled() {
9362
9394
  return process.env.GUTTERPRESS_PREVIEW_INCREMENTAL !== "0";
9363
9395
  }
9364
9396
  async function renderPreviewBook(inputPath, config, opts) {
9365
- const { plugins, pluginCss, pluginStylePaths } = await loadPluginsWithCss(config.extensions, inputPath, (ref, err) => warn2(`Skipping plugin "${ref}" in preview — ${err.message}`));
9397
+ const { plugins, pluginStyles } = await loadPluginsWithCss(config.extensions, inputPath, (ref, err) => warn2(`Skipping plugin "${ref}" in preview — ${err.message}`));
9366
9398
  return renderChapters(inputPath, {
9367
9399
  title: config.title ?? "Document",
9368
9400
  styles: config.styles,
9369
9401
  files: opts.files,
9370
9402
  plugins,
9371
- pluginCss,
9372
- pluginStylePaths,
9403
+ pluginStyles,
9373
9404
  wrapChapters: opts.wrapChapters,
9374
9405
  annotateSourceChapters: true,
9375
9406
  ...opts.onCssAssets ? { onCssAssets: opts.onCssAssets } : {},
@@ -10888,9 +10919,9 @@ async function getSystemDiagnostics() {
10888
10919
  };
10889
10920
  }
10890
10921
  // src/lib/project-scaffold.ts
10891
- import { access, copyFile, cp as cp3, mkdir as mkdir9, readdir as readdir6, readFile as readFile29, stat as stat6, writeFile as writeFile8 } from "node:fs/promises";
10892
- import { constants as FS, existsSync as existsSync13 } from "node:fs";
10893
- import path18 from "node:path";
10922
+ import { access, copyFile, cp as cp2, mkdir as mkdir7, readdir as readdir6, readFile as readFile27, stat as stat5, writeFile as writeFile8 } from "node:fs/promises";
10923
+ import { constants as FS, existsSync as existsSync12 } from "node:fs";
10924
+ import path16 from "node:path";
10894
10925
 
10895
10926
  // src/lib/manifest-doc.ts
10896
10927
  import { existsSync as existsSync11 } from "node:fs";
@@ -10952,1916 +10983,1901 @@ function scalarString(item) {
10952
10983
  return typeof item === "string" ? item : null;
10953
10984
  }
10954
10985
 
10955
- // src/lib/extension-manager.ts
10956
- import { cp as cp2, mkdir as mkdir8, readFile as readFile28, rm as rm4, stat as stat5 } from "node:fs/promises";
10957
- import { existsSync as existsSync12 } from "node:fs";
10958
- import path17 from "node:path";
10959
- import { isSeq as isSeq2, isMap, isScalar, YAMLMap, Scalar } from "yaml";
10960
-
10961
- // src/lib/npm-plugin-installer.ts
10962
- import { createHash as createHash7, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
10963
- import { createReadStream as createReadStream2 } from "node:fs";
10964
- import {
10965
- lstat as lstat2,
10966
- mkdir as mkdir7,
10967
- mkdtemp as mkdtemp2,
10968
- open as open2,
10969
- readFile as readFile27,
10970
- realpath as realpath2,
10971
- rename as rename4,
10972
- rm as rm3
10973
- } from "node:fs/promises";
10974
- import path16 from "node:path";
10975
- import { Transform } from "node:stream";
10976
- import { pipeline } from "node:stream/promises";
10977
- import { createGunzip } from "node:zlib";
10978
- import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
10979
-
10980
- // src/lib/fetch-timeout.ts
10981
- class FetchPassthroughError extends Error {
10982
- }
10983
-
10984
- class FriendlyHttpError extends FetchPassthroughError {
10986
+ // src/lib/project-scaffold.ts
10987
+ function starterOverridesCss() {
10988
+ return `/**
10989
+ * Your book's own styles.
10990
+ *
10991
+ * This file loads AFTER every extension, so any rule you add here overrides
10992
+ * a look's defaults. A new book has no look until you add one:
10993
+ * gutterpress ext add clean-book <book> --look (a built-in look)
10994
+ * gutterpress ext add <package> <book> (a look from npm)
10995
+ * A book has one look at a time; adding another turns the current one off.
10996
+ */
10997
+ `;
10985
10998
  }
10986
10999
 
10987
- class FetchUnavailableError extends Error {
10988
- }
10989
- function composeSignal(signal, timeoutMs) {
10990
- const timeout = AbortSignal.timeout(timeoutMs);
10991
- return {
10992
- signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
10993
- timeout
10994
- };
10995
- }
10996
- async function withFetchTimeout(options, run2) {
10997
- const composed = composeSignal(options.signal, options.timeoutMs);
10998
- try {
10999
- return await run2(composed.signal);
11000
- } catch (cause) {
11001
- if (cause instanceof FetchPassthroughError)
11002
- throw cause;
11003
- if (options.signal?.aborted)
11004
- throw cause;
11005
- const offline = typeof options.offlineMessage === "function" ? options.offlineMessage(cause) : options.offlineMessage;
11006
- const timedOut = composed.timeout.aborted || cause instanceof Error && cause.name === "TimeoutError";
11007
- if (cause instanceof Error && cause.name === "AbortError" && !timedOut)
11008
- throw cause;
11009
- throw new FetchUnavailableError(timedOut ? options.timeoutMessage ?? offline : offline, { cause });
11000
+ class CreateProjectErrorImpl extends Error {
11001
+ code;
11002
+ constructor(code, message) {
11003
+ super(message);
11004
+ this.name = "CreateProjectError";
11005
+ this.code = code;
11010
11006
  }
11011
11007
  }
11012
-
11013
- // src/lib/npm-plugin-installer.ts
11014
- var DEFAULT_LIMITS = {
11015
- metadataBytes: 15 * 1024 * 1024,
11016
- packageTarballBytes: 50 * 1024 * 1024,
11017
- totalNetworkBytes: 250 * 1024 * 1024,
11018
- packageFileBytes: 50 * 1024 * 1024,
11019
- totalUnpackedBytes: 512 * 1024 * 1024,
11020
- totalFiles: 50000,
11021
- totalPackages: 512,
11022
- dependencyDepth: 64
11023
- };
11024
- var METADATA_TIMEOUT_MS = 30000;
11025
- var TARBALL_TIMEOUT_MS = 120000;
11026
-
11027
- class SecurityInstallError extends FetchPassthroughError {
11028
- }
11029
-
11030
- class PackageUnavailableError extends Error {
11008
+ function slugifyProjectName(name) {
11009
+ return slugify(name);
11031
11010
  }
11032
- function isOptionalAvailabilityError(error2) {
11033
- return error2 instanceof PackageUnavailableError || error2 instanceof FriendlyHttpError || error2 instanceof FetchUnavailableError;
11011
+ function escapeYamlScalar(value) {
11012
+ return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
11034
11013
  }
11035
- function object3(value) {
11036
- return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
11014
+ var DEFAULT_AUTHOR = "Anonymous";
11015
+ function isPositivePoints(value) {
11016
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
11037
11017
  }
11038
- function stringMap2(value) {
11039
- const map = object3(value);
11040
- if (!map)
11041
- return {};
11042
- const result = {};
11043
- for (const [name, selector] of Object.entries(map)) {
11044
- if (typeof selector === "string" && selector.trim())
11045
- result[name] = selector.trim();
11018
+ function requirePreset(preset, customPage) {
11019
+ if (!preset || !PRESET_IDS.includes(preset)) {
11020
+ const got = preset ? ` (got "${preset}")` : "";
11021
+ throw new CreateProjectErrorImpl("preset-required", `A preset is required to create a book${got}. Choose one of: ${PRESET_IDS.join(", ")}.`);
11046
11022
  }
11047
- return result;
11048
- }
11049
- function mb(bytes) {
11050
- return `${Math.ceil(bytes / 1024 / 1024)}MB`;
11051
- }
11052
- function isContained2(root, candidate) {
11053
- const relative2 = path16.relative(root, candidate);
11054
- return relative2 === "" || !relative2.startsWith("..") && !path16.isAbsolute(relative2);
11055
- }
11056
- function mergeLimits(overrides) {
11057
- const limits = { ...DEFAULT_LIMITS, ...overrides };
11058
- for (const [name, value] of Object.entries(limits)) {
11059
- if (!Number.isSafeInteger(value) || value <= 0) {
11060
- throw new Error(`Invalid npm plugin install limit ${name}: ${value}`);
11023
+ if (preset === "custom") {
11024
+ const missing = [];
11025
+ if (!customPage || !isPositivePoints(customPage.width))
11026
+ missing.push("page width");
11027
+ if (!customPage || !isPositivePoints(customPage.height))
11028
+ missing.push("page height");
11029
+ if (missing.length > 0) {
11030
+ throw new CreateProjectErrorImpl("custom-page-required", `The custom preset needs a trim size: ${missing.join(" and ")} in points ` + `(72pt = 1in — e.g. US Letter is 612 x 792).`);
11061
11031
  }
11032
+ } else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
11033
+ throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
11062
11034
  }
11063
- return limits;
11035
+ if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
11036
+ throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
11037
+ }
11038
+ return preset;
11064
11039
  }
11065
- async function streamResponse(ctx, response, maxBytes, totalLimitMessage, onChunk) {
11066
- if (!response.body)
11067
- throw new Error("Registry response had no body.");
11068
- const declared = Number(response.headers.get("content-length"));
11069
- if (Number.isFinite(declared) && declared > maxBytes) {
11070
- throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
11040
+ function resolveScaffoldTargets(requested, preset) {
11041
+ const ids = requested ?? [...PRESETS[preset].defaultTargets];
11042
+ const unknown = ids.filter((id) => !TARGETS[id]);
11043
+ if (unknown.length > 0) {
11044
+ throw new CreateProjectErrorImpl("invalid-targets", `Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
11071
11045
  }
11072
- if (Number.isFinite(declared) && declared > ctx.limits.totalNetworkBytes - ctx.totalNetworkBytes) {
11073
- throw new SecurityInstallError(totalLimitMessage);
11046
+ return [...new Set(ids)];
11047
+ }
11048
+ async function scaffoldProject(options) {
11049
+ const name = (options.name ?? "").trim();
11050
+ if (!name) {
11051
+ throw new CreateProjectErrorImpl("invalid-name", "A project name is required.");
11052
+ }
11053
+ const slug = slugifyProjectName(name);
11054
+ const folderName = (options.folderName ?? slug).trim();
11055
+ if (!folderName || folderName !== slugifyProjectName(folderName)) {
11056
+ throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
11057
+ }
11058
+ const parentDir = options.parentDir;
11059
+ if (!parentDir || !path16.isAbsolute(parentDir)) {
11060
+ throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
11074
11061
  }
11075
- const reader = response.body.getReader();
11076
- let total = 0;
11077
11062
  try {
11078
- while (true) {
11079
- const { done, value } = await reader.read();
11080
- if (done)
11081
- break;
11082
- total += value.length;
11083
- if (total > maxBytes) {
11084
- await reader.cancel("size limit exceeded").catch(() => {});
11085
- throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
11063
+ await access(parentDir, FS.W_OK);
11064
+ } catch {
11065
+ throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
11066
+ }
11067
+ const projectDir = path16.join(parentDir, folderName);
11068
+ let targetExists = true;
11069
+ try {
11070
+ await access(projectDir, FS.F_OK);
11071
+ } catch {
11072
+ targetExists = false;
11073
+ }
11074
+ if (targetExists) {
11075
+ throw new CreateProjectErrorImpl("target-exists", `A folder named "${folderName}" already exists here. Choose a different name or location.`);
11076
+ }
11077
+ const template = options.template ?? "book";
11078
+ const customTemplateDir = options.templateDir;
11079
+ const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
11080
+ const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
11081
+ try {
11082
+ await mkdir7(projectDir, { recursive: true });
11083
+ if (customTemplateDir) {
11084
+ const entries = await readdir6(customTemplateDir, { withFileTypes: true });
11085
+ for (const entry of entries) {
11086
+ if (entry.name === ".gutterpress-template.json")
11087
+ continue;
11088
+ await cp2(path16.join(customTemplateDir, entry.name), path16.join(projectDir, entry.name), { recursive: true });
11086
11089
  }
11087
- if (ctx.totalNetworkBytes + value.length > ctx.limits.totalNetworkBytes) {
11088
- await reader.cancel("total size limit exceeded").catch(() => {});
11089
- throw new SecurityInstallError(totalLimitMessage);
11090
+ await mkdir7(path16.join(projectDir, "assets"), { recursive: true });
11091
+ } else {
11092
+ const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
11093
+ const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
11094
+ await mkdir7(path16.join(projectDir, "assets"), { recursive: true });
11095
+ await copyFile(tplManifest, path16.join(projectDir, MANIFEST_FILENAMES[0]));
11096
+ await copyFile(tplChapter, path16.join(projectDir, "chapter-01.md"));
11097
+ await mkdir7(path16.join(projectDir, "styles"), { recursive: true });
11098
+ await writeFile8(path16.join(projectDir, "styles", "book.css"), starterOverridesCss(), "utf8");
11099
+ }
11100
+ } catch (e) {
11101
+ throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
11102
+ }
11103
+ try {
11104
+ await ensureGitignoreHasDist(projectDir);
11105
+ } catch (e) {
11106
+ throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
11107
+ }
11108
+ const manifestPath = MANIFEST_FILENAMES.map((name2) => path16.join(projectDir, name2)).find(existsSync12) ?? path16.join(projectDir, MANIFEST_FILENAMES[0]);
11109
+ const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
11110
+ const substitutions = {
11111
+ "{{TITLE}}": escapeYamlScalar(name),
11112
+ "{{AUTHOR}}": escapeYamlScalar(author)
11113
+ };
11114
+ let openFile = path16.join(projectDir, "chapter-01.md");
11115
+ try {
11116
+ await fillTemplateFile(manifestPath, substitutions);
11117
+ if (preset) {
11118
+ const { doc, file } = await loadManifestDoc(projectDir);
11119
+ doc.set("preset", preset);
11120
+ doc.set("targets", doc.createNode(targets ?? []));
11121
+ if (options.customPage) {
11122
+ const page = {
11123
+ width: options.customPage.width,
11124
+ height: options.customPage.height
11125
+ };
11126
+ if (options.customPage.tolerance !== undefined) {
11127
+ page.tolerance = options.customPage.tolerance;
11128
+ }
11129
+ doc.set("page", doc.createNode(page));
11090
11130
  }
11091
- ctx.totalNetworkBytes += value.length;
11092
- await onChunk(value);
11131
+ await writeManifestDoc(file, doc);
11093
11132
  }
11094
- } finally {
11095
- reader.releaseLock();
11133
+ const firstSource = await firstSourceFile(manifestPath);
11134
+ if (firstSource)
11135
+ openFile = path16.join(projectDir, firstSource);
11136
+ await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
11137
+ } catch (e) {
11138
+ throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
11096
11139
  }
11097
- return total;
11098
- }
11099
- async function fetchPackument(ctx, name) {
11100
- let pending = ctx.packuments.get(name);
11101
- if (pending)
11102
- return pending;
11103
- pending = withFetchTimeout({
11104
- timeoutMs: METADATA_TIMEOUT_MS,
11105
- signal: ctx.signal,
11106
- timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
11107
- offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
11108
- }, async (signal) => {
11109
- const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
11110
- signal,
11111
- redirect: "error",
11112
- headers: { accept: "application/vnd.npm.install-v1+json" }
11113
- });
11114
- if (response.status === 404) {
11115
- throw new FriendlyHttpError(`npm package "${name}" was not found.`);
11116
- }
11117
- if (!response.ok) {
11118
- throw new FriendlyHttpError(`Looking up ${name} on npm failed (HTTP ${response.status}).`);
11119
- }
11120
- const chunks = [];
11121
- const size = await streamResponse(ctx, response, ctx.limits.metadataBytes, `Plugin dependency metadata exceeds the ${mb(ctx.limits.totalNetworkBytes)} total download limit.`, (chunk2) => {
11122
- chunks.push(chunk2);
11123
- });
11124
- const bytes = new Uint8Array(size);
11125
- let offset = 0;
11126
- for (const chunk2 of chunks) {
11127
- bytes.set(chunk2, offset);
11128
- offset += chunk2.length;
11129
- }
11130
- let metadata = null;
11140
+ const requested = options.versionHistory ?? "local-git";
11141
+ let versionHistory = "none";
11142
+ let versionHistoryError;
11143
+ if (requested === "local-git") {
11131
11144
  try {
11132
- metadata = object3(JSON.parse(new TextDecoder().decode(bytes)));
11133
- } catch {}
11134
- if (!metadata || metadata.name !== name) {
11135
- throw new FriendlyHttpError(`npm returned invalid metadata for "${name}".`);
11136
- }
11137
- return metadata;
11138
- }).catch((error2) => {
11139
- ctx.packuments.delete(name);
11140
- throw error2;
11141
- });
11142
- ctx.packuments.set(name, pending);
11143
- return pending;
11144
- }
11145
- function parseIntegrity(dist) {
11146
- const integrity = typeof dist.integrity === "string" ? dist.integrity.trim() : "";
11147
- const parsed = integrity.split(/\s+/).map((token) => token.match(/^(sha512|sha384|sha256|sha1)-([A-Za-z0-9+/]+={0,2})(?:\?.*)?$/)).filter((match) => Boolean(match?.[1] && match[2]));
11148
- for (const algorithm of ["sha512", "sha384", "sha256"]) {
11149
- const match = parsed.find((candidate) => candidate[1] === algorithm);
11150
- if (!match)
11151
- continue;
11152
- const digest = Buffer.from(match[2], "base64");
11153
- if (digest.length > 0) {
11154
- return { algorithm, digest, label: `${algorithm}-${match[2]}`, legacySha1: false };
11145
+ const { providerFor } = await import("./source-provider-jkppq9fc.js");
11146
+ const provider = providerFor({ type: "local-folder", path: projectDir });
11147
+ await provider.initVersionHistory({
11148
+ projectDir,
11149
+ authorName: options.author?.trim() || undefined,
11150
+ initialMessage: "Created project"
11151
+ });
11152
+ versionHistory = "local-git";
11153
+ } catch (e) {
11154
+ versionHistory = "none";
11155
+ const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
11156
+ const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
11157
+ versionHistoryError = enclosingRepoDir ? `This book was added to ${path16.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
11155
11158
  }
11156
11159
  }
11157
- const sha1 = parsed.find((candidate) => candidate[1] === "sha1");
11158
- if (sha1) {
11159
- return {
11160
- algorithm: "sha1",
11161
- digest: Buffer.from(sha1[2], "base64"),
11162
- label: `sha1-${sha1[2]}`,
11163
- legacySha1: true
11164
- };
11165
- }
11166
- if (integrity) {
11167
- throw new SecurityInstallError("The npm registry supplied integrity data, but no supported SHA-256-or-stronger hash.");
11168
- }
11169
- const shasum = typeof dist.shasum === "string" ? dist.shasum.trim().toLowerCase() : "";
11170
- if (/^[a-f0-9]{40}$/.test(shasum)) {
11171
- return {
11172
- algorithm: "sha1",
11173
- digest: Buffer.from(shasum, "hex"),
11174
- label: `sha1-hex-${shasum}`,
11175
- legacySha1: true
11176
- };
11160
+ const result = {
11161
+ projectDir,
11162
+ manifestPath,
11163
+ openFile,
11164
+ versionHistory
11165
+ };
11166
+ if (versionHistoryError !== undefined) {
11167
+ result.versionHistoryError = versionHistoryError;
11177
11168
  }
11178
- throw new SecurityInstallError("The npm registry provided no usable integrity hash, so the package was not installed.");
11169
+ return result;
11179
11170
  }
11180
- function unsupportedSelector(selector) {
11181
- return /^(?:file:|git\+|https?:|ssh:|github:|workspace:|link:|npm:)/i.test(selector);
11171
+ function prettifyFolderName(base) {
11172
+ const words = base.replace(/[-_]+/g, " ").trim();
11173
+ return words.replace(/\b\w/g, (c) => c.toUpperCase()) || "Untitled Book";
11182
11174
  }
11183
- async function selectPackage(ctx, name, selector) {
11184
- if (selector && unsupportedSelector(selector)) {
11185
- throw new PackageUnavailableError(`Dependency selector "${selector}" for ${name} is not a registry semver range or dist-tag.`);
11186
- }
11187
- const metadata = await fetchPackument(ctx, name);
11188
- const versions = object3(metadata.versions);
11189
- const tags = object3(metadata["dist-tags"]);
11190
- if (!versions || !tags)
11191
- throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
11192
- let version;
11193
- if (!selector) {
11194
- if (typeof tags.latest === "string")
11195
- version = tags.latest;
11196
- } else if (object3(versions[selector])) {
11197
- version = selector;
11198
- } else if (typeof tags[selector] === "string") {
11199
- version = tags[selector];
11200
- } else if (validRange2(selector)) {
11201
- version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
11175
+ async function adoptFolder(options) {
11176
+ const dir = options.dir;
11177
+ if (typeof dir !== "string" || !path16.isAbsolute(dir)) {
11178
+ throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
11202
11179
  }
11203
- if (!version || !isExactNpmVersion(version)) {
11204
- throw new PackageUnavailableError(`No exact npm version of "${name}" satisfies "${selector ?? "latest"}".`);
11180
+ let st;
11181
+ try {
11182
+ st = await stat5(dir);
11183
+ } catch {
11184
+ throw new CreateProjectErrorImpl("scaffold-io", `Folder not found: ${dir}`);
11205
11185
  }
11206
- const selected = object3(versions[version]);
11207
- if (!selected || selected.name !== name || selected.version !== version) {
11208
- throw new SecurityInstallError(`npm metadata identity failed for "${name}@${version}".`);
11186
+ if (!st.isDirectory()) {
11187
+ throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
11209
11188
  }
11210
- const dist = object3(selected.dist);
11211
- if (!dist || typeof dist.tarball !== "string") {
11212
- throw new PackageUnavailableError(`npm metadata for "${name}@${version}" has no tarball.`);
11189
+ if (MANIFEST_FILENAMES.some((name) => existsSync12(path16.join(dir, name)))) {
11190
+ throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
11213
11191
  }
11214
- let tarball;
11192
+ const template = options.template ?? "book";
11193
+ const title = (options.title ?? "").trim() || prettifyFolderName(path16.basename(dir));
11194
+ const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
11195
+ let mdFiles;
11215
11196
  try {
11216
- tarball = new URL(dist.tarball);
11217
- } catch {
11218
- throw new SecurityInstallError(`npm returned an invalid tarball URL for "${name}@${version}".`);
11197
+ const entries = await readdir6(dir, { withFileTypes: true });
11198
+ mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
11199
+ if (mdFiles.length === 0) {
11200
+ const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
11201
+ await copyFile(tplChapter, path16.join(dir, "chapter-01.md"));
11202
+ await fillTemplateFile(path16.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
11203
+ mdFiles = ["chapter-01.md"];
11204
+ }
11205
+ await mkdir7(path16.join(dir, "styles"), { recursive: true });
11206
+ const bookCssPath = path16.join(dir, "styles", "book.css");
11207
+ if (!existsSync12(bookCssPath)) {
11208
+ await writeFile8(bookCssPath, starterOverridesCss(), "utf8");
11209
+ }
11210
+ const filesYaml = mdFiles.map((f) => ` - "${escapeYamlScalar(f)}"`).join(`
11211
+ `);
11212
+ const manifest = `title: "${escapeYamlScalar(title)}"
11213
+ authors:
11214
+ - "${escapeYamlScalar(author)}"
11215
+ preset: dtrpg
11216
+ targets:
11217
+ - dtrpg
11218
+ source:
11219
+ files:
11220
+ ${filesYaml}
11221
+ styles:
11222
+ - styles/book.css
11223
+ `;
11224
+ await writeFile8(path16.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
11225
+ await mkdir7(path16.join(dir, "assets"), { recursive: true });
11226
+ await ensureGitignoreHasDist(dir);
11227
+ } catch (e) {
11228
+ if (e instanceof CreateProjectErrorImpl)
11229
+ throw e;
11230
+ throw new CreateProjectErrorImpl("scaffold-io", `Could not set up the folder: ${e instanceof Error ? e.message : String(e)}`);
11219
11231
  }
11220
- if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
11221
- throw new SecurityInstallError(`npm returned an unexpected tarball host for "${name}@${version}".`);
11232
+ const requested = options.versionHistory ?? "local-git";
11233
+ let versionHistory = "none";
11234
+ let versionHistoryError;
11235
+ if (requested === "local-git") {
11236
+ try {
11237
+ const { providerFor } = await import("./source-provider-jkppq9fc.js");
11238
+ const provider = providerFor({ type: "local-folder", path: dir });
11239
+ await provider.initVersionHistory({
11240
+ projectDir: dir,
11241
+ authorName: options.author?.trim() || undefined,
11242
+ initialMessage: "Set up as a gutterpress book"
11243
+ });
11244
+ versionHistory = "local-git";
11245
+ } catch (e) {
11246
+ versionHistory = "none";
11247
+ versionHistoryError = e instanceof Error ? e.message : String(e);
11248
+ }
11222
11249
  }
11223
- return { name, version, tarball: tarball.href, integrity: parseIntegrity(dist) };
11250
+ const result = {
11251
+ projectDir: dir,
11252
+ manifestPath: path16.join(dir, MANIFEST_FILENAMES[0]),
11253
+ openFile: path16.join(dir, mdFiles[0]),
11254
+ versionHistory
11255
+ };
11256
+ if (versionHistoryError !== undefined)
11257
+ result.versionHistoryError = versionHistoryError;
11258
+ return result;
11224
11259
  }
11225
- async function downloadTarball(ctx, pkg, archivePath) {
11226
- const file = await open2(archivePath, "wx");
11227
- const hash = createHash7(pkg.integrity.algorithm);
11260
+ async function ensureGitignoreHasDist(projectDir) {
11261
+ const gitignorePath = path16.join(projectDir, ".gitignore");
11262
+ let existing = null;
11228
11263
  try {
11229
- await withFetchTimeout({
11230
- timeoutMs: TARBALL_TIMEOUT_MS,
11231
- signal: ctx.signal,
11232
- timeoutMessage: `Downloading ${pkg.name}@${pkg.version} timed out.`,
11233
- offlineMessage: (cause) => `Downloading ${pkg.name}@${pkg.version} failed (${cause instanceof Error ? cause.message : String(cause)}).`
11234
- }, async (signal) => {
11235
- const response = await ctx.fetch(pkg.tarball, { signal, redirect: "error" });
11236
- if (!response.ok) {
11237
- throw new FriendlyHttpError(`Downloading ${pkg.name}@${pkg.version} failed (HTTP ${response.status}).`);
11238
- }
11239
- await streamResponse(ctx, response, ctx.limits.packageTarballBytes, `Plugin dependency downloads exceed the ${mb(ctx.limits.totalNetworkBytes)} total limit.`, async (chunk2) => {
11240
- hash.update(chunk2);
11241
- try {
11242
- await file.write(chunk2);
11243
- } catch (cause) {
11244
- throw new SecurityInstallError(`Could not store ${pkg.name}@${pkg.version} while downloading.`, { cause });
11245
- }
11246
- });
11247
- });
11248
- } finally {
11249
- await file.close();
11250
- }
11251
- const actual = hash.digest();
11252
- if (actual.length !== pkg.integrity.digest.length || !timingSafeEqual(actual, pkg.integrity.digest)) {
11253
- throw new SecurityInstallError(`The tarball for ${pkg.name}@${pkg.version} failed its registry integrity check.`);
11264
+ existing = await readFile27(gitignorePath, "utf8");
11265
+ } catch {}
11266
+ if (existing === null) {
11267
+ await writeFile8(gitignorePath, `dist/
11268
+ `, "utf8");
11269
+ return;
11254
11270
  }
11271
+ const alreadyIgnoresDist = existing.split(`
11272
+ `).some((line) => /^\/?dist\/?$/.test(line.trim()));
11273
+ if (alreadyIgnoresDist)
11274
+ return;
11275
+ const needsLeadingNewline = existing.length > 0 && !existing.endsWith(`
11276
+ `);
11277
+ await writeFile8(gitignorePath, existing + (needsLeadingNewline ? `
11278
+ ` : "") + `dist/
11279
+ `, "utf8");
11255
11280
  }
11256
- function safeTarRelative(entryPath, packageName) {
11257
- if (!entryPath || entryPath.includes("\\") || entryPath.startsWith("/") || /^[a-zA-Z]:/.test(entryPath)) {
11258
- throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath || "(empty)"}`);
11281
+ async function firstSourceFile(manifestPath) {
11282
+ let text;
11283
+ try {
11284
+ text = await readFile27(manifestPath, "utf8");
11285
+ } catch {
11286
+ return;
11259
11287
  }
11260
- const parts = entryPath.split("/");
11261
- if (parts.at(-1) === "")
11262
- parts.pop();
11263
- const legacyTypesRoot = packageName.startsWith("@types/") ? packageName.slice("@types/".length) : null;
11264
- if (parts.some((part) => !part || part === "." || part === "..") || parts[0] !== "package" && parts[0] !== legacyTypesRoot) {
11265
- throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath}`);
11288
+ const filesIdx = text.search(/^\s*files:\s*$/m);
11289
+ if (filesIdx === -1)
11290
+ return;
11291
+ const rest = text.slice(filesIdx);
11292
+ const m = rest.match(/^\s*-\s*"?([^"\n]+?)"?\s*$/m);
11293
+ return m ? m[1].trim() : undefined;
11294
+ }
11295
+ async function fillTemplateFile(filePath, substitutions) {
11296
+ let text = await readFile27(filePath, "utf8");
11297
+ for (const [token, value] of Object.entries(substitutions)) {
11298
+ text = text.split(token).join(value);
11266
11299
  }
11267
- const relative2 = parts.slice(1).join("/");
11268
- if (!relative2)
11269
- return { relative: relative2, key: "" };
11270
- assertWindowsSafeRelativePath(relative2);
11271
- if (relative2.normalize("NFKC").split("/").some((part) => part.toLowerCase() === "node_modules")) {
11272
- throw new SecurityInstallError(`The npm tarball contains bundled node_modules (${entryPath}); bundled dependencies are not accepted.`);
11300
+ await writeFile8(filePath, text, "utf8");
11301
+ }
11302
+ // src/lib/extension-scaffold.ts
11303
+ import { access as access2, mkdir as mkdir8, readFile as readFile28, writeFile as writeFile9 } from "node:fs/promises";
11304
+ import { constants as FS2 } from "node:fs";
11305
+ import path17 from "node:path";
11306
+ var EXTENSION_KINDS = ["plugin", "theme"];
11307
+ var RESERVED_PREFIX = "gp-";
11308
+ var TEMPLATE_FILES = {
11309
+ plugin: [
11310
+ ["package.json", "package.json"],
11311
+ ["README.md", "README.md"],
11312
+ ["plugin.js.tpl", "plugin.js"],
11313
+ ["styles/plugin.css", "styles/plugin.css"],
11314
+ ["snippets/term-box.md", "snippets/term-box.md"],
11315
+ ["test/fixture.md", "test/fixture.md"],
11316
+ ["test/expected.html", "test/expected.html"],
11317
+ ["test/plugin.test.js.tpl", "test/plugin.test.js"]
11318
+ ],
11319
+ theme: [
11320
+ ["package.json", "package.json"],
11321
+ ["README.md", "README.md"],
11322
+ ["components.yaml", "components.yaml"],
11323
+ ["snippets/callout.md", "snippets/callout.md"],
11324
+ ["styles/tokens.css", "styles/tokens.css"],
11325
+ ["styles/base.css", "styles/base.css"],
11326
+ ["styles/components.css", "styles/components.css"],
11327
+ ["styles/page-templates.css", "styles/page-templates.css"],
11328
+ ["styles/page-rules.css", "styles/page-rules.css"],
11329
+ ["styles/book.css", "styles/book.css"]
11330
+ ]
11331
+ };
11332
+ var OPEN_FILE = {
11333
+ plugin: "plugin.js",
11334
+ theme: "styles/tokens.css"
11335
+ };
11336
+ var DEFAULT_DESCRIPTION = {
11337
+ plugin: "A Gutterpress plugin.",
11338
+ theme: "A Gutterpress theme."
11339
+ };
11340
+ var DEFAULT_AUTHOR2 = "Anonymous";
11341
+
11342
+ class ExtensionScaffoldError extends Error {
11343
+ code;
11344
+ constructor(code, message) {
11345
+ super(message);
11346
+ this.name = "CreateProjectError";
11347
+ this.code = code;
11273
11348
  }
11274
- return { relative: relative2, key: windowsPathKey(relative2) };
11275
11349
  }
11276
- async function extractTarball(ctx, archivePath, packageDir, packageName) {
11277
- const { extract } = await import("tar");
11278
- let rejected = null;
11279
- const seen = new Set;
11280
- const metadataTypes = new Set([
11281
- "GlobalExtendedHeader",
11282
- "ExtendedHeader",
11283
- "NextFileHasLongLinkpath",
11284
- "NextFileHasLongPath",
11285
- "OldGnuLongPath",
11286
- "OldExtendedHeader"
11287
- ]);
11288
- await mkdir7(packageDir, { recursive: true });
11289
- const unpack = extract({
11290
- cwd: packageDir,
11291
- strip: 1,
11292
- strict: true,
11293
- preservePaths: false,
11294
- preserveOwner: false,
11295
- noMtime: true,
11296
- chmod: false,
11297
- maxDepth: ctx.limits.dependencyDepth * 4,
11298
- filter: (entryPath, rawEntry) => {
11299
- if (rejected)
11300
- return false;
11301
- const entry = rawEntry;
11302
- if (metadataTypes.has(entry.type))
11303
- return true;
11304
- if (entry.type === "Link" || entry.type === "SymbolicLink") {
11305
- rejected = new SecurityInstallError(`The npm tarball contains an unsafe link: ${entryPath}`);
11306
- return false;
11307
- }
11308
- if (entry.type !== "File" && entry.type !== "OldFile" && entry.type !== "Directory") {
11309
- rejected = new SecurityInstallError(`The npm tarball contains unsupported entry type ${entry.type}: ${entryPath}`);
11310
- return false;
11311
- }
11312
- try {
11313
- const { relative: relative2, key } = safeTarRelative(entryPath, packageName);
11314
- if (!relative2) {
11315
- if (entry.type === "Directory")
11316
- return true;
11317
- throw new SecurityInstallError(`The npm tarball uses package/ as a file.`);
11318
- }
11319
- if (seen.has(key)) {
11320
- throw new SecurityInstallError(`The npm tarball has a Windows-colliding path: ${entryPath}`);
11321
- }
11322
- seen.add(key);
11323
- ctx.totalFiles++;
11324
- if (ctx.totalFiles > ctx.limits.totalFiles) {
11325
- throw new SecurityInstallError(`Plugin graph exceeds ${ctx.limits.totalFiles} files.`);
11326
- }
11327
- if (entry.type !== "Directory") {
11328
- if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > ctx.limits.packageFileBytes) {
11329
- throw new SecurityInstallError(`Package file exceeds the ${mb(ctx.limits.packageFileBytes)} limit: ${entryPath}`);
11330
- }
11331
- }
11332
- return true;
11333
- } catch (error2) {
11334
- rejected = error2 instanceof Error ? error2 : new SecurityInstallError(String(error2));
11335
- return false;
11336
- }
11337
- }
11338
- });
11339
- const limiter = new Transform({
11340
- transform(chunk2, _encoding, callback) {
11341
- ctx.totalUnpackedBytes += chunk2.length;
11342
- if (ctx.totalUnpackedBytes > ctx.limits.totalUnpackedBytes) {
11343
- callback(new SecurityInstallError(`Plugin graph expands beyond the ${mb(ctx.limits.totalUnpackedBytes)} limit.`));
11344
- } else
11345
- callback(null, chunk2);
11346
- }
11347
- });
11348
- try {
11349
- await pipeline(createReadStream2(archivePath), createGunzip(), limiter, unpack);
11350
- } catch (error2) {
11351
- if (rejected)
11352
- throw rejected;
11353
- if (error2 instanceof SecurityInstallError)
11354
- throw error2;
11355
- throw new SecurityInstallError(`The npm tarball could not be safely extracted (${error2 instanceof Error ? error2.message : String(error2)}).`, { cause: error2 });
11350
+ function resolveExtensionPrefix(slug, requested) {
11351
+ const raw = (requested ?? `${slug}-`).trim();
11352
+ const prefix = raw.endsWith("-") ? raw : `${raw}-`;
11353
+ if (!/^[a-z][a-z0-9-]*-$/.test(prefix)) {
11354
+ throw new ExtensionScaffoldError("invalid-name", `"${raw}" is not a usable prefix. Use lower-case letters, digits and hyphens, ` + `starting with a letter — for example "${slug}-" or "fn-".`);
11356
11355
  }
11357
- if (rejected)
11358
- throw rejected;
11356
+ if (prefix === RESERVED_PREFIX) {
11357
+ throw new ExtensionScaffoldError("invalid-name", `"${RESERVED_PREFIX}" is reserved for Gutterpress core's own classes and custom ` + `properties. Taking it would silently override them — choose your own prefix.`);
11358
+ }
11359
+ return prefix;
11359
11360
  }
11360
- async function readInstalledManifest(packageDir, selected) {
11361
- const packageJsonPath = path16.join(packageDir, "package.json");
11362
- let manifest = null;
11361
+ function identifierFor(slug) {
11362
+ const camel = slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
11363
+ return /^[0-9]/.test(camel) ? `_${camel}` : camel;
11364
+ }
11365
+ function escapeJsonScalar(value) {
11366
+ return JSON.stringify(value).slice(1, -1);
11367
+ }
11368
+ async function scaffoldExtension(options) {
11369
+ const kind = options.kind;
11370
+ if (kind !== "plugin" && kind !== "theme") {
11371
+ throw new ExtensionScaffoldError("invalid-name", `Unknown extension kind "${String(kind)}". Choose one of: ${EXTENSION_KINDS.join(", ")}.`);
11372
+ }
11373
+ const name = (options.name ?? "").trim();
11374
+ if (!name) {
11375
+ throw new ExtensionScaffoldError("invalid-name", "A name is required.");
11376
+ }
11377
+ const slug = (options.folderName ?? slugify(name)).trim();
11378
+ if (!slug || slug !== slugify(slug)) {
11379
+ throw new ExtensionScaffoldError("invalid-name", `Could not derive a valid folder name from "${name}".`);
11380
+ }
11381
+ const prefix = resolveExtensionPrefix(slug, options.prefix);
11382
+ const parentDir = options.parentDir;
11383
+ if (!parentDir || !path17.isAbsolute(parentDir)) {
11384
+ throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
11385
+ }
11363
11386
  try {
11364
- const info2 = await lstat2(packageJsonPath);
11365
- if (!info2.isFile() || info2.isSymbolicLink())
11366
- throw new Error("not a regular file");
11367
- manifest = object3(JSON.parse(await readFile27(packageJsonPath, "utf8")));
11368
- } catch (error2) {
11369
- throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
11387
+ await access2(parentDir, FS2.W_OK);
11388
+ } catch {
11389
+ throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
11370
11390
  }
11371
- if (manifest?.name !== selected.name || manifest.version !== selected.version) {
11372
- throw new SecurityInstallError(`Downloaded ${String(manifest?.name)}@${String(manifest?.version)} instead of ${selected.name}@${selected.version}.`);
11391
+ const extensionDir = path17.join(parentDir, slug);
11392
+ let targetExists = true;
11393
+ try {
11394
+ await access2(extensionDir, FS2.F_OK);
11395
+ } catch {
11396
+ targetExists = false;
11373
11397
  }
11374
- return manifest;
11375
- }
11376
- async function optionalPackageEntry(packageDir, manifest, condition) {
11398
+ if (targetExists) {
11399
+ throw new ExtensionScaffoldError("target-exists", `A folder named "${slug}" already exists here. Choose a different name or location.`);
11400
+ }
11401
+ const author = (options.author ?? "").trim() || DEFAULT_AUTHOR2;
11402
+ const description = (options.description ?? "").trim() || DEFAULT_DESCRIPTION[kind];
11403
+ const plain = {
11404
+ "{{NAME}}": name,
11405
+ "{{SLUG}}": slug,
11406
+ "{{PREFIX}}": prefix,
11407
+ "{{AUTHOR}}": author,
11408
+ "{{DESCRIPTION}}": description,
11409
+ "{{IDENT}}": identifierFor(slug)
11410
+ };
11411
+ const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
11412
+ const entries = TEMPLATE_FILES[kind];
11413
+ const written = [];
11377
11414
  try {
11378
- return await resolvePackageEntry(packageDir, manifest, condition);
11379
- } catch (error2) {
11380
- if (error2 instanceof Error && error2.message.includes("no contained JavaScript entry point")) {
11381
- return;
11415
+ await mkdir8(extensionDir, { recursive: true });
11416
+ for (const [assetName, relDest] of entries) {
11417
+ const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
11418
+ const dest = path17.join(extensionDir, relDest);
11419
+ await mkdir8(path17.dirname(dest), { recursive: true });
11420
+ const text = await readFile28(src, "utf8");
11421
+ const table = relDest.endsWith(".json") ? jsonSafe : plain;
11422
+ await writeFile9(dest, substitute(text, table), "utf8");
11423
+ written.push(relDest);
11382
11424
  }
11383
- throw error2;
11425
+ } catch (e) {
11426
+ throw new ExtensionScaffoldError("scaffold-io", `Could not create the ${kind} files: ${e instanceof Error ? e.message : String(e)}`);
11384
11427
  }
11428
+ return {
11429
+ extensionDir,
11430
+ manifestPath: path17.join(extensionDir, "package.json"),
11431
+ kind,
11432
+ slug,
11433
+ prefix,
11434
+ openFile: path17.join(extensionDir, OPEN_FILE[kind]),
11435
+ files: written
11436
+ };
11385
11437
  }
11386
- function platformAllowed(rule, current) {
11387
- let values;
11388
- if (typeof rule === "string") {
11389
- values = [rule];
11390
- } else if (Array.isArray(rule) && rule.every((item) => typeof item === "string")) {
11391
- values = rule;
11392
- } else {
11393
- return true;
11438
+ function substitute(text, table) {
11439
+ let out = text;
11440
+ for (const [token, value] of Object.entries(table)) {
11441
+ out = out.split(token).join(value);
11394
11442
  }
11395
- if (values.length === 0)
11396
- return true;
11397
- if (values.length === 1 && values[0] === "any")
11398
- return true;
11399
- if (values.includes(`!${current}`))
11400
- return false;
11401
- const positives = values.filter((value) => !value.startsWith("!"));
11402
- return positives.length === 0 || positives.includes(current);
11443
+ return out;
11403
11444
  }
11404
- function relativePackagePath(parentPackagePath, name) {
11405
- const segments = name.split("/");
11406
- return parentPackagePath ? `${parentPackagePath}/node_modules/${segments.join("/")}` : `node_modules/${segments.join("/")}`;
11407
- }
11408
- async function installPackage(ctx, name, selector, parentPackagePath, ancestors, depth, optional) {
11409
- const recordStart = ctx.records.length;
11410
- const skippedStart = ctx.skipped.length;
11411
- const warningsBefore = new Set(ctx.warnings);
11412
- const discardOptionalSubtree = () => {
11413
- ctx.records.splice(recordStart);
11414
- ctx.skipped.splice(skippedStart);
11415
- ctx.warnings = warningsBefore;
11416
- };
11417
- if (depth > ctx.limits.dependencyDepth) {
11418
- throw new SecurityInstallError(`Dependency graph exceeds depth ${ctx.limits.dependencyDepth}.`);
11419
- }
11420
- let selected;
11421
- try {
11422
- selected = await selectPackage(ctx, name, selector);
11423
- } catch (error2) {
11424
- if (optional && isOptionalAvailabilityError(error2)) {
11425
- discardOptionalSubtree();
11426
- return { skipped: error2 instanceof Error ? error2.message : String(error2) };
11427
- }
11428
- throw error2;
11429
- }
11430
- const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
11431
- if (ancestor)
11432
- return { path: ancestor.path };
11433
- if (ctx.records.length >= ctx.limits.totalPackages) {
11434
- throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
11435
- }
11436
- const relativePath = relativePackagePath(parentPackagePath, name);
11437
- const packageDir = path16.join(ctx.stageRoot, ...relativePath.split("/"));
11438
- const archivePath = path16.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
11439
- try {
11440
- await mkdir7(path16.dirname(packageDir), { recursive: true });
11441
- await downloadTarball(ctx, selected, archivePath);
11442
- await extractTarball(ctx, archivePath, packageDir, selected.name);
11443
- const manifest = await readInstalledManifest(packageDir, selected);
11444
- if (!platformAllowed(manifest.os, process.platform) || !platformAllowed(manifest.cpu, process.arch)) {
11445
- const reason = `${name}@${selected.version} does not support ${process.platform}/${process.arch}`;
11446
- if (optional) {
11447
- await rm3(packageDir, { recursive: true, force: true });
11448
- return { skipped: reason };
11449
- }
11450
- throw new PackageUnavailableError(reason);
11451
- }
11452
- const importEntry = await optionalPackageEntry(packageDir, manifest, "import");
11453
- const requireEntry = await optionalPackageEntry(packageDir, manifest, "require");
11454
- if (parentPackagePath === null && !importEntry && !requireEntry) {
11455
- throw new SecurityInstallError(`${name}@${selected.version} has no JavaScript plugin entry.`);
11456
- }
11457
- const record = {
11458
- path: relativePath,
11459
- name,
11460
- version: selected.version,
11461
- tarball: selected.tarball,
11462
- integrity: selected.integrity.label,
11463
- legacySha1: selected.integrity.legacySha1,
11464
- ...importEntry ? { entry: `${relativePath}/${importEntry}` } : {},
11465
- ...requireEntry ? { requireEntry: `${relativePath}/${requireEntry}` } : {},
11466
- dependencies: {},
11467
- packageDir,
11468
- manifest
11469
- };
11470
- ctx.records.push(record);
11471
- if (selected.integrity.legacySha1) {
11472
- ctx.warnings.add(`${name}@${selected.version} is an old npm release with only SHA-1 registry integrity. It was accepted for compatibility and recorded as legacy SHA-1 in the vendor receipt.`);
11473
- }
11474
- const dependencies = stringMap2(manifest.dependencies);
11475
- const optionalDependencies = stringMap2(manifest.optionalDependencies);
11476
- for (const optionalName of Object.keys(optionalDependencies))
11477
- delete dependencies[optionalName];
11478
- const peerDependencies = stringMap2(manifest.peerDependencies);
11479
- const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
11480
- for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
11481
- delete peerDependencies[existing];
11482
- }
11483
- const nextAncestors = [...ancestors, record];
11484
- const requiredEntries = [
11485
- ...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
11486
- ...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
11487
- ].sort((a, b) => a.dependency.localeCompare(b.dependency));
11488
- for (const { dependency, range } of requiredEntries) {
11489
- const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
11490
- if (!child.path)
11491
- throw new Error(`Required dependency ${dependency} was unexpectedly skipped.`);
11492
- record.dependencies[dependency] = child.path;
11493
- }
11494
- const optionalEntries = Object.entries(optionalDependencies).sort(([a], [b]) => a.localeCompare(b));
11495
- for (const [dependency, range] of optionalEntries) {
11496
- const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, true);
11497
- if (child.path)
11498
- record.dependencies[dependency] = child.path;
11499
- else if (child.skipped) {
11500
- ctx.skipped.push({ from: relativePath, name: dependency, selector: range, kind: "optional", reason: child.skipped });
11501
- }
11502
- }
11503
- for (const [dependency, range] of Object.entries(peerDependencies)) {
11504
- if (object3(peerMeta[dependency])?.optional === true) {
11505
- ctx.skipped.push({
11506
- from: relativePath,
11507
- name: dependency,
11508
- selector: range,
11509
- kind: "optional-peer",
11510
- reason: "Optional peer dependencies are not auto-installed."
11511
- });
11512
- }
11513
- }
11514
- return { path: relativePath };
11515
- } catch (error2) {
11516
- await rm3(packageDir, { recursive: true, force: true });
11517
- if (optional && isOptionalAvailabilityError(error2)) {
11518
- discardOptionalSubtree();
11519
- return { skipped: error2 instanceof Error ? error2.message : String(error2) };
11520
- }
11521
- throw error2;
11522
- } finally {
11523
- await rm3(archivePath, { force: true });
11524
- }
11525
- }
11526
- async function ensureDirectory(pathname) {
11527
- try {
11528
- const info2 = await lstat2(pathname);
11529
- if (info2.isSymbolicLink() || !info2.isDirectory()) {
11530
- throw new Error(`Plugin install path is not a normal directory: ${pathname}`);
11531
- }
11532
- } catch (error2) {
11533
- if (error2.code !== "ENOENT")
11534
- throw error2;
11535
- await mkdir7(pathname);
11536
- }
11537
- }
11538
- async function prepareVendorParent(projectDir, name) {
11539
- const plugins = path16.join(projectDir, PLUGINS_DIR);
11540
- const npm = path16.join(plugins, VENDORED_NPM_DIR);
11541
- const packageParent = path16.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
11542
- await mkdir7(projectDir, { recursive: true });
11543
- await ensureDirectory(plugins);
11544
- await ensureDirectory(npm);
11545
- await ensureDirectory(packageParent);
11546
- const [realNpm, realPackageParent] = await Promise.all([
11547
- realpath2(npm),
11548
- realpath2(packageParent)
11549
- ]);
11550
- if (!isContained2(projectDir, realNpm) || !isContained2(projectDir, realPackageParent)) {
11551
- throw new Error("Plugin install path resolves outside the project.");
11552
- }
11553
- return realNpm;
11554
- }
11555
- function publicReceipt(record) {
11556
- return {
11557
- path: record.path,
11558
- name: record.name,
11559
- version: record.version,
11560
- tarball: record.tarball,
11561
- integrity: record.integrity,
11562
- legacySha1: record.legacySha1,
11563
- ...record.entry ? { entry: record.entry } : {},
11564
- ...record.requireEntry ? { requireEntry: record.requireEntry } : {},
11565
- dependencies: record.dependencies
11566
- };
11567
- }
11568
- async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
11569
- const requestedProjectDir = path16.resolve(projectDirInput);
11570
- await mkdir7(requestedProjectDir, { recursive: true });
11571
- const projectDir = await realpath2(requestedProjectDir);
11572
- const requested = parseNpmPluginSpec(packageSpec);
11573
- const npmRoot = await prepareVendorParent(projectDir, requested.name);
11574
- const container = await mkdtemp2(path16.join(npmRoot, ".install-"));
11575
- const stageRoot = path16.join(container, "root");
11576
- const downloadsDir = path16.join(container, "downloads");
11577
- await Promise.all([mkdir7(stageRoot), mkdir7(downloadsDir)]);
11578
- const ctx = {
11579
- fetch: options.fetch ?? globalThis.fetch,
11580
- signal: options.signal,
11581
- limits: mergeLimits(options.limits),
11582
- stageRoot,
11583
- downloadsDir,
11584
- packuments: new Map,
11585
- records: [],
11586
- skipped: [],
11587
- warnings: new Set,
11588
- totalNetworkBytes: 0,
11589
- totalUnpackedBytes: 0,
11590
- totalFiles: 0,
11591
- archiveCounter: 0
11592
- };
11593
- let finalRoot = "";
11594
- let backupRoot = null;
11445
+ // src/lib/project-templates.ts
11446
+ import { cp as cp3, mkdir as mkdir9, readFile as readFile29, readdir as readdir7, stat as stat6, writeFile as writeFile10 } from "node:fs/promises";
11447
+ import { constants as FS3, existsSync as existsSync13 } from "node:fs";
11448
+ import { access as access3 } from "node:fs/promises";
11449
+ import path18 from "node:path";
11450
+ import { isMap, isScalar, isSeq as isSeq2, parseDocument as parseDocument2 } from "yaml";
11451
+ var BUILT_IN_TEMPLATE_IDS = [
11452
+ "book",
11453
+ "zine",
11454
+ "technical"
11455
+ ];
11456
+ async function readTemplateChoices(manifestPath) {
11595
11457
  try {
11596
- const rootResult = await installPackage(ctx, requested.name, requested.selector, null, [], 0, false);
11597
- if (!rootResult.path)
11598
- throw new Error("Root plugin package was unexpectedly skipped.");
11599
- const rootRecord = ctx.records.find((record) => record.path === rootResult.path);
11600
- if (!rootRecord)
11601
- throw new Error("Root plugin package is missing from the dependency graph.");
11602
- const importEntry = rootRecord.entry ?? rootRecord.requireEntry;
11603
- if (!importEntry)
11604
- throw new Error("Root plugin package has no recorded JavaScript entry.");
11605
- const importFormat = packageEntryFormat(rootRecord.manifest, importEntry);
11606
- const entry = importFormat === "commonjs" ? rootRecord.requireEntry ?? importEntry : importEntry;
11607
- const format = packageEntryFormat(rootRecord.manifest, entry);
11608
- const tree = await computeVendorTreeDigest(stageRoot);
11609
- const receipt = {
11610
- schemaVersion: VENDOR_RECEIPT_VERSION,
11611
- root: {
11612
- name: rootRecord.name,
11613
- version: rootRecord.version,
11614
- packagePath: rootRecord.path,
11615
- entry,
11616
- format
11617
- },
11618
- packages: ctx.records.map(publicReceipt).sort((a, b) => a.path.localeCompare(b.path)),
11619
- skipped: ctx.skipped,
11620
- tree: { algorithm: "sha256", ...tree }
11621
- };
11622
- const receiptFile = await open2(path16.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
11623
- try {
11624
- await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
11625
- `, "utf8");
11626
- await receiptFile.sync();
11627
- } finally {
11628
- await receiptFile.close();
11629
- }
11630
- finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
11631
- const realFinalParent = await realpath2(path16.dirname(finalRoot));
11632
- if (!isContained2(projectDir, realFinalParent)) {
11633
- throw new Error("Plugin install destination resolves outside the project.");
11634
- }
11635
- try {
11636
- await lstat2(finalRoot);
11637
- backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
11638
- await rename4(finalRoot, backupRoot);
11639
- } catch (error2) {
11640
- if (error2.code !== "ENOENT")
11641
- throw error2;
11642
- }
11643
- try {
11644
- await rename4(stageRoot, finalRoot);
11645
- } catch (error2) {
11646
- if (backupRoot) {
11647
- try {
11648
- await rename4(backupRoot, finalRoot);
11649
- } catch (restoreError) {
11650
- throw new Error(`Could not publish the new plugin tree or restore the previous tree: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`, { cause: error2 });
11651
- }
11652
- }
11653
- throw error2;
11458
+ const text = await readFile29(manifestPath, "utf8");
11459
+ const doc = parseDocument2(text);
11460
+ const out = {};
11461
+ const preset = doc.get("preset");
11462
+ if (typeof preset === "string" && preset)
11463
+ out.preset = preset;
11464
+ const targets = doc.get("targets", true);
11465
+ if (isSeq2(targets)) {
11466
+ out.targets = targets.items.map((i) => scalarString(i)).filter((s) => s !== null);
11654
11467
  }
11655
- return {
11656
- name: rootRecord.name,
11657
- version: rootRecord.version,
11658
- installRoot: finalRoot,
11659
- backupRoot,
11660
- warnings: [...ctx.warnings]
11661
- };
11662
- } finally {
11663
- await rm3(container, { recursive: true, force: true }).catch(() => {});
11664
- }
11665
- }
11666
- async function finalizeNpmPluginInstall(installed) {
11667
- if (installed.backupRoot) {
11668
- await rm3(installed.backupRoot, { recursive: true, force: true });
11468
+ return out;
11469
+ } catch {
11470
+ return {};
11669
11471
  }
11670
11472
  }
11671
- async function rollbackNpmPluginInstall(installed) {
11672
- await rm3(installed.installRoot, { recursive: true, force: true });
11673
- if (installed.backupRoot)
11674
- await rename4(installed.backupRoot, installed.installRoot);
11675
- }
11676
-
11677
- // src/lib/extension-manager.ts
11678
- var EXTENSIONS_DIR = "extensions";
11679
- var RECOMMENDED_EXTENSIONS = [
11680
- {
11681
- use: "markdown-it-mark",
11682
- label: "Highlight",
11683
- description: "Highlighted text with `==marked==` -> `<mark>`."
11684
- },
11685
- {
11686
- use: "markdown-it-sub",
11687
- label: "Subscript",
11688
- description: "Subscript text with `H~2~O`."
11689
- },
11690
- {
11691
- use: "markdown-it-sup",
11692
- label: "Superscript",
11693
- description: "Superscript text with `29^th^`."
11473
+ var BUILT_IN_META = {
11474
+ book: {
11475
+ label: "Book",
11476
+ description: "A clean starting point for a novel, memoir, or any long-form book."
11694
11477
  },
11695
- {
11696
- use: "markdown-it-abbr",
11697
- label: "Abbreviations",
11698
- description: "Define `*[HTML]: Hyper Text...` and get `<abbr>` tooltips."
11478
+ zine: {
11479
+ label: "Zine",
11480
+ description: "A short, personal zine made to be printed, folded, and shared."
11699
11481
  },
11700
- {
11701
- use: "gutterpress-gfm-alerts",
11702
- label: "Callouts",
11703
- description: "GitHub-style `> [!NOTE]` alert boxes (Note/Tip/Important/Warning/Caution)."
11482
+ technical: {
11483
+ label: "Technical document",
11484
+ description: "A manual or guide with steps, reference tables, and code examples."
11704
11485
  }
11705
- ];
11706
- var BUILT_IN_STYLE_SET_IDS = ["clean-book", "zine", "technical-doc"];
11707
- var mutationQueues = new Map;
11708
- function withMutationLock(projectDir, mutation) {
11709
- const resolved = path17.resolve(projectDir);
11710
- const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
11711
- const previous = mutationQueues.get(key) ?? Promise.resolve();
11712
- const run2 = previous.then(mutation, mutation);
11713
- const tail = run2.then(() => {
11714
- return;
11715
- }, () => {
11486
+ };
11487
+ async function listBuiltInTemplates() {
11488
+ return Promise.all(BUILT_IN_TEMPLATE_IDS.map(async (id) => ({
11489
+ id,
11490
+ label: BUILT_IN_META[id].label,
11491
+ description: BUILT_IN_META[id].description,
11492
+ kind: "builtin",
11493
+ ...await readTemplateChoices(await getAssetPath(`templates/${id}/manifest.yaml`))
11494
+ })));
11495
+ }
11496
+ var SKIP_ENTRIES = new Set([
11497
+ ".git",
11498
+ "node_modules",
11499
+ "out",
11500
+ "dist",
11501
+ "build",
11502
+ ".gutterpress",
11503
+ ".DS_Store"
11504
+ ]);
11505
+ var VENDOR_DIR = "shared";
11506
+ function collectEscaping(nodes, getValue, escapes) {
11507
+ const out = [];
11508
+ for (const node of nodes) {
11509
+ const value = getValue(node);
11510
+ if (value !== null && escapes(value))
11511
+ out.push({ node, value });
11512
+ }
11513
+ return out;
11514
+ }
11515
+ function extensionEntryPath(node) {
11516
+ const use = isMap(node) ? scalarString(node.get("use", true)) : scalarString(node);
11517
+ return use !== null && isPathSpecifier(use) ? use : null;
11518
+ }
11519
+ function dropEscaping(doc, seq, key, escaping) {
11520
+ if (!isSeq2(seq))
11716
11521
  return;
11717
- });
11718
- mutationQueues.set(key, tail);
11719
- tail.then(() => {
11720
- if (mutationQueues.get(key) === tail)
11721
- mutationQueues.delete(key);
11722
- });
11723
- return run2;
11522
+ const drop = new Set(escaping.map((e) => e.node));
11523
+ seq.items = seq.items.filter((node) => !drop.has(node));
11524
+ if (seq.items.length === 0)
11525
+ doc.delete(key);
11724
11526
  }
11725
- function entryUse(item) {
11726
- if (isScalar(item))
11727
- return typeof item.value === "string" ? item.value.trim() : null;
11728
- if (isMap(item)) {
11729
- const u = item.get("use");
11730
- return typeof u === "string" ? u.trim() : null;
11527
+ function rewriteEntry(node, newPath) {
11528
+ if (isMap(node))
11529
+ node.set("use", newPath);
11530
+ else if (isScalar(node))
11531
+ node.value = newPath;
11532
+ }
11533
+ function commonAncestorDir(absFiles) {
11534
+ const dirSegs = absFiles.map((f) => path18.resolve(f).split(path18.sep).slice(0, -1));
11535
+ if (dirSegs.length === 0)
11536
+ return "";
11537
+ let common = dirSegs[0];
11538
+ for (const segs of dirSegs.slice(1)) {
11539
+ let i = 0;
11540
+ while (i < common.length && i < segs.length && common[i] === segs[i])
11541
+ i++;
11542
+ common = common.slice(0, i);
11731
11543
  }
11732
- return null;
11544
+ return common.join(path18.sep) || path18.sep;
11733
11545
  }
11734
- function rawEntry(item) {
11735
- const use = entryUse(item);
11736
- if (!use)
11737
- return null;
11738
- if (!isMap(item))
11739
- return { use, enabled: true };
11740
- const x = item.get("export");
11741
- return {
11742
- use,
11743
- ...typeof x === "string" && x.trim() ? { export: x.trim() } : {},
11744
- enabled: item.get("enabled") !== false
11745
- };
11546
+ function pickVendorDir(templateDir, base) {
11547
+ const preferred = path18.basename(base) || VENDOR_DIR;
11548
+ if (!existsSync13(path18.join(templateDir, preferred)))
11549
+ return preferred;
11550
+ for (let n = 1;; n++) {
11551
+ const candidate = `${preferred}-${n}`;
11552
+ if (!existsSync13(path18.join(templateDir, candidate)))
11553
+ return candidate;
11554
+ }
11746
11555
  }
11747
- function sameExtension(a, b) {
11748
- if (a.trim() === b.trim())
11749
- return true;
11750
- let pa;
11751
- let pb;
11556
+ var TEMPLATE_META_FILE = ".gutterpress-template.json";
11557
+ async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
11558
+ const { doc, file } = await loadManifestDoc(templateDir);
11559
+ const escapes = (p) => escapesProjectRoot(sourceProjectDir, path18.resolve(sourceProjectDir, p));
11560
+ const stylesSeq = doc.get("styles", true);
11561
+ const extensionsSeq = doc.get("extensions", true);
11562
+ const escapingStyles = collectEscaping(isSeq2(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
11563
+ const escapingExtensions = collectEscaping(isSeq2(extensionsSeq) ? extensionsSeq.items : [], extensionEntryPath, escapes);
11564
+ if (escapingStyles.length === 0 && escapingExtensions.length === 0) {
11565
+ return { vendoredRefs: [], excludedRefs: [] };
11566
+ }
11567
+ if (mode === "exclude") {
11568
+ const excludedRefs = [...escapingStyles, ...escapingExtensions].map((e) => e.value);
11569
+ dropEscaping(doc, stylesSeq, "styles", escapingStyles);
11570
+ dropEscaping(doc, extensionsSeq, "extensions", escapingExtensions);
11571
+ await writeManifestDoc(file, doc);
11572
+ return { vendoredRefs: [], excludedRefs };
11573
+ }
11574
+ const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
11575
+ const extensionFiles = escapingExtensions.map((p) => path18.resolve(sourceProjectDir, p.value));
11576
+ const externalFiles = [...new Set([...styleClosure, ...extensionFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
11577
+ const base = commonAncestorDir(externalFiles);
11578
+ const vendorDir = pickVendorDir(templateDir, base);
11579
+ await Promise.all(externalFiles.filter((abs) => existsSync13(abs)).map(async (abs) => {
11580
+ const dest = path18.join(templateDir, vendorDir, path18.relative(base, abs));
11581
+ await mkdir9(path18.dirname(dest), { recursive: true });
11582
+ await cp3(abs, dest, { recursive: true });
11583
+ }));
11584
+ const bookLocal = (value) => toPosixPath(path18.join(vendorDir, path18.relative(base, path18.resolve(sourceProjectDir, value))));
11585
+ const vendoredRefs = [];
11586
+ for (const { node, value } of escapingStyles) {
11587
+ const rewritten = bookLocal(value);
11588
+ rewriteEntry(node, rewritten);
11589
+ vendoredRefs.push(rewritten);
11590
+ }
11591
+ for (const { node, value } of escapingExtensions) {
11592
+ const rewritten = bookLocal(value);
11593
+ rewriteEntry(node, `./${rewritten}`);
11594
+ vendoredRefs.push(rewritten);
11595
+ }
11596
+ await writeManifestDoc(file, doc);
11597
+ return { vendoredRefs, excludedRefs: [] };
11598
+ }
11599
+ async function saveProjectAsTemplate(options) {
11600
+ const { projectDir, name, templatesRoot } = options;
11601
+ const id = slugify(name);
11602
+ if (!id)
11603
+ throw new Error(`Could not derive a template id from "${name}".`);
11604
+ const dir = path18.join(templatesRoot, id);
11605
+ let exists = true;
11752
11606
  try {
11753
- pa = parseExtensionSpecifier(a);
11754
- pb = parseExtensionSpecifier(b);
11607
+ await access3(dir, FS3.F_OK);
11755
11608
  } catch {
11756
- return false;
11609
+ exists = false;
11757
11610
  }
11758
- return pa.kind === "npm" && pb.kind === "npm" && pa.name === pb.name;
11759
- }
11760
- function indexOfUse(seq, use) {
11761
- const items = seq.items;
11762
- const exact = items.findIndex((item) => entryUse(item) === use.trim());
11763
- if (exact >= 0)
11764
- return exact;
11765
- return items.findIndex((item) => {
11766
- const u = entryUse(item);
11767
- return !!u && sameExtension(u, use);
11768
- });
11769
- }
11770
- function manifestPathFor(projectDir, abs) {
11771
- const rel = path17.relative(path17.resolve(projectDir), abs).split(path17.sep).join("/");
11772
- if (rel === "") {
11773
- throw new Error("An extension cannot be the project folder itself.");
11611
+ if (exists) {
11612
+ throw new Error(`A template named "${name}" already exists. Choose a different name.`);
11774
11613
  }
11775
- if (isPathSpecifier(rel))
11776
- return rel;
11777
- return `./${rel}`;
11778
- }
11779
- var NO_CARRIES = {
11780
- markdown: false,
11781
- styles: false,
11782
- snippets: false,
11783
- components: false
11784
- };
11785
- async function describeFolder(dir, base, fallbackLabel, labelFrom = prettify) {
11786
- const meta = await readExtensionMeta(dir);
11787
- const styles = extensionStyleList(meta);
11614
+ await mkdir9(dir, { recursive: true });
11615
+ const entries = await readdir7(projectDir, { withFileTypes: true });
11616
+ for (const entry of entries) {
11617
+ if (SKIP_ENTRIES.has(entry.name))
11618
+ continue;
11619
+ if (entry.name === TEMPLATE_META_FILE)
11620
+ continue;
11621
+ await cp3(path18.join(projectDir, entry.name), path18.join(dir, entry.name), { recursive: true });
11622
+ }
11623
+ const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
11624
+ const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
11625
+ if (manifestName)
11626
+ await retokeniseManifest(path18.join(dir, manifestName));
11627
+ await writeFile10(path18.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
11788
11628
  return {
11789
- ...base,
11790
- label: meta.name?.trim() ? labelFrom(meta.name.trim()) : fallbackLabel,
11791
- ...meta.description?.trim() ? { description: meta.description.trim() } : {},
11792
- ...meta.author?.trim() ? { author: meta.author.trim() } : {},
11793
- ...meta.preview !== undefined ? { preview: meta.preview } : {},
11794
- ...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
11795
- ...styles.length > 0 ? { styles } : {},
11796
- carries: extensionCarries(meta),
11797
- dir
11629
+ id,
11630
+ label: name,
11631
+ description: "Your saved template.",
11632
+ kind: "custom",
11633
+ dir,
11634
+ ...shared
11798
11635
  };
11799
11636
  }
11800
- async function describeEntry(projectDir, raw) {
11801
- const warnings = [];
11802
- let parsed = null;
11637
+ async function retokeniseManifest(manifestPath) {
11638
+ let text;
11803
11639
  try {
11804
- parsed = parseExtensionSpecifier(raw.use);
11805
- } catch (error2) {
11806
- warnings.push(error2 instanceof Error ? error2.message : String(error2));
11807
- }
11808
- const common = {
11809
- use: raw.use,
11810
- enabled: raw.enabled,
11811
- ...raw.export ? { export: raw.export } : {}
11812
- };
11813
- const withWarnings = (entry) => warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
11814
- if (!parsed) {
11815
- return withWarnings({ ...common, kind: "npm", name: raw.use, label: raw.use, carries: NO_CARRIES });
11640
+ text = await readFile29(manifestPath, "utf8");
11641
+ } catch {
11642
+ return;
11816
11643
  }
11817
- if (parsed.kind === "bundled") {
11818
- const bundledName = parsed.name;
11819
- const rec = RECOMMENDED_EXTENSIONS.find((r) => r.use === bundledName);
11820
- return withWarnings({
11821
- ...common,
11822
- kind: "bundled",
11823
- name: bundledName,
11824
- label: rec?.label ?? bundledName,
11825
- ...rec ? { description: rec.description } : {},
11826
- carries: { ...NO_CARRIES, markdown: true }
11827
- });
11644
+ text = text.replace(/^title:\s*.*$/m, 'title: "{{TITLE}}"').replace(/^(authors:[^\n]*\n[ \t]*-[ \t]*).*$/m, '$1"{{AUTHOR}}"');
11645
+ await writeFile10(manifestPath, text, "utf8");
11646
+ }
11647
+ async function listCustomTemplates(templatesRoot) {
11648
+ let entries;
11649
+ try {
11650
+ entries = await readdir7(templatesRoot, { withFileTypes: true });
11651
+ } catch {
11652
+ return [];
11828
11653
  }
11829
- if (parsed.kind === "path") {
11830
- const abs = path17.resolve(projectDir, parsed.path);
11831
- const base2 = { ...common, kind: "path", name: parsed.path };
11832
- let info2;
11654
+ const out = [];
11655
+ for (const entry of entries) {
11656
+ if (!entry.isDirectory())
11657
+ continue;
11658
+ const dir = path18.join(templatesRoot, entry.name);
11659
+ let label = prettify(entry.name);
11833
11660
  try {
11834
- info2 = await stat5(abs);
11835
- } catch {
11836
- warnings.push(`Not found: ${abs}`);
11837
- return withWarnings({ ...base2, label: path17.basename(abs), carries: NO_CARRIES });
11838
- }
11839
- if (info2.isDirectory()) {
11840
- return withWarnings(await describeFolder(abs, base2, prettify(path17.basename(abs))));
11841
- }
11842
- return withWarnings({
11843
- ...base2,
11844
- label: path17.basename(abs),
11845
- carries: { ...NO_CARRIES, markdown: true }
11661
+ const meta = JSON.parse(await readFile29(path18.join(dir, TEMPLATE_META_FILE), "utf8"));
11662
+ if (typeof meta.label === "string" && meta.label)
11663
+ label = meta.label;
11664
+ } catch {}
11665
+ out.push({
11666
+ id: entry.name,
11667
+ label,
11668
+ description: "Your saved template.",
11669
+ kind: "custom",
11670
+ dir,
11671
+ ...await readTemplateChoices(path18.join(dir, MANIFEST_FILENAMES[0]))
11846
11672
  });
11847
11673
  }
11848
- const base = {
11849
- ...common,
11850
- kind: "npm",
11851
- name: parsed.name,
11852
- ...parsed.version ? { version: parsed.version } : {}
11674
+ out.sort((a, b) => a.label.localeCompare(b.label));
11675
+ return out;
11676
+ }
11677
+ async function importTemplateFromFolder(options) {
11678
+ const { sourceDir, templatesRoot } = options;
11679
+ const srcStat = await stat6(sourceDir);
11680
+ if (!srcStat.isDirectory()) {
11681
+ throw new Error("The chosen path is not a folder.");
11682
+ }
11683
+ const name = options.name?.trim() || prettify(path18.basename(sourceDir));
11684
+ return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
11685
+ }
11686
+ // src/lib/snippets.ts
11687
+ import { readdir as readdir8, readFile as readFile32, writeFile as writeFile11, mkdir as mkdir12, rm as rm5 } from "node:fs/promises";
11688
+ import path21 from "node:path";
11689
+
11690
+ // src/lib/extension-manager.ts
11691
+ import { cp as cp4, mkdir as mkdir11, readFile as readFile31, rm as rm4, stat as stat7 } from "node:fs/promises";
11692
+ import { existsSync as existsSync14 } from "node:fs";
11693
+ import path20 from "node:path";
11694
+ import { isSeq as isSeq3, isMap as isMap2, isScalar as isScalar2, YAMLMap, Scalar } from "yaml";
11695
+
11696
+ // src/lib/npm-plugin-installer.ts
11697
+ import { createHash as createHash7, randomUUID as randomUUID3, timingSafeEqual } from "node:crypto";
11698
+ import { createReadStream as createReadStream2 } from "node:fs";
11699
+ import {
11700
+ lstat as lstat2,
11701
+ mkdir as mkdir10,
11702
+ mkdtemp as mkdtemp2,
11703
+ open as open2,
11704
+ readFile as readFile30,
11705
+ realpath as realpath2,
11706
+ rename as rename4,
11707
+ rm as rm3
11708
+ } from "node:fs/promises";
11709
+ import path19 from "node:path";
11710
+ import { Transform } from "node:stream";
11711
+ import { pipeline } from "node:stream/promises";
11712
+ import { createGunzip } from "node:zlib";
11713
+ import { maxSatisfying, satisfies as satisfies2, validRange as validRange2 } from "semver";
11714
+
11715
+ // src/lib/fetch-timeout.ts
11716
+ class FetchPassthroughError extends Error {
11717
+ }
11718
+
11719
+ class FriendlyHttpError extends FetchPassthroughError {
11720
+ }
11721
+
11722
+ class FetchUnavailableError extends Error {
11723
+ }
11724
+ function composeSignal(signal, timeoutMs) {
11725
+ const timeout = AbortSignal.timeout(timeoutMs);
11726
+ return {
11727
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
11728
+ timeout
11853
11729
  };
11854
- if (!parsed.version) {
11855
- warnings.push(`Not pinned — run \`gutterpress ext add ${parsed.name}\` to install it and pin an exact version.`);
11856
- return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
11857
- }
11858
- let installRoot = null;
11730
+ }
11731
+ async function withFetchTimeout(options, run2) {
11732
+ const composed = composeSignal(options.signal, options.timeoutMs);
11859
11733
  try {
11860
- installRoot = await resolveVendoredPluginInstallRoot(projectDir, parsed.name, parsed.version);
11861
- } catch (error2) {
11862
- warnings.push(error2 instanceof Error ? error2.message : String(error2));
11863
- }
11864
- if (!installRoot) {
11865
- warnings.push(`Not installed — run \`gutterpress ext add ${raw.use}\`.`);
11866
- return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
11867
- }
11868
- const pkgDir = vendoredNpmPluginPackageDir(installRoot, parsed.name);
11869
- if (!existsSync12(path17.join(pkgDir, EXTENSION_MANIFEST_FILENAME))) {
11870
- return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
11734
+ return await run2(composed.signal);
11735
+ } catch (cause) {
11736
+ if (cause instanceof FetchPassthroughError)
11737
+ throw cause;
11738
+ if (options.signal?.aborted)
11739
+ throw cause;
11740
+ const offline = typeof options.offlineMessage === "function" ? options.offlineMessage(cause) : options.offlineMessage;
11741
+ const timedOut = composed.timeout.aborted || cause instanceof Error && cause.name === "TimeoutError";
11742
+ if (cause instanceof Error && cause.name === "AbortError" && !timedOut)
11743
+ throw cause;
11744
+ throw new FetchUnavailableError(timedOut ? options.timeoutMessage ?? offline : offline, { cause });
11871
11745
  }
11872
- const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
11873
- return withWarnings({
11874
- ...described,
11875
- carries: { ...described.carries, markdown: true }
11876
- });
11877
11746
  }
11878
- async function listProjectExtensions(projectDir) {
11879
- const { doc } = await loadManifestDoc(projectDir);
11880
- const seq = doc.get("extensions", true);
11881
- if (!isSeq2(seq))
11882
- return [];
11883
- const out = [];
11884
- for (const item of seq.items) {
11885
- const raw = rawEntry(item);
11886
- if (!raw)
11887
- continue;
11888
- out.push(await describeEntry(projectDir, raw));
11747
+
11748
+ // src/lib/npm-plugin-installer.ts
11749
+ var DEFAULT_LIMITS = {
11750
+ metadataBytes: 15 * 1024 * 1024,
11751
+ packageTarballBytes: 50 * 1024 * 1024,
11752
+ totalNetworkBytes: 250 * 1024 * 1024,
11753
+ packageFileBytes: 50 * 1024 * 1024,
11754
+ totalUnpackedBytes: 512 * 1024 * 1024,
11755
+ totalFiles: 50000,
11756
+ totalPackages: 512,
11757
+ dependencyDepth: 64
11758
+ };
11759
+ var METADATA_TIMEOUT_MS = 30000;
11760
+ var TARBALL_TIMEOUT_MS = 120000;
11761
+
11762
+ class SecurityInstallError extends FetchPassthroughError {
11763
+ }
11764
+
11765
+ class PackageUnavailableError extends Error {
11766
+ }
11767
+ function isOptionalAvailabilityError(error2) {
11768
+ return error2 instanceof PackageUnavailableError || error2 instanceof FriendlyHttpError || error2 instanceof FetchUnavailableError;
11769
+ }
11770
+ function object3(value) {
11771
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
11772
+ }
11773
+ function stringMap2(value) {
11774
+ const map = object3(value);
11775
+ if (!map)
11776
+ return {};
11777
+ const result = {};
11778
+ for (const [name, selector] of Object.entries(map)) {
11779
+ if (typeof selector === "string" && selector.trim())
11780
+ result[name] = selector.trim();
11889
11781
  }
11890
- return out;
11782
+ return result;
11891
11783
  }
11892
- async function describeExtension(projectDir, use) {
11893
- return (await listProjectExtensions(projectDir)).find((e) => sameExtension(e.use, use)) ?? null;
11784
+ function mb(bytes) {
11785
+ return `${Math.ceil(bytes / 1024 / 1024)}MB`;
11894
11786
  }
11895
- function extensionConfigFor(entry) {
11896
- return {
11897
- use: entry.use,
11898
- ...entry.kind === "path" ? { path: entry.name } : { name: entry.name, ...entry.version ? { version: entry.version } : {} },
11899
- ...entry.export ? { export: entry.export } : {},
11900
- options: {}
11901
- };
11787
+ function isContained2(root, candidate) {
11788
+ const relative2 = path19.relative(root, candidate);
11789
+ return relative2 === "" || !relative2.startsWith("..") && !path19.isAbsolute(relative2);
11902
11790
  }
11903
- async function setExtensionEnabled(projectDir, use, enabled) {
11904
- return withMutationLock(projectDir, async () => {
11905
- const { doc, file } = await loadManifestDoc(projectDir);
11906
- const seq = doc.get("extensions", true);
11907
- const idx = isSeq2(seq) ? indexOfUse(seq, use) : -1;
11908
- if (!isSeq2(seq) || idx < 0)
11909
- throw new Error(`Extension "${use}" is not in the manifest.`);
11910
- const item = seq.items[idx];
11911
- if (enabled) {
11912
- if (isMap(item)) {
11913
- item.delete("enabled");
11914
- if (item.items.length === 1 && typeof item.get("use") === "string") {
11915
- seq.items[idx] = new Scalar(item.get("use"));
11916
- }
11791
+ function mergeLimits(overrides) {
11792
+ const limits = { ...DEFAULT_LIMITS, ...overrides };
11793
+ for (const [name, value] of Object.entries(limits)) {
11794
+ if (!Number.isSafeInteger(value) || value <= 0) {
11795
+ throw new Error(`Invalid npm plugin install limit ${name}: ${value}`);
11796
+ }
11797
+ }
11798
+ return limits;
11799
+ }
11800
+ async function streamResponse(ctx, response, maxBytes, totalLimitMessage, onChunk) {
11801
+ if (!response.body)
11802
+ throw new Error("Registry response had no body.");
11803
+ const declared = Number(response.headers.get("content-length"));
11804
+ if (Number.isFinite(declared) && declared > maxBytes) {
11805
+ throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
11806
+ }
11807
+ if (Number.isFinite(declared) && declared > ctx.limits.totalNetworkBytes - ctx.totalNetworkBytes) {
11808
+ throw new SecurityInstallError(totalLimitMessage);
11809
+ }
11810
+ const reader = response.body.getReader();
11811
+ let total = 0;
11812
+ try {
11813
+ while (true) {
11814
+ const { done, value } = await reader.read();
11815
+ if (done)
11816
+ break;
11817
+ total += value.length;
11818
+ if (total > maxBytes) {
11819
+ await reader.cancel("size limit exceeded").catch(() => {});
11820
+ throw new SecurityInstallError(`Registry response is too large (max ${mb(maxBytes)}).`);
11917
11821
  }
11918
- } else if (isMap(item)) {
11919
- item.set("enabled", false);
11920
- } else {
11921
- const map = new YAMLMap(doc.schema);
11922
- map.set("use", entryUse(item));
11923
- map.set("enabled", false);
11924
- seq.items[idx] = map;
11822
+ if (ctx.totalNetworkBytes + value.length > ctx.limits.totalNetworkBytes) {
11823
+ await reader.cancel("total size limit exceeded").catch(() => {});
11824
+ throw new SecurityInstallError(totalLimitMessage);
11825
+ }
11826
+ ctx.totalNetworkBytes += value.length;
11827
+ await onChunk(value);
11925
11828
  }
11926
- await writeManifestDoc(file, doc);
11927
- });
11829
+ } finally {
11830
+ reader.releaseLock();
11831
+ }
11832
+ return total;
11928
11833
  }
11929
- async function reorderExtensions(projectDir, order) {
11930
- return withMutationLock(projectDir, async () => {
11931
- const { doc, file } = await loadManifestDoc(projectDir);
11932
- const seq = doc.get("extensions", true);
11933
- if (!isSeq2(seq))
11934
- throw new Error(`No extensions are configured in ${path17.basename(file)}.`);
11935
- const items = seq.items;
11936
- const current = items.map((item) => entryUse(item));
11937
- const wanted = order.map((u) => u.trim());
11938
- const sameSet = current.length === wanted.length && [...current].sort().join(`
11939
- `) === [...wanted].sort().join(`
11940
- `);
11941
- if (!sameSet) {
11942
- throw new Error("The new order must list every configured extension exactly once — reload the list and try again.");
11834
+ async function fetchPackument(ctx, name) {
11835
+ let pending = ctx.packuments.get(name);
11836
+ if (pending)
11837
+ return pending;
11838
+ pending = withFetchTimeout({
11839
+ timeoutMs: METADATA_TIMEOUT_MS,
11840
+ signal: ctx.signal,
11841
+ timeoutMessage: `Looking up ${name} on npm timed out. Check your connection and try again.`,
11842
+ offlineMessage: (cause) => `Looking up ${name} on npm failed (${cause instanceof Error ? cause.message : String(cause)}).`
11843
+ }, async (signal) => {
11844
+ const response = await ctx.fetch(`${npmRegistryUrl()}/${encodeURIComponent(name)}`, {
11845
+ signal,
11846
+ redirect: "error",
11847
+ headers: { accept: "application/vnd.npm.install-v1+json" }
11848
+ });
11849
+ if (response.status === 404) {
11850
+ throw new FriendlyHttpError(`npm package "${name}" was not found.`);
11943
11851
  }
11944
- const remaining = [...items];
11945
- seq.items = wanted.map((u) => {
11946
- const i = remaining.findIndex((item) => entryUse(item) === u);
11947
- return remaining.splice(i, 1)[0];
11852
+ if (!response.ok) {
11853
+ throw new FriendlyHttpError(`Looking up ${name} on npm failed (HTTP ${response.status}).`);
11854
+ }
11855
+ const chunks = [];
11856
+ const size = await streamResponse(ctx, response, ctx.limits.metadataBytes, `Plugin dependency metadata exceeds the ${mb(ctx.limits.totalNetworkBytes)} total download limit.`, (chunk2) => {
11857
+ chunks.push(chunk2);
11948
11858
  });
11949
- await writeManifestDoc(file, doc);
11950
- });
11951
- }
11952
- async function removeExtension(projectDir, use) {
11953
- return withMutationLock(projectDir, async () => {
11954
- const { doc, file } = await loadManifestDoc(projectDir);
11955
- const seq = doc.get("extensions", true);
11956
- const idx = isSeq2(seq) ? indexOfUse(seq, use) : -1;
11957
- if (!isSeq2(seq) || idx < 0)
11958
- throw new Error(`Extension "${use}" is not in the manifest.`);
11959
- const written = entryUse(seq.items[idx]);
11960
- seq.items.splice(idx, 1);
11961
- if (seq.items.length === 0)
11962
- doc.delete("extensions");
11963
- await writeManifestDoc(file, doc);
11964
- let parsed = null;
11965
- try {
11966
- parsed = parseExtensionSpecifier(written);
11967
- } catch {
11968
- return;
11859
+ const bytes = new Uint8Array(size);
11860
+ let offset = 0;
11861
+ for (const chunk2 of chunks) {
11862
+ bytes.set(chunk2, offset);
11863
+ offset += chunk2.length;
11969
11864
  }
11970
- if (parsed.kind === "npm" && parsed.version) {
11971
- await rm4(vendoredNpmPluginRoot(projectDir, parsed.name, parsed.version), {
11972
- recursive: true,
11973
- force: true
11974
- });
11975
- clearVendoredPluginResolver(projectDir, parsed.name, parsed.version);
11865
+ let metadata = null;
11866
+ try {
11867
+ metadata = object3(JSON.parse(new TextDecoder().decode(bytes)));
11868
+ } catch {}
11869
+ if (!metadata || metadata.name !== name) {
11870
+ throw new FriendlyHttpError(`npm returned invalid metadata for "${name}".`);
11976
11871
  }
11872
+ return metadata;
11873
+ }).catch((error2) => {
11874
+ ctx.packuments.delete(name);
11875
+ throw error2;
11977
11876
  });
11877
+ ctx.packuments.set(name, pending);
11878
+ return pending;
11978
11879
  }
11979
- async function upsertEntry(projectDir, use, exportName) {
11980
- const { doc, file } = await loadManifestDoc(projectDir);
11981
- const seq = ensureSeq(doc, "extensions");
11982
- const idx = indexOfUse(seq, use);
11983
- if (idx >= 0) {
11984
- const item = seq.items[idx];
11985
- if (isMap(item)) {
11986
- item.set("use", use);
11987
- if (exportName)
11988
- item.set("export", exportName);
11989
- else
11990
- item.delete("export");
11991
- if (item.items.length === 1)
11992
- seq.items[idx] = new Scalar(use);
11993
- } else if (exportName) {
11994
- const map = new YAMLMap(doc.schema);
11995
- map.set("use", use);
11996
- map.set("export", exportName);
11997
- seq.items[idx] = map;
11998
- } else {
11999
- seq.items[idx] = new Scalar(use);
11880
+ function parseIntegrity(dist) {
11881
+ const integrity = typeof dist.integrity === "string" ? dist.integrity.trim() : "";
11882
+ const parsed = integrity.split(/\s+/).map((token) => token.match(/^(sha512|sha384|sha256|sha1)-([A-Za-z0-9+/]+={0,2})(?:\?.*)?$/)).filter((match) => Boolean(match?.[1] && match[2]));
11883
+ for (const algorithm of ["sha512", "sha384", "sha256"]) {
11884
+ const match = parsed.find((candidate) => candidate[1] === algorithm);
11885
+ if (!match)
11886
+ continue;
11887
+ const digest = Buffer.from(match[2], "base64");
11888
+ if (digest.length > 0) {
11889
+ return { algorithm, digest, label: `${algorithm}-${match[2]}`, legacySha1: false };
12000
11890
  }
12001
- } else if (exportName) {
12002
- const map = new YAMLMap(doc.schema);
12003
- map.set("use", use);
12004
- map.set("export", exportName);
12005
- seq.add(map);
12006
- } else {
12007
- seq.add(new Scalar(use));
12008
11891
  }
12009
- await writeManifestDoc(file, doc);
11892
+ const sha1 = parsed.find((candidate) => candidate[1] === "sha1");
11893
+ if (sha1) {
11894
+ return {
11895
+ algorithm: "sha1",
11896
+ digest: Buffer.from(sha1[2], "base64"),
11897
+ label: `sha1-${sha1[2]}`,
11898
+ legacySha1: true
11899
+ };
11900
+ }
11901
+ if (integrity) {
11902
+ throw new SecurityInstallError("The npm registry supplied integrity data, but no supported SHA-256-or-stronger hash.");
11903
+ }
11904
+ const shasum = typeof dist.shasum === "string" ? dist.shasum.trim().toLowerCase() : "";
11905
+ if (/^[a-f0-9]{40}$/.test(shasum)) {
11906
+ return {
11907
+ algorithm: "sha1",
11908
+ digest: Buffer.from(shasum, "hex"),
11909
+ label: `sha1-hex-${shasum}`,
11910
+ legacySha1: true
11911
+ };
11912
+ }
11913
+ throw new SecurityInstallError("The npm registry provided no usable integrity hash, so the package was not installed.");
12010
11914
  }
12011
- async function addPathExtension(projectDir, abs, exportName) {
11915
+ function unsupportedSelector(selector) {
11916
+ return /^(?:file:|git\+|https?:|ssh:|github:|workspace:|link:|npm:)/i.test(selector);
11917
+ }
11918
+ async function selectPackage(ctx, name, selector) {
11919
+ if (selector && unsupportedSelector(selector)) {
11920
+ throw new PackageUnavailableError(`Dependency selector "${selector}" for ${name} is not a registry semver range or dist-tag.`);
11921
+ }
11922
+ const metadata = await fetchPackument(ctx, name);
11923
+ const versions = object3(metadata.versions);
11924
+ const tags = object3(metadata["dist-tags"]);
11925
+ if (!versions || !tags)
11926
+ throw new PackageUnavailableError(`npm metadata for "${name}" has no versions.`);
11927
+ let version;
11928
+ if (!selector) {
11929
+ if (typeof tags.latest === "string")
11930
+ version = tags.latest;
11931
+ } else if (object3(versions[selector])) {
11932
+ version = selector;
11933
+ } else if (typeof tags[selector] === "string") {
11934
+ version = tags[selector];
11935
+ } else if (validRange2(selector)) {
11936
+ version = maxSatisfying(Object.keys(versions), selector) ?? undefined;
11937
+ }
11938
+ if (!version || !isExactNpmVersion(version)) {
11939
+ throw new PackageUnavailableError(`No exact npm version of "${name}" satisfies "${selector ?? "latest"}".`);
11940
+ }
11941
+ const selected = object3(versions[version]);
11942
+ if (!selected || selected.name !== name || selected.version !== version) {
11943
+ throw new SecurityInstallError(`npm metadata identity failed for "${name}@${version}".`);
11944
+ }
11945
+ const dist = object3(selected.dist);
11946
+ if (!dist || typeof dist.tarball !== "string") {
11947
+ throw new PackageUnavailableError(`npm metadata for "${name}@${version}" has no tarball.`);
11948
+ }
11949
+ let tarball;
12012
11950
  try {
12013
- await stat5(abs);
11951
+ tarball = new URL(dist.tarball);
12014
11952
  } catch {
12015
- throw new Error(`Extension not found: ${abs}`);
11953
+ throw new SecurityInstallError(`npm returned an invalid tarball URL for "${name}@${version}".`);
12016
11954
  }
12017
- const use = manifestPathFor(projectDir, abs);
12018
- await loadPlugin(extensionConfigFor({ use, kind: "path", name: use, export: exportName }), projectDir);
12019
- await upsertEntry(projectDir, use, exportName);
12020
- return await describeExtension(projectDir, use);
11955
+ if (tarball.origin !== new URL(npmRegistryUrl()).origin || tarball.username || tarball.password) {
11956
+ throw new SecurityInstallError(`npm returned an unexpected tarball host for "${name}@${version}".`);
11957
+ }
11958
+ return { name, version, tarball: tarball.href, integrity: parseIntegrity(dist) };
12021
11959
  }
12022
- async function addExtension(projectDir, specifier, options = {}) {
12023
- return withMutationLock(projectDir, () => addExtensionUnlocked(projectDir, specifier, options));
11960
+ async function downloadTarball(ctx, pkg, archivePath) {
11961
+ const file = await open2(archivePath, "wx");
11962
+ const hash = createHash7(pkg.integrity.algorithm);
11963
+ try {
11964
+ await withFetchTimeout({
11965
+ timeoutMs: TARBALL_TIMEOUT_MS,
11966
+ signal: ctx.signal,
11967
+ timeoutMessage: `Downloading ${pkg.name}@${pkg.version} timed out.`,
11968
+ offlineMessage: (cause) => `Downloading ${pkg.name}@${pkg.version} failed (${cause instanceof Error ? cause.message : String(cause)}).`
11969
+ }, async (signal) => {
11970
+ const response = await ctx.fetch(pkg.tarball, { signal, redirect: "error" });
11971
+ if (!response.ok) {
11972
+ throw new FriendlyHttpError(`Downloading ${pkg.name}@${pkg.version} failed (HTTP ${response.status}).`);
11973
+ }
11974
+ await streamResponse(ctx, response, ctx.limits.packageTarballBytes, `Plugin dependency downloads exceed the ${mb(ctx.limits.totalNetworkBytes)} total limit.`, async (chunk2) => {
11975
+ hash.update(chunk2);
11976
+ try {
11977
+ await file.write(chunk2);
11978
+ } catch (cause) {
11979
+ throw new SecurityInstallError(`Could not store ${pkg.name}@${pkg.version} while downloading.`, { cause });
11980
+ }
11981
+ });
11982
+ });
11983
+ } finally {
11984
+ await file.close();
11985
+ }
11986
+ const actual = hash.digest();
11987
+ if (actual.length !== pkg.integrity.digest.length || !timingSafeEqual(actual, pkg.integrity.digest)) {
11988
+ throw new SecurityInstallError(`The tarball for ${pkg.name}@${pkg.version} failed its registry integrity check.`);
11989
+ }
12024
11990
  }
12025
- async function addExtensionUnlocked(projectDir, specifier, options) {
12026
- const input = specifier.trim();
12027
- const exportName = options.exportName?.trim() || undefined;
12028
- if (path17.isAbsolute(input))
12029
- return addPathExtension(projectDir, input, exportName);
12030
- const parsed = parseExtensionSpecifier(input);
12031
- if (parsed.kind === "path") {
12032
- return addPathExtension(projectDir, path17.resolve(projectDir, parsed.path), exportName);
11991
+ function safeTarRelative(entryPath, packageName) {
11992
+ if (!entryPath || entryPath.includes("\\") || entryPath.startsWith("/") || /^[a-zA-Z]:/.test(entryPath)) {
11993
+ throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath || "(empty)"}`);
12033
11994
  }
12034
- if (parsed.kind === "bundled") {
12035
- if (exportName)
12036
- throw new Error(`Bundled extension "${parsed.name}" does not take a named export.`);
12037
- await upsertEntry(projectDir, parsed.name);
12038
- return await describeExtension(projectDir, parsed.name);
11995
+ const parts = entryPath.split("/");
11996
+ if (parts.at(-1) === "")
11997
+ parts.pop();
11998
+ const legacyTypesRoot = packageName.startsWith("@types/") ? packageName.slice("@types/".length) : null;
11999
+ if (parts.some((part) => !part || part === "." || part === "..") || parts[0] !== "package" && parts[0] !== legacyTypesRoot) {
12000
+ throw new SecurityInstallError(`The npm tarball contains an unsafe path: ${entryPath}`);
12039
12001
  }
12040
- const existing = await describeExtension(projectDir, parsed.name);
12041
- const chosenExport = exportName ?? existing?.export;
12042
- const installed = await installNpmPlugin(projectDir, input, options);
12043
- const use = pinnedNpmSpecifier(installed.name, installed.version);
12044
- try {
12045
- await loadPlugin(extensionConfigFor({
12046
- use,
12047
- kind: "npm",
12048
- name: installed.name,
12049
- version: installed.version,
12050
- export: chosenExport
12051
- }), projectDir);
12052
- await options.__testFailBeforeManifestCommit?.();
12053
- await upsertEntry(projectDir, use, chosenExport);
12054
- } catch (cause) {
12055
- try {
12056
- await rollbackNpmPluginInstall(installed);
12057
- clearVendoredPluginResolver(projectDir, installed.name, installed.version);
12058
- } catch (rollbackError) {
12059
- throw new Error(`Extension install failed and its previous vendor tree could not be restored: ` + `${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, { cause });
12002
+ const relative2 = parts.slice(1).join("/");
12003
+ if (!relative2)
12004
+ return { relative: relative2, key: "" };
12005
+ assertWindowsSafeRelativePath(relative2);
12006
+ if (relative2.normalize("NFKC").split("/").some((part) => part.toLowerCase() === "node_modules")) {
12007
+ throw new SecurityInstallError(`The npm tarball contains bundled node_modules (${entryPath}); bundled dependencies are not accepted.`);
12008
+ }
12009
+ return { relative: relative2, key: windowsPathKey(relative2) };
12010
+ }
12011
+ async function extractTarball(ctx, archivePath, packageDir, packageName) {
12012
+ const { extract } = await import("tar");
12013
+ let rejected = null;
12014
+ const seen = new Set;
12015
+ const metadataTypes = new Set([
12016
+ "GlobalExtendedHeader",
12017
+ "ExtendedHeader",
12018
+ "NextFileHasLongLinkpath",
12019
+ "NextFileHasLongPath",
12020
+ "OldGnuLongPath",
12021
+ "OldExtendedHeader"
12022
+ ]);
12023
+ await mkdir10(packageDir, { recursive: true });
12024
+ const unpack = extract({
12025
+ cwd: packageDir,
12026
+ strip: 1,
12027
+ strict: true,
12028
+ preservePaths: false,
12029
+ preserveOwner: false,
12030
+ noMtime: true,
12031
+ chmod: false,
12032
+ maxDepth: ctx.limits.dependencyDepth * 4,
12033
+ filter: (entryPath, rawEntry) => {
12034
+ if (rejected)
12035
+ return false;
12036
+ const entry = rawEntry;
12037
+ if (metadataTypes.has(entry.type))
12038
+ return true;
12039
+ if (entry.type === "Link" || entry.type === "SymbolicLink") {
12040
+ rejected = new SecurityInstallError(`The npm tarball contains an unsafe link: ${entryPath}`);
12041
+ return false;
12042
+ }
12043
+ if (entry.type !== "File" && entry.type !== "OldFile" && entry.type !== "Directory") {
12044
+ rejected = new SecurityInstallError(`The npm tarball contains unsupported entry type ${entry.type}: ${entryPath}`);
12045
+ return false;
12046
+ }
12047
+ try {
12048
+ const { relative: relative2, key } = safeTarRelative(entryPath, packageName);
12049
+ if (!relative2) {
12050
+ if (entry.type === "Directory")
12051
+ return true;
12052
+ throw new SecurityInstallError(`The npm tarball uses package/ as a file.`);
12053
+ }
12054
+ if (seen.has(key)) {
12055
+ throw new SecurityInstallError(`The npm tarball has a Windows-colliding path: ${entryPath}`);
12056
+ }
12057
+ seen.add(key);
12058
+ ctx.totalFiles++;
12059
+ if (ctx.totalFiles > ctx.limits.totalFiles) {
12060
+ throw new SecurityInstallError(`Plugin graph exceeds ${ctx.limits.totalFiles} files.`);
12061
+ }
12062
+ if (entry.type !== "Directory") {
12063
+ if (!Number.isSafeInteger(entry.size) || entry.size < 0 || entry.size > ctx.limits.packageFileBytes) {
12064
+ throw new SecurityInstallError(`Package file exceeds the ${mb(ctx.limits.packageFileBytes)} limit: ${entryPath}`);
12065
+ }
12066
+ }
12067
+ return true;
12068
+ } catch (error2) {
12069
+ rejected = error2 instanceof Error ? error2 : new SecurityInstallError(String(error2));
12070
+ return false;
12071
+ }
12072
+ }
12073
+ });
12074
+ const limiter = new Transform({
12075
+ transform(chunk2, _encoding, callback) {
12076
+ ctx.totalUnpackedBytes += chunk2.length;
12077
+ if (ctx.totalUnpackedBytes > ctx.limits.totalUnpackedBytes) {
12078
+ callback(new SecurityInstallError(`Plugin graph expands beyond the ${mb(ctx.limits.totalUnpackedBytes)} limit.`));
12079
+ } else
12080
+ callback(null, chunk2);
12060
12081
  }
12061
- throw new Error(`Downloaded ${use}, but it is not a loadable markdown-it plugin: ` + `${cause instanceof Error ? cause.message : String(cause)}`, { cause });
12062
- }
12063
- const warnings = [...installed.warnings];
12064
- await finalizeNpmPluginInstall(installed).catch((error2) => {
12065
- warnings.push(`The extension was installed, but an old backup could not be removed: ` + `${error2 instanceof Error ? error2.message : String(error2)}`);
12066
12082
  });
12067
- const entry = await describeExtension(projectDir, use);
12068
- return warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
12069
- }
12070
- async function listBuiltInStyleSets() {
12071
- const assets = await getAssetsDir();
12072
- const out = [];
12073
- for (const id of BUILT_IN_STYLE_SET_IDS) {
12074
- const meta = await readExtensionMeta(path17.join(assets, "themes", id));
12075
- out.push({
12076
- id,
12077
- name: prettify(meta.name?.trim() || id),
12078
- description: meta.description?.trim() || ""
12079
- });
12083
+ try {
12084
+ await pipeline(createReadStream2(archivePath), createGunzip(), limiter, unpack);
12085
+ } catch (error2) {
12086
+ if (rejected)
12087
+ throw rejected;
12088
+ if (error2 instanceof SecurityInstallError)
12089
+ throw error2;
12090
+ throw new SecurityInstallError(`The npm tarball could not be safely extracted (${error2 instanceof Error ? error2.message : String(error2)}).`, { cause: error2 });
12080
12091
  }
12081
- return out;
12092
+ if (rejected)
12093
+ throw rejected;
12082
12094
  }
12083
- async function uniqueExtensionId(projectDir, base) {
12084
- const slug = slugify(base) || "extension";
12085
- let candidate = slug;
12086
- for (let n = 2;existsSync12(path17.join(projectDir, EXTENSIONS_DIR, candidate)); n++) {
12087
- candidate = `${slug}-${n}`;
12095
+ async function readInstalledManifest(packageDir, selected) {
12096
+ const packageJsonPath = path19.join(packageDir, "package.json");
12097
+ let manifest = null;
12098
+ try {
12099
+ const info2 = await lstat2(packageJsonPath);
12100
+ if (!info2.isFile() || info2.isSymbolicLink())
12101
+ throw new Error("not a regular file");
12102
+ manifest = object3(JSON.parse(await readFile30(packageJsonPath, "utf8")));
12103
+ } catch (error2) {
12104
+ throw new SecurityInstallError(`Downloaded ${selected.name}@${selected.version} has no valid package.json (${error2 instanceof Error ? error2.message : String(error2)}).`);
12088
12105
  }
12089
- return candidate;
12090
- }
12091
- async function addBuiltInStyleSet(projectDir, id) {
12092
- if (!BUILT_IN_STYLE_SET_IDS.includes(id)) {
12093
- throw new Error(`Unknown built-in look "${id}" (one of: ${BUILT_IN_STYLE_SET_IDS.join(", ")}).`);
12106
+ if (manifest?.name !== selected.name || manifest.version !== selected.version) {
12107
+ throw new SecurityInstallError(`Downloaded ${String(manifest?.name)}@${String(manifest?.version)} instead of ${selected.name}@${selected.version}.`);
12094
12108
  }
12095
- return withMutationLock(projectDir, async () => {
12096
- const dest = path17.join(projectDir, EXTENSIONS_DIR, id);
12097
- if (!existsSync12(dest)) {
12098
- const src = path17.join(await getAssetsDir(), "themes", id);
12099
- await mkdir8(path17.dirname(dest), { recursive: true });
12100
- await cp2(src, dest, { recursive: true });
12101
- }
12102
- return addExtensionUnlocked(projectDir, `./${EXTENSIONS_DIR}/${id}`, {});
12103
- });
12109
+ return manifest;
12104
12110
  }
12105
- async function validateProjectExtensions(projectDir) {
12106
- const out = [];
12107
- for (const entry of await listProjectExtensions(projectDir)) {
12108
- if (!entry.enabled) {
12109
- out.push({ use: entry.use, kind: entry.kind, enabled: false, ok: true });
12110
- continue;
12111
- }
12112
- try {
12113
- await loadPlugin(extensionConfigFor(entry), projectDir);
12114
- out.push({ use: entry.use, kind: entry.kind, enabled: true, ok: true });
12115
- } catch (e) {
12116
- out.push({
12117
- use: entry.use,
12118
- kind: entry.kind,
12119
- enabled: true,
12120
- ok: false,
12121
- error: e instanceof Error ? e.message : String(e)
12122
- });
12111
+ async function optionalPackageEntry(packageDir, manifest, condition) {
12112
+ try {
12113
+ return await resolvePackageEntry(packageDir, manifest, condition);
12114
+ } catch (error2) {
12115
+ if (error2 instanceof Error && error2.message.includes("no contained JavaScript entry point")) {
12116
+ return;
12123
12117
  }
12118
+ throw error2;
12124
12119
  }
12125
- return out;
12126
- }
12127
- async function readExtensionCss(projectDir, use) {
12128
- const entry = await describeExtension(projectDir, use);
12129
- if (!entry)
12130
- throw new Error(`Extension "${use}" is not in the manifest.`);
12131
- if (!entry.dir)
12132
- throw new Error(`Extension "${use}" has no stylesheets to preview.`);
12133
- const meta = await readExtensionMeta(entry.dir);
12134
- const sheets = resolveExtension(entry.dir, meta, `Extension "${use}"`).styles ?? [];
12135
- if (sheets.length === 0)
12136
- throw new Error(`Extension "${use}" declares no stylesheets.`);
12137
- const parts = [];
12138
- for (const sheet of sheets)
12139
- parts.push(await readFile28(sheet, "utf8"));
12140
- return parts.join(`
12141
-
12142
- `);
12143
- }
12144
-
12145
- // src/lib/project-scaffold.ts
12146
- var STARTER_THEME_FOR_TEMPLATE = {
12147
- book: "clean-book",
12148
- zine: "zine",
12149
- technical: "technical-doc"
12150
- };
12151
- function starterOverridesCss(themeId) {
12152
- return `/**
12153
- * Your book's own styles.
12154
- *
12155
- * Your book's look lives in ${EXTENSIONS_DIR}/${themeId}/theme.css — an
12156
- * extension listed under \`extensions:\` in manifest.yaml. Add or swap looks
12157
- * with \`gutterpress ext add\` (or the desktop's Look view) without losing
12158
- * anything you write here. This file loads AFTER every extension, so any
12159
- * rule you add below overrides the look's defaults.
12160
- *
12161
- * Most everyday adjustments (colors, fonts, margins, spacing) are already
12162
- * exposed as CSS custom properties on the theme's :root — try those first.
12163
- */
12164
- `;
12165
- }
12166
-
12167
- class CreateProjectErrorImpl extends Error {
12168
- code;
12169
- constructor(code, message) {
12170
- super(message);
12171
- this.name = "CreateProjectError";
12172
- this.code = code;
12173
- }
12174
- }
12175
- function slugifyProjectName(name) {
12176
- return slugify(name);
12177
- }
12178
- function escapeYamlScalar(value) {
12179
- return value.replace(/\\/g, "\\\\").replace(/"/g, "\\\"");
12180
- }
12181
- var DEFAULT_AUTHOR = "Anonymous";
12182
- function isPositivePoints(value) {
12183
- return typeof value === "number" && Number.isFinite(value) && value > 0;
12184
12120
  }
12185
- function requirePreset(preset, customPage) {
12186
- if (!preset || !PRESET_IDS.includes(preset)) {
12187
- const got = preset ? ` (got "${preset}")` : "";
12188
- throw new CreateProjectErrorImpl("preset-required", `A preset is required to create a book${got}. Choose one of: ${PRESET_IDS.join(", ")}.`);
12189
- }
12190
- if (preset === "custom") {
12191
- const missing = [];
12192
- if (!customPage || !isPositivePoints(customPage.width))
12193
- missing.push("page width");
12194
- if (!customPage || !isPositivePoints(customPage.height))
12195
- missing.push("page height");
12196
- if (missing.length > 0) {
12197
- throw new CreateProjectErrorImpl("custom-page-required", `The custom preset needs a trim size: ${missing.join(" and ")} in points ` + `(72pt = 1in — e.g. US Letter is 612 x 792).`);
12198
- }
12199
- } else if (customPage && (!isPositivePoints(customPage.width) || !isPositivePoints(customPage.height))) {
12200
- throw new CreateProjectErrorImpl("custom-page-required", "Page overrides need a positive width and height in points (72pt = 1in).");
12201
- }
12202
- if (customPage?.tolerance !== undefined && !isPositivePoints(customPage.tolerance)) {
12203
- throw new CreateProjectErrorImpl("custom-page-required", "Page tolerance must be a positive number of points.");
12121
+ function platformAllowed(rule, current) {
12122
+ let values;
12123
+ if (typeof rule === "string") {
12124
+ values = [rule];
12125
+ } else if (Array.isArray(rule) && rule.every((item) => typeof item === "string")) {
12126
+ values = rule;
12127
+ } else {
12128
+ return true;
12204
12129
  }
12205
- return preset;
12130
+ if (values.length === 0)
12131
+ return true;
12132
+ if (values.length === 1 && values[0] === "any")
12133
+ return true;
12134
+ if (values.includes(`!${current}`))
12135
+ return false;
12136
+ const positives = values.filter((value) => !value.startsWith("!"));
12137
+ return positives.length === 0 || positives.includes(current);
12206
12138
  }
12207
- function resolveScaffoldTargets(requested, preset) {
12208
- const ids = requested ?? [...PRESETS[preset].defaultTargets];
12209
- const unknown = ids.filter((id) => !TARGETS[id]);
12210
- if (unknown.length > 0) {
12211
- throw new CreateProjectErrorImpl("invalid-targets", `Unknown publish target${unknown.length > 1 ? "s" : ""} "${unknown.join('", "')}". ` + `Known targets: ${TARGET_IDS.join(", ")}.`);
12212
- }
12213
- return [...new Set(ids)];
12139
+ function relativePackagePath(parentPackagePath, name) {
12140
+ const segments = name.split("/");
12141
+ return parentPackagePath ? `${parentPackagePath}/node_modules/${segments.join("/")}` : `node_modules/${segments.join("/")}`;
12214
12142
  }
12215
- async function scaffoldProject(options) {
12216
- const name = (options.name ?? "").trim();
12217
- if (!name) {
12218
- throw new CreateProjectErrorImpl("invalid-name", "A project name is required.");
12219
- }
12220
- const slug = slugifyProjectName(name);
12221
- const folderName = (options.folderName ?? slug).trim();
12222
- if (!folderName || folderName !== slugifyProjectName(folderName)) {
12223
- throw new CreateProjectErrorImpl("invalid-name", `Could not derive a valid folder name from "${name}".`);
12224
- }
12225
- const parentDir = options.parentDir;
12226
- if (!parentDir || !path18.isAbsolute(parentDir)) {
12227
- throw new CreateProjectErrorImpl("parent-not-writable", "The save location must be an absolute path.");
12228
- }
12229
- try {
12230
- await access(parentDir, FS.W_OK);
12231
- } catch {
12232
- throw new CreateProjectErrorImpl("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
12143
+ async function installPackage(ctx, name, selector, parentPackagePath, ancestors, depth, optional) {
12144
+ const recordStart = ctx.records.length;
12145
+ const skippedStart = ctx.skipped.length;
12146
+ const warningsBefore = new Set(ctx.warnings);
12147
+ const discardOptionalSubtree = () => {
12148
+ ctx.records.splice(recordStart);
12149
+ ctx.skipped.splice(skippedStart);
12150
+ ctx.warnings = warningsBefore;
12151
+ };
12152
+ if (depth > ctx.limits.dependencyDepth) {
12153
+ throw new SecurityInstallError(`Dependency graph exceeds depth ${ctx.limits.dependencyDepth}.`);
12233
12154
  }
12234
- const projectDir = path18.join(parentDir, folderName);
12235
- let targetExists = true;
12155
+ let selected;
12236
12156
  try {
12237
- await access(projectDir, FS.F_OK);
12238
- } catch {
12239
- targetExists = false;
12157
+ selected = await selectPackage(ctx, name, selector);
12158
+ } catch (error2) {
12159
+ if (optional && isOptionalAvailabilityError(error2)) {
12160
+ discardOptionalSubtree();
12161
+ return { skipped: error2 instanceof Error ? error2.message : String(error2) };
12162
+ }
12163
+ throw error2;
12240
12164
  }
12241
- if (targetExists) {
12242
- throw new CreateProjectErrorImpl("target-exists", `A folder named "${folderName}" already exists here. Choose a different name or location.`);
12165
+ const ancestor = ancestors.find((candidate) => candidate.name === name && (candidate.version === selected.version || (selector ? satisfies2(candidate.version, selector) : false)));
12166
+ if (ancestor)
12167
+ return { path: ancestor.path };
12168
+ if (ctx.records.length >= ctx.limits.totalPackages) {
12169
+ throw new SecurityInstallError(`Dependency graph exceeds ${ctx.limits.totalPackages} packages.`);
12243
12170
  }
12244
- const template = options.template ?? "book";
12245
- const customTemplateDir = options.templateDir;
12246
- const starterThemeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
12247
- const preset = customTemplateDir && options.preset === undefined ? undefined : requirePreset(options.preset, options.customPage);
12248
- const targets = preset ? resolveScaffoldTargets(options.targets, preset) : undefined;
12171
+ const relativePath = relativePackagePath(parentPackagePath, name);
12172
+ const packageDir = path19.join(ctx.stageRoot, ...relativePath.split("/"));
12173
+ const archivePath = path19.join(ctx.downloadsDir, `${ctx.archiveCounter++}.tgz`);
12249
12174
  try {
12250
- await mkdir9(projectDir, { recursive: true });
12251
- if (customTemplateDir) {
12252
- const entries = await readdir6(customTemplateDir, { withFileTypes: true });
12253
- for (const entry of entries) {
12254
- if (entry.name === ".gutterpress-template.json")
12255
- continue;
12256
- await cp3(path18.join(customTemplateDir, entry.name), path18.join(projectDir, entry.name), { recursive: true });
12175
+ await mkdir10(path19.dirname(packageDir), { recursive: true });
12176
+ await downloadTarball(ctx, selected, archivePath);
12177
+ await extractTarball(ctx, archivePath, packageDir, selected.name);
12178
+ const manifest = await readInstalledManifest(packageDir, selected);
12179
+ if (!platformAllowed(manifest.os, process.platform) || !platformAllowed(manifest.cpu, process.arch)) {
12180
+ const reason = `${name}@${selected.version} does not support ${process.platform}/${process.arch}`;
12181
+ if (optional) {
12182
+ await rm3(packageDir, { recursive: true, force: true });
12183
+ return { skipped: reason };
12257
12184
  }
12258
- await mkdir9(path18.join(projectDir, "assets"), { recursive: true });
12259
- } else {
12260
- const tplManifest = await getAssetPath(`templates/${template}/manifest.yaml`);
12261
- const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
12262
- await mkdir9(path18.join(projectDir, "assets"), { recursive: true });
12263
- await copyFile(tplManifest, path18.join(projectDir, MANIFEST_FILENAMES[0]));
12264
- await copyFile(tplChapter, path18.join(projectDir, "chapter-01.md"));
12265
- await mkdir9(path18.join(projectDir, "styles"), { recursive: true });
12266
- await writeFile8(path18.join(projectDir, "styles", "book.css"), starterOverridesCss(starterThemeId), "utf8");
12185
+ throw new PackageUnavailableError(reason);
12186
+ }
12187
+ const importEntry = await optionalPackageEntry(packageDir, manifest, "import");
12188
+ const requireEntry = await optionalPackageEntry(packageDir, manifest, "require");
12189
+ if (parentPackagePath === null && !importEntry && !requireEntry) {
12190
+ throw new SecurityInstallError(`${name}@${selected.version} has no JavaScript plugin entry.`);
12191
+ }
12192
+ const record = {
12193
+ path: relativePath,
12194
+ name,
12195
+ version: selected.version,
12196
+ tarball: selected.tarball,
12197
+ integrity: selected.integrity.label,
12198
+ legacySha1: selected.integrity.legacySha1,
12199
+ ...importEntry ? { entry: `${relativePath}/${importEntry}` } : {},
12200
+ ...requireEntry ? { requireEntry: `${relativePath}/${requireEntry}` } : {},
12201
+ dependencies: {},
12202
+ packageDir,
12203
+ manifest
12204
+ };
12205
+ ctx.records.push(record);
12206
+ if (selected.integrity.legacySha1) {
12207
+ ctx.warnings.add(`${name}@${selected.version} is an old npm release with only SHA-1 registry integrity. It was accepted for compatibility and recorded as legacy SHA-1 in the vendor receipt.`);
12267
12208
  }
12268
- } catch (e) {
12269
- throw new CreateProjectErrorImpl("scaffold-io", `Could not create the project files: ${e instanceof Error ? e.message : String(e)}`);
12270
- }
12271
- try {
12272
- await ensureGitignoreHasDist(projectDir);
12273
- } catch (e) {
12274
- throw new CreateProjectErrorImpl("scaffold-io", `Could not write .gitignore: ${e instanceof Error ? e.message : String(e)}`);
12275
- }
12276
- const manifestPath = MANIFEST_FILENAMES.map((name2) => path18.join(projectDir, name2)).find(existsSync13) ?? path18.join(projectDir, MANIFEST_FILENAMES[0]);
12277
- const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
12278
- const substitutions = {
12279
- "{{TITLE}}": escapeYamlScalar(name),
12280
- "{{AUTHOR}}": escapeYamlScalar(author)
12281
- };
12282
- let openFile = path18.join(projectDir, "chapter-01.md");
12283
- try {
12284
- await fillTemplateFile(manifestPath, substitutions);
12285
- if (!customTemplateDir) {
12286
- await addBuiltInStyleSet(projectDir, starterThemeId);
12209
+ const dependencies = stringMap2(manifest.dependencies);
12210
+ const optionalDependencies = stringMap2(manifest.optionalDependencies);
12211
+ for (const optionalName of Object.keys(optionalDependencies))
12212
+ delete dependencies[optionalName];
12213
+ const peerDependencies = stringMap2(manifest.peerDependencies);
12214
+ const peerMeta = object3(manifest.peerDependenciesMeta) ?? {};
12215
+ for (const existing of [...Object.keys(dependencies), ...Object.keys(optionalDependencies)]) {
12216
+ delete peerDependencies[existing];
12287
12217
  }
12288
- if (preset) {
12289
- const { doc, file } = await loadManifestDoc(projectDir);
12290
- doc.set("preset", preset);
12291
- doc.set("targets", doc.createNode(targets ?? []));
12292
- if (options.customPage) {
12293
- const page = {
12294
- width: options.customPage.width,
12295
- height: options.customPage.height
12296
- };
12297
- if (options.customPage.tolerance !== undefined) {
12298
- page.tolerance = options.customPage.tolerance;
12299
- }
12300
- doc.set("page", doc.createNode(page));
12218
+ const nextAncestors = [...ancestors, record];
12219
+ const requiredEntries = [
12220
+ ...Object.entries(dependencies).map(([dependency, range]) => ({ dependency, range, kind: "required" })),
12221
+ ...Object.entries(peerDependencies).filter(([dependency]) => object3(peerMeta[dependency])?.optional !== true).map(([dependency, range]) => ({ dependency, range, kind: "peer" }))
12222
+ ].sort((a, b) => a.dependency.localeCompare(b.dependency));
12223
+ for (const { dependency, range } of requiredEntries) {
12224
+ const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, false);
12225
+ if (!child.path)
12226
+ throw new Error(`Required dependency ${dependency} was unexpectedly skipped.`);
12227
+ record.dependencies[dependency] = child.path;
12228
+ }
12229
+ const optionalEntries = Object.entries(optionalDependencies).sort(([a], [b]) => a.localeCompare(b));
12230
+ for (const [dependency, range] of optionalEntries) {
12231
+ const child = await installPackage(ctx, dependency, range, relativePath, nextAncestors, depth + 1, true);
12232
+ if (child.path)
12233
+ record.dependencies[dependency] = child.path;
12234
+ else if (child.skipped) {
12235
+ ctx.skipped.push({ from: relativePath, name: dependency, selector: range, kind: "optional", reason: child.skipped });
12301
12236
  }
12302
- await writeManifestDoc(file, doc);
12303
12237
  }
12304
- const firstSource = await firstSourceFile(manifestPath);
12305
- if (firstSource)
12306
- openFile = path18.join(projectDir, firstSource);
12307
- await fillTemplateFile(openFile, { "{{TITLE}}": name }).catch(() => {});
12308
- } catch (e) {
12309
- throw new CreateProjectErrorImpl("scaffold-io", `Could not finalise the project files: ${e instanceof Error ? e.message : String(e)}`);
12310
- }
12311
- const requested = options.versionHistory ?? "local-git";
12312
- let versionHistory = "none";
12313
- let versionHistoryError;
12314
- if (requested === "local-git") {
12315
- try {
12316
- const { providerFor } = await import("./source-provider-jkppq9fc.js");
12317
- const provider = providerFor({ type: "local-folder", path: projectDir });
12318
- await provider.initVersionHistory({
12319
- projectDir,
12320
- authorName: options.author?.trim() || undefined,
12321
- initialMessage: "Created project"
12322
- });
12323
- versionHistory = "local-git";
12324
- } catch (e) {
12325
- versionHistory = "none";
12326
- const { findEnclosingRepoDir } = await import("./project-source-ekcyp63q.js");
12327
- const enclosingRepoDir = await findEnclosingRepoDir(projectDir);
12328
- versionHistoryError = enclosingRepoDir ? `This book was added to ${path18.basename(enclosingRepoDir)}'s shared version history; save a snapshot to record it.` : e instanceof Error ? e.message : String(e);
12238
+ for (const [dependency, range] of Object.entries(peerDependencies)) {
12239
+ if (object3(peerMeta[dependency])?.optional === true) {
12240
+ ctx.skipped.push({
12241
+ from: relativePath,
12242
+ name: dependency,
12243
+ selector: range,
12244
+ kind: "optional-peer",
12245
+ reason: "Optional peer dependencies are not auto-installed."
12246
+ });
12247
+ }
12329
12248
  }
12249
+ return { path: relativePath };
12250
+ } catch (error2) {
12251
+ await rm3(packageDir, { recursive: true, force: true });
12252
+ if (optional && isOptionalAvailabilityError(error2)) {
12253
+ discardOptionalSubtree();
12254
+ return { skipped: error2 instanceof Error ? error2.message : String(error2) };
12255
+ }
12256
+ throw error2;
12257
+ } finally {
12258
+ await rm3(archivePath, { force: true });
12330
12259
  }
12331
- const result = {
12332
- projectDir,
12333
- manifestPath,
12334
- openFile,
12335
- versionHistory
12336
- };
12337
- if (versionHistoryError !== undefined) {
12338
- result.versionHistoryError = versionHistoryError;
12339
- }
12340
- return result;
12341
- }
12342
- function prettifyFolderName(base) {
12343
- const words = base.replace(/[-_]+/g, " ").trim();
12344
- return words.replace(/\b\w/g, (c) => c.toUpperCase()) || "Untitled Book";
12345
12260
  }
12346
- async function adoptFolder(options) {
12347
- const dir = options.dir;
12348
- if (typeof dir !== "string" || !path18.isAbsolute(dir)) {
12349
- throw new CreateProjectErrorImpl("scaffold-io", "An absolute folder path is required.");
12350
- }
12351
- let st;
12261
+ async function ensureDirectory(pathname) {
12352
12262
  try {
12353
- st = await stat6(dir);
12354
- } catch {
12355
- throw new CreateProjectErrorImpl("scaffold-io", `Folder not found: ${dir}`);
12356
- }
12357
- if (!st.isDirectory()) {
12358
- throw new CreateProjectErrorImpl("scaffold-io", `Not a folder: ${dir}`);
12263
+ const info2 = await lstat2(pathname);
12264
+ if (info2.isSymbolicLink() || !info2.isDirectory()) {
12265
+ throw new Error(`Plugin install path is not a normal directory: ${pathname}`);
12266
+ }
12267
+ } catch (error2) {
12268
+ if (error2.code !== "ENOENT")
12269
+ throw error2;
12270
+ await mkdir10(pathname);
12359
12271
  }
12360
- if (MANIFEST_FILENAMES.some((name) => existsSync13(path18.join(dir, name)))) {
12361
- throw new CreateProjectErrorImpl("target-exists", "This folder is already a Gutterpress project.");
12272
+ }
12273
+ async function prepareVendorParent(projectDir, name) {
12274
+ const plugins = path19.join(projectDir, PLUGINS_DIR);
12275
+ const npm = path19.join(plugins, VENDORED_NPM_DIR);
12276
+ const packageParent = path19.dirname(vendoredNpmPluginRoot(projectDir, name, "0.0.0"));
12277
+ await mkdir10(projectDir, { recursive: true });
12278
+ await ensureDirectory(plugins);
12279
+ await ensureDirectory(npm);
12280
+ await ensureDirectory(packageParent);
12281
+ const [realNpm, realPackageParent] = await Promise.all([
12282
+ realpath2(npm),
12283
+ realpath2(packageParent)
12284
+ ]);
12285
+ if (!isContained2(projectDir, realNpm) || !isContained2(projectDir, realPackageParent)) {
12286
+ throw new Error("Plugin install path resolves outside the project.");
12362
12287
  }
12363
- const template = options.template ?? "book";
12364
- const title = (options.title ?? "").trim() || prettifyFolderName(path18.basename(dir));
12365
- const author = (options.author ?? "").trim() || DEFAULT_AUTHOR;
12366
- let mdFiles;
12288
+ return realNpm;
12289
+ }
12290
+ function publicReceipt(record) {
12291
+ return {
12292
+ path: record.path,
12293
+ name: record.name,
12294
+ version: record.version,
12295
+ tarball: record.tarball,
12296
+ integrity: record.integrity,
12297
+ legacySha1: record.legacySha1,
12298
+ ...record.entry ? { entry: record.entry } : {},
12299
+ ...record.requireEntry ? { requireEntry: record.requireEntry } : {},
12300
+ dependencies: record.dependencies
12301
+ };
12302
+ }
12303
+ async function installNpmPlugin(projectDirInput, packageSpec, options = {}) {
12304
+ const requestedProjectDir = path19.resolve(projectDirInput);
12305
+ await mkdir10(requestedProjectDir, { recursive: true });
12306
+ const projectDir = await realpath2(requestedProjectDir);
12307
+ const requested = parseNpmPluginSpec(packageSpec);
12308
+ const npmRoot = await prepareVendorParent(projectDir, requested.name);
12309
+ const container = await mkdtemp2(path19.join(npmRoot, ".install-"));
12310
+ const stageRoot = path19.join(container, "root");
12311
+ const downloadsDir = path19.join(container, "downloads");
12312
+ await Promise.all([mkdir10(stageRoot), mkdir10(downloadsDir)]);
12313
+ const ctx = {
12314
+ fetch: options.fetch ?? globalThis.fetch,
12315
+ signal: options.signal,
12316
+ limits: mergeLimits(options.limits),
12317
+ stageRoot,
12318
+ downloadsDir,
12319
+ packuments: new Map,
12320
+ records: [],
12321
+ skipped: [],
12322
+ warnings: new Set,
12323
+ totalNetworkBytes: 0,
12324
+ totalUnpackedBytes: 0,
12325
+ totalFiles: 0,
12326
+ archiveCounter: 0
12327
+ };
12328
+ let finalRoot = "";
12329
+ let backupRoot = null;
12367
12330
  try {
12368
- const entries = await readdir6(dir, { withFileTypes: true });
12369
- mdFiles = entries.filter((e) => e.isFile() && /\.(md|markdown)$/i.test(e.name)).map((e) => e.name).sort((a, b) => a.localeCompare(b));
12370
- if (mdFiles.length === 0) {
12371
- const tplChapter = await getAssetPath(`templates/${template}/chapter-01.md`);
12372
- await copyFile(tplChapter, path18.join(dir, "chapter-01.md"));
12373
- await fillTemplateFile(path18.join(dir, "chapter-01.md"), { "{{TITLE}}": title }).catch(() => {});
12374
- mdFiles = ["chapter-01.md"];
12375
- }
12376
- const themeId = STARTER_THEME_FOR_TEMPLATE[template] ?? "clean-book";
12377
- await mkdir9(path18.join(dir, "styles"), { recursive: true });
12378
- const bookCssPath = path18.join(dir, "styles", "book.css");
12379
- const hadOwnStylesheet = existsSync13(bookCssPath);
12380
- if (!hadOwnStylesheet) {
12381
- await writeFile8(bookCssPath, starterOverridesCss(themeId), "utf8");
12331
+ const rootResult = await installPackage(ctx, requested.name, requested.selector, null, [], 0, false);
12332
+ if (!rootResult.path)
12333
+ throw new Error("Root plugin package was unexpectedly skipped.");
12334
+ const rootRecord = ctx.records.find((record) => record.path === rootResult.path);
12335
+ if (!rootRecord)
12336
+ throw new Error("Root plugin package is missing from the dependency graph.");
12337
+ const importEntry = rootRecord.entry ?? rootRecord.requireEntry;
12338
+ if (!importEntry)
12339
+ throw new Error("Root plugin package has no recorded JavaScript entry.");
12340
+ const importFormat = packageEntryFormat(rootRecord.manifest, importEntry);
12341
+ const entry = importFormat === "commonjs" ? rootRecord.requireEntry ?? importEntry : importEntry;
12342
+ const format = packageEntryFormat(rootRecord.manifest, entry);
12343
+ const tree = await computeVendorTreeDigest(stageRoot);
12344
+ const receipt = {
12345
+ schemaVersion: VENDOR_RECEIPT_VERSION,
12346
+ root: {
12347
+ name: rootRecord.name,
12348
+ version: rootRecord.version,
12349
+ packagePath: rootRecord.path,
12350
+ entry,
12351
+ format
12352
+ },
12353
+ packages: ctx.records.map(publicReceipt).sort((a, b) => a.path.localeCompare(b.path)),
12354
+ skipped: ctx.skipped,
12355
+ tree: { algorithm: "sha256", ...tree }
12356
+ };
12357
+ const receiptFile = await open2(path19.join(stageRoot, VENDOR_RECEIPT_FILE), "wx");
12358
+ try {
12359
+ await receiptFile.writeFile(`${JSON.stringify(receipt, null, 2)}
12360
+ `, "utf8");
12361
+ await receiptFile.sync();
12362
+ } finally {
12363
+ await receiptFile.close();
12382
12364
  }
12383
- const filesYaml = mdFiles.map((f) => ` - "${escapeYamlScalar(f)}"`).join(`
12384
- `);
12385
- const manifest = `title: "${escapeYamlScalar(title)}"
12386
- authors:
12387
- - "${escapeYamlScalar(author)}"
12388
- preset: dtrpg
12389
- targets:
12390
- - dtrpg
12391
- source:
12392
- files:
12393
- ${filesYaml}
12394
- styles:
12395
- - styles/book.css
12396
- `;
12397
- await writeFile8(path18.join(dir, MANIFEST_FILENAMES[0]), manifest, "utf8");
12398
- await mkdir9(path18.join(dir, "assets"), { recursive: true });
12399
- if (!hadOwnStylesheet) {
12400
- await addBuiltInStyleSet(dir, themeId);
12365
+ finalRoot = vendoredNpmPluginRoot(projectDir, rootRecord.name, rootRecord.version);
12366
+ const realFinalParent = await realpath2(path19.dirname(finalRoot));
12367
+ if (!isContained2(projectDir, realFinalParent)) {
12368
+ throw new Error("Plugin install destination resolves outside the project.");
12369
+ }
12370
+ try {
12371
+ await lstat2(finalRoot);
12372
+ backupRoot = `${finalRoot}.backup-${randomUUID3()}`;
12373
+ await rename4(finalRoot, backupRoot);
12374
+ } catch (error2) {
12375
+ if (error2.code !== "ENOENT")
12376
+ throw error2;
12401
12377
  }
12402
- await ensureGitignoreHasDist(dir);
12403
- } catch (e) {
12404
- if (e instanceof CreateProjectErrorImpl)
12405
- throw e;
12406
- throw new CreateProjectErrorImpl("scaffold-io", `Could not set up the folder: ${e instanceof Error ? e.message : String(e)}`);
12407
- }
12408
- const requested = options.versionHistory ?? "local-git";
12409
- let versionHistory = "none";
12410
- let versionHistoryError;
12411
- if (requested === "local-git") {
12412
12378
  try {
12413
- const { providerFor } = await import("./source-provider-jkppq9fc.js");
12414
- const provider = providerFor({ type: "local-folder", path: dir });
12415
- await provider.initVersionHistory({
12416
- projectDir: dir,
12417
- authorName: options.author?.trim() || undefined,
12418
- initialMessage: "Set up as a gutterpress book"
12419
- });
12420
- versionHistory = "local-git";
12421
- } catch (e) {
12422
- versionHistory = "none";
12423
- versionHistoryError = e instanceof Error ? e.message : String(e);
12379
+ await rename4(stageRoot, finalRoot);
12380
+ } catch (error2) {
12381
+ if (backupRoot) {
12382
+ try {
12383
+ await rename4(backupRoot, finalRoot);
12384
+ } catch (restoreError) {
12385
+ throw new Error(`Could not publish the new plugin tree or restore the previous tree: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}`, { cause: error2 });
12386
+ }
12387
+ }
12388
+ throw error2;
12424
12389
  }
12390
+ return {
12391
+ name: rootRecord.name,
12392
+ version: rootRecord.version,
12393
+ installRoot: finalRoot,
12394
+ backupRoot,
12395
+ warnings: [...ctx.warnings]
12396
+ };
12397
+ } finally {
12398
+ await rm3(container, { recursive: true, force: true }).catch(() => {});
12425
12399
  }
12426
- const result = {
12427
- projectDir: dir,
12428
- manifestPath: path18.join(dir, MANIFEST_FILENAMES[0]),
12429
- openFile: path18.join(dir, mdFiles[0]),
12430
- versionHistory
12431
- };
12432
- if (versionHistoryError !== undefined)
12433
- result.versionHistoryError = versionHistoryError;
12434
- return result;
12435
12400
  }
12436
- async function ensureGitignoreHasDist(projectDir) {
12437
- const gitignorePath = path18.join(projectDir, ".gitignore");
12438
- let existing = null;
12439
- try {
12440
- existing = await readFile29(gitignorePath, "utf8");
12441
- } catch {}
12442
- if (existing === null) {
12443
- await writeFile8(gitignorePath, `dist/
12444
- `, "utf8");
12445
- return;
12401
+ async function finalizeNpmPluginInstall(installed) {
12402
+ if (installed.backupRoot) {
12403
+ await rm3(installed.backupRoot, { recursive: true, force: true });
12446
12404
  }
12447
- const alreadyIgnoresDist = existing.split(`
12448
- `).some((line) => /^\/?dist\/?$/.test(line.trim()));
12449
- if (alreadyIgnoresDist)
12450
- return;
12451
- const needsLeadingNewline = existing.length > 0 && !existing.endsWith(`
12452
- `);
12453
- await writeFile8(gitignorePath, existing + (needsLeadingNewline ? `
12454
- ` : "") + `dist/
12455
- `, "utf8");
12456
12405
  }
12457
- async function firstSourceFile(manifestPath) {
12458
- let text;
12459
- try {
12460
- text = await readFile29(manifestPath, "utf8");
12461
- } catch {
12462
- return;
12406
+ async function rollbackNpmPluginInstall(installed) {
12407
+ await rm3(installed.installRoot, { recursive: true, force: true });
12408
+ if (installed.backupRoot)
12409
+ await rename4(installed.backupRoot, installed.installRoot);
12410
+ }
12411
+
12412
+ // src/lib/extension-manager.ts
12413
+ var EXTENSIONS_DIR = "extensions";
12414
+ var RECOMMENDED_EXTENSIONS = [
12415
+ {
12416
+ use: "markdown-it-mark",
12417
+ label: "Highlight",
12418
+ description: "Highlighted text with `==marked==` -> `<mark>`."
12419
+ },
12420
+ {
12421
+ use: "markdown-it-sub",
12422
+ label: "Subscript",
12423
+ description: "Subscript text with `H~2~O`."
12424
+ },
12425
+ {
12426
+ use: "markdown-it-sup",
12427
+ label: "Superscript",
12428
+ description: "Superscript text with `29^th^`."
12429
+ },
12430
+ {
12431
+ use: "markdown-it-abbr",
12432
+ label: "Abbreviations",
12433
+ description: "Define `*[HTML]: Hyper Text...` and get `<abbr>` tooltips."
12434
+ },
12435
+ {
12436
+ use: "gutterpress-gfm-alerts",
12437
+ label: "Callouts",
12438
+ description: "GitHub-style `> [!NOTE]` alert boxes (Note/Tip/Important/Warning/Caution)."
12463
12439
  }
12464
- const filesIdx = text.search(/^\s*files:\s*$/m);
12465
- if (filesIdx === -1)
12440
+ ];
12441
+ var BUILT_IN_STYLE_SET_IDS = ["clean-book", "zine", "technical-doc"];
12442
+ var mutationQueues = new Map;
12443
+ function withMutationLock(projectDir, mutation) {
12444
+ const resolved = path20.resolve(projectDir);
12445
+ const key = process.platform === "win32" ? resolved.toLowerCase() : resolved;
12446
+ const previous = mutationQueues.get(key) ?? Promise.resolve();
12447
+ const run2 = previous.then(mutation, mutation);
12448
+ const tail = run2.then(() => {
12466
12449
  return;
12467
- const rest = text.slice(filesIdx);
12468
- const m = rest.match(/^\s*-\s*"?([^"\n]+?)"?\s*$/m);
12469
- return m ? m[1].trim() : undefined;
12450
+ }, () => {
12451
+ return;
12452
+ });
12453
+ mutationQueues.set(key, tail);
12454
+ tail.then(() => {
12455
+ if (mutationQueues.get(key) === tail)
12456
+ mutationQueues.delete(key);
12457
+ });
12458
+ return run2;
12470
12459
  }
12471
- async function fillTemplateFile(filePath, substitutions) {
12472
- let text = await readFile29(filePath, "utf8");
12473
- for (const [token, value] of Object.entries(substitutions)) {
12474
- text = text.split(token).join(value);
12460
+ function entryUse(item) {
12461
+ if (isScalar2(item))
12462
+ return typeof item.value === "string" ? item.value.trim() : null;
12463
+ if (isMap2(item)) {
12464
+ const u = item.get("use");
12465
+ return typeof u === "string" ? u.trim() : null;
12475
12466
  }
12476
- await writeFile8(filePath, text, "utf8");
12467
+ return null;
12477
12468
  }
12478
- // src/lib/extension-scaffold.ts
12479
- import { access as access2, mkdir as mkdir10, readFile as readFile30, writeFile as writeFile9 } from "node:fs/promises";
12480
- import { constants as FS2 } from "node:fs";
12481
- import path19 from "node:path";
12482
- var EXTENSION_KINDS = ["plugin", "theme"];
12483
- var RESERVED_PREFIX = "gp-";
12484
- var TEMPLATE_FILES = {
12485
- plugin: [
12486
- ["package.json", "package.json"],
12487
- ["README.md", "README.md"],
12488
- ["plugin.js.tpl", "plugin.js"],
12489
- ["styles/plugin.css", "styles/plugin.css"],
12490
- ["snippets/term-box.md", "snippets/term-box.md"],
12491
- ["test/fixture.md", "test/fixture.md"],
12492
- ["test/expected.html", "test/expected.html"],
12493
- ["test/plugin.test.js.tpl", "test/plugin.test.js"]
12494
- ],
12495
- theme: [
12496
- ["package.json", "package.json"],
12497
- ["README.md", "README.md"],
12498
- ["components.yaml", "components.yaml"],
12499
- ["snippets/callout.md", "snippets/callout.md"],
12500
- ["styles/tokens.css", "styles/tokens.css"],
12501
- ["styles/base.css", "styles/base.css"],
12502
- ["styles/components.css", "styles/components.css"],
12503
- ["styles/page-templates.css", "styles/page-templates.css"],
12504
- ["styles/page-rules.css", "styles/page-rules.css"],
12505
- ["styles/book.css", "styles/book.css"]
12506
- ]
12507
- };
12508
- var OPEN_FILE = {
12509
- plugin: "plugin.js",
12510
- theme: "styles/tokens.css"
12511
- };
12512
- var DEFAULT_DESCRIPTION = {
12513
- plugin: "A Gutterpress plugin.",
12514
- theme: "A Gutterpress theme."
12515
- };
12516
- var DEFAULT_AUTHOR2 = "Anonymous";
12517
-
12518
- class ExtensionScaffoldError extends Error {
12519
- code;
12520
- constructor(code, message) {
12521
- super(message);
12522
- this.name = "CreateProjectError";
12523
- this.code = code;
12524
- }
12469
+ function rawEntry(item) {
12470
+ const use = entryUse(item);
12471
+ if (!use)
12472
+ return null;
12473
+ if (!isMap2(item))
12474
+ return { use, enabled: true };
12475
+ const x = item.get("export");
12476
+ return {
12477
+ use,
12478
+ ...typeof x === "string" && x.trim() ? { export: x.trim() } : {},
12479
+ enabled: item.get("enabled") !== false
12480
+ };
12525
12481
  }
12526
- function resolveExtensionPrefix(slug, requested) {
12527
- const raw = (requested ?? `${slug}-`).trim();
12528
- const prefix = raw.endsWith("-") ? raw : `${raw}-`;
12529
- if (!/^[a-z][a-z0-9-]*-$/.test(prefix)) {
12530
- throw new ExtensionScaffoldError("invalid-name", `"${raw}" is not a usable prefix. Use lower-case letters, digits and hyphens, ` + `starting with a letter — for example "${slug}-" or "fn-".`);
12531
- }
12532
- if (prefix === RESERVED_PREFIX) {
12533
- throw new ExtensionScaffoldError("invalid-name", `"${RESERVED_PREFIX}" is reserved for Gutterpress core's own classes and custom ` + `properties. Taking it would silently override them — choose your own prefix.`);
12482
+ function sameExtension(a, b) {
12483
+ if (a.trim() === b.trim())
12484
+ return true;
12485
+ let pa;
12486
+ let pb;
12487
+ try {
12488
+ pa = parseExtensionSpecifier(a);
12489
+ pb = parseExtensionSpecifier(b);
12490
+ } catch {
12491
+ return false;
12534
12492
  }
12535
- return prefix;
12536
- }
12537
- function identifierFor(slug) {
12538
- const camel = slug.replace(/-([a-z0-9])/g, (_m, c) => c.toUpperCase());
12539
- return /^[0-9]/.test(camel) ? `_${camel}` : camel;
12493
+ return pa.kind === "npm" && pb.kind === "npm" && pa.name === pb.name;
12540
12494
  }
12541
- function escapeJsonScalar(value) {
12542
- return JSON.stringify(value).slice(1, -1);
12495
+ function indexOfUse(seq, use) {
12496
+ const items = seq.items;
12497
+ const exact = items.findIndex((item) => entryUse(item) === use.trim());
12498
+ if (exact >= 0)
12499
+ return exact;
12500
+ return items.findIndex((item) => {
12501
+ const u = entryUse(item);
12502
+ return !!u && sameExtension(u, use);
12503
+ });
12543
12504
  }
12544
- async function scaffoldExtension(options) {
12545
- const kind = options.kind;
12546
- if (kind !== "plugin" && kind !== "theme") {
12547
- throw new ExtensionScaffoldError("invalid-name", `Unknown extension kind "${String(kind)}". Choose one of: ${EXTENSION_KINDS.join(", ")}.`);
12548
- }
12549
- const name = (options.name ?? "").trim();
12550
- if (!name) {
12551
- throw new ExtensionScaffoldError("invalid-name", "A name is required.");
12552
- }
12553
- const slug = (options.folderName ?? slugify(name)).trim();
12554
- if (!slug || slug !== slugify(slug)) {
12555
- throw new ExtensionScaffoldError("invalid-name", `Could not derive a valid folder name from "${name}".`);
12556
- }
12557
- const prefix = resolveExtensionPrefix(slug, options.prefix);
12558
- const parentDir = options.parentDir;
12559
- if (!parentDir || !path19.isAbsolute(parentDir)) {
12560
- throw new ExtensionScaffoldError("parent-not-writable", "The save location must be an absolute path.");
12505
+ function manifestPathFor(projectDir, abs) {
12506
+ const rel = path20.relative(path20.resolve(projectDir), abs).split(path20.sep).join("/");
12507
+ if (rel === "") {
12508
+ throw new Error("An extension cannot be the project folder itself.");
12561
12509
  }
12510
+ if (isPathSpecifier(rel))
12511
+ return rel;
12512
+ return `./${rel}`;
12513
+ }
12514
+ var NO_CARRIES = {
12515
+ markdown: false,
12516
+ styles: false,
12517
+ snippets: false,
12518
+ components: false
12519
+ };
12520
+ async function describeFolder(dir, base, fallbackLabel, labelFrom = prettify) {
12521
+ const meta = await readExtensionMeta(dir);
12522
+ const styles = extensionStyleList(meta);
12523
+ return {
12524
+ ...base,
12525
+ label: meta.name?.trim() ? labelFrom(meta.name.trim()) : fallbackLabel,
12526
+ ...meta.description?.trim() ? { description: meta.description.trim() } : {},
12527
+ ...meta.author?.trim() ? { author: meta.author.trim() } : {},
12528
+ ...meta.preview !== undefined ? { preview: meta.preview } : {},
12529
+ ...meta.tokensFile?.trim() ? { tokensFile: meta.tokensFile.trim() } : {},
12530
+ ...styles.length > 0 ? { styles } : {},
12531
+ carries: extensionCarries(meta),
12532
+ dir
12533
+ };
12534
+ }
12535
+ async function describeEntry(projectDir, raw) {
12536
+ const warnings = [];
12537
+ let parsed = null;
12562
12538
  try {
12563
- await access2(parentDir, FS2.W_OK);
12564
- } catch {
12565
- throw new ExtensionScaffoldError("parent-not-writable", `The chosen save location can't be written to: ${parentDir}`);
12539
+ parsed = parseExtensionSpecifier(raw.use);
12540
+ } catch (error2) {
12541
+ warnings.push(error2 instanceof Error ? error2.message : String(error2));
12566
12542
  }
12567
- const extensionDir = path19.join(parentDir, slug);
12568
- let targetExists = true;
12569
- try {
12570
- await access2(extensionDir, FS2.F_OK);
12571
- } catch {
12572
- targetExists = false;
12543
+ const common = {
12544
+ use: raw.use,
12545
+ enabled: raw.enabled,
12546
+ ...raw.export ? { export: raw.export } : {}
12547
+ };
12548
+ const withWarnings = (entry) => warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
12549
+ if (!parsed) {
12550
+ return withWarnings({ ...common, kind: "npm", name: raw.use, label: raw.use, carries: NO_CARRIES });
12573
12551
  }
12574
- if (targetExists) {
12575
- throw new ExtensionScaffoldError("target-exists", `A folder named "${slug}" already exists here. Choose a different name or location.`);
12552
+ if (parsed.kind === "bundled") {
12553
+ const bundledName = parsed.name;
12554
+ const rec = RECOMMENDED_EXTENSIONS.find((r) => r.use === bundledName);
12555
+ return withWarnings({
12556
+ ...common,
12557
+ kind: "bundled",
12558
+ name: bundledName,
12559
+ label: rec?.label ?? bundledName,
12560
+ ...rec ? { description: rec.description } : {},
12561
+ carries: { ...NO_CARRIES, markdown: true }
12562
+ });
12576
12563
  }
12577
- const author = (options.author ?? "").trim() || DEFAULT_AUTHOR2;
12578
- const description = (options.description ?? "").trim() || DEFAULT_DESCRIPTION[kind];
12579
- const plain = {
12580
- "{{NAME}}": name,
12581
- "{{SLUG}}": slug,
12582
- "{{PREFIX}}": prefix,
12583
- "{{AUTHOR}}": author,
12584
- "{{DESCRIPTION}}": description,
12585
- "{{IDENT}}": identifierFor(slug)
12586
- };
12587
- const jsonSafe = Object.fromEntries(Object.entries(plain).map(([token, value]) => [token, escapeJsonScalar(value)]));
12588
- const entries = TEMPLATE_FILES[kind];
12589
- const written = [];
12590
- try {
12591
- await mkdir10(extensionDir, { recursive: true });
12592
- for (const [assetName, relDest] of entries) {
12593
- const src = await getAssetPath(`extension-templates/${kind}/${assetName}`);
12594
- const dest = path19.join(extensionDir, relDest);
12595
- await mkdir10(path19.dirname(dest), { recursive: true });
12596
- const text = await readFile30(src, "utf8");
12597
- const table = relDest.endsWith(".json") ? jsonSafe : plain;
12598
- await writeFile9(dest, substitute(text, table), "utf8");
12599
- written.push(relDest);
12564
+ if (parsed.kind === "path") {
12565
+ const abs = path20.resolve(projectDir, parsed.path);
12566
+ const base2 = { ...common, kind: "path", name: parsed.path };
12567
+ let info2;
12568
+ try {
12569
+ info2 = await stat7(abs);
12570
+ } catch {
12571
+ warnings.push(`Not found: ${abs}`);
12572
+ return withWarnings({ ...base2, label: path20.basename(abs), carries: NO_CARRIES });
12600
12573
  }
12601
- } catch (e) {
12602
- throw new ExtensionScaffoldError("scaffold-io", `Could not create the ${kind} files: ${e instanceof Error ? e.message : String(e)}`);
12574
+ if (info2.isDirectory()) {
12575
+ return withWarnings(await describeFolder(abs, base2, prettify(path20.basename(abs))));
12576
+ }
12577
+ return withWarnings({
12578
+ ...base2,
12579
+ label: path20.basename(abs),
12580
+ carries: { ...NO_CARRIES, markdown: true }
12581
+ });
12603
12582
  }
12604
- return {
12605
- extensionDir,
12606
- manifestPath: path19.join(extensionDir, "package.json"),
12607
- kind,
12608
- slug,
12609
- prefix,
12610
- openFile: path19.join(extensionDir, OPEN_FILE[kind]),
12611
- files: written
12583
+ const base = {
12584
+ ...common,
12585
+ kind: "npm",
12586
+ name: parsed.name,
12587
+ ...parsed.version ? { version: parsed.version } : {}
12612
12588
  };
12613
- }
12614
- function substitute(text, table) {
12615
- let out = text;
12616
- for (const [token, value] of Object.entries(table)) {
12617
- out = out.split(token).join(value);
12589
+ if (!parsed.version) {
12590
+ warnings.push(`Not pinned — run \`gutterpress ext add ${parsed.name}\` to install it and pin an exact version.`);
12591
+ return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
12618
12592
  }
12619
- return out;
12620
- }
12621
- // src/lib/project-templates.ts
12622
- import { cp as cp4, mkdir as mkdir11, readFile as readFile31, readdir as readdir7, stat as stat7, writeFile as writeFile10 } from "node:fs/promises";
12623
- import { constants as FS3, existsSync as existsSync14 } from "node:fs";
12624
- import { access as access3 } from "node:fs/promises";
12625
- import path20 from "node:path";
12626
- import { isMap as isMap2, isScalar as isScalar2, isSeq as isSeq3, parseDocument as parseDocument2 } from "yaml";
12627
- var BUILT_IN_TEMPLATE_IDS = [
12628
- "book",
12629
- "zine",
12630
- "technical"
12631
- ];
12632
- async function readTemplateChoices(manifestPath) {
12593
+ let installRoot = null;
12633
12594
  try {
12634
- const text = await readFile31(manifestPath, "utf8");
12635
- const doc = parseDocument2(text);
12636
- const out = {};
12637
- const preset = doc.get("preset");
12638
- if (typeof preset === "string" && preset)
12639
- out.preset = preset;
12640
- const targets = doc.get("targets", true);
12641
- if (isSeq3(targets)) {
12642
- out.targets = targets.items.map((i) => scalarString(i)).filter((s) => s !== null);
12643
- }
12644
- return out;
12645
- } catch {
12646
- return {};
12595
+ installRoot = await resolveVendoredPluginInstallRoot(projectDir, parsed.name, parsed.version);
12596
+ } catch (error2) {
12597
+ warnings.push(error2 instanceof Error ? error2.message : String(error2));
12647
12598
  }
12648
- }
12649
- var BUILT_IN_META = {
12650
- book: {
12651
- label: "Book",
12652
- description: "A clean starting point for a novel, memoir, or any long-form book."
12653
- },
12654
- zine: {
12655
- label: "Zine",
12656
- description: "A short, personal zine made to be printed, folded, and shared."
12657
- },
12658
- technical: {
12659
- label: "Technical document",
12660
- description: "A manual or guide with steps, reference tables, and code examples."
12599
+ if (!installRoot) {
12600
+ warnings.push(`Not installed — run \`gutterpress ext add ${raw.use}\`.`);
12601
+ return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
12661
12602
  }
12662
- };
12663
- async function listBuiltInTemplates() {
12664
- return Promise.all(BUILT_IN_TEMPLATE_IDS.map(async (id) => ({
12665
- id,
12666
- label: BUILT_IN_META[id].label,
12667
- description: BUILT_IN_META[id].description,
12668
- kind: "builtin",
12669
- ...await readTemplateChoices(await getAssetPath(`templates/${id}/manifest.yaml`))
12670
- })));
12603
+ const pkgDir = vendoredNpmPluginPackageDir(installRoot, parsed.name);
12604
+ if (!existsSync14(path20.join(pkgDir, EXTENSION_MANIFEST_FILENAME))) {
12605
+ return withWarnings({ ...base, label: parsed.name, carries: { ...NO_CARRIES, markdown: true } });
12606
+ }
12607
+ const described = await describeFolder(pkgDir, base, parsed.name, (name) => name);
12608
+ return withWarnings({
12609
+ ...described,
12610
+ carries: { ...described.carries, markdown: true }
12611
+ });
12671
12612
  }
12672
- var SKIP_ENTRIES = new Set([
12673
- ".git",
12674
- "node_modules",
12675
- "out",
12676
- "dist",
12677
- "build",
12678
- ".gutterpress",
12679
- ".DS_Store"
12680
- ]);
12681
- var VENDOR_DIR = "shared";
12682
- function collectEscaping(nodes, getValue, escapes) {
12613
+ async function listProjectExtensions(projectDir) {
12614
+ const { doc } = await loadManifestDoc(projectDir);
12615
+ const seq = doc.get("extensions", true);
12616
+ if (!isSeq3(seq))
12617
+ return [];
12683
12618
  const out = [];
12684
- for (const node of nodes) {
12685
- const value = getValue(node);
12686
- if (value !== null && escapes(value))
12687
- out.push({ node, value });
12619
+ for (const item of seq.items) {
12620
+ const raw = rawEntry(item);
12621
+ if (!raw)
12622
+ continue;
12623
+ out.push(await describeEntry(projectDir, raw));
12688
12624
  }
12689
12625
  return out;
12690
12626
  }
12691
- function extensionEntryPath(node) {
12692
- const use = isMap2(node) ? scalarString(node.get("use", true)) : scalarString(node);
12693
- return use !== null && isPathSpecifier(use) ? use : null;
12694
- }
12695
- function dropEscaping(doc, seq, key, escaping) {
12696
- if (!isSeq3(seq))
12697
- return;
12698
- const drop = new Set(escaping.map((e) => e.node));
12699
- seq.items = seq.items.filter((node) => !drop.has(node));
12700
- if (seq.items.length === 0)
12701
- doc.delete(key);
12627
+ async function describeExtension(projectDir, use) {
12628
+ return (await listProjectExtensions(projectDir)).find((e) => sameExtension(e.use, use)) ?? null;
12702
12629
  }
12703
- function rewriteEntry(node, newPath) {
12704
- if (isMap2(node))
12705
- node.set("use", newPath);
12706
- else if (isScalar2(node))
12707
- node.value = newPath;
12630
+ function extensionConfigFor(entry) {
12631
+ return {
12632
+ use: entry.use,
12633
+ ...entry.kind === "path" ? { path: entry.name } : { name: entry.name, ...entry.version ? { version: entry.version } : {} },
12634
+ ...entry.export ? { export: entry.export } : {},
12635
+ options: {}
12636
+ };
12708
12637
  }
12709
- function commonAncestorDir(absFiles) {
12710
- const dirSegs = absFiles.map((f) => path20.resolve(f).split(path20.sep).slice(0, -1));
12711
- if (dirSegs.length === 0)
12712
- return "";
12713
- let common = dirSegs[0];
12714
- for (const segs of dirSegs.slice(1)) {
12715
- let i = 0;
12716
- while (i < common.length && i < segs.length && common[i] === segs[i])
12717
- i++;
12718
- common = common.slice(0, i);
12719
- }
12720
- return common.join(path20.sep) || path20.sep;
12638
+ async function setExtensionEnabled(projectDir, use, enabled) {
12639
+ return withMutationLock(projectDir, async () => {
12640
+ const { doc, file } = await loadManifestDoc(projectDir);
12641
+ const seq = doc.get("extensions", true);
12642
+ const idx = isSeq3(seq) ? indexOfUse(seq, use) : -1;
12643
+ if (!isSeq3(seq) || idx < 0)
12644
+ throw new Error(`Extension "${use}" is not in the manifest.`);
12645
+ const item = seq.items[idx];
12646
+ if (enabled) {
12647
+ if (isMap2(item)) {
12648
+ item.delete("enabled");
12649
+ if (item.items.length === 1 && typeof item.get("use") === "string") {
12650
+ seq.items[idx] = new Scalar(item.get("use"));
12651
+ }
12652
+ }
12653
+ } else if (isMap2(item)) {
12654
+ item.set("enabled", false);
12655
+ } else {
12656
+ const map = new YAMLMap(doc.schema);
12657
+ map.set("use", entryUse(item));
12658
+ map.set("enabled", false);
12659
+ seq.items[idx] = map;
12660
+ }
12661
+ await writeManifestDoc(file, doc);
12662
+ });
12721
12663
  }
12722
- function pickVendorDir(templateDir, base) {
12723
- const preferred = path20.basename(base) || VENDOR_DIR;
12724
- if (!existsSync14(path20.join(templateDir, preferred)))
12725
- return preferred;
12726
- for (let n = 1;; n++) {
12727
- const candidate = `${preferred}-${n}`;
12728
- if (!existsSync14(path20.join(templateDir, candidate)))
12729
- return candidate;
12730
- }
12664
+ async function reorderExtensions(projectDir, order) {
12665
+ return withMutationLock(projectDir, async () => {
12666
+ const { doc, file } = await loadManifestDoc(projectDir);
12667
+ const seq = doc.get("extensions", true);
12668
+ if (!isSeq3(seq))
12669
+ throw new Error(`No extensions are configured in ${path20.basename(file)}.`);
12670
+ const items = seq.items;
12671
+ const current = items.map((item) => entryUse(item));
12672
+ const wanted = order.map((u) => u.trim());
12673
+ const sameSet = current.length === wanted.length && [...current].sort().join(`
12674
+ `) === [...wanted].sort().join(`
12675
+ `);
12676
+ if (!sameSet) {
12677
+ throw new Error("The new order must list every configured extension exactly once — reload the list and try again.");
12678
+ }
12679
+ const remaining = [...items];
12680
+ seq.items = wanted.map((u) => {
12681
+ const i = remaining.findIndex((item) => entryUse(item) === u);
12682
+ return remaining.splice(i, 1)[0];
12683
+ });
12684
+ await writeManifestDoc(file, doc);
12685
+ });
12731
12686
  }
12732
- var TEMPLATE_META_FILE = ".gutterpress-template.json";
12733
- async function reconcileSharedRefs(templateDir, sourceProjectDir, mode) {
12734
- const { doc, file } = await loadManifestDoc(templateDir);
12735
- const escapes = (p) => escapesProjectRoot(sourceProjectDir, path20.resolve(sourceProjectDir, p));
12736
- const stylesSeq = doc.get("styles", true);
12737
- const extensionsSeq = doc.get("extensions", true);
12738
- const escapingStyles = collectEscaping(isSeq3(stylesSeq) ? stylesSeq.items : [], scalarString, escapes);
12739
- const escapingExtensions = collectEscaping(isSeq3(extensionsSeq) ? extensionsSeq.items : [], extensionEntryPath, escapes);
12740
- if (escapingStyles.length === 0 && escapingExtensions.length === 0) {
12741
- return { vendoredRefs: [], excludedRefs: [] };
12742
- }
12743
- if (mode === "exclude") {
12744
- const excludedRefs = [...escapingStyles, ...escapingExtensions].map((e) => e.value);
12745
- dropEscaping(doc, stylesSeq, "styles", escapingStyles);
12746
- dropEscaping(doc, extensionsSeq, "extensions", escapingExtensions);
12687
+ async function removeExtension(projectDir, use) {
12688
+ return withMutationLock(projectDir, async () => {
12689
+ const { doc, file } = await loadManifestDoc(projectDir);
12690
+ const seq = doc.get("extensions", true);
12691
+ const idx = isSeq3(seq) ? indexOfUse(seq, use) : -1;
12692
+ if (!isSeq3(seq) || idx < 0)
12693
+ throw new Error(`Extension "${use}" is not in the manifest.`);
12694
+ const written = entryUse(seq.items[idx]);
12695
+ seq.items.splice(idx, 1);
12696
+ if (seq.items.length === 0)
12697
+ doc.delete("extensions");
12747
12698
  await writeManifestDoc(file, doc);
12748
- return { vendoredRefs: [], excludedRefs };
12749
- }
12750
- const styleClosure = await collectStyleDependencies(sourceProjectDir, escapingStyles.map((s) => s.value));
12751
- const extensionFiles = escapingExtensions.map((p) => path20.resolve(sourceProjectDir, p.value));
12752
- const externalFiles = [...new Set([...styleClosure, ...extensionFiles])].filter((f) => escapesProjectRoot(sourceProjectDir, f));
12753
- const base = commonAncestorDir(externalFiles);
12754
- const vendorDir = pickVendorDir(templateDir, base);
12755
- await Promise.all(externalFiles.filter((abs) => existsSync14(abs)).map(async (abs) => {
12756
- const dest = path20.join(templateDir, vendorDir, path20.relative(base, abs));
12757
- await mkdir11(path20.dirname(dest), { recursive: true });
12758
- await cp4(abs, dest, { recursive: true });
12759
- }));
12760
- const bookLocal = (value) => toPosixPath(path20.join(vendorDir, path20.relative(base, path20.resolve(sourceProjectDir, value))));
12761
- const vendoredRefs = [];
12762
- for (const { node, value } of escapingStyles) {
12763
- const rewritten = bookLocal(value);
12764
- rewriteEntry(node, rewritten);
12765
- vendoredRefs.push(rewritten);
12766
- }
12767
- for (const { node, value } of escapingExtensions) {
12768
- const rewritten = bookLocal(value);
12769
- rewriteEntry(node, `./${rewritten}`);
12770
- vendoredRefs.push(rewritten);
12699
+ let parsed = null;
12700
+ try {
12701
+ parsed = parseExtensionSpecifier(written);
12702
+ } catch {
12703
+ return;
12704
+ }
12705
+ if (parsed.kind === "npm" && parsed.version) {
12706
+ await rm4(vendoredNpmPluginRoot(projectDir, parsed.name, parsed.version), {
12707
+ recursive: true,
12708
+ force: true
12709
+ });
12710
+ clearVendoredPluginResolver(projectDir, parsed.name, parsed.version);
12711
+ }
12712
+ });
12713
+ }
12714
+ async function upsertEntry(projectDir, use, exportName) {
12715
+ const { doc, file } = await loadManifestDoc(projectDir);
12716
+ const seq = ensureSeq(doc, "extensions");
12717
+ const idx = indexOfUse(seq, use);
12718
+ if (idx >= 0) {
12719
+ const item = seq.items[idx];
12720
+ if (isMap2(item)) {
12721
+ item.set("use", use);
12722
+ if (exportName)
12723
+ item.set("export", exportName);
12724
+ else
12725
+ item.delete("export");
12726
+ if (item.items.length === 1)
12727
+ seq.items[idx] = new Scalar(use);
12728
+ } else if (exportName) {
12729
+ const map = new YAMLMap(doc.schema);
12730
+ map.set("use", use);
12731
+ map.set("export", exportName);
12732
+ seq.items[idx] = map;
12733
+ } else {
12734
+ seq.items[idx] = new Scalar(use);
12735
+ }
12736
+ } else if (exportName) {
12737
+ const map = new YAMLMap(doc.schema);
12738
+ map.set("use", use);
12739
+ map.set("export", exportName);
12740
+ seq.add(map);
12741
+ } else {
12742
+ seq.add(new Scalar(use));
12771
12743
  }
12772
12744
  await writeManifestDoc(file, doc);
12773
- return { vendoredRefs, excludedRefs: [] };
12774
12745
  }
12775
- async function saveProjectAsTemplate(options) {
12776
- const { projectDir, name, templatesRoot } = options;
12777
- const id = slugify(name);
12778
- if (!id)
12779
- throw new Error(`Could not derive a template id from "${name}".`);
12780
- const dir = path20.join(templatesRoot, id);
12781
- let exists = true;
12746
+ async function addPathExtension(projectDir, abs, exportName) {
12782
12747
  try {
12783
- await access3(dir, FS3.F_OK);
12748
+ await stat7(abs);
12784
12749
  } catch {
12785
- exists = false;
12750
+ throw new Error(`Extension not found: ${abs}`);
12786
12751
  }
12787
- if (exists) {
12788
- throw new Error(`A template named "${name}" already exists. Choose a different name.`);
12752
+ const use = manifestPathFor(projectDir, abs);
12753
+ await loadPlugin(extensionConfigFor({ use, kind: "path", name: use, export: exportName }), projectDir);
12754
+ await upsertEntry(projectDir, use, exportName);
12755
+ return await describeExtension(projectDir, use);
12756
+ }
12757
+ async function addExtension(projectDir, specifier, options = {}) {
12758
+ return withMutationLock(projectDir, () => addExtensionUnlocked(projectDir, specifier, options));
12759
+ }
12760
+ async function addExtensionUnlocked(projectDir, specifier, options) {
12761
+ const input = specifier.trim();
12762
+ const exportName = options.exportName?.trim() || undefined;
12763
+ if (path20.isAbsolute(input))
12764
+ return addPathExtension(projectDir, input, exportName);
12765
+ const parsed = parseExtensionSpecifier(input);
12766
+ if (parsed.kind === "path") {
12767
+ return addPathExtension(projectDir, path20.resolve(projectDir, parsed.path), exportName);
12789
12768
  }
12790
- await mkdir11(dir, { recursive: true });
12791
- const entries = await readdir7(projectDir, { withFileTypes: true });
12792
- for (const entry of entries) {
12793
- if (SKIP_ENTRIES.has(entry.name))
12794
- continue;
12795
- if (entry.name === TEMPLATE_META_FILE)
12796
- continue;
12797
- await cp4(path20.join(projectDir, entry.name), path20.join(dir, entry.name), { recursive: true });
12769
+ if (parsed.kind === "bundled") {
12770
+ if (exportName)
12771
+ throw new Error(`Bundled extension "${parsed.name}" does not take a named export.`);
12772
+ await upsertEntry(projectDir, parsed.name);
12773
+ return await describeExtension(projectDir, parsed.name);
12798
12774
  }
12799
- const manifestName = MANIFEST_FILENAMES.find((name2) => entries.some((entry) => entry.name === name2));
12800
- const shared = manifestName ? await reconcileSharedRefs(dir, projectDir, options.sharedRefs ?? "vendor") : { vendoredRefs: [], excludedRefs: [] };
12801
- if (manifestName)
12802
- await retokeniseManifest(path20.join(dir, manifestName));
12803
- await writeFile10(path20.join(dir, TEMPLATE_META_FILE), JSON.stringify({ label: name }, null, 2), "utf8");
12804
- return {
12805
- id,
12806
- label: name,
12807
- description: "Your saved template.",
12808
- kind: "custom",
12809
- dir,
12810
- ...shared
12811
- };
12812
- }
12813
- async function retokeniseManifest(manifestPath) {
12814
- let text;
12775
+ const existing = await describeExtension(projectDir, parsed.name);
12776
+ const chosenExport = exportName ?? existing?.export;
12777
+ const installed = await installNpmPlugin(projectDir, input, options);
12778
+ const use = pinnedNpmSpecifier(installed.name, installed.version);
12815
12779
  try {
12816
- text = await readFile31(manifestPath, "utf8");
12817
- } catch {
12818
- return;
12780
+ await loadPlugin(extensionConfigFor({
12781
+ use,
12782
+ kind: "npm",
12783
+ name: installed.name,
12784
+ version: installed.version,
12785
+ export: chosenExport
12786
+ }), projectDir);
12787
+ await options.__testFailBeforeManifestCommit?.();
12788
+ await upsertEntry(projectDir, use, chosenExport);
12789
+ } catch (cause) {
12790
+ try {
12791
+ await rollbackNpmPluginInstall(installed);
12792
+ clearVendoredPluginResolver(projectDir, installed.name, installed.version);
12793
+ } catch (rollbackError) {
12794
+ throw new Error(`Extension install failed and its previous vendor tree could not be restored: ` + `${rollbackError instanceof Error ? rollbackError.message : String(rollbackError)}`, { cause });
12795
+ }
12796
+ throw new Error(`Downloaded ${use}, but it is not a loadable markdown-it plugin: ` + `${cause instanceof Error ? cause.message : String(cause)}`, { cause });
12819
12797
  }
12820
- text = text.replace(/^title:\s*.*$/m, 'title: "{{TITLE}}"').replace(/^(authors:[^\n]*\n[ \t]*-[ \t]*).*$/m, '$1"{{AUTHOR}}"');
12821
- await writeFile10(manifestPath, text, "utf8");
12798
+ const warnings = [...installed.warnings];
12799
+ await finalizeNpmPluginInstall(installed).catch((error2) => {
12800
+ warnings.push(`The extension was installed, but an old backup could not be removed: ` + `${error2 instanceof Error ? error2.message : String(error2)}`);
12801
+ });
12802
+ const entry = await describeExtension(projectDir, use);
12803
+ return warnings.length > 0 ? { ...entry, warnings: [...entry.warnings ?? [], ...warnings] } : entry;
12822
12804
  }
12823
- async function listCustomTemplates(templatesRoot) {
12824
- let entries;
12825
- try {
12826
- entries = await readdir7(templatesRoot, { withFileTypes: true });
12827
- } catch {
12828
- return [];
12829
- }
12805
+ async function listBuiltInStyleSets() {
12806
+ const assets = await getAssetsDir();
12830
12807
  const out = [];
12831
- for (const entry of entries) {
12832
- if (!entry.isDirectory())
12833
- continue;
12834
- const dir = path20.join(templatesRoot, entry.name);
12835
- let label = prettify(entry.name);
12836
- try {
12837
- const meta = JSON.parse(await readFile31(path20.join(dir, TEMPLATE_META_FILE), "utf8"));
12838
- if (typeof meta.label === "string" && meta.label)
12839
- label = meta.label;
12840
- } catch {}
12808
+ for (const id of BUILT_IN_STYLE_SET_IDS) {
12809
+ const meta = await readExtensionMeta(path20.join(assets, "themes", id));
12841
12810
  out.push({
12842
- id: entry.name,
12843
- label,
12844
- description: "Your saved template.",
12845
- kind: "custom",
12846
- dir,
12847
- ...await readTemplateChoices(path20.join(dir, MANIFEST_FILENAMES[0]))
12811
+ id,
12812
+ name: prettify(meta.name?.trim() || id),
12813
+ description: meta.description?.trim() || ""
12848
12814
  });
12849
12815
  }
12850
- out.sort((a, b) => a.label.localeCompare(b.label));
12851
12816
  return out;
12852
12817
  }
12853
- async function importTemplateFromFolder(options) {
12854
- const { sourceDir, templatesRoot } = options;
12855
- const srcStat = await stat7(sourceDir);
12856
- if (!srcStat.isDirectory()) {
12857
- throw new Error("The chosen path is not a folder.");
12818
+ async function uniqueExtensionId(projectDir, base) {
12819
+ const slug = slugify(base) || "extension";
12820
+ let candidate = slug;
12821
+ for (let n = 2;existsSync14(path20.join(projectDir, EXTENSIONS_DIR, candidate)); n++) {
12822
+ candidate = `${slug}-${n}`;
12858
12823
  }
12859
- const name = options.name?.trim() || prettify(path20.basename(sourceDir));
12860
- return saveProjectAsTemplate({ projectDir: sourceDir, name, templatesRoot });
12824
+ return candidate;
12825
+ }
12826
+ async function addBuiltInStyleSet(projectDir, id) {
12827
+ if (!BUILT_IN_STYLE_SET_IDS.includes(id)) {
12828
+ throw new Error(`Unknown built-in look "${id}" (one of: ${BUILT_IN_STYLE_SET_IDS.join(", ")}).`);
12829
+ }
12830
+ return withMutationLock(projectDir, async () => {
12831
+ const dest = path20.join(projectDir, EXTENSIONS_DIR, id);
12832
+ if (!existsSync14(dest)) {
12833
+ const src = path20.join(await getAssetsDir(), "themes", id);
12834
+ await mkdir11(path20.dirname(dest), { recursive: true });
12835
+ await cp4(src, dest, { recursive: true });
12836
+ }
12837
+ return addExtensionUnlocked(projectDir, `./${EXTENSIONS_DIR}/${id}`, {});
12838
+ });
12839
+ }
12840
+ async function validateProjectExtensions(projectDir) {
12841
+ const out = [];
12842
+ for (const entry of await listProjectExtensions(projectDir)) {
12843
+ if (!entry.enabled) {
12844
+ out.push({ use: entry.use, kind: entry.kind, enabled: false, ok: true });
12845
+ continue;
12846
+ }
12847
+ try {
12848
+ await loadPlugin(extensionConfigFor(entry), projectDir);
12849
+ out.push({ use: entry.use, kind: entry.kind, enabled: true, ok: true });
12850
+ } catch (e) {
12851
+ out.push({
12852
+ use: entry.use,
12853
+ kind: entry.kind,
12854
+ enabled: true,
12855
+ ok: false,
12856
+ error: e instanceof Error ? e.message : String(e)
12857
+ });
12858
+ }
12859
+ }
12860
+ return out;
12861
+ }
12862
+ async function readExtensionCss(projectDir, use) {
12863
+ const entry = await describeExtension(projectDir, use);
12864
+ if (!entry)
12865
+ throw new Error(`Extension "${use}" is not in the manifest.`);
12866
+ if (!entry.dir)
12867
+ throw new Error(`Extension "${use}" has no stylesheets to preview.`);
12868
+ const meta = await readExtensionMeta(entry.dir);
12869
+ const sheets = resolveExtension(entry.dir, meta, `Extension "${use}"`).styles ?? [];
12870
+ if (sheets.length === 0)
12871
+ throw new Error(`Extension "${use}" declares no stylesheets.`);
12872
+ const parts = [];
12873
+ for (const sheet of sheets)
12874
+ parts.push(await readFile31(sheet, "utf8"));
12875
+ return parts.join(`
12876
+
12877
+ `);
12861
12878
  }
12879
+
12862
12880
  // src/lib/snippets.ts
12863
- import { readdir as readdir8, readFile as readFile32, writeFile as writeFile11, mkdir as mkdir12, rm as rm5 } from "node:fs/promises";
12864
- import path21 from "node:path";
12865
12881
  var SNIPPETS_DIR = "snippets";
12866
12882
  var PLACEHOLDER_RE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
12867
12883
  function extractVariables(template) {
@@ -16745,4 +16761,4 @@ async function setPublishProviderConfig(projectDir, providerId, values) {
16745
16761
  await writeManifestDoc(file, doc);
16746
16762
  return settingsFromDoc(doc);
16747
16763
  }
16748
- export { DEFAULT_NPM_REGISTRY, npmRegistryUrl, PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEntry, extensionCarries, pathEscapesFolder, assertExtensionContained, resolveExtension, loadPluginsWithCss, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, rulePageContainment, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, listMergedSnippets, readExtensionSnippet, importExtensionFromFile, importExtensionFromUrl, MAX_SEARCH_BYTES, SEARCH_KEYWORDS, searchNpmExtensions, setManifestFields, readManifestFields, setActiveStyles, 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, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, refreshRemoteCopies, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };
16764
+ export { DEFAULT_NPM_REGISTRY, npmRegistryUrl, PLUGINS_DIR, BUNDLED_EXTENSIONS, isBundledExtension, isPathSpecifier, parseExtensionSpecifier, PRESET_IDS, PRESETS, TARGETS, TARGET_IDS, publishTargetFor, MANIFEST_FILENAMES, hasProjectManifest, loadManifest, loadManifestWithPath, resolveConfig, SYNC_SNAPSHOT_MESSAGE, log, resolveActiveStyles, listProjectStyles, EXTENSION_MANIFEST_FILENAME, readExtensionMeta, extensionStyleList, extensionEntry, extensionCarries, pathEscapesFolder, assertExtensionContained, resolveExtension, loadPluginsWithCss, getChecks, getCheckById, resolveCheckSelectors, formatReport, ruleRemoteUrls, ruleRiskyProps, ruleSyntax, rulePageContainment, checkCss, inspectImage, runChecks, checkToolAvailability, reportMissingTools, executeValidation, executeAndReport, splitOutPath, runBuild, openPath, startPreviewServer, defaultConfigDir, FileTokenStore, extractUrlCredential, getSystemDiagnostics, slugifyProjectName, escapeYamlScalar, scaffoldProject, adoptFolder, EXTENSION_KINDS, RESERVED_PREFIX, resolveExtensionPrefix, scaffoldExtension, BUILT_IN_TEMPLATE_IDS, listBuiltInTemplates, saveProjectAsTemplate, listCustomTemplates, importTemplateFromFolder, EXTENSIONS_DIR, RECOMMENDED_EXTENSIONS, BUILT_IN_STYLE_SET_IDS, sameExtension, listProjectExtensions, describeExtension, setExtensionEnabled, reorderExtensions, removeExtension, addExtension, listBuiltInStyleSets, addBuiltInStyleSet, validateProjectExtensions, readExtensionCss, SNIPPETS_DIR, extractVariables, substituteVariables, listSnippets, readSnippet, saveSnippet, deleteSnippet, listMergedSnippets, readExtensionSnippet, importExtensionFromFile, importExtensionFromUrl, MAX_SEARCH_BYTES, SEARCH_KEYWORDS, searchNpmExtensions, setManifestFields, readManifestFields, setActiveStyles, 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, GDRIVE_HOST, resolveGoogleClientId, resolveGoogleClientSecret, GOOGLE_NOT_CONFIGURED_MESSAGE, requireGoogleClientCredentials, pkceChallengeFromVerifier, GoogleAuthProvider, revokeGoogleCredential, publishCredentialKey, publishConnectionStatus, listPublishAccounts, connectGoogleDrive, listGitHubRepositories, listRepoBooks, listGitHubBranches, sanitizeCloneFolderName, cloneRepository, isSshRemoteUrl, testRemoteAccess, knownForgeTokenUrl, connectGenericHost, parseRemoteOrigin, forgeKindForHost, diagnoseProjectRemote, syncProject, refreshRemoteCopies, listPublishProviders, publishProviderFor, resolvePublishFormat, resolvePublishRequest, runPublish, connectPublishProvider, disconnectPublishCredential, PublishSelectionsStore, readPublishSettings, setPublishProviderConfig };