tileflow 0.1.0-alpha.22 → 0.1.0-alpha.23

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.
@@ -4,24 +4,32 @@ import {
4
4
  isRootCliVersionRequest
5
5
  } from "./chunk-GPPOECR5.js";
6
6
  import {
7
+ apiOriginOption,
7
8
  apiProfileCanTargetMap,
8
9
  assertHostedStyleBudget,
9
10
  authorizedTeamRequest,
11
+ configureJsonErrors,
10
12
  createCliProgram,
11
13
  createManagedMapSetup,
14
+ deploymentRestoreSteps,
12
15
  emitFailure,
16
+ emitGuidedFailure,
13
17
  emitJson,
18
+ emitRestoreFailure,
14
19
  fetchHostedMapStatus,
15
20
  hostedApiKeyOption,
16
21
  hostedApiUrlOption,
17
22
  installAccountSession,
18
23
  isHostedApiKey,
24
+ isRestoreJsonCommand,
19
25
  listMapSetupTeams,
20
26
  loadAuthConfig,
21
27
  normalizeApiOrigin,
22
28
  parseProjectReference,
29
+ parseRestoreJson,
23
30
  pollDeviceToken,
24
31
  prepareAccountLogin,
32
+ printNextSteps,
25
33
  publishHostedStyle,
26
34
  registerTilesetCommands,
27
35
  registerTilesetInspectCommand,
@@ -40,7 +48,7 @@ import {
40
48
  validateAccountSession,
41
49
  validateApiKey,
42
50
  writeAuthFileAtomic
43
- } from "./chunk-5QK6JCLT.js";
51
+ } from "./chunk-EXDH7EHK.js";
44
52
 
45
53
  // src/cli-main.ts
46
54
  import { serve } from "@hono/node-server";
@@ -48,9 +56,9 @@ import { createHash as createHash3, randomBytes as randomBytes2 } from "crypto";
48
56
  import { existsSync, realpathSync } from "fs";
49
57
  import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
50
58
  import { hostname } from "os";
51
- import { dirname as dirname12, isAbsolute as isAbsolute5, relative as relative6, resolve as resolve11, sep as sep5 } from "path";
59
+ import { dirname as dirname12, isAbsolute as isAbsolute5, relative as relative6, resolve as resolve12, sep as sep5 } from "path";
52
60
  import { createInterface as createInterface2 } from "readline/promises";
53
- import pc7 from "picocolors";
61
+ import pc8 from "picocolors";
54
62
  import { createLocalCoordinates, setupCoordinates } from "@tileflow/coordinates-runtime";
