tileflow 0.1.0-alpha.11 → 0.1.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +17 -0
  2. package/dist/index.js +339 -8
  3. package/package.json +4 -4
package/README.md CHANGED
@@ -151,6 +151,23 @@ on that route. Capture does not start the server or discover ports. It accepts l
151
151
  selects exactly one target, verifies the concrete theme, and waits for the component's idle marker.
152
152
  Do not run a separate `tileflow preview` server beside the application.
153
153
 
154
+ ## Render a timed shot
155
+
156
+ Commit a [capture shot](https://github.com/tileflow/tileflow-sdk/blob/main/packages/core/docs/capture-shots.md)
157
+ under `shots` on the same map: a concrete theme, a viewport, and two or more center-camera
158
+ keyframes, optionally with a theme blend. Then render it:
159
+
160
+ ```sh
161
+ npx tileflow capture --shot dusk-flyover --json
162
+ npx tileflow capture --shot dusk-flyover --format frames --out-dir evidence --json
163
+ ```
164
+
165
+ The first command writes `.tileflow/captures/shots/dusk-flyover.webm`; the second writes
166
+ `evidence/dusk-flyover/frame-0000.png` and the following frames. Each output has a sibling
167
+ `.receipt.json` shot receipt. Repeat `--shot` or use `--all-shots` to render several shots; `--out`
168
+ names the `.webm` file or frame directory of a single shot. Every frame waits for the map to be
169
+ ready, so a slow machine renders more slowly without dropping frames.
170
+
154
171
  ## Review pixels without accepting changes
155
172
 
156
173
  After reviewing a render, create its baseline. Compare subsequent renders separately:
package/dist/index.js CHANGED
@@ -649,6 +649,7 @@ import { isAbsolute, relative as relative2, resolve as resolve3, sep as sep2 } f
649
649
  import {
650
650
  createTileflowCaptureSession,
651
651
  serializeTileflowCaptureReceipt,
652
+ serializeTileflowShotReceipt,
652
653
  setupTileflowCaptureBrowser,
653
654
  TileflowCaptureError,
654
655
  tileflowSyntheticAssetOrigin,
@@ -658,7 +659,8 @@ import {
658
659
  compareCodeUnits as compareCodeUnits3,
659
660
  normalizeTileflowCaptureScene,
660
661
  tileflowCaptureSceneNameSchema,
661
- tileflowCaptureSceneSchema
662
+ tileflowCaptureSceneSchema,
663
+ tileflowCaptureShotNameSchema
662
664
  } from "@tileflow/core";
663
665
  import {
664
666
  createTileflowArtifactDiagnostics,
@@ -670,8 +672,8 @@ import { TileflowStyleValidationError } from "@tileflow/dev/validation";
670
672
 
671
673
  // src/capture-output.ts
672
674
  import { randomUUID } from "crypto";
673
- import { lstat, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, unlink as unlink2 } from "fs/promises";
674
- import { basename, dirname as dirname3, relative, resolve as resolve2, sep } from "path";
675
+ import { lstat, mkdir as mkdir2, open as open2, readdir, readFile as readFile2, rename as rename2, rm, unlink as unlink2 } from "fs/promises";
676
+ import { basename, dirname as dirname3, join, relative, resolve as resolve2, sep } from "path";
675
677
  var maximumAtomicArtifactBytes = 256 * 1024 * 1024;
676
678
  async function writeCapturePair(input) {
677
679
  const outputPath = resolve2(input.outputPath);
@@ -815,6 +817,119 @@ async function writeAtomicFile(input) {
815
817
  await removeOptional(temporary);
816
818
  }
817
819
  }
820
+ var shotFrameFilePattern = /^frame-\d{4}\.png$/u;
821
+ function shotFrameFileName(index) {
822
+ return `frame-${String(index).padStart(4, "0")}.png`;
823
+ }
824
+ async function stageShotFrameDirectory(input) {
825
+ const boundaryPath = resolve2(input.boundaryPath);
826
+ const directory = resolve2(input.directory);
827
+ await assertNoSymlinkComponents(dirname3(directory), boundaryPath);
828
+ await listShotFrameDirectory(directory);
829
+ await mkdir2(dirname3(directory), { recursive: true });
830
+ await assertNoSymlinkComponents(dirname3(directory), boundaryPath);
831
+ const staging = temporarySibling(directory, `${process.pid}.${randomUUID()}`, "frames");
832
+ await mkdir2(staging);
833
+ return {
834
+ boundaryPath,
835
+ directory,
836
+ staging,
837
+ write: (index, png) => writeSyncedExclusive(join(staging, shotFrameFileName(index)), png),
838
+ discard: () => rm(staging, { force: true, recursive: true })
839
+ };
840
+ }
841
+ async function commitShotFrameDirectory(input) {
842
+ const { stage } = input;
843
+ const receiptPath = resolve2(input.receiptPath);
844
+ const receipt = new TextEncoder().encode(input.receipt);
845
+ await assertNoSymlinkComponents(dirname3(stage.directory), stage.boundaryPath);
846
+ await assertNoSymlinkComponents(dirname3(receiptPath), stage.boundaryPath);
847
+ await assertNotSymlink(receiptPath);
848
+ const existingFrames = await listShotFrameDirectory(stage.directory);
849
+ const existingReceipt = await readOptional(receiptPath);
850
+ if (existingFrames && existingReceipt && Buffer.from(existingReceipt).equals(Buffer.from(receipt)) && await sameShotFrames(stage.directory, existingFrames, stage.staging)) {
851
+ await stage.discard();
852
+ return false;
853
+ }
854
+ if ((existingFrames || existingReceipt) && !input.managed && !input.force) {
855
+ throw new Error(
856
+ "Shot output already exists with different contents. Use --force to replace it."
857
+ );
858
+ }
859
+ const transaction = `${process.pid}.${randomUUID()}`;
860
+ const receiptTemporary = temporarySibling(receiptPath, transaction, "new");
861
+ const directoryBackup = temporarySibling(stage.directory, transaction, "previous");
862
+ const receiptBackup = temporarySibling(receiptPath, transaction, "previous");
863
+ const state = {
864
+ directoryBackedUp: false,
865
+ directoryInstalled: false,
866
+ receiptBackedUp: false,
867
+ receiptInstalled: false
868
+ };
869
+ try {
870
+ await writeSyncedExclusive(receiptTemporary, receipt);
871
+ await syncDirectory2(stage.staging);
872
+ if (existingFrames) {
873
+ await rename2(stage.directory, directoryBackup);
874
+ state.directoryBackedUp = true;
875
+ }
876
+ if (existingReceipt) {
877
+ await rename2(receiptPath, receiptBackup);
878
+ state.receiptBackedUp = true;
879
+ }
880
+ await rename2(stage.staging, stage.directory);
881
+ state.directoryInstalled = true;
882
+ await rename2(receiptTemporary, receiptPath);
883
+ state.receiptInstalled = true;
884
+ await syncDirectory2(dirname3(stage.directory));
885
+ if (dirname3(receiptPath) !== dirname3(stage.directory))
886
+ await syncDirectory2(dirname3(receiptPath));
887
+ } catch (error) {
888
+ if (state.receiptInstalled) await removeOptional(receiptPath).catch(() => void 0);
889
+ if (state.receiptBackedUp) await rename2(receiptBackup, receiptPath).catch(() => void 0);
890
+ if (state.directoryInstalled) {
891
+ await rename2(stage.directory, stage.staging).catch(() => void 0);
892
+ }
893
+ if (state.directoryBackedUp) {
894
+ await rename2(directoryBackup, stage.directory).catch(() => void 0);
895
+ }
896
+ throw error;
897
+ } finally {
898
+ await removeOptional(receiptTemporary).catch(() => void 0);
899
+ }
900
+ await Promise.allSettled([
901
+ rm(directoryBackup, { force: true, recursive: true }),
902
+ removeOptional(receiptBackup)
903
+ ]);
904
+ return true;
905
+ }
906
+ async function listShotFrameDirectory(directory) {
907
+ const info = await lstatOptional(directory);
908
+ if (!info) return void 0;
909
+ if (info.isSymbolicLink()) throw new Error("Capture output cannot be a symbolic link.");
910
+ if (!info.isDirectory()) throw new Error("Shot frame output must be a directory.");
911
+ const entries = await readdir(directory, { withFileTypes: true });
912
+ if (entries.some((entry) => !entry.isFile() || !shotFrameFilePattern.test(entry.name))) {
913
+ throw new Error(
914
+ "Shot frame output contains files that are not Tileflow frames. Choose an empty or Tileflow-owned directory."
915
+ );
916
+ }
917
+ return entries.map((entry) => entry.name).sort();
918
+ }
919
+ async function sameShotFrames(directory, names, staging) {
920
+ const staged = (await readdir(staging)).sort();
921
+ if (staged.length !== names.length || staged.some((name, index) => name !== names[index])) {
922
+ return false;
923
+ }
924
+ for (const name of names) {
925
+ const [existing, next] = await Promise.all([
926
+ readOptional(join(directory, name)),
927
+ readOptional(join(staging, name))
928
+ ]);
929
+ if (!existing || !next || !Buffer.from(existing).equals(Buffer.from(next))) return false;
930
+ }
931
+ return true;
932
+ }
818
933
  function captureReceiptPath(outputPath) {
819
934
  return outputPath.toLowerCase().endsWith(".png") ? `${outputPath.slice(0, -4)}.receipt.json` : `${outputPath}.receipt.json`;
820
935
  }
@@ -955,7 +1070,14 @@ function registerCaptureCommands(program2, dependencies) {
955
1070
  controller.close();
956
1071
  }
957
1072
  });
958
- program2.command("capture").description("Render Tileflow scenes to PNG with a pinned headless browser").argument("[scenes...]", "named committed capture scenes").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--all", "capture every configured scene").option("--map <name>", "capture one map with an uncommitted camera").option("--theme <name>", "required concrete theme for exploratory --map capture").option("--center <lng,lat>", "exploratory center camera").option("--bounds <west,south,east,north>", "exploratory bounds camera").option("--zoom <number>", "exploratory center zoom").option("--bearing <number>", "exploratory camera bearing").option("--pitch <number>", "exploratory camera pitch").option("--width <pixels>", "exploratory viewport width").option("--height <pixels>", "exploratory viewport height").option("--dpr <ratio>", "exploratory device pixel ratio: 1 or 2").option("--app-origin <origin>", "loopback origin for committed application scenes").option("--url <url>", "one-off full loopback URL for exactly one named scene").option("--selector <selector>", "one-off application target selector").option("--frame <frame>", "application capture frame: map or viewport").option("--out <path>", "PNG path for one capture").option("--out-dir <path>", "directory for one or more captures").option("--force", "replace different explicit output files").option("--json", "print deterministic schema-version-1 JSON").option("--watch", "keep running and recapture after each valid local edit").option(
1073
+ program2.command("capture").description(
1074
+ "Render Tileflow scenes to PNG, or timed shots to video, with a pinned headless browser"
1075
+ ).argument("[scenes...]", "named committed capture scenes").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--all", "capture every configured scene").option(
1076
+ "--shot <name>",
1077
+ "render a committed timed shot; repeat for more shots",
1078
+ collectOption,
1079
+ []
1080
+ ).option("--all-shots", "render every committed timed shot").option("--format <format>", "shot output: webm (default) or frames").option("--map <name>", "capture one map with an uncommitted camera").option("--theme <name>", "required concrete theme for exploratory --map capture").option("--center <lng,lat>", "exploratory center camera").option("--bounds <west,south,east,north>", "exploratory bounds camera").option("--zoom <number>", "exploratory center zoom").option("--bearing <number>", "exploratory camera bearing").option("--pitch <number>", "exploratory camera pitch").option("--width <pixels>", "exploratory viewport width").option("--height <pixels>", "exploratory viewport height").option("--dpr <ratio>", "exploratory device pixel ratio: 1 or 2").option("--app-origin <origin>", "loopback origin for committed application scenes").option("--url <url>", "one-off full loopback URL for exactly one named scene").option("--selector <selector>", "one-off application target selector").option("--frame <frame>", "application capture frame: map or viewport").option("--out <path>", "PNG path for one scene, or .webm/frame directory for one shot").option("--out-dir <path>", "directory for one or more captures").option("--force", "replace different explicit output files").option("--json", "print deterministic schema-version-1 JSON").option("--watch", "keep running and recapture after each valid local edit").option(
959
1081
  "--no-browser-install",
960
1082
  "require the pinned browser to be prepared; never use system Chrome"
961
1083
  ).addHelpText(
@@ -965,11 +1087,205 @@ function registerCaptureCommands(program2, dependencies) {
965
1087
  try {
966
1088
  await runCaptureCommand(scenes ?? [], options);
967
1089
  } catch (error) {
968
- printCaptureError(error, { command: "capture", json: options.json });
1090
+ printCaptureError(error, {
1091
+ command: isShotSelection(options) ? "capture.shot" : "capture",
1092
+ json: options.json
1093
+ });
969
1094
  process.exitCode = 1;
970
1095
  }
971
1096
  });
972
1097
  }
1098
+ function collectOption(value, previous) {
1099
+ return [...previous, value];
1100
+ }
1101
+ function isShotSelection(options) {
1102
+ return Boolean(options.allShots) || (options.shot?.length ?? 0) > 0;
1103
+ }
1104
+ function createTileflowShotCaptureJson(shots, cwd) {
1105
+ return {
1106
+ schemaVersion: 1,
1107
+ command: "capture.shot",
1108
+ shots: [...shots].sort((left, right) => compareCodeUnits3(left.capture.shot, right.capture.shot)).map(({ capture, outputPath, receiptPath }) => ({
1109
+ shot: capture.shot,
1110
+ map: capture.map,
1111
+ theme: capture.theme,
1112
+ status: "captured",
1113
+ format: capture.format,
1114
+ ...capture.codec ? { codec: capture.codec } : {},
1115
+ outputPath: relativePath(cwd, outputPath),
1116
+ receiptPath: relativePath(cwd, receiptPath),
1117
+ sha256: capture.sha256,
1118
+ framesSha256: capture.framesSha256,
1119
+ frameCount: capture.frameCount,
1120
+ fps: capture.fps,
1121
+ durationMs: capture.durationMs,
1122
+ width: capture.width,
1123
+ height: capture.height,
1124
+ dpr: capture.dpr,
1125
+ renderer: capture.renderer,
1126
+ networkDependent: capture.networkDependent,
1127
+ warnings: [...capture.warnings].sort(compareCodeUnits3)
1128
+ }))
1129
+ };
1130
+ }
1131
+ function serializeTileflowShotCaptureJson(value) {
1132
+ return `${JSON.stringify(value, null, 2)}
1133
+ `;
1134
+ }
1135
+ function shotReceiptPath(outputPath) {
1136
+ return outputPath.toLowerCase().endsWith(".webm") ? `${outputPath.slice(0, -5)}.receipt.json` : `${outputPath}.receipt.json`;
1137
+ }
1138
+ async function runCaptureShotCommand(options) {
1139
+ const cwd = process.cwd();
1140
+ const format = options.format === "frames" ? "frames" : "webm";
1141
+ const controller = installSignalAbortController();
1142
+ const session = createTileflowCaptureSession({
1143
+ allowBrowserInstall: options.browserInstall,
1144
+ config: options.config,
1145
+ cwd,
1146
+ onBrowserInstallProgress: options.json ? void 0 : printInstallProgress,
1147
+ signal: controller.signal
1148
+ });
1149
+ const stages = /* @__PURE__ */ new Map();
1150
+ try {
1151
+ const onFrame = format === "frames" ? async (frame) => {
1152
+ let stage = stages.get(frame.shot);
1153
+ if (!stage) {
1154
+ const target = resolveShotOutputTarget(frame.shot, options, format, cwd);
1155
+ stage = await stageShotFrameDirectory({
1156
+ boundaryPath: target.boundaryPath,
1157
+ directory: target.outputPath
1158
+ });
1159
+ stages.set(frame.shot, stage);
1160
+ }
1161
+ await stage.write(frame.index, frame.png);
1162
+ } : void 0;
1163
+ const shotOptions = { format, ...onFrame ? { onFrame } : {}, signal: controller.signal };
1164
+ const result = options.allShots ? await session.captureAllShots(shotOptions) : await session.captureShots(options.shot ?? [], shotOptions);
1165
+ const written = [];
1166
+ for (const capture of result.shots) {
1167
+ const target = resolveShotOutputTarget(capture.shot, options, format, cwd);
1168
+ const receipt = serializeTileflowShotReceipt(capture.receipt);
1169
+ if (capture.video) {
1170
+ await writeAtomicFileSet({
1171
+ boundaryPath: target.boundaryPath,
1172
+ files: [
1173
+ { path: target.outputPath, source: capture.video },
1174
+ { path: target.receiptPath, source: receipt }
1175
+ ],
1176
+ force: Boolean(options.force),
1177
+ label: "Shot output",
1178
+ managed: target.managed
1179
+ });
1180
+ } else {
1181
+ const stage = stages.get(capture.shot);
1182
+ if (!stage) throw new Error(`Shot ${capture.shot} produced no frames.`);
1183
+ await commitShotFrameDirectory({
1184
+ force: Boolean(options.force),
1185
+ managed: target.managed,
1186
+ receipt,
1187
+ receiptPath: target.receiptPath,
1188
+ stage
1189
+ });
1190
+ stages.delete(capture.shot);
1191
+ }
1192
+ written.push({ capture, outputPath: target.outputPath, receiptPath: target.receiptPath });
1193
+ }
1194
+ if (!options.out && !options.outDir) await warnIfManagedCapturesAreNotIgnored(cwd);
1195
+ const document = createTileflowShotCaptureJson(written, cwd);
1196
+ if (options.json) {
1197
+ process.stdout.write(serializeTileflowShotCaptureJson(document));
1198
+ return;
1199
+ }
1200
+ for (const entry of document.shots) {
1201
+ console.log(
1202
+ `Rendered ${entry.shot} -> ${entry.outputPath} (${entry.frameCount} frames, ${entry.sha256.slice(0, 12)}).`
1203
+ );
1204
+ if (entry.networkDependent) {
1205
+ console.error(`Shot ${entry.shot} used remote map resources.`);
1206
+ }
1207
+ }
1208
+ } finally {
1209
+ await Promise.allSettled([...stages.values()].map((stage) => stage.discard()));
1210
+ await session.close();
1211
+ controller.close();
1212
+ }
1213
+ }
1214
+ function resolveShotOutputTarget(shot, options, format, cwd) {
1215
+ const managed = !options.out && !options.outDir;
1216
+ const directory = resolve3(cwd, options.outDir ?? ".tileflow/captures/shots");
1217
+ const outputPath = options.out ? resolve3(cwd, options.out) : resolve3(directory, format === "webm" ? `${shot}.webm` : shot);
1218
+ const receiptPath = shotReceiptPath(outputPath);
1219
+ if (managed) {
1220
+ const relativeToRoot = relative2(directory, outputPath);
1221
+ if (relativeToRoot.startsWith("..") || relativeToRoot.includes(`..${sep2}`)) {
1222
+ throw new Error("Managed shot output escaped .tileflow/captures/shots.");
1223
+ }
1224
+ }
1225
+ relativePath(cwd, outputPath);
1226
+ relativePath(cwd, receiptPath);
1227
+ return {
1228
+ boundaryPath: managed ? cwd : options.outDir ? directory : resolve3(outputPath, ".."),
1229
+ managed,
1230
+ outputPath,
1231
+ receiptPath
1232
+ };
1233
+ }
1234
+ function validateShotSelectionOptions(positionalScenes, options) {
1235
+ const shots = options.shot ?? [];
1236
+ if (options.allShots && shots.length > 0) {
1237
+ throw new Error("Use either --shot or --all-shots, not both.");
1238
+ }
1239
+ if (positionalScenes.length > 0 || options.all || options.map) {
1240
+ throw new Error("Render shots separately from scenes; --shot and --all-shots take no scenes.");
1241
+ }
1242
+ if (options.watch) {
1243
+ throw new Error("--watch captures scenes; it does not render shots.");
1244
+ }
1245
+ const sceneOnlyOptions = [
1246
+ options.center,
1247
+ options.bounds,
1248
+ options.zoom,
1249
+ options.bearing,
1250
+ options.pitch,
1251
+ options.width,
1252
+ options.height,
1253
+ options.dpr,
1254
+ options.theme,
1255
+ options.url,
1256
+ options.appOrigin,
1257
+ options.selector,
1258
+ options.frame
1259
+ ];
1260
+ if (sceneOnlyOptions.some((value) => value !== void 0)) {
1261
+ throw new Error(
1262
+ "Shots render committed cameras and themes; camera, viewport, theme, and application flags apply to scenes."
1263
+ );
1264
+ }
1265
+ for (const shot of shots) {
1266
+ if (!tileflowCaptureShotNameSchema.safeParse(shot).success) {
1267
+ throw new Error("--shot expects a portable Tileflow shot name.");
1268
+ }
1269
+ }
1270
+ if (options.format !== void 0 && options.format !== "webm" && options.format !== "frames") {
1271
+ throw new Error("--format expects webm or frames.");
1272
+ }
1273
+ if (options.out && options.outDir) {
1274
+ throw new Error("Use either --out or --out-dir, not both.");
1275
+ }
1276
+ if (options.out) {
1277
+ if (options.allShots || new Set(shots).size !== 1) {
1278
+ throw new Error("--out can be used only when rendering one shot. Use --out-dir instead.");
1279
+ }
1280
+ const isVideoPath = options.out.toLowerCase().endsWith(".webm");
1281
+ if ((options.format ?? "webm") === "webm" && !isVideoPath) {
1282
+ throw new Error("--out must name a .webm file.");
1283
+ }
1284
+ if (options.format === "frames" && (isVideoPath || /\.(?:json|png)$/iu.test(options.out))) {
1285
+ throw new Error("--out must name a directory for --format frames.");
1286
+ }
1287
+ }
1288
+ }
973
1289
  function createTileflowCaptureJson(captures, cwd) {
974
1290
  return {
975
1291
  schemaVersion: 1,
@@ -1019,6 +1335,10 @@ function serializeTileflowCaptureFailureJson(value) {
1019
1335
  async function runCaptureCommand(positionalScenes, options) {
1020
1336
  validateSelectionOptions(positionalScenes, options);
1021
1337
  await withTileflowConfigSecretsHidden(async () => {
1338
+ if (isShotSelection(options)) {
1339
+ await runCaptureShotCommand(options);
1340
+ return;
1341
+ }
1022
1342
  if (options.watch) {
1023
1343
  await runCaptureWatchCommand(positionalScenes, options);
1024
1344
  return;
@@ -1267,6 +1587,13 @@ function waitForAbort(signal) {
1267
1587
  );
1268
1588
  }
1269
1589
  function validateSelectionOptions(positionalScenes, options) {
1590
+ if (isShotSelection(options)) {
1591
+ validateShotSelectionOptions(positionalScenes, options);
1592
+ return;
1593
+ }
1594
+ if (options.format !== void 0) {
1595
+ throw new Error("--format applies to --shot and --all-shots.");
1596
+ }
1270
1597
  const exploratoryOptions = [
1271
1598
  options.center,
1272
1599
  options.bounds,
@@ -1325,7 +1652,9 @@ function validateSelectionOptions(positionalScenes, options) {
1325
1652
  throw new Error("Exploratory camera, viewport, and theme flags require --map.");
1326
1653
  }
1327
1654
  if (!options.all && positionalScenes.length === 0) {
1328
- throw new Error("Select at least one scene, use --all, or use --map for exploration.");
1655
+ throw new Error(
1656
+ "Select at least one scene, use --all, use --map for exploration, or select shots with --shot."
1657
+ );
1329
1658
  }
1330
1659
  }
1331
1660
  function createExploratoryScene(options) {
@@ -1467,6 +1796,8 @@ function captureFailurePhase(error, code) {
1467
1796
  if (code === "MAP_LOAD_FAILED") return "map-load";
1468
1797
  if (code === "SCREENSHOT_FAILED" || code === "INVALID_PNG") return "screenshot";
1469
1798
  if (code === "SCENE_NOT_FOUND" || code.startsWith("APPLICATION_")) return "scene-resolution";
1799
+ if (code === "SHOT_NOT_FOUND") return "shot-resolution";
1800
+ if (code.startsWith("VIDEO_")) return "video-encode";
1470
1801
  if (code === "ABORTED") return "capture";
1471
1802
  return "input-validation";
1472
1803
  }
@@ -3137,7 +3468,7 @@ import {
3137
3468
  // src/icon-diff-report.ts
3138
3469
  import { randomUUID as randomUUID2 } from "crypto";
3139
3470
  import { mkdir as mkdir3, readFile as readFile3, rename as rename3, unlink as unlink3, writeFile } from "fs/promises";
3140
- import { basename as basename2, dirname as dirname6, join } from "path";
3471
+ import { basename as basename2, dirname as dirname6, join as join2 } from "path";
3141
3472
  import {
3142
3473
  compareCodeUnits as compareCodeUnits5,
3143
3474
  sha256Hex,
@@ -3604,7 +3935,7 @@ async function writeAtomicReport(path, html, force) {
3604
3935
  if (!isMissingFile3(error)) throw error;
3605
3936
  }
3606
3937
  await mkdir3(dirname6(path), { recursive: true });
3607
- const temporaryPath = join(
3938
+ const temporaryPath = join2(
3608
3939
  dirname6(path),
3609
3940
  `.${basename2(path)}.${process.pid}.${randomUUID2()}.tmp`
3610
3941
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileflow",
3
- "version": "0.1.0-alpha.11",
3
+ "version": "0.1.0-alpha.12",
4
4
  "license": "Apache-2.0",
5
5
  "description": "CLI-only tools for Tileflow config, capture, visual testing, and deploy.",
6
6
  "type": "module",
@@ -36,11 +36,11 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@hono/node-server": "2.1.3",
39
- "@tileflow/capture": ">=0.1.0-alpha.29 <0.1.0-beta.0",
39
+ "@tileflow/capture": ">=0.1.0-alpha.30 <0.1.0-beta.0",
40
40
  "@tileflow/coordinates": ">=0.1.0-alpha.2 <0.1.0-beta.0",
41
41
  "@tileflow/coordinates-runtime": ">=0.1.0-alpha.5 <0.1.0-beta.0",
42
- "@tileflow/core": ">=0.1.0-alpha.32 <0.1.0-beta.0",
43
- "@tileflow/dev": ">=0.1.0-alpha.30 <0.1.0-beta.0",
42
+ "@tileflow/core": ">=0.1.0-alpha.34 <0.1.0-beta.0",
43
+ "@tileflow/dev": ">=0.1.0-alpha.31 <0.1.0-beta.0",
44
44
  "@tileflow/maps": ">=0.1.0-alpha.8 <0.1.0-beta.0",
45
45
  "commander": "^15.0.0",
46
46
  "hono": "4.13.7",