tileflow 0.0.0-bootstrap.0 → 0.1.0-alpha.0
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 +153 -62
- package/dist/index.js +2126 -358
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -3,20 +3,20 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { createHash as
|
|
6
|
+
import { createHash as createHash4, randomBytes as randomBytes2 } from "crypto";
|
|
7
7
|
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
8
|
-
import { readFile as
|
|
8
|
+
import { readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
|
|
9
9
|
import { hostname } from "os";
|
|
10
|
-
import { dirname as
|
|
10
|
+
import { dirname as dirname10, isAbsolute as isAbsolute4, relative as relative5, resolve as resolve7, sep as sep5 } from "path";
|
|
11
11
|
import { createInterface } from "readline/promises";
|
|
12
|
-
import
|
|
12
|
+
import pc3 from "picocolors";
|
|
13
13
|
import {
|
|
14
|
+
auditTileflowMapThemeValues,
|
|
14
15
|
getTileflowStyleFontFaces,
|
|
15
|
-
|
|
16
|
+
parseResolvedTileflowMap as parseResolvedTileflowMap3,
|
|
16
17
|
tileflowMapIdSchema as tileflowMapIdSchema2
|
|
17
18
|
} from "@tileflow/core";
|
|
18
19
|
import {
|
|
19
|
-
collectTileflowMapBuildLineage,
|
|
20
20
|
createTileflowMapBuildManifest
|
|
21
21
|
} from "@tileflow/core/build";
|
|
22
22
|
import {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
createTileflowArtifactSession as createTileflowArtifactSession3,
|
|
28
28
|
createTileflowBuildProvenance,
|
|
29
29
|
createTileflowStyles,
|
|
30
|
+
prepareTileflowLocalTilesets,
|
|
30
31
|
writeTileflowBuildArtifacts
|
|
31
32
|
} from "@tileflow/dev/artifacts";
|
|
32
33
|
import {
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
defaultTileflowConfigPath,
|
|
36
37
|
defaultTileflowManifestPath,
|
|
37
38
|
getTileflowMapNames as getTileflowMapNames3,
|
|
38
|
-
loadTileflowConfigWithInputs as
|
|
39
|
+
loadTileflowConfigWithInputs as loadTileflowConfigWithInputs3,
|
|
39
40
|
TileflowValidationError as TileflowValidationError4
|
|
40
41
|
} from "@tileflow/dev/config";
|
|
41
42
|
import {
|
|
@@ -50,9 +51,10 @@ import {
|
|
|
50
51
|
createTileflowDevRequestHandler
|
|
51
52
|
} from "@tileflow/dev/server";
|
|
52
53
|
import {
|
|
53
|
-
createTileflowCommandFailureDocument as
|
|
54
|
-
createTileflowCommandSummary,
|
|
55
|
-
|
|
54
|
+
createTileflowCommandFailureDocument as createTileflowCommandFailureDocument4,
|
|
55
|
+
createTileflowCommandSummary as createTileflowCommandSummary2,
|
|
56
|
+
createTileflowStructuredDiagnostics as createTileflowStructuredDiagnostics2,
|
|
57
|
+
serializeTileflowCommandDocument as serializeTileflowCommandDocument4,
|
|
56
58
|
TileflowStyleValidationError as TileflowStyleValidationError2
|
|
57
59
|
} from "@tileflow/dev/validation";
|
|
58
60
|
|
|
@@ -265,6 +267,405 @@ function compareCodeUnits(left, right) {
|
|
|
265
267
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
266
268
|
}
|
|
267
269
|
|
|
270
|
+
// src/ai-commands.ts
|
|
271
|
+
import { dirname as dirname2 } from "path";
|
|
272
|
+
import {
|
|
273
|
+
createStyleResult,
|
|
274
|
+
diffTileflowMaps,
|
|
275
|
+
parseResolvedTileflowMap,
|
|
276
|
+
tileflowAuthoringManifestSchemaVersion
|
|
277
|
+
} from "@tileflow/core";
|
|
278
|
+
import { loadTileflowConfigWithInputs } from "@tileflow/dev/config";
|
|
279
|
+
import { prepareTileflowCatalogIcons } from "@tileflow/dev/icons";
|
|
280
|
+
import { sanitizeTileflowInspectableValue } from "@tileflow/dev/inspect";
|
|
281
|
+
import {
|
|
282
|
+
createTileflowCommandFailureDocument,
|
|
283
|
+
createTileflowCommandSummary,
|
|
284
|
+
createTileflowStructuredDiagnostics,
|
|
285
|
+
sanitizeDiagnosticSecrets,
|
|
286
|
+
serializeTileflowCommandDocument
|
|
287
|
+
} from "@tileflow/dev/validation";
|
|
288
|
+
|
|
289
|
+
// src/config-execution.ts
|
|
290
|
+
function hideTileflowConfigSecrets() {
|
|
291
|
+
const hadApiKey = Object.hasOwn(process.env, "TILEFLOW_API_KEY");
|
|
292
|
+
const apiKey = process.env.TILEFLOW_API_KEY;
|
|
293
|
+
const argv = [...process.argv];
|
|
294
|
+
let restored = false;
|
|
295
|
+
delete process.env.TILEFLOW_API_KEY;
|
|
296
|
+
replaceProcessArgv(stripApiKeyArguments(argv));
|
|
297
|
+
return {
|
|
298
|
+
restore() {
|
|
299
|
+
if (restored) return;
|
|
300
|
+
restored = true;
|
|
301
|
+
if (hadApiKey) {
|
|
302
|
+
process.env.TILEFLOW_API_KEY = apiKey;
|
|
303
|
+
} else {
|
|
304
|
+
delete process.env.TILEFLOW_API_KEY;
|
|
305
|
+
}
|
|
306
|
+
replaceProcessArgv(argv);
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
async function withTileflowConfigSecretsHidden(operation) {
|
|
311
|
+
const scope = hideTileflowConfigSecrets();
|
|
312
|
+
try {
|
|
313
|
+
return await operation();
|
|
314
|
+
} finally {
|
|
315
|
+
scope.restore();
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function stripApiKeyArguments(argv) {
|
|
319
|
+
const sanitized = [];
|
|
320
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
321
|
+
const argument = argv[index];
|
|
322
|
+
if (argument === "--api-key") {
|
|
323
|
+
index += 1;
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
if (argument.startsWith("--api-key=")) continue;
|
|
327
|
+
sanitized.push(argument);
|
|
328
|
+
}
|
|
329
|
+
return sanitized;
|
|
330
|
+
}
|
|
331
|
+
function replaceProcessArgv(argv) {
|
|
332
|
+
process.argv.splice(0, process.argv.length, ...argv);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// src/ai-commands.ts
|
|
336
|
+
function registerAiCommands(program2, dependencies) {
|
|
337
|
+
program2.command("explain").description("Compile one map/theme and explain semantic compiler decisions").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--map <name>", "compile one exact configured map").option("--theme <name>", "compile one exact concrete theme; defaults to the map default").option("--inspection", "include physical-layer provenance in the compilation report").option("--api-base-url <url>", "Tileflow API base URL", dependencies.defaultApiUrl).option("--json", "print deterministic schema-version-1 JSON").action(
|
|
338
|
+
async (options) => runExplain(options, dependencies.createStyleResult ?? createStyleResult)
|
|
339
|
+
);
|
|
340
|
+
program2.command("semantic-diff").description("Compare resolved semantic maps from two configs or one workspace").option("-c, --config <path>", "workspace config path for the --from/--to shortcut").option("--from <map>", "before map ID in --config workspace mode").option("--to <map>", "after map ID in --config workspace mode").option("--from-config <path>", "config path used as the before state").option("--to-config <path>", "config path used as the after state").option("--from-map <map>", "before map ID; required only when --from-config is multi-map").option("--to-map <map>", "after map ID; required only when --to-config is multi-map").option("--json", "print deterministic schema-version-1 JSON").action(
|
|
341
|
+
async (options) => runSemanticDiff(options, dependencies.defaultConfigPath)
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
async function runExplain(options, compileStyle) {
|
|
345
|
+
let selection;
|
|
346
|
+
try {
|
|
347
|
+
const output = await withTileflowConfigSecretsHidden(async () => {
|
|
348
|
+
const loaded = await loadTileflowConfigWithInputs(options.config);
|
|
349
|
+
const mapName = selectMap(loaded, options.map, "EXPLAIN");
|
|
350
|
+
const themeName = selectTheme(loaded, mapName, options.theme);
|
|
351
|
+
selection = { map: mapName, theme: themeName };
|
|
352
|
+
const prepared = await prepareTileflowCatalogIcons(loaded.project, {
|
|
353
|
+
assetBaseUrl: "/tileflow",
|
|
354
|
+
baseDirectory: dirname2(loaded.configFile),
|
|
355
|
+
cwd: process.cwd()
|
|
356
|
+
});
|
|
357
|
+
const authoredMap = loaded.authoringMaps[mapName] ?? loaded.project.maps[mapName];
|
|
358
|
+
const metadata = loaded.project.mapMetadata?.[mapName];
|
|
359
|
+
const result = compileStyle(authoredMap, {
|
|
360
|
+
apiBaseUrl: options.apiBaseUrl,
|
|
361
|
+
inspection: options.inspection,
|
|
362
|
+
map: metadata ? {
|
|
363
|
+
id: metadata.id,
|
|
364
|
+
lineage: metadata.lineage.map(({ id }) => id),
|
|
365
|
+
version: metadata.version
|
|
366
|
+
} : void 0,
|
|
367
|
+
preparedAssets: prepared.mapAssets[mapName],
|
|
368
|
+
theme: themeName
|
|
369
|
+
});
|
|
370
|
+
const structuredDiagnostics = result.diagnostics.length === 0 ? [] : createTileflowStructuredDiagnostics({ diagnostics: result.diagnostics }, process.cwd(), {
|
|
371
|
+
code: "COMPILATION_FAILED",
|
|
372
|
+
phase: "compilation"
|
|
373
|
+
});
|
|
374
|
+
const compilation = {
|
|
375
|
+
diagnostics: structuredDiagnostics.map(toSafeCompilationDiagnostic),
|
|
376
|
+
ok: result.ok,
|
|
377
|
+
report: result.report
|
|
378
|
+
};
|
|
379
|
+
return { compilation, result, structuredDiagnostics };
|
|
380
|
+
});
|
|
381
|
+
if (!selection) throw new Error("Explain selection was not resolved.");
|
|
382
|
+
if (!output.result.ok) {
|
|
383
|
+
const failure = createTileflowCommandFailureDocument(
|
|
384
|
+
"explain",
|
|
385
|
+
{ diagnostics: output.structuredDiagnostics },
|
|
386
|
+
process.cwd(),
|
|
387
|
+
{ code: "COMPILATION_FAILED", phase: "compilation" }
|
|
388
|
+
);
|
|
389
|
+
const document2 = {
|
|
390
|
+
...failure,
|
|
391
|
+
authoringManifestSchemaVersion: tileflowAuthoringManifestSchemaVersion,
|
|
392
|
+
selection,
|
|
393
|
+
compilation: output.compilation
|
|
394
|
+
};
|
|
395
|
+
if (options.json) process.stderr.write(serializeTileflowCommandDocument(document2));
|
|
396
|
+
else printHumanExplainFailure(document2);
|
|
397
|
+
process.exitCode = 1;
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
const summary = createTileflowCommandSummary({
|
|
401
|
+
code: "COMPILATION_EXPLAINED",
|
|
402
|
+
command: "explain",
|
|
403
|
+
message: `Compiled and explained ${selection.map}/${selection.theme}.`,
|
|
404
|
+
ok: true,
|
|
405
|
+
path: "",
|
|
406
|
+
phase: "compilation",
|
|
407
|
+
severity: "info",
|
|
408
|
+
suggestion: "Use the report targets and planner decisions to make semantic authoring edits."
|
|
409
|
+
});
|
|
410
|
+
const document = {
|
|
411
|
+
...summary,
|
|
412
|
+
authoringManifestSchemaVersion: tileflowAuthoringManifestSchemaVersion,
|
|
413
|
+
selection,
|
|
414
|
+
compilation: output.compilation,
|
|
415
|
+
diagnostics: output.structuredDiagnostics
|
|
416
|
+
};
|
|
417
|
+
if (options.json) process.stdout.write(serializeTileflowCommandDocument(document));
|
|
418
|
+
else printHumanExplain(document);
|
|
419
|
+
} catch (error) {
|
|
420
|
+
const failure = createTileflowCommandFailureDocument("explain", error, process.cwd(), {
|
|
421
|
+
code: "EXPLAIN_FAILED",
|
|
422
|
+
phase: "compilation"
|
|
423
|
+
});
|
|
424
|
+
if (options.json) process.stderr.write(serializeTileflowCommandDocument(failure));
|
|
425
|
+
else printHumanCommandFailure(failure);
|
|
426
|
+
process.exitCode = 1;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
async function runSemanticDiff(options, defaultConfigPath2) {
|
|
430
|
+
try {
|
|
431
|
+
const difference = await withTileflowConfigSecretsHidden(async () => {
|
|
432
|
+
const plan = createSemanticDiffPlan(options, defaultConfigPath2);
|
|
433
|
+
if (plan.mode === "configs") {
|
|
434
|
+
const fromLoaded = await loadTileflowConfigWithInputs(plan.fromConfig);
|
|
435
|
+
const fromName2 = selectConfigMap(fromLoaded, plan.fromMap, "from");
|
|
436
|
+
const toLoaded = await loadTileflowConfigWithInputs(plan.toConfig);
|
|
437
|
+
const toName2 = selectConfigMap(toLoaded, plan.toMap, "to");
|
|
438
|
+
return sanitizeSemanticDiff(
|
|
439
|
+
diffTileflowMaps(
|
|
440
|
+
fromLoaded.authoringMaps[fromName2] ?? fromLoaded.project.maps[fromName2],
|
|
441
|
+
toLoaded.authoringMaps[toName2] ?? toLoaded.project.maps[toName2]
|
|
442
|
+
),
|
|
443
|
+
process.cwd()
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
const loaded = await loadTileflowConfigWithInputs(plan.config);
|
|
447
|
+
const fromName = selectExactMap(loaded, plan.from, "SEMANTIC_DIFF_FROM", "from");
|
|
448
|
+
const toName = selectExactMap(loaded, plan.to, "SEMANTIC_DIFF_TO", "to");
|
|
449
|
+
const difference2 = diffTileflowMaps(
|
|
450
|
+
loaded.authoringMaps[fromName] ?? loaded.project.maps[fromName],
|
|
451
|
+
loaded.authoringMaps[toName] ?? loaded.project.maps[toName]
|
|
452
|
+
);
|
|
453
|
+
return sanitizeSemanticDiff(difference2, process.cwd());
|
|
454
|
+
});
|
|
455
|
+
const summary = createTileflowCommandSummary({
|
|
456
|
+
code: "SEMANTIC_DIFF_READY",
|
|
457
|
+
command: "semantic-diff",
|
|
458
|
+
message: `Compared ${difference.from.id} to ${difference.to.id} (${difference.summary.total} semantic changes).`,
|
|
459
|
+
ok: true,
|
|
460
|
+
path: "",
|
|
461
|
+
phase: "semantic-diff",
|
|
462
|
+
severity: "info",
|
|
463
|
+
suggestion: difference.summary.total === 0 ? "The resolved semantic designs are equivalent." : "Apply changes by JSON Pointer through the public semantic module API."
|
|
464
|
+
});
|
|
465
|
+
const document = { ...summary, diff: difference, diagnostics: [] };
|
|
466
|
+
if (options.json) process.stdout.write(serializeTileflowCommandDocument(document));
|
|
467
|
+
else printHumanSemanticDiff(difference);
|
|
468
|
+
} catch (error) {
|
|
469
|
+
const failure = createTileflowCommandFailureDocument("semantic-diff", error, process.cwd(), {
|
|
470
|
+
code: "SEMANTIC_DIFF_FAILED",
|
|
471
|
+
phase: "semantic-diff"
|
|
472
|
+
});
|
|
473
|
+
if (options.json) process.stderr.write(serializeTileflowCommandDocument(failure));
|
|
474
|
+
else printHumanCommandFailure(failure);
|
|
475
|
+
process.exitCode = 1;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
function createSemanticDiffPlan(options, defaultConfigPath2) {
|
|
479
|
+
const usesConfigPair = options.fromConfig !== void 0 || options.toConfig !== void 0;
|
|
480
|
+
if (usesConfigPair) {
|
|
481
|
+
const conflictingWorkspaceOption = [
|
|
482
|
+
["config", options.config],
|
|
483
|
+
["from", options.from],
|
|
484
|
+
["to", options.to]
|
|
485
|
+
].find((entry) => entry[1] !== void 0);
|
|
486
|
+
if (conflictingWorkspaceOption) {
|
|
487
|
+
throw commandSelectionError(
|
|
488
|
+
"SEMANTIC_DIFF_MODE_CONFLICT",
|
|
489
|
+
`--${conflictingWorkspaceOption[0]} cannot be combined with --from-config/--to-config; use either paired configs or the --config --from --to workspace shortcut.`,
|
|
490
|
+
conflictingWorkspaceOption[0]
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
if (!options.fromConfig) {
|
|
494
|
+
throw commandSelectionError(
|
|
495
|
+
"SEMANTIC_DIFF_FROM_CONFIG_REQUIRED",
|
|
496
|
+
"Paired-config mode requires --from-config <path>.",
|
|
497
|
+
"from-config"
|
|
498
|
+
);
|
|
499
|
+
}
|
|
500
|
+
if (!options.toConfig) {
|
|
501
|
+
throw commandSelectionError(
|
|
502
|
+
"SEMANTIC_DIFF_TO_CONFIG_REQUIRED",
|
|
503
|
+
"Paired-config mode requires --to-config <path>.",
|
|
504
|
+
"to-config"
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
return {
|
|
508
|
+
fromConfig: options.fromConfig,
|
|
509
|
+
...options.fromMap === void 0 ? {} : { fromMap: options.fromMap },
|
|
510
|
+
mode: "configs",
|
|
511
|
+
toConfig: options.toConfig,
|
|
512
|
+
...options.toMap === void 0 ? {} : { toMap: options.toMap }
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
const configSelector = options.fromMap === void 0 ? options.toMap : options.fromMap;
|
|
516
|
+
if (configSelector !== void 0) {
|
|
517
|
+
const path = options.fromMap === void 0 ? "to-map" : "from-map";
|
|
518
|
+
throw commandSelectionError(
|
|
519
|
+
"SEMANTIC_DIFF_MODE_CONFLICT",
|
|
520
|
+
`--${path} is valid only with --from-config and --to-config.`,
|
|
521
|
+
path
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
if (!options.from || !options.to) {
|
|
525
|
+
throw commandSelectionError(
|
|
526
|
+
"SEMANTIC_DIFF_SELECTION_REQUIRED",
|
|
527
|
+
"Workspace mode requires both --from <map> and --to <map>.",
|
|
528
|
+
!options.from ? "from" : "to"
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
return {
|
|
532
|
+
config: options.config ?? defaultConfigPath2,
|
|
533
|
+
from: options.from,
|
|
534
|
+
mode: "workspace",
|
|
535
|
+
to: options.to
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
function selectMap(loaded, requested, code) {
|
|
539
|
+
const mapNames = Object.keys(loaded.project.maps).sort(compareCodeUnits2);
|
|
540
|
+
if (requested) return selectExactMap(loaded, requested, `${code}_MAP`, "map");
|
|
541
|
+
if (mapNames.length === 1) return mapNames[0];
|
|
542
|
+
throw commandSelectionError(
|
|
543
|
+
`${code}_MAP_REQUIRED`,
|
|
544
|
+
`This config defines ${mapNames.length} maps; select one with --map. Available maps: ${mapNames.join(", ")}.`,
|
|
545
|
+
"map"
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
function selectConfigMap(loaded, requested, side) {
|
|
549
|
+
const mapNames = Object.keys(loaded.project.maps).sort(compareCodeUnits2);
|
|
550
|
+
const sideCode = side.toUpperCase();
|
|
551
|
+
const path = `${side}-map`;
|
|
552
|
+
if (mapNames.length === 1) {
|
|
553
|
+
if (requested === void 0) return mapNames[0];
|
|
554
|
+
throw commandSelectionError(
|
|
555
|
+
`SEMANTIC_DIFF_${sideCode}_MAP_UNNECESSARY`,
|
|
556
|
+
`--${path} is not valid because --${side}-config defines exactly one map (${mapNames[0]}).`,
|
|
557
|
+
path
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
if (requested === void 0) {
|
|
561
|
+
throw commandSelectionError(
|
|
562
|
+
`SEMANTIC_DIFF_${sideCode}_MAP_REQUIRED`,
|
|
563
|
+
`--${side}-config defines ${mapNames.length} maps; select one with --${path}. Available maps: ${mapNames.join(", ")}.`,
|
|
564
|
+
path
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
return selectExactMap(loaded, requested, `SEMANTIC_DIFF_${sideCode}_MAP`, path);
|
|
568
|
+
}
|
|
569
|
+
function selectExactMap(loaded, requested, code, path) {
|
|
570
|
+
if (Object.hasOwn(loaded.project.maps, requested)) return requested;
|
|
571
|
+
const mapNames = Object.keys(loaded.project.maps).sort(compareCodeUnits2);
|
|
572
|
+
throw commandSelectionError(
|
|
573
|
+
`${code}_NOT_FOUND`,
|
|
574
|
+
`Unknown Tileflow map "${requested}". Available maps: ${mapNames.join(", ") || "(none)"}.`,
|
|
575
|
+
path
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
function selectTheme(loaded, mapName, requested) {
|
|
579
|
+
const map = parseResolvedTileflowMap(loaded.project.maps[mapName]);
|
|
580
|
+
const themeNames = Object.keys(map.themes).sort(compareCodeUnits2);
|
|
581
|
+
const themeName = requested ?? map.defaultTheme;
|
|
582
|
+
if (Object.hasOwn(map.themes, themeName)) return themeName;
|
|
583
|
+
throw commandSelectionError(
|
|
584
|
+
"EXPLAIN_THEME_NOT_FOUND",
|
|
585
|
+
`Unknown theme "${themeName}" for map "${mapName}". Available themes: ${themeNames.join(", ")}.`,
|
|
586
|
+
"theme"
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
function commandSelectionError(code, message, path) {
|
|
590
|
+
return Object.assign(new Error(message), {
|
|
591
|
+
code,
|
|
592
|
+
issues: [{ code, message, path, phase: "command-validation" }],
|
|
593
|
+
phase: "command-validation"
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
function sanitizeSemanticDiff(difference, cwd) {
|
|
597
|
+
return {
|
|
598
|
+
...difference,
|
|
599
|
+
changes: difference.changes.map((change) => ({
|
|
600
|
+
...change,
|
|
601
|
+
path: sanitizeDiagnosticSecrets(change.path),
|
|
602
|
+
..."before" in change ? {
|
|
603
|
+
before: sanitizeTileflowInspectableValue(
|
|
604
|
+
change.before,
|
|
605
|
+
cwd
|
|
606
|
+
)
|
|
607
|
+
} : {},
|
|
608
|
+
..."after" in change ? {
|
|
609
|
+
after: sanitizeTileflowInspectableValue(change.after, cwd)
|
|
610
|
+
} : {}
|
|
611
|
+
}))
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
function toSafeCompilationDiagnostic(diagnostic) {
|
|
615
|
+
return {
|
|
616
|
+
code: diagnostic.code,
|
|
617
|
+
...diagnostic.domain ? { domain: diagnostic.domain } : {},
|
|
618
|
+
message: diagnostic.message,
|
|
619
|
+
path: diagnostic.path,
|
|
620
|
+
phase: diagnostic.phase,
|
|
621
|
+
severity: diagnostic.severity === "info" ? "warning" : diagnostic.severity,
|
|
622
|
+
...diagnostic.suggestion ? { suggestion: diagnostic.suggestion } : {},
|
|
623
|
+
...diagnostic.target ? { target: diagnostic.target } : {}
|
|
624
|
+
};
|
|
625
|
+
}
|
|
626
|
+
function printHumanExplain(document) {
|
|
627
|
+
console.log(document.message);
|
|
628
|
+
const emitted = document.compilation.report.domains.filter(({ status }) => status === "emitted");
|
|
629
|
+
const suppressed = document.compilation.report.domains.length - emitted.length;
|
|
630
|
+
console.log(`Domains: ${emitted.length} emitted, ${suppressed} suppressed.`);
|
|
631
|
+
console.log(`Semantic targets: ${document.compilation.report.targets.length}.`);
|
|
632
|
+
for (const decision of document.compilation.report.planner) {
|
|
633
|
+
console.log(`Planner ${decision.stage}: ${decision.inputCount} -> ${decision.outputCount}.`);
|
|
634
|
+
}
|
|
635
|
+
console.log(
|
|
636
|
+
document.compilation.diagnostics.length === 0 ? "Diagnostics: none." : `Diagnostics: ${document.compilation.diagnostics.length}.`
|
|
637
|
+
);
|
|
638
|
+
console.log("Run tileflow explain --json for the complete machine-readable report.");
|
|
639
|
+
}
|
|
640
|
+
function printHumanExplainFailure(document) {
|
|
641
|
+
console.error(`${document.message} [${document.code}; ${document.phase}]`);
|
|
642
|
+
for (const diagnostic of document.compilation.diagnostics) {
|
|
643
|
+
console.error(`- ${diagnostic.path || "(root)"}: ${diagnostic.message}`);
|
|
644
|
+
}
|
|
645
|
+
console.error(`Suggestion: ${document.suggestion}`);
|
|
646
|
+
}
|
|
647
|
+
function printHumanSemanticDiff(difference) {
|
|
648
|
+
console.log(
|
|
649
|
+
`Compared ${difference.from.id}@${difference.from.version} to ${difference.to.id}@${difference.to.version}.`
|
|
650
|
+
);
|
|
651
|
+
console.log(
|
|
652
|
+
`Changes: ${difference.summary.total} (${difference.summary.add} add, ${difference.summary.remove} remove, ${difference.summary.change} change).`
|
|
653
|
+
);
|
|
654
|
+
for (const change of difference.changes) {
|
|
655
|
+
console.log(`${change.kind.padEnd(6)} ${change.path || "/"}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function printHumanCommandFailure(document) {
|
|
659
|
+
console.error(`${document.message} [${document.code}; ${document.phase}]`);
|
|
660
|
+
for (const diagnostic of document.diagnostics.slice(1)) {
|
|
661
|
+
console.error(`- ${diagnostic.path || "(root)"}: ${diagnostic.message}`);
|
|
662
|
+
}
|
|
663
|
+
console.error(`Suggestion: ${document.suggestion}`);
|
|
664
|
+
}
|
|
665
|
+
function compareCodeUnits2(left, right) {
|
|
666
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
667
|
+
}
|
|
668
|
+
|
|
268
669
|
// src/capture-command.ts
|
|
269
670
|
import { spawn } from "child_process";
|
|
270
671
|
import { isAbsolute, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
@@ -277,7 +678,7 @@ import {
|
|
|
277
678
|
TileflowVisualReviewError
|
|
278
679
|
} from "@tileflow/capture";
|
|
279
680
|
import {
|
|
280
|
-
compareCodeUnits as
|
|
681
|
+
compareCodeUnits as compareCodeUnits3,
|
|
281
682
|
normalizeTileflowCaptureScene,
|
|
282
683
|
tileflowCaptureSceneNameSchema,
|
|
283
684
|
tileflowCaptureSceneSchema
|
|
@@ -293,12 +694,12 @@ import { TileflowStyleValidationError } from "@tileflow/dev/validation";
|
|
|
293
694
|
// src/capture-output.ts
|
|
294
695
|
import { randomUUID } from "crypto";
|
|
295
696
|
import { lstat, mkdir as mkdir2, open as open2, readFile as readFile2, rename as rename2, unlink as unlink2 } from "fs/promises";
|
|
296
|
-
import { basename, dirname as
|
|
697
|
+
import { basename, dirname as dirname3, relative, resolve as resolve2, sep } from "path";
|
|
297
698
|
var maximumAtomicArtifactBytes = 256 * 1024 * 1024;
|
|
298
699
|
async function writeCapturePair(input) {
|
|
299
700
|
const outputPath = resolve2(input.outputPath);
|
|
300
701
|
const receiptPath = resolve2(input.receiptPath);
|
|
301
|
-
const boundaryPath = resolve2(input.boundaryPath ??
|
|
702
|
+
const boundaryPath = resolve2(input.boundaryPath ?? dirname3(outputPath));
|
|
302
703
|
if (outputPath === receiptPath) {
|
|
303
704
|
throw new Error("Capture PNG and receipt paths must be different.");
|
|
304
705
|
}
|
|
@@ -325,7 +726,7 @@ async function writeAtomicFileSet(input) {
|
|
|
325
726
|
throw new Error(`${input.label} paths must be unique and include at least one file.`);
|
|
326
727
|
}
|
|
327
728
|
for (const path of targets) {
|
|
328
|
-
await assertNoSymlinkComponents(
|
|
729
|
+
await assertNoSymlinkComponents(dirname3(path), boundaryPath);
|
|
329
730
|
await assertNotSymlink(path);
|
|
330
731
|
}
|
|
331
732
|
const existing = /* @__PURE__ */ new Map();
|
|
@@ -345,12 +746,12 @@ async function writeAtomicFileSet(input) {
|
|
|
345
746
|
);
|
|
346
747
|
}
|
|
347
748
|
await Promise.all(
|
|
348
|
-
[...new Set(files.map((file) =>
|
|
749
|
+
[...new Set(files.map((file) => dirname3(file.path)))].map(
|
|
349
750
|
(path) => mkdir2(path, { recursive: true })
|
|
350
751
|
)
|
|
351
752
|
);
|
|
352
753
|
for (const path of targets) {
|
|
353
|
-
await assertNoSymlinkComponents(
|
|
754
|
+
await assertNoSymlinkComponents(dirname3(path), boundaryPath);
|
|
354
755
|
await assertNotSymlink(path);
|
|
355
756
|
}
|
|
356
757
|
const transaction = `${process.pid}.${randomUUID()}`;
|
|
@@ -378,7 +779,7 @@ async function writeAtomicFileSet(input) {
|
|
|
378
779
|
stateByPath.get(file.path).installed = true;
|
|
379
780
|
}
|
|
380
781
|
await Promise.all(
|
|
381
|
-
[...new Set(targets.map((path) =>
|
|
782
|
+
[...new Set(targets.map((path) => dirname3(path)))].map((path) => syncDirectory2(path))
|
|
382
783
|
);
|
|
383
784
|
const cleanup = await Promise.allSettled(states.map((state) => removeOptional(state.backup)));
|
|
384
785
|
cleanup.forEach((result, index) => {
|
|
@@ -415,7 +816,7 @@ async function writeAtomicFile(input) {
|
|
|
415
816
|
const boundaryPath = resolve2(input.boundaryPath);
|
|
416
817
|
const path = resolve2(input.path);
|
|
417
818
|
const source = typeof input.source === "string" ? new TextEncoder().encode(input.source) : new Uint8Array(input.source);
|
|
418
|
-
await assertNoSymlinkComponents(
|
|
819
|
+
await assertNoSymlinkComponents(dirname3(path), boundaryPath);
|
|
419
820
|
await assertNotSymlink(path);
|
|
420
821
|
const existing = await readOptional(path);
|
|
421
822
|
if (existing !== void 0 && Buffer.from(existing).equals(Buffer.from(source))) return false;
|
|
@@ -424,14 +825,14 @@ async function writeAtomicFile(input) {
|
|
|
424
825
|
`${input.label} already exists with different contents. Use --force to replace it.`
|
|
425
826
|
);
|
|
426
827
|
}
|
|
427
|
-
await mkdir2(
|
|
428
|
-
await assertNoSymlinkComponents(
|
|
828
|
+
await mkdir2(dirname3(path), { recursive: true });
|
|
829
|
+
await assertNoSymlinkComponents(dirname3(path), boundaryPath);
|
|
429
830
|
await assertNotSymlink(path);
|
|
430
831
|
const temporary = temporarySibling(path, `${process.pid}.${randomUUID()}`, "new");
|
|
431
832
|
try {
|
|
432
833
|
await writeSyncedExclusive(temporary, source);
|
|
433
834
|
await rename2(temporary, path);
|
|
434
|
-
await syncDirectory2(
|
|
835
|
+
await syncDirectory2(dirname3(path));
|
|
435
836
|
return true;
|
|
436
837
|
} finally {
|
|
437
838
|
await removeOptional(temporary);
|
|
@@ -466,7 +867,7 @@ async function assertBoundaryOrNearestExistingAncestorIsNotSymlink(path) {
|
|
|
466
867
|
throw new Error("Capture paths cannot contain symbolic links.");
|
|
467
868
|
}
|
|
468
869
|
if (info) return;
|
|
469
|
-
const parent =
|
|
870
|
+
const parent = dirname3(current);
|
|
470
871
|
if (parent === current) return;
|
|
471
872
|
current = parent;
|
|
472
873
|
}
|
|
@@ -501,7 +902,7 @@ async function syncDirectory2(path) {
|
|
|
501
902
|
}
|
|
502
903
|
}
|
|
503
904
|
function temporarySibling(path, transaction, role) {
|
|
504
|
-
return resolve2(
|
|
905
|
+
return resolve2(dirname3(path), `.${basename(path)}.${transaction}.${role}.tmp`);
|
|
505
906
|
}
|
|
506
907
|
async function readOptional(path) {
|
|
507
908
|
const info = await lstatOptional(path);
|
|
@@ -532,52 +933,6 @@ function isMissingFile2(error) {
|
|
|
532
933
|
return Boolean(error && typeof error === "object" && "code" in error && error.code === "ENOENT");
|
|
533
934
|
}
|
|
534
935
|
|
|
535
|
-
// src/config-execution.ts
|
|
536
|
-
function hideTileflowConfigSecrets() {
|
|
537
|
-
const hadApiKey = Object.hasOwn(process.env, "TILEFLOW_API_KEY");
|
|
538
|
-
const apiKey = process.env.TILEFLOW_API_KEY;
|
|
539
|
-
const argv = [...process.argv];
|
|
540
|
-
let restored = false;
|
|
541
|
-
delete process.env.TILEFLOW_API_KEY;
|
|
542
|
-
replaceProcessArgv(stripApiKeyArguments(argv));
|
|
543
|
-
return {
|
|
544
|
-
restore() {
|
|
545
|
-
if (restored) return;
|
|
546
|
-
restored = true;
|
|
547
|
-
if (hadApiKey) {
|
|
548
|
-
process.env.TILEFLOW_API_KEY = apiKey;
|
|
549
|
-
} else {
|
|
550
|
-
delete process.env.TILEFLOW_API_KEY;
|
|
551
|
-
}
|
|
552
|
-
replaceProcessArgv(argv);
|
|
553
|
-
}
|
|
554
|
-
};
|
|
555
|
-
}
|
|
556
|
-
async function withTileflowConfigSecretsHidden(operation) {
|
|
557
|
-
const scope = hideTileflowConfigSecrets();
|
|
558
|
-
try {
|
|
559
|
-
return await operation();
|
|
560
|
-
} finally {
|
|
561
|
-
scope.restore();
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
function stripApiKeyArguments(argv) {
|
|
565
|
-
const sanitized = [];
|
|
566
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
567
|
-
const argument = argv[index];
|
|
568
|
-
if (argument === "--api-key") {
|
|
569
|
-
index += 1;
|
|
570
|
-
continue;
|
|
571
|
-
}
|
|
572
|
-
if (argument.startsWith("--api-key=")) continue;
|
|
573
|
-
sanitized.push(argument);
|
|
574
|
-
}
|
|
575
|
-
return sanitized;
|
|
576
|
-
}
|
|
577
|
-
function replaceProcessArgv(argv) {
|
|
578
|
-
process.argv.splice(0, process.argv.length, ...argv);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
936
|
// src/capture-command.ts
|
|
582
937
|
function registerCaptureCommands(program2, dependencies) {
|
|
583
938
|
const setup = program2.command("setup").description("Provision local Tileflow tooling");
|
|
@@ -642,7 +997,7 @@ function createTileflowCaptureJson(captures, cwd) {
|
|
|
642
997
|
return {
|
|
643
998
|
schemaVersion: 1,
|
|
644
999
|
command: "capture",
|
|
645
|
-
captures: [...captures].sort((left, right) =>
|
|
1000
|
+
captures: [...captures].sort((left, right) => compareCodeUnits3(left.capture.scene, right.capture.scene)).map(({ capture, definition, outputPath, receiptPath }) => ({
|
|
646
1001
|
scene: capture.scene,
|
|
647
1002
|
map: capture.map,
|
|
648
1003
|
theme: capture.theme,
|
|
@@ -657,7 +1012,7 @@ function createTileflowCaptureJson(captures, cwd) {
|
|
|
657
1012
|
dpr: capture.dpr,
|
|
658
1013
|
renderer: capture.renderer,
|
|
659
1014
|
networkDependent: capture.networkDependent,
|
|
660
|
-
warnings: [...capture.warnings].sort(
|
|
1015
|
+
warnings: [...capture.warnings].sort(compareCodeUnits3)
|
|
661
1016
|
}))
|
|
662
1017
|
};
|
|
663
1018
|
}
|
|
@@ -898,7 +1253,13 @@ async function runCaptureWatchCommand(positionalScenes, options) {
|
|
|
898
1253
|
emit({ event: "recovered", generation: state.generation });
|
|
899
1254
|
invalidSinceLastReady = false;
|
|
900
1255
|
}
|
|
901
|
-
const
|
|
1256
|
+
const acquisition = artifactSession.acquireArtifacts(state.generation);
|
|
1257
|
+
if (!acquisition) return;
|
|
1258
|
+
const task = captureGeneration(
|
|
1259
|
+
state.generation,
|
|
1260
|
+
acquisition.artifacts,
|
|
1261
|
+
activeCapture.signal
|
|
1262
|
+
).finally(() => acquisition.release());
|
|
902
1263
|
activeTasks.add(task);
|
|
903
1264
|
void task.finally(() => activeTasks.delete(task));
|
|
904
1265
|
};
|
|
@@ -1185,8 +1546,8 @@ function runGit(cwd, args) {
|
|
|
1185
1546
|
// src/config-inspect-command.ts
|
|
1186
1547
|
import { inspectTileflowConfig } from "@tileflow/dev/inspect";
|
|
1187
1548
|
import {
|
|
1188
|
-
createTileflowCommandFailureDocument,
|
|
1189
|
-
serializeTileflowCommandDocument
|
|
1549
|
+
createTileflowCommandFailureDocument as createTileflowCommandFailureDocument2,
|
|
1550
|
+
serializeTileflowCommandDocument as serializeTileflowCommandDocument2
|
|
1190
1551
|
} from "@tileflow/dev/validation";
|
|
1191
1552
|
function registerConfigInspectCommand(inspect, dependencies) {
|
|
1192
1553
|
inspect.description("Inspect resolved maps, lineage, and merge provenance").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--map <name>", "inspect one exact configured map").option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
|
|
@@ -1195,17 +1556,17 @@ function registerConfigInspectCommand(inspect, dependencies) {
|
|
|
1195
1556
|
() => inspectTileflowConfig({ config: options.config, cwd: process.cwd(), map: options.map })
|
|
1196
1557
|
);
|
|
1197
1558
|
if (options.json) {
|
|
1198
|
-
process.stdout.write(
|
|
1559
|
+
process.stdout.write(serializeTileflowCommandDocument2(document));
|
|
1199
1560
|
return;
|
|
1200
1561
|
}
|
|
1201
1562
|
printHumanConfigInspection(document);
|
|
1202
1563
|
} catch (error) {
|
|
1203
|
-
const failure =
|
|
1564
|
+
const failure = createTileflowCommandFailureDocument2("inspect", error, process.cwd(), {
|
|
1204
1565
|
code: "INSPECTION_FAILED",
|
|
1205
1566
|
phase: "config-inspection"
|
|
1206
1567
|
});
|
|
1207
1568
|
if (options.json) {
|
|
1208
|
-
process.stderr.write(
|
|
1569
|
+
process.stderr.write(serializeTileflowCommandDocument2(failure));
|
|
1209
1570
|
} else {
|
|
1210
1571
|
console.error(`${failure.message} [${failure.code}; ${failure.phase}]`);
|
|
1211
1572
|
for (const diagnostic of failure.diagnostics.slice(1)) {
|
|
@@ -1378,13 +1739,13 @@ function tileflowDevOrigin(host, port) {
|
|
|
1378
1739
|
}
|
|
1379
1740
|
|
|
1380
1741
|
// src/feature-inspect-command.ts
|
|
1381
|
-
import { dirname as
|
|
1742
|
+
import { dirname as dirname4 } from "path";
|
|
1382
1743
|
import {
|
|
1383
1744
|
getFirstTileflowMapName,
|
|
1384
1745
|
loadValidTileflowConfigWithInputs,
|
|
1385
1746
|
TileflowValidationError as TileflowValidationError2
|
|
1386
1747
|
} from "@tileflow/dev/config";
|
|
1387
|
-
import { prepareTileflowCatalogIcons } from "@tileflow/dev/icons";
|
|
1748
|
+
import { prepareTileflowCatalogIcons as prepareTileflowCatalogIcons2 } from "@tileflow/dev/icons";
|
|
1388
1749
|
import { inspectTileflowFeatures } from "@tileflow/dev/inspect";
|
|
1389
1750
|
function registerFeatureInspectCommand(inspect, dependencies) {
|
|
1390
1751
|
inspect.command("features").description("Inspect bounded vector features near a map camera").requiredOption("--center <longitude,latitude>", "camera center").requiredOption("--zoom <number>", "camera zoom from 0 to 24").requiredOption("--layers <names>", "comma-separated source-layer names").option(
|
|
@@ -1419,9 +1780,9 @@ async function runFeatureInspect(options) {
|
|
|
1419
1780
|
);
|
|
1420
1781
|
const project = loaded.project;
|
|
1421
1782
|
const mapName = options.map ?? getFirstTileflowMapName(project);
|
|
1422
|
-
const prepared = await
|
|
1783
|
+
const prepared = await prepareTileflowCatalogIcons2(project, {
|
|
1423
1784
|
assetBaseUrl: "/tileflow",
|
|
1424
|
-
baseDirectory:
|
|
1785
|
+
baseDirectory: dirname4(loaded.configFile),
|
|
1425
1786
|
cwd: process.cwd()
|
|
1426
1787
|
});
|
|
1427
1788
|
const inspection = await inspectTileflowFeatures(project, mapName, {
|
|
@@ -1514,7 +1875,7 @@ var publicHttpUrlSchema = z2.url().max(2048).refine((value) => {
|
|
|
1514
1875
|
var hostedStyleDeploymentResponseSchema = z2.object({
|
|
1515
1876
|
changed: z2.boolean().optional(),
|
|
1516
1877
|
deploymentId: safeIdentifierSchema.optional(),
|
|
1517
|
-
mapId:
|
|
1878
|
+
mapId: z2.string().regex(/^map_[A-Za-z0-9_-]{16}$/u),
|
|
1518
1879
|
themes: z2.record(
|
|
1519
1880
|
safeIdentifierSchema,
|
|
1520
1881
|
z2.object({
|
|
@@ -1522,8 +1883,7 @@ var hostedStyleDeploymentResponseSchema = z2.object({
|
|
|
1522
1883
|
styleUrl: publicHttpUrlSchema
|
|
1523
1884
|
}).strict()
|
|
1524
1885
|
).refine((themes) => Object.keys(themes).length > 0),
|
|
1525
|
-
version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
|
|
1526
|
-
worldPromotionId: safeIdentifierSchema.optional()
|
|
1886
|
+
version: z2.number().int().positive().max(Number.MAX_SAFE_INTEGER).optional()
|
|
1527
1887
|
}).strict();
|
|
1528
1888
|
var hostedIconPackageResponseSchema = z2.object({
|
|
1529
1889
|
changed: z2.boolean().optional(),
|
|
@@ -1548,8 +1908,8 @@ var hostedStatusStyleSchema = z2.object({
|
|
|
1548
1908
|
size: z2.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER),
|
|
1549
1909
|
uploaded: z2.iso.datetime({ offset: true })
|
|
1550
1910
|
});
|
|
1551
|
-
var
|
|
1552
|
-
|
|
1911
|
+
var hostedMapStatusSchema = z2.object({
|
|
1912
|
+
mapId: z2.string().regex(/^map_[A-Za-z0-9_-]{16}$/u),
|
|
1553
1913
|
styles: z2.array(hostedStatusStyleSchema).max(1e4)
|
|
1554
1914
|
});
|
|
1555
1915
|
async function readBoundedResponseText(response, label) {
|
|
@@ -1685,7 +2045,7 @@ async function validateApiKey(apiUrl, apiKey, options = {}) {
|
|
|
1685
2045
|
return { error: `API key validation failed (${response.status}).`, ok: false };
|
|
1686
2046
|
}
|
|
1687
2047
|
const body = asRecord(response.body);
|
|
1688
|
-
if (!response.json || typeof body.apiKeyId !== "string" || body.credentialType !== "project_api_key" || !isProjectIdentity(body.organization) || !isProjectIdentity(body.project) || typeof body.projectId !== "string" || body.projectId !== body.project.id || !Array.isArray(body.scopes)) {
|
|
2048
|
+
if (!response.json || typeof body.apiKeyId !== "string" || body.credentialType !== "project_api_key" || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || !isProjectIdentity(body.organization) || !isProjectIdentity(body.project) || typeof body.projectId !== "string" || body.projectId !== body.project.id || !Array.isArray(body.scopes)) {
|
|
1689
2049
|
return { error: "API key validation returned an invalid response.", ok: false };
|
|
1690
2050
|
}
|
|
1691
2051
|
return {
|
|
@@ -1693,6 +2053,7 @@ async function validateApiKey(apiUrl, apiKey, options = {}) {
|
|
|
1693
2053
|
value: {
|
|
1694
2054
|
apiKeyId: body.apiKeyId,
|
|
1695
2055
|
credentialType: body.credentialType,
|
|
2056
|
+
mapId: body.mapId,
|
|
1696
2057
|
organization: body.organization,
|
|
1697
2058
|
project: body.project,
|
|
1698
2059
|
projectId: body.projectId,
|
|
@@ -1700,6 +2061,33 @@ async function validateApiKey(apiUrl, apiKey, options = {}) {
|
|
|
1700
2061
|
}
|
|
1701
2062
|
};
|
|
1702
2063
|
}
|
|
2064
|
+
async function requestMapCapability(session, target, scopes, options = {}) {
|
|
2065
|
+
let response;
|
|
2066
|
+
try {
|
|
2067
|
+
response = await requestHostedJson(
|
|
2068
|
+
session.apiOrigin,
|
|
2069
|
+
"/v1/cli/map-capabilities",
|
|
2070
|
+
{
|
|
2071
|
+
...jsonRequest("POST", { ...target, scopes }),
|
|
2072
|
+
headers: {
|
|
2073
|
+
...authorizationHeaders(session.accountSession),
|
|
2074
|
+
"Content-Type": "application/json"
|
|
2075
|
+
}
|
|
2076
|
+
},
|
|
2077
|
+
options
|
|
2078
|
+
);
|
|
2079
|
+
} catch (error) {
|
|
2080
|
+
return { error: safeTransportError(error, "Map capability request failed."), ok: false };
|
|
2081
|
+
}
|
|
2082
|
+
if (!response.ok) {
|
|
2083
|
+
return { error: `Map capability request failed (${response.status}).`, ok: false };
|
|
2084
|
+
}
|
|
2085
|
+
const body = asRecord(response.body);
|
|
2086
|
+
if (!response.json || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || typeof body.mapId !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(body.mapId) || body.mapId !== target.mapId || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
|
|
2087
|
+
return { error: "Map capability response was invalid.", ok: false };
|
|
2088
|
+
}
|
|
2089
|
+
return { capability: body.capability, mapId: body.mapId, ok: true };
|
|
2090
|
+
}
|
|
1703
2091
|
async function requestProjectCapability(session, project, scopes, options = {}) {
|
|
1704
2092
|
let response;
|
|
1705
2093
|
try {
|
|
@@ -1730,6 +2118,53 @@ async function requestProjectCapability(session, project, scopes, options = {})
|
|
|
1730
2118
|
}
|
|
1731
2119
|
return { capability: body.capability, ok: true };
|
|
1732
2120
|
}
|
|
2121
|
+
async function requestTeamCapability(session, team, scopes, options = {}) {
|
|
2122
|
+
let response;
|
|
2123
|
+
try {
|
|
2124
|
+
response = await requestHostedJson(
|
|
2125
|
+
session.apiOrigin,
|
|
2126
|
+
"/v1/cli/team-capabilities",
|
|
2127
|
+
{
|
|
2128
|
+
...jsonRequest("POST", { scopes, team }),
|
|
2129
|
+
headers: {
|
|
2130
|
+
...authorizationHeaders(session.accountSession),
|
|
2131
|
+
"Content-Type": "application/json"
|
|
2132
|
+
}
|
|
2133
|
+
},
|
|
2134
|
+
options
|
|
2135
|
+
);
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
return { error: safeTransportError(error, "Team capability request failed."), ok: false };
|
|
2138
|
+
}
|
|
2139
|
+
if (!response.ok) {
|
|
2140
|
+
return { error: `Team capability request failed (${response.status}).`, ok: false };
|
|
2141
|
+
}
|
|
2142
|
+
const body = asRecord(response.body);
|
|
2143
|
+
if (!response.json || body.schemaVersion !== 1 || typeof body.capability !== "string" || !body.capability.startsWith("tf_cap_") || body.capability.length > 8192 || body.reference !== team || !isProjectIdentity(body.team) || `@${body.team.slug}` !== team || !validIsoDate(body.expiresAt) || !Array.isArray(body.scopes) || body.scopes.join("\0") !== [...scopes].sort().join("\0")) {
|
|
2144
|
+
return { error: "Team capability response was invalid.", ok: false };
|
|
2145
|
+
}
|
|
2146
|
+
return { capability: body.capability, ok: true, team: body.team };
|
|
2147
|
+
}
|
|
2148
|
+
async function listAccountTeams(session, options = {}) {
|
|
2149
|
+
const response = await requestHostedJson(
|
|
2150
|
+
session.apiOrigin,
|
|
2151
|
+
"/v1/cli/teams",
|
|
2152
|
+
{ headers: authorizationHeaders(session.accountSession) },
|
|
2153
|
+
options
|
|
2154
|
+
);
|
|
2155
|
+
const body = asRecord(response.body);
|
|
2156
|
+
if (!response.ok) return { error: `Team discovery failed (${response.status}).`, ok: false };
|
|
2157
|
+
if (!response.json || body.schemaVersion !== 1 || !Array.isArray(body.teams) || !body.teams.every(isProjectIdentity)) {
|
|
2158
|
+
return { error: "Team discovery returned an invalid response.", ok: false };
|
|
2159
|
+
}
|
|
2160
|
+
const teams = [...body.teams].sort(
|
|
2161
|
+
(left, right) => left.slug < right.slug ? -1 : left.slug > right.slug ? 1 : 0
|
|
2162
|
+
);
|
|
2163
|
+
if (new Set(teams.map(({ id }) => id)).size !== teams.length || teams.some((team, index) => index > 0 && teams[index - 1].slug >= team.slug)) {
|
|
2164
|
+
return { error: "Team discovery returned unstable ordering.", ok: false };
|
|
2165
|
+
}
|
|
2166
|
+
return { ok: true, teams };
|
|
2167
|
+
}
|
|
1733
2168
|
async function publishHostedStyle(api, body, responseLabel, options = {}) {
|
|
1734
2169
|
const response = await requestHostedJson(
|
|
1735
2170
|
api.apiUrl,
|
|
@@ -1816,7 +2251,7 @@ async function uploadHostedFontBundle(api, bundle, options = {}) {
|
|
|
1816
2251
|
}
|
|
1817
2252
|
return { ok: true, value };
|
|
1818
2253
|
}
|
|
1819
|
-
async function
|
|
2254
|
+
async function fetchHostedMapStatus(api, options = {}) {
|
|
1820
2255
|
const response = await requestHostedJson(
|
|
1821
2256
|
api.apiUrl,
|
|
1822
2257
|
"/v1/status",
|
|
@@ -1824,7 +2259,7 @@ async function fetchHostedProjectStatus(api, options = {}) {
|
|
|
1824
2259
|
options
|
|
1825
2260
|
);
|
|
1826
2261
|
if (!response.ok) throw new Error(`Status failed: ${response.status}.`);
|
|
1827
|
-
return parseResponse(response,
|
|
2262
|
+
return parseResponse(response, hostedMapStatusSchema, "Status response");
|
|
1828
2263
|
}
|
|
1829
2264
|
async function revokeHostedAccountSession(apiUrl, accountSession, options = {}) {
|
|
1830
2265
|
try {
|
|
@@ -1981,20 +2416,54 @@ function safeTransportError(error, fallback) {
|
|
|
1981
2416
|
}
|
|
1982
2417
|
|
|
1983
2418
|
// src/hosted-preflight.ts
|
|
1984
|
-
import {
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
2419
|
+
import {
|
|
2420
|
+
compareCodeUnits as compareCodeUnits4,
|
|
2421
|
+
tileflowHostedAlphaCompatibility
|
|
2422
|
+
} from "@tileflow/core";
|
|
2423
|
+
function prepareTileflowHostedThemeFamily(mapName, map, styles) {
|
|
2424
|
+
const prepared = Object.fromEntries(
|
|
2425
|
+
Object.entries(styles).sort(([left], [right]) => compareCodeUnits4(left, right)).map(([themeName, style]) => [themeName, cloneStyle(style)])
|
|
2426
|
+
);
|
|
2427
|
+
const teamSources = Object.fromEntries(
|
|
2428
|
+
Object.entries(map.sources ?? {}).sort(([left], [right]) => compareCodeUnits4(left, right)).map(([sourceId, source]) => {
|
|
2429
|
+
for (const [themeName, style] of Object.entries(prepared)) {
|
|
2430
|
+
const definition = style.sources[sourceId];
|
|
2431
|
+
if (!definition) {
|
|
2432
|
+
throw Object.assign(
|
|
2433
|
+
new Error(`Hosted theme ${themeName} is missing logical source ${sourceId}.`),
|
|
2434
|
+
{
|
|
2435
|
+
code: "TF_HOSTED_SOURCE_MISSING",
|
|
2436
|
+
path: `maps.${mapName}.themes.${themeName}.sources.${sourceId}`
|
|
2437
|
+
}
|
|
2438
|
+
);
|
|
2439
|
+
}
|
|
2440
|
+
definition.url = `tileflow://hosted-sources/${sourceId}`;
|
|
2441
|
+
delete definition.tiles;
|
|
2442
|
+
}
|
|
2443
|
+
return [
|
|
2444
|
+
sourceId,
|
|
2445
|
+
Object.freeze({
|
|
2446
|
+
tileset: source.tileset,
|
|
2447
|
+
type: source.type
|
|
2448
|
+
})
|
|
2449
|
+
];
|
|
2450
|
+
})
|
|
2451
|
+
);
|
|
2452
|
+
return { styles: prepared, teamSources };
|
|
2453
|
+
}
|
|
2454
|
+
function inspectTileflowHostedCompatibility(project, styles) {
|
|
2455
|
+
const issues = [];
|
|
2456
|
+
const mapNames = Object.keys(project.maps).sort(compareCodeUnits4);
|
|
2457
|
+
if (mapNames.length > tileflowHostedAlphaCompatibility.maxMapsPerDeploy) {
|
|
2458
|
+
issues.push({
|
|
2459
|
+
message: `Hosted alpha accepts at most ${tileflowHostedAlphaCompatibility.maxMapsPerDeploy} maps per deploy.`,
|
|
2460
|
+
path: "maps"
|
|
2461
|
+
});
|
|
2462
|
+
}
|
|
2463
|
+
for (const mapName of [...mapNames].sort(compareCodeUnits4)) {
|
|
2464
|
+
const themes = styles[mapName] ?? {};
|
|
2465
|
+
for (const themeName of Object.keys(themes).sort(compareCodeUnits4)) {
|
|
2466
|
+
const data = themes[themeName]?.metadata?.["tileflow:data"];
|
|
1998
2467
|
if (!data || typeof data !== "object" || Array.isArray(data) || data.kind !== "tileflow-world") {
|
|
1999
2468
|
issues.push({
|
|
2000
2469
|
map: mapName,
|
|
@@ -2006,9 +2475,12 @@ function inspectTileflowHostedCompatibility(project, styles) {
|
|
|
2006
2475
|
}
|
|
2007
2476
|
return issues;
|
|
2008
2477
|
}
|
|
2478
|
+
function cloneStyle(style) {
|
|
2479
|
+
return JSON.parse(JSON.stringify(style));
|
|
2480
|
+
}
|
|
2009
2481
|
|
|
2010
2482
|
// src/icon-diff-command.ts
|
|
2011
|
-
import { dirname as
|
|
2483
|
+
import { dirname as dirname6, resolve as resolve4 } from "path";
|
|
2012
2484
|
import { z as z4 } from "zod";
|
|
2013
2485
|
import {
|
|
2014
2486
|
diffTileflowIconPackageManifests,
|
|
@@ -2022,17 +2494,17 @@ import {
|
|
|
2022
2494
|
import {
|
|
2023
2495
|
assertValidTileflowConfig,
|
|
2024
2496
|
getTileflowMapNames,
|
|
2025
|
-
loadTileflowConfigWithInputs
|
|
2497
|
+
loadTileflowConfigWithInputs as loadTileflowConfigWithInputs2
|
|
2026
2498
|
} from "@tileflow/dev/config";
|
|
2027
2499
|
import { compileTileflowIconPackages } from "@tileflow/dev/icons";
|
|
2028
2500
|
|
|
2029
2501
|
// src/icon-diff-report.ts
|
|
2030
2502
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
2031
2503
|
import { mkdir as mkdir3, readFile as readFile3, rename as rename3, unlink as unlink3, writeFile } from "fs/promises";
|
|
2032
|
-
import { basename as basename2, dirname as
|
|
2504
|
+
import { basename as basename2, dirname as dirname5, join } from "path";
|
|
2033
2505
|
import { z as z3 } from "zod";
|
|
2034
2506
|
import {
|
|
2035
|
-
compareCodeUnits as
|
|
2507
|
+
compareCodeUnits as compareCodeUnits5,
|
|
2036
2508
|
sha256Hex,
|
|
2037
2509
|
tileflowIconIdSchema,
|
|
2038
2510
|
tileflowIconPackageLimits as tileflowIconPackageLimits2
|
|
@@ -2153,9 +2625,9 @@ async function readBoundedResponse(response, exactMaximum) {
|
|
|
2153
2625
|
}
|
|
2154
2626
|
function renderReport(document, before, after, mapName) {
|
|
2155
2627
|
const changeGroups = [
|
|
2156
|
-
{ id: "added", label: "Added", names: [...document.icons.added].sort(
|
|
2157
|
-
{ id: "modified", label: "Modified", names: [...document.icons.modified].sort(
|
|
2158
|
-
{ id: "removed", label: "Removed", names: [...document.icons.removed].sort(
|
|
2628
|
+
{ id: "added", label: "Added", names: [...document.icons.added].sort(compareCodeUnits5) },
|
|
2629
|
+
{ id: "modified", label: "Modified", names: [...document.icons.modified].sort(compareCodeUnits5) },
|
|
2630
|
+
{ id: "removed", label: "Removed", names: [...document.icons.removed].sort(compareCodeUnits5) }
|
|
2159
2631
|
];
|
|
2160
2632
|
const changedCount = changeGroups.reduce((total, group) => total + group.names.length, 0);
|
|
2161
2633
|
const beforeIconCount = document.icons.unchangedCount + document.icons.modified.length + document.icons.removed.length;
|
|
@@ -2500,9 +2972,9 @@ async function writeAtomicReport(path, html, force) {
|
|
|
2500
2972
|
} catch (error) {
|
|
2501
2973
|
if (!isMissingFile3(error)) throw error;
|
|
2502
2974
|
}
|
|
2503
|
-
await mkdir3(
|
|
2975
|
+
await mkdir3(dirname5(path), { recursive: true });
|
|
2504
2976
|
const temporaryPath = join(
|
|
2505
|
-
|
|
2977
|
+
dirname5(path),
|
|
2506
2978
|
`.${basename2(path)}.${process.pid}.${randomUUID2()}.tmp`
|
|
2507
2979
|
);
|
|
2508
2980
|
try {
|
|
@@ -2583,7 +3055,7 @@ var tileflowIconDiffDocumentSchema = z4.object({
|
|
|
2583
3055
|
hasChanges: z4.boolean()
|
|
2584
3056
|
}).strict();
|
|
2585
3057
|
function registerIconDiffCommand(icons, dependencies) {
|
|
2586
|
-
icons.command("diff").description("Preview a local icon package against an active hosted revision").requiredOption("--against <environment>", "named map environment to compare").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).
|
|
3058
|
+
icons.command("diff").description("Preview a local icon package against an active hosted revision").requiredOption("--against <environment>", "named map environment to compare").option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).requiredOption("--map-id <id>", "managed Map destination").option("--json", "print deterministic schema-version-1 JSON").option("--report <path>", "write a self-contained HTML visual report").option("--open", "open an explicitly requested report").option("--force", "replace a different explicitly requested report").action(async (options) => {
|
|
2587
3059
|
try {
|
|
2588
3060
|
await runIconDiff(options, dependencies);
|
|
2589
3061
|
} catch (error) {
|
|
@@ -2603,12 +3075,8 @@ async function runIconDiff(options, dependencies) {
|
|
|
2603
3075
|
throw new Error("--force requires --report <path>");
|
|
2604
3076
|
}
|
|
2605
3077
|
const environment = environmentSchema.parse(options.against);
|
|
2606
|
-
const api = await dependencies.resolveApi(options);
|
|
2607
|
-
if (!api) {
|
|
2608
|
-
throw new Error("Missing Tileflow API key. Run tileflow login or set TILEFLOW_API_KEY.");
|
|
2609
|
-
}
|
|
2610
3078
|
const loaded = await withTileflowConfigSecretsHidden(
|
|
2611
|
-
() =>
|
|
3079
|
+
() => loadTileflowConfigWithInputs2(options.config)
|
|
2612
3080
|
);
|
|
2613
3081
|
const project = loaded.project;
|
|
2614
3082
|
assertValidTileflowConfig(project);
|
|
@@ -2618,9 +3086,13 @@ async function runIconDiff(options, dependencies) {
|
|
|
2618
3086
|
`Unknown map environment "${environment}". Available maps: ${mapNames.join(", ") || "(none)"}`
|
|
2619
3087
|
);
|
|
2620
3088
|
}
|
|
3089
|
+
const api = await dependencies.resolveApi(options);
|
|
3090
|
+
if (!api) {
|
|
3091
|
+
throw new Error("Missing Tileflow API key. Run tileflow login or set TILEFLOW_API_KEY.");
|
|
3092
|
+
}
|
|
2621
3093
|
const selectedProject = { ...project, maps: { [environment]: project.maps[environment] } };
|
|
2622
3094
|
const compiled = await compileTileflowIconPackages(selectedProject, {
|
|
2623
|
-
baseDirectory:
|
|
3095
|
+
baseDirectory: dirname6(loaded.configFile),
|
|
2624
3096
|
cwd: process.cwd(),
|
|
2625
3097
|
target: "hosted"
|
|
2626
3098
|
});
|
|
@@ -2763,8 +3235,8 @@ function formatSignedBytes(bytes) {
|
|
|
2763
3235
|
}
|
|
2764
3236
|
|
|
2765
3237
|
// src/icon-list-command.ts
|
|
2766
|
-
import { dirname as
|
|
2767
|
-
import { compareCodeUnits as
|
|
3238
|
+
import { dirname as dirname7 } from "path";
|
|
3239
|
+
import { compareCodeUnits as compareCodeUnits6 } from "@tileflow/core";
|
|
2768
3240
|
import {
|
|
2769
3241
|
getTileflowMapNames as getTileflowMapNames2,
|
|
2770
3242
|
loadValidTileflowConfigWithInputs as loadValidTileflowConfigWithInputs2,
|
|
@@ -2795,7 +3267,7 @@ function createTileflowIconListJson(inspection) {
|
|
|
2795
3267
|
return {
|
|
2796
3268
|
schemaVersion: 2,
|
|
2797
3269
|
pathBase: "cwd",
|
|
2798
|
-
maps: [...inspection.maps].sort((left, right) =>
|
|
3270
|
+
maps: [...inspection.maps].sort((left, right) => compareCodeUnits6(left.name, right.name)).map((map) => createMapJson(map, inspection.catalogs))
|
|
2799
3271
|
};
|
|
2800
3272
|
}
|
|
2801
3273
|
function serializeTileflowIconListJson(value) {
|
|
@@ -2807,14 +3279,14 @@ async function runIconList(options) {
|
|
|
2807
3279
|
() => loadValidTileflowConfigWithInputs2(options.config)
|
|
2808
3280
|
);
|
|
2809
3281
|
const project = loaded.project;
|
|
2810
|
-
const mapIds = getTileflowMapNames2(project).sort(
|
|
3282
|
+
const mapIds = getTileflowMapNames2(project).sort(compareCodeUnits6);
|
|
2811
3283
|
if (options.map && !Object.hasOwn(project.maps, options.map)) {
|
|
2812
3284
|
throw new Error(
|
|
2813
3285
|
`Unknown map "${options.map}". Available maps: ${mapIds.join(", ") || "(none)"}`
|
|
2814
3286
|
);
|
|
2815
3287
|
}
|
|
2816
3288
|
const inspection = await inspectTileflowIconCatalogs(project, {
|
|
2817
|
-
baseDirectory:
|
|
3289
|
+
baseDirectory: dirname7(loaded.configFile),
|
|
2818
3290
|
cwd: process.cwd(),
|
|
2819
3291
|
...options.map ? { mapNames: [options.map] } : {}
|
|
2820
3292
|
});
|
|
@@ -2844,7 +3316,7 @@ function createMapJson(map, catalogs) {
|
|
|
2844
3316
|
winner: replacement.winner
|
|
2845
3317
|
})),
|
|
2846
3318
|
packageHash: mapIcons.packageHash,
|
|
2847
|
-
sources: [...catalog.icons].sort((left, right) =>
|
|
3319
|
+
sources: [...catalog.icons].sort((left, right) => compareCodeUnits6(left.id, right.id)).map((icon) => ({
|
|
2848
3320
|
id: icon.id,
|
|
2849
3321
|
path: icon.source.path,
|
|
2850
3322
|
format: icon.source.format,
|
|
@@ -2874,6 +3346,83 @@ function printIssues(heading, issues) {
|
|
|
2874
3346
|
);
|
|
2875
3347
|
}
|
|
2876
3348
|
|
|
3349
|
+
// src/language-command.ts
|
|
3350
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3351
|
+
import { fileURLToPath } from "url";
|
|
3352
|
+
import {
|
|
3353
|
+
serializeCanonicalJson,
|
|
3354
|
+
tileflowAuthoringManifest
|
|
3355
|
+
} from "@tileflow/core";
|
|
3356
|
+
import {
|
|
3357
|
+
createTileflowCommandFailureDocument as createTileflowCommandFailureDocument3,
|
|
3358
|
+
serializeTileflowCommandDocument as serializeTileflowCommandDocument3
|
|
3359
|
+
} from "@tileflow/dev/validation";
|
|
3360
|
+
function registerLanguageCommand(program2, dependencies = {}) {
|
|
3361
|
+
const language = program2.command("language").description("Inspect the complete machine-readable Tileflow semantic language");
|
|
3362
|
+
language.command("manifest").description("Print the semantic compiler authoring manifest").option("--json", "print deterministic JSON").action(
|
|
3363
|
+
async (options) => runLanguageCommand(
|
|
3364
|
+
"language-manifest",
|
|
3365
|
+
options,
|
|
3366
|
+
() => printManifest((dependencies.loadManifest ?? (() => tileflowAuthoringManifest))(), options)
|
|
3367
|
+
)
|
|
3368
|
+
);
|
|
3369
|
+
language.command("schema").description("Print the generated authoring and resolved JSON Schema").option("--json", "print deterministic JSON").action(
|
|
3370
|
+
async (options) => runLanguageCommand(
|
|
3371
|
+
"language-schema",
|
|
3372
|
+
options,
|
|
3373
|
+
async () => printSchema(await (dependencies.loadSchema ?? loadPackagedSchema)(), options)
|
|
3374
|
+
)
|
|
3375
|
+
);
|
|
3376
|
+
}
|
|
3377
|
+
async function runLanguageCommand(command2, options, operation) {
|
|
3378
|
+
try {
|
|
3379
|
+
await operation();
|
|
3380
|
+
} catch (error) {
|
|
3381
|
+
const failure = createTileflowCommandFailureDocument3(command2, error, process.cwd(), {
|
|
3382
|
+
code: command2 === "language-manifest" ? "LANGUAGE_MANIFEST_FAILED" : "LANGUAGE_SCHEMA_FAILED",
|
|
3383
|
+
phase: "language-discovery"
|
|
3384
|
+
});
|
|
3385
|
+
if (options.json) process.stderr.write(serializeTileflowCommandDocument3(failure));
|
|
3386
|
+
else {
|
|
3387
|
+
console.error(`${failure.message} [${failure.code}; ${failure.phase}]`);
|
|
3388
|
+
console.error(`Suggestion: ${failure.suggestion}`);
|
|
3389
|
+
}
|
|
3390
|
+
process.exitCode = 1;
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
function printManifest(manifest, options) {
|
|
3394
|
+
if (options.json) {
|
|
3395
|
+
process.stdout.write(`${serializeCanonicalJson(manifest)}
|
|
3396
|
+
`);
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
console.log(
|
|
3400
|
+
`${manifest.language} (${manifest.compiler.name} v${manifest.compiler.version}); ${manifest.domains.length} domains, ${manifest.expressions.builders.length} expression builders.`
|
|
3401
|
+
);
|
|
3402
|
+
console.log("Run tileflow language manifest --json for the complete machine-readable document.");
|
|
3403
|
+
}
|
|
3404
|
+
function printSchema(schema, options) {
|
|
3405
|
+
if (options.json) {
|
|
3406
|
+
process.stdout.write(`${serializeCanonicalJson(schema)}
|
|
3407
|
+
`);
|
|
3408
|
+
return;
|
|
3409
|
+
}
|
|
3410
|
+
console.log(
|
|
3411
|
+
`Tileflow config reference schema ${String(schema.schemaVersion)}: ${String(schema.$id)}.`
|
|
3412
|
+
);
|
|
3413
|
+
console.log("Run tileflow language schema --json for the complete machine-readable document.");
|
|
3414
|
+
}
|
|
3415
|
+
async function loadPackagedSchema() {
|
|
3416
|
+
const schemaUrl = import.meta.resolve("@tileflow/core/config-reference.json");
|
|
3417
|
+
const parsed = JSON.parse(await readFile4(fileURLToPath(schemaUrl), "utf8"));
|
|
3418
|
+
if (!isRecord(parsed))
|
|
3419
|
+
throw new Error("Packaged Tileflow config reference is not a JSON object.");
|
|
3420
|
+
return parsed;
|
|
3421
|
+
}
|
|
3422
|
+
function isRecord(value) {
|
|
3423
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
3424
|
+
}
|
|
3425
|
+
|
|
2877
3426
|
// src/open-external.ts
|
|
2878
3427
|
import { spawn as spawnChild } from "child_process";
|
|
2879
3428
|
import { platform as hostPlatform } from "os";
|
|
@@ -3050,147 +3599,1233 @@ async function fetchAccountProjects(session, includeArchived) {
|
|
|
3050
3599
|
if (!Array.isArray(body.items) || !body.items.every(isAccountProjectTarget)) {
|
|
3051
3600
|
return { error: "Destination discovery returned an invalid response.", ok: false, status: 502 };
|
|
3052
3601
|
}
|
|
3053
|
-
items.push(...body.items);
|
|
3054
|
-
cursor = typeof body.nextCursor === "string" ? body.nextCursor : null;
|
|
3055
|
-
if (!cursor) return validateTargetOrder(items);
|
|
3056
|
-
}
|
|
3057
|
-
return { error: "Destination discovery exceeded its safe page limit.", ok: false, status: 502 };
|
|
3602
|
+
items.push(...body.items);
|
|
3603
|
+
cursor = typeof body.nextCursor === "string" ? body.nextCursor : null;
|
|
3604
|
+
if (!cursor) return validateTargetOrder(items);
|
|
3605
|
+
}
|
|
3606
|
+
return { error: "Destination discovery exceeded its safe page limit.", ok: false, status: 502 };
|
|
3607
|
+
}
|
|
3608
|
+
async function resolveAccountProjectTarget(session, requestedProject) {
|
|
3609
|
+
const result = await fetchAccountProjects(session, false);
|
|
3610
|
+
if (!result.ok) return { error: result.error, kind: "remote", status: result.status };
|
|
3611
|
+
if (requestedProject) {
|
|
3612
|
+
const parsed = parseProjectReference(requestedProject);
|
|
3613
|
+
if (!parsed) return { kind: "invalid", targets: result.items };
|
|
3614
|
+
const target = result.items.find((candidate) => candidate.reference === requestedProject);
|
|
3615
|
+
return target ? { kind: "selected", target } : { kind: "missing", targets: result.items };
|
|
3616
|
+
}
|
|
3617
|
+
if (result.items.length === 1) return { kind: "selected", target: result.items[0] };
|
|
3618
|
+
return result.items.length ? { kind: "ambiguous", targets: result.items } : { kind: "missing", targets: [] };
|
|
3619
|
+
}
|
|
3620
|
+
async function requireCommandSession(options, dependencies) {
|
|
3621
|
+
let config;
|
|
3622
|
+
try {
|
|
3623
|
+
config = await dependencies.loadAuthConfig();
|
|
3624
|
+
} catch (error) {
|
|
3625
|
+
emitLocalFailure(options.json, "auth_state_unavailable", safeMessage(error));
|
|
3626
|
+
return null;
|
|
3627
|
+
}
|
|
3628
|
+
const resolved = resolveAccountSession(config, options.apiUrl ?? dependencies.defaultApiUrl);
|
|
3629
|
+
if (resolved.kind !== "selected") {
|
|
3630
|
+
emitLocalFailure(
|
|
3631
|
+
options.json,
|
|
3632
|
+
`account_session_${resolved.kind}`,
|
|
3633
|
+
resolved.kind === "expired" ? "The Tileflow account session has expired. Run tileflow login." : "Not logged in. Run tileflow login."
|
|
3634
|
+
);
|
|
3635
|
+
return null;
|
|
3636
|
+
}
|
|
3637
|
+
return resolved.session;
|
|
3638
|
+
}
|
|
3639
|
+
async function accountRequest(session, path, init) {
|
|
3640
|
+
let response;
|
|
3641
|
+
try {
|
|
3642
|
+
response = await requestHostedJson(session.apiOrigin, path, {
|
|
3643
|
+
...init,
|
|
3644
|
+
headers: { ...init.headers, Authorization: `Bearer ${session.accountSession}` }
|
|
3645
|
+
});
|
|
3646
|
+
} catch (error) {
|
|
3647
|
+
return {
|
|
3648
|
+
body: null,
|
|
3649
|
+
error: error instanceof Error && /safe size limit|timed out/u.test(error.message) ? error.message : "Hosted request failed.",
|
|
3650
|
+
ok: false,
|
|
3651
|
+
status: 502
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3654
|
+
if (response.ok && !response.json) {
|
|
3655
|
+
return {
|
|
3656
|
+
body: null,
|
|
3657
|
+
error: "Hosted response returned invalid JSON.",
|
|
3658
|
+
ok: false,
|
|
3659
|
+
status: 502
|
|
3660
|
+
};
|
|
3661
|
+
}
|
|
3662
|
+
return response.ok ? { body: response.body, ok: true, status: response.status } : {
|
|
3663
|
+
body: response.body,
|
|
3664
|
+
error: `Request failed (${response.status}).`,
|
|
3665
|
+
ok: false,
|
|
3666
|
+
status: response.status
|
|
3667
|
+
};
|
|
3668
|
+
}
|
|
3669
|
+
function validateTargetOrder(items) {
|
|
3670
|
+
const references = items.map((item) => item.reference);
|
|
3671
|
+
if (new Set(references).size !== references.length || references.some((reference, index) => index > 0 && references[index - 1] >= reference)) {
|
|
3672
|
+
return {
|
|
3673
|
+
error: "Destination discovery returned unstable ordering.",
|
|
3674
|
+
ok: false,
|
|
3675
|
+
status: 502
|
|
3676
|
+
};
|
|
3677
|
+
}
|
|
3678
|
+
return { items, ok: true };
|
|
3679
|
+
}
|
|
3680
|
+
function uniqueOrganizations(items) {
|
|
3681
|
+
return [...new Map(items.map((item) => [item.organization.id, item.organization])).values()];
|
|
3682
|
+
}
|
|
3683
|
+
function parseOrganizationReference(value) {
|
|
3684
|
+
if (!value) return null;
|
|
3685
|
+
const match = /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u.exec(value);
|
|
3686
|
+
return match?.[1] ?? null;
|
|
3687
|
+
}
|
|
3688
|
+
function emitAmbiguousOrganizations(json, organizations, slug, name) {
|
|
3689
|
+
const references = organizations.map((organization) => `@${organization.slug}`);
|
|
3690
|
+
const retry = `tileflow projects create ${slug} --name ${JSON.stringify(name.trim())} --organization ${references[0]}`;
|
|
3691
|
+
return emitLocalFailure(
|
|
3692
|
+
json,
|
|
3693
|
+
"organization_ambiguous",
|
|
3694
|
+
`More than one organization is available: ${references.join(", ")}. Retry with: ${retry}`,
|
|
3695
|
+
{ options: references, retry }
|
|
3696
|
+
);
|
|
3697
|
+
}
|
|
3698
|
+
function emitRemoteFailure(json, result) {
|
|
3699
|
+
return emitLocalFailure(json, `http_${result.status}`, result.error);
|
|
3700
|
+
}
|
|
3701
|
+
function emitLocalFailure(json, code, message, details = {}) {
|
|
3702
|
+
if (json) {
|
|
3703
|
+
console.error(message);
|
|
3704
|
+
emitJson({ schemaVersion: 1, ok: false, error: { code, ...details } });
|
|
3705
|
+
} else {
|
|
3706
|
+
console.error(`${pc.red("\u2715")} ${message}`);
|
|
3707
|
+
}
|
|
3708
|
+
process.exitCode = 1;
|
|
3709
|
+
}
|
|
3710
|
+
function emitJson(value) {
|
|
3711
|
+
process.stdout.write(`${JSON.stringify(value)}
|
|
3712
|
+
`);
|
|
3713
|
+
}
|
|
3714
|
+
function asRecord2(value) {
|
|
3715
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3716
|
+
}
|
|
3717
|
+
function isAccountProjectTarget(value) {
|
|
3718
|
+
const target = asRecord2(value);
|
|
3719
|
+
return isIdentity(target.organization) && isProjectItem(target.project) && typeof target.reference === "string" && target.reference === `@${target.organization.slug}/${target.project.slug}`;
|
|
3720
|
+
}
|
|
3721
|
+
function isIdentity(value) {
|
|
3722
|
+
const identity = asRecord2(value);
|
|
3723
|
+
return typeof identity.id === "string" && typeof identity.name === "string" && typeof identity.slug === "string";
|
|
3724
|
+
}
|
|
3725
|
+
function isProjectItem(value) {
|
|
3726
|
+
const item = asRecord2(value);
|
|
3727
|
+
return isIdentity(value) && (item.archivedAt === null || typeof item.archivedAt === "string") && typeof item.createdAt === "string" && typeof item.updatedAt === "string";
|
|
3728
|
+
}
|
|
3729
|
+
function validSlug(value) {
|
|
3730
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value);
|
|
3731
|
+
}
|
|
3732
|
+
function validName(value) {
|
|
3733
|
+
const normalized = value.trim();
|
|
3734
|
+
return normalized.length > 0 && normalized.length <= 200 && !/[\p{Cc}]/u.test(normalized);
|
|
3735
|
+
}
|
|
3736
|
+
function safeMessage(error) {
|
|
3737
|
+
return error instanceof Error && error.message.length <= 300 ? error.message : "Tileflow auth state is unavailable.";
|
|
3738
|
+
}
|
|
3739
|
+
|
|
3740
|
+
// src/tileset-command.ts
|
|
3741
|
+
import pc2 from "picocolors";
|
|
3742
|
+
import { serializeCanonicalJson as serializeCanonicalJson3 } from "@tileflow/core";
|
|
3743
|
+
import { inspectTileflowPmtiles } from "@tileflow/dev/tilesets";
|
|
3744
|
+
import { z as z6 } from "zod";
|
|
3745
|
+
|
|
3746
|
+
// src/tileset-upload.ts
|
|
3747
|
+
import { createHash } from "crypto";
|
|
3748
|
+
import { open as open3, stat } from "fs/promises";
|
|
3749
|
+
import { z as z5 } from "zod";
|
|
3750
|
+
import { serializeCanonicalJson as serializeCanonicalJson2 } from "@tileflow/core";
|
|
3751
|
+
var teamTilesetPartSize = 16777216;
|
|
3752
|
+
var maximumTeamTilesetBytes = 4e9;
|
|
3753
|
+
var authorizationBatchSize = 16;
|
|
3754
|
+
var uploadConcurrency = 4;
|
|
3755
|
+
var maximumPartAttempts = 4;
|
|
3756
|
+
var maximumControlAttempts = 4;
|
|
3757
|
+
var maximumInitiatingWaitMs = 2 * 60 * 1e3;
|
|
3758
|
+
var partTransferTimeoutMs = 10 * 60 * 1e3;
|
|
3759
|
+
var authorizationClockSkewMs = 3e4;
|
|
3760
|
+
var minimumPartAuthorizationLifetimeMs = partTransferTimeoutMs + authorizationClockSkewMs;
|
|
3761
|
+
var TeamTilesetUploadError = class extends Error {
|
|
3762
|
+
code;
|
|
3763
|
+
details;
|
|
3764
|
+
constructor(code, message, details = {}) {
|
|
3765
|
+
super(message);
|
|
3766
|
+
this.name = "TeamTilesetUploadError";
|
|
3767
|
+
this.code = code;
|
|
3768
|
+
this.details = details;
|
|
3769
|
+
}
|
|
3770
|
+
};
|
|
3771
|
+
function createTeamTilesetPartLayout(byteCount) {
|
|
3772
|
+
if (!Number.isSafeInteger(byteCount) || byteCount < 1 || byteCount > maximumTeamTilesetBytes) {
|
|
3773
|
+
throw new TeamTilesetUploadError(
|
|
3774
|
+
"TF_TILESET_VERSION_TOO_LARGE",
|
|
3775
|
+
"PMTiles archive exceeds the 4,000,000,000-byte platform maximum.",
|
|
3776
|
+
{
|
|
3777
|
+
actualBytes: Number.isSafeInteger(byteCount) && byteCount >= 0 ? byteCount : 0,
|
|
3778
|
+
limitBytes: maximumTeamTilesetBytes
|
|
3779
|
+
}
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
return Object.freeze(
|
|
3783
|
+
Array.from({ length: Math.ceil(byteCount / teamTilesetPartSize) }, (_, index) => {
|
|
3784
|
+
const position = index * teamTilesetPartSize;
|
|
3785
|
+
return Object.freeze({
|
|
3786
|
+
byteCount: Math.min(teamTilesetPartSize, byteCount - position),
|
|
3787
|
+
partNumber: index + 1,
|
|
3788
|
+
position
|
|
3789
|
+
});
|
|
3790
|
+
})
|
|
3791
|
+
);
|
|
3792
|
+
}
|
|
3793
|
+
async function inspectNodeTeamTilesetArchive(path) {
|
|
3794
|
+
let value;
|
|
3795
|
+
try {
|
|
3796
|
+
value = await stat(path, { bigint: true });
|
|
3797
|
+
} catch {
|
|
3798
|
+
throw new TeamTilesetUploadError(
|
|
3799
|
+
"TF_TILESET_ARCHIVE_INVALID",
|
|
3800
|
+
"PMTiles archive is unavailable."
|
|
3801
|
+
);
|
|
3802
|
+
}
|
|
3803
|
+
if (!value.isFile()) {
|
|
3804
|
+
throw new TeamTilesetUploadError(
|
|
3805
|
+
"TF_TILESET_ARCHIVE_INVALID",
|
|
3806
|
+
"PMTiles archive must be a file."
|
|
3807
|
+
);
|
|
3808
|
+
}
|
|
3809
|
+
return checkedIdentity(value.dev, value.ino, value.mtimeNs, value.size);
|
|
3810
|
+
}
|
|
3811
|
+
async function openNodeTeamTilesetArchive(path, expectedIdentity) {
|
|
3812
|
+
const handle = await open3(path, "r");
|
|
3813
|
+
const identity = async () => {
|
|
3814
|
+
let pathStat;
|
|
3815
|
+
let handleStat;
|
|
3816
|
+
try {
|
|
3817
|
+
[pathStat, handleStat] = await Promise.all([
|
|
3818
|
+
stat(path, { bigint: true }),
|
|
3819
|
+
handle.stat({ bigint: true })
|
|
3820
|
+
]);
|
|
3821
|
+
} catch {
|
|
3822
|
+
throw localFileChanged();
|
|
3823
|
+
}
|
|
3824
|
+
if (!pathStat.isFile() || !handleStat.isFile()) throw localFileChanged();
|
|
3825
|
+
const pathIdentity = checkedIdentity(
|
|
3826
|
+
pathStat.dev,
|
|
3827
|
+
pathStat.ino,
|
|
3828
|
+
pathStat.mtimeNs,
|
|
3829
|
+
pathStat.size
|
|
3830
|
+
);
|
|
3831
|
+
const handleIdentity = checkedIdentity(
|
|
3832
|
+
handleStat.dev,
|
|
3833
|
+
handleStat.ino,
|
|
3834
|
+
handleStat.mtimeNs,
|
|
3835
|
+
handleStat.size
|
|
3836
|
+
);
|
|
3837
|
+
assertFileIdentity(pathIdentity, handleIdentity);
|
|
3838
|
+
return pathIdentity;
|
|
3839
|
+
};
|
|
3840
|
+
try {
|
|
3841
|
+
assertFileIdentity(expectedIdentity, await identity());
|
|
3842
|
+
} catch (error) {
|
|
3843
|
+
await handle.close();
|
|
3844
|
+
throw error;
|
|
3845
|
+
}
|
|
3846
|
+
return {
|
|
3847
|
+
close: () => handle.close(),
|
|
3848
|
+
identity,
|
|
3849
|
+
async read(position, byteCount) {
|
|
3850
|
+
const bytes = Buffer.allocUnsafe(byteCount);
|
|
3851
|
+
let offset = 0;
|
|
3852
|
+
while (offset < byteCount) {
|
|
3853
|
+
const result = await handle.read(bytes, offset, byteCount - offset, position + offset);
|
|
3854
|
+
if (result.bytesRead === 0) throw localFileChanged();
|
|
3855
|
+
offset += result.bytesRead;
|
|
3856
|
+
}
|
|
3857
|
+
return bytes;
|
|
3858
|
+
}
|
|
3859
|
+
};
|
|
3860
|
+
}
|
|
3861
|
+
async function prepareTeamTilesetUpload(file) {
|
|
3862
|
+
const identity = await file.identity();
|
|
3863
|
+
const layout = createTeamTilesetPartLayout(identity.size);
|
|
3864
|
+
const parts = [];
|
|
3865
|
+
for (const part of layout) {
|
|
3866
|
+
const bytes = await file.read(part.position, part.byteCount);
|
|
3867
|
+
if (bytes.byteLength !== part.byteCount) throw localFileChanged();
|
|
3868
|
+
parts.push(
|
|
3869
|
+
Object.freeze({
|
|
3870
|
+
byteCount: part.byteCount,
|
|
3871
|
+
md5Base64: createHash("md5").update(bytes).digest("base64"),
|
|
3872
|
+
partNumber: part.partNumber,
|
|
3873
|
+
sha256Base64: createHash("sha256").update(bytes).digest("base64")
|
|
3874
|
+
})
|
|
3875
|
+
);
|
|
3876
|
+
}
|
|
3877
|
+
assertFileIdentity(identity, await file.identity());
|
|
3878
|
+
const contentIdentity = {
|
|
3879
|
+
algorithm: "sha256-fixed-parts-v1",
|
|
3880
|
+
byteCount: identity.size,
|
|
3881
|
+
partSize: teamTilesetPartSize,
|
|
3882
|
+
parts: parts.map(({ byteCount, partNumber, sha256Base64 }) => ({
|
|
3883
|
+
byteCount,
|
|
3884
|
+
partNumber,
|
|
3885
|
+
sha256Base64
|
|
3886
|
+
}))
|
|
3887
|
+
};
|
|
3888
|
+
return Object.freeze({
|
|
3889
|
+
byteCount: identity.size,
|
|
3890
|
+
contentHash: createHash("sha256").update(serializeCanonicalJson2(contentIdentity)).digest("hex"),
|
|
3891
|
+
contentHashAlgorithm: "sha256-fixed-parts-v1",
|
|
3892
|
+
identity,
|
|
3893
|
+
partSize: teamTilesetPartSize,
|
|
3894
|
+
parts: Object.freeze(parts),
|
|
3895
|
+
schemaVersion: 1
|
|
3896
|
+
});
|
|
3897
|
+
}
|
|
3898
|
+
function createTeamTilesetUploadIdempotencyKey(tilesetId, upload) {
|
|
3899
|
+
if (!/^tls_[A-Za-z0-9_-]{16,76}$/u.test(tilesetId)) {
|
|
3900
|
+
throw new TeamTilesetUploadError(
|
|
3901
|
+
"TF_TILESET_NOT_FOUND",
|
|
3902
|
+
"Tileset registration returned an invalid ID."
|
|
3903
|
+
);
|
|
3904
|
+
}
|
|
3905
|
+
return createHash("sha256").update(
|
|
3906
|
+
serializeCanonicalJson2({
|
|
3907
|
+
byteCount: upload.byteCount,
|
|
3908
|
+
contentHash: upload.contentHash,
|
|
3909
|
+
contentHashAlgorithm: upload.contentHashAlgorithm,
|
|
3910
|
+
tilesetId
|
|
3911
|
+
})
|
|
3912
|
+
).digest("hex");
|
|
3913
|
+
}
|
|
3914
|
+
async function publishPreparedTeamTilesetUpload(input) {
|
|
3915
|
+
assertFileIdentity(input.upload.identity, await input.file.identity());
|
|
3916
|
+
const beginInput = {
|
|
3917
|
+
byteCount: input.upload.byteCount,
|
|
3918
|
+
contentHash: input.upload.contentHash,
|
|
3919
|
+
contentHashAlgorithm: input.upload.contentHashAlgorithm,
|
|
3920
|
+
idempotencyKey: createTeamTilesetUploadIdempotencyKey(input.tilesetId, input.upload),
|
|
3921
|
+
parts: input.upload.parts,
|
|
3922
|
+
partSize: input.upload.partSize,
|
|
3923
|
+
schemaVersion: 1
|
|
3924
|
+
};
|
|
3925
|
+
const begun = await input.transport.begin(beginInput);
|
|
3926
|
+
assertSessionMatches(begun, input.upload);
|
|
3927
|
+
const changed = begun.state !== "published";
|
|
3928
|
+
if (begun.state === "published") return publicationResult(begun, input.upload, changed);
|
|
3929
|
+
let current = await input.transport.status(begun.uploadId);
|
|
3930
|
+
assertSessionMatches(current, input.upload, true);
|
|
3931
|
+
assertUsableSession(current);
|
|
3932
|
+
let initiatingWaitMs = 0;
|
|
3933
|
+
for (let attempt = 1; current.state === "initiating"; attempt += 1) {
|
|
3934
|
+
const delay = Math.min(
|
|
3935
|
+
2e3,
|
|
3936
|
+
250 * 2 ** Math.min(attempt - 1, 3),
|
|
3937
|
+
maximumInitiatingWaitMs - initiatingWaitMs
|
|
3938
|
+
);
|
|
3939
|
+
if (delay <= 0) {
|
|
3940
|
+
throw new TeamTilesetUploadError(
|
|
3941
|
+
"TF_TILESET_UPLOAD_UNAVAILABLE",
|
|
3942
|
+
"Tileset upload initialization did not converge within two minutes."
|
|
3943
|
+
);
|
|
3944
|
+
}
|
|
3945
|
+
await (input.sleep ?? defaultSleep)(delay);
|
|
3946
|
+
initiatingWaitMs += delay;
|
|
3947
|
+
current = await input.transport.status(current.uploadId);
|
|
3948
|
+
assertSessionMatches(current, input.upload, true);
|
|
3949
|
+
assertUsableSession(current);
|
|
3950
|
+
}
|
|
3951
|
+
if (current.state === "uploading") {
|
|
3952
|
+
const uploading = current;
|
|
3953
|
+
const missingParts = input.upload.parts.filter(
|
|
3954
|
+
(part) => !uploading.parts.find((candidate) => candidate.partNumber === part.partNumber)?.uploaded
|
|
3955
|
+
);
|
|
3956
|
+
await uploadMissingParts({ ...input, missingParts, uploadId: uploading.uploadId });
|
|
3957
|
+
assertFileIdentity(input.upload.identity, await input.file.identity());
|
|
3958
|
+
current = await input.transport.complete(uploading.uploadId);
|
|
3959
|
+
assertSessionMatches(current, input.upload);
|
|
3960
|
+
assertUsableSession(current);
|
|
3961
|
+
}
|
|
3962
|
+
while (current.state !== "published") {
|
|
3963
|
+
if (!["publishing", "ready_to_publish", "validating"].includes(current.state)) {
|
|
3964
|
+
throw new TeamTilesetUploadError(
|
|
3965
|
+
"TF_TILESET_UPLOAD_UNAVAILABLE",
|
|
3966
|
+
"Tileset upload cannot continue from its current state."
|
|
3967
|
+
);
|
|
3968
|
+
}
|
|
3969
|
+
input.onProgress?.({
|
|
3970
|
+
completedParts: input.upload.parts.length,
|
|
3971
|
+
phase: "validating",
|
|
3972
|
+
totalParts: input.upload.parts.length
|
|
3973
|
+
});
|
|
3974
|
+
await (input.sleep ?? defaultSleep)(2e3);
|
|
3975
|
+
current = await input.transport.status(current.uploadId);
|
|
3976
|
+
assertSessionMatches(current, input.upload, true);
|
|
3977
|
+
assertUsableSession(current);
|
|
3978
|
+
}
|
|
3979
|
+
return publicationResult(current, input.upload, changed);
|
|
3980
|
+
}
|
|
3981
|
+
function createHostedTeamTilesetUploadTransport(input) {
|
|
3982
|
+
const basePath = `/v1/tilesets/${encodeURIComponent(input.tilesetId)}/uploads`;
|
|
3983
|
+
const control = async (path, init) => {
|
|
3984
|
+
for (let attempt = 1; attempt <= maximumControlAttempts; attempt += 1) {
|
|
3985
|
+
let response;
|
|
3986
|
+
try {
|
|
3987
|
+
response = await requestHostedJson(
|
|
3988
|
+
input.apiOrigin,
|
|
3989
|
+
path,
|
|
3990
|
+
{
|
|
3991
|
+
...init,
|
|
3992
|
+
headers: {
|
|
3993
|
+
Authorization: `Bearer ${input.credential}`,
|
|
3994
|
+
...init.body ? { "Content-Type": "application/json" } : {},
|
|
3995
|
+
...init.headers
|
|
3996
|
+
}
|
|
3997
|
+
},
|
|
3998
|
+
{ fetch: input.fetch }
|
|
3999
|
+
);
|
|
4000
|
+
} catch {
|
|
4001
|
+
if (attempt === maximumControlAttempts) {
|
|
4002
|
+
throw new TeamTilesetUploadError(
|
|
4003
|
+
"TF_TILESET_UPLOAD_UNAVAILABLE",
|
|
4004
|
+
"Tileset upload control request did not complete."
|
|
4005
|
+
);
|
|
4006
|
+
}
|
|
4007
|
+
}
|
|
4008
|
+
if (response?.ok) return response.body;
|
|
4009
|
+
if (response && !isRetryableStatus(response.status)) throw responseError(response);
|
|
4010
|
+
if (attempt === maximumControlAttempts) {
|
|
4011
|
+
if (response) throw responseError(response);
|
|
4012
|
+
break;
|
|
4013
|
+
}
|
|
4014
|
+
await (input.sleep ?? defaultSleep)(Math.min(2e3, 250 * 2 ** (attempt - 1)));
|
|
4015
|
+
}
|
|
4016
|
+
throw new TeamTilesetUploadError(
|
|
4017
|
+
"TF_TILESET_UPLOAD_UNAVAILABLE",
|
|
4018
|
+
"Tileset upload control request did not complete."
|
|
4019
|
+
);
|
|
4020
|
+
};
|
|
4021
|
+
return {
|
|
4022
|
+
async authorize(uploadId, partNumbers) {
|
|
4023
|
+
const body = await control(
|
|
4024
|
+
`${basePath}/${encodeURIComponent(uploadId)}/part-authorizations`,
|
|
4025
|
+
{
|
|
4026
|
+
body: JSON.stringify({ partNumbers, schemaVersion: 1 }),
|
|
4027
|
+
method: "POST"
|
|
4028
|
+
}
|
|
4029
|
+
);
|
|
4030
|
+
return authorizationResponseSchema.parse(body).authorizations;
|
|
4031
|
+
},
|
|
4032
|
+
async begin(body) {
|
|
4033
|
+
return parseUploadSession(
|
|
4034
|
+
await control(basePath, { body: JSON.stringify(body), method: "POST" })
|
|
4035
|
+
);
|
|
4036
|
+
},
|
|
4037
|
+
async complete(uploadId) {
|
|
4038
|
+
return parseUploadSession(
|
|
4039
|
+
await control(`${basePath}/${encodeURIComponent(uploadId)}/complete`, {
|
|
4040
|
+
body: JSON.stringify({ schemaVersion: 1 }),
|
|
4041
|
+
method: "POST"
|
|
4042
|
+
})
|
|
4043
|
+
);
|
|
4044
|
+
},
|
|
4045
|
+
async putPart(authorization, bytes) {
|
|
4046
|
+
let response;
|
|
4047
|
+
try {
|
|
4048
|
+
const body = new Uint8Array(
|
|
4049
|
+
bytes.buffer,
|
|
4050
|
+
bytes.byteOffset,
|
|
4051
|
+
bytes.byteLength
|
|
4052
|
+
);
|
|
4053
|
+
response = await (input.fetch ?? globalThis.fetch)(authorization.url, {
|
|
4054
|
+
body,
|
|
4055
|
+
headers: authorization.headers,
|
|
4056
|
+
method: "PUT",
|
|
4057
|
+
redirect: "error",
|
|
4058
|
+
signal: AbortSignal.timeout(partTransferTimeoutMs)
|
|
4059
|
+
});
|
|
4060
|
+
} catch {
|
|
4061
|
+
throw new Error("Direct part transfer failed.");
|
|
4062
|
+
}
|
|
4063
|
+
const etag = response.headers.get("etag");
|
|
4064
|
+
await response.body?.cancel().catch(() => void 0);
|
|
4065
|
+
return { etag, status: response.status };
|
|
4066
|
+
},
|
|
4067
|
+
async recordPart(uploadId, body) {
|
|
4068
|
+
partReceiptResponseSchema.parse(
|
|
4069
|
+
await control(`${basePath}/${encodeURIComponent(uploadId)}/parts`, {
|
|
4070
|
+
body: JSON.stringify(body),
|
|
4071
|
+
method: "POST"
|
|
4072
|
+
})
|
|
4073
|
+
);
|
|
4074
|
+
},
|
|
4075
|
+
async status(uploadId) {
|
|
4076
|
+
return parseUploadSession(
|
|
4077
|
+
await control(`${basePath}/${encodeURIComponent(uploadId)}`, { method: "GET" })
|
|
4078
|
+
);
|
|
4079
|
+
}
|
|
4080
|
+
};
|
|
4081
|
+
}
|
|
4082
|
+
async function uploadMissingParts(input) {
|
|
4083
|
+
let completedParts = input.upload.parts.length - input.missingParts.length;
|
|
4084
|
+
for (let offset = 0; offset < input.missingParts.length; offset += authorizationBatchSize) {
|
|
4085
|
+
const authorizationParts = input.missingParts.slice(offset, offset + authorizationBatchSize);
|
|
4086
|
+
const initial = indexAuthorizations(
|
|
4087
|
+
await input.transport.authorize(
|
|
4088
|
+
input.uploadId,
|
|
4089
|
+
authorizationParts.map(({ partNumber }) => partNumber)
|
|
4090
|
+
),
|
|
4091
|
+
authorizationParts
|
|
4092
|
+
);
|
|
4093
|
+
for (let groupOffset = 0; groupOffset < authorizationParts.length; groupOffset += uploadConcurrency) {
|
|
4094
|
+
assertFileIdentity(input.upload.identity, await input.file.identity());
|
|
4095
|
+
const group = authorizationParts.slice(groupOffset, groupOffset + uploadConcurrency);
|
|
4096
|
+
const settled = await Promise.allSettled(
|
|
4097
|
+
group.map(async (part) => {
|
|
4098
|
+
const position = (part.partNumber - 1) * teamTilesetPartSize;
|
|
4099
|
+
const bytes = await input.file.read(position, part.byteCount);
|
|
4100
|
+
if (bytes.byteLength !== part.byteCount) throw localFileChanged();
|
|
4101
|
+
await uploadPartWithRetry({
|
|
4102
|
+
...input,
|
|
4103
|
+
initialAuthorization: initial.get(part.partNumber),
|
|
4104
|
+
part,
|
|
4105
|
+
bytes
|
|
4106
|
+
});
|
|
4107
|
+
})
|
|
4108
|
+
);
|
|
4109
|
+
const failed = settled.find(
|
|
4110
|
+
(result) => result.status === "rejected"
|
|
4111
|
+
);
|
|
4112
|
+
if (failed) throw failed.reason;
|
|
4113
|
+
completedParts += group.length;
|
|
4114
|
+
input.onProgress?.({
|
|
4115
|
+
completedParts,
|
|
4116
|
+
phase: "uploading",
|
|
4117
|
+
totalParts: input.upload.parts.length
|
|
4118
|
+
});
|
|
4119
|
+
}
|
|
4120
|
+
}
|
|
4121
|
+
}
|
|
4122
|
+
async function uploadPartWithRetry(input) {
|
|
4123
|
+
let authorization = input.initialAuthorization;
|
|
4124
|
+
for (let attempt = 1; attempt <= maximumPartAttempts; attempt += 1) {
|
|
4125
|
+
if (!authorizationCoversTransfer(authorization, input.now?.() ?? Date.now())) {
|
|
4126
|
+
const refreshed2 = await input.transport.authorize(input.uploadId, [input.part.partNumber]);
|
|
4127
|
+
authorization = indexAuthorizations(refreshed2, [input.part]).get(input.part.partNumber);
|
|
4128
|
+
if (!authorizationCoversTransfer(authorization, input.now?.() ?? Date.now())) {
|
|
4129
|
+
throw invalidRemoteResponse();
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
assertAuthorizationMatches(authorization, input.part);
|
|
4133
|
+
let result;
|
|
4134
|
+
try {
|
|
4135
|
+
result = await input.transport.putPart(authorization, input.bytes);
|
|
4136
|
+
} catch {
|
|
4137
|
+
result = { etag: null, status: 503 };
|
|
4138
|
+
}
|
|
4139
|
+
if (result.status >= 200 && result.status < 300) {
|
|
4140
|
+
const etag = normalizePartEtag(result.etag, input.part.md5Base64);
|
|
4141
|
+
await input.transport.recordPart(input.uploadId, {
|
|
4142
|
+
etag,
|
|
4143
|
+
partNumber: input.part.partNumber
|
|
4144
|
+
});
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if ((result.status === 401 || result.status === 403) && !authorizationCoversTransfer(authorization, input.now?.() ?? Date.now()) && attempt < maximumPartAttempts) {
|
|
4148
|
+
const refreshed2 = await input.transport.authorize(input.uploadId, [input.part.partNumber]);
|
|
4149
|
+
authorization = indexAuthorizations(refreshed2, [input.part]).get(input.part.partNumber);
|
|
4150
|
+
continue;
|
|
4151
|
+
}
|
|
4152
|
+
if (!isRetryableStatus(result.status)) {
|
|
4153
|
+
throw new TeamTilesetUploadError(
|
|
4154
|
+
result.status === 401 || result.status === 403 ? "TF_TILESET_UPLOAD_AUTHORIZATION_FAILED" : "TF_TILESET_UPLOAD_PART_INVALID",
|
|
4155
|
+
"Direct part transfer was rejected."
|
|
4156
|
+
);
|
|
4157
|
+
}
|
|
4158
|
+
if (attempt === maximumPartAttempts) break;
|
|
4159
|
+
await (input.sleep ?? defaultSleep)(Math.min(2e3, 250 * 2 ** (attempt - 1)));
|
|
4160
|
+
const refreshed = await input.transport.authorize(input.uploadId, [input.part.partNumber]);
|
|
4161
|
+
authorization = indexAuthorizations(refreshed, [input.part]).get(input.part.partNumber);
|
|
4162
|
+
}
|
|
4163
|
+
throw new TeamTilesetUploadError(
|
|
4164
|
+
"TF_TILESET_UPLOAD_UNAVAILABLE",
|
|
4165
|
+
"Direct part transfer did not complete after bounded retries."
|
|
4166
|
+
);
|
|
4167
|
+
}
|
|
4168
|
+
function authorizationCoversTransfer(authorization, now) {
|
|
4169
|
+
return Date.parse(authorization.expiresAt) - now >= minimumPartAuthorizationLifetimeMs;
|
|
4170
|
+
}
|
|
4171
|
+
function isRetryableStatus(status) {
|
|
4172
|
+
return status === 408 || status === 429 || status >= 500;
|
|
4173
|
+
}
|
|
4174
|
+
function indexAuthorizations(authorizations, parts) {
|
|
4175
|
+
const expected = new Map(parts.map((part) => [part.partNumber, part]));
|
|
4176
|
+
const indexed = /* @__PURE__ */ new Map();
|
|
4177
|
+
for (const authorization of authorizations) {
|
|
4178
|
+
const part = expected.get(authorization.partNumber);
|
|
4179
|
+
if (!part || indexed.has(authorization.partNumber)) {
|
|
4180
|
+
throw invalidRemoteResponse();
|
|
4181
|
+
}
|
|
4182
|
+
assertAuthorizationMatches(authorization, part);
|
|
4183
|
+
indexed.set(authorization.partNumber, authorization);
|
|
4184
|
+
}
|
|
4185
|
+
if (indexed.size !== expected.size) throw invalidRemoteResponse();
|
|
4186
|
+
return indexed;
|
|
4187
|
+
}
|
|
4188
|
+
function assertAuthorizationMatches(authorization, part) {
|
|
4189
|
+
if (authorization.partNumber !== part.partNumber || authorization.method !== "PUT" || authorization.headers["Content-Length"] !== String(part.byteCount) || authorization.headers["Content-MD5"] !== part.md5Base64) {
|
|
4190
|
+
throw invalidRemoteResponse();
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
function normalizePartEtag(value, md5Base64) {
|
|
4194
|
+
if (!value || !/^(?:[0-9a-f]{32}|"[0-9a-f]{32}")$/u.test(value)) {
|
|
4195
|
+
throw new TeamTilesetUploadError(
|
|
4196
|
+
"TF_TILESET_UPLOAD_PART_INVALID",
|
|
4197
|
+
"Direct part transfer returned an invalid ETag."
|
|
4198
|
+
);
|
|
4199
|
+
}
|
|
4200
|
+
const normalized = value.replaceAll('"', "");
|
|
4201
|
+
if (normalized !== Buffer.from(md5Base64, "base64").toString("hex")) {
|
|
4202
|
+
throw new TeamTilesetUploadError(
|
|
4203
|
+
"TF_TILESET_UPLOAD_PART_INVALID",
|
|
4204
|
+
"Direct part transfer returned a conflicting ETag."
|
|
4205
|
+
);
|
|
4206
|
+
}
|
|
4207
|
+
return normalized;
|
|
4208
|
+
}
|
|
4209
|
+
function assertSessionMatches(session, upload, requireParts = false) {
|
|
4210
|
+
if (session.byteCount !== upload.byteCount || session.contentHash !== upload.contentHash || session.contentHashAlgorithm !== upload.contentHashAlgorithm) {
|
|
4211
|
+
throw invalidRemoteResponse();
|
|
4212
|
+
}
|
|
4213
|
+
if (session.state === "published") return;
|
|
4214
|
+
if (session.partCount !== upload.parts.length || session.partSize !== upload.partSize || requireParts && session.parts.length !== upload.parts.length || session.parts.some((part, index) => {
|
|
4215
|
+
const expected = upload.parts[index];
|
|
4216
|
+
return !expected || part.partNumber !== expected.partNumber || part.byteCount !== expected.byteCount;
|
|
4217
|
+
})) {
|
|
4218
|
+
throw invalidRemoteResponse();
|
|
4219
|
+
}
|
|
4220
|
+
}
|
|
4221
|
+
function assertUsableSession(session) {
|
|
4222
|
+
if (session.state === "published") return;
|
|
4223
|
+
if (session.failure) {
|
|
4224
|
+
throw new TeamTilesetUploadError(session.failure.code, failureMessage(session.failure.code));
|
|
4225
|
+
}
|
|
4226
|
+
if (["aborted", "aborting", "expired", "expiring", "rejected", "rejecting"].includes(session.state)) {
|
|
4227
|
+
throw new TeamTilesetUploadError(
|
|
4228
|
+
"TF_TILESET_UPLOAD_UNAVAILABLE",
|
|
4229
|
+
"Tileset upload is no longer resumable."
|
|
4230
|
+
);
|
|
4231
|
+
}
|
|
4232
|
+
}
|
|
4233
|
+
function publicationResult(session, upload, changed) {
|
|
4234
|
+
return Object.freeze({
|
|
4235
|
+
byteCount: upload.byteCount,
|
|
4236
|
+
changed,
|
|
4237
|
+
contentHash: upload.contentHash,
|
|
4238
|
+
contentHashAlgorithm: upload.contentHashAlgorithm,
|
|
4239
|
+
state: "published",
|
|
4240
|
+
version: session.publishedVersion,
|
|
4241
|
+
versionId: session.versionId
|
|
4242
|
+
});
|
|
4243
|
+
}
|
|
4244
|
+
function assertFileIdentity(expected, actual) {
|
|
4245
|
+
const inodeChanged = expected.inode !== 0n && actual.inode !== 0n && (expected.inode !== actual.inode || expected.device !== actual.device);
|
|
4246
|
+
if (inodeChanged || expected.size !== actual.size || expected.mtimeNanoseconds !== actual.mtimeNanoseconds) {
|
|
4247
|
+
throw localFileChanged();
|
|
4248
|
+
}
|
|
4249
|
+
}
|
|
4250
|
+
function checkedIdentity(device, inode, mtimeNanoseconds, size) {
|
|
4251
|
+
const numericSize = Number(size);
|
|
4252
|
+
if (numericSize === 0) {
|
|
4253
|
+
throw new TeamTilesetUploadError("TF_TILESET_ARCHIVE_EMPTY", "PMTiles archive is empty.");
|
|
4254
|
+
}
|
|
4255
|
+
if (numericSize > maximumTeamTilesetBytes) {
|
|
4256
|
+
throw new TeamTilesetUploadError(
|
|
4257
|
+
"TF_TILESET_VERSION_TOO_LARGE",
|
|
4258
|
+
"PMTiles archive exceeds the 4,000,000,000-byte platform maximum.",
|
|
4259
|
+
{ actualBytes: numericSize, limitBytes: maximumTeamTilesetBytes }
|
|
4260
|
+
);
|
|
4261
|
+
}
|
|
4262
|
+
return Object.freeze({ device, inode, mtimeNanoseconds, size: numericSize });
|
|
4263
|
+
}
|
|
4264
|
+
function localFileChanged() {
|
|
4265
|
+
return new TeamTilesetUploadError(
|
|
4266
|
+
"TF_TILESET_LOCAL_FILE_CHANGED",
|
|
4267
|
+
"PMTiles archive changed during publication."
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
4270
|
+
function invalidRemoteResponse() {
|
|
4271
|
+
return new TeamTilesetUploadError(
|
|
4272
|
+
"TF_TILESET_UPLOAD_UNAVAILABLE",
|
|
4273
|
+
"Tileset upload returned an invalid response."
|
|
4274
|
+
);
|
|
4275
|
+
}
|
|
4276
|
+
function failureMessage(code) {
|
|
4277
|
+
if (code === "TF_TILESET_SCHEMA_INCOMPATIBLE") {
|
|
4278
|
+
return "Tileset schema is incompatible with retained Map deployments.";
|
|
4279
|
+
}
|
|
4280
|
+
if (code === "TF_TILESET_UPLOAD_CHECKSUM_MISMATCH") {
|
|
4281
|
+
return "Tileset upload checksum validation failed.";
|
|
4282
|
+
}
|
|
4283
|
+
if (code === "TF_TILESET_UPLOAD_EXPIRED") return "Tileset upload expired.";
|
|
4284
|
+
return "Tileset upload failed.";
|
|
4285
|
+
}
|
|
4286
|
+
function responseError(response) {
|
|
4287
|
+
const body = asRecord3(response.body);
|
|
4288
|
+
const code = typeof body.code === "string" && /^TF_[A-Z0-9_]{1,96}$/u.test(body.code) ? body.code : "TF_TILESET_UPLOAD_UNAVAILABLE";
|
|
4289
|
+
const details = numericDetails(body.details);
|
|
4290
|
+
return new TeamTilesetUploadError(code, failureMessage(code), details);
|
|
4291
|
+
}
|
|
4292
|
+
function numericDetails(value) {
|
|
4293
|
+
const source = asRecord3(value);
|
|
4294
|
+
const details = {};
|
|
4295
|
+
for (const key of ["actualBytes", "limitBytes"]) {
|
|
4296
|
+
const candidate = source[key];
|
|
4297
|
+
if (Number.isSafeInteger(candidate) && candidate >= 0) {
|
|
4298
|
+
details[key] = candidate;
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
return Object.freeze(details);
|
|
4302
|
+
}
|
|
4303
|
+
function parseUploadSession(value) {
|
|
4304
|
+
return uploadSessionSchema.parse(value);
|
|
4305
|
+
}
|
|
4306
|
+
function asRecord3(value) {
|
|
4307
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4308
|
+
}
|
|
4309
|
+
function defaultSleep(milliseconds) {
|
|
4310
|
+
return new Promise((resolve8) => setTimeout(resolve8, milliseconds));
|
|
4311
|
+
}
|
|
4312
|
+
var identifierSchema = z5.string().min(20).max(80).regex(/^[A-Za-z0-9_-]+$/u);
|
|
4313
|
+
var uploadPartStatusSchema = z5.object({
|
|
4314
|
+
byteCount: z5.number().int().min(1).max(teamTilesetPartSize),
|
|
4315
|
+
partNumber: z5.number().int().min(1).max(239),
|
|
4316
|
+
uploaded: z5.boolean(),
|
|
4317
|
+
validationState: z5.enum(["mismatch", "pending", "processing", "valid"])
|
|
4318
|
+
}).strict();
|
|
4319
|
+
var publishedVersionSchema = z5.object({
|
|
4320
|
+
byteCount: z5.number().int().min(1).max(maximumTeamTilesetBytes),
|
|
4321
|
+
contentHash: z5.string().regex(/^[0-9a-f]{64}$/u),
|
|
4322
|
+
contentHashAlgorithm: z5.literal("sha256-fixed-parts-v1"),
|
|
4323
|
+
publishedVersion: z5.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
4324
|
+
schemaVersion: z5.literal(1),
|
|
4325
|
+
state: z5.literal("published"),
|
|
4326
|
+
versionId: identifierSchema.regex(/^tsv_[A-Za-z0-9_-]+$/u)
|
|
4327
|
+
}).strict();
|
|
4328
|
+
var uploadControlSchema = z5.object({
|
|
4329
|
+
byteCount: z5.number().int().min(1).max(maximumTeamTilesetBytes),
|
|
4330
|
+
contentHash: z5.string().regex(/^[0-9a-f]{64}$/u),
|
|
4331
|
+
contentHashAlgorithm: z5.literal("sha256-fixed-parts-v1"),
|
|
4332
|
+
expiresAt: z5.iso.datetime({ offset: true }),
|
|
4333
|
+
failure: z5.object({ code: z5.string().regex(/^TF_[A-Z0-9_]{1,96}$/u) }).strict().nullable(),
|
|
4334
|
+
partCount: z5.number().int().min(1).max(239),
|
|
4335
|
+
parts: z5.array(uploadPartStatusSchema).max(239),
|
|
4336
|
+
partSize: z5.literal(teamTilesetPartSize),
|
|
4337
|
+
publishedVersion: z5.null(),
|
|
4338
|
+
schemaVersion: z5.literal(1),
|
|
4339
|
+
state: z5.enum([
|
|
4340
|
+
"aborted",
|
|
4341
|
+
"aborting",
|
|
4342
|
+
"expired",
|
|
4343
|
+
"expiring",
|
|
4344
|
+
"initiating",
|
|
4345
|
+
"publishing",
|
|
4346
|
+
"ready_to_publish",
|
|
4347
|
+
"rejected",
|
|
4348
|
+
"rejecting",
|
|
4349
|
+
"uploading",
|
|
4350
|
+
"validating"
|
|
4351
|
+
]),
|
|
4352
|
+
uploadId: z5.string().min(20).max(80).regex(/^tsu_[A-Za-z0-9_-]+$/u),
|
|
4353
|
+
versionId: z5.null()
|
|
4354
|
+
}).strict();
|
|
4355
|
+
var uploadSessionSchema = z5.union([publishedVersionSchema, uploadControlSchema]);
|
|
4356
|
+
var authorizationSchema = z5.object({
|
|
4357
|
+
expiresAt: z5.iso.datetime({ offset: true }),
|
|
4358
|
+
headers: z5.object({
|
|
4359
|
+
"Content-Length": z5.string().regex(/^[1-9][0-9]{0,9}$/u),
|
|
4360
|
+
"Content-MD5": z5.string().regex(/^[A-Za-z0-9+/]{22}==$/u)
|
|
4361
|
+
}).strict(),
|
|
4362
|
+
method: z5.literal("PUT"),
|
|
4363
|
+
partNumber: z5.number().int().min(1).max(239),
|
|
4364
|
+
url: z5.url().max(8192).refine((value) => {
|
|
4365
|
+
const url = new URL(value);
|
|
4366
|
+
return url.protocol === "https:" && !url.username && !url.password && !url.hash;
|
|
4367
|
+
})
|
|
4368
|
+
}).strict();
|
|
4369
|
+
var authorizationResponseSchema = z5.object({
|
|
4370
|
+
authorizations: z5.array(authorizationSchema).min(1).max(16),
|
|
4371
|
+
schemaVersion: z5.literal(1)
|
|
4372
|
+
}).strict();
|
|
4373
|
+
var partReceiptResponseSchema = z5.object({
|
|
4374
|
+
changed: z5.boolean(),
|
|
4375
|
+
etag: z5.string().regex(/^[0-9a-f]{32}$/u),
|
|
4376
|
+
partNumber: z5.number().int().min(1).max(239),
|
|
4377
|
+
schemaVersion: z5.literal(1)
|
|
4378
|
+
}).strict();
|
|
4379
|
+
|
|
4380
|
+
// src/tileset-command.ts
|
|
4381
|
+
function registerTilesetCommands(program2, dependencies) {
|
|
4382
|
+
const tileset = program2.command("tileset").description("Inspect and manage geospatial tilesets");
|
|
4383
|
+
tileset.command("inspect").description("Inspect one local PMTiles archive deterministically").argument("<archive>", "repository-local PMTiles archive").option("--json", "print deterministic inspection-schema-version-1 JSON").option("--no-sample", "read authoritative header and metadata without bounded MVT sampling").option(
|
|
4384
|
+
"--include-values <fields>",
|
|
4385
|
+
"include bounded observed values for comma-separated field names (requires --json)"
|
|
4386
|
+
).action(
|
|
4387
|
+
async (archive, options) => {
|
|
4388
|
+
try {
|
|
4389
|
+
if (options.includeValues && !options.json) {
|
|
4390
|
+
throw inspectionOptionError("--include-values requires --json.");
|
|
4391
|
+
}
|
|
4392
|
+
const inspection = await inspectTileflowPmtiles(archive, {
|
|
4393
|
+
includeValues: parseIncludedValueFields(options.includeValues),
|
|
4394
|
+
sample: options.sample
|
|
4395
|
+
});
|
|
4396
|
+
if (options.json) {
|
|
4397
|
+
process.stdout.write(`${serializeCanonicalJson3(inspection)}
|
|
4398
|
+
`);
|
|
4399
|
+
return;
|
|
4400
|
+
}
|
|
4401
|
+
console.log(`${pc2.green("\u2713")} PMTiles archive is valid.`);
|
|
4402
|
+
console.log(` Type: ${inspection.contract.tileType}`);
|
|
4403
|
+
console.log(` Zooms: ${inspection.contract.minzoom}-${inspection.contract.maxzoom}`);
|
|
4404
|
+
console.log(` Source layers: ${inspection.contract.sourceLayers.length}`);
|
|
4405
|
+
if (inspection.observation) {
|
|
4406
|
+
console.log(
|
|
4407
|
+
` Sample: ${inspection.observation.featuresRead} features in ${inspection.observation.tilesRead} tiles`
|
|
4408
|
+
);
|
|
4409
|
+
}
|
|
4410
|
+
for (const warning of inspection.warnings) {
|
|
4411
|
+
console.log(`${pc2.yellow("!")} ${warning.message}`);
|
|
4412
|
+
}
|
|
4413
|
+
} catch (error) {
|
|
4414
|
+
const record = asRecord4(error);
|
|
4415
|
+
const code = typeof record.code === "string" ? record.code : "TF_TILESET_INSPECTION_FAILED";
|
|
4416
|
+
const path = typeof record.path === "string" ? record.path : "archive";
|
|
4417
|
+
const message = error instanceof Error ? error.message : "Tileset inspection failed.";
|
|
4418
|
+
if (options.json) {
|
|
4419
|
+
process.stderr.write(
|
|
4420
|
+
`${serializeCanonicalJson3({ error: { code, message, path }, ok: false, schemaVersion: 1 })}
|
|
4421
|
+
`
|
|
4422
|
+
);
|
|
4423
|
+
} else {
|
|
4424
|
+
console.error(`${pc2.red("Error:")} ${message}`);
|
|
4425
|
+
}
|
|
4426
|
+
process.exitCode = 1;
|
|
4427
|
+
}
|
|
4428
|
+
}
|
|
4429
|
+
);
|
|
4430
|
+
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").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Team data API key", process.env.TILEFLOW_API_KEY).option("--json", "print deterministic schema-version-2 JSON").action(
|
|
4431
|
+
async (archive, options) => {
|
|
4432
|
+
let uploadFile = null;
|
|
4433
|
+
try {
|
|
4434
|
+
if (!isTilesetId(options.id)) {
|
|
4435
|
+
return emitFailure(options.json, "invalid_tileset_id", "Tileset ID is invalid.");
|
|
4436
|
+
}
|
|
4437
|
+
const initialIdentity = await inspectNodeTeamTilesetArchive(archive);
|
|
4438
|
+
await inspectTileflowPmtiles(archive, { sample: false });
|
|
4439
|
+
uploadFile = await openNodeTeamTilesetArchive(archive, initialIdentity);
|
|
4440
|
+
const prepared = await prepareTeamTilesetUpload(uploadFile);
|
|
4441
|
+
const authority = await resolveTeamAuthority(options, ["tilesets:write"], dependencies);
|
|
4442
|
+
if (!authority) return;
|
|
4443
|
+
const registration = await requestHostedJson(authority.apiOrigin, "/v1/tilesets", {
|
|
4444
|
+
body: JSON.stringify({
|
|
4445
|
+
...options.attribution ? { attribution: options.attribution } : {},
|
|
4446
|
+
format: "pmtiles",
|
|
4447
|
+
name: options.name?.trim() || options.id,
|
|
4448
|
+
slug: options.id
|
|
4449
|
+
}),
|
|
4450
|
+
headers: {
|
|
4451
|
+
Authorization: `Bearer ${authority.credential}`,
|
|
4452
|
+
"Content-Type": "application/json"
|
|
4453
|
+
},
|
|
4454
|
+
method: "POST"
|
|
4455
|
+
});
|
|
4456
|
+
const registered = asRecord4(registration.body);
|
|
4457
|
+
const resourceId = String(registered.tilesetId ?? "");
|
|
4458
|
+
if (!registration.ok || !/^tls_[A-Za-z0-9_-]{16,76}$/u.test(resourceId)) {
|
|
4459
|
+
return emitFailure(
|
|
4460
|
+
options.json,
|
|
4461
|
+
`http_${registration.status}`,
|
|
4462
|
+
`Tileset registration failed (${registration.status}).`
|
|
4463
|
+
);
|
|
4464
|
+
}
|
|
4465
|
+
let reportedParts = 0;
|
|
4466
|
+
let reportedValidation = false;
|
|
4467
|
+
const publication = await publishPreparedTeamTilesetUpload({
|
|
4468
|
+
file: uploadFile,
|
|
4469
|
+
onProgress: options.json ? void 0 : ({ completedParts, phase, totalParts }) => {
|
|
4470
|
+
if (phase === "validating") {
|
|
4471
|
+
if (!reportedValidation) {
|
|
4472
|
+
process.stderr.write("Validating uploaded bytes.\n");
|
|
4473
|
+
reportedValidation = true;
|
|
4474
|
+
}
|
|
4475
|
+
return;
|
|
4476
|
+
}
|
|
4477
|
+
if (completedParts === totalParts || completedParts - reportedParts >= 16) {
|
|
4478
|
+
process.stderr.write(`Uploaded ${completedParts}/${totalParts} parts.
|
|
4479
|
+
`);
|
|
4480
|
+
reportedParts = completedParts;
|
|
4481
|
+
}
|
|
4482
|
+
},
|
|
4483
|
+
tilesetId: resourceId,
|
|
4484
|
+
transport: createHostedTeamTilesetUploadTransport({
|
|
4485
|
+
apiOrigin: authority.apiOrigin,
|
|
4486
|
+
credential: authority.credential,
|
|
4487
|
+
tilesetId: resourceId
|
|
4488
|
+
}),
|
|
4489
|
+
upload: prepared
|
|
4490
|
+
});
|
|
4491
|
+
const document = {
|
|
4492
|
+
command: "tileset publish",
|
|
4493
|
+
publication: publication.changed ? "changed" : "unchanged",
|
|
4494
|
+
schemaVersion: 2,
|
|
4495
|
+
team: authority.team ? { id: authority.team.id, slug: authority.team.slug } : null,
|
|
4496
|
+
tileset: {
|
|
4497
|
+
byteCount: publication.byteCount,
|
|
4498
|
+
contentHash: publication.contentHash,
|
|
4499
|
+
contentHashAlgorithm: publication.contentHashAlgorithm,
|
|
4500
|
+
logicalId: options.id,
|
|
4501
|
+
state: publication.state,
|
|
4502
|
+
tilesetId: resourceId,
|
|
4503
|
+
version: { id: publication.versionId, number: publication.version }
|
|
4504
|
+
}
|
|
4505
|
+
};
|
|
4506
|
+
if (options.json) return emitJson2(document);
|
|
4507
|
+
console.log(
|
|
4508
|
+
`${pc2.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc2.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
|
|
4509
|
+
);
|
|
4510
|
+
} catch (error) {
|
|
4511
|
+
if (error instanceof TeamTilesetUploadError) {
|
|
4512
|
+
emitFailure(options.json, error.code, error.message, error.details);
|
|
4513
|
+
} else {
|
|
4514
|
+
emitFailure(options.json, "tileset_publish_failed", "Tileset publication failed.");
|
|
4515
|
+
}
|
|
4516
|
+
} finally {
|
|
4517
|
+
await uploadFile?.close().catch(() => void 0);
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
);
|
|
4521
|
+
tileset.command("list").description("List tilesets owned by one Team").option("--team <team>", "target Team as @team").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Team data API key", process.env.TILEFLOW_API_KEY).option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
|
|
4522
|
+
try {
|
|
4523
|
+
const authority = await resolveTeamAuthority(options, ["tilesets:read"], dependencies);
|
|
4524
|
+
if (!authority) return;
|
|
4525
|
+
const items = await fetchAllTeamTilesets(authority);
|
|
4526
|
+
const document = { command: "tileset list", schemaVersion: 1, tilesets: items };
|
|
4527
|
+
if (options.json) return emitJson2(document);
|
|
4528
|
+
if (items.length === 0) return console.log(pc2.gray("No Team tilesets."));
|
|
4529
|
+
for (const item of items) {
|
|
4530
|
+
console.log(` ${pc2.bold(item.logicalName)} ${pc2.gray(item.status)}`);
|
|
4531
|
+
}
|
|
4532
|
+
} catch {
|
|
4533
|
+
emitFailure(options.json, "tileset_list_failed", "Tileset list failed.");
|
|
4534
|
+
}
|
|
4535
|
+
});
|
|
4536
|
+
tileset.command("status").description("Show one Team tileset").argument("<id>", "Team-local logical ID or opaque resource ID").option("--team <team>", "target Team as @team").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Team data API key", process.env.TILEFLOW_API_KEY).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
|
|
4537
|
+
try {
|
|
4538
|
+
if (!isTilesetId(id))
|
|
4539
|
+
return emitFailure(options.json, "invalid_tileset_id", "Tileset ID is invalid.");
|
|
4540
|
+
const authority = await resolveTeamAuthority(options, ["tilesets:read"], dependencies);
|
|
4541
|
+
if (!authority) return;
|
|
4542
|
+
const response = await authorizedTeamRequest(
|
|
4543
|
+
authority,
|
|
4544
|
+
`/v1/tilesets/${encodeURIComponent(id)}`,
|
|
4545
|
+
"GET"
|
|
4546
|
+
);
|
|
4547
|
+
if (!response.ok) {
|
|
4548
|
+
return emitFailure(
|
|
4549
|
+
options.json,
|
|
4550
|
+
`http_${response.status}`,
|
|
4551
|
+
`Tileset status failed (${response.status}).`
|
|
4552
|
+
);
|
|
4553
|
+
}
|
|
4554
|
+
const parsed = tilesetDetailResponseSchema.parse(response.body);
|
|
4555
|
+
const document = { command: "tileset status", schemaVersion: 1, tileset: parsed.tileset };
|
|
4556
|
+
if (options.json) return emitJson2(document);
|
|
4557
|
+
const item = parsed.tileset;
|
|
4558
|
+
console.log(`${pc2.bold(item.name)} ${pc2.gray(item.state)}`);
|
|
4559
|
+
console.log(` ID: ${item.id}`);
|
|
4560
|
+
console.log(` Current version: ${String(item.currentVersion ?? "unpublished")}`);
|
|
4561
|
+
} catch {
|
|
4562
|
+
emitFailure(options.json, "tileset_status_failed", "Tileset status failed.");
|
|
4563
|
+
}
|
|
4564
|
+
});
|
|
4565
|
+
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").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Team data API key", process.env.TILEFLOW_API_KEY).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
|
|
4566
|
+
try {
|
|
4567
|
+
if (!isTilesetId(id) || options.confirm !== id) {
|
|
4568
|
+
return emitFailure(
|
|
4569
|
+
options.json,
|
|
4570
|
+
"purge_confirmation_mismatch",
|
|
4571
|
+
"Purge confirmation must match the tileset ID."
|
|
4572
|
+
);
|
|
4573
|
+
}
|
|
4574
|
+
const authority = await resolveTeamAuthority(options, ["tilesets:write"], dependencies);
|
|
4575
|
+
if (!authority) return;
|
|
4576
|
+
const response = await requestHostedJson(
|
|
4577
|
+
authority.apiOrigin,
|
|
4578
|
+
`/v1/tilesets/${encodeURIComponent(id)}`,
|
|
4579
|
+
{
|
|
4580
|
+
headers: {
|
|
4581
|
+
Authorization: `Bearer ${authority.credential}`,
|
|
4582
|
+
"X-Tileflow-Confirm-Tileset-Purge": id
|
|
4583
|
+
},
|
|
4584
|
+
method: "DELETE"
|
|
4585
|
+
}
|
|
4586
|
+
);
|
|
4587
|
+
if (!response.ok) {
|
|
4588
|
+
const inUse = tilesetInUseResponseSchema.safeParse(response.body);
|
|
4589
|
+
if (inUse.success) {
|
|
4590
|
+
return emitFailure(
|
|
4591
|
+
options.json,
|
|
4592
|
+
inUse.data.code,
|
|
4593
|
+
"Tileset is used by retained Map deployments.",
|
|
4594
|
+
{
|
|
4595
|
+
dependencies: inUse.data.dependencies,
|
|
4596
|
+
nextCursor: inUse.data.nextCursor
|
|
4597
|
+
}
|
|
4598
|
+
);
|
|
4599
|
+
}
|
|
4600
|
+
return emitFailure(
|
|
4601
|
+
options.json,
|
|
4602
|
+
`http_${response.status}`,
|
|
4603
|
+
`Tileset purge failed (${response.status}).`
|
|
4604
|
+
);
|
|
4605
|
+
}
|
|
4606
|
+
const parsed = tilesetPurgeResponseSchema.parse(response.body);
|
|
4607
|
+
const document = { command: "tileset purge", schemaVersion: 1, ...parsed };
|
|
4608
|
+
if (options.json) return emitJson2(document);
|
|
4609
|
+
console.log(`${pc2.green("\u2713")} Purge ${parsed.reclamation}.`);
|
|
4610
|
+
} catch {
|
|
4611
|
+
emitFailure(options.json, "tileset_purge_failed", "Tileset purge failed.");
|
|
4612
|
+
}
|
|
4613
|
+
});
|
|
3058
4614
|
}
|
|
3059
|
-
async function
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
4615
|
+
async function resolveTeamAuthority(options, scopes, dependencies) {
|
|
4616
|
+
let apiOrigin;
|
|
4617
|
+
try {
|
|
4618
|
+
apiOrigin = normalizeApiOrigin(options.apiUrl ?? dependencies.defaultApiUrl);
|
|
4619
|
+
} catch (error) {
|
|
4620
|
+
emitFailure(options.json, "invalid_api_url", safeMessage2(error));
|
|
4621
|
+
return null;
|
|
4622
|
+
}
|
|
4623
|
+
if (options.apiKey) {
|
|
4624
|
+
if (!/^tf_live_[0-9a-f]{48}$/u.test(options.apiKey)) {
|
|
4625
|
+
emitFailure(options.json, "invalid_team_data_key", "Team data API key is invalid.");
|
|
4626
|
+
return null;
|
|
4627
|
+
}
|
|
4628
|
+
if (options.team) {
|
|
4629
|
+
emitFailure(
|
|
4630
|
+
options.json,
|
|
4631
|
+
"team_selector_with_api_key",
|
|
4632
|
+
"A Team data key already selects its Team; omit --team."
|
|
4633
|
+
);
|
|
4634
|
+
return null;
|
|
4635
|
+
}
|
|
4636
|
+
return { apiOrigin, credential: options.apiKey, team: null };
|
|
3067
4637
|
}
|
|
3068
|
-
if (result.items.length === 1) return { kind: "selected", target: result.items[0] };
|
|
3069
|
-
return result.items.length ? { kind: "ambiguous", targets: result.items } : { kind: "missing", targets: [] };
|
|
3070
|
-
}
|
|
3071
|
-
async function requireCommandSession(options, dependencies) {
|
|
3072
4638
|
let config;
|
|
3073
4639
|
try {
|
|
3074
4640
|
config = await dependencies.loadAuthConfig();
|
|
3075
4641
|
} catch (error) {
|
|
3076
|
-
|
|
4642
|
+
emitFailure(options.json, "auth_state_unavailable", safeMessage2(error));
|
|
3077
4643
|
return null;
|
|
3078
4644
|
}
|
|
3079
|
-
const
|
|
3080
|
-
if (
|
|
3081
|
-
|
|
4645
|
+
const selected = resolveAccountSession(config, options.apiUrl ?? dependencies.defaultApiUrl);
|
|
4646
|
+
if (selected.kind !== "selected") {
|
|
4647
|
+
emitFailure(
|
|
3082
4648
|
options.json,
|
|
3083
|
-
`account_session_${
|
|
3084
|
-
|
|
4649
|
+
`account_session_${selected.kind}`,
|
|
4650
|
+
selected.kind === "expired" ? "Account session expired. Run tileflow login." : "Run tileflow login."
|
|
3085
4651
|
);
|
|
3086
4652
|
return null;
|
|
3087
4653
|
}
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
try {
|
|
3093
|
-
response = await requestHostedJson(session.apiOrigin, path, {
|
|
3094
|
-
...init,
|
|
3095
|
-
headers: { ...init.headers, Authorization: `Bearer ${session.accountSession}` }
|
|
3096
|
-
});
|
|
3097
|
-
} catch (error) {
|
|
3098
|
-
return {
|
|
3099
|
-
body: null,
|
|
3100
|
-
error: error instanceof Error && /safe size limit|timed out/u.test(error.message) ? error.message : "Hosted request failed.",
|
|
3101
|
-
ok: false,
|
|
3102
|
-
status: 502
|
|
3103
|
-
};
|
|
4654
|
+
const discovered = await listAccountTeams(selected.session);
|
|
4655
|
+
if (!discovered.ok) {
|
|
4656
|
+
emitFailure(options.json, "team_discovery_failed", discovered.error);
|
|
4657
|
+
return null;
|
|
3104
4658
|
}
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
ok: false,
|
|
3110
|
-
status: 502
|
|
3111
|
-
};
|
|
4659
|
+
const requested = options.team ? /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u.exec(options.team) : null;
|
|
4660
|
+
if (options.team && !requested) {
|
|
4661
|
+
emitFailure(options.json, "invalid_team", "Team must use @team syntax.");
|
|
4662
|
+
return null;
|
|
3112
4663
|
}
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
}
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
return {
|
|
3124
|
-
error: "Destination discovery returned unstable ordering.",
|
|
3125
|
-
ok: false,
|
|
3126
|
-
status: 502
|
|
3127
|
-
};
|
|
4664
|
+
const team = requested ? discovered.teams.find(({ slug }) => slug === requested[1]) : discovered.teams.length === 1 ? discovered.teams[0] : null;
|
|
4665
|
+
if (!team) {
|
|
4666
|
+
const options_ = discovered.teams.map(({ slug }) => `@${slug}`);
|
|
4667
|
+
emitFailure(
|
|
4668
|
+
options.json,
|
|
4669
|
+
options_.length > 1 ? "team_ambiguous" : "team_not_found",
|
|
4670
|
+
options_.length > 1 ? `Choose one Team with --team: ${options_.join(", ")}.` : "No matching Team is available.",
|
|
4671
|
+
{ options: options_ }
|
|
4672
|
+
);
|
|
4673
|
+
return null;
|
|
3128
4674
|
}
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
4675
|
+
const capability = await requestTeamCapability(selected.session, `@${team.slug}`, scopes);
|
|
4676
|
+
if (!capability.ok) {
|
|
4677
|
+
emitFailure(options.json, "team_capability_failed", capability.error);
|
|
4678
|
+
return null;
|
|
4679
|
+
}
|
|
4680
|
+
return {
|
|
4681
|
+
apiOrigin: selected.session.apiOrigin,
|
|
4682
|
+
credential: capability.capability,
|
|
4683
|
+
team
|
|
4684
|
+
};
|
|
3133
4685
|
}
|
|
3134
|
-
function
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
4686
|
+
function authorizedTeamRequest(authority, path, method) {
|
|
4687
|
+
return requestHostedJson(authority.apiOrigin, path, {
|
|
4688
|
+
headers: { Authorization: `Bearer ${authority.credential}` },
|
|
4689
|
+
method
|
|
4690
|
+
});
|
|
3138
4691
|
}
|
|
3139
|
-
function
|
|
3140
|
-
const
|
|
3141
|
-
const
|
|
3142
|
-
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
4692
|
+
async function fetchAllTeamTilesets(authority) {
|
|
4693
|
+
const items = [];
|
|
4694
|
+
const seenCursors = /* @__PURE__ */ new Set();
|
|
4695
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
4696
|
+
let cursor = null;
|
|
4697
|
+
for (let page = 0; page < 100; page += 1) {
|
|
4698
|
+
const query = new URLSearchParams({ limit: "100" });
|
|
4699
|
+
if (cursor) query.set("cursor", cursor);
|
|
4700
|
+
const response = await authorizedTeamRequest(
|
|
4701
|
+
authority,
|
|
4702
|
+
`/v1/tilesets?${query.toString()}`,
|
|
4703
|
+
"GET"
|
|
4704
|
+
);
|
|
4705
|
+
if (!response.ok) {
|
|
4706
|
+
throw new TeamTilesetUploadError(
|
|
4707
|
+
`http_${response.status}`,
|
|
4708
|
+
`Tileset list failed (${response.status}).`
|
|
4709
|
+
);
|
|
4710
|
+
}
|
|
4711
|
+
const parsed = tilesetListResponseSchema.parse(response.body);
|
|
4712
|
+
for (const item of parsed.tilesets) {
|
|
4713
|
+
if (seenIds.has(item.id)) throw new Error("Tileset list returned a duplicate resource.");
|
|
4714
|
+
const previous = items.at(-1);
|
|
4715
|
+
if (previous && (previous.logicalName > item.logicalName || previous.logicalName === item.logicalName && previous.id >= item.id)) {
|
|
4716
|
+
throw new Error("Tileset list returned unstable ordering.");
|
|
4717
|
+
}
|
|
4718
|
+
seenIds.add(item.id);
|
|
4719
|
+
items.push(item);
|
|
4720
|
+
}
|
|
4721
|
+
cursor = parsed.nextCursor ?? null;
|
|
4722
|
+
if (!cursor) return Object.freeze(items);
|
|
4723
|
+
if (seenCursors.has(cursor)) throw new Error("Tileset list repeated a cursor.");
|
|
4724
|
+
seenCursors.add(cursor);
|
|
4725
|
+
}
|
|
4726
|
+
throw new Error("Tileset list exceeded its safe page limit.");
|
|
3148
4727
|
}
|
|
3149
|
-
function
|
|
3150
|
-
|
|
4728
|
+
function emitJson2(value) {
|
|
4729
|
+
process.stdout.write(`${serializeCanonicalJson3(value)}
|
|
4730
|
+
`);
|
|
3151
4731
|
}
|
|
3152
|
-
function
|
|
4732
|
+
function emitFailure(json, code, message, context = {}) {
|
|
3153
4733
|
if (json) {
|
|
3154
|
-
|
|
3155
|
-
|
|
4734
|
+
process.stderr.write(
|
|
4735
|
+
`${serializeCanonicalJson3({ error: { code, ...context, message }, ok: false, schemaVersion: 1 })}
|
|
4736
|
+
`
|
|
4737
|
+
);
|
|
3156
4738
|
} else {
|
|
3157
|
-
console.error(`${
|
|
4739
|
+
console.error(`${pc2.red("Error:")} ${message}`);
|
|
3158
4740
|
}
|
|
3159
4741
|
process.exitCode = 1;
|
|
3160
4742
|
}
|
|
3161
|
-
function
|
|
3162
|
-
|
|
3163
|
-
`);
|
|
3164
|
-
}
|
|
3165
|
-
function asRecord2(value) {
|
|
3166
|
-
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3167
|
-
}
|
|
3168
|
-
function isAccountProjectTarget(value) {
|
|
3169
|
-
const target = asRecord2(value);
|
|
3170
|
-
return isIdentity(target.organization) && isProjectItem(target.project) && typeof target.reference === "string" && target.reference === `@${target.organization.slug}/${target.project.slug}`;
|
|
3171
|
-
}
|
|
3172
|
-
function isIdentity(value) {
|
|
3173
|
-
const identity = asRecord2(value);
|
|
3174
|
-
return typeof identity.id === "string" && typeof identity.name === "string" && typeof identity.slug === "string";
|
|
4743
|
+
function isTilesetId(value) {
|
|
4744
|
+
return /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(value) && !["terrain", "world"].includes(value);
|
|
3175
4745
|
}
|
|
3176
|
-
function
|
|
3177
|
-
|
|
3178
|
-
|
|
4746
|
+
function parseIncludedValueFields(value) {
|
|
4747
|
+
if (value === void 0) return void 0;
|
|
4748
|
+
const fields = value.split(",").map((field) => field.trim());
|
|
4749
|
+
if (fields.some((field) => !field)) {
|
|
4750
|
+
throw inspectionOptionError("--include-values requires comma-separated field names.");
|
|
4751
|
+
}
|
|
4752
|
+
return fields;
|
|
3179
4753
|
}
|
|
3180
|
-
function
|
|
3181
|
-
return
|
|
4754
|
+
function inspectionOptionError(message) {
|
|
4755
|
+
return Object.assign(new Error(message), {
|
|
4756
|
+
code: "TF_TILESET_INSPECTION_OPTIONS_INVALID",
|
|
4757
|
+
path: "includeValues"
|
|
4758
|
+
});
|
|
3182
4759
|
}
|
|
3183
|
-
function
|
|
3184
|
-
|
|
3185
|
-
return normalized.length > 0 && normalized.length <= 200 && !/[\p{Cc}]/u.test(normalized);
|
|
4760
|
+
function safeMessage2(error) {
|
|
4761
|
+
return error instanceof Error ? error.message : "Tileset command failed.";
|
|
3186
4762
|
}
|
|
3187
|
-
function
|
|
3188
|
-
return
|
|
4763
|
+
function asRecord4(value) {
|
|
4764
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
3189
4765
|
}
|
|
4766
|
+
var safeIdentifierSchema2 = z6.string().min(1).max(160).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u);
|
|
4767
|
+
var physicalTilesetIdSchema = z6.string().min(20).max(80).regex(/^tls_[A-Za-z0-9_-]+$/u);
|
|
4768
|
+
var cursorSchema = z6.string().min(1).max(2048).nullable();
|
|
4769
|
+
var tilesetInventoryItemSchema = z6.object({
|
|
4770
|
+
currentVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
|
|
4771
|
+
hasCurrent: z6.boolean(),
|
|
4772
|
+
id: physicalTilesetIdSchema,
|
|
4773
|
+
logicalName: z6.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
4774
|
+
name: z6.string().min(1).max(200),
|
|
4775
|
+
retainedBytes: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
4776
|
+
retainedDeploymentCount: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
4777
|
+
status: z6.enum(["active", "purging", "retired"]),
|
|
4778
|
+
updatedAt: z6.iso.datetime({ offset: true }),
|
|
4779
|
+
usedByMapCount: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER)
|
|
4780
|
+
}).strict();
|
|
4781
|
+
var tilesetListResponseSchema = z6.object({
|
|
4782
|
+
nextCursor: cursorSchema.optional(),
|
|
4783
|
+
schemaVersion: z6.literal(1),
|
|
4784
|
+
tilesets: z6.array(tilesetInventoryItemSchema).max(100)
|
|
4785
|
+
}).strict();
|
|
4786
|
+
var tilesetDependencySchema = z6.object({
|
|
4787
|
+
active: z6.boolean(),
|
|
4788
|
+
deploymentId: safeIdentifierSchema2,
|
|
4789
|
+
deploymentVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER),
|
|
4790
|
+
environment: z6.string().min(1).max(64),
|
|
4791
|
+
mapId: safeIdentifierSchema2,
|
|
4792
|
+
sourceId: safeIdentifierSchema2
|
|
4793
|
+
}).strict();
|
|
4794
|
+
var tilesetDetailResponseSchema = z6.object({
|
|
4795
|
+
schemaVersion: z6.literal(1),
|
|
4796
|
+
tileset: z6.object({
|
|
4797
|
+
attribution: z6.string().max(4096).nullable(),
|
|
4798
|
+
currentVersion: z6.number().int().positive().max(Number.MAX_SAFE_INTEGER).nullable(),
|
|
4799
|
+
dependencies: z6.array(tilesetDependencySchema).max(25),
|
|
4800
|
+
dependencyNextCursor: cursorSchema,
|
|
4801
|
+
id: physicalTilesetIdSchema,
|
|
4802
|
+
name: z6.string().min(1).max(200),
|
|
4803
|
+
slug: z6.string().min(1).max(128).regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/u),
|
|
4804
|
+
state: z6.enum(["active", "purging", "purged"])
|
|
4805
|
+
}).strict()
|
|
4806
|
+
}).strict();
|
|
4807
|
+
var purgeDependencySchema = z6.object({
|
|
4808
|
+
deploymentId: safeIdentifierSchema2,
|
|
4809
|
+
mapId: safeIdentifierSchema2,
|
|
4810
|
+
sourceId: safeIdentifierSchema2
|
|
4811
|
+
}).strict();
|
|
4812
|
+
var tilesetInUseResponseSchema = z6.object({
|
|
4813
|
+
code: z6.literal("TF_TILESET_IN_USE"),
|
|
4814
|
+
dependencies: z6.array(purgeDependencySchema).max(32),
|
|
4815
|
+
error: z6.literal("Tileset is used by retained Map deployments"),
|
|
4816
|
+
nextCursor: cursorSchema
|
|
4817
|
+
}).strict();
|
|
4818
|
+
var tilesetPurgeResponseSchema = z6.object({
|
|
4819
|
+
deliveryFence: z6.enum(["complete", "pending"]),
|
|
4820
|
+
pendingVersions: z6.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
|
|
4821
|
+
reclamation: z6.enum(["complete", "pending"]),
|
|
4822
|
+
state: z6.enum(["purged", "purging"]),
|
|
4823
|
+
tilesetId: physicalTilesetIdSchema
|
|
4824
|
+
}).strict();
|
|
3190
4825
|
|
|
3191
4826
|
// src/visual-command.ts
|
|
3192
|
-
import { lstat as lstat3, readFile as
|
|
3193
|
-
import { dirname as
|
|
4827
|
+
import { lstat as lstat3, readFile as readFile6 } from "fs/promises";
|
|
4828
|
+
import { dirname as dirname9, isAbsolute as isAbsolute3, parse as parse2, relative as relative4, resolve as resolve6, sep as sep4 } from "path";
|
|
3194
4829
|
import {
|
|
3195
4830
|
analyzeTileflowCaptureReference,
|
|
3196
4831
|
compareTileflowCaptureToBaseline,
|
|
@@ -3204,12 +4839,12 @@ import {
|
|
|
3204
4839
|
tileflowVisualArtifactLimits,
|
|
3205
4840
|
validateTileflowVisualReferencePng
|
|
3206
4841
|
} from "@tileflow/capture";
|
|
3207
|
-
import { compareCodeUnits as
|
|
4842
|
+
import { compareCodeUnits as compareCodeUnits8 } from "@tileflow/core";
|
|
3208
4843
|
|
|
3209
4844
|
// src/visual-compare-command.ts
|
|
3210
|
-
import { createHash as
|
|
3211
|
-
import { lstat as lstat2, readFile as
|
|
3212
|
-
import { basename as basename3, dirname as
|
|
4845
|
+
import { createHash as createHash3 } from "crypto";
|
|
4846
|
+
import { lstat as lstat2, readFile as readFile5, realpath } from "fs/promises";
|
|
4847
|
+
import { basename as basename3, dirname as dirname8, extname, isAbsolute as isAbsolute2, parse, relative as relative3, resolve as resolve5, sep as sep3 } from "path";
|
|
3213
4848
|
import {
|
|
3214
4849
|
compareTileflowCapturesForReview,
|
|
3215
4850
|
createTileflowCaptureSession as createTileflowCaptureSession2,
|
|
@@ -3218,9 +4853,9 @@ import {
|
|
|
3218
4853
|
tileflowSyntheticAssetOrigin as tileflowSyntheticAssetOrigin2
|
|
3219
4854
|
} from "@tileflow/capture";
|
|
3220
4855
|
import {
|
|
3221
|
-
compareCodeUnits as
|
|
4856
|
+
compareCodeUnits as compareCodeUnits7,
|
|
3222
4857
|
normalizeTileflowCaptureScene as normalizeTileflowCaptureScene2,
|
|
3223
|
-
|
|
4858
|
+
parseResolvedTileflowMap as parseResolvedTileflowMap2,
|
|
3224
4859
|
resolveThemeSelection,
|
|
3225
4860
|
tileflowCaptureSceneLimits
|
|
3226
4861
|
} from "@tileflow/core";
|
|
@@ -3231,7 +4866,7 @@ import {
|
|
|
3231
4866
|
} from "@tileflow/dev/artifacts";
|
|
3232
4867
|
|
|
3233
4868
|
// src/visual-compare-report.ts
|
|
3234
|
-
import { createHash } from "crypto";
|
|
4869
|
+
import { createHash as createHash2 } from "crypto";
|
|
3235
4870
|
var tileflowVisualCompareReportSchemaVersion = 1;
|
|
3236
4871
|
var tileflowVisualCompareReportLimits = Object.freeze({
|
|
3237
4872
|
maximumEmbeddedPngBytes: 64 * 1024 * 1024,
|
|
@@ -3286,7 +4921,7 @@ function renderTileflowVisualCompareReport(input) {
|
|
|
3286
4921
|
};
|
|
3287
4922
|
const rowHtml = rows.map((row, index) => renderRow(row, index)).join("\n");
|
|
3288
4923
|
const script = reportScript();
|
|
3289
|
-
const scriptHash =
|
|
4924
|
+
const scriptHash = createHash2("sha256").update(script).digest("base64");
|
|
3290
4925
|
return `<!doctype html>
|
|
3291
4926
|
<html lang="en">
|
|
3292
4927
|
<head>
|
|
@@ -3650,7 +5285,7 @@ async function prepareVisualCompareOutput(plan, leftArtifacts, rightArtifacts, c
|
|
|
3650
5285
|
documentPath: relativePath(cwd, plan.output.documentPath),
|
|
3651
5286
|
reportPath: relativePath(cwd, plan.output.reportPath)
|
|
3652
5287
|
},
|
|
3653
|
-
warnings: [...warnings].sort(
|
|
5288
|
+
warnings: [...warnings].sort(compareCodeUnits7)
|
|
3654
5289
|
};
|
|
3655
5290
|
const report = renderTileflowVisualCompareReport({
|
|
3656
5291
|
generatedBy: "Tileflow CLI",
|
|
@@ -3938,7 +5573,7 @@ async function runVisualCompareWatch(plan, openReport) {
|
|
|
3938
5573
|
}
|
|
3939
5574
|
}
|
|
3940
5575
|
function resolveVisualCompareSide(artifacts, config, requestedMap, requestedTheme, role) {
|
|
3941
|
-
const availableMaps = Object.keys(artifacts.project.maps).sort(
|
|
5576
|
+
const availableMaps = Object.keys(artifacts.project.maps).sort(compareCodeUnits7);
|
|
3942
5577
|
const map = requestedMap ?? (availableMaps.length === 1 ? availableMaps[0] : void 0);
|
|
3943
5578
|
if (!map) {
|
|
3944
5579
|
throw new Error(
|
|
@@ -3951,7 +5586,7 @@ function resolveVisualCompareSide(artifacts, config, requestedMap, requestedThem
|
|
|
3951
5586
|
`Unknown ${role} map "${map}". Available maps: ${availableMaps.join(", ") || "(none)"}.`
|
|
3952
5587
|
);
|
|
3953
5588
|
}
|
|
3954
|
-
const resolvedMap =
|
|
5589
|
+
const resolvedMap = parseResolvedTileflowMap2(candidate);
|
|
3955
5590
|
const theme = resolveThemeSelection(resolvedMap, requestedTheme).name;
|
|
3956
5591
|
if (!artifacts.styles[map]?.[theme]) {
|
|
3957
5592
|
throw new Error(`The ${role} map theme "${map}/${theme}" was not compiled.`);
|
|
@@ -4007,7 +5642,7 @@ async function resolveVisualCompareOutput(cwd, value, managed, force, watch, zoo
|
|
|
4007
5642
|
if (extname(reportPath).toLowerCase() !== ".html") {
|
|
4008
5643
|
throw new Error("--report must name an .html file.");
|
|
4009
5644
|
}
|
|
4010
|
-
const reportDirectory =
|
|
5645
|
+
const reportDirectory = dirname8(reportPath);
|
|
4011
5646
|
if (reportDirectory === cwd || reportDirectory === parse(reportPath).root) {
|
|
4012
5647
|
throw new Error("Visual compare output must use a dedicated subdirectory.");
|
|
4013
5648
|
}
|
|
@@ -4147,7 +5782,7 @@ function createVisualCompareInventory(assetDirectory, assets) {
|
|
|
4147
5782
|
throw new Error("Visual compare produced an invalid managed asset path.");
|
|
4148
5783
|
}
|
|
4149
5784
|
return { path: relativeAssetPath, sha256: sha256Source(source) };
|
|
4150
|
-
}).sort((left, right) =>
|
|
5785
|
+
}).sort((left, right) => compareCodeUnits7(left.path, right.path))
|
|
4151
5786
|
};
|
|
4152
5787
|
}
|
|
4153
5788
|
function serializeVisualCompareInventory(inventory) {
|
|
@@ -4155,7 +5790,7 @@ function serializeVisualCompareInventory(inventory) {
|
|
|
4155
5790
|
`;
|
|
4156
5791
|
}
|
|
4157
5792
|
async function readVisualCompareInventory(assetDirectory, inventoryPath, boundary) {
|
|
4158
|
-
await assertNoSymlinkComponents(
|
|
5793
|
+
await assertNoSymlinkComponents(dirname8(inventoryPath), boundary);
|
|
4159
5794
|
let info;
|
|
4160
5795
|
try {
|
|
4161
5796
|
info = await lstat2(inventoryPath);
|
|
@@ -4171,7 +5806,7 @@ async function readVisualCompareInventory(assetDirectory, inventoryPath, boundar
|
|
|
4171
5806
|
}
|
|
4172
5807
|
let parsed;
|
|
4173
5808
|
try {
|
|
4174
|
-
parsed = JSON.parse(await
|
|
5809
|
+
parsed = JSON.parse(await readFile5(inventoryPath, "utf8"));
|
|
4175
5810
|
} catch (error) {
|
|
4176
5811
|
throw new Error("Visual compare inventory is not valid bounded JSON.", { cause: error });
|
|
4177
5812
|
}
|
|
@@ -4179,7 +5814,7 @@ async function readVisualCompareInventory(assetDirectory, inventoryPath, boundar
|
|
|
4179
5814
|
const assets = [];
|
|
4180
5815
|
for (const asset of inventory.assets) {
|
|
4181
5816
|
const path = resolve5(assetDirectory, asset.path);
|
|
4182
|
-
await assertNoSymlinkComponents(
|
|
5817
|
+
await assertNoSymlinkComponents(dirname8(path), boundary);
|
|
4183
5818
|
let assetInfo;
|
|
4184
5819
|
try {
|
|
4185
5820
|
assetInfo = await lstat2(path);
|
|
@@ -4193,7 +5828,7 @@ async function readVisualCompareInventory(assetDirectory, inventoryPath, boundar
|
|
|
4193
5828
|
if (assetInfo.size > tileflowVisualCompareReportLimits.maximumEmbeddedPngBytes) {
|
|
4194
5829
|
throw new Error("A managed visual compare asset exceeds its bounded byte limit.");
|
|
4195
5830
|
}
|
|
4196
|
-
const source = await
|
|
5831
|
+
const source = await readFile5(path);
|
|
4197
5832
|
if (sha256Source(source) !== asset.sha256) {
|
|
4198
5833
|
throw new Error(
|
|
4199
5834
|
"A previously managed visual compare asset was modified; move or remove it explicitly."
|
|
@@ -4222,7 +5857,7 @@ function validateVisualCompareInventory(input) {
|
|
|
4222
5857
|
return { path: asset.path, sha256: asset.sha256 };
|
|
4223
5858
|
});
|
|
4224
5859
|
const paths = assets.map((asset) => asset.path);
|
|
4225
|
-
const sortedPaths = [...paths].sort(
|
|
5860
|
+
const sortedPaths = [...paths].sort(compareCodeUnits7);
|
|
4226
5861
|
if (new Set(paths).size !== paths.length || paths.some((path, index) => path !== sortedPaths[index])) {
|
|
4227
5862
|
throw new Error("Visual compare inventory asset paths must be unique and sorted.");
|
|
4228
5863
|
}
|
|
@@ -4242,12 +5877,12 @@ function isVisualCompareOwnedAssetName(value) {
|
|
|
4242
5877
|
return Number.isInteger(row) && row >= 1 && row <= tileflowVisualCompareLimits.maximumRows;
|
|
4243
5878
|
}
|
|
4244
5879
|
function hasExactKeys(record, keys) {
|
|
4245
|
-
const actual = Object.keys(record).sort(
|
|
4246
|
-
const expected = [...keys].sort(
|
|
5880
|
+
const actual = Object.keys(record).sort(compareCodeUnits7);
|
|
5881
|
+
const expected = [...keys].sort(compareCodeUnits7);
|
|
4247
5882
|
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
|
|
4248
5883
|
}
|
|
4249
5884
|
function sha256Source(source) {
|
|
4250
|
-
return
|
|
5885
|
+
return createHash3("sha256").update(typeof source === "string" ? Buffer.from(source, "utf8") : Buffer.from(source)).digest("hex");
|
|
4251
5886
|
}
|
|
4252
5887
|
async function canonicalPathIdentity(path) {
|
|
4253
5888
|
const absolute = resolve5(path);
|
|
@@ -4260,7 +5895,7 @@ async function canonicalPathIdentity(path) {
|
|
|
4260
5895
|
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
4261
5896
|
} catch (error) {
|
|
4262
5897
|
if (!isMissingPathComponent(error)) throw error;
|
|
4263
|
-
const parent =
|
|
5898
|
+
const parent = dirname8(current);
|
|
4264
5899
|
if (parent === current) {
|
|
4265
5900
|
const canonical = absolute.normalize("NFC");
|
|
4266
5901
|
return process.platform === "win32" ? canonical.toLowerCase() : canonical;
|
|
@@ -4417,7 +6052,7 @@ function createTileflowVisualDiffJson(comparisons, cwd) {
|
|
|
4417
6052
|
return {
|
|
4418
6053
|
schemaVersion: 1,
|
|
4419
6054
|
command: "visual.diff",
|
|
4420
|
-
comparisons: [...comparisons].sort((left, right) =>
|
|
6055
|
+
comparisons: [...comparisons].sort((left, right) => compareCodeUnits8(left.comparison.scene, right.comparison.scene)).map(({ comparison, baselinePath, actualPath, diffPath, reportPath }) => ({
|
|
4421
6056
|
...createTileflowVisualComparisonDocument(comparison),
|
|
4422
6057
|
baselinePath: relativePath(cwd, baselinePath),
|
|
4423
6058
|
actualPath: relativePath(cwd, actualPath),
|
|
@@ -4632,7 +6267,7 @@ async function runVisualUpdate(scenes, options) {
|
|
|
4632
6267
|
renderer: capture.renderer,
|
|
4633
6268
|
networkDependent: capture.networkDependent
|
|
4634
6269
|
},
|
|
4635
|
-
warnings: [...capture.warnings].sort(
|
|
6270
|
+
warnings: [...capture.warnings].sort(compareCodeUnits8)
|
|
4636
6271
|
});
|
|
4637
6272
|
outputFiles.push(
|
|
4638
6273
|
{ path: files.pngPath, source: capture.png },
|
|
@@ -4649,7 +6284,7 @@ async function runVisualUpdate(scenes, options) {
|
|
|
4649
6284
|
const document = {
|
|
4650
6285
|
schemaVersion: 1,
|
|
4651
6286
|
command: "visual.update",
|
|
4652
|
-
updates: updates.sort((left, right) =>
|
|
6287
|
+
updates: updates.sort((left, right) => compareCodeUnits8(left.scene, right.scene))
|
|
4653
6288
|
};
|
|
4654
6289
|
if (options.json) {
|
|
4655
6290
|
process.stdout.write(serializeTileflowVisualCommandJson(document));
|
|
@@ -4705,7 +6340,7 @@ async function readBaselineFiles(directory, scene) {
|
|
|
4705
6340
|
return { kind: "complete", pngPath, receiptPath, baseline: { png, receipt } };
|
|
4706
6341
|
}
|
|
4707
6342
|
async function readOptionalArtifact(path, boundary, maximumBytes) {
|
|
4708
|
-
await assertNoSymlinkComponents(
|
|
6343
|
+
await assertNoSymlinkComponents(dirname9(path), boundary);
|
|
4709
6344
|
let info;
|
|
4710
6345
|
try {
|
|
4711
6346
|
info = await lstat3(path);
|
|
@@ -4723,7 +6358,7 @@ async function readOptionalArtifact(path, boundary, maximumBytes) {
|
|
|
4723
6358
|
throw new TileflowCaptureError2("BASELINE_INVALID", "A visual baseline artifact is too large.");
|
|
4724
6359
|
}
|
|
4725
6360
|
await assertNotSymlink(path);
|
|
4726
|
-
return new Uint8Array(await
|
|
6361
|
+
return new Uint8Array(await readFile6(path));
|
|
4727
6362
|
}
|
|
4728
6363
|
async function readVisualReference(path) {
|
|
4729
6364
|
let info;
|
|
@@ -4747,7 +6382,7 @@ async function readVisualReference(path) {
|
|
|
4747
6382
|
);
|
|
4748
6383
|
}
|
|
4749
6384
|
await assertNotSymlink(path);
|
|
4750
|
-
return new Uint8Array(await
|
|
6385
|
+
return new Uint8Array(await readFile6(path));
|
|
4751
6386
|
}
|
|
4752
6387
|
async function resolveVisualDirectory(cwd, value, kind) {
|
|
4753
6388
|
const path = resolve6(cwd, value);
|
|
@@ -4810,7 +6445,9 @@ var defaultApiUrl = defaultTileflowApiUrl;
|
|
|
4810
6445
|
var defaultAppUrl = "https://tileflow.dev";
|
|
4811
6446
|
var defaultConfigPath = defaultTileflowConfigPath;
|
|
4812
6447
|
var defaultManifestPath = defaultTileflowManifestPath;
|
|
4813
|
-
program.name("tileflow").description("Beautiful maps from config.").version(packageJson.version).showHelpAfterError(
|
|
6448
|
+
program.name("tileflow").description("Beautiful maps from config.").version(packageJson.version).showHelpAfterError(pc3.gray("\nRun tileflow <command> --help for usage.")).showSuggestionAfterError();
|
|
6449
|
+
registerLanguageCommand(program);
|
|
6450
|
+
registerTilesetCommands(program, { defaultApiUrl, loadAuthConfig });
|
|
4814
6451
|
program.command("init").description("Create a starter tileflow.config.ts").option("-f, --force", "overwrite an existing config").action(async (options) => {
|
|
4815
6452
|
const configPath = resolve7(process.cwd(), defaultConfigPath);
|
|
4816
6453
|
const ignorePath = resolve7(process.cwd(), ".gitignore");
|
|
@@ -4829,9 +6466,9 @@ program.command("init").description("Create a starter tileflow.config.ts").optio
|
|
|
4829
6466
|
}
|
|
4830
6467
|
logSuccess(`Created ${pathLabel(defaultConfigPath)}.`);
|
|
4831
6468
|
printKeyValue("Path", pathLabel(configPath));
|
|
4832
|
-
printKeyValue("Map",
|
|
6469
|
+
printKeyValue("Map", pc3.bold("madrid"));
|
|
4833
6470
|
printNextSteps([
|
|
4834
|
-
`Install ${
|
|
6471
|
+
`Install ${pc3.cyan("@tileflow/core")} and ${pc3.cyan("@tileflow/maps")} in this project if needed.`,
|
|
4835
6472
|
`Preview locally with ${command("tileflow preview")}.`,
|
|
4836
6473
|
`Validate the config with ${command("tileflow validate")}.`
|
|
4837
6474
|
]);
|
|
@@ -4927,7 +6564,7 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
4927
6564
|
).option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
|
|
4928
6565
|
if (options.target !== "local" && options.target !== "hosted") {
|
|
4929
6566
|
if (options.json) {
|
|
4930
|
-
const failure =
|
|
6567
|
+
const failure = createTileflowCommandFailureDocument4(
|
|
4931
6568
|
"validate",
|
|
4932
6569
|
{
|
|
4933
6570
|
code: "INVALID_TARGET",
|
|
@@ -4944,7 +6581,7 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
4944
6581
|
process.cwd(),
|
|
4945
6582
|
{ code: "INVALID_TARGET", phase: "command-validation" }
|
|
4946
6583
|
);
|
|
4947
|
-
process.stderr.write(
|
|
6584
|
+
process.stderr.write(serializeTileflowCommandDocument4(failure));
|
|
4948
6585
|
process.exitCode = 1;
|
|
4949
6586
|
return;
|
|
4950
6587
|
}
|
|
@@ -4959,10 +6596,10 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
4959
6596
|
try {
|
|
4960
6597
|
if (!options.json) logInfo(`Validating ${pathLabel(options.config)}.`);
|
|
4961
6598
|
const loaded = await withTileflowConfigSecretsHidden(
|
|
4962
|
-
() =>
|
|
6599
|
+
() => loadTileflowConfigWithInputs3(options.config)
|
|
4963
6600
|
);
|
|
4964
6601
|
const project = loaded.project;
|
|
4965
|
-
const baseDirectory =
|
|
6602
|
+
const baseDirectory = dirname10(loaded.configFile);
|
|
4966
6603
|
failureDefaults = { code: "CONFIG_INVALID", phase: "config-validation" };
|
|
4967
6604
|
assertValidTileflowConfig2(project);
|
|
4968
6605
|
const mapNames = getTileflowMapNames3(project).sort();
|
|
@@ -4981,8 +6618,14 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
4981
6618
|
apiBaseUrl: options.apiBaseUrl,
|
|
4982
6619
|
mapAssets
|
|
4983
6620
|
});
|
|
6621
|
+
failureDefaults = { code: "TF_LOCAL_TILESET_INVALID", phase: "local-tileset-resolution" };
|
|
6622
|
+
const localTilesets = await prepareTileflowLocalTilesets(project, compiledStyles, {
|
|
6623
|
+
assetBaseUrl: "/tileflow",
|
|
6624
|
+
baseDirectory,
|
|
6625
|
+
cwd: process.cwd()
|
|
6626
|
+
});
|
|
4984
6627
|
failureDefaults = { code: "FONT_COMPILATION_FAILED", phase: "font-compilation" };
|
|
4985
|
-
const { styles } = await prepareTileflowStyleFonts(project,
|
|
6628
|
+
const { styles } = await prepareTileflowStyleFonts(project, localTilesets.styles, {
|
|
4986
6629
|
assetBaseUrl: "/tileflow",
|
|
4987
6630
|
baseDirectory,
|
|
4988
6631
|
cwd: process.cwd(),
|
|
@@ -5006,6 +6649,17 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
5006
6649
|
phase: "hosted-validation"
|
|
5007
6650
|
});
|
|
5008
6651
|
}
|
|
6652
|
+
const diagnostics = createTileflowStructuredDiagnostics2(
|
|
6653
|
+
{
|
|
6654
|
+
diagnostics: mapNames.flatMap(
|
|
6655
|
+
(mapName) => auditTileflowMapThemeValues(project.maps[mapName]).filter(
|
|
6656
|
+
({ severity }) => severity === "warning"
|
|
6657
|
+
)
|
|
6658
|
+
)
|
|
6659
|
+
},
|
|
6660
|
+
process.cwd(),
|
|
6661
|
+
{ code: "VALIDATION_WARNING", phase: "theme-audit" }
|
|
6662
|
+
);
|
|
5009
6663
|
const checks = [
|
|
5010
6664
|
"Config schema",
|
|
5011
6665
|
"Icon asset closure",
|
|
@@ -5015,7 +6669,7 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
5015
6669
|
...options.target === "hosted" ? ["Hosted compatibility"] : []
|
|
5016
6670
|
];
|
|
5017
6671
|
if (options.json) {
|
|
5018
|
-
const summary =
|
|
6672
|
+
const summary = createTileflowCommandSummary2({
|
|
5019
6673
|
code: "VALIDATION_OK",
|
|
5020
6674
|
command: "validate",
|
|
5021
6675
|
message: `Tileflow config is valid (${plural(mapNames.length, "map")}).`,
|
|
@@ -5026,27 +6680,30 @@ program.command("validate").description("Validate a Tileflow config").option("-c
|
|
|
5026
6680
|
suggestion: "No changes are required."
|
|
5027
6681
|
});
|
|
5028
6682
|
process.stdout.write(
|
|
5029
|
-
|
|
6683
|
+
serializeTileflowCommandDocument4({
|
|
5030
6684
|
...summary,
|
|
5031
6685
|
target: options.target,
|
|
5032
6686
|
maps: mapNames,
|
|
5033
6687
|
checks,
|
|
5034
|
-
diagnostics
|
|
6688
|
+
diagnostics
|
|
5035
6689
|
})
|
|
5036
6690
|
);
|
|
5037
6691
|
return;
|
|
5038
6692
|
}
|
|
5039
6693
|
logSuccess(`Config is valid (${plural(mapNames.length, "map")}).`);
|
|
5040
6694
|
printChecks(checks);
|
|
6695
|
+
for (const diagnostic of diagnostics) {
|
|
6696
|
+
logWarning(`${diagnostic.path || "(root)"}: ${diagnostic.message}`);
|
|
6697
|
+
}
|
|
5041
6698
|
} catch (error) {
|
|
5042
6699
|
if (!options.json) throw error;
|
|
5043
|
-
const failure =
|
|
6700
|
+
const failure = createTileflowCommandFailureDocument4(
|
|
5044
6701
|
"validate",
|
|
5045
6702
|
error,
|
|
5046
6703
|
process.cwd(),
|
|
5047
6704
|
failureDefaults
|
|
5048
6705
|
);
|
|
5049
|
-
process.stderr.write(
|
|
6706
|
+
process.stderr.write(serializeTileflowCommandDocument4(failure));
|
|
5050
6707
|
process.exitCode = 1;
|
|
5051
6708
|
}
|
|
5052
6709
|
});
|
|
@@ -5224,7 +6881,7 @@ program.command("preview").alias("dev").description("Run a local map preview").o
|
|
|
5224
6881
|
});
|
|
5225
6882
|
}
|
|
5226
6883
|
);
|
|
5227
|
-
program.command("deploy").description("Deploy maps to Tileflow and write the frontend manifest").option("-c, --config <path>", "config path", defaultConfigPath).option("--manifest <path>", "manifest path written for frontend bundlers", defaultManifestPath).option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).option("--
|
|
6884
|
+
program.command("deploy").description("Deploy maps to Tileflow and write the frontend manifest").option("-c, --config <path>", "config path", defaultConfigPath).option("--manifest <path>", "manifest path written for frontend bundlers", defaultManifestPath).option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).option("--map-id <id>", "managed Map destination").option("--map <name>", "configured map to connect when the repository contains multiple maps").option(
|
|
5228
6885
|
"--overwrite-self-hosted-manifest",
|
|
5229
6886
|
"explicitly replace an existing self-hosted manifest at --manifest"
|
|
5230
6887
|
).action(
|
|
@@ -5232,6 +6889,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5232
6889
|
const source = resolveDeploySource(process.env);
|
|
5233
6890
|
const resolveApi = (selectedMap) => requireApiOptions(options, {
|
|
5234
6891
|
allowStoredCredential: allowsStoredDeployCredential(source),
|
|
6892
|
+
assertExplicitKeyMap: true,
|
|
5235
6893
|
capabilityScopes: ["styles:write"],
|
|
5236
6894
|
retryCommand: cliInvocation([
|
|
5237
6895
|
"tileflow",
|
|
@@ -5243,21 +6901,23 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5243
6901
|
"--api-url",
|
|
5244
6902
|
options.apiUrl ?? defaultApiUrl,
|
|
5245
6903
|
...options.overwriteSelfHostedManifest ? ["--overwrite-self-hosted-manifest"] : [],
|
|
5246
|
-
...options.
|
|
6904
|
+
...options.mapId ? ["--map-id", options.mapId] : [],
|
|
6905
|
+
...options.map && selectedMap ? ["--map", selectedMap] : []
|
|
5247
6906
|
])
|
|
5248
6907
|
});
|
|
5249
6908
|
const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
|
|
5250
6909
|
delete process.env.TILEFLOW_API_KEY;
|
|
5251
6910
|
logInfo(`Deploying ${pathLabel(options.config)}.`);
|
|
5252
6911
|
const loaded = await withTileflowConfigSecretsHidden(
|
|
5253
|
-
() =>
|
|
6912
|
+
() => loadTileflowConfigWithInputs3(options.config)
|
|
5254
6913
|
);
|
|
5255
6914
|
const project = loaded.project;
|
|
5256
|
-
const baseDirectory =
|
|
6915
|
+
const baseDirectory = dirname10(loaded.configFile);
|
|
5257
6916
|
assertValidTileflowConfig2(project);
|
|
5258
6917
|
const configuredMapNames = getTileflowMapNames3(project);
|
|
5259
|
-
const mapNames =
|
|
6918
|
+
const mapNames = selectManagedDeployMaps(configuredMapNames, options);
|
|
5260
6919
|
if (!mapNames) return;
|
|
6920
|
+
if (!validateDeployTarget(options)) return;
|
|
5261
6921
|
if (!validateDeployManifestMapNames(mapNames)) return;
|
|
5262
6922
|
const deploymentProject = mapNames.length === configuredMapNames.length ? project : {
|
|
5263
6923
|
...project,
|
|
@@ -5284,9 +6944,14 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5284
6944
|
apiBaseUrl: apiUrl,
|
|
5285
6945
|
mapAssets: preflightMapAssets
|
|
5286
6946
|
});
|
|
5287
|
-
const
|
|
6947
|
+
const preflightLocalTilesets = await prepareTileflowLocalTilesets(
|
|
5288
6948
|
deploymentProject,
|
|
5289
6949
|
compiledPreflightStyles,
|
|
6950
|
+
{ assetBaseUrl: apiUrl, baseDirectory, cwd: process.cwd() }
|
|
6951
|
+
);
|
|
6952
|
+
const preflightFonts = await prepareTileflowStyleFonts(
|
|
6953
|
+
deploymentProject,
|
|
6954
|
+
preflightLocalTilesets.styles,
|
|
5290
6955
|
{
|
|
5291
6956
|
assetBaseUrl: `${apiUrl}/fonts/preflight`,
|
|
5292
6957
|
baseDirectory,
|
|
@@ -5299,6 +6964,14 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5299
6964
|
)) {
|
|
5300
6965
|
return;
|
|
5301
6966
|
}
|
|
6967
|
+
const mapsWithHostedFontBundles = Object.keys(preflightFonts.bundles).sort();
|
|
6968
|
+
if (mapsWithHostedFontBundles.length > 0) {
|
|
6969
|
+
logError("Hosted deploy does not yet support package-owned web fonts.");
|
|
6970
|
+
printKeyValue("Maps", mapsWithHostedFontBundles.join(", "));
|
|
6971
|
+
printNextSteps(["Use self-hosted delivery or an explicit public glyph provider."]);
|
|
6972
|
+
process.exitCode = 1;
|
|
6973
|
+
return;
|
|
6974
|
+
}
|
|
5302
6975
|
let outputManifest = null;
|
|
5303
6976
|
try {
|
|
5304
6977
|
outputManifest = await loadExistingDeployManifest(options.manifest, {
|
|
@@ -5315,7 +6988,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5315
6988
|
process.exitCode = 1;
|
|
5316
6989
|
return;
|
|
5317
6990
|
}
|
|
5318
|
-
const existingManifest =
|
|
6991
|
+
const existingManifest = outputManifest;
|
|
5319
6992
|
const api = await resolveApi(mapNames[0]);
|
|
5320
6993
|
if (!api) return;
|
|
5321
6994
|
const hostedSpriteByPackageHash = /* @__PURE__ */ new Map();
|
|
@@ -5335,7 +7008,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5335
7008
|
);
|
|
5336
7009
|
const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
|
|
5337
7010
|
logSuccess(
|
|
5338
|
-
`${action} icon package ${
|
|
7011
|
+
`${action} icon package ${pc3.bold(binding?.label ?? "Icons")} (${plural(iconPackage.manifest.iconNames.length, "icon")}, ${formatBytes3(totalBytes)}, ${iconPackage.contentHash.slice(0, 12)}).`
|
|
5339
7012
|
);
|
|
5340
7013
|
hostedSpriteByPackageHash.set(iconPackage.contentHash, uploaded.value.spriteUrl);
|
|
5341
7014
|
}
|
|
@@ -5348,12 +7021,21 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5348
7021
|
apiBaseUrl: api.apiUrl,
|
|
5349
7022
|
mapAssets: hostedMapAssets
|
|
5350
7023
|
});
|
|
5351
|
-
const
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
cwd: process.cwd()
|
|
5355
|
-
|
|
5356
|
-
|
|
7024
|
+
const hostedLocalTilesets = await prepareTileflowLocalTilesets(
|
|
7025
|
+
deploymentProject,
|
|
7026
|
+
compiledHostedStyles,
|
|
7027
|
+
{ assetBaseUrl: api.apiUrl, baseDirectory, cwd: process.cwd() }
|
|
7028
|
+
);
|
|
7029
|
+
const hostedFonts = await prepareTileflowStyleFonts(
|
|
7030
|
+
deploymentProject,
|
|
7031
|
+
hostedLocalTilesets.styles,
|
|
7032
|
+
{
|
|
7033
|
+
assetBaseUrl: `${api.apiUrl}/font-bundles/preflight`,
|
|
7034
|
+
baseDirectory,
|
|
7035
|
+
cwd: process.cwd(),
|
|
7036
|
+
target: "hosted"
|
|
7037
|
+
}
|
|
7038
|
+
);
|
|
5357
7039
|
for (const mapName of mapNames) {
|
|
5358
7040
|
if (hostedFonts.bundles[mapName]?.contentHash !== preflightFonts.bundles[mapName]?.contentHash) {
|
|
5359
7041
|
throw new Error(`Font bundle changed after authentication for map ${mapName}.`);
|
|
@@ -5378,7 +7060,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5378
7060
|
);
|
|
5379
7061
|
hostedFontBaseByHash.set(bundle.contentHash, uploaded.value.baseUrl);
|
|
5380
7062
|
}
|
|
5381
|
-
const
|
|
7063
|
+
const fontBoundStyles = Object.fromEntries(
|
|
5382
7064
|
mapNames.map((mapName) => {
|
|
5383
7065
|
const themeStyles = hostedFonts.styles[mapName];
|
|
5384
7066
|
const bundle = hostedFonts.bundles[mapName];
|
|
@@ -5396,6 +7078,19 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5396
7078
|
];
|
|
5397
7079
|
})
|
|
5398
7080
|
);
|
|
7081
|
+
const hostedThemeFamilies = Object.fromEntries(
|
|
7082
|
+
mapNames.map((mapName) => [
|
|
7083
|
+
mapName,
|
|
7084
|
+
prepareTileflowHostedThemeFamily(
|
|
7085
|
+
mapName,
|
|
7086
|
+
deploymentProject.maps[mapName],
|
|
7087
|
+
fontBoundStyles[mapName]
|
|
7088
|
+
)
|
|
7089
|
+
])
|
|
7090
|
+
);
|
|
7091
|
+
const styles = Object.fromEntries(
|
|
7092
|
+
Object.entries(hostedThemeFamilies).map(([mapName, family]) => [mapName, family.styles])
|
|
7093
|
+
);
|
|
5399
7094
|
const buildManifest = await createTileflowMapBuildManifest(
|
|
5400
7095
|
Object.fromEntries(
|
|
5401
7096
|
mapNames.map((mapName) => {
|
|
@@ -5413,7 +7108,9 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5413
7108
|
})),
|
|
5414
7109
|
...hostedFonts.bundles[mapName]?.files ?? []
|
|
5415
7110
|
],
|
|
5416
|
-
lineage: deploymentProject.mapMetadata?.[mapName]?.lineage ??
|
|
7111
|
+
lineage: deploymentProject.mapMetadata?.[mapName]?.lineage ?? [
|
|
7112
|
+
{ id: map.id, mapVersion: map.version }
|
|
7113
|
+
],
|
|
5417
7114
|
map,
|
|
5418
7115
|
sourceAssets: {
|
|
5419
7116
|
fonts: hostedFonts.sourceIdentities[mapName] ?? [],
|
|
@@ -5431,11 +7128,11 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5431
7128
|
const iconPackage = iconBinding ? packagesByHash.get(iconBinding.packageHash) : void 0;
|
|
5432
7129
|
const fontBundle = hostedFonts.bundles[mapName];
|
|
5433
7130
|
return {
|
|
5434
|
-
allowedOrigins: deploymentProject.maps[mapName]?.delivery?.hosted?.allowedOrigins,
|
|
5435
7131
|
iconBinding,
|
|
5436
7132
|
iconPackage,
|
|
5437
7133
|
fontBundle,
|
|
5438
7134
|
mapName,
|
|
7135
|
+
teamSources: hostedThemeFamilies[mapName].teamSources,
|
|
5439
7136
|
buildManifest: {
|
|
5440
7137
|
maps: { [mapName]: buildManifest.maps[mapName] },
|
|
5441
7138
|
...buildManifest.provenance ? { provenance: buildManifest.provenance } : {},
|
|
@@ -5447,30 +7144,32 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5447
7144
|
const deployedMaps = {};
|
|
5448
7145
|
for (const deployment of deployments) {
|
|
5449
7146
|
const {
|
|
5450
|
-
allowedOrigins,
|
|
5451
7147
|
buildManifest: buildManifest2,
|
|
5452
7148
|
fontBundle,
|
|
5453
7149
|
iconBinding,
|
|
5454
7150
|
iconPackage,
|
|
5455
7151
|
mapName,
|
|
7152
|
+
teamSources,
|
|
5456
7153
|
styles: themeStyles
|
|
5457
7154
|
} = deployment;
|
|
5458
7155
|
const themeNames = Object.keys(themeStyles).sort();
|
|
5459
7156
|
logInfo(
|
|
5460
|
-
`Deploying compiled map ${
|
|
7157
|
+
`Deploying compiled map ${pc3.bold(mapName)} (${plural(themeNames.length, "theme")}).`
|
|
5461
7158
|
);
|
|
5462
7159
|
const response = await publishHostedStyle(
|
|
5463
7160
|
api,
|
|
5464
7161
|
{
|
|
5465
7162
|
artifact: {
|
|
5466
7163
|
buildManifest: buildManifest2,
|
|
7164
|
+
kind: "tileflow-map-deployment",
|
|
5467
7165
|
mapId: mapName,
|
|
5468
|
-
schemaVersion:
|
|
5469
|
-
styles: themeStyles
|
|
7166
|
+
schemaVersion: 1,
|
|
7167
|
+
styles: themeStyles,
|
|
7168
|
+
teamSources
|
|
5470
7169
|
},
|
|
5471
7170
|
environment: mapName,
|
|
5472
|
-
|
|
5473
|
-
|
|
7171
|
+
managedMapId: options.mapId,
|
|
7172
|
+
usageMode: "session",
|
|
5474
7173
|
...iconBinding && iconPackage ? {
|
|
5475
7174
|
iconPackage: {
|
|
5476
7175
|
contentHash: iconPackage.contentHash,
|
|
@@ -5493,8 +7192,9 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5493
7192
|
return;
|
|
5494
7193
|
}
|
|
5495
7194
|
const body = response.value;
|
|
5496
|
-
|
|
5497
|
-
|
|
7195
|
+
const expectedMapId = options.mapId ?? api.mapId;
|
|
7196
|
+
if (expectedMapId && body.mapId !== expectedMapId) {
|
|
7197
|
+
logError(`Deploy response did not confirm the requested Map for ${mapName}.`);
|
|
5498
7198
|
process.exitCode = 1;
|
|
5499
7199
|
return;
|
|
5500
7200
|
}
|
|
@@ -5504,7 +7204,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5504
7204
|
process.exitCode = 1;
|
|
5505
7205
|
return;
|
|
5506
7206
|
}
|
|
5507
|
-
const resolvedMap =
|
|
7207
|
+
const resolvedMap = parseResolvedTileflowMap3(deploymentProject.maps[mapName]);
|
|
5508
7208
|
const mapBuild = buildManifest2.maps[mapName];
|
|
5509
7209
|
deployedMaps[mapName] = {
|
|
5510
7210
|
defaultTheme: resolvedMap.defaultTheme,
|
|
@@ -5528,13 +7228,14 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5528
7228
|
})
|
|
5529
7229
|
),
|
|
5530
7230
|
...resolvedMap.view ? { view: resolvedMap.view } : {},
|
|
5531
|
-
|
|
7231
|
+
usageMode: "session",
|
|
7232
|
+
worldGeneration: "v1"
|
|
5532
7233
|
};
|
|
5533
7234
|
const versionLabel = Number.isInteger(body.version) ? ` (v${body.version})` : "";
|
|
5534
7235
|
if (body.changed === false) {
|
|
5535
|
-
logSuccess(`Unchanged ${
|
|
7236
|
+
logSuccess(`Unchanged ${pc3.bold(mapName)}${versionLabel}.`);
|
|
5536
7237
|
} else {
|
|
5537
|
-
logSuccess(`Published ${
|
|
7238
|
+
logSuccess(`Published ${pc3.bold(mapName)}${versionLabel}.`);
|
|
5538
7239
|
}
|
|
5539
7240
|
}
|
|
5540
7241
|
const manifestPath = await writeDeployManifest(options.manifest, {
|
|
@@ -5545,10 +7246,21 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
5545
7246
|
logSuccess("Deployed Tileflow maps.");
|
|
5546
7247
|
printKeyValue("Manifest", pathLabel(manifestPath));
|
|
5547
7248
|
printDeployedMaps(deployedMaps);
|
|
5548
|
-
|
|
7249
|
+
const deployedMapId = Object.values(deployedMaps)[0]?.mapId;
|
|
7250
|
+
if (deployedMapId) {
|
|
7251
|
+
printNextSteps([
|
|
7252
|
+
`Check hosted state with ${command(`tileflow status --map-id ${deployedMapId}`)}.`
|
|
7253
|
+
]);
|
|
7254
|
+
}
|
|
5549
7255
|
}
|
|
5550
7256
|
);
|
|
5551
|
-
program.command("status").description("Show deployed compiled styles").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).option("--
|
|
7257
|
+
program.command("status").description("Show deployed compiled styles").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).option("--map-id <id>", "managed Map destination").option("--json", "print raw JSON").action(async (options) => {
|
|
7258
|
+
if (!options.mapId || !/^map_[A-Za-z0-9_-]{16}$/u.test(options.mapId)) {
|
|
7259
|
+
if (options.json) console.error("Status requires a valid --map-id.");
|
|
7260
|
+
else logError("Status requires a valid --map-id.");
|
|
7261
|
+
process.exitCode = 1;
|
|
7262
|
+
return;
|
|
7263
|
+
}
|
|
5552
7264
|
let api;
|
|
5553
7265
|
try {
|
|
5554
7266
|
api = await requireApiOptions(options, {
|
|
@@ -5576,7 +7288,7 @@ program.command("status").description("Show deployed compiled styles").option("-
|
|
|
5576
7288
|
}
|
|
5577
7289
|
let status;
|
|
5578
7290
|
try {
|
|
5579
|
-
status = await
|
|
7291
|
+
status = await fetchHostedMapStatus(api);
|
|
5580
7292
|
} catch (error) {
|
|
5581
7293
|
const message = safeStatusError(error);
|
|
5582
7294
|
if (options.json) console.error(message);
|
|
@@ -5584,11 +7296,17 @@ program.command("status").description("Show deployed compiled styles").option("-
|
|
|
5584
7296
|
process.exitCode = 1;
|
|
5585
7297
|
return;
|
|
5586
7298
|
}
|
|
7299
|
+
if (status.mapId !== options.mapId) {
|
|
7300
|
+
if (options.json) console.error("Status response belongs to another Map.");
|
|
7301
|
+
else logError("Status response belongs to another Map.");
|
|
7302
|
+
process.exitCode = 1;
|
|
7303
|
+
return;
|
|
7304
|
+
}
|
|
5587
7305
|
if (options.json) {
|
|
5588
7306
|
console.log(JSON.stringify(status, null, 2));
|
|
5589
7307
|
return;
|
|
5590
7308
|
}
|
|
5591
|
-
|
|
7309
|
+
printMapStatus(status, api.apiUrl);
|
|
5592
7310
|
});
|
|
5593
7311
|
var iconsCommand = program.command("icons").description("Inspect managed Tileflow icons");
|
|
5594
7312
|
registerIconListCommand(iconsCommand, { defaultConfigPath });
|
|
@@ -5599,6 +7317,7 @@ registerIconDiffCommand(iconsCommand, {
|
|
|
5599
7317
|
resolveApi: (options) => {
|
|
5600
7318
|
const source = resolveDeploySource(process.env);
|
|
5601
7319
|
return requireApiOptions(options, {
|
|
7320
|
+
assertExplicitKeyMap: true,
|
|
5602
7321
|
allowStoredCredential: allowsStoredDeployCredential(source),
|
|
5603
7322
|
capabilityScopes: ["status:read"],
|
|
5604
7323
|
retryCommand: cliInvocation([
|
|
@@ -5608,6 +7327,8 @@ registerIconDiffCommand(iconsCommand, {
|
|
|
5608
7327
|
"--against",
|
|
5609
7328
|
options.against,
|
|
5610
7329
|
...options.config ? ["--config", options.config] : [],
|
|
7330
|
+
"--map-id",
|
|
7331
|
+
options.mapId,
|
|
5611
7332
|
"--api-url",
|
|
5612
7333
|
options.apiUrl ?? defaultApiUrl
|
|
5613
7334
|
]),
|
|
@@ -5618,6 +7339,7 @@ registerIconDiffCommand(iconsCommand, {
|
|
|
5618
7339
|
var inspectCommand = program.command("inspect").description("Inspect map data for authoring");
|
|
5619
7340
|
registerConfigInspectCommand(inspectCommand, { defaultConfigPath });
|
|
5620
7341
|
registerFeatureInspectCommand(inspectCommand, { defaultConfigPath });
|
|
7342
|
+
registerAiCommands(program, { defaultApiUrl, defaultConfigPath });
|
|
5621
7343
|
registerCaptureCommands(program, { defaultConfigPath });
|
|
5622
7344
|
registerVisualCommands(program, {
|
|
5623
7345
|
defaultConfigPath,
|
|
@@ -5857,7 +7579,7 @@ async function loginWithDeviceFlow(options) {
|
|
|
5857
7579
|
codeChallenge,
|
|
5858
7580
|
deviceName: hostname()
|
|
5859
7581
|
});
|
|
5860
|
-
printKeyValue("Code",
|
|
7582
|
+
printKeyValue("Code", pc3.bold(authorization.userCode));
|
|
5861
7583
|
printKeyValue("URL", link(authorization.verificationUriComplete));
|
|
5862
7584
|
if (options.noBrowser) {
|
|
5863
7585
|
logMuted("Open the URL above to continue.");
|
|
@@ -5867,7 +7589,7 @@ async function loginWithDeviceFlow(options) {
|
|
|
5867
7589
|
output: process.stdout
|
|
5868
7590
|
});
|
|
5869
7591
|
try {
|
|
5870
|
-
await readline.question(`${
|
|
7592
|
+
await readline.question(`${pc3.cyan("?")} Press Enter to open Tileflow Dashboard...`);
|
|
5871
7593
|
openBrowser(authorization.verificationUriComplete);
|
|
5872
7594
|
} finally {
|
|
5873
7595
|
readline.close();
|
|
@@ -5901,7 +7623,7 @@ async function loginWithDeviceFlow(options) {
|
|
|
5901
7623
|
printKeyValue("Dashboard", link(options.appUrl));
|
|
5902
7624
|
}
|
|
5903
7625
|
function createPkceChallenge(verifier) {
|
|
5904
|
-
return
|
|
7626
|
+
return createHash4("sha256").update(verifier).digest("base64url");
|
|
5905
7627
|
}
|
|
5906
7628
|
function randomBase64Url(byteLength) {
|
|
5907
7629
|
return randomBytes2(byteLength).toString("base64url");
|
|
@@ -5920,7 +7642,7 @@ function openBrowser(url, errorsToStderr = false) {
|
|
|
5920
7642
|
async function writeDeployManifest(manifestPath, manifest) {
|
|
5921
7643
|
const outputPath = resolve7(process.cwd(), manifestPath);
|
|
5922
7644
|
await writeAtomicFile({
|
|
5923
|
-
boundaryPath:
|
|
7645
|
+
boundaryPath: dirname10(outputPath),
|
|
5924
7646
|
force: true,
|
|
5925
7647
|
label: "Deploy manifest",
|
|
5926
7648
|
managed: true,
|
|
@@ -5938,36 +7660,46 @@ function createCompiledMapAssets(compiled, resolveSprite) {
|
|
|
5938
7660
|
])
|
|
5939
7661
|
);
|
|
5940
7662
|
}
|
|
5941
|
-
function
|
|
5942
|
-
if (options.map && !options.
|
|
5943
|
-
logError("--map
|
|
5944
|
-
process.exitCode = 1;
|
|
5945
|
-
return null;
|
|
5946
|
-
}
|
|
5947
|
-
if (!options.worldPromotion) return mapNames;
|
|
5948
|
-
if (!/^wpr_[A-Za-z0-9_-]{8,80}$/u.test(options.worldPromotion)) {
|
|
5949
|
-
logError("World promotion reference is invalid.");
|
|
7663
|
+
function selectManagedDeployMaps(mapNames, options) {
|
|
7664
|
+
if (options.map && !options.mapId) {
|
|
7665
|
+
logError("--map requires --map-id.");
|
|
5950
7666
|
process.exitCode = 1;
|
|
5951
7667
|
return null;
|
|
5952
7668
|
}
|
|
5953
7669
|
if (options.map) {
|
|
5954
7670
|
if (!mapNames.includes(options.map)) {
|
|
5955
|
-
logError(`Unknown Tileflow map
|
|
7671
|
+
logError(`Unknown Tileflow map: ${options.map}.`);
|
|
5956
7672
|
printKeyValue("Maps", mapNames.join(", "));
|
|
5957
7673
|
process.exitCode = 1;
|
|
5958
7674
|
return null;
|
|
5959
7675
|
}
|
|
5960
7676
|
return [options.map];
|
|
5961
7677
|
}
|
|
7678
|
+
if (!options.mapId) return mapNames;
|
|
5962
7679
|
if (mapNames.length === 1) return [mapNames[0]];
|
|
5963
|
-
logError(
|
|
7680
|
+
logError(
|
|
7681
|
+
"This managed target requires an explicit map because the config contains multiple maps."
|
|
7682
|
+
);
|
|
5964
7683
|
printKeyValue("Maps", mapNames.join(", "));
|
|
5965
7684
|
printNextSteps([
|
|
5966
|
-
`Retry with ${command(`tileflow deploy --
|
|
7685
|
+
`Retry with ${command(`tileflow deploy --map-id ${options.mapId} --map ${mapNames[0]}`)}.`
|
|
5967
7686
|
]);
|
|
5968
7687
|
process.exitCode = 1;
|
|
5969
7688
|
return null;
|
|
5970
7689
|
}
|
|
7690
|
+
function validateDeployTarget(options) {
|
|
7691
|
+
if (!options.mapId) {
|
|
7692
|
+
logError("Managed deploy requires --map-id.");
|
|
7693
|
+
process.exitCode = 1;
|
|
7694
|
+
return false;
|
|
7695
|
+
}
|
|
7696
|
+
if (options.mapId && !/^map_[A-Za-z0-9_-]{16}$/u.test(options.mapId)) {
|
|
7697
|
+
logError("Managed Map ID is invalid.");
|
|
7698
|
+
process.exitCode = 1;
|
|
7699
|
+
return false;
|
|
7700
|
+
}
|
|
7701
|
+
return true;
|
|
7702
|
+
}
|
|
5971
7703
|
function validateDeployManifestMapNames(mapNames) {
|
|
5972
7704
|
for (const mapName of mapNames) {
|
|
5973
7705
|
if (!tileflowMapIdSchema2.safeParse(mapName).success) {
|
|
@@ -5981,7 +7713,7 @@ function validateDeployManifestMapNames(mapNames) {
|
|
|
5981
7713
|
async function loadExistingDeployManifest(manifestPath, options) {
|
|
5982
7714
|
let source;
|
|
5983
7715
|
try {
|
|
5984
|
-
source = await
|
|
7716
|
+
source = await readFile7(resolve7(process.cwd(), manifestPath), "utf8");
|
|
5985
7717
|
} catch (error) {
|
|
5986
7718
|
if (error.code === "ENOENT") return null;
|
|
5987
7719
|
throw new Error("Existing Tileflow manifest could not be read.");
|
|
@@ -6014,13 +7746,32 @@ async function loadExistingDeployManifest(manifestPath, options) {
|
|
|
6014
7746
|
}
|
|
6015
7747
|
async function requireApiOptions(options, behavior = {}) {
|
|
6016
7748
|
const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
|
|
7749
|
+
const requestedMapId = options.mapId;
|
|
6017
7750
|
const requested = options.project ? parseProjectReference(options.project) : null;
|
|
7751
|
+
if (requestedMapId && !/^map_[A-Za-z0-9_-]{16}$/u.test(requestedMapId)) {
|
|
7752
|
+
if (!behavior.silent) logError("Managed Map ID is invalid.");
|
|
7753
|
+
process.exitCode = 1;
|
|
7754
|
+
return null;
|
|
7755
|
+
}
|
|
6018
7756
|
if (options.project && !requested) {
|
|
6019
7757
|
if (!behavior.silent) logError("Managed destination must use @organization/project syntax.");
|
|
6020
7758
|
process.exitCode = 1;
|
|
6021
7759
|
return null;
|
|
6022
7760
|
}
|
|
6023
7761
|
if (options.apiKey) {
|
|
7762
|
+
if (requestedMapId && behavior.assertExplicitKeyMap) {
|
|
7763
|
+
const profile = await validateApiKey(apiUrl, options.apiKey);
|
|
7764
|
+
if (!profile.ok) {
|
|
7765
|
+
if (!behavior.silent) logError(profile.error);
|
|
7766
|
+
process.exitCode = 1;
|
|
7767
|
+
return null;
|
|
7768
|
+
}
|
|
7769
|
+
if (profile.value.mapId !== requestedMapId) {
|
|
7770
|
+
if (!behavior.silent) logError("This API key belongs to another Map.");
|
|
7771
|
+
process.exitCode = 1;
|
|
7772
|
+
return null;
|
|
7773
|
+
}
|
|
7774
|
+
}
|
|
6024
7775
|
if (requested) {
|
|
6025
7776
|
const profile = await validateApiKey(apiUrl, options.apiKey);
|
|
6026
7777
|
if (!profile.ok) {
|
|
@@ -6028,7 +7779,7 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
6028
7779
|
process.exitCode = 1;
|
|
6029
7780
|
return null;
|
|
6030
7781
|
}
|
|
6031
|
-
if (projectReference(profile.value) !== options.project) {
|
|
7782
|
+
if (requested && projectReference(profile.value) !== options.project) {
|
|
6032
7783
|
if (!behavior.silent) {
|
|
6033
7784
|
logError(
|
|
6034
7785
|
`This project key belongs to ${projectReference(profile.value)}, not ${options.project}.`
|
|
@@ -6038,14 +7789,18 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
6038
7789
|
return null;
|
|
6039
7790
|
}
|
|
6040
7791
|
}
|
|
6041
|
-
return {
|
|
7792
|
+
return {
|
|
7793
|
+
apiKey: options.apiKey,
|
|
7794
|
+
apiUrl,
|
|
7795
|
+
...requestedMapId ? { mapId: requestedMapId } : {}
|
|
7796
|
+
};
|
|
6042
7797
|
}
|
|
6043
7798
|
const allowStoredCredential = behavior.allowStoredCredential !== false && allowsStoredDeployCredential(resolveDeploySource(process.env));
|
|
6044
7799
|
if (!allowStoredCredential) {
|
|
6045
7800
|
if (!behavior.silent) {
|
|
6046
|
-
logError("CI requires an explicit
|
|
7801
|
+
logError("CI requires an explicit Map-scoped Tileflow API key.");
|
|
6047
7802
|
printNextSteps([
|
|
6048
|
-
`Set ${
|
|
7803
|
+
`Set ${pc3.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
|
|
6049
7804
|
"The saved personal account session is never used in CI."
|
|
6050
7805
|
]);
|
|
6051
7806
|
}
|
|
@@ -6071,6 +7826,19 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
6071
7826
|
process.exitCode = 1;
|
|
6072
7827
|
return null;
|
|
6073
7828
|
}
|
|
7829
|
+
if (requestedMapId) {
|
|
7830
|
+
const capability2 = await requestMapCapability(
|
|
7831
|
+
resolvedSession.session,
|
|
7832
|
+
{ mapId: requestedMapId },
|
|
7833
|
+
behavior.capabilityScopes ?? ["status:read"]
|
|
7834
|
+
);
|
|
7835
|
+
if (!capability2.ok) {
|
|
7836
|
+
if (!behavior.silent) logError(capability2.error);
|
|
7837
|
+
process.exitCode = 1;
|
|
7838
|
+
return null;
|
|
7839
|
+
}
|
|
7840
|
+
return { apiKey: capability2.capability, apiUrl, mapId: capability2.mapId };
|
|
7841
|
+
}
|
|
6074
7842
|
const target = await resolveAccountProjectTarget(resolvedSession.session, options.project);
|
|
6075
7843
|
if (target.kind === "remote") {
|
|
6076
7844
|
if (!behavior.silent) logError(target.error);
|
|
@@ -6138,17 +7906,17 @@ function printCliError(error) {
|
|
|
6138
7906
|
}
|
|
6139
7907
|
const message = error instanceof Error ? error.message : "Command failed.";
|
|
6140
7908
|
if (message.includes("Cannot find module '@tileflow/core'")) {
|
|
6141
|
-
logError(`Cannot load ${
|
|
7909
|
+
logError(`Cannot load ${pc3.cyan("@tileflow/core")} from this project.`);
|
|
6142
7910
|
printNextSteps([
|
|
6143
|
-
`Install ${
|
|
7911
|
+
`Install ${pc3.cyan("@tileflow/core")} in this project.`,
|
|
6144
7912
|
`Run the command again from the project root.`
|
|
6145
7913
|
]);
|
|
6146
7914
|
return;
|
|
6147
7915
|
}
|
|
6148
7916
|
if (message.includes("Cannot find module '@tileflow/maps'")) {
|
|
6149
|
-
logError(`Cannot load ${
|
|
7917
|
+
logError(`Cannot load ${pc3.cyan("@tileflow/maps")} from this project.`);
|
|
6150
7918
|
printNextSteps([
|
|
6151
|
-
`Install ${
|
|
7919
|
+
`Install ${pc3.cyan("@tileflow/maps")} in this project.`,
|
|
6152
7920
|
`Run the command again from the project root.`
|
|
6153
7921
|
]);
|
|
6154
7922
|
return;
|
|
@@ -6159,51 +7927,51 @@ function printCliError(error) {
|
|
|
6159
7927
|
}
|
|
6160
7928
|
}
|
|
6161
7929
|
function logSuccess(message) {
|
|
6162
|
-
console.log(`${
|
|
7930
|
+
console.log(`${pc3.green("\u2713")} ${message}`);
|
|
6163
7931
|
}
|
|
6164
7932
|
function logError(message) {
|
|
6165
|
-
console.log(`${
|
|
7933
|
+
console.log(`${pc3.red("\u2715")} ${message}`);
|
|
6166
7934
|
}
|
|
6167
7935
|
function logWarning(message) {
|
|
6168
|
-
console.log(`${
|
|
7936
|
+
console.log(`${pc3.yellow("!")} ${message}`);
|
|
6169
7937
|
}
|
|
6170
7938
|
function logInfo(message) {
|
|
6171
|
-
console.log(`${
|
|
7939
|
+
console.log(`${pc3.cyan("i")} ${message}`);
|
|
6172
7940
|
}
|
|
6173
7941
|
function logMuted(message) {
|
|
6174
|
-
console.log(
|
|
7942
|
+
console.log(pc3.gray(message));
|
|
6175
7943
|
}
|
|
6176
7944
|
function printTitle(title, subtitle) {
|
|
6177
|
-
console.log(
|
|
7945
|
+
console.log(pc3.bold(title));
|
|
6178
7946
|
if (subtitle) {
|
|
6179
|
-
console.log(
|
|
7947
|
+
console.log(pc3.gray(subtitle));
|
|
6180
7948
|
}
|
|
6181
7949
|
}
|
|
6182
7950
|
function printKeyValue(label, value) {
|
|
6183
|
-
console.log(`${
|
|
7951
|
+
console.log(`${pc3.gray(`${label}:`.padEnd(13))} ${value}`);
|
|
6184
7952
|
}
|
|
6185
7953
|
function printChecks(items) {
|
|
6186
7954
|
for (const item of items) {
|
|
6187
|
-
console.log(` ${
|
|
7955
|
+
console.log(` ${pc3.green("\u2713")} ${item}`);
|
|
6188
7956
|
}
|
|
6189
7957
|
}
|
|
6190
7958
|
function printNextSteps(steps) {
|
|
6191
7959
|
if (steps.length === 0) return;
|
|
6192
7960
|
console.log(`
|
|
6193
|
-
${
|
|
7961
|
+
${pc3.bold("Next steps")}`);
|
|
6194
7962
|
for (const step of steps) {
|
|
6195
|
-
console.log(` ${
|
|
7963
|
+
console.log(` ${pc3.cyan("\u2022")} ${step}`);
|
|
6196
7964
|
}
|
|
6197
7965
|
}
|
|
6198
7966
|
function printValidationErrors(messages) {
|
|
6199
7967
|
logError("Tileflow config has errors.");
|
|
6200
7968
|
for (const message of messages) {
|
|
6201
|
-
const path = message.path ?
|
|
6202
|
-
console.log(` ${
|
|
7969
|
+
const path = message.path ? pc3.cyan(message.path) : pc3.cyan("(root)");
|
|
7970
|
+
console.log(` ${pc3.red("\u2715")} ${path} ${pc3.gray("-")} ${message.message}`);
|
|
6203
7971
|
}
|
|
6204
7972
|
}
|
|
6205
7973
|
function printAccountDetails(session) {
|
|
6206
|
-
printKeyValue("Account",
|
|
7974
|
+
printKeyValue("Account", pc3.bold(session.account.name));
|
|
6207
7975
|
printKeyValue("Email", session.account.email);
|
|
6208
7976
|
printKeyValue("API", link(session.apiOrigin));
|
|
6209
7977
|
printKeyValue("Expires", formatDate(session.expiresAt));
|
|
@@ -6232,11 +8000,11 @@ function printDeployedMaps(maps) {
|
|
|
6232
8000
|
const entries = Object.entries(maps);
|
|
6233
8001
|
if (entries.length === 0) return;
|
|
6234
8002
|
console.log(`
|
|
6235
|
-
${
|
|
8003
|
+
${pc3.bold("Maps")}`);
|
|
6236
8004
|
for (const [name, map] of entries) {
|
|
6237
8005
|
for (const [themeName, theme] of Object.entries(map.themes)) {
|
|
6238
8006
|
const label = `${name}/${themeName}`;
|
|
6239
|
-
console.log(` ${
|
|
8007
|
+
console.log(` ${pc3.green("\u2713")} ${label.padEnd(24)} ${link(theme.styleUrl)}`);
|
|
6240
8008
|
}
|
|
6241
8009
|
}
|
|
6242
8010
|
}
|
|
@@ -6254,7 +8022,7 @@ function plural(count, singular, pluralForm = `${singular}s`) {
|
|
|
6254
8022
|
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
6255
8023
|
}
|
|
6256
8024
|
function command(value) {
|
|
6257
|
-
return
|
|
8025
|
+
return pc3.cyan(value);
|
|
6258
8026
|
}
|
|
6259
8027
|
function cliInvocation(parts) {
|
|
6260
8028
|
return parts.map(quoteCliArgument).join(" ");
|
|
@@ -6263,10 +8031,10 @@ function quoteCliArgument(value) {
|
|
|
6263
8031
|
return /^[A-Za-z0-9_./:@%+=,-]+$/u.test(value) ? value : `'${value.replaceAll("'", `'"'"'`)}'`;
|
|
6264
8032
|
}
|
|
6265
8033
|
function link(value) {
|
|
6266
|
-
return
|
|
8034
|
+
return pc3.cyan(value);
|
|
6267
8035
|
}
|
|
6268
8036
|
function pathLabel(value) {
|
|
6269
|
-
return
|
|
8037
|
+
return pc3.cyan(value);
|
|
6270
8038
|
}
|
|
6271
8039
|
function starterConfig() {
|
|
6272
8040
|
return `import { defineMap, defineTheme, labels, poi } from "@tileflow/core";
|
|
@@ -6295,7 +8063,7 @@ export default defineMap({
|
|
|
6295
8063
|
systemThemes: { light: "light", dark: "dark" },
|
|
6296
8064
|
modules: {
|
|
6297
8065
|
labels: labels({ roads: "major" }),
|
|
6298
|
-
poi: poi({
|
|
8066
|
+
poi: poi({ density: 3, icons: true })
|
|
6299
8067
|
},
|
|
6300
8068
|
view: {
|
|
6301
8069
|
center: [-3.7038, 40.4168],
|
|
@@ -6310,17 +8078,17 @@ function printHostedCompatibilityIssues(issues) {
|
|
|
6310
8078
|
process.exitCode = 1;
|
|
6311
8079
|
return false;
|
|
6312
8080
|
}
|
|
6313
|
-
function
|
|
8081
|
+
function printMapStatus(status, apiUrl) {
|
|
6314
8082
|
printTitle("Tileflow status");
|
|
6315
|
-
printKeyValue("
|
|
8083
|
+
printKeyValue("Map", pc3.bold(status.mapId));
|
|
6316
8084
|
console.log(`
|
|
6317
|
-
${
|
|
8085
|
+
${pc3.bold("Styles")}`);
|
|
6318
8086
|
if (status.styles.length === 0) {
|
|
6319
8087
|
logMuted(" No styles deployed.");
|
|
6320
8088
|
}
|
|
6321
8089
|
for (const style of status.styles) {
|
|
6322
8090
|
console.log(
|
|
6323
|
-
` ${
|
|
8091
|
+
` ${pc3.green("\u2713")} ${style.environment.padEnd(16)} ${link(`${apiUrl}/maps/${style.mapId}/style.json`).padEnd(36)} ${formatBytes3(style.size).padStart(10)} ${pc3.gray(formatDate(style.uploaded))}`
|
|
6324
8092
|
);
|
|
6325
8093
|
}
|
|
6326
8094
|
}
|