55
63
  import {
56
64
  auditTileflowMapThemeValues as auditTileflowMapThemeValues2,
@@ -1002,13 +1010,13 @@ async function saveCaptureRecord(capture, cwd) {
1002
1010
  throw new Error(
1003
1011
  "Capture has no normalized scene definition. Rebuild @tileflow/capture before using this CLI."
1004
1012
  );
1005
- const record = {
1013
+ const record2 = {
1006
1014
  schemaVersion: 1,
1007
1015
  definition: capture.definition,
1008
1016
  receipt: capture.receipt,
1009
1017
  warnings: capture.warnings
1010
1018
  };
1011
- const source = serializeCanonicalJson(record);
1019
+ const source = serializeCanonicalJson(record2);
1012
1020
  if (Buffer.byteLength(source) > maximumRecordBytes)
1013
1021
  throw new Error("Capture record exceeds its byte limit.");
1014
1022
  const id = await sha256Hex(source);
@@ -1033,11 +1041,11 @@ async function readCaptureRecord(id, directory) {
1033
1041
  const source = await readBoundedFile(resolve2(root, `${id}.json`), maximumRecordBytes);
1034
1042
  if (await sha256Hex(source) !== id)
1035
1043
  throw new Error("Saved capture record does not match its ID.");
1036
- const record = JSON.parse(source.toString("utf8"));
1037
- if (record.schemaVersion !== 1) throw new Error("Unsupported capture record version.");
1038
- const receipt = parseTileflowCaptureReceipt(JSON.stringify(record.receipt));
1044
+ const record2 = JSON.parse(source.toString("utf8"));
1045
+ if (record2.schemaVersion !== 1) throw new Error("Unsupported capture record version.");
1046
+ const receipt = parseTileflowCaptureReceipt(JSON.stringify(record2.receipt));
1039
1047
  const definition = normalizeTileflowCaptureScene(
1040
- tileflowCaptureSceneSchema.parse(record.definition)
1048
+ tileflowCaptureSceneSchema.parse(record2.definition)
1041
1049
  );
1042
1050
  if (!definition.theme) throw new Error("Saved capture requires a concrete theme.");
1043
1051
  if (await sha256Hex(serializeCanonicalJson(definition)) !== receipt.scene.sha256)
@@ -1048,7 +1056,7 @@ async function readCaptureRecord(id, directory) {
1048
1056
  );
1049
1057
  if (await sha256Hex(png) !== receipt.image.sha256)
1050
1058
  throw new Error("Saved capture PNG does not match its receipt.");
1051
- if (!Array.isArray(record.warnings) || record.warnings.some((warning) => typeof warning !== "string"))
1059
+ if (!Array.isArray(record2.warnings) || record2.warnings.some((warning) => typeof warning !== "string"))
1052
1060
  throw new Error("Invalid capture warnings.");
1053
1061
  return {
1054
1062
  definition: { ...definition, theme: definition.theme },
@@ -1067,7 +1075,7 @@ async function readCaptureRecord(id, directory) {
1067
1075
  dpr: receipt.image.dpr,
1068
1076
  renderer: receipt.renderer,
1069
1077
  networkDependent: receipt.networkDependent,
1070
- warnings: record.warnings
1078
+ warnings: record2.warnings
1071
1079
  }
1072
1080
  };
1073
1081
  }
@@ -2929,11 +2937,11 @@ function validateVisualCompareInventory(input) {
2929
2937
  if (!input || typeof input !== "object" || Array.isArray(input)) {
2930
2938
  throw new Error("Visual compare inventory has an invalid shape.");
2931
2939
  }
2932
- const record = input;
2933
- if (!hasExactKeys(record, ["assets", "kind", "schemaVersion"]) || record.schemaVersion !== tileflowVisualCompareInventorySchemaVersion || record.kind !== "tileflow-visual-compare-assets" || !Array.isArray(record.assets) || record.assets.length > tileflowVisualCompareLimits.maximumRows * 5) {
2940
+ const record2 = input;
2941
+ if (!hasExactKeys(record2, ["assets", "kind", "schemaVersion"]) || record2.schemaVersion !== tileflowVisualCompareInventorySchemaVersion || record2.kind !== "tileflow-visual-compare-assets" || !Array.isArray(record2.assets) || record2.assets.length > tileflowVisualCompareLimits.maximumRows * 5) {
2934
2942
  throw new Error("Visual compare inventory has an invalid shape.");
2935
2943
  }
2936
- const assets = record.assets.map((entry) => {
2944
+ const assets = record2.assets.map((entry) => {
2937
2945
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
2938
2946
  throw new Error("Visual compare inventory has an invalid asset entry.");
2939
2947
  }
@@ -2963,8 +2971,8 @@ function isVisualCompareOwnedAssetName(value) {
2963
2971
  const row = Number(match[1]);
2964
2972
  return Number.isInteger(row) && row >= 1 && row <= tileflowVisualCompareLimits.maximumRows;
2965
2973
  }
2966
- function hasExactKeys(record, keys) {
2967
- const actual = Object.keys(record).sort(compareCodeUnits3);
2974
+ function hasExactKeys(record2, keys) {
2975
+ const actual = Object.keys(record2).sort(compareCodeUnits3);
2968
2976
  const expected = [...keys].sort(compareCodeUnits3);
2969
2977
  return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
2970
2978
  }
@@ -3323,7 +3331,7 @@ function registerOperation(coordinates, name, dependencies, parse3) {
3323
3331
  const controller = (dependencies.createSignalController ?? installSignalAbortController)();
3324
3332
  let local;
3325
3333
  try {
3326
- const request = await resolveRequest(
3334
+ const request2 = await resolveRequest(
3327
3335
  name,
3328
3336
  options,
3329
3337
  parse3,
@@ -3336,7 +3344,7 @@ function registerOperation(coordinates, name, dependencies, parse3) {
3336
3344
  allowDevelopment: options.development,
3337
3345
  signal: controller.signal
3338
3346
  });
3339
- const response = await local[name](request, { signal: controller.signal });
3347
+ const response = await local[name](request2, { signal: controller.signal });
3340
3348
  await local.close();
3341
3349
  local = void 0;
3342
3350
  printDocument(response);
@@ -3428,7 +3436,7 @@ async function readCoordinatesInput(input, signal) {
3428
3436
  async function readStandardInput(signal) {
3429
3437
  const chunks = [];
3430
3438
  let length = 0;
3431
- return new Promise((resolve12, reject) => {
3439
+ return new Promise((resolve13, reject) => {
3432
3440
  const finish = (result) => {
3433
3441
  process.stdin.removeListener("data", onData);
3434
3442
  process.stdin.removeListener("end", onEnd);
@@ -3450,7 +3458,7 @@ async function readStandardInput(signal) {
3450
3458
  }
3451
3459
  chunks.push(bytes);
3452
3460
  };
3453
- const onEnd = () => finish(() => resolve12(decodeInput(Buffer.concat(chunks))));
3461
+ const onEnd = () => finish(() => resolve13(decodeInput(Buffer.concat(chunks))));
3454
3462
  const onError = () => finish(() => reject(new CoordinatesInputError("INVALID_VALUE")));
3455
3463
  if (signal.aborted) {
3456
3464
  onAbort();
@@ -3692,6 +3700,186 @@ function isSafeDeploySourceText(value) {
3692
3700
  return true;
3693
3701
  }
3694
3702
 
3703
+ // src/deployment-command.ts
3704
+ import pc from "picocolors";
3705
+ import { z } from "zod";
3706
+ var deploymentSchema = z.object({
3707
+ active: z.boolean(),
3708
+ createdAt: z.string().min(1).max(64),
3709
+ deploymentId: z.string().min(1).max(128),
3710
+ restoredFromVersion: z.number().int().positive().nullable(),
3711
+ source: z.object({ kind: z.string().min(1).max(32), revision: z.string().max(128).optional() }).strict().nullable(),
3712
+ version: z.number().int().positive()
3713
+ }).strict();
3714
+ var deploymentListResponseSchema = z.object({
3715
+ deployments: z.array(deploymentSchema).max(100),
3716
+ mapId: z.string().regex(/^map_[A-Za-z0-9_-]{16}$/u),
3717
+ schemaVersion: z.literal(1)
3718
+ }).strict();
3719
+ var deploymentRestoreResponseSchema = z.object({
3720
+ changed: z.boolean(),
3721
+ deploymentId: z.string().min(1).max(128),
3722
+ familyUrl: z.string().url().max(2048).optional(),
3723
+ mapId: z.string().regex(/^map_[A-Za-z0-9_-]{16}$/u),
3724
+ restoredFromVersion: z.number().int().positive(),
3725
+ schemaVersion: z.literal(1),
3726
+ version: z.number().int().positive()
3727
+ }).strict();
3728
+ var failureSchema = z.object({ code: z.string().max(80), error: z.string().max(400) }).partial();
3729
+ function registerDeploymentCommands(program2, dependencies) {
3730
+ const deployments = program2.command("deployments").description("List a Map\u2019s retained deployments and restore an earlier one");
3731
+ const withOptions = (command2) => command2.requiredOption("--map-id <id>", "managed Map destination").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Tileflow API key")).option("--json", "print deterministic schema-version-1 JSON");
3732
+ const readHistory = async (api, json2) => {
3733
+ const response = await request(api, "/v1/styles/deployments", "GET");
3734
+ if (!response.ok) {
3735
+ emitHttpFailure(json2, response.status, response.body, api);
3736
+ return null;
3737
+ }
3738
+ const parsed = deploymentListResponseSchema.safeParse(response.body);
3739
+ if (!parsed.success || parsed.data.mapId !== api.mapId) {
3740
+ emitGuidedFailure(
3741
+ json2,
3742
+ "deployment_list_invalid",
3743
+ "The deployment history was not understood.",
3744
+ [{ description: "Update to a CLI version that supports deployments, then try again." }]
3745
+ );
3746
+ return null;
3747
+ }
3748
+ return parsed.data.deployments;
3749
+ };
3750
+ withOptions(
3751
+ deployments.command("list").description("List the retained deployments, newest first")
3752
+ ).action(async (options) => {
3753
+ const api = await dependencies.resolveApi(options, {
3754
+ capabilityScopes: ["status:read"],
3755
+ onFailure: options.json ? (code, message) => emitRestoreFailure(true, code, message) : void 0
3756
+ });
3757
+ if (!api) return;
3758
+ const history = await readHistory(api, options.json);
3759
+ if (!history) return;
3760
+ const earlier = history.find((item) => !item.active);
3761
+ const nextSteps = earlier ? [
3762
+ {
3763
+ description: `Publish an earlier version again, for example version ${earlier.version}. It becomes a new version and nothing is lost.`,
3764
+ command: `tileflow deployments restore ${earlier.version} --map-id ${api.mapId}${apiOriginOption(api.apiUrl)}`
3765
+ }
3766
+ ] : [];
3767
+ if (options.json) {
3768
+ return emitJson({
3769
+ command: "deployments list",
3770
+ deployments: history,
3771
+ nextSteps,
3772
+ schemaVersion: 1
3773
+ });
3774
+ }
3775
+ if (history.length === 0) return console.log(pc.gray("Nothing is published on this Map."));
3776
+ for (const item of history) {
3777
+ const origin = item.restoredFromVersion ? `restored from v${item.restoredFromVersion}` : "";
3778
+ console.log(
3779
+ ` v${item.version} ${item.active ? pc.green("live") : pc.gray("superseded")} ${item.createdAt} ${[item.source?.kind ?? "unknown", origin].filter(Boolean).join(", ")}`
3780
+ );
3781
+ }
3782
+ printNextSteps(nextSteps);
3783
+ });
3784
+ withOptions(
3785
+ deployments.command("restore").description("Publish an earlier deployment again, as a new version of the Map").argument("<version>", "the version to restore")
3786
+ ).action(async (value, options) => {
3787
+ if (!/^[1-9][0-9]{0,9}$/u.test(value)) {
3788
+ return emitRestoreFailure(
3789
+ options.json,
3790
+ "invalid_version",
3791
+ "Version must be a positive integer."
3792
+ );
3793
+ }
3794
+ const version = Number(value);
3795
+ const api = await dependencies.resolveApi(options, {
3796
+ capabilityScopes: ["status:read", "styles:write"],
3797
+ onFailure: options.json ? (code, message) => emitRestoreFailure(true, code, message) : void 0
3798
+ });
3799
+ if (!api) return;
3800
+ const history = await readHistory(api, options.json);
3801
+ if (!history) return;
3802
+ const live = history.find((item) => item.active);
3803
+ if (!live) {
3804
+ return emitGuidedFailure(
3805
+ options.json,
3806
+ "nothing_published",
3807
+ "Nothing is published on this Map.",
3808
+ deploymentRestoreSteps({ code: "nothing_published", mapId: api.mapId, apiUrl: api.apiUrl })
3809
+ );
3810
+ }
3811
+ if (!history.some((item) => item.version === version)) {
3812
+ return emitGuidedFailure(
3813
+ options.json,
3814
+ "deployment_not_found",
3815
+ `Version ${version} is not in this Map's retained history.`,
3816
+ deploymentRestoreSteps({
3817
+ code: "deployment_not_found",
3818
+ mapId: api.mapId,
3819
+ apiUrl: api.apiUrl
3820
+ })
3821
+ );
3822
+ }
3823
+ const response = await request(api, `/v1/styles/deployments/${version}/restore`, "POST", {
3824
+ expectedCurrentVersion: live.version,
3825
+ source: resolveDeploySource(process.env)
3826
+ });
3827
+ if (!response.ok) return emitHttpFailure(options.json, response.status, response.body, api);
3828
+ const parsed = deploymentRestoreResponseSchema.safeParse(response.body);
3829
+ if (!parsed.success || parsed.data.mapId !== api.mapId) {
3830
+ return emitGuidedFailure(
3831
+ options.json,
3832
+ "deployment_restore_unconfirmed",
3833
+ `The restore of version ${version} was not confirmed.`,
3834
+ deploymentRestoreSteps({
3835
+ code: "deployment_restore_unconfirmed",
3836
+ mapId: api.mapId,
3837
+ apiUrl: api.apiUrl
3838
+ })
3839
+ );
3840
+ }
3841
+ const nextSteps = parsed.data.changed ? [
3842
+ {
3843
+ description: `Your repository still holds what version ${live.version} published, so the next deploy publishes it again. Revert the repository to keep this restore.`
3844
+ },
3845
+ {
3846
+ description: `To undo this restore, publish version ${live.version} again.`,
3847
+ command: `tileflow deployments restore ${live.version} --map-id ${api.mapId}${apiOriginOption(api.apiUrl)}`
3848
+ }
3849
+ ] : [];
3850
+ if (options.json) {
3851
+ return emitJson({ command: "deployments restore", nextSteps, ok: true, ...parsed.data });
3852
+ }
3853
+ console.log(
3854
+ parsed.data.changed ? `${pc.green("\u2713")} Version ${version} is live again as version ${parsed.data.version}. Version ${live.version} stays in the history.` : `${pc.green("\u2713")} The content of version ${version} is already live, as version ${parsed.data.version}.`
3855
+ );
3856
+ if (parsed.data.changed) console.log(pc.gray(" New sessions pick it up within minutes."));
3857
+ printNextSteps(nextSteps);
3858
+ });
3859
+ }
3860
+ function request(api, path, method, body) {
3861
+ return requestHostedJson(api.apiUrl, path, {
3862
+ ...body === void 0 ? {} : { body: JSON.stringify(body) },
3863
+ headers: {
3864
+ Authorization: `Bearer ${api.apiKey}`,
3865
+ "X-Tileflow-Map-Id": api.mapId,
3866
+ ...body === void 0 ? {} : { "Content-Type": "application/json" }
3867
+ },
3868
+ method
3869
+ });
3870
+ }
3871
+ function emitHttpFailure(json2, status, body, api) {
3872
+ const failure2 = failureSchema.safeParse(body);
3873
+ const message = failure2.success ? failure2.data.error : void 0;
3874
+ const code = (failure2.success ? failure2.data.code : void 0) ?? `http_${status}`;
3875
+ return emitGuidedFailure(
3876
+ json2,
3877
+ code,
3878
+ message ? `${message.replace(/\.$/u, "")}.` : `The request failed with ${status}.`,
3879
+ deploymentRestoreSteps({ code, mapId: api.mapId, apiUrl: api.apiUrl })
3880
+ );
3881
+ }
3882
+
3695
3883
  // src/describe-command.ts
3696
3884
  import { tileflowAuthoringManifest as tileflowAuthoringManifest2 } from "@tileflow/core";
3697
3885
  import {
@@ -3849,9 +4037,9 @@ function tileflowDevOrigin(host, port) {
3849
4037
  return `http://${isIP(host) === 6 ? `[${host}]` : host}:${port}`;
3850
4038
  }
3851
4039
  var loopbackHosts = /* @__PURE__ */ new Set(["127.0.0.1", "localhost", "[::1]"]);
3852
- function redirectTileflowDevNavigation(request, origin) {
3853
- if (request.method !== "GET" && request.method !== "HEAD") return;
3854
- const url = new URL(request.url);
4040
+ function redirectTileflowDevNavigation(request2, origin) {
4041
+ if (request2.method !== "GET" && request2.method !== "HEAD") return;
4042
+ const url = new URL(request2.url);
3855
4043
  const canonical = new URL(origin);
3856
4044
  if (url.pathname !== "/" || url.origin === canonical.origin || !loopbackHosts.has(url.hostname) || !loopbackHosts.has(canonical.hostname) || url.protocol !== canonical.protocol || url.port !== canonical.port)
3857
4045
  return;
@@ -4021,7 +4209,9 @@ function prepareTileflowHostedThemeFamily(mapName, map, styles) {
4021
4209
  Object.entries(styles).sort(([left], [right]) => compareCodeUnits4(left, right)).map(([themeName, style]) => [themeName, cloneStyle(style)])
4022
4210
  );
4023
4211
  const teamSources = Object.fromEntries(
4024
- Object.entries(map.sources ?? {}).sort(([left], [right]) => compareCodeUnits4(left, right)).map(([sourceId, source]) => {
4212
+ Object.entries(map.sources ?? {}).flatMap(
4213
+ ([sourceId, source]) => source.kind === "hosted-tileset" ? [[sourceId, source]] : []
4214
+ ).sort(([left], [right]) => compareCodeUnits4(left, right)).map(([sourceId, source]) => {
4025
4215
  for (const [themeName, style] of Object.entries(prepared)) {
4026
4216
  const definition = style.sources[sourceId];
4027
4217
  if (!definition) {
@@ -4415,7 +4605,7 @@ async function runHostedNativeDeploy(options, ports) {
4415
4605
 
4416
4606
  // src/icon-diff-command.ts
4417
4607
  import { dirname as dirname7, resolve as resolve6 } from "path";
4418
- import { z } from "zod";
4608
+ import { z as z2 } from "zod";
4419
4609
  import {
4420
4610
  diffTileflowIconPackageManifests,
4421
4611
  hashTileflowIconPackageManifest,
@@ -4812,7 +5002,7 @@ function technicalDetails(document, beforeIconCount, afterIconCount) {
4812
5002
  ["Icon count", String(beforeIconCount), String(afterIconCount)]
4813
5003
  ];
4814
5004
  return `<details class="technical-details"><summary>Details</summary><div class="details-content"><p class="details-context">${escapeHtml2(context)}</p><table class="details-table"><thead><tr><th>Metric</th><th>Before</th><th>Next</th></tr></thead><tbody>${rows.map(
4815
- ([metric, previous, proposed]) => `<tr><td>${escapeHtml2(metric ?? "")}</td><td><code>${escapeHtml2(previous ?? "")}</code></td><td><code>${escapeHtml2(proposed ?? "")}</code></td></tr>`
5005
+ ([metric2, previous, proposed]) => `<tr><td>${escapeHtml2(metric2 ?? "")}</td><td><code>${escapeHtml2(previous ?? "")}</code></td><td><code>${escapeHtml2(proposed ?? "")}</code></td></tr>`
4816
5006
  ).join("")}</tbody></table></div></details>`;
4817
5007
  }
4818
5008
  function imageDefinitions(before, after) {
@@ -4934,56 +5124,56 @@ function isMissingFile3(error) {
4934
5124
 
4935
5125
  // src/icon-diff-command.ts
4936
5126
  var environmentSchema = tileflowMapIdSchema;
4937
- var publicHttpUrlSchema = z.url().max(2048).refine((value) => {
5127
+ var publicHttpUrlSchema = z2.url().max(2048).refine((value) => {
4938
5128
  const url = new URL(value);
4939
5129
  return (url.protocol === "http:" || url.protocol === "https:") && !url.username && !url.password;
4940
5130
  });
4941
- var iconPackageBaselineResponseSchema = z.object({
4942
- baseline: z.object({
4943
- deployedAt: z.iso.datetime({ offset: true }),
4944
- deploymentId: z.string().min(1).max(128),
4945
- package: z.object({
5131
+ var iconPackageBaselineResponseSchema = z2.object({
5132
+ baseline: z2.object({
5133
+ deployedAt: z2.iso.datetime({ offset: true }),
5134
+ deploymentId: z2.string().min(1).max(128),
5135
+ package: z2.object({
4946
5136
  contentHash: tileflowIconPackageContentHashSchema,
4947
5137
  label: tileflowIconPackageLabelSchema,
4948
5138
  manifest: tileflowIconPackageManifestSchema,
4949
5139
  spriteUrl: publicHttpUrlSchema,
4950
- totalBytes: z.number().int().positive().max(tileflowIconPackageLimits.maxGeneratedPackageBytes)
5140
+ totalBytes: z2.number().int().positive().max(tileflowIconPackageLimits.maxGeneratedPackageBytes)
4951
5141
  }).strict().nullable(),
4952
- version: z.number().int().positive()
5142
+ version: z2.number().int().positive()
4953
5143
  }).strict().nullable(),
4954
5144
  environment: environmentSchema,
4955
- schemaVersion: z.literal(1)
5145
+ schemaVersion: z2.literal(1)
4956
5146
  }).strict();
4957
- var packageSummarySchema = z.object({
5147
+ var packageSummarySchema = z2.object({
4958
5148
  contentHash: tileflowIconPackageContentHashSchema,
4959
- iconCount: z.number().int().nonnegative().max(tileflowIconPackageLimits.maxIconCount),
5149
+ iconCount: z2.number().int().nonnegative().max(tileflowIconPackageLimits.maxIconCount),
4960
5150
  label: tileflowIconPackageLabelSchema,
4961
- totalBytes: z.number().int().nonnegative().max(tileflowIconPackageLimits.maxGeneratedPackageBytes)
5151
+ totalBytes: z2.number().int().nonnegative().max(tileflowIconPackageLimits.maxGeneratedPackageBytes)
4962
5152
  }).strict();
4963
- var tileflowIconDiffDocumentSchema = z.object({
4964
- schemaVersion: z.literal(1),
5153
+ var tileflowIconDiffDocumentSchema = z2.object({
5154
+ schemaVersion: z2.literal(1),
4965
5155
  environment: environmentSchema,
4966
- baseline: z.object({
4967
- deploymentId: z.string(),
5156
+ baseline: z2.object({
5157
+ deploymentId: z2.string(),
4968
5158
  package: packageSummarySchema.nullable(),
4969
- version: z.number().int().positive()
5159
+ version: z2.number().int().positive()
4970
5160
  }).strict().nullable(),
4971
- proposed: z.object({
5161
+ proposed: z2.object({
4972
5162
  package: packageSummarySchema.nullable()
4973
5163
  }).strict(),
4974
- icons: z.object({
4975
- added: z.array(z.string()),
4976
- removed: z.array(z.string()),
4977
- modified: z.array(z.string()),
4978
- unchangedCount: z.number().int().nonnegative()
5164
+ icons: z2.object({
5165
+ added: z2.array(z2.string()),
5166
+ removed: z2.array(z2.string()),
5167
+ modified: z2.array(z2.string()),
5168
+ unchangedCount: z2.number().int().nonnegative()
4979
5169
  }).strict(),
4980
- generatedBytes: z.object({
4981
- before: z.number().int().nonnegative(),
4982
- after: z.number().int().nonnegative(),
4983
- delta: z.number().int()
5170
+ generatedBytes: z2.object({
5171
+ before: z2.number().int().nonnegative(),
5172
+ after: z2.number().int().nonnegative(),
5173
+ delta: z2.number().int()
4984
5174
  }).strict(),
4985
- artifacts: z.object({ report: z.string().nullable() }).strict(),
4986
- hasChanges: z.boolean()
5175
+ artifacts: z2.object({ report: z2.string().nullable() }).strict(),
5176
+ hasChanges: z2.boolean()
4987
5177
  }).strict();
4988
5178
  function registerIconDiffCommand(icons, dependencies) {
4989
5179
  icons.command("diff").description("Preview a local icon package against an active hosted revision").requiredOption("--against <environment>", "named map environment to compare").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Tileflow API key")).requiredOption("--map-id <id>", "managed Map destination").option("--json", "print deterministic schema-version-1 JSON").option("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").option("--report <path>", "write a self-contained HTML visual report").option("--open", "open an explicitly requested report").option("--force", "replace a different explicitly requested report").action(async (options) => {
@@ -5316,7 +5506,7 @@ function sameContributors(left, right) {
5316
5506
 
5317
5507
  // src/icon-lock-command.ts
5318
5508
  import { dirname as dirname9 } from "path";
5319
- import pc2 from "picocolors";
5509
+ import pc3 from "picocolors";
5320
5510
  import {
5321
5511
  collectTileflowIconSetReferences,
5322
5512
  compareCodeUnits as compareCodeUnits7,
@@ -5334,8 +5524,8 @@ import { readTileflowIconsLockfileText, writeTileflowIconsLockfile } from "@tile
5334
5524
 
5335
5525
  // src/icon-set-command.ts
5336
5526
  import { isAbsolute as isAbsolute3 } from "path";
5337
- import pc from "picocolors";
5338
- import { z as z2 } from "zod";
5527
+ import pc2 from "picocolors";
5528
+ import { z as z3 } from "zod";
5339
5529
  import {
5340
5530
  tileflowIconPackageLimits as tileflowIconPackageLimits2,
5341
5531
  tileflowIconSetPackageIdSchema,
@@ -5399,7 +5589,7 @@ function registerIconSetCommands(program2, dependencies) {
5399
5589
  "PUT",
5400
5590
  { body, headers: { "Idempotency-Key": options.idempotencyKey } }
5401
5591
  );
5402
- if (!response.ok) return emitHttpFailure(options.json, response.status, "publish");
5592
+ if (!response.ok) return emitHttpFailure2(options.json, response.status, "publish");
5403
5593
  const parsed = publishResponseSchema.parse(response.body);
5404
5594
  if (parsed.revision.pin && parsed.revision.pin.contentHash !== iconPackage.contentHash) {
5405
5595
  return emitFailure(
@@ -5460,7 +5650,7 @@ function registerIconSetCommands(program2, dependencies) {
5460
5650
  };
5461
5651
  if (options.json) return emitJson(document);
5462
5652
  console.log(
5463
- `${pc.green("\u2713")} ${parsed.changed ? "Published" : "Unchanged"} ${pc.bold(parsed.revision.reference)} revision ${parsed.revision.version}.`
5653
+ `${pc2.green("\u2713")} ${parsed.changed ? "Published" : "Unchanged"} ${pc2.bold(parsed.revision.reference)} revision ${parsed.revision.version}.`
5464
5654
  );
5465
5655
  } catch (error) {
5466
5656
  emitFailure(options.json, "icon_set_publish_failed", boundedMessage(error, "publish"));
@@ -5488,10 +5678,10 @@ function registerIconSetCommands(program2, dependencies) {
5488
5678
  team: describeTeam(authority)
5489
5679
  };
5490
5680
  if (options.json) return emitJson(document);
5491
- if (sets.length === 0) return console.log(pc.gray("No Team Icon Sets."));
5681
+ if (sets.length === 0) return console.log(pc2.gray("No Team Icon Sets."));
5492
5682
  for (const set of sets) {
5493
5683
  console.log(
5494
- ` ${pc.bold(set.reference)} ${pc.gray(set.archivedAt ? "archived" : `v${String(set.latestVersion ?? 0)}`)}`
5684
+ ` ${pc2.bold(set.reference)} ${pc2.gray(set.archivedAt ? "archived" : `v${String(set.latestVersion ?? 0)}`)}`
5495
5685
  );
5496
5686
  }
5497
5687
  } catch (error) {
@@ -5573,7 +5763,7 @@ function registerIconSetCommands(program2, dependencies) {
5573
5763
  headers: { "Content-Type": "application/json" }
5574
5764
  }
5575
5765
  );
5576
- if (!response.ok) return emitHttpFailure(options.json, response.status, name);
5766
+ if (!response.ok) return emitHttpFailure2(options.json, response.status, name);
5577
5767
  const parsed = catalogDetailResponseSchema.parse(response.body);
5578
5768
  const document = {
5579
5769
  command: `icon-set ${name}`,
@@ -5583,7 +5773,7 @@ function registerIconSetCommands(program2, dependencies) {
5583
5773
  };
5584
5774
  if (options.json) return emitJson(document);
5585
5775
  console.log(
5586
- `${pc.green("\u2713")} ${archived ? "Archived" : "Restored"} ${pc.bold(parsed.set.reference)}.`
5776
+ `${pc2.green("\u2713")} ${archived ? "Archived" : "Restored"} ${pc2.bold(parsed.set.reference)}.`
5587
5777
  );
5588
5778
  } catch (error) {
5589
5779
  emitFailure(options.json, `icon_set_${name}_failed`, boundedMessage(error, name));
@@ -5654,7 +5844,7 @@ function registerIconSetCommands(program2, dependencies) {
5654
5844
  }
5655
5845
  }
5656
5846
  );
5657
- if (!response.ok) return emitHttpFailure(options.json, response.status, "purge");
5847
+ if (!response.ok) return emitHttpFailure2(options.json, response.status, "purge");
5658
5848
  const parsed = purgeResponseSchema.parse(response.body);
5659
5849
  const document = {
5660
5850
  command: "icon-set purge",
@@ -5663,8 +5853,8 @@ function registerIconSetCommands(program2, dependencies) {
5663
5853
  team: describeTeam(authority)
5664
5854
  };
5665
5855
  if (options.json) return emitJson(document);
5666
- console.log(`${pc.green("\u2713")} Purged ${pc.bold(options.confirm)}.`);
5667
- console.log(pc.yellow(`! ${parsed.result.warning}`));
5856
+ console.log(`${pc2.green("\u2713")} Purged ${pc2.bold(options.confirm)}.`);
5857
+ console.log(pc2.yellow(`! ${parsed.result.warning}`));
5668
5858
  } catch (error) {
5669
5859
  emitFailure(options.json, "icon_set_purge_failed", boundedMessage(error, "purge"));
5670
5860
  }
@@ -5678,7 +5868,7 @@ function registerIconSetCommands(program2, dependencies) {
5678
5868
  const authority = await resolveTeamAuthority(options, ["icons:read"], dependencies);
5679
5869
  if (!authority) return;
5680
5870
  const result = await run(authority);
5681
- if ("status" in result) return emitHttpFailure(options.json, result.status, command2);
5871
+ if ("status" in result) return emitHttpFailure2(options.json, result.status, command2);
5682
5872
  const document = {
5683
5873
  command: command2,
5684
5874
  schemaVersion: 1,
@@ -5736,7 +5926,7 @@ var IconSetTransportError = class extends Error {
5736
5926
  }
5737
5927
  status;
5738
5928
  };
5739
- function emitHttpFailure(json2, status, command2) {
5929
+ function emitHttpFailure2(json2, status, command2) {
5740
5930
  emitFailure(json2, `http_${status}`, `Icon Set ${command2} failed (${status}).`);
5741
5931
  }
5742
5932
  function boundedMessage(error, command2) {
@@ -5781,27 +5971,27 @@ function isIconSetSlug(value) {
5781
5971
  function isIdempotencyKey(value) {
5782
5972
  return value.length >= 8 && value.length <= 128 && /^[A-Za-z0-9._:-]+$/u.test(value);
5783
5973
  }
5784
- var cursorSchema = z2.string().min(1).max(2048).nullable();
5785
- var isoDateSchema = z2.iso.datetime({ offset: true });
5786
- var setSchema = z2.object({
5787
- id: z2.string().regex(/^ics_[A-Za-z0-9_-]{16}$/u),
5974
+ var cursorSchema = z3.string().min(1).max(2048).nullable();
5975
+ var isoDateSchema = z3.iso.datetime({ offset: true });
5976
+ var setSchema = z3.object({
5977
+ id: z3.string().regex(/^ics_[A-Za-z0-9_-]{16}$/u),
5788
5978
  reference: tileflowIconSetReferenceSchema,
5789
- name: z2.string().max(200),
5790
- description: z2.string().max(2e3),
5979
+ name: z3.string().max(200),
5980
+ description: z3.string().max(2e3),
5791
5981
  archivedAt: isoDateSchema.nullable(),
5792
- latestVersion: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
5982
+ latestVersion: z3.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
5793
5983
  createdAt: isoDateSchema,
5794
5984
  updatedAt: isoDateSchema
5795
5985
  }).strict();
5796
- var revisionBaseSchema = z2.object({
5797
- id: z2.string().regex(/^icv_[A-Za-z0-9_-]{16}$/u),
5986
+ var revisionBaseSchema = z3.object({
5987
+ id: z3.string().regex(/^icv_[A-Za-z0-9_-]{16}$/u),
5798
5988
  reference: tileflowIconSetReferenceSchema,
5799
- version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
5989
+ version: z3.number().int().positive().max(Number.MAX_SAFE_INTEGER),
5800
5990
  publishedAt: isoDateSchema,
5801
5991
  purgedAt: isoDateSchema.nullable(),
5802
- iconCount: z2.number().int().min(0).max(tileflowIconPackageLimits2.maxIconCount),
5803
- totalBytes: z2.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
5804
- publisher: z2.object({ kind: z2.enum(["credential", "membership"]), id: z2.string().max(200).nullable() }).strict()
5992
+ iconCount: z3.number().int().min(0).max(tileflowIconPackageLimits2.maxIconCount),
5993
+ totalBytes: z3.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
5994
+ publisher: z3.object({ kind: z3.enum(["credential", "membership"]), id: z3.string().max(200).nullable() }).strict()
5805
5995
  }).strict();
5806
5996
  var revisionSchema = revisionBaseSchema.extend({ pin: tileflowIconSetPinSchema.nullable() }).transform(({ id, reference, version, publishedAt, purgedAt, iconCount, totalBytes, pin }) => ({
5807
5997
  iconCount,
@@ -5825,53 +6015,53 @@ var revisionSummarySchema = revisionBaseSchema.extend({ packageId: tileflowIconS
5825
6015
  version
5826
6016
  })
5827
6017
  );
5828
- var catalogListResponseSchema = z2.object({
5829
- schemaVersion: z2.literal(1),
5830
- sets: z2.array(setSchema).max(pageSize),
6018
+ var catalogListResponseSchema = z3.object({
6019
+ schemaVersion: z3.literal(1),
6020
+ sets: z3.array(setSchema).max(pageSize),
5831
6021
  nextCursor: cursorSchema.optional()
5832
6022
  }).strict();
5833
- var catalogDetailResponseSchema = z2.object({ schemaVersion: z2.literal(1), set: setSchema }).strict();
5834
- var versionsResponseSchema = z2.object({
5835
- schemaVersion: z2.literal(1),
5836
- versions: z2.array(revisionSummarySchema).max(pageSize),
6023
+ var catalogDetailResponseSchema = z3.object({ schemaVersion: z3.literal(1), set: setSchema }).strict();
6024
+ var versionsResponseSchema = z3.object({
6025
+ schemaVersion: z3.literal(1),
6026
+ versions: z3.array(revisionSummarySchema).max(pageSize),
5837
6027
  nextCursor: cursorSchema.optional()
5838
6028
  }).strict();
5839
- var revisionResponseSchema = z2.object({
5840
- schemaVersion: z2.literal(1),
5841
- archived: z2.boolean(),
6029
+ var revisionResponseSchema = z3.object({
6030
+ schemaVersion: z3.literal(1),
6031
+ archived: z3.boolean(),
5842
6032
  revision: revisionSchema
5843
6033
  }).strict();
5844
- var publishResponseSchema = z2.object({ schemaVersion: z2.literal(1), changed: z2.boolean(), revision: revisionSchema }).strict();
5845
- var usesResponseSchema = z2.object({
5846
- schemaVersion: z2.literal(1),
5847
- uses: z2.array(
5848
- z2.object({
5849
- deploymentId: z2.string().min(1).max(160),
5850
- deploymentVersion: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
5851
- mapId: z2.string().min(1).max(160),
5852
- mapName: z2.string().max(200).nullable(),
5853
- mapState: z2.string().max(64),
5854
- versionId: z2.string().regex(/^icv_[A-Za-z0-9_-]{16}$/u),
5855
- version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
5856
- ordinal: z2.number().int().min(0).max(31),
5857
- relationship: z2.enum(["active", "retained"])
6034
+ var publishResponseSchema = z3.object({ schemaVersion: z3.literal(1), changed: z3.boolean(), revision: revisionSchema }).strict();
6035
+ var usesResponseSchema = z3.object({
6036
+ schemaVersion: z3.literal(1),
6037
+ uses: z3.array(
6038
+ z3.object({
6039
+ deploymentId: z3.string().min(1).max(160),
6040
+ deploymentVersion: z3.number().int().positive().max(Number.MAX_SAFE_INTEGER),
6041
+ mapId: z3.string().min(1).max(160),
6042
+ mapName: z3.string().max(200).nullable(),
6043
+ mapState: z3.string().max(64),
6044
+ versionId: z3.string().regex(/^icv_[A-Za-z0-9_-]{16}$/u),
6045
+ version: z3.number().int().positive().max(Number.MAX_SAFE_INTEGER),
6046
+ ordinal: z3.number().int().min(0).max(31),
6047
+ relationship: z3.enum(["active", "retained"])
5858
6048
  }).strict()
5859
6049
  ).max(pageSize),
5860
6050
  nextCursor: cursorSchema.optional(),
5861
- coverage: z2.literal("known-hosted-deployments"),
5862
- includesUndeployedRepositoryLocks: z2.literal(false)
6051
+ coverage: z3.literal("known-hosted-deployments"),
6052
+ includesUndeployedRepositoryLocks: z3.literal(false)
5863
6053
  }).strict();
5864
- var purgeResponseSchema = z2.object({
5865
- schemaVersion: z2.literal(1),
5866
- result: z2.object({
5867
- schemaVersion: z2.literal(1),
5868
- versionId: z2.string().regex(/^icv_[A-Za-z0-9_-]{16}$/u),
5869
- version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
5870
- purged: z2.boolean(),
5871
- knownRetainedDeployments: z2.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
5872
- physicalBytesReleased: z2.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
5873
- unknownRepositoryLocks: z2.boolean(),
5874
- warning: z2.string().max(400)
6054
+ var purgeResponseSchema = z3.object({
6055
+ schemaVersion: z3.literal(1),
6056
+ result: z3.object({
6057
+ schemaVersion: z3.literal(1),
6058
+ versionId: z3.string().regex(/^icv_[A-Za-z0-9_-]{16}$/u),
6059
+ version: z3.number().int().positive().max(Number.MAX_SAFE_INTEGER),
6060
+ purged: z3.boolean(),
6061
+ knownRetainedDeployments: z3.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
6062
+ physicalBytesReleased: z3.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
6063
+ unknownRepositoryLocks: z3.boolean(),
6064
+ warning: z3.string().max(400)
5875
6065
  }).strict()
5876
6066
  }).strict();
5877
6067
 
@@ -5986,14 +6176,14 @@ function registerIconLockCommands(program2, dependencies) {
5986
6176
  }))
5987
6177
  };
5988
6178
  if (options.json) return emitJson(document);
5989
- if (document.sets.length === 0) return console.log(pc2.gray("No Icon Set references declared."));
6179
+ if (document.sets.length === 0) return console.log(pc3.gray("No Icon Set references declared."));
5990
6180
  for (const entry of document.sets) {
5991
6181
  console.log(
5992
- ` ${pc2.bold(entry.reference)} ${pc2.gray(`v${String(entry.version)}`)}${entry.changed ? "" : pc2.gray(" (unchanged)")}`
6182
+ ` ${pc3.bold(entry.reference)} ${pc3.gray(`v${String(entry.version)}`)}${entry.changed ? "" : pc3.gray(" (unchanged)")}`
5993
6183
  );
5994
6184
  }
5995
6185
  for (const reference of missing) {
5996
- console.log(pc2.yellow(`! ${reference} is declared but not locked.`));
6186
+ console.log(pc3.yellow(`! ${reference} is declared but not locked.`));
5997
6187
  }
5998
6188
  }
5999
6189
  async function resolvePin(authority, reference, version) {
@@ -6109,12 +6299,12 @@ async function readCurrentPins(contents) {
6109
6299
  }
6110
6300
 
6111
6301
  // src/map-commands.ts
6112
- import pc4 from "picocolors";
6302
+ import pc5 from "picocolors";
6113
6303
 
6114
6304
  // src/map-setup.ts
6115
6305
  import { randomBytes } from "crypto";
6116
6306
  import { createInterface } from "readline/promises";
6117
- import pc3 from "picocolors";
6307
+ import pc4 from "picocolors";
6118
6308
  var maximumOrigins = 20;
6119
6309
  var exactHost = /^(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*|\[[0-9a-f:.]+\])$/u;
6120
6310
  var teamReference = /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u;
@@ -6252,9 +6442,9 @@ function createTerminalPrompts(json2 = false) {
6252
6442
  if (json2 || !process.stdin.isTTY || !process.stdout.isTTY) return null;
6253
6443
  return {
6254
6444
  async chooseTeam(teams) {
6255
- console.log(`${pc3.cyan("?")} Which Team should own the new Map?`);
6445
+ console.log(`${pc4.cyan("?")} Which Team should own the new Map?`);
6256
6446
  teams.forEach(
6257
- (team, index) => console.log(` ${index + 1}) ${team.name} ${pc3.gray(`@${team.slug}`)}`)
6447
+ (team, index) => console.log(` ${index + 1}) ${team.name} ${pc4.gray(`@${team.slug}`)}`)
6258
6448
  );
6259
6449
  const answer = await ask("Team number", (value) => {
6260
6450
  const index = Number(value);
@@ -6263,7 +6453,7 @@ function createTerminalPrompts(json2 = false) {
6263
6453
  return answer;
6264
6454
  },
6265
6455
  async chooseAccess() {
6266
- console.log(`${pc3.cyan("?")} Which websites may load this Map in a browser?`);
6456
+ console.log(`${pc4.cyan("?")} Which websites may load this Map in a browser?`);
6267
6457
  console.log(" 1) Only selected websites (recommended)");
6268
6458
  console.log(" 2) Any website");
6269
6459
  const mode = await ask(
@@ -6274,7 +6464,7 @@ function createTerminalPrompts(json2 = false) {
6274
6464
  if (mode === "any") return { kind: "any" };
6275
6465
  return ask("Website origins, separated by spaces (https://example.com)", (value) => {
6276
6466
  const parsed = parseOriginList(value.split(/[\s,]+/u).filter(Boolean));
6277
- if (!parsed.ok) console.log(` ${pc3.yellow(parsed.error)}`);
6467
+ if (!parsed.ok) console.log(` ${pc4.yellow(parsed.error)}`);
6278
6468
  return parsed.ok ? parsed.value : null;
6279
6469
  });
6280
6470
  }
@@ -6329,8 +6519,8 @@ function registerMapCommands(program2, dependencies) {
6329
6519
  }
6330
6520
  printCreatedMap(result.value);
6331
6521
  console.log(`
6332
- ${pc4.bold("Next step")}`);
6333
- console.log(` ${pc4.cyan("\u2022")} Publish the repository map with ${pc4.cyan(next)}.`);
6522
+ ${pc5.bold("Next step")}`);
6523
+ console.log(` ${pc5.cyan("\u2022")} Publish the repository map with ${pc5.cyan(next)}.`);
6334
6524
  });
6335
6525
  }
6336
6526
  function addMapSetupOptions(command2) {
@@ -6346,7 +6536,7 @@ function hasMapSetupOptions(options) {
6346
6536
  async function createMapForDeploy(options, dependencies) {
6347
6537
  const session = await loadSession(options, dependencies);
6348
6538
  if (!session) return null;
6349
- console.log(`${pc4.cyan("i")} No --map-id given; creating a managed Map for this deploy.`);
6539
+ console.log(`${pc5.cyan("i")} No --map-id given; creating a managed Map for this deploy.`);
6350
6540
  const result = await setUpManagedMap(session, options, {
6351
6541
  prompts: dependencies.prompts === void 0 ? createTerminalPrompts() : dependencies.prompts
6352
6542
  });
@@ -6356,7 +6546,7 @@ async function createMapForDeploy(options, dependencies) {
6356
6546
  }
6357
6547
  printCreatedMap(result.value);
6358
6548
  console.log(
6359
- pc4.gray(
6549
+ pc5.gray(
6360
6550
  `If this deploy is interrupted, rerun it with --setup-key ${result.setupKey} to reuse the same Map.`
6361
6551
  )
6362
6552
  );
@@ -6364,10 +6554,10 @@ async function createMapForDeploy(options, dependencies) {
6364
6554
  }
6365
6555
  function printCreatedMap(created) {
6366
6556
  console.log(
6367
- `${pc4.green("\u2713")} Created ${pc4.bold(created.map.id)} in ${created.team.name} ${pc4.gray(`@${created.team.slug}`)}.`
6557
+ `${pc5.green("\u2713")} Created ${pc5.bold(created.map.id)} in ${created.team.name} ${pc5.gray(`@${created.team.slug}`)}.`
6368
6558
  );
6369
6559
  console.log(
6370
- `${pc4.gray("Websites:".padEnd(13))} ${describeBrowserAccess(created.allowedOrigins)}`
6560
+ `${pc5.gray("Websites:".padEnd(13))} ${describeBrowserAccess(created.allowedOrigins)}`
6371
6561
  );
6372
6562
  }
6373
6563
  async function loadSession(options, dependencies) {
@@ -6400,8 +6590,8 @@ async function loadSession(options, dependencies) {
6400
6590
  // src/model-command.ts
6401
6591
  import { lstat as lstat5 } from "fs/promises";
6402
6592
  import { basename as basename4, dirname as dirname10, relative as relative4, resolve as resolve7 } from "path";
6403
- import pc5 from "picocolors";
6404
- import { z as z3 } from "zod";
6593
+ import pc6 from "picocolors";
6594
+ import { z as z4 } from "zod";
6405
6595
  import { tileflowModelContentType } from "@tileflow/core";
6406
6596
  import {
6407
6597
  assertValidTileflowConfig as assertValidTileflowConfig3,
@@ -6464,7 +6654,7 @@ function registerModelCommands(program2, dependencies) {
6464
6654
  "PUT",
6465
6655
  { body }
6466
6656
  );
6467
- if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
6657
+ if (!response.ok) return emitHttpFailure3(options.json, response.status, response.body, name);
6468
6658
  const parsed = publishResponseSchema2.safeParse(response.body);
6469
6659
  if (!parsed.success || parsed.data.model.current?.contentHash !== model.sha256) {
6470
6660
  return emitFailure(
@@ -6478,7 +6668,7 @@ function registerModelCommands(program2, dependencies) {
6478
6668
  const current = parsed.data.model.current;
6479
6669
  const action = parsed.data.changed ? "Published" : "Unchanged";
6480
6670
  console.log(
6481
- `${pc5.green("\u2713")} ${action} 3D model ${pc5.bold(name)} version ${current.version} (${formatBytes3(current.byteLength)}, ${formatCount(current.summary.levels[0]?.triangles ?? 0)} triangles).`
6671
+ `${pc6.green("\u2713")} ${action} 3D model ${pc6.bold(name)} version ${current.version} (${formatBytes3(current.byteLength)}, ${formatCount(current.summary.levels[0]?.triangles ?? 0)} triangles).`
6482
6672
  );
6483
6673
  }
6484
6674
  }
@@ -6510,7 +6700,7 @@ function registerModelCommands(program2, dependencies) {
6510
6700
  `/v1/models${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`,
6511
6701
  "GET"
6512
6702
  );
6513
- if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body);
6703
+ if (!response.ok) return emitHttpFailure3(options.json, response.status, response.body);
6514
6704
  const parsed = listResponseSchema.safeParse(response.body);
6515
6705
  if (!parsed.success)
6516
6706
  return emitFailure(options.json, "invalid_response", "The model list was not readable.");
@@ -6527,7 +6717,7 @@ function registerModelCommands(program2, dependencies) {
6527
6717
  for (const model of listed) {
6528
6718
  const current = model.current;
6529
6719
  console.log(
6530
- `${pc5.bold(model.name)} ${current ? `version ${current.version}, ${formatBytes3(current.byteLength)}` : "no current version"} ${model.usedByMaps === 1 ? "1 map" : `${model.usedByMaps} maps`}${model.state === "purging" ? pc5.yellow(" purging") : ""}`
6720
+ `${pc6.bold(model.name)} ${current ? `version ${current.version}, ${formatBytes3(current.byteLength)}` : "no current version"} ${model.usedByMaps === 1 ? "1 map" : `${model.usedByMaps} maps`}${model.state === "purging" ? pc6.yellow(" purging") : ""}`
6531
6721
  );
6532
6722
  }
6533
6723
  }
@@ -6544,7 +6734,7 @@ function registerModelCommands(program2, dependencies) {
6544
6734
  if (options.json) return emitJson({ ...detail, command: "models status", ok: true });
6545
6735
  const { model, versions, uses } = detail;
6546
6736
  console.log(
6547
- `${pc5.bold(model.name)}${model.state === "purging" ? pc5.yellow(" (purging)") : ""}`
6737
+ `${pc6.bold(model.name)}${model.state === "purging" ? pc6.yellow(" (purging)") : ""}`
6548
6738
  );
6549
6739
  for (const version of versions) {
6550
6740
  const role = version.state === "current" ? "current" : version.state === "previous" ? "previous" : version.state;
@@ -6582,7 +6772,7 @@ function registerModelCommands(program2, dependencies) {
6582
6772
  headers: { "Content-Type": "application/json" }
6583
6773
  }
6584
6774
  );
6585
- if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
6775
+ if (!response.ok) return emitHttpFailure3(options.json, response.status, response.body, name);
6586
6776
  const parsed = restoreResponseSchema.safeParse(response.body);
6587
6777
  if (!parsed.success || parsed.data.model.current?.version !== previous.version) {
6588
6778
  return emitFailure(
@@ -6599,7 +6789,7 @@ function registerModelCommands(program2, dependencies) {
6599
6789
  model: parsed.data.model
6600
6790
  });
6601
6791
  console.log(
6602
- `${pc5.green("\u2713")} ${pc5.bold(name)} is at version ${previous.version} again; version ${current.version} is its previous one. Maps draw it on their next load.`
6792
+ `${pc6.green("\u2713")} ${pc6.bold(name)} is at version ${previous.version} again; version ${current.version} is its previous one. Maps draw it on their next load.`
6603
6793
  );
6604
6794
  });
6605
6795
  withTeamOptions2(
@@ -6624,7 +6814,7 @@ function registerModelCommands(program2, dependencies) {
6624
6814
  headers: { "X-Tileflow-Confirm-Model-Purge": name }
6625
6815
  }
6626
6816
  );
6627
- if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
6817
+ if (!response.ok) return emitHttpFailure3(options.json, response.status, response.body, name);
6628
6818
  const parsed = purgeResponseSchema2.safeParse(response.body);
6629
6819
  if (!parsed.success)
6630
6820
  return emitFailure(options.json, "invalid_response", "The purge was not confirmed.");
@@ -6636,7 +6826,7 @@ function registerModelCommands(program2, dependencies) {
6636
6826
  purge: parsed.data.purge
6637
6827
  });
6638
6828
  console.log(
6639
- `${pc5.green("\u2713")} ${pc5.bold(name)} is being purged; its name is free once its files are deleted.`
6829
+ `${pc6.green("\u2713")} ${pc6.bold(name)} is being purged; its name is free once its files are deleted.`
6640
6830
  );
6641
6831
  });
6642
6832
  }
@@ -6650,7 +6840,7 @@ async function readDetail(authority, name, json2) {
6650
6840
  "GET"
6651
6841
  );
6652
6842
  if (!response.ok) {
6653
- emitHttpFailure2(json2, response.status, response.body, name);
6843
+ emitHttpFailure3(json2, response.status, response.body, name);
6654
6844
  return void 0;
6655
6845
  }
6656
6846
  const parsed = detailResponseSchema.safeParse(response.body);
@@ -6728,8 +6918,8 @@ function addModel(models, name, model) {
6728
6918
  throw new Error(`Two different models are named ${name}; a Team has one model of each name.`);
6729
6919
  models.set(name, model);
6730
6920
  }
6731
- function emitHttpFailure2(json2, status, body, name) {
6732
- const error = z3.object({ error: z3.object({ code: z3.string().max(80), message: z3.string().max(400) }).partial() }).partial().safeParse(body);
6921
+ function emitHttpFailure3(json2, status, body, name) {
6922
+ const error = z4.object({ error: z4.object({ code: z4.string().max(80), message: z4.string().max(400) }).partial() }).partial().safeParse(body);
6733
6923
  const message = error.success ? error.data.error?.message : void 0;
6734
6924
  const code = error.success ? error.data.error?.code : void 0;
6735
6925
  if (status === 404)
@@ -6752,64 +6942,64 @@ function formatBytes3(bytes) {
6752
6942
  function formatCount(count) {
6753
6943
  return new Intl.NumberFormat("en-US").format(count);
6754
6944
  }
6755
- var summarySchema = z3.object({
6756
- levels: z3.array(
6757
- z3.object({
6758
- level: z3.enum(["near", "mid", "far"]),
6759
- vertices: z3.number().int().nonnegative(),
6760
- triangles: z3.number().int().nonnegative()
6945
+ var summarySchema = z4.object({
6946
+ levels: z4.array(
6947
+ z4.object({
6948
+ level: z4.enum(["near", "mid", "far"]),
6949
+ vertices: z4.number().int().nonnegative(),
6950
+ triangles: z4.number().int().nonnegative()
6761
6951
  }).strict()
6762
6952
  ).min(1).max(3),
6763
- width: z3.number().nonnegative(),
6764
- depth: z3.number().nonnegative(),
6765
- height: z3.number().nonnegative(),
6766
- footprint: z3.object({
6767
- rings: z3.number().int().nonnegative(),
6768
- points: z3.number().int().nonnegative(),
6769
- area: z3.number().nonnegative()
6953
+ width: z4.number().nonnegative(),
6954
+ depth: z4.number().nonnegative(),
6955
+ height: z4.number().nonnegative(),
6956
+ footprint: z4.object({
6957
+ rings: z4.number().int().nonnegative(),
6958
+ points: z4.number().int().nonnegative(),
6959
+ area: z4.number().nonnegative()
6770
6960
  }).strict()
6771
6961
  }).strict();
6772
- var versionSchema = z3.object({
6773
- id: z3.string().regex(/^tmv_[A-Za-z0-9_-]{16,64}$/u),
6774
- version: z3.number().int().positive(),
6775
- state: z3.enum(["current", "previous", "gc_pending", "deleting", "deleted"]),
6776
- contentHash: z3.string().regex(sha256),
6777
- byteLength: z3.number().int().positive(),
6778
- publishedAt: z3.iso.datetime({ offset: true }),
6962
+ var versionSchema = z4.object({
6963
+ id: z4.string().regex(/^tmv_[A-Za-z0-9_-]{16,64}$/u),
6964
+ version: z4.number().int().positive(),
6965
+ state: z4.enum(["current", "previous", "gc_pending", "deleting", "deleted"]),
6966
+ contentHash: z4.string().regex(sha256),
6967
+ byteLength: z4.number().int().positive(),
6968
+ publishedAt: z4.iso.datetime({ offset: true }),
6779
6969
  summary: summarySchema
6780
6970
  }).strict();
6781
- var modelSchema = z3.object({
6782
- id: z3.string().regex(/^tmd_[A-Za-z0-9_-]{16,64}$/u),
6783
- name: z3.string().regex(modelName).max(64),
6784
- state: z3.enum(["active", "purging"]),
6971
+ var modelSchema = z4.object({
6972
+ id: z4.string().regex(/^tmd_[A-Za-z0-9_-]{16,64}$/u),
6973
+ name: z4.string().regex(modelName).max(64),
6974
+ state: z4.enum(["active", "purging"]),
6785
6975
  current: versionSchema.nullable(),
6786
6976
  previous: versionSchema.nullable(),
6787
- url: z3.url(),
6788
- usedByMaps: z3.number().int().nonnegative(),
6789
- updatedAt: z3.iso.datetime({ offset: true })
6977
+ url: z4.url(),
6978
+ usedByMaps: z4.number().int().nonnegative(),
6979
+ updatedAt: z4.iso.datetime({ offset: true })
6790
6980
  }).strict();
6791
- var listResponseSchema = z3.object({
6792
- schemaVersion: z3.literal(1),
6793
- models: z3.array(modelSchema).max(100),
6794
- nextCursor: z3.string().min(1).max(512).optional()
6981
+ var listResponseSchema = z4.object({
6982
+ schemaVersion: z4.literal(1),
6983
+ models: z4.array(modelSchema).max(100),
6984
+ nextCursor: z4.string().min(1).max(512).optional()
6795
6985
  }).strict();
6796
- var detailResponseSchema = z3.object({
6797
- schemaVersion: z3.literal(1),
6986
+ var detailResponseSchema = z4.object({
6987
+ schemaVersion: z4.literal(1),
6798
6988
  model: modelSchema,
6799
- versions: z3.array(versionSchema).max(100),
6800
- uses: z3.array(
6801
- z3.object({
6802
- mapId: z3.string().min(1).max(160),
6803
- mapName: z3.string().max(200).nullable(),
6804
- relationship: z3.enum(["active", "retained"])
6989
+ versions: z4.array(versionSchema).max(100),
6990
+ uses: z4.array(
6991
+ z4.object({
6992
+ mapId: z4.string().min(1).max(160),
6993
+ mapName: z4.string().max(200).nullable(),
6994
+ relationship: z4.enum(["active", "retained"])
6805
6995
  }).strict()
6806
6996
  ).max(100)
6807
6997
  }).strict();
6808
- var publishResponseSchema2 = z3.object({ schemaVersion: z3.literal(1), changed: z3.boolean(), model: modelSchema }).strict();
6809
- var restoreResponseSchema = z3.object({ schemaVersion: z3.literal(1), model: modelSchema }).strict();
6810
- var purgeResponseSchema2 = z3.object({
6811
- schemaVersion: z3.literal(1),
6812
- purge: z3.object({ name: z3.string().regex(modelName), state: z3.enum(["purging", "purged"]) }).strict()
6998
+ var publishResponseSchema2 = z4.object({ schemaVersion: z4.literal(1), changed: z4.boolean(), model: modelSchema }).strict();
6999
+ var restoreResponseSchema = z4.object({ schemaVersion: z4.literal(1), model: modelSchema }).strict();
7000
+ var purgeResponseSchema2 = z4.object({
7001
+ schemaVersion: z4.literal(1),
7002
+ purge: z4.object({ name: z4.string().regex(modelName), state: z4.enum(["purging", "purged"]) }).strict()
6813
7003
  }).strict();
6814
7004
 
6815
7005
  // src/native-preview-command.ts
@@ -6850,17 +7040,17 @@ function createTileflowNativePreviewRequestHandler(options) {
6850
7040
  headers: { "Content-Type": "application/json; charset=utf-8" },
6851
7041
  status: 404
6852
7042
  });
6853
- const handler = async (request) => {
7043
+ const handler = async (request2) => {
6854
7044
  let pathname;
6855
7045
  try {
6856
- pathname = new URL(request.url).pathname;
7046
+ pathname = new URL(request2.url).pathname;
6857
7047
  } catch {
6858
7048
  return notFound();
6859
7049
  }
6860
7050
  if (pathname === tileflowNativePreviewBasePath || pathname === `${tileflowNativePreviewBasePath}/` || nativePreviewUnavailablePaths.has(pathname) || pathname.startsWith(`${tileflowNativePreviewBasePath}/__runtime/`) || pathname.startsWith(`${tileflowNativePreviewBasePath}/__inspection/`)) {
6861
7051
  return notFound();
6862
7052
  }
6863
- return delegate(request);
7053
+ return delegate(request2);
6864
7054
  };
6865
7055
  return Object.assign(handler, {
6866
7056
  close: delegate.close,
@@ -6900,7 +7090,7 @@ function resolveTileflowExternalOpenInvocation(target, platform) {
6900
7090
  }
6901
7091
 
6902
7092
  // src/project-commands.ts
6903
- import pc6 from "picocolors";
7093
+ import pc7 from "picocolors";
6904
7094
  function registerProjectCommands(program2, dependencies) {
6905
7095
  const projects = program2.command("projects", { hidden: true }).description("Manage internal application destinations");
6906
7096
  projects.command("list").description("List application destinations accessible to the authenticated Tileflow account").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--include-archived", "include archived application destinations").option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
@@ -6916,12 +7106,12 @@ function registerProjectCommands(program2, dependencies) {
6916
7106
  };
6917
7107
  if (options.json) return emitJson2(document);
6918
7108
  if (!result.items.length) {
6919
- console.log(pc6.gray("No managed destinations found."));
7109
+ console.log(pc7.gray("No managed destinations found."));
6920
7110
  return;
6921
7111
  }
6922
7112
  for (const target of result.items) {
6923
7113
  console.log(
6924
- ` ${target.project.archivedAt ? pc6.yellow("\u25CB") : pc6.green("\u25CF")} ${pc6.bold(target.project.name)} ${pc6.gray(target.reference)}${target.project.archivedAt ? pc6.gray(" (archived)") : ""}`
7114
+ ` ${target.project.archivedAt ? pc7.yellow("\u25CB") : pc7.green("\u25CF")} ${pc7.bold(target.project.name)} ${pc7.gray(target.reference)}${target.project.archivedAt ? pc7.gray(" (archived)") : ""}`
6925
7115
  );
6926
7116
  }
6927
7117
  });
@@ -6974,8 +7164,8 @@ function registerProjectCommands(program2, dependencies) {
6974
7164
  ...asRecord(result.body)
6975
7165
  };
6976
7166
  if (options.json) return emitJson2(document);
6977
- console.log(`${pc6.green("\u2713")} Application ${pc6.bold(options.name.trim())} is ready.`);
6978
- console.log(pc6.gray(`Its technical selector is --project @${organization.slug}/${slug}.`));
7167
+ console.log(`${pc7.green("\u2713")} Application ${pc7.bold(options.name.trim())} is ready.`);
7168
+ console.log(pc7.gray(`Its technical selector is --project @${organization.slug}/${slug}.`));
6979
7169
  }
6980
7170
  );
6981
7171
  projects.command("show <target>").description("Show one application destination").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--json", "print deterministic schema-version-1 JSON").action(async (target, options) => {
@@ -6997,8 +7187,8 @@ function registerProjectCommands(program2, dependencies) {
6997
7187
  if (!isProjectItem(project)) {
6998
7188
  return emitLocalFailure(false, "invalid_response", "Destination response was invalid.");
6999
7189
  }
7000
- console.log(`${pc6.bold(project.name)} ${pc6.gray(target)}`);
7001
- console.log(project.archivedAt ? pc6.yellow("Archived") : pc6.green("Active"));
7190
+ console.log(`${pc7.bold(project.name)} ${pc7.gray(target)}`);
7191
+ console.log(project.archivedAt ? pc7.yellow("Archived") : pc7.green("Active"));
7002
7192
  });
7003
7193
  for (const archived of [true, false]) {
7004
7194
  const verb = archived ? "archive" : "unarchive";
@@ -7025,7 +7215,7 @@ function registerProjectCommands(program2, dependencies) {
7025
7215
  ...asRecord(result.body)
7026
7216
  };
7027
7217
  if (options.json) return emitJson2(document);
7028
- console.log(`${pc6.green("\u2713")} Application ${archived ? "archived" : "unarchived"}.`);
7218
+ console.log(`${pc7.green("\u2713")} Application ${archived ? "archived" : "unarchived"}.`);
7029
7219
  });
7030
7220
  }
7031
7221
  }
@@ -7136,7 +7326,7 @@ function emitLocalFailure(json2, code, message, details = {}) {
7136
7326
  console.error(message);
7137
7327
  emitJson2({ schemaVersion: 1, ok: false, error: { code, ...details } });
7138
7328
  } else {
7139
- console.error(`${pc6.red("\u2715")} ${message}`);
7329
+ console.error(`${pc7.red("\u2715")} ${message}`);
7140
7330
  }
7141
7331
  process.exitCode = 1;
7142
7332
  }
@@ -7281,7 +7471,7 @@ async function runRendererArtifactCommand(command2, options) {
7281
7471
 
7282
7472
  // src/visual-command.ts
7283
7473
  import { lstat as lstat6, readFile as readFile5 } from "fs/promises";
7284
- import { dirname as dirname11, isAbsolute as isAbsolute4, parse as parse2, relative as relative5, resolve as resolve9, sep as sep4 } from "path";
7474
+ import { dirname as dirname11, isAbsolute as isAbsolute4, parse as parse2, relative as relative5, resolve as resolve10, sep as sep4 } from "path";
7285
7475
  import {
7286
7476
  analyzeTileflowCaptureReference,
7287
7477
  compareTileflowCaptureToBaseline,
@@ -7296,6 +7486,565 @@ import {
7296
7486
  validateTileflowVisualReferencePng
7297
7487
  } from "@tileflow/capture";
7298
7488
  import { compareCodeUnits as compareCodeUnits8 } from "@tileflow/core";
7489
+
7490
+ // src/visual-approval-command.ts
7491
+ import { execFile } from "child_process";
7492
+ import { resolve as resolve9 } from "path";
7493
+ import { setTimeout as delay } from "timers/promises";
7494
+ import { TileflowVisualReviewError as TileflowVisualReviewError2 } from "@tileflow/capture";
7495
+ var statusName = "Tileflow visual review";
7496
+ var approveJobName = "tileflow-visual-review-approve";
7497
+ var defaultEvidenceBranch = "tileflow-visual-review";
7498
+ var defaultOutputDirectory = ".tileflow/diffs/review";
7499
+ var defaultTimeoutSeconds = 300;
7500
+ var pollIntervalMs = 5e3;
7501
+ var maximumReportBytes = 1024 * 1024;
7502
+ var maximumImageBytes = 5 * 1024 * 1024;
7503
+ var maximumHostOutputBytes = 16 * 1024 * 1024;
7504
+ var maximumText = 500;
7505
+ var scenePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
7506
+ var fullShaPattern = /^[0-9a-f]{40}$/u;
7507
+ var commitPattern = /^[0-9a-f]{7,40}$/u;
7508
+ var branchPattern = /^[A-Za-z0-9._/-]+$/u;
7509
+ var sceneStatuses = /* @__PURE__ */ new Set([
7510
+ "changed",
7511
+ "missing-baseline",
7512
+ "runtime-mismatch",
7513
+ "scene-mismatch",
7514
+ "unchanged"
7515
+ ]);
7516
+ var pngSignature = [137, 80, 78, 71, 13, 10, 26, 10];
7517
+ var imageKinds = ["before", "after", "diff"];
7518
+ function registerVisualApprovalCommands(visual) {
7519
+ const hostOptions = (command2) => command2.argument("[change]", "pull or merge request number; defaults to the current branch").option("--provider <provider>", "github or gitlab; detected from the origin remote").option(
7520
+ "--evidence-branch <branch>",
7521
+ "branch configured for the review evidence",
7522
+ defaultEvidenceBranch
7523
+ ).option("--json", "print deterministic schema-version-1 JSON");
7524
+ hostOptions(
7525
+ visual.command("report").description("Show the visual review of a pull or merge request and download its images")
7526
+ ).option("--output-dir <path>", "directory for downloaded images", defaultOutputDirectory).action(async (change, options) => {
7527
+ try {
7528
+ await runVisualReport(change, options);
7529
+ } catch (error) {
7530
+ printCaptureError(error, { command: "visual.report", json: options.json });
7531
+ process.exitCode = 1;
7532
+ }
7533
+ });
7534
+ hostOptions(
7535
+ visual.command("approve").description("Approve the reviewed renders of one exact commit after a person accepts them")
7536
+ ).requiredOption("--commit <sha>", "the commit whose report was reviewed").option(
7537
+ "--timeout <seconds>",
7538
+ "seconds to wait for the result; 0 returns once requested",
7539
+ String(defaultTimeoutSeconds)
7540
+ ).action(async (change, options) => {
7541
+ try {
7542
+ await runVisualApprove(change, options);
7543
+ } catch (error) {
7544
+ printCaptureError(error, { command: "visual.approve", json: options.json });
7545
+ process.exitCode = 1;
7546
+ }
7547
+ });
7548
+ }
7549
+ async function runVisualReport(change, options) {
7550
+ const branch = evidenceBranch(options.evidenceBranch);
7551
+ const cwd = process.cwd();
7552
+ const host = await createHost(options.provider, cwd);
7553
+ const pull = await host.findChange(changeNumber(change));
7554
+ const review = await loadReview(host, pull, branch);
7555
+ const paths = await downloadImages(host, pull, review, branch, cwd, options.outputDir);
7556
+ const document = createReportJson(host, pull, review, paths);
7557
+ if (options.json) {
7558
+ process.stdout.write(`${JSON.stringify(document, null, 2)}
7559
+ `);
7560
+ return;
7561
+ }
7562
+ process.stdout.write(formatReport(host, document));
7563
+ }
7564
+ async function runVisualApprove(change, options) {
7565
+ const branch = evidenceBranch(options.evidenceBranch);
7566
+ const timeoutSeconds = timeout(options.timeout);
7567
+ if (!commitPattern.test(options.commit)) {
7568
+ throw reviewError(
7569
+ "--commit expects a commit hash of 7 to 40 lowercase hexadecimal characters."
7570
+ );
7571
+ }
7572
+ const host = await createHost(options.provider, process.cwd());
7573
+ const pull = await host.findChange(changeNumber(change));
7574
+ if (!pull.headSha.startsWith(options.commit)) {
7575
+ throw reviewError(
7576
+ `The current commit is ${pull.headSha.slice(0, 12)}, not ${options.commit}. Review the latest report before approving.`
7577
+ );
7578
+ }
7579
+ const review = await loadReview(host, pull, branch);
7580
+ const finish = (state, approvedBy) => {
7581
+ const document = {
7582
+ schemaVersion: 1,
7583
+ command: "visual.approve",
7584
+ provider: host.provider,
7585
+ number: pull.number,
7586
+ commit: pull.headSha,
7587
+ state,
7588
+ approvedBy
7589
+ };
7590
+ process.stdout.write(
7591
+ options.json ? `${JSON.stringify(document, null, 2)}
7592
+ ` : state === "approved" ? `Approved ${pull.headSha.slice(0, 12)}${approvedBy ? ` as ${clean(approvedBy)}` : ""}.
7593
+ ` : `Approval requested for ${pull.headSha.slice(0, 12)}.
7594
+ `
7595
+ );
7596
+ };
7597
+ if (review.state === "approved") {
7598
+ finish("approved", review.approvedBy);
7599
+ return;
7600
+ }
7601
+ if (review.state !== "needs-approval") throw notApprovable(host, review);
7602
+ const poll = await host.requestApproval(pull);
7603
+ if (timeoutSeconds === 0) {
7604
+ finish("requested", null);
7605
+ return;
7606
+ }
7607
+ const deadline = Date.now() + timeoutSeconds * 1e3;
7608
+ for (; ; ) {
7609
+ const outcome = await poll();
7610
+ if (outcome.state === "approved") {
7611
+ finish("approved", outcome.by);
7612
+ return;
7613
+ }
7614
+ if (outcome.state === "refused") {
7615
+ throw reviewError(`Approval not recorded: ${outcome.reason}`);
7616
+ }
7617
+ if (Date.now() >= deadline) {
7618
+ throw reviewError(
7619
+ "The approval is still running. Check its result with tileflow visual report."
7620
+ );
7621
+ }
7622
+ await delay(pollIntervalMs);
7623
+ }
7624
+ }
7625
+ function notApprovable(host, review) {
7626
+ if (review.state === "closed") return reviewError(`This ${host.noun} is closed.`);
7627
+ if (review.state === "pending") return reviewError("This commit has no report yet.");
7628
+ if (review.state === "passed") return reviewError("This report has nothing to approve.");
7629
+ return reviewError("This report cannot be approved. Read its failure and warnings.");
7630
+ }
7631
+ async function loadReview(host, pull, branch) {
7632
+ const status = await host.readStatus(pull.headSha);
7633
+ if (!pull.open) return { approvedBy: null, report: null, state: "closed", status };
7634
+ const raw = await host.readEvidence(
7635
+ branch,
7636
+ `pr-${pull.number}/${pull.headSha}/report.json`,
7637
+ maximumReportBytes
7638
+ );
7639
+ if (!raw) return { approvedBy: null, report: null, state: "pending", status };
7640
+ const report = parseEvidenceReport(parseJson(raw, "evidence report"), pull);
7641
+ const approvedBy = await readApprover(host, pull, branch);
7642
+ return { approvedBy, report, state: reviewState(report, approvedBy), status };
7643
+ }
7644
+ function reviewState(report, approvedBy) {
7645
+ if (approvedBy) return "approved";
7646
+ if (report.failure || !report.approvable) return "blocked";
7647
+ return report.scenes.some(({ needsApproval }) => needsApproval) ? "needs-approval" : "passed";
7648
+ }
7649
+ async function readApprover(host, pull, branch) {
7650
+ const raw = await host.readEvidence(
7651
+ branch,
7652
+ `approvals/pr-${pull.number}.json`,
7653
+ maximumReportBytes
7654
+ );
7655
+ if (!raw) return null;
7656
+ const approval = record(parseJson(raw, "approval"), "approval");
7657
+ return approval.headSha === pull.headSha ? text(approval.approvedBy) : null;
7658
+ }
7659
+ function parseEvidenceReport(value, pull) {
7660
+ const document = record(value, "report");
7661
+ if (document.schemaVersion !== 1) throw reviewError("Unsupported evidence report version.");
7662
+ if (document.headSha !== pull.headSha || !fullShaPattern.test(pull.headSha)) {
7663
+ throw reviewError("The evidence report describes another commit.");
7664
+ }
7665
+ return {
7666
+ approvable: document.approvable === true,
7667
+ cliVersion: text(document.cliVersion),
7668
+ failure: document.failure == null ? null : text(document.failure),
7669
+ scenes: list(document.scenes, "scenes").map(parseScene),
7670
+ semantic: list(document.semantic, "semantic diff"),
7671
+ warnings: list(document.warnings, "warnings").map(text)
7672
+ };
7673
+ }
7674
+ function parseScene(value) {
7675
+ const scene = record(value, "scene");
7676
+ if (typeof scene.scene !== "string" || !scenePattern.test(scene.scene)) {
7677
+ throw reviewError("The evidence report has an unsupported scene name.");
7678
+ }
7679
+ if (typeof scene.status !== "string" || !sceneStatuses.has(scene.status)) {
7680
+ throw reviewError("The evidence report has an unknown scene status.");
7681
+ }
7682
+ const images = record(scene.images, "scene images");
7683
+ const noise = scene.noise === true;
7684
+ return {
7685
+ exactPixels: metric(scene.exactPixels),
7686
+ images: {
7687
+ before: images.before === true,
7688
+ after: images.after === true,
7689
+ diff: images.diff === true
7690
+ },
7691
+ map: text(scene.map),
7692
+ needsApproval: scene.status !== "unchanged" && !noise,
7693
+ noise,
7694
+ perceptualPixels: metric(scene.perceptualPixels),
7695
+ scene: scene.scene,
7696
+ status: scene.status,
7697
+ totalPixels: metric(scene.totalPixels)
7698
+ };
7699
+ }
7700
+ async function downloadImages(host, pull, review, branch, cwd, outputDirectory) {
7701
+ const paths = /* @__PURE__ */ new Map();
7702
+ const scenes = review.report?.scenes.filter(({ needsApproval }) => needsApproval) ?? [];
7703
+ if (scenes.length === 0) return paths;
7704
+ const boundaryPath = resolve9(cwd, outputDirectory);
7705
+ const directory = resolve9(boundaryPath, `${pull.number}-${pull.headSha.slice(0, 12)}`);
7706
+ const files = [];
7707
+ for (const scene of scenes) {
7708
+ for (const kind of imageKinds) {
7709
+ if (!scene.images[kind]) continue;
7710
+ const name = `${scene.scene}.${kind}.png`;
7711
+ const bytes = await host.readEvidence(
7712
+ branch,
7713
+ `pr-${pull.number}/${pull.headSha}/${name}`,
7714
+ maximumImageBytes
7715
+ );
7716
+ if (!bytes) throw reviewError(`The evidence is missing ${name}.`);
7717
+ if (!isPng(bytes)) throw reviewError(`The evidence file ${name} is not a PNG.`);
7718
+ const path = resolve9(directory, name);
7719
+ files.push({ path, source: bytes });
7720
+ paths.set(`${scene.scene}.${kind}`, relativePath(cwd, path));
7721
+ }
7722
+ }
7723
+ if (files.length > 0) {
7724
+ await writeAtomicFileSet({
7725
+ boundaryPath,
7726
+ files,
7727
+ force: true,
7728
+ label: "Visual review images",
7729
+ managed: false
7730
+ });
7731
+ }
7732
+ return paths;
7733
+ }
7734
+ function createReportJson(host, pull, review, paths) {
7735
+ const report = review.report;
7736
+ return {
7737
+ schemaVersion: 1,
7738
+ command: "visual.report",
7739
+ provider: host.provider,
7740
+ number: pull.number,
7741
+ url: pull.url,
7742
+ commit: pull.headSha,
7743
+ state: review.state,
7744
+ status: review.status,
7745
+ approvedBy: review.approvedBy,
7746
+ cliVersion: report?.cliVersion ?? null,
7747
+ failure: report?.failure ?? null,
7748
+ scenes: (report?.scenes ?? []).map(({ images: _images, ...scene }) => ({
7749
+ ...scene,
7750
+ beforePath: paths.get(`${scene.scene}.before`) ?? null,
7751
+ afterPath: paths.get(`${scene.scene}.after`) ?? null,
7752
+ diffPath: paths.get(`${scene.scene}.diff`) ?? null
7753
+ })),
7754
+ semantic: report?.semantic ?? [],
7755
+ warnings: report?.warnings ?? [],
7756
+ nextStep: nextStep(pull, review),
7757
+ untrustedFields: ["failure", "scenes[].map", "semantic", "warnings"]
7758
+ };
7759
+ }
7760
+ function nextStep(pull, review) {
7761
+ if (review.state === "pending")
7762
+ return "Wait for the capture to finish, then read the report again.";
7763
+ if (review.state !== "needs-approval") return null;
7764
+ return `Show the before and after images to the person responsible for this map. Only after they accept them, run: tileflow visual approve ${pull.number} --commit ${pull.headSha}`;
7765
+ }
7766
+ function formatReport(host, document) {
7767
+ const lines = [
7768
+ `${host.noun} ${document.number} at ${document.commit.slice(0, 12)}: ${document.state}`,
7769
+ ...document.approvedBy ? [`Approved by ${clean(document.approvedBy)}`] : [],
7770
+ ...document.failure ? [`Failure: ${clean(document.failure)}`] : []
7771
+ ];
7772
+ for (const scene of document.scenes) {
7773
+ lines.push(` ${scene.scene}: ${scene.noise ? "renderer noise" : scene.status}`);
7774
+ for (const path of [scene.beforePath, scene.afterPath, scene.diffPath]) {
7775
+ if (path) lines.push(` ${path}`);
7776
+ }
7777
+ }
7778
+ for (const warning of document.warnings) lines.push(`Warning: ${clean(warning)}`);
7779
+ if (document.nextStep) lines.push("", document.nextStep);
7780
+ return `${lines.join("\n")}
7781
+ `;
7782
+ }
7783
+ async function createHost(requested, cwd) {
7784
+ const provider = requested ?? await detectProvider(cwd);
7785
+ if (provider === "github") return createGitHubHost(cwd);
7786
+ if (provider === "gitlab") return createGitLabHost(cwd);
7787
+ throw reviewError("--provider expects github or gitlab.");
7788
+ }
7789
+ async function detectProvider(cwd) {
7790
+ const remote = await runProcess("git", ["remote", "get-url", "origin"], cwd).catch(() => null);
7791
+ const url = remote ? Buffer.from(remote).toString("utf8") : "";
7792
+ if (/github/iu.test(url)) return "github";
7793
+ if (/gitlab/iu.test(url)) return "gitlab";
7794
+ throw reviewError("Could not tell the Git host from the origin remote. Pass --provider.");
7795
+ }
7796
+ function createGitHubHost(cwd) {
7797
+ const repository = "repos/{owner}/{repo}";
7798
+ const gh = (args) => runProcess("gh", args, cwd);
7799
+ const api = async (path, ...args) => parseJson(await gh(["api", ...args, path]), "GitHub response");
7800
+ const readStatus = async (sha) => {
7801
+ const combined = record(
7802
+ await api(`${repository}/commits/${sha}/status?per_page=100`),
7803
+ "GitHub status"
7804
+ );
7805
+ const status = list(combined.statuses, "GitHub statuses").map((item) => record(item, "GitHub status")).find(({ context }) => context === statusName);
7806
+ return status ? { description: text(status.description), state: text(status.state) } : null;
7807
+ };
7808
+ return {
7809
+ provider: "github",
7810
+ noun: "pull request",
7811
+ async findChange(number) {
7812
+ const pull = record(
7813
+ parseJson(
7814
+ await gh([
7815
+ "pr",
7816
+ "view",
7817
+ ...number === void 0 ? [] : [String(number)],
7818
+ "--json",
7819
+ "number,url,headRefOid,state"
7820
+ ]),
7821
+ "GitHub pull request"
7822
+ ),
7823
+ "GitHub pull request"
7824
+ );
7825
+ return {
7826
+ headSha: text(pull.headRefOid),
7827
+ number: Number(pull.number),
7828
+ open: pull.state === "OPEN",
7829
+ url: text(pull.url)
7830
+ };
7831
+ },
7832
+ async readEvidence(branch, path, maximumBytes) {
7833
+ return readOptional2(
7834
+ () => gh([
7835
+ "api",
7836
+ "-H",
7837
+ "Accept: application/vnd.github.raw+json",
7838
+ `${repository}/contents/${encodeURI(path)}?ref=${encodeURIComponent(branch)}`
7839
+ ]),
7840
+ maximumBytes
7841
+ );
7842
+ },
7843
+ readStatus,
7844
+ async requestApproval(change) {
7845
+ const comment = record(
7846
+ await api(
7847
+ `${repository}/issues/${change.number}/comments`,
7848
+ "-X",
7849
+ "POST",
7850
+ "-f",
7851
+ `body=/tileflow approve ${change.headSha}`
7852
+ ),
7853
+ "GitHub comment"
7854
+ );
7855
+ const query = `since=${encodeURIComponent(text(comment.created_at))}&per_page=100`;
7856
+ return async () => {
7857
+ const status = await readStatus(change.headSha);
7858
+ const approved = /^Approved by (.+)$/u.exec(status?.description ?? "");
7859
+ if (approved) return { state: "approved", by: approved[1] ?? null };
7860
+ const replies = list(
7861
+ await api(`${repository}/issues/${change.number}/comments?${query}`),
7862
+ "GitHub comments"
7863
+ ).map((item) => record(item, "GitHub comment"));
7864
+ const refusal = replies.find(
7865
+ ({ body, id }) => Number(id) > Number(comment.id) && isRefusal(body)
7866
+ );
7867
+ return refusal ? { state: "refused", reason: refusalReason(refusal.body) } : { state: "pending" };
7868
+ };
7869
+ }
7870
+ };
7871
+ }
7872
+ function createGitLabHost(cwd) {
7873
+ const project = "projects/:id";
7874
+ const glab = (args) => runProcess("glab", args, cwd);
7875
+ const api = async (path, ...args) => parseJson(await glab(["api", ...args, path]), "GitLab response");
7876
+ const change = (value) => {
7877
+ const mergeRequest = record(value, "GitLab merge request");
7878
+ return {
7879
+ headSha: text(mergeRequest.sha),
7880
+ number: Number(mergeRequest.iid),
7881
+ open: mergeRequest.state === "opened",
7882
+ url: text(mergeRequest.web_url)
7883
+ };
7884
+ };
7885
+ const refusalNote = async (number) => {
7886
+ const notes = list(
7887
+ await api(
7888
+ `${project}/merge_requests/${number}/notes?sort=desc&order_by=created_at&per_page=20`
7889
+ ),
7890
+ "GitLab notes"
7891
+ ).map((item) => record(item, "GitLab note"));
7892
+ const note = notes.find(({ body }) => isRefusal(body));
7893
+ return note ? refusalReason(note.body) : "the approve job failed. Read its log.";
7894
+ };
7895
+ return {
7896
+ provider: "gitlab",
7897
+ noun: "merge request",
7898
+ async findChange(number) {
7899
+ if (number !== void 0) return change(await api(`${project}/merge_requests/${number}`));
7900
+ const branch = Buffer.from(await runProcess("git", ["branch", "--show-current"], cwd)).toString("utf8").trim();
7901
+ const open4 = list(
7902
+ await api(
7903
+ `${project}/merge_requests?state=opened&source_branch=${encodeURIComponent(branch)}`
7904
+ ),
7905
+ "GitLab merge requests"
7906
+ );
7907
+ if (open4.length === 0) throw reviewError(`No open merge request for branch ${branch}.`);
7908
+ return change(open4[0]);
7909
+ },
7910
+ async readEvidence(branch, path, maximumBytes) {
7911
+ return readOptional2(
7912
+ () => glab([
7913
+ "api",
7914
+ `${project}/repository/files/${encodeURIComponent(path)}/raw?ref=${encodeURIComponent(branch)}`
7915
+ ]),
7916
+ maximumBytes
7917
+ );
7918
+ },
7919
+ async readStatus(sha) {
7920
+ const statuses = list(
7921
+ await api(`${project}/repository/commits/${sha}/statuses?all=true&per_page=100`),
7922
+ "GitLab statuses"
7923
+ ).map((item) => record(item, "GitLab status")).filter(({ name }) => name === statusName).sort((left, right) => Number(right.id) - Number(left.id));
7924
+ const latest = statuses[0];
7925
+ return latest ? { description: text(latest.description), state: text(latest.status) } : null;
7926
+ },
7927
+ async requestApproval(pull) {
7928
+ const pipelines = list(
7929
+ await api(`${project}/merge_requests/${pull.number}/pipelines`),
7930
+ "GitLab pipelines"
7931
+ ).map((item) => record(item, "GitLab pipeline"));
7932
+ const pipeline = pipelines.find(({ sha }) => sha === pull.headSha);
7933
+ if (!pipeline) throw reviewError("No pipeline exists for this commit.");
7934
+ const jobs = list(
7935
+ await api(`${project}/pipelines/${Number(pipeline.id)}/jobs?per_page=100`),
7936
+ "GitLab jobs"
7937
+ ).map((item) => record(item, "GitLab job")).filter(({ name }) => name === approveJobName).sort((left, right) => Number(right.id) - Number(left.id));
7938
+ const job = jobs[0];
7939
+ if (!job) throw reviewError(`Pipeline ${Number(pipeline.id)} has no ${approveJobName} job.`);
7940
+ const action = job.status === "manual" ? "play" : "retry";
7941
+ const started = record(
7942
+ await api(`${project}/jobs/${Number(job.id)}/${action}`, "-X", "POST"),
7943
+ "GitLab job"
7944
+ );
7945
+ return async () => {
7946
+ const current = record(await api(`${project}/jobs/${Number(started.id)}`), "GitLab job");
7947
+ if (current.status === "success") {
7948
+ const user = current.user ? record(current.user, "GitLab user") : null;
7949
+ return { state: "approved", by: user ? text(user.username) : null };
7950
+ }
7951
+ if (current.status === "failed" || current.status === "canceled") {
7952
+ return { state: "refused", reason: await refusalNote(pull.number) };
7953
+ }
7954
+ return { state: "pending" };
7955
+ };
7956
+ }
7957
+ };
7958
+ }
7959
+ var refusalPrefix = "Approval not recorded: ";
7960
+ var isRefusal = (body) => typeof body === "string" && body.startsWith(refusalPrefix);
7961
+ var refusalReason = (body) => text(body).slice(refusalPrefix.length);
7962
+ async function readOptional2(read, maximumBytes) {
7963
+ try {
7964
+ const bytes = await read();
7965
+ if (bytes.length > maximumBytes) throw reviewError("An evidence file is too large.");
7966
+ return bytes;
7967
+ } catch (error) {
7968
+ if (error instanceof HostNotFoundError) return null;
7969
+ throw error;
7970
+ }
7971
+ }
7972
+ var HostNotFoundError = class extends Error {
7973
+ };
7974
+ function runProcess(command2, args, cwd) {
7975
+ return new Promise((resolvePromise, reject) => {
7976
+ execFile(
7977
+ command2,
7978
+ args,
7979
+ { cwd, encoding: "buffer", maxBuffer: maximumHostOutputBytes },
7980
+ (error, stdout, stderr) => {
7981
+ if (!error) {
7982
+ resolvePromise(stdout);
7983
+ return;
7984
+ }
7985
+ if (error.code === "ENOENT") {
7986
+ reject(reviewError(`${command2} is not installed. Install it and sign in with it first.`));
7987
+ return;
7988
+ }
7989
+ const detail = Buffer.from(stderr).toString("utf8").replace(/\s+/gu, " ").trim();
7990
+ reject(
7991
+ /HTTP 404|404 Not Found/u.test(detail) ? new HostNotFoundError(detail) : reviewError(`${command2} failed: ${detail.slice(0, maximumText) || error.message}`)
7992
+ );
7993
+ }
7994
+ );
7995
+ });
7996
+ }
7997
+ function changeNumber(value) {
7998
+ if (value === void 0) return void 0;
7999
+ if (!/^[1-9][0-9]{0,9}$/u.test(value)) {
8000
+ throw reviewError("The pull or merge request must be a positive number.");
8001
+ }
8002
+ return Number(value);
8003
+ }
8004
+ function evidenceBranch(value) {
8005
+ if (!branchPattern.test(value) || value.includes("..")) {
8006
+ throw reviewError("--evidence-branch is not a valid branch name.");
8007
+ }
8008
+ return value;
8009
+ }
8010
+ function timeout(value) {
8011
+ if (!/^[0-9]{1,5}$/u.test(value)) throw reviewError("--timeout expects whole seconds.");
8012
+ return Number(value);
8013
+ }
8014
+ function isPng(bytes) {
8015
+ return pngSignature.every((byte, index) => bytes[index] === byte);
8016
+ }
8017
+ function parseJson(bytes, label) {
8018
+ try {
8019
+ return JSON.parse(Buffer.from(bytes).toString("utf8"));
8020
+ } catch {
8021
+ throw reviewError(`The ${label} is not valid JSON.`);
8022
+ }
8023
+ }
8024
+ function record(value, label) {
8025
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
8026
+ throw reviewError(`The ${label} is invalid.`);
8027
+ }
8028
+ return value;
8029
+ }
8030
+ function list(value, label) {
8031
+ if (!Array.isArray(value)) throw reviewError(`The ${label} list is invalid.`);
8032
+ return value;
8033
+ }
8034
+ function text(value) {
8035
+ return String(value ?? "").slice(0, maximumText);
8036
+ }
8037
+ function metric(value) {
8038
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null;
8039
+ }
8040
+ function clean(value) {
8041
+ return value.replace(/\p{Cc}/gu, " ");
8042
+ }
8043
+ function reviewError(message) {
8044
+ return new TileflowVisualReviewError2(message);
8045
+ }
8046
+
8047
+ // src/visual-command.ts
7299
8048
  function registerVisualCommands(program2, dependencies, canonical = false) {
7300
8049
  const visual = program2.command(canonical ? "compare" : "visual").description("Review deliberate variants, external references or approved baselines");
7301
8050
  registerVisualCompareCommand(visual, dependencies, canonical ? "styles" : "compare");
@@ -7332,6 +8081,7 @@ function registerVisualCommands(program2, dependencies, canonical = false) {
7332
8081
  process.exitCode = 1;
7333
8082
  }
7334
8083
  });
8084
+ registerVisualApprovalCommands(visual);
7335
8085
  return visual;
7336
8086
  }
7337
8087
  function createTileflowVisualDiffJson(comparisons, cwd) {
@@ -7366,14 +8116,14 @@ async function runVisualAnalyze(scene, options) {
7366
8116
  validateVisualSelection([scene]);
7367
8117
  const region = options.region === void 0 ? void 0 : parseTileflowVisualRegion(options.region);
7368
8118
  const cwd = process.cwd();
7369
- const referencePath = resolve9(cwd, options.reference);
8119
+ const referencePath = resolve10(cwd, options.reference);
7370
8120
  const referencePng = await readVisualReference(referencePath);
7371
8121
  const referenceDimensions = validateTileflowVisualReferencePng(referencePng);
7372
8122
  if (region) assertTileflowVisualRegionFits(region, referenceDimensions, "reference PNG");
7373
8123
  const outputDirectory = await resolveVisualDirectory(cwd, options.outputDir, "output");
7374
- const actualPath = resolve9(outputDirectory.path, `${scene}.actual.png`);
7375
- const managedDiffPath = resolve9(outputDirectory.path, `${scene}.diff.png`);
7376
- const reportPath = resolve9(outputDirectory.path, `${scene}.analysis.json`);
8124
+ const actualPath = resolve10(outputDirectory.path, `${scene}.actual.png`);
8125
+ const managedDiffPath = resolve10(outputDirectory.path, `${scene}.diff.png`);
8126
+ const reportPath = resolve10(outputDirectory.path, `${scene}.analysis.json`);
7377
8127
  assertReferenceIsReadOnly(referencePath, [actualPath, managedDiffPath, reportPath]);
7378
8128
  const controller = installSignalAbortController();
7379
8129
  const session = createTileflowCaptureSession3({
@@ -7462,15 +8212,15 @@ async function runVisualDiff(scenes, options) {
7462
8212
  capture,
7463
8213
  files.kind === "complete" ? files.baseline : void 0
7464
8214
  );
7465
- const actualPath = resolve9(outputDirectory.path, `${capture.scene}.actual.png`);
7466
- const diffPath = comparison.diffPng ? resolve9(outputDirectory.path, `${capture.scene}.diff.png`) : void 0;
8215
+ const actualPath = resolve10(outputDirectory.path, `${capture.scene}.actual.png`);
8216
+ const diffPath = comparison.diffPng ? resolve10(outputDirectory.path, `${capture.scene}.diff.png`) : void 0;
7467
8217
  prepared.push({
7468
8218
  capture,
7469
8219
  comparison,
7470
8220
  baselinePath: files.pngPath,
7471
8221
  actualPath,
7472
8222
  diffPath,
7473
- reportPath: resolve9(outputDirectory.path, `${capture.scene}.visual.json`)
8223
+ reportPath: resolve10(outputDirectory.path, `${capture.scene}.visual.json`)
7474
8224
  });
7475
8225
  }
7476
8226
  const document = createTileflowVisualDiffJson(prepared, cwd);
@@ -7490,7 +8240,7 @@ async function runVisualDiff(scenes, options) {
7490
8240
  }
7491
8241
  ]),
7492
8242
  removePaths: prepared.flatMap(
7493
- (item) => item.diffPath ? [] : [resolve9(outputDirectory.path, `${item.capture.scene}.diff.png`)]
8243
+ (item) => item.diffPath ? [] : [resolve10(outputDirectory.path, `${item.capture.scene}.diff.png`)]
7494
8244
  )
7495
8245
  });
7496
8246
  if (options.json) {
@@ -7611,7 +8361,7 @@ async function resolvePreviousBaseline(capture, files) {
7611
8361
  }
7612
8362
  }
7613
8363
  async function readBaselineFiles(directory, scene) {
7614
- const pngPath = resolve9(directory.path, `${scene}.png`);
8364
+ const pngPath = resolve10(directory.path, `${scene}.png`);
7615
8365
  const receiptPath = captureReceiptPath(pngPath);
7616
8366
  const [png, receipt] = await Promise.all([
7617
8367
  readOptionalArtifact(pngPath, directory.boundary, tileflowVisualArtifactLimits2.maximumPngBytes),
@@ -7671,12 +8421,12 @@ async function readVisualReference(path) {
7671
8421
  return new Uint8Array(await readFile5(path));
7672
8422
  }
7673
8423
  async function resolveVisualDirectory(cwd, value, kind) {
7674
- const path = resolve9(cwd, value);
8424
+ const path = resolve10(cwd, value);
7675
8425
  if (path === cwd || path === parse2(path).root) {
7676
8426
  throw new Error(`Visual ${kind} directory must be a dedicated subdirectory.`);
7677
8427
  }
7678
8428
  if (kind === "baseline") {
7679
- for (const managed of [resolve9(cwd, ".tileflow/captures"), resolve9(cwd, ".tileflow/diffs")]) {
8429
+ for (const managed of [resolve10(cwd, ".tileflow/captures"), resolve10(cwd, ".tileflow/diffs")]) {
7680
8430
  if (pathsOverlap2(path, managed)) {
7681
8431
  throw new Error("Visual baselines must not overlap Tileflow-managed capture/diff outputs.");
7682
8432
  }
@@ -7712,7 +8462,7 @@ function assertReferenceIsReadOnly(referencePath, outputPaths) {
7712
8462
  }
7713
8463
  }
7714
8464
  function portablePathIdentity2(path) {
7715
- return resolve9(path).normalize("NFC").toLowerCase();
8465
+ return resolve10(path).normalize("NFC").toLowerCase();
7716
8466
  }
7717
8467
  function isInside2(parent, child) {
7718
8468
  const path = relative5(parent, child);
@@ -7724,8 +8474,8 @@ function isMissingFile4(error) {
7724
8474
 
7725
8475
  // src/workbench-service.ts
7726
8476
  import { opendir, stat } from "fs/promises";
7727
- import { resolve as resolve10 } from "path";
7728
- import { z as z4 } from "zod";
8477
+ import { resolve as resolve11 } from "path";
8478
+ import { z as z5 } from "zod";
7729
8479
  import {
7730
8480
  compareTileflowCapturesForReview as compareTileflowCapturesForReview3,
7731
8481
  createTileflowCaptureSession as createTileflowCaptureSession4,
@@ -7738,22 +8488,22 @@ import {
7738
8488
  } from "@tileflow/core";
7739
8489
  import { resolveTileflowPreview } from "@tileflow/dev/preview";
7740
8490
  var prefix = "/__workbench";
7741
- var captureInput = z4.object({
7742
- side: z4.enum(["left", "right"]).default("left"),
7743
- generation: z4.number().int().nonnegative(),
8491
+ var captureInput = z5.object({
8492
+ side: z5.enum(["left", "right"]).default("left"),
8493
+ generation: z5.number().int().nonnegative(),
7744
8494
  name: tileflowCaptureSceneNameSchema2,
7745
8495
  definition: tileflowCaptureSceneSchema3
7746
8496
  }).strict();
7747
- var compareInput = z4.object({ before: z4.string().regex(/^[a-f0-9]{64}$/u), after: z4.string().regex(/^[a-f0-9]{64}$/u) }).strict();
7748
- var deleteInput = z4.object({ id: z4.string().regex(/^[a-f0-9]{64}$/u) }).strict();
7749
- var contextInput = z4.object({
7750
- client: z4.string().regex(/^[a-zA-Z0-9-]{1,80}$/u),
7751
- side: z4.enum(["left", "right"]),
7752
- generation: z4.number().int(),
7753
- context: z4.record(z4.string(), z4.unknown())
8497
+ var compareInput = z5.object({ before: z5.string().regex(/^[a-f0-9]{64}$/u), after: z5.string().regex(/^[a-f0-9]{64}$/u) }).strict();
8498
+ var deleteInput = z5.object({ id: z5.string().regex(/^[a-f0-9]{64}$/u) }).strict();
8499
+ var contextInput = z5.object({
8500
+ client: z5.string().regex(/^[a-zA-Z0-9-]{1,80}$/u),
8501
+ side: z5.enum(["left", "right"]),
8502
+ generation: z5.number().int(),
8503
+ context: z5.record(z5.string(), z5.unknown())
7754
8504
  }).strict();
7755
8505
  function createWorkbenchService(options) {
7756
- const directory = resolve10(options.cwd, captureRecordDirectory);
8506
+ const directory = resolve11(options.cwd, captureRecordDirectory);
7757
8507
  const contexts = /* @__PURE__ */ new Map();
7758
8508
  let captureSession = options.captureSession;
7759
8509
  let pending;
@@ -7825,7 +8575,7 @@ function createWorkbenchService(options) {
7825
8575
  break;
7826
8576
  }
7827
8577
  if (!entry.isFile() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) continue;
7828
- const info = await stat(resolve10(directory, entry.name));
8578
+ const info = await stat(resolve11(directory, entry.name));
7829
8579
  entries.push({ id: entry.name.slice(0, -5), modified: info.mtimeMs });
7830
8580
  }
7831
8581
  } catch (error) {
@@ -7836,15 +8586,15 @@ function createWorkbenchService(options) {
7836
8586
  const warnings = [];
7837
8587
  for (const entry of entries.slice(offset, offset + 20)) {
7838
8588
  try {
7839
- const record = await readCaptureRecord(entry.id, directory);
8589
+ const record2 = await readCaptureRecord(entry.id, directory);
7840
8590
  records.push({
7841
8591
  id: entry.id,
7842
- scene: record.capture.scene,
7843
- map: record.capture.map,
7844
- theme: record.capture.theme,
7845
- definition: record.definition,
7846
- styleSha256: record.capture.styleSha256,
7847
- warnings: record.capture.warnings,
8592
+ scene: record2.capture.scene,
8593
+ map: record2.capture.map,
8594
+ theme: record2.capture.theme,
8595
+ definition: record2.definition,
8596
+ styleSha256: record2.capture.styleSha256,
8597
+ warnings: record2.capture.warnings,
7848
8598
  savedAt: new Date(entry.modified).toISOString(),
7849
8599
  imageUrl: `${prefix}/captures/${entry.id}.png`
7850
8600
  });
@@ -7918,9 +8668,9 @@ function createWorkbenchService(options) {
7918
8668
  }
7919
8669
  return {
7920
8670
  url: prefix,
7921
- async handle(request) {
7922
- const url = new URL(request.url);
7923
- if (url.origin !== options.origin || request.headers.has("origin") && request.headers.get("origin") !== options.origin || request.headers.get("sec-fetch-site") === "cross-site")
8671
+ async handle(request2) {
8672
+ const url = new URL(request2.url);
8673
+ if (url.origin !== options.origin || request2.headers.has("origin") && request2.headers.get("origin") !== options.origin || request2.headers.get("sec-fetch-site") === "cross-site")
7924
8674
  return json(
7925
8675
  { code: "ORIGIN_MISMATCH", message: "Use Inspector from its local origin." },
7926
8676
  403
@@ -7928,7 +8678,7 @@ function createWorkbenchService(options) {
7928
8678
  if (closed) return json({ code: "SESSION_CLOSED", message: "Inspector has stopped." }, 503);
7929
8679
  const path = url.pathname.slice(prefix.length);
7930
8680
  try {
7931
- if (request.method === "GET") {
8681
+ if (request2.method === "GET") {
7932
8682
  if (path === "" || path === "/")
7933
8683
  return json({
7934
8684
  capabilities: [
@@ -7964,13 +8714,13 @@ function createWorkbenchService(options) {
7964
8714
  });
7965
8715
  }
7966
8716
  if (path === "/captures") {
7967
- const offset = z4.coerce.number().int().min(0).max(5e3).parse(url.searchParams.get("offset") ?? 0);
8717
+ const offset = z5.coerce.number().int().min(0).max(5e3).parse(url.searchParams.get("offset") ?? 0);
7968
8718
  return json(await history(offset));
7969
8719
  }
7970
8720
  const image = /^\/captures\/([a-f0-9]{64})\.png$/u.exec(path);
7971
8721
  if (image) {
7972
- const record = await readCaptureRecord(image[1], directory);
7973
- return new Response(Buffer.from(record.capture.png), {
8722
+ const record2 = await readCaptureRecord(image[1], directory);
8723
+ return new Response(Buffer.from(record2.capture.png), {
7974
8724
  headers: {
7975
8725
  "Content-Type": "image/png",
7976
8726
  "Cache-Control": "no-store",
@@ -7980,14 +8730,14 @@ function createWorkbenchService(options) {
7980
8730
  }
7981
8731
  return json({ code: "NOT_FOUND", message: "Unknown Inspector endpoint." }, 404);
7982
8732
  }
7983
- if (request.method !== "POST")
8733
+ if (request2.method !== "POST")
7984
8734
  return json({ code: "METHOD_NOT_ALLOWED", message: "Use GET or POST." }, 405);
7985
- if (request.headers.get("x-tileflow-workbench") !== "1" || request.headers.get("content-type")?.split(";")[0] !== "application/json")
8735
+ if (request2.headers.get("x-tileflow-workbench") !== "1" || request2.headers.get("content-type")?.split(";")[0] !== "application/json")
7986
8736
  return json(
7987
8737
  { code: "INVALID_REQUEST", message: "Send JSON with X-Tileflow-Workbench: 1." },
7988
8738
  400
7989
8739
  );
7990
- const body = await readBody(request);
8740
+ const body = await readBody(request2);
7991
8741
  if (path === "/context") {
7992
8742
  const { client, ...value } = contextInput.parse(body);
7993
8743
  if (!options.sides[value.side]) throw new Error("Unknown Inspector side.");
@@ -8025,8 +8775,8 @@ function createWorkbenchService(options) {
8025
8775
  }
8026
8776
  };
8027
8777
  }
8028
- async function readBody(request) {
8029
- const reader = request.body?.getReader();
8778
+ async function readBody(request2) {
8779
+ const reader = request2.body?.getReader();
8030
8780
  if (!reader) throw new Error("Missing JSON body.");
8031
8781
  const chunks = [];
8032
8782
  let bytes = 0;
@@ -8100,11 +8850,11 @@ registerTilesetCommands(program, { defaultApiUrl, loadAuthConfig });
8100
8850
  registerIconSetCommands(program, { defaultApiUrl, loadAuthConfig });
8101
8851
  registerModelCommands(program, { defaultApiUrl, loadAuthConfig });
8102
8852
  program.command("init").description("Create a starter tileflow.config.ts").option("-f, --force", "overwrite an existing config").action(async (options) => {
8103
- const configPath = resolve11(process.cwd(), defaultConfigPath);
8104
- const ignorePath = resolve11(process.cwd(), ".gitignore");
8853
+ const configPath = resolve12(process.cwd(), defaultConfigPath);
8854
+ const ignorePath = resolve12(process.cwd(), ".gitignore");
8105
8855
  if (existsSync(configPath) && !options.force) {
8106
8856
  logWarning(`${pathLabel(defaultConfigPath)} already exists.`);
8107
- printNextSteps([`Run ${command("tileflow init --force")} to replace it.`]);
8857
+ printNextSteps2([`Run ${command("tileflow init --force")} to replace it.`]);
8108
8858
  return;
8109
8859
  }
8110
8860
  await writeFile2(configPath, starterConfig(), "utf8");
@@ -8117,9 +8867,9 @@ program.command("init").description("Create a starter tileflow.config.ts").optio
8117
8867
  }
8118
8868
  logSuccess(`Created ${pathLabel(defaultConfigPath)}.`);
8119
8869
  printKeyValue("Path", pathLabel(configPath));
8120
- printKeyValue("Map", pc7.bold("madrid"));
8121
- printNextSteps([
8122
- `Install ${pc7.cyan("@tileflow/core")} and ${pc7.cyan("@tileflow/maps")} in this project if needed.`,
8870
+ printKeyValue("Map", pc8.bold("madrid"));
8871
+ printNextSteps2([
8872
+ `Install ${pc8.cyan("@tileflow/core")} and ${pc8.cyan("@tileflow/maps")} in this project if needed.`,
8123
8873
  `Preview locally with ${command("tileflow preview")}.`,
8124
8874
  `Validate the config with ${command("tileflow validate")}.`
8125
8875
  ]);
@@ -8185,7 +8935,7 @@ program.command("whoami").description("Show the authenticated Tileflow account")
8185
8935
  return;
8186
8936
  }
8187
8937
  logError(message);
8188
- printNextSteps([`Run ${command("tileflow login")} to authorize this machine.`]);
8938
+ printNextSteps2([`Run ${command("tileflow login")} to authorize this machine.`]);
8189
8939
  process.exitCode = 1;
8190
8940
  return;
8191
8941
  }
@@ -8245,7 +8995,7 @@ program.command("validate").description("Validate a Tileflow config").option("-c
8245
8995
  return;
8246
8996
  }
8247
8997
  logError(`Invalid validation target: ${options.target}`);
8248
- printNextSteps([
8998
+ printNextSteps2([
8249
8999
  `Use ${command("tileflow validate --target local")} or ${command("tileflow validate --target hosted")}.`
8250
9000
  ]);
8251
9001
  process.exitCode = 1;
@@ -8398,7 +9148,7 @@ program.command("build").description("Generate static Tileflow styles").option("
8398
9148
  })
8399
9149
  );
8400
9150
  logSuccess("Built Tileflow artifacts.");
8401
- printKeyValue("Output", pathLabel(resolve11(process.cwd(), options.out)));
9151
+ printKeyValue("Output", pathLabel(resolve12(process.cwd(), options.out)));
8402
9152
  }
8403
9153
  );
8404
9154
  program.command("preview").description("Open Inspector with inspection and optional comparison").option("-c, --config <path>", "config path", defaultConfigPath).option("--against-config <path>", "second config for live comparison in Inspector").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("--no-open", "start Inspector without opening the browser").option("--scene <name>", "preview one committed standalone map scene").option(
@@ -8631,7 +9381,7 @@ program.command("preview").description("Open Inspector with inspection and optio
8631
9381
  await new Promise((resolveListening, rejectListening) => {
8632
9382
  const createdServer = serve(
8633
9383
  {
8634
- fetch: (request) => (renderer === "web" ? redirectTileflowDevNavigation(request, origin) : void 0) ?? fetch2(request),
9384
+ fetch: (request2) => (renderer === "web" ? redirectTileflowDevNavigation(request2, origin) : void 0) ?? fetch2(request2),
8635
9385
  hostname: host,
8636
9386
  port
8637
9387
  },
@@ -8716,7 +9466,7 @@ addMapSetupOptions(
8716
9466
  );
8717
9467
  if (result) {
8718
9468
  logSuccess(
8719
- `${result.changed ? "Published" : "Unchanged"} ${pc7.bold(result.mapName)} web + native-v1 (v${result.version}).`
9469
+ `${result.changed ? "Published" : "Unchanged"} ${pc8.bold(result.mapName)} web + native-v1 (v${result.version}).`
8720
9470
  );
8721
9471
  printKeyValue("Manifest", pathLabel(result.manifestPath));
8722
9472
  printKeyValue("Native", link(result.nativeManifestUrl));
@@ -8791,7 +9541,7 @@ addMapSetupOptions(
8791
9541
  if (mapsWithHostedFontBundles.length > 0) {
8792
9542
  logError("Hosted deploy does not yet support package-owned web fonts.");
8793
9543
  printKeyValue("Maps", mapsWithHostedFontBundles.join(", "));
8794
- printNextSteps(["Use self-hosted delivery or an explicit public glyph provider."]);
9544
+ printNextSteps2(["Use self-hosted delivery or an explicit public glyph provider."]);
8795
9545
  process.exitCode = 1;
8796
9546
  return;
8797
9547
  }
@@ -8866,8 +9616,8 @@ addMapSetupOptions(
8866
9616
  if (!options.mapId) {
8867
9617
  const deployedMapId2 = existingManifest?.maps[mapNames[0]]?.mapId;
8868
9618
  if (deployedMapId2) {
8869
- logError(`${pc7.bold(mapNames[0])} is already deployed to ${deployedMapId2}.`);
8870
- printNextSteps([
9619
+ logError(`${pc8.bold(mapNames[0])} is already deployed to ${deployedMapId2}.`);
9620
+ printNextSteps2([
8871
9621
  `Deploy it again with ${command(`tileflow deploy --map-id ${deployedMapId2}`)}.`
8872
9622
  ]);
8873
9623
  process.exitCode = 1;
@@ -8896,7 +9646,7 @@ addMapSetupOptions(
8896
9646
  );
8897
9647
  const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
8898
9648
  logSuccess(
8899
- `${action} icon package ${pc7.bold(binding?.label ?? "Icons")} (${plural(iconPackage.manifest.iconNames.length, "icon")}, ${formatBytes4(totalBytes)}, ${iconPackage.contentHash.slice(0, 12)}).`
9649
+ `${action} icon package ${pc8.bold(binding?.label ?? "Icons")} (${plural(iconPackage.manifest.iconNames.length, "icon")}, ${formatBytes4(totalBytes)}, ${iconPackage.contentHash.slice(0, 12)}).`
8900
9650
  );
8901
9651
  hostedSpriteByPackageHash.set(iconPackage.contentHash, uploaded.value.spriteUrl);
8902
9652
  }
@@ -9059,7 +9809,7 @@ addMapSetupOptions(
9059
9809
  }
9060
9810
  const themeNames = Object.keys(themeStyles).sort();
9061
9811
  logInfo(
9062
- `Deploying compiled map ${pc7.bold(mapName)} (${plural(themeNames.length, "theme")}).`
9812
+ `Deploying compiled map ${pc8.bold(mapName)} (${plural(themeNames.length, "theme")}).`
9063
9813
  );
9064
9814
  const response = await publishHostedStyle(
9065
9815
  api,
@@ -9142,14 +9892,14 @@ addMapSetupOptions(
9142
9892
  };
9143
9893
  const versionLabel = Number.isInteger(body.version) ? ` (v${body.version})` : "";
9144
9894
  if (body.changed === false) {
9145
- logSuccess(`Unchanged ${pc7.bold(mapName)}${versionLabel}.`);
9895
+ logSuccess(`Unchanged ${pc8.bold(mapName)}${versionLabel}.`);
9146
9896
  } else {
9147
- logSuccess(`Published ${pc7.bold(mapName)}${versionLabel}.`);
9897
+ logSuccess(`Published ${pc8.bold(mapName)}${versionLabel}.`);
9148
9898
  }
9149
9899
  for (const teamModel of body.teamModels ?? []) {
9150
9900
  if (teamModel.sources !== "differ") continue;
9151
9901
  logWarning(
9152
- `3D model ${pc7.bold(teamModel.model)} draws its published version ${teamModel.version}, which was not prepared from this project's files. Run \`tileflow models publish\` to publish them.`
9902
+ `3D model ${pc8.bold(teamModel.model)} draws its published version ${teamModel.version}, which was not prepared from this project's files. Run \`tileflow models publish\` to publish them.`
9153
9903
  );
9154
9904
  }
9155
9905
  }
@@ -9165,7 +9915,7 @@ addMapSetupOptions(
9165
9915
  printDeployedMaps(deployedMaps);
9166
9916
  const deployedMapId = Object.values(deployedMaps)[0]?.mapId;
9167
9917
  if (deployedMapId) {
9168
- printNextSteps([
9918
+ printNextSteps2([
9169
9919
  ...createdMapId ? [`Deploy later changes with ${command(`tileflow deploy --map-id ${createdMapId}`)}.`] : [],
9170
9920
  `Check hosted state with ${command(`tileflow status --map-id ${deployedMapId}`)}.`
9171
9921
  ]);
@@ -9219,6 +9969,16 @@ program.command("status").description("Show deployed compiled styles").addOption
9219
9969
  }
9220
9970
  printMapStatus(status, api.apiUrl);
9221
9971
  });
9972
+ registerDeploymentCommands(program, {
9973
+ resolveApi: (options, behavior) => {
9974
+ const source = resolveDeploySource(process.env);
9975
+ return requireApiOptions(options, {
9976
+ ...behavior,
9977
+ allowStoredCredential: allowsStoredDeployCredential(source),
9978
+ assertExplicitKeyMap: true
9979
+ });
9980
+ }
9981
+ });
9222
9982
  var iconsCommand = program.command("icons").description("Inspect managed Tileflow icons");
9223
9983
  registerIconLockCommands(program, { defaultApiUrl, defaultConfigPath, loadAuthConfig });
9224
9984
  registerIconListCommand(iconsCommand, { defaultConfigPath });
@@ -9271,6 +10031,8 @@ registerMapCommands(program, { defaultApiUrl, loadAuthConfig });
9271
10031
  if (isRootCliVersionRequest(process.argv.slice(2))) {
9272
10032
  process.stdout.write(`${getCliVersion()}
9273
10033
  `);
10034
+ } else if (isRestoreJsonCommand(process.argv.slice(2))) {
10035
+ await parseRestoreJson(program);
9274
10036
  } else {
9275
10037
  const structuredCommand = process.argv[2] ?? "tileflow";
9276
10038
  const structuredErrors = process.argv.includes("--json") && [
@@ -9284,14 +10046,7 @@ if (isRootCliVersionRequest(process.argv.slice(2))) {
9284
10046
  "semantic-diff",
9285
10047
  "language"
9286
10048
  ].includes(structuredCommand);
9287
- if (structuredErrors) {
9288
- const configureStructuredErrors = (command2) => {
9289
- command2.exitOverride().configureOutput({ writeErr: () => {
9290
- } });
9291
- for (const child of command2.commands) configureStructuredErrors(child);
9292
- };
9293
- configureStructuredErrors(program);
9294
- }
10049
+ if (structuredErrors) configureJsonErrors(program);
9295
10050
  program.parseAsync().catch((error) => {
9296
10051
  if (structuredErrors) {
9297
10052
  if (typeof error === "object" && error !== null && "exitCode" in error && error.exitCode === 0)
@@ -9469,7 +10224,7 @@ async function runTileflowComparisonPreview(options, address) {
9469
10224
  await new Promise((resolveListening, rejectListening) => {
9470
10225
  const createdServer = serve(
9471
10226
  {
9472
- fetch: (request) => redirectTileflowDevNavigation(request, origin) ?? fetch2(request),
10227
+ fetch: (request2) => redirectTileflowDevNavigation(request2, origin) ?? fetch2(request2),
9473
10228
  hostname: address.host,
9474
10229
  port: address.port
9475
10230
  },
@@ -9514,8 +10269,8 @@ async function runTileflowComparisonPreview(options, address) {
9514
10269
  }
9515
10270
  function comparisonCaptureConfigArgument(configPath) {
9516
10271
  const cwd = realpathSync(process.cwd());
9517
- const requestedConfigPath = resolve11(process.cwd(), configPath);
9518
- const defaultRequestedPath = resolve11(process.cwd(), defaultConfigPath);
10272
+ const requestedConfigPath = resolve12(process.cwd(), configPath);
10273
+ const defaultRequestedPath = resolve12(process.cwd(), defaultConfigPath);
9519
10274
  const absoluteConfigPath = existsSync(requestedConfigPath) ? realpathSync(requestedConfigPath) : requestedConfigPath;
9520
10275
  const absoluteDefaultPath = existsSync(defaultRequestedPath) ? realpathSync(defaultRequestedPath) : defaultRequestedPath;
9521
10276
  if (absoluteConfigPath === absoluteDefaultPath) return void 0;
@@ -9569,7 +10324,7 @@ async function loginWithDeviceFlow(options) {
9569
10324
  throw new Error("The dashboard returned an unexpected Tileflow API origin.");
9570
10325
  }
9571
10326
  const previousSession = authState.kind === "sessions" ? authState.config.sessions[apiUrl] : void 0;
9572
- printKeyValue("Code", pc7.bold(authorization.userCode));
10327
+ printKeyValue("Code", pc8.bold(authorization.userCode));
9573
10328
  printKeyValue("URL", link(authorization.verificationUriComplete));
9574
10329
  if (options.noBrowser) {
9575
10330
  logMuted("Open the URL above to continue.");
@@ -9579,7 +10334,7 @@ async function loginWithDeviceFlow(options) {
9579
10334
  output: process.stdout
9580
10335
  });
9581
10336
  try {
9582
- await readline.question(`${pc7.cyan("?")} Press Enter to open Tileflow Dashboard...`);
10337
+ await readline.question(`${pc8.cyan("?")} Press Enter to open Tileflow Dashboard...`);
9583
10338
  openBrowser(authorization.verificationUriComplete);
9584
10339
  } finally {
9585
10340
  readline.close();
@@ -9644,7 +10399,7 @@ function openBrowser(url, errorsToStderr = false) {
9644
10399
  });
9645
10400
  }
9646
10401
  async function writeDeployManifest(manifestPath, manifest) {
9647
- const outputPath = resolve11(process.cwd(), manifestPath);
10402
+ const outputPath = resolve12(process.cwd(), manifestPath);
9648
10403
  await writeAtomicFile({
9649
10404
  boundaryPath: dirname12(outputPath),
9650
10405
  force: true,
@@ -9696,7 +10451,7 @@ function selectManagedDeployMaps(mapNames, options) {
9696
10451
  "This managed target requires an explicit map because the config contains multiple maps."
9697
10452
  );
9698
10453
  printKeyValue("Maps", mapNames.join(", "));
9699
- printNextSteps([
10454
+ printNextSteps2([
9700
10455
  `Retry with ${command(`tileflow deploy ${options.mapId ? `--map-id ${options.mapId} ` : ""}--map ${mapNames[0]}`)}.`
9701
10456
  ]);
9702
10457
  process.exitCode = 1;
@@ -9705,7 +10460,7 @@ function selectManagedDeployMaps(mapNames, options) {
9705
10460
  function validateMapSetupTarget(options, source) {
9706
10461
  const fail = (message, steps = []) => {
9707
10462
  logError(message);
9708
- printNextSteps(steps);
10463
+ printNextSteps2(steps);
9709
10464
  process.exitCode = 1;
9710
10465
  return false;
9711
10466
  };
@@ -9745,7 +10500,7 @@ function validateDeployManifestMapNames(mapNames) {
9745
10500
  async function loadExistingDeployManifest(manifestPath, options) {
9746
10501
  let source;
9747
10502
  try {
9748
- source = await readFile6(resolve11(process.cwd(), manifestPath), "utf8");
10503
+ source = await readFile6(resolve12(process.cwd(), manifestPath), "utf8");
9749
10504
  } catch (error) {
9750
10505
  if (error.code === "ENOENT") return null;
9751
10506
  throw new Error("Existing Tileflow manifest could not be read.");
@@ -9777,64 +10532,59 @@ async function loadExistingDeployManifest(manifestPath, options) {
9777
10532
  return manifest;
9778
10533
  }
9779
10534
  async function requireApiOptions(options, behavior = {}) {
10535
+ const refuse = (code, message, steps = []) => {
10536
+ if (behavior.onFailure) behavior.onFailure(code, message);
10537
+ else if (!behavior.silent) {
10538
+ logError(message);
10539
+ if (steps.length) printNextSteps2(steps);
10540
+ }
10541
+ process.exitCode = 1;
10542
+ return null;
10543
+ };
9780
10544
  const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
9781
10545
  const requestedMapId = options.mapId;
9782
10546
  if (!requestedMapId || !/^map_[A-Za-z0-9_-]{16}$/u.test(requestedMapId)) {
9783
- if (!behavior.silent) logError("Managed Map ID is invalid.");
9784
- process.exitCode = 1;
9785
- return null;
10547
+ return refuse("invalid_map_id", "Managed Map ID is invalid.");
9786
10548
  }
9787
10549
  if (options.apiKey !== void 0) {
9788
10550
  if (!isHostedApiKey(options.apiKey)) {
9789
- if (!behavior.silent) logError("Tileflow API key is invalid.");
9790
- process.exitCode = 1;
9791
- return null;
10551
+ return refuse("invalid_api_key", "Tileflow API key is invalid.");
9792
10552
  }
9793
10553
  if (behavior.assertExplicitKeyMap) {
9794
10554
  const profile = await validateApiKey(apiUrl, options.apiKey);
9795
10555
  if (!profile.ok) {
9796
- if (!behavior.silent) logError(profile.error);
9797
- process.exitCode = 1;
9798
- return null;
10556
+ return refuse("api_key_validation_failed", profile.error);
9799
10557
  }
9800
10558
  if (!apiProfileCanTargetMap(profile.value, requestedMapId)) {
9801
- if (!behavior.silent) logError("This API key belongs to another Map.");
9802
- process.exitCode = 1;
9803
- return null;
10559
+ return refuse("api_key_map_mismatch", "This API key belongs to another Map.");
9804
10560
  }
9805
10561
  }
9806
10562
  return { apiKey: options.apiKey, apiUrl, mapId: requestedMapId };
9807
10563
  }
9808
10564
  const allowStoredCredential = behavior.allowStoredCredential !== false && allowsStoredDeployCredential(resolveDeploySource(process.env));
9809
10565
  if (!allowStoredCredential) {
9810
- if (!behavior.silent) {
9811
- logError("CI requires an explicit Tileflow credential with access to the target Map.");
9812
- printNextSteps([
9813
- `Set ${pc7.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
10566
+ return refuse(
10567
+ "api_key_required",
10568
+ "CI requires an explicit Tileflow credential with access to the target Map.",
10569
+ [
10570
+ `Set ${pc8.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
9814
10571
  "The saved personal account session is never used in CI."
9815
- ]);
9816
- }
9817
- process.exitCode = 1;
9818
- return null;
10572
+ ]
10573
+ );
9819
10574
  }
9820
10575
  let config;
9821
10576
  try {
9822
10577
  config = await loadAuthConfig();
9823
10578
  } catch (error) {
9824
- if (!behavior.silent) logError(safeAuthError(error));
9825
- process.exitCode = 1;
9826
- return null;
10579
+ return refuse("auth_state_unavailable", safeAuthError(error));
9827
10580
  }
9828
10581
  const resolvedSession = resolveAccountSession(config, apiUrl);
9829
10582
  if (resolvedSession.kind !== "selected") {
9830
- if (!behavior.silent) {
9831
- logError(
9832
- resolvedSession.kind === "expired" ? "The Tileflow account session has expired." : "No Tileflow account session is saved."
9833
- );
9834
- printNextSteps([`Run ${command("tileflow login")} to authorize this machine.`]);
9835
- }
9836
- process.exitCode = 1;
9837
- return null;
10583
+ return refuse(
10584
+ `account_session_${resolvedSession.kind}`,
10585
+ resolvedSession.kind === "expired" ? "The Tileflow account session has expired." : "No Tileflow account session is saved.",
10586
+ [`Run ${command("tileflow login")} to authorize this machine.`]
10587
+ );
9838
10588
  }
9839
10589
  const capability = await requestMapCapability(
9840
10590
  resolvedSession.session,
@@ -9842,9 +10592,7 @@ async function requireApiOptions(options, behavior = {}) {
9842
10592
  behavior.capabilityScopes ?? ["status:read"]
9843
10593
  );
9844
10594
  if (!capability.ok) {
9845
- if (!behavior.silent) logError(capability.error);
9846
- process.exitCode = 1;
9847
- return null;
10595
+ return refuse("map_capability_failed", capability.error);
9848
10596
  }
9849
10597
  return { apiKey: capability.capability, apiUrl, mapId: capability.mapId };
9850
10598
  }
@@ -9879,72 +10627,72 @@ function printCliError(error) {
9879
10627
  }
9880
10628
  const message = error instanceof Error ? error.message : "Command failed.";
9881
10629
  if (message.includes("Cannot find module '@tileflow/core'")) {
9882
- logError(`Cannot load ${pc7.cyan("@tileflow/core")} from this project.`);
9883
- printNextSteps([
9884
- `Install ${pc7.cyan("@tileflow/core")} in this project.`,
10630
+ logError(`Cannot load ${pc8.cyan("@tileflow/core")} from this project.`);
10631
+ printNextSteps2([
10632
+ `Install ${pc8.cyan("@tileflow/core")} in this project.`,
9885
10633
  `Run the command again from the project root.`
9886
10634
  ]);
9887
10635
  return;
9888
10636
  }
9889
10637
  if (message.includes("Cannot find module '@tileflow/maps'")) {
9890
- logError(`Cannot load ${pc7.cyan("@tileflow/maps")} from this project.`);
9891
- printNextSteps([
9892
- `Install ${pc7.cyan("@tileflow/maps")} in this project.`,
10638
+ logError(`Cannot load ${pc8.cyan("@tileflow/maps")} from this project.`);
10639
+ printNextSteps2([
10640
+ `Install ${pc8.cyan("@tileflow/maps")} in this project.`,
9893
10641
  `Run the command again from the project root.`
9894
10642
  ]);
9895
10643
  return;
9896
10644
  }
9897
10645
  logError(message.split("\n")[0] ?? message);
9898
10646
  if (message.includes(defaultConfigPath) || message.includes("config")) {
9899
- printNextSteps([`Run ${command("tileflow validate")} to check the config.`]);
10647
+ printNextSteps2([`Run ${command("tileflow validate")} to check the config.`]);
9900
10648
  }
9901
10649
  }
9902
10650
  function logSuccess(message) {
9903
- console.log(`${pc7.green("\u2713")} ${message}`);
10651
+ console.log(`${pc8.green("\u2713")} ${message}`);
9904
10652
  }
9905
10653
  function logError(message) {
9906
- console.log(`${pc7.red("\u2715")} ${message}`);
10654
+ console.log(`${pc8.red("\u2715")} ${message}`);
9907
10655
  }
9908
10656
  function logWarning(message) {
9909
- console.log(`${pc7.yellow("!")} ${message}`);
10657
+ console.log(`${pc8.yellow("!")} ${message}`);
9910
10658
  }
9911
10659
  function logInfo(message) {
9912
- console.log(`${pc7.cyan("i")} ${message}`);
10660
+ console.log(`${pc8.cyan("i")} ${message}`);
9913
10661
  }
9914
10662
  function logMuted(message) {
9915
- console.log(pc7.gray(message));
10663
+ console.log(pc8.gray(message));
9916
10664
  }
9917
10665
  function printTitle(title, subtitle) {
9918
- console.log(pc7.bold(title));
10666
+ console.log(pc8.bold(title));
9919
10667
  if (subtitle) {
9920
- console.log(pc7.gray(subtitle));
10668
+ console.log(pc8.gray(subtitle));
9921
10669
  }
9922
10670
  }
9923
10671
  function printKeyValue(label, value) {
9924
- console.log(`${pc7.gray(`${label}:`.padEnd(13))} ${value}`);
10672
+ console.log(`${pc8.gray(`${label}:`.padEnd(13))} ${value}`);
9925
10673
  }
9926
10674
  function printChecks(items) {
9927
10675
  for (const item of items) {
9928
- console.log(` ${pc7.green("\u2713")} ${item}`);
10676
+ console.log(` ${pc8.green("\u2713")} ${item}`);
9929
10677
  }
9930
10678
  }
9931
- function printNextSteps(steps) {
10679
+ function printNextSteps2(steps) {
9932
10680
  if (steps.length === 0) return;
9933
10681
  console.log(`
9934
- ${pc7.bold("Next steps")}`);
10682
+ ${pc8.bold("Next steps")}`);
9935
10683
  for (const step of steps) {
9936
- console.log(` ${pc7.cyan("\u2022")} ${step}`);
10684
+ console.log(` ${pc8.cyan("\u2022")} ${step}`);
9937
10685
  }
9938
10686
  }
9939
10687
  function printValidationErrors(messages) {
9940
10688
  logError("Tileflow config has errors.");
9941
10689
  for (const message of messages) {
9942
- const path = message.path ? pc7.cyan(message.path) : pc7.cyan("(root)");
9943
- console.log(` ${pc7.red("\u2715")} ${path} ${pc7.gray("-")} ${message.message}`);
10690
+ const path = message.path ? pc8.cyan(message.path) : pc8.cyan("(root)");
10691
+ console.log(` ${pc8.red("\u2715")} ${path} ${pc8.gray("-")} ${message.message}`);
9944
10692
  }
9945
10693
  }
9946
10694
  function printAccountDetails(session) {
9947
- printKeyValue("Account", pc7.bold(session.account.name));
10695
+ printKeyValue("Account", pc8.bold(session.account.name));
9948
10696
  printKeyValue("Email", session.account.email);
9949
10697
  printKeyValue("API", link(session.apiOrigin));
9950
10698
  printKeyValue("Expires", formatDate(session.expiresAt));
@@ -9973,11 +10721,11 @@ function printDeployedMaps(maps) {
9973
10721
  const entries = Object.entries(maps);
9974
10722
  if (entries.length === 0) return;
9975
10723
  console.log(`
9976
- ${pc7.bold("Maps")}`);
10724
+ ${pc8.bold("Maps")}`);
9977
10725
  for (const [name, map] of entries) {
9978
10726
  for (const [themeName, theme] of Object.entries(map.themes)) {
9979
10727
  const label = `${name}/${themeName}`;
9980
- console.log(` ${pc7.green("\u2713")} ${label.padEnd(24)} ${link(theme.styleUrl)}`);
10728
+ console.log(` ${pc8.green("\u2713")} ${label.padEnd(24)} ${link(theme.styleUrl)}`);
9981
10729
  }
9982
10730
  }
9983
10731
  }
@@ -9995,13 +10743,13 @@ function plural(count, singular, pluralForm = `${singular}s`) {
9995
10743
  return `${count} ${count === 1 ? singular : pluralForm}`;
9996
10744
  }
9997
10745
  function command(value) {
9998
- return pc7.cyan(value);
10746
+ return pc8.cyan(value);
9999
10747
  }
10000
10748
  function link(value) {
10001
- return pc7.cyan(value);
10749
+ return pc8.cyan(value);
10002
10750
  }
10003
10751
  function pathLabel(value) {
10004
- return pc7.cyan(value);
10752
+ return pc8.cyan(value);
10005
10753
  }
10006
10754
  function starterConfig() {
10007
10755
  return `import { defineMap, defineTheme, labels, poi } from "@tileflow/core";
@@ -10047,15 +10795,15 @@ function printHostedCompatibilityIssues(issues) {
10047
10795
  }
10048
10796
  function printMapStatus(status, apiUrl) {
10049
10797
  printTitle("Tileflow status");
10050
- printKeyValue("Map", pc7.bold(status.mapId));
10798
+ printKeyValue("Map", pc8.bold(status.mapId));
10051
10799
  console.log(`
10052
- ${pc7.bold("Styles")}`);
10800
+ ${pc8.bold("Styles")}`);
10053
10801
  if (status.styles.length === 0) {
10054
10802
  logMuted(" No styles deployed.");
10055
10803
  }
10056
10804
  for (const style of status.styles) {
10057
10805
  console.log(
10058
- ` ${pc7.green("\u2713")} ${style.environment.padEnd(16)} ${link(`${apiUrl}/maps/${style.mapId}/style.json`).padEnd(36)} ${formatBytes4(style.size).padStart(10)} ${pc7.gray(formatDate(style.uploaded))}`
10806
+ ` ${pc8.green("\u2713")} ${style.environment.padEnd(16)} ${link(`${apiUrl}/maps/${style.mapId}/style.json`).padEnd(36)} ${formatBytes4(style.size).padStart(10)} ${pc8.gray(formatDate(style.uploaded))}`
10059
10807
  );
10060
10808
  }
10061
10809
  }