@bendyline/docblocks-cli 2.6.2 → 2.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -166,7 +166,7 @@ var init_cancellation = __esm({
166
166
  });
167
167
 
168
168
  // src/mcp/artifact-store.ts
169
- import { createHash, randomUUID as randomUUID2 } from "crypto";
169
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
170
170
  import { mkdtemp as mkdtemp2, open as open3, rm as rm2 } from "fs/promises";
171
171
  import { join as join2 } from "path";
172
172
  import { tmpdir } from "os";
@@ -216,9 +216,9 @@ function immutableArtifactRef(ref) {
216
216
  );
217
217
  return Object.freeze({ ...ref, appliedOptions, engineVersions });
218
218
  }
219
- async function writeFileCancellable(path9, bytes, signal) {
219
+ async function writeFileCancellable(path10, bytes, signal) {
220
220
  throwIfAborted4(signal);
221
- const handle = await open3(path9, "wx");
221
+ const handle = await open3(path10, "wx");
222
222
  try {
223
223
  let offset = 0;
224
224
  while (offset < bytes.byteLength) {
@@ -233,9 +233,9 @@ async function writeFileCancellable(path9, bytes, signal) {
233
233
  await handle.close();
234
234
  }
235
235
  }
236
- async function readFileCancellable(path9, options) {
236
+ async function readFileCancellable(path10, options) {
237
237
  throwIfAborted4(options.signal);
238
- const handle = await open3(path9, "r");
238
+ const handle = await open3(path10, "r");
239
239
  try {
240
240
  const stats = await handle.stat();
241
241
  throwIfAborted4(options.signal);
@@ -275,7 +275,7 @@ async function hashBytesCancellable(bytes, signal) {
275
275
  return hash.digest("hex");
276
276
  }
277
277
  async function removeFilesBestEffort(paths) {
278
- await Promise.allSettled(paths.map((path9) => rm2(path9, { force: true })));
278
+ await Promise.allSettled(paths.map((path10) => rm2(path10, { force: true })));
279
279
  }
280
280
  async function waitForIoDrain(leases, timeoutMs) {
281
281
  if (leases.length === 0) return;
@@ -388,8 +388,8 @@ var init_artifact_store = __esm({
388
388
  throw new Error("MCP artifact exceeds the configured per-artifact limit");
389
389
  }
390
390
  const sha2562 = await hashBytesCancellable(bytes, signal);
391
- const id = randomUUID2();
392
- const path9 = join2(await this.directory, `${id}.artifact`);
391
+ const id = randomUUID3();
392
+ const path10 = join2(await this.directory, `${id}.artifact`);
393
393
  throwIfAborted4(signal);
394
394
  const created = /* @__PURE__ */ new Date();
395
395
  const ref = immutableArtifactRef({
@@ -424,7 +424,7 @@ var init_artifact_store = __esm({
424
424
  });
425
425
  await removeFilesBestEffort(acquisition.expiredPaths);
426
426
  try {
427
- await this.dependencies.writeFile(path9, bytes, acquisition.io.controller.signal);
427
+ await this.dependencies.writeFile(path10, bytes, acquisition.io.controller.signal);
428
428
  throwIfAborted4(acquisition.io.controller.signal);
429
429
  await this.lock(() => {
430
430
  this.assertActive();
@@ -432,7 +432,7 @@ var init_artifact_store = __esm({
432
432
  this.releaseReservationLocked(reservation);
433
433
  this.records.set(id, {
434
434
  ref,
435
- path: path9,
435
+ path: path10,
436
436
  conversionReport: null,
437
437
  reportBytes: 0,
438
438
  activeReaders: 0,
@@ -443,7 +443,7 @@ var init_artifact_store = __esm({
443
443
  return ref;
444
444
  } catch (caught) {
445
445
  await this.lock(() => this.releaseReservationLocked(reservation));
446
- await rm2(path9, { force: true }).catch(() => void 0);
446
+ await rm2(path10, { force: true }).catch(() => void 0);
447
447
  throw caught;
448
448
  } finally {
449
449
  this.completeIoLease(acquisition.io);
@@ -556,8 +556,8 @@ var init_artifact_store = __esm({
556
556
  for (const [id, stored] of this.records) {
557
557
  const expiresAt = stored.ref.expiresAt ? Date.parse(stored.ref.expiresAt) : Number.POSITIVE_INFINITY;
558
558
  if (expiresAt > now) continue;
559
- const path9 = this.removeRecordLocked(id, stored);
560
- if (path9) paths.push(path9);
559
+ const path10 = this.removeRecordLocked(id, stored);
560
+ if (path10) paths.push(path10);
561
561
  }
562
562
  return paths;
563
563
  }
@@ -697,8 +697,8 @@ var init_artifact_store = __esm({
697
697
 
698
698
  // src/mcp/authority.ts
699
699
  import { lstat as lstat3, link as link2, open as open4, realpath as realpath4, rename as rename2, stat as stat5, unlink } from "fs/promises";
700
- import path7 from "path";
701
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
700
+ import path8 from "path";
701
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
702
702
  import { MCP_WIRE_LIMITS as MCP_WIRE_LIMITS2 } from "@bendyline/docblocks/mcp";
703
703
  async function resolveRoots(values) {
704
704
  const roots = [];
@@ -718,7 +718,7 @@ function rootIdFor(physicalPath) {
718
718
  return `root-${createHash2("sha256").update(normalized).digest("hex").slice(0, 16)}`;
719
719
  }
720
720
  function rootLabel(physicalPath) {
721
- const candidate = path7.basename(physicalPath) || "root";
721
+ const candidate = path8.basename(physicalPath) || "root";
722
722
  let label = "";
723
723
  for (const character of candidate) {
724
724
  const codePoint = character.codePointAt(0) ?? 0;
@@ -729,7 +729,7 @@ function rootLabel(physicalPath) {
729
729
  return label || "root";
730
730
  }
731
731
  function parseRootRelativePath(value) {
732
- if (!value || value.length > MAX_PATH_CHARACTERS || value.includes("\0") || value.includes("\\") || path7.posix.isAbsolute(value) || /^[a-zA-Z]:/u.test(value)) {
732
+ if (!value || value.length > MAX_PATH_CHARACTERS || value.includes("\0") || value.includes("\\") || path8.posix.isAbsolute(value) || /^[a-zA-Z]:/u.test(value)) {
733
733
  throw new Error("Invalid MCP root-relative path");
734
734
  }
735
735
  const segments = value.split("/");
@@ -747,7 +747,7 @@ async function nearestExistingPath(candidate) {
747
747
  await lstat3(current);
748
748
  return current;
749
749
  } catch {
750
- const parent = path7.dirname(current);
750
+ const parent = path8.dirname(current);
751
751
  if (parent === current) throw new Error("MCP output has no accessible parent directory");
752
752
  current = parent;
753
753
  }
@@ -757,7 +757,7 @@ function validatePath(value) {
757
757
  if (!value || value.length > MAX_PATH_CHARACTERS || value.includes("\0")) {
758
758
  throw new Error("Invalid MCP filesystem path");
759
759
  }
760
- return path7.resolve(value);
760
+ return path8.resolve(value);
761
761
  }
762
762
  function validatePortableFilename(value) {
763
763
  const stem = value.split(".", 1)[0]?.toLowerCase() ?? "";
@@ -774,7 +774,7 @@ function hasUnsafeFilenameCharacter(value) {
774
774
  }
775
775
  function samePath(left, right) {
776
776
  const normalize = (value) => {
777
- const resolved = path7.resolve(value);
777
+ const resolved = path8.resolve(value);
778
778
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
779
779
  };
780
780
  return normalize(left) === normalize(right);
@@ -836,7 +836,7 @@ async function hashFileBounded(filePath, maximumBytes, signal, afterChunk) {
836
836
  }
837
837
  }
838
838
  async function readMaterializationParentIdentity(target, writeRoots) {
839
- const parent = path7.dirname(target);
839
+ const parent = path8.dirname(target);
840
840
  let lexical;
841
841
  let physical;
842
842
  let info;
@@ -980,7 +980,7 @@ var init_authority = __esm({
980
980
  "Call list_roots and copy a returned read-enabled root id. If no roots are listed, use an inline markdown or bundle source, or restart the server with --allow-read."
981
981
  );
982
982
  }
983
- const candidate = path7.join(root.physical, ...parseRootRelativePath(workspacePath));
983
+ const candidate = path8.join(root.physical, ...parseRootRelativePath(workspacePath));
984
984
  const physical = await realpath4(candidate).catch(() => null);
985
985
  if (!physical || !isPathInside(root.physical, physical)) {
986
986
  throw new Error("MCP read path is outside the selected root");
@@ -996,9 +996,9 @@ var init_authority = __esm({
996
996
  "Call list_roots and copy a returned write-enabled root id. If none is writable, keep the result as a session artifact or restart the server with --allow-write."
997
997
  );
998
998
  }
999
- const candidate = path7.join(root.physical, ...parseRootRelativePath(workspacePath));
999
+ const candidate = path8.join(root.physical, ...parseRootRelativePath(workspacePath));
1000
1000
  const authorized = await this.authorizeWrite(candidate);
1001
- const physicalParent = await realpath4(path7.dirname(authorized));
1001
+ const physicalParent = await realpath4(path8.dirname(authorized));
1002
1002
  if (!isPathInside(root.physical, physicalParent)) {
1003
1003
  throw new Error("MCP write path is outside the selected root");
1004
1004
  }
@@ -1017,7 +1017,7 @@ var init_authority = __esm({
1017
1017
  }
1018
1018
  async authorizeWrite(requestedPath) {
1019
1019
  const candidate = validatePath(requestedPath);
1020
- const filename = path7.basename(candidate);
1020
+ const filename = path8.basename(candidate);
1021
1021
  validatePortableFilename(filename);
1022
1022
  const existing = await lstat3(candidate).catch(() => null);
1023
1023
  if (existing?.isSymbolicLink()) {
@@ -1026,16 +1026,16 @@ var init_authority = __esm({
1026
1026
  if (existing && !existing.isFile()) {
1027
1027
  throw new Error("MCP output target must be a regular file");
1028
1028
  }
1029
- const nearest = await nearestExistingPath(existing ? candidate : path7.dirname(candidate));
1029
+ const nearest = await nearestExistingPath(existing ? candidate : path8.dirname(candidate));
1030
1030
  const physicalTarget = existing ? await realpath4(candidate) : null;
1031
- const physicalParent = physicalTarget ? path7.dirname(physicalTarget) : await realpath4(nearest);
1031
+ const physicalParent = physicalTarget ? path8.dirname(physicalTarget) : await realpath4(nearest);
1032
1032
  if (!this.writeRoots.some((root) => isPathInside(root.physical, physicalParent))) {
1033
1033
  throw new Error("MCP write path is outside the configured roots");
1034
1034
  }
1035
- if (!existing && !samePath(nearest, path7.dirname(candidate))) {
1035
+ if (!existing && !samePath(nearest, path8.dirname(candidate))) {
1036
1036
  throw new Error("MCP output parent directory does not exist");
1037
1037
  }
1038
- return physicalTarget ?? path7.join(physicalParent, filename);
1038
+ return physicalTarget ?? path8.join(physicalParent, filename);
1039
1039
  }
1040
1040
  /** Publish bounded bytes through an operation-owned sibling file. */
1041
1041
  async materializeBytes(requestedPath, content, maximumBytes, options = {}) {
@@ -1079,9 +1079,9 @@ var init_authority = __esm({
1079
1079
  );
1080
1080
  if (actual !== expectedSha256) throw new Error("MCP output hash precondition failed");
1081
1081
  }
1082
- const temporary = path7.join(
1083
- path7.dirname(target),
1084
- `.${path7.basename(target)}.${process.pid}.${randomUUID3()}.tmp`
1082
+ const temporary = path8.join(
1083
+ path8.dirname(target),
1084
+ `.${path8.basename(target)}.${process.pid}.${randomUUID4()}.tmp`
1085
1085
  );
1086
1086
  let handle = null;
1087
1087
  try {
@@ -1181,7 +1181,7 @@ import { createHash as createHash3 } from "crypto";
1181
1181
  import { createRequire } from "module";
1182
1182
  import { readFileSync } from "fs";
1183
1183
  import { readdir, readFile as readFile2, stat as stat6 } from "fs/promises";
1184
- import path8 from "path";
1184
+ import path9 from "path";
1185
1185
  function getPackageVersion() {
1186
1186
  const pkg = readPackageJson();
1187
1187
  if (typeof pkg?.version === "string" && pkg.version.length > 0) {
@@ -1217,10 +1217,10 @@ async function computeRuntimeVersion(packageName, resolutionSpecifier) {
1217
1217
  try {
1218
1218
  const entry = require2.resolve(resolutionSpecifier);
1219
1219
  const root = await findPackageRoot(entry, packageName);
1220
- const files = await collectRuntimeFiles(path8.join(root, "dist"));
1220
+ const files = await collectRuntimeFiles(path9.join(root, "dist"));
1221
1221
  const hash = createHash3("sha256");
1222
1222
  for (const file of files) {
1223
- const relative = path8.relative(root, file).split(path8.sep).join("/");
1223
+ const relative = path9.relative(root, file).split(path9.sep).join("/");
1224
1224
  hash.update(relative);
1225
1225
  hash.update("\0");
1226
1226
  hash.update(await readFile2(file));
@@ -1240,16 +1240,16 @@ function readPackageJson() {
1240
1240
  }
1241
1241
  }
1242
1242
  async function findPackageRoot(entry, packageName) {
1243
- let directory = path8.dirname(entry);
1243
+ let directory = path9.dirname(entry);
1244
1244
  for (let depth = 0; depth < 12; depth += 1) {
1245
1245
  try {
1246
1246
  const parsed = JSON.parse(
1247
- await readFile2(path8.join(directory, "package.json"), "utf8")
1247
+ await readFile2(path9.join(directory, "package.json"), "utf8")
1248
1248
  );
1249
1249
  if (parsed.name === packageName) return directory;
1250
1250
  } catch {
1251
1251
  }
1252
- const parent = path8.dirname(directory);
1252
+ const parent = path9.dirname(directory);
1253
1253
  if (parent === directory) break;
1254
1254
  directory = parent;
1255
1255
  }
@@ -1262,7 +1262,7 @@ async function collectRuntimeFiles(directory) {
1262
1262
  while (pending.length > 0) {
1263
1263
  const current = pending.pop();
1264
1264
  for (const entry of await readdir(current, { withFileTypes: true })) {
1265
- const candidate = path8.join(current, entry.name);
1265
+ const candidate = path9.join(current, entry.name);
1266
1266
  if (entry.isDirectory()) {
1267
1267
  pending.push(candidate);
1268
1268
  } else if (entry.isFile() && /\.(?:js|json|css|wasm)$/iu.test(entry.name)) {
@@ -1346,9 +1346,9 @@ var init_output_bounds = __esm({
1346
1346
 
1347
1347
  // src/mcp/document-service.ts
1348
1348
  import { createHash as createHash5 } from "crypto";
1349
- import { basename as basename3, extname as extname3 } from "path";
1349
+ import { basename as basename4, extname as extname3 } from "path";
1350
1350
  import { MCP_WIRE_LIMITS as MCP_WIRE_LIMITS4 } from "@bendyline/docblocks/mcp";
1351
- import { decodeUtf8Text as decodeUtf8Text3, tryParseWorkspacePath } from "@bendyline/docblocks/filesystem";
1351
+ import { decodeUtf8Text as decodeUtf8Text4, tryParseWorkspacePath as tryParseWorkspacePath2 } from "@bendyline/docblocks/filesystem";
1352
1352
  async function readAssetManifest(container, signal) {
1353
1353
  const bytes = await container.readFile(ASSET_MANIFEST_PATH);
1354
1354
  if (!bytes) return /* @__PURE__ */ new Map();
@@ -1358,7 +1358,7 @@ async function readAssetManifest(container, signal) {
1358
1358
  }
1359
1359
  try {
1360
1360
  const parsed = JSON.parse(
1361
- decodeUtf8Text3(bytes, {
1361
+ decodeUtf8Text4(bytes, {
1362
1362
  label: "The document asset manifest",
1363
1363
  path: ASSET_MANIFEST_PATH
1364
1364
  })
@@ -1372,12 +1372,12 @@ async function readAssetManifest(container, signal) {
1372
1372
  if (!isExactRecord(candidate, ["path", "mimeType", "altText", "credit", "license"])) {
1373
1373
  throw new Error("The document asset manifest contains an invalid entry");
1374
1374
  }
1375
- const path9 = tryParseWorkspacePath(candidate.path);
1376
- if (path9 === null || path9 === "" || path9 === "index.md" || path9.startsWith(".docblocks/") || !isNullableMimeType(candidate.mimeType) || !isNullableMetadata(candidate.altText) || !isNullableMetadata(candidate.credit) || !isNullableMetadata(candidate.license) || entries.has(path9)) {
1375
+ const path10 = tryParseWorkspacePath2(candidate.path);
1376
+ if (path10 === null || path10 === "" || path10 === "index.md" || path10.startsWith(".docblocks/") || !isNullableMimeType(candidate.mimeType) || !isNullableMetadata(candidate.altText) || !isNullableMetadata(candidate.credit) || !isNullableMetadata(candidate.license) || entries.has(path10)) {
1377
1377
  throw new Error("The document asset manifest contains invalid metadata");
1378
1378
  }
1379
- entries.set(path9, {
1380
- path: path9,
1379
+ entries.set(path10, {
1380
+ path: path10,
1381
1381
  mimeType: candidate.mimeType,
1382
1382
  altText: candidate.altText,
1383
1383
  credit: candidate.credit,
@@ -1513,7 +1513,7 @@ function throwIfAborted5(signal) {
1513
1513
  throwIfAborted(signal, "MCP operation was cancelled");
1514
1514
  }
1515
1515
  function baseNameFromName(name) {
1516
- const leaf = basename3(name.replace(/\\/gu, "/")) || "document";
1516
+ const leaf = basename4(name.replace(/\\/gu, "/")) || "document";
1517
1517
  const extension = extname3(leaf);
1518
1518
  return (extension ? leaf.slice(0, -extension.length) : leaf) || "document";
1519
1519
  }
@@ -1605,7 +1605,7 @@ var init_document_service = __esm({
1605
1605
  const physical = await this.authority.authorizeRootRead(source.rootId, source.path);
1606
1606
  const bytes = await this.authority.readFile(physical, signal);
1607
1607
  throwIfAborted5(signal);
1608
- const filename = basename3(source.path);
1608
+ const filename = basename4(source.path);
1609
1609
  return {
1610
1610
  bytes,
1611
1611
  filename,
@@ -1662,7 +1662,7 @@ var init_document_service = __esm({
1662
1662
  case "file": {
1663
1663
  const physical = await this.authority.authorizeRootRead(source.rootId, source.path);
1664
1664
  const bytes = await this.authority.readFile(physical, signal);
1665
- const name = basename3(source.path);
1665
+ const name = basename4(source.path);
1666
1666
  return {
1667
1667
  source: { kind: "bytes", data: bytes, filename: name },
1668
1668
  sourceFormat: normalizeFormat(source.format ?? formatFromName(name)),
@@ -2670,7 +2670,7 @@ var init_intelligence = __esm({
2670
2670
  // src/mcp/preview-service.ts
2671
2671
  import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm3, writeFile as writeFile2 } from "fs/promises";
2672
2672
  import { tmpdir as tmpdir2 } from "os";
2673
- import { basename as basename4, extname as extname4, join as join3 } from "path";
2673
+ import { basename as basename5, extname as extname4, join as join3 } from "path";
2674
2674
  import { DOCBLOCKS_MCP_WIRE_VERSION as DOCBLOCKS_MCP_WIRE_VERSION2 } from "@bendyline/docblocks/mcp";
2675
2675
  async function previewEngineVersions() {
2676
2676
  return [
@@ -2908,7 +2908,7 @@ function thumbnailFilter(width, height) {
2908
2908
  return `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`;
2909
2909
  }
2910
2910
  function baseNameFromFilename(filename) {
2911
- const leaf = basename4(filename.replace(/\\/gu, "/")) || "video";
2911
+ const leaf = basename5(filename.replace(/\\/gu, "/")) || "video";
2912
2912
  const extension = extname4(leaf);
2913
2913
  return (extension ? leaf.slice(0, -extension.length) : leaf) || "video";
2914
2914
  }
@@ -3706,7 +3706,7 @@ async function convertPreparedDocument(artifacts, prepared, request, signal, pro
3706
3706
  }
3707
3707
  }
3708
3708
  function createConversionCacheKey(prepared, request, target, fidelity, engineVersions) {
3709
- const canonical = canonicalJson({
3709
+ const canonical2 = canonicalJson({
3710
3710
  source: {
3711
3711
  sha256: prepared.sourceSha256,
3712
3712
  format: prepared.sourceFormat,
@@ -3727,7 +3727,7 @@ function createConversionCacheKey(prepared, request, target, fidelity, engineVer
3727
3727
  title: request.title ?? null
3728
3728
  }
3729
3729
  });
3730
- return createHash6("sha256").update(canonical).digest("hex");
3730
+ return createHash6("sha256").update(canonical2).digest("hex");
3731
3731
  }
3732
3732
  async function findCompletedConversion(artifacts, key) {
3733
3733
  const cache = completedConversionCache(artifacts);
@@ -4305,10 +4305,10 @@ function registerAgenticTools(server, context) {
4305
4305
  async ({ source, targets, themeId, transformId, autoTemplates, title }, extra) => {
4306
4306
  try {
4307
4307
  return await context.runOperation(extra.signal, async (operationSignal) => {
4308
- const canonical = requireDocumentSource(source);
4308
+ const canonical2 = requireDocumentSource(source);
4309
4309
  const documents = new DocumentService(await context.authority, context.artifacts);
4310
4310
  await sendProgress(extra, 0, targets.length + 1, "Resolving document");
4311
- const prepared = await documents.prepare(canonical, operationSignal);
4311
+ const prepared = await documents.prepare(canonical2, operationSignal);
4312
4312
  const results = await convertPreparedDocument(
4313
4313
  context.artifacts,
4314
4314
  prepared,
@@ -4350,9 +4350,9 @@ function registerAgenticTools(server, context) {
4350
4350
  async ({ source }, extra) => {
4351
4351
  try {
4352
4352
  return await context.runOperation(extra.signal, async (operationSignal) => {
4353
- const canonical = requireDocumentSource(source);
4353
+ const canonical2 = requireDocumentSource(source);
4354
4354
  const documents = new DocumentService(await context.authority, context.artifacts);
4355
- const prepared = await documents.prepare(canonical, operationSignal);
4355
+ const prepared = await documents.prepare(canonical2, operationSignal);
4356
4356
  const [result] = await convertPreparedDocument(
4357
4357
  context.artifacts,
4358
4358
  prepared,
@@ -4385,7 +4385,7 @@ function registerAgenticTools(server, context) {
4385
4385
  async ({ artifactUri, destination }, extra) => {
4386
4386
  try {
4387
4387
  return await context.runOperation(extra.signal, async (operationSignal) => {
4388
- const canonical = requireWire(
4388
+ const canonical2 = requireWire(
4389
4389
  parseMaterializationOptions(destination),
4390
4390
  "materialization options"
4391
4391
  );
@@ -4395,15 +4395,15 @@ function registerAgenticTools(server, context) {
4395
4395
  context.artifacts.read(artifactUri, operationSignal)
4396
4396
  ]);
4397
4397
  throwIfAborted5(operationSignal);
4398
- const outputPath = await authority.authorizeRootWrite(canonical.rootId, canonical.path);
4398
+ const outputPath = await authority.authorizeRootWrite(canonical2.rootId, canonical2.path);
4399
4399
  await authority.materializeBytes(outputPath, bytes, bytes.byteLength, {
4400
- ifExists: canonical.ifExists,
4401
- expectedSha256: canonical.expectedSha256 ?? void 0,
4400
+ ifExists: canonical2.ifExists,
4401
+ expectedSha256: canonical2.expectedSha256 ?? void 0,
4402
4402
  signal: operationSignal
4403
4403
  });
4404
4404
  const payload = {
4405
4405
  artifact,
4406
- destination: { rootId: canonical.rootId, path: canonical.path },
4406
+ destination: { rootId: canonical2.rootId, path: canonical2.path },
4407
4407
  sha256: artifact.sha256
4408
4408
  };
4409
4409
  return textAndStructured(payload);
@@ -4710,6 +4710,7 @@ function registerTemplateTools(server, context) {
4710
4710
  title: boundNullableWireText(block.title, MCP_WIRE_LIMITS7.labelCharacters),
4711
4711
  profile: {
4712
4712
  ...profile,
4713
+ hasCompoundBody: profile.hasCompoundBody ?? false,
4713
4714
  hasAsciiDiagram: profile.hasAsciiDiagram ?? false,
4714
4715
  hasTimeline: profile.hasTimeline ?? false,
4715
4716
  hasTree: profile.hasTree ?? false
@@ -4866,6 +4867,7 @@ function registerTemplateTools(server, context) {
4866
4867
  title: boundNullableWireText(block.title, MCP_WIRE_LIMITS7.labelCharacters),
4867
4868
  profile: {
4868
4869
  ...profile,
4870
+ hasCompoundBody: profile.hasCompoundBody ?? false,
4869
4871
  hasAsciiDiagram: profile.hasAsciiDiagram ?? false,
4870
4872
  hasTimeline: profile.hasTimeline ?? false,
4871
4873
  hasTree: profile.hasTree ?? false
@@ -6158,12 +6160,12 @@ var init_server = __esm({
6158
6160
  });
6159
6161
 
6160
6162
  // src/program.ts
6161
- import { Command as Command9 } from "commander";
6163
+ import { Command as Command10 } from "commander";
6162
6164
 
6163
6165
  // src/commands/build.ts
6164
6166
  import { Command } from "commander";
6165
6167
  import { mkdir, opendir, readFile, stat as stat2, writeFile } from "fs/promises";
6166
- import path4 from "path";
6168
+ import path5 from "path";
6167
6169
 
6168
6170
  // src/render-html.ts
6169
6171
  init_contained_file();
@@ -6172,7 +6174,7 @@ init_node_error();
6172
6174
  import { realpath, stat } from "fs/promises";
6173
6175
  import path3 from "path";
6174
6176
  import { markdownToDoc } from "@bendyline/squisq/doc";
6175
- import { parseMarkdown } from "@bendyline/squisq/markdown";
6177
+ import { parseMarkdown, readFrontmatterThemeId } from "@bendyline/squisq/markdown";
6176
6178
  import { collectImagePaths, docToHtml } from "@bendyline/squisq-formats/html";
6177
6179
  import { PLAYER_BUNDLE } from "@bendyline/squisq-react/standalone-source";
6178
6180
 
@@ -6194,25 +6196,35 @@ var DEFAULT_MAX_ASSET_BYTES = 50 * 1024 * 1024;
6194
6196
  var MAX_SINGLE_ASSET_BYTES = 20 * 1024 * 1024;
6195
6197
  async function renderMarkdownHtml(markdown, options) {
6196
6198
  await assertKnownThemeId(options.themeId);
6199
+ const warn = options.onWarning ?? ((message) => console.warn(message));
6197
6200
  const markdownDoc = parseMarkdown(markdown);
6198
6201
  const doc = markdownToDoc(markdownDoc);
6202
+ const themeId = options.themeId ?? (readFrontmatterThemeId(markdownDoc.frontmatter) ? void 0 : await usableFallbackThemeId(options.fallbackThemeId, warn));
6199
6203
  const images = options.sourcePath && options.assetRoot ? await readReferencedImages(
6200
6204
  doc,
6201
6205
  options.sourcePath,
6202
6206
  options.assetRoot,
6203
6207
  options.maxAssetBytes ?? DEFAULT_MAX_ASSET_BYTES,
6204
6208
  options.allowReferencedAsset,
6205
- options.onWarning ?? ((message) => console.warn(message))
6209
+ warn
6206
6210
  ) : void 0;
6207
6211
  return docToHtml(doc, {
6208
6212
  playerScript: PLAYER_BUNDLE,
6209
6213
  images,
6210
6214
  title: options.title,
6211
6215
  mode: options.mode ?? "static",
6212
- themeId: options.themeId,
6216
+ themeId,
6213
6217
  showCodeCopyButton: true
6214
6218
  });
6215
6219
  }
6220
+ async function usableFallbackThemeId(fallbackThemeId, warn) {
6221
+ if (!fallbackThemeId) return void 0;
6222
+ if ((await getAvailableThemeIds()).includes(fallbackThemeId)) return fallbackThemeId;
6223
+ warn(
6224
+ `Ignoring unknown fallback theme "${fallbackThemeId}"; the document's default theme applies.`
6225
+ );
6226
+ return void 0;
6227
+ }
6216
6228
  async function readReferencedImages(doc, sourcePath, assetRoot, maxAssetBytes, allowReferencedAsset, warn) {
6217
6229
  const images = /* @__PURE__ */ new Map();
6218
6230
  const root = await realpathIfPresent(path3.resolve(assetRoot));
@@ -6310,7 +6322,564 @@ function positiveLimit(value, fallback, label) {
6310
6322
 
6311
6323
  // src/commands/build.ts
6312
6324
  init_node_error();
6313
- import { decodeUtf8Text } from "@bendyline/docblocks/filesystem";
6325
+
6326
+ // src/internal/workspace-settings.ts
6327
+ import { decodeUtf8Text, parseWorkspacePath as parseWorkspacePath2 } from "@bendyline/docblocks/filesystem";
6328
+ import {
6329
+ WORKSPACE_SETTINGS_LIMITS,
6330
+ WORKSPACE_SETTINGS_PATH,
6331
+ parseWorkspaceSettingsText,
6332
+ resolveWorkspaceDefaultThemeId
6333
+ } from "@bendyline/docblocks/workspace-settings";
6334
+
6335
+ // src/internal/workspace-io.ts
6336
+ init_link_support();
6337
+ import { constants as fsConstants } from "fs";
6338
+ import fs2 from "fs/promises";
6339
+ import { randomUUID } from "crypto";
6340
+ import path4 from "path";
6341
+ import {
6342
+ FsError,
6343
+ mapNodeErrorCodeToFsErrorCode,
6344
+ parseFileSystemVersion,
6345
+ parseWorkspacePath,
6346
+ tryParseWorkspacePath
6347
+ } from "@bendyline/docblocks/filesystem";
6348
+ init_node_error();
6349
+ init_paths();
6350
+ var NODE_WORKSPACE_IO_LIMITS = {
6351
+ maxFileBytes: 256 * 1024 * 1024,
6352
+ maxDirectoryEntries: 1e5
6353
+ };
6354
+ var LISTING_STAT_CONCURRENCY = 16;
6355
+ var TEMPORARY_PREFIX = ".docblocks-tmp-";
6356
+ async function createNodeWorkspaceIO(rootDir, options = {}) {
6357
+ const requested = path4.resolve(rootDir);
6358
+ let root;
6359
+ try {
6360
+ root = await fs2.realpath(requested);
6361
+ } catch (error) {
6362
+ if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) {
6363
+ throw new FsError("not-found", `Workspace folder not found: ${requested}`, {
6364
+ operation: "stat"
6365
+ });
6366
+ }
6367
+ throw toFsError(error, "stat", "");
6368
+ }
6369
+ let info;
6370
+ try {
6371
+ info = await fs2.stat(root);
6372
+ } catch (error) {
6373
+ throw toFsError(error, "stat", "");
6374
+ }
6375
+ if (!info.isDirectory()) {
6376
+ throw new FsError("type-mismatch", `Workspace root is not a folder: ${requested}`, {
6377
+ operation: "stat"
6378
+ });
6379
+ }
6380
+ return new NodeWorkspaceIO(root, {
6381
+ maxFileBytes: positiveLimit(
6382
+ options.maxFileBytes,
6383
+ NODE_WORKSPACE_IO_LIMITS.maxFileBytes,
6384
+ "workspace file byte"
6385
+ ),
6386
+ maxDirectoryEntries: positiveLimit(
6387
+ options.maxDirectoryEntries,
6388
+ NODE_WORKSPACE_IO_LIMITS.maxDirectoryEntries,
6389
+ "workspace folder entry"
6390
+ )
6391
+ });
6392
+ }
6393
+ var NodeWorkspaceIO = class {
6394
+ constructor(root, limits) {
6395
+ this.root = root;
6396
+ this.limits = limits;
6397
+ }
6398
+ async stat(input) {
6399
+ const workspacePath = canonical(input, "stat");
6400
+ try {
6401
+ const located = await this.locate(workspacePath, "stat");
6402
+ if (!located) return null;
6403
+ const info = await lstatOrNull(located);
6404
+ if (!info) return null;
6405
+ return snapshotFor(workspacePath, assertSupportedKind(info, workspacePath, "stat"), info);
6406
+ } catch (error) {
6407
+ throw toFsError(error, "stat", workspacePath);
6408
+ }
6409
+ }
6410
+ async readFile(input) {
6411
+ const workspacePath = canonical(input, "read");
6412
+ try {
6413
+ const located = await this.locate(workspacePath, "read");
6414
+ if (!located) return null;
6415
+ const before = await lstatOrNull(located);
6416
+ if (!before) return null;
6417
+ if (assertSupportedKind(before, workspacePath, "read") !== "file") {
6418
+ throw new FsError("type-mismatch", `"${workspacePath}" is a folder, not a file.`, {
6419
+ operation: "read",
6420
+ path: workspacePath
6421
+ });
6422
+ }
6423
+ this.assertFileSize(before.size, workspacePath, "read");
6424
+ return await this.readLocatedFile(located, before, workspacePath);
6425
+ } catch (error) {
6426
+ if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
6427
+ throw toFsError(error, "read", workspacePath);
6428
+ }
6429
+ }
6430
+ async readDirectory(input) {
6431
+ const workspacePath = canonical(input, "list");
6432
+ try {
6433
+ let directory = this.root;
6434
+ if (workspacePath) {
6435
+ const located = await this.locate(workspacePath, "list");
6436
+ const info = located ? await lstatOrNull(located) : null;
6437
+ if (!located || !info) {
6438
+ throw new FsError("not-found", `Folder "${workspacePath}" does not exist.`, {
6439
+ operation: "list",
6440
+ path: workspacePath
6441
+ });
6442
+ }
6443
+ if (assertSupportedKind(info, workspacePath, "list") !== "directory") {
6444
+ throw new FsError("type-mismatch", `"${workspacePath}" is a file, not a folder.`, {
6445
+ operation: "list",
6446
+ path: workspacePath
6447
+ });
6448
+ }
6449
+ directory = located;
6450
+ }
6451
+ const names = await this.listNames(directory, workspacePath);
6452
+ const entries = [];
6453
+ for (let index = 0; index < names.length; index += LISTING_STAT_CONCURRENCY) {
6454
+ const batch = names.slice(index, index + LISTING_STAT_CONCURRENCY);
6455
+ const snapshots = await Promise.all(
6456
+ batch.map(async (name) => {
6457
+ const info = await lstatOrNull(path4.join(directory, name));
6458
+ if (!info || info.isSymbolicLink()) return null;
6459
+ const kind = info.isFile() ? "file" : info.isDirectory() ? "directory" : null;
6460
+ if (!kind) return null;
6461
+ const childPath = parseWorkspacePath(workspacePath ? `${workspacePath}/${name}` : name);
6462
+ return snapshotFor(childPath, kind, info);
6463
+ })
6464
+ );
6465
+ for (const snapshot of snapshots) if (snapshot) entries.push(snapshot);
6466
+ }
6467
+ return Object.freeze(entries.sort(compareSnapshots));
6468
+ } catch (error) {
6469
+ throw toFsError(error, "list", workspacePath);
6470
+ }
6471
+ }
6472
+ async writeFile(input, data, options = {}) {
6473
+ const workspacePath = canonical(input, "write");
6474
+ if (!workspacePath) {
6475
+ throw new FsError("invalid-path", "The workspace root cannot be written.", {
6476
+ operation: "write",
6477
+ path: workspacePath
6478
+ });
6479
+ }
6480
+ const bytes = ArrayBuffer.isView(data) ? new Uint8Array(data.buffer, data.byteOffset, data.byteLength) : new Uint8Array(data);
6481
+ const mode = options.mode ?? "upsert";
6482
+ const expectedVersion = options.expectedVersion;
6483
+ let temporary = null;
6484
+ try {
6485
+ this.assertFileSize(BigInt(bytes.byteLength), workspacePath, "write");
6486
+ const parent = await this.ensureParent(workspacePath, options.createParents === true);
6487
+ const target = path4.join(parent, basename(workspacePath));
6488
+ let current = await lstatOrNull(target);
6489
+ assertWritable(current, workspacePath, mode, expectedVersion);
6490
+ temporary = path4.join(parent, `${TEMPORARY_PREFIX}${randomUUID()}`);
6491
+ const handle = await fs2.open(temporary, "wx", 438);
6492
+ try {
6493
+ await handle.writeFile(bytes);
6494
+ if (current?.isFile()) await handle.chmod(Number(current.mode & 0o7777n));
6495
+ } finally {
6496
+ await handle.close();
6497
+ }
6498
+ current = await lstatOrNull(target);
6499
+ assertWritable(current, workspacePath, mode, expectedVersion);
6500
+ if (!current && (mode === "create" || expectedVersion === null)) {
6501
+ await publishExclusive(temporary, target, bytes, workspacePath, mode);
6502
+ } else {
6503
+ await fs2.rename(temporary, target);
6504
+ temporary = null;
6505
+ }
6506
+ const written = await fs2.lstat(target, { bigint: true });
6507
+ const snapshot = snapshotFor(workspacePath, "file", written);
6508
+ if (snapshot.kind !== "file") throw new Error("Unreachable: wrote a non-file entry.");
6509
+ return snapshot;
6510
+ } catch (error) {
6511
+ throw toFsError(error, "write", workspacePath);
6512
+ } finally {
6513
+ if (temporary) await fs2.rm(temporary, { force: true }).catch(() => void 0);
6514
+ }
6515
+ }
6516
+ /**
6517
+ * Physical absolute path for `workspacePath`, with its parent proven to be
6518
+ * inside the root, or null when a parent is missing or is a file.
6519
+ */
6520
+ async locate(workspacePath, operation) {
6521
+ if (!workspacePath) return this.root;
6522
+ const segments = workspacePath.split("/");
6523
+ const name = segments.pop();
6524
+ let parent;
6525
+ try {
6526
+ parent = await fs2.realpath(path4.join(this.root, ...segments));
6527
+ } catch (error) {
6528
+ if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
6529
+ throw error;
6530
+ }
6531
+ if (!isPathInside(this.root, parent)) throw escapeError(workspacePath, operation);
6532
+ return path4.join(parent, name);
6533
+ }
6534
+ /** Resolve (and optionally create) the physical parent folder of a write. */
6535
+ async ensureParent(workspacePath, createParents) {
6536
+ const segments = workspacePath.split("/").slice(0, -1);
6537
+ let current = this.root;
6538
+ let walked = "";
6539
+ for (const segment of segments) {
6540
+ walked = walked ? `${walked}/${segment}` : segment;
6541
+ const next = path4.join(current, segment);
6542
+ let info = await lstatOrNull(next);
6543
+ if (!info) {
6544
+ if (!createParents) {
6545
+ throw new FsError("not-found", `Parent folder "${walked}" does not exist.`, {
6546
+ operation: "write",
6547
+ path: workspacePath
6548
+ });
6549
+ }
6550
+ try {
6551
+ await fs2.mkdir(next);
6552
+ } catch (error) {
6553
+ if (!isNodeErrorCode(error, "EEXIST")) throw error;
6554
+ }
6555
+ info = await fs2.lstat(next, { bigint: true });
6556
+ }
6557
+ if (info.isSymbolicLink()) {
6558
+ const physical = await fs2.realpath(next);
6559
+ if (!isPathInside(this.root, physical)) throw escapeError(workspacePath, "write");
6560
+ if (!(await fs2.stat(physical)).isDirectory()) throw parentFileError(walked, workspacePath);
6561
+ current = physical;
6562
+ } else if (info.isDirectory()) {
6563
+ current = next;
6564
+ } else {
6565
+ throw parentFileError(walked, workspacePath);
6566
+ }
6567
+ }
6568
+ const physicalParent = await fs2.realpath(current);
6569
+ if (!isPathInside(this.root, physicalParent)) throw escapeError(workspacePath, "write");
6570
+ return physicalParent;
6571
+ }
6572
+ async listNames(directory, workspacePath) {
6573
+ const names = [];
6574
+ let seen = 0;
6575
+ const handle = await fs2.opendir(directory);
6576
+ for await (const dirent of handle) {
6577
+ seen += 1;
6578
+ if (seen > this.limits.maxDirectoryEntries) {
6579
+ throw new FsError(
6580
+ "not-supported",
6581
+ `Folder "${workspacePath || "."}" has more than ${this.limits.maxDirectoryEntries} entries.`,
6582
+ { operation: "list", path: workspacePath }
6583
+ );
6584
+ }
6585
+ if (dirent.isSymbolicLink()) continue;
6586
+ if (tryParseWorkspacePath(dirent.name) !== dirent.name) continue;
6587
+ names.push(dirent.name);
6588
+ }
6589
+ return names;
6590
+ }
6591
+ async readLocatedFile(located, before, workspacePath) {
6592
+ const noFollow = typeof fsConstants.O_NOFOLLOW === "number" ? fsConstants.O_NOFOLLOW : 0;
6593
+ const handle = await fs2.open(located, fsConstants.O_RDONLY | noFollow);
6594
+ try {
6595
+ const opened = await handle.stat({ bigint: true });
6596
+ if (!opened.isFile() || opened.ino !== before.ino) throw changedError(workspacePath);
6597
+ const physical = await fs2.realpath(located);
6598
+ if (!isPathInside(this.root, physical)) throw escapeError(workspacePath, "read");
6599
+ this.assertFileSize(opened.size, workspacePath, "read");
6600
+ const size = Number(opened.size);
6601
+ const buffer = new Uint8Array(size);
6602
+ let offset = 0;
6603
+ while (offset < size) {
6604
+ const { bytesRead } = await handle.read(buffer, offset, size - offset, offset);
6605
+ if (bytesRead === 0) break;
6606
+ offset += bytesRead;
6607
+ }
6608
+ const probe = new Uint8Array(1);
6609
+ const grew = (await handle.read(probe, 0, 1, offset)).bytesRead > 0;
6610
+ const after = await handle.stat({ bigint: true });
6611
+ if (grew || offset !== size || !sameFileState(opened, after)) {
6612
+ throw changedError(workspacePath);
6613
+ }
6614
+ const entry = snapshotFor(workspacePath, "file", opened);
6615
+ if (entry.kind !== "file") throw new Error("Unreachable: read a non-file entry.");
6616
+ return Object.freeze({ entry, data: buffer.buffer });
6617
+ } finally {
6618
+ await handle.close();
6619
+ }
6620
+ }
6621
+ assertFileSize(size, workspacePath, operation) {
6622
+ if (size > BigInt(this.limits.maxFileBytes)) {
6623
+ throw new FsError(
6624
+ "not-supported",
6625
+ `"${workspacePath}" is larger than the ${this.limits.maxFileBytes}-byte workspace file limit.`,
6626
+ { operation, path: workspacePath }
6627
+ );
6628
+ }
6629
+ }
6630
+ };
6631
+ function canonical(input, operation) {
6632
+ try {
6633
+ return parseWorkspacePath(input);
6634
+ } catch (error) {
6635
+ throw toFsError(error, operation, input);
6636
+ }
6637
+ }
6638
+ function basename(workspacePath) {
6639
+ const slash = workspacePath.lastIndexOf("/");
6640
+ return slash < 0 ? workspacePath : workspacePath.slice(slash + 1);
6641
+ }
6642
+ async function lstatOrNull(absolutePath) {
6643
+ try {
6644
+ return await fs2.lstat(absolutePath, { bigint: true });
6645
+ } catch (error) {
6646
+ if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
6647
+ throw error;
6648
+ }
6649
+ }
6650
+ function assertSupportedKind(info, workspacePath, operation) {
6651
+ if (info.isSymbolicLink()) {
6652
+ throw new FsError(
6653
+ "not-supported",
6654
+ `"${workspacePath}" is a symbolic link; workspace outputs never follow links.`,
6655
+ { operation, path: workspacePath }
6656
+ );
6657
+ }
6658
+ if (info.isFile()) return "file";
6659
+ if (info.isDirectory()) return "directory";
6660
+ throw new FsError("not-supported", `"${workspacePath}" is not a regular file or folder.`, {
6661
+ operation,
6662
+ path: workspacePath
6663
+ });
6664
+ }
6665
+ function versionFor(kind, info) {
6666
+ return parseFileSystemVersion(
6667
+ `node-v1:${kind}:ino=${info.ino}:size=${kind === "file" ? info.size : 0n}:mtime-ns=${info.mtimeNs}`
6668
+ );
6669
+ }
6670
+ function snapshotFor(workspacePath, kind, info) {
6671
+ const base = {
6672
+ path: workspacePath,
6673
+ name: basename(workspacePath),
6674
+ version: versionFor(kind, info),
6675
+ lastModified: info.mtime.toISOString()
6676
+ };
6677
+ if (kind === "directory") return Object.freeze({ ...base, kind, size: null });
6678
+ const size = Number(info.size);
6679
+ if (!Number.isSafeInteger(size)) {
6680
+ throw new FsError("not-supported", `"${workspacePath}" is too large to describe.`, {
6681
+ path: workspacePath
6682
+ });
6683
+ }
6684
+ return Object.freeze({ ...base, kind, size });
6685
+ }
6686
+ function sameFileState(left, right) {
6687
+ return left.ino === right.ino && left.size === right.size && left.mtimeNs === right.mtimeNs;
6688
+ }
6689
+ function assertWritable(current, workspacePath, mode, expectedVersion) {
6690
+ if (current) {
6691
+ const kind = assertSupportedKind(current, workspacePath, "write");
6692
+ if (kind === "directory") {
6693
+ throw new FsError("type-mismatch", `"${workspacePath}" is a folder; it cannot be replaced.`, {
6694
+ operation: "write",
6695
+ path: workspacePath
6696
+ });
6697
+ }
6698
+ if (mode === "create") {
6699
+ throw new FsError("already-exists", `"${workspacePath}" already exists.`, {
6700
+ operation: "write",
6701
+ path: workspacePath
6702
+ });
6703
+ }
6704
+ } else if (mode === "replace") {
6705
+ throw new FsError("not-found", `"${workspacePath}" does not exist.`, {
6706
+ operation: "write",
6707
+ path: workspacePath
6708
+ });
6709
+ }
6710
+ if (expectedVersion === void 0) return;
6711
+ const matches = expectedVersion === null ? !current : !!current && versionFor("file", current) === expectedVersion;
6712
+ if (!matches) {
6713
+ throw new FsError("conflict", `"${workspacePath}" changed since it was read.`, {
6714
+ operation: "write",
6715
+ path: workspacePath,
6716
+ retryable: true
6717
+ });
6718
+ }
6719
+ }
6720
+ async function publishExclusive(temporary, target, bytes, workspacePath, mode) {
6721
+ try {
6722
+ await fs2.link(temporary, target);
6723
+ return;
6724
+ } catch (error) {
6725
+ if (isNodeErrorCode(error, "EEXIST")) throw existsRaceError(workspacePath, mode);
6726
+ if (!isLinkUnsupportedError(error)) throw error;
6727
+ }
6728
+ let handle;
6729
+ try {
6730
+ handle = await fs2.open(target, "wx", 438);
6731
+ } catch (error) {
6732
+ if (isNodeErrorCode(error, "EEXIST")) throw existsRaceError(workspacePath, mode);
6733
+ throw error;
6734
+ }
6735
+ try {
6736
+ await handle.writeFile(bytes);
6737
+ } finally {
6738
+ await handle.close();
6739
+ }
6740
+ }
6741
+ function existsRaceError(workspacePath, mode) {
6742
+ return mode === "create" ? new FsError("already-exists", `"${workspacePath}" already exists.`, {
6743
+ operation: "write",
6744
+ path: workspacePath
6745
+ }) : new FsError("conflict", `"${workspacePath}" was created by another writer.`, {
6746
+ operation: "write",
6747
+ path: workspacePath,
6748
+ retryable: true
6749
+ });
6750
+ }
6751
+ function escapeError(workspacePath, operation) {
6752
+ return new FsError(
6753
+ "path-escape",
6754
+ `"${workspacePath}" resolves outside the workspace folder through a symbolic link.`,
6755
+ { operation, path: workspacePath }
6756
+ );
6757
+ }
6758
+ function parentFileError(parent, workspacePath) {
6759
+ return new FsError("type-mismatch", `"${parent}" is a file, not a folder.`, {
6760
+ operation: "write",
6761
+ path: workspacePath
6762
+ });
6763
+ }
6764
+ function changedError(workspacePath) {
6765
+ return new FsError("conflict", `"${workspacePath}" changed while it was being read.`, {
6766
+ operation: "read",
6767
+ path: workspacePath,
6768
+ retryable: true
6769
+ });
6770
+ }
6771
+ function compareSnapshots(left, right) {
6772
+ if (left.kind !== right.kind) return left.kind === "directory" ? -1 : 1;
6773
+ return left.name < right.name ? -1 : left.name > right.name ? 1 : 0;
6774
+ }
6775
+ function toFsError(error, operation, workspacePath) {
6776
+ if (error instanceof FsError) return error;
6777
+ const message = error instanceof Error ? error.message : String(error);
6778
+ const code = nodeErrorCode(error);
6779
+ if (code === "ELOOP") {
6780
+ return new FsError(
6781
+ "not-supported",
6782
+ `"${workspacePath}" is a symbolic link; workspace outputs never follow links.`,
6783
+ { operation, path: workspacePath }
6784
+ );
6785
+ }
6786
+ if (code) {
6787
+ return new FsError(
6788
+ mapNodeErrorCodeToFsErrorCode(code),
6789
+ `Could not ${OPERATION_VERBS[operation]} "${workspacePath || "."}": ${message}`,
6790
+ { operation, path: workspacePath }
6791
+ );
6792
+ }
6793
+ return new FsError("io", message, { operation, path: workspacePath });
6794
+ }
6795
+ function nodeErrorCode(error) {
6796
+ if (!(error instanceof Error) || !("code" in error)) return null;
6797
+ const { code } = error;
6798
+ return typeof code === "string" ? code : null;
6799
+ }
6800
+ var OPERATION_VERBS = {
6801
+ "parse-path": "parse",
6802
+ read: "read",
6803
+ stat: "inspect",
6804
+ list: "list",
6805
+ write: "write",
6806
+ "create-directory": "create",
6807
+ remove: "remove",
6808
+ move: "move",
6809
+ snapshot: "snapshot",
6810
+ watch: "watch",
6811
+ dispose: "close"
6812
+ };
6813
+
6814
+ // src/internal/workspace-settings.ts
6815
+ var MAX_SETTINGS_FILE_BYTES = WORKSPACE_SETTINGS_LIMITS.maxFileBytes + 3;
6816
+ async function readWorkspaceSettings(io) {
6817
+ const settingsPath = parseWorkspacePath2(WORKSPACE_SETTINGS_PATH);
6818
+ const info = await io.stat(settingsPath);
6819
+ if (!info) return { status: "missing" };
6820
+ if (info.kind !== "file") {
6821
+ return { status: "invalid", message: `${WORKSPACE_SETTINGS_PATH} is a folder, not a file.` };
6822
+ }
6823
+ if (info.size > MAX_SETTINGS_FILE_BYTES) return tooLarge();
6824
+ const read = await io.readFile(settingsPath);
6825
+ if (!read) return { status: "missing" };
6826
+ if (read.data.byteLength > MAX_SETTINGS_FILE_BYTES) return tooLarge();
6827
+ let text;
6828
+ try {
6829
+ text = decodeUtf8Text(read.data, {
6830
+ label: "Workspace settings",
6831
+ path: WORKSPACE_SETTINGS_PATH
6832
+ });
6833
+ } catch (error) {
6834
+ return {
6835
+ status: "invalid",
6836
+ message: error instanceof Error ? error.message : `${WORKSPACE_SETTINGS_PATH} is not UTF-8.`
6837
+ };
6838
+ }
6839
+ return parseWorkspaceSettingsText(text);
6840
+ }
6841
+ function describeWorkspaceSettingsProblem(message) {
6842
+ return message.includes(WORKSPACE_SETTINGS_PATH) ? message : `${WORKSPACE_SETTINGS_PATH}: ${message}`;
6843
+ }
6844
+ function tooLarge() {
6845
+ return {
6846
+ status: "invalid",
6847
+ message: `${WORKSPACE_SETTINGS_PATH} is larger than ${WORKSPACE_SETTINGS_LIMITS.maxFileBytes / 1024} KiB.`
6848
+ };
6849
+ }
6850
+ async function createWorkspaceThemeFallback(rootDir, warn) {
6851
+ const io = await createNodeWorkspaceIO(rootDir);
6852
+ let lastWarning = null;
6853
+ const report = (message) => {
6854
+ if (message !== null && message !== lastWarning) warn(message);
6855
+ lastWarning = message;
6856
+ };
6857
+ return {
6858
+ async resolve() {
6859
+ const result = await readWorkspaceSettings(io);
6860
+ if (result.status === "missing") {
6861
+ report(null);
6862
+ return void 0;
6863
+ }
6864
+ if (result.status !== "ok") {
6865
+ report(`Ignoring workspace settings. ${describeWorkspaceSettingsProblem(result.message)}`);
6866
+ return void 0;
6867
+ }
6868
+ const themeId = resolveWorkspaceDefaultThemeId(result.settings);
6869
+ if (themeId && !(await getAvailableThemeIds()).includes(themeId)) {
6870
+ report(
6871
+ `Ignoring workspace default theme "${themeId}" from ${WORKSPACE_SETTINGS_PATH}: it is not a known theme.`
6872
+ );
6873
+ return void 0;
6874
+ }
6875
+ report(null);
6876
+ return themeId;
6877
+ }
6878
+ };
6879
+ }
6880
+
6881
+ // src/commands/build.ts
6882
+ import { decodeUtf8Text as decodeUtf8Text2 } from "@bendyline/docblocks/filesystem";
6314
6883
  var DEFAULT_MAX_BUILD_ENTRIES = 1e5;
6315
6884
  var DEFAULT_MAX_BUILD_DEPTH = 64;
6316
6885
  var DEFAULT_MAX_BUILD_INPUT_BYTES = 20 * 1024 * 1024;
@@ -6319,8 +6888,8 @@ var DEFAULT_MAX_BUILD_OUTPUT_BYTES = 128 * 1024 * 1024;
6319
6888
  var DEFAULT_MAX_BUILD_TOTAL_OUTPUT_BYTES = 1024 * 1024 * 1024;
6320
6889
  async function runBuild(opts) {
6321
6890
  await assertKnownThemeId(opts.theme);
6322
- const inputDir = path4.resolve(opts.input);
6323
- const outputDir = path4.resolve(opts.output);
6891
+ const inputDir = path5.resolve(opts.input);
6892
+ const outputDir = path5.resolve(opts.output);
6324
6893
  let inputStat;
6325
6894
  try {
6326
6895
  inputStat = await stat2(inputDir);
@@ -6329,6 +6898,8 @@ async function runBuild(opts) {
6329
6898
  throw new Error(`Input directory not found: ${inputDir}`);
6330
6899
  }
6331
6900
  if (!inputStat.isDirectory()) throw new Error(`Input is not a directory: ${inputDir}`);
6901
+ const warn = opts.onWarning ?? ((message) => console.warn(message));
6902
+ const fallbackThemeId = opts.theme || opts.ignoreWorkspaceSettings ? void 0 : await (await createWorkspaceThemeFallback(inputDir, warn)).resolve();
6332
6903
  const markdownFiles = await listMarkdownFiles(inputDir, {
6333
6904
  maxEntries: positiveLimit(opts.maxEntries, DEFAULT_MAX_BUILD_ENTRIES, "build entry"),
6334
6905
  maxDepth: positiveLimit(opts.maxDepth, DEFAULT_MAX_BUILD_DEPTH, "build depth")
@@ -6343,8 +6914,8 @@ async function runBuild(opts) {
6343
6914
  let totalInputBytes = 0;
6344
6915
  let totalOutputBytes = 0;
6345
6916
  for (const sourcePath of markdownFiles) {
6346
- const relativePath = path4.relative(inputDir, sourcePath);
6347
- const outputPath = path4.join(outputDir, replaceMarkdownExtension(relativePath));
6917
+ const relativePath = path5.relative(inputDir, sourcePath);
6918
+ const outputPath = path5.join(outputDir, replaceMarkdownExtension(relativePath));
6348
6919
  const sourceBytes = await readFile(sourcePath);
6349
6920
  if (sourceBytes.byteLength > byteLimits.maxInputBytes) {
6350
6921
  throw new Error(
@@ -6357,13 +6928,15 @@ async function runBuild(opts) {
6357
6928
  `Build inputs exceed the ${byteLimits.maxTotalInputBytes}-byte aggregate limit.`
6358
6929
  );
6359
6930
  }
6360
- const source = decodeUtf8Text(sourceBytes, { label: "Build input", path: sourcePath });
6931
+ const source = decodeUtf8Text2(sourceBytes, { label: "Build input", path: sourcePath });
6361
6932
  const html = await renderMarkdownHtml(source, {
6362
6933
  title: titleFromPath(relativePath),
6363
6934
  sourcePath,
6364
6935
  assetRoot: inputDir,
6365
6936
  themeId: opts.theme,
6366
- mode: "static"
6937
+ fallbackThemeId,
6938
+ mode: "static",
6939
+ onWarning: warn
6367
6940
  });
6368
6941
  const outputBytes = Buffer.byteLength(html, "utf8");
6369
6942
  if (outputBytes > byteLimits.maxOutputBytes) {
@@ -6377,18 +6950,22 @@ async function runBuild(opts) {
6377
6950
  `Build outputs exceed the ${byteLimits.maxTotalOutputBytes}-byte aggregate limit.`
6378
6951
  );
6379
6952
  }
6380
- await mkdir(path4.dirname(outputPath), { recursive: true });
6953
+ await mkdir(path5.dirname(outputPath), { recursive: true });
6381
6954
  await writeFile(outputPath, html, "utf-8");
6382
6955
  builtFiles.push(outputPath);
6383
6956
  }
6384
6957
  return { inputDir, outputDir, builtFiles };
6385
6958
  }
6386
- var buildCommand = new Command("build").description("Build markdown files into HTML output").option("-i, --input <dir>", "input directory", ".").option("-o, --output <dir>", "output directory", "dist").option("-t, --theme <id>", "Squisq theme ID to apply").option("--max-input-bytes <bytes>", "maximum bytes in one Markdown input").option("--max-total-input-bytes <bytes>", "maximum aggregate Markdown input bytes").option("--max-output-bytes <bytes>", "maximum bytes in one generated HTML file").option("--max-total-output-bytes <bytes>", "maximum aggregate generated HTML bytes").option("--allow-large-build", "disable default byte budgets for a trusted bulk build").action(async (opts) => {
6959
+ var buildCommand = new Command("build").description("Build markdown files into HTML output").option("-i, --input <dir>", "input directory", ".").option("-o, --output <dir>", "output directory", "dist").option("-t, --theme <id>", "Squisq theme ID to apply").option(
6960
+ "--ignore-workspace-settings",
6961
+ "do not apply the default theme from <input>/.docblocks/workspace.json"
6962
+ ).option("--max-input-bytes <bytes>", "maximum bytes in one Markdown input").option("--max-total-input-bytes <bytes>", "maximum aggregate Markdown input bytes").option("--max-output-bytes <bytes>", "maximum bytes in one generated HTML file").option("--max-total-output-bytes <bytes>", "maximum aggregate generated HTML bytes").option("--allow-large-build", "disable default byte budgets for a trusted bulk build").action(async (opts) => {
6387
6963
  try {
6388
6964
  const result = await runBuild({
6389
6965
  input: opts.input,
6390
6966
  output: opts.output,
6391
6967
  theme: opts.theme,
6968
+ ignoreWorkspaceSettings: opts.ignoreWorkspaceSettings,
6392
6969
  maxInputBytes: parseOptionalByteLimit("--max-input-bytes", opts.maxInputBytes),
6393
6970
  maxTotalInputBytes: parseOptionalByteLimit(
6394
6971
  "--max-total-input-bytes",
@@ -6472,7 +7049,7 @@ async function listMarkdownFiles(root, limits) {
6472
7049
  if (entryCount > limits.maxEntries) {
6473
7050
  throw new Error(`Build traversal exceeded ${limits.maxEntries} filesystem entries.`);
6474
7051
  }
6475
- const absolutePath = path4.join(current.directory, entry.name);
7052
+ const absolutePath = path5.join(current.directory, entry.name);
6476
7053
  if (entry.isDirectory()) {
6477
7054
  if (current.depth >= limits.maxDepth) {
6478
7055
  throw new Error(`Build traversal exceeded a depth of ${limits.maxDepth}.`);
@@ -6492,7 +7069,7 @@ function replaceMarkdownExtension(filePath) {
6492
7069
  return filePath.replace(/\.(md|markdown)$/i, ".html");
6493
7070
  }
6494
7071
  function titleFromPath(filePath) {
6495
- const baseName = path4.basename(filePath).replace(/\.(md|markdown)$/i, "");
7072
+ const baseName = path5.basename(filePath).replace(/\.(md|markdown)$/i, "");
6496
7073
  return baseName || "DocBlocks Document";
6497
7074
  }
6498
7075
 
@@ -6501,11 +7078,11 @@ import { Command as Command2 } from "commander";
6501
7078
  import { createServer } from "http";
6502
7079
  import { realpath as realpath2, stat as stat3 } from "fs/promises";
6503
7080
  import { isIP } from "net";
6504
- import path6 from "path";
7081
+ import path7 from "path";
6505
7082
  init_contained_file();
6506
7083
 
6507
7084
  // src/preview-policy.ts
6508
- import path5 from "path";
7085
+ import path6 from "path";
6509
7086
  var PREVIEW_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
6510
7087
  ".css",
6511
7088
  ".gif",
@@ -6551,14 +7128,14 @@ var SENSITIVE_FILE_NAMES = /* @__PURE__ */ new Set([
6551
7128
  "id_rsa"
6552
7129
  ]);
6553
7130
  function isAllowedPreviewPath(rootPath, requestedPath, physicalPath, purpose) {
6554
- const root = path5.resolve(rootPath);
6555
- const requested = path5.resolve(requestedPath);
6556
- const physical = path5.resolve(physicalPath);
7131
+ const root = path6.resolve(rootPath);
7132
+ const requested = path6.resolve(requestedPath);
7133
+ const physical = path6.resolve(physicalPath);
6557
7134
  for (const candidate of /* @__PURE__ */ new Set([requested, physical])) {
6558
- const relative = path5.relative(root, candidate);
7135
+ const relative = path6.relative(root, candidate);
6559
7136
  if (!isContainedRelativePath(relative) || containsSensitiveSegment(relative)) return false;
6560
7137
  if (purpose !== "directory") {
6561
- const extension = path5.extname(path5.basename(candidate)).toLowerCase();
7138
+ const extension = path6.extname(path6.basename(candidate)).toLowerCase();
6562
7139
  const allowedExtensions = purpose === "embedded-image" ? EMBEDDED_IMAGE_EXTENSIONS : PREVIEW_FILE_EXTENSIONS;
6563
7140
  if (!allowedExtensions.has(extension)) return false;
6564
7141
  }
@@ -6568,15 +7145,15 @@ function isAllowedPreviewPath(rootPath, requestedPath, physicalPath, purpose) {
6568
7145
  function containsSensitiveSegment(relativePath) {
6569
7146
  return relativePath.split(/[\\/]+/).filter(Boolean).some((segment) => {
6570
7147
  const lower = segment.toLowerCase();
6571
- return lower.startsWith(".") || SENSITIVE_FILE_NAMES.has(lower) || SENSITIVE_FILE_EXTENSIONS.has(path5.extname(lower));
7148
+ return lower.startsWith(".") || SENSITIVE_FILE_NAMES.has(lower) || SENSITIVE_FILE_EXTENSIONS.has(path6.extname(lower));
6572
7149
  });
6573
7150
  }
6574
7151
  function isContainedRelativePath(relativePath) {
6575
- return relativePath === "" || !relativePath.startsWith(`..${path5.sep}`) && relativePath !== ".." && !path5.isAbsolute(relativePath);
7152
+ return relativePath === "" || !relativePath.startsWith(`..${path6.sep}`) && relativePath !== ".." && !path6.isAbsolute(relativePath);
6576
7153
  }
6577
7154
 
6578
7155
  // src/commands/serve.ts
6579
- import { decodeUtf8Text as decodeUtf8Text2 } from "@bendyline/docblocks/filesystem";
7156
+ import { decodeUtf8Text as decodeUtf8Text3 } from "@bendyline/docblocks/filesystem";
6580
7157
  init_node_error();
6581
7158
  var DEFAULT_HOST = "127.0.0.1";
6582
7159
  var DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
@@ -6584,7 +7161,7 @@ var DEFAULT_MAX_CONCURRENT_READS = 16;
6584
7161
  var MAX_ALLOWED_HOSTS = 32;
6585
7162
  async function startPreviewServer(opts) {
6586
7163
  await assertKnownThemeId(opts.theme);
6587
- const requestedRoot = path6.resolve(opts.dir);
7164
+ const requestedRoot = path7.resolve(opts.dir);
6588
7165
  let root;
6589
7166
  try {
6590
7167
  root = await realpath2(requestedRoot);
@@ -6595,6 +7172,8 @@ async function startPreviewServer(opts) {
6595
7172
  const rootStat = await stat3(root);
6596
7173
  if (!rootStat.isDirectory()) throw new Error(`Preview root is not a directory: ${root}`);
6597
7174
  const host = validateHost(opts.host ?? DEFAULT_HOST, opts.allowNetwork === true);
7175
+ const warn = opts.onWarning ?? ((message) => console.warn(message));
7176
+ const themeFallback = opts.theme || opts.ignoreWorkspaceSettings ? null : await createWorkspaceThemeFallback(root, warn);
6598
7177
  const allowedHosts = validateAllowedHosts(opts.allowedHosts ?? []);
6599
7178
  const maxFileBytes = positiveLimit(
6600
7179
  opts.maxFileBytes,
@@ -6624,7 +7203,7 @@ async function startPreviewServer(opts) {
6624
7203
  return;
6625
7204
  }
6626
7205
  activeRequests += 1;
6627
- handlePreviewRequest(req, res, root, opts.theme, maxFileBytes).catch((error) => {
7206
+ handlePreviewRequest(req, res, root, { themeId: opts.theme, themeFallback, warn }, maxFileBytes).catch((error) => {
6628
7207
  reportPreviewFailure(req, error);
6629
7208
  if (!res.headersSent) sendText(res, 500, "Internal server error", req.method === "HEAD");
6630
7209
  else res.end();
@@ -6643,13 +7222,17 @@ async function startPreviewServer(opts) {
6643
7222
  const port = typeof address === "object" && address ? address.port : opts.port;
6644
7223
  return { server, root, host, url: `http://${urlHost(host)}:${port}/` };
6645
7224
  }
6646
- var serveCommand = new Command2("serve").description("Start a local dev server for previewing markdown files").option("-p, --port <port>", "port to listen on", "3000").option("-d, --dir <dir>", "directory to serve", ".").option("-t, --theme <id>", "Squisq theme ID to apply").option("--host <host>", "interface to bind (loopback by default)", DEFAULT_HOST).option("--allow-network", "allow a non-loopback --host").option("--allow-host <host...>", "additional Host header names this server answers to").action(
7225
+ var serveCommand = new Command2("serve").description("Start a local dev server for previewing markdown files").option("-p, --port <port>", "port to listen on", "3000").option("-d, --dir <dir>", "directory to serve", ".").option("-t, --theme <id>", "Squisq theme ID to apply").option(
7226
+ "--ignore-workspace-settings",
7227
+ "do not apply the default theme from <dir>/.docblocks/workspace.json"
7228
+ ).option("--host <host>", "interface to bind (loopback by default)", DEFAULT_HOST).option("--allow-network", "allow a non-loopback --host").option("--allow-host <host...>", "additional Host header names this server answers to").action(
6647
7229
  async (opts) => {
6648
7230
  try {
6649
7231
  const server = await startPreviewServer({
6650
7232
  port: parsePort(opts.port),
6651
7233
  dir: opts.dir,
6652
7234
  theme: opts.theme,
7235
+ ignoreWorkspaceSettings: opts.ignoreWorkspaceSettings,
6653
7236
  host: opts.host,
6654
7237
  allowNetwork: opts.allowNetwork,
6655
7238
  allowedHosts: opts.allowHost
@@ -6662,7 +7245,7 @@ var serveCommand = new Command2("serve").description("Start a local dev server f
6662
7245
  }
6663
7246
  }
6664
7247
  );
6665
- async function handlePreviewRequest(req, res, root, themeId, maxFileBytes) {
7248
+ async function handlePreviewRequest(req, res, root, render, maxFileBytes) {
6666
7249
  if (req.method !== "GET" && req.method !== "HEAD") {
6667
7250
  sendText(res, 405, "Method not allowed");
6668
7251
  return;
@@ -6686,16 +7269,18 @@ async function handlePreviewRequest(req, res, root, themeId, maxFileBytes) {
6686
7269
  return;
6687
7270
  }
6688
7271
  if (target.markdown) {
6689
- const markdown = decodeUtf8Text2(await readContainedFile(root, target.filePath, maxFileBytes), {
7272
+ const markdown = decodeUtf8Text3(await readContainedFile(root, target.filePath, maxFileBytes), {
6690
7273
  label: "Preview document",
6691
7274
  path: target.filePath
6692
7275
  });
6693
7276
  const html = await renderMarkdownHtml(markdown, {
6694
- title: path6.basename(target.filePath).replace(/\.(md|markdown)$/i, ""),
7277
+ title: path7.basename(target.filePath).replace(/\.(md|markdown)$/i, ""),
6695
7278
  sourcePath: target.filePath,
6696
7279
  assetRoot: root,
6697
- themeId,
7280
+ themeId: render.themeId,
7281
+ fallbackThemeId: render.themeFallback ? await render.themeFallback.resolve() : void 0,
6698
7282
  mode: "static",
7283
+ onWarning: render.warn,
6699
7284
  allowReferencedAsset: ({ assetRoot, requestedPath, physicalPath }) => isAllowedPreviewPath(assetRoot, requestedPath, physicalPath, "embedded-image")
6700
7285
  });
6701
7286
  sendBody(res, 200, html, "text/html; charset=utf-8", headOnly);
@@ -6708,7 +7293,7 @@ async function resolveServeTarget(root, requestUrl, maxFileBytes = DEFAULT_MAX_F
6708
7293
  if (requestUrl.length > 8192 || requestUrl.includes("\0")) return { kind: "bad-request" };
6709
7294
  let physicalRoot;
6710
7295
  try {
6711
- physicalRoot = await realpath2(path6.resolve(root));
7296
+ physicalRoot = await realpath2(path7.resolve(root));
6712
7297
  } catch (error) {
6713
7298
  if (isMissingPathError2(error)) return { kind: "missing" };
6714
7299
  if (isPermissionError(error)) return { kind: "forbidden" };
@@ -6717,7 +7302,7 @@ async function resolveServeTarget(root, requestUrl, maxFileBytes = DEFAULT_MAX_F
6717
7302
  const decodedPath = decodeRequestPath(requestUrl);
6718
7303
  if (!decodedPath) return { kind: "bad-request" };
6719
7304
  const requestedPath = decodedPath.replace(/^\/+/, "");
6720
- const candidate = path6.resolve(physicalRoot, requestedPath);
7305
+ const candidate = path7.resolve(physicalRoot, requestedPath);
6721
7306
  if (!isAllowedPreviewPath(physicalRoot, candidate, candidate, "directory")) {
6722
7307
  return { kind: "forbidden" };
6723
7308
  }
@@ -6777,7 +7362,7 @@ async function inspectTarget(root, candidate, maxFileBytes) {
6777
7362
  }
6778
7363
  async function directoryIndexTarget(root, dirPath, maxFileBytes) {
6779
7364
  for (const fileName of ["index.html", "index.htm", "index.md", "index.markdown"]) {
6780
- const target = await inspectTarget(root, path6.join(dirPath, fileName), maxFileBytes);
7365
+ const target = await inspectTarget(root, path7.join(dirPath, fileName), maxFileBytes);
6781
7366
  if (target.kind === "file" || target.kind === "forbidden" || target.kind === "too-large") {
6782
7367
  return target;
6783
7368
  }
@@ -6912,7 +7497,7 @@ function urlHost(host) {
6912
7497
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
6913
7498
  }
6914
7499
  function contentTypeFor(filePath) {
6915
- const ext = path6.extname(filePath).toLowerCase();
7500
+ const ext = path7.extname(filePath).toLowerCase();
6916
7501
  switch (ext) {
6917
7502
  case ".html":
6918
7503
  case ".htm":
@@ -6979,8 +7564,8 @@ import {
6979
7564
  rm,
6980
7565
  stat as stat4
6981
7566
  } from "fs/promises";
6982
- import { randomUUID } from "crypto";
6983
- import { dirname, basename, extname, join, resolve } from "path";
7567
+ import { randomUUID as randomUUID2 } from "crypto";
7568
+ import { dirname, basename as basename2, extname, join, resolve } from "path";
6984
7569
  import { Command as Command3 } from "commander";
6985
7570
  init_link_support();
6986
7571
  init_node_error();
@@ -7057,7 +7642,7 @@ async function runConvert(inputPath, opts) {
7057
7642
  throwIfAborted2(opts.signal);
7058
7643
  const resolvedInput = resolve(inputPath);
7059
7644
  const outputDir = opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput);
7060
- const inputBasename = basename(resolvedInput);
7645
+ const inputBasename = basename2(resolvedInput);
7061
7646
  const inputExt = extname(inputBasename);
7062
7647
  const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
7063
7648
  const maxInputBytes = positiveLimit(
@@ -7199,7 +7784,7 @@ async function runConvert(inputPath, opts) {
7199
7784
  if (totalOutputBytes > maxTotalOutputBytes) {
7200
7785
  throw new Error(`Conversion outputs exceed the ${maxTotalOutputBytes}-byte aggregate limit.`);
7201
7786
  }
7202
- const suggestedFilename = basename(conversion.suggestedFilename);
7787
+ const suggestedFilename = basename2(conversion.suggestedFilename);
7203
7788
  const outPath = join(outputDir, suggestedFilename);
7204
7789
  const outputPathKey = process.platform === "win32" ? outPath.toLowerCase() : outPath;
7205
7790
  if (outputPathKeys.has(outputPathKey)) {
@@ -7315,7 +7900,7 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
7315
7900
  throwIfAborted2(signal);
7316
7901
  const output = outputs[index];
7317
7902
  if (!output) continue;
7318
- const stagedPath = join(stagingDirectory, `${index}-${randomUUID()}.stage`);
7903
+ const stagedPath = join(stagingDirectory, `${index}-${randomUUID2()}.stage`);
7319
7904
  await writePrivateFile(stagedPath, output.bytes);
7320
7905
  stagedOutputs.push({ outputPath: output.path, stagedPath, bytes: output.bytes });
7321
7906
  }
@@ -7345,7 +7930,7 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
7345
7930
  throw new Error(`Refusing to replace a non-file destination: ${output.outputPath}`);
7346
7931
  }
7347
7932
  if (existing) {
7348
- state.backupPath = join(stagingDirectory, `${index}-${randomUUID()}.backup`);
7933
+ state.backupPath = join(stagingDirectory, `${index}-${randomUUID2()}.backup`);
7349
7934
  await rename(output.outputPath, state.backupPath);
7350
7935
  }
7351
7936
  await rename(output.stagedPath, output.outputPath);
@@ -7373,8 +7958,8 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
7373
7958
  }
7374
7959
  }
7375
7960
  }
7376
- async function writePrivateFile(path9, bytes) {
7377
- const handle = await open2(path9, "wx", 384);
7961
+ async function writePrivateFile(path10, bytes) {
7962
+ const handle = await open2(path10, "wx", 384);
7378
7963
  try {
7379
7964
  await handle.writeFile(bytes);
7380
7965
  await handle.sync();
@@ -7433,9 +8018,9 @@ async function assertReplaceableOutputs(outputPaths) {
7433
8018
  }
7434
8019
  }
7435
8020
  }
7436
- async function lstatIfPresent(path9) {
8021
+ async function lstatIfPresent(path10) {
7437
8022
  try {
7438
- return await lstat(path9);
8023
+ return await lstat(path10);
7439
8024
  } catch (error) {
7440
8025
  if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
7441
8026
  throw error;
@@ -7454,11 +8039,11 @@ function throwIfAborted2(signal) {
7454
8039
  }
7455
8040
  async function applyTransformToMarkdown(markdownDoc, container, transformStyle, themeId) {
7456
8041
  const { markdownToDoc: markdownToDoc3, docToMarkdown } = await import("@bendyline/squisq/doc");
7457
- const { readFrontmatterThemeId } = await import("@bendyline/squisq/markdown");
8042
+ const { readFrontmatterThemeId: readFrontmatterThemeId2 } = await import("@bendyline/squisq/markdown");
7458
8043
  const { applyTransform, extractDocImages } = await import("@bendyline/squisq/transform");
7459
8044
  const doc = markdownToDoc3(markdownDoc);
7460
8045
  const images = extractDocImages(doc.blocks);
7461
- const effectiveTheme = themeId ?? readFrontmatterThemeId(markdownDoc.frontmatter);
8046
+ const effectiveTheme = themeId ?? readFrontmatterThemeId2(markdownDoc.frontmatter);
7462
8047
  const result = applyTransform(doc, transformStyle, { themeId: effectiveTheme, images });
7463
8048
  const projected = projectTransformOntoMarkdownSource(doc, result.doc);
7464
8049
  const transformedMarkdown = docToMarkdown(projected);
@@ -7649,7 +8234,7 @@ function isConversionErrorMetadata(error) {
7649
8234
  // src/commands/video.ts
7650
8235
  init_cancellation();
7651
8236
  import { lstat as lstat2, mkdir as mkdir3 } from "fs/promises";
7652
- import { dirname as dirname2, basename as basename2, extname as extname2, resolve as resolve2 } from "path";
8237
+ import { dirname as dirname2, basename as basename3, extname as extname2, resolve as resolve2 } from "path";
7653
8238
  import { Command as Command4 } from "commander";
7654
8239
  import {
7655
8240
  resolveDimensions,
@@ -7691,7 +8276,7 @@ async function runVideo(inputPath, opts, onProgress, dependencies = {}) {
7691
8276
  width: opts.width,
7692
8277
  height: opts.height
7693
8278
  });
7694
- const inputBasename = basename2(resolvedInput);
8279
+ const inputBasename = basename3(resolvedInput);
7695
8280
  const inputExt = extname2(inputBasename);
7696
8281
  const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
7697
8282
  const outputPath = opts.output ? resolve2(opts.output) : resolve2(dirname2(resolvedInput), `${baseName}.mp4`);
@@ -7982,7 +8567,7 @@ var transformsCommand = new Command7("transforms").description("List all availab
7982
8567
  import { open as open5 } from "fs/promises";
7983
8568
  import { resolve as resolve3 } from "path";
7984
8569
  import { Command as Command8 } from "commander";
7985
- import { decodeUtf8Text as decodeUtf8Text4 } from "@bendyline/docblocks/filesystem";
8570
+ import { decodeUtf8Text as decodeUtf8Text5 } from "@bendyline/docblocks/filesystem";
7986
8571
  init_node_error();
7987
8572
  var DEFAULT_MAX_PARSE_INPUT_BYTES = 20 * 1024 * 1024;
7988
8573
  var DEFAULT_MAX_PARSE_OUTPUT_BYTES = 128 * 1024 * 1024;
@@ -7999,7 +8584,7 @@ async function runParse(inputPath, options = {}) {
7999
8584
  "parse output byte"
8000
8585
  );
8001
8586
  const bytes = await readBoundedFile(resolvedInput, maxInputBytes);
8002
- const content = decodeUtf8Text4(bytes, { label: "Parse input", path: resolvedInput });
8587
+ const content = decodeUtf8Text5(bytes, { label: "Parse input", path: resolvedInput });
8003
8588
  const { parseMarkdown: parseMarkdown2 } = await import("@bendyline/squisq/markdown");
8004
8589
  const markdownDoc = parseMarkdown2(content);
8005
8590
  const stats = {
@@ -8062,9 +8647,95 @@ var parseCommand = new Command8("parse").description("Parse a markdown file and
8062
8647
  }
8063
8648
  });
8064
8649
 
8650
+ // src/commands/workspace.ts
8651
+ import { Command as Command9 } from "commander";
8652
+ import { FsError as FsError2 } from "@bendyline/docblocks/filesystem";
8653
+ import {
8654
+ WORKSPACE_SETTINGS_PATH as WORKSPACE_SETTINGS_PATH2,
8655
+ refreshWorkspaceOutputs,
8656
+ resolveCatalogOutputs
8657
+ } from "@bendyline/docblocks/workspace-settings";
8658
+ import { htmlToMarkdownDocSync, markdownDocToPlainHtml } from "@bendyline/squisq-formats/html";
8659
+ async function runWorkspaceRefresh(options = {}) {
8660
+ const io = await createNodeWorkspaceIO(options.dir ?? ".");
8661
+ const dryRun = options.dryRun === true;
8662
+ const settingsRead = await readWorkspaceSettings(io);
8663
+ if (settingsRead.status === "missing") {
8664
+ return { root: io.root, status: "missing", documentCount: 0, outputs: [], dryRun };
8665
+ }
8666
+ if (settingsRead.status !== "ok") {
8667
+ throw new Error(describeWorkspaceSettingsProblem(settingsRead.message));
8668
+ }
8669
+ const settings = settingsRead.settings;
8670
+ const resolved = resolveCatalogOutputs(settings);
8671
+ if (!resolved) {
8672
+ return { root: io.root, status: "disabled", documentCount: 0, outputs: [], dryRun };
8673
+ }
8674
+ const warn = options.onWarning ?? ((message) => console.warn(message));
8675
+ const catalogThemeId = resolved.html?.themeId;
8676
+ if (catalogThemeId && !(await getAvailableThemeIds()).includes(catalogThemeId)) {
8677
+ warn(
8678
+ `Catalog theme "${catalogThemeId}" from ${WORKSPACE_SETTINGS_PATH2} is not a known theme; the page renders with the standard theme.`
8679
+ );
8680
+ }
8681
+ const refresh = () => refreshWorkspaceOutputs(io, settings, renderer, {
8682
+ signal: options.signal,
8683
+ force: options.force === true,
8684
+ dryRun,
8685
+ limits: options.limits
8686
+ });
8687
+ let result;
8688
+ try {
8689
+ result = await refresh();
8690
+ } catch (error) {
8691
+ if (!(error instanceof FsError2) || error.code !== "conflict") throw error;
8692
+ result = await refresh();
8693
+ }
8694
+ return {
8695
+ root: io.root,
8696
+ status: "refreshed",
8697
+ documentCount: result.documentCount,
8698
+ outputs: result.outputs,
8699
+ dryRun
8700
+ };
8701
+ }
8702
+ var renderer = {
8703
+ renderCatalogHtml: (document2, { title, themeId }) => markdownDocToPlainHtml(document2, { title, themeId }),
8704
+ importHtml: (html) => htmlToMarkdownDocSync(html)
8705
+ };
8706
+ function describeWorkspaceOutput(output) {
8707
+ const status = output.status === "would-write" ? "would write" : output.status;
8708
+ return output.message ? `${status} ${output.path}: ${output.message}` : `${status} ${output.path}`;
8709
+ }
8710
+ var refreshCommand = new Command9("refresh").description(`Regenerate the catalog outputs enabled in <dir>/${WORKSPACE_SETTINGS_PATH2}`).argument("[dir]", "workspace root", ".").option("--force", "rewrite the catalog page even when its inputs are unchanged").option("--dry-run", "report what would change without writing anything").action(async (dir, opts) => {
8711
+ try {
8712
+ const result = await runWorkspaceRefresh({ dir, force: opts.force, dryRun: opts.dryRun });
8713
+ if (result.status === "missing") {
8714
+ console.error(
8715
+ `No workspace settings found at ${WORKSPACE_SETTINGS_PATH2} in ${result.root}; no outputs are enabled.`
8716
+ );
8717
+ return;
8718
+ }
8719
+ if (result.status === "disabled") {
8720
+ console.error(`${WORKSPACE_SETTINGS_PATH2} enables no catalog outputs; nothing to refresh.`);
8721
+ return;
8722
+ }
8723
+ for (const output of result.outputs) console.error(describeWorkspaceOutput(output));
8724
+ console.error(
8725
+ `${result.dryRun ? "Dry run: " : ""}${result.documentCount} document(s) cataloged in ${result.root}.`
8726
+ );
8727
+ if (result.outputs.some((output) => output.status === "blocked")) process.exitCode = 1;
8728
+ } catch (err) {
8729
+ const message = err instanceof Error ? err.message : String(err);
8730
+ console.error(`Error: ${message}`);
8731
+ process.exitCode = 1;
8732
+ }
8733
+ });
8734
+ var workspaceCommand = new Command9("workspace").description(`Maintain workspace outputs configured in ${WORKSPACE_SETTINGS_PATH2}`).addCommand(refreshCommand);
8735
+
8065
8736
  // src/program.ts
8066
8737
  init_version();
8067
- var program = new Command9().name("docblocks").description("Build, preview, convert, render, inspect, and automate documents").version(getPackageVersion()).addCommand(buildCommand).addCommand(serveCommand).addCommand(convertCommand).addCommand(videoCommand).addCommand(mcpCommand).addCommand(themesCommand).addCommand(transformsCommand).addCommand(parseCommand);
8738
+ var program = new Command10().name("docblocks").description("Build, preview, convert, render, inspect, and automate documents").version(getPackageVersion()).addCommand(buildCommand).addCommand(serveCommand).addCommand(convertCommand).addCommand(videoCommand).addCommand(mcpCommand).addCommand(themesCommand).addCommand(transformsCommand).addCommand(parseCommand).addCommand(workspaceCommand);
8068
8739
 
8069
8740
  // src/bin.ts
8070
8741
  program.parseAsync().catch((error) => {