tileflow 0.1.0-alpha.13 → 0.1.0-alpha.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7,9 +7,9 @@ import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
7
7
  import { existsSync, readFileSync, realpathSync } from "fs";
8
8
  import { readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
9
9
  import { hostname } from "os";
10
- import { dirname as dirname12, isAbsolute as isAbsolute5, relative as relative5, resolve as resolve11, sep as sep5 } from "path";
10
+ import { dirname as dirname13, isAbsolute as isAbsolute5, relative as relative6, resolve as resolve12, sep as sep5 } from "path";
11
11
  import { createInterface } from "readline/promises";
12
- import pc6 from "picocolors";
12
+ import pc7 from "picocolors";
13
13
  import { createLocalCoordinates, setupCoordinates } from "@tileflow/coordinates-runtime";
14
14
  import {
15
15
  auditTileflowMapThemeValues as auditTileflowMapThemeValues2,
@@ -33,12 +33,12 @@ import {
33
33
  writeTileflowBuildArtifacts
34
34
  } from "@tileflow/dev/artifacts";
35
35
  import {
36
- assertValidTileflowConfig as assertValidTileflowConfig3,
36
+ assertValidTileflowConfig as assertValidTileflowConfig4,
37
37
  defaultTileflowApiUrl,
38
- defaultTileflowConfigPath,
38
+ defaultTileflowConfigPath as defaultTileflowConfigPath2,
39
39
  defaultTileflowManifestPath,
40
40
  getTileflowMapNames as getTileflowMapNames5,
41
- loadTileflowConfigWithInputs as loadTileflowConfigWithInputs4,
41
+ loadTileflowConfigWithInputs as loadTileflowConfigWithInputs5,
42
42
  TileflowValidationError as TileflowValidationError4
43
43
  } from "@tileflow/dev/config";
44
44
  import {
@@ -3572,7 +3572,7 @@ async function readCoordinatesInput(input, signal) {
3572
3572
  async function readStandardInput(signal) {
3573
3573
  const chunks = [];
3574
3574
  let length = 0;
3575
- return new Promise((resolve12, reject) => {
3575
+ return new Promise((resolve13, reject) => {
3576
3576
  const finish = (result) => {
3577
3577
  process.stdin.removeListener("data", onData);
3578
3578
  process.stdin.removeListener("end", onEnd);
@@ -3594,7 +3594,7 @@ async function readStandardInput(signal) {
3594
3594
  }
3595
3595
  chunks.push(bytes);
3596
3596
  };
3597
- const onEnd = () => finish(() => resolve12(decodeInput(Buffer.concat(chunks))));
3597
+ const onEnd = () => finish(() => resolve13(decodeInput(Buffer.concat(chunks))));
3598
3598
  const onError = () => finish(() => reject(new CoordinatesInputError("INVALID_VALUE")));
3599
3599
  if (signal.aborted) {
3600
3600
  onAbort();
@@ -3922,7 +3922,7 @@ function isRecord(value) {
3922
3922
 
3923
3923
  // src/describe-command.ts
3924
3924
  function registerDescribeCommand(program2) {
3925
- program2.command("describe").description("Discover domains and query the generated authoring schema without loading a map").argument("[topic]", "domain name, expressions, or schema").option(
3925
+ program2.command("describe").description("Discover domains and query the generated authoring schema without loading a map").argument("[topic]", "domain name, themes, expressions, or schema").option(
3926
3926
  "--ref <pointer>",
3927
3927
  "exact local schema reference, such as #/$defs/TileflowRoadsModuleOptions"
3928
3928
  ).option("--path <pointer>", "JSON Pointer within the selected topic or schema", "").option("--limit <count>", "maximum returned tree entries (1\u2013500)", "80").option("--depth <count>", "maximum returned tree depth (1\u201312)", "4").option("--json", "print deterministic bounded JSON").action(
@@ -3933,7 +3933,7 @@ function registerDescribeCommand(program2) {
3933
3933
  const manifest = tileflowAuthoringManifest2;
3934
3934
  let value;
3935
3935
  let schemaRef;
3936
- if (options.ref || topic && topic !== "expressions") {
3936
+ if (options.ref || topic && topic !== "expressions" && topic !== "themes") {
3937
3937
  const schema = await loadPackagedSchema();
3938
3938
  const domain = manifest.domains.find((entry) => entry.name === topic);
3939
3939
  if (topic && topic !== "schema" && !domain)
@@ -3946,6 +3946,8 @@ function registerDescribeCommand(program2) {
3946
3946
  value = selectJsonPointer(schema, schemaRef.replace(/^#/u, ""));
3947
3947
  } else if (topic === "expressions") {
3948
3948
  value = manifest.expressions;
3949
+ } else if (topic === "themes") {
3950
+ value = manifest.themes;
3949
3951
  } else {
3950
3952
  value = {
3951
3953
  domains: manifest.domains.map(({ name, api, optionsSchemaRef }) => ({
@@ -3953,7 +3955,7 @@ function registerDescribeCommand(program2) {
3953
3955
  api,
3954
3956
  schemaRef: optionsSchemaRef
3955
3957
  })),
3956
- topics: ["expressions", "schema"],
3958
+ topics: ["expressions", "schema", "themes"],
3957
3959
  commands: ["describe", "inspect", "capture", "compare", "preview"]
3958
3960
  };
3959
3961
  }
@@ -4150,7 +4152,18 @@ var hostedStyleDeploymentResponseSchema = z2.object({
4150
4152
  styleUrl: publicHttpUrlSchema
4151
4153
  }).strict()
4152
4154
  ).refine((themes) => Object.keys(themes).length > 0),
4153
- version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
4155
+ version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional(),
4156
+ /**
4157
+ * The Team 3D models the map places, each at its published current version, and whether that
4158
+ * version was prepared from the same glTF binaries as the map's own files.
4159
+ */
4160
+ teamModels: z2.array(
4161
+ z2.object({
4162
+ model: z2.string().regex(/^[a-z0-9]+(?:-[a-z0-9]+)*$/u).max(64),
4163
+ version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER),
4164
+ sources: z2.enum(["match", "differ", "unknown"])
4165
+ }).strict()
4166
+ ).max(256).optional()
4154
4167
  }).strict();
4155
4168
  var hostedIconPackageResponseSchema = z2.object({
4156
4169
  changed: z2.boolean().optional(),
@@ -4261,7 +4274,7 @@ async function startDeviceAuthorization(appUrl, body, options = {}) {
4261
4274
  async function pollDeviceToken(appUrl, authorization, input, options = {}) {
4262
4275
  const expiresAt = Date.now() + authorization.expiresIn * 1e3;
4263
4276
  const intervalMs = Math.max(authorization.interval, 1) * 1e3;
4264
- const wait = input.sleep ?? ((milliseconds) => new Promise((resolve12) => setTimeout(resolve12, milliseconds)));
4277
+ const wait = input.sleep ?? ((milliseconds) => new Promise((resolve13) => setTimeout(resolve13, milliseconds)));
4265
4278
  while (Date.now() < expiresAt) {
4266
4279
  await wait(intervalMs);
4267
4280
  const response = await requestHostedJson(
@@ -4561,7 +4574,7 @@ async function withHostedResponse(apiUrl, path, init, options, consume) {
4561
4574
  }
4562
4575
  function raceWithAbort(operation, signal) {
4563
4576
  if (signal.aborted) return Promise.reject(signal.reason);
4564
- return new Promise((resolve12, reject) => {
4577
+ return new Promise((resolve13, reject) => {
4565
4578
  const cleanup = () => signal.removeEventListener("abort", onAbort);
4566
4579
  const onAbort = () => {
4567
4580
  cleanup();
@@ -4571,7 +4584,7 @@ function raceWithAbort(operation, signal) {
4571
4584
  operation.then(
4572
4585
  (value) => {
4573
4586
  cleanup();
4574
- resolve12(value);
4587
+ resolve13(value);
4575
4588
  },
4576
4589
  (error) => {
4577
4590
  cleanup();
@@ -4773,22 +4786,6 @@ function inspectTileflowHostedCompatibility(project, styles) {
4773
4786
  });
4774
4787
  }
4775
4788
  }
4776
- const own = /* @__PURE__ */ new Set();
4777
- for (const style of Object.values(themes)) {
4778
- const models = style?.metadata?.["tileflow:buildings"]?.models;
4779
- if (!Array.isArray(models)) continue;
4780
- for (const entry of models) {
4781
- const { model, url } = entry ?? {};
4782
- if (typeof url === "string" && url.includes(`models/${mapName}/`) && typeof model === "string")
4783
- own.add(model);
4784
- }
4785
- }
4786
- if (own.size > 0)
4787
- issues.push({
4788
- map: mapName,
4789
- message: `Hosted deploy does not take a map's own 3D building models yet. Map ${mapName} places ${[...own].sort(compareCodeUnits5).join(", ")}; keep the map local or remove them from buildings.threeDimensional.models.`,
4790
- path: `maps.${mapName}.modules.buildings.threeDimensional.models`
4791
- });
4792
4789
  }
4793
4790
  return issues;
4794
4791
  }
@@ -4797,20 +4794,50 @@ function cloneStyle(style) {
4797
4794
  }
4798
4795
 
4799
4796
  // src/model-assets.ts
4800
- import { prepareTileflowMapTreeModels } from "@tileflow/dev/icons";
4797
+ import { tileflowTeamModelUrl } from "@tileflow/core";
4798
+ import {
4799
+ prepareTileflowMapTreeModels
4800
+ } from "@tileflow/dev/icons";
4801
4801
  async function prepareModelAssets(project, options) {
4802
- const assets = {};
4802
+ const prepared = { assets: {}, buildings: {}, identities: {} };
4803
4803
  for (const [name, map] of Object.entries(project.maps)) {
4804
4804
  if (!map.models?.length) continue;
4805
- const prepared = await prepareTileflowMapTreeModels(map.models, {
4805
+ const models = await prepareTileflowMapTreeModels(map.models, {
4806
4806
  ...options,
4807
4807
  cwd: process.cwd(),
4808
4808
  configPath: `maps.${name}.models`
4809
4809
  });
4810
- assets[name] = { models: prepared.models };
4810
+ prepared.assets[name] = { models: models.models };
4811
+ if (Object.keys(models.buildings).length > 0) prepared.buildings[name] = models.buildings;
4812
+ if (models.identities.length > 0) prepared.identities[name] = models.identities;
4813
+ }
4814
+ return prepared;
4815
+ }
4816
+ function mapAssetsWithBuildingModels(prepared, target) {
4817
+ const assets = { ...prepared.assets };
4818
+ for (const [mapName, buildings] of Object.entries(prepared.buildings)) {
4819
+ const base = target.hosted ? "" : target.localBase(mapName).replace(/\/+$/u, "");
4820
+ assets[mapName] = {
4821
+ ...assets[mapName],
4822
+ buildingModels: Object.fromEntries(
4823
+ Object.entries(buildings).map(([id, model]) => [
4824
+ id,
4825
+ target.hosted ? tileflowTeamModelUrl(id) : `${base}/${model.fileName}`
4826
+ ])
4827
+ )
4828
+ };
4811
4829
  }
4812
4830
  return assets;
4813
4831
  }
4832
+ function hostedModelSourceIdentities(prepared, mapName) {
4833
+ const buildings = prepared.buildings[mapName] ?? {};
4834
+ const identities = (prepared.identities[mapName] ?? []).filter(({ id }) => !(id in buildings));
4835
+ return identities.length > 0 ? identities : void 0;
4836
+ }
4837
+ function teamModelSources(prepared, mapName) {
4838
+ const buildings = Object.entries(prepared.buildings[mapName] ?? {});
4839
+ return buildings.length > 0 ? Object.fromEntries(buildings.map(([id, model]) => [id, model.sources])) : void 0;
4840
+ }
4814
4841
 
4815
4842
  // src/hosted-native-deploy.ts
4816
4843
  function failure(message) {
@@ -4850,13 +4877,17 @@ async function prepareNativeHostedDeploy(options) {
4850
4877
  const iconPackage = binding ? compiledIcons.packages.find((entry) => entry.contentHash === binding.packageHash) : void 0;
4851
4878
  const sprite = binding ? `${apiOrigin}/sprites/preflight/${binding.packageHash}/sprite` : void 0;
4852
4879
  const modelAssets = await prepareModelAssets(project, { baseDirectory, target: "hosted" });
4880
+ if (modelAssets.buildings[mapName])
4881
+ throw failure(
4882
+ "A deployment with --with-native does not take a map's own 3D building models yet. Deploy without --with-native, or remove them from the map's models directories."
4883
+ );
4853
4884
  const mapAssets = {
4854
- ...modelAssets,
4885
+ ...modelAssets.assets,
4855
4886
  ...Object.fromEntries(
4856
4887
  compiledIcons.bindings.map((entry) => [
4857
4888
  entry.mapName,
4858
4889
  {
4859
- ...modelAssets[entry.mapName],
4890
+ ...modelAssets.assets[entry.mapName],
4860
4891
  icons: {
4861
4892
  ids: entry.iconIds,
4862
4893
  sprite: `${apiOrigin}/sprites/preflight/${entry.packageHash}/sprite`
@@ -6886,6 +6917,421 @@ async function readCurrentPins(contents) {
6886
6917
  return (await parseTileflowIconsLockfile(contents)).sets;
6887
6918
  }
6888
6919
 
6920
+ // src/model-command.ts
6921
+ import { lstat as lstat5 } from "fs/promises";
6922
+ import { basename as basename4, dirname as dirname11, relative as relative4, resolve as resolve8 } from "path";
6923
+ import pc4 from "picocolors";
6924
+ import { z as z5 } from "zod";
6925
+ import { tileflowModelContentType } from "@tileflow/core";
6926
+ import {
6927
+ assertValidTileflowConfig as assertValidTileflowConfig3,
6928
+ defaultTileflowConfigPath,
6929
+ loadTileflowConfigWithInputs as loadTileflowConfigWithInputs4
6930
+ } from "@tileflow/dev/config";
6931
+ import {
6932
+ prepareTileflowMapTreeModels as prepareTileflowMapTreeModels2
6933
+ } from "@tileflow/dev/icons";
6934
+ var modelName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
6935
+ var sha256 = /^[a-f0-9]{64}$/u;
6936
+ var maximumListPages = 20;
6937
+ function registerModelCommands(program2, dependencies) {
6938
+ const models = program2.command("models").description("Publish and manage the Team 3D models that maps place");
6939
+ withTeamOptions2(
6940
+ models.command("publish").description(
6941
+ "Prepare building models and publish each as its Team 3D model's current version"
6942
+ ).argument(
6943
+ "[paths...]",
6944
+ "model files (<name>.glb) or directories; defaults to the maps' models directories"
6945
+ ).option(
6946
+ "-c, --config <path>",
6947
+ "config path, read when no path is given",
6948
+ defaultTileflowConfigPath
6949
+ )
6950
+ ).action(async (paths, options) => {
6951
+ let prepared;
6952
+ try {
6953
+ prepared = paths.length > 0 ? await prepareFromPaths(paths) : await prepareFromConfig(options.config);
6954
+ } catch (error) {
6955
+ return emitFailure(options.json, "model_preparation_failed", safeMessage(error));
6956
+ }
6957
+ if (prepared.size === 0) {
6958
+ return emitFailure(
6959
+ options.json,
6960
+ "no_building_models",
6961
+ "No building models were found. A building model is a .glb whose materials name its parts: surface, secondary, glass, accent and roof."
6962
+ );
6963
+ }
6964
+ const authority = await resolveTeamAuthority(options, ["models:write"], dependencies);
6965
+ if (!authority) return;
6966
+ const results = [];
6967
+ for (const [name, model] of [...prepared].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
6968
+ const body = new FormData();
6969
+ body.append(
6970
+ "metadata",
6971
+ new Blob([JSON.stringify({ sources: model.sources })], { type: "application/json" }),
6972
+ "metadata.json"
6973
+ );
6974
+ const bytes = new Uint8Array(model.bytes.byteLength);
6975
+ bytes.set(model.bytes);
6976
+ body.append(
6977
+ "file",
6978
+ new Blob([bytes.buffer], { type: tileflowModelContentType }),
6979
+ `${name}.glb`
6980
+ );
6981
+ const response = await authorizedTeamRequest(
6982
+ authority,
6983
+ `/v1/models/${encodeURIComponent(name)}/versions/${model.sha256}`,
6984
+ "PUT",
6985
+ { body }
6986
+ );
6987
+ if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
6988
+ const parsed = publishResponseSchema2.safeParse(response.body);
6989
+ if (!parsed.success || parsed.data.model.current?.contentHash !== model.sha256) {
6990
+ return emitFailure(
6991
+ options.json,
6992
+ "model_publish_unconfirmed",
6993
+ `The publication of ${name} did not confirm the submitted model file.`
6994
+ );
6995
+ }
6996
+ results.push(parsed.data);
6997
+ if (!options.json) {
6998
+ const current = parsed.data.model.current;
6999
+ const action = parsed.data.changed ? "Published" : "Unchanged";
7000
+ console.log(
7001
+ `${pc4.green("\u2713")} ${action} 3D model ${pc4.bold(name)} version ${current.version} (${formatBytes3(current.byteLength)}, ${formatCount(current.summary.levels[0]?.triangles ?? 0)} triangles).`
7002
+ );
7003
+ }
7004
+ }
7005
+ if (options.json) {
7006
+ emitJson({
7007
+ command: "models publish",
7008
+ ok: true,
7009
+ schemaVersion: 1,
7010
+ models: results.map(({ changed, model }) => ({
7011
+ changed,
7012
+ contentHash: model.current.contentHash,
7013
+ name: model.name,
7014
+ version: model.current.version
7015
+ }))
7016
+ });
7017
+ } else if (results.some(({ changed }) => changed)) {
7018
+ console.log("Maps that place them draw the new versions on their next load.");
7019
+ }
7020
+ });
7021
+ withTeamOptions2(models.command("list").description("List the Team's 3D models")).action(
7022
+ async (options) => {
7023
+ const authority = await resolveTeamAuthority(options, ["models:read"], dependencies);
7024
+ if (!authority) return;
7025
+ const listed = [];
7026
+ let cursor;
7027
+ for (let page = 0; page < maximumListPages; page += 1) {
7028
+ const response = await authorizedTeamRequest(
7029
+ authority,
7030
+ `/v1/models${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`,
7031
+ "GET"
7032
+ );
7033
+ if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body);
7034
+ const parsed = listResponseSchema.safeParse(response.body);
7035
+ if (!parsed.success)
7036
+ return emitFailure(options.json, "invalid_response", "The model list was not readable.");
7037
+ listed.push(...parsed.data.models);
7038
+ cursor = parsed.data.nextCursor;
7039
+ if (!cursor) break;
7040
+ }
7041
+ if (options.json)
7042
+ return emitJson({ command: "models list", ok: true, schemaVersion: 1, models: listed });
7043
+ if (listed.length === 0) {
7044
+ console.log("The Team has no 3D models. Publish one with tileflow models publish.");
7045
+ return;
7046
+ }
7047
+ for (const model of listed) {
7048
+ const current = model.current;
7049
+ console.log(
7050
+ `${pc4.bold(model.name)} ${current ? `version ${current.version}, ${formatBytes3(current.byteLength)}` : "no current version"} ${model.usedByMaps === 1 ? "1 map" : `${model.usedByMaps} maps`}${model.state === "purging" ? pc4.yellow(" purging") : ""}`
7051
+ );
7052
+ }
7053
+ }
7054
+ );
7055
+ withTeamOptions2(
7056
+ models.command("status").description("Show a 3D model's versions and the maps that place it").argument("<name>", "model name")
7057
+ ).action(async (name, options) => {
7058
+ if (!modelName.test(name))
7059
+ return emitFailure(options.json, "invalid_model", "Model name is invalid.");
7060
+ const authority = await resolveTeamAuthority(options, ["models:read"], dependencies);
7061
+ if (!authority) return;
7062
+ const detail = await readDetail(authority, name, options.json);
7063
+ if (!detail) return;
7064
+ if (options.json) return emitJson({ ...detail, command: "models status", ok: true });
7065
+ const { model, versions, uses } = detail;
7066
+ console.log(
7067
+ `${pc4.bold(model.name)}${model.state === "purging" ? pc4.yellow(" (purging)") : ""}`
7068
+ );
7069
+ for (const version of versions) {
7070
+ const role = version.state === "current" ? "current" : version.state === "previous" ? "previous" : version.state;
7071
+ console.log(
7072
+ ` version ${version.version} ${role} ${formatBytes3(version.byteLength)} ${formatCount(version.summary.levels[0]?.triangles ?? 0)} triangles ${version.publishedAt}`
7073
+ );
7074
+ }
7075
+ console.log(
7076
+ uses.length === 0 ? " No map places it." : ` Placed by ${uses.map(({ mapName, mapId }) => mapName ?? mapId).join(", ")}.`
7077
+ );
7078
+ });
7079
+ withTeamOptions2(
7080
+ models.command("restore").description("Make a 3D model's previous version current again, in every map that places it").argument("<name>", "model name")
7081
+ ).action(async (name, options) => {
7082
+ if (!modelName.test(name))
7083
+ return emitFailure(options.json, "invalid_model", "Model name is invalid.");
7084
+ const authority = await resolveTeamAuthority(options, ["models:write"], dependencies);
7085
+ if (!authority) return;
7086
+ const detail = await readDetail(authority, name, options.json);
7087
+ if (!detail) return;
7088
+ const { current, previous } = detail.model;
7089
+ if (!current || !previous) {
7090
+ return emitFailure(
7091
+ options.json,
7092
+ "no_previous_version",
7093
+ `${name} has no previous version to restore.`
7094
+ );
7095
+ }
7096
+ const response = await authorizedTeamRequest(
7097
+ authority,
7098
+ `/v1/models/${encodeURIComponent(name)}/restore`,
7099
+ "POST",
7100
+ {
7101
+ body: JSON.stringify({ expectedCurrentVersion: current.version }),
7102
+ headers: { "Content-Type": "application/json" }
7103
+ }
7104
+ );
7105
+ if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
7106
+ const parsed = restoreResponseSchema.safeParse(response.body);
7107
+ if (!parsed.success || parsed.data.model.current?.version !== previous.version) {
7108
+ return emitFailure(
7109
+ options.json,
7110
+ "model_restore_unconfirmed",
7111
+ `The restore of ${name} was not confirmed.`
7112
+ );
7113
+ }
7114
+ if (options.json)
7115
+ return emitJson({
7116
+ command: "models restore",
7117
+ ok: true,
7118
+ schemaVersion: 1,
7119
+ model: parsed.data.model
7120
+ });
7121
+ console.log(
7122
+ `${pc4.green("\u2713")} ${pc4.bold(name)} is at version ${previous.version} again; version ${current.version} is its previous one. Maps draw it on their next load.`
7123
+ );
7124
+ });
7125
+ withTeamOptions2(
7126
+ models.command("purge").description("Delete a 3D model and its versions; refused while a map places it").argument("<name>", "model name").option("--confirm <name>", "repeat the model name to confirm")
7127
+ ).action(async (name, options) => {
7128
+ if (!modelName.test(name))
7129
+ return emitFailure(options.json, "invalid_model", "Model name is invalid.");
7130
+ if (options.confirm !== name) {
7131
+ return emitFailure(
7132
+ options.json,
7133
+ "confirmation_required",
7134
+ `Repeat the model name to purge it: --confirm ${name}`
7135
+ );
7136
+ }
7137
+ const authority = await resolveTeamAuthority(options, ["models:write"], dependencies);
7138
+ if (!authority) return;
7139
+ const response = await authorizedTeamRequest(
7140
+ authority,
7141
+ `/v1/models/${encodeURIComponent(name)}`,
7142
+ "DELETE",
7143
+ {
7144
+ headers: { "X-Tileflow-Confirm-Model-Purge": name }
7145
+ }
7146
+ );
7147
+ if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
7148
+ const parsed = purgeResponseSchema2.safeParse(response.body);
7149
+ if (!parsed.success)
7150
+ return emitFailure(options.json, "invalid_response", "The purge was not confirmed.");
7151
+ if (options.json)
7152
+ return emitJson({
7153
+ command: "models purge",
7154
+ ok: true,
7155
+ schemaVersion: 1,
7156
+ purge: parsed.data.purge
7157
+ });
7158
+ console.log(
7159
+ `${pc4.green("\u2713")} ${pc4.bold(name)} is being purged; its name is free once its files are deleted.`
7160
+ );
7161
+ });
7162
+ }
7163
+ function withTeamOptions2(command2) {
7164
+ return command2.option("--team <team>", "Team, as @team, when the account has several").option("--api-key <key>", "Team data API key with the 3D models permission").option("--api-url <url>", "Tileflow API origin").option("--json", "machine-readable output");
7165
+ }
7166
+ async function readDetail(authority, name, json2) {
7167
+ const response = await authorizedTeamRequest(
7168
+ authority,
7169
+ `/v1/models/${encodeURIComponent(name)}`,
7170
+ "GET"
7171
+ );
7172
+ if (!response.ok) {
7173
+ emitHttpFailure2(json2, response.status, response.body, name);
7174
+ return void 0;
7175
+ }
7176
+ const parsed = detailResponseSchema.safeParse(response.body);
7177
+ if (!parsed.success) {
7178
+ emitFailure(json2, "invalid_response", `The details of ${name} were not readable.`);
7179
+ return void 0;
7180
+ }
7181
+ return parsed.data;
7182
+ }
7183
+ async function prepareFromPaths(paths) {
7184
+ const cwd = process.cwd();
7185
+ const directories = /* @__PURE__ */ new Map();
7186
+ for (const path of paths) {
7187
+ const absolute = resolve8(cwd, path);
7188
+ const info = await lstat5(absolute).catch(() => void 0);
7189
+ if (!info) throw new Error(`${path} does not exist.`);
7190
+ const directory = info.isDirectory() ? absolute : dirname11(absolute);
7191
+ if (relative4(cwd, directory).startsWith(".."))
7192
+ throw new Error(`${path} is outside the current directory.`);
7193
+ if (info.isDirectory()) {
7194
+ directories.set(directory, "all");
7195
+ continue;
7196
+ }
7197
+ const file = basename4(absolute);
7198
+ if (!file.toLowerCase().endsWith(".glb"))
7199
+ throw new Error(`${path} is not a glTF binary (.glb).`);
7200
+ const name = file.replace(/(\.mid|\.far)?\.glb$/iu, "");
7201
+ const selected = directories.get(directory);
7202
+ if (selected === "all") continue;
7203
+ directories.set(directory, /* @__PURE__ */ new Set([...selected ?? [], name]));
7204
+ }
7205
+ const models = /* @__PURE__ */ new Map();
7206
+ for (const [directory, selected] of directories) {
7207
+ const parent = dirname11(directory);
7208
+ const local = `./${basename4(directory)}`;
7209
+ const display = relative4(cwd, directory) || ".";
7210
+ const prepared = await prepareTileflowMapTreeModels2([local], {
7211
+ baseDirectory: parent,
7212
+ configPath: display,
7213
+ cwd: parent,
7214
+ target: "hosted"
7215
+ });
7216
+ for (const [name, model] of Object.entries(prepared.buildings)) {
7217
+ if (selected !== "all" && !selected.has(name)) continue;
7218
+ addModel(models, name, model);
7219
+ }
7220
+ if (selected !== "all") {
7221
+ for (const name of selected)
7222
+ if (!prepared.buildings[name])
7223
+ throw new Error(
7224
+ `${display}/${name}.glb is not a building model: its materials must name its parts (surface, secondary, glass, accent, roof).`
7225
+ );
7226
+ }
7227
+ }
7228
+ return models;
7229
+ }
7230
+ async function prepareFromConfig(configPath) {
7231
+ const loaded = await withTileflowConfigSecretsHidden(
7232
+ () => loadTileflowConfigWithInputs4(configPath)
7233
+ );
7234
+ const project = loaded.project;
7235
+ assertValidTileflowConfig3(project);
7236
+ const prepared = await prepareModelAssets(project, {
7237
+ baseDirectory: dirname11(loaded.configFile),
7238
+ target: "hosted"
7239
+ });
7240
+ const models = /* @__PURE__ */ new Map();
7241
+ for (const buildings of Object.values(prepared.buildings))
7242
+ for (const [name, model] of Object.entries(buildings)) addModel(models, name, model);
7243
+ return models;
7244
+ }
7245
+ function addModel(models, name, model) {
7246
+ const known = models.get(name);
7247
+ if (known && known.sha256 !== model.sha256)
7248
+ throw new Error(`Two different models are named ${name}; a Team has one model of each name.`);
7249
+ models.set(name, model);
7250
+ }
7251
+ function emitHttpFailure2(json2, status, body, name) {
7252
+ const error = z5.object({ error: z5.object({ code: z5.string().max(80), message: z5.string().max(400) }).partial() }).partial().safeParse(body);
7253
+ const message = error.success ? error.data.error?.message : void 0;
7254
+ const code = error.success ? error.data.error?.code : void 0;
7255
+ if (status === 404)
7256
+ return emitFailure(
7257
+ json2,
7258
+ "model_not_found",
7259
+ name ? `The Team has no 3D model named ${name}.` : "Not found."
7260
+ );
7261
+ return emitFailure(
7262
+ json2,
7263
+ code ?? `http_${status}`,
7264
+ message ?? `The request failed with ${status}.`
7265
+ );
7266
+ }
7267
+ function formatBytes3(bytes) {
7268
+ if (bytes < 1024) return `${bytes} B`;
7269
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
7270
+ return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
7271
+ }
7272
+ function formatCount(count) {
7273
+ return new Intl.NumberFormat("en-US").format(count);
7274
+ }
7275
+ var summarySchema = z5.object({
7276
+ levels: z5.array(
7277
+ z5.object({
7278
+ level: z5.enum(["near", "mid", "far"]),
7279
+ vertices: z5.number().int().nonnegative(),
7280
+ triangles: z5.number().int().nonnegative()
7281
+ }).strict()
7282
+ ).min(1).max(3),
7283
+ width: z5.number().nonnegative(),
7284
+ depth: z5.number().nonnegative(),
7285
+ height: z5.number().nonnegative(),
7286
+ footprint: z5.object({
7287
+ rings: z5.number().int().nonnegative(),
7288
+ points: z5.number().int().nonnegative(),
7289
+ area: z5.number().nonnegative()
7290
+ }).strict()
7291
+ }).strict();
7292
+ var versionSchema = z5.object({
7293
+ id: z5.string().regex(/^tmv_[A-Za-z0-9_-]{16,64}$/u),
7294
+ version: z5.number().int().positive(),
7295
+ state: z5.enum(["current", "previous", "gc_pending", "deleting", "deleted"]),
7296
+ contentHash: z5.string().regex(sha256),
7297
+ byteLength: z5.number().int().positive(),
7298
+ publishedAt: z5.iso.datetime({ offset: true }),
7299
+ summary: summarySchema
7300
+ }).strict();
7301
+ var modelSchema = z5.object({
7302
+ id: z5.string().regex(/^tmd_[A-Za-z0-9_-]{16,64}$/u),
7303
+ name: z5.string().regex(modelName).max(64),
7304
+ state: z5.enum(["active", "purging"]),
7305
+ current: versionSchema.nullable(),
7306
+ previous: versionSchema.nullable(),
7307
+ url: z5.url(),
7308
+ usedByMaps: z5.number().int().nonnegative(),
7309
+ updatedAt: z5.iso.datetime({ offset: true })
7310
+ }).strict();
7311
+ var listResponseSchema = z5.object({
7312
+ schemaVersion: z5.literal(1),
7313
+ models: z5.array(modelSchema).max(100),
7314
+ nextCursor: z5.string().min(1).max(512).optional()
7315
+ }).strict();
7316
+ var detailResponseSchema = z5.object({
7317
+ schemaVersion: z5.literal(1),
7318
+ model: modelSchema,
7319
+ versions: z5.array(versionSchema).max(100),
7320
+ uses: z5.array(
7321
+ z5.object({
7322
+ mapId: z5.string().min(1).max(160),
7323
+ mapName: z5.string().max(200).nullable(),
7324
+ relationship: z5.enum(["active", "retained"])
7325
+ }).strict()
7326
+ ).max(100)
7327
+ }).strict();
7328
+ var publishResponseSchema2 = z5.object({ schemaVersion: z5.literal(1), changed: z5.boolean(), model: modelSchema }).strict();
7329
+ var restoreResponseSchema = z5.object({ schemaVersion: z5.literal(1), model: modelSchema }).strict();
7330
+ var purgeResponseSchema2 = z5.object({
7331
+ schemaVersion: z5.literal(1),
7332
+ purge: z5.object({ name: z5.string().regex(modelName), state: z5.enum(["purging", "purged"]) }).strict()
7333
+ }).strict();
7334
+
6889
7335
  // src/native-preview-command.ts
6890
7336
  import { tileflowNativeProfile } from "@tileflow/core/native-profile";
6891
7337
  import {
@@ -6974,7 +7420,7 @@ function resolveTileflowExternalOpenInvocation(target, platform) {
6974
7420
  }
6975
7421
 
6976
7422
  // src/project-commands.ts
6977
- import pc4 from "picocolors";
7423
+ import pc5 from "picocolors";
6978
7424
  function registerProjectCommands(program2, dependencies) {
6979
7425
  const projects = program2.command("projects", { hidden: true }).description("Manage internal application destinations");
6980
7426
  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) => {
@@ -6990,12 +7436,12 @@ function registerProjectCommands(program2, dependencies) {
6990
7436
  };
6991
7437
  if (options.json) return emitJson2(document);
6992
7438
  if (!result.items.length) {
6993
- console.log(pc4.gray("No managed destinations found."));
7439
+ console.log(pc5.gray("No managed destinations found."));
6994
7440
  return;
6995
7441
  }
6996
7442
  for (const target of result.items) {
6997
7443
  console.log(
6998
- ` ${target.project.archivedAt ? pc4.yellow("\u25CB") : pc4.green("\u25CF")} ${pc4.bold(target.project.name)} ${pc4.gray(target.reference)}${target.project.archivedAt ? pc4.gray(" (archived)") : ""}`
7444
+ ` ${target.project.archivedAt ? pc5.yellow("\u25CB") : pc5.green("\u25CF")} ${pc5.bold(target.project.name)} ${pc5.gray(target.reference)}${target.project.archivedAt ? pc5.gray(" (archived)") : ""}`
6999
7445
  );
7000
7446
  }
7001
7447
  });
@@ -7048,8 +7494,8 @@ function registerProjectCommands(program2, dependencies) {
7048
7494
  ...asRecord2(result.body)
7049
7495
  };
7050
7496
  if (options.json) return emitJson2(document);
7051
- console.log(`${pc4.green("\u2713")} Application ${pc4.bold(options.name.trim())} is ready.`);
7052
- console.log(pc4.gray(`Its technical selector is --project @${organization.slug}/${slug}.`));
7497
+ console.log(`${pc5.green("\u2713")} Application ${pc5.bold(options.name.trim())} is ready.`);
7498
+ console.log(pc5.gray(`Its technical selector is --project @${organization.slug}/${slug}.`));
7053
7499
  }
7054
7500
  );
7055
7501
  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) => {
@@ -7071,8 +7517,8 @@ function registerProjectCommands(program2, dependencies) {
7071
7517
  if (!isProjectItem(project)) {
7072
7518
  return emitLocalFailure(false, "invalid_response", "Destination response was invalid.");
7073
7519
  }
7074
- console.log(`${pc4.bold(project.name)} ${pc4.gray(target)}`);
7075
- console.log(project.archivedAt ? pc4.yellow("Archived") : pc4.green("Active"));
7520
+ console.log(`${pc5.bold(project.name)} ${pc5.gray(target)}`);
7521
+ console.log(project.archivedAt ? pc5.yellow("Archived") : pc5.green("Active"));
7076
7522
  });
7077
7523
  for (const archived of [true, false]) {
7078
7524
  const verb = archived ? "archive" : "unarchive";
@@ -7099,7 +7545,7 @@ function registerProjectCommands(program2, dependencies) {
7099
7545
  ...asRecord2(result.body)
7100
7546
  };
7101
7547
  if (options.json) return emitJson2(document);
7102
- console.log(`${pc4.green("\u2713")} Application ${archived ? "archived" : "unarchived"}.`);
7548
+ console.log(`${pc5.green("\u2713")} Application ${archived ? "archived" : "unarchived"}.`);
7103
7549
  });
7104
7550
  }
7105
7551
  }
@@ -7210,7 +7656,7 @@ function emitLocalFailure(json2, code, message, details = {}) {
7210
7656
  console.error(message);
7211
7657
  emitJson2({ schemaVersion: 1, ok: false, error: { code, ...details } });
7212
7658
  } else {
7213
- console.error(`${pc4.red("\u2715")} ${message}`);
7659
+ console.error(`${pc5.red("\u2715")} ${message}`);
7214
7660
  }
7215
7661
  process.exitCode = 1;
7216
7662
  }
@@ -7245,7 +7691,7 @@ function safeMessage2(error) {
7245
7691
  }
7246
7692
 
7247
7693
  // src/renderer-artifact-command.ts
7248
- import { resolve as resolve8 } from "path";
7694
+ import { resolve as resolve9 } from "path";
7249
7695
  import { auditTileflowMapThemeValues } from "@tileflow/core";
7250
7696
  import {
7251
7697
  createTileflowNativeDiagnostic,
@@ -7333,7 +7779,7 @@ async function runRendererArtifactCommand(command2, options) {
7333
7779
  if (artifacts.nativeBuild)
7334
7780
  console.log("Profile: native-v1 (static artifacts; no device qualification).");
7335
7781
  if (command2 === "build")
7336
- console.log(`Output: ${resolve8(outDir, ...renderer === "native" ? ["native"] : [])}`);
7782
+ console.log(`Output: ${resolve9(outDir, ...renderer === "native" ? ["native"] : [])}`);
7337
7783
  }
7338
7784
  } catch (error) {
7339
7785
  const document = createTileflowCommandFailureDocument6(command2, error, process.cwd(), {
@@ -7354,15 +7800,15 @@ async function runRendererArtifactCommand(command2, options) {
7354
7800
  }
7355
7801
 
7356
7802
  // src/tileset-command.ts
7357
- import pc5 from "picocolors";
7358
- import { z as z6 } from "zod";
7803
+ import pc6 from "picocolors";
7804
+ import { z as z7 } from "zod";
7359
7805
  import { serializeCanonicalJson as serializeCanonicalJson6 } from "@tileflow/core";
7360
7806
  import { inspectTileflowPmtiles } from "@tileflow/dev/tilesets";
7361
7807
 
7362
7808
  // src/tileset-upload.ts
7363
7809
  import { createHash as createHash3 } from "crypto";
7364
7810
  import { open as open5, stat } from "fs/promises";
7365
- import { z as z5 } from "zod";
7811
+ import { z as z6 } from "zod";
7366
7812
  import { serializeCanonicalJson as serializeCanonicalJson5 } from "@tileflow/core";
7367
7813
  var teamTilesetPartSize = 16777216;
7368
7814
  var maximumTeamTilesetBytes = 4e9;
@@ -7923,36 +8369,36 @@ function asRecord3(value) {
7923
8369
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
7924
8370
  }
7925
8371
  function defaultSleep(milliseconds) {
7926
- return new Promise((resolve12) => setTimeout(resolve12, milliseconds));
7927
- }
7928
- var identifierSchema = z5.string().min(20).max(80).regex(/^[A-Za-z0-9_-]+$/u);
7929
- var uploadPartStatusSchema = z5.object({
7930
- byteCount: z5.number().int().min(1).max(teamTilesetPartSize),
7931
- partNumber: z5.number().int().min(1).max(239),
7932
- uploaded: z5.boolean(),
7933
- validationState: z5.enum(["mismatch", "pending", "processing", "valid"])
8372
+ return new Promise((resolve13) => setTimeout(resolve13, milliseconds));
8373
+ }
8374
+ var identifierSchema = z6.string().min(20).max(80).regex(/^[A-Za-z0-9_-]+$/u);
8375
+ var uploadPartStatusSchema = z6.object({
8376
+ byteCount: z6.number().int().min(1).max(teamTilesetPartSize),
8377
+ partNumber: z6.number().int().min(1).max(239),
8378
+ uploaded: z6.boolean(),
8379
+ validationState: z6.enum(["mismatch", "pending", "processing", "valid"])
7934
8380
  }).strict();
7935
- var publishedVersionSchema = z5.object({
7936
- byteCount: z5.number().int().min(1).max(maximumTeamTilesetBytes),
7937
- contentHash: z5.string().regex(/^[0-9a-f]{64}$/u),
7938
- contentHashAlgorithm: z5.literal("sha256-fixed-parts-v1"),
7939
- publishedVersion: z5.number().int().positive().max(Number.MAX_SAFE_INTEGER),
7940
- schemaVersion: z5.literal(1),
7941
- state: z5.literal("published"),
8381
+ var publishedVersionSchema = z6.object({
8382
+ byteCount: z6.number().int().min(1).max(maximumTeamTilesetBytes),
8383
+ contentHash: z6.string().regex(/^[0-9a-f]{64}$/u),
8384
+ contentHashAlgorithm: z6.literal("sha256-fixed-parts-v1"),
8385
+ publishedVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER),
8386
+ schemaVersion: z6.literal(1),
8387
+ state: z6.literal("published"),
7942
8388
  versionId: identifierSchema.regex(/^tsv_[A-Za-z0-9_-]+$/u)
7943
8389
  }).strict();
7944
- var uploadControlSchema = z5.object({
7945
- byteCount: z5.number().int().min(1).max(maximumTeamTilesetBytes),
7946
- contentHash: z5.string().regex(/^[0-9a-f]{64}$/u),
7947
- contentHashAlgorithm: z5.literal("sha256-fixed-parts-v1"),
7948
- expiresAt: z5.iso.datetime({ offset: true }),
7949
- failure: z5.object({ code: z5.string().regex(/^TF_[A-Z0-9_]{1,96}$/u) }).strict().nullable(),
7950
- partCount: z5.number().int().min(1).max(239),
7951
- parts: z5.array(uploadPartStatusSchema).max(239),
7952
- partSize: z5.literal(teamTilesetPartSize),
7953
- publishedVersion: z5.null(),
7954
- schemaVersion: z5.literal(1),
7955
- state: z5.enum([
8390
+ var uploadControlSchema = z6.object({
8391
+ byteCount: z6.number().int().min(1).max(maximumTeamTilesetBytes),
8392
+ contentHash: z6.string().regex(/^[0-9a-f]{64}$/u),
8393
+ contentHashAlgorithm: z6.literal("sha256-fixed-parts-v1"),
8394
+ expiresAt: z6.iso.datetime({ offset: true }),
8395
+ failure: z6.object({ code: z6.string().regex(/^TF_[A-Z0-9_]{1,96}$/u) }).strict().nullable(),
8396
+ partCount: z6.number().int().min(1).max(239),
8397
+ parts: z6.array(uploadPartStatusSchema).max(239),
8398
+ partSize: z6.literal(teamTilesetPartSize),
8399
+ publishedVersion: z6.null(),
8400
+ schemaVersion: z6.literal(1),
8401
+ state: z6.enum([
7956
8402
  "aborted",
7957
8403
  "aborting",
7958
8404
  "expired",
@@ -7965,32 +8411,32 @@ var uploadControlSchema = z5.object({
7965
8411
  "uploading",
7966
8412
  "validating"
7967
8413
  ]),
7968
- uploadId: z5.string().min(20).max(80).regex(/^tsu_[A-Za-z0-9_-]+$/u),
7969
- versionId: z5.null()
8414
+ uploadId: z6.string().min(20).max(80).regex(/^tsu_[A-Za-z0-9_-]+$/u),
8415
+ versionId: z6.null()
7970
8416
  }).strict();
7971
- var uploadSessionSchema = z5.union([publishedVersionSchema, uploadControlSchema]);
7972
- var authorizationSchema = z5.object({
7973
- expiresAt: z5.iso.datetime({ offset: true }),
7974
- headers: z5.object({
7975
- "Content-Length": z5.string().regex(/^[1-9][0-9]{0,9}$/u),
7976
- "Content-MD5": z5.string().regex(/^[A-Za-z0-9+/]{22}==$/u)
8417
+ var uploadSessionSchema = z6.union([publishedVersionSchema, uploadControlSchema]);
8418
+ var authorizationSchema = z6.object({
8419
+ expiresAt: z6.iso.datetime({ offset: true }),
8420
+ headers: z6.object({
8421
+ "Content-Length": z6.string().regex(/^[1-9][0-9]{0,9}$/u),
8422
+ "Content-MD5": z6.string().regex(/^[A-Za-z0-9+/]{22}==$/u)
7977
8423
  }).strict(),
7978
- method: z5.literal("PUT"),
7979
- partNumber: z5.number().int().min(1).max(239),
7980
- url: z5.url().max(8192).refine((value) => {
8424
+ method: z6.literal("PUT"),
8425
+ partNumber: z6.number().int().min(1).max(239),
8426
+ url: z6.url().max(8192).refine((value) => {
7981
8427
  const url = new URL(value);
7982
8428
  return url.protocol === "https:" && !url.username && !url.password && !url.hash;
7983
8429
  })
7984
8430
  }).strict();
7985
- var authorizationResponseSchema = z5.object({
7986
- authorizations: z5.array(authorizationSchema).min(1).max(16),
7987
- schemaVersion: z5.literal(1)
8431
+ var authorizationResponseSchema = z6.object({
8432
+ authorizations: z6.array(authorizationSchema).min(1).max(16),
8433
+ schemaVersion: z6.literal(1)
7988
8434
  }).strict();
7989
- var partReceiptResponseSchema = z5.object({
7990
- changed: z5.boolean(),
7991
- etag: z5.string().regex(/^[0-9a-f]{32}$/u),
7992
- partNumber: z5.number().int().min(1).max(239),
7993
- schemaVersion: z5.literal(1)
8435
+ var partReceiptResponseSchema = z6.object({
8436
+ changed: z6.boolean(),
8437
+ etag: z6.string().regex(/^[0-9a-f]{32}$/u),
8438
+ partNumber: z6.number().int().min(1).max(239),
8439
+ schemaVersion: z6.literal(1)
7994
8440
  }).strict();
7995
8441
 
7996
8442
  // src/tileset-command.ts
@@ -8013,7 +8459,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
8013
8459
  `);
8014
8460
  return;
8015
8461
  }
8016
- console.log(`${pc5.green("\u2713")} PMTiles archive is valid.`);
8462
+ console.log(`${pc6.green("\u2713")} PMTiles archive is valid.`);
8017
8463
  console.log(` Type: ${inspection.contract.tileType}`);
8018
8464
  console.log(` Zooms: ${inspection.contract.minzoom}-${inspection.contract.maxzoom}`);
8019
8465
  console.log(` Source layers: ${inspection.contract.sourceLayers.length}`);
@@ -8023,7 +8469,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
8023
8469
  );
8024
8470
  }
8025
8471
  for (const warning of inspection.warnings) {
8026
- console.log(`${pc5.yellow("!")} ${warning.message}`);
8472
+ console.log(`${pc6.yellow("!")} ${warning.message}`);
8027
8473
  }
8028
8474
  } catch (error) {
8029
8475
  const record = asRecord4(error);
@@ -8036,7 +8482,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
8036
8482
  `
8037
8483
  );
8038
8484
  } else {
8039
- console.error(`${pc5.red("Error:")} ${message}`);
8485
+ console.error(`${pc6.red("Error:")} ${message}`);
8040
8486
  }
8041
8487
  process.exitCode = 1;
8042
8488
  }
@@ -8124,7 +8570,7 @@ function registerTilesetCommands(program2, dependencies) {
8124
8570
  };
8125
8571
  if (options.json) return emitJson(document);
8126
8572
  console.log(
8127
- `${pc5.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc5.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
8573
+ `${pc6.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc6.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
8128
8574
  );
8129
8575
  } catch (error) {
8130
8576
  if (error instanceof TeamTilesetUploadError) {
@@ -8144,9 +8590,9 @@ function registerTilesetCommands(program2, dependencies) {
8144
8590
  const items = await fetchAllTeamTilesets(authority);
8145
8591
  const document = { command: "tileset list", schemaVersion: 1, tilesets: items };
8146
8592
  if (options.json) return emitJson(document);
8147
- if (items.length === 0) return console.log(pc5.gray("No Team tilesets."));
8593
+ if (items.length === 0) return console.log(pc6.gray("No Team tilesets."));
8148
8594
  for (const item of items) {
8149
- console.log(` ${pc5.bold(item.logicalName)} ${pc5.gray(item.status)}`);
8595
+ console.log(` ${pc6.bold(item.logicalName)} ${pc6.gray(item.status)}`);
8150
8596
  }
8151
8597
  } catch {
8152
8598
  emitFailure(options.json, "tileset_list_failed", "Tileset list failed.");
@@ -8174,7 +8620,7 @@ function registerTilesetCommands(program2, dependencies) {
8174
8620
  const document = { command: "tileset status", schemaVersion: 1, tileset: parsed.tileset };
8175
8621
  if (options.json) return emitJson(document);
8176
8622
  const item = parsed.tileset;
8177
- console.log(`${pc5.bold(item.name)} ${pc5.gray(item.state)}`);
8623
+ console.log(`${pc6.bold(item.name)} ${pc6.gray(item.state)}`);
8178
8624
  console.log(` ID: ${item.id}`);
8179
8625
  console.log(` Current version: ${String(item.currentVersion ?? "unpublished")}`);
8180
8626
  } catch {
@@ -8225,7 +8671,7 @@ function registerTilesetCommands(program2, dependencies) {
8225
8671
  const parsed = tilesetPurgeResponseSchema.parse(response.body);
8226
8672
  const document = { command: "tileset purge", schemaVersion: 1, ...parsed };
8227
8673
  if (options.json) return emitJson(document);
8228
- console.log(`${pc5.green("\u2713")} Purge ${parsed.reclamation}.`);
8674
+ console.log(`${pc6.green("\u2713")} Purge ${parsed.reclamation}.`);
8229
8675
  } catch {
8230
8676
  emitFailure(options.json, "tileset_purge_failed", "Tileset purge failed.");
8231
8677
  }
@@ -8287,69 +8733,69 @@ function inspectionOptionError(message) {
8287
8733
  function asRecord4(value) {
8288
8734
  return value && typeof value === "object" && !Array.isArray(value) ? value : {};
8289
8735
  }
8290
- var safeIdentifierSchema2 = z6.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
8291
- var physicalTilesetIdSchema = z6.string().min(20).max(80).regex(/^tls_[A-Za-z0-9_-]+$/u);
8292
- var cursorSchema2 = z6.string().min(1).max(2048).nullable();
8293
- var tilesetInventoryItemSchema = z6.object({
8294
- currentVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
8295
- hasCurrent: z6.boolean(),
8736
+ var safeIdentifierSchema2 = z7.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
8737
+ var physicalTilesetIdSchema = z7.string().min(20).max(80).regex(/^tls_[A-Za-z0-9_-]+$/u);
8738
+ var cursorSchema2 = z7.string().min(1).max(2048).nullable();
8739
+ var tilesetInventoryItemSchema = z7.object({
8740
+ currentVersion: z7.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
8741
+ hasCurrent: z7.boolean(),
8296
8742
  id: physicalTilesetIdSchema,
8297
- logicalName: z6.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
8298
- name: z6.string().min(1).max(200),
8299
- retainedBytes: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
8300
- retainedDeploymentCount: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
8301
- status: z6.enum(["active", "purging", "retired"]),
8302
- updatedAt: z6.iso.datetime({ offset: true }),
8303
- usedByMapCount: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER)
8743
+ logicalName: z7.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
8744
+ name: z7.string().min(1).max(200),
8745
+ retainedBytes: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
8746
+ retainedDeploymentCount: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
8747
+ status: z7.enum(["active", "purging", "retired"]),
8748
+ updatedAt: z7.iso.datetime({ offset: true }),
8749
+ usedByMapCount: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER)
8304
8750
  }).strict();
8305
- var tilesetListResponseSchema = z6.object({
8751
+ var tilesetListResponseSchema = z7.object({
8306
8752
  nextCursor: cursorSchema2.optional(),
8307
- schemaVersion: z6.literal(1),
8308
- tilesets: z6.array(tilesetInventoryItemSchema).max(100)
8753
+ schemaVersion: z7.literal(1),
8754
+ tilesets: z7.array(tilesetInventoryItemSchema).max(100)
8309
8755
  }).strict();
8310
- var tilesetDependencySchema = z6.object({
8311
- active: z6.boolean(),
8756
+ var tilesetDependencySchema = z7.object({
8757
+ active: z7.boolean(),
8312
8758
  deploymentId: safeIdentifierSchema2,
8313
- deploymentVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER),
8314
- environment: z6.string().min(1).max(64),
8759
+ deploymentVersion: z7.number().int().positive().max(Number.MAX_SAFE_INTEGER),
8760
+ environment: z7.string().min(1).max(64),
8315
8761
  mapId: safeIdentifierSchema2,
8316
8762
  sourceId: safeIdentifierSchema2
8317
8763
  }).strict();
8318
- var tilesetDetailResponseSchema = z6.object({
8319
- schemaVersion: z6.literal(1),
8320
- tileset: z6.object({
8321
- attribution: z6.string().max(4096).nullable(),
8322
- currentVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
8323
- dependencies: z6.array(tilesetDependencySchema).max(25),
8764
+ var tilesetDetailResponseSchema = z7.object({
8765
+ schemaVersion: z7.literal(1),
8766
+ tileset: z7.object({
8767
+ attribution: z7.string().max(4096).nullable(),
8768
+ currentVersion: z7.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
8769
+ dependencies: z7.array(tilesetDependencySchema).max(25),
8324
8770
  dependencyNextCursor: cursorSchema2,
8325
8771
  id: physicalTilesetIdSchema,
8326
- name: z6.string().min(1).max(200),
8327
- slug: z6.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
8328
- state: z6.enum(["active", "purging", "purged"])
8772
+ name: z7.string().min(1).max(200),
8773
+ slug: z7.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
8774
+ state: z7.enum(["active", "purging", "purged"])
8329
8775
  }).strict()
8330
8776
  }).strict();
8331
- var purgeDependencySchema = z6.object({
8777
+ var purgeDependencySchema = z7.object({
8332
8778
  deploymentId: safeIdentifierSchema2,
8333
8779
  mapId: safeIdentifierSchema2,
8334
8780
  sourceId: safeIdentifierSchema2
8335
8781
  }).strict();
8336
- var tilesetInUseResponseSchema = z6.object({
8337
- code: z6.literal("TF_TILESET_IN_USE"),
8338
- dependencies: z6.array(purgeDependencySchema).max(32),
8339
- error: z6.literal("Tileset is used by retained Map deployments"),
8782
+ var tilesetInUseResponseSchema = z7.object({
8783
+ code: z7.literal("TF_TILESET_IN_USE"),
8784
+ dependencies: z7.array(purgeDependencySchema).max(32),
8785
+ error: z7.literal("Tileset is used by retained Map deployments"),
8340
8786
  nextCursor: cursorSchema2
8341
8787
  }).strict();
8342
- var tilesetPurgeResponseSchema = z6.object({
8343
- deliveryFence: z6.enum(["complete", "pending"]),
8344
- pendingVersions: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
8345
- reclamation: z6.enum(["complete", "pending"]),
8346
- state: z6.enum(["purged", "purging"]),
8788
+ var tilesetPurgeResponseSchema = z7.object({
8789
+ deliveryFence: z7.enum(["complete", "pending"]),
8790
+ pendingVersions: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
8791
+ reclamation: z7.enum(["complete", "pending"]),
8792
+ state: z7.enum(["purged", "purging"]),
8347
8793
  tilesetId: physicalTilesetIdSchema
8348
8794
  }).strict();
8349
8795
 
8350
8796
  // src/visual-command.ts
8351
- import { lstat as lstat5, readFile as readFile6 } from "fs/promises";
8352
- import { dirname as dirname11, isAbsolute as isAbsolute4, parse as parse2, relative as relative4, resolve as resolve9, sep as sep4 } from "path";
8797
+ import { lstat as lstat6, readFile as readFile6 } from "fs/promises";
8798
+ import { dirname as dirname12, isAbsolute as isAbsolute4, parse as parse2, relative as relative5, resolve as resolve10, sep as sep4 } from "path";
8353
8799
  import {
8354
8800
  analyzeTileflowCaptureReference,
8355
8801
  compareTileflowCaptureToBaseline,
@@ -8434,14 +8880,14 @@ async function runVisualAnalyze(scene, options) {
8434
8880
  validateVisualSelection([scene]);
8435
8881
  const region = options.region === void 0 ? void 0 : parseTileflowVisualRegion(options.region);
8436
8882
  const cwd = process.cwd();
8437
- const referencePath = resolve9(cwd, options.reference);
8883
+ const referencePath = resolve10(cwd, options.reference);
8438
8884
  const referencePng = await readVisualReference(referencePath);
8439
8885
  const referenceDimensions = validateTileflowVisualReferencePng(referencePng);
8440
8886
  if (region) assertTileflowVisualRegionFits(region, referenceDimensions, "reference PNG");
8441
8887
  const outputDirectory = await resolveVisualDirectory(cwd, options.outputDir, "output");
8442
- const actualPath = resolve9(outputDirectory.path, `${scene}.actual.png`);
8443
- const managedDiffPath = resolve9(outputDirectory.path, `${scene}.diff.png`);
8444
- const reportPath = resolve9(outputDirectory.path, `${scene}.analysis.json`);
8888
+ const actualPath = resolve10(outputDirectory.path, `${scene}.actual.png`);
8889
+ const managedDiffPath = resolve10(outputDirectory.path, `${scene}.diff.png`);
8890
+ const reportPath = resolve10(outputDirectory.path, `${scene}.analysis.json`);
8445
8891
  assertReferenceIsReadOnly(referencePath, [actualPath, managedDiffPath, reportPath]);
8446
8892
  const controller = installSignalAbortController();
8447
8893
  const session = createTileflowCaptureSession3({
@@ -8530,15 +8976,15 @@ async function runVisualDiff(scenes, options) {
8530
8976
  capture,
8531
8977
  files.kind === "complete" ? files.baseline : void 0
8532
8978
  );
8533
- const actualPath = resolve9(outputDirectory.path, `${capture.scene}.actual.png`);
8534
- const diffPath = comparison.diffPng ? resolve9(outputDirectory.path, `${capture.scene}.diff.png`) : void 0;
8979
+ const actualPath = resolve10(outputDirectory.path, `${capture.scene}.actual.png`);
8980
+ const diffPath = comparison.diffPng ? resolve10(outputDirectory.path, `${capture.scene}.diff.png`) : void 0;
8535
8981
  prepared.push({
8536
8982
  capture,
8537
8983
  comparison,
8538
8984
  baselinePath: files.pngPath,
8539
8985
  actualPath,
8540
8986
  diffPath,
8541
- reportPath: resolve9(outputDirectory.path, `${capture.scene}.visual.json`)
8987
+ reportPath: resolve10(outputDirectory.path, `${capture.scene}.visual.json`)
8542
8988
  });
8543
8989
  }
8544
8990
  const document = createTileflowVisualDiffJson(prepared, cwd);
@@ -8558,7 +9004,7 @@ async function runVisualDiff(scenes, options) {
8558
9004
  }
8559
9005
  ]),
8560
9006
  removePaths: prepared.flatMap(
8561
- (item) => item.diffPath ? [] : [resolve9(outputDirectory.path, `${item.capture.scene}.diff.png`)]
9007
+ (item) => item.diffPath ? [] : [resolve10(outputDirectory.path, `${item.capture.scene}.diff.png`)]
8562
9008
  )
8563
9009
  });
8564
9010
  if (options.json) {
@@ -8679,7 +9125,7 @@ async function resolvePreviousBaseline(capture, files) {
8679
9125
  }
8680
9126
  }
8681
9127
  async function readBaselineFiles(directory, scene) {
8682
- const pngPath = resolve9(directory.path, `${scene}.png`);
9128
+ const pngPath = resolve10(directory.path, `${scene}.png`);
8683
9129
  const receiptPath = captureReceiptPath(pngPath);
8684
9130
  const [png, receipt] = await Promise.all([
8685
9131
  readOptionalArtifact(pngPath, directory.boundary, tileflowVisualArtifactLimits2.maximumPngBytes),
@@ -8694,10 +9140,10 @@ async function readBaselineFiles(directory, scene) {
8694
9140
  return { kind: "complete", pngPath, receiptPath, baseline: { png, receipt } };
8695
9141
  }
8696
9142
  async function readOptionalArtifact(path, boundary, maximumBytes) {
8697
- await assertNoSymlinkComponents(dirname11(path), boundary);
9143
+ await assertNoSymlinkComponents(dirname12(path), boundary);
8698
9144
  let info;
8699
9145
  try {
8700
- info = await lstat5(path);
9146
+ info = await lstat6(path);
8701
9147
  } catch (error) {
8702
9148
  if (isMissingFile5(error)) return void 0;
8703
9149
  throw error;
@@ -8717,7 +9163,7 @@ async function readOptionalArtifact(path, boundary, maximumBytes) {
8717
9163
  async function readVisualReference(path) {
8718
9164
  let info;
8719
9165
  try {
8720
- info = await lstat5(path);
9166
+ info = await lstat6(path);
8721
9167
  } catch (error) {
8722
9168
  throw new TileflowCaptureError2("BASELINE_INVALID", "The visual reference PNG cannot be read.", {
8723
9169
  cause: error
@@ -8739,12 +9185,12 @@ async function readVisualReference(path) {
8739
9185
  return new Uint8Array(await readFile6(path));
8740
9186
  }
8741
9187
  async function resolveVisualDirectory(cwd, value, kind) {
8742
- const path = resolve9(cwd, value);
9188
+ const path = resolve10(cwd, value);
8743
9189
  if (path === cwd || path === parse2(path).root) {
8744
9190
  throw new Error(`Visual ${kind} directory must be a dedicated subdirectory.`);
8745
9191
  }
8746
9192
  if (kind === "baseline") {
8747
- for (const managed of [resolve9(cwd, ".tileflow/captures"), resolve9(cwd, ".tileflow/diffs")]) {
9193
+ for (const managed of [resolve10(cwd, ".tileflow/captures"), resolve10(cwd, ".tileflow/diffs")]) {
8748
9194
  if (pathsOverlap2(path, managed)) {
8749
9195
  throw new Error("Visual baselines must not overlap Tileflow-managed capture/diff outputs.");
8750
9196
  }
@@ -8753,7 +9199,7 @@ async function resolveVisualDirectory(cwd, value, kind) {
8753
9199
  const boundary = isInside2(cwd, path) ? cwd : path;
8754
9200
  await assertNoSymlinkComponents(path, boundary);
8755
9201
  try {
8756
- const info = await lstat5(path);
9202
+ const info = await lstat6(path);
8757
9203
  if (info.isSymbolicLink() || !info.isDirectory()) {
8758
9204
  throw new Error(`Visual ${kind} path must be a non-symlink directory.`);
8759
9205
  }
@@ -8780,10 +9226,10 @@ function assertReferenceIsReadOnly(referencePath, outputPaths) {
8780
9226
  }
8781
9227
  }
8782
9228
  function portablePathIdentity2(path) {
8783
- return resolve9(path).normalize("NFC").toLowerCase();
9229
+ return resolve10(path).normalize("NFC").toLowerCase();
8784
9230
  }
8785
9231
  function isInside2(parent, child) {
8786
- const path = relative4(parent, child);
9232
+ const path = relative5(parent, child);
8787
9233
  return path === "" || path !== ".." && !path.startsWith(`..${sep4}`) && !isAbsolute4(path);
8788
9234
  }
8789
9235
  function isMissingFile5(error) {
@@ -8792,8 +9238,8 @@ function isMissingFile5(error) {
8792
9238
 
8793
9239
  // src/workbench-service.ts
8794
9240
  import { opendir, stat as stat2 } from "fs/promises";
8795
- import { resolve as resolve10 } from "path";
8796
- import { z as z7 } from "zod";
9241
+ import { resolve as resolve11 } from "path";
9242
+ import { z as z8 } from "zod";
8797
9243
  import {
8798
9244
  compareTileflowCapturesForReview as compareTileflowCapturesForReview3,
8799
9245
  createTileflowCaptureSession as createTileflowCaptureSession4,
@@ -8806,22 +9252,22 @@ import {
8806
9252
  } from "@tileflow/core";
8807
9253
  import { resolveTileflowPreview } from "@tileflow/dev/preview";
8808
9254
  var prefix = "/__workbench";
8809
- var captureInput = z7.object({
8810
- side: z7.enum(["left", "right"]).default("left"),
8811
- generation: z7.number().int().nonnegative(),
9255
+ var captureInput = z8.object({
9256
+ side: z8.enum(["left", "right"]).default("left"),
9257
+ generation: z8.number().int().nonnegative(),
8812
9258
  name: tileflowCaptureSceneNameSchema2,
8813
9259
  definition: tileflowCaptureSceneSchema3
8814
9260
  }).strict();
8815
- var compareInput = z7.object({ before: z7.string().regex(/^[a-f0-9]{64}$/u), after: z7.string().regex(/^[a-f0-9]{64}$/u) }).strict();
8816
- var deleteInput = z7.object({ id: z7.string().regex(/^[a-f0-9]{64}$/u) }).strict();
8817
- var contextInput = z7.object({
8818
- client: z7.string().regex(/^[a-zA-Z0-9-]{1,80}$/u),
8819
- side: z7.enum(["left", "right"]),
8820
- generation: z7.number().int(),
8821
- context: z7.record(z7.string(), z7.unknown())
9261
+ var compareInput = z8.object({ before: z8.string().regex(/^[a-f0-9]{64}$/u), after: z8.string().regex(/^[a-f0-9]{64}$/u) }).strict();
9262
+ var deleteInput = z8.object({ id: z8.string().regex(/^[a-f0-9]{64}$/u) }).strict();
9263
+ var contextInput = z8.object({
9264
+ client: z8.string().regex(/^[a-zA-Z0-9-]{1,80}$/u),
9265
+ side: z8.enum(["left", "right"]),
9266
+ generation: z8.number().int(),
9267
+ context: z8.record(z8.string(), z8.unknown())
8822
9268
  }).strict();
8823
9269
  function createWorkbenchService(options) {
8824
- const directory = resolve10(options.cwd, captureRecordDirectory);
9270
+ const directory = resolve11(options.cwd, captureRecordDirectory);
8825
9271
  const contexts = /* @__PURE__ */ new Map();
8826
9272
  let captureSession = options.captureSession;
8827
9273
  let pending;
@@ -8893,7 +9339,7 @@ function createWorkbenchService(options) {
8893
9339
  break;
8894
9340
  }
8895
9341
  if (!entry.isFile() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) continue;
8896
- const info = await stat2(resolve10(directory, entry.name));
9342
+ const info = await stat2(resolve11(directory, entry.name));
8897
9343
  entries.push({ id: entry.name.slice(0, -5), modified: info.mtimeMs });
8898
9344
  }
8899
9345
  } catch (error) {
@@ -9032,7 +9478,7 @@ function createWorkbenchService(options) {
9032
9478
  });
9033
9479
  }
9034
9480
  if (path === "/captures") {
9035
- const offset = z7.coerce.number().int().min(0).max(5e3).parse(url.searchParams.get("offset") ?? 0);
9481
+ const offset = z8.coerce.number().int().min(0).max(5e3).parse(url.searchParams.get("offset") ?? 0);
9036
9482
  return json(await history(offset));
9037
9483
  }
9038
9484
  const image = /^\/captures\/([a-f0-9]{64})\.png$/u.exec(path);
@@ -9163,17 +9609,18 @@ var packageJson = JSON.parse(
9163
9609
  var program = new Command();
9164
9610
  var defaultApiUrl = defaultTileflowApiUrl;
9165
9611
  var defaultAppUrl = "https://tileflow.dev";
9166
- var defaultConfigPath = defaultTileflowConfigPath;
9612
+ var defaultConfigPath = defaultTileflowConfigPath2;
9167
9613
  var defaultManifestPath = defaultTileflowManifestPath;
9168
- program.name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(pc6.gray("\nRun tileflow <command> --help for usage.")).showSuggestionAfterError();
9614
+ program.name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(pc7.gray("\nRun tileflow <command> --help for usage.")).showSuggestionAfterError();
9169
9615
  program.addHelpText("after", "\n -V, --version output the version number\n");
9170
9616
  registerDescribeCommand(program);
9171
9617
  registerLanguageCommand(program);
9172
9618
  registerTilesetCommands(program, { defaultApiUrl, loadAuthConfig });
9173
9619
  registerIconSetCommands(program, { defaultApiUrl, loadAuthConfig });
9620
+ registerModelCommands(program, { defaultApiUrl, loadAuthConfig });
9174
9621
  program.command("init").description("Create a starter tileflow.config.ts").option("-f, --force", "overwrite an existing config").action(async (options) => {
9175
- const configPath = resolve11(process.cwd(), defaultConfigPath);
9176
- const ignorePath = resolve11(process.cwd(), ".gitignore");
9622
+ const configPath = resolve12(process.cwd(), defaultConfigPath);
9623
+ const ignorePath = resolve12(process.cwd(), ".gitignore");
9177
9624
  if (existsSync(configPath) && !options.force) {
9178
9625
  logWarning(`${pathLabel(defaultConfigPath)} already exists.`);
9179
9626
  printNextSteps([`Run ${command("tileflow init --force")} to replace it.`]);
@@ -9189,9 +9636,9 @@ program.command("init").description("Create a starter tileflow.config.ts").optio
9189
9636
  }
9190
9637
  logSuccess(`Created ${pathLabel(defaultConfigPath)}.`);
9191
9638
  printKeyValue("Path", pathLabel(configPath));
9192
- printKeyValue("Map", pc6.bold("madrid"));
9639
+ printKeyValue("Map", pc7.bold("madrid"));
9193
9640
  printNextSteps([
9194
- `Install ${pc6.cyan("@tileflow/core")} and ${pc6.cyan("@tileflow/maps")} in this project if needed.`,
9641
+ `Install ${pc7.cyan("@tileflow/core")} and ${pc7.cyan("@tileflow/maps")} in this project if needed.`,
9195
9642
  `Preview locally with ${command("tileflow preview")}.`,
9196
9643
  `Validate the config with ${command("tileflow validate")}.`
9197
9644
  ]);
@@ -9327,12 +9774,12 @@ program.command("validate").description("Validate a Tileflow config").option("-c
9327
9774
  try {
9328
9775
  if (!options.json) logInfo(`Validating ${pathLabel(options.config)}.`);
9329
9776
  const loaded = await withTileflowConfigSecretsHidden(
9330
- () => loadTileflowConfigWithInputs4(options.config)
9777
+ () => loadTileflowConfigWithInputs5(options.config)
9331
9778
  );
9332
9779
  const project = loaded.project;
9333
- const baseDirectory = dirname12(loaded.configFile);
9780
+ const baseDirectory = dirname13(loaded.configFile);
9334
9781
  failureDefaults = { code: "CONFIG_INVALID", phase: "config-validation" };
9335
- assertValidTileflowConfig3(project);
9782
+ assertValidTileflowConfig4(project);
9336
9783
  const mapNames = getTileflowMapNames5(project).sort();
9337
9784
  failureDefaults = { code: "ICON_COMPILATION_FAILED", phase: "icon-compilation" };
9338
9785
  const compiledIcons = await compileTileflowIconPackages3(project, {
@@ -9348,7 +9795,10 @@ program.command("validate").description("Validate a Tileflow config").option("-c
9348
9795
  const mapAssets = createCompiledMapAssets(
9349
9796
  compiledIcons,
9350
9797
  (binding) => options.target === "hosted" ? `${normalizeUrl(options.apiBaseUrl)}/sprites/preflight/${binding.packageHash}/sprite` : `/tileflow/icons/${binding.mapName}/sprite`,
9351
- modelAssets
9798
+ mapAssetsWithBuildingModels(
9799
+ modelAssets,
9800
+ options.target === "hosted" ? { hosted: true } : { localBase: (mapName) => `/tileflow/models/${mapName}` }
9801
+ )
9352
9802
  );
9353
9803
  failureDefaults = { code: "STYLE_INVALID", phase: "style-validation" };
9354
9804
  const compiledStyles = createTileflowStyles2(project, {
@@ -9467,7 +9917,7 @@ program.command("build").description("Generate static Tileflow styles").option("
9467
9917
  })
9468
9918
  );
9469
9919
  logSuccess("Built Tileflow artifacts.");
9470
- printKeyValue("Output", pathLabel(resolve11(process.cwd(), options.out)));
9920
+ printKeyValue("Output", pathLabel(resolve12(process.cwd(), options.out)));
9471
9921
  }
9472
9922
  );
9473
9923
  program.command("preview").description("Open Map DevTools with inspection and optional comparison").option("-c, --config <path>", "config path", defaultConfigPath).option("--against-config <path>", "second config for live comparison in Map DevTools").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 Map DevTools without opening the browser").option("--scene <name>", "preview one committed standalone map scene").option(
@@ -9780,7 +10230,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9780
10230
  );
9781
10231
  if (result) {
9782
10232
  logSuccess(
9783
- `${result.changed ? "Published" : "Unchanged"} ${pc6.bold(result.mapName)} web + native-v1 (v${result.version}).`
10233
+ `${result.changed ? "Published" : "Unchanged"} ${pc7.bold(result.mapName)} web + native-v1 (v${result.version}).`
9784
10234
  );
9785
10235
  printKeyValue("Manifest", pathLabel(result.manifestPath));
9786
10236
  printKeyValue("Native", link(result.nativeManifestUrl));
@@ -9789,11 +10239,11 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9789
10239
  }
9790
10240
  logInfo(`Deploying ${pathLabel(options.config)}.`);
9791
10241
  const loaded = await withTileflowConfigSecretsHidden(
9792
- () => loadTileflowConfigWithInputs4(options.config)
10242
+ () => loadTileflowConfigWithInputs5(options.config)
9793
10243
  );
9794
10244
  const project = loaded.project;
9795
- const baseDirectory = dirname12(loaded.configFile);
9796
- assertValidTileflowConfig3(project);
10245
+ const baseDirectory = dirname13(loaded.configFile);
10246
+ assertValidTileflowConfig4(project);
9797
10247
  const configuredMapNames = getTileflowMapNames5(project);
9798
10248
  const mapNames = selectManagedDeployMaps(configuredMapNames, options);
9799
10249
  if (!mapNames) return;
@@ -9824,7 +10274,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9824
10274
  const preflightMapAssets = createCompiledMapAssets(
9825
10275
  compiledIcons,
9826
10276
  (binding) => `${apiUrl}/sprites/preflight/${binding.packageHash}/sprite`,
9827
- modelAssets
10277
+ mapAssetsWithBuildingModels(modelAssets, { hosted: true })
9828
10278
  );
9829
10279
  const compiledPreflightStyles = createTileflowStyles2(deploymentProject, {
9830
10280
  apiBaseUrl: apiUrl,
@@ -9894,7 +10344,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9894
10344
  );
9895
10345
  const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
9896
10346
  logSuccess(
9897
- `${action} icon package ${pc6.bold(binding?.label ?? "Icons")} (${plural(iconPackage.manifest.iconNames.length, "icon")}, ${formatBytes3(totalBytes)}, ${iconPackage.contentHash.slice(0, 12)}).`
10347
+ `${action} icon package ${pc7.bold(binding?.label ?? "Icons")} (${plural(iconPackage.manifest.iconNames.length, "icon")}, ${formatBytes4(totalBytes)}, ${iconPackage.contentHash.slice(0, 12)}).`
9898
10348
  );
9899
10349
  hostedSpriteByPackageHash.set(iconPackage.contentHash, uploaded.value.spriteUrl);
9900
10350
  }
@@ -9905,7 +10355,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9905
10355
  if (!sprite) throw new Error(`Missing hosted sprite URL for map ${binding.mapName}`);
9906
10356
  return sprite;
9907
10357
  },
9908
- modelAssets
10358
+ mapAssetsWithBuildingModels(modelAssets, { hosted: true })
9909
10359
  );
9910
10360
  const compiledHostedStyles = createTileflowStyles2(deploymentProject, {
9911
10361
  apiBaseUrl: api.apiUrl,
@@ -9946,7 +10396,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
9946
10396
  }
9947
10397
  const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
9948
10398
  logSuccess(
9949
- `${action} font bundle (${plural(bundle.manifest.fontFaces.length, "face")}, ${formatBytes3(uploaded.value.totalBytes)}, ${bundle.contentHash.slice(0, 12)}).`
10399
+ `${action} font bundle (${plural(bundle.manifest.fontFaces.length, "face")}, ${formatBytes4(uploaded.value.totalBytes)}, ${bundle.contentHash.slice(0, 12)}).`
9950
10400
  );
9951
10401
  hostedFontBaseByHash.set(bundle.contentHash, uploaded.value.baseUrl);
9952
10402
  }
@@ -10005,7 +10455,8 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10005
10455
  sourceAssets: {
10006
10456
  fonts: hostedFonts.sourceIdentities[mapName] ?? [],
10007
10457
  icons: compiledIcons.sourceIdentities[mapName] ?? [],
10008
- ...compiledIcons.compositions[mapName] ? { iconComposition: compiledIcons.compositions[mapName] } : {}
10458
+ ...compiledIcons.compositions[mapName] ? { iconComposition: compiledIcons.compositions[mapName] } : {},
10459
+ ...hostedModelSourceIdentities(modelAssets, mapName) ? { models: hostedModelSourceIdentities(modelAssets, mapName) } : {}
10009
10460
  },
10010
10461
  styles: styles[mapName]
10011
10462
  }
@@ -10018,11 +10469,13 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10018
10469
  const iconBinding = bindingsByMap.get(mapName);
10019
10470
  const iconPackage = iconBinding ? packagesByHash.get(iconBinding.packageHash) : void 0;
10020
10471
  const fontBundle = hostedFonts.bundles[mapName];
10472
+ const modelSources = teamModelSources(modelAssets, mapName);
10021
10473
  return {
10022
10474
  iconBinding,
10023
10475
  iconComposition: compiledIcons.compositions[mapName],
10024
10476
  iconPackage,
10025
10477
  fontBundle,
10478
+ modelSources,
10026
10479
  mapName,
10027
10480
  teamSources: hostedThemeFamilies[mapName].teamSources,
10028
10481
  buildManifest: {
@@ -10042,6 +10495,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10042
10495
  iconComposition,
10043
10496
  iconPackage,
10044
10497
  mapName,
10498
+ modelSources,
10045
10499
  teamSources,
10046
10500
  styles: themeStyles
10047
10501
  } = deployment;
@@ -10052,7 +10506,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10052
10506
  }
10053
10507
  const themeNames = Object.keys(themeStyles).sort();
10054
10508
  logInfo(
10055
- `Deploying compiled map ${pc6.bold(mapName)} (${plural(themeNames.length, "theme")}).`
10509
+ `Deploying compiled map ${pc7.bold(mapName)} (${plural(themeNames.length, "theme")}).`
10056
10510
  );
10057
10511
  const response = await publishHostedStyle(
10058
10512
  api,
@@ -10076,6 +10530,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10076
10530
  } : {},
10077
10531
  ...iconComposition ? { iconComposition } : {},
10078
10532
  ...fontBundle ? { fontBundle: { contentHash: fontBundle.contentHash } } : {},
10533
+ ...modelSources ? { teamModelSources: modelSources } : {},
10079
10534
  source
10080
10535
  },
10081
10536
  `Deploy response for ${mapName}`
@@ -10133,9 +10588,15 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
10133
10588
  };
10134
10589
  const versionLabel = Number.isInteger(body.version) ? ` (v${body.version})` : "";
10135
10590
  if (body.changed === false) {
10136
- logSuccess(`Unchanged ${pc6.bold(mapName)}${versionLabel}.`);
10591
+ logSuccess(`Unchanged ${pc7.bold(mapName)}${versionLabel}.`);
10137
10592
  } else {
10138
- logSuccess(`Published ${pc6.bold(mapName)}${versionLabel}.`);
10593
+ logSuccess(`Published ${pc7.bold(mapName)}${versionLabel}.`);
10594
+ }
10595
+ for (const teamModel of body.teamModels ?? []) {
10596
+ if (teamModel.sources !== "differ") continue;
10597
+ logWarning(
10598
+ `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.`
10599
+ );
10139
10600
  }
10140
10601
  }
10141
10602
  const manifestPath = await writeDeployManifest(options.manifest, {
@@ -10497,12 +10958,12 @@ async function runTileflowComparisonPreview(options, address) {
10497
10958
  }
10498
10959
  function comparisonCaptureConfigArgument(configPath) {
10499
10960
  const cwd = realpathSync(process.cwd());
10500
- const requestedConfigPath = resolve11(process.cwd(), configPath);
10501
- const defaultRequestedPath = resolve11(process.cwd(), defaultConfigPath);
10961
+ const requestedConfigPath = resolve12(process.cwd(), configPath);
10962
+ const defaultRequestedPath = resolve12(process.cwd(), defaultConfigPath);
10502
10963
  const absoluteConfigPath = existsSync(requestedConfigPath) ? realpathSync(requestedConfigPath) : requestedConfigPath;
10503
10964
  const absoluteDefaultPath = existsSync(defaultRequestedPath) ? realpathSync(defaultRequestedPath) : defaultRequestedPath;
10504
10965
  if (absoluteConfigPath === absoluteDefaultPath) return void 0;
10505
- const relativeConfigPath = relative5(cwd, absoluteConfigPath);
10966
+ const relativeConfigPath = relative6(cwd, absoluteConfigPath);
10506
10967
  if (relativeConfigPath === "" || relativeConfigPath === ".." || relativeConfigPath.startsWith(`..${sep5}`) || isAbsolute5(relativeConfigPath)) {
10507
10968
  throw new Error(
10508
10969
  "Comparison configs must be inside the current working directory so Copy command can remain reproducible."
@@ -10542,7 +11003,7 @@ async function loginWithDeviceFlow(options) {
10542
11003
  codeChallenge,
10543
11004
  deviceName: hostname()
10544
11005
  });
10545
- printKeyValue("Code", pc6.bold(authorization.userCode));
11006
+ printKeyValue("Code", pc7.bold(authorization.userCode));
10546
11007
  printKeyValue("URL", link(authorization.verificationUriComplete));
10547
11008
  if (options.noBrowser) {
10548
11009
  logMuted("Open the URL above to continue.");
@@ -10552,7 +11013,7 @@ async function loginWithDeviceFlow(options) {
10552
11013
  output: process.stdout
10553
11014
  });
10554
11015
  try {
10555
- await readline.question(`${pc6.cyan("?")} Press Enter to open Tileflow Dashboard...`);
11016
+ await readline.question(`${pc7.cyan("?")} Press Enter to open Tileflow Dashboard...`);
10556
11017
  openBrowser(authorization.verificationUriComplete);
10557
11018
  } finally {
10558
11019
  readline.close();
@@ -10603,9 +11064,9 @@ function openBrowser(url, errorsToStderr = false) {
10603
11064
  });
10604
11065
  }
10605
11066
  async function writeDeployManifest(manifestPath, manifest) {
10606
- const outputPath = resolve11(process.cwd(), manifestPath);
11067
+ const outputPath = resolve12(process.cwd(), manifestPath);
10607
11068
  await writeAtomicFile({
10608
- boundaryPath: dirname12(outputPath),
11069
+ boundaryPath: dirname13(outputPath),
10609
11070
  force: true,
10610
11071
  label: "Deploy manifest",
10611
11072
  managed: true,
@@ -10693,7 +11154,7 @@ function validateDeployManifestMapNames(mapNames) {
10693
11154
  async function loadExistingDeployManifest(manifestPath, options) {
10694
11155
  let source;
10695
11156
  try {
10696
- source = await readFile7(resolve11(process.cwd(), manifestPath), "utf8");
11157
+ source = await readFile7(resolve12(process.cwd(), manifestPath), "utf8");
10697
11158
  } catch (error) {
10698
11159
  if (error.code === "ENOENT") return null;
10699
11160
  throw new Error("Existing Tileflow manifest could not be read.");
@@ -10753,7 +11214,7 @@ async function requireApiOptions(options, behavior = {}) {
10753
11214
  if (!behavior.silent) {
10754
11215
  logError("CI requires an explicit Tileflow credential with access to the target Map.");
10755
11216
  printNextSteps([
10756
- `Set ${pc6.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
11217
+ `Set ${pc7.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
10757
11218
  "The saved personal account session is never used in CI."
10758
11219
  ]);
10759
11220
  }
@@ -10822,17 +11283,17 @@ function printCliError(error) {
10822
11283
  }
10823
11284
  const message = error instanceof Error ? error.message : "Command failed.";
10824
11285
  if (message.includes("Cannot find module '@tileflow/core'")) {
10825
- logError(`Cannot load ${pc6.cyan("@tileflow/core")} from this project.`);
11286
+ logError(`Cannot load ${pc7.cyan("@tileflow/core")} from this project.`);
10826
11287
  printNextSteps([
10827
- `Install ${pc6.cyan("@tileflow/core")} in this project.`,
11288
+ `Install ${pc7.cyan("@tileflow/core")} in this project.`,
10828
11289
  `Run the command again from the project root.`
10829
11290
  ]);
10830
11291
  return;
10831
11292
  }
10832
11293
  if (message.includes("Cannot find module '@tileflow/maps'")) {
10833
- logError(`Cannot load ${pc6.cyan("@tileflow/maps")} from this project.`);
11294
+ logError(`Cannot load ${pc7.cyan("@tileflow/maps")} from this project.`);
10834
11295
  printNextSteps([
10835
- `Install ${pc6.cyan("@tileflow/maps")} in this project.`,
11296
+ `Install ${pc7.cyan("@tileflow/maps")} in this project.`,
10836
11297
  `Run the command again from the project root.`
10837
11298
  ]);
10838
11299
  return;
@@ -10843,51 +11304,51 @@ function printCliError(error) {
10843
11304
  }
10844
11305
  }
10845
11306
  function logSuccess(message) {
10846
- console.log(`${pc6.green("\u2713")} ${message}`);
11307
+ console.log(`${pc7.green("\u2713")} ${message}`);
10847
11308
  }
10848
11309
  function logError(message) {
10849
- console.log(`${pc6.red("\u2715")} ${message}`);
11310
+ console.log(`${pc7.red("\u2715")} ${message}`);
10850
11311
  }
10851
11312
  function logWarning(message) {
10852
- console.log(`${pc6.yellow("!")} ${message}`);
11313
+ console.log(`${pc7.yellow("!")} ${message}`);
10853
11314
  }
10854
11315
  function logInfo(message) {
10855
- console.log(`${pc6.cyan("i")} ${message}`);
11316
+ console.log(`${pc7.cyan("i")} ${message}`);
10856
11317
  }
10857
11318
  function logMuted(message) {
10858
- console.log(pc6.gray(message));
11319
+ console.log(pc7.gray(message));
10859
11320
  }
10860
11321
  function printTitle(title, subtitle) {
10861
- console.log(pc6.bold(title));
11322
+ console.log(pc7.bold(title));
10862
11323
  if (subtitle) {
10863
- console.log(pc6.gray(subtitle));
11324
+ console.log(pc7.gray(subtitle));
10864
11325
  }
10865
11326
  }
10866
11327
  function printKeyValue(label, value) {
10867
- console.log(`${pc6.gray(`${label}:`.padEnd(13))} ${value}`);
11328
+ console.log(`${pc7.gray(`${label}:`.padEnd(13))} ${value}`);
10868
11329
  }
10869
11330
  function printChecks(items) {
10870
11331
  for (const item of items) {
10871
- console.log(` ${pc6.green("\u2713")} ${item}`);
11332
+ console.log(` ${pc7.green("\u2713")} ${item}`);
10872
11333
  }
10873
11334
  }
10874
11335
  function printNextSteps(steps) {
10875
11336
  if (steps.length === 0) return;
10876
11337
  console.log(`
10877
- ${pc6.bold("Next steps")}`);
11338
+ ${pc7.bold("Next steps")}`);
10878
11339
  for (const step of steps) {
10879
- console.log(` ${pc6.cyan("\u2022")} ${step}`);
11340
+ console.log(` ${pc7.cyan("\u2022")} ${step}`);
10880
11341
  }
10881
11342
  }
10882
11343
  function printValidationErrors(messages) {
10883
11344
  logError("Tileflow config has errors.");
10884
11345
  for (const message of messages) {
10885
- const path = message.path ? pc6.cyan(message.path) : pc6.cyan("(root)");
10886
- console.log(` ${pc6.red("\u2715")} ${path} ${pc6.gray("-")} ${message.message}`);
11346
+ const path = message.path ? pc7.cyan(message.path) : pc7.cyan("(root)");
11347
+ console.log(` ${pc7.red("\u2715")} ${path} ${pc7.gray("-")} ${message.message}`);
10887
11348
  }
10888
11349
  }
10889
11350
  function printAccountDetails(session) {
10890
- printKeyValue("Account", pc6.bold(session.account.name));
11351
+ printKeyValue("Account", pc7.bold(session.account.name));
10891
11352
  printKeyValue("Email", session.account.email);
10892
11353
  printKeyValue("API", link(session.apiOrigin));
10893
11354
  printKeyValue("Expires", formatDate(session.expiresAt));
@@ -10916,11 +11377,11 @@ function printDeployedMaps(maps) {
10916
11377
  const entries = Object.entries(maps);
10917
11378
  if (entries.length === 0) return;
10918
11379
  console.log(`
10919
- ${pc6.bold("Maps")}`);
11380
+ ${pc7.bold("Maps")}`);
10920
11381
  for (const [name, map] of entries) {
10921
11382
  for (const [themeName, theme] of Object.entries(map.themes)) {
10922
11383
  const label = `${name}/${themeName}`;
10923
- console.log(` ${pc6.green("\u2713")} ${label.padEnd(24)} ${link(theme.styleUrl)}`);
11384
+ console.log(` ${pc7.green("\u2713")} ${label.padEnd(24)} ${link(theme.styleUrl)}`);
10924
11385
  }
10925
11386
  }
10926
11387
  }
@@ -10938,13 +11399,13 @@ function plural(count, singular, pluralForm = `${singular}s`) {
10938
11399
  return `${count} ${count === 1 ? singular : pluralForm}`;
10939
11400
  }
10940
11401
  function command(value) {
10941
- return pc6.cyan(value);
11402
+ return pc7.cyan(value);
10942
11403
  }
10943
11404
  function link(value) {
10944
- return pc6.cyan(value);
11405
+ return pc7.cyan(value);
10945
11406
  }
10946
11407
  function pathLabel(value) {
10947
- return pc6.cyan(value);
11408
+ return pc7.cyan(value);
10948
11409
  }
10949
11410
  function starterConfig() {
10950
11411
  return `import { defineMap, defineTheme, labels, poi } from "@tileflow/core";
@@ -10990,19 +11451,19 @@ function printHostedCompatibilityIssues(issues) {
10990
11451
  }
10991
11452
  function printMapStatus(status, apiUrl) {
10992
11453
  printTitle("Tileflow status");
10993
- printKeyValue("Map", pc6.bold(status.mapId));
11454
+ printKeyValue("Map", pc7.bold(status.mapId));
10994
11455
  console.log(`
10995
- ${pc6.bold("Styles")}`);
11456
+ ${pc7.bold("Styles")}`);
10996
11457
  if (status.styles.length === 0) {
10997
11458
  logMuted(" No styles deployed.");
10998
11459
  }
10999
11460
  for (const style of status.styles) {
11000
11461
  console.log(
11001
- ` ${pc6.green("\u2713")} ${style.environment.padEnd(16)} ${link(`${apiUrl}/maps/${style.mapId}/style.json`).padEnd(36)} ${formatBytes3(style.size).padStart(10)} ${pc6.gray(formatDate(style.uploaded))}`
11462
+ ` ${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))}`
11002
11463
  );
11003
11464
  }
11004
11465
  }
11005
- function formatBytes3(bytes) {
11466
+ function formatBytes4(bytes) {
11006
11467
  if (bytes < 1024) return `${bytes} B`;
11007
11468
  const units = ["KB", "MB", "GB", "TB"];
11008
11469
  let value = bytes;