tileflow 0.1.0-alpha.19 → 0.1.0-alpha.20

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.
Files changed (3) hide show
  1. package/README.md +32 -0
  2. package/dist/index.js +273 -67
  3. package/package.json +6 -6
package/README.md CHANGED
@@ -211,6 +211,30 @@ for CI, provide an appropriately scoped key through the secret environment varia
211
211
  `TILEFLOW_API_KEY`. CI does not fall back to a saved personal login. Never put a key in a config,
212
212
  browser bundle, copied prompt, or command-line argument.
213
213
 
214
+ If `login` finds the older local file containing an API key and API URL, it replaces that file only
215
+ after a new account login succeeds. The old API key is not converted into an account session.
216
+ Unrecognized local files are left untouched and stop login before authorization begins.
217
+
218
+ For commands accepting an API key, `--api-key` takes precedence over `TILEFLOW_API_KEY`;
219
+ only an absent key allows a saved personal session. An empty, invalid, expired or revoked key
220
+ fails without switching identities. `--api-url` takes precedence over `TILEFLOW_API_URL`.
221
+ This selection also applies to `models`, `tileset`, Icon Set commands and Map status. Keys
222
+ never appear in help output. When a personal account can manage several Teams, pass an
223
+ explicit `--team @team`; a Team key already selects its Team.
224
+
225
+ `login` always authorizes a personal session through the dashboard, even when an API key is
226
+ configured. `--no-browser` prints the URL without prompting to open it. Polling retries temporary
227
+ connection/server failures with bounded backoff and honors rate-limit waits until the authorization
228
+ expires. Denied or invalid authorizations stop immediately. A new session is validated and saved
229
+ before the previous session for that API origin is revoked. Failed validation or persistence
230
+ attempts to revoke the newly issued session and preserves the saved login. If cleanup cannot
231
+ reach the server, the CLI explains that the session must be revoked in Dashboard settings.
232
+
233
+ `logout` revokes only the saved personal session for the selected API origin. If server revocation
234
+ fails, local access is preserved so logout can be retried. API keys remain independent: an API key
235
+ in the environment still authorizes resource commands after logout. Personal session tokens stay
236
+ in the local auth file with restricted permissions; this CLI does not use an OS credential store.
237
+
214
238
  ```sh
215
239
  npx tileflow login
216
240
  npx tileflow whoami --json
@@ -226,6 +250,11 @@ with `--origin`, or `--any-website`. It prints the Map ID to use for later deplo
226
250
  credential or the portable `id` in your config. CI always deploys an existing Map with a deploy key
227
251
  for that destination. `logout` removes the saved personal session.
228
252
 
253
+ Deploy sends compact web cartography and data requirements, retaining concrete native artifacts.
254
+ The web budget is 2,000,000 UTF-8 bytes for the whole family; icon receipts and native payloads
255
+ have separate bounds.
256
+ Oversized maps fail before uploads with their size and limit; no automatic simplification occurs.
257
+
229
258
  Deploy publishes the compiled concrete-theme family and managed icon assets; it does not ask the
230
259
  server to compile TypeScript. Repeating an unchanged deployment reuses its version. Build writers
231
260
  refuse to overwrite hosted manifests by default, and deploy refuses to replace a self-hosted
@@ -245,6 +274,9 @@ and uploads a managed dataset using Team data authority, not a map-only deploy k
245
274
  uploads are resumable; a completed version becomes current at stable delivery URLs. Purge is
246
275
  explicit, requires confirmation, and can be blocked by retained deployment dependencies.
247
276
 
277
+ Administratively disabled uploads return `TF_TILESET_UPLOAD_DISABLED` without retrying.
278
+ Temporary service failures retain bounded retries.
279
+
248
280
  `inspect features` samples bounded vector-source features. `icons list` and `icons diff` inspect
249
281
  prepared icon identities; they do not turn package directories into a separate hosted library.
250
282
  Use the installed command's help for exact arguments and supported JSON output.
package/dist/index.js CHANGED
@@ -18,6 +18,7 @@ import {
18
18
  tileflowMapIdSchema as tileflowMapIdSchema2
19
19
  } from "@tileflow/core";
20
20
  import {
21
+ compactTileflowDeploymentArtifact as compactTileflowDeploymentArtifact3,
21
22
  createTileflowMapBuildManifest as createTileflowMapBuildManifest2
22
23
  } from "@tileflow/core/build";
23
24
  import {
@@ -101,6 +102,17 @@ var authConfigSchema = z.object({
101
102
  }
102
103
  }
103
104
  });
105
+ var legacyAuthConfigSchema = z.object({
106
+ apiKey: z.string().min(1).max(4096),
107
+ apiUrl: z.url().max(2048).refine((value) => {
108
+ try {
109
+ normalizeApiOrigin(value);
110
+ return true;
111
+ } catch {
112
+ return false;
113
+ }
114
+ })
115
+ }).strict();
104
116
  function emptyAuthConfig() {
105
117
  return Object.freeze({ sessions: Object.freeze({}), version: 2 });
106
118
  }
@@ -119,12 +131,35 @@ async function readAuthFile(path = authConfigPath()) {
119
131
  return { kind: "invalid" };
120
132
  }
121
133
  const config = authConfigSchema.safeParse(parsed);
122
- return config.success ? { config: freezeConfig(config.data), kind: "sessions" } : { kind: "invalid" };
134
+ if (config.success) return { config: freezeConfig(config.data), kind: "sessions" };
135
+ return legacyAuthConfigSchema.safeParse(parsed).success ? { kind: "legacy" } : { kind: "invalid" };
136
+ }
137
+ async function prepareAccountLogin(path = authConfigPath()) {
138
+ const state = await readAuthFile(path);
139
+ if (state.kind === "invalid") {
140
+ throw new Error("Tileflow auth state is invalid; no changes were made.");
141
+ }
142
+ const directory = dirname(path);
143
+ const probePath = `${path}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`;
144
+ await mkdir(directory, { mode: 448, recursive: true });
145
+ const handle = await open(probePath, "wx", 384);
146
+ try {
147
+ await handle.sync();
148
+ } finally {
149
+ await handle.close();
150
+ await unlink(probePath);
151
+ }
152
+ return state;
123
153
  }
124
154
  async function loadAuthConfig(path = authConfigPath()) {
125
155
  const state = await readAuthFile(path);
126
156
  if (state.kind === "empty") return emptyAuthConfig();
127
157
  if (state.kind === "sessions") return state.config;
158
+ if (state.kind === "legacy") {
159
+ throw new Error(
160
+ "Tileflow CLI login uses an older format. Run tileflow login to sign in again."
161
+ );
162
+ }
128
163
  throw new Error("Tileflow auth state is invalid; no changes were made.");
129
164
  }
130
165
  async function installAccountSession(session, path = authConfigPath()) {
@@ -4129,6 +4164,7 @@ function printFeatureInspectError(error) {
4129
4164
 
4130
4165
  // src/hosted-client.ts
4131
4166
  import { serializeTileflowFontBundleManifest } from "@tileflow/core";
4167
+ import { compactTileflowDeploymentArtifact, tileflowDeploymentWebBytes } from "@tileflow/core/build";
4132
4168
 
4133
4169
  // src/hosted-response.ts
4134
4170
  import { z as z2 } from "zod";
@@ -4145,6 +4181,7 @@ var hostedStyleDeploymentResponseSchema = z2.object({
4145
4181
  changed: z2.boolean().optional(),
4146
4182
  deploymentId: safeIdentifierSchema.optional(),
4147
4183
  mapId: z2.string().regex(/^map_[A-Za-z0-9_-]{16}$/u),
4184
+ familyUrl: publicHttpUrlSchema.optional(),
4148
4185
  themes: z2.record(
4149
4186
  safeIdentifierSchema,
4150
4187
  z2.object({
@@ -4238,17 +4275,23 @@ function apiProfileCanTargetMap(profile, mapId) {
4238
4275
  }
4239
4276
  async function requestHostedJson(apiUrl, path, init = {}, options = {}) {
4240
4277
  return withHostedResponse(apiUrl, path, init, options, async (response) => {
4278
+ const retryAfter = response.headers.get("retry-after");
4279
+ const retryAfterMs = retryAfter === null ? void 0 : /^\d+$/u.test(retryAfter) ? Number(retryAfter) * 1e3 : Math.max(0, Date.parse(retryAfter) - Date.now());
4280
+ const metadata = {
4281
+ ok: response.ok,
4282
+ status: response.status,
4283
+ ...Number.isFinite(retryAfterMs) ? { retryAfterMs } : {}
4284
+ };
4241
4285
  const source = await readBoundedResponseText(response, "Hosted API");
4242
- if (!source) return { body: null, json: true, ok: response.ok, status: response.status };
4286
+ if (!source) return { body: null, json: true, ...metadata };
4243
4287
  try {
4244
4288
  return {
4245
4289
  body: JSON.parse(source),
4246
4290
  json: true,
4247
- ok: response.ok,
4248
- status: response.status
4291
+ ...metadata
4249
4292
  };
4250
4293
  } catch {
4251
- return { body: null, json: false, ok: response.ok, status: response.status };
4294
+ return { body: null, json: false, ...metadata };
4252
4295
  }
4253
4296
  });
4254
4297
  }
@@ -4272,24 +4315,57 @@ async function startDeviceAuthorization(appUrl, body, options = {}) {
4272
4315
  return response.body;
4273
4316
  }
4274
4317
  async function pollDeviceToken(appUrl, authorization, input, options = {}) {
4275
- const expiresAt = Date.now() + authorization.expiresIn * 1e3;
4276
- const intervalMs = Math.max(authorization.interval, 1) * 1e3;
4318
+ const now = input.now ?? Date.now;
4319
+ const expiresAt = input.expiresAt ?? now() + authorization.expiresIn * 1e3;
4320
+ let intervalMs = Math.max(authorization.interval, 1) * 1e3;
4321
+ let delayMs = intervalMs;
4277
4322
  const wait = input.sleep ?? ((milliseconds) => new Promise((resolve13) => setTimeout(resolve13, milliseconds)));
4278
- while (Date.now() < expiresAt) {
4279
- await wait(intervalMs);
4280
- const response = await requestHostedJson(
4281
- appUrl,
4282
- "/api/cli/device/token",
4283
- jsonRequest("POST", {
4284
- codeVerifier: input.codeVerifier,
4285
- deviceCode: authorization.deviceCode
4286
- }),
4287
- options
4288
- );
4323
+ while (now() < expiresAt) {
4324
+ options.signal?.throwIfAborted();
4325
+ await wait(Math.min(delayMs, expiresAt - now()));
4326
+ options.signal?.throwIfAborted();
4327
+ if (now() >= expiresAt) break;
4328
+ let response;
4329
+ try {
4330
+ response = await requestHostedJson(
4331
+ appUrl,
4332
+ "/api/cli/device/token",
4333
+ jsonRequest("POST", {
4334
+ codeVerifier: input.codeVerifier,
4335
+ deviceCode: authorization.deviceCode
4336
+ }),
4337
+ {
4338
+ ...options,
4339
+ timeoutMs: Math.min(
4340
+ options.timeoutMs ?? defaultHostedRequestTimeoutMs,
4341
+ expiresAt - now()
4342
+ )
4343
+ }
4344
+ );
4345
+ } catch (error2) {
4346
+ options.signal?.throwIfAborted();
4347
+ if (!(error2 instanceof TypeError) && !(error2 instanceof Error && error2.name === "TimeoutError"))
4348
+ throw error2;
4349
+ delayMs = Math.min(Math.max(delayMs * 2, intervalMs), 6e4);
4350
+ continue;
4351
+ }
4289
4352
  if (response.ok && isDeviceToken(response.body)) return response.body;
4290
4353
  const error = asRecord(response.body).error;
4291
- if (error === "authorization_pending") continue;
4292
4354
  if (error === "access_denied") throw new Error("CLI authorization was denied.");
4355
+ if (error === "expired_token") break;
4356
+ if (error === "slow_down" || response.status === 429) {
4357
+ intervalMs += 5e3;
4358
+ delayMs = Math.max(intervalMs, response.retryAfterMs ?? 0);
4359
+ continue;
4360
+ }
4361
+ if (error === "authorization_pending") {
4362
+ delayMs = intervalMs;
4363
+ continue;
4364
+ }
4365
+ if ([408, 500, 502, 503, 504].includes(response.status)) {
4366
+ delayMs = Math.max(Math.min(delayMs * 2, 6e4), response.retryAfterMs ?? 0);
4367
+ continue;
4368
+ }
4293
4369
  throw new Error(`CLI authorization failed (${response.status}).`);
4294
4370
  }
4295
4371
  throw new Error("CLI authorization expired. Run `tileflow login` again.");
@@ -4530,11 +4606,14 @@ function isMapSetupTeam(value) {
4530
4606
  );
4531
4607
  }
4532
4608
  async function publishHostedStyle(api, body, responseLabel, options = {}) {
4609
+ const input = body;
4610
+ const compactBody = { ...input, artifact: compactTileflowDeploymentArtifact(input.artifact) };
4611
+ assertHostedStyleBudget(compactBody);
4533
4612
  const response = await requestHostedJson(
4534
4613
  api.apiUrl,
4535
4614
  "/v1/styles",
4536
4615
  {
4537
- ...jsonRequest("POST", body),
4616
+ ...jsonRequest("POST", compactBody),
4538
4617
  headers: { ...authorizationHeaders(api.apiKey, api.mapId), "Content-Type": "application/json" }
4539
4618
  },
4540
4619
  options
@@ -4545,6 +4624,15 @@ async function publishHostedStyle(api, body, responseLabel, options = {}) {
4545
4624
  value: parseResponse(response, hostedStyleDeploymentResponseSchema, responseLabel)
4546
4625
  };
4547
4626
  }
4627
+ function assertHostedStyleBudget(body) {
4628
+ const observedBytes = tileflowDeploymentWebBytes(body);
4629
+ const limitBytes = 2e6;
4630
+ if (observedBytes > limitBytes)
4631
+ throw Object.assign(
4632
+ new Error(`Style content is ${observedBytes} bytes; the limit is ${limitBytes} bytes.`),
4633
+ { code: "STYLE_DEPLOYMENT_TOO_LARGE", scope: "web", observedBytes, limitBytes }
4634
+ );
4635
+ }
4548
4636
  async function uploadHostedIconPackage(api, iconPackage, options = {}) {
4549
4637
  const formData = new FormData();
4550
4638
  const fieldNames = {
@@ -4792,6 +4880,7 @@ import {
4792
4880
  serializeCanonicalJson as serializeCanonicalJson3
4793
4881
  } from "@tileflow/core";
4794
4882
  import {
4883
+ compactTileflowDeploymentArtifact as compactTileflowDeploymentArtifact2,
4795
4884
  createTileflowMapBuildManifest,
4796
4885
  parseTileflowRendererDeploymentArtifact,
4797
4886
  tileflowRendererDeploymentResponseSchema
@@ -4978,7 +5067,11 @@ async function prepareNativeHostedDeploy(options) {
4978
5067
  ])
4979
5068
  )
4980
5069
  };
4981
- const styles = createTileflowStyles(project, { apiBaseUrl: apiOrigin, mapAssets });
5070
+ const styles = createTileflowStyles(project, {
5071
+ apiBaseUrl: apiOrigin,
5072
+ mapAssets,
5073
+ runtimeColors: true
5074
+ });
4982
5075
  const local = await prepareTileflowLocalTilesets(project, styles, {
4983
5076
  assetBaseUrl: apiOrigin,
4984
5077
  baseDirectory,
@@ -5074,6 +5167,21 @@ async function runHostedNativeDeploy(options, ports) {
5074
5167
  const preflight = await parseTileflowRendererDeploymentArtifact(prepared.artifact);
5075
5168
  if (preflight.mapId !== prepared.mapName)
5076
5169
  throw failure("The prepared map does not match its artifact.");
5170
+ const requestBody = (artifact2) => ({
5171
+ artifact: compactTileflowDeploymentArtifact2(artifact2),
5172
+ environment: prepared.mapName,
5173
+ managedMapId: mapId,
5174
+ usageMode: "session",
5175
+ source: ports.source,
5176
+ ...prepared.iconBinding ? {
5177
+ iconPackage: {
5178
+ contentHash: prepared.iconBinding.packageHash,
5179
+ label: prepared.iconBinding.label
5180
+ }
5181
+ } : {},
5182
+ ...prepared.iconComposition ? { iconComposition: prepared.iconComposition } : {}
5183
+ });
5184
+ assertHostedStyleBudget(requestBody(preflight));
5077
5185
  const existing = await ports.loadManifest(options.manifest, {
5078
5186
  overwriteSelfHosted: options.overwriteSelfHostedManifest === true
5079
5187
  });
@@ -5109,6 +5217,8 @@ async function runHostedNativeDeploy(options, ports) {
5109
5217
  if (prepared.iconComposition && prepared.iconComposition.packageHash !== iconPackage?.contentHash)
5110
5218
  throw failure("Icon composition does not match the confirmed effective package.");
5111
5219
  let response;
5220
+ const body = requestBody(artifact);
5221
+ assertHostedStyleBudget(body);
5112
5222
  try {
5113
5223
  response = await requestHostedJson(
5114
5224
  apiOrigin,
@@ -5120,20 +5230,7 @@ async function runHostedNativeDeploy(options, ports) {
5120
5230
  "X-Tileflow-Map-Id": mapId,
5121
5231
  "Content-Type": "application/json"
5122
5232
  },
5123
- body: serializeCanonicalJson3({
5124
- artifact,
5125
- environment: prepared.mapName,
5126
- managedMapId: mapId,
5127
- usageMode: "session",
5128
- source: ports.source,
5129
- ...iconPackage && prepared.iconBinding ? {
5130
- iconPackage: {
5131
- contentHash: iconPackage.contentHash,
5132
- label: prepared.iconBinding.label
5133
- }
5134
- } : {},
5135
- ...prepared.iconComposition ? { iconComposition: prepared.iconComposition } : {}
5136
- })
5233
+ body: serializeCanonicalJson3(body)
5137
5234
  },
5138
5235
  ports.request
5139
5236
  );
@@ -5161,13 +5258,14 @@ async function runHostedNativeDeploy(options, ports) {
5161
5258
  const build = artifact.renderers.web.buildManifest.maps[artifact.mapId];
5162
5259
  const manifest = parseTileflowRuntimeManifest({
5163
5260
  apiUrl: apiOrigin,
5164
- version: build.timeOfDay || Object.values(existing?.maps ?? {}).some((map) => map.timeOfDay) ? 2 : 1,
5261
+ version: result.renderers.web.familyUrl || Object.values(existing?.maps ?? {}).some((map) => map.familyUrl) ? 3 : build.timeOfDay || Object.values(existing?.maps ?? {}).some((map) => map.timeOfDay) ? 2 : 1,
5165
5262
  maps: {
5166
5263
  ...existing?.maps ?? {},
5167
5264
  [prepared.mapName]: {
5168
5265
  defaultTheme: build.defaultTheme,
5169
5266
  environment: prepared.mapName,
5170
5267
  mapId,
5268
+ ...result.renderers.web.familyUrl ? { familyUrl: result.renderers.web.familyUrl } : {},
5171
5269
  ...build.systemThemes ? { systemThemes: build.systemThemes } : {},
5172
5270
  ...build.timeOfDay ? { timeOfDay: build.timeOfDay } : {},
5173
5271
  ...artifact.view ? { view: artifact.view } : {},
@@ -5204,6 +5302,21 @@ async function runHostedNativeDeploy(options, ports) {
5204
5302
  }
5205
5303
  }
5206
5304
 
5305
+ // src/hosted-options.ts
5306
+ import { Option } from "commander";
5307
+ function hostedApiKeyOption(description = "Tileflow API key") {
5308
+ return new Option("--api-key <key>", description).env("TILEFLOW_API_KEY");
5309
+ }
5310
+ function hostedApiUrlOption() {
5311
+ return new Option("--api-url <url>", "Tileflow API URL").env("TILEFLOW_API_URL");
5312
+ }
5313
+ function isHostedApiKey(value) {
5314
+ return /^tf_live_[0-9a-f]{48}$/u.test(value);
5315
+ }
5316
+ function allowsStoredAccountSession(env = process.env) {
5317
+ return !env.CI && env.GITHUB_ACTIONS !== "true" && env.GITLAB_CI !== "true";
5318
+ }
5319
+
5207
5320
  // src/icon-diff-command.ts
5208
5321
  import { dirname as dirname8, resolve as resolve7 } from "path";
5209
5322
  import { z as z3 } from "zod";
@@ -5777,7 +5890,7 @@ var tileflowIconDiffDocumentSchema = z3.object({
5777
5890
  hasChanges: z3.boolean()
5778
5891
  }).strict();
5779
5892
  function registerIconDiffCommand(icons, dependencies) {
5780
- 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("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").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) => {
5893
+ 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).addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Tileflow API key")).requiredOption("--map-id <id>", "managed Map destination").option("--json", "print deterministic schema-version-1 JSON").option("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").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) => {
5781
5894
  try {
5782
5895
  await runIconDiff(options, dependencies);
5783
5896
  } catch (error) {
@@ -6134,8 +6247,8 @@ async function resolveTeamAuthority(options, scopes, dependencies) {
6134
6247
  emitFailure(options.json, "invalid_api_url", safeMessage(error));
6135
6248
  return null;
6136
6249
  }
6137
- if (options.apiKey) {
6138
- if (!/^tf_live_[0-9a-f]{48}$/u.test(options.apiKey)) {
6250
+ if (options.apiKey !== void 0) {
6251
+ if (!isHostedApiKey(options.apiKey)) {
6139
6252
  emitFailure(options.json, "invalid_team_data_key", "Team data API key is invalid.");
6140
6253
  return null;
6141
6254
  }
@@ -6149,6 +6262,14 @@ async function resolveTeamAuthority(options, scopes, dependencies) {
6149
6262
  }
6150
6263
  return { apiOrigin, credential: options.apiKey, team: null };
6151
6264
  }
6265
+ if (!allowsStoredAccountSession()) {
6266
+ emitFailure(
6267
+ options.json,
6268
+ "api_key_required",
6269
+ "CI requires TILEFLOW_API_KEY or --api-key. Saved personal logins are not used in CI."
6270
+ );
6271
+ return null;
6272
+ }
6152
6273
  let config;
6153
6274
  try {
6154
6275
  config = await dependencies.loadAuthConfig();
@@ -6589,7 +6710,7 @@ function registerIconSetCommands(program2, dependencies) {
6589
6710
  }
6590
6711
  }
6591
6712
  function withTeamOptions(command2) {
6592
- return command2.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");
6713
+ return command2.option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON");
6593
6714
  }
6594
6715
  async function readAllPages(authority, path, query, schema, select, identify) {
6595
6716
  const items = [];
@@ -6949,7 +7070,7 @@ function registerIconLockCommands(program2, dependencies) {
6949
7070
  }
6950
7071
  }
6951
7072
  function withLockOptions(command2, dependencies) {
6952
- return command2.option("-c, --config <path>", "config path", dependencies.defaultConfigPath).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");
7073
+ return command2.option("-c, --config <path>", "config path", dependencies.defaultConfigPath).option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON");
6953
7074
  }
6954
7075
  function collectDeclaredReferences(project) {
6955
7076
  const references = [];
@@ -7532,7 +7653,7 @@ function registerModelCommands(program2, dependencies) {
7532
7653
  });
7533
7654
  }
7534
7655
  function withTeamOptions2(command2) {
7535
- return command2.option("--team <team>", "Team, as @team, when the account has several").option("--api-key <key>", "Team data API key with the 3D models permission").option("--api-url <url>", "Tileflow API origin").option("--json", "machine-readable output");
7656
+ return command2.option("--team <team>", "Team, as @team, when the account has several").addOption(hostedApiKeyOption("Team data API key with the 3D models permission")).addOption(hostedApiUrlOption()).option("--json", "machine-readable output");
7536
7657
  }
7537
7658
  async function readDetail(authority, name, json2) {
7538
7659
  const response = await authorizedTeamRequest(
@@ -8439,7 +8560,9 @@ function createHostedTeamTilesetUploadTransport(input) {
8439
8560
  }
8440
8561
  }
8441
8562
  if (response?.ok) return response.body;
8442
- if (response && !isRetryableStatus(response.status)) throw responseError(response);
8563
+ if (response && (asRecord3(response.body).code === "TF_TILESET_UPLOAD_DISABLED" || !isRetryableStatus(response.status))) {
8564
+ throw responseError(response);
8565
+ }
8443
8566
  if (attempt === maximumControlAttempts) {
8444
8567
  if (response) throw responseError(response);
8445
8568
  break;
@@ -8707,6 +8830,7 @@ function invalidRemoteResponse() {
8707
8830
  );
8708
8831
  }
8709
8832
  function failureMessage(code) {
8833
+ if (code === "TF_TILESET_UPLOAD_DISABLED") return "New tileset uploads are disabled.";
8710
8834
  if (code === "TF_TILESET_SCHEMA_INCOMPATIBLE") {
8711
8835
  return "Tileset schema is incompatible with retained Map deployments.";
8712
8836
  }
@@ -8863,7 +8987,7 @@ function registerTilesetInspectCommand(parent, name = "inspect") {
8863
8987
  function registerTilesetCommands(program2, dependencies) {
8864
8988
  const tileset = program2.command("tileset").description("Inspect and manage geospatial tilesets");
8865
8989
  registerTilesetInspectCommand(tileset);
8866
- 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(
8990
+ tileset.command("publish").description("Register and publish one PMTiles archive to a Team").argument("<archive>", "repository-local PMTiles archive").requiredOption("--id <id>", "Team-local logical tileset ID").option("--name <name>", "display name").option("--attribution <text>", "data attribution").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-2 JSON").action(
8867
8991
  async (archive, options) => {
8868
8992
  let uploadFile = null;
8869
8993
  try {
@@ -8954,7 +9078,7 @@ function registerTilesetCommands(program2, dependencies) {
8954
9078
  }
8955
9079
  }
8956
9080
  );
8957
- 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) => {
9081
+ tileset.command("list").description("List tilesets owned by one Team").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
8958
9082
  try {
8959
9083
  const authority = await resolveTeamAuthority(options, ["tilesets:read"], dependencies);
8960
9084
  if (!authority) return;
@@ -8969,7 +9093,7 @@ function registerTilesetCommands(program2, dependencies) {
8969
9093
  emitFailure(options.json, "tileset_list_failed", "Tileset list failed.");
8970
9094
  }
8971
9095
  });
8972
- 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) => {
9096
+ 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").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
8973
9097
  try {
8974
9098
  if (!isTilesetId(id))
8975
9099
  return emitFailure(options.json, "invalid_tileset_id", "Tileset ID is invalid.");
@@ -8998,7 +9122,7 @@ function registerTilesetCommands(program2, dependencies) {
8998
9122
  emitFailure(options.json, "tileset_status_failed", "Tileset status failed.");
8999
9123
  }
9000
9124
  });
9001
- 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) => {
9125
+ tileset.command("purge").description("Permanently purge one unreferenced Team tileset").argument("<id>", "Team-local logical ID or opaque resource ID").requiredOption("--confirm <id>", "repeat the exact tileset ID").option("--team <team>", "target Team as @team").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Team data API key")).option("--json", "print deterministic schema-version-1 JSON").action(async (id, options) => {
9002
9126
  try {
9003
9127
  if (!isTilesetId(id) || options.confirm !== id) {
9004
9128
  return emitFailure(
@@ -10014,7 +10138,7 @@ program.command("init").description("Create a starter tileflow.config.ts").optio
10014
10138
  `Validate the config with ${command("tileflow validate")}.`
10015
10139
  ]);
10016
10140
  });
10017
- program.command("login").description("Authorize this machine for your Tileflow account").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option(
10141
+ program.command("login").description("Authorize this machine for your Tileflow account").addOption(hostedApiUrlOption()).option(
10018
10142
  "--app-url <url>",
10019
10143
  "Tileflow Dashboard URL",
10020
10144
  process.env.TILEFLOW_APP_URL ?? defaultAppUrl
@@ -10025,7 +10149,7 @@ program.command("login").description("Authorize this machine for your Tileflow a
10025
10149
  await loginWithDeviceFlow({
10026
10150
  expectedApiUrl: apiUrl,
10027
10151
  appUrl,
10028
- noBrowser: Boolean(options.noBrowser)
10152
+ noBrowser: options.browser === false
10029
10153
  });
10030
10154
  } catch (error) {
10031
10155
  const message = error instanceof Error ? error.message : "Login failed.";
@@ -10033,7 +10157,7 @@ program.command("login").description("Authorize this machine for your Tileflow a
10033
10157
  process.exitCode = 1;
10034
10158
  }
10035
10159
  });
10036
- program.command("logout").description("Revoke this origin's Tileflow account session").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).action(async (options) => {
10160
+ program.command("logout").description("Revoke this origin's Tileflow account session").addOption(hostedApiUrlOption()).action(async (options) => {
10037
10161
  const apiUrl = normalizeApiOrigin(options.apiUrl ?? defaultApiUrl);
10038
10162
  const config = await loadAuthConfig().catch((error) => {
10039
10163
  logError(safeAuthError(error));
@@ -10061,7 +10185,7 @@ program.command("logout").description("Revoke this origin's Tileflow account ses
10061
10185
  }
10062
10186
  logSuccess("Signed out of this Tileflow origin.");
10063
10187
  });
10064
- program.command("whoami").description("Show the authenticated Tileflow account").option("--api-url <url>", "Tileflow API URL", process.env.TILEFLOW_API_URL).option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
10188
+ program.command("whoami").description("Show the authenticated Tileflow account").addOption(hostedApiUrlOption()).option("--json", "print deterministic schema-version-1 JSON").action(async (options) => {
10065
10189
  const config = await loadAuthConfig().catch((error) => {
10066
10190
  logAuthCommandError(options.json, safeAuthError(error));
10067
10191
  return null;
@@ -10580,7 +10704,7 @@ addMapSetupOptions(
10580
10704
  program.command("deploy").description(
10581
10705
  "Deploy maps to Tileflow and write the frontend manifest; without --map-id, create the Map first"
10582
10706
  )
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(
10707
+ ).option("-c, --config <path>", "config path", defaultConfigPath).option("--manifest <path>", "manifest path written for frontend bundlers", defaultManifestPath).addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("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(
10584
10708
  "--overwrite-self-hosted-manifest",
10585
10709
  "explicitly replace an existing self-hosted manifest at --manifest"
10586
10710
  ).option("--cache-dir <path>", "verified Icon Set artifact cache root").option("--offline", "fail a locked Icon Set cache miss instead of hydrating it").action(
@@ -10653,6 +10777,7 @@ addMapSetupOptions(
10653
10777
  mapAssetsWithBuildingModels(modelAssets, { hosted: true })
10654
10778
  );
10655
10779
  const compiledPreflightStyles = createTileflowStyles2(deploymentProject, {
10780
+ runtimeColors: true,
10656
10781
  apiBaseUrl: apiUrl,
10657
10782
  mapAssets: preflightMapAssets
10658
10783
  });
@@ -10684,6 +10809,56 @@ addMapSetupOptions(
10684
10809
  process.exitCode = 1;
10685
10810
  return;
10686
10811
  }
10812
+ for (const mapName of mapNames) {
10813
+ const map = deploymentProject.maps[mapName];
10814
+ const family = prepareTileflowHostedThemeFamily(
10815
+ mapName,
10816
+ map,
10817
+ preflightFonts.styles[mapName]
10818
+ );
10819
+ const binding = bindingsByMap.get(mapName);
10820
+ const iconPackage = binding && packagesByHash.get(binding.packageHash);
10821
+ const manifest = await createTileflowMapBuildManifest2(
10822
+ {
10823
+ [mapName]: {
10824
+ map,
10825
+ lineage: deploymentProject.mapMetadata?.[mapName]?.lineage ?? [
10826
+ { id: map.id, mapVersion: map.version }
10827
+ ],
10828
+ assets: (iconPackage?.files ?? []).map((file) => ({
10829
+ contentType: file.contentType,
10830
+ fileName: `icons/${mapName}/${file.fileName}`,
10831
+ source: file.source
10832
+ })),
10833
+ sourceAssets: {
10834
+ fonts: preflightFonts.sourceIdentities[mapName] ?? [],
10835
+ icons: compiledIcons.sourceIdentities[mapName] ?? [],
10836
+ ...compiledIcons.compositions[mapName] ? { iconComposition: compiledIcons.compositions[mapName] } : {},
10837
+ ...hostedModelSourceIdentities(modelAssets, mapName) ? { models: hostedModelSourceIdentities(modelAssets, mapName) } : {}
10838
+ },
10839
+ styles: family.styles
10840
+ }
10841
+ },
10842
+ { provenance: await createTileflowBuildProvenance2(process.cwd()) }
10843
+ );
10844
+ assertHostedStyleBudget({
10845
+ artifact: compactTileflowDeploymentArtifact3({
10846
+ kind: "tileflow-map-deployment",
10847
+ schemaVersion: 1,
10848
+ mapId: mapName,
10849
+ buildManifest: manifest,
10850
+ styles: family.styles,
10851
+ teamSources: family.teamSources
10852
+ }),
10853
+ environment: mapName,
10854
+ managedMapId: options.mapId ?? "map_preflight0000000",
10855
+ usageMode: "session",
10856
+ source,
10857
+ ...iconPackage ? { iconPackage: { contentHash: iconPackage.contentHash, label: binding.label } } : {},
10858
+ ...compiledIcons.compositions[mapName] ? { iconComposition: compiledIcons.compositions[mapName] } : {},
10859
+ ...teamModelSources(modelAssets, mapName) ? { teamModelSources: teamModelSources(modelAssets, mapName) } : {}
10860
+ });
10861
+ }
10687
10862
  let outputManifest = null;
10688
10863
  try {
10689
10864
  outputManifest = await loadExistingDeployManifest(options.manifest, {
@@ -10749,6 +10924,7 @@ addMapSetupOptions(
10749
10924
  mapAssetsWithBuildingModels(modelAssets, { hosted: true })
10750
10925
  );
10751
10926
  const compiledHostedStyles = createTileflowStyles2(deploymentProject, {
10927
+ runtimeColors: true,
10752
10928
  apiBaseUrl: api.apiUrl,
10753
10929
  mapAssets: hostedMapAssets
10754
10930
  });
@@ -10956,6 +11132,7 @@ addMapSetupOptions(
10956
11132
  environment: mapName,
10957
11133
  mapId: body.mapId,
10958
11134
  ...resolvedMap.systemThemes ? { systemThemes: resolvedMap.systemThemes } : {},
11135
+ ...body.familyUrl ? { familyUrl: body.familyUrl } : {},
10959
11136
  ...resolvedMap.timeOfDay && Object.keys(resolvedMap.timeOfDay).length ? { timeOfDay: resolvedMap.timeOfDay } : {},
10960
11137
  themes: Object.fromEntries(
10961
11138
  themeNames.map((themeName) => {
@@ -10994,8 +11171,8 @@ addMapSetupOptions(
10994
11171
  apiUrl: api.apiUrl,
10995
11172
  maps: { ...existingManifest?.maps ?? {}, ...deployedMaps },
10996
11173
  version: Object.values({ ...existingManifest?.maps, ...deployedMaps }).some(
10997
- (map) => map.timeOfDay
10998
- ) ? 2 : 1
11174
+ (map) => map.familyUrl
11175
+ ) ? 3 : Object.values({ ...existingManifest?.maps, ...deployedMaps }).some((map) => map.timeOfDay) ? 2 : 1
10999
11176
  });
11000
11177
  logSuccess("Deployed Tileflow maps.");
11001
11178
  printKeyValue("Manifest", pathLabel(manifestPath));
@@ -11009,7 +11186,7 @@ addMapSetupOptions(
11009
11186
  }
11010
11187
  }
11011
11188
  );
11012
- 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) => {
11189
+ program.command("status").description("Show deployed compiled styles").addOption(hostedApiUrlOption()).addOption(hostedApiKeyOption("Tileflow API key")).option("--map-id <id>", "managed Map destination").option("--json", "print raw JSON").action(async (options) => {
11013
11190
  if (!options.mapId || !/^map_[A-Za-z0-9_-]{16}$/u.test(options.mapId)) {
11014
11191
  if (options.json) console.error("Status requires a valid --map-id.");
11015
11192
  else logError("Status requires a valid --map-id.");
@@ -11389,13 +11566,23 @@ function closeNodeServer(server) {
11389
11566
  return new Promise((resolveClose) => server.close(() => resolveClose()));
11390
11567
  }
11391
11568
  async function loginWithDeviceFlow(options) {
11569
+ const authState = await prepareAccountLogin();
11570
+ if (authState.kind === "legacy") {
11571
+ logWarning("An older Tileflow CLI login will be replaced after authorization.");
11572
+ }
11392
11573
  const codeVerifier = randomBase64Url(32);
11393
11574
  const codeChallenge = createPkceChallenge(codeVerifier);
11394
11575
  printTitle("Tileflow login", "Authorize this machine from the Tileflow dashboard.");
11576
+ const authorizationStartedAt = Date.now();
11395
11577
  const authorization = await startDeviceAuthorization(options.appUrl, {
11396
11578
  codeChallenge,
11397
11579
  deviceName: hostname()
11398
11580
  });
11581
+ const apiUrl = normalizeApiOrigin(options.expectedApiUrl);
11582
+ if (normalizeApiOrigin(authorization.apiUrl) !== apiUrl) {
11583
+ throw new Error("The dashboard returned an unexpected Tileflow API origin.");
11584
+ }
11585
+ const previousSession = authState.kind === "sessions" ? authState.config.sessions[apiUrl] : void 0;
11399
11586
  printKeyValue("Code", pc9.bold(authorization.userCode));
11400
11587
  printKeyValue("URL", link(authorization.verificationUriComplete));
11401
11588
  if (options.noBrowser) {
@@ -11416,12 +11603,9 @@ async function loginWithDeviceFlow(options) {
11416
11603
  }
11417
11604
  logInfo("Waiting for authorization...");
11418
11605
  const token = await pollDeviceToken(options.appUrl, authorization, {
11419
- codeVerifier
11606
+ codeVerifier,
11607
+ expiresAt: authorizationStartedAt + authorization.expiresIn * 1e3
11420
11608
  });
11421
- const apiUrl = normalizeApiOrigin(token.apiUrl);
11422
- if (apiUrl !== normalizeApiOrigin(options.expectedApiUrl)) {
11423
- throw new Error(`The dashboard returned an unexpected Tileflow API origin: ${apiUrl}.`);
11424
- }
11425
11609
  const session = {
11426
11610
  account: token.account,
11427
11611
  accountSession: token.accountSession,
@@ -11430,13 +11614,30 @@ async function loginWithDeviceFlow(options) {
11430
11614
  expiresAt: token.expiresAt,
11431
11615
  sessionId: token.sessionId
11432
11616
  };
11433
- const profile = await validateAccountSession(session);
11434
- if (!profile.ok) {
11435
- throw new Error(profile.error);
11617
+ let account = session.account;
11618
+ try {
11619
+ if (normalizeApiOrigin(token.apiUrl) !== apiUrl) {
11620
+ throw new Error("The dashboard returned an unexpected Tileflow API origin.");
11621
+ }
11622
+ const profile = await validateAccountSession(session);
11623
+ if (!profile.ok) throw new Error(profile.error);
11624
+ account = profile.value.account;
11625
+ await installAccountSession({ ...session, account });
11626
+ } catch (error) {
11627
+ if (!await revokeHostedAccountSession(apiUrl, session.accountSession)) {
11628
+ logWarning(
11629
+ "Could not revoke the new account session after login failed. Revoke it in Dashboard settings."
11630
+ );
11631
+ }
11632
+ throw error;
11633
+ }
11634
+ if (previousSession && previousSession.accountSession !== session.accountSession && !await revokeHostedAccountSession(apiUrl, previousSession.accountSession)) {
11635
+ logWarning(
11636
+ "The new login was saved, but the previous session could not be revoked. Revoke it in Dashboard settings."
11637
+ );
11436
11638
  }
11437
- await installAccountSession({ ...session, account: profile.value.account });
11438
11639
  logSuccess("Signed in to Tileflow.");
11439
- printAccountDetails({ ...session, account: profile.value.account });
11640
+ printAccountDetails({ ...session, account });
11440
11641
  printKeyValue("Dashboard", link(options.appUrl));
11441
11642
  }
11442
11643
  function createPkceChallenge(verifier) {
@@ -11529,7 +11730,7 @@ function validateMapSetupTarget(options, source) {
11529
11730
  }
11530
11731
  const createFirst = `Create the Map with ${command("tileflow maps create")} after ${command("tileflow login")}, then pass --map-id.`;
11531
11732
  if (options.withNative) return fail("--with-native requires one valid --map-id.", [createFirst]);
11532
- if (options.apiKey || !allowsStoredDeployCredential(source)) {
11733
+ if (options.apiKey !== void 0 || !allowsStoredDeployCredential(source)) {
11533
11734
  return fail("Managed deploy with a Tileflow credential or in CI requires --map-id.", [
11534
11735
  createFirst
11535
11736
  ]);
@@ -11597,7 +11798,12 @@ async function requireApiOptions(options, behavior = {}) {
11597
11798
  process.exitCode = 1;
11598
11799
  return null;
11599
11800
  }
11600
- if (options.apiKey) {
11801
+ if (options.apiKey !== void 0) {
11802
+ if (!isHostedApiKey(options.apiKey)) {
11803
+ if (!behavior.silent) logError("Tileflow API key is invalid.");
11804
+ process.exitCode = 1;
11805
+ return null;
11806
+ }
11601
11807
  if (behavior.assertExplicitKeyMap) {
11602
11808
  const profile = await validateApiKey(apiUrl, options.apiKey);
11603
11809
  if (!profile.ok) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tileflow",
3
- "version": "0.1.0-alpha.19",
3
+ "version": "0.1.0-alpha.20",
4
4
  "license": "Apache-2.0",
5
5
  "description": "CLI-only tools for Tileflow config, capture, visual testing, and deploy.",
6
6
  "type": "module",
@@ -35,12 +35,12 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@hono/node-server": "2.1.3",
38
- "@tileflow/capture": ">=0.1.0-alpha.36 <0.1.0-beta.0",
38
+ "@tileflow/capture": ">=0.1.0-alpha.37 <0.1.0-beta.0",
39
39
  "@tileflow/coordinates": ">=0.1.0-alpha.3 <0.1.0-beta.0",
40
40
  "@tileflow/coordinates-runtime": ">=0.1.0-alpha.6 <0.1.0-beta.0",
41
- "@tileflow/core": ">=0.1.0-alpha.43 <0.1.0-beta.0",
42
- "@tileflow/dev": ">=0.1.0-alpha.39 <0.1.0-beta.0",
43
- "@tileflow/maps": ">=0.1.0-alpha.17 <0.1.0-beta.0",
41
+ "@tileflow/core": ">=0.1.0-alpha.45 <0.1.0-beta.0",
42
+ "@tileflow/dev": ">=0.1.0-alpha.41 <0.1.0-beta.0",
43
+ "@tileflow/maps": ">=0.1.0-alpha.18 <0.1.0-beta.0",
44
44
  "commander": "^15.0.0",
45
45
  "hono": "4.13.14",
46
46
  "picocolors": "^1.1.1",
@@ -55,7 +55,7 @@
55
55
  "cli": "tsx src/index.ts",
56
56
  "typecheck": "tsc -p tsconfig.json --noEmit",
57
57
  "lint": "tsc -p tsconfig.json --noEmit",
58
- "test": "tsx --test test/*.test.ts",
58
+ "test": "tsx --test --test-concurrency=2 test/*.test.ts",
59
59
  "verify": "pnpm run typecheck && pnpm run test"
60
60
  }
61
61
  }