naro-cli 0.21.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -326,6 +326,162 @@ var STALL_WINDOW_MS = 10 * 6e4;
326
326
  // ../../packages/core/src/deployment-strategy.ts
327
327
  var MIN_SUPPORTED_NEXT_MAJOR = 15;
328
328
 
329
+ // ../../packages/core/src/environment-guidance.ts
330
+ var DATA_COMMANDS = /* @__PURE__ */ new Set([
331
+ "db",
332
+ "migration",
333
+ "gen",
334
+ "backend",
335
+ "policy",
336
+ "storage",
337
+ "realtime"
338
+ ]);
339
+ var BACKEND_SWITCHES = /* @__PURE__ */ new Set([
340
+ "enable",
341
+ "provision",
342
+ "disable"
343
+ ]);
344
+ var PRODUCTION_SWITCH_NOTE = "production's switch, which every environment follows";
345
+ var SWITCH_CONTEXT = /\bturned off\b|\bturn(?:s|ing)? (?:it|them) (?:back )?on\b|\bbackend is on\b/i;
346
+ var SUBCOMMANDS = {
347
+ db: /* @__PURE__ */ new Set([
348
+ "info",
349
+ "enable",
350
+ "query",
351
+ "push",
352
+ "dump",
353
+ "restore",
354
+ "pull",
355
+ "reset",
356
+ "lint",
357
+ "table",
358
+ "index",
359
+ "create",
360
+ "add-column",
361
+ "rename-column",
362
+ "rename",
363
+ "drop-column",
364
+ "drop"
365
+ ]),
366
+ migration: /* @__PURE__ */ new Set(["list", "ls", "new", "up", "repair"]),
367
+ gen: /* @__PURE__ */ new Set(["types"]),
368
+ backend: /* @__PURE__ */ new Set([
369
+ "info",
370
+ "status",
371
+ "enable",
372
+ "provision",
373
+ "disable",
374
+ "keys",
375
+ "rotate",
376
+ "auth",
377
+ "email",
378
+ "oauth",
379
+ "redirects",
380
+ "require-verification",
381
+ "signups",
382
+ "password",
383
+ "set",
384
+ "add",
385
+ "remove",
386
+ "users",
387
+ "create",
388
+ "delete"
389
+ ]),
390
+ policy: /* @__PURE__ */ new Set(["list", "ls", "set", "create", "drop", "remove", "rm"]),
391
+ storage: /* @__PURE__ */ new Set([
392
+ "enable",
393
+ "disable",
394
+ "bucket",
395
+ "list",
396
+ "create",
397
+ "update",
398
+ "empty",
399
+ "remove",
400
+ "cp",
401
+ "mv",
402
+ "ls",
403
+ "rm"
404
+ ]),
405
+ realtime: /* @__PURE__ */ new Set(["enable", "disable", "tables", "add", "remove"])
406
+ };
407
+ function kindOf(words) {
408
+ const command = words[1] ?? "";
409
+ if (!DATA_COMMANDS.has(command)) return "leave";
410
+ if (words.some((word) => word === "--env" || word.startsWith("--env=")))
411
+ return "leave";
412
+ if (command === "migration" && words[2] === "new") return "leave";
413
+ if (command === "backend" && BACKEND_SWITCHES.has(words[2] ?? ""))
414
+ return words.length === 3 ? "switch" : "leave";
415
+ return "env";
416
+ }
417
+ function quoted(span, slug, switchContext) {
418
+ const words = span.split(" ");
419
+ switch (kindOf(words)) {
420
+ case "env":
421
+ return `\`${span} --env ${slug}\``;
422
+ case "switch":
423
+ return words[2] !== "disable" && !switchContext ? `\`naro environments provision ${slug}\`` : `\`${span}\` (${PRODUCTION_SWITCH_NOTE})`;
424
+ default:
425
+ return `\`${span}\``;
426
+ }
427
+ }
428
+ var QUOTED = /`(naro [^`\n]+)`/g;
429
+ var BARE = /(?<![`\w/-])naro ([a-z]+)((?: [a-z][a-z-]*)*)/g;
430
+ function bare(match, command, rest, after, slug, switchContext) {
431
+ const vocabulary = SUBCOMMANDS[command];
432
+ if (vocabulary === void 0) return match;
433
+ const words = rest.split(" ").filter((word) => word !== "");
434
+ let count2 = 0;
435
+ while (count2 < words.length && vocabulary.has(words[count2] ?? "")) count2++;
436
+ if (count2 === 0) return match;
437
+ const head = ["naro", command, ...words.slice(0, count2)];
438
+ const tail = words.slice(count2);
439
+ const following = after.split(/\n|[.;,)](?:\s|$)/, 1)[0] ?? "";
440
+ if (/(?:^|\s)--env[\s=]/.test(following) || after.startsWith(` \u2014 ${PRODUCTION_SWITCH_NOTE}`))
441
+ return match;
442
+ const shown = head.join(" ");
443
+ const remainder = tail.length > 0 ? ` ${tail.join(" ")}` : "";
444
+ switch (kindOf(head)) {
445
+ case "env":
446
+ return `${shown} --env ${slug}${remainder}`;
447
+ case "switch":
448
+ return head[2] !== "disable" && !switchContext ? `naro environments provision ${slug}${remainder}` : `${shown} \u2014 ${PRODUCTION_SWITCH_NOTE}${remainder}`;
449
+ default:
450
+ return match;
451
+ }
452
+ }
453
+ function environmentGuidance(text2, slug) {
454
+ if (slug === null || !text2.includes("naro ")) return text2;
455
+ const switchContext = SWITCH_CONTEXT.test(text2);
456
+ const spans = [];
457
+ const marked = text2.replaceAll(
458
+ QUOTED,
459
+ (match, span, offset) => {
460
+ const noted = text2.startsWith(
461
+ ` (${PRODUCTION_SWITCH_NOTE})`,
462
+ offset + match.length
463
+ );
464
+ spans.push(noted ? match : quoted(span, slug, switchContext));
465
+ return `\uE000${String(spans.length - 1)}\uE000`;
466
+ }
467
+ );
468
+ const rewritten = marked.replaceAll(
469
+ BARE,
470
+ (match, command, rest, offset) => bare(
471
+ match,
472
+ command,
473
+ rest,
474
+ marked.slice(offset + match.length),
475
+ slug,
476
+ switchContext
477
+ )
478
+ );
479
+ return rewritten.replaceAll(
480
+ /\uE000(\d+)\uE000/g,
481
+ (_match, index) => spans[Number(index)] ?? ""
482
+ );
483
+ }
484
+
329
485
  // ../../packages/core/src/errors.ts
