tileflow 0.1.0-alpha.10 → 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.
package/README.md CHANGED
@@ -23,8 +23,8 @@ executable. With another package manager, use its local-execution command and th
23
23
  do not add a second lockfile or rely on a globally installed CLI.
24
24
 
25
25
  `init` creates a singular `tileflow.config.ts` extending the official Streets map. `preview` serves
26
- the exported map using its configured view. `dev` is a compatibility alias for `preview`, not a
27
- replacement for an application's normal development server.
26
+ the exported map using its configured view; it does not replace an application's normal development
27
+ server.
28
28
 
29
29
  Local validation, compilation, and asset preparation require no account or API key. Preview and
30
30
  capture may fetch configured remote tiles and fonts. Config files are executable trusted code,
@@ -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:
@@ -264,7 +281,7 @@ for the verified platform, setup, and development-artifact restrictions.
264
281
 
265
282
  Run `npx tileflow --help` or `npx tileflow <command> --help` for the installed command surface.
266
283
  The families cover map authoring, semantic language discovery, local visual evidence, datasets and
267
- icons, account sessions, coordinates, and hosted delivery. Hidden compatibility commands are not a
284
+ icons, account sessions, coordinates, and hosted delivery. Hidden support commands are not a
268
285
  recommended application workflow.
269
286
 
270
287
  A manifest 404 usually means assets were not built/served at the component's configured URL.
package/dist/index.js CHANGED
@@ -88,7 +88,7 @@ var sessionSchema = z.object({
88
88
  }).strict().refine((value) => Date.parse(value.expiresAt) > Date.parse(value.createdAt), {
89
89
  message: "session expiry must follow creation"
90
90
  });
