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.
@@ -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
- // src/hosted-options.ts
224
- import { Option } from "commander";
225
- function hostedApiKeyOption(description = "Tileflow API key") {
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((resolve2) => setTimeout(resolve2, milliseconds)));
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((resolve2, reject) => {
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
- resolve2(value);
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/tileset-command.ts
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 { z as z4 } from "zod";
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
- emitFailure(options.json, "invalid_api_url", safeMessage(error));
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
- emitFailure(options.json, "invalid_team_data_key", "Team data API key is invalid.");
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
- emitFailure(
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
- emitFailure(
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
- emitFailure(options.json, "auth_state_unavailable", safeMessage(error));
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
- emitFailure(
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
- emitFailure(options.json, "team_discovery_failed", discovered.error);
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
- emitFailure(options.json, "invalid_team", "Team must use @team syntax.");
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
- emitFailure(
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
- emitFailure(
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 serializeCanonicalJson2 } from "@tileflow/core";
1471
+ import { serializeCanonicalJson as serializeCanonicalJson3 } from "@tileflow/core";
1088
1472
  var teamTilesetPartSize = 16777216;
1089
- var maximumTeamTilesetBytes = 4e9;
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
- "TF_TILESET_VERSION_TOO_LARGE",
1112
- "PMTiles archive exceeds the 4,000,000,000-byte platform maximum.",
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(serializeCanonicalJson2(contentIdentity)).digest("hex"),
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
- serializeCanonicalJson2({
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
- "TF_TILESET_VERSION_TOO_LARGE",
1633
- "PMTiles archive exceeds the 4,000,000,000-byte platform maximum.",
1634
- { actualBytes: numericSize, limitBytes: maximumTeamTilesetBytes }
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((resolve2) => setTimeout(resolve2, milliseconds));
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(239),
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(239),
1711
- parts: z3.array(uploadPartStatusSchema).max(239),
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(239),
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(239),
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(`${serializeCanonicalJson3(inspection)}
2153
+ process.stdout.write(`${serializeCanonicalJson4(inspection)}
1773
2154
  `);
1774
2155
  return;
1775
2156
  }
1776
- console.log(`${pc3.green("\u2713")} PMTiles archive is valid.`);
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(`${pc3.yellow("!")} ${warning.message}`);
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
- `${serializeCanonicalJson3({ error: { code, message, path }, ok: false, schemaVersion: 1 })}
2176
+ `${serializeCanonicalJson4({ error: { code, message, path }, ok: false, schemaVersion: 1 })}
1796
2177
  `
1797
2178
  );
1798
2179
  } else {
1799
- console.error(`${pc3.red("Error:")} ${message}`);
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
- `${pc3.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc3.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
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(pc3.gray("No Team tilesets."));
2288
+ if (items.length === 0) return console.log(pc5.gray("No Team tilesets."));
1907
2289
  for (const item of items) {
1908
- console.log(` ${pc3.bold(item.logicalName)} ${pc3.gray(item.status)}`);
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(`${pc3.bold(item.name)} ${pc3.gray(item.state)}`);
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(`${pc3.green("\u2713")} Purge ${parsed.reclamation}.`);
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
- hostedApiKeyOption,
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
  };