330
486
  var DEPLOYMENT_ERROR_CODES = [
331
487
  "CLONE_FAILED",
@@ -436,7 +592,11 @@ var DEPLOYMENT_ERROR_CODES = [
436
592
  "ENVIRONMENT_BRANCH_IS_PRODUCTION",
437
593
  /** The plan's environment count is full. */
438
594
  "ENVIRONMENT_LIMIT_REACHED",
439
- /** The project uses a Naro database, which environments do not support yet (stages 1 and 2 only). */
595
+ /**
596
+ * The project uses a Naro database, which environments did not yet support
597
+ * (stages 1 and 2 only). No route answers it since named environments 3c;
598
+ * kept for clients of a control plane from before.
599
+ */
440
600
  "ENVIRONMENT_DATABASE_UNSUPPORTED",
441
601
  "ENVIRONMENT_NOT_FOUND",
442
602
  /** The project's new default branch is the one an environment deploys. */
@@ -460,6 +620,9 @@ var DeploymentError = class extends Error {
460
620
  }
461
621
  };
462
622
 
623
+ // ../../packages/core/src/functions.ts
624
+ var FUNCTION_WORKER_NAME_PREFIX = "naro-fn-";
625
+
463
626
  // ../../packages/core/src/git-remote.ts
464
627
  var SCP_LIKE = /^[^@\s/]+@([^:\s/]+):(.+)$/;
465
628
  function parseGitHubRemote(url2) {
@@ -15064,6 +15227,8 @@ function subdomainProblem(value) {
15064
15227
  if (value.endsWith("-preview")) return "reserved";
15065
15228
  if (value === BACKEND_WORKER_NAME_PREFIX.slice(0, -1) || value.startsWith(BACKEND_WORKER_NAME_PREFIX))
15066
15229
  return "reserved";
15230
+ if (value === FUNCTION_WORKER_NAME_PREFIX.slice(0, -1) || value.startsWith(FUNCTION_WORKER_NAME_PREFIX))
15231
+ return "reserved";
15067
15232
  if (RESERVED_SUBDOMAINS.includes(value))
15068
15233
  return "reserved";
15069
15234
  return null;
@@ -15399,6 +15564,72 @@ function workerNameForSurface(subdomain, surface) {
15399
15564
  }
15400
15565
  }
15401
15566
 
15567
+ // ../../packages/core/src/usage-alert-mail.ts
15568
+ var REQUESTS = {
15569
+ en: {
15570
+ subject80: (team) => `${team} has used 80% of its monthly requests`,
15571
+ subject100: (team) => `${team} has reached its monthly request limit`,
15572
+ usage: (team, used, limit, month) => `${team} has used ${used} of the ${limit} requests the Hobby plan includes for ${month}.`,
15573
+ warning: "When it reaches the limit, requests past it may be blocked until the month ends (UTC).",
15574
+ blocked: "Sites on this team answer 503 until the month ends (UTC).",
15575
+ running: "Sites keep running for now.",
15576
+ button: "View usage",
15577
+ footer: (team) => `You're receiving this because you're an owner or billing member of ${team} on naro.`
15578
+ },
15579
+ ko: {
15580
+ subject80: (team) => `${team} \uD300\uC774 \uC774\uBC88 \uB2EC \uC694\uCCAD \uD55C\uB3C4\uC758 80%\uB97C \uC37C\uC2B5\uB2C8\uB2E4`,
15581
+ subject100: (team) => `${team} \uD300\uC774 \uC774\uBC88 \uB2EC \uC694\uCCAD \uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4`,
15582
+ usage: (team, used, limit, month) => `${team} \uD300\uC774 ${month} Hobby \uD50C\uB79C\uC5D0 \uD3EC\uD568\uB41C \uC694\uCCAD ${limit}\uAC74 \uC911 ${used}\uAC74\uC744 \uC37C\uC2B5\uB2C8\uB2E4.`,
15583
+ warning: "\uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD558\uBA74 \uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uADF8 \uB4A4 \uC694\uCCAD\uC774 \uB9C9\uD790 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
15584
+ blocked: "\uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uC774 \uD300\uC758 \uC0AC\uC774\uD2B8\uB294 503\uC73C\uB85C \uC751\uB2F5\uD569\uB2C8\uB2E4.",
15585
+ running: "\uC9C0\uAE08\uC740 \uC0AC\uC774\uD2B8\uAC00 \uACC4\uC18D \uB3D9\uC791\uD569\uB2C8\uB2E4.",
15586
+ button: "\uC0AC\uC6A9\uB7C9 \uBCF4\uAE30",
15587
+ footer: (team) => `naro\uC5D0\uC11C ${team} \uD300\uC758 owner \uB610\uB294 billing \uC5ED\uD560\uC774\uB77C \uC774 \uBA54\uC77C\uC744 \uBC1B\uC558\uC2B5\uB2C8\uB2E4.`
15588
+ },
15589
+ ja: {
15590
+ subject80: (team) => `${team} \u30C1\u30FC\u30E0\u304C\u4ECA\u6708\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u4E0A\u9650\u306E80%\u3092\u4F7F\u7528\u3057\u307E\u3057\u305F`,
15591
+ subject100: (team) => `${team} \u30C1\u30FC\u30E0\u304C\u4ECA\u6708\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F`,
15592
+ usage: (team, used, limit, month) => `${team} \u30C1\u30FC\u30E0\u306F ${month} \u306E Hobby \u30D7\u30E9\u30F3\u306B\u542B\u307E\u308C\u308B ${limit} \u30EA\u30AF\u30A8\u30B9\u30C8\u306E\u3046\u3061 ${used} \u3092\u4F7F\u7528\u3057\u307E\u3057\u305F\u3002`,
15593
+ warning: "\u4E0A\u9650\u306B\u9054\u3059\u308B\u3068\u3001\u6708\u672B(UTC)\u307E\u3067\u4EE5\u964D\u306E\u30EA\u30AF\u30A8\u30B9\u30C8\u304C\u30D6\u30ED\u30C3\u30AF\u3055\u308C\u308B\u5834\u5408\u304C\u3042\u308A\u307E\u3059\u3002",
15594
+ blocked: "\u6708\u672B(UTC)\u307E\u3067\u3001\u3053\u306E\u30C1\u30FC\u30E0\u306E\u30B5\u30A4\u30C8\u306F503\u3092\u8FD4\u3057\u307E\u3059\u3002",
15595
+ running: "\u73FE\u5728\u3001\u30B5\u30A4\u30C8\u306F\u5F15\u304D\u7D9A\u304D\u52D5\u4F5C\u3057\u3066\u3044\u307E\u3059\u3002",
15596
+ button: "\u4F7F\u7528\u91CF\u3092\u898B\u308B",
15597
+ footer: (team) => `naro \u3067 ${team} \u30C1\u30FC\u30E0\u306E owner \u307E\u305F\u306F billing \u30ED\u30FC\u30EB\u306E\u305F\u3081\u3001\u3053\u306E\u30E1\u30FC\u30EB\u3092\u304A\u9001\u308A\u3057\u3066\u3044\u307E\u3059\u3002`
15598
+ }
15599
+ };
15600
+ var BUILDS = {
15601
+ en: {
15602
+ subject80: (team) => `${team} has used 80% of its monthly builds`,
15603
+ subject100: (team) => `${team} has reached its monthly build limit`,
15604
+ usage: (team, used, limit, month) => `${team} has used ${used} of the ${limit} builds the Hobby plan includes for ${month}.`,
15605
+ warning: "When it reaches the limit, new deploys may be refused until the month ends (UTC).",
15606
+ blocked: "New deploys on this team are refused until the month ends (UTC).",
15607
+ running: "Deploys keep working for now.",
15608
+ button: REQUESTS.en.button,
15609
+ footer: REQUESTS.en.footer
15610
+ },
15611
+ ko: {
15612
+ subject80: (team) => `${team} \uD300\uC774 \uC774\uBC88 \uB2EC \uBE4C\uB4DC \uD55C\uB3C4\uC758 80%\uB97C \uC37C\uC2B5\uB2C8\uB2E4`,
15613
+ subject100: (team) => `${team} \uD300\uC774 \uC774\uBC88 \uB2EC \uBE4C\uB4DC \uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD588\uC2B5\uB2C8\uB2E4`,
15614
+ usage: (team, used, limit, month) => `${team} \uD300\uC774 ${month} Hobby \uD50C\uB79C\uC5D0 \uD3EC\uD568\uB41C \uBE4C\uB4DC ${limit}\uAC74 \uC911 ${used}\uAC74\uC744 \uC37C\uC2B5\uB2C8\uB2E4.`,
15615
+ warning: "\uD55C\uB3C4\uC5D0 \uB3C4\uB2EC\uD558\uBA74 \uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uC0C8 \uBC30\uD3EC\uAC00 \uAC70\uC808\uB420 \uC218 \uC788\uC2B5\uB2C8\uB2E4.",
15616
+ blocked: "\uC774\uBC88 \uB2EC\uC774 \uB05D\uB0A0 \uB54C\uAE4C\uC9C0(UTC) \uC774 \uD300\uC758 \uC0C8 \uBC30\uD3EC\uB294 \uAC70\uC808\uB429\uB2C8\uB2E4.",
15617
+ running: "\uC9C0\uAE08\uC740 \uBC30\uD3EC\uAC00 \uACC4\uC18D \uB429\uB2C8\uB2E4.",
15618
+ button: REQUESTS.ko.button,
15619
+ footer: REQUESTS.ko.footer
15620
+ },
15621
+ ja: {
15622
+ subject80: (team) => `${team} \u30C1\u30FC\u30E0\u304C\u4ECA\u6708\u306E\u30D3\u30EB\u30C9\u4E0A\u9650\u306E80%\u3092\u4F7F\u7528\u3057\u307E\u3057\u305F`,
15623
+ subject100: (team) => `${team} \u30C1\u30FC\u30E0\u304C\u4ECA\u6708\u306E\u30D3\u30EB\u30C9\u4E0A\u9650\u306B\u9054\u3057\u307E\u3057\u305F`,
15624
+ usage: (team, used, limit, month) => `${team} \u30C1\u30FC\u30E0\u306F ${month} \u306E Hobby \u30D7\u30E9\u30F3\u306B\u542B\u307E\u308C\u308B ${limit} \u30D3\u30EB\u30C9\u306E\u3046\u3061 ${used} \u3092\u4F7F\u7528\u3057\u307E\u3057\u305F\u3002`,
15625
+ warning: "\u4E0A\u9650\u306B\u9054\u3059\u308B\u3068\u3001\u6708\u672B(UTC)\u307E\u3067\u65B0\u3057\u3044\u30C7\u30D7\u30ED\u30A4\u304C\u62D2\u5426\u3055\u308C\u308B\u5834\u5408\u304C\u3042\u308A\u307E\u3059\u3002",
15626
+ blocked: "\u6708\u672B(UTC)\u307E\u3067\u3001\u3053\u306E\u30C1\u30FC\u30E0\u306E\u65B0\u3057\u3044\u30C7\u30D7\u30ED\u30A4\u306F\u62D2\u5426\u3055\u308C\u307E\u3059\u3002",
15627
+ running: "\u73FE\u5728\u3001\u30C7\u30D7\u30ED\u30A4\u306F\u5F15\u304D\u7D9A\u304D\u884C\u3048\u307E\u3059\u3002",
15628
+ button: REQUESTS.ja.button,
15629
+ footer: REQUESTS.ja.footer
15630
+ }
15631
+ };
15632
+
15402
15633
  // ../../packages/build/src/next-version.ts
15403
15634
  var NEXT_RELEASE_DATES = {
15404
15635
  16: "2025-10-21",
@@ -19244,7 +19475,7 @@ async function errorFromResponse(response, context) {
19244
19475
  if (typeof serverDetails === "object" && serverDetails !== null && !Array.isArray(serverDetails))
19245
19476
  Object.assign(details, serverDetails);
19246
19477
  const retryAfter = response.headers.get("retry-after")?.trim() ?? "";
19247
- if (response.status === 429 && /^\d{1,9}$/.test(retryAfter))
19478
+ if ((response.status === 429 || response.status === 503) && /^\d{1,9}$/.test(retryAfter))
19248
19479
  details.retryAfter = Number(retryAfter);
19249
19480
  if (body?.error?.fields !== void 0) details.fields = body.error.fields;
19250
19481
  if (typeof body?.error?.installUrl === "string")
@@ -19712,6 +19943,16 @@ function isReservedEnvTarget(value) {
19712
19943
  function isEnvTargetName(value) {
19713
19944
  return environmentSlugProblem(value) !== "invalid";
19714
19945
  }
19946
+ var NARO_SYSTEM_ENV_NAMES = [
19947
+ "NARO_URL",
19948
+ "NEXT_PUBLIC_NARO_URL",
19949
+ "VITE_NARO_URL",
19950
+ "PUBLIC_NARO_URL",
19951
+ "NARO_ANON_KEY",
19952
+ "NEXT_PUBLIC_NARO_ANON_KEY",
19953
+ "VITE_NARO_ANON_KEY",
19954
+ "PUBLIC_NARO_ANON_KEY"
19955
+ ];
19715
19956
 
19716
19957
  // ../../packages/sdk/src/insights.ts
19717
19958
  var SPEED_INSIGHTS_METRICS = [
@@ -19745,6 +19986,7 @@ var BACKEND_STEPS = [
19745
19986
  "worker",
19746
19987
  "routes",
19747
19988
  "migrate",
19989
+ "schema",
19748
19990
  "health"
19749
19991
  ];
19750
19992
  var STALLED_GRACE_MS = 2 * 6e4;
@@ -19808,9 +20050,17 @@ function buildLogEntry(row) {
19808
20050
  }
19809
20051
 
19810
20052
  // ../../packages/sdk/src/client.ts
20053
+ var DATA_PATH = /^\/api\/projects\/[^/?]+\/(?:database|backend(?!\/functions))(?:[/?]|$)/;
20054
+ function withDataEnvironment(path, environment) {
20055
+ if (environment === null || !DATA_PATH.test(path)) return path;
20056
+ return `${path}${path.includes("?") ? "&" : "?"}environment=${encodeURIComponent(environment)}`;
20057
+ }
19811
20058
  var DEFAULT_REQUEST_TIMEOUT_MS = 45e3;
19812
- var NaroClient = class {
20059
+ var NaroClient = class _NaroClient {
19813
20060
  baseUrl;
20061
+ /** The environment whose data the data calls act on; null: production. */
20062
+ environment;
20063
+ options;
19814
20064
  getAccessToken;
19815
20065
  doFetch;
19816
20066
  requestTimeoutMs;
@@ -19825,11 +20075,14 @@ var NaroClient = class {
19825
20075
  backend;
19826
20076
  storage;
19827
20077
  realtime;
20078
+ functions;
19828
20079
  insights;
19829
20080
  analytics;
19830
20081
  protection;
19831
20082
  environments;
19832
20083
  constructor(options) {
20084
+ this.options = options;
20085
+ this.environment = options.environment ?? null;
19833
20086
  this.baseUrl = (options.baseUrl ?? controlPlaneUrl()).replace(/\/$/, "");
19834
20087
  this.getAccessToken = options.getAccessToken;
19835
20088
  this.doFetch = options.fetch ?? fetch;
@@ -19845,25 +20098,41 @@ var NaroClient = class {
19845
20098
  this.backend = new BackendApi(this);
19846
20099
  this.storage = new StorageApi(this);
19847
20100
  this.realtime = new RealtimeApi(this);
20101
+ this.functions = new FunctionsApi(this);
19848
20102
  this.insights = new InsightsApi(this);
19849
20103
  this.analytics = new AnalyticsApi(this);
19850
20104
  this.protection = new ProtectionApi(this);
19851
20105
  this.environments = new EnvironmentsApi(this);
19852
20106
  }
20107
+ /**
20108
+ * The same client acting on `environment`'s data — or production's, for
20109
+ * null. Every other call is the same: the scope is only the
20110
+ * `?environment=` on database and backend paths.
20111
+ */
20112
+ inEnvironment(environment) {
20113
+ if (environment === this.environment) return this;
20114
+ const options = { ...this.options };
20115
+ delete options.environment;
20116
+ if (environment !== null) options.environment = environment;
20117
+ return new _NaroClient(options);
20118
+ }
19853
20119
  async request(path, options) {
19854
20120
  const token = await this.getAccessToken();
19855
20121
  const timeoutMs = options.timeoutMs ?? this.requestTimeoutMs;
19856
20122
  let response;
19857
20123
  try {
19858
- response = await this.doFetch(`${this.baseUrl}${path}`, {
19859
- method: options.method ?? "GET",
19860
- headers: {
19861
- Authorization: `Bearer ${token}`,
19862
- ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
19863
- },
19864
- ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
19865
- signal: AbortSignal.timeout(timeoutMs)
19866
- });
20124
+ response = await this.doFetch(
20125
+ `${this.baseUrl}${withDataEnvironment(path, this.environment)}`,
20126
+ {
20127
+ method: options.method ?? "GET",
20128
+ headers: {
20129
+ Authorization: `Bearer ${token}`,
20130
+ ...options.body !== void 0 ? { "Content-Type": "application/json" } : {}
20131
+ },
20132
+ ...options.body !== void 0 ? { body: JSON.stringify(options.body) } : {},
20133
+ signal: AbortSignal.timeout(timeoutMs)
20134
+ }
20135
+ );
19867
20136
  } catch (error51) {
19868
20137
  if (error51 instanceof Error && (error51.name === "TimeoutError" || error51.name === "AbortError")) {
19869
20138
  throw new NaroError(
@@ -20061,17 +20330,23 @@ var DeploymentsApi = class {
20061
20330
  * `PROMOTE_TARGET_UNSUPPORTED` before anything is promoted. The target is
20062
20331
  * trimmed once, as the control plane trims it, so `" production"` is
20063
20332
  * production and asks no preview.
20333
+ *
20334
+ * `check: true` asks the preview for production too, for a caller that
20335
+ * cannot show it first and returns it instead (MCP's `promote_deployment`):
20336
+ * a 405 there means a control plane from before previews, and production
20337
+ * is promoted unchecked as before. The preview asked is the result's
20338
+ * `check`, so its migration warnings reach the caller (spec §9.4).
20064
20339
  */
20065
20340
  async promote(projectId, deploymentId, options = {}) {
20066
20341
  const target2 = options.target?.trim();
20067
- if (target2 !== void 0 && target2 !== "production") {
20342
+ const toEnvironment = target2 !== void 0 && target2 !== "production";
20343
+ let check2;
20344
+ if (toEnvironment || options.check === true) {
20068
20345
  try {
20069
- await this.promoteCheck(projectId, deploymentId, target2);
20346
+ check2 = await this.promoteCheck(projectId, deploymentId, target2);
20070
20347
  } catch (error51) {
20071
- if (isNaroError(error51) && error51.status === 405) {
20072
- throw promoteTargetUnsupported(error51);
20073
- }
20074
- throw error51;
20348
+ if (!isNaroError(error51) || error51.status !== 405) throw error51;
20349
+ if (toEnvironment) throw promoteTargetUnsupported(error51);
20075
20350
  }
20076
20351
  }
20077
20352
  const body = await this.client.request(this.promotePath(projectId, deploymentId), {
@@ -20081,7 +20356,8 @@ var DeploymentsApi = class {
20081
20356
  });
20082
20357
  return {
20083
20358
  ...deploymentInfo(body.deployment),
20084
- mode: body.mode ?? "reupload"
20359
+ mode: body.mode ?? "reupload",
20360
+ ...check2 === void 0 ? {} : { check: check2 }
20085
20361
  };
20086
20362
  }
20087
20363
  /**
@@ -20582,6 +20858,19 @@ var EnvironmentsApi = class {
20582
20858
  { method: "DELETE", context: "Delete environment" }
20583
20859
  );
20584
20860
  }
20861
+ /**
20862
+ * Makes or continues the environment's data — its D1, Naro backend and
20863
+ * Storage bucket (spec §11.2): the run goes on after the answer, which
20864
+ * says `data.status` `provisioning`. A run that stopped (`error`) starts
20865
+ * again from where it stopped; `none` when the project uses neither.
20866
+ */
20867
+ async provision(projectId, environmentId) {
20868
+ const body = await this.client.request(
20869
+ `${this.base(projectId)}/${encodeURIComponent(environmentId)}/provision`,
20870
+ { method: "POST", context: "Provision environment" }
20871
+ );
20872
+ return body.environment;
20873
+ }
20585
20874
  };
20586
20875
  var EXPORT_FIRST_POLL_MS = 1e3;
20587
20876
  var EXPORT_MAX_POLL_MS = 5e3;
@@ -21615,6 +21904,202 @@ var RealtimeApi = class {
21615
21904
  );
21616
21905
  }
21617
21906
  };
21907
+ var FunctionsApi = class {
21908
+ constructor(client) {
21909
+ this.client = client;
21910
+ }
21911
+ path(projectId, suffix = "") {
21912
+ return `/api/projects/${encodeURIComponent(projectId)}/backend/functions${suffix}`;
21913
+ }
21914
+ /**
21915
+ * A segment no URL can carry is refused before a request, like
21916
+ * `StorageApi.bucketPath`: URL parsing folds `/`, `.` and `..` into the
21917
+ * path, an empty one is the collection, and `encodeURIComponent` throws on
21918
+ * a lone surrogate. The server checks the name's own rule.
21919
+ */
21920
+ segment(value, what) {
21921
+ if (value === "" || value === "." || value === ".." || value.includes("/") || LONE_SURROGATE.test(value))
21922
+ throw new NaroError("INVALID_PATH", `The ${what} is not valid.`);
21923
+ return encodeURIComponent(value);
21924
+ }
21925
+ functionPath(projectId, name, suffix = "") {
21926
+ return this.path(
21927
+ projectId,
21928
+ `/${this.segment(name, "function name")}${suffix}`
21929
+ );
21930
+ }
21931
+ /** The project's functions, the plan's limits, and (3-3e) this month's usage. */
21932
+ async list(projectId) {
21933
+ return this.client.request(this.path(projectId), {
21934
+ context: "List functions"
21935
+ });
21936
+ }
21937
+ /** One function and its newest 20 versions. `FUNCTION_NOT_FOUND` (404). */
21938
+ async get(projectId, name) {
21939
+ return this.client.request(
21940
+ this.functionPath(projectId, name),
21941
+ { context: "Read a function" }
21942
+ );
21943
+ }
21944
+ /**
21945
+ * A new version — the function too, the first time — and where to PUT its
21946
+ * bundle: `upload.url` with `upload.headers` (the checksum header is
21947
+ * signed). Refusals in the order they are checked: `BACKEND_DISABLED`,
21948
+ * `UNAUTHORIZED`, `NOT_FOUND`, `FORBIDDEN` (`deployment.create`),
21949
+ * `FUNCTIONS_DISABLED`, `FORBIDDEN` (a suspended project),
21950
+ * `VALIDATION_FAILED` (the name or the request), `BUNDLE_TOO_LARGE`,
21951
+ * `DATABASE_NOT_ENABLED`, `BACKEND_NOT_READY`, `BACKEND_OFF`,
21952
+ * `BACKEND_BUSY` (the project's Naro worker is not the current version and
21953
+ * could not be brought up to date just now), `FUNCTION_LIMIT` (a new name
21954
+ * over the plan's cap) and `FUNCTION_BUSY` (the function is being deleted).
21955
+ * It never turns the backend on. Two minutes.
21956
+ */
21957
+ async createVersion(projectId, name, request2) {
21958
+ return this.client.request(
21959
+ this.functionPath(projectId, name, "/versions"),
21960
+ {
21961
+ method: "POST",
21962
+ body: request2,
21963
+ context: "Create a function version",
21964
+ timeoutMs: PROVISION_TIMEOUT_MS
21965
+ }
21966
+ );
21967
+ }
21968
+ /**
21969
+ * Uploads a version whose bundle is in R2 and makes it live. `versionId` is
21970
+ * `CreatedFunctionVersion.version.id`. The refusals before anything is
21971
+ * uploaded leave the function as it was: `FUNCTION_NOT_FOUND`,
21972
+ * `VERSION_NOT_FOUND`, `VERSION_NOT_DEPLOYABLE` (pruned),
21973
+ * `BACKEND_NOT_READY`, `BACKEND_OFF`, `FUNCTION_BUSY`, `BUNDLE_MISSING`,
21974
+ * `BUNDLE_MISMATCH`. Then `FUNCTION_DEPLOY_FAILED` (Cloudflare refused or
21975
+ * could not be reached; the version that was live still runs),
21976
+ * `FUNCTIONS_RATE_LIMITED` (503, `retry-after`),
21977
+ * `FUNCTION_DEPLOY_INCOMPLETE` (the code runs but the function list did
21978
+ * not take it, so callers may still get the previous `verifyJwt` setting
21979
+ * or 404 — deploy again) and `BACKEND_OUTDATED` (the project's
21980
+ * database predates the function list — `backend.enable` updates it).
21981
+ * Two minutes.
21982
+ */
21983
+ async deploy(projectId, name, versionId) {
21984
+ const body = await this.client.request(
21985
+ this.functionPath(
21986
+ projectId,
21987
+ name,
21988
+ `/versions/${this.segment(versionId, "version id")}/deploy`
21989
+ ),
21990
+ {
21991
+ method: "POST",
21992
+ context: "Deploy a function",
21993
+ timeoutMs: PROVISION_TIMEOUT_MS
21994
+ }
21995
+ );
21996
+ return body.function;
21997
+ }
21998
+ /**
21999
+ * Puts a stored version live again: the one numbered `version`, which must
22000
+ * have been live before (`VERSION_NOT_FOUND` otherwise — put a version that
22001
+ * never went live up with `deploy`), or without it the deployed version
22002
+ * before the live one. The code comes back, not the old keys or secrets:
22003
+ * the bindings are the project's now. The refusals are `deploy`'s, and
22004
+ * `VERSION_NOT_FOUND` also says there is no earlier deployed version.
22005
+ * Two minutes.
22006
+ */
22007
+ async rollback(projectId, name, version2) {
22008
+ const body = await this.client.request(
22009
+ this.functionPath(projectId, name, "/rollback"),
22010
+ {
22011
+ method: "POST",
22012
+ body: version2 === void 0 ? {} : { version: version2 },
22013
+ context: "Roll back a function",
22014
+ timeoutMs: PROVISION_TIMEOUT_MS
22015
+ }
22016
+ );
22017
+ return body.function;
22018
+ }
22019
+ /**
22020
+ * `verifyJwt` without an upload, live within seconds; the next deploy sets
22021
+ * it again from the version it puts live. `FUNCTION_NOT_FOUND`,
22022
+ * `FUNCTION_BUSY`, `FUNCTION_UPDATE_FAILED` (502: the function list in the
22023
+ * project's database could not be written — nothing changed, send it again)
22024
+ * and `BACKEND_OUTDATED`.
22025
+ */
22026
+ async update(projectId, name, patch) {
22027
+ const body = await this.client.request(
22028
+ this.functionPath(projectId, name),
22029
+ { method: "PATCH", body: patch, context: "Update a function" }
22030
+ );
22031
+ return body.function;
22032
+ }
22033
+ /**
22034
+ * Its URL stops working within seconds; its stored versions are removed by
22035
+ * the control plane's sweep. `FUNCTION_NOT_FOUND`, `FUNCTION_BUSY`.
22036
+ */
22037
+ async remove(projectId, name) {
22038
+ await this.client.request(
22039
+ this.functionPath(projectId, name),
22040
+ {
22041
+ method: "DELETE",
22042
+ context: "Delete a function"
22043
+ }
22044
+ );
22045
+ }
22046
+ /**
22047
+ * The function's newest 200 log lines, optionally of one level (`log`,
22048
+ * `info`, `warn`, `error` or `debug`; anything else is `400
22049
+ * VALIDATION_FAILED`). `LOGS_DISABLED` (503) while the control plane has no
22050
+ * log tail.
22051
+ */
22052
+ async logs(projectId, name, options = {}) {
22053
+ const query = options.level ? `?${new URLSearchParams({ level: options.level }).toString()}` : "";
22054
+ const body = await this.client.request(
22055
+ this.functionPath(projectId, name, `/logs${query}`),
22056
+ { context: "Read function logs" }
22057
+ );
22058
+ return body.entries;
22059
+ }
22060
+ /** The project's function secrets: names, digests and when — never a value. `env.read`. */
22061
+ async secrets(projectId) {
22062
+ const body = await this.client.request(
22063
+ this.path(projectId, "-secrets"),
22064
+ { context: "List function secrets" }
22065
+ );
22066
+ return body.secrets;
22067
+ }
22068
+ /**
22069
+ * Sets and unsets function secrets in one change (`env.write.production`;
22070
+ * a suspended project is refused). `VALIDATION_FAILED` names the first
22071
+ * problem — a name, a reserved name (`NARO_*`, `ASSETS`, `DB`) or
22072
+ * `__proto__`, a value over 5,120 bytes, more than 100 secrets. Live
22073
+ * functions carry the change once synced: `pending` says how many. The
22074
+ * control plane starts two at once and its five-minute sweep goes on with
22075
+ * the rest, 20 a run; `sync` brings them up now — call it until `pending`
22076
+ * is 0 to be sure, as a change racing another can leave a function only
22077
+ * `sync` finds (known-issues #322).
22078
+ */
22079
+ async setSecrets(projectId, change) {
22080
+ return this.client.request(
22081
+ this.path(projectId, "-secrets"),
22082
+ { method: "PUT", body: change, context: "Change function secrets" }
22083
+ );
22084
+ }
22085
+ /**
22086
+ * Uploads stale functions again — `limit` of them — and with `registry`
22087
+ * writes the function list in the project's database again. Call it
22088
+ * again while `pending` is above 0 and the call made progress.
22089
+ * `FUNCTIONS_RATE_LIMITED` (503) carries `details.retryAfter`. Two minutes.
22090
+ */
22091
+ async sync(projectId, options = {}) {
22092
+ return this.client.request(
22093
+ this.path(projectId, "-sync"),
22094
+ {
22095
+ method: "POST",
22096
+ body: options,
22097
+ context: "Sync functions",
22098
+ timeoutMs: PROVISION_TIMEOUT_MS
22099
+ }
22100
+ );
22101
+ }
22102
+ };
21618
22103
 
21619
22104
  // ../../packages/sdk/src/database.ts
21620
22105
  var BOOKMARK_RE = /^[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{8}-[0-9a-f]{32}$/;
@@ -21637,8 +22122,8 @@ var MAX_CELL_CHARS = 60;
21637
22122
  function formatCell(value) {
21638
22123
  if (value === null || value === void 0) return "NULL";
21639
22124
  const text2 = typeof value === "string" ? value : typeof value === "object" ? JSON.stringify(value) : String(value);
21640
- const oneLine = text2.replace(/\s+/g, " ");
21641
- return oneLine.length > MAX_CELL_CHARS ? `${oneLine.slice(0, MAX_CELL_CHARS - 1)}\u2026` : oneLine;
22125
+ const oneLine2 = text2.replace(/\s+/g, " ");
22126
+ return oneLine2.length > MAX_CELL_CHARS ? `${oneLine2.slice(0, MAX_CELL_CHARS - 1)}\u2026` : oneLine2;
21642
22127
  }
21643
22128
  function renderRows(statement2) {
21644
22129
  if (statement2.columns.length === 0) return [];
@@ -22056,7 +22541,7 @@ function isWordChar(char) {
22056
22541
  function asciiUpper2(text2) {
22057
22542
  return text2.replace(/[a-z]/g, (letter) => letter.toUpperCase());
22058
22543
  }
22059
- function quoted(sql, start, close, doubles) {
22544
+ function quoted2(sql, start, close, doubles) {
22060
22545
  let value = "";
22061
22546
  for (let index = start + 1; index < sql.length; index++) {
22062
22547
  const char = sql.charAt(index);
@@ -22089,7 +22574,7 @@ function tokenize(sql) {
22089
22574
  index = end + 2;
22090
22575
  } else if (char === '"' || char === "`" || char === "[" || char === "'") {
22091
22576
  const close = char === "[" ? "]" : char;
22092
- const read = quoted(sql, index, close, char !== "[");
22577
+ const read = quoted2(sql, index, close, char !== "[");
22093
22578
  if (!read) return void 0;
22094
22579
  tokens.push({
22095
22580
  kind: char === "'" ? "string" : "name",
@@ -24179,9 +24664,9 @@ function readCondition(stream) {
24179
24664
  const token = stream.peek();
24180
24665
  const state = newState(stream);
24181
24666
  const expr = readOr(state);
24182
- const bare = state.foldernames.find(({ node }) => node.index === null);
24183
- if (bare)
24184
- throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, bare.token.position);
24667
+ const bare2 = state.foldernames.find(({ node }) => node.index === null);
24668
+ if (bare2)
24669
+ throw new PolicySyntaxError(FOLDERNAME_INDEX_PROBLEM, bare2.token.position);
24185
24670
  return requireCondition(expr, token);
24186
24671
  }
24187
24672
  function checkExpressionSize(expr, position) {
@@ -24589,8 +25074,8 @@ function policiesOn(policies, table) {
24589
25074
  const folded = asciiLower(table);
24590
25075
  return policies.filter((policy) => asciiLower(policy.table) === folded);
24591
25076
  }
24592
- function foldPolicyName(name, quoted2) {
24593
- return quoted2 ? name : asciiLower(name);
25077
+ function foldPolicyName(name, quoted3) {
25078
+ return quoted3 ? name : asciiLower(name);
24594
25079
  }
24595
25080
  function sqlStringLiteral(text2) {
24596
25081
  return text2 === null ? "NULL" : quoteString(text2);
@@ -28258,10 +28743,10 @@ function headTokens(text2) {
28258
28743
  const char = text2.charAt(index);
28259
28744
  if (index >= text2.length) break;
28260
28745
  if (char === '"' || char === "`" || char === "'") {
28261
- const quoted2 = readQuoted(text2, index, char);
28262
- if (!quoted2) break;
28263
- tokens.push({ kind: "name", text: quoted2.text });
28264
- index = quoted2.end;
28746
+ const quoted3 = readQuoted(text2, index, char);
28747
+ if (!quoted3) break;
28748
+ tokens.push({ kind: "name", text: quoted3.text });
28749
+ index = quoted3.end;
28265
28750
  } else if (char === "[") {
28266
28751
  const close = text2.indexOf("]", index + 1);
28267
28752
  if (close < 0) break;
@@ -28428,6 +28913,13 @@ function readSqlHead(text2) {
28428
28913
  return OTHER;
28429
28914
  }
28430
28915
 
28916
+ // ../../packages/data-core/src/functions-registry.ts
28917
+ var FUNCTIONS_TABLE = "_naro_functions";
28918
+ var FUNCTIONS_REGISTRY_VERSION = 9;
28919
+ var FUNCTIONS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS ${FUNCTIONS_TABLE} (name TEXT NOT NULL PRIMARY KEY CHECK (name GLOB '[a-z]*' AND length(name) BETWEEN 1 AND 64), verify_jwt INTEGER NOT NULL DEFAULT 1 CHECK (verify_jwt IN (0, 1)), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')))`;
28920
+ var MAX_REGISTERED_FUNCTIONS = 2e3;
28921
+ var FUNCTIONS_REGISTRY_SQL = `SELECT name, verify_jwt FROM ${FUNCTIONS_TABLE} ORDER BY name LIMIT ${String(MAX_REGISTERED_FUNCTIONS)}`;
28922
+
28431
28923
  // ../../packages/data-core/src/policy-index.ts
28432
28924
  var INDEX_SQL = `SELECT t.name AS table_name, il.name AS index_name, il."partial" AS partial, ix.name AS column_name, ix.coll AS collation FROM pragma_table_list AS t JOIN pragma_index_list(t.name, t.schema) AS il JOIN pragma_index_xinfo(il.name, t.schema) AS ix WHERE t.schema = 'main' AND t.type = 'table' AND ${systemTablePredicate("t.name")} AND ix.seqno = 0`;
28433
28925
  function indexesFromRows(rows) {
@@ -30069,13 +30561,13 @@ function ownerComment(text2, position) {
30069
30561
  if (rest !== "" && !/^[ \t]/.test(rest)) return void 0;
30070
30562
  let column2 = rest.replace(/^[ \t]+|[ \t]+$/g, "");
30071
30563
  if (column2.startsWith('"')) {
30072
- const quoted2 = /^"((?:[^"]|"")*)"$/.exec(column2);
30073
- if (!quoted2)
30564
+ const quoted3 = /^"((?:[^"]|"")*)"$/.exec(column2);
30565
+ if (!quoted3)
30074
30566
  throw new PolicySyntaxError(
30075
30567
  "naro:owner needs one column name, bare or in double quotes",
30076
30568
  position
30077
30569
  );
30078
- column2 = (quoted2[1] ?? "").replaceAll('""', '"');
30570
+ column2 = (quoted3[1] ?? "").replaceAll('""', '"');
30079
30571
  }
30080
30572
  if (column2 === "")
30081
30573
  throw new PolicySyntaxError("naro:owner needs a column name", position);
@@ -30282,6 +30774,15 @@ var SYSTEM_MIGRATIONS = [
30282
30774
  version: REALTIME_TABLES_VERSION,
30283
30775
  name: "realtime",
30284
30776
  statements: [REALTIME_TABLES_SQL]
30777
+ },
30778
+ {
30779
+ // Functions 3-3a (design 2026-10-29 §6.3): the function registry the
30780
+ // gate reads. Every backend gets it, Functions on or not; the control
30781
+ // plane (3-3b) writes it. A worker from before it never reads it, and a
30782
+ // worker that reads it on a D1 without it answers 409 BACKEND_OUTDATED.
30783
+ version: FUNCTIONS_REGISTRY_VERSION,
30784
+ name: "functions",
30785
+ statements: [FUNCTIONS_TABLE_SQL]
30285
30786
  }
30286
30787
  ];
30287
30788
 
@@ -31053,13 +31554,13 @@ function compareMigrations(local, applied) {
31053
31554
  rows.push({ name, status: "applied", appliedAt: exact.appliedAt });
31054
31555
  continue;
31055
31556
  }
31056
- const bare = ledger.get(name.replace(/\.sql$/, ""));
31057
- if (bare) {
31058
- ledger.delete(bare.name);
31557
+ const bare2 = ledger.get(name.replace(/\.sql$/, ""));
31558
+ if (bare2) {
31559
+ ledger.delete(bare2.name);
31059
31560
  rows.push({
31060
31561
  name,
31061
31562
  status: "applied",
31062
- appliedAt: bare.appliedAt,
31563
+ appliedAt: bare2.appliedAt,
31063
31564
  warning: "ledger-name-missing-extension"
31064
31565
  });
31065
31566
  continue;
@@ -32002,6 +32503,10 @@ function failureCode(error51, fallback) {
32002
32503
  const code = error51 && typeof error51 === "object" ? error51.code : void 0;
32003
32504
  return typeof code === "string" && DEPLOYMENT_CODES.has(code) ? code : fallback;
32004
32505
  }
32506
+ var SYSTEM_NAMES = new Set(NARO_SYSTEM_ENV_NAMES);
32507
+ function isNaroSystemName(key) {
32508
+ return SYSTEM_NAMES.has(key);
32509
+ }
32005
32510
  function slugify2(value) {
32006
32511
  const slug = value.replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 54);
32007
32512
  return slug || "app";
@@ -32184,6 +32689,16 @@ async function deployDirectory(sourceDir, options, log2) {
32184
32689
  const provided = options.env ?? {};
32185
32690
  missing = scanned.filter((key) => !existing.has(key) && !(key in provided));
32186
32691
  }
32692
+ if (environment !== void 0 && options.projectId && missing.some((key) => isNaroSystemName(key))) {
32693
+ const backend = await client.backend.info(options.projectId).catch(() => null);
32694
+ if (backend !== null && backend.status !== "none") {
32695
+ const set2 = missing.filter((key) => isNaroSystemName(key));
32696
+ missing = missing.filter((key) => !isNaroSystemName(key));
32697
+ log2(
32698
+ `\u25B8 naro sets ${set2.join(", ")} for ${environment} to its own backend's: not asked for`
32699
+ );
32700
+ }
32701
+ }
32187
32702
  let collected = {};
32188
32703
  if (missing.length > 0) {
32189
32704
  log2(
@@ -32229,7 +32744,16 @@ async function deployDirectory(sourceDir, options, log2) {
32229
32744
  repositoryUrl: sourceDir,
32230
32745
  branch: ctx.branch,
32231
32746
  rootDirectory: options.rootDirectory ?? null,
32232
- env: { ...ctx.env, ...envToSend },
32747
+ // An environment's deploy: the control plane saved what this sent for
32748
+ // that environment and then chose its values, so a key sent and a key
32749
+ // chosen usually hold the same value — except the names naro sets for
32750
+ // an environment with a backend (named environments spec §8.3), which
32751
+ // must win here too, or this build would call another backend than
32752
+ // the worker's bindings, and a key the server failed to save
32753
+ // (`envSkipped`), whose stored value the worker gets and so this build
32754
+ // takes too. Production and preview keep a value sent over the stored
32755
+ // one, as before.
32756
+ env: ctx.environment ? { ...envToSend, ...ctx.env } : { ...ctx.env, ...envToSend },
32233
32757
  strategy: ctx.strategy ?? null,
32234
32758
  ...ctx.vinextEnabled === void 0 ? {} : { vinextEnabled: ctx.vinextEnabled }
32235
32759
  },
@@ -32320,10 +32844,12 @@ function deploymentUrl(detail, ctx, production) {
32320
32844
  }
32321
32845
 
32322
32846
  export {
32847
+ environmentGuidance,
32323
32848
  parseGitHubRemote,
32324
32849
  schemaMigrationTitle,
32325
32850
  projectPath,
32326
32851
  splitStatements,
32852
+ environmentSlugProblem,
32327
32853
  ANALYTICS_FILTER_KEYS,
32328
32854
  NaroError,
32329
32855
  promoteTargetUnsupported,