91
- var authConfigV2Schema = z.object({
91
+ var authConfigSchema = z.object({
92
92
  sessions: z.record(z.string().max(2048), sessionSchema),
93
93
  version: z.literal(2)
94
94
  }).strict().superRefine((value, context) => {
@@ -101,16 +101,6 @@ var authConfigV2Schema = z.object({
101
101
  }
102
102
  }
103
103
  });
104
- var legacyAuthConfigSchema = z.object({
105
- apiKey: z.string().min(16).max(4096),
106
- apiUrl: z.string().min(1).max(2048),
107
- appUrl: z.string().max(2048).optional(),
108
- createdAt: z.string().max(64).optional(),
109
- deviceName: z.string().max(200).optional(),
110
- keyPrefix: z.string().max(64).optional(),
111
- projectId: z.string().max(200).optional(),
112
- scopes: z.array(z.string().max(128)).max(32).optional()
113
- }).strict();
114
104
  function emptyAuthConfig() {
115
105
  return Object.freeze({ sessions: Object.freeze({}), version: 2 });
116
106
  }
@@ -128,24 +118,13 @@ async function readAuthFile(path = authConfigPath()) {
128
118
  } catch {
129
119
  return { kind: "invalid" };
130
120
  }
131
- const v2 = authConfigV2Schema.safeParse(parsed);
132
- if (v2.success) return { config: freezeConfig(v2.data), kind: "v2" };
133
- const legacy = legacyAuthConfigSchema.safeParse(parsed);
134
- if (legacy.success) {
135
- return { config: Object.freeze(legacy.data), kind: "legacy_project_login" };
136
- }
137
- if (isSupersededProfileState(parsed)) return { kind: "superseded_profiles" };
138
- return { kind: "invalid" };
121
+ const config = authConfigSchema.safeParse(parsed);
122
+ return config.success ? { config: freezeConfig(config.data), kind: "sessions" } : { kind: "invalid" };
139
123
  }
140
124
  async function loadAuthConfig(path = authConfigPath()) {
141
125
  const state = await readAuthFile(path);
142
126
  if (state.kind === "empty") return emptyAuthConfig();
143
- if (state.kind === "v2") return state.config;
144
- if (state.kind === "legacy_project_login" || state.kind === "superseded_profiles") {
145
- throw new Error(
146
- "The saved login uses the retired destination-credential model. Run `tileflow login` to authorize your Tileflow account."
147
- );
148
- }
127
+ if (state.kind === "sessions") return state.config;
149
128
  throw new Error("Tileflow auth state is invalid; no changes were made.");
150
129
  }
151
130
  async function installAccountSession(session, path = authConfigPath()) {
@@ -157,7 +136,7 @@ async function installAccountSession(session, path = authConfigPath()) {
157
136
  if (state.kind === "invalid") {
158
137
  throw new Error("Tileflow auth state is invalid; no changes were made.");
159
138
  }
160
- const current = state.kind === "v2" ? state.config : emptyAuthConfig();
139
+ const current = state.kind === "sessions" ? state.config : emptyAuthConfig();
161
140
  const sessions = { ...current.sessions, [normalized.apiOrigin]: normalized };
162
141
  if (Object.keys(sessions).length > maximumOrigins) {
163
142
  throw new Error("Tileflow CLI API-origin limit reached.");
@@ -180,7 +159,7 @@ function removeAccountSession(config, apiUrl) {
180
159
  return { config: freezeConfig({ sessions, version: 2 }), removed };
181
160
  }
182
161
  async function writeAuthFileAtomic(config, path = authConfigPath()) {
183
- const normalized = normalizeConfig(authConfigV2Schema.parse(config));
162
+ const normalized = normalizeConfig(authConfigSchema.parse(config));
184
163
  const source = `${JSON.stringify(normalized, null, 2)}
185
164
  `;
186
165
  if (Buffer.byteLength(source, "utf8") > maximumAuthFileBytes) {
@@ -226,9 +205,6 @@ function parseProjectReference(value) {
226
205
  );
227
206
  return match ? { organizationSlug: match[1], projectSlug: match[2] } : null;
228
207
  }
229
- function projectReference(value) {
230
- return `@${value.organization.slug}/${value.project.slug}`;
231
- }
232
208
  function authConfigPath() {
233
209
  return resolve(homedir(), ".tileflow", "config.json");
234
210
  }
@@ -250,11 +226,6 @@ function freezeConfig(config) {
250
226
  );
251
227
  return Object.freeze({ sessions: Object.freeze(sessions), version: 2 });
252
228
  }
253
- function isSupersededProfileState(value) {
254
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
255
- const record = value;
256
- return record.version === 2 && Boolean(record.profiles && typeof record.profiles === "object") && Boolean(record.selections && typeof record.selections === "object");
257
- }
258
229
  async function syncDirectory(path) {
259
230
  if (process.platform === "win32") return;
260
231
  const directory = await open(path, "r");
@@ -678,6 +649,7 @@ import { isAbsolute, relative as relative2, resolve as resolve3, sep as sep2 } f
678
649
  import {
679
650
  createTileflowCaptureSession,
680
651
  serializeTileflowCaptureReceipt,
652
+ serializeTileflowShotReceipt,
681
653
  setupTileflowCaptureBrowser,
682
654
  TileflowCaptureError,
683
655
  tileflowSyntheticAssetOrigin,
@@ -687,7 +659,8 @@ import {
687
659
  compareCodeUnits as compareCodeUnits3,
688
660
  normalizeTileflowCaptureScene,
689
661
  tileflowCaptureSceneNameSchema,
690
- tileflowCaptureSceneSchema
662
+ tileflowCaptureSceneSchema,
663
+ tileflowCaptureShotNameSchema
691
664
  } from "@tileflow/core";
692
665
  import {
693
666
  createTileflowArtifactDiagnostics,
@@ -699,8 +672,8 @@ import { TileflowStyleValidationError } from "@tileflow/dev/validation";
699
672
 
700
673
  // src/capture-output.ts
701
674
  import { randomUUID } from "crypto";
702
- import { lstat, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, unlink as unlink2 } from "fs/promises";
703
- 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";
704
677
  var maximumAtomicArtifactBytes = 256 * 1024 * 1024;
705
678
  async function writeCapturePair(input) {
706
679
  const outputPath = resolve2(input.outputPath);
@@ -844,6 +817,119 @@ async function writeAtomicFile(input) {
844
817
  await removeOptional(temporary);
845
818
  }
846
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
+ }
847
933
  function captureReceiptPath(outputPath) {
848
934
  return outputPath.toLowerCase().endsWith(".png") ? `${outputPath.slice(0, -4)}.receipt.json` : `${outputPath}.receipt.json`;
849
935
  }
@@ -984,7 +1070,14 @@ function registerCaptureCommands(program2, dependencies) {
984
1070
  controller.close();
985
1071
  }
986
1072
  });
987
- 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(
988
1081
  "--no-browser-install",
989
1082
  "require the pinned browser to be prepared; never use system Chrome"
990
1083
  ).addHelpText(
@@ -994,11 +1087,205 @@ function registerCaptureCommands(program2, dependencies) {
994
1087
  try {
995
1088
  await runCaptureCommand(scenes ?? [], options);
996
1089
  } catch (error) {
997
- printCaptureError(error, { command: "capture", json: options.json });
1090
+ printCaptureError(error, {
1091
+ command: isShotSelection(options) ? "capture.shot" : "capture",
1092
+ json: options.json
1093
+ });
998
1094
  process.exitCode = 1;
999
1095
  }
1000
1096
  });
1001
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
+ }
1002
1289
  function createTileflowCaptureJson(captures, cwd) {
1003
1290
  return {
1004
1291
  schemaVersion: 1,
@@ -1048,6 +1335,10 @@ function serializeTileflowCaptureFailureJson(value) {
1048
1335
  async function runCaptureCommand(positionalScenes, options) {
1049
1336
  validateSelectionOptions(positionalScenes, options);
1050
1337
  await withTileflowConfigSecretsHidden(async () => {
1338
+ if (isShotSelection(options)) {
1339
+ await runCaptureShotCommand(options);
1340
+ return;
1341
+ }
1051
1342
  if (options.watch) {
1052
1343
  await runCaptureWatchCommand(positionalScenes, options);
1053
1344
  return;
@@ -1296,6 +1587,13 @@ function waitForAbort(signal) {
1296
1587
  );
1297
1588
  }
1298
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
+ }
1299
1597
  const exploratoryOptions = [
1300
1598
  options.center,
1301
1599
  options.bounds,
@@ -1354,7 +1652,9 @@ function validateSelectionOptions(positionalScenes, options) {
1354
1652
  throw new Error("Exploratory camera, viewport, and theme flags require --map.");
1355
1653
  }
1356
1654
  if (!options.all && positionalScenes.length === 0) {
1357
- 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
+ );
1358
1658
  }
1359
1659
  }
1360
1660
  function createExploratoryScene(options) {
@@ -1496,6 +1796,8 @@ function captureFailurePhase(error, code) {
1496
1796
  if (code === "MAP_LOAD_FAILED") return "map-load";
1497
1797
  if (code === "SCREENSHOT_FAILED" || code === "INVALID_PNG") return "screenshot";
1498
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";
1499
1801
  if (code === "ABORTED") return "capture";
1500
1802
  return "input-validation";
1501
1803
  }
@@ -2464,47 +2766,17 @@ async function requestMapCapability(session, target, scopes, options = {}) {
2464
2766
  } catch (error) {
2465
2767
  return { error: safeTransportError(error, "Map capability request failed."), ok: false };
2466
2768
  }
2467
- if (!response.ok) {
2468
- return { error: `Map capability request failed (${response.status}).`, ok: false };
2469
- }
2470
- const body = asRecord(response.body);
2471
- if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || body.mapId !== target.mapId || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
2472
- return { error: "Map capability response was invalid.", ok: false };
2473
- }
2474
- return { capability: body.capability, mapId: body.mapId, ok: true };
2475
- }
2476
- async function requestProjectCapability(session, project, scopes, options = {}) {
2477
- let response;
2478
- try {
2479
- response = await requestHostedJson(
2480
- session.apiOrigin,
2481
- "/v1/cli/project-capabilities",
2482
- {
2483
- ...jsonRequest("POST", { project, scopes }),
2484
- headers: {
2485
- ...authorizationHeaders(session.accountSession),
2486
- "Content-Type": "application/json"
2487
- }
2488
- },
2489
- options
2490
- );
2491
- } catch (error) {
2492
- return {
2493
- error: safeTransportError(error, "Project capability request failed."),
2494
- ok: false
2495
- };
2496
- }
2497
2769
  if (!response.ok) {
2498
2770
  return {
2499
- error: response.status === 401 ? rejectedAccountSessionMessage : `Project capability request failed (${response.status}).`,
2771
+ error: response.status === 401 ? rejectedAccountSessionMessage : `Map capability request failed (${response.status}).`,
2500
2772
  ok: false
2501
2773
  };
2502
2774
  }
2503
2775
  const body = asRecord(response.body);
2504
- if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || body.reference !== project || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
2505
- return { error: "Project capability response was invalid.", ok: false };
2776
+ if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || body.mapId !== target.mapId || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
2777
+ return { error: "Map capability response was invalid.", ok: false };
2506
2778
  }
2507
- return { capability: body.capability, ok: true };
2779
+ return { capability: body.capability, mapId: body.mapId, ok: true };
2508
2780
  }
2509
2781
  async function requestTeamCapability(session, team, scopes, options = {}) {
2510
2782
  let response;
@@ -3196,7 +3468,7 @@ import {
3196
3468
  // src/icon-diff-report.ts
3197
3469
  import { randomUUID as randomUUID2 } from "crypto";
3198
3470
  import { mkdir as mkdir3, readFile as readFile3, rename as rename3, unlink as unlink3, writeFile } from "fs/promises";
3199
- import { basename as basename2, dirname as dirname6, join } from "path";
3471
+ import { basename as basename2, dirname as dirname6, join as join2 } from "path";
3200
3472
  import {
3201
3473
  compareCodeUnits as compareCodeUnits5,
3202
3474
  sha256Hex,
@@ -3663,7 +3935,7 @@ async function writeAtomicReport(path, html, force) {
3663
3935
  if (!isMissingFile3(error)) throw error;
3664
3936
  }
3665
3937
  await mkdir3(dirname6(path), { recursive: true });
3666
- const temporaryPath = join(
3938
+ const temporaryPath = join2(
3667
3939
  dirname6(path),
3668
3940
  `.${basename2(path)}.${process.pid}.${randomUUID2()}.tmp`
3669
3941
  );
@@ -5292,18 +5564,6 @@ async function fetchAccountProjects(session, includeArchived) {
5292
5564
  }
5293
5565
  return { error: "Destination discovery exceeded its safe page limit.", ok: false, status: 502 };
5294
5566
  }
5295
- async function resolveAccountProjectTarget(session, requestedProject) {
5296
- const result = await fetchAccountProjects(session, false);
5297
- if (!result.ok) return { error: result.error, kind: "remote", status: result.status };
5298
- if (requestedProject) {
5299
- const parsed = parseProjectReference(requestedProject);
5300
- if (!parsed) return { kind: "invalid", targets: result.items };
5301
- const target = result.items.find((candidate) => candidate.reference === requestedProject);
5302
- return target ? { kind: "selected", target } : { kind: "missing", targets: result.items };
5303
- }
5304
- if (result.items.length === 1) return { kind: "selected", target: result.items[0] };
5305
- return result.items.length ? { kind: "ambiguous", targets: result.items } : { kind: "missing", targets: [] };
5306
- }
5307
5567
  async function requireCommandSession(options, dependencies) {
5308
5568
  let config;
5309
5569
  try {
@@ -8445,7 +8705,7 @@ program.command("build").description("Generate static Tileflow styles").option("
8445
8705
  printKeyValue("Output", pathLabel(resolve8(process.cwd(), options.out)));
8446
8706
  }
8447
8707
  );
8448
- program.command("preview").alias("dev").description("Run a local map preview").option("-c, --config <path>", "config path", defaultConfigPath).option("--against-config <path>", "second config for the comparison workbench").option("--against-map <name>", "map on the right; defaults to --map").option("--against-scene <name>", "committed standalone scene on the right").option("--against-theme <name>", "concrete theme on the right; defaults to --theme").option("--host <host>", "bind host: an explicit IP address or localhost", defaultTileflowDevHost).option("--map <name>", "preview one configured map").option("--theme <name>", "preview one concrete theme; defaults to the map default").option("--renderer <renderer>", "artifact renderer: web or native", "web").option("-p, --port <port>", "preview port", "3333").option("--scene <name>", "preview one committed standalone map scene").option(
8708
+ program.command("preview").description("Run a local map preview").option("-c, --config <path>", "config path", defaultConfigPath).option("--against-config <path>", "second config for the comparison workbench").option("--against-map <name>", "map on the right; defaults to --map").option("--against-scene <name>", "committed standalone scene on the right").option("--against-theme <name>", "concrete theme on the right; defaults to --theme").option("--host <host>", "bind host: an explicit IP address or localhost", defaultTileflowDevHost).option("--map <name>", "preview one configured map").option("--theme <name>", "preview one concrete theme; defaults to the map default").option("--renderer <renderer>", "artifact renderer: web or native", "web").option("-p, --port <port>", "preview port", "3333").option("--scene <name>", "preview one committed standalone map scene").option(
8449
8709
  "--api-base-url <url>",
8450
8710
  "Tileflow API base URL used to resolve official map assets",
8451
8711
  process.env.TILEFLOW_API_URL ?? defaultApiUrl
@@ -8461,7 +8721,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
8461
8721
  process.stdout.write(
8462
8722
  `${JSON.stringify({
8463
8723
  schemaVersion: 1,
8464
- command: "dev",
8724
+ command: "preview",
8465
8725
  event: "error",
8466
8726
  code: first?.code ?? "NATIVE_RENDERER_UNSUPPORTED",
8467
8727
  phase: first?.phase ?? "command-validation",
@@ -8481,7 +8741,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
8481
8741
  process.stdout.write(
8482
8742
  `${JSON.stringify({
8483
8743
  schemaVersion: 1,
8484
- command: "dev",
8744
+ command: "preview",
8485
8745
  event: "error",
8486
8746
  code: "NATIVE_RENDERER_UNSUPPORTED",
8487
8747
  phase: "command-validation",
@@ -8613,7 +8873,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
8613
8873
  process.stdout.write(
8614
8874
  `${JSON.stringify({
8615
8875
  schemaVersion: 1,
8616
- command: "dev",
8876
+ command: "preview",
8617
8877
  event: recovered ? "recovered" : state.status,
8618
8878
  generation: state.generation,
8619
8879
  ..."lastGoodGeneration" in state && state.lastGoodGeneration !== void 0 ? { lastGoodGeneration: state.lastGoodGeneration } : {},
@@ -8682,7 +8942,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
8682
8942
  process.stdout.write(
8683
8943
  `${JSON.stringify({
8684
8944
  schemaVersion: 1,
8685
- command: "dev",
8945
+ command: "preview",
8686
8946
  event: "stopped",
8687
8947
  generation,
8688
8948
  ...nativeLifecycleFields
@@ -8700,24 +8960,10 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
8700
8960
  ).option("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").action(
8701
8961
  async (options) => {
8702
8962
  const source = resolveDeploySource(process.env);
8703
- const resolveApi = (selectedMap) => requireApiOptions(options, {
8963
+ const resolveApi = () => requireApiOptions(options, {
8704
8964
  allowStoredCredential: allowsStoredDeployCredential(source),
8705
8965
  assertExplicitKeyMap: true,
8706
- capabilityScopes: ["styles:write"],
8707
- retryCommand: cliInvocation([
8708
- "tileflow",
8709
- "deploy",
8710
- "--config",
8711
- options.config,
8712
- "--manifest",
8713
- options.manifest,
8714
- "--api-url",
8715
- options.apiUrl ?? defaultApiUrl,
8716
- ...options.overwriteSelfHostedManifest ? ["--overwrite-self-hosted-manifest"] : [],
8717
- ...options.mapId ? ["--map-id", options.mapId] : [],
8718
- ...options.map && selectedMap ? ["--map", selectedMap] : [],
8719
- ...options.withNative ? ["--with-native"] : []
8720
- ])
8966
+ capabilityScopes: ["styles:write"]
8721
8967
  });
8722
8968
  const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
8723
8969
  delete process.env.TILEFLOW_API_KEY;
@@ -8823,7 +9069,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
8823
9069
  return;
8824
9070
  }
8825
9071
  const existingManifest = outputManifest;
8826
- const api = await resolveApi(mapNames[0]);
9072
+ const api = await resolveApi();
8827
9073
  if (!api) return;
8828
9074
  const hostedSpriteByPackageHash = /* @__PURE__ */ new Map();
8829
9075
  for (const iconPackage of compiledIcons.packages) {
@@ -9108,13 +9354,6 @@ program.command("status").description("Show deployed compiled styles").option("-
9108
9354
  try {
9109
9355
  api = await requireApiOptions(options, {
9110
9356
  capabilityScopes: ["status:read"],
9111
- retryCommand: cliInvocation([
9112
- "tileflow",
9113
- "status",
9114
- "--api-url",
9115
- options.apiUrl ?? defaultApiUrl,
9116
- ...options.json ? ["--json"] : []
9117
- ]),
9118
9357
  silent: options.json
9119
9358
  });
9120
9359
  } catch (error) {
@@ -9164,18 +9403,6 @@ registerIconDiffCommand(iconsCommand, {
9164
9403
  assertExplicitKeyMap: true,
9165
9404
  allowStoredCredential: allowsStoredDeployCredential(source),
9166
9405
  capabilityScopes: ["status:read"],
9167
- retryCommand: cliInvocation([
9168
- "tileflow",
9169
- "icons",
9170
- "diff",
9171
- "--against",
9172
- options.against,
9173
- ...options.config ? ["--config", options.config] : [],
9174
- "--map-id",
9175
- options.mapId,
9176
- "--api-url",
9177
- options.apiUrl ?? defaultApiUrl
9178
- ]),
9179
9406
  silent: true
9180
9407
  });
9181
9408
  }
@@ -9223,7 +9450,7 @@ async function runTileflowComparisonPreview(options, address) {
9223
9450
  const emit = (event) => {
9224
9451
  if (!options.json) return;
9225
9452
  process.stdout.write(
9226
- `${JSON.stringify({ schemaVersion: 1, command: "dev.compare", ...event })}
9453
+ `${JSON.stringify({ schemaVersion: 1, command: "preview.compare", ...event })}
9227
9454
  `
9228
9455
  );
9229
9456
  };
@@ -9613,19 +9840,13 @@ async function loadExistingDeployManifest(manifestPath, options) {
9613
9840
  async function requireApiOptions(options, behavior = {}) {
9614
9841
  const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
9615
9842
  const requestedMapId = options.mapId;
9616
- const requested = options.project ? parseProjectReference(options.project) : null;
9617
- if (requestedMapId && !/^map_[A-Za-z0-9_-]{16}$/u.test(requestedMapId)) {
9843
+ if (!requestedMapId || !/^map_[A-Za-z0-9_-]{16}$/u.test(requestedMapId)) {
9618
9844
  if (!behavior.silent) logError("Managed Map ID is invalid.");
9619
9845
  process.exitCode = 1;
9620
9846
  return null;
9621
9847
  }
9622
- if (options.project && !requested) {
9623
- if (!behavior.silent) logError("Managed destination must use @organization/project syntax.");
9624
- process.exitCode = 1;
9625
- return null;
9626
- }
9627
9848
  if (options.apiKey) {
9628
- if (requestedMapId && behavior.assertExplicitKeyMap) {
9849
+ if (behavior.assertExplicitKeyMap) {
9629
9850
  const profile = await validateApiKey(apiUrl, options.apiKey);
9630
9851
  if (!profile.ok) {
9631
9852
  if (!behavior.silent) logError(profile.error);
@@ -9638,33 +9859,7 @@ async function requireApiOptions(options, behavior = {}) {
9638
9859
  return null;
9639
9860
  }
9640
9861
  }
9641
- if (requested) {
9642
- const profile = await validateApiKey(apiUrl, options.apiKey);
9643
- if (!profile.ok) {
9644
- if (!behavior.silent) logError(profile.error);
9645
- process.exitCode = 1;
9646
- return null;
9647
- }
9648
- if (profile.value.credentialType === "team_api_key") {
9649
- if (!behavior.silent) logError("Team credentials require an explicit --map-id.");
9650
- process.exitCode = 1;
9651
- return null;
9652
- }
9653
- if (requested && projectReference(profile.value) !== options.project) {
9654
- if (!behavior.silent) {
9655
- logError(
9656
- `This project key belongs to ${projectReference(profile.value)}, not ${options.project}.`
9657
- );
9658
- }
9659
- process.exitCode = 1;
9660
- return null;
9661
- }
9662
- }
9663
- return {
9664
- apiKey: options.apiKey,
9665
- apiUrl,
9666
- ...requestedMapId ? { mapId: requestedMapId } : {}
9667
- };
9862
+ return { apiKey: options.apiKey, apiUrl, mapId: requestedMapId };
9668
9863
  }
9669
9864
  const allowStoredCredential = behavior.allowStoredCredential !== false && allowsStoredDeployCredential(resolveDeploySource(process.env));
9670
9865
  if (!allowStoredCredential) {
@@ -9697,46 +9892,9 @@ async function requireApiOptions(options, behavior = {}) {
9697
9892
  process.exitCode = 1;
9698
9893
  return null;
9699
9894
  }
9700
- if (requestedMapId) {
9701
- const capability2 = await requestMapCapability(
9702
- resolvedSession.session,
9703
- { mapId: requestedMapId },
9704
- behavior.capabilityScopes ?? ["status:read"]
9705
- );
9706
- if (!capability2.ok) {
9707
- if (!behavior.silent) logError(capability2.error);
9708
- process.exitCode = 1;
9709
- return null;
9710
- }
9711
- return { apiKey: capability2.capability, apiUrl, mapId: capability2.mapId };
9712
- }
9713
- const target = await resolveAccountProjectTarget(resolvedSession.session, options.project);
9714
- if (target.kind === "remote") {
9715
- if (!behavior.silent) logError(target.error);
9716
- process.exitCode = 1;
9717
- return null;
9718
- }
9719
- if (target.kind !== "selected") {
9720
- if (!behavior.silent) {
9721
- const references = target.targets.map((candidate) => candidate.reference);
9722
- if (target.kind === "ambiguous") {
9723
- const retry = `${behavior.retryCommand ?? "tileflow <command>"} --project ${references[0]}`;
9724
- logError(`Managed destination is ambiguous: ${references.join(", ")}.`);
9725
- printNextSteps([`Retry exactly with ${command(retry)}.`]);
9726
- } else if (target.kind === "invalid") {
9727
- logError("Managed destination must use @organization/project syntax.");
9728
- } else {
9729
- logError(
9730
- options.project ? `Destination ${options.project} is not accessible to this account.` : "No active managed destination is accessible to this account."
9731
- );
9732
- }
9733
- }
9734
- process.exitCode = 1;
9735
- return null;
9736
- }
9737
- const capability = await requestProjectCapability(
9895
+ const capability = await requestMapCapability(
9738
9896
  resolvedSession.session,
9739
- target.target.reference,
9897
+ { mapId: requestedMapId },
9740
9898
  behavior.capabilityScopes ?? ["status:read"]
9741
9899
  );
9742
9900
  if (!capability.ok) {
@@ -9744,7 +9902,7 @@ async function requireApiOptions(options, behavior = {}) {
9744
9902
  process.exitCode = 1;
9745
9903
  return null;
9746
9904
  }
9747
- return { apiKey: capability.capability, apiUrl };
9905
+ return { apiKey: capability.capability, apiUrl, mapId: capability.mapId };
9748
9906
  }
9749
9907
  function printTileflowPreviewError(error) {
9750
9908
  if (error instanceof TileflowStyleValidationError2) {
@@ -9895,12 +10053,6 @@ function plural(count, singular, pluralForm = `${singular}s`) {
9895
10053
  function command(value) {
9896
10054
  return pc6.cyan(value);
9897
10055
  }
9898
- function cliInvocation(parts) {
9899
- return parts.map(quoteCliArgument).join(" ");
9900
- }
9901
- function quoteCliArgument(value) {
9902
- return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
9903
- }
9904
10056
  function link(value) {
9905
10057
  return pc6.cyan(value);
9906
10058
  }
@@ -37,8 +37,8 @@ npx tileflow preview --scene madrid-mobile
37
37
  To work on another map, point `--config` at another singular config. Multi-map catalogs are internal
38
38
  orchestration used by this repository's workbench and are not a second public config shape.
39
39
 
40
- `tileflow dev` remains a compatibility alias. The `preview` name distinguishes this SDK preview
41
- server from an application's own `npm run dev` process.
40
+ The `preview` name distinguishes this SDK preview server from an application's own `npm run dev`
41
+ process.
42
42
 
43
43
  `--api-base-url` selects the origin used by official World, Bathymetry vector, Bathymetry DEM,
44
44
  Nautical, and terrain TileJSON sources in the generated preview style. In particular, a hybrid
@@ -5,7 +5,7 @@ Start with the [tileflow guide](https://github.com/tileflow/tileflow-sdk/blob/ma
5
5
  | Family | Commands |
6
6
  | ----------------- | ---------------------------------------------------------------------------------------------- |
7
7
  | Semantic language | `language manifest`, `language schema` |
8
- | Map authoring | `init`, `validate`, `inspect`, `explain`, `semantic-diff`, `build`, `preview` (`dev` alias) |
8
+ | Map authoring | `init`, `validate`, `inspect`, `explain`, `semantic-diff`, `build`, `preview` |
9
9
  | Local evidence | `setup capture`, `capture`, `visual compare`, `visual analyze`, `visual diff`, `visual update` |
10
10
  | Data and assets | `tileset inspect`, `tileset publish/list/status/purge`, `inspect features`, `icons list/diff` |
11
11
  | Team Icon Sets | `icon-set publish/list/status/versions/uses/archive/unarchive/purge` |
@@ -15,7 +15,7 @@ Start with the [tileflow guide](https://github.com/tileflow/tileflow-sdk/blob/ma
15
15
 
16
16
  Run `tileflow <command> --help` (or the family help, such as `tileflow icons --help`) for the exact
17
17
  arguments and bounded JSON modes. `preview` defaults to the existing web renderer. The explicit
18
- `preview --renderer native` / `dev --renderer native` mode serves watched `native-v1` artifacts only;
18
+ `preview --renderer native` mode serves watched `native-v1` artifacts only;
19
19
  Metro remains the React Native JavaScript development server. See
20
20
  [Native artifacts and local preview](./native-artifacts.md) for the endpoint and qualification
21
21
  boundary.
@@ -28,5 +28,5 @@ authority when resolving revisions and are the only commands that select revisio
28
28
  `tileflow.icons.lock.json`. See the
29
29
  [Team Icon Set workflow](https://github.com/tileflow/tileflow-sdk/blob/main/packages/cli/docs/team-icon-sets.md).
30
30
 
31
- The CLI retains the hidden `projects` command family as a compatibility and support surface for the
32
- internal application boundary. It is not part of the ordinary workflow or the product catalog.
31
+ The hidden `projects` command family lists and manages the internal application destinations of an
32
+ account for support. It is not part of the ordinary workflow or the product catalog.
@@ -30,7 +30,7 @@ The ordinary web commands and their default artifact bytes retain their existing
30
30
 
31
31
  ## Watch and serve native artifacts
32
32
 
33
- `preview` and its `dev` alias use the same renderer selector:
33
+ `preview` selects the renderer with `--renderer`:
34
34
 
35
35
  ```sh
36
36
  npx tileflow preview --renderer native --map stores --theme light
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileflow",
3
- "version": "0.1.0-alpha.10",
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",
@@ -35,15 +35,15 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@hono/node-server": "2.0.10",
39
- "@tileflow/capture": ">=0.1.0-alpha.28 <0.1.0-beta.0",
40
- "@tileflow/coordinates": ">=0.1.0-alpha.1 <0.1.0-beta.0",
41
- "@tileflow/coordinates-runtime": ">=0.1.0-alpha.4 <0.1.0-beta.0",
42
- "@tileflow/core": ">=0.1.0-alpha.31 <0.1.0-beta.0",
43
- "@tileflow/dev": ">=0.1.0-alpha.29 <0.1.0-beta.0",
44
- "@tileflow/maps": ">=0.1.0-alpha.7 <0.1.0-beta.0",
38
+ "@hono/node-server": "2.1.3",
39
+ "@tileflow/capture": ">=0.1.0-alpha.30 <0.1.0-beta.0",
40
+ "@tileflow/coordinates": ">=0.1.0-alpha.2 <0.1.0-beta.0",
41
+ "@tileflow/coordinates-runtime": ">=0.1.0-alpha.5 <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
+ "@tileflow/maps": ">=0.1.0-alpha.8 <0.1.0-beta.0",
45
45
  "commander": "^15.0.0",
46
- "hono": "4.13.5",
46
+ "hono": "4.13.7",
47
47
  "picocolors": "^1.1.1",
48
48
  "zod": "^4.4.3"
49
49
  },