tileflow 0.1.0-alpha.22 → 0.1.0-alpha.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -20
- package/REFERENCE.md +429 -0
- package/dist/build-RQF35KW6.js +21 -0
- package/dist/chunk-43VQMAQO.js +52 -0
- package/dist/chunk-5U4J6E6Y.js +4911 -0
- package/dist/{chunk-5QK6JCLT.js → chunk-EXDH7EHK.js} +726 -68
- package/dist/chunk-JVTGXPGV.js +382 -0
- package/dist/{cli-main-HRFGWUWR.js → cli-main-K3PKVCIP.js} +1099 -351
- package/dist/errors-C3VZMCM5.js +20 -0
- package/dist/index.js +2 -2
- package/dist/inspect-source-5CD3AV5J.js +227 -0
- package/dist/recipe-3BPMNDPX.js +23 -0
- package/dist/{tileset-entry-ITUSGCM3.js → tileset-entry-V5RUQGZG.js} +4 -1
- package/package.json +14 -5
- package/schemas/tileset-recipe.v1.json +171 -0
|
@@ -219,20 +219,10 @@ import pc from "picocolors";
|
|
|
219
219
|
function createCliProgram() {
|
|
220
220
|
return new Command().name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(pc.gray("\nRun tileflow <command> --help for usage.")).showSuggestionAfterError().addHelpText("after", "\n -V, --version output the version number\n");
|
|
221
221
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
return new Option("--api-key <key>", description).env("TILEFLOW_API_KEY");
|
|
227
|
-
}
|
|
228
|
-
function hostedApiUrlOption() {
|
|
229
|
-
return new Option("--api-url <url>", "Tileflow API URL").env("TILEFLOW_API_URL");
|
|
230
|
-
}
|
|
231
|
-
function isHostedApiKey(value) {
|
|
232
|
-
return /^tf_live_[0-9a-f]{48}$/u.test(value);
|
|
233
|
-
}
|
|
234
|
-
function allowsStoredAccountSession(env = process.env) {
|
|
235
|
-
return !env.CI && env.GITHUB_ACTIONS !== "true" && env.GITLAB_CI !== "true";
|
|
222
|
+
function configureJsonErrors(command) {
|
|
223
|
+
command.exitOverride().configureOutput({ writeErr: () => {
|
|
224
|
+
} });
|
|
225
|
+
for (const child of command.commands) configureJsonErrors(child);
|
|
236
226
|
}
|
|
237
227
|
|
|
238
228
|
// src/hosted-client.ts
|
|
@@ -392,7 +382,7 @@ async function pollDeviceToken(appUrl, authorization, input, options = {}) {
|
|
|
392
382
|
const expiresAt = input.expiresAt ?? now() + authorization.expiresIn * 1e3;
|
|
393
383
|
let intervalMs = Math.max(authorization.interval, 1) * 1e3;
|
|
394
384
|
let delayMs = intervalMs;
|
|
395
|
-
const wait = input.sleep ?? ((milliseconds) => new Promise((
|
|
385
|
+
const wait = input.sleep ?? ((milliseconds) => new Promise((resolve3) => setTimeout(resolve3, milliseconds)));
|
|
396
386
|
while (now() < expiresAt) {
|
|
397
387
|
options.signal?.throwIfAborted();
|
|
398
388
|
await wait(Math.min(delayMs, expiresAt - now()));
|
|
@@ -817,7 +807,7 @@ async function withHostedResponse(apiUrl, path, init, options, consume) {
|
|
|
817
807
|
}
|
|
818
808
|
function raceWithAbort(operation, signal) {
|
|
819
809
|
if (signal.aborted) return Promise.reject(signal.reason);
|
|
820
|
-
return new Promise((
|
|
810
|
+
return new Promise((resolve3, reject) => {
|
|
821
811
|
const cleanup = () => signal.removeEventListener("abort", onAbort);
|
|
822
812
|
const onAbort = () => {
|
|
823
813
|
cleanup();
|
|
@@ -827,7 +817,7 @@ function raceWithAbort(operation, signal) {
|
|
|
827
817
|
operation.then(
|
|
828
818
|
(value) => {
|
|
829
819
|
cleanup();
|
|
830
|
-
|
|
820
|
+
resolve3(value);
|
|
831
821
|
},
|
|
832
822
|
(error) => {
|
|
833
823
|
cleanup();
|
|
@@ -945,30 +935,44 @@ function safeTransportError(error, fallback) {
|
|
|
945
935
|
return fallback;
|
|
946
936
|
}
|
|
947
937
|
|
|
948
|
-
// src/
|
|
938
|
+
// src/hosted-options.ts
|
|
939
|
+
import { Option } from "commander";
|
|
940
|
+
function hostedApiKeyOption(description = "Tileflow API key") {
|
|
941
|
+
return new Option("--api-key <key>", description).env("TILEFLOW_API_KEY");
|
|
942
|
+
}
|
|
943
|
+
function hostedApiUrlOption() {
|
|
944
|
+
return new Option("--api-url <url>", "Tileflow API URL").env("TILEFLOW_API_URL");
|
|
945
|
+
}
|
|
946
|
+
function isHostedApiKey(value) {
|
|
947
|
+
return /^tf_live_[0-9a-f]{48}$/u.test(value);
|
|
948
|
+
}
|
|
949
|
+
function allowsStoredAccountSession(env = process.env) {
|
|
950
|
+
return !env.CI && env.GITHUB_ACTIONS !== "true" && env.GITLAB_CI !== "true";
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
// src/restore-guidance.ts
|
|
954
|
+
import { CommanderError } from "commander";
|
|
949
955
|
import pc3 from "picocolors";
|
|
950
|
-
import {
|
|
951
|
-
import { serializeCanonicalJson as serializeCanonicalJson3 } from "@tileflow/core";
|
|
952
|
-
import { inspectTileflowPmtiles } from "@tileflow/dev/tilesets";
|
|
956
|
+
import { defaultTileflowApiUrl } from "@tileflow/dev/config";
|
|
953
957
|
|
|
954
958
|
// src/hosted-team.ts
|
|
955
959
|
import pc2 from "picocolors";
|
|
956
960
|
import { serializeCanonicalJson } from "@tileflow/core";
|
|
957
|
-
async function resolveTeamAuthority(options, scopes, dependencies) {
|
|
961
|
+
async function resolveTeamAuthority(options, scopes, dependencies, fail = emitFailure) {
|
|
958
962
|
let apiOrigin;
|
|
959
963
|
try {
|
|
960
964
|
apiOrigin = normalizeApiOrigin(options.apiUrl ?? dependencies.defaultApiUrl);
|
|
961
965
|
} catch (error) {
|
|
962
|
-
|
|
966
|
+
fail(options.json, "invalid_api_url", safeMessage(error));
|
|
963
967
|
return null;
|
|
964
968
|
}
|
|
965
969
|
if (options.apiKey !== void 0) {
|
|
966
970
|
if (!isHostedApiKey(options.apiKey)) {
|
|
967
|
-
|
|
971
|
+
fail(options.json, "invalid_team_data_key", "Team data API key is invalid.");
|
|
968
972
|
return null;
|
|
969
973
|
}
|
|
970
974
|
if (options.team) {
|
|
971
|
-
|
|
975
|
+
fail(
|
|
972
976
|
options.json,
|
|
973
977
|
"team_selector_with_api_key",
|
|
974
978
|
"A Team data key already selects its Team; omit --team."
|
|
@@ -978,7 +982,7 @@ async function resolveTeamAuthority(options, scopes, dependencies) {
|
|
|
978
982
|
return { apiOrigin, credential: options.apiKey, team: null };
|
|
979
983
|
}
|
|
980
984
|
if (!allowsStoredAccountSession()) {
|
|
981
|
-
|
|
985
|
+
fail(
|
|
982
986
|
options.json,
|
|
983
987
|
"api_key_required",
|
|
984
988
|
"CI requires TILEFLOW_API_KEY or --api-key. Saved personal logins are not used in CI."
|
|
@@ -989,12 +993,12 @@ async function resolveTeamAuthority(options, scopes, dependencies) {
|
|
|
989
993
|
try {
|
|
990
994
|
config = await dependencies.loadAuthConfig();
|
|
991
995
|
} catch (error) {
|
|
992
|
-
|
|
996
|
+
fail(options.json, "auth_state_unavailable", safeMessage(error));
|
|
993
997
|
return null;
|
|
994
998
|
}
|
|
995
999
|
const selected = resolveAccountSession(config, options.apiUrl ?? dependencies.defaultApiUrl);
|
|
996
1000
|
if (selected.kind !== "selected") {
|
|
997
|
-
|
|
1001
|
+
fail(
|
|
998
1002
|
options.json,
|
|
999
1003
|
`account_session_${selected.kind}`,
|
|
1000
1004
|
selected.kind === "expired" ? "Account session expired. Run tileflow login." : "Run tileflow login."
|
|
@@ -1003,18 +1007,18 @@ async function resolveTeamAuthority(options, scopes, dependencies) {
|
|
|
1003
1007
|
}
|
|
1004
1008
|
const discovered = await listAccountTeams(selected.session);
|
|
1005
1009
|
if (!discovered.ok) {
|
|
1006
|
-
|
|
1010
|
+
fail(options.json, "team_discovery_failed", discovered.error);
|
|
1007
1011
|
return null;
|
|
1008
1012
|
}
|
|
1009
1013
|
const requested = options.team ? /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u.exec(options.team) : null;
|
|
1010
1014
|
if (options.team && !requested) {
|
|
1011
|
-
|
|
1015
|
+
fail(options.json, "invalid_team", "Team must use @team syntax.");
|
|
1012
1016
|
return null;
|
|
1013
1017
|
}
|
|
1014
1018
|
const team = requested ? discovered.teams.find(({ slug }) => slug === requested[1]) : discovered.teams.length === 1 ? discovered.teams[0] : null;
|
|
1015
1019
|
if (!team) {
|
|
1016
1020
|
const options_ = discovered.teams.map(({ slug }) => `@${slug}`);
|
|
1017
|
-
|
|
1021
|
+
fail(
|
|
1018
1022
|
options.json,
|
|
1019
1023
|
options_.length > 1 ? "team_ambiguous" : "team_not_found",
|
|
1020
1024
|
options_.length > 1 ? `Choose one Team with --team: ${options_.join(", ")}.` : "No matching Team is available.",
|
|
@@ -1024,7 +1028,7 @@ async function resolveTeamAuthority(options, scopes, dependencies) {
|
|
|
1024
1028
|
}
|
|
1025
1029
|
const capability = await requestTeamCapability(selected.session, `@${team.slug}`, scopes);
|
|
1026
1030
|
if (!capability.ok || capability.team.id !== team.id) {
|
|
1027
|
-
|
|
1031
|
+
fail(
|
|
1028
1032
|
options.json,
|
|
1029
1033
|
"team_capability_failed",
|
|
1030
1034
|
capability.ok ? "Team capability response was invalid." : capability.error
|
|
@@ -1080,13 +1084,395 @@ function safeMessage(error) {
|
|
|
1080
1084
|
return error instanceof Error ? error.message : "Team command failed.";
|
|
1081
1085
|
}
|
|
1082
1086
|
|
|
1087
|
+
// src/restore-guidance.ts
|
|
1088
|
+
function printNextSteps(steps, write = console.log) {
|
|
1089
|
+
if (steps.length === 0) return;
|
|
1090
|
+
write(`
|
|
1091
|
+
${pc3.bold("Next steps")}`);
|
|
1092
|
+
for (const step of steps) {
|
|
1093
|
+
write(` ${pc3.cyan("\u2022")} ${step.description}`);
|
|
1094
|
+
if (step.command) write(` ${pc3.cyan(step.command)}`);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
function emitGuidedFailure(json, code, message, nextSteps, context = {}) {
|
|
1098
|
+
emitFailure(json, code, message, { ...context, nextSteps });
|
|
1099
|
+
if (!json) printNextSteps(nextSteps, console.error);
|
|
1100
|
+
}
|
|
1101
|
+
function emitRestoreFailure(json, code, message, context = {}) {
|
|
1102
|
+
emitGuidedFailure(json, code, message, [], context);
|
|
1103
|
+
}
|
|
1104
|
+
function apiOriginOption(origin, defaultOrigin = defaultTileflowApiUrl) {
|
|
1105
|
+
if (origin === defaultOrigin) return "";
|
|
1106
|
+
const argument = /^[A-Za-z0-9.:/-]+$/u.test(origin) ? origin : "'" + origin.replaceAll("'", "'\\''") + "'";
|
|
1107
|
+
return ` --api-url ${argument}`;
|
|
1108
|
+
}
|
|
1109
|
+
var credentialSteps = (scope, originOption) => [
|
|
1110
|
+
{
|
|
1111
|
+
description: `Use a credential with the ${scope} scope, or sign in as someone who can manage this Team's content.`,
|
|
1112
|
+
command: `tileflow login${originOption}`
|
|
1113
|
+
}
|
|
1114
|
+
];
|
|
1115
|
+
function deploymentRestoreSteps(input) {
|
|
1116
|
+
const origin = apiOriginOption(input.apiUrl);
|
|
1117
|
+
const target = `--map-id ${input.mapId}${origin}`;
|
|
1118
|
+
const list = `tileflow deployments list ${target}`;
|
|
1119
|
+
switch (input.code) {
|
|
1120
|
+
case "TF_DEPLOYMENT_CURRENT_CHANGED":
|
|
1121
|
+
return [
|
|
1122
|
+
{
|
|
1123
|
+
description: "The live version moved while restoring. Read it, then repeat the restore if it is still what you want.",
|
|
1124
|
+
command: list
|
|
1125
|
+
}
|
|
1126
|
+
];
|
|
1127
|
+
case "deployment_not_found":
|
|
1128
|
+
case "TF_DEPLOYMENT_NOT_FOUND":
|
|
1129
|
+
return [{ description: "Choose a version that is still retained.", command: list }];
|
|
1130
|
+
case "TF_DEPLOYMENT_NOT_RESTORABLE":
|
|
1131
|
+
case "TF_DEPLOYMENT_ASSETS_UNAVAILABLE":
|
|
1132
|
+
return [
|
|
1133
|
+
{ description: "Choose another retained version.", command: list },
|
|
1134
|
+
{
|
|
1135
|
+
description: "Or publish the map again from its repository.",
|
|
1136
|
+
command: `tileflow deploy ${target}`
|
|
1137
|
+
}
|
|
1138
|
+
];
|
|
1139
|
+
case "TF_DEPLOYMENT_SOURCE_INCOMPATIBLE":
|
|
1140
|
+
return [
|
|
1141
|
+
{ description: "Choose a version whose data still exists.", command: list },
|
|
1142
|
+
{
|
|
1143
|
+
description: "Or publish the data named in this error to its tileset, then repeat the restore."
|
|
1144
|
+
}
|
|
1145
|
+
];
|
|
1146
|
+
case "nothing_published":
|
|
1147
|
+
return [{ description: "Publish the map first.", command: `tileflow deploy ${target}` }];
|
|
1148
|
+
case "TF_MAP_UNAVAILABLE":
|
|
1149
|
+
return [
|
|
1150
|
+
{
|
|
1151
|
+
description: "Only an active Map can restore. Make the Map active, then repeat the restore."
|
|
1152
|
+
}
|
|
1153
|
+
];
|
|
1154
|
+
case "TF_DEPLOYMENT_RESTORE_FORBIDDEN":
|
|
1155
|
+
case "http_401":
|
|
1156
|
+
case "http_403":
|
|
1157
|
+
return credentialSteps("styles:write", origin);
|
|
1158
|
+
default:
|
|
1159
|
+
return [{ description: "Check what is live before trying again.", command: list }];
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
var windingDownStates = /* @__PURE__ */ new Set(["aborting", "expiring", "publishing", "rejecting"]);
|
|
1163
|
+
function tilesetRestoreSteps(input) {
|
|
1164
|
+
const boundRestore = `${input.restore} --expected-current-version ${input.attemptedVersion}`;
|
|
1165
|
+
if (input.attemptedVersion !== void 0 && (input.uncertain || input.code === "TF_TILESET_DELIVERY_PENDING" || input.code === "TF_TILESET_RESTORE_UNAVAILABLE")) {
|
|
1166
|
+
return [
|
|
1167
|
+
{ description: "Check which version is current.", command: input.status },
|
|
1168
|
+
{
|
|
1169
|
+
description: "Retry the original restore using its expected version.",
|
|
1170
|
+
command: boundRestore
|
|
1171
|
+
}
|
|
1172
|
+
];
|
|
1173
|
+
}
|
|
1174
|
+
switch (input.code) {
|
|
1175
|
+
case "TF_TILESET_UPLOAD_IN_PROGRESS":
|
|
1176
|
+
if (!input.upload || windingDownStates.has(input.upload.state)) {
|
|
1177
|
+
return [
|
|
1178
|
+
{
|
|
1179
|
+
description: "The upload is finishing on its own. Wait a minute, then repeat the restore.",
|
|
1180
|
+
command: input.pinned ? boundRestore : input.restore
|
|
1181
|
+
}
|
|
1182
|
+
];
|
|
1183
|
+
}
|
|
1184
|
+
return [
|
|
1185
|
+
{
|
|
1186
|
+
description: `Upload ${input.upload.id} is ${input.upload.state} and expires at ${input.upload.expiresAt}. It may belong to a teammate or a CI run: abort it only if nothing is still uploading.`,
|
|
1187
|
+
command: `${input.abort} --upload-id ${input.upload.id}`
|
|
1188
|
+
},
|
|
1189
|
+
{
|
|
1190
|
+
description: "After aborting, or once the upload ends, repeat the restore.",
|
|
1191
|
+
command: input.pinned ? boundRestore : input.restore
|
|
1192
|
+
}
|
|
1193
|
+
];
|
|
1194
|
+
case "no_previous_version":
|
|
1195
|
+
case "TF_TILESET_NO_PREVIOUS_VERSION":
|
|
1196
|
+
return [
|
|
1197
|
+
{
|
|
1198
|
+
description: "There is nothing to go back to. Publish a corrected archive instead."
|
|
1199
|
+
}
|
|
1200
|
+
];
|
|
1201
|
+
case "TF_TILESET_CURRENT_VERSION_CHANGED":
|
|
1202
|
+
return [
|
|
1203
|
+
{
|
|
1204
|
+
description: input.pinned ? "Nothing was swapped. The original restore may have finished, or another change updated the current version." : "The current version moved while restoring. Read it, then repeat the restore if it is still what you want.",
|
|
1205
|
+
command: input.status
|
|
1206
|
+
}
|
|
1207
|
+
];
|
|
1208
|
+
case "TF_TILESET_RESTORE_INCOMPATIBLE":
|
|
1209
|
+
return [
|
|
1210
|
+
{ description: "See which Maps use this tileset.", command: input.status },
|
|
1211
|
+
{
|
|
1212
|
+
description: "Restore or redeploy the live Map that needs the newer data, then repeat this restore."
|
|
1213
|
+
}
|
|
1214
|
+
];
|
|
1215
|
+
case "TF_TILESET_PURGING":
|
|
1216
|
+
return [{ description: "This tileset is being purged, so it cannot be restored." }];
|
|
1217
|
+
case "http_401":
|
|
1218
|
+
case "http_403":
|
|
1219
|
+
return credentialSteps("tilesets:write", input.origin);
|
|
1220
|
+
default:
|
|
1221
|
+
return [{ description: "Check the tileset before trying again.", command: input.status }];
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
function isRestoreJsonCommand(args) {
|
|
1225
|
+
const [group, subcommand, operation] = args;
|
|
1226
|
+
return args.includes("--json") && (group === "deployments" && (subcommand === "list" || subcommand === "restore") || group === "tileset" && (subcommand === "restore" || subcommand === "upload" && operation === "abort"));
|
|
1227
|
+
}
|
|
1228
|
+
async function parseRestoreJson(program) {
|
|
1229
|
+
configureJsonErrors(program);
|
|
1230
|
+
try {
|
|
1231
|
+
await program.parseAsync();
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
if (error instanceof CommanderError) {
|
|
1234
|
+
if (error.exitCode !== 0)
|
|
1235
|
+
emitRestoreFailure(
|
|
1236
|
+
true,
|
|
1237
|
+
"invalid_command_arguments",
|
|
1238
|
+
"Invalid command arguments. Check the command help."
|
|
1239
|
+
);
|
|
1240
|
+
} else {
|
|
1241
|
+
emitRestoreFailure(
|
|
1242
|
+
true,
|
|
1243
|
+
"hosted_request_failed",
|
|
1244
|
+
"The command could not complete its Hosted request."
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// src/tileset-command.ts
|
|
1251
|
+
import pc5 from "picocolors";
|
|
1252
|
+
import { z as z4 } from "zod";
|
|
1253
|
+
import { serializeCanonicalJson as serializeCanonicalJson4 } from "@tileflow/core";
|
|
1254
|
+
import { inspectTileflowPmtiles } from "@tileflow/dev/tilesets";
|
|
1255
|
+
|
|
1256
|
+
// src/tileset-build-command.ts
|
|
1257
|
+
import { Option as Option2 } from "commander";
|
|
1258
|
+
import { resolve as resolve2 } from "path";
|
|
1259
|
+
import pc4 from "picocolors";
|
|
1260
|
+
import { serializeCanonicalJson as serializeCanonicalJson2 } from "@tileflow/core";
|
|
1261
|
+
var progressFormats = ["text", "ndjson"];
|
|
1262
|
+
var formats = [
|
|
1263
|
+
"csv",
|
|
1264
|
+
"geojson",
|
|
1265
|
+
"geojsonseq",
|
|
1266
|
+
"geopackage",
|
|
1267
|
+
"gpx",
|
|
1268
|
+
"kml",
|
|
1269
|
+
"shapefile"
|
|
1270
|
+
];
|
|
1271
|
+
var recipeFlags = [
|
|
1272
|
+
"attribution",
|
|
1273
|
+
"description",
|
|
1274
|
+
"exclude",
|
|
1275
|
+
"featureId",
|
|
1276
|
+
"format",
|
|
1277
|
+
"hashFeatureId",
|
|
1278
|
+
"include",
|
|
1279
|
+
"inputLayer",
|
|
1280
|
+
"latColumn",
|
|
1281
|
+
"layer",
|
|
1282
|
+
"lonColumn",
|
|
1283
|
+
"maxzoom",
|
|
1284
|
+
"minzoom",
|
|
1285
|
+
"name",
|
|
1286
|
+
"rankAscending",
|
|
1287
|
+
"rankBy",
|
|
1288
|
+
"reproject",
|
|
1289
|
+
"sourceCrs",
|
|
1290
|
+
"zipEntry"
|
|
1291
|
+
];
|
|
1292
|
+
function registerTilesetBuildCommand(parent) {
|
|
1293
|
+
parent.command("build").description(
|
|
1294
|
+
"Build a PMTiles archive locally from GeoJSON, CSV, Shapefile, GeoPackage, KML or GPX"
|
|
1295
|
+
).argument(
|
|
1296
|
+
"<input>",
|
|
1297
|
+
"GeoJSON, newline-delimited GeoJSON, CSV, .shp, .zip holding a Shapefile, .gpkg, .kml, .kmz or .gpx"
|
|
1298
|
+
).option("-o, --output <path>", "archive path (default: the input name with .pmtiles)").option("--force", "replace an existing output file").option("--layer <name>", "source-layer name (default: from the input file name)").option("--format <format>", `override detection: ${formats.join(", ")}`).option("--minzoom <zoom>", "lowest zoom to write", "0").option("--maxzoom <zoom>", "highest zoom to write (default: chosen from the data)").option("--include <fields>", "keep only these comma-separated attributes").option("--exclude <fields>", "drop these comma-separated attributes").option("--rank-by <field>", "keep the highest values of this numeric attribute when thinning").option("--rank-ascending", "with --rank-by, keep the lowest values instead").option("--feature-id <field>", "use this attribute as the feature ID").option("--hash-feature-id", "with --feature-id, hash each value into an integer ID").option("--zip-entry <path>", "the .shp or .kml to read from an archive holding several").option("--input-layer <name>", "the feature table to read from a GeoPackage holding several").option("--lon-column <name>", "CSV longitude column").option("--lat-column <name>", "CSV latitude column").option("--source-crs <crs>", "state the input CRS: EPSG:<code>, proj string, WKT or .prj path").option("--reproject", "convert a stated non-WGS84 CRS to WGS84").option("--name <name>", "archive display name").option("--description <text>", "archive description").option("--attribution <text>", "data attribution stored in the archive").option("--recipe <path>", "read every build setting from this recipe file").option("--print-recipe", "print the recipe these options make, without building").option("--inspect", "report the format, CRS and fields as JSON, without tiling").option("--sample-features <count>", "with --inspect, features to read (default: 10000)").option("--progress <format>", "report progress on stderr: text or ndjson").option("--json", "print deterministic schema-version-1 JSON").addOption(new Option2("--delete-input-after-read").hideHelp()).addOption(new Option2("--max-extracted-bytes <bytes>").hideHelp()).action(async (input, options, command) => {
|
|
1299
|
+
try {
|
|
1300
|
+
const { buildTileset, chooseLayer } = await import("./build-RQF35KW6.js");
|
|
1301
|
+
const { TilesetBuildError } = await import("./errors-C3VZMCM5.js");
|
|
1302
|
+
const recipes = await import("./recipe-3BPMNDPX.js");
|
|
1303
|
+
const settings = options.recipe === void 0 ? settingsFromFlags(options, TilesetBuildError) : (() => {
|
|
1304
|
+
const conflict = recipeFlags.find(
|
|
1305
|
+
(name) => command.getOptionValueSource(name) === "cli"
|
|
1306
|
+
);
|
|
1307
|
+
if (conflict !== void 0) {
|
|
1308
|
+
throw new TilesetBuildError(
|
|
1309
|
+
"TF_TILESET_BUILD_RECIPE_CONFLICT",
|
|
1310
|
+
"recipe",
|
|
1311
|
+
`--recipe holds every build setting; remove --${kebab(conflict)}.`
|
|
1312
|
+
);
|
|
1313
|
+
}
|
|
1314
|
+
return recipes.settingsFromRecipe(recipes.readTilesetRecipe(options.recipe));
|
|
1315
|
+
})();
|
|
1316
|
+
if (options.inspect) {
|
|
1317
|
+
const { inspectTilesetSource } = await import("./inspect-source-5CD3AV5J.js");
|
|
1318
|
+
const inspection = await inspectTilesetSource({
|
|
1319
|
+
...settings,
|
|
1320
|
+
input,
|
|
1321
|
+
...options.sampleFeatures === void 0 ? {} : {
|
|
1322
|
+
sampleFeatures: positiveInteger(
|
|
1323
|
+
options.sampleFeatures,
|
|
1324
|
+
"--sample-features",
|
|
1325
|
+
TilesetBuildError
|
|
1326
|
+
)
|
|
1327
|
+
}
|
|
1328
|
+
});
|
|
1329
|
+
process.stdout.write(`${serializeCanonicalJson2(inspection)}
|
|
1330
|
+
`);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
if (options.printRecipe) {
|
|
1334
|
+
const layer2 = chooseLayer(settings.layer, settings.inputLayer, resolve2(input));
|
|
1335
|
+
const recipe = recipes.recipeFromSettings(settings, layer2);
|
|
1336
|
+
recipes.parseTilesetRecipe(JSON.stringify(recipe));
|
|
1337
|
+
process.stdout.write(`${serializeCanonicalJson2(recipe)}
|
|
1338
|
+
`);
|
|
1339
|
+
return;
|
|
1340
|
+
}
|
|
1341
|
+
if (options.progress !== void 0 && !progressFormats.includes(options.progress)) {
|
|
1342
|
+
throw new TilesetBuildError(
|
|
1343
|
+
"TF_TILESET_BUILD_PROGRESS_INVALID",
|
|
1344
|
+
"progress",
|
|
1345
|
+
`--progress must be one of: ${progressFormats.join(", ")}.`
|
|
1346
|
+
);
|
|
1347
|
+
}
|
|
1348
|
+
const progress = options.progress ?? (options.json ? void 0 : "text");
|
|
1349
|
+
const result = await buildTileset({
|
|
1350
|
+
...settings,
|
|
1351
|
+
input,
|
|
1352
|
+
...options.deleteInputAfterRead ? { deleteInputAfterRead: true } : {},
|
|
1353
|
+
...options.force ? { force: true } : {},
|
|
1354
|
+
...options.maxExtractedBytes === void 0 ? {} : {
|
|
1355
|
+
maximumExtractedBytes: positiveInteger(
|
|
1356
|
+
options.maxExtractedBytes,
|
|
1357
|
+
"--max-extracted-bytes",
|
|
1358
|
+
TilesetBuildError
|
|
1359
|
+
)
|
|
1360
|
+
},
|
|
1361
|
+
...progress === void 0 ? {} : {
|
|
1362
|
+
onProgress: (event) => process.stderr.write(
|
|
1363
|
+
`${progress === "ndjson" ? serializeCanonicalJson2(event) : event.message}
|
|
1364
|
+
`
|
|
1365
|
+
)
|
|
1366
|
+
},
|
|
1367
|
+
...options.output === void 0 ? {} : { output: options.output }
|
|
1368
|
+
});
|
|
1369
|
+
if (options.json) {
|
|
1370
|
+
process.stdout.write(`${serializeCanonicalJson2(result)}
|
|
1371
|
+
`);
|
|
1372
|
+
return;
|
|
1373
|
+
}
|
|
1374
|
+
const { archive, crs, layer } = result;
|
|
1375
|
+
console.log(`${pc4.green("\u2713")} Wrote ${pc4.bold(archive.path)}.`);
|
|
1376
|
+
console.log(` Features: ${result.input.features}`);
|
|
1377
|
+
console.log(` Layer: ${layer.id} (${layer.fields.length} fields)`);
|
|
1378
|
+
console.log(` Zooms: ${archive.minzoom}-${archive.maxzoom}`);
|
|
1379
|
+
console.log(` Size: ${archive.byteCount} bytes in ${archive.tileContents} tiles`);
|
|
1380
|
+
console.log(` CRS: ${crs.label}${crs.reprojected ? ", reprojected to WGS84" : ""}`);
|
|
1381
|
+
console.log(` Bounds: ${archive.bounds.map((value) => value.toFixed(4)).join(", ")}`);
|
|
1382
|
+
const thinned = result.zooms.reduce((total, zoom) => total + zoom.thinnedFeatures, 0);
|
|
1383
|
+
if (thinned > 0) {
|
|
1384
|
+
console.log(` Thinned below zoom ${archive.maxzoom}: ${thinned} tile features`);
|
|
1385
|
+
}
|
|
1386
|
+
for (const warning of result.warnings) console.log(`${pc4.yellow("!")} ${warning}`);
|
|
1387
|
+
console.log(`Next: tileflow tileset publish ${archive.path} --id ${layer.id} --team @team`);
|
|
1388
|
+
} catch (error) {
|
|
1389
|
+
const record = error ?? {};
|
|
1390
|
+
const code = typeof record.code === "string" ? record.code : "TF_TILESET_BUILD_FAILED";
|
|
1391
|
+
const path = typeof record.path === "string" ? record.path : "input";
|
|
1392
|
+
const message = error instanceof Error ? error.message : "Tileset build failed.";
|
|
1393
|
+
if (options.json) {
|
|
1394
|
+
process.stderr.write(
|
|
1395
|
+
`${serializeCanonicalJson2({ error: { code, message, path }, ok: false, schemaVersion: 1 })}
|
|
1396
|
+
`
|
|
1397
|
+
);
|
|
1398
|
+
} else {
|
|
1399
|
+
console.error(`${pc4.red("Error:")} ${message}`);
|
|
1400
|
+
}
|
|
1401
|
+
process.exitCode = 1;
|
|
1402
|
+
}
|
|
1403
|
+
});
|
|
1404
|
+
}
|
|
1405
|
+
function settingsFromFlags(options, BuildError) {
|
|
1406
|
+
if (options.format !== void 0 && !formats.includes(options.format)) {
|
|
1407
|
+
throw new BuildError(
|
|
1408
|
+
"TF_TILESET_BUILD_FORMAT_UNSUPPORTED",
|
|
1409
|
+
"format",
|
|
1410
|
+
`--format must be one of: ${formats.join(", ")}.`
|
|
1411
|
+
);
|
|
1412
|
+
}
|
|
1413
|
+
if (options.include !== void 0 && options.exclude !== void 0) {
|
|
1414
|
+
throw new BuildError(
|
|
1415
|
+
"TF_TILESET_BUILD_FIELDS_INVALID",
|
|
1416
|
+
"fields",
|
|
1417
|
+
"--include and --exclude cannot be combined."
|
|
1418
|
+
);
|
|
1419
|
+
}
|
|
1420
|
+
if (options.hashFeatureId && options.featureId === void 0) {
|
|
1421
|
+
throw new BuildError(
|
|
1422
|
+
"TF_TILESET_BUILD_FEATURE_ID_INVALID",
|
|
1423
|
+
"featureId",
|
|
1424
|
+
"--hash-feature-id needs --feature-id."
|
|
1425
|
+
);
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
...options.attribution === void 0 ? {} : { attribution: options.attribution },
|
|
1429
|
+
...options.description === void 0 ? {} : { description: options.description },
|
|
1430
|
+
...options.exclude === void 0 ? {} : { exclude: fieldList(options.exclude) },
|
|
1431
|
+
...options.featureId === void 0 ? {} : { featureId: { field: options.featureId, ...options.hashFeatureId ? { hash: true } : {} } },
|
|
1432
|
+
...options.format === void 0 ? {} : { format: options.format },
|
|
1433
|
+
...options.include === void 0 ? {} : { include: fieldList(options.include) },
|
|
1434
|
+
...options.inputLayer === void 0 ? {} : { inputLayer: options.inputLayer },
|
|
1435
|
+
...options.latColumn === void 0 ? {} : { latColumn: options.latColumn },
|
|
1436
|
+
...options.layer === void 0 ? {} : { layer: options.layer },
|
|
1437
|
+
...options.lonColumn === void 0 ? {} : { lonColumn: options.lonColumn },
|
|
1438
|
+
...options.maxzoom === void 0 ? {} : { maxzoom: zoomValue(options.maxzoom) },
|
|
1439
|
+
minzoom: zoomValue(options.minzoom ?? "0"),
|
|
1440
|
+
...options.name === void 0 ? {} : { name: options.name },
|
|
1441
|
+
...options.rankAscending ? { rankAscending: true } : {},
|
|
1442
|
+
...options.rankBy === void 0 ? {} : { rankBy: options.rankBy },
|
|
1443
|
+
...options.reproject ? { reproject: true } : {},
|
|
1444
|
+
...options.sourceCrs === void 0 ? {} : { sourceCrs: options.sourceCrs },
|
|
1445
|
+
...options.zipEntry === void 0 ? {} : { zipEntry: options.zipEntry }
|
|
1446
|
+
};
|
|
1447
|
+
}
|
|
1448
|
+
function positiveInteger(value, flag, BuildError) {
|
|
1449
|
+
const parsed = /^[1-9]\d*$/u.test(value) ? Number(value) : Number.NaN;
|
|
1450
|
+
if (Number.isSafeInteger(parsed)) return parsed;
|
|
1451
|
+
throw new BuildError(
|
|
1452
|
+
"TF_TILESET_BUILD_OPTION_INVALID",
|
|
1453
|
+
flag.slice(2),
|
|
1454
|
+
`${flag} must be a positive integer.`
|
|
1455
|
+
);
|
|
1456
|
+
}
|
|
1457
|
+
function kebab(name) {
|
|
1458
|
+
return name.replace(/[A-Z]/gu, (letter) => `-${letter.toLowerCase()}`);
|
|
1459
|
+
}
|
|
1460
|
+
function fieldList(value) {
|
|
1461
|
+
return value.split(",").map((name) => name.trim()).filter(Boolean);
|
|
1462
|
+
}
|
|
1463
|
+
function zoomValue(value) {
|
|
1464
|
+
return /^\d+$/u.test(value) ? Number(value) : Number.NaN;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1083
1467
|
// src/tileset-upload.ts
|
|
1084
1468
|
import { createHash } from "crypto";
|
|
1085
1469
|
import { open as open2, stat } from "fs/promises";
|
|
1086
1470
|
import { z as z3 } from "zod";
|
|
1087
|
-
import { serializeCanonicalJson as
|
|
1471
|
+
import { serializeCanonicalJson as serializeCanonicalJson3 } from "@tileflow/core";
|
|
1088
1472
|
var teamTilesetPartSize = 16777216;
|
|
1089
|
-
var maximumTeamTilesetBytes =
|
|
1473
|
+
var maximumTeamTilesetBytes = 8e9;
|
|
1474
|
+
var archiveLimitMessage = `PMTiles archive exceeds the ${maximumTeamTilesetBytes.toLocaleString("en-US")}-byte platform maximum.`;
|
|
1475
|
+
var maximumTeamTilesetParts = Math.ceil(maximumTeamTilesetBytes / teamTilesetPartSize);
|
|
1090
1476
|
var authorizationBatchSize = 16;
|
|
1091
1477
|
var uploadConcurrency = 4;
|
|
1092
1478
|
var maximumPartAttempts = 4;
|
|
@@ -1107,14 +1493,10 @@ var TeamTilesetUploadError = class extends Error {
|
|
|
1107
1493
|
};
|
|
1108
1494
|
function createTeamTilesetPartLayout(byteCount) {
|
|
1109
1495
|
if (!Number.isSafeInteger(byteCount) || byteCount < 1 || byteCount > maximumTeamTilesetBytes) {
|
|
1110
|
-
throw new TeamTilesetUploadError(
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
actualBytes: Number.isSafeInteger(byteCount) && byteCount >= 0 ? byteCount : 0,
|
|
1115
|
-
limitBytes: maximumTeamTilesetBytes
|
|
1116
|
-
}
|
|
1117
|
-
);
|
|
1496
|
+
throw new TeamTilesetUploadError("TF_TILESET_VERSION_TOO_LARGE", archiveLimitMessage, {
|
|
1497
|
+
actualBytes: Number.isSafeInteger(byteCount) && byteCount >= 0 ? byteCount : 0,
|
|
1498
|
+
limitBytes: maximumTeamTilesetBytes
|
|
1499
|
+
});
|
|
1118
1500
|
}
|
|
1119
1501
|
return Object.freeze(
|
|
1120
1502
|
Array.from({ length: Math.ceil(byteCount / teamTilesetPartSize) }, (_, index) => {
|
|
@@ -1238,7 +1620,7 @@ async function prepareTeamTilesetUpload(file) {
|
|
|
1238
1620
|
};
|
|
1239
1621
|
return Object.freeze({
|
|
1240
1622
|
byteCount: identity.size,
|
|
1241
|
-
contentHash: createHash("sha256").update(
|
|
1623
|
+
contentHash: createHash("sha256").update(serializeCanonicalJson3(contentIdentity)).digest("hex"),
|
|
1242
1624
|
contentHashAlgorithm: "sha256-fixed-parts-v1",
|
|
1243
1625
|
identity,
|
|
1244
1626
|
partSize: teamTilesetPartSize,
|
|
@@ -1254,7 +1636,7 @@ function createTeamTilesetUploadIdempotencyKey(tilesetId, upload) {
|
|
|
1254
1636
|
);
|
|
1255
1637
|
}
|
|
1256
1638
|
return createHash("sha256").update(
|
|
1257
|
-
|
|
1639
|
+
serializeCanonicalJson3({
|
|
1258
1640
|
byteCount: upload.byteCount,
|
|
1259
1641
|
contentHash: upload.contentHash,
|
|
1260
1642
|
contentHashAlgorithm: upload.contentHashAlgorithm,
|
|
@@ -1628,11 +2010,10 @@ function checkedIdentity(device, inode, mtimeNanoseconds, size) {
|
|
|
1628
2010
|
throw new TeamTilesetUploadError("TF_TILESET_ARCHIVE_EMPTY", "PMTiles archive is empty.");
|
|
1629
2011
|
}
|
|
1630
2012
|
if (numericSize > maximumTeamTilesetBytes) {
|
|
1631
|
-
throw new TeamTilesetUploadError(
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
);
|
|
2013
|
+
throw new TeamTilesetUploadError("TF_TILESET_VERSION_TOO_LARGE", archiveLimitMessage, {
|
|
2014
|
+
actualBytes: numericSize,
|
|
2015
|
+
limitBytes: maximumTeamTilesetBytes
|
|
2016
|
+
});
|
|
1636
2017
|
}
|
|
1637
2018
|
return Object.freeze({ device, inode, mtimeNanoseconds, size: numericSize });
|
|
1638
2019
|
}
|
|
@@ -1683,12 +2064,12 @@ function asRecord2(value) {
|
|
|
1683
2064
|
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
1684
2065
|
}
|
|
1685
2066
|
function defaultSleep(milliseconds) {
|
|
1686
|
-
return new Promise((
|
|
2067
|
+
return new Promise((resolve3) => setTimeout(resolve3, milliseconds));
|
|
1687
2068
|
}
|
|
1688
2069
|
var identifierSchema = z3.string().min(20).max(80).regex(/^[A-Za-z0-9_-]+$/u);
|
|
1689
2070
|
var uploadPartStatusSchema = z3.object({
|
|
1690
2071
|
byteCount: z3.number().int().min(1).max(teamTilesetPartSize),
|
|
1691
|
-
partNumber: z3.number().int().min(1).max(
|
|
2072
|
+
partNumber: z3.number().int().min(1).max(maximumTeamTilesetParts),
|
|
1692
2073
|
uploaded: z3.boolean(),
|
|
1693
2074
|
validationState: z3.enum(["mismatch", "pending", "processing", "valid"])
|
|
1694
2075
|
}).strict();
|
|
@@ -1707,8 +2088,8 @@ var uploadControlSchema = z3.object({
|
|
|
1707
2088
|
contentHashAlgorithm: z3.literal("sha256-fixed-parts-v1"),
|
|
1708
2089
|
expiresAt: z3.iso.datetime({ offset: true }),
|
|
1709
2090
|
failure: z3.object({ code: z3.string().regex(/^TF_[A-Z0-9_]{1,96}$/u) }).strict().nullable(),
|
|
1710
|
-
partCount: z3.number().int().min(1).max(
|
|
1711
|
-
parts: z3.array(uploadPartStatusSchema).max(
|
|
2091
|
+
partCount: z3.number().int().min(1).max(maximumTeamTilesetParts),
|
|
2092
|
+
parts: z3.array(uploadPartStatusSchema).max(maximumTeamTilesetParts),
|
|
1712
2093
|
partSize: z3.literal(teamTilesetPartSize),
|
|
1713
2094
|
publishedVersion: z3.null(),
|
|
1714
2095
|
schemaVersion: z3.literal(1),
|
|
@@ -1736,7 +2117,7 @@ var authorizationSchema = z3.object({
|
|
|
1736
2117
|
"Content-MD5": z3.string().regex(/^[A-Za-z0-9+/]{22}==$/u)
|
|
1737
2118
|
}).strict(),
|
|
1738
2119
|
method: z3.literal("PUT"),
|
|
1739
|
-
partNumber: z3.number().int().min(1).max(
|
|
2120
|
+
partNumber: z3.number().int().min(1).max(maximumTeamTilesetParts),
|
|
1740
2121
|
url: z3.url().max(8192).refine((value) => {
|
|
1741
2122
|
const url = new URL(value);
|
|
1742
2123
|
return url.protocol === "https:" && !url.username && !url.password && !url.hash;
|
|
@@ -1749,7 +2130,7 @@ var authorizationResponseSchema = z3.object({
|
|
|
1749
2130
|
var partReceiptResponseSchema = z3.object({
|
|
1750
2131
|
changed: z3.boolean(),
|
|
1751
2132
|
etag: z3.string().regex(/^[0-9a-f]{32}$/u),
|
|
1752
|
-
partNumber: z3.number().int().min(1).max(
|
|
2133
|
+
partNumber: z3.number().int().min(1).max(maximumTeamTilesetParts),
|
|
1753
2134
|
schemaVersion: z3.literal(1)
|
|
1754
2135
|
}).strict();
|
|
1755
2136
|
|
|
@@ -1769,11 +2150,11 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
1769
2150
|
sample: options.sample
|
|
1770
2151
|
});
|
|
1771
2152
|
if (options.json) {
|
|
1772
|
-
process.stdout.write(`${
|
|
2153
|
+
process.stdout.write(`${serializeCanonicalJson4(inspection)}
|
|
1773
2154
|
`);
|
|
1774
2155
|
return;
|
|
1775
2156
|
}
|
|
1776
|
-
console.log(`${
|
|
2157
|
+
console.log(`${pc5.green("\u2713")} PMTiles archive is valid.`);
|
|
1777
2158
|
console.log(` Type: ${inspection.contract.tileType}`);
|
|
1778
2159
|
console.log(` Zooms: ${inspection.contract.minzoom}-${inspection.contract.maxzoom}`);
|
|
1779
2160
|
console.log(` Source layers: ${inspection.contract.sourceLayers.length}`);
|
|
@@ -1783,7 +2164,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
1783
2164
|
);
|
|
1784
2165
|
}
|
|
1785
2166
|
for (const warning of inspection.warnings) {
|
|
1786
|
-
console.log(`${
|
|
2167
|
+
console.log(`${pc5.yellow("!")} ${warning.message}`);
|
|
1787
2168
|
}
|
|
1788
2169
|
} catch (error) {
|
|
1789
2170
|
const record = asRecord3(error);
|
|
@@ -1792,11 +2173,11 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
1792
2173
|
const message = error instanceof Error ? error.message : "Tileset inspection failed.";
|
|
1793
2174
|
if (options.json) {
|
|
1794
2175
|
process.stderr.write(
|
|
1795
|
-
`${
|
|
2176
|
+
`${serializeCanonicalJson4({ error: { code, message, path }, ok: false, schemaVersion: 1 })}
|
|
1796
2177
|
`
|
|
1797
2178
|
);
|
|
1798
2179
|
} else {
|
|
1799
|
-
console.error(`${
|
|
2180
|
+
console.error(`${pc5.red("Error:")} ${message}`);
|
|
1800
2181
|
}
|
|
1801
2182
|
process.exitCode = 1;
|
|
1802
2183
|
}
|
|
@@ -1805,6 +2186,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
1805
2186
|
}
|
|
1806
2187
|
function registerTilesetCommands(program, dependencies) {
|
|
1807
2188
|
const tileset = program.command("tileset").description("Inspect and manage geospatial tilesets");
|
|
2189
|
+
registerTilesetBuildCommand(tileset);
|
|
1808
2190
|
registerTilesetInspectCommand(tileset);
|
|
1809
2191
|
tileset.command("publish").description("Register and publish one PMTiles archive to a Team").argument("<archive>", "repository-local PMTiles archive").requiredOption("--id <id>", "Team-local logical tileset ID").option("--name <name>", "display name").option("--attribution <text>", "data attribution").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-2 JSON").action(
|
|
1810
2192
|
async (archive, options) => {
|
|
@@ -1883,7 +2265,7 @@ function registerTilesetCommands(program, dependencies) {
|
|
|
1883
2265
|
};
|
|
1884
2266
|
if (options.json) return emitJson(document);
|
|
1885
2267
|
console.log(
|
|
1886
|
-
`${
|
|
2268
|
+
`${pc5.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc5.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
|
|
1887
2269
|
);
|
|
1888
2270
|
} catch (error) {
|
|
1889
2271
|
if (error instanceof TeamTilesetUploadError) {
|
|
@@ -1903,9 +2285,9 @@ function registerTilesetCommands(program, dependencies) {
|
|
|
1903
2285
|
const items = await fetchAllTeamTilesets(authority);
|
|
1904
2286
|
const document = { command: "tileset list", schemaVersion: 1, tilesets: items };
|
|
1905
2287
|
if (options.json) return emitJson(document);
|
|
1906
|
-
if (items.length === 0) return console.log(
|
|
2288
|
+
if (items.length === 0) return console.log(pc5.gray("No Team tilesets."));
|
|
1907
2289
|
for (const item of items) {
|
|
1908
|
-
console.log(` ${
|
|
2290
|
+
console.log(` ${pc5.bold(item.logicalName)} ${pc5.gray(item.status)}`);
|
|
1909
2291
|
}
|
|
1910
2292
|
} catch {
|
|
1911
2293
|
emitFailure(options.json, "tileset_list_failed", "Tileset list failed.");
|
|
@@ -1933,13 +2315,249 @@ function registerTilesetCommands(program, dependencies) {
|
|
|
1933
2315
|
const document = { command: "tileset status", schemaVersion: 1, tileset: parsed.tileset };
|
|
1934
2316
|
if (options.json) return emitJson(document);
|
|
1935
2317
|
const item = parsed.tileset;
|
|
1936
|
-
console.log(`${
|
|
2318
|
+
console.log(`${pc5.bold(item.name)} ${pc5.gray(item.state)}`);
|
|
1937
2319
|
console.log(` ID: ${item.id}`);
|
|
1938
2320
|
console.log(` Current version: ${String(item.currentVersion ?? "unpublished")}`);
|
|
1939
2321
|
} catch {
|
|
1940
2322
|
emitFailure(options.json, "tileset_status_failed", "Tileset status failed.");
|
|
1941
2323
|
}
|
|
1942
2324
|
});
|
|
2325
|
+
tileset.command("restore").description("Make a tileset's previous version current again, for every Map that uses it").argument("<id>", "Team-local logical ID or opaque resource ID").option("--expected-current-version <n>", "restore against this original current version").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(
|
|
2326
|
+
async (id, options) => {
|
|
2327
|
+
let commands;
|
|
2328
|
+
let attemptedVersion;
|
|
2329
|
+
const pinned = options.expectedCurrentVersion !== void 0;
|
|
2330
|
+
const refuse = (code, message, upload, uncertain = false) => emitGuidedFailure(
|
|
2331
|
+
options.json,
|
|
2332
|
+
code,
|
|
2333
|
+
message,
|
|
2334
|
+
commands ? tilesetRestoreSteps({
|
|
2335
|
+
...commands,
|
|
2336
|
+
code,
|
|
2337
|
+
upload,
|
|
2338
|
+
attemptedVersion,
|
|
2339
|
+
pinned,
|
|
2340
|
+
uncertain
|
|
2341
|
+
}) : [],
|
|
2342
|
+
upload ? { upload } : {}
|
|
2343
|
+
);
|
|
2344
|
+
try {
|
|
2345
|
+
if (!isTilesetId(id))
|
|
2346
|
+
return emitRestoreFailure(options.json, "invalid_tileset_id", "Tileset ID is invalid.");
|
|
2347
|
+
const requestedVersion = Number(options.expectedCurrentVersion);
|
|
2348
|
+
if (options.expectedCurrentVersion !== void 0 && (!/^\d+$/u.test(options.expectedCurrentVersion) || !Number.isSafeInteger(requestedVersion) || requestedVersion < 1)) {
|
|
2349
|
+
return emitGuidedFailure(
|
|
2350
|
+
options.json,
|
|
2351
|
+
"invalid_expected_current_version",
|
|
2352
|
+
"Expected current version must be a positive integer.",
|
|
2353
|
+
[]
|
|
2354
|
+
);
|
|
2355
|
+
}
|
|
2356
|
+
const authority = await resolveTeamAuthority(
|
|
2357
|
+
options,
|
|
2358
|
+
["tilesets:write"],
|
|
2359
|
+
dependencies,
|
|
2360
|
+
emitRestoreFailure
|
|
2361
|
+
);
|
|
2362
|
+
if (!authority) return;
|
|
2363
|
+
commands = tilesetCommands(id, authority, dependencies.defaultApiUrl);
|
|
2364
|
+
const path = `/v1/tilesets/${encodeURIComponent(id)}`;
|
|
2365
|
+
const detail = await authorizedTeamRequest(authority, path, "GET");
|
|
2366
|
+
if (!detail.ok) {
|
|
2367
|
+
return refuse(`http_${detail.status}`, `Tileset status failed (${detail.status}).`);
|
|
2368
|
+
}
|
|
2369
|
+
const selected = tilesetDetailResponseSchema.parse(detail.body).tileset;
|
|
2370
|
+
const currentVersion = selected.currentVersion;
|
|
2371
|
+
if (!authority.team)
|
|
2372
|
+
commands = tilesetCommands(selected.id, authority, dependencies.defaultApiUrl);
|
|
2373
|
+
const expectedCurrentVersion = pinned ? requestedVersion : currentVersion;
|
|
2374
|
+
if (expectedCurrentVersion === null) {
|
|
2375
|
+
return refuse("no_previous_version", `${id} has no previous version to restore.`);
|
|
2376
|
+
}
|
|
2377
|
+
attemptedVersion = expectedCurrentVersion;
|
|
2378
|
+
const response = await authorizedTeamRequest(authority, `${path}/restore`, "POST", {
|
|
2379
|
+
body: JSON.stringify({ expectedCurrentVersion }),
|
|
2380
|
+
headers: { "Content-Type": "application/json" }
|
|
2381
|
+
});
|
|
2382
|
+
if (!response.ok) {
|
|
2383
|
+
const failure = tilesetRestoreFailureSchema.safeParse(response.body);
|
|
2384
|
+
const body = failure.success ? failure.data : {};
|
|
2385
|
+
return refuse(
|
|
2386
|
+
body.code ?? `http_${response.status}`,
|
|
2387
|
+
body.error ? `${body.error.replace(/\.$/u, "")}.` : `Tileset restore failed (${response.status}).`,
|
|
2388
|
+
body.upload,
|
|
2389
|
+
response.status >= 500 || !failure.success || !response.json
|
|
2390
|
+
);
|
|
2391
|
+
}
|
|
2392
|
+
const restored = tilesetRestoreResponseSchema.parse(response.body).tileset;
|
|
2393
|
+
const nextSteps = restored.outcome === "repaired" ? [
|
|
2394
|
+
{
|
|
2395
|
+
description: "That finished an earlier change. Nothing was swapped this time; run restore again only if you still want to go back a version.",
|
|
2396
|
+
command: commands.status
|
|
2397
|
+
}
|
|
2398
|
+
] : [
|
|
2399
|
+
{
|
|
2400
|
+
description: `To undo, ${pinned ? "run this command" : "run the same command"} to swap versions ${restored.currentVersion} and ${restored.previousVersion} back.`,
|
|
2401
|
+
command: pinned ? `${commands.restore} --expected-current-version ${restored.currentVersion}` : commands.restore
|
|
2402
|
+
},
|
|
2403
|
+
{
|
|
2404
|
+
description: "Publishing a new version retires the one this restore replaced, so check the archive first."
|
|
2405
|
+
}
|
|
2406
|
+
];
|
|
2407
|
+
if (options.json) {
|
|
2408
|
+
return emitJson({
|
|
2409
|
+
command: "tileset restore",
|
|
2410
|
+
nextSteps,
|
|
2411
|
+
ok: true,
|
|
2412
|
+
schemaVersion: 1,
|
|
2413
|
+
tileset: restored
|
|
2414
|
+
});
|
|
2415
|
+
}
|
|
2416
|
+
console.log(
|
|
2417
|
+
restored.outcome === "repaired" ? `${pc5.green("\u2713")} ${pc5.bold(id)} is at version ${restored.currentVersion}, and readers have caught up.` : `${pc5.green("\u2713")} ${pc5.bold(id)} is at version ${restored.currentVersion} again; version ${restored.previousVersion} is its previous one. Maps read it within minutes, without deploying.`
|
|
2418
|
+
);
|
|
2419
|
+
printNextSteps(nextSteps);
|
|
2420
|
+
} catch {
|
|
2421
|
+
if (attemptedVersion !== void 0)
|
|
2422
|
+
return refuse(
|
|
2423
|
+
"tileset_restore_failed",
|
|
2424
|
+
"The restore outcome could not be confirmed.",
|
|
2425
|
+
void 0,
|
|
2426
|
+
true
|
|
2427
|
+
);
|
|
2428
|
+
emitRestoreFailure(options.json, "tileset_restore_failed", "Tileset restore failed.");
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
);
|
|
2432
|
+
tileset.command("upload").description("Manage an upload that has not been published").command("abort").description("Abort the upload in progress on one tileset, so a restore or publish can proceed").argument("<id>", "Team-local logical ID or opaque resource ID").option("--upload-id <id>", "abort only this upload session").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
|
|
2433
|
+
try {
|
|
2434
|
+
if (!isTilesetId(id))
|
|
2435
|
+
return emitRestoreFailure(options.json, "invalid_tileset_id", "Tileset ID is invalid.");
|
|
2436
|
+
const authority = await resolveTeamAuthority(
|
|
2437
|
+
options,
|
|
2438
|
+
["tilesets:write"],
|
|
2439
|
+
dependencies,
|
|
2440
|
+
emitRestoreFailure
|
|
2441
|
+
);
|
|
2442
|
+
if (!authority) return;
|
|
2443
|
+
let commands = tilesetCommands(id, authority, dependencies.defaultApiUrl);
|
|
2444
|
+
const check = () => [{ description: "Check the tileset.", command: commands.status }];
|
|
2445
|
+
const detail = await authorizedTeamRequest(
|
|
2446
|
+
authority,
|
|
2447
|
+
`/v1/tilesets/${encodeURIComponent(id)}`,
|
|
2448
|
+
"GET"
|
|
2449
|
+
);
|
|
2450
|
+
if (!detail.ok) {
|
|
2451
|
+
return emitGuidedFailure(
|
|
2452
|
+
options.json,
|
|
2453
|
+
`http_${detail.status}`,
|
|
2454
|
+
`Tileset status failed (${detail.status}).`,
|
|
2455
|
+
check()
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
const tilesetId = tilesetDetailResponseSchema.parse(detail.body).tileset.id;
|
|
2459
|
+
if (!authority.team)
|
|
2460
|
+
commands = tilesetCommands(tilesetId, authority, dependencies.defaultApiUrl);
|
|
2461
|
+
const uploads = `/v1/tilesets/${encodeURIComponent(tilesetId)}/uploads`;
|
|
2462
|
+
const active = await authorizedTeamRequest(authority, uploads, "GET");
|
|
2463
|
+
if (!active.ok) {
|
|
2464
|
+
return emitGuidedFailure(
|
|
2465
|
+
options.json,
|
|
2466
|
+
`http_${active.status}`,
|
|
2467
|
+
`Reading the upload failed (${active.status}).`,
|
|
2468
|
+
check()
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
const { upload } = activeUploadResponseSchema.parse(active.body);
|
|
2472
|
+
const proceed = [
|
|
2473
|
+
{
|
|
2474
|
+
description: "Restore the previous version if that was the aim.",
|
|
2475
|
+
command: commands.restore
|
|
2476
|
+
}
|
|
2477
|
+
];
|
|
2478
|
+
if (!upload) {
|
|
2479
|
+
if (options.json) {
|
|
2480
|
+
return emitJson({
|
|
2481
|
+
aborted: false,
|
|
2482
|
+
command: "tileset upload abort",
|
|
2483
|
+
nextSteps: proceed,
|
|
2484
|
+
ok: true,
|
|
2485
|
+
schemaVersion: 1,
|
|
2486
|
+
upload: null
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
console.log(pc5.gray(`No upload is in progress on ${id}.`));
|
|
2490
|
+
return printNextSteps(proceed);
|
|
2491
|
+
}
|
|
2492
|
+
if (options.uploadId !== void 0 && options.uploadId !== upload.id) {
|
|
2493
|
+
return emitGuidedFailure(
|
|
2494
|
+
options.json,
|
|
2495
|
+
"tileset_upload_changed",
|
|
2496
|
+
"The active upload changed. Nothing was aborted.",
|
|
2497
|
+
check(),
|
|
2498
|
+
{ upload }
|
|
2499
|
+
);
|
|
2500
|
+
}
|
|
2501
|
+
const response = await authorizedTeamRequest(
|
|
2502
|
+
authority,
|
|
2503
|
+
`${uploads}/${encodeURIComponent(upload.id)}`,
|
|
2504
|
+
"DELETE"
|
|
2505
|
+
);
|
|
2506
|
+
if (!response.ok) {
|
|
2507
|
+
const failure = tilesetRestoreFailureSchema.safeParse(response.body);
|
|
2508
|
+
const body = failure.success ? failure.data : {};
|
|
2509
|
+
return emitGuidedFailure(
|
|
2510
|
+
options.json,
|
|
2511
|
+
body.code ?? `http_${response.status}`,
|
|
2512
|
+
body.error ? `${body.error.replace(/\.$/u, "")}.` : `Aborting upload ${upload.id} failed (${response.status}).`,
|
|
2513
|
+
[
|
|
2514
|
+
{
|
|
2515
|
+
description: `Upload ${upload.id} was ${upload.state}. A late stage finishes on its own: wait a minute, then check it.`,
|
|
2516
|
+
command: commands.status
|
|
2517
|
+
}
|
|
2518
|
+
],
|
|
2519
|
+
{ upload }
|
|
2520
|
+
);
|
|
2521
|
+
}
|
|
2522
|
+
const result = parseUploadSession(response.body);
|
|
2523
|
+
if (result.state === "published" || result.uploadId !== upload.id) {
|
|
2524
|
+
return emitGuidedFailure(
|
|
2525
|
+
options.json,
|
|
2526
|
+
"tileset_upload_abort_unconfirmed",
|
|
2527
|
+
"The cancellation response did not identify the requested upload.",
|
|
2528
|
+
check()
|
|
2529
|
+
);
|
|
2530
|
+
}
|
|
2531
|
+
const aborted = result.state === "aborted";
|
|
2532
|
+
const nextSteps = aborted ? proceed : check();
|
|
2533
|
+
const updated = {
|
|
2534
|
+
...upload,
|
|
2535
|
+
state: result.state,
|
|
2536
|
+
byteCount: result.byteCount,
|
|
2537
|
+
expiresAt: result.expiresAt
|
|
2538
|
+
};
|
|
2539
|
+
if (options.json) {
|
|
2540
|
+
return emitJson({
|
|
2541
|
+
aborted,
|
|
2542
|
+
command: "tileset upload abort",
|
|
2543
|
+
nextSteps,
|
|
2544
|
+
ok: true,
|
|
2545
|
+
schemaVersion: 1,
|
|
2546
|
+
upload: updated
|
|
2547
|
+
});
|
|
2548
|
+
}
|
|
2549
|
+
const ended = result.state === "expired" || result.state === "rejected";
|
|
2550
|
+
const message = aborted ? `Aborted upload ${upload.id} on ${id}.` : ended ? `Upload ${upload.id} is ${result.state}.` : `Cancellation of upload ${upload.id} is pending (${result.state}).`;
|
|
2551
|
+
console.log(message);
|
|
2552
|
+
printNextSteps(nextSteps);
|
|
2553
|
+
} catch {
|
|
2554
|
+
emitRestoreFailure(
|
|
2555
|
+
options.json,
|
|
2556
|
+
"tileset_upload_abort_failed",
|
|
2557
|
+
"Aborting the upload failed."
|
|
2558
|
+
);
|
|
2559
|
+
}
|
|
2560
|
+
});
|
|
1943
2561
|
tileset.command("purge").description("Permanently purge one unreferenced Team tileset").argument("<id>", "Team-local logical ID or opaque resource ID").requiredOption("--confirm <id>", "repeat the exact tileset ID").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
|
|
1944
2562
|
try {
|
|
1945
2563
|
if (!isTilesetId(id) || options.confirm !== id) {
|
|
@@ -1983,12 +2601,22 @@ function registerTilesetCommands(program, dependencies) {
|
|
|
1983
2601
|
const parsed = tilesetPurgeResponseSchema.parse(response.body);
|
|
1984
2602
|
const document = { command: "tileset purge", schemaVersion: 1, ...parsed };
|
|
1985
2603
|
if (options.json) return emitJson(document);
|
|
1986
|
-
console.log(`${
|
|
2604
|
+
console.log(`${pc5.green("\u2713")} Purge ${parsed.reclamation}.`);
|
|
1987
2605
|
} catch {
|
|
1988
2606
|
emitFailure(options.json, "tileset_purge_failed", "Tileset purge failed.");
|
|
1989
2607
|
}
|
|
1990
2608
|
});
|
|
1991
2609
|
}
|
|
2610
|
+
function tilesetCommands(id, authority, defaultApiUrl) {
|
|
2611
|
+
const origin = apiOriginOption(authority.apiOrigin, defaultApiUrl);
|
|
2612
|
+
const target = `${id}${authority.team ? ` --team @${authority.team.slug}` : ""}${origin}`;
|
|
2613
|
+
return {
|
|
2614
|
+
abort: `tileflow tileset upload abort ${target}`,
|
|
2615
|
+
restore: `tileflow tileset restore ${target}`,
|
|
2616
|
+
status: `tileflow tileset status ${target}`,
|
|
2617
|
+
origin
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
1992
2620
|
async function fetchAllTeamTilesets(authority) {
|
|
1993
2621
|
const items = [];
|
|
1994
2622
|
const seenCursors = /* @__PURE__ */ new Set();
|
|
@@ -2073,6 +2701,28 @@ var tilesetDependencySchema = z4.object({
|
|
|
2073
2701
|
mapId: safeIdentifierSchema2,
|
|
2074
2702
|
sourceId: safeIdentifierSchema2
|
|
2075
2703
|
}).strict();
|
|
2704
|
+
var tilesetRestoreResponseSchema = z4.object({
|
|
2705
|
+
schemaVersion: z4.literal(1),
|
|
2706
|
+
tileset: z4.object({
|
|
2707
|
+
currentVersion: z4.number().int().positive(),
|
|
2708
|
+
id: physicalTilesetIdSchema,
|
|
2709
|
+
outcome: z4.enum(["repaired", "restored"]),
|
|
2710
|
+
previousVersion: z4.number().int().positive().nullable(),
|
|
2711
|
+
slug: z4.string().min(1).max(128)
|
|
2712
|
+
}).strict()
|
|
2713
|
+
}).strict();
|
|
2714
|
+
var blockingUploadSchema = z4.object({
|
|
2715
|
+
byteCount: z4.number().int().nonnegative(),
|
|
2716
|
+
expiresAt: z4.string().min(1).max(64),
|
|
2717
|
+
id: safeIdentifierSchema2.max(128),
|
|
2718
|
+
state: z4.string().min(1).max(32)
|
|
2719
|
+
}).strict();
|
|
2720
|
+
var activeUploadResponseSchema = z4.object({ schemaVersion: z4.literal(1), upload: blockingUploadSchema.nullable() }).strict();
|
|
2721
|
+
var tilesetRestoreFailureSchema = z4.object({
|
|
2722
|
+
code: z4.string().max(80).optional(),
|
|
2723
|
+
error: z4.string().max(400).optional(),
|
|
2724
|
+
upload: blockingUploadSchema.optional()
|
|
2725
|
+
});
|
|
2076
2726
|
var tilesetDetailResponseSchema = z4.object({
|
|
2077
2727
|
schemaVersion: z4.literal(1),
|
|
2078
2728
|
tileset: z4.object({
|
|
@@ -2117,9 +2767,7 @@ export {
|
|
|
2117
2767
|
normalizeApiOrigin,
|
|
2118
2768
|
parseProjectReference,
|
|
2119
2769
|
createCliProgram,
|
|
2120
|
-
|
|
2121
|
-
hostedApiUrlOption,
|
|
2122
|
-
isHostedApiKey,
|
|
2770
|
+
configureJsonErrors,
|
|
2123
2771
|
apiProfileCanTargetMap,
|
|
2124
2772
|
requestHostedJson,
|
|
2125
2773
|
startDeviceAuthorization,
|
|
@@ -2135,11 +2783,21 @@ export {
|
|
|
2135
2783
|
uploadHostedFontBundle,
|
|
2136
2784
|
fetchHostedMapStatus,
|
|
2137
2785
|
revokeHostedAccountSession,
|
|
2786
|
+
hostedApiKeyOption,
|
|
2787
|
+
hostedApiUrlOption,
|
|
2788
|
+
isHostedApiKey,
|
|
2138
2789
|
resolveTeamAuthority,
|
|
2139
2790
|
authorizedTeamRequest,
|
|
2140
2791
|
emitJson,
|
|
2141
2792
|
emitFailure,
|
|
2142
2793
|
safeMessage,
|
|
2794
|
+
printNextSteps,
|
|
2795
|
+
emitGuidedFailure,
|
|
2796
|
+
emitRestoreFailure,
|
|
2797
|
+
apiOriginOption,
|
|
2798
|
+
deploymentRestoreSteps,
|
|
2799
|
+
isRestoreJsonCommand,
|
|
2800
|
+
parseRestoreJson,
|
|
2143
2801
|
registerTilesetInspectCommand,
|
|
2144
2802
|
registerTilesetCommands
|
|
2145
2803
|
};
|