tileflow 0.1.0-alpha.14 → 0.1.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/index.js +1090 -317
- package/docs/command-families.md +4 -0
- package/docs/hosted-authentication-and-deploy.md +46 -11
- package/docs/native-artifacts.md +2 -2
- package/docs/team-3d-models.md +36 -0
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { createHash as createHash4, randomBytes as
|
|
6
|
+
import { createHash as createHash4, randomBytes as randomBytes3 } 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
|
|
11
|
-
import { createInterface } from "readline/promises";
|
|
12
|
-
import
|
|
10
|
+
import { dirname as dirname13, isAbsolute as isAbsolute5, relative as relative6, resolve as resolve12, sep as sep5 } from "path";
|
|
11
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
12
|
+
import pc9 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
|
|
36
|
+
assertValidTileflowConfig as assertValidTileflowConfig4,
|
|
37
37
|
defaultTileflowApiUrl,
|
|
38
|
-
defaultTileflowConfigPath,
|
|
38
|
+
defaultTileflowConfigPath as defaultTileflowConfigPath2,
|
|
39
39
|
defaultTileflowManifestPath,
|
|
40
40
|
getTileflowMapNames as getTileflowMapNames5,
|
|
41
|
-
loadTileflowConfigWithInputs as
|
|
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((
|
|
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(() =>
|
|
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();
|
|
@@ -4128,14 +4128,11 @@ function printFeatureInspectError(error) {
|
|
|
4128
4128
|
}
|
|
4129
4129
|
|
|
4130
4130
|
// src/hosted-client.ts
|
|
4131
|
-
import {
|
|
4132
|
-
serializeTileflowFontBundleManifest,
|
|
4133
|
-
serializeTileflowModelBundleManifest
|
|
4134
|
-
} from "@tileflow/core";
|
|
4131
|
+
import { serializeTileflowFontBundleManifest } from "@tileflow/core";
|
|
4135
4132
|
|
|
4136
4133
|
// src/hosted-response.ts
|
|
4137
4134
|
import { z as z2 } from "zod";
|
|
4138
|
-
import { tileflowIconPackageLimits
|
|
4135
|
+
import { tileflowIconPackageLimits } from "@tileflow/core";
|
|
4139
4136
|
var maximumHostedResponseBytes = 1024 * 1024;
|
|
4140
4137
|
var safeTextSchema = z2.string().min(1).max(300).refine((value) => !/[\p{Cc}]/u.test(value));
|
|
4141
4138
|
var safeIdentifierSchema = z2.string().min(1).max(200).regex(/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u);
|
|
@@ -4155,7 +4152,18 @@ var hostedStyleDeploymentResponseSchema = z2.object({
|
|
|
4155
4152
|
styleUrl: publicHttpUrlSchema
|
|
4156
4153
|
}).strict()
|
|
4157
4154
|
).refine((themes) => Object.keys(themes).length > 0),
|
|
4158
|
-
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()
|
|
4159
4167
|
}).strict();
|
|
4160
4168
|
var hostedIconPackageResponseSchema = z2.object({
|
|
4161
4169
|
changed: z2.boolean().optional(),
|
|
@@ -4173,14 +4181,6 @@ var hostedFontBundleResponseSchema = z2.object({
|
|
|
4173
4181
|
id: z2.string().regex(/^fnb_[A-Za-z0-9_-]{16,64}$/u),
|
|
4174
4182
|
totalBytes: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
|
4175
4183
|
});
|
|
4176
|
-
var hostedModelBundleResponseSchema = z2.object({
|
|
4177
|
-
baseUrl: publicHttpUrlSchema,
|
|
4178
|
-
changed: z2.boolean().optional(),
|
|
4179
|
-
contentHash: z2.string().regex(/^[a-f0-9]{64}$/u),
|
|
4180
|
-
id: z2.string().regex(/^mdb_[A-Za-z0-9_-]{16,64}$/u),
|
|
4181
|
-
modelCount: z2.number().int().positive().max(tileflowModelBundleLimits.maxModelCount),
|
|
4182
|
-
totalBytes: z2.number().int().positive().max(tileflowModelBundleLimits.maxPackageBytes)
|
|
4183
|
-
});
|
|
4184
4184
|
var hostedStatusStyleSchema = z2.object({
|
|
4185
4185
|
environment: safeTextSchema,
|
|
4186
4186
|
key: safeStorageKeySchema,
|
|
@@ -4274,7 +4274,7 @@ async function startDeviceAuthorization(appUrl, body, options = {}) {
|
|
|
4274
4274
|
async function pollDeviceToken(appUrl, authorization, input, options = {}) {
|
|
4275
4275
|
const expiresAt = Date.now() + authorization.expiresIn * 1e3;
|
|
4276
4276
|
const intervalMs = Math.max(authorization.interval, 1) * 1e3;
|
|
4277
|
-
const wait = input.sleep ?? ((milliseconds) => new Promise((
|
|
4277
|
+
const wait = input.sleep ?? ((milliseconds) => new Promise((resolve13) => setTimeout(resolve13, milliseconds)));
|
|
4278
4278
|
while (Date.now() < expiresAt) {
|
|
4279
4279
|
await wait(intervalMs);
|
|
4280
4280
|
const response = await requestHostedJson(
|
|
@@ -4447,6 +4447,88 @@ async function listAccountTeams(session, options = {}) {
|
|
|
4447
4447
|
}
|
|
4448
4448
|
return { ok: true, teams };
|
|
4449
4449
|
}
|
|
4450
|
+
async function listMapSetupTeams(session, options = {}) {
|
|
4451
|
+
let response;
|
|
4452
|
+
try {
|
|
4453
|
+
response = await requestHostedJson(
|
|
4454
|
+
session.apiOrigin,
|
|
4455
|
+
"/v1/cli/map-setup/teams",
|
|
4456
|
+
{ headers: authorizationHeaders(session.accountSession) },
|
|
4457
|
+
options
|
|
4458
|
+
);
|
|
4459
|
+
} catch (error) {
|
|
4460
|
+
return { error: safeTransportError(error, "Team discovery failed."), ok: false };
|
|
4461
|
+
}
|
|
4462
|
+
if (!response.ok) {
|
|
4463
|
+
return {
|
|
4464
|
+
error: response.status === 401 ? rejectedAccountSessionMessage : `Team discovery failed (${response.status}).`,
|
|
4465
|
+
ok: false
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
const body = asRecord(response.body);
|
|
4469
|
+
if (!response.json || body.schemaVersion !== 1 || !Array.isArray(body.teams) || !body.teams.every(isMapSetupTeam)) {
|
|
4470
|
+
return { error: "Team discovery returned an invalid response.", ok: false };
|
|
4471
|
+
}
|
|
4472
|
+
return { ok: true, teams: body.teams };
|
|
4473
|
+
}
|
|
4474
|
+
async function createManagedMapSetup(session, input, options = {}) {
|
|
4475
|
+
let response;
|
|
4476
|
+
try {
|
|
4477
|
+
response = await requestHostedJson(
|
|
4478
|
+
session.apiOrigin,
|
|
4479
|
+
"/v1/cli/maps",
|
|
4480
|
+
{
|
|
4481
|
+
...jsonRequest("POST", input),
|
|
4482
|
+
headers: {
|
|
4483
|
+
...authorizationHeaders(session.accountSession),
|
|
4484
|
+
"Content-Type": "application/json"
|
|
4485
|
+
}
|
|
4486
|
+
},
|
|
4487
|
+
options
|
|
4488
|
+
);
|
|
4489
|
+
} catch (error) {
|
|
4490
|
+
return {
|
|
4491
|
+
code: "setup_failed",
|
|
4492
|
+
error: safeTransportError(error, "Map setup request failed."),
|
|
4493
|
+
ok: false
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
const body = asRecord(response.body);
|
|
4497
|
+
if (!response.ok) {
|
|
4498
|
+
if (response.status === 401) {
|
|
4499
|
+
return { code: "account_session_rejected", error: rejectedAccountSessionMessage, ok: false };
|
|
4500
|
+
}
|
|
4501
|
+
const code = mapSetupFailureMessages[body.code] ? body.code : null;
|
|
4502
|
+
return code ? { code, error: mapSetupFailureMessages[code], ok: false } : { code: "setup_failed", error: `Map setup failed (${response.status}).`, ok: false };
|
|
4503
|
+
}
|
|
4504
|
+
const map = asRecord(body.map);
|
|
4505
|
+
const team = asRecord(body.team);
|
|
4506
|
+
const delivery = asRecord(body.delivery);
|
|
4507
|
+
if (!response.json || body.schemaVersion !== 1 || typeof map.id !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(map.id) || map.name !== null && typeof map.name !== "string" || map.state !== "setup_pending" && map.state !== "active" || typeof team.name !== "string" || `@${String(team.slug)}` !== input.team || !Array.isArray(delivery.allowedOrigins) || !delivery.allowedOrigins.every((origin) => typeof origin === "string") || delivery.allowedOrigins.length !== input.allowedOrigins.length) {
|
|
4508
|
+
return { code: "setup_failed", error: "Map setup returned an invalid response.", ok: false };
|
|
4509
|
+
}
|
|
4510
|
+
return {
|
|
4511
|
+
ok: true,
|
|
4512
|
+
value: {
|
|
4513
|
+
allowedOrigins: delivery.allowedOrigins,
|
|
4514
|
+
map: { id: map.id, name: map.name, state: map.state },
|
|
4515
|
+
team: { name: team.name, slug: team.slug }
|
|
4516
|
+
}
|
|
4517
|
+
};
|
|
4518
|
+
}
|
|
4519
|
+
var mapSetupFailureMessages = {
|
|
4520
|
+
invalid_request: "Tileflow rejected the Map setup request.",
|
|
4521
|
+
map_limit: "This Team has reached its Map limit. Archive or delete a Map, or change the plan, in the Tileflow dashboard.",
|
|
4522
|
+
permission_required: "Only a Team Admin can create Maps.",
|
|
4523
|
+
setup_conflict: "This setup key was already used with another Team or browser access choice. Omit --setup-key to start a new setup.",
|
|
4524
|
+
team_not_found: "No Team you administer matches this Team."
|
|
4525
|
+
};
|
|
4526
|
+
function isMapSetupTeam(value) {
|
|
4527
|
+
const team = asRecord(value);
|
|
4528
|
+
return typeof team.eligible === "boolean" && typeof team.name === "string" && typeof team.slug === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(team.slug) && (team.unavailableReason === void 0 || team.unavailableReason === "map_limit") && Object.keys(team).every(
|
|
4529
|
+
(key) => ["eligible", "name", "slug", "unavailableReason"].includes(key)
|
|
4530
|
+
);
|
|
4531
|
+
}
|
|
4450
4532
|
async function publishHostedStyle(api, body, responseLabel, options = {}) {
|
|
4451
4533
|
const response = await requestHostedJson(
|
|
4452
4534
|
api.apiUrl,
|
|
@@ -4533,42 +4615,6 @@ async function uploadHostedFontBundle(api, bundle, options = {}) {
|
|
|
4533
4615
|
}
|
|
4534
4616
|
return { ok: true, value };
|
|
4535
4617
|
}
|
|
4536
|
-
async function uploadHostedModelBundle(api, bundle, options = {}) {
|
|
4537
|
-
const formData = new FormData();
|
|
4538
|
-
formData.append(
|
|
4539
|
-
"manifest",
|
|
4540
|
-
new Blob([serializeTileflowModelBundleManifest(bundle.manifest)], {
|
|
4541
|
-
type: "application/json"
|
|
4542
|
-
}),
|
|
4543
|
-
"manifest.json"
|
|
4544
|
-
);
|
|
4545
|
-
for (const file of bundle.files) {
|
|
4546
|
-
const bytes = new Uint8Array(file.source.byteLength);
|
|
4547
|
-
bytes.set(file.source);
|
|
4548
|
-
formData.append(
|
|
4549
|
-
`file:${encodeURIComponent(file.fileName)}`,
|
|
4550
|
-
new Blob([bytes.buffer], { type: file.contentType }),
|
|
4551
|
-
file.fileName
|
|
4552
|
-
);
|
|
4553
|
-
}
|
|
4554
|
-
const response = await requestHostedJson(
|
|
4555
|
-
api.apiUrl,
|
|
4556
|
-
`/v1/model-bundles/${encodeURIComponent(bundle.contentHash)}`,
|
|
4557
|
-
{ body: formData, headers: authorizationHeaders(api.apiKey, api.mapId), method: "PUT" },
|
|
4558
|
-
options
|
|
4559
|
-
);
|
|
4560
|
-
if (!response.ok) return { ok: false, status: response.status };
|
|
4561
|
-
const value = parseResponse(
|
|
4562
|
-
response,
|
|
4563
|
-
hostedModelBundleResponseSchema,
|
|
4564
|
-
"Model bundle upload response"
|
|
4565
|
-
);
|
|
4566
|
-
const publicBaseUrl = new URL(value.baseUrl);
|
|
4567
|
-
if (value.contentHash !== bundle.contentHash || value.modelCount !== bundle.manifest.models.length || value.totalBytes !== bundle.manifest.models.reduce((total, model) => total + model.byteLength, 0) || publicBaseUrl.pathname !== `/model-bundles/${value.id}` || publicBaseUrl.search !== "") {
|
|
4568
|
-
throw new Error("Model bundle upload response did not confirm the submitted bundle.");
|
|
4569
|
-
}
|
|
4570
|
-
return { ok: true, value };
|
|
4571
|
-
}
|
|
4572
4618
|
async function fetchHostedMapStatus(api, options = {}) {
|
|
4573
4619
|
const response = await requestHostedJson(
|
|
4574
4620
|
api.apiUrl,
|
|
@@ -4610,7 +4656,7 @@ async function withHostedResponse(apiUrl, path, init, options, consume) {
|
|
|
4610
4656
|
}
|
|
4611
4657
|
function raceWithAbort(operation, signal) {
|
|
4612
4658
|
if (signal.aborted) return Promise.reject(signal.reason);
|
|
4613
|
-
return new Promise((
|
|
4659
|
+
return new Promise((resolve13, reject) => {
|
|
4614
4660
|
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
4615
4661
|
const onAbort = () => {
|
|
4616
4662
|
cleanup();
|
|
@@ -4620,7 +4666,7 @@ function raceWithAbort(operation, signal) {
|
|
|
4620
4666
|
operation.then(
|
|
4621
4667
|
(value) => {
|
|
4622
4668
|
cleanup();
|
|
4623
|
-
|
|
4669
|
+
resolve13(value);
|
|
4624
4670
|
},
|
|
4625
4671
|
(error) => {
|
|
4626
4672
|
cleanup();
|
|
@@ -4830,12 +4876,12 @@ function cloneStyle(style) {
|
|
|
4830
4876
|
}
|
|
4831
4877
|
|
|
4832
4878
|
// src/model-assets.ts
|
|
4879
|
+
import { tileflowTeamModelUrl } from "@tileflow/core";
|
|
4833
4880
|
import {
|
|
4834
|
-
createTileflowModelBundle,
|
|
4835
4881
|
prepareTileflowMapTreeModels
|
|
4836
4882
|
} from "@tileflow/dev/icons";
|
|
4837
4883
|
async function prepareModelAssets(project, options) {
|
|
4838
|
-
const prepared = { assets: {},
|
|
4884
|
+
const prepared = { assets: {}, buildings: {}, identities: {} };
|
|
4839
4885
|
for (const [name, map] of Object.entries(project.maps)) {
|
|
4840
4886
|
if (!map.models?.length) continue;
|
|
4841
4887
|
const models = await prepareTileflowMapTreeModels(map.models, {
|
|
@@ -4844,25 +4890,36 @@ async function prepareModelAssets(project, options) {
|
|
|
4844
4890
|
configPath: `maps.${name}.models`
|
|
4845
4891
|
});
|
|
4846
4892
|
prepared.assets[name] = { models: models.models };
|
|
4847
|
-
|
|
4848
|
-
if (bundle) prepared.bundles[name] = bundle;
|
|
4893
|
+
if (Object.keys(models.buildings).length > 0) prepared.buildings[name] = models.buildings;
|
|
4849
4894
|
if (models.identities.length > 0) prepared.identities[name] = models.identities;
|
|
4850
4895
|
}
|
|
4851
4896
|
return prepared;
|
|
4852
4897
|
}
|
|
4853
|
-
function mapAssetsWithBuildingModels(prepared,
|
|
4898
|
+
function mapAssetsWithBuildingModels(prepared, target) {
|
|
4854
4899
|
const assets = { ...prepared.assets };
|
|
4855
|
-
for (const [mapName,
|
|
4856
|
-
const base =
|
|
4900
|
+
for (const [mapName, buildings] of Object.entries(prepared.buildings)) {
|
|
4901
|
+
const base = target.hosted ? "" : target.localBase(mapName).replace(/\/+$/u, "");
|
|
4857
4902
|
assets[mapName] = {
|
|
4858
4903
|
...assets[mapName],
|
|
4859
4904
|
buildingModels: Object.fromEntries(
|
|
4860
|
-
|
|
4905
|
+
Object.entries(buildings).map(([id, model]) => [
|
|
4906
|
+
id,
|
|
4907
|
+
target.hosted ? tileflowTeamModelUrl(id) : `${base}/${model.fileName}`
|
|
4908
|
+
])
|
|
4861
4909
|
)
|
|
4862
4910
|
};
|
|
4863
4911
|
}
|
|
4864
4912
|
return assets;
|
|
4865
4913
|
}
|
|
4914
|
+
function hostedModelSourceIdentities(prepared, mapName) {
|
|
4915
|
+
const buildings = prepared.buildings[mapName] ?? {};
|
|
4916
|
+
const identities = (prepared.identities[mapName] ?? []).filter(({ id }) => !(id in buildings));
|
|
4917
|
+
return identities.length > 0 ? identities : void 0;
|
|
4918
|
+
}
|
|
4919
|
+
function teamModelSources(prepared, mapName) {
|
|
4920
|
+
const buildings = Object.entries(prepared.buildings[mapName] ?? {});
|
|
4921
|
+
return buildings.length > 0 ? Object.fromEntries(buildings.map(([id, model]) => [id, model.sources])) : void 0;
|
|
4922
|
+
}
|
|
4866
4923
|
|
|
4867
4924
|
// src/hosted-native-deploy.ts
|
|
4868
4925
|
function failure(message) {
|
|
@@ -4902,7 +4959,7 @@ async function prepareNativeHostedDeploy(options) {
|
|
|
4902
4959
|
const iconPackage = binding ? compiledIcons.packages.find((entry) => entry.contentHash === binding.packageHash) : void 0;
|
|
4903
4960
|
const sprite = binding ? `${apiOrigin}/sprites/preflight/${binding.packageHash}/sprite` : void 0;
|
|
4904
4961
|
const modelAssets = await prepareModelAssets(project, { baseDirectory, target: "hosted" });
|
|
4905
|
-
if (modelAssets.
|
|
4962
|
+
if (modelAssets.buildings[mapName])
|
|
4906
4963
|
throw failure(
|
|
4907
4964
|
"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."
|
|
4908
4965
|
);
|
|
@@ -6942,6 +6999,710 @@ async function readCurrentPins(contents) {
|
|
|
6942
6999
|
return (await parseTileflowIconsLockfile(contents)).sets;
|
|
6943
7000
|
}
|
|
6944
7001
|
|
|
7002
|
+
// src/map-commands.ts
|
|
7003
|
+
import pc5 from "picocolors";
|
|
7004
|
+
|
|
7005
|
+
// src/map-setup.ts
|
|
7006
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
7007
|
+
import { createInterface } from "readline/promises";
|
|
7008
|
+
import pc4 from "picocolors";
|
|
7009
|
+
var maximumOrigins2 = 20;
|
|
7010
|
+
var exactHost = /^(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*|\[[0-9a-f:.]+\])$/u;
|
|
7011
|
+
var teamReference = /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u;
|
|
7012
|
+
var setupKeyPattern = /^[\x21-\x7e]{8,128}$/u;
|
|
7013
|
+
function resolveMapSetupTeam(teams, requestedSlug) {
|
|
7014
|
+
if (requestedSlug !== void 0) {
|
|
7015
|
+
const team = teams.find(({ slug }) => slug === requestedSlug);
|
|
7016
|
+
if (!team) return { kind: "not_found" };
|
|
7017
|
+
return team.eligible ? { kind: "selected", team } : { kind: "map_limit" };
|
|
7018
|
+
}
|
|
7019
|
+
const eligible = teams.filter((team) => team.eligible);
|
|
7020
|
+
if (eligible.length === 1) return { kind: "selected", team: eligible[0] };
|
|
7021
|
+
if (eligible.length > 1) return { kind: "choose", teams: eligible };
|
|
7022
|
+
return teams.some((team) => team.unavailableReason === "map_limit") ? { kind: "map_limit" } : { kind: "none" };
|
|
7023
|
+
}
|
|
7024
|
+
function normalizeBrowserOrigin(value) {
|
|
7025
|
+
let url;
|
|
7026
|
+
try {
|
|
7027
|
+
url = new URL(value);
|
|
7028
|
+
} catch {
|
|
7029
|
+
return null;
|
|
7030
|
+
}
|
|
7031
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash || !exactHost.test(url.hostname) || value.toLowerCase() !== url.origin.toLowerCase()) {
|
|
7032
|
+
return null;
|
|
7033
|
+
}
|
|
7034
|
+
return url.origin.toLowerCase();
|
|
7035
|
+
}
|
|
7036
|
+
function parseBrowserAccess(options) {
|
|
7037
|
+
const values = options.origin ?? [];
|
|
7038
|
+
if (options.anyWebsite && values.length > 0) {
|
|
7039
|
+
return { error: "Choose either --any-website or --origin, not both.", ok: false };
|
|
7040
|
+
}
|
|
7041
|
+
if (options.anyWebsite) return { ok: true, value: { kind: "any" } };
|
|
7042
|
+
if (values.length === 0) return { ok: true, value: null };
|
|
7043
|
+
return parseOriginList(values);
|
|
7044
|
+
}
|
|
7045
|
+
function parseOriginList(values) {
|
|
7046
|
+
const origins = /* @__PURE__ */ new Set();
|
|
7047
|
+
for (const value of values) {
|
|
7048
|
+
const origin = normalizeBrowserOrigin(value.trim());
|
|
7049
|
+
if (!origin) {
|
|
7050
|
+
return {
|
|
7051
|
+
error: `${JSON.stringify(value)} is not an exact website origin such as https://example.com (no path or wildcard).`,
|
|
7052
|
+
ok: false
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
origins.add(origin);
|
|
7056
|
+
}
|
|
7057
|
+
if (origins.size === 0 || origins.size > maximumOrigins2) {
|
|
7058
|
+
return { error: `Enter between 1 and ${maximumOrigins2} website origins.`, ok: false };
|
|
7059
|
+
}
|
|
7060
|
+
return { ok: true, value: { kind: "selected", origins: [...origins].sort() } };
|
|
7061
|
+
}
|
|
7062
|
+
function checkMapSetupOptions(options) {
|
|
7063
|
+
if (options.team !== void 0 && !teamReference.test(options.team)) {
|
|
7064
|
+
return { code: "invalid_team", error: "Team must use @team syntax.", ok: false };
|
|
7065
|
+
}
|
|
7066
|
+
if (options.setupKey !== void 0 && !setupKeyPattern.test(options.setupKey)) {
|
|
7067
|
+
return {
|
|
7068
|
+
code: "invalid_setup_key",
|
|
7069
|
+
error: "Setup key must be 8 to 128 visible ASCII characters without spaces.",
|
|
7070
|
+
ok: false
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
if (options.name !== void 0 && (options.name.length < 1 || options.name.length > 200 || options.name !== options.name.trim() || /\p{Cc}/u.test(options.name))) {
|
|
7074
|
+
return { code: "invalid_name", error: "Map name must be 1 to 200 characters.", ok: false };
|
|
7075
|
+
}
|
|
7076
|
+
const access = parseBrowserAccess(options);
|
|
7077
|
+
return access.ok ? { access: access.value, ok: true } : { code: "invalid_browser_access", error: access.error, ok: false };
|
|
7078
|
+
}
|
|
7079
|
+
async function setUpManagedMap(session, options, dependencies) {
|
|
7080
|
+
const checked = checkMapSetupOptions(options);
|
|
7081
|
+
if (!checked.ok) return checked;
|
|
7082
|
+
const discovered = await listMapSetupTeams(session, dependencies.request);
|
|
7083
|
+
if (!discovered.ok) {
|
|
7084
|
+
return { code: "team_discovery_failed", error: discovered.error, ok: false };
|
|
7085
|
+
}
|
|
7086
|
+
const resolution = resolveMapSetupTeam(
|
|
7087
|
+
discovered.teams,
|
|
7088
|
+
options.team ? teamReference.exec(options.team)[1] : void 0
|
|
7089
|
+
);
|
|
7090
|
+
let team = null;
|
|
7091
|
+
if (resolution.kind === "selected") team = resolution.team;
|
|
7092
|
+
else if (resolution.kind === "choose") {
|
|
7093
|
+
team = dependencies.prompts ? await dependencies.prompts.chooseTeam(resolution.teams) : null;
|
|
7094
|
+
if (!team) {
|
|
7095
|
+
const choices = resolution.teams.map(({ slug }) => `@${slug}`);
|
|
7096
|
+
return {
|
|
7097
|
+
code: "team_ambiguous",
|
|
7098
|
+
error: `Several Teams can own this Map. Choose one with --team: ${choices.join(", ")}.`,
|
|
7099
|
+
ok: false,
|
|
7100
|
+
options: choices
|
|
7101
|
+
};
|
|
7102
|
+
}
|
|
7103
|
+
} else if (resolution.kind === "map_limit") {
|
|
7104
|
+
return {
|
|
7105
|
+
code: "map_limit",
|
|
7106
|
+
error: options.team ? `${options.team} has reached its Map limit. Archive or delete a Map, or change the plan, in the Tileflow dashboard.` : "Every Team you administer has reached its Map limit. Archive or delete a Map, or change the plan, in the Tileflow dashboard.",
|
|
7107
|
+
ok: false
|
|
7108
|
+
};
|
|
7109
|
+
} else if (resolution.kind === "not_found") {
|
|
7110
|
+
return { code: "team_not_found", error: `You do not administer ${options.team}.`, ok: false };
|
|
7111
|
+
} else {
|
|
7112
|
+
return {
|
|
7113
|
+
code: "no_eligible_team",
|
|
7114
|
+
error: "This account does not administer a Team. Ask a Team Admin to create the Map.",
|
|
7115
|
+
ok: false
|
|
7116
|
+
};
|
|
7117
|
+
}
|
|
7118
|
+
const access = checked.access ?? (dependencies.prompts ? await dependencies.prompts.chooseAccess() : null);
|
|
7119
|
+
if (!access) {
|
|
7120
|
+
return {
|
|
7121
|
+
code: "browser_access_required",
|
|
7122
|
+
error: "Choose which websites may load this Map: --origin <origin> (repeatable) for exact sites, or --any-website.",
|
|
7123
|
+
ok: false
|
|
7124
|
+
};
|
|
7125
|
+
}
|
|
7126
|
+
const setupKey = options.setupKey ?? `cli-${randomBytes2(16).toString("hex")}`;
|
|
7127
|
+
const created = await createManagedMapSetup(
|
|
7128
|
+
session,
|
|
7129
|
+
{
|
|
7130
|
+
allowedOrigins: access.kind === "any" ? [] : access.origins,
|
|
7131
|
+
idempotencyKey: setupKey,
|
|
7132
|
+
...options.name ? { name: options.name } : {},
|
|
7133
|
+
team: `@${team.slug}`
|
|
7134
|
+
},
|
|
7135
|
+
dependencies.request
|
|
7136
|
+
);
|
|
7137
|
+
return created.ok ? { ok: true, setupKey, value: created.value } : created;
|
|
7138
|
+
}
|
|
7139
|
+
function describeBrowserAccess(allowedOrigins) {
|
|
7140
|
+
return allowedOrigins.length === 0 ? "Any website" : allowedOrigins.join(", ");
|
|
7141
|
+
}
|
|
7142
|
+
function createTerminalPrompts(json2 = false) {
|
|
7143
|
+
if (json2 || !process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
7144
|
+
return {
|
|
7145
|
+
async chooseTeam(teams) {
|
|
7146
|
+
console.log(`${pc4.cyan("?")} Which Team should own the new Map?`);
|
|
7147
|
+
teams.forEach(
|
|
7148
|
+
(team, index) => console.log(` ${index + 1}) ${team.name} ${pc4.gray(`@${team.slug}`)}`)
|
|
7149
|
+
);
|
|
7150
|
+
const answer = await ask("Team number", (value) => {
|
|
7151
|
+
const index = Number(value);
|
|
7152
|
+
return Number.isInteger(index) && index >= 1 && index <= teams.length ? teams[index - 1] : null;
|
|
7153
|
+
});
|
|
7154
|
+
return answer;
|
|
7155
|
+
},
|
|
7156
|
+
async chooseAccess() {
|
|
7157
|
+
console.log(`${pc4.cyan("?")} Which websites may load this Map in a browser?`);
|
|
7158
|
+
console.log(" 1) Only selected websites (recommended)");
|
|
7159
|
+
console.log(" 2) Any website");
|
|
7160
|
+
const mode = await ask(
|
|
7161
|
+
"Choice",
|
|
7162
|
+
(value) => value === "1" ? "selected" : value === "2" ? "any" : null
|
|
7163
|
+
);
|
|
7164
|
+
if (mode === null) return null;
|
|
7165
|
+
if (mode === "any") return { kind: "any" };
|
|
7166
|
+
return ask("Website origins, separated by spaces (https://example.com)", (value) => {
|
|
7167
|
+
const parsed = parseOriginList(value.split(/[\s,]+/u).filter(Boolean));
|
|
7168
|
+
if (!parsed.ok) console.log(` ${pc4.yellow(parsed.error)}`);
|
|
7169
|
+
return parsed.ok ? parsed.value : null;
|
|
7170
|
+
});
|
|
7171
|
+
}
|
|
7172
|
+
};
|
|
7173
|
+
}
|
|
7174
|
+
async function ask(question, parse3) {
|
|
7175
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
7176
|
+
try {
|
|
7177
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
7178
|
+
const value = parse3((await readline.question(` ${question}: `)).trim());
|
|
7179
|
+
if (value !== null) return value;
|
|
7180
|
+
}
|
|
7181
|
+
return null;
|
|
7182
|
+
} finally {
|
|
7183
|
+
readline.close();
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
|
|
7187
|
+
// src/map-commands.ts
|
|
7188
|
+
function registerMapCommands(program2, dependencies) {
|
|
7189
|
+
const maps = program2.command("maps").description("Manage Tileflow managed Maps");
|
|
7190
|
+
addMapSetupOptions(
|
|
7191
|
+
maps.command("create").description("Create a managed Map for this account, ready for tileflow deploy --map-id").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--name <name>", "Map name shown in the dashboard").option("--json", "print deterministic schema-version-1 JSON")
|
|
7192
|
+
).action(async (options) => {
|
|
7193
|
+
const session = await loadSession(options, dependencies);
|
|
7194
|
+
if (!session) return;
|
|
7195
|
+
const result = await setUpManagedMap(session, options, {
|
|
7196
|
+
prompts: createTerminalPrompts(options.json)
|
|
7197
|
+
});
|
|
7198
|
+
if (!result.ok) {
|
|
7199
|
+
emitFailure(
|
|
7200
|
+
options.json,
|
|
7201
|
+
result.code,
|
|
7202
|
+
result.error,
|
|
7203
|
+
result.options ? { options: result.options } : {}
|
|
7204
|
+
);
|
|
7205
|
+
return;
|
|
7206
|
+
}
|
|
7207
|
+
const next = `tileflow deploy --map-id ${result.value.map.id}`;
|
|
7208
|
+
if (options.json) {
|
|
7209
|
+
emitJson({
|
|
7210
|
+
command: "maps create",
|
|
7211
|
+
delivery: { allowedOrigins: result.value.allowedOrigins },
|
|
7212
|
+
map: result.value.map,
|
|
7213
|
+
next,
|
|
7214
|
+
ok: true,
|
|
7215
|
+
schemaVersion: 1,
|
|
7216
|
+
setupKey: result.setupKey,
|
|
7217
|
+
team: result.value.team
|
|
7218
|
+
});
|
|
7219
|
+
return;
|
|
7220
|
+
}
|
|
7221
|
+
printCreatedMap(result.value);
|
|
7222
|
+
console.log(`
|
|
7223
|
+
${pc5.bold("Next step")}`);
|
|
7224
|
+
console.log(` ${pc5.cyan("\u2022")} Publish the repository map with ${pc5.cyan(next)}.`);
|
|
7225
|
+
});
|
|
7226
|
+
}
|
|
7227
|
+
function addMapSetupOptions(command2) {
|
|
7228
|
+
return command2.option("--team <team>", "Team that owns a new Map, as @team").option(
|
|
7229
|
+
"--origin <origin>",
|
|
7230
|
+
"exact website origin that may load a new Map (repeatable)",
|
|
7231
|
+
(value, previous = []) => [...previous, value]
|
|
7232
|
+
).option("--any-website", "let any website load a new Map").option("--setup-key <key>", "repeat an interrupted Map creation with the key it printed");
|
|
7233
|
+
}
|
|
7234
|
+
function hasMapSetupOptions(options) {
|
|
7235
|
+
return options.team !== void 0 || (options.origin?.length ?? 0) > 0 || options.anyWebsite === true || options.setupKey !== void 0;
|
|
7236
|
+
}
|
|
7237
|
+
async function createMapForDeploy(options, dependencies) {
|
|
7238
|
+
const session = await loadSession(options, dependencies);
|
|
7239
|
+
if (!session) return null;
|
|
7240
|
+
console.log(`${pc5.cyan("i")} No --map-id given; creating a managed Map for this deploy.`);
|
|
7241
|
+
const result = await setUpManagedMap(session, options, {
|
|
7242
|
+
prompts: dependencies.prompts === void 0 ? createTerminalPrompts() : dependencies.prompts
|
|
7243
|
+
});
|
|
7244
|
+
if (!result.ok) {
|
|
7245
|
+
emitFailure(false, result.code, result.error);
|
|
7246
|
+
return null;
|
|
7247
|
+
}
|
|
7248
|
+
printCreatedMap(result.value);
|
|
7249
|
+
console.log(
|
|
7250
|
+
pc5.gray(
|
|
7251
|
+
`If this deploy is interrupted, rerun it with --setup-key ${result.setupKey} to reuse the same Map.`
|
|
7252
|
+
)
|
|
7253
|
+
);
|
|
7254
|
+
return result.value.map.id;
|
|
7255
|
+
}
|
|
7256
|
+
function printCreatedMap(created) {
|
|
7257
|
+
console.log(
|
|
7258
|
+
`${pc5.green("\u2713")} Created ${pc5.bold(created.map.id)} in ${created.team.name} ${pc5.gray(`@${created.team.slug}`)}.`
|
|
7259
|
+
);
|
|
7260
|
+
console.log(
|
|
7261
|
+
`${pc5.gray("Websites:".padEnd(13))} ${describeBrowserAccess(created.allowedOrigins)}`
|
|
7262
|
+
);
|
|
7263
|
+
}
|
|
7264
|
+
async function loadSession(options, dependencies) {
|
|
7265
|
+
let apiUrl;
|
|
7266
|
+
try {
|
|
7267
|
+
apiUrl = normalizeApiOrigin(options.apiUrl ?? dependencies.defaultApiUrl);
|
|
7268
|
+
} catch (error) {
|
|
7269
|
+
emitFailure(options.json, "invalid_api_url", safeMessage(error));
|
|
7270
|
+
return null;
|
|
7271
|
+
}
|
|
7272
|
+
let config;
|
|
7273
|
+
try {
|
|
7274
|
+
config = await dependencies.loadAuthConfig();
|
|
7275
|
+
} catch (error) {
|
|
7276
|
+
emitFailure(options.json, "auth_state_unavailable", safeMessage(error));
|
|
7277
|
+
return null;
|
|
7278
|
+
}
|
|
7279
|
+
const selected = resolveAccountSession(config, apiUrl);
|
|
7280
|
+
if (selected.kind !== "selected") {
|
|
7281
|
+
emitFailure(
|
|
7282
|
+
options.json,
|
|
7283
|
+
`account_session_${selected.kind}`,
|
|
7284
|
+
selected.kind === "expired" ? "The Tileflow account session has expired. Run tileflow login." : "Creating a Map needs a Tileflow account session. Run tileflow login."
|
|
7285
|
+
);
|
|
7286
|
+
return null;
|
|
7287
|
+
}
|
|
7288
|
+
return selected.session;
|
|
7289
|
+
}
|
|
7290
|
+
|
|
7291
|
+
// src/model-command.ts
|
|
7292
|
+
import { lstat as lstat5 } from "fs/promises";
|
|
7293
|
+
import { basename as basename4, dirname as dirname11, relative as relative4, resolve as resolve8 } from "path";
|
|
7294
|
+
import pc6 from "picocolors";
|
|
7295
|
+
import { z as z5 } from "zod";
|
|
7296
|
+
import { tileflowModelContentType } from "@tileflow/core";
|
|
7297
|
+
import {
|
|
7298
|
+
assertValidTileflowConfig as assertValidTileflowConfig3,
|
|
7299
|
+
defaultTileflowConfigPath,
|
|
7300
|
+
loadTileflowConfigWithInputs as loadTileflowConfigWithInputs4
|
|
7301
|
+
} from "@tileflow/dev/config";
|
|
7302
|
+
import {
|
|
7303
|
+
prepareTileflowMapTreeModels as prepareTileflowMapTreeModels2
|
|
7304
|
+
} from "@tileflow/dev/icons";
|
|
7305
|
+
var modelName = /^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
7306
|
+
var sha256 = /^[a-f0-9]{64}$/u;
|
|
7307
|
+
var maximumListPages = 20;
|
|
7308
|
+
function registerModelCommands(program2, dependencies) {
|
|
7309
|
+
const models = program2.command("models").description("Publish and manage the Team 3D models that maps place");
|
|
7310
|
+
withTeamOptions2(
|
|
7311
|
+
models.command("publish").description(
|
|
7312
|
+
"Prepare building models and publish each as its Team 3D model's current version"
|
|
7313
|
+
).argument(
|
|
7314
|
+
"[paths...]",
|
|
7315
|
+
"model files (<name>.glb) or directories; defaults to the maps' models directories"
|
|
7316
|
+
).option(
|
|
7317
|
+
"-c, --config <path>",
|
|
7318
|
+
"config path, read when no path is given",
|
|
7319
|
+
defaultTileflowConfigPath
|
|
7320
|
+
)
|
|
7321
|
+
).action(async (paths, options) => {
|
|
7322
|
+
let prepared;
|
|
7323
|
+
try {
|
|
7324
|
+
prepared = paths.length > 0 ? await prepareFromPaths(paths) : await prepareFromConfig(options.config);
|
|
7325
|
+
} catch (error) {
|
|
7326
|
+
return emitFailure(options.json, "model_preparation_failed", safeMessage(error));
|
|
7327
|
+
}
|
|
7328
|
+
if (prepared.size === 0) {
|
|
7329
|
+
return emitFailure(
|
|
7330
|
+
options.json,
|
|
7331
|
+
"no_building_models",
|
|
7332
|
+
"No building models were found. A building model is a .glb whose materials name its parts: surface, secondary, glass, accent and roof."
|
|
7333
|
+
);
|
|
7334
|
+
}
|
|
7335
|
+
const authority = await resolveTeamAuthority(options, ["models:write"], dependencies);
|
|
7336
|
+
if (!authority) return;
|
|
7337
|
+
const results = [];
|
|
7338
|
+
for (const [name, model] of [...prepared].sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0)) {
|
|
7339
|
+
const body = new FormData();
|
|
7340
|
+
body.append(
|
|
7341
|
+
"metadata",
|
|
7342
|
+
new Blob([JSON.stringify({ sources: model.sources })], { type: "application/json" }),
|
|
7343
|
+
"metadata.json"
|
|
7344
|
+
);
|
|
7345
|
+
const bytes = new Uint8Array(model.bytes.byteLength);
|
|
7346
|
+
bytes.set(model.bytes);
|
|
7347
|
+
body.append(
|
|
7348
|
+
"file",
|
|
7349
|
+
new Blob([bytes.buffer], { type: tileflowModelContentType }),
|
|
7350
|
+
`${name}.glb`
|
|
7351
|
+
);
|
|
7352
|
+
const response = await authorizedTeamRequest(
|
|
7353
|
+
authority,
|
|
7354
|
+
`/v1/models/${encodeURIComponent(name)}/versions/${model.sha256}`,
|
|
7355
|
+
"PUT",
|
|
7356
|
+
{ body }
|
|
7357
|
+
);
|
|
7358
|
+
if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
|
|
7359
|
+
const parsed = publishResponseSchema2.safeParse(response.body);
|
|
7360
|
+
if (!parsed.success || parsed.data.model.current?.contentHash !== model.sha256) {
|
|
7361
|
+
return emitFailure(
|
|
7362
|
+
options.json,
|
|
7363
|
+
"model_publish_unconfirmed",
|
|
7364
|
+
`The publication of ${name} did not confirm the submitted model file.`
|
|
7365
|
+
);
|
|
7366
|
+
}
|
|
7367
|
+
results.push(parsed.data);
|
|
7368
|
+
if (!options.json) {
|
|
7369
|
+
const current = parsed.data.model.current;
|
|
7370
|
+
const action = parsed.data.changed ? "Published" : "Unchanged";
|
|
7371
|
+
console.log(
|
|
7372
|
+
`${pc6.green("\u2713")} ${action} 3D model ${pc6.bold(name)} version ${current.version} (${formatBytes3(current.byteLength)}, ${formatCount(current.summary.levels[0]?.triangles ?? 0)} triangles).`
|
|
7373
|
+
);
|
|
7374
|
+
}
|
|
7375
|
+
}
|
|
7376
|
+
if (options.json) {
|
|
7377
|
+
emitJson({
|
|
7378
|
+
command: "models publish",
|
|
7379
|
+
ok: true,
|
|
7380
|
+
schemaVersion: 1,
|
|
7381
|
+
models: results.map(({ changed, model }) => ({
|
|
7382
|
+
changed,
|
|
7383
|
+
contentHash: model.current.contentHash,
|
|
7384
|
+
name: model.name,
|
|
7385
|
+
version: model.current.version
|
|
7386
|
+
}))
|
|
7387
|
+
});
|
|
7388
|
+
} else if (results.some(({ changed }) => changed)) {
|
|
7389
|
+
console.log("Maps that place them draw the new versions on their next load.");
|
|
7390
|
+
}
|
|
7391
|
+
});
|
|
7392
|
+
withTeamOptions2(models.command("list").description("List the Team's 3D models")).action(
|
|
7393
|
+
async (options) => {
|
|
7394
|
+
const authority = await resolveTeamAuthority(options, ["models:read"], dependencies);
|
|
7395
|
+
if (!authority) return;
|
|
7396
|
+
const listed = [];
|
|
7397
|
+
let cursor;
|
|
7398
|
+
for (let page = 0; page < maximumListPages; page += 1) {
|
|
7399
|
+
const response = await authorizedTeamRequest(
|
|
7400
|
+
authority,
|
|
7401
|
+
`/v1/models${cursor ? `?cursor=${encodeURIComponent(cursor)}` : ""}`,
|
|
7402
|
+
"GET"
|
|
7403
|
+
);
|
|
7404
|
+
if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body);
|
|
7405
|
+
const parsed = listResponseSchema.safeParse(response.body);
|
|
7406
|
+
if (!parsed.success)
|
|
7407
|
+
return emitFailure(options.json, "invalid_response", "The model list was not readable.");
|
|
7408
|
+
listed.push(...parsed.data.models);
|
|
7409
|
+
cursor = parsed.data.nextCursor;
|
|
7410
|
+
if (!cursor) break;
|
|
7411
|
+
}
|
|
7412
|
+
if (options.json)
|
|
7413
|
+
return emitJson({ command: "models list", ok: true, schemaVersion: 1, models: listed });
|
|
7414
|
+
if (listed.length === 0) {
|
|
7415
|
+
console.log("The Team has no 3D models. Publish one with tileflow models publish.");
|
|
7416
|
+
return;
|
|
7417
|
+
}
|
|
7418
|
+
for (const model of listed) {
|
|
7419
|
+
const current = model.current;
|
|
7420
|
+
console.log(
|
|
7421
|
+
`${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") : ""}`
|
|
7422
|
+
);
|
|
7423
|
+
}
|
|
7424
|
+
}
|
|
7425
|
+
);
|
|
7426
|
+
withTeamOptions2(
|
|
7427
|
+
models.command("status").description("Show a 3D model's versions and the maps that place it").argument("<name>", "model name")
|
|
7428
|
+
).action(async (name, options) => {
|
|
7429
|
+
if (!modelName.test(name))
|
|
7430
|
+
return emitFailure(options.json, "invalid_model", "Model name is invalid.");
|
|
7431
|
+
const authority = await resolveTeamAuthority(options, ["models:read"], dependencies);
|
|
7432
|
+
if (!authority) return;
|
|
7433
|
+
const detail = await readDetail(authority, name, options.json);
|
|
7434
|
+
if (!detail) return;
|
|
7435
|
+
if (options.json) return emitJson({ ...detail, command: "models status", ok: true });
|
|
7436
|
+
const { model, versions, uses } = detail;
|
|
7437
|
+
console.log(
|
|
7438
|
+
`${pc6.bold(model.name)}${model.state === "purging" ? pc6.yellow(" (purging)") : ""}`
|
|
7439
|
+
);
|
|
7440
|
+
for (const version of versions) {
|
|
7441
|
+
const role = version.state === "current" ? "current" : version.state === "previous" ? "previous" : version.state;
|
|
7442
|
+
console.log(
|
|
7443
|
+
` version ${version.version} ${role} ${formatBytes3(version.byteLength)} ${formatCount(version.summary.levels[0]?.triangles ?? 0)} triangles ${version.publishedAt}`
|
|
7444
|
+
);
|
|
7445
|
+
}
|
|
7446
|
+
console.log(
|
|
7447
|
+
uses.length === 0 ? " No map places it." : ` Placed by ${uses.map(({ mapName, mapId }) => mapName ?? mapId).join(", ")}.`
|
|
7448
|
+
);
|
|
7449
|
+
});
|
|
7450
|
+
withTeamOptions2(
|
|
7451
|
+
models.command("restore").description("Make a 3D model's previous version current again, in every map that places it").argument("<name>", "model name")
|
|
7452
|
+
).action(async (name, options) => {
|
|
7453
|
+
if (!modelName.test(name))
|
|
7454
|
+
return emitFailure(options.json, "invalid_model", "Model name is invalid.");
|
|
7455
|
+
const authority = await resolveTeamAuthority(options, ["models:write"], dependencies);
|
|
7456
|
+
if (!authority) return;
|
|
7457
|
+
const detail = await readDetail(authority, name, options.json);
|
|
7458
|
+
if (!detail) return;
|
|
7459
|
+
const { current, previous } = detail.model;
|
|
7460
|
+
if (!current || !previous) {
|
|
7461
|
+
return emitFailure(
|
|
7462
|
+
options.json,
|
|
7463
|
+
"no_previous_version",
|
|
7464
|
+
`${name} has no previous version to restore.`
|
|
7465
|
+
);
|
|
7466
|
+
}
|
|
7467
|
+
const response = await authorizedTeamRequest(
|
|
7468
|
+
authority,
|
|
7469
|
+
`/v1/models/${encodeURIComponent(name)}/restore`,
|
|
7470
|
+
"POST",
|
|
7471
|
+
{
|
|
7472
|
+
body: JSON.stringify({ expectedCurrentVersion: current.version }),
|
|
7473
|
+
headers: { "Content-Type": "application/json" }
|
|
7474
|
+
}
|
|
7475
|
+
);
|
|
7476
|
+
if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
|
|
7477
|
+
const parsed = restoreResponseSchema.safeParse(response.body);
|
|
7478
|
+
if (!parsed.success || parsed.data.model.current?.version !== previous.version) {
|
|
7479
|
+
return emitFailure(
|
|
7480
|
+
options.json,
|
|
7481
|
+
"model_restore_unconfirmed",
|
|
7482
|
+
`The restore of ${name} was not confirmed.`
|
|
7483
|
+
);
|
|
7484
|
+
}
|
|
7485
|
+
if (options.json)
|
|
7486
|
+
return emitJson({
|
|
7487
|
+
command: "models restore",
|
|
7488
|
+
ok: true,
|
|
7489
|
+
schemaVersion: 1,
|
|
7490
|
+
model: parsed.data.model
|
|
7491
|
+
});
|
|
7492
|
+
console.log(
|
|
7493
|
+
`${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.`
|
|
7494
|
+
);
|
|
7495
|
+
});
|
|
7496
|
+
withTeamOptions2(
|
|
7497
|
+
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")
|
|
7498
|
+
).action(async (name, options) => {
|
|
7499
|
+
if (!modelName.test(name))
|
|
7500
|
+
return emitFailure(options.json, "invalid_model", "Model name is invalid.");
|
|
7501
|
+
if (options.confirm !== name) {
|
|
7502
|
+
return emitFailure(
|
|
7503
|
+
options.json,
|
|
7504
|
+
"confirmation_required",
|
|
7505
|
+
`Repeat the model name to purge it: --confirm ${name}`
|
|
7506
|
+
);
|
|
7507
|
+
}
|
|
7508
|
+
const authority = await resolveTeamAuthority(options, ["models:write"], dependencies);
|
|
7509
|
+
if (!authority) return;
|
|
7510
|
+
const response = await authorizedTeamRequest(
|
|
7511
|
+
authority,
|
|
7512
|
+
`/v1/models/${encodeURIComponent(name)}`,
|
|
7513
|
+
"DELETE",
|
|
7514
|
+
{
|
|
7515
|
+
headers: { "X-Tileflow-Confirm-Model-Purge": name }
|
|
7516
|
+
}
|
|
7517
|
+
);
|
|
7518
|
+
if (!response.ok) return emitHttpFailure2(options.json, response.status, response.body, name);
|
|
7519
|
+
const parsed = purgeResponseSchema2.safeParse(response.body);
|
|
7520
|
+
if (!parsed.success)
|
|
7521
|
+
return emitFailure(options.json, "invalid_response", "The purge was not confirmed.");
|
|
7522
|
+
if (options.json)
|
|
7523
|
+
return emitJson({
|
|
7524
|
+
command: "models purge",
|
|
7525
|
+
ok: true,
|
|
7526
|
+
schemaVersion: 1,
|
|
7527
|
+
purge: parsed.data.purge
|
|
7528
|
+
});
|
|
7529
|
+
console.log(
|
|
7530
|
+
`${pc6.green("\u2713")} ${pc6.bold(name)} is being purged; its name is free once its files are deleted.`
|
|
7531
|
+
);
|
|
7532
|
+
});
|
|
7533
|
+
}
|
|
7534
|
+
function withTeamOptions2(command2) {
|
|
7535
|
+
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");
|
|
7536
|
+
}
|
|
7537
|
+
async function readDetail(authority, name, json2) {
|
|
7538
|
+
const response = await authorizedTeamRequest(
|
|
7539
|
+
authority,
|
|
7540
|
+
`/v1/models/${encodeURIComponent(name)}`,
|
|
7541
|
+
"GET"
|
|
7542
|
+
);
|
|
7543
|
+
if (!response.ok) {
|
|
7544
|
+
emitHttpFailure2(json2, response.status, response.body, name);
|
|
7545
|
+
return void 0;
|
|
7546
|
+
}
|
|
7547
|
+
const parsed = detailResponseSchema.safeParse(response.body);
|
|
7548
|
+
if (!parsed.success) {
|
|
7549
|
+
emitFailure(json2, "invalid_response", `The details of ${name} were not readable.`);
|
|
7550
|
+
return void 0;
|
|
7551
|
+
}
|
|
7552
|
+
return parsed.data;
|
|
7553
|
+
}
|
|
7554
|
+
async function prepareFromPaths(paths) {
|
|
7555
|
+
const cwd = process.cwd();
|
|
7556
|
+
const directories = /* @__PURE__ */ new Map();
|
|
7557
|
+
for (const path of paths) {
|
|
7558
|
+
const absolute = resolve8(cwd, path);
|
|
7559
|
+
const info = await lstat5(absolute).catch(() => void 0);
|
|
7560
|
+
if (!info) throw new Error(`${path} does not exist.`);
|
|
7561
|
+
const directory = info.isDirectory() ? absolute : dirname11(absolute);
|
|
7562
|
+
if (relative4(cwd, directory).startsWith(".."))
|
|
7563
|
+
throw new Error(`${path} is outside the current directory.`);
|
|
7564
|
+
if (info.isDirectory()) {
|
|
7565
|
+
directories.set(directory, "all");
|
|
7566
|
+
continue;
|
|
7567
|
+
}
|
|
7568
|
+
const file = basename4(absolute);
|
|
7569
|
+
if (!file.toLowerCase().endsWith(".glb"))
|
|
7570
|
+
throw new Error(`${path} is not a glTF binary (.glb).`);
|
|
7571
|
+
const name = file.replace(/(\.mid|\.far)?\.glb$/iu, "");
|
|
7572
|
+
const selected = directories.get(directory);
|
|
7573
|
+
if (selected === "all") continue;
|
|
7574
|
+
directories.set(directory, /* @__PURE__ */ new Set([...selected ?? [], name]));
|
|
7575
|
+
}
|
|
7576
|
+
const models = /* @__PURE__ */ new Map();
|
|
7577
|
+
for (const [directory, selected] of directories) {
|
|
7578
|
+
const parent = dirname11(directory);
|
|
7579
|
+
const local = `./${basename4(directory)}`;
|
|
7580
|
+
const display = relative4(cwd, directory) || ".";
|
|
7581
|
+
const prepared = await prepareTileflowMapTreeModels2([local], {
|
|
7582
|
+
baseDirectory: parent,
|
|
7583
|
+
configPath: display,
|
|
7584
|
+
cwd: parent,
|
|
7585
|
+
target: "hosted"
|
|
7586
|
+
});
|
|
7587
|
+
for (const [name, model] of Object.entries(prepared.buildings)) {
|
|
7588
|
+
if (selected !== "all" && !selected.has(name)) continue;
|
|
7589
|
+
addModel(models, name, model);
|
|
7590
|
+
}
|
|
7591
|
+
if (selected !== "all") {
|
|
7592
|
+
for (const name of selected)
|
|
7593
|
+
if (!prepared.buildings[name])
|
|
7594
|
+
throw new Error(
|
|
7595
|
+
`${display}/${name}.glb is not a building model: its materials must name its parts (surface, secondary, glass, accent, roof).`
|
|
7596
|
+
);
|
|
7597
|
+
}
|
|
7598
|
+
}
|
|
7599
|
+
return models;
|
|
7600
|
+
}
|
|
7601
|
+
async function prepareFromConfig(configPath) {
|
|
7602
|
+
const loaded = await withTileflowConfigSecretsHidden(
|
|
7603
|
+
() => loadTileflowConfigWithInputs4(configPath)
|
|
7604
|
+
);
|
|
7605
|
+
const project = loaded.project;
|
|
7606
|
+
assertValidTileflowConfig3(project);
|
|
7607
|
+
const prepared = await prepareModelAssets(project, {
|
|
7608
|
+
baseDirectory: dirname11(loaded.configFile),
|
|
7609
|
+
target: "hosted"
|
|
7610
|
+
});
|
|
7611
|
+
const models = /* @__PURE__ */ new Map();
|
|
7612
|
+
for (const buildings of Object.values(prepared.buildings))
|
|
7613
|
+
for (const [name, model] of Object.entries(buildings)) addModel(models, name, model);
|
|
7614
|
+
return models;
|
|
7615
|
+
}
|
|
7616
|
+
function addModel(models, name, model) {
|
|
7617
|
+
const known = models.get(name);
|
|
7618
|
+
if (known && known.sha256 !== model.sha256)
|
|
7619
|
+
throw new Error(`Two different models are named ${name}; a Team has one model of each name.`);
|
|
7620
|
+
models.set(name, model);
|
|
7621
|
+
}
|
|
7622
|
+
function emitHttpFailure2(json2, status, body, name) {
|
|
7623
|
+
const error = z5.object({ error: z5.object({ code: z5.string().max(80), message: z5.string().max(400) }).partial() }).partial().safeParse(body);
|
|
7624
|
+
const message = error.success ? error.data.error?.message : void 0;
|
|
7625
|
+
const code = error.success ? error.data.error?.code : void 0;
|
|
7626
|
+
if (status === 404)
|
|
7627
|
+
return emitFailure(
|
|
7628
|
+
json2,
|
|
7629
|
+
"model_not_found",
|
|
7630
|
+
name ? `The Team has no 3D model named ${name}.` : "Not found."
|
|
7631
|
+
);
|
|
7632
|
+
return emitFailure(
|
|
7633
|
+
json2,
|
|
7634
|
+
code ?? `http_${status}`,
|
|
7635
|
+
message ?? `The request failed with ${status}.`
|
|
7636
|
+
);
|
|
7637
|
+
}
|
|
7638
|
+
function formatBytes3(bytes) {
|
|
7639
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
7640
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`;
|
|
7641
|
+
return `${(bytes / 1024 / 1024).toFixed(1)} MiB`;
|
|
7642
|
+
}
|
|
7643
|
+
function formatCount(count) {
|
|
7644
|
+
return new Intl.NumberFormat("en-US").format(count);
|
|
7645
|
+
}
|
|
7646
|
+
var summarySchema = z5.object({
|
|
7647
|
+
levels: z5.array(
|
|
7648
|
+
z5.object({
|
|
7649
|
+
level: z5.enum(["near", "mid", "far"]),
|
|
7650
|
+
vertices: z5.number().int().nonnegative(),
|
|
7651
|
+
triangles: z5.number().int().nonnegative()
|
|
7652
|
+
}).strict()
|
|
7653
|
+
).min(1).max(3),
|
|
7654
|
+
width: z5.number().nonnegative(),
|
|
7655
|
+
depth: z5.number().nonnegative(),
|
|
7656
|
+
height: z5.number().nonnegative(),
|
|
7657
|
+
footprint: z5.object({
|
|
7658
|
+
rings: z5.number().int().nonnegative(),
|
|
7659
|
+
points: z5.number().int().nonnegative(),
|
|
7660
|
+
area: z5.number().nonnegative()
|
|
7661
|
+
}).strict()
|
|
7662
|
+
}).strict();
|
|
7663
|
+
var versionSchema = z5.object({
|
|
7664
|
+
id: z5.string().regex(/^tmv_[A-Za-z0-9_-]{16,64}$/u),
|
|
7665
|
+
version: z5.number().int().positive(),
|
|
7666
|
+
state: z5.enum(["current", "previous", "gc_pending", "deleting", "deleted"]),
|
|
7667
|
+
contentHash: z5.string().regex(sha256),
|
|
7668
|
+
byteLength: z5.number().int().positive(),
|
|
7669
|
+
publishedAt: z5.iso.datetime({ offset: true }),
|
|
7670
|
+
summary: summarySchema
|
|
7671
|
+
}).strict();
|
|
7672
|
+
var modelSchema = z5.object({
|
|
7673
|
+
id: z5.string().regex(/^tmd_[A-Za-z0-9_-]{16,64}$/u),
|
|
7674
|
+
name: z5.string().regex(modelName).max(64),
|
|
7675
|
+
state: z5.enum(["active", "purging"]),
|
|
7676
|
+
current: versionSchema.nullable(),
|
|
7677
|
+
previous: versionSchema.nullable(),
|
|
7678
|
+
url: z5.url(),
|
|
7679
|
+
usedByMaps: z5.number().int().nonnegative(),
|
|
7680
|
+
updatedAt: z5.iso.datetime({ offset: true })
|
|
7681
|
+
}).strict();
|
|
7682
|
+
var listResponseSchema = z5.object({
|
|
7683
|
+
schemaVersion: z5.literal(1),
|
|
7684
|
+
models: z5.array(modelSchema).max(100),
|
|
7685
|
+
nextCursor: z5.string().min(1).max(512).optional()
|
|
7686
|
+
}).strict();
|
|
7687
|
+
var detailResponseSchema = z5.object({
|
|
7688
|
+
schemaVersion: z5.literal(1),
|
|
7689
|
+
model: modelSchema,
|
|
7690
|
+
versions: z5.array(versionSchema).max(100),
|
|
7691
|
+
uses: z5.array(
|
|
7692
|
+
z5.object({
|
|
7693
|
+
mapId: z5.string().min(1).max(160),
|
|
7694
|
+
mapName: z5.string().max(200).nullable(),
|
|
7695
|
+
relationship: z5.enum(["active", "retained"])
|
|
7696
|
+
}).strict()
|
|
7697
|
+
).max(100)
|
|
7698
|
+
}).strict();
|
|
7699
|
+
var publishResponseSchema2 = z5.object({ schemaVersion: z5.literal(1), changed: z5.boolean(), model: modelSchema }).strict();
|
|
7700
|
+
var restoreResponseSchema = z5.object({ schemaVersion: z5.literal(1), model: modelSchema }).strict();
|
|
7701
|
+
var purgeResponseSchema2 = z5.object({
|
|
7702
|
+
schemaVersion: z5.literal(1),
|
|
7703
|
+
purge: z5.object({ name: z5.string().regex(modelName), state: z5.enum(["purging", "purged"]) }).strict()
|
|
7704
|
+
}).strict();
|
|
7705
|
+
|
|
6945
7706
|
// src/native-preview-command.ts
|
|
6946
7707
|
import { tileflowNativeProfile } from "@tileflow/core/native-profile";
|
|
6947
7708
|
import {
|
|
@@ -7030,7 +7791,7 @@ function resolveTileflowExternalOpenInvocation(target, platform) {
|
|
|
7030
7791
|
}
|
|
7031
7792
|
|
|
7032
7793
|
// src/project-commands.ts
|
|
7033
|
-
import
|
|
7794
|
+
import pc7 from "picocolors";
|
|
7034
7795
|
function registerProjectCommands(program2, dependencies) {
|
|
7035
7796
|
const projects = program2.command("projects", { hidden: true }).description("Manage internal application destinations");
|
|
7036
7797
|
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) => {
|
|
@@ -7046,12 +7807,12 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7046
7807
|
};
|
|
7047
7808
|
if (options.json) return emitJson2(document);
|
|
7048
7809
|
if (!result.items.length) {
|
|
7049
|
-
console.log(
|
|
7810
|
+
console.log(pc7.gray("No managed destinations found."));
|
|
7050
7811
|
return;
|
|
7051
7812
|
}
|
|
7052
7813
|
for (const target of result.items) {
|
|
7053
7814
|
console.log(
|
|
7054
|
-
` ${target.project.archivedAt ?
|
|
7815
|
+
` ${target.project.archivedAt ? pc7.yellow("\u25CB") : pc7.green("\u25CF")} ${pc7.bold(target.project.name)} ${pc7.gray(target.reference)}${target.project.archivedAt ? pc7.gray(" (archived)") : ""}`
|
|
7055
7816
|
);
|
|
7056
7817
|
}
|
|
7057
7818
|
});
|
|
@@ -7104,8 +7865,8 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7104
7865
|
...asRecord2(result.body)
|
|
7105
7866
|
};
|
|
7106
7867
|
if (options.json) return emitJson2(document);
|
|
7107
|
-
console.log(`${
|
|
7108
|
-
console.log(
|
|
7868
|
+
console.log(`${pc7.green("\u2713")} Application ${pc7.bold(options.name.trim())} is ready.`);
|
|
7869
|
+
console.log(pc7.gray(`Its technical selector is --project @${organization.slug}/${slug}.`));
|
|
7109
7870
|
}
|
|
7110
7871
|
);
|
|
7111
7872
|
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) => {
|
|
@@ -7127,8 +7888,8 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7127
7888
|
if (!isProjectItem(project)) {
|
|
7128
7889
|
return emitLocalFailure(false, "invalid_response", "Destination response was invalid.");
|
|
7129
7890
|
}
|
|
7130
|
-
console.log(`${
|
|
7131
|
-
console.log(project.archivedAt ?
|
|
7891
|
+
console.log(`${pc7.bold(project.name)} ${pc7.gray(target)}`);
|
|
7892
|
+
console.log(project.archivedAt ? pc7.yellow("Archived") : pc7.green("Active"));
|
|
7132
7893
|
});
|
|
7133
7894
|
for (const archived of [true, false]) {
|
|
7134
7895
|
const verb = archived ? "archive" : "unarchive";
|
|
@@ -7155,7 +7916,7 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7155
7916
|
...asRecord2(result.body)
|
|
7156
7917
|
};
|
|
7157
7918
|
if (options.json) return emitJson2(document);
|
|
7158
|
-
console.log(`${
|
|
7919
|
+
console.log(`${pc7.green("\u2713")} Application ${archived ? "archived" : "unarchived"}.`);
|
|
7159
7920
|
});
|
|
7160
7921
|
}
|
|
7161
7922
|
}
|
|
@@ -7266,7 +8027,7 @@ function emitLocalFailure(json2, code, message, details = {}) {
|
|
|
7266
8027
|
console.error(message);
|
|
7267
8028
|
emitJson2({ schemaVersion: 1, ok: false, error: { code, ...details } });
|
|
7268
8029
|
} else {
|
|
7269
|
-
console.error(`${
|
|
8030
|
+
console.error(`${pc7.red("\u2715")} ${message}`);
|
|
7270
8031
|
}
|
|
7271
8032
|
process.exitCode = 1;
|
|
7272
8033
|
}
|
|
@@ -7301,7 +8062,7 @@ function safeMessage2(error) {
|
|
|
7301
8062
|
}
|
|
7302
8063
|
|
|
7303
8064
|
// src/renderer-artifact-command.ts
|
|
7304
|
-
import { resolve as
|
|
8065
|
+
import { resolve as resolve9 } from "path";
|
|
7305
8066
|
import { auditTileflowMapThemeValues } from "@tileflow/core";
|
|
7306
8067
|
import {
|
|
7307
8068
|
createTileflowNativeDiagnostic,
|
|
@@ -7389,7 +8150,7 @@ async function runRendererArtifactCommand(command2, options) {
|
|
|
7389
8150
|
if (artifacts.nativeBuild)
|
|
7390
8151
|
console.log("Profile: native-v1 (static artifacts; no device qualification).");
|
|
7391
8152
|
if (command2 === "build")
|
|
7392
|
-
console.log(`Output: ${
|
|
8153
|
+
console.log(`Output: ${resolve9(outDir, ...renderer === "native" ? ["native"] : [])}`);
|
|
7393
8154
|
}
|
|
7394
8155
|
} catch (error) {
|
|
7395
8156
|
const document = createTileflowCommandFailureDocument6(command2, error, process.cwd(), {
|
|
@@ -7410,15 +8171,15 @@ async function runRendererArtifactCommand(command2, options) {
|
|
|
7410
8171
|
}
|
|
7411
8172
|
|
|
7412
8173
|
// src/tileset-command.ts
|
|
7413
|
-
import
|
|
7414
|
-
import { z as
|
|
8174
|
+
import pc8 from "picocolors";
|
|
8175
|
+
import { z as z7 } from "zod";
|
|
7415
8176
|
import { serializeCanonicalJson as serializeCanonicalJson6 } from "@tileflow/core";
|
|
7416
8177
|
import { inspectTileflowPmtiles } from "@tileflow/dev/tilesets";
|
|
7417
8178
|
|
|
7418
8179
|
// src/tileset-upload.ts
|
|
7419
8180
|
import { createHash as createHash3 } from "crypto";
|
|
7420
8181
|
import { open as open5, stat } from "fs/promises";
|
|
7421
|
-
import { z as
|
|
8182
|
+
import { z as z6 } from "zod";
|
|
7422
8183
|
import { serializeCanonicalJson as serializeCanonicalJson5 } from "@tileflow/core";
|
|
7423
8184
|
var teamTilesetPartSize = 16777216;
|
|
7424
8185
|
var maximumTeamTilesetBytes = 4e9;
|
|
@@ -7979,36 +8740,36 @@ function asRecord3(value) {
|
|
|
7979
8740
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
7980
8741
|
}
|
|
7981
8742
|
function defaultSleep(milliseconds) {
|
|
7982
|
-
return new Promise((
|
|
7983
|
-
}
|
|
7984
|
-
var identifierSchema =
|
|
7985
|
-
var uploadPartStatusSchema =
|
|
7986
|
-
byteCount:
|
|
7987
|
-
partNumber:
|
|
7988
|
-
uploaded:
|
|
7989
|
-
validationState:
|
|
8743
|
+
return new Promise((resolve13) => setTimeout(resolve13, milliseconds));
|
|
8744
|
+
}
|
|
8745
|
+
var identifierSchema = z6.string().min(20).max(80).regex(/^[A-Za-z0-9_-]+$/u);
|
|
8746
|
+
var uploadPartStatusSchema = z6.object({
|
|
8747
|
+
byteCount: z6.number().int().min(1).max(teamTilesetPartSize),
|
|
8748
|
+
partNumber: z6.number().int().min(1).max(239),
|
|
8749
|
+
uploaded: z6.boolean(),
|
|
8750
|
+
validationState: z6.enum(["mismatch", "pending", "processing", "valid"])
|
|
7990
8751
|
}).strict();
|
|
7991
|
-
var publishedVersionSchema =
|
|
7992
|
-
byteCount:
|
|
7993
|
-
contentHash:
|
|
7994
|
-
contentHashAlgorithm:
|
|
7995
|
-
publishedVersion:
|
|
7996
|
-
schemaVersion:
|
|
7997
|
-
state:
|
|
8752
|
+
var publishedVersionSchema = z6.object({
|
|
8753
|
+
byteCount: z6.number().int().min(1).max(maximumTeamTilesetBytes),
|
|
8754
|
+
contentHash: z6.string().regex(/^[0-9a-f]{64}$/u),
|
|
8755
|
+
contentHashAlgorithm: z6.literal("sha256-fixed-parts-v1"),
|
|
8756
|
+
publishedVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
8757
|
+
schemaVersion: z6.literal(1),
|
|
8758
|
+
state: z6.literal("published"),
|
|
7998
8759
|
versionId: identifierSchema.regex(/^tsv_[A-Za-z0-9_-]+$/u)
|
|
7999
8760
|
}).strict();
|
|
8000
|
-
var uploadControlSchema =
|
|
8001
|
-
byteCount:
|
|
8002
|
-
contentHash:
|
|
8003
|
-
contentHashAlgorithm:
|
|
8004
|
-
expiresAt:
|
|
8005
|
-
failure:
|
|
8006
|
-
partCount:
|
|
8007
|
-
parts:
|
|
8008
|
-
partSize:
|
|
8009
|
-
publishedVersion:
|
|
8010
|
-
schemaVersion:
|
|
8011
|
-
state:
|
|
8761
|
+
var uploadControlSchema = z6.object({
|
|
8762
|
+
byteCount: z6.number().int().min(1).max(maximumTeamTilesetBytes),
|
|
8763
|
+
contentHash: z6.string().regex(/^[0-9a-f]{64}$/u),
|
|
8764
|
+
contentHashAlgorithm: z6.literal("sha256-fixed-parts-v1"),
|
|
8765
|
+
expiresAt: z6.iso.datetime({ offset: true }),
|
|
8766
|
+
failure: z6.object({ code: z6.string().regex(/^TF_[A-Z0-9_]{1,96}$/u) }).strict().nullable(),
|
|
8767
|
+
partCount: z6.number().int().min(1).max(239),
|
|
8768
|
+
parts: z6.array(uploadPartStatusSchema).max(239),
|
|
8769
|
+
partSize: z6.literal(teamTilesetPartSize),
|
|
8770
|
+
publishedVersion: z6.null(),
|
|
8771
|
+
schemaVersion: z6.literal(1),
|
|
8772
|
+
state: z6.enum([
|
|
8012
8773
|
"aborted",
|
|
8013
8774
|
"aborting",
|
|
8014
8775
|
"expired",
|
|
@@ -8021,32 +8782,32 @@ var uploadControlSchema = z5.object({
|
|
|
8021
8782
|
"uploading",
|
|
8022
8783
|
"validating"
|
|
8023
8784
|
]),
|
|
8024
|
-
uploadId:
|
|
8025
|
-
versionId:
|
|
8785
|
+
uploadId: z6.string().min(20).max(80).regex(/^tsu_[A-Za-z0-9_-]+$/u),
|
|
8786
|
+
versionId: z6.null()
|
|
8026
8787
|
}).strict();
|
|
8027
|
-
var uploadSessionSchema =
|
|
8028
|
-
var authorizationSchema =
|
|
8029
|
-
expiresAt:
|
|
8030
|
-
headers:
|
|
8031
|
-
"Content-Length":
|
|
8032
|
-
"Content-MD5":
|
|
8788
|
+
var uploadSessionSchema = z6.union([publishedVersionSchema, uploadControlSchema]);
|
|
8789
|
+
var authorizationSchema = z6.object({
|
|
8790
|
+
expiresAt: z6.iso.datetime({ offset: true }),
|
|
8791
|
+
headers: z6.object({
|
|
8792
|
+
"Content-Length": z6.string().regex(/^[1-9][0-9]{0,9}$/u),
|
|
8793
|
+
"Content-MD5": z6.string().regex(/^[A-Za-z0-9+/]{22}==$/u)
|
|
8033
8794
|
}).strict(),
|
|
8034
|
-
method:
|
|
8035
|
-
partNumber:
|
|
8036
|
-
url:
|
|
8795
|
+
method: z6.literal("PUT"),
|
|
8796
|
+
partNumber: z6.number().int().min(1).max(239),
|
|
8797
|
+
url: z6.url().max(8192).refine((value) => {
|
|
8037
8798
|
const url = new URL(value);
|
|
8038
8799
|
return url.protocol === "https:" && !url.username && !url.password && !url.hash;
|
|
8039
8800
|
})
|
|
8040
8801
|
}).strict();
|
|
8041
|
-
var authorizationResponseSchema =
|
|
8042
|
-
authorizations:
|
|
8043
|
-
schemaVersion:
|
|
8802
|
+
var authorizationResponseSchema = z6.object({
|
|
8803
|
+
authorizations: z6.array(authorizationSchema).min(1).max(16),
|
|
8804
|
+
schemaVersion: z6.literal(1)
|
|
8044
8805
|
}).strict();
|
|
8045
|
-
var partReceiptResponseSchema =
|
|
8046
|
-
changed:
|
|
8047
|
-
etag:
|
|
8048
|
-
partNumber:
|
|
8049
|
-
schemaVersion:
|
|
8806
|
+
var partReceiptResponseSchema = z6.object({
|
|
8807
|
+
changed: z6.boolean(),
|
|
8808
|
+
etag: z6.string().regex(/^[0-9a-f]{32}$/u),
|
|
8809
|
+
partNumber: z6.number().int().min(1).max(239),
|
|
8810
|
+
schemaVersion: z6.literal(1)
|
|
8050
8811
|
}).strict();
|
|
8051
8812
|
|
|
8052
8813
|
// src/tileset-command.ts
|
|
@@ -8069,7 +8830,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
8069
8830
|
`);
|
|
8070
8831
|
return;
|
|
8071
8832
|
}
|
|
8072
|
-
console.log(`${
|
|
8833
|
+
console.log(`${pc8.green("\u2713")} PMTiles archive is valid.`);
|
|
8073
8834
|
console.log(` Type: ${inspection.contract.tileType}`);
|
|
8074
8835
|
console.log(` Zooms: ${inspection.contract.minzoom}-${inspection.contract.maxzoom}`);
|
|
8075
8836
|
console.log(` Source layers: ${inspection.contract.sourceLayers.length}`);
|
|
@@ -8079,7 +8840,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
8079
8840
|
);
|
|
8080
8841
|
}
|
|
8081
8842
|
for (const warning of inspection.warnings) {
|
|
8082
|
-
console.log(`${
|
|
8843
|
+
console.log(`${pc8.yellow("!")} ${warning.message}`);
|
|
8083
8844
|
}
|
|
8084
8845
|
} catch (error) {
|
|
8085
8846
|
const record = asRecord4(error);
|
|
@@ -8092,7 +8853,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
8092
8853
|
`
|
|
8093
8854
|
);
|
|
8094
8855
|
} else {
|
|
8095
|
-
console.error(`${
|
|
8856
|
+
console.error(`${pc8.red("Error:")} ${message}`);
|
|
8096
8857
|
}
|
|
8097
8858
|
process.exitCode = 1;
|
|
8098
8859
|
}
|
|
@@ -8180,7 +8941,7 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8180
8941
|
};
|
|
8181
8942
|
if (options.json) return emitJson(document);
|
|
8182
8943
|
console.log(
|
|
8183
|
-
`${
|
|
8944
|
+
`${pc8.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc8.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
|
|
8184
8945
|
);
|
|
8185
8946
|
} catch (error) {
|
|
8186
8947
|
if (error instanceof TeamTilesetUploadError) {
|
|
@@ -8200,9 +8961,9 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8200
8961
|
const items = await fetchAllTeamTilesets(authority);
|
|
8201
8962
|
const document = { command: "tileset list", schemaVersion: 1, tilesets: items };
|
|
8202
8963
|
if (options.json) return emitJson(document);
|
|
8203
|
-
if (items.length === 0) return console.log(
|
|
8964
|
+
if (items.length === 0) return console.log(pc8.gray("No Team tilesets."));
|
|
8204
8965
|
for (const item of items) {
|
|
8205
|
-
console.log(` ${
|
|
8966
|
+
console.log(` ${pc8.bold(item.logicalName)} ${pc8.gray(item.status)}`);
|
|
8206
8967
|
}
|
|
8207
8968
|
} catch {
|
|
8208
8969
|
emitFailure(options.json, "tileset_list_failed", "Tileset list failed.");
|
|
@@ -8230,7 +8991,7 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8230
8991
|
const document = { command: "tileset status", schemaVersion: 1, tileset: parsed.tileset };
|
|
8231
8992
|
if (options.json) return emitJson(document);
|
|
8232
8993
|
const item = parsed.tileset;
|
|
8233
|
-
console.log(`${
|
|
8994
|
+
console.log(`${pc8.bold(item.name)} ${pc8.gray(item.state)}`);
|
|
8234
8995
|
console.log(` ID: ${item.id}`);
|
|
8235
8996
|
console.log(` Current version: ${String(item.currentVersion ?? "unpublished")}`);
|
|
8236
8997
|
} catch {
|
|
@@ -8281,7 +9042,7 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8281
9042
|
const parsed = tilesetPurgeResponseSchema.parse(response.body);
|
|
8282
9043
|
const document = { command: "tileset purge", schemaVersion: 1, ...parsed };
|
|
8283
9044
|
if (options.json) return emitJson(document);
|
|
8284
|
-
console.log(`${
|
|
9045
|
+
console.log(`${pc8.green("\u2713")} Purge ${parsed.reclamation}.`);
|
|
8285
9046
|
} catch {
|
|
8286
9047
|
emitFailure(options.json, "tileset_purge_failed", "Tileset purge failed.");
|
|
8287
9048
|
}
|
|
@@ -8343,69 +9104,69 @@ function inspectionOptionError(message) {
|
|
|
8343
9104
|
function asRecord4(value) {
|
|
8344
9105
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
8345
9106
|
}
|
|
8346
|
-
var safeIdentifierSchema2 =
|
|
8347
|
-
var physicalTilesetIdSchema =
|
|
8348
|
-
var cursorSchema2 =
|
|
8349
|
-
var tilesetInventoryItemSchema =
|
|
8350
|
-
currentVersion:
|
|
8351
|
-
hasCurrent:
|
|
9107
|
+
var safeIdentifierSchema2 = z7.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
|
|
9108
|
+
var physicalTilesetIdSchema = z7.string().min(20).max(80).regex(/^tls_[A-Za-z0-9_-]+$/u);
|
|
9109
|
+
var cursorSchema2 = z7.string().min(1).max(2048).nullable();
|
|
9110
|
+
var tilesetInventoryItemSchema = z7.object({
|
|
9111
|
+
currentVersion: z7.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
|
|
9112
|
+
hasCurrent: z7.boolean(),
|
|
8352
9113
|
id: physicalTilesetIdSchema,
|
|
8353
|
-
logicalName:
|
|
8354
|
-
name:
|
|
8355
|
-
retainedBytes:
|
|
8356
|
-
retainedDeploymentCount:
|
|
8357
|
-
status:
|
|
8358
|
-
updatedAt:
|
|
8359
|
-
usedByMapCount:
|
|
9114
|
+
logicalName: z7.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
9115
|
+
name: z7.string().min(1).max(200),
|
|
9116
|
+
retainedBytes: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
9117
|
+
retainedDeploymentCount: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
9118
|
+
status: z7.enum(["active", "purging", "retired"]),
|
|
9119
|
+
updatedAt: z7.iso.datetime({ offset: true }),
|
|
9120
|
+
usedByMapCount: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER)
|
|
8360
9121
|
}).strict();
|
|
8361
|
-
var tilesetListResponseSchema =
|
|
9122
|
+
var tilesetListResponseSchema = z7.object({
|
|
8362
9123
|
nextCursor: cursorSchema2.optional(),
|
|
8363
|
-
schemaVersion:
|
|
8364
|
-
tilesets:
|
|
9124
|
+
schemaVersion: z7.literal(1),
|
|
9125
|
+
tilesets: z7.array(tilesetInventoryItemSchema).max(100)
|
|
8365
9126
|
}).strict();
|
|
8366
|
-
var tilesetDependencySchema =
|
|
8367
|
-
active:
|
|
9127
|
+
var tilesetDependencySchema = z7.object({
|
|
9128
|
+
active: z7.boolean(),
|
|
8368
9129
|
deploymentId: safeIdentifierSchema2,
|
|
8369
|
-
deploymentVersion:
|
|
8370
|
-
environment:
|
|
9130
|
+
deploymentVersion: z7.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
9131
|
+
environment: z7.string().min(1).max(64),
|
|
8371
9132
|
mapId: safeIdentifierSchema2,
|
|
8372
9133
|
sourceId: safeIdentifierSchema2
|
|
8373
9134
|
}).strict();
|
|
8374
|
-
var tilesetDetailResponseSchema =
|
|
8375
|
-
schemaVersion:
|
|
8376
|
-
tileset:
|
|
8377
|
-
attribution:
|
|
8378
|
-
currentVersion:
|
|
8379
|
-
dependencies:
|
|
9135
|
+
var tilesetDetailResponseSchema = z7.object({
|
|
9136
|
+
schemaVersion: z7.literal(1),
|
|
9137
|
+
tileset: z7.object({
|
|
9138
|
+
attribution: z7.string().max(4096).nullable(),
|
|
9139
|
+
currentVersion: z7.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
|
|
9140
|
+
dependencies: z7.array(tilesetDependencySchema).max(25),
|
|
8380
9141
|
dependencyNextCursor: cursorSchema2,
|
|
8381
9142
|
id: physicalTilesetIdSchema,
|
|
8382
|
-
name:
|
|
8383
|
-
slug:
|
|
8384
|
-
state:
|
|
9143
|
+
name: z7.string().min(1).max(200),
|
|
9144
|
+
slug: z7.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
9145
|
+
state: z7.enum(["active", "purging", "purged"])
|
|
8385
9146
|
}).strict()
|
|
8386
9147
|
}).strict();
|
|
8387
|
-
var purgeDependencySchema =
|
|
9148
|
+
var purgeDependencySchema = z7.object({
|
|
8388
9149
|
deploymentId: safeIdentifierSchema2,
|
|
8389
9150
|
mapId: safeIdentifierSchema2,
|
|
8390
9151
|
sourceId: safeIdentifierSchema2
|
|
8391
9152
|
}).strict();
|
|
8392
|
-
var tilesetInUseResponseSchema =
|
|
8393
|
-
code:
|
|
8394
|
-
dependencies:
|
|
8395
|
-
error:
|
|
9153
|
+
var tilesetInUseResponseSchema = z7.object({
|
|
9154
|
+
code: z7.literal("TF_TILESET_IN_USE"),
|
|
9155
|
+
dependencies: z7.array(purgeDependencySchema).max(32),
|
|
9156
|
+
error: z7.literal("Tileset is used by retained Map deployments"),
|
|
8396
9157
|
nextCursor: cursorSchema2
|
|
8397
9158
|
}).strict();
|
|
8398
|
-
var tilesetPurgeResponseSchema =
|
|
8399
|
-
deliveryFence:
|
|
8400
|
-
pendingVersions:
|
|
8401
|
-
reclamation:
|
|
8402
|
-
state:
|
|
9159
|
+
var tilesetPurgeResponseSchema = z7.object({
|
|
9160
|
+
deliveryFence: z7.enum(["complete", "pending"]),
|
|
9161
|
+
pendingVersions: z7.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
9162
|
+
reclamation: z7.enum(["complete", "pending"]),
|
|
9163
|
+
state: z7.enum(["purged", "purging"]),
|
|
8403
9164
|
tilesetId: physicalTilesetIdSchema
|
|
8404
9165
|
}).strict();
|
|
8405
9166
|
|
|
8406
9167
|
// src/visual-command.ts
|
|
8407
|
-
import { lstat as
|
|
8408
|
-
import { dirname as
|
|
9168
|
+
import { lstat as lstat6, readFile as readFile6 } from "fs/promises";
|
|
9169
|
+
import { dirname as dirname12, isAbsolute as isAbsolute4, parse as parse2, relative as relative5, resolve as resolve10, sep as sep4 } from "path";
|
|
8409
9170
|
import {
|
|
8410
9171
|
analyzeTileflowCaptureReference,
|
|
8411
9172
|
compareTileflowCaptureToBaseline,
|
|
@@ -8490,14 +9251,14 @@ async function runVisualAnalyze(scene, options) {
|
|
|
8490
9251
|
validateVisualSelection([scene]);
|
|
8491
9252
|
const region = options.region === void 0 ? void 0 : parseTileflowVisualRegion(options.region);
|
|
8492
9253
|
const cwd = process.cwd();
|
|
8493
|
-
const referencePath =
|
|
9254
|
+
const referencePath = resolve10(cwd, options.reference);
|
|
8494
9255
|
const referencePng = await readVisualReference(referencePath);
|
|
8495
9256
|
const referenceDimensions = validateTileflowVisualReferencePng(referencePng);
|
|
8496
9257
|
if (region) assertTileflowVisualRegionFits(region, referenceDimensions, "reference PNG");
|
|
8497
9258
|
const outputDirectory = await resolveVisualDirectory(cwd, options.outputDir, "output");
|
|
8498
|
-
const actualPath =
|
|
8499
|
-
const managedDiffPath =
|
|
8500
|
-
const reportPath =
|
|
9259
|
+
const actualPath = resolve10(outputDirectory.path, `${scene}.actual.png`);
|
|
9260
|
+
const managedDiffPath = resolve10(outputDirectory.path, `${scene}.diff.png`);
|
|
9261
|
+
const reportPath = resolve10(outputDirectory.path, `${scene}.analysis.json`);
|
|
8501
9262
|
assertReferenceIsReadOnly(referencePath, [actualPath, managedDiffPath, reportPath]);
|
|
8502
9263
|
const controller = installSignalAbortController();
|
|
8503
9264
|
const session = createTileflowCaptureSession3({
|
|
@@ -8586,15 +9347,15 @@ async function runVisualDiff(scenes, options) {
|
|
|
8586
9347
|
capture,
|
|
8587
9348
|
files.kind === "complete" ? files.baseline : void 0
|
|
8588
9349
|
);
|
|
8589
|
-
const actualPath =
|
|
8590
|
-
const diffPath = comparison.diffPng ?
|
|
9350
|
+
const actualPath = resolve10(outputDirectory.path, `${capture.scene}.actual.png`);
|
|
9351
|
+
const diffPath = comparison.diffPng ? resolve10(outputDirectory.path, `${capture.scene}.diff.png`) : void 0;
|
|
8591
9352
|
prepared.push({
|
|
8592
9353
|
capture,
|
|
8593
9354
|
comparison,
|
|
8594
9355
|
baselinePath: files.pngPath,
|
|
8595
9356
|
actualPath,
|
|
8596
9357
|
diffPath,
|
|
8597
|
-
reportPath:
|
|
9358
|
+
reportPath: resolve10(outputDirectory.path, `${capture.scene}.visual.json`)
|
|
8598
9359
|
});
|
|
8599
9360
|
}
|
|
8600
9361
|
const document = createTileflowVisualDiffJson(prepared, cwd);
|
|
@@ -8614,7 +9375,7 @@ async function runVisualDiff(scenes, options) {
|
|
|
8614
9375
|
}
|
|
8615
9376
|
]),
|
|
8616
9377
|
removePaths: prepared.flatMap(
|
|
8617
|
-
(item) => item.diffPath ? [] : [
|
|
9378
|
+
(item) => item.diffPath ? [] : [resolve10(outputDirectory.path, `${item.capture.scene}.diff.png`)]
|
|
8618
9379
|
)
|
|
8619
9380
|
});
|
|
8620
9381
|
if (options.json) {
|
|
@@ -8735,7 +9496,7 @@ async function resolvePreviousBaseline(capture, files) {
|
|
|
8735
9496
|
}
|
|
8736
9497
|
}
|
|
8737
9498
|
async function readBaselineFiles(directory, scene) {
|
|
8738
|
-
const pngPath =
|
|
9499
|
+
const pngPath = resolve10(directory.path, `${scene}.png`);
|
|
8739
9500
|
const receiptPath = captureReceiptPath(pngPath);
|
|
8740
9501
|
const [png, receipt] = await Promise.all([
|
|
8741
9502
|
readOptionalArtifact(pngPath, directory.boundary, tileflowVisualArtifactLimits2.maximumPngBytes),
|
|
@@ -8750,10 +9511,10 @@ async function readBaselineFiles(directory, scene) {
|
|
|
8750
9511
|
return { kind: "complete", pngPath, receiptPath, baseline: { png, receipt } };
|
|
8751
9512
|
}
|
|
8752
9513
|
async function readOptionalArtifact(path, boundary, maximumBytes) {
|
|
8753
|
-
await assertNoSymlinkComponents(
|
|
9514
|
+
await assertNoSymlinkComponents(dirname12(path), boundary);
|
|
8754
9515
|
let info;
|
|
8755
9516
|
try {
|
|
8756
|
-
info = await
|
|
9517
|
+
info = await lstat6(path);
|
|
8757
9518
|
} catch (error) {
|
|
8758
9519
|
if (isMissingFile5(error)) return void 0;
|
|
8759
9520
|
throw error;
|
|
@@ -8773,7 +9534,7 @@ async function readOptionalArtifact(path, boundary, maximumBytes) {
|
|
|
8773
9534
|
async function readVisualReference(path) {
|
|
8774
9535
|
let info;
|
|
8775
9536
|
try {
|
|
8776
|
-
info = await
|
|
9537
|
+
info = await lstat6(path);
|
|
8777
9538
|
} catch (error) {
|
|
8778
9539
|
throw new TileflowCaptureError2("BASELINE_INVALID", "The visual reference PNG cannot be read.", {
|
|
8779
9540
|
cause: error
|
|
@@ -8795,12 +9556,12 @@ async function readVisualReference(path) {
|
|
|
8795
9556
|
return new Uint8Array(await readFile6(path));
|
|
8796
9557
|
}
|
|
8797
9558
|
async function resolveVisualDirectory(cwd, value, kind) {
|
|
8798
|
-
const path =
|
|
9559
|
+
const path = resolve10(cwd, value);
|
|
8799
9560
|
if (path === cwd || path === parse2(path).root) {
|
|
8800
9561
|
throw new Error(`Visual ${kind} directory must be a dedicated subdirectory.`);
|
|
8801
9562
|
}
|
|
8802
9563
|
if (kind === "baseline") {
|
|
8803
|
-
for (const managed of [
|
|
9564
|
+
for (const managed of [resolve10(cwd, ".tileflow/captures"), resolve10(cwd, ".tileflow/diffs")]) {
|
|
8804
9565
|
if (pathsOverlap2(path, managed)) {
|
|
8805
9566
|
throw new Error("Visual baselines must not overlap Tileflow-managed capture/diff outputs.");
|
|
8806
9567
|
}
|
|
@@ -8809,7 +9570,7 @@ async function resolveVisualDirectory(cwd, value, kind) {
|
|
|
8809
9570
|
const boundary = isInside2(cwd, path) ? cwd : path;
|
|
8810
9571
|
await assertNoSymlinkComponents(path, boundary);
|
|
8811
9572
|
try {
|
|
8812
|
-
const info = await
|
|
9573
|
+
const info = await lstat6(path);
|
|
8813
9574
|
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
8814
9575
|
throw new Error(`Visual ${kind} path must be a non-symlink directory.`);
|
|
8815
9576
|
}
|
|
@@ -8836,10 +9597,10 @@ function assertReferenceIsReadOnly(referencePath, outputPaths) {
|
|
|
8836
9597
|
}
|
|
8837
9598
|
}
|
|
8838
9599
|
function portablePathIdentity2(path) {
|
|
8839
|
-
return
|
|
9600
|
+
return resolve10(path).normalize("NFC").toLowerCase();
|
|
8840
9601
|
}
|
|
8841
9602
|
function isInside2(parent, child) {
|
|
8842
|
-
const path =
|
|
9603
|
+
const path = relative5(parent, child);
|
|
8843
9604
|
return path === "" || path !== ".." && !path.startsWith(`..${sep4}`) && !isAbsolute4(path);
|
|
8844
9605
|
}
|
|
8845
9606
|
function isMissingFile5(error) {
|
|
@@ -8848,8 +9609,8 @@ function isMissingFile5(error) {
|
|
|
8848
9609
|
|
|
8849
9610
|
// src/workbench-service.ts
|
|
8850
9611
|
import { opendir, stat as stat2 } from "fs/promises";
|
|
8851
|
-
import { resolve as
|
|
8852
|
-
import { z as
|
|
9612
|
+
import { resolve as resolve11 } from "path";
|
|
9613
|
+
import { z as z8 } from "zod";
|
|
8853
9614
|
import {
|
|
8854
9615
|
compareTileflowCapturesForReview as compareTileflowCapturesForReview3,
|
|
8855
9616
|
createTileflowCaptureSession as createTileflowCaptureSession4,
|
|
@@ -8862,22 +9623,22 @@ import {
|
|
|
8862
9623
|
} from "@tileflow/core";
|
|
8863
9624
|
import { resolveTileflowPreview } from "@tileflow/dev/preview";
|
|
8864
9625
|
var prefix = "/__workbench";
|
|
8865
|
-
var captureInput =
|
|
8866
|
-
side:
|
|
8867
|
-
generation:
|
|
9626
|
+
var captureInput = z8.object({
|
|
9627
|
+
side: z8.enum(["left", "right"]).default("left"),
|
|
9628
|
+
generation: z8.number().int().nonnegative(),
|
|
8868
9629
|
name: tileflowCaptureSceneNameSchema2,
|
|
8869
9630
|
definition: tileflowCaptureSceneSchema3
|
|
8870
9631
|
}).strict();
|
|
8871
|
-
var compareInput =
|
|
8872
|
-
var deleteInput =
|
|
8873
|
-
var contextInput =
|
|
8874
|
-
client:
|
|
8875
|
-
side:
|
|
8876
|
-
generation:
|
|
8877
|
-
context:
|
|
9632
|
+
var compareInput = z8.object({ before: z8.string().regex(/^[a-f0-9]{64}$/u), after: z8.string().regex(/^[a-f0-9]{64}$/u) }).strict();
|
|
9633
|
+
var deleteInput = z8.object({ id: z8.string().regex(/^[a-f0-9]{64}$/u) }).strict();
|
|
9634
|
+
var contextInput = z8.object({
|
|
9635
|
+
client: z8.string().regex(/^[a-zA-Z0-9-]{1,80}$/u),
|
|
9636
|
+
side: z8.enum(["left", "right"]),
|
|
9637
|
+
generation: z8.number().int(),
|
|
9638
|
+
context: z8.record(z8.string(), z8.unknown())
|
|
8878
9639
|
}).strict();
|
|
8879
9640
|
function createWorkbenchService(options) {
|
|
8880
|
-
const directory =
|
|
9641
|
+
const directory = resolve11(options.cwd, captureRecordDirectory);
|
|
8881
9642
|
const contexts = /* @__PURE__ */ new Map();
|
|
8882
9643
|
let captureSession = options.captureSession;
|
|
8883
9644
|
let pending;
|
|
@@ -8949,7 +9710,7 @@ function createWorkbenchService(options) {
|
|
|
8949
9710
|
break;
|
|
8950
9711
|
}
|
|
8951
9712
|
if (!entry.isFile() || !/^[a-f0-9]{64}\.json$/u.test(entry.name)) continue;
|
|
8952
|
-
const info = await stat2(
|
|
9713
|
+
const info = await stat2(resolve11(directory, entry.name));
|
|
8953
9714
|
entries.push({ id: entry.name.slice(0, -5), modified: info.mtimeMs });
|
|
8954
9715
|
}
|
|
8955
9716
|
} catch (error) {
|
|
@@ -9088,7 +9849,7 @@ function createWorkbenchService(options) {
|
|
|
9088
9849
|
});
|
|
9089
9850
|
}
|
|
9090
9851
|
if (path === "/captures") {
|
|
9091
|
-
const offset =
|
|
9852
|
+
const offset = z8.coerce.number().int().min(0).max(5e3).parse(url.searchParams.get("offset") ?? 0);
|
|
9092
9853
|
return json(await history(offset));
|
|
9093
9854
|
}
|
|
9094
9855
|
const image = /^\/captures\/([a-f0-9]{64})\.png$/u.exec(path);
|
|
@@ -9219,17 +9980,18 @@ var packageJson = JSON.parse(
|
|
|
9219
9980
|
var program = new Command();
|
|
9220
9981
|
var defaultApiUrl = defaultTileflowApiUrl;
|
|
9221
9982
|
var defaultAppUrl = "https://tileflow.dev";
|
|
9222
|
-
var defaultConfigPath =
|
|
9983
|
+
var defaultConfigPath = defaultTileflowConfigPath2;
|
|
9223
9984
|
var defaultManifestPath = defaultTileflowManifestPath;
|
|
9224
|
-
program.name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(
|
|
9985
|
+
program.name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(pc9.gray("\nRun tileflow <command> --help for usage.")).showSuggestionAfterError();
|
|
9225
9986
|
program.addHelpText("after", "\n -V, --version output the version number\n");
|
|
9226
9987
|
registerDescribeCommand(program);
|
|
9227
9988
|
registerLanguageCommand(program);
|
|
9228
9989
|
registerTilesetCommands(program, { defaultApiUrl, loadAuthConfig });
|
|
9229
9990
|
registerIconSetCommands(program, { defaultApiUrl, loadAuthConfig });
|
|
9991
|
+
registerModelCommands(program, { defaultApiUrl, loadAuthConfig });
|
|
9230
9992
|
program.command("init").description("Create a starter tileflow.config.ts").option("-f, --force", "overwrite an existing config").action(async (options) => {
|
|
9231
|
-
const configPath =
|
|
9232
|
-
const ignorePath =
|
|
9993
|
+
const configPath = resolve12(process.cwd(), defaultConfigPath);
|
|
9994
|
+
const ignorePath = resolve12(process.cwd(), ".gitignore");
|
|
9233
9995
|
if (existsSync(configPath) && !options.force) {
|
|
9234
9996
|
logWarning(`${pathLabel(defaultConfigPath)} already exists.`);
|
|
9235
9997
|
printNextSteps([`Run ${command("tileflow init --force")} to replace it.`]);
|
|
@@ -9245,9 +10007,9 @@ program.command("init").description("Create a starter tileflow.config.ts").optio
|
|
|
9245
10007
|
}
|
|
9246
10008
|
logSuccess(`Created ${pathLabel(defaultConfigPath)}.`);
|
|
9247
10009
|
printKeyValue("Path", pathLabel(configPath));
|
|
9248
|
-
printKeyValue("Map",
|
|
10010
|
+
printKeyValue("Map", pc9.bold("madrid"));
|
|
9249
10011
|
printNextSteps([
|
|
9250
|
-
`Install ${
|
|
10012
|
+
`Install ${pc9.cyan("@tileflow/core")} and ${pc9.cyan("@tileflow/maps")} in this project if needed.`,
|
|
9251
10013
|
`Preview locally with ${command("tileflow preview")}.`,
|
|
9252
10014
|
`Validate the config with ${command("tileflow validate")}.`
|
|
9253
10015
|
]);
|
|
@@ -9383,12 +10145,12 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
9383
10145
|
try {
|
|
9384
10146
|
if (!options.json) logInfo(`Validating ${pathLabel(options.config)}.`);
|
|
9385
10147
|
const loaded = await withTileflowConfigSecretsHidden(
|
|
9386
|
-
() =>
|
|
10148
|
+
() => loadTileflowConfigWithInputs5(options.config)
|
|
9387
10149
|
);
|
|
9388
10150
|
const project = loaded.project;
|
|
9389
|
-
const baseDirectory =
|
|
10151
|
+
const baseDirectory = dirname13(loaded.configFile);
|
|
9390
10152
|
failureDefaults = { code: "CONFIG_INVALID", phase: "config-validation" };
|
|
9391
|
-
|
|
10153
|
+
assertValidTileflowConfig4(project);
|
|
9392
10154
|
const mapNames = getTileflowMapNames5(project).sort();
|
|
9393
10155
|
failureDefaults = { code: "ICON_COMPILATION_FAILED", phase: "icon-compilation" };
|
|
9394
10156
|
const compiledIcons = await compileTileflowIconPackages3(project, {
|
|
@@ -9406,7 +10168,7 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
9406
10168
|
(binding) => options.target === "hosted" ? `${normalizeUrl(options.apiBaseUrl)}/sprites/preflight/${binding.packageHash}/sprite` : `/tileflow/icons/${binding.mapName}/sprite`,
|
|
9407
10169
|
mapAssetsWithBuildingModels(
|
|
9408
10170
|
modelAssets,
|
|
9409
|
-
|
|
10171
|
+
options.target === "hosted" ? { hosted: true } : { localBase: (mapName) => `/tileflow/models/${mapName}` }
|
|
9410
10172
|
)
|
|
9411
10173
|
);
|
|
9412
10174
|
failureDefaults = { code: "STYLE_INVALID", phase: "style-validation" };
|
|
@@ -9526,7 +10288,7 @@ program.command("build").description("Generate static Tileflow styles").option("
|
|
|
9526
10288
|
})
|
|
9527
10289
|
);
|
|
9528
10290
|
logSuccess("Built Tileflow artifacts.");
|
|
9529
|
-
printKeyValue("Output", pathLabel(
|
|
10291
|
+
printKeyValue("Output", pathLabel(resolve12(process.cwd(), options.out)));
|
|
9530
10292
|
}
|
|
9531
10293
|
);
|
|
9532
10294
|
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(
|
|
@@ -9814,12 +10576,17 @@ program.command("preview").description("Open Map DevTools with inspection and op
|
|
|
9814
10576
|
});
|
|
9815
10577
|
}
|
|
9816
10578
|
);
|
|
9817
|
-
|
|
10579
|
+
addMapSetupOptions(
|
|
10580
|
+
program.command("deploy").description(
|
|
10581
|
+
"Deploy maps to Tileflow and write the frontend manifest; without --map-id, create the Map first"
|
|
10582
|
+
)
|
|
10583
|
+
).option("-c, --config <path>", "config path", defaultConfigPath).option("--manifest <path>", "manifest path written for frontend bundlers", defaultManifestPath).option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).option("--map-id <id>", "managed Map destination; omit it to create a new Map").option("--with-native", "publish web and native-v1 together for the selected managed Map").option("--map <name>", "configured map to connect when the repository contains multiple maps").option(
|
|
9818
10584
|
"--overwrite-self-hosted-manifest",
|
|
9819
10585
|
"explicitly replace an existing self-hosted manifest at --manifest"
|
|
9820
10586
|
).option("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").action(
|
|
9821
10587
|
async (options) => {
|
|
9822
10588
|
const source = resolveDeploySource(process.env);
|
|
10589
|
+
if (!validateMapSetupTarget(options, source)) return;
|
|
9823
10590
|
const resolveApi = () => requireApiOptions(options, {
|
|
9824
10591
|
allowStoredCredential: allowsStoredDeployCredential(source),
|
|
9825
10592
|
assertExplicitKeyMap: true,
|
|
@@ -9839,7 +10606,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
9839
10606
|
);
|
|
9840
10607
|
if (result) {
|
|
9841
10608
|
logSuccess(
|
|
9842
|
-
`${result.changed ? "Published" : "Unchanged"} ${
|
|
10609
|
+
`${result.changed ? "Published" : "Unchanged"} ${pc9.bold(result.mapName)} web + native-v1 (v${result.version}).`
|
|
9843
10610
|
);
|
|
9844
10611
|
printKeyValue("Manifest", pathLabel(result.manifestPath));
|
|
9845
10612
|
printKeyValue("Native", link(result.nativeManifestUrl));
|
|
@@ -9848,11 +10615,11 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
9848
10615
|
}
|
|
9849
10616
|
logInfo(`Deploying ${pathLabel(options.config)}.`);
|
|
9850
10617
|
const loaded = await withTileflowConfigSecretsHidden(
|
|
9851
|
-
() =>
|
|
10618
|
+
() => loadTileflowConfigWithInputs5(options.config)
|
|
9852
10619
|
);
|
|
9853
10620
|
const project = loaded.project;
|
|
9854
|
-
const baseDirectory =
|
|
9855
|
-
|
|
10621
|
+
const baseDirectory = dirname13(loaded.configFile);
|
|
10622
|
+
assertValidTileflowConfig4(project);
|
|
9856
10623
|
const configuredMapNames = getTileflowMapNames5(project);
|
|
9857
10624
|
const mapNames = selectManagedDeployMaps(configuredMapNames, options);
|
|
9858
10625
|
if (!mapNames) return;
|
|
@@ -9883,7 +10650,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
9883
10650
|
const preflightMapAssets = createCompiledMapAssets(
|
|
9884
10651
|
compiledIcons,
|
|
9885
10652
|
(binding) => `${apiUrl}/sprites/preflight/${binding.packageHash}/sprite`,
|
|
9886
|
-
mapAssetsWithBuildingModels(modelAssets,
|
|
10653
|
+
mapAssetsWithBuildingModels(modelAssets, { hosted: true })
|
|
9887
10654
|
);
|
|
9888
10655
|
const compiledPreflightStyles = createTileflowStyles2(deploymentProject, {
|
|
9889
10656
|
apiBaseUrl: apiUrl,
|
|
@@ -9934,6 +10701,21 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
9934
10701
|
return;
|
|
9935
10702
|
}
|
|
9936
10703
|
const existingManifest = outputManifest;
|
|
10704
|
+
let createdMapId = null;
|
|
10705
|
+
if (!options.mapId) {
|
|
10706
|
+
const deployedMapId2 = existingManifest?.maps[mapNames[0]]?.mapId;
|
|
10707
|
+
if (deployedMapId2) {
|
|
10708
|
+
logError(`${pc9.bold(mapNames[0])} is already deployed to ${deployedMapId2}.`);
|
|
10709
|
+
printNextSteps([
|
|
10710
|
+
`Deploy it again with ${command(`tileflow deploy --map-id ${deployedMapId2}`)}.`
|
|
10711
|
+
]);
|
|
10712
|
+
process.exitCode = 1;
|
|
10713
|
+
return;
|
|
10714
|
+
}
|
|
10715
|
+
createdMapId = await createMapForDeploy(options, { defaultApiUrl, loadAuthConfig });
|
|
10716
|
+
if (!createdMapId) return;
|
|
10717
|
+
options.mapId = createdMapId;
|
|
10718
|
+
}
|
|
9937
10719
|
const api = await resolveApi();
|
|
9938
10720
|
if (!api) return;
|
|
9939
10721
|
const hostedSpriteByPackageHash = /* @__PURE__ */ new Map();
|
|
@@ -9953,29 +10735,10 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
9953
10735
|
);
|
|
9954
10736
|
const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
|
|
9955
10737
|
logSuccess(
|
|
9956
|
-
`${action} icon package ${
|
|
10738
|
+
`${action} icon package ${pc9.bold(binding?.label ?? "Icons")} (${plural(iconPackage.manifest.iconNames.length, "icon")}, ${formatBytes4(totalBytes)}, ${iconPackage.contentHash.slice(0, 12)}).`
|
|
9957
10739
|
);
|
|
9958
10740
|
hostedSpriteByPackageHash.set(iconPackage.contentHash, uploaded.value.spriteUrl);
|
|
9959
10741
|
}
|
|
9960
|
-
const hostedModelBaseByHash = /* @__PURE__ */ new Map();
|
|
9961
|
-
const uniqueModelBundles = [
|
|
9962
|
-
...new Map(
|
|
9963
|
-
Object.values(modelAssets.bundles).map((bundle) => [bundle.contentHash, bundle])
|
|
9964
|
-
).values()
|
|
9965
|
-
].sort((left, right) => left.contentHash.localeCompare(right.contentHash));
|
|
9966
|
-
for (const bundle of uniqueModelBundles) {
|
|
9967
|
-
const uploaded = await uploadHostedModelBundle(api, bundle);
|
|
9968
|
-
if (!uploaded.ok) {
|
|
9969
|
-
logError(`Model bundle upload failed: ${uploaded.status}.`);
|
|
9970
|
-
process.exitCode = 1;
|
|
9971
|
-
return;
|
|
9972
|
-
}
|
|
9973
|
-
const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
|
|
9974
|
-
logSuccess(
|
|
9975
|
-
`${action} 3D model bundle (${plural(bundle.manifest.models.length, "model")}, ${formatBytes3(uploaded.value.totalBytes)}, ${bundle.contentHash.slice(0, 12)}).`
|
|
9976
|
-
);
|
|
9977
|
-
hostedModelBaseByHash.set(bundle.contentHash, uploaded.value.baseUrl);
|
|
9978
|
-
}
|
|
9979
10742
|
const hostedMapAssets = createCompiledMapAssets(
|
|
9980
10743
|
compiledIcons,
|
|
9981
10744
|
(binding) => {
|
|
@@ -9983,11 +10746,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
9983
10746
|
if (!sprite) throw new Error(`Missing hosted sprite URL for map ${binding.mapName}`);
|
|
9984
10747
|
return sprite;
|
|
9985
10748
|
},
|
|
9986
|
-
mapAssetsWithBuildingModels(modelAssets,
|
|
9987
|
-
const base = hostedModelBaseByHash.get(bundle.contentHash);
|
|
9988
|
-
if (!base) throw new Error(`Missing hosted model bundle URL for map ${mapName}.`);
|
|
9989
|
-
return base;
|
|
9990
|
-
})
|
|
10749
|
+
mapAssetsWithBuildingModels(modelAssets, { hosted: true })
|
|
9991
10750
|
);
|
|
9992
10751
|
const compiledHostedStyles = createTileflowStyles2(deploymentProject, {
|
|
9993
10752
|
apiBaseUrl: api.apiUrl,
|
|
@@ -10028,7 +10787,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10028
10787
|
}
|
|
10029
10788
|
const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
|
|
10030
10789
|
logSuccess(
|
|
10031
|
-
`${action} font bundle (${plural(bundle.manifest.fontFaces.length, "face")}, ${
|
|
10790
|
+
`${action} font bundle (${plural(bundle.manifest.fontFaces.length, "face")}, ${formatBytes4(uploaded.value.totalBytes)}, ${bundle.contentHash.slice(0, 12)}).`
|
|
10032
10791
|
);
|
|
10033
10792
|
hostedFontBaseByHash.set(bundle.contentHash, uploaded.value.baseUrl);
|
|
10034
10793
|
}
|
|
@@ -10078,12 +10837,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10078
10837
|
fileName: `icons/${mapName}/${file.fileName}`,
|
|
10079
10838
|
source: file.source
|
|
10080
10839
|
})),
|
|
10081
|
-
...hostedFonts.bundles[mapName]?.files ?? []
|
|
10082
|
-
...(modelAssets.bundles[mapName]?.files ?? []).map((file) => ({
|
|
10083
|
-
contentType: file.contentType,
|
|
10084
|
-
fileName: `models/${mapName}/${file.fileName}`,
|
|
10085
|
-
source: file.source
|
|
10086
|
-
}))
|
|
10840
|
+
...hostedFonts.bundles[mapName]?.files ?? []
|
|
10087
10841
|
],
|
|
10088
10842
|
lineage: deploymentProject.mapMetadata?.[mapName]?.lineage ?? [
|
|
10089
10843
|
{ id: map.id, mapVersion: map.version }
|
|
@@ -10093,7 +10847,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10093
10847
|
fonts: hostedFonts.sourceIdentities[mapName] ?? [],
|
|
10094
10848
|
icons: compiledIcons.sourceIdentities[mapName] ?? [],
|
|
10095
10849
|
...compiledIcons.compositions[mapName] ? { iconComposition: compiledIcons.compositions[mapName] } : {},
|
|
10096
|
-
...modelAssets
|
|
10850
|
+
...hostedModelSourceIdentities(modelAssets, mapName) ? { models: hostedModelSourceIdentities(modelAssets, mapName) } : {}
|
|
10097
10851
|
},
|
|
10098
10852
|
styles: styles[mapName]
|
|
10099
10853
|
}
|
|
@@ -10106,13 +10860,13 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10106
10860
|
const iconBinding = bindingsByMap.get(mapName);
|
|
10107
10861
|
const iconPackage = iconBinding ? packagesByHash.get(iconBinding.packageHash) : void 0;
|
|
10108
10862
|
const fontBundle = hostedFonts.bundles[mapName];
|
|
10109
|
-
const
|
|
10863
|
+
const modelSources = teamModelSources(modelAssets, mapName);
|
|
10110
10864
|
return {
|
|
10111
10865
|
iconBinding,
|
|
10112
10866
|
iconComposition: compiledIcons.compositions[mapName],
|
|
10113
10867
|
iconPackage,
|
|
10114
10868
|
fontBundle,
|
|
10115
|
-
|
|
10869
|
+
modelSources,
|
|
10116
10870
|
mapName,
|
|
10117
10871
|
teamSources: hostedThemeFamilies[mapName].teamSources,
|
|
10118
10872
|
buildManifest: {
|
|
@@ -10132,7 +10886,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10132
10886
|
iconComposition,
|
|
10133
10887
|
iconPackage,
|
|
10134
10888
|
mapName,
|
|
10135
|
-
|
|
10889
|
+
modelSources,
|
|
10136
10890
|
teamSources,
|
|
10137
10891
|
styles: themeStyles
|
|
10138
10892
|
} = deployment;
|
|
@@ -10143,7 +10897,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10143
10897
|
}
|
|
10144
10898
|
const themeNames = Object.keys(themeStyles).sort();
|
|
10145
10899
|
logInfo(
|
|
10146
|
-
`Deploying compiled map ${
|
|
10900
|
+
`Deploying compiled map ${pc9.bold(mapName)} (${plural(themeNames.length, "theme")}).`
|
|
10147
10901
|
);
|
|
10148
10902
|
const response = await publishHostedStyle(
|
|
10149
10903
|
api,
|
|
@@ -10167,7 +10921,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10167
10921
|
} : {},
|
|
10168
10922
|
...iconComposition ? { iconComposition } : {},
|
|
10169
10923
|
...fontBundle ? { fontBundle: { contentHash: fontBundle.contentHash } } : {},
|
|
10170
|
-
...
|
|
10924
|
+
...modelSources ? { teamModelSources: modelSources } : {},
|
|
10171
10925
|
source
|
|
10172
10926
|
},
|
|
10173
10927
|
`Deploy response for ${mapName}`
|
|
@@ -10225,9 +10979,15 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10225
10979
|
};
|
|
10226
10980
|
const versionLabel = Number.isInteger(body.version) ? ` (v${body.version})` : "";
|
|
10227
10981
|
if (body.changed === false) {
|
|
10228
|
-
logSuccess(`Unchanged ${
|
|
10982
|
+
logSuccess(`Unchanged ${pc9.bold(mapName)}${versionLabel}.`);
|
|
10229
10983
|
} else {
|
|
10230
|
-
logSuccess(`Published ${
|
|
10984
|
+
logSuccess(`Published ${pc9.bold(mapName)}${versionLabel}.`);
|
|
10985
|
+
}
|
|
10986
|
+
for (const teamModel of body.teamModels ?? []) {
|
|
10987
|
+
if (teamModel.sources !== "differ") continue;
|
|
10988
|
+
logWarning(
|
|
10989
|
+
`3D model ${pc9.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.`
|
|
10990
|
+
);
|
|
10231
10991
|
}
|
|
10232
10992
|
}
|
|
10233
10993
|
const manifestPath = await writeDeployManifest(options.manifest, {
|
|
@@ -10243,6 +11003,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10243
11003
|
const deployedMapId = Object.values(deployedMaps)[0]?.mapId;
|
|
10244
11004
|
if (deployedMapId) {
|
|
10245
11005
|
printNextSteps([
|
|
11006
|
+
...createdMapId ? [`Deploy later changes with ${command(`tileflow deploy --map-id ${createdMapId}`)}.`] : [],
|
|
10246
11007
|
`Check hosted state with ${command(`tileflow status --map-id ${deployedMapId}`)}.`
|
|
10247
11008
|
]);
|
|
10248
11009
|
}
|
|
@@ -10343,6 +11104,7 @@ registerProjectCommands(program, {
|
|
|
10343
11104
|
defaultApiUrl,
|
|
10344
11105
|
loadAuthConfig
|
|
10345
11106
|
});
|
|
11107
|
+
registerMapCommands(program, { defaultApiUrl, loadAuthConfig });
|
|
10346
11108
|
if (isRootCliVersionRequest(process.argv.slice(2))) {
|
|
10347
11109
|
process.stdout.write(`${packageJson.version}
|
|
10348
11110
|
`);
|
|
@@ -10589,12 +11351,12 @@ async function runTileflowComparisonPreview(options, address) {
|
|
|
10589
11351
|
}
|
|
10590
11352
|
function comparisonCaptureConfigArgument(configPath) {
|
|
10591
11353
|
const cwd = realpathSync(process.cwd());
|
|
10592
|
-
const requestedConfigPath =
|
|
10593
|
-
const defaultRequestedPath =
|
|
11354
|
+
const requestedConfigPath = resolve12(process.cwd(), configPath);
|
|
11355
|
+
const defaultRequestedPath = resolve12(process.cwd(), defaultConfigPath);
|
|
10594
11356
|
const absoluteConfigPath = existsSync(requestedConfigPath) ? realpathSync(requestedConfigPath) : requestedConfigPath;
|
|
10595
11357
|
const absoluteDefaultPath = existsSync(defaultRequestedPath) ? realpathSync(defaultRequestedPath) : defaultRequestedPath;
|
|
10596
11358
|
if (absoluteConfigPath === absoluteDefaultPath) return void 0;
|
|
10597
|
-
const relativeConfigPath =
|
|
11359
|
+
const relativeConfigPath = relative6(cwd, absoluteConfigPath);
|
|
10598
11360
|
if (relativeConfigPath === "" || relativeConfigPath === ".." || relativeConfigPath.startsWith(`..${sep5}`) || isAbsolute5(relativeConfigPath)) {
|
|
10599
11361
|
throw new Error(
|
|
10600
11362
|
"Comparison configs must be inside the current working directory so Copy command can remain reproducible."
|
|
@@ -10634,17 +11396,17 @@ async function loginWithDeviceFlow(options) {
|
|
|
10634
11396
|
codeChallenge,
|
|
10635
11397
|
deviceName: hostname()
|
|
10636
11398
|
});
|
|
10637
|
-
printKeyValue("Code",
|
|
11399
|
+
printKeyValue("Code", pc9.bold(authorization.userCode));
|
|
10638
11400
|
printKeyValue("URL", link(authorization.verificationUriComplete));
|
|
10639
11401
|
if (options.noBrowser) {
|
|
10640
11402
|
logMuted("Open the URL above to continue.");
|
|
10641
11403
|
} else if (process.stdin.isTTY) {
|
|
10642
|
-
const readline =
|
|
11404
|
+
const readline = createInterface2({
|
|
10643
11405
|
input: process.stdin,
|
|
10644
11406
|
output: process.stdout
|
|
10645
11407
|
});
|
|
10646
11408
|
try {
|
|
10647
|
-
await readline.question(`${
|
|
11409
|
+
await readline.question(`${pc9.cyan("?")} Press Enter to open Tileflow Dashboard...`);
|
|
10648
11410
|
openBrowser(authorization.verificationUriComplete);
|
|
10649
11411
|
} finally {
|
|
10650
11412
|
readline.close();
|
|
@@ -10681,7 +11443,7 @@ function createPkceChallenge(verifier) {
|
|
|
10681
11443
|
return createHash4("sha256").update(verifier).digest("base64url");
|
|
10682
11444
|
}
|
|
10683
11445
|
function randomBase64Url(byteLength) {
|
|
10684
|
-
return
|
|
11446
|
+
return randomBytes3(byteLength).toString("base64url");
|
|
10685
11447
|
}
|
|
10686
11448
|
function openBrowser(url, errorsToStderr = false) {
|
|
10687
11449
|
openTileflowExternal(url, {
|
|
@@ -10695,9 +11457,9 @@ function openBrowser(url, errorsToStderr = false) {
|
|
|
10695
11457
|
});
|
|
10696
11458
|
}
|
|
10697
11459
|
async function writeDeployManifest(manifestPath, manifest) {
|
|
10698
|
-
const outputPath =
|
|
11460
|
+
const outputPath = resolve12(process.cwd(), manifestPath);
|
|
10699
11461
|
await writeAtomicFile({
|
|
10700
|
-
boundaryPath:
|
|
11462
|
+
boundaryPath: dirname13(outputPath),
|
|
10701
11463
|
force: true,
|
|
10702
11464
|
label: "Deploy manifest",
|
|
10703
11465
|
managed: true,
|
|
@@ -10733,11 +11495,6 @@ function createCompiledMapAssets(compiled, resolveSprite, modelAssets) {
|
|
|
10733
11495
|
};
|
|
10734
11496
|
}
|
|
10735
11497
|
function selectManagedDeployMaps(mapNames, options) {
|
|
10736
|
-
if (options.map && !options.mapId) {
|
|
10737
|
-
logError("--map requires --map-id.");
|
|
10738
|
-
process.exitCode = 1;
|
|
10739
|
-
return null;
|
|
10740
|
-
}
|
|
10741
11498
|
if (options.map) {
|
|
10742
11499
|
if (!mapNames.includes(options.map)) {
|
|
10743
11500
|
logError(`Unknown Tileflow map: ${options.map}.`);
|
|
@@ -10747,24 +11504,40 @@ function selectManagedDeployMaps(mapNames, options) {
|
|
|
10747
11504
|
}
|
|
10748
11505
|
return [options.map];
|
|
10749
11506
|
}
|
|
10750
|
-
if (!options.mapId) return mapNames;
|
|
10751
11507
|
if (mapNames.length === 1) return [mapNames[0]];
|
|
10752
11508
|
logError(
|
|
10753
11509
|
"This managed target requires an explicit map because the config contains multiple maps."
|
|
10754
11510
|
);
|
|
10755
11511
|
printKeyValue("Maps", mapNames.join(", "));
|
|
10756
11512
|
printNextSteps([
|
|
10757
|
-
`Retry with ${command(`tileflow deploy
|
|
11513
|
+
`Retry with ${command(`tileflow deploy ${options.mapId ? `--map-id ${options.mapId} ` : ""}--map ${mapNames[0]}`)}.`
|
|
10758
11514
|
]);
|
|
10759
11515
|
process.exitCode = 1;
|
|
10760
11516
|
return null;
|
|
10761
11517
|
}
|
|
10762
|
-
function
|
|
10763
|
-
|
|
10764
|
-
logError(
|
|
11518
|
+
function validateMapSetupTarget(options, source) {
|
|
11519
|
+
const fail = (message, steps = []) => {
|
|
11520
|
+
logError(message);
|
|
11521
|
+
printNextSteps(steps);
|
|
10765
11522
|
process.exitCode = 1;
|
|
10766
11523
|
return false;
|
|
11524
|
+
};
|
|
11525
|
+
if (options.mapId) {
|
|
11526
|
+
return hasMapSetupOptions(options) ? fail(
|
|
11527
|
+
"--team, --origin, --any-website and --setup-key apply only when deploy creates a Map."
|
|
11528
|
+
) : true;
|
|
11529
|
+
}
|
|
11530
|
+
const createFirst = `Create the Map with ${command("tileflow maps create")} after ${command("tileflow login")}, then pass --map-id.`;
|
|
11531
|
+
if (options.withNative) return fail("--with-native requires one valid --map-id.", [createFirst]);
|
|
11532
|
+
if (options.apiKey || !allowsStoredDeployCredential(source)) {
|
|
11533
|
+
return fail("Managed deploy with a Tileflow credential or in CI requires --map-id.", [
|
|
11534
|
+
createFirst
|
|
11535
|
+
]);
|
|
10767
11536
|
}
|
|
11537
|
+
const checked = checkMapSetupOptions(options);
|
|
11538
|
+
return checked.ok ? true : fail(checked.error);
|
|
11539
|
+
}
|
|
11540
|
+
function validateDeployTarget(options) {
|
|
10768
11541
|
if (options.mapId && !/^map_[A-Za-z0-9_-]{16}$/u.test(options.mapId)) {
|
|
10769
11542
|
logError("Managed Map ID is invalid.");
|
|
10770
11543
|
process.exitCode = 1;
|
|
@@ -10785,7 +11558,7 @@ function validateDeployManifestMapNames(mapNames) {
|
|
|
10785
11558
|
async function loadExistingDeployManifest(manifestPath, options) {
|
|
10786
11559
|
let source;
|
|
10787
11560
|
try {
|
|
10788
|
-
source = await readFile7(
|
|
11561
|
+
source = await readFile7(resolve12(process.cwd(), manifestPath), "utf8");
|
|
10789
11562
|
} catch (error) {
|
|
10790
11563
|
if (error.code === "ENOENT") return null;
|
|
10791
11564
|
throw new Error("Existing Tileflow manifest could not be read.");
|
|
@@ -10845,7 +11618,7 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
10845
11618
|
if (!behavior.silent) {
|
|
10846
11619
|
logError("CI requires an explicit Tileflow credential with access to the target Map.");
|
|
10847
11620
|
printNextSteps([
|
|
10848
|
-
`Set ${
|
|
11621
|
+
`Set ${pc9.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
|
|
10849
11622
|
"The saved personal account session is never used in CI."
|
|
10850
11623
|
]);
|
|
10851
11624
|
}
|
|
@@ -10914,17 +11687,17 @@ function printCliError(error) {
|
|
|
10914
11687
|
}
|
|
10915
11688
|
const message = error instanceof Error ? error.message : "Command failed.";
|
|
10916
11689
|
if (message.includes("Cannot find module '@tileflow/core'")) {
|
|
10917
|
-
logError(`Cannot load ${
|
|
11690
|
+
logError(`Cannot load ${pc9.cyan("@tileflow/core")} from this project.`);
|
|
10918
11691
|
printNextSteps([
|
|
10919
|
-
`Install ${
|
|
11692
|
+
`Install ${pc9.cyan("@tileflow/core")} in this project.`,
|
|
10920
11693
|
`Run the command again from the project root.`
|
|
10921
11694
|
]);
|
|
10922
11695
|
return;
|
|
10923
11696
|
}
|
|
10924
11697
|
if (message.includes("Cannot find module '@tileflow/maps'")) {
|
|
10925
|
-
logError(`Cannot load ${
|
|
11698
|
+
logError(`Cannot load ${pc9.cyan("@tileflow/maps")} from this project.`);
|
|
10926
11699
|
printNextSteps([
|
|
10927
|
-
`Install ${
|
|
11700
|
+
`Install ${pc9.cyan("@tileflow/maps")} in this project.`,
|
|
10928
11701
|
`Run the command again from the project root.`
|
|
10929
11702
|
]);
|
|
10930
11703
|
return;
|
|
@@ -10935,51 +11708,51 @@ function printCliError(error) {
|
|
|
10935
11708
|
}
|
|
10936
11709
|
}
|
|
10937
11710
|
function logSuccess(message) {
|
|
10938
|
-
console.log(`${
|
|
11711
|
+
console.log(`${pc9.green("\u2713")} ${message}`);
|
|
10939
11712
|
}
|
|
10940
11713
|
function logError(message) {
|
|
10941
|
-
console.log(`${
|
|
11714
|
+
console.log(`${pc9.red("\u2715")} ${message}`);
|
|
10942
11715
|
}
|
|
10943
11716
|
function logWarning(message) {
|
|
10944
|
-
console.log(`${
|
|
11717
|
+
console.log(`${pc9.yellow("!")} ${message}`);
|
|
10945
11718
|
}
|
|
10946
11719
|
function logInfo(message) {
|
|
10947
|
-
console.log(`${
|
|
11720
|
+
console.log(`${pc9.cyan("i")} ${message}`);
|
|
10948
11721
|
}
|
|
10949
11722
|
function logMuted(message) {
|
|
10950
|
-
console.log(
|
|
11723
|
+
console.log(pc9.gray(message));
|
|
10951
11724
|
}
|
|
10952
11725
|
function printTitle(title, subtitle) {
|
|
10953
|
-
console.log(
|
|
11726
|
+
console.log(pc9.bold(title));
|
|
10954
11727
|
if (subtitle) {
|
|
10955
|
-
console.log(
|
|
11728
|
+
console.log(pc9.gray(subtitle));
|
|
10956
11729
|
}
|
|
10957
11730
|
}
|
|
10958
11731
|
function printKeyValue(label, value) {
|
|
10959
|
-
console.log(`${
|
|
11732
|
+
console.log(`${pc9.gray(`${label}:`.padEnd(13))} ${value}`);
|
|
10960
11733
|
}
|
|
10961
11734
|
function printChecks(items) {
|
|
10962
11735
|
for (const item of items) {
|
|
10963
|
-
console.log(` ${
|
|
11736
|
+
console.log(` ${pc9.green("\u2713")} ${item}`);
|
|
10964
11737
|
}
|
|
10965
11738
|
}
|
|
10966
11739
|
function printNextSteps(steps) {
|
|
10967
11740
|
if (steps.length === 0) return;
|
|
10968
11741
|
console.log(`
|
|
10969
|
-
${
|
|
11742
|
+
${pc9.bold("Next steps")}`);
|
|
10970
11743
|
for (const step of steps) {
|
|
10971
|
-
console.log(` ${
|
|
11744
|
+
console.log(` ${pc9.cyan("\u2022")} ${step}`);
|
|
10972
11745
|
}
|
|
10973
11746
|
}
|
|
10974
11747
|
function printValidationErrors(messages) {
|
|
10975
11748
|
logError("Tileflow config has errors.");
|
|
10976
11749
|
for (const message of messages) {
|
|
10977
|
-
const path = message.path ?
|
|
10978
|
-
console.log(` ${
|
|
11750
|
+
const path = message.path ? pc9.cyan(message.path) : pc9.cyan("(root)");
|
|
11751
|
+
console.log(` ${pc9.red("\u2715")} ${path} ${pc9.gray("-")} ${message.message}`);
|
|
10979
11752
|
}
|
|
10980
11753
|
}
|
|
10981
11754
|
function printAccountDetails(session) {
|
|
10982
|
-
printKeyValue("Account",
|
|
11755
|
+
printKeyValue("Account", pc9.bold(session.account.name));
|
|
10983
11756
|
printKeyValue("Email", session.account.email);
|
|
10984
11757
|
printKeyValue("API", link(session.apiOrigin));
|
|
10985
11758
|
printKeyValue("Expires", formatDate(session.expiresAt));
|
|
@@ -11008,11 +11781,11 @@ function printDeployedMaps(maps) {
|
|
|
11008
11781
|
const entries = Object.entries(maps);
|
|
11009
11782
|
if (entries.length === 0) return;
|
|
11010
11783
|
console.log(`
|
|
11011
|
-
${
|
|
11784
|
+
${pc9.bold("Maps")}`);
|
|
11012
11785
|
for (const [name, map] of entries) {
|
|
11013
11786
|
for (const [themeName, theme] of Object.entries(map.themes)) {
|
|
11014
11787
|
const label = `${name}/${themeName}`;
|
|
11015
|
-
console.log(` ${
|
|
11788
|
+
console.log(` ${pc9.green("\u2713")} ${label.padEnd(24)} ${link(theme.styleUrl)}`);
|
|
11016
11789
|
}
|
|
11017
11790
|
}
|
|
11018
11791
|
}
|
|
@@ -11030,13 +11803,13 @@ function plural(count, singular, pluralForm = `${singular}s`) {
|
|
|
11030
11803
|
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
11031
11804
|
}
|
|
11032
11805
|
function command(value) {
|
|
11033
|
-
return
|
|
11806
|
+
return pc9.cyan(value);
|
|
11034
11807
|
}
|
|
11035
11808
|
function link(value) {
|
|
11036
|
-
return
|
|
11809
|
+
return pc9.cyan(value);
|
|
11037
11810
|
}
|
|
11038
11811
|
function pathLabel(value) {
|
|
11039
|
-
return
|
|
11812
|
+
return pc9.cyan(value);
|
|
11040
11813
|
}
|
|
11041
11814
|
function starterConfig() {
|
|
11042
11815
|
return `import { defineMap, defineTheme, labels, poi } from "@tileflow/core";
|
|
@@ -11082,19 +11855,19 @@ function printHostedCompatibilityIssues(issues) {
|
|
|
11082
11855
|
}
|
|
11083
11856
|
function printMapStatus(status, apiUrl) {
|
|
11084
11857
|
printTitle("Tileflow status");
|
|
11085
|
-
printKeyValue("Map",
|
|
11858
|
+
printKeyValue("Map", pc9.bold(status.mapId));
|
|
11086
11859
|
console.log(`
|
|
11087
|
-
${
|
|
11860
|
+
${pc9.bold("Styles")}`);
|
|
11088
11861
|
if (status.styles.length === 0) {
|
|
11089
11862
|
logMuted(" No styles deployed.");
|
|
11090
11863
|
}
|
|
11091
11864
|
for (const style of status.styles) {
|
|
11092
11865
|
console.log(
|
|
11093
|
-
` ${
|
|
11866
|
+
` ${pc9.green("\u2713")} ${style.environment.padEnd(16)} ${link(`${apiUrl}/maps/${style.mapId}/style.json`).padEnd(36)} ${formatBytes4(style.size).padStart(10)} ${pc9.gray(formatDate(style.uploaded))}`
|
|
11094
11867
|
);
|
|
11095
11868
|
}
|
|
11096
11869
|
}
|
|
11097
|
-
function
|
|
11870
|
+
function formatBytes4(bytes) {
|
|
11098
11871
|
if (bytes < 1024) return `${bytes} B`;
|
|
11099
11872
|
const units = ["KB", "MB", "GB", "TB"];
|
|
11100
11873
|
let value = bytes;
|