@bendyline/docblocks-cli 2.6.1 → 2.7.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.
package/dist/bin.js CHANGED
@@ -1,8 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  var __defProp = Object.defineProperty;
3
3
  var __getOwnPropNames = Object.getOwnPropertyNames;
4
- var __esm = (fn, res) => function __init() {
5
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
4
+ var __esm = (fn, res, err) => function __init() {
5
+ if (err) throw err[0];
6
+ try {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ } catch (e) {
9
+ throw err = [e], e;
10
+ }
6
11
  };
7
12
  var __export = (target, all) => {
8
13
  for (var name in all)
@@ -161,7 +166,7 @@ var init_cancellation = __esm({
161
166
  });
162
167
 
163
168
  // src/mcp/artifact-store.ts
164
- import { createHash, randomUUID as randomUUID2 } from "crypto";
169
+ import { createHash, randomUUID as randomUUID3 } from "crypto";
165
170
  import { mkdtemp as mkdtemp2, open as open3, rm as rm2 } from "fs/promises";
166
171
  import { join as join2 } from "path";
167
172
  import { tmpdir } from "os";
@@ -211,9 +216,9 @@ function immutableArtifactRef(ref) {
211
216
  );
212
217
  return Object.freeze({ ...ref, appliedOptions, engineVersions });
213
218
  }
214
- async function writeFileCancellable(path9, bytes, signal) {
219
+ async function writeFileCancellable(path10, bytes, signal) {
215
220
  throwIfAborted4(signal);
216
- const handle = await open3(path9, "wx");
221
+ const handle = await open3(path10, "wx");
217
222
  try {
218
223
  let offset = 0;
219
224
  while (offset < bytes.byteLength) {
@@ -228,9 +233,9 @@ async function writeFileCancellable(path9, bytes, signal) {
228
233
  await handle.close();
229
234
  }
230
235
  }
231
- async function readFileCancellable(path9, options) {
236
+ async function readFileCancellable(path10, options) {
232
237
  throwIfAborted4(options.signal);
233
- const handle = await open3(path9, "r");
238
+ const handle = await open3(path10, "r");
234
239
  try {
235
240
  const stats = await handle.stat();
236
241
  throwIfAborted4(options.signal);
@@ -270,7 +275,7 @@ async function hashBytesCancellable(bytes, signal) {
270
275
  return hash.digest("hex");
271
276
  }
272
277
  async function removeFilesBestEffort(paths) {
273
- await Promise.allSettled(paths.map((path9) => rm2(path9, { force: true })));
278
+ await Promise.allSettled(paths.map((path10) => rm2(path10, { force: true })));
274
279
  }
275
280
  async function waitForIoDrain(leases, timeoutMs) {
276
281
  if (leases.length === 0) return;
@@ -383,8 +388,8 @@ var init_artifact_store = __esm({
383
388
  throw new Error("MCP artifact exceeds the configured per-artifact limit");
384
389
  }
385
390
  const sha2562 = await hashBytesCancellable(bytes, signal);
386
- const id = randomUUID2();
387
- const path9 = join2(await this.directory, `${id}.artifact`);
391
+ const id = randomUUID3();
392
+ const path10 = join2(await this.directory, `${id}.artifact`);
388
393
  throwIfAborted4(signal);
389
394
  const created = /* @__PURE__ */ new Date();
390
395
  const ref = immutableArtifactRef({
@@ -419,7 +424,7 @@ var init_artifact_store = __esm({
419
424
  });
420
425
  await removeFilesBestEffort(acquisition.expiredPaths);
421
426
  try {
422
- await this.dependencies.writeFile(path9, bytes, acquisition.io.controller.signal);
427
+ await this.dependencies.writeFile(path10, bytes, acquisition.io.controller.signal);
423
428
  throwIfAborted4(acquisition.io.controller.signal);
424
429
  await this.lock(() => {
425
430
  this.assertActive();
@@ -427,7 +432,7 @@ var init_artifact_store = __esm({
427
432
  this.releaseReservationLocked(reservation);
428
433
  this.records.set(id, {
429
434
  ref,
430
- path: path9,
435
+ path: path10,
431
436
  conversionReport: null,
432
437
  reportBytes: 0,
433
438
  activeReaders: 0,
@@ -438,7 +443,7 @@ var init_artifact_store = __esm({
438
443
  return ref;
439
444
  } catch (caught) {
440
445
  await this.lock(() => this.releaseReservationLocked(reservation));
441
- await rm2(path9, { force: true }).catch(() => void 0);
446
+ await rm2(path10, { force: true }).catch(() => void 0);
442
447
  throw caught;
443
448
  } finally {
444
449
  this.completeIoLease(acquisition.io);
@@ -551,8 +556,8 @@ var init_artifact_store = __esm({
551
556
  for (const [id, stored] of this.records) {
552
557
  const expiresAt = stored.ref.expiresAt ? Date.parse(stored.ref.expiresAt) : Number.POSITIVE_INFINITY;
553
558
  if (expiresAt > now) continue;
554
- const path9 = this.removeRecordLocked(id, stored);
555
- if (path9) paths.push(path9);
559
+ const path10 = this.removeRecordLocked(id, stored);
560
+ if (path10) paths.push(path10);
556
561
  }
557
562
  return paths;
558
563
  }
@@ -692,8 +697,8 @@ var init_artifact_store = __esm({
692
697
 
693
698
  // src/mcp/authority.ts
694
699
  import { lstat as lstat3, link as link2, open as open4, realpath as realpath4, rename as rename2, stat as stat5, unlink } from "fs/promises";
695
- import path7 from "path";
696
- import { createHash as createHash2, randomUUID as randomUUID3 } from "crypto";
700
+ import path8 from "path";
701
+ import { createHash as createHash2, randomUUID as randomUUID4 } from "crypto";
697
702
  import { MCP_WIRE_LIMITS as MCP_WIRE_LIMITS2 } from "@bendyline/docblocks/mcp";
698
703
  async function resolveRoots(values) {
699
704
  const roots = [];
@@ -713,7 +718,7 @@ function rootIdFor(physicalPath) {
713
718
  return `root-${createHash2("sha256").update(normalized).digest("hex").slice(0, 16)}`;
714
719
  }
715
720
  function rootLabel(physicalPath) {
716
- const candidate = path7.basename(physicalPath) || "root";
721
+ const candidate = path8.basename(physicalPath) || "root";
717
722
  let label = "";
718
723
  for (const character of candidate) {
719
724
  const codePoint = character.codePointAt(0) ?? 0;
@@ -724,7 +729,7 @@ function rootLabel(physicalPath) {
724
729
  return label || "root";
725
730
  }
726
731
  function parseRootRelativePath(value) {
727
- 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)) {
728
733
  throw new Error("Invalid MCP root-relative path");
729
734
  }
730
735
  const segments = value.split("/");
@@ -742,7 +747,7 @@ async function nearestExistingPath(candidate) {
742
747
  await lstat3(current);
743
748
  return current;
744
749
  } catch {
745
- const parent = path7.dirname(current);
750
+ const parent = path8.dirname(current);
746
751
  if (parent === current) throw new Error("MCP output has no accessible parent directory");
747
752
  current = parent;
748
753
  }
@@ -752,7 +757,7 @@ function validatePath(value) {
752
757
  if (!value || value.length > MAX_PATH_CHARACTERS || value.includes("\0")) {
753
758
  throw new Error("Invalid MCP filesystem path");
754
759
  }
755
- return path7.resolve(value);
760
+ return path8.resolve(value);
756
761
  }
757
762
  function validatePortableFilename(value) {
758
763
  const stem = value.split(".", 1)[0]?.toLowerCase() ?? "";
@@ -769,7 +774,7 @@ function hasUnsafeFilenameCharacter(value) {
769
774
  }
770
775
  function samePath(left, right) {
771
776
  const normalize = (value) => {
772
- const resolved = path7.resolve(value);
777
+ const resolved = path8.resolve(value);
773
778
  return process.platform === "win32" ? resolved.toLowerCase() : resolved;
774
779
  };
775
780
  return normalize(left) === normalize(right);
@@ -831,7 +836,7 @@ async function hashFileBounded(filePath, maximumBytes, signal, afterChunk) {
831
836
  }
832
837
  }
833
838
  async function readMaterializationParentIdentity(target, writeRoots) {
834
- const parent = path7.dirname(target);
839
+ const parent = path8.dirname(target);
835
840
  let lexical;
836
841
  let physical;
837
842
  let info;
@@ -975,7 +980,7 @@ var init_authority = __esm({
975
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."
976
981
  );
977
982
  }
978
- const candidate = path7.join(root.physical, ...parseRootRelativePath(workspacePath));
983
+ const candidate = path8.join(root.physical, ...parseRootRelativePath(workspacePath));
979
984
  const physical = await realpath4(candidate).catch(() => null);
980
985
  if (!physical || !isPathInside(root.physical, physical)) {
981
986
  throw new Error("MCP read path is outside the selected root");
@@ -991,9 +996,9 @@ var init_authority = __esm({
991
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."
992
997
  );
993
998
  }
994
- const candidate = path7.join(root.physical, ...parseRootRelativePath(workspacePath));
999
+ const candidate = path8.join(root.physical, ...parseRootRelativePath(workspacePath));
995
1000
  const authorized = await this.authorizeWrite(candidate);
996
- const physicalParent = await realpath4(path7.dirname(authorized));
1001
+ const physicalParent = await realpath4(path8.dirname(authorized));
997
1002
  if (!isPathInside(root.physical, physicalParent)) {
998
1003
  throw new Error("MCP write path is outside the selected root");
999
1004
  }
@@ -1012,7 +1017,7 @@ var init_authority = __esm({
1012
1017
  }
1013
1018
  async authorizeWrite(requestedPath) {
1014
1019
  const candidate = validatePath(requestedPath);
1015
- const filename = path7.basename(candidate);
1020
+ const filename = path8.basename(candidate);
1016
1021
  validatePortableFilename(filename);
1017
1022
  const existing = await lstat3(candidate).catch(() => null);
1018
1023
  if (existing?.isSymbolicLink()) {
@@ -1021,16 +1026,16 @@ var init_authority = __esm({
1021
1026
  if (existing && !existing.isFile()) {
1022
1027
  throw new Error("MCP output target must be a regular file");
1023
1028
  }
1024
- const nearest = await nearestExistingPath(existing ? candidate : path7.dirname(candidate));
1029
+ const nearest = await nearestExistingPath(existing ? candidate : path8.dirname(candidate));
1025
1030
  const physicalTarget = existing ? await realpath4(candidate) : null;
1026
- const physicalParent = physicalTarget ? path7.dirname(physicalTarget) : await realpath4(nearest);
1031
+ const physicalParent = physicalTarget ? path8.dirname(physicalTarget) : await realpath4(nearest);
1027
1032
  if (!this.writeRoots.some((root) => isPathInside(root.physical, physicalParent))) {
1028
1033
  throw new Error("MCP write path is outside the configured roots");
1029
1034
  }
1030
- if (!existing && !samePath(nearest, path7.dirname(candidate))) {
1035
+ if (!existing && !samePath(nearest, path8.dirname(candidate))) {
1031
1036
  throw new Error("MCP output parent directory does not exist");
1032
1037
  }
1033
- return physicalTarget ?? path7.join(physicalParent, filename);
1038
+ return physicalTarget ?? path8.join(physicalParent, filename);
1034
1039
  }
1035
1040
  /** Publish bounded bytes through an operation-owned sibling file. */
1036
1041
  async materializeBytes(requestedPath, content, maximumBytes, options = {}) {
@@ -1074,9 +1079,9 @@ var init_authority = __esm({
1074
1079
  );
1075
1080
  if (actual !== expectedSha256) throw new Error("MCP output hash precondition failed");
1076
1081
  }
1077
- const temporary = path7.join(
1078
- path7.dirname(target),
1079
- `.${path7.basename(target)}.${process.pid}.${randomUUID3()}.tmp`
1082
+ const temporary = path8.join(
1083
+ path8.dirname(target),
1084
+ `.${path8.basename(target)}.${process.pid}.${randomUUID4()}.tmp`
1080
1085
  );
1081
1086
  let handle = null;
1082
1087
  try {
@@ -1176,7 +1181,7 @@ import { createHash as createHash3 } from "crypto";
1176
1181
  import { createRequire } from "module";
1177
1182
  import { readFileSync } from "fs";
1178
1183
  import { readdir, readFile as readFile2, stat as stat6 } from "fs/promises";
1179
- import path8 from "path";
1184
+ import path9 from "path";
1180
1185
  function getPackageVersion() {
1181
1186
  const pkg = readPackageJson();
1182
1187
  if (typeof pkg?.version === "string" && pkg.version.length > 0) {
@@ -1212,10 +1217,10 @@ async function computeRuntimeVersion(packageName, resolutionSpecifier) {
1212
1217
  try {
1213
1218
  const entry = require2.resolve(resolutionSpecifier);
1214
1219
  const root = await findPackageRoot(entry, packageName);
1215
- const files = await collectRuntimeFiles(path8.join(root, "dist"));
1220
+ const files = await collectRuntimeFiles(path9.join(root, "dist"));
1216
1221
  const hash = createHash3("sha256");
1217
1222
  for (const file of files) {
1218
- const relative = path8.relative(root, file).split(path8.sep).join("/");
1223
+ const relative = path9.relative(root, file).split(path9.sep).join("/");
1219
1224
  hash.update(relative);
1220
1225
  hash.update("\0");
1221
1226
  hash.update(await readFile2(file));
@@ -1235,16 +1240,16 @@ function readPackageJson() {
1235
1240
  }
1236
1241
  }
1237
1242
  async function findPackageRoot(entry, packageName) {
1238
- let directory = path8.dirname(entry);
1243
+ let directory = path9.dirname(entry);
1239
1244
  for (let depth = 0; depth < 12; depth += 1) {
1240
1245
  try {
1241
1246
  const parsed = JSON.parse(
1242
- await readFile2(path8.join(directory, "package.json"), "utf8")
1247
+ await readFile2(path9.join(directory, "package.json"), "utf8")
1243
1248
  );
1244
1249
  if (parsed.name === packageName) return directory;
1245
1250
  } catch {
1246
1251
  }
1247
- const parent = path8.dirname(directory);
1252
+ const parent = path9.dirname(directory);
1248
1253
  if (parent === directory) break;
1249
1254
  directory = parent;
1250
1255
  }
@@ -1257,7 +1262,7 @@ async function collectRuntimeFiles(directory) {
1257
1262
  while (pending.length > 0) {
1258
1263
  const current = pending.pop();
1259
1264
  for (const entry of await readdir(current, { withFileTypes: true })) {
1260
- const candidate = path8.join(current, entry.name);
1265
+ const candidate = path9.join(current, entry.name);
1261
1266
  if (entry.isDirectory()) {
1262
1267
  pending.push(candidate);
1263
1268
  } else if (entry.isFile() && /\.(?:js|json|css|wasm)$/iu.test(entry.name)) {
@@ -1341,9 +1346,9 @@ var init_output_bounds = __esm({
1341
1346
 
1342
1347
  // src/mcp/document-service.ts
1343
1348
  import { createHash as createHash5 } from "crypto";
1344
- import { basename as basename3, extname as extname3 } from "path";
1349
+ import { basename as basename4, extname as extname3 } from "path";
1345
1350
  import { MCP_WIRE_LIMITS as MCP_WIRE_LIMITS4 } from "@bendyline/docblocks/mcp";
1346
- import { decodeUtf8Text as decodeUtf8Text3, tryParseWorkspacePath } from "@bendyline/docblocks/filesystem";
1351
+ import { decodeUtf8Text as decodeUtf8Text4, tryParseWorkspacePath as tryParseWorkspacePath2 } from "@bendyline/docblocks/filesystem";
1347
1352
  async function readAssetManifest(container, signal) {
1348
1353
  const bytes = await container.readFile(ASSET_MANIFEST_PATH);
1349
1354
  if (!bytes) return /* @__PURE__ */ new Map();
@@ -1353,7 +1358,7 @@ async function readAssetManifest(container, signal) {
1353
1358
  }
1354
1359
  try {
1355
1360
  const parsed = JSON.parse(
1356
- decodeUtf8Text3(bytes, {
1361
+ decodeUtf8Text4(bytes, {
1357
1362
  label: "The document asset manifest",
1358
1363
  path: ASSET_MANIFEST_PATH
1359
1364
  })
@@ -1367,12 +1372,12 @@ async function readAssetManifest(container, signal) {
1367
1372
  if (!isExactRecord(candidate, ["path", "mimeType", "altText", "credit", "license"])) {
1368
1373
  throw new Error("The document asset manifest contains an invalid entry");
1369
1374
  }
1370
- const path9 = tryParseWorkspacePath(candidate.path);
1371
- 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)) {
1372
1377
  throw new Error("The document asset manifest contains invalid metadata");
1373
1378
  }
1374
- entries.set(path9, {
1375
- path: path9,
1379
+ entries.set(path10, {
1380
+ path: path10,
1376
1381
  mimeType: candidate.mimeType,
1377
1382
  altText: candidate.altText,
1378
1383
  credit: candidate.credit,
@@ -1508,7 +1513,7 @@ function throwIfAborted5(signal) {
1508
1513
  throwIfAborted(signal, "MCP operation was cancelled");
1509
1514
  }
1510
1515
  function baseNameFromName(name) {
1511
- const leaf = basename3(name.replace(/\\/gu, "/")) || "document";
1516
+ const leaf = basename4(name.replace(/\\/gu, "/")) || "document";
1512
1517
  const extension = extname3(leaf);
1513
1518
  return (extension ? leaf.slice(0, -extension.length) : leaf) || "document";
1514
1519
  }
@@ -1600,7 +1605,7 @@ var init_document_service = __esm({
1600
1605
  const physical = await this.authority.authorizeRootRead(source.rootId, source.path);
1601
1606
  const bytes = await this.authority.readFile(physical, signal);
1602
1607
  throwIfAborted5(signal);
1603
- const filename = basename3(source.path);
1608
+ const filename = basename4(source.path);
1604
1609
  return {
1605
1610
  bytes,
1606
1611
  filename,
@@ -1657,7 +1662,7 @@ var init_document_service = __esm({
1657
1662
  case "file": {
1658
1663
  const physical = await this.authority.authorizeRootRead(source.rootId, source.path);
1659
1664
  const bytes = await this.authority.readFile(physical, signal);
1660
- const name = basename3(source.path);
1665
+ const name = basename4(source.path);
1661
1666
  return {
1662
1667
  source: { kind: "bytes", data: bytes, filename: name },
1663
1668
  sourceFormat: normalizeFormat(source.format ?? formatFromName(name)),
@@ -2665,7 +2670,7 @@ var init_intelligence = __esm({
2665
2670
  // src/mcp/preview-service.ts
2666
2671
  import { mkdtemp as mkdtemp3, readFile as readFile3, rm as rm3, writeFile as writeFile2 } from "fs/promises";
2667
2672
  import { tmpdir as tmpdir2 } from "os";
2668
- 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";
2669
2674
  import { DOCBLOCKS_MCP_WIRE_VERSION as DOCBLOCKS_MCP_WIRE_VERSION2 } from "@bendyline/docblocks/mcp";
2670
2675
  async function previewEngineVersions() {
2671
2676
  return [
@@ -2903,7 +2908,7 @@ function thumbnailFilter(width, height) {
2903
2908
  return `scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2`;
2904
2909
  }
2905
2910
  function baseNameFromFilename(filename) {
2906
- const leaf = basename4(filename.replace(/\\/gu, "/")) || "video";
2911
+ const leaf = basename5(filename.replace(/\\/gu, "/")) || "video";
2907
2912
  const extension = extname4(leaf);
2908
2913
  return (extension ? leaf.slice(0, -extension.length) : leaf) || "video";
2909
2914
  }
@@ -3701,7 +3706,7 @@ async function convertPreparedDocument(artifacts, prepared, request, signal, pro
3701
3706
  }
3702
3707
  }
3703
3708
  function createConversionCacheKey(prepared, request, target, fidelity, engineVersions) {
3704
- const canonical = canonicalJson({
3709
+ const canonical2 = canonicalJson({
3705
3710
  source: {
3706
3711
  sha256: prepared.sourceSha256,
3707
3712
  format: prepared.sourceFormat,
@@ -3722,7 +3727,7 @@ function createConversionCacheKey(prepared, request, target, fidelity, engineVer
3722
3727
  title: request.title ?? null
3723
3728
  }
3724
3729
  });
3725
- return createHash6("sha256").update(canonical).digest("hex");
3730
+ return createHash6("sha256").update(canonical2).digest("hex");
3726
3731
  }
3727
3732
  async function findCompletedConversion(artifacts, key) {
3728
3733
  const cache = completedConversionCache(artifacts);
@@ -4300,10 +4305,10 @@ function registerAgenticTools(server, context) {
4300
4305
  async ({ source, targets, themeId, transformId, autoTemplates, title }, extra) => {
4301
4306
  try {
4302
4307
  return await context.runOperation(extra.signal, async (operationSignal) => {
4303
- const canonical = requireDocumentSource(source);
4308
+ const canonical2 = requireDocumentSource(source);
4304
4309
  const documents = new DocumentService(await context.authority, context.artifacts);
4305
4310
  await sendProgress(extra, 0, targets.length + 1, "Resolving document");
4306
- const prepared = await documents.prepare(canonical, operationSignal);
4311
+ const prepared = await documents.prepare(canonical2, operationSignal);
4307
4312
  const results = await convertPreparedDocument(
4308
4313
  context.artifacts,
4309
4314
  prepared,
@@ -4345,9 +4350,9 @@ function registerAgenticTools(server, context) {
4345
4350
  async ({ source }, extra) => {
4346
4351
  try {
4347
4352
  return await context.runOperation(extra.signal, async (operationSignal) => {
4348
- const canonical = requireDocumentSource(source);
4353
+ const canonical2 = requireDocumentSource(source);
4349
4354
  const documents = new DocumentService(await context.authority, context.artifacts);
4350
- const prepared = await documents.prepare(canonical, operationSignal);
4355
+ const prepared = await documents.prepare(canonical2, operationSignal);
4351
4356
  const [result] = await convertPreparedDocument(
4352
4357
  context.artifacts,
4353
4358
  prepared,
@@ -4380,7 +4385,7 @@ function registerAgenticTools(server, context) {
4380
4385
  async ({ artifactUri, destination }, extra) => {
4381
4386
  try {
4382
4387
  return await context.runOperation(extra.signal, async (operationSignal) => {
4383
- const canonical = requireWire(
4388
+ const canonical2 = requireWire(
4384
4389
  parseMaterializationOptions(destination),
4385
4390
  "materialization options"
4386
4391
  );
@@ -4390,15 +4395,15 @@ function registerAgenticTools(server, context) {
4390
4395
  context.artifacts.read(artifactUri, operationSignal)
4391
4396
  ]);
4392
4397
  throwIfAborted5(operationSignal);
4393
- const outputPath = await authority.authorizeRootWrite(canonical.rootId, canonical.path);
4398
+ const outputPath = await authority.authorizeRootWrite(canonical2.rootId, canonical2.path);
4394
4399
  await authority.materializeBytes(outputPath, bytes, bytes.byteLength, {
4395
- ifExists: canonical.ifExists,
4396
- expectedSha256: canonical.expectedSha256 ?? void 0,
4400
+ ifExists: canonical2.ifExists,
4401
+ expectedSha256: canonical2.expectedSha256 ?? void 0,
4397
4402
  signal: operationSignal
4398
4403
  });
4399
4404
  const payload = {
4400
4405
  artifact,
4401
- destination: { rootId: canonical.rootId, path: canonical.path },
4406
+ destination: { rootId: canonical2.rootId, path: canonical2.path },
4402
4407
  sha256: artifact.sha256
4403
4408
  };
4404
4409
  return textAndStructured(payload);
@@ -4705,6 +4710,7 @@ function registerTemplateTools(server, context) {
4705
4710
  title: boundNullableWireText(block.title, MCP_WIRE_LIMITS7.labelCharacters),
4706
4711
  profile: {
4707
4712
  ...profile,
4713
+ hasCompoundBody: profile.hasCompoundBody ?? false,
4708
4714
  hasAsciiDiagram: profile.hasAsciiDiagram ?? false,
4709
4715
  hasTimeline: profile.hasTimeline ?? false,
4710
4716
  hasTree: profile.hasTree ?? false
@@ -4861,6 +4867,7 @@ function registerTemplateTools(server, context) {
4861
4867
  title: boundNullableWireText(block.title, MCP_WIRE_LIMITS7.labelCharacters),
4862
4868
  profile: {
4863
4869
  ...profile,
4870
+ hasCompoundBody: profile.hasCompoundBody ?? false,
4864
4871
  hasAsciiDiagram: profile.hasAsciiDiagram ?? false,
4865
4872
  hasTimeline: profile.hasTimeline ?? false,
4866
4873
  hasTree: profile.hasTree ?? false
@@ -6153,12 +6160,12 @@ var init_server = __esm({
6153
6160
  });
6154
6161
 
6155
6162
  // src/program.ts
6156
- import { Command as Command9 } from "commander";
6163
+ import { Command as Command10 } from "commander";
6157
6164
 
6158
6165
  // src/commands/build.ts
6159
6166
  import { Command } from "commander";
6160
6167
  import { mkdir, opendir, readFile, stat as stat2, writeFile } from "fs/promises";
6161
- import path4 from "path";
6168
+ import path5 from "path";
6162
6169
 
6163
6170
  // src/render-html.ts
6164
6171
  init_contained_file();
@@ -6167,7 +6174,7 @@ init_node_error();
6167
6174
  import { realpath, stat } from "fs/promises";
6168
6175
  import path3 from "path";
6169
6176
  import { markdownToDoc } from "@bendyline/squisq/doc";
6170
- import { parseMarkdown } from "@bendyline/squisq/markdown";
6177
+ import { parseMarkdown, readFrontmatterThemeId } from "@bendyline/squisq/markdown";
6171
6178
  import { collectImagePaths, docToHtml } from "@bendyline/squisq-formats/html";
6172
6179
  import { PLAYER_BUNDLE } from "@bendyline/squisq-react/standalone-source";
6173
6180
 
@@ -6189,25 +6196,35 @@ var DEFAULT_MAX_ASSET_BYTES = 50 * 1024 * 1024;
6189
6196
  var MAX_SINGLE_ASSET_BYTES = 20 * 1024 * 1024;
6190
6197
  async function renderMarkdownHtml(markdown, options) {
6191
6198
  await assertKnownThemeId(options.themeId);
6199
+ const warn = options.onWarning ?? ((message) => console.warn(message));
6192
6200
  const markdownDoc = parseMarkdown(markdown);
6193
6201
  const doc = markdownToDoc(markdownDoc);
6202
+ const themeId = options.themeId ?? (readFrontmatterThemeId(markdownDoc.frontmatter) ? void 0 : await usableFallbackThemeId(options.fallbackThemeId, warn));
6194
6203
  const images = options.sourcePath && options.assetRoot ? await readReferencedImages(
6195
6204
  doc,
6196
6205
  options.sourcePath,
6197
6206
  options.assetRoot,
6198
6207
  options.maxAssetBytes ?? DEFAULT_MAX_ASSET_BYTES,
6199
6208
  options.allowReferencedAsset,
6200
- options.onWarning ?? ((message) => console.warn(message))
6209
+ warn
6201
6210
  ) : void 0;
6202
6211
  return docToHtml(doc, {
6203
6212
  playerScript: PLAYER_BUNDLE,
6204
6213
  images,
6205
6214
  title: options.title,
6206
6215
  mode: options.mode ?? "static",
6207
- themeId: options.themeId,
6216
+ themeId,
6208
6217
  showCodeCopyButton: true
6209
6218
  });
6210
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
+ }
6211
6228
  async function readReferencedImages(doc, sourcePath, assetRoot, maxAssetBytes, allowReferencedAsset, warn) {
6212
6229
  const images = /* @__PURE__ */ new Map();
6213
6230
  const root = await realpathIfPresent(path3.resolve(assetRoot));
@@ -6305,7 +6322,564 @@ function positiveLimit(value, fallback, label) {
6305
6322
 
6306
6323
  // src/commands/build.ts
6307
6324
  init_node_error();
6308
- 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";
6309
6883
  var DEFAULT_MAX_BUILD_ENTRIES = 1e5;
6310
6884
  var DEFAULT_MAX_BUILD_DEPTH = 64;
6311
6885
  var DEFAULT_MAX_BUILD_INPUT_BYTES = 20 * 1024 * 1024;
@@ -6314,8 +6888,8 @@ var DEFAULT_MAX_BUILD_OUTPUT_BYTES = 128 * 1024 * 1024;
6314
6888
  var DEFAULT_MAX_BUILD_TOTAL_OUTPUT_BYTES = 1024 * 1024 * 1024;
6315
6889
  async function runBuild(opts) {
6316
6890
  await assertKnownThemeId(opts.theme);
6317
- const inputDir = path4.resolve(opts.input);
6318
- const outputDir = path4.resolve(opts.output);
6891
+ const inputDir = path5.resolve(opts.input);
6892
+ const outputDir = path5.resolve(opts.output);
6319
6893
  let inputStat;
6320
6894
  try {
6321
6895
  inputStat = await stat2(inputDir);
@@ -6324,6 +6898,8 @@ async function runBuild(opts) {
6324
6898
  throw new Error(`Input directory not found: ${inputDir}`);
6325
6899
  }
6326
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();
6327
6903
  const markdownFiles = await listMarkdownFiles(inputDir, {
6328
6904
  maxEntries: positiveLimit(opts.maxEntries, DEFAULT_MAX_BUILD_ENTRIES, "build entry"),
6329
6905
  maxDepth: positiveLimit(opts.maxDepth, DEFAULT_MAX_BUILD_DEPTH, "build depth")
@@ -6338,8 +6914,8 @@ async function runBuild(opts) {
6338
6914
  let totalInputBytes = 0;
6339
6915
  let totalOutputBytes = 0;
6340
6916
  for (const sourcePath of markdownFiles) {
6341
- const relativePath = path4.relative(inputDir, sourcePath);
6342
- const outputPath = path4.join(outputDir, replaceMarkdownExtension(relativePath));
6917
+ const relativePath = path5.relative(inputDir, sourcePath);
6918
+ const outputPath = path5.join(outputDir, replaceMarkdownExtension(relativePath));
6343
6919
  const sourceBytes = await readFile(sourcePath);
6344
6920
  if (sourceBytes.byteLength > byteLimits.maxInputBytes) {
6345
6921
  throw new Error(
@@ -6352,13 +6928,15 @@ async function runBuild(opts) {
6352
6928
  `Build inputs exceed the ${byteLimits.maxTotalInputBytes}-byte aggregate limit.`
6353
6929
  );
6354
6930
  }
6355
- const source = decodeUtf8Text(sourceBytes, { label: "Build input", path: sourcePath });
6931
+ const source = decodeUtf8Text2(sourceBytes, { label: "Build input", path: sourcePath });
6356
6932
  const html = await renderMarkdownHtml(source, {
6357
6933
  title: titleFromPath(relativePath),
6358
6934
  sourcePath,
6359
6935
  assetRoot: inputDir,
6360
6936
  themeId: opts.theme,
6361
- mode: "static"
6937
+ fallbackThemeId,
6938
+ mode: "static",
6939
+ onWarning: warn
6362
6940
  });
6363
6941
  const outputBytes = Buffer.byteLength(html, "utf8");
6364
6942
  if (outputBytes > byteLimits.maxOutputBytes) {
@@ -6372,18 +6950,22 @@ async function runBuild(opts) {
6372
6950
  `Build outputs exceed the ${byteLimits.maxTotalOutputBytes}-byte aggregate limit.`
6373
6951
  );
6374
6952
  }
6375
- await mkdir(path4.dirname(outputPath), { recursive: true });
6953
+ await mkdir(path5.dirname(outputPath), { recursive: true });
6376
6954
  await writeFile(outputPath, html, "utf-8");
6377
6955
  builtFiles.push(outputPath);
6378
6956
  }
6379
6957
  return { inputDir, outputDir, builtFiles };
6380
6958
  }
6381
- 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) => {
6382
6963
  try {
6383
6964
  const result = await runBuild({
6384
6965
  input: opts.input,
6385
6966
  output: opts.output,
6386
6967
  theme: opts.theme,
6968
+ ignoreWorkspaceSettings: opts.ignoreWorkspaceSettings,
6387
6969
  maxInputBytes: parseOptionalByteLimit("--max-input-bytes", opts.maxInputBytes),
6388
6970
  maxTotalInputBytes: parseOptionalByteLimit(
6389
6971
  "--max-total-input-bytes",
@@ -6467,7 +7049,7 @@ async function listMarkdownFiles(root, limits) {
6467
7049
  if (entryCount > limits.maxEntries) {
6468
7050
  throw new Error(`Build traversal exceeded ${limits.maxEntries} filesystem entries.`);
6469
7051
  }
6470
- const absolutePath = path4.join(current.directory, entry.name);
7052
+ const absolutePath = path5.join(current.directory, entry.name);
6471
7053
  if (entry.isDirectory()) {
6472
7054
  if (current.depth >= limits.maxDepth) {
6473
7055
  throw new Error(`Build traversal exceeded a depth of ${limits.maxDepth}.`);
@@ -6487,7 +7069,7 @@ function replaceMarkdownExtension(filePath) {
6487
7069
  return filePath.replace(/\.(md|markdown)$/i, ".html");
6488
7070
  }
6489
7071
  function titleFromPath(filePath) {
6490
- const baseName = path4.basename(filePath).replace(/\.(md|markdown)$/i, "");
7072
+ const baseName = path5.basename(filePath).replace(/\.(md|markdown)$/i, "");
6491
7073
  return baseName || "DocBlocks Document";
6492
7074
  }
6493
7075
 
@@ -6496,11 +7078,11 @@ import { Command as Command2 } from "commander";
6496
7078
  import { createServer } from "http";
6497
7079
  import { realpath as realpath2, stat as stat3 } from "fs/promises";
6498
7080
  import { isIP } from "net";
6499
- import path6 from "path";
7081
+ import path7 from "path";
6500
7082
  init_contained_file();
6501
7083
 
6502
7084
  // src/preview-policy.ts
6503
- import path5 from "path";
7085
+ import path6 from "path";
6504
7086
  var PREVIEW_FILE_EXTENSIONS = /* @__PURE__ */ new Set([
6505
7087
  ".css",
6506
7088
  ".gif",
@@ -6546,14 +7128,14 @@ var SENSITIVE_FILE_NAMES = /* @__PURE__ */ new Set([
6546
7128
  "id_rsa"
6547
7129
  ]);
6548
7130
  function isAllowedPreviewPath(rootPath, requestedPath, physicalPath, purpose) {
6549
- const root = path5.resolve(rootPath);
6550
- const requested = path5.resolve(requestedPath);
6551
- const physical = path5.resolve(physicalPath);
7131
+ const root = path6.resolve(rootPath);
7132
+ const requested = path6.resolve(requestedPath);
7133
+ const physical = path6.resolve(physicalPath);
6552
7134
  for (const candidate of /* @__PURE__ */ new Set([requested, physical])) {
6553
- const relative = path5.relative(root, candidate);
7135
+ const relative = path6.relative(root, candidate);
6554
7136
  if (!isContainedRelativePath(relative) || containsSensitiveSegment(relative)) return false;
6555
7137
  if (purpose !== "directory") {
6556
- const extension = path5.extname(path5.basename(candidate)).toLowerCase();
7138
+ const extension = path6.extname(path6.basename(candidate)).toLowerCase();
6557
7139
  const allowedExtensions = purpose === "embedded-image" ? EMBEDDED_IMAGE_EXTENSIONS : PREVIEW_FILE_EXTENSIONS;
6558
7140
  if (!allowedExtensions.has(extension)) return false;
6559
7141
  }
@@ -6563,15 +7145,15 @@ function isAllowedPreviewPath(rootPath, requestedPath, physicalPath, purpose) {
6563
7145
  function containsSensitiveSegment(relativePath) {
6564
7146
  return relativePath.split(/[\\/]+/).filter(Boolean).some((segment) => {
6565
7147
  const lower = segment.toLowerCase();
6566
- 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));
6567
7149
  });
6568
7150
  }
6569
7151
  function isContainedRelativePath(relativePath) {
6570
- return relativePath === "" || !relativePath.startsWith(`..${path5.sep}`) && relativePath !== ".." && !path5.isAbsolute(relativePath);
7152
+ return relativePath === "" || !relativePath.startsWith(`..${path6.sep}`) && relativePath !== ".." && !path6.isAbsolute(relativePath);
6571
7153
  }
6572
7154
 
6573
7155
  // src/commands/serve.ts
6574
- import { decodeUtf8Text as decodeUtf8Text2 } from "@bendyline/docblocks/filesystem";
7156
+ import { decodeUtf8Text as decodeUtf8Text3 } from "@bendyline/docblocks/filesystem";
6575
7157
  init_node_error();
6576
7158
  var DEFAULT_HOST = "127.0.0.1";
6577
7159
  var DEFAULT_MAX_FILE_BYTES = 50 * 1024 * 1024;
@@ -6579,7 +7161,7 @@ var DEFAULT_MAX_CONCURRENT_READS = 16;
6579
7161
  var MAX_ALLOWED_HOSTS = 32;
6580
7162
  async function startPreviewServer(opts) {
6581
7163
  await assertKnownThemeId(opts.theme);
6582
- const requestedRoot = path6.resolve(opts.dir);
7164
+ const requestedRoot = path7.resolve(opts.dir);
6583
7165
  let root;
6584
7166
  try {
6585
7167
  root = await realpath2(requestedRoot);
@@ -6590,6 +7172,8 @@ async function startPreviewServer(opts) {
6590
7172
  const rootStat = await stat3(root);
6591
7173
  if (!rootStat.isDirectory()) throw new Error(`Preview root is not a directory: ${root}`);
6592
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);
6593
7177
  const allowedHosts = validateAllowedHosts(opts.allowedHosts ?? []);
6594
7178
  const maxFileBytes = positiveLimit(
6595
7179
  opts.maxFileBytes,
@@ -6619,7 +7203,7 @@ async function startPreviewServer(opts) {
6619
7203
  return;
6620
7204
  }
6621
7205
  activeRequests += 1;
6622
- handlePreviewRequest(req, res, root, opts.theme, maxFileBytes).catch((error) => {
7206
+ handlePreviewRequest(req, res, root, { themeId: opts.theme, themeFallback, warn }, maxFileBytes).catch((error) => {
6623
7207
  reportPreviewFailure(req, error);
6624
7208
  if (!res.headersSent) sendText(res, 500, "Internal server error", req.method === "HEAD");
6625
7209
  else res.end();
@@ -6638,13 +7222,17 @@ async function startPreviewServer(opts) {
6638
7222
  const port = typeof address === "object" && address ? address.port : opts.port;
6639
7223
  return { server, root, host, url: `http://${urlHost(host)}:${port}/` };
6640
7224
  }
6641
- 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(
6642
7229
  async (opts) => {
6643
7230
  try {
6644
7231
  const server = await startPreviewServer({
6645
7232
  port: parsePort(opts.port),
6646
7233
  dir: opts.dir,
6647
7234
  theme: opts.theme,
7235
+ ignoreWorkspaceSettings: opts.ignoreWorkspaceSettings,
6648
7236
  host: opts.host,
6649
7237
  allowNetwork: opts.allowNetwork,
6650
7238
  allowedHosts: opts.allowHost
@@ -6657,7 +7245,7 @@ var serveCommand = new Command2("serve").description("Start a local dev server f
6657
7245
  }
6658
7246
  }
6659
7247
  );
6660
- async function handlePreviewRequest(req, res, root, themeId, maxFileBytes) {
7248
+ async function handlePreviewRequest(req, res, root, render, maxFileBytes) {
6661
7249
  if (req.method !== "GET" && req.method !== "HEAD") {
6662
7250
  sendText(res, 405, "Method not allowed");
6663
7251
  return;
@@ -6681,16 +7269,18 @@ async function handlePreviewRequest(req, res, root, themeId, maxFileBytes) {
6681
7269
  return;
6682
7270
  }
6683
7271
  if (target.markdown) {
6684
- const markdown = decodeUtf8Text2(await readContainedFile(root, target.filePath, maxFileBytes), {
7272
+ const markdown = decodeUtf8Text3(await readContainedFile(root, target.filePath, maxFileBytes), {
6685
7273
  label: "Preview document",
6686
7274
  path: target.filePath
6687
7275
  });
6688
7276
  const html = await renderMarkdownHtml(markdown, {
6689
- title: path6.basename(target.filePath).replace(/\.(md|markdown)$/i, ""),
7277
+ title: path7.basename(target.filePath).replace(/\.(md|markdown)$/i, ""),
6690
7278
  sourcePath: target.filePath,
6691
7279
  assetRoot: root,
6692
- themeId,
7280
+ themeId: render.themeId,
7281
+ fallbackThemeId: render.themeFallback ? await render.themeFallback.resolve() : void 0,
6693
7282
  mode: "static",
7283
+ onWarning: render.warn,
6694
7284
  allowReferencedAsset: ({ assetRoot, requestedPath, physicalPath }) => isAllowedPreviewPath(assetRoot, requestedPath, physicalPath, "embedded-image")
6695
7285
  });
6696
7286
  sendBody(res, 200, html, "text/html; charset=utf-8", headOnly);
@@ -6703,7 +7293,7 @@ async function resolveServeTarget(root, requestUrl, maxFileBytes = DEFAULT_MAX_F
6703
7293
  if (requestUrl.length > 8192 || requestUrl.includes("\0")) return { kind: "bad-request" };
6704
7294
  let physicalRoot;
6705
7295
  try {
6706
- physicalRoot = await realpath2(path6.resolve(root));
7296
+ physicalRoot = await realpath2(path7.resolve(root));
6707
7297
  } catch (error) {
6708
7298
  if (isMissingPathError2(error)) return { kind: "missing" };
6709
7299
  if (isPermissionError(error)) return { kind: "forbidden" };
@@ -6712,7 +7302,7 @@ async function resolveServeTarget(root, requestUrl, maxFileBytes = DEFAULT_MAX_F
6712
7302
  const decodedPath = decodeRequestPath(requestUrl);
6713
7303
  if (!decodedPath) return { kind: "bad-request" };
6714
7304
  const requestedPath = decodedPath.replace(/^\/+/, "");
6715
- const candidate = path6.resolve(physicalRoot, requestedPath);
7305
+ const candidate = path7.resolve(physicalRoot, requestedPath);
6716
7306
  if (!isAllowedPreviewPath(physicalRoot, candidate, candidate, "directory")) {
6717
7307
  return { kind: "forbidden" };
6718
7308
  }
@@ -6772,7 +7362,7 @@ async function inspectTarget(root, candidate, maxFileBytes) {
6772
7362
  }
6773
7363
  async function directoryIndexTarget(root, dirPath, maxFileBytes) {
6774
7364
  for (const fileName of ["index.html", "index.htm", "index.md", "index.markdown"]) {
6775
- const target = await inspectTarget(root, path6.join(dirPath, fileName), maxFileBytes);
7365
+ const target = await inspectTarget(root, path7.join(dirPath, fileName), maxFileBytes);
6776
7366
  if (target.kind === "file" || target.kind === "forbidden" || target.kind === "too-large") {
6777
7367
  return target;
6778
7368
  }
@@ -6907,7 +7497,7 @@ function urlHost(host) {
6907
7497
  return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host;
6908
7498
  }
6909
7499
  function contentTypeFor(filePath) {
6910
- const ext = path6.extname(filePath).toLowerCase();
7500
+ const ext = path7.extname(filePath).toLowerCase();
6911
7501
  switch (ext) {
6912
7502
  case ".html":
6913
7503
  case ".htm":
@@ -6974,8 +7564,8 @@ import {
6974
7564
  rm,
6975
7565
  stat as stat4
6976
7566
  } from "fs/promises";
6977
- import { randomUUID } from "crypto";
6978
- 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";
6979
7569
  import { Command as Command3 } from "commander";
6980
7570
  init_link_support();
6981
7571
  init_node_error();
@@ -7052,7 +7642,7 @@ async function runConvert(inputPath, opts) {
7052
7642
  throwIfAborted2(opts.signal);
7053
7643
  const resolvedInput = resolve(inputPath);
7054
7644
  const outputDir = opts.outputDir ? resolve(opts.outputDir) : dirname(resolvedInput);
7055
- const inputBasename = basename(resolvedInput);
7645
+ const inputBasename = basename2(resolvedInput);
7056
7646
  const inputExt = extname(inputBasename);
7057
7647
  const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
7058
7648
  const maxInputBytes = positiveLimit(
@@ -7194,7 +7784,7 @@ async function runConvert(inputPath, opts) {
7194
7784
  if (totalOutputBytes > maxTotalOutputBytes) {
7195
7785
  throw new Error(`Conversion outputs exceed the ${maxTotalOutputBytes}-byte aggregate limit.`);
7196
7786
  }
7197
- const suggestedFilename = basename(conversion.suggestedFilename);
7787
+ const suggestedFilename = basename2(conversion.suggestedFilename);
7198
7788
  const outPath = join(outputDir, suggestedFilename);
7199
7789
  const outputPathKey = process.platform === "win32" ? outPath.toLowerCase() : outPath;
7200
7790
  if (outputPathKeys.has(outputPathKey)) {
@@ -7310,7 +7900,7 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
7310
7900
  throwIfAborted2(signal);
7311
7901
  const output = outputs[index];
7312
7902
  if (!output) continue;
7313
- const stagedPath = join(stagingDirectory, `${index}-${randomUUID()}.stage`);
7903
+ const stagedPath = join(stagingDirectory, `${index}-${randomUUID2()}.stage`);
7314
7904
  await writePrivateFile(stagedPath, output.bytes);
7315
7905
  stagedOutputs.push({ outputPath: output.path, stagedPath, bytes: output.bytes });
7316
7906
  }
@@ -7340,7 +7930,7 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
7340
7930
  throw new Error(`Refusing to replace a non-file destination: ${output.outputPath}`);
7341
7931
  }
7342
7932
  if (existing) {
7343
- state.backupPath = join(stagingDirectory, `${index}-${randomUUID()}.backup`);
7933
+ state.backupPath = join(stagingDirectory, `${index}-${randomUUID2()}.backup`);
7344
7934
  await rename(output.outputPath, state.backupPath);
7345
7935
  }
7346
7936
  await rename(output.stagedPath, output.outputPath);
@@ -7368,8 +7958,8 @@ async function publishConversionBatch(outputs, outputDir, allowOverwrite, signal
7368
7958
  }
7369
7959
  }
7370
7960
  }
7371
- async function writePrivateFile(path9, bytes) {
7372
- const handle = await open2(path9, "wx", 384);
7961
+ async function writePrivateFile(path10, bytes) {
7962
+ const handle = await open2(path10, "wx", 384);
7373
7963
  try {
7374
7964
  await handle.writeFile(bytes);
7375
7965
  await handle.sync();
@@ -7428,9 +8018,9 @@ async function assertReplaceableOutputs(outputPaths) {
7428
8018
  }
7429
8019
  }
7430
8020
  }
7431
- async function lstatIfPresent(path9) {
8021
+ async function lstatIfPresent(path10) {
7432
8022
  try {
7433
- return await lstat(path9);
8023
+ return await lstat(path10);
7434
8024
  } catch (error) {
7435
8025
  if (isNodeErrorCode(error, ["ENOENT", "ENOTDIR"])) return null;
7436
8026
  throw error;
@@ -7449,11 +8039,11 @@ function throwIfAborted2(signal) {
7449
8039
  }
7450
8040
  async function applyTransformToMarkdown(markdownDoc, container, transformStyle, themeId) {
7451
8041
  const { markdownToDoc: markdownToDoc3, docToMarkdown } = await import("@bendyline/squisq/doc");
7452
- const { readFrontmatterThemeId } = await import("@bendyline/squisq/markdown");
8042
+ const { readFrontmatterThemeId: readFrontmatterThemeId2 } = await import("@bendyline/squisq/markdown");
7453
8043
  const { applyTransform, extractDocImages } = await import("@bendyline/squisq/transform");
7454
8044
  const doc = markdownToDoc3(markdownDoc);
7455
8045
  const images = extractDocImages(doc.blocks);
7456
- const effectiveTheme = themeId ?? readFrontmatterThemeId(markdownDoc.frontmatter);
8046
+ const effectiveTheme = themeId ?? readFrontmatterThemeId2(markdownDoc.frontmatter);
7457
8047
  const result = applyTransform(doc, transformStyle, { themeId: effectiveTheme, images });
7458
8048
  const projected = projectTransformOntoMarkdownSource(doc, result.doc);
7459
8049
  const transformedMarkdown = docToMarkdown(projected);
@@ -7644,7 +8234,7 @@ function isConversionErrorMetadata(error) {
7644
8234
  // src/commands/video.ts
7645
8235
  init_cancellation();
7646
8236
  import { lstat as lstat2, mkdir as mkdir3 } from "fs/promises";
7647
- 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";
7648
8238
  import { Command as Command4 } from "commander";
7649
8239
  import {
7650
8240
  resolveDimensions,
@@ -7686,7 +8276,7 @@ async function runVideo(inputPath, opts, onProgress, dependencies = {}) {
7686
8276
  width: opts.width,
7687
8277
  height: opts.height
7688
8278
  });
7689
- const inputBasename = basename2(resolvedInput);
8279
+ const inputBasename = basename3(resolvedInput);
7690
8280
  const inputExt = extname2(inputBasename);
7691
8281
  const baseName = inputExt ? inputBasename.slice(0, -inputExt.length) : inputBasename;
7692
8282
  const outputPath = opts.output ? resolve2(opts.output) : resolve2(dirname2(resolvedInput), `${baseName}.mp4`);
@@ -7977,7 +8567,7 @@ var transformsCommand = new Command7("transforms").description("List all availab
7977
8567
  import { open as open5 } from "fs/promises";
7978
8568
  import { resolve as resolve3 } from "path";
7979
8569
  import { Command as Command8 } from "commander";
7980
- import { decodeUtf8Text as decodeUtf8Text4 } from "@bendyline/docblocks/filesystem";
8570
+ import { decodeUtf8Text as decodeUtf8Text5 } from "@bendyline/docblocks/filesystem";
7981
8571
  init_node_error();
7982
8572
  var DEFAULT_MAX_PARSE_INPUT_BYTES = 20 * 1024 * 1024;
7983
8573
  var DEFAULT_MAX_PARSE_OUTPUT_BYTES = 128 * 1024 * 1024;
@@ -7994,7 +8584,7 @@ async function runParse(inputPath, options = {}) {
7994
8584
  "parse output byte"
7995
8585
  );
7996
8586
  const bytes = await readBoundedFile(resolvedInput, maxInputBytes);
7997
- const content = decodeUtf8Text4(bytes, { label: "Parse input", path: resolvedInput });
8587
+ const content = decodeUtf8Text5(bytes, { label: "Parse input", path: resolvedInput });
7998
8588
  const { parseMarkdown: parseMarkdown2 } = await import("@bendyline/squisq/markdown");
7999
8589
  const markdownDoc = parseMarkdown2(content);
8000
8590
  const stats = {
@@ -8057,9 +8647,95 @@ var parseCommand = new Command8("parse").description("Parse a markdown file and
8057
8647
  }
8058
8648
  });
8059
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
+
8060
8736
  // src/program.ts
8061
8737
  init_version();
8062
- 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);
8063
8739
 
8064
8740
  // src/bin.ts
8065
8741
  program.parseAsync().catch((error) => {