tileflow 0.1.0-alpha.15 → 0.1.0-alpha.17
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 +17 -13
- package/dist/index.js +499 -95
- package/docs/hosted-authentication-and-deploy.md +35 -2
- package/docs/local-visual-feedback.md +3 -3
- package/docs/map-authoring.md +11 -11
- package/docs/native-artifacts.md +3 -3
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { serve } from "@hono/node-server";
|
|
5
5
|
import { Command } from "commander";
|
|
6
|
-
import { createHash as createHash4, randomBytes as
|
|
6
|
+
import { createHash as createHash4, randomBytes as randomBytes3 } from "crypto";
|
|
7
7
|
import { existsSync, readFileSync, realpathSync } from "fs";
|
|
8
8
|
import { readFile as readFile7, writeFile as writeFile2 } from "fs/promises";
|
|
9
9
|
import { hostname } from "os";
|
|
10
10
|
import { dirname as dirname13, isAbsolute as isAbsolute5, relative as relative6, resolve as resolve12, sep as sep5 } from "path";
|
|
11
|
-
import { createInterface } from "readline/promises";
|
|
12
|
-
import
|
|
11
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
12
|
+
import pc9 from "picocolors";
|
|
13
13
|
import { createLocalCoordinates, setupCoordinates } from "@tileflow/coordinates-runtime";
|
|
14
14
|
import {
|
|
15
15
|
auditTileflowMapThemeValues as auditTileflowMapThemeValues2,
|
|
@@ -4447,6 +4447,88 @@ async function listAccountTeams(session, options = {}) {
|
|
|
4447
4447
|
}
|
|
4448
4448
|
return { ok: true, teams };
|
|
4449
4449
|
}
|
|
4450
|
+
async function listMapSetupTeams(session, options = {}) {
|
|
4451
|
+
let response;
|
|
4452
|
+
try {
|
|
4453
|
+
response = await requestHostedJson(
|
|
4454
|
+
session.apiOrigin,
|
|
4455
|
+
"/v1/cli/map-setup/teams",
|
|
4456
|
+
{ headers: authorizationHeaders(session.accountSession) },
|
|
4457
|
+
options
|
|
4458
|
+
);
|
|
4459
|
+
} catch (error) {
|
|
4460
|
+
return { error: safeTransportError(error, "Team discovery failed."), ok: false };
|
|
4461
|
+
}
|
|
4462
|
+
if (!response.ok) {
|
|
4463
|
+
return {
|
|
4464
|
+
error: response.status === 401 ? rejectedAccountSessionMessage : `Team discovery failed (${response.status}).`,
|
|
4465
|
+
ok: false
|
|
4466
|
+
};
|
|
4467
|
+
}
|
|
4468
|
+
const body = asRecord(response.body);
|
|
4469
|
+
if (!response.json || body.schemaVersion !== 1 || !Array.isArray(body.teams) || !body.teams.every(isMapSetupTeam)) {
|
|
4470
|
+
return { error: "Team discovery returned an invalid response.", ok: false };
|
|
4471
|
+
}
|
|
4472
|
+
return { ok: true, teams: body.teams };
|
|
4473
|
+
}
|
|
4474
|
+
async function createManagedMapSetup(session, input, options = {}) {
|
|
4475
|
+
let response;
|
|
4476
|
+
try {
|
|
4477
|
+
response = await requestHostedJson(
|
|
4478
|
+
session.apiOrigin,
|
|
4479
|
+
"/v1/cli/maps",
|
|
4480
|
+
{
|
|
4481
|
+
...jsonRequest("POST", input),
|
|
4482
|
+
headers: {
|
|
4483
|
+
...authorizationHeaders(session.accountSession),
|
|
4484
|
+
"Content-Type": "application/json"
|
|
4485
|
+
}
|
|
4486
|
+
},
|
|
4487
|
+
options
|
|
4488
|
+
);
|
|
4489
|
+
} catch (error) {
|
|
4490
|
+
return {
|
|
4491
|
+
code: "setup_failed",
|
|
4492
|
+
error: safeTransportError(error, "Map setup request failed."),
|
|
4493
|
+
ok: false
|
|
4494
|
+
};
|
|
4495
|
+
}
|
|
4496
|
+
const body = asRecord(response.body);
|
|
4497
|
+
if (!response.ok) {
|
|
4498
|
+
if (response.status === 401) {
|
|
4499
|
+
return { code: "account_session_rejected", error: rejectedAccountSessionMessage, ok: false };
|
|
4500
|
+
}
|
|
4501
|
+
const code = mapSetupFailureMessages[body.code] ? body.code : null;
|
|
4502
|
+
return code ? { code, error: mapSetupFailureMessages[code], ok: false } : { code: "setup_failed", error: `Map setup failed (${response.status}).`, ok: false };
|
|
4503
|
+
}
|
|
4504
|
+
const map = asRecord(body.map);
|
|
4505
|
+
const team = asRecord(body.team);
|
|
4506
|
+
const delivery = asRecord(body.delivery);
|
|
4507
|
+
if (!response.json || body.schemaVersion !== 1 || typeof map.id !== "string" || !/^map_[A-Za-z0-9_-]{16}$/u.test(map.id) || map.name !== null && typeof map.name !== "string" || map.state !== "setup_pending" && map.state !== "active" || typeof team.name !== "string" || `@${String(team.slug)}` !== input.team || !Array.isArray(delivery.allowedOrigins) || !delivery.allowedOrigins.every((origin) => typeof origin === "string") || delivery.allowedOrigins.length !== input.allowedOrigins.length) {
|
|
4508
|
+
return { code: "setup_failed", error: "Map setup returned an invalid response.", ok: false };
|
|
4509
|
+
}
|
|
4510
|
+
return {
|
|
4511
|
+
ok: true,
|
|
4512
|
+
value: {
|
|
4513
|
+
allowedOrigins: delivery.allowedOrigins,
|
|
4514
|
+
map: { id: map.id, name: map.name, state: map.state },
|
|
4515
|
+
team: { name: team.name, slug: team.slug }
|
|
4516
|
+
}
|
|
4517
|
+
};
|
|
4518
|
+
}
|
|
4519
|
+
var mapSetupFailureMessages = {
|
|
4520
|
+
invalid_request: "Tileflow rejected the Map setup request.",
|
|
4521
|
+
map_limit: "This Team has reached its Map limit. Archive or delete a Map, or change the plan, in the Tileflow dashboard.",
|
|
4522
|
+
permission_required: "Only a Team Admin can create Maps.",
|
|
4523
|
+
setup_conflict: "This setup key was already used with another Team or browser access choice. Omit --setup-key to start a new setup.",
|
|
4524
|
+
team_not_found: "No Team you administer matches this Team."
|
|
4525
|
+
};
|
|
4526
|
+
function isMapSetupTeam(value) {
|
|
4527
|
+
const team = asRecord(value);
|
|
4528
|
+
return typeof team.eligible === "boolean" && typeof team.name === "string" && typeof team.slug === "string" && /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u.test(team.slug) && (team.unavailableReason === void 0 || team.unavailableReason === "map_limit") && Object.keys(team).every(
|
|
4529
|
+
(key) => ["eligible", "name", "slug", "unavailableReason"].includes(key)
|
|
4530
|
+
);
|
|
4531
|
+
}
|
|
4450
4532
|
async function publishHostedStyle(api, body, responseLabel, options = {}) {
|
|
4451
4533
|
const response = await requestHostedJson(
|
|
4452
4534
|
api.apiUrl,
|
|
@@ -6917,10 +6999,299 @@ async function readCurrentPins(contents) {
|
|
|
6917
6999
|
return (await parseTileflowIconsLockfile(contents)).sets;
|
|
6918
7000
|
}
|
|
6919
7001
|
|
|
7002
|
+
// src/map-commands.ts
|
|
7003
|
+
import pc5 from "picocolors";
|
|
7004
|
+
|
|
7005
|
+
// src/map-setup.ts
|
|
7006
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
7007
|
+
import { createInterface } from "readline/promises";
|
|
7008
|
+
import pc4 from "picocolors";
|
|
7009
|
+
var maximumOrigins2 = 20;
|
|
7010
|
+
var exactHost = /^(?:[a-z0-9-]+(?:\.[a-z0-9-]+)*|\[[0-9a-f:.]+\])$/u;
|
|
7011
|
+
var teamReference = /^@([A-Za-z0-9][A-Za-z0-9._-]{0,127})$/u;
|
|
7012
|
+
var setupKeyPattern = /^[\x21-\x7e]{8,128}$/u;
|
|
7013
|
+
function resolveMapSetupTeam(teams, requestedSlug) {
|
|
7014
|
+
if (requestedSlug !== void 0) {
|
|
7015
|
+
const team = teams.find(({ slug }) => slug === requestedSlug);
|
|
7016
|
+
if (!team) return { kind: "not_found" };
|
|
7017
|
+
return team.eligible ? { kind: "selected", team } : { kind: "map_limit" };
|
|
7018
|
+
}
|
|
7019
|
+
const eligible = teams.filter((team) => team.eligible);
|
|
7020
|
+
if (eligible.length === 1) return { kind: "selected", team: eligible[0] };
|
|
7021
|
+
if (eligible.length > 1) return { kind: "choose", teams: eligible };
|
|
7022
|
+
return teams.some((team) => team.unavailableReason === "map_limit") ? { kind: "map_limit" } : { kind: "none" };
|
|
7023
|
+
}
|
|
7024
|
+
function normalizeBrowserOrigin(value) {
|
|
7025
|
+
let url;
|
|
7026
|
+
try {
|
|
7027
|
+
url = new URL(value);
|
|
7028
|
+
} catch {
|
|
7029
|
+
return null;
|
|
7030
|
+
}
|
|
7031
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.pathname !== "/" || url.search || url.hash || !exactHost.test(url.hostname) || value.toLowerCase() !== url.origin.toLowerCase()) {
|
|
7032
|
+
return null;
|
|
7033
|
+
}
|
|
7034
|
+
return url.origin.toLowerCase();
|
|
7035
|
+
}
|
|
7036
|
+
function parseBrowserAccess(options) {
|
|
7037
|
+
const values = options.origin ?? [];
|
|
7038
|
+
if (options.anyWebsite && values.length > 0) {
|
|
7039
|
+
return { error: "Choose either --any-website or --origin, not both.", ok: false };
|
|
7040
|
+
}
|
|
7041
|
+
if (options.anyWebsite) return { ok: true, value: { kind: "any" } };
|
|
7042
|
+
if (values.length === 0) return { ok: true, value: null };
|
|
7043
|
+
return parseOriginList(values);
|
|
7044
|
+
}
|
|
7045
|
+
function parseOriginList(values) {
|
|
7046
|
+
const origins = /* @__PURE__ */ new Set();
|
|
7047
|
+
for (const value of values) {
|
|
7048
|
+
const origin = normalizeBrowserOrigin(value.trim());
|
|
7049
|
+
if (!origin) {
|
|
7050
|
+
return {
|
|
7051
|
+
error: `${JSON.stringify(value)} is not an exact website origin such as https://example.com (no path or wildcard).`,
|
|
7052
|
+
ok: false
|
|
7053
|
+
};
|
|
7054
|
+
}
|
|
7055
|
+
origins.add(origin);
|
|
7056
|
+
}
|
|
7057
|
+
if (origins.size === 0 || origins.size > maximumOrigins2) {
|
|
7058
|
+
return { error: `Enter between 1 and ${maximumOrigins2} website origins.`, ok: false };
|
|
7059
|
+
}
|
|
7060
|
+
return { ok: true, value: { kind: "selected", origins: [...origins].sort() } };
|
|
7061
|
+
}
|
|
7062
|
+
function checkMapSetupOptions(options) {
|
|
7063
|
+
if (options.team !== void 0 && !teamReference.test(options.team)) {
|
|
7064
|
+
return { code: "invalid_team", error: "Team must use @team syntax.", ok: false };
|
|
7065
|
+
}
|
|
7066
|
+
if (options.setupKey !== void 0 && !setupKeyPattern.test(options.setupKey)) {
|
|
7067
|
+
return {
|
|
7068
|
+
code: "invalid_setup_key",
|
|
7069
|
+
error: "Setup key must be 8 to 128 visible ASCII characters without spaces.",
|
|
7070
|
+
ok: false
|
|
7071
|
+
};
|
|
7072
|
+
}
|
|
7073
|
+
if (options.name !== void 0 && (options.name.length < 1 || options.name.length > 200 || options.name !== options.name.trim() || /\p{Cc}/u.test(options.name))) {
|
|
7074
|
+
return { code: "invalid_name", error: "Map name must be 1 to 200 characters.", ok: false };
|
|
7075
|
+
}
|
|
7076
|
+
const access = parseBrowserAccess(options);
|
|
7077
|
+
return access.ok ? { access: access.value, ok: true } : { code: "invalid_browser_access", error: access.error, ok: false };
|
|
7078
|
+
}
|
|
7079
|
+
async function setUpManagedMap(session, options, dependencies) {
|
|
7080
|
+
const checked = checkMapSetupOptions(options);
|
|
7081
|
+
if (!checked.ok) return checked;
|
|
7082
|
+
const discovered = await listMapSetupTeams(session, dependencies.request);
|
|
7083
|
+
if (!discovered.ok) {
|
|
7084
|
+
return { code: "team_discovery_failed", error: discovered.error, ok: false };
|
|
7085
|
+
}
|
|
7086
|
+
const resolution = resolveMapSetupTeam(
|
|
7087
|
+
discovered.teams,
|
|
7088
|
+
options.team ? teamReference.exec(options.team)[1] : void 0
|
|
7089
|
+
);
|
|
7090
|
+
let team = null;
|
|
7091
|
+
if (resolution.kind === "selected") team = resolution.team;
|
|
7092
|
+
else if (resolution.kind === "choose") {
|
|
7093
|
+
team = dependencies.prompts ? await dependencies.prompts.chooseTeam(resolution.teams) : null;
|
|
7094
|
+
if (!team) {
|
|
7095
|
+
const choices = resolution.teams.map(({ slug }) => `@${slug}`);
|
|
7096
|
+
return {
|
|
7097
|
+
code: "team_ambiguous",
|
|
7098
|
+
error: `Several Teams can own this Map. Choose one with --team: ${choices.join(", ")}.`,
|
|
7099
|
+
ok: false,
|
|
7100
|
+
options: choices
|
|
7101
|
+
};
|
|
7102
|
+
}
|
|
7103
|
+
} else if (resolution.kind === "map_limit") {
|
|
7104
|
+
return {
|
|
7105
|
+
code: "map_limit",
|
|
7106
|
+
error: options.team ? `${options.team} has reached its Map limit. Archive or delete a Map, or change the plan, in the Tileflow dashboard.` : "Every Team you administer has reached its Map limit. Archive or delete a Map, or change the plan, in the Tileflow dashboard.",
|
|
7107
|
+
ok: false
|
|
7108
|
+
};
|
|
7109
|
+
} else if (resolution.kind === "not_found") {
|
|
7110
|
+
return { code: "team_not_found", error: `You do not administer ${options.team}.`, ok: false };
|
|
7111
|
+
} else {
|
|
7112
|
+
return {
|
|
7113
|
+
code: "no_eligible_team",
|
|
7114
|
+
error: "This account does not administer a Team. Ask a Team Admin to create the Map.",
|
|
7115
|
+
ok: false
|
|
7116
|
+
};
|
|
7117
|
+
}
|
|
7118
|
+
const access = checked.access ?? (dependencies.prompts ? await dependencies.prompts.chooseAccess() : null);
|
|
7119
|
+
if (!access) {
|
|
7120
|
+
return {
|
|
7121
|
+
code: "browser_access_required",
|
|
7122
|
+
error: "Choose which websites may load this Map: --origin <origin> (repeatable) for exact sites, or --any-website.",
|
|
7123
|
+
ok: false
|
|
7124
|
+
};
|
|
7125
|
+
}
|
|
7126
|
+
const setupKey = options.setupKey ?? `cli-${randomBytes2(16).toString("hex")}`;
|
|
7127
|
+
const created = await createManagedMapSetup(
|
|
7128
|
+
session,
|
|
7129
|
+
{
|
|
7130
|
+
allowedOrigins: access.kind === "any" ? [] : access.origins,
|
|
7131
|
+
idempotencyKey: setupKey,
|
|
7132
|
+
...options.name ? { name: options.name } : {},
|
|
7133
|
+
team: `@${team.slug}`
|
|
7134
|
+
},
|
|
7135
|
+
dependencies.request
|
|
7136
|
+
);
|
|
7137
|
+
return created.ok ? { ok: true, setupKey, value: created.value } : created;
|
|
7138
|
+
}
|
|
7139
|
+
function describeBrowserAccess(allowedOrigins) {
|
|
7140
|
+
return allowedOrigins.length === 0 ? "Any website" : allowedOrigins.join(", ");
|
|
7141
|
+
}
|
|
7142
|
+
function createTerminalPrompts(json2 = false) {
|
|
7143
|
+
if (json2 || !process.stdin.isTTY || !process.stdout.isTTY) return null;
|
|
7144
|
+
return {
|
|
7145
|
+
async chooseTeam(teams) {
|
|
7146
|
+
console.log(`${pc4.cyan("?")} Which Team should own the new Map?`);
|
|
7147
|
+
teams.forEach(
|
|
7148
|
+
(team, index) => console.log(` ${index + 1}) ${team.name} ${pc4.gray(`@${team.slug}`)}`)
|
|
7149
|
+
);
|
|
7150
|
+
const answer = await ask("Team number", (value) => {
|
|
7151
|
+
const index = Number(value);
|
|
7152
|
+
return Number.isInteger(index) && index >= 1 && index <= teams.length ? teams[index - 1] : null;
|
|
7153
|
+
});
|
|
7154
|
+
return answer;
|
|
7155
|
+
},
|
|
7156
|
+
async chooseAccess() {
|
|
7157
|
+
console.log(`${pc4.cyan("?")} Which websites may load this Map in a browser?`);
|
|
7158
|
+
console.log(" 1) Only selected websites (recommended)");
|
|
7159
|
+
console.log(" 2) Any website");
|
|
7160
|
+
const mode = await ask(
|
|
7161
|
+
"Choice",
|
|
7162
|
+
(value) => value === "1" ? "selected" : value === "2" ? "any" : null
|
|
7163
|
+
);
|
|
7164
|
+
if (mode === null) return null;
|
|
7165
|
+
if (mode === "any") return { kind: "any" };
|
|
7166
|
+
return ask("Website origins, separated by spaces (https://example.com)", (value) => {
|
|
7167
|
+
const parsed = parseOriginList(value.split(/[\s,]+/u).filter(Boolean));
|
|
7168
|
+
if (!parsed.ok) console.log(` ${pc4.yellow(parsed.error)}`);
|
|
7169
|
+
return parsed.ok ? parsed.value : null;
|
|
7170
|
+
});
|
|
7171
|
+
}
|
|
7172
|
+
};
|
|
7173
|
+
}
|
|
7174
|
+
async function ask(question, parse3) {
|
|
7175
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
7176
|
+
try {
|
|
7177
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
7178
|
+
const value = parse3((await readline.question(` ${question}: `)).trim());
|
|
7179
|
+
if (value !== null) return value;
|
|
7180
|
+
}
|
|
7181
|
+
return null;
|
|
7182
|
+
} finally {
|
|
7183
|
+
readline.close();
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
|
|
7187
|
+
// src/map-commands.ts
|
|
7188
|
+
function registerMapCommands(program2, dependencies) {
|
|
7189
|
+
const maps = program2.command("maps").description("Manage Tileflow managed Maps");
|
|
7190
|
+
addMapSetupOptions(
|
|
7191
|
+
maps.command("create").description("Create a managed Map for this account, ready for tileflow deploy --map-id").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--name <name>", "Map name shown in the dashboard").option("--json", "print deterministic schema-version-1 JSON")
|
|
7192
|
+
).action(async (options) => {
|
|
7193
|
+
const session = await loadSession(options, dependencies);
|
|
7194
|
+
if (!session) return;
|
|
7195
|
+
const result = await setUpManagedMap(session, options, {
|
|
7196
|
+
prompts: createTerminalPrompts(options.json)
|
|
7197
|
+
});
|
|
7198
|
+
if (!result.ok) {
|
|
7199
|
+
emitFailure(
|
|
7200
|
+
options.json,
|
|
7201
|
+
result.code,
|
|
7202
|
+
result.error,
|
|
7203
|
+
result.options ? { options: result.options } : {}
|
|
7204
|
+
);
|
|
7205
|
+
return;
|
|
7206
|
+
}
|
|
7207
|
+
const next = `tileflow deploy --map-id ${result.value.map.id}`;
|
|
7208
|
+
if (options.json) {
|
|
7209
|
+
emitJson({
|
|
7210
|
+
command: "maps create",
|
|
7211
|
+
delivery: { allowedOrigins: result.value.allowedOrigins },
|
|
7212
|
+
map: result.value.map,
|
|
7213
|
+
next,
|
|
7214
|
+
ok: true,
|
|
7215
|
+
schemaVersion: 1,
|
|
7216
|
+
setupKey: result.setupKey,
|
|
7217
|
+
team: result.value.team
|
|
7218
|
+
});
|
|
7219
|
+
return;
|
|
7220
|
+
}
|
|
7221
|
+
printCreatedMap(result.value);
|
|
7222
|
+
console.log(`
|
|
7223
|
+
${pc5.bold("Next step")}`);
|
|
7224
|
+
console.log(` ${pc5.cyan("\u2022")} Publish the repository map with ${pc5.cyan(next)}.`);
|
|
7225
|
+
});
|
|
7226
|
+
}
|
|
7227
|
+
function addMapSetupOptions(command2) {
|
|
7228
|
+
return command2.option("--team <team>", "Team that owns a new Map, as @team").option(
|
|
7229
|
+
"--origin <origin>",
|
|
7230
|
+
"exact website origin that may load a new Map (repeatable)",
|
|
7231
|
+
(value, previous = []) => [...previous, value]
|
|
7232
|
+
).option("--any-website", "let any website load a new Map").option("--setup-key <key>", "repeat an interrupted Map creation with the key it printed");
|
|
7233
|
+
}
|
|
7234
|
+
function hasMapSetupOptions(options) {
|
|
7235
|
+
return options.team !== void 0 || (options.origin?.length ?? 0) > 0 || options.anyWebsite === true || options.setupKey !== void 0;
|
|
7236
|
+
}
|
|
7237
|
+
async function createMapForDeploy(options, dependencies) {
|
|
7238
|
+
const session = await loadSession(options, dependencies);
|
|
7239
|
+
if (!session) return null;
|
|
7240
|
+
console.log(`${pc5.cyan("i")} No --map-id given; creating a managed Map for this deploy.`);
|
|
7241
|
+
const result = await setUpManagedMap(session, options, {
|
|
7242
|
+
prompts: dependencies.prompts === void 0 ? createTerminalPrompts() : dependencies.prompts
|
|
7243
|
+
});
|
|
7244
|
+
if (!result.ok) {
|
|
7245
|
+
emitFailure(false, result.code, result.error);
|
|
7246
|
+
return null;
|
|
7247
|
+
}
|
|
7248
|
+
printCreatedMap(result.value);
|
|
7249
|
+
console.log(
|
|
7250
|
+
pc5.gray(
|
|
7251
|
+
`If this deploy is interrupted, rerun it with --setup-key ${result.setupKey} to reuse the same Map.`
|
|
7252
|
+
)
|
|
7253
|
+
);
|
|
7254
|
+
return result.value.map.id;
|
|
7255
|
+
}
|
|
7256
|
+
function printCreatedMap(created) {
|
|
7257
|
+
console.log(
|
|
7258
|
+
`${pc5.green("\u2713")} Created ${pc5.bold(created.map.id)} in ${created.team.name} ${pc5.gray(`@${created.team.slug}`)}.`
|
|
7259
|
+
);
|
|
7260
|
+
console.log(
|
|
7261
|
+
`${pc5.gray("Websites:".padEnd(13))} ${describeBrowserAccess(created.allowedOrigins)}`
|
|
7262
|
+
);
|
|
7263
|
+
}
|
|
7264
|
+
async function loadSession(options, dependencies) {
|
|
7265
|
+
let apiUrl;
|
|
7266
|
+
try {
|
|
7267
|
+
apiUrl = normalizeApiOrigin(options.apiUrl ?? dependencies.defaultApiUrl);
|
|
7268
|
+
} catch (error) {
|
|
7269
|
+
emitFailure(options.json, "invalid_api_url", safeMessage(error));
|
|
7270
|
+
return null;
|
|
7271
|
+
}
|
|
7272
|
+
let config;
|
|
7273
|
+
try {
|
|
7274
|
+
config = await dependencies.loadAuthConfig();
|
|
7275
|
+
} catch (error) {
|
|
7276
|
+
emitFailure(options.json, "auth_state_unavailable", safeMessage(error));
|
|
7277
|
+
return null;
|
|
7278
|
+
}
|
|
7279
|
+
const selected = resolveAccountSession(config, apiUrl);
|
|
7280
|
+
if (selected.kind !== "selected") {
|
|
7281
|
+
emitFailure(
|
|
7282
|
+
options.json,
|
|
7283
|
+
`account_session_${selected.kind}`,
|
|
7284
|
+
selected.kind === "expired" ? "The Tileflow account session has expired. Run tileflow login." : "Creating a Map needs a Tileflow account session. Run tileflow login."
|
|
7285
|
+
);
|
|
7286
|
+
return null;
|
|
7287
|
+
}
|
|
7288
|
+
return selected.session;
|
|
7289
|
+
}
|
|
7290
|
+
|
|
6920
7291
|
// src/model-command.ts
|
|
6921
7292
|
import { lstat as lstat5 } from "fs/promises";
|
|
6922
7293
|
import { basename as basename4, dirname as dirname11, relative as relative4, resolve as resolve8 } from "path";
|
|
6923
|
-
import
|
|
7294
|
+
import pc6 from "picocolors";
|
|
6924
7295
|
import { z as z5 } from "zod";
|
|
6925
7296
|
import { tileflowModelContentType } from "@tileflow/core";
|
|
6926
7297
|
import {
|
|
@@ -6998,7 +7369,7 @@ function registerModelCommands(program2, dependencies) {
|
|
|
6998
7369
|
const current = parsed.data.model.current;
|
|
6999
7370
|
const action = parsed.data.changed ? "Published" : "Unchanged";
|
|
7000
7371
|
console.log(
|
|
7001
|
-
`${
|
|
7372
|
+
`${pc6.green("\u2713")} ${action} 3D model ${pc6.bold(name)} version ${current.version} (${formatBytes3(current.byteLength)}, ${formatCount(current.summary.levels[0]?.triangles ?? 0)} triangles).`
|
|
7002
7373
|
);
|
|
7003
7374
|
}
|
|
7004
7375
|
}
|
|
@@ -7047,7 +7418,7 @@ function registerModelCommands(program2, dependencies) {
|
|
|
7047
7418
|
for (const model of listed) {
|
|
7048
7419
|
const current = model.current;
|
|
7049
7420
|
console.log(
|
|
7050
|
-
`${
|
|
7421
|
+
`${pc6.bold(model.name)} ${current ? `version ${current.version}, ${formatBytes3(current.byteLength)}` : "no current version"} ${model.usedByMaps === 1 ? "1 map" : `${model.usedByMaps} maps`}${model.state === "purging" ? pc6.yellow(" purging") : ""}`
|
|
7051
7422
|
);
|
|
7052
7423
|
}
|
|
7053
7424
|
}
|
|
@@ -7064,7 +7435,7 @@ function registerModelCommands(program2, dependencies) {
|
|
|
7064
7435
|
if (options.json) return emitJson({ ...detail, command: "models status", ok: true });
|
|
7065
7436
|
const { model, versions, uses } = detail;
|
|
7066
7437
|
console.log(
|
|
7067
|
-
`${
|
|
7438
|
+
`${pc6.bold(model.name)}${model.state === "purging" ? pc6.yellow(" (purging)") : ""}`
|
|
7068
7439
|
);
|
|
7069
7440
|
for (const version of versions) {
|
|
7070
7441
|
const role = version.state === "current" ? "current" : version.state === "previous" ? "previous" : version.state;
|
|
@@ -7119,7 +7490,7 @@ function registerModelCommands(program2, dependencies) {
|
|
|
7119
7490
|
model: parsed.data.model
|
|
7120
7491
|
});
|
|
7121
7492
|
console.log(
|
|
7122
|
-
`${
|
|
7493
|
+
`${pc6.green("\u2713")} ${pc6.bold(name)} is at version ${previous.version} again; version ${current.version} is its previous one. Maps draw it on their next load.`
|
|
7123
7494
|
);
|
|
7124
7495
|
});
|
|
7125
7496
|
withTeamOptions2(
|
|
@@ -7156,7 +7527,7 @@ function registerModelCommands(program2, dependencies) {
|
|
|
7156
7527
|
purge: parsed.data.purge
|
|
7157
7528
|
});
|
|
7158
7529
|
console.log(
|
|
7159
|
-
`${
|
|
7530
|
+
`${pc6.green("\u2713")} ${pc6.bold(name)} is being purged; its name is free once its files are deleted.`
|
|
7160
7531
|
);
|
|
7161
7532
|
});
|
|
7162
7533
|
}
|
|
@@ -7420,7 +7791,7 @@ function resolveTileflowExternalOpenInvocation(target, platform) {
|
|
|
7420
7791
|
}
|
|
7421
7792
|
|
|
7422
7793
|
// src/project-commands.ts
|
|
7423
|
-
import
|
|
7794
|
+
import pc7 from "picocolors";
|
|
7424
7795
|
function registerProjectCommands(program2, dependencies) {
|
|
7425
7796
|
const projects = program2.command("projects", { hidden: true }).description("Manage internal application destinations");
|
|
7426
7797
|
projects.command("list").description("List application destinations accessible to the authenticated Tileflow account").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--include-archived", "include archived application destinations").option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
|
|
@@ -7436,12 +7807,12 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7436
7807
|
};
|
|
7437
7808
|
if (options.json) return emitJson2(document);
|
|
7438
7809
|
if (!result.items.length) {
|
|
7439
|
-
console.log(
|
|
7810
|
+
console.log(pc7.gray("No managed destinations found."));
|
|
7440
7811
|
return;
|
|
7441
7812
|
}
|
|
7442
7813
|
for (const target of result.items) {
|
|
7443
7814
|
console.log(
|
|
7444
|
-
` ${target.project.archivedAt ?
|
|
7815
|
+
` ${target.project.archivedAt ? pc7.yellow("\u25CB") : pc7.green("\u25CF")} ${pc7.bold(target.project.name)} ${pc7.gray(target.reference)}${target.project.archivedAt ? pc7.gray(" (archived)") : ""}`
|
|
7445
7816
|
);
|
|
7446
7817
|
}
|
|
7447
7818
|
});
|
|
@@ -7494,8 +7865,8 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7494
7865
|
...asRecord2(result.body)
|
|
7495
7866
|
};
|
|
7496
7867
|
if (options.json) return emitJson2(document);
|
|
7497
|
-
console.log(`${
|
|
7498
|
-
console.log(
|
|
7868
|
+
console.log(`${pc7.green("\u2713")} Application ${pc7.bold(options.name.trim())} is ready.`);
|
|
7869
|
+
console.log(pc7.gray(`Its technical selector is --project @${organization.slug}/${slug}.`));
|
|
7499
7870
|
}
|
|
7500
7871
|
);
|
|
7501
7872
|
projects.command("show <target>").description("Show one application destination").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--json", "print deterministic schema-version-1 JSON").action(async (target, options) => {
|
|
@@ -7517,8 +7888,8 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7517
7888
|
if (!isProjectItem(project)) {
|
|
7518
7889
|
return emitLocalFailure(false, "invalid_response", "Destination response was invalid.");
|
|
7519
7890
|
}
|
|
7520
|
-
console.log(`${
|
|
7521
|
-
console.log(project.archivedAt ?
|
|
7891
|
+
console.log(`${pc7.bold(project.name)} ${pc7.gray(target)}`);
|
|
7892
|
+
console.log(project.archivedAt ? pc7.yellow("Archived") : pc7.green("Active"));
|
|
7522
7893
|
});
|
|
7523
7894
|
for (const archived of [true, false]) {
|
|
7524
7895
|
const verb = archived ? "archive" : "unarchive";
|
|
@@ -7545,7 +7916,7 @@ function registerProjectCommands(program2, dependencies) {
|
|
|
7545
7916
|
...asRecord2(result.body)
|
|
7546
7917
|
};
|
|
7547
7918
|
if (options.json) return emitJson2(document);
|
|
7548
|
-
console.log(`${
|
|
7919
|
+
console.log(`${pc7.green("\u2713")} Application ${archived ? "archived" : "unarchived"}.`);
|
|
7549
7920
|
});
|
|
7550
7921
|
}
|
|
7551
7922
|
}
|
|
@@ -7656,7 +8027,7 @@ function emitLocalFailure(json2, code, message, details = {}) {
|
|
|
7656
8027
|
console.error(message);
|
|
7657
8028
|
emitJson2({ schemaVersion: 1, ok: false, error: { code, ...details } });
|
|
7658
8029
|
} else {
|
|
7659
|
-
console.error(`${
|
|
8030
|
+
console.error(`${pc7.red("\u2715")} ${message}`);
|
|
7660
8031
|
}
|
|
7661
8032
|
process.exitCode = 1;
|
|
7662
8033
|
}
|
|
@@ -7800,7 +8171,7 @@ async function runRendererArtifactCommand(command2, options) {
|
|
|
7800
8171
|
}
|
|
7801
8172
|
|
|
7802
8173
|
// src/tileset-command.ts
|
|
7803
|
-
import
|
|
8174
|
+
import pc8 from "picocolors";
|
|
7804
8175
|
import { z as z7 } from "zod";
|
|
7805
8176
|
import { serializeCanonicalJson as serializeCanonicalJson6 } from "@tileflow/core";
|
|
7806
8177
|
import { inspectTileflowPmtiles } from "@tileflow/dev/tilesets";
|
|
@@ -8459,7 +8830,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
8459
8830
|
`);
|
|
8460
8831
|
return;
|
|
8461
8832
|
}
|
|
8462
|
-
console.log(`${
|
|
8833
|
+
console.log(`${pc8.green("\u2713")} PMTiles archive is valid.`);
|
|
8463
8834
|
console.log(` Type: ${inspection.contract.tileType}`);
|
|
8464
8835
|
console.log(` Zooms: ${inspection.contract.minzoom}-${inspection.contract.maxzoom}`);
|
|
8465
8836
|
console.log(` Source layers: ${inspection.contract.sourceLayers.length}`);
|
|
@@ -8469,7 +8840,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
8469
8840
|
);
|
|
8470
8841
|
}
|
|
8471
8842
|
for (const warning of inspection.warnings) {
|
|
8472
|
-
console.log(`${
|
|
8843
|
+
console.log(`${pc8.yellow("!")} ${warning.message}`);
|
|
8473
8844
|
}
|
|
8474
8845
|
} catch (error) {
|
|
8475
8846
|
const record = asRecord4(error);
|
|
@@ -8482,7 +8853,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
|
|
|
8482
8853
|
`
|
|
8483
8854
|
);
|
|
8484
8855
|
} else {
|
|
8485
|
-
console.error(`${
|
|
8856
|
+
console.error(`${pc8.red("Error:")} ${message}`);
|
|
8486
8857
|
}
|
|
8487
8858
|
process.exitCode = 1;
|
|
8488
8859
|
}
|
|
@@ -8570,7 +8941,7 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8570
8941
|
};
|
|
8571
8942
|
if (options.json) return emitJson(document);
|
|
8572
8943
|
console.log(
|
|
8573
|
-
`${
|
|
8944
|
+
`${pc8.green("\u2713")} ${publication.changed ? "Published" : "Unchanged"} ${pc8.bold(options.id)} in ${authority.team ? `@${authority.team.slug}` : "the Team bound to the data key"}.`
|
|
8574
8945
|
);
|
|
8575
8946
|
} catch (error) {
|
|
8576
8947
|
if (error instanceof TeamTilesetUploadError) {
|
|
@@ -8590,9 +8961,9 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8590
8961
|
const items = await fetchAllTeamTilesets(authority);
|
|
8591
8962
|
const document = { command: "tileset list", schemaVersion: 1, tilesets: items };
|
|
8592
8963
|
if (options.json) return emitJson(document);
|
|
8593
|
-
if (items.length === 0) return console.log(
|
|
8964
|
+
if (items.length === 0) return console.log(pc8.gray("No Team tilesets."));
|
|
8594
8965
|
for (const item of items) {
|
|
8595
|
-
console.log(` ${
|
|
8966
|
+
console.log(` ${pc8.bold(item.logicalName)} ${pc8.gray(item.status)}`);
|
|
8596
8967
|
}
|
|
8597
8968
|
} catch {
|
|
8598
8969
|
emitFailure(options.json, "tileset_list_failed", "Tileset list failed.");
|
|
@@ -8620,7 +8991,7 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8620
8991
|
const document = { command: "tileset status", schemaVersion: 1, tileset: parsed.tileset };
|
|
8621
8992
|
if (options.json) return emitJson(document);
|
|
8622
8993
|
const item = parsed.tileset;
|
|
8623
|
-
console.log(`${
|
|
8994
|
+
console.log(`${pc8.bold(item.name)} ${pc8.gray(item.state)}`);
|
|
8624
8995
|
console.log(` ID: ${item.id}`);
|
|
8625
8996
|
console.log(` Current version: ${String(item.currentVersion ?? "unpublished")}`);
|
|
8626
8997
|
} catch {
|
|
@@ -8671,7 +9042,7 @@ function registerTilesetCommands(program2, dependencies) {
|
|
|
8671
9042
|
const parsed = tilesetPurgeResponseSchema.parse(response.body);
|
|
8672
9043
|
const document = { command: "tileset purge", schemaVersion: 1, ...parsed };
|
|
8673
9044
|
if (options.json) return emitJson(document);
|
|
8674
|
-
console.log(`${
|
|
9045
|
+
console.log(`${pc8.green("\u2713")} Purge ${parsed.reclamation}.`);
|
|
8675
9046
|
} catch {
|
|
8676
9047
|
emitFailure(options.json, "tileset_purge_failed", "Tileset purge failed.");
|
|
8677
9048
|
}
|
|
@@ -9377,9 +9748,9 @@ function createWorkbenchService(options) {
|
|
|
9377
9748
|
if (path === "/capture") {
|
|
9378
9749
|
const { side: id, generation, name, definition } = captureInput.parse(input);
|
|
9379
9750
|
if (definition.target?.kind === "application")
|
|
9380
|
-
throw new Error("
|
|
9751
|
+
throw new Error("Inspector captures require a map scene.");
|
|
9381
9752
|
const side = options.sides[id];
|
|
9382
|
-
if (!side) throw new Error("Unknown
|
|
9753
|
+
if (!side) throw new Error("Unknown Inspector side.");
|
|
9383
9754
|
const state = side.session.getState();
|
|
9384
9755
|
if (state.status !== "ready" || state.generation !== generation)
|
|
9385
9756
|
return json(
|
|
@@ -9436,10 +9807,10 @@ function createWorkbenchService(options) {
|
|
|
9436
9807
|
const url = new URL(request.url);
|
|
9437
9808
|
if (url.origin !== options.origin || request.headers.has("origin") && request.headers.get("origin") !== options.origin || request.headers.get("sec-fetch-site") === "cross-site")
|
|
9438
9809
|
return json(
|
|
9439
|
-
{ code: "ORIGIN_MISMATCH", message: "Use
|
|
9810
|
+
{ code: "ORIGIN_MISMATCH", message: "Use Inspector from its local origin." },
|
|
9440
9811
|
403
|
|
9441
9812
|
);
|
|
9442
|
-
if (closed) return json({ code: "SESSION_CLOSED", message: "
|
|
9813
|
+
if (closed) return json({ code: "SESSION_CLOSED", message: "Inspector has stopped." }, 503);
|
|
9443
9814
|
const path = url.pathname.slice(prefix.length);
|
|
9444
9815
|
try {
|
|
9445
9816
|
if (request.method === "GET") {
|
|
@@ -9492,7 +9863,7 @@ function createWorkbenchService(options) {
|
|
|
9492
9863
|
}
|
|
9493
9864
|
});
|
|
9494
9865
|
}
|
|
9495
|
-
return json({ code: "NOT_FOUND", message: "Unknown
|
|
9866
|
+
return json({ code: "NOT_FOUND", message: "Unknown Inspector endpoint." }, 404);
|
|
9496
9867
|
}
|
|
9497
9868
|
if (request.method !== "POST")
|
|
9498
9869
|
return json({ code: "METHOD_NOT_ALLOWED", message: "Use GET or POST." }, 405);
|
|
@@ -9504,14 +9875,14 @@ function createWorkbenchService(options) {
|
|
|
9504
9875
|
const body = await readBody(request);
|
|
9505
9876
|
if (path === "/context") {
|
|
9506
9877
|
const { client, ...value } = contextInput.parse(body);
|
|
9507
|
-
if (!options.sides[value.side]) throw new Error("Unknown
|
|
9878
|
+
if (!options.sides[value.side]) throw new Error("Unknown Inspector side.");
|
|
9508
9879
|
contexts.delete(client);
|
|
9509
9880
|
contexts.set(client, { ...value, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
9510
9881
|
if (contexts.size > 8) contexts.delete(contexts.keys().next().value);
|
|
9511
9882
|
return json({ saved: true });
|
|
9512
9883
|
}
|
|
9513
9884
|
if (path !== "/capture" && path !== "/compare" && path !== "/captures/delete")
|
|
9514
|
-
return json({ code: "NOT_FOUND", message: "Unknown
|
|
9885
|
+
return json({ code: "NOT_FOUND", message: "Unknown Inspector action." }, 404);
|
|
9515
9886
|
if (pending)
|
|
9516
9887
|
return json(
|
|
9517
9888
|
{
|
|
@@ -9527,7 +9898,7 @@ function createWorkbenchService(options) {
|
|
|
9527
9898
|
pending = void 0;
|
|
9528
9899
|
}
|
|
9529
9900
|
} catch (error) {
|
|
9530
|
-
const message = error instanceof Error ? error.message : "
|
|
9901
|
+
const message = error instanceof Error ? error.message : "Inspector operation failed.";
|
|
9531
9902
|
return json({ code: "WORKBENCH_OPERATION_FAILED", message: message.slice(0, 1500) }, 400);
|
|
9532
9903
|
}
|
|
9533
9904
|
},
|
|
@@ -9551,7 +9922,7 @@ async function readBody(request) {
|
|
|
9551
9922
|
bytes += value.byteLength;
|
|
9552
9923
|
if (bytes > 64 * 1024) {
|
|
9553
9924
|
await reader.cancel();
|
|
9554
|
-
throw new Error("
|
|
9925
|
+
throw new Error("Inspector request exceeds 64 KiB.");
|
|
9555
9926
|
}
|
|
9556
9927
|
chunks.push(value);
|
|
9557
9928
|
}
|
|
@@ -9571,15 +9942,15 @@ function json(value, status = 200) {
|
|
|
9571
9942
|
});
|
|
9572
9943
|
}
|
|
9573
9944
|
function registerWorkbenchInspection(inspect) {
|
|
9574
|
-
inspect.command("session").description("Read the active local
|
|
9945
|
+
inspect.command("session").description("Read the active local Inspector context and capabilities").option("--url <origin>", "local Inspector origin", "http://127.0.0.1:3333").option("--json", "print the versioned context document").action(async (options) => {
|
|
9575
9946
|
try {
|
|
9576
9947
|
const url = new URL(options.url);
|
|
9577
9948
|
if (!["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) || url.protocol !== "http:" || url.username || url.password)
|
|
9578
|
-
throw new Error("--url must identify a local
|
|
9949
|
+
throw new Error("--url must identify a local Inspector HTTP server.");
|
|
9579
9950
|
const response = await fetch(new URL(`${prefix}/context`, url.origin), {
|
|
9580
9951
|
signal: AbortSignal.timeout(5e3)
|
|
9581
9952
|
});
|
|
9582
|
-
if (!response.ok) throw new Error(`
|
|
9953
|
+
if (!response.ok) throw new Error(`Inspector returned HTTP ${response.status}.`);
|
|
9583
9954
|
const document = await response.json();
|
|
9584
9955
|
console.log(
|
|
9585
9956
|
JSON.stringify(
|
|
@@ -9611,7 +9982,7 @@ var defaultApiUrl = defaultTileflowApiUrl;
|
|
|
9611
9982
|
var defaultAppUrl = "https://tileflow.dev";
|
|
9612
9983
|
var defaultConfigPath = defaultTileflowConfigPath2;
|
|
9613
9984
|
var defaultManifestPath = defaultTileflowManifestPath;
|
|
9614
|
-
program.name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(
|
|
9985
|
+
program.name("tileflow").description("Beautiful maps from config.").enablePositionalOptions().showHelpAfterError(pc9.gray("\nRun tileflow <command> --help for usage.")).showSuggestionAfterError();
|
|
9615
9986
|
program.addHelpText("after", "\n -V, --version output the version number\n");
|
|
9616
9987
|
registerDescribeCommand(program);
|
|
9617
9988
|
registerLanguageCommand(program);
|
|
@@ -9636,9 +10007,9 @@ program.command("init").description("Create a starter tileflow.config.ts").optio
|
|
|
9636
10007
|
}
|
|
9637
10008
|
logSuccess(`Created ${pathLabel(defaultConfigPath)}.`);
|
|
9638
10009
|
printKeyValue("Path", pathLabel(configPath));
|
|
9639
|
-
printKeyValue("Map",
|
|
10010
|
+
printKeyValue("Map", pc9.bold("madrid"));
|
|
9640
10011
|
printNextSteps([
|
|
9641
|
-
`Install ${
|
|
10012
|
+
`Install ${pc9.cyan("@tileflow/core")} and ${pc9.cyan("@tileflow/maps")} in this project if needed.`,
|
|
9642
10013
|
`Preview locally with ${command("tileflow preview")}.`,
|
|
9643
10014
|
`Validate the config with ${command("tileflow validate")}.`
|
|
9644
10015
|
]);
|
|
@@ -9920,7 +10291,7 @@ program.command("build").description("Generate static Tileflow styles").option("
|
|
|
9920
10291
|
printKeyValue("Output", pathLabel(resolve12(process.cwd(), options.out)));
|
|
9921
10292
|
}
|
|
9922
10293
|
);
|
|
9923
|
-
program.command("preview").description("Open
|
|
10294
|
+
program.command("preview").description("Open Inspector with inspection and optional comparison").option("-c, --config <path>", "config path", defaultConfigPath).option("--against-config <path>", "second config for live comparison in Inspector").option("--against-map <name>", "map on the right; defaults to --map").option("--against-scene <name>", "committed standalone scene on the right").option("--against-theme <name>", "concrete theme on the right; defaults to --theme").option("--host <host>", "bind host: an explicit IP address or localhost", defaultTileflowDevHost).option("--map <name>", "preview one configured map").option("--theme <name>", "preview one concrete theme; defaults to the map default").option("--renderer <renderer>", "artifact renderer: web or native", "web").option("-p, --port <port>", "preview port", "3333").option("--no-open", "start Inspector without opening the browser").option("--scene <name>", "preview one committed standalone map scene").option(
|
|
9924
10295
|
"--api-base-url <url>",
|
|
9925
10296
|
"Tileflow API base URL used to resolve official map assets",
|
|
9926
10297
|
process.env.TILEFLOW_API_URL ?? defaultApiUrl
|
|
@@ -10102,7 +10473,7 @@ program.command("preview").description("Open Map DevTools with inspection and op
|
|
|
10102
10473
|
previewUrl: workbenchPreviewUrl(previewBasePath, options),
|
|
10103
10474
|
sidecarUrl: `${previewBasePath}/__inspection/${selectedPreview?.mapName ?? "pending"}/${selectedPreview?.themeName ?? "pending"}.json`
|
|
10104
10475
|
},
|
|
10105
|
-
title: "Tileflow
|
|
10476
|
+
title: "Tileflow Inspector",
|
|
10106
10477
|
workbench
|
|
10107
10478
|
});
|
|
10108
10479
|
let invalidSinceLastReady = false;
|
|
@@ -10172,7 +10543,7 @@ program.command("preview").description("Open Map DevTools with inspection and op
|
|
|
10172
10543
|
}
|
|
10173
10544
|
logMuted("Metro remains the JavaScript development server. Press Ctrl+C to stop.");
|
|
10174
10545
|
} else {
|
|
10175
|
-
logSuccess("Tileflow
|
|
10546
|
+
logSuccess("Tileflow Inspector is running and watching for changes.");
|
|
10176
10547
|
printKeyValue("Local", link(origin));
|
|
10177
10548
|
printKeyValue("Config", pathLabel(options.config));
|
|
10178
10549
|
if (options.map) printKeyValue("Map", options.map);
|
|
@@ -10205,12 +10576,17 @@ program.command("preview").description("Open Map DevTools with inspection and op
|
|
|
10205
10576
|
});
|
|
10206
10577
|
}
|
|
10207
10578
|
);
|
|
10208
|
-
|
|
10579
|
+
addMapSetupOptions(
|
|
10580
|
+
program.command("deploy").description(
|
|
10581
|
+
"Deploy maps to Tileflow and write the frontend manifest; without --map-id, create the Map first"
|
|
10582
|
+
)
|
|
10583
|
+
).option("-c, --config <path>", "config path", defaultConfigPath).option("--manifest <path>", "manifest path written for frontend bundlers", defaultManifestPath).option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--api-key <key>", "Tileflow API key", process.env.TILEFLOW_API_KEY).option("--map-id <id>", "managed Map destination; omit it to create a new Map").option("--with-native", "publish web and native-v1 together for the selected managed Map").option("--map <name>", "configured map to connect when the repository contains multiple maps").option(
|
|
10209
10584
|
"--overwrite-self-hosted-manifest",
|
|
10210
10585
|
"explicitly replace an existing self-hosted manifest at --manifest"
|
|
10211
10586
|
).option("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").action(
|
|
10212
10587
|
async (options) => {
|
|
10213
10588
|
const source = resolveDeploySource(process.env);
|
|
10589
|
+
if (!validateMapSetupTarget(options, source)) return;
|
|
10214
10590
|
const resolveApi = () => requireApiOptions(options, {
|
|
10215
10591
|
allowStoredCredential: allowsStoredDeployCredential(source),
|
|
10216
10592
|
assertExplicitKeyMap: true,
|
|
@@ -10230,7 +10606,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10230
10606
|
);
|
|
10231
10607
|
if (result) {
|
|
10232
10608
|
logSuccess(
|
|
10233
|
-
`${result.changed ? "Published" : "Unchanged"} ${
|
|
10609
|
+
`${result.changed ? "Published" : "Unchanged"} ${pc9.bold(result.mapName)} web + native-v1 (v${result.version}).`
|
|
10234
10610
|
);
|
|
10235
10611
|
printKeyValue("Manifest", pathLabel(result.manifestPath));
|
|
10236
10612
|
printKeyValue("Native", link(result.nativeManifestUrl));
|
|
@@ -10325,6 +10701,21 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10325
10701
|
return;
|
|
10326
10702
|
}
|
|
10327
10703
|
const existingManifest = outputManifest;
|
|
10704
|
+
let createdMapId = null;
|
|
10705
|
+
if (!options.mapId) {
|
|
10706
|
+
const deployedMapId2 = existingManifest?.maps[mapNames[0]]?.mapId;
|
|
10707
|
+
if (deployedMapId2) {
|
|
10708
|
+
logError(`${pc9.bold(mapNames[0])} is already deployed to ${deployedMapId2}.`);
|
|
10709
|
+
printNextSteps([
|
|
10710
|
+
`Deploy it again with ${command(`tileflow deploy --map-id ${deployedMapId2}`)}.`
|
|
10711
|
+
]);
|
|
10712
|
+
process.exitCode = 1;
|
|
10713
|
+
return;
|
|
10714
|
+
}
|
|
10715
|
+
createdMapId = await createMapForDeploy(options, { defaultApiUrl, loadAuthConfig });
|
|
10716
|
+
if (!createdMapId) return;
|
|
10717
|
+
options.mapId = createdMapId;
|
|
10718
|
+
}
|
|
10328
10719
|
const api = await resolveApi();
|
|
10329
10720
|
if (!api) return;
|
|
10330
10721
|
const hostedSpriteByPackageHash = /* @__PURE__ */ new Map();
|
|
@@ -10344,7 +10735,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10344
10735
|
);
|
|
10345
10736
|
const action = uploaded.value.changed === false ? "Reused" : "Uploaded";
|
|
10346
10737
|
logSuccess(
|
|
10347
|
-
`${action} icon package ${
|
|
10738
|
+
`${action} icon package ${pc9.bold(binding?.label ?? "Icons")} (${plural(iconPackage.manifest.iconNames.length, "icon")}, ${formatBytes4(totalBytes)}, ${iconPackage.contentHash.slice(0, 12)}).`
|
|
10348
10739
|
);
|
|
10349
10740
|
hostedSpriteByPackageHash.set(iconPackage.contentHash, uploaded.value.spriteUrl);
|
|
10350
10741
|
}
|
|
@@ -10506,7 +10897,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10506
10897
|
}
|
|
10507
10898
|
const themeNames = Object.keys(themeStyles).sort();
|
|
10508
10899
|
logInfo(
|
|
10509
|
-
`Deploying compiled map ${
|
|
10900
|
+
`Deploying compiled map ${pc9.bold(mapName)} (${plural(themeNames.length, "theme")}).`
|
|
10510
10901
|
);
|
|
10511
10902
|
const response = await publishHostedStyle(
|
|
10512
10903
|
api,
|
|
@@ -10588,14 +10979,14 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10588
10979
|
};
|
|
10589
10980
|
const versionLabel = Number.isInteger(body.version) ? ` (v${body.version})` : "";
|
|
10590
10981
|
if (body.changed === false) {
|
|
10591
|
-
logSuccess(`Unchanged ${
|
|
10982
|
+
logSuccess(`Unchanged ${pc9.bold(mapName)}${versionLabel}.`);
|
|
10592
10983
|
} else {
|
|
10593
|
-
logSuccess(`Published ${
|
|
10984
|
+
logSuccess(`Published ${pc9.bold(mapName)}${versionLabel}.`);
|
|
10594
10985
|
}
|
|
10595
10986
|
for (const teamModel of body.teamModels ?? []) {
|
|
10596
10987
|
if (teamModel.sources !== "differ") continue;
|
|
10597
10988
|
logWarning(
|
|
10598
|
-
`3D model ${
|
|
10989
|
+
`3D model ${pc9.bold(teamModel.model)} draws its published version ${teamModel.version}, which was not prepared from this project's files. Run \`tileflow models publish\` to publish them.`
|
|
10599
10990
|
);
|
|
10600
10991
|
}
|
|
10601
10992
|
}
|
|
@@ -10612,6 +11003,7 @@ program.command("deploy").description("Deploy maps to Tileflow and write the fro
|
|
|
10612
11003
|
const deployedMapId = Object.values(deployedMaps)[0]?.mapId;
|
|
10613
11004
|
if (deployedMapId) {
|
|
10614
11005
|
printNextSteps([
|
|
11006
|
+
...createdMapId ? [`Deploy later changes with ${command(`tileflow deploy --map-id ${createdMapId}`)}.`] : [],
|
|
10615
11007
|
`Check hosted state with ${command(`tileflow status --map-id ${deployedMapId}`)}.`
|
|
10616
11008
|
]);
|
|
10617
11009
|
}
|
|
@@ -10712,6 +11104,7 @@ registerProjectCommands(program, {
|
|
|
10712
11104
|
defaultApiUrl,
|
|
10713
11105
|
loadAuthConfig
|
|
10714
11106
|
});
|
|
11107
|
+
registerMapCommands(program, { defaultApiUrl, loadAuthConfig });
|
|
10715
11108
|
if (isRootCliVersionRequest(process.argv.slice(2))) {
|
|
10716
11109
|
process.stdout.write(`${packageJson.version}
|
|
10717
11110
|
`);
|
|
@@ -10908,7 +11301,7 @@ async function runTileflowComparisonPreview(options, address) {
|
|
|
10908
11301
|
}),
|
|
10909
11302
|
sidecarUrl: `${rightBasePath}/__inspection/${rightPreview.mapName}/${rightPreview.themeName}.json`
|
|
10910
11303
|
},
|
|
10911
|
-
title: "Tileflow
|
|
11304
|
+
title: "Tileflow Inspector"
|
|
10912
11305
|
});
|
|
10913
11306
|
await new Promise((resolveListening, rejectListening) => {
|
|
10914
11307
|
const createdServer = serve(
|
|
@@ -10924,7 +11317,7 @@ async function runTileflowComparisonPreview(options, address) {
|
|
|
10924
11317
|
});
|
|
10925
11318
|
serverStarted = true;
|
|
10926
11319
|
if (!options.json) {
|
|
10927
|
-
logSuccess("Tileflow
|
|
11320
|
+
logSuccess("Tileflow Inspector is running and watching both configs.");
|
|
10928
11321
|
printKeyValue("Local", link(origin));
|
|
10929
11322
|
printKeyValue("Left", `${leftPreview.label} \xB7 ${pathLabel(options.config)}`);
|
|
10930
11323
|
printKeyValue("Right", `${rightPreview.label} \xB7 ${pathLabel(rightConfig)}`);
|
|
@@ -11003,17 +11396,17 @@ async function loginWithDeviceFlow(options) {
|
|
|
11003
11396
|
codeChallenge,
|
|
11004
11397
|
deviceName: hostname()
|
|
11005
11398
|
});
|
|
11006
|
-
printKeyValue("Code",
|
|
11399
|
+
printKeyValue("Code", pc9.bold(authorization.userCode));
|
|
11007
11400
|
printKeyValue("URL", link(authorization.verificationUriComplete));
|
|
11008
11401
|
if (options.noBrowser) {
|
|
11009
11402
|
logMuted("Open the URL above to continue.");
|
|
11010
11403
|
} else if (process.stdin.isTTY) {
|
|
11011
|
-
const readline =
|
|
11404
|
+
const readline = createInterface2({
|
|
11012
11405
|
input: process.stdin,
|
|
11013
11406
|
output: process.stdout
|
|
11014
11407
|
});
|
|
11015
11408
|
try {
|
|
11016
|
-
await readline.question(`${
|
|
11409
|
+
await readline.question(`${pc9.cyan("?")} Press Enter to open Tileflow Dashboard...`);
|
|
11017
11410
|
openBrowser(authorization.verificationUriComplete);
|
|
11018
11411
|
} finally {
|
|
11019
11412
|
readline.close();
|
|
@@ -11050,7 +11443,7 @@ function createPkceChallenge(verifier) {
|
|
|
11050
11443
|
return createHash4("sha256").update(verifier).digest("base64url");
|
|
11051
11444
|
}
|
|
11052
11445
|
function randomBase64Url(byteLength) {
|
|
11053
|
-
return
|
|
11446
|
+
return randomBytes3(byteLength).toString("base64url");
|
|
11054
11447
|
}
|
|
11055
11448
|
function openBrowser(url, errorsToStderr = false) {
|
|
11056
11449
|
openTileflowExternal(url, {
|
|
@@ -11102,11 +11495,6 @@ function createCompiledMapAssets(compiled, resolveSprite, modelAssets) {
|
|
|
11102
11495
|
};
|
|
11103
11496
|
}
|
|
11104
11497
|
function selectManagedDeployMaps(mapNames, options) {
|
|
11105
|
-
if (options.map && !options.mapId) {
|
|
11106
|
-
logError("--map requires --map-id.");
|
|
11107
|
-
process.exitCode = 1;
|
|
11108
|
-
return null;
|
|
11109
|
-
}
|
|
11110
11498
|
if (options.map) {
|
|
11111
11499
|
if (!mapNames.includes(options.map)) {
|
|
11112
11500
|
logError(`Unknown Tileflow map: ${options.map}.`);
|
|
@@ -11116,24 +11504,40 @@ function selectManagedDeployMaps(mapNames, options) {
|
|
|
11116
11504
|
}
|
|
11117
11505
|
return [options.map];
|
|
11118
11506
|
}
|
|
11119
|
-
if (!options.mapId) return mapNames;
|
|
11120
11507
|
if (mapNames.length === 1) return [mapNames[0]];
|
|
11121
11508
|
logError(
|
|
11122
11509
|
"This managed target requires an explicit map because the config contains multiple maps."
|
|
11123
11510
|
);
|
|
11124
11511
|
printKeyValue("Maps", mapNames.join(", "));
|
|
11125
11512
|
printNextSteps([
|
|
11126
|
-
`Retry with ${command(`tileflow deploy
|
|
11513
|
+
`Retry with ${command(`tileflow deploy ${options.mapId ? `--map-id ${options.mapId} ` : ""}--map ${mapNames[0]}`)}.`
|
|
11127
11514
|
]);
|
|
11128
11515
|
process.exitCode = 1;
|
|
11129
11516
|
return null;
|
|
11130
11517
|
}
|
|
11131
|
-
function
|
|
11132
|
-
|
|
11133
|
-
logError(
|
|
11518
|
+
function validateMapSetupTarget(options, source) {
|
|
11519
|
+
const fail = (message, steps = []) => {
|
|
11520
|
+
logError(message);
|
|
11521
|
+
printNextSteps(steps);
|
|
11134
11522
|
process.exitCode = 1;
|
|
11135
11523
|
return false;
|
|
11524
|
+
};
|
|
11525
|
+
if (options.mapId) {
|
|
11526
|
+
return hasMapSetupOptions(options) ? fail(
|
|
11527
|
+
"--team, --origin, --any-website and --setup-key apply only when deploy creates a Map."
|
|
11528
|
+
) : true;
|
|
11529
|
+
}
|
|
11530
|
+
const createFirst = `Create the Map with ${command("tileflow maps create")} after ${command("tileflow login")}, then pass --map-id.`;
|
|
11531
|
+
if (options.withNative) return fail("--with-native requires one valid --map-id.", [createFirst]);
|
|
11532
|
+
if (options.apiKey || !allowsStoredDeployCredential(source)) {
|
|
11533
|
+
return fail("Managed deploy with a Tileflow credential or in CI requires --map-id.", [
|
|
11534
|
+
createFirst
|
|
11535
|
+
]);
|
|
11136
11536
|
}
|
|
11537
|
+
const checked = checkMapSetupOptions(options);
|
|
11538
|
+
return checked.ok ? true : fail(checked.error);
|
|
11539
|
+
}
|
|
11540
|
+
function validateDeployTarget(options) {
|
|
11137
11541
|
if (options.mapId && !/^map_[A-Za-z0-9_-]{16}$/u.test(options.mapId)) {
|
|
11138
11542
|
logError("Managed Map ID is invalid.");
|
|
11139
11543
|
process.exitCode = 1;
|
|
@@ -11214,7 +11618,7 @@ async function requireApiOptions(options, behavior = {}) {
|
|
|
11214
11618
|
if (!behavior.silent) {
|
|
11215
11619
|
logError("CI requires an explicit Tileflow credential with access to the target Map.");
|
|
11216
11620
|
printNextSteps([
|
|
11217
|
-
`Set ${
|
|
11621
|
+
`Set ${pc9.cyan("TILEFLOW_API_KEY")} from the CI secret store.`,
|
|
11218
11622
|
"The saved personal account session is never used in CI."
|
|
11219
11623
|
]);
|
|
11220
11624
|
}
|
|
@@ -11283,17 +11687,17 @@ function printCliError(error) {
|
|
|
11283
11687
|
}
|
|
11284
11688
|
const message = error instanceof Error ? error.message : "Command failed.";
|
|
11285
11689
|
if (message.includes("Cannot find module '@tileflow/core'")) {
|
|
11286
|
-
logError(`Cannot load ${
|
|
11690
|
+
logError(`Cannot load ${pc9.cyan("@tileflow/core")} from this project.`);
|
|
11287
11691
|
printNextSteps([
|
|
11288
|
-
`Install ${
|
|
11692
|
+
`Install ${pc9.cyan("@tileflow/core")} in this project.`,
|
|
11289
11693
|
`Run the command again from the project root.`
|
|
11290
11694
|
]);
|
|
11291
11695
|
return;
|
|
11292
11696
|
}
|
|
11293
11697
|
if (message.includes("Cannot find module '@tileflow/maps'")) {
|
|
11294
|
-
logError(`Cannot load ${
|
|
11698
|
+
logError(`Cannot load ${pc9.cyan("@tileflow/maps")} from this project.`);
|
|
11295
11699
|
printNextSteps([
|
|
11296
|
-
`Install ${
|
|
11700
|
+
`Install ${pc9.cyan("@tileflow/maps")} in this project.`,
|
|
11297
11701
|
`Run the command again from the project root.`
|
|
11298
11702
|
]);
|
|
11299
11703
|
return;
|
|
@@ -11304,51 +11708,51 @@ function printCliError(error) {
|
|
|
11304
11708
|
}
|
|
11305
11709
|
}
|
|
11306
11710
|
function logSuccess(message) {
|
|
11307
|
-
console.log(`${
|
|
11711
|
+
console.log(`${pc9.green("\u2713")} ${message}`);
|
|
11308
11712
|
}
|
|
11309
11713
|
function logError(message) {
|
|
11310
|
-
console.log(`${
|
|
11714
|
+
console.log(`${pc9.red("\u2715")} ${message}`);
|
|
11311
11715
|
}
|
|
11312
11716
|
function logWarning(message) {
|
|
11313
|
-
console.log(`${
|
|
11717
|
+
console.log(`${pc9.yellow("!")} ${message}`);
|
|
11314
11718
|
}
|
|
11315
11719
|
function logInfo(message) {
|
|
11316
|
-
console.log(`${
|
|
11720
|
+
console.log(`${pc9.cyan("i")} ${message}`);
|
|
11317
11721
|
}
|
|
11318
11722
|
function logMuted(message) {
|
|
11319
|
-
console.log(
|
|
11723
|
+
console.log(pc9.gray(message));
|
|
11320
11724
|
}
|
|
11321
11725
|
function printTitle(title, subtitle) {
|
|
11322
|
-
console.log(
|
|
11726
|
+
console.log(pc9.bold(title));
|
|
11323
11727
|
if (subtitle) {
|
|
11324
|
-
console.log(
|
|
11728
|
+
console.log(pc9.gray(subtitle));
|
|
11325
11729
|
}
|
|
11326
11730
|
}
|
|
11327
11731
|
function printKeyValue(label, value) {
|
|
11328
|
-
console.log(`${
|
|
11732
|
+
console.log(`${pc9.gray(`${label}:`.padEnd(13))} ${value}`);
|
|
11329
11733
|
}
|
|
11330
11734
|
function printChecks(items) {
|
|
11331
11735
|
for (const item of items) {
|
|
11332
|
-
console.log(` ${
|
|
11736
|
+
console.log(` ${pc9.green("\u2713")} ${item}`);
|
|
11333
11737
|
}
|
|
11334
11738
|
}
|
|
11335
11739
|
function printNextSteps(steps) {
|
|
11336
11740
|
if (steps.length === 0) return;
|
|
11337
11741
|
console.log(`
|
|
11338
|
-
${
|
|
11742
|
+
${pc9.bold("Next steps")}`);
|
|
11339
11743
|
for (const step of steps) {
|
|
11340
|
-
console.log(` ${
|
|
11744
|
+
console.log(` ${pc9.cyan("\u2022")} ${step}`);
|
|
11341
11745
|
}
|
|
11342
11746
|
}
|
|
11343
11747
|
function printValidationErrors(messages) {
|
|
11344
11748
|
logError("Tileflow config has errors.");
|
|
11345
11749
|
for (const message of messages) {
|
|
11346
|
-
const path = message.path ?
|
|
11347
|
-
console.log(` ${
|
|
11750
|
+
const path = message.path ? pc9.cyan(message.path) : pc9.cyan("(root)");
|
|
11751
|
+
console.log(` ${pc9.red("\u2715")} ${path} ${pc9.gray("-")} ${message.message}`);
|
|
11348
11752
|
}
|
|
11349
11753
|
}
|
|
11350
11754
|
function printAccountDetails(session) {
|
|
11351
|
-
printKeyValue("Account",
|
|
11755
|
+
printKeyValue("Account", pc9.bold(session.account.name));
|
|
11352
11756
|
printKeyValue("Email", session.account.email);
|
|
11353
11757
|
printKeyValue("API", link(session.apiOrigin));
|
|
11354
11758
|
printKeyValue("Expires", formatDate(session.expiresAt));
|
|
@@ -11377,11 +11781,11 @@ function printDeployedMaps(maps) {
|
|
|
11377
11781
|
const entries = Object.entries(maps);
|
|
11378
11782
|
if (entries.length === 0) return;
|
|
11379
11783
|
console.log(`
|
|
11380
|
-
${
|
|
11784
|
+
${pc9.bold("Maps")}`);
|
|
11381
11785
|
for (const [name, map] of entries) {
|
|
11382
11786
|
for (const [themeName, theme] of Object.entries(map.themes)) {
|
|
11383
11787
|
const label = `${name}/${themeName}`;
|
|
11384
|
-
console.log(` ${
|
|
11788
|
+
console.log(` ${pc9.green("\u2713")} ${label.padEnd(24)} ${link(theme.styleUrl)}`);
|
|
11385
11789
|
}
|
|
11386
11790
|
}
|
|
11387
11791
|
}
|
|
@@ -11399,13 +11803,13 @@ function plural(count, singular, pluralForm = `${singular}s`) {
|
|
|
11399
11803
|
return `${count} ${count === 1 ? singular : pluralForm}`;
|
|
11400
11804
|
}
|
|
11401
11805
|
function command(value) {
|
|
11402
|
-
return
|
|
11806
|
+
return pc9.cyan(value);
|
|
11403
11807
|
}
|
|
11404
11808
|
function link(value) {
|
|
11405
|
-
return
|
|
11809
|
+
return pc9.cyan(value);
|
|
11406
11810
|
}
|
|
11407
11811
|
function pathLabel(value) {
|
|
11408
|
-
return
|
|
11812
|
+
return pc9.cyan(value);
|
|
11409
11813
|
}
|
|
11410
11814
|
function starterConfig() {
|
|
11411
11815
|
return `import { defineMap, defineTheme, labels, poi } from "@tileflow/core";
|
|
@@ -11451,15 +11855,15 @@ function printHostedCompatibilityIssues(issues) {
|
|
|
11451
11855
|
}
|
|
11452
11856
|
function printMapStatus(status, apiUrl) {
|
|
11453
11857
|
printTitle("Tileflow status");
|
|
11454
|
-
printKeyValue("Map",
|
|
11858
|
+
printKeyValue("Map", pc9.bold(status.mapId));
|
|
11455
11859
|
console.log(`
|
|
11456
|
-
${
|
|
11860
|
+
${pc9.bold("Styles")}`);
|
|
11457
11861
|
if (status.styles.length === 0) {
|
|
11458
11862
|
logMuted(" No styles deployed.");
|
|
11459
11863
|
}
|
|
11460
11864
|
for (const style of status.styles) {
|
|
11461
11865
|
console.log(
|
|
11462
|
-
` ${
|
|
11866
|
+
` ${pc9.green("\u2713")} ${style.environment.padEnd(16)} ${link(`${apiUrl}/maps/${style.mapId}/style.json`).padEnd(36)} ${formatBytes4(style.size).padStart(10)} ${pc9.gray(formatDate(style.uploaded))}`
|
|
11463
11867
|
);
|
|
11464
11868
|
}
|
|
11465
11869
|
}
|