@lukoweb/apitogo 0.2.39 → 0.2.41

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/dist/cli/cli.js CHANGED
@@ -5463,6 +5463,7 @@ import colors4 from "picocolors";
5463
5463
  import { gt } from "semver";
5464
5464
 
5465
5465
  // src/cli/common/json-only-deploy.ts
5466
+ init_logger();
5466
5467
  function isDeployJsonOnlyArgv() {
5467
5468
  const args = process.argv;
5468
5469
  let cmdIdx = -1;
@@ -5478,6 +5479,14 @@ function isDeployJsonOnlyArgv() {
5478
5479
  }
5479
5480
  return args.slice(cmdIdx).some((a) => a === "--json-only" || a.startsWith("--json-only="));
5480
5481
  }
5482
+ function applyJsonOnlyDeployQuietMode() {
5483
+ process.env.APITOGO_JSON_ONLY = "1";
5484
+ const noop = () => {
5485
+ };
5486
+ logger.info = noop;
5487
+ logger.warn = noop;
5488
+ logger.warnOnce = noop;
5489
+ }
5481
5490
 
5482
5491
  // src/cli/common/output.ts
5483
5492
  import * as Sentry from "@sentry/node";
@@ -5886,6 +5895,8 @@ var PortalSchema = z.strictObject({
5886
5895
  }).optional(),
5887
5896
  console: z.strictObject({ enabled }).optional(),
5888
5897
  theme: path3.optional(),
5898
+ /** A custom home page: a React component file that the portal serves at `/`. */
5899
+ landing: path3.optional(),
5889
5900
  requireAuth: z.union([z.boolean(), z.strictObject({ exclude: pathList })], {
5890
5901
  error: 'must be true, false or { "exclude": ["/route", ...] }'
5891
5902
  }).optional(),
@@ -6447,6 +6458,7 @@ function resolveConfigV2(config2, configPath) {
6447
6458
  },
6448
6459
  console: { enabled: portal.console?.enabled ?? true },
6449
6460
  theme: portal.theme ? path7.resolve(configDir, portal.theme) : path7.join(dir, "theme.json"),
6461
+ ...portal.landing ? { landing: path7.resolve(configDir, portal.landing) } : {},
6450
6462
  requireAuth: portal.requireAuth ?? false,
6451
6463
  search: provider === "external" ? "none" : portal.search ?? "pagefind"
6452
6464
  };
@@ -6563,7 +6575,7 @@ function loadConfigV2(pathOrDir) {
6563
6575
  }
6564
6576
 
6565
6577
  // src/config/v2/rules.ts
6566
- import { readFile as readFile2, stat as stat2 } from "node:fs/promises";
6578
+ import { readdir as readdir2, readFile as readFile2, stat as stat2 } from "node:fs/promises";
6567
6579
  import path9 from "node:path";
6568
6580
  import { glob } from "glob";
6569
6581
  import { parse as parseYaml } from "yaml";
@@ -6575,6 +6587,10 @@ async function isFile(target) {
6575
6587
  async function isDirectory(target) {
6576
6588
  return (await stat2(target).catch(() => void 0))?.isDirectory() === true;
6577
6589
  }
6590
+ async function isEmptyFolder(target) {
6591
+ const entries = await readdir2(target).catch(() => []);
6592
+ return entries.every((name) => name === ".gitkeep");
6593
+ }
6578
6594
  function collectSecretRefs(config2) {
6579
6595
  const found = [];
6580
6596
  const walk = (value, at) => {
@@ -6816,6 +6832,13 @@ async function gatewayRules(resolved, out) {
6816
6832
  message: `gateway.origin.source does not exist: ${inside === "" ? "." : inside}.`,
6817
6833
  hint: "Point it at the folder that holds your backend code."
6818
6834
  });
6835
+ } else if (await isEmptyFolder(origin.source)) {
6836
+ out.warn({
6837
+ code: "GATEWAY_BACKEND_EMPTY",
6838
+ path: "gateway.origin.source",
6839
+ message: `gateway.origin.source (${inside}) is empty: there is no code to run yet.`,
6840
+ hint: "Put your backend code in that folder before you deploy."
6841
+ });
6819
6842
  }
6820
6843
  }
6821
6844
  if (origin.url !== void 0) {
@@ -7934,7 +7957,9 @@ function planPortal(profile, manifest) {
7934
7957
  keysApiBaseUrl: plans.keysApiBaseUrl,
7935
7958
  billing: plans.billing,
7936
7959
  links: {
7937
- ...profile.modules.docs ? { docs: "/introduction" } : profile.docs.url ? { docs: profile.docs.url } : {},
7960
+ // Hosted docs are linked once their pages are known (see `buildPortalWorkspace`): there is no page to
7961
+ // name before that, and a guessed one was a 404 on every portal whose first page is not called `introduction`.
7962
+ ...!profile.modules.docs && profile.docs.url ? { docs: profile.docs.url } : {},
7938
7963
  ...plan2.apiReference ? { api: `/${API_REFERENCE_PATH}` } : {}
7939
7964
  }
7940
7965
  };
@@ -7964,6 +7989,15 @@ function buildSiteConfig(resolved, plan2, theme, extras = {}) {
7964
7989
  },
7965
7990
  modules: {
7966
7991
  docs: { enabled: plan2.docs },
7992
+ // A custom home page (`portal.landing`) is served at `/`; the build imports the component from its absolute path.
7993
+ ...portal?.landing ? {
7994
+ landing: {
7995
+ enabled: true,
7996
+ path: "/",
7997
+ layout: "landing",
7998
+ page: portal.landing.split(path12.sep).join("/")
7999
+ }
8000
+ } : {},
7967
8001
  // The plans pages (pricing, choosing a plan) exist only when billing is on, and the plans service is what makes it so.
7968
8002
  // Switching billing on also makes the user panel register its own placeholder plans page, at `/account/<path>`.
7969
8003
  // Its default path is `plans`, which is where the billing plugin puts the real page (subscribe, switch, cancel),
@@ -7992,6 +8026,14 @@ function buildSiteConfig(resolved, plan2, theme, extras = {}) {
7992
8026
  ]
7993
8027
  } : {},
7994
8028
  ...navigation.length > 0 ? { navigation } : {},
8029
+ // The sidebar link above is only seen inside the docs; the header is what every page shows.
8030
+ ...plan2.apiReference ? {
8031
+ header: {
8032
+ navigation: [
8033
+ { label: "API Reference", to: `/${API_REFERENCE_PATH}` }
8034
+ ]
8035
+ }
8036
+ } : {},
7995
8037
  ...redirects.length > 0 ? { redirects } : {},
7996
8038
  ...extras.protectedRoutes?.length ? { protectedRoutes: extras.protectedRoutes } : {},
7997
8039
  ...theme !== void 0 ? { theme } : {},
@@ -8100,7 +8142,14 @@ async function preparePortal(resolved, manifest, mode, options = {}) {
8100
8142
  const hostedDocRoutes = plan2.docs ? await docRoutes(profile.docs.content, {
8101
8143
  includeDrafts: mode === "development"
8102
8144
  }) : [];
8103
- const home = homeRedirect({
8145
+ if (plan2.plansPlugin && plan2.docs) {
8146
+ const landing = hostedDocRoutes.some(
8147
+ (route) => route === "/" || route === "/index"
8148
+ ) ? "/" : pickLandingRoute(hostedDocRoutes);
8149
+ if (landing)
8150
+ plan2.plansPlugin.links = { ...plan2.plansPlugin.links, docs: landing };
8151
+ }
8152
+ const home = resolved.portal?.landing ? void 0 : homeRedirect({
8104
8153
  hostsDocs: plan2.docs,
8105
8154
  docRoutes: hostedDocRoutes,
8106
8155
  externalDocsUrl: profile.docs.url,
@@ -8277,8 +8326,156 @@ async function portalArtifactHash(resolved, manifestText, cliVersion, wrapper =
8277
8326
 
8278
8327
  // src/config/v2/services.ts
8279
8328
  init_invariant();
8280
- import { access as access3, mkdir as mkdir5, rename as rename3, rm as rm4, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
8329
+ import {
8330
+ access as access3,
8331
+ mkdir as mkdir5,
8332
+ readdir as readdir3,
8333
+ rename as rename3,
8334
+ rm as rm4,
8335
+ stat as stat3,
8336
+ writeFile as writeFile7
8337
+ } from "node:fs/promises";
8281
8338
  import path14 from "node:path";
8339
+
8340
+ // src/config/v2/starter-landing.ts
8341
+ function starterLandingPage(options) {
8342
+ const name = JSON.stringify(options.name);
8343
+ const plansImport = options.withPlans ? 'import { usePlansPreview } from "@lukoweb/apitogo-plugin-dev-portal-billing";\n' : "";
8344
+ const plansHook = options.withPlans ? " // The plans of the project's catalog: the same ones the pricing page shows.\n const plans = usePlansPreview();\n\n" : "";
8345
+ const plansSection = options.withPlans ? " <PlansPreviewSection plans={plans} />\n" : "";
8346
+ return `import {
8347
+ ApiShowcaseSection,
8348
+ FeaturesSection,
8349
+ HeroSection,
8350
+ LandingShell,
8351
+ ${options.withPlans ? "PlansPreviewSection,\n " : ""}PricingCtaSection,
8352
+ StepsSection,
8353
+ } from "@lukoweb/apitogo-module-landing";
8354
+ ${plansImport}
8355
+ const NAME = ${name};
8356
+
8357
+ export default function LandingPage() {
8358
+ ${plansHook} return (
8359
+ <LandingShell
8360
+ title={NAME}
8361
+ description="Documentation, an API reference, sign-in, API keys and plans for your API, in one place."
8362
+ >
8363
+ <HeroSection
8364
+ hero={{
8365
+ badge: "API PORTAL",
8366
+ title: NAME,
8367
+ description:
8368
+ "Everything your developers need to use your API: documentation, an interactive reference, sign-in, API keys and plans.",
8369
+ }}
8370
+ primaryAction={{ label: "Get started", href: "/signup" }}
8371
+ secondaryAction={{ label: "Read the docs", href: "/introduction" }}
8372
+ codeExamples={[
8373
+ {
8374
+ filename: "Call the API",
8375
+ language: "bash",
8376
+ code: \`$ curl https://your-api.example.com/health \\\\
8377
+ -H "x-api-key: YOUR_API_KEY"
8378
+ # { "status": "ok" }\`,
8379
+ },
8380
+ ]}
8381
+ />
8382
+ <FeaturesSection
8383
+ section={{
8384
+ label: "What you get",
8385
+ title: "A developer portal that is ready on day one",
8386
+ description:
8387
+ "Edit this page in apitogo/portal/landing/LandingPage.tsx to make it yours.",
8388
+ }}
8389
+ features={[
8390
+ {
8391
+ title: "Documentation",
8392
+ description:
8393
+ "Write pages in Markdown or MDX and they appear in the portal with a sidebar and search.",
8394
+ icon: "book-open",
8395
+ },
8396
+ {
8397
+ title: "API reference",
8398
+ description:
8399
+ "An interactive reference built from your OpenAPI file, with a try-it console.",
8400
+ icon: "code",
8401
+ },
8402
+ {
8403
+ title: "API keys",
8404
+ description:
8405
+ "People sign up, pick a plan and create keys themselves, with no work for you.",
8406
+ icon: "shield-check",
8407
+ },
8408
+ {
8409
+ title: "Plans and limits",
8410
+ description:
8411
+ "Rate limits and monthly quotas per plan, enforced at the gateway.",
8412
+ icon: "gauge",
8413
+ },
8414
+ {
8415
+ title: "Usage",
8416
+ description:
8417
+ "Each developer sees their own request counts and how much of the plan is left.",
8418
+ icon: "activity",
8419
+ },
8420
+ {
8421
+ title: "Sign-in",
8422
+ description:
8423
+ "Accounts and login pages that carry your name and theme.",
8424
+ icon: "boxes",
8425
+ },
8426
+ ]}
8427
+ />
8428
+ <StepsSection
8429
+ steps={[
8430
+ {
8431
+ step: "01",
8432
+ title: "Create an account",
8433
+ description: "Sign up in a minute, no approval queue.",
8434
+ },
8435
+ {
8436
+ step: "02",
8437
+ title: "Pick a plan",
8438
+ description: "Start on the free plan and move up when you need more.",
8439
+ },
8440
+ {
8441
+ step: "03",
8442
+ title: "Call the API",
8443
+ description: "Create a key and make your first request.",
8444
+ },
8445
+ ]}
8446
+ />
8447
+ <ApiShowcaseSection
8448
+ apiShowcase={{
8449
+ title: "Sample endpoints",
8450
+ subtitle: "A slice of what is available. The full list is in the API reference.",
8451
+ browseAllHref: "/api",
8452
+ browseAllLabel: "Browse all APIs",
8453
+ layout: "list",
8454
+ items: [
8455
+ {
8456
+ name: "Health check",
8457
+ path: "/health",
8458
+ method: "GET",
8459
+ summary: "Tells you the service is up.",
8460
+ },
8461
+ ],
8462
+ }}
8463
+ />
8464
+ ${plansSection} <PricingCtaSection
8465
+ variant="banner"
8466
+ pricingCta={{
8467
+ title: "Start building today",
8468
+ description: "Create an account and get your API key in minutes.",
8469
+ primaryAction: { label: "Get started", href: "/signup" },
8470
+ }}
8471
+ />
8472
+ </LandingShell>
8473
+ );
8474
+ }
8475
+ `;
8476
+ }
8477
+
8478
+ // src/config/v2/services.ts
8282
8479
  var SERVICES = ["portal", "gateway", "identity", "plans"];
8283
8480
  var ServiceEditError = class extends ApitogoError {
8284
8481
  constructor(message, developerHint, missingServices = []) {
@@ -8288,11 +8485,34 @@ var ServiceEditError = class extends ApitogoError {
8288
8485
  }
8289
8486
  };
8290
8487
  var httpsProblem = (value) => /^https:\/\/\S+$/i.test(value) ? void 0 : "The address must start with https://";
8291
- var DEFAULT_RUNTIME_VERSION = {
8292
- nodejs: "NODE_22_LTS",
8293
- python: "PYTHON_312",
8294
- dotnet: "DOTNET_8"
8488
+ var RUNTIME_VERSIONS = {
8489
+ nodejs: [
8490
+ { value: "NODE_22_LTS", label: "Node.js 22 (LTS)" },
8491
+ { value: "NODE_24_LTS", label: "Node.js 24 (LTS)" }
8492
+ ],
8493
+ python: [
8494
+ { value: "PYTHON_312", label: "Python 3.12" },
8495
+ { value: "PYTHON_310", label: "Python 3.10" },
8496
+ { value: "PYTHON_311", label: "Python 3.11" },
8497
+ { value: "PYTHON_313", label: "Python 3.13" },
8498
+ { value: "PYTHON_314", label: "Python 3.14" }
8499
+ ],
8500
+ dotnet: [
8501
+ { value: "DOTNET_8", label: ".NET 8" },
8502
+ { value: "DOTNET_9", label: ".NET 9" },
8503
+ { value: "DOTNET_10", label: ".NET 10" }
8504
+ ]
8295
8505
  };
8506
+ function runtimeVersionSlug(runtime, value) {
8507
+ const upper = value.trim().toUpperCase();
8508
+ const known = RUNTIME_VERSIONS[runtime].map(
8509
+ (choice) => choice.value
8510
+ );
8511
+ if (known.includes(upper)) return upper;
8512
+ const digits = value.trim().toLowerCase().replace(/^(v|python|\.net|dotnet|node(js)?)[ _-]?/, "").replace(/(\.x|_lts| lts)$/, "");
8513
+ const candidate = runtime === "nodejs" ? `NODE_${digits.split(".")[0]}_LTS` : runtime === "python" ? `PYTHON_${digits.replace(".", "")}` : `DOTNET_${digits.split(".")[0]}`;
8514
+ return known.includes(candidate) ? candidate : value;
8515
+ }
8296
8516
  var SERVICE_LABELS = {
8297
8517
  portal: "Developer website",
8298
8518
  gateway: "API protection",
@@ -8341,6 +8561,31 @@ var PLAN_TEMPLATES = {
8341
8561
  }
8342
8562
  }
8343
8563
  ],
8564
+ // Sample plans for the starter: a free plan and two with a price on request, so they show on the pricing page and
8565
+ // the home page and still deploy on a Free project (a plan with a price needs a paid platform plan and billing).
8566
+ starter: [
8567
+ {
8568
+ sku: "free",
8569
+ displayName: "Free",
8570
+ default: true,
8571
+ limits: {
8572
+ rateLimit: { calls: 3, periodSeconds: 60 },
8573
+ quota: { calls: 1e3, periodDays: 30 }
8574
+ }
8575
+ },
8576
+ {
8577
+ sku: "pro",
8578
+ displayName: "Pro",
8579
+ pricingModel: "custom",
8580
+ priceLabel: "Contact us"
8581
+ },
8582
+ {
8583
+ sku: "enterprise",
8584
+ displayName: "Enterprise",
8585
+ pricingModel: "custom",
8586
+ priceLabel: "Contact us"
8587
+ }
8588
+ ],
8344
8589
  tiered: [
8345
8590
  {
8346
8591
  sku: "free",
@@ -8411,6 +8656,7 @@ async function buildPortal(context) {
8411
8656
  }
8412
8657
  const name = o.name?.trim() || resolved.project.name;
8413
8658
  let content = [];
8659
+ let landing;
8414
8660
  if (!external) {
8415
8661
  if (Array.isArray(o.docs) && o.docs.length > 0) {
8416
8662
  content = await docsFromPaths(dir, o.docs);
@@ -8442,7 +8688,28 @@ async function buildPortal(context) {
8442
8688
  const pages = path14.join(resolved.root, "portal", "pages");
8443
8689
  if (template === "blank")
8444
8690
  await addFile(context, path14.join(pages, ".gitkeep"), "");
8445
- else
8691
+ else if (options.template === "starter") {
8692
+ await addFile(
8693
+ context,
8694
+ path14.join(pages, "introduction.mdx"),
8695
+ starterPage(name)
8696
+ );
8697
+ const landingFile = path14.join(
8698
+ resolved.root,
8699
+ "portal",
8700
+ "landing",
8701
+ "LandingPage.tsx"
8702
+ );
8703
+ await addFile(
8704
+ context,
8705
+ landingFile,
8706
+ starterLandingPage({
8707
+ name,
8708
+ withPlans: context.config.plans !== void 0 && context.config.plans.enabled !== false
8709
+ })
8710
+ );
8711
+ landing = rel(dir, landingFile);
8712
+ } else
8446
8713
  await addFile(
8447
8714
  context,
8448
8715
  path14.join(pages, "index.mdx"),
@@ -8454,11 +8721,22 @@ async function buildPortal(context) {
8454
8721
  ...options.disabled ? { enabled: false } : {},
8455
8722
  name,
8456
8723
  ...external ? { docs: { provider: "external", url: docsUrl } } : content.length > 0 ? { docs: { content } } : {},
8724
+ ...landing ? { landing } : {},
8457
8725
  ...o.noConsole ? { console: { enabled: false } } : {},
8458
8726
  ...o.requireAuth ? { requireAuth: true } : {},
8459
8727
  ...o.search && !external ? { search: o.search } : {}
8460
8728
  };
8461
8729
  }
8730
+ async function createSourceFolder(context, sourcePath) {
8731
+ const folder = path14.join(context.dir, sourcePath);
8732
+ const entries = await readdir3(folder).catch(() => []);
8733
+ if (entries.some((name) => name !== ".gitkeep")) return;
8734
+ if (entries.length === 0)
8735
+ await addFile(context, path14.join(folder, ".gitkeep"), "");
8736
+ context.notes.push(
8737
+ `The folder ${sourcePath}/ is empty: put your backend code there before you deploy.`
8738
+ );
8739
+ }
8462
8740
  async function buildGateway(context) {
8463
8741
  const { options, dir, resolved } = context;
8464
8742
  const o = options.gateway ?? {};
@@ -8499,22 +8777,28 @@ async function buildGateway(context) {
8499
8777
  { value: "python", label: "Python" },
8500
8778
  { value: "dotnet", label: ".NET" }
8501
8779
  ]) : void 0);
8502
- const runtimeVersion = o.runtimeVersion ?? (ask4 && runtime ? await ask4.text("Runtime version", {
8503
- default: DEFAULT_RUNTIME_VERSION[runtime]
8504
- }) : void 0);
8780
+ const runtimeVersion = o.runtimeVersion !== void 0 && runtime ? runtimeVersionSlug(runtime, o.runtimeVersion) : ask4 && runtime ? await ask4.choose(
8781
+ "Runtime version",
8782
+ RUNTIME_VERSIONS[runtime].map((choice, index) => ({
8783
+ value: choice.value,
8784
+ label: index === 0 ? `${choice.label} (suggested)` : choice.label
8785
+ }))
8786
+ ) : void 0;
8505
8787
  if (!source || !runtime || !runtimeVersion) {
8506
8788
  throw new ServiceEditError(
8507
8789
  "--origin managed needs --source, --runtime and --runtime-version.",
8508
- "For example: --source server --runtime nodejs --runtime-version NODE_22_LTS."
8790
+ runtime ? `For ${runtime}, --runtime-version is one of: ${RUNTIME_VERSIONS[runtime].map((choice) => choice.value).join(", ")} (a short form such as ${runtime === "nodejs" ? "22" : runtime === "python" ? "3.12" : "8"} works too).` : "For example: --source server --runtime nodejs --runtime-version NODE_22_LTS."
8509
8791
  );
8510
8792
  }
8793
+ const sourcePath = insideProject(dir, source, "Source folder");
8511
8794
  origin = {
8512
8795
  mode,
8513
- source: insideProject(dir, source, "Source folder"),
8796
+ source: sourcePath,
8514
8797
  runtime,
8515
8798
  runtimeVersion,
8516
8799
  ...o.exclude?.length ? { exclude: o.exclude } : {}
8517
8800
  };
8801
+ await createSourceFolder(context, sourcePath);
8518
8802
  }
8519
8803
  let specs;
8520
8804
  let requestedSpecs = o.spec ?? [];
@@ -8637,7 +8921,7 @@ async function buildIdentity(context) {
8637
8921
  async function buildPlans(context) {
8638
8922
  const { options, dir, resolved } = context;
8639
8923
  const o = options.plans ?? {};
8640
- const template = o.template ?? (options.template === "blank" ? "none" : options.template === "starter" ? "free" : "free");
8924
+ const template = o.template ?? (options.template === "blank" ? "none" : options.template === "starter" ? "starter" : "free");
8641
8925
  const billing = o.billing ?? (template === "tiered" ? "stripe" : "none");
8642
8926
  if (template === "tiered" && billing === "none") {
8643
8927
  throw new ServiceEditError(
@@ -9010,7 +9294,7 @@ init_invariant();
9010
9294
 
9011
9295
  // src/cli/check/v2.ts
9012
9296
  init_apitogo_config_core();
9013
- import { readFile as readFile14 } from "node:fs/promises";
9297
+ import { readFile as readFile15 } from "node:fs/promises";
9014
9298
  import path49 from "node:path";
9015
9299
 
9016
9300
  // src/cli/platform/client/platform-api.ts
@@ -9833,8 +10117,8 @@ var PlatformApiClient = class {
9833
10117
  }
9834
10118
  };
9835
10119
  async function readFileBytes(filePath) {
9836
- const { readFile: readFile28 } = await import("node:fs/promises");
9837
- const buffer = await readFile28(filePath);
10120
+ const { readFile: readFile29 } = await import("node:fs/promises");
10121
+ const buffer = await readFile29(filePath);
9838
10122
  const arrayBuffer = new ArrayBuffer(buffer.byteLength);
9839
10123
  new Uint8Array(arrayBuffer).set(buffer);
9840
10124
  return arrayBuffer;
@@ -9931,10 +10215,10 @@ function resolveProjectId(explicit, config2) {
9931
10215
  import {
9932
10216
  access as access4,
9933
10217
  mkdir as mkdir12,
9934
- readFile as readFile11,
10218
+ readFile as readFile12,
9935
10219
  rename as rename6,
9936
10220
  rm as rm8,
9937
- writeFile as writeFile14
10221
+ writeFile as writeFile15
9938
10222
  } from "node:fs/promises";
9939
10223
  import path48 from "node:path";
9940
10224
 
@@ -10087,7 +10371,7 @@ import {
10087
10371
  // package.json
10088
10372
  var package_default = {
10089
10373
  name: "@lukoweb/apitogo",
10090
- version: "0.2.39",
10374
+ version: "0.2.41",
10091
10375
  type: "module",
10092
10376
  sideEffects: [
10093
10377
  "**/*.css",
@@ -14935,6 +15219,7 @@ init_package_json();
14935
15219
 
14936
15220
  // src/cli/init/v2-packages.ts
14937
15221
  import { existsSync as existsSync5 } from "node:fs";
15222
+ import { readFile as readFile11, writeFile as writeFile14 } from "node:fs/promises";
14938
15223
  import path47 from "node:path";
14939
15224
  var CORE_PACKAGES = [
14940
15225
  "@lukoweb/apitogo",
@@ -14947,6 +15232,29 @@ var portalPackageSpecs = (version) => [
14947
15232
  "react-dom@19"
14948
15233
  ];
14949
15234
  var installCommand = (version) => `npm install --save-dev ${portalPackageSpecs(version).join(" ")}`;
15235
+ function packageNameFrom(name) {
15236
+ const slug = name.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "");
15237
+ return slug || "portal";
15238
+ }
15239
+ async function ensurePackageJson(dir, name) {
15240
+ const file = path47.join(dir, "package.json");
15241
+ if (existsSync5(file)) return false;
15242
+ await writeFile14(
15243
+ file,
15244
+ `${JSON.stringify(
15245
+ {
15246
+ name: packageNameFrom(name),
15247
+ version: "0.0.1",
15248
+ private: true,
15249
+ type: "module"
15250
+ },
15251
+ null,
15252
+ 2
15253
+ )}
15254
+ `
15255
+ );
15256
+ return true;
15257
+ }
14950
15258
  function isPackageInstalled(startDir, name) {
14951
15259
  let dir = path47.resolve(startDir);
14952
15260
  for (; ; ) {
@@ -15028,7 +15336,7 @@ function portalBuildStep(context, options = {}, build2 = runBuild) {
15028
15336
  await mkdir12(path48.dirname(buildDir), { recursive: true });
15029
15337
  await rename6(path48.join(root, "dist"), buildDir);
15030
15338
  if (prepared.publishedManifest) {
15031
- await writeFile14(
15339
+ await writeFile15(
15032
15340
  path48.join(buildDir, MANIFEST_FILE),
15033
15341
  `${JSON.stringify(prepared.publishedManifest, null, 2)}
15034
15342
  `
@@ -15058,7 +15366,7 @@ async function buildV2(options) {
15058
15366
  }
15059
15367
 
15060
15368
  // src/cli/check/gateway-online.ts
15061
- import { readFile as readFile12 } from "node:fs/promises";
15369
+ import { readFile as readFile13 } from "node:fs/promises";
15062
15370
  var SPEC_SEND_LIMIT = 5 * 1024 * 1024 + 64 * 1024;
15063
15371
  var issue2 = (code, path101, message, hint) => ({ code, path: path101, message, hint });
15064
15372
  function capabilityIssues(gateway, answer) {
@@ -15122,7 +15430,7 @@ async function onlineGatewayRules(gateway, projectId, api, derivedCors = []) {
15122
15430
  const specFile = gateway.specs[0];
15123
15431
  if (specFile) {
15124
15432
  try {
15125
- const text = await readFile12(specFile, "utf8");
15433
+ const text = await readFile13(specFile, "utf8");
15126
15434
  if (text.length <= SPEC_SEND_LIMIT) spec = text;
15127
15435
  } catch {
15128
15436
  }
@@ -15150,7 +15458,7 @@ async function onlineGatewayRules(gateway, projectId, api, derivedCors = []) {
15150
15458
 
15151
15459
  // src/cli/deploy/plans-file.ts
15152
15460
  init_invariant();
15153
- import { readFile as readFile13 } from "node:fs/promises";
15461
+ import { readFile as readFile14 } from "node:fs/promises";
15154
15462
  import { z as z14 } from "zod";
15155
15463
  var PlansFileError = class extends ApitogoError {
15156
15464
  constructor(code, message, developerHint) {
@@ -15194,7 +15502,7 @@ var issuesOf = (error) => error.issues.map(
15194
15502
  async function readPlansFile(file) {
15195
15503
  let text;
15196
15504
  try {
15197
- text = await readFile13(file, "utf8");
15505
+ text = await readFile14(file, "utf8");
15198
15506
  } catch {
15199
15507
  throw new PlansFileError(
15200
15508
  "PLANS_FILE_UNREADABLE",
@@ -15385,7 +15693,7 @@ async function onlinePlansRules(resolved, projectId, api) {
15385
15693
  async function isConfigV2(dir) {
15386
15694
  try {
15387
15695
  return /"version"\s*:\s*2\b/.test(
15388
- await readFile14(path49.join(dir, APITOGO_CONFIG_FILENAME), "utf8")
15696
+ await readFile15(path49.join(dir, APITOGO_CONFIG_FILENAME), "utf8")
15389
15697
  );
15390
15698
  } catch {
15391
15699
  return false;
@@ -15534,10 +15842,8 @@ async function validatePortalBuild(dir) {
15534
15842
  async function runCheckV2(dir, args = {}, deps = {}) {
15535
15843
  const configPath = path49.join(dir, APITOGO_CONFIG_FILENAME);
15536
15844
  let report;
15537
- let layout;
15538
15845
  try {
15539
15846
  const { config: config2, resolved } = loadConfigV2(dir);
15540
- layout = resolved.layout;
15541
15847
  const online = await fetchSecretNames(
15542
15848
  config2,
15543
15849
  resolved.project.id,
@@ -15584,7 +15890,6 @@ async function runCheckV2(dir, args = {}, deps = {}) {
15584
15890
  status: report.errors.length > 0 ? "failed" : "ok",
15585
15891
  configVersion: 2,
15586
15892
  configPath,
15587
- layout,
15588
15893
  ...report
15589
15894
  };
15590
15895
  }
@@ -15757,15 +16062,18 @@ import { existsSync as existsSync6 } from "node:fs";
15757
16062
  import path51 from "node:path";
15758
16063
  var USER_PANEL_PACKAGE = "@lukoweb/apitogo-module-user-panel";
15759
16064
  var BILLING_PLUGIN_PACKAGE = "@lukoweb/apitogo-plugin-dev-portal-billing";
16065
+ var LANDING_PACKAGE = "@lukoweb/apitogo-module-landing";
15760
16066
  function packagesForConfig(config2) {
15761
16067
  const portal = config2.portal;
15762
- if (!portal || portal.enabled === false || portal.console?.enabled === false)
15763
- return [];
16068
+ if (!portal || portal.enabled === false) return [];
16069
+ const landing = portal.landing ? [LANDING_PACKAGE] : [];
16070
+ if (portal.console?.enabled === false) return landing;
15764
16071
  const identity = config2.identity && config2.identity.enabled !== false;
15765
16072
  const plans = config2.plans && config2.plans.enabled !== false;
15766
16073
  return [
15767
16074
  ...identity || plans ? [USER_PANEL_PACKAGE] : [],
15768
- ...plans ? [BILLING_PLUGIN_PACKAGE] : []
16075
+ ...plans ? [BILLING_PLUGIN_PACKAGE] : [],
16076
+ ...landing
15769
16077
  ];
15770
16078
  }
15771
16079
  function isInstalled(dir, name) {
@@ -15783,10 +16091,20 @@ function detectPackageManager(dir) {
15783
16091
  if (existsSync6(path51.join(dir, "yarn.lock"))) return "yarn";
15784
16092
  return "npm";
15785
16093
  }
15786
- function installCommand2(manager, names, version) {
15787
- const specs = names.map((name) => `${name}@${version}`);
15788
- return manager === "npm" ? { command: "npm", args: ["install", ...specs] } : { command: manager, args: ["add", ...specs] };
16094
+ function installSpecsCommand(manager, specs) {
16095
+ return manager === "npm" ? {
16096
+ command: "npm",
16097
+ // Only errors: the audit summary and engine warnings of the dependencies are not about this project.
16098
+ args: [
16099
+ "install",
16100
+ "--no-audit",
16101
+ "--no-fund",
16102
+ "--loglevel=error",
16103
+ ...specs
16104
+ ]
16105
+ } : { command: manager, args: ["add", ...specs] };
15789
16106
  }
16107
+ var specName = (spec) => spec.slice(0, spec.lastIndexOf("@"));
15790
16108
  var runInstaller = (dir, { command: command2, args }) => new Promise((resolve, reject) => {
15791
16109
  const child = spawn(command2, args, {
15792
16110
  cwd: dir,
@@ -15803,10 +16121,24 @@ var runInstaller = (dir, { command: command2, args }) => new Promise((resolve, r
15803
16121
  )
15804
16122
  );
15805
16123
  });
16124
+ function packagesToInstall(dir, config2) {
16125
+ const portal = config2.portal && config2.portal.enabled !== false;
16126
+ return {
16127
+ core: portal ? missingCorePackages(dir) : [],
16128
+ plugins: missingPackages(dir, packagesForConfig(config2))
16129
+ };
16130
+ }
15806
16131
  async function installMissingPackages(dir, config2, version, options) {
15807
- const missing = missingPackages(dir, packagesForConfig(config2));
16132
+ const { core, plugins } = packagesToInstall(dir, config2);
16133
+ const missing = [...core, ...plugins];
15808
16134
  if (missing.length === 0) return void 0;
15809
- const command2 = installCommand2(detectPackageManager(dir), missing, version);
16135
+ const specs = [
16136
+ ...portalPackageSpecs(version).filter(
16137
+ (spec) => core.includes(specName(spec))
16138
+ ),
16139
+ ...plugins.map((name) => `${name}@${version}`)
16140
+ ];
16141
+ const command2 = installSpecsCommand(detectPackageManager(dir), specs);
15810
16142
  const shown = `${command2.command} ${command2.args.join(" ")}`;
15811
16143
  if (!options.install)
15812
16144
  return {
@@ -15815,13 +16147,7 @@ async function installMissingPackages(dir, config2, version, options) {
15815
16147
  reason: "--no-install was given",
15816
16148
  command: shown
15817
16149
  };
15818
- if (!existsSync6(path51.join(dir, "package.json")))
15819
- return {
15820
- status: "skipped",
15821
- packages: missing,
15822
- reason: "there is no package.json here",
15823
- command: shown
15824
- };
16150
+ await ensurePackageJson(dir, config2.project?.name ?? path51.basename(dir));
15825
16151
  await (options.installer ?? runInstaller)(dir, command2);
15826
16152
  return { status: "installed", packages: missing, command: shown };
15827
16153
  }
@@ -15983,7 +16309,7 @@ async function run(action, argv, plan2, deps = {}) {
15983
16309
  }
15984
16310
  }
15985
16311
  }
15986
- const needed = action === "add" ? missingPackages(dir, packagesForConfig(edit.config)) : [];
16312
+ const needed = action === "add" ? Object.values(packagesToInstall(dir, edit.config)).flat() : [];
15987
16313
  if (!json) {
15988
16314
  for (const line2 of formatServiceEdit(edit))
15989
16315
  printDiagnosticsToConsole(line2);
@@ -16446,7 +16772,7 @@ function formatDevPortalBackendResponse(projectId, backend) {
16446
16772
  // src/cli/platform/helpers/dev-portal-manifest.ts
16447
16773
  init_gateway_limits();
16448
16774
  import { existsSync as existsSync7 } from "node:fs";
16449
- import { access as access5, mkdir as mkdir13, readFile as readFile15, writeFile as writeFile15 } from "node:fs/promises";
16775
+ import { access as access5, mkdir as mkdir13, readFile as readFile16, writeFile as writeFile16 } from "node:fs/promises";
16450
16776
  import path56 from "node:path";
16451
16777
 
16452
16778
  // src/cli/make-config/json-config.ts
@@ -17682,7 +18008,7 @@ function applyOpenApiToSiteConfig(config2, options) {
17682
18008
  async function readManifest2(siteDirectory) {
17683
18009
  const filePath = manifestFilePath(siteDirectory);
17684
18010
  if (await pathExists(filePath)) {
17685
- const raw = await readFile15(filePath, "utf8");
18011
+ const raw = await readFile16(filePath, "utf8");
17686
18012
  const config2 = JSON.parse(raw);
17687
18013
  return extractManifestFromConfig(config2);
17688
18014
  }
@@ -17693,7 +18019,7 @@ async function writeManifest2(siteDirectory, manifest) {
17693
18019
  await mkdir13(path56.dirname(filePath), { recursive: true });
17694
18020
  let config2 = {};
17695
18021
  if (await pathExists(filePath)) {
17696
- const raw = await readFile15(filePath, "utf8");
18022
+ const raw = await readFile16(filePath, "utf8");
17697
18023
  config2 = JSON.parse(raw);
17698
18024
  }
17699
18025
  const {
@@ -17702,7 +18028,7 @@ async function writeManifest2(siteDirectory, manifest) {
17702
18028
  ...safeManifest
17703
18029
  } = manifest;
17704
18030
  config2 = applyManifestToConfig(config2, safeManifest);
17705
- await writeFile15(filePath, `${JSON.stringify(config2, null, 2)}
18031
+ await writeFile16(filePath, `${JSON.stringify(config2, null, 2)}
17706
18032
  `, "utf8");
17707
18033
  return filePath;
17708
18034
  }
@@ -17712,9 +18038,9 @@ import {
17712
18038
  access as access6,
17713
18039
  appendFile,
17714
18040
  mkdir as mkdir14,
17715
- readFile as readFile16,
18041
+ readFile as readFile17,
17716
18042
  unlink,
17717
- writeFile as writeFile16
18043
+ writeFile as writeFile17
17718
18044
  } from "node:fs/promises";
17719
18045
  import path57 from "node:path";
17720
18046
  var ENV_FILENAME = ".env";
@@ -17818,7 +18144,7 @@ function applyEnvOverrides2(config2, env) {
17818
18144
  }
17819
18145
  async function readEnvFile(filePath) {
17820
18146
  try {
17821
- const raw = await readFile16(filePath, "utf8");
18147
+ const raw = await readFile17(filePath, "utf8");
17822
18148
  const env = {};
17823
18149
  for (const line2 of raw.split("\n")) {
17824
18150
  const trimmed = line2.trim();
@@ -17951,7 +18277,7 @@ var SECRETS_COLLECTION_INSTRUCTION = "For go-live, plans must already be in apit
17951
18277
 
17952
18278
  // src/cli/platform/helpers/dev-portal-site-validation.ts
17953
18279
  init_gateway_limits();
17954
- import { readFile as readFile17 } from "node:fs/promises";
18280
+ import { readFile as readFile18 } from "node:fs/promises";
17955
18281
  import { createRequire as createRequire2 } from "node:module";
17956
18282
  import path58 from "node:path";
17957
18283
  var cachedFallbackTs;
@@ -18129,7 +18455,7 @@ async function validateDevPortalSiteConfig(sourceDirectory, options) {
18129
18455
  let parsed;
18130
18456
  let parseFailed = false;
18131
18457
  try {
18132
- parsed = JSON.parse(await readFile17(jsonPath, "utf8"));
18458
+ parsed = JSON.parse(await readFile18(jsonPath, "utf8"));
18133
18459
  } catch (error) {
18134
18460
  parseFailed = true;
18135
18461
  issues.push({
@@ -18168,7 +18494,7 @@ async function validateDevPortalSiteConfig(sourceDirectory, options) {
18168
18494
  } else {
18169
18495
  for (const relativeFile of existingTsxFiles) {
18170
18496
  checkedFiles.push(relativeFile);
18171
- const content = await readFile17(
18497
+ const content = await readFile18(
18172
18498
  path58.join(resolvedDir, relativeFile),
18173
18499
  "utf8"
18174
18500
  );
@@ -18191,7 +18517,7 @@ async function validateDevPortalSiteConfig(sourceDirectory, options) {
18191
18517
 
18192
18518
  // src/cli/platform/helpers/npm-build.ts
18193
18519
  import { spawn as spawn2 } from "node:child_process";
18194
- import { access as access7, readFile as readFile18 } from "node:fs/promises";
18520
+ import { access as access7, readFile as readFile19 } from "node:fs/promises";
18195
18521
  import path59 from "node:path";
18196
18522
  var BUILD_LOG_TAIL_BYTES = 8192;
18197
18523
  var MIN_NODE_MAJOR = 20;
@@ -18828,7 +19154,7 @@ var check_default = {
18828
19154
 
18829
19155
  // src/cli/make-config/handler.ts
18830
19156
  import { constants as constants2 } from "node:fs";
18831
- import { access as access11, mkdir as mkdir16, readFile as readFile22, writeFile as writeFile20 } from "node:fs/promises";
19157
+ import { access as access11, mkdir as mkdir16, readFile as readFile23, writeFile as writeFile21 } from "node:fs/promises";
18832
19158
  import path69 from "node:path";
18833
19159
  import { stdin as input2 } from "node:process";
18834
19160
  import { glob as glob8 } from "glob";
@@ -18968,12 +19294,12 @@ function withDefaultIncludedActionKeys(plan2, actionKeys) {
18968
19294
  }
18969
19295
 
18970
19296
  // src/cli/scaffold/dev-portal-config-pages.ts
18971
- import { access as access8, readFile as readFile19, rename as rename7, rm as rm9, writeFile as writeFile17 } from "node:fs/promises";
19297
+ import { access as access8, readFile as readFile20, rename as rename7, rm as rm9, writeFile as writeFile18 } from "node:fs/promises";
18972
19298
  import path63 from "node:path";
18973
19299
  async function writeConfigAtomically(filePath, content) {
18974
19300
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
18975
19301
  try {
18976
- await writeFile17(temporaryPath, content, "utf8");
19302
+ await writeFile18(temporaryPath, content, "utf8");
18977
19303
  await rename7(temporaryPath, filePath);
18978
19304
  } catch (error) {
18979
19305
  await rm9(temporaryPath, { force: true });
@@ -19733,7 +20059,7 @@ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label2) {
19733
20059
  if (!configPath) {
19734
20060
  return { updated: false };
19735
20061
  }
19736
- const original = await readFile19(configPath, "utf8");
20062
+ const original = await readFile20(configPath, "utf8");
19737
20063
  const patched = patchApitogoConfigForNewPage(original, slug, label2, {
19738
20064
  typescript: configPath.endsWith(".tsx")
19739
20065
  });
@@ -19745,7 +20071,7 @@ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label2) {
19745
20071
  }
19746
20072
 
19747
20073
  // src/cli/scaffold/wire-openapi.ts
19748
- import { access as access9, readFile as readFile20, writeFile as writeFile18 } from "node:fs/promises";
20074
+ import { access as access9, readFile as readFile21, writeFile as writeFile19 } from "node:fs/promises";
19749
20075
  import path66 from "node:path";
19750
20076
 
19751
20077
  // src/cli/platform/helpers/openapi.ts
@@ -20004,13 +20330,13 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20004
20330
  }
20005
20331
  const jsonConfigPath = manifestFilePath(resolvedSource);
20006
20332
  if (await pathExists3(jsonConfigPath)) {
20007
- const raw = await readFile20(jsonConfigPath, "utf8");
20333
+ const raw = await readFile21(jsonConfigPath, "utf8");
20008
20334
  const siteConfig = JSON.parse(raw);
20009
20335
  const updated = applyOpenApiToSiteConfig(siteConfig, {
20010
20336
  input: input5,
20011
20337
  apiPath: "api"
20012
20338
  });
20013
- await writeFile18(
20339
+ await writeFile19(
20014
20340
  jsonConfigPath,
20015
20341
  `${JSON.stringify(updated, null, 2)}
20016
20342
  `,
@@ -20018,10 +20344,10 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20018
20344
  );
20019
20345
  } else {
20020
20346
  const tsxConfigPath = path66.join(resolvedSource, "apitogo.config.tsx");
20021
- const configContent = await readFile20(tsxConfigPath, "utf8");
20347
+ const configContent = await readFile21(tsxConfigPath, "utf8");
20022
20348
  try {
20023
20349
  const patched = patchApitogoConfigOpenApiToFile(configContent, input5);
20024
- await writeFile18(tsxConfigPath, patched);
20350
+ await writeFile19(tsxConfigPath, patched);
20025
20351
  } catch (error) {
20026
20352
  return {
20027
20353
  status: "failed",
@@ -20374,7 +20700,7 @@ export default function ${componentName}() {
20374
20700
  // src/cli/make-config/scaffold-project.ts
20375
20701
  init_package_json();
20376
20702
  import { constants } from "node:fs";
20377
- import { access as access10, mkdir as mkdir15, readFile as readFile21, writeFile as writeFile19 } from "node:fs/promises";
20703
+ import { access as access10, mkdir as mkdir15, readFile as readFile22, writeFile as writeFile20 } from "node:fs/promises";
20378
20704
  import path68 from "node:path";
20379
20705
  import { glob as glob7 } from "glob";
20380
20706
 
@@ -20416,7 +20742,7 @@ async function findOpenApiSpecPath(dir) {
20416
20742
  });
20417
20743
  return extra[0] ? toPosix2(extra[0]) : null;
20418
20744
  }
20419
- async function ensurePackageJson(dir) {
20745
+ async function ensurePackageJson2(dir) {
20420
20746
  const pkgPath = path68.join(dir, "package.json");
20421
20747
  const apitogoVer = apitogoVersionRange();
20422
20748
  const baseDeps = {
@@ -20452,11 +20778,11 @@ async function ensurePackageJson(dir) {
20452
20778
  typescript: "^5"
20453
20779
  }
20454
20780
  };
20455
- await writeFile19(pkgPath, `${JSON.stringify(pkg2, null, 2)}
20781
+ await writeFile20(pkgPath, `${JSON.stringify(pkg2, null, 2)}
20456
20782
  `, "utf8");
20457
20783
  return true;
20458
20784
  }
20459
- const raw = await readFile21(pkgPath, "utf8");
20785
+ const raw = await readFile22(pkgPath, "utf8");
20460
20786
  let pkg;
20461
20787
  try {
20462
20788
  pkg = JSON.parse(raw);
@@ -20479,7 +20805,7 @@ async function ensurePackageJson(dir) {
20479
20805
  for (const [k, v] of Object.entries(baseDeps)) {
20480
20806
  if (!deps[k]) deps[k] = v;
20481
20807
  }
20482
- await writeFile19(pkgPath, `${JSON.stringify(pkg, null, 2)}
20808
+ await writeFile20(pkgPath, `${JSON.stringify(pkg, null, 2)}
20483
20809
  `, "utf8");
20484
20810
  return false;
20485
20811
  }
@@ -20514,7 +20840,7 @@ async function ensureTsconfigJson(dir) {
20514
20840
  "user-panel"
20515
20841
  ]
20516
20842
  };
20517
- await writeFile19(tsPath, `${JSON.stringify(tsconfig, null, 2)}
20843
+ await writeFile20(tsPath, `${JSON.stringify(tsconfig, null, 2)}
20518
20844
  `, "utf8");
20519
20845
  return true;
20520
20846
  }
@@ -20536,17 +20862,17 @@ async function ensurePortalStubFiles(dir) {
20536
20862
  const landingPath = path68.join(dir, "landing", "LandingPage.tsx");
20537
20863
  if (!await fileExists2(landingPath)) {
20538
20864
  await mkdir15(path68.join(dir, "landing"), { recursive: true });
20539
- await writeFile19(landingPath, LANDING_PAGE_SOURCE, "utf8");
20865
+ await writeFile20(landingPath, LANDING_PAGE_SOURCE, "utf8");
20540
20866
  }
20541
20867
  const plansPath = path68.join(dir, "user-panel", "AccountPlansPage.tsx");
20542
20868
  if (!await fileExists2(plansPath)) {
20543
20869
  await mkdir15(path68.join(dir, "user-panel"), { recursive: true });
20544
- await writeFile19(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
20870
+ await writeFile20(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
20545
20871
  }
20546
20872
  const dashboardPath = path68.join(dir, "user-panel", "DashboardPage.tsx");
20547
20873
  if (!await fileExists2(dashboardPath)) {
20548
20874
  await mkdir15(path68.join(dir, "user-panel"), { recursive: true });
20549
- await writeFile19(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
20875
+ await writeFile20(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
20550
20876
  }
20551
20877
  await ensurePublicBrandingFiles(dir);
20552
20878
  }
@@ -20558,11 +20884,11 @@ async function ensurePublicBrandingFiles(dir) {
20558
20884
  if (await fileExists2(dest)) {
20559
20885
  continue;
20560
20886
  }
20561
- await writeFile19(dest, content, "utf8");
20887
+ await writeFile20(dest, content, "utf8");
20562
20888
  }
20563
20889
  const faviconDest = path68.join(destDir, "Icon.png");
20564
20890
  if (!await fileExists2(faviconDest)) {
20565
- await writeFile19(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
20891
+ await writeFile20(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
20566
20892
  }
20567
20893
  }
20568
20894
  async function ensureThinConfigTsx(dir) {
@@ -20570,7 +20896,7 @@ async function ensureThinConfigTsx(dir) {
20570
20896
  if (await fileExists2(tsxPath)) {
20571
20897
  return false;
20572
20898
  }
20573
- await writeFile19(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
20899
+ await writeFile20(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
20574
20900
  return true;
20575
20901
  }
20576
20902
 
@@ -20585,7 +20911,7 @@ var fileExists3 = async (filePath) => {
20585
20911
  }
20586
20912
  };
20587
20913
  var readJsonConfig = async (filePath) => {
20588
- const raw = await readFile22(filePath, "utf8");
20914
+ const raw = await readFile23(filePath, "utf8");
20589
20915
  const parsed = JSON.parse(raw);
20590
20916
  if (!isPlainObject3(parsed)) {
20591
20917
  throw new Error(`${APITOGO_JSON_FILENAME} must be a JSON object.`);
@@ -20631,13 +20957,13 @@ async function persistConfig(config2, ctx) {
20631
20957
  force: ctx.force
20632
20958
  })
20633
20959
  };
20634
- await writeFile20(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
20960
+ await writeFile21(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
20635
20961
  `, "utf8");
20636
20962
  const tsxPath = path69.join(ctx.dir, "apitogo.config.tsx");
20637
20963
  if (await fileExists3(tsxPath) && tsxRoutes.length > 0) {
20638
- const tsxSource = await readFile22(tsxPath, "utf8");
20964
+ const tsxSource = await readFile23(tsxPath, "utf8");
20639
20965
  if (isManagedConfigWrapper(tsxSource)) {
20640
- await writeFile20(
20966
+ await writeFile21(
20641
20967
  tsxPath,
20642
20968
  buildThinWrapperWithCustomPages(tsxRoutes),
20643
20969
  "utf8"
@@ -20669,7 +20995,7 @@ async function makeConfig(argv) {
20669
20995
  const changes = [];
20670
20996
  try {
20671
20997
  const discoveredOpenApi = await findOpenApiSpecPath(dir);
20672
- const createdPackageJson = await ensurePackageJson(dir);
20998
+ const createdPackageJson = await ensurePackageJson2(dir);
20673
20999
  const createdTsconfig = await ensureTsconfigJson(dir);
20674
21000
  if (createdPackageJson && !json) {
20675
21001
  printSuccessToConsole(
@@ -20741,7 +21067,7 @@ async function makeConfig(argv) {
20741
21067
  if (action2.kind === "page") {
20742
21068
  const pagePath = path69.join(dir, action2.page.relativePath);
20743
21069
  await mkdir16(path69.dirname(pagePath), { recursive: true });
20744
- await writeFile20(
21070
+ await writeFile21(
20745
21071
  pagePath,
20746
21072
  buildStarterDocsPageTsx(action2.page.slug, action2.page.title),
20747
21073
  "utf8"
@@ -20850,7 +21176,7 @@ async function makeConfig(argv) {
20850
21176
  argv.pageContent,
20851
21177
  pageTemplate
20852
21178
  );
20853
- await writeFile20(filePath, tsx, "utf8");
21179
+ await writeFile21(filePath, tsx, "utf8");
20854
21180
  pageResult = {
20855
21181
  action: "updated",
20856
21182
  slug,
@@ -20870,7 +21196,7 @@ async function makeConfig(argv) {
20870
21196
  pageTemplate
20871
21197
  );
20872
21198
  await mkdir16(path69.dirname(filePath), { recursive: true });
20873
- await writeFile20(filePath, tsx, "utf8");
21199
+ await writeFile21(filePath, tsx, "utf8");
20874
21200
  const configResult = await wireNewDevPortalPageInConfig(
20875
21201
  dir,
20876
21202
  slug,
@@ -21009,11 +21335,21 @@ var config_default = {
21009
21335
  // src/cli/create/handler.ts
21010
21336
  init_apitogo_config_core();
21011
21337
  import { execFile } from "node:child_process";
21012
- import { readFile as readFile23 } from "node:fs/promises";
21338
+ import { readFile as readFile24 } from "node:fs/promises";
21013
21339
  import path70 from "node:path";
21014
21340
  import { promisify } from "node:util";
21015
21341
  var execFileAsync = promisify(execFile);
21342
+ function nextStepHint(dir) {
21343
+ const linked = loadConfigV2(dir).resolved.project.id;
21344
+ return linked ? "Next: apitogo deploy" : "Next: apitogo project create, then apitogo deploy";
21345
+ }
21016
21346
  var DONE = "done";
21347
+ var STARTER_SERVICES = [
21348
+ "gateway",
21349
+ "identity",
21350
+ "plans",
21351
+ "portal"
21352
+ ];
21017
21353
  async function gitHasUncommittedChanges(dir) {
21018
21354
  try {
21019
21355
  const { stdout } = await execFileAsync("git", ["status", "--porcelain"], {
@@ -21027,7 +21363,7 @@ async function gitHasUncommittedChanges(dir) {
21027
21363
  async function defaultName(dir) {
21028
21364
  try {
21029
21365
  const parsed = JSON.parse(
21030
- await readFile23(path70.join(dir, "package.json"), "utf8")
21366
+ await readFile24(path70.join(dir, "package.json"), "utf8")
21031
21367
  );
21032
21368
  if (typeof parsed.name === "string" && parsed.name.trim())
21033
21369
  return parsed.name.replace(/^@[^/]+\//, "");
@@ -21060,7 +21396,8 @@ async function createProject(argv, deps = {}) {
21060
21396
  };
21061
21397
  try {
21062
21398
  const dir = path70.resolve(process.cwd(), argv.dir ?? ".");
21063
- const requested = parseServicesFlag(argv.services);
21399
+ let requested = parseServicesFlag(argv.services);
21400
+ let template = argv.template;
21064
21401
  if (!asker && requested.length === 0) {
21065
21402
  throw new InitError(
21066
21403
  "create needs a terminal to ask its questions, and none is available.",
@@ -21098,10 +21435,23 @@ async function createProject(argv, deps = {}) {
21098
21435
  } else {
21099
21436
  say(`${APITOGO_CONFIG_FILENAME} exists: continuing from it.`);
21100
21437
  }
21438
+ if (!template && requested.length === 0 && asker)
21439
+ template = await asker.choose(
21440
+ "What do you want to start from?",
21441
+ [
21442
+ {
21443
+ value: "starter",
21444
+ label: "Starter: every service with examples (home page, sample plans, API reference)"
21445
+ },
21446
+ { value: "blank", label: "Blank: choose the services one by one" }
21447
+ ]
21448
+ );
21449
+ if (template === "starter" && requested.length === 0)
21450
+ requested = STARTER_SERVICES;
21101
21451
  const stepArgs = (service) => ({
21102
21452
  service,
21103
21453
  dir: argv.dir,
21104
- template: argv.template,
21454
+ template,
21105
21455
  dryRun: argv.dryRun,
21106
21456
  install: argv.install,
21107
21457
  json,
@@ -21153,9 +21503,8 @@ async function createProject(argv, deps = {}) {
21153
21503
  );
21154
21504
  return;
21155
21505
  }
21156
- printResultToConsole("Setup finished. The same steps as commands:");
21157
- for (const command2 of commands) printDiagnosticsToConsole(` ${command2}`);
21158
- printHintToConsole("Next: apitogo deploy");
21506
+ printResultToConsole("Setup finished.");
21507
+ printHintToConsole(nextStepHint(dir));
21159
21508
  } catch (error) {
21160
21509
  const message = error instanceof Error ? error.message : String(error);
21161
21510
  const hint = error instanceof InitError ? error.developerHint : void 0;
@@ -21345,6 +21694,16 @@ function buildProgressSummary(options) {
21345
21694
  terminalStatus: options.terminalStatus
21346
21695
  };
21347
21696
  }
21697
+ var MODULE_DISPLAY_NAMES = {
21698
+ "dev-portal": "Developer website",
21699
+ apihub: "API hosting"
21700
+ };
21701
+ function friendlyModuleNames(text) {
21702
+ return text.replace(
21703
+ /(?:Module|module) (dev-portal|apihub)(?![\w-])/g,
21704
+ (_match, id) => MODULE_DISPLAY_NAMES[id] ?? id
21705
+ );
21706
+ }
21348
21707
  function createConsoleProgressReporter(stream = process.stderr) {
21349
21708
  const shared = { spinner: void 0 };
21350
21709
  return {
@@ -21364,7 +21723,9 @@ function createScopedConsoleProgressReporter(stream, stepLabel, shared) {
21364
21723
  const reporter = {
21365
21724
  async onStep(step2, detail) {
21366
21725
  const message = (!animated ? detail?.message : void 0) ?? (detail?.platformStatus ? `${step2}: ${detail.platformStatus}` : step2);
21367
- const line2 = stepLabel ? `[${stepLabel}] ${message}` : message;
21726
+ const line2 = friendlyModuleNames(
21727
+ stepLabel ? `[${stepLabel}] ${message}` : message
21728
+ );
21368
21729
  if (line2 === lastLine) return;
21369
21730
  lastLine = line2;
21370
21731
  if (!animated) {
@@ -21383,7 +21744,9 @@ function createScopedConsoleProgressReporter(stream, stepLabel, shared) {
21383
21744
  async onNewMessages(messages) {
21384
21745
  for (const entry of messages) {
21385
21746
  writeLine2(
21386
- stepLabel ? `[${stepLabel}] ${entry.message}` : entry.message
21747
+ friendlyModuleNames(
21748
+ stepLabel ? `[${stepLabel}] ${entry.message}` : entry.message
21749
+ )
21387
21750
  );
21388
21751
  }
21389
21752
  },
@@ -22247,7 +22610,7 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
22247
22610
  }
22248
22611
 
22249
22612
  // src/cli/platform/helpers/openapi-gateway-servers.ts
22250
- import { mkdir as mkdir17, readFile as readFile24, writeFile as writeFile21 } from "node:fs/promises";
22613
+ import { mkdir as mkdir17, readFile as readFile25, writeFile as writeFile22 } from "node:fs/promises";
22251
22614
  import path73 from "node:path";
22252
22615
  import YAML3 from "yaml";
22253
22616
  var GATEWAY_URL_ENV_KEY = "APITOGO_GATEWAY_URL";
@@ -22279,7 +22642,7 @@ async function resolveProjectGatewayUrl(client, projectId) {
22279
22642
  return null;
22280
22643
  }
22281
22644
  async function readOpenApiDocument(filePath) {
22282
- const content = await readFile24(filePath, "utf8");
22645
+ const content = await readFile25(filePath, "utf8");
22283
22646
  const ext = path73.extname(filePath).toLowerCase();
22284
22647
  const parsed = ext === ".json" ? JSON.parse(content) : YAML3.parse(content);
22285
22648
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -22295,7 +22658,7 @@ async function materializePublishOpenApiWithGateway(siteDirectory, sourceOpenApi
22295
22658
  const doc = await readOpenApiDocument(source);
22296
22659
  const rewritten = withGatewayServers(doc, gatewayUrl);
22297
22660
  await mkdir17(path73.dirname(outputPath), { recursive: true });
22298
- await writeFile21(
22661
+ await writeFile22(
22299
22662
  outputPath,
22300
22663
  `${JSON.stringify(rewritten, null, 2)}
22301
22664
  `,
@@ -22308,7 +22671,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22308
22671
  path73.resolve(siteDirectory),
22309
22672
  APITOGO_CONFIG_FILENAME2
22310
22673
  );
22311
- const raw = await readFile24(configPath, "utf8");
22674
+ const raw = await readFile25(configPath, "utf8");
22312
22675
  const config2 = JSON.parse(raw);
22313
22676
  const previousApis = config2.apis;
22314
22677
  const previousBackend = config2.backend;
@@ -22321,11 +22684,11 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22321
22684
  const backend = config2.backend && typeof config2.backend === "object" ? { ...config2.backend } : {};
22322
22685
  backend.openApiPath = relativeInput;
22323
22686
  config2.backend = backend;
22324
- await writeFile21(configPath, `${JSON.stringify(config2, null, 2)}
22687
+ await writeFile22(configPath, `${JSON.stringify(config2, null, 2)}
22325
22688
  `, "utf8");
22326
22689
  return {
22327
22690
  restore: async () => {
22328
- const currentRaw = await readFile24(configPath, "utf8");
22691
+ const currentRaw = await readFile25(configPath, "utf8");
22329
22692
  const current = JSON.parse(currentRaw);
22330
22693
  if (previousApis === void 0) {
22331
22694
  delete current.apis;
@@ -22337,7 +22700,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22337
22700
  } else {
22338
22701
  current.backend = previousBackend;
22339
22702
  }
22340
- await writeFile21(
22703
+ await writeFile22(
22341
22704
  configPath,
22342
22705
  `${JSON.stringify(current, null, 2)}
22343
22706
  `,
@@ -22360,7 +22723,7 @@ async function prepareOpenApiServersForGatewayBuild(options) {
22360
22723
  );
22361
22724
  if (!openApiPath) {
22362
22725
  try {
22363
- const raw = await readFile24(
22726
+ const raw = await readFile25(
22364
22727
  path73.join(siteDirectory, APITOGO_CONFIG_FILENAME2),
22365
22728
  "utf8"
22366
22729
  );
@@ -22434,7 +22797,7 @@ function collectManifestPublicActionKeys(gateway) {
22434
22797
  }
22435
22798
 
22436
22799
  // src/cli/platform/helpers/publish-ledger.ts
22437
- import { mkdir as mkdir18, readFile as readFile25, writeFile as writeFile22 } from "node:fs/promises";
22800
+ import { mkdir as mkdir18, readFile as readFile26, writeFile as writeFile23 } from "node:fs/promises";
22438
22801
  import path74 from "node:path";
22439
22802
  var PUBLISH_LEDGER_RELATIVE_PATH = ".apitogo/publish-state.json";
22440
22803
  function publishLedgerPath(siteDirectory) {
@@ -22443,7 +22806,7 @@ function publishLedgerPath(siteDirectory) {
22443
22806
  async function readPublishLedger(siteDirectory) {
22444
22807
  const filePath = publishLedgerPath(siteDirectory);
22445
22808
  try {
22446
- const raw = await readFile25(filePath, "utf8");
22809
+ const raw = await readFile26(filePath, "utf8");
22447
22810
  const parsed = JSON.parse(raw);
22448
22811
  if (parsed?.version !== 1 || !Array.isArray(parsed.steps)) {
22449
22812
  return null;
@@ -22461,7 +22824,7 @@ async function writePublishLedger(siteDirectory, ledger) {
22461
22824
  version: 1,
22462
22825
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22463
22826
  };
22464
- await writeFile22(filePath, `${JSON.stringify(next, null, 2)}
22827
+ await writeFile23(filePath, `${JSON.stringify(next, null, 2)}
22465
22828
  `, "utf8");
22466
22829
  return filePath;
22467
22830
  }
@@ -22975,14 +23338,14 @@ async function createProjectWithWarmPoolRetry(client, body, options) {
22975
23338
 
22976
23339
  // src/cli/platform/helpers/backend-packaging.ts
22977
23340
  import { spawn as spawn3 } from "node:child_process";
22978
- import { mkdtemp, readdir as readdir2, rm as rm10 } from "node:fs/promises";
23341
+ import { mkdtemp, readdir as readdir4, rm as rm10 } from "node:fs/promises";
22979
23342
  import { tmpdir } from "node:os";
22980
23343
  import path75 from "node:path";
22981
23344
  async function directoryHasRuntimeConfig(dir) {
22982
23345
  if (!await pathExists(dir)) {
22983
23346
  return false;
22984
23347
  }
22985
- const entries = await readdir2(dir);
23348
+ const entries = await readdir4(dir);
22986
23349
  return entries.some((name) => name.endsWith(".runtimeconfig.json"));
22987
23350
  }
22988
23351
  function runDotnetPublish(projectDir, outputDir) {
@@ -23018,7 +23381,7 @@ function runDotnetPublish(projectDir, outputDir) {
23018
23381
  });
23019
23382
  }
23020
23383
  async function findCsproj(projectDir) {
23021
- const entries = await readdir2(projectDir);
23384
+ const entries = await readdir4(projectDir);
23022
23385
  const csproj = entries.find((name) => name.endsWith(".csproj"));
23023
23386
  return csproj ? path75.join(projectDir, csproj) : null;
23024
23387
  }
@@ -24622,7 +24985,7 @@ async function resolveProjectIdForPublish(client, config2, args, wait, progress,
24622
24985
  import path80 from "node:path";
24623
24986
 
24624
24987
  // src/cli/platform/helpers/dev-portal-site-api-url.ts
24625
- import { readdir as readdir3, readFile as readFile26, writeFile as writeFile23 } from "node:fs/promises";
24988
+ import { readdir as readdir5, readFile as readFile27, writeFile as writeFile24 } from "node:fs/promises";
24626
24989
  import path77 from "node:path";
24627
24990
  var SITE_API_BASE_URL_ENV_KEY = "VITE_APITOGO_AUTHENTICATION_API_BASE_URL";
24628
24991
  var PLACEHOLDER_HOST_MARKERS = [
@@ -24647,7 +25010,7 @@ async function readSiteApiBaseUrl(sourceDirectory) {
24647
25010
  const resolved = path77.resolve(sourceDirectory);
24648
25011
  for (const envFile of [".env", ".env.production"]) {
24649
25012
  try {
24650
- const content = await readFile26(path77.join(resolved, envFile), "utf8");
25013
+ const content = await readFile27(path77.join(resolved, envFile), "utf8");
24651
25014
  const lines = [...content.matchAll(ENV_LINE_GLOBAL_RE)];
24652
25015
  if (lines.length === 0) {
24653
25016
  continue;
@@ -24678,19 +25041,19 @@ async function patchSiteApiBaseUrl(sourceDirectory, apiBaseUrl) {
24678
25041
  for (const envFile of [".env.production", ".env"]) {
24679
25042
  const envPath = path77.join(resolved, envFile);
24680
25043
  try {
24681
- const content = await readFile26(envPath, "utf8");
25044
+ const content = await readFile27(envPath, "utf8");
24682
25045
  const withoutAssignments = content.replace(ENV_LINE_GLOBAL_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
24683
25046
  const patched = withoutAssignments ? `${withoutAssignments}
24684
25047
  ${line2}
24685
25048
  ` : `${line2}
24686
25049
  `;
24687
- await writeFile23(envPath, patched);
25050
+ await writeFile24(envPath, patched);
24688
25051
  patchedExisting = true;
24689
25052
  } catch {
24690
25053
  }
24691
25054
  }
24692
25055
  if (!patchedExisting) {
24693
- await writeFile23(path77.join(resolved, ".env"), `${line2}
25056
+ await writeFile24(path77.join(resolved, ".env"), `${line2}
24694
25057
  `);
24695
25058
  }
24696
25059
  }
@@ -24729,7 +25092,7 @@ async function ensureLiveSiteApiBaseUrlForBuild(_client, config2, args, resolveF
24729
25092
  async function* walkFiles(root) {
24730
25093
  let entries;
24731
25094
  try {
24732
- entries = await readdir3(root, { withFileTypes: true });
25095
+ entries = await readdir5(root, { withFileTypes: true });
24733
25096
  } catch {
24734
25097
  return;
24735
25098
  }
@@ -24758,7 +25121,7 @@ async function assertDeployableDistHasLiveApiBaseUrl(distDirectory, expectedApiB
24758
25121
  let found = false;
24759
25122
  let foundPlaceholder = false;
24760
25123
  for await (const file of walkFiles(dist)) {
24761
- const content = await readFile26(file, "utf8");
25124
+ const content = await readFile27(file, "utf8");
24762
25125
  if (content.includes(needle)) {
24763
25126
  found = true;
24764
25127
  }
@@ -26525,7 +26888,12 @@ function printPlanCatalog(payload) {
26525
26888
  if (typeof payload.freeProjectsQuotaHint === "string") {
26526
26889
  printDiagnosticsToConsole(payload.freeProjectsQuotaHint);
26527
26890
  }
26528
- const catalog = Array.isArray(payload.platformPlanCatalog) ? payload.platformPlanCatalog : [];
26891
+ printPlanOptions(
26892
+ Array.isArray(payload.platformPlanCatalog) ? payload.platformPlanCatalog : []
26893
+ );
26894
+ printDiagnosticsToConsole("");
26895
+ }
26896
+ function printPlanOptions(catalog) {
26529
26897
  for (const entry of catalog) {
26530
26898
  if (!entry || typeof entry !== "object") continue;
26531
26899
  const plan2 = entry;
@@ -26543,7 +26911,12 @@ function printPlanCatalog(payload) {
26543
26911
  printHintToConsole(` - ${limitation}`);
26544
26912
  }
26545
26913
  }
26914
+ }
26915
+ async function pickPlatformPlan() {
26916
+ printDiagnosticsToConsole("Which API ToGo plan should this project use?");
26917
+ printPlanOptions(PLATFORM_PLAN_CATALOG);
26546
26918
  printDiagnosticsToConsole("");
26919
+ return askPlanChoice();
26547
26920
  }
26548
26921
  async function askPlanChoice() {
26549
26922
  const rl = createInterface4({ input: input3, output: output2 });
@@ -26897,7 +27270,7 @@ import path82 from "node:path";
26897
27270
 
26898
27271
  // src/cli/deploy/gateway.ts
26899
27272
  import { createHash as createHash4 } from "node:crypto";
26900
- import { readFile as readFile27 } from "node:fs/promises";
27273
+ import { readFile as readFile28 } from "node:fs/promises";
26901
27274
  init_invariant();
26902
27275
  var GatewayDeployError = class extends ApitogoError {
26903
27276
  constructor(code, message, developerHint) {
@@ -26906,6 +27279,12 @@ var GatewayDeployError = class extends ApitogoError {
26906
27279
  this.name = "GatewayDeployError";
26907
27280
  }
26908
27281
  };
27282
+ var API_HOSTING_MODULE = "apihub";
27283
+ function isHostingNotActivated(error) {
27284
+ return error instanceof PlatformApiError && /module is not activated/i.test(
27285
+ `${error.message} ${error.problem?.detail ?? ""}`
27286
+ );
27287
+ }
26909
27288
  var sha256 = (text) => createHash4("sha256").update(text).digest("hex");
26910
27289
  function gatewayNotDeployableReason(gateway) {
26911
27290
  if (gateway.specs.length > 1)
@@ -26929,7 +27308,7 @@ async function readSpec(gateway) {
26929
27308
  "Add one to gateway.specs in the config."
26930
27309
  );
26931
27310
  try {
26932
- return await readFile27(file);
27311
+ return await readFile28(file);
26933
27312
  } catch {
26934
27313
  throw new GatewayDeployError(
26935
27314
  "GATEWAY_SPEC_UNREADABLE",
@@ -27044,6 +27423,32 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
27044
27423
  const timeoutMs = deps.reconcileTimeoutMs ?? 42e4;
27045
27424
  const backendTimeoutMs = deps.backendTimeoutMs ?? 20 * 6e4;
27046
27425
  const intervalMs = deps.pollIntervalMs ?? 2e3;
27426
+ const claimApiHosting = async () => {
27427
+ await client.activateModule?.(projectId, API_HOSTING_MODULE);
27428
+ const deadline = now() + backendTimeoutMs;
27429
+ for (; ; ) {
27430
+ const state = await client.getInfrastructureStatus?.(
27431
+ projectId,
27432
+ API_HOSTING_MODULE
27433
+ );
27434
+ if (!state || isTerminalInfrastructureStatus(state.status)) {
27435
+ if (state?.status.toLowerCase() === "failed")
27436
+ throw new GatewayDeployError(
27437
+ "GATEWAY_HOSTING_FAILED",
27438
+ `The platform could not prepare the API hosting: ${state.error ?? "no reason given"}.`,
27439
+ "Run `apitogo project create` again, or `apitogo deploy` once it is fixed."
27440
+ );
27441
+ return;
27442
+ }
27443
+ if (now() >= deadline)
27444
+ throw new GatewayDeployError(
27445
+ "GATEWAY_HOSTING_TIMEOUT",
27446
+ `The API hosting was not ready within ${Math.round(backendTimeoutMs / 6e4)} minutes.`,
27447
+ "Run deploy again to keep waiting."
27448
+ );
27449
+ await sleep5(intervalMs);
27450
+ }
27451
+ };
27047
27452
  const backendStep = {
27048
27453
  name: "backend",
27049
27454
  hash: () => backend?.hash ?? "",
@@ -27061,14 +27466,28 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
27061
27466
  );
27062
27467
  zipPath = packaged.zipPath;
27063
27468
  let started;
27469
+ const upload = () => client.deployBackend(projectId, {
27470
+ archivePath: packaged.zipPath,
27471
+ runtime: backend.runtime,
27472
+ runtimeVersion: backend.runtimeVersion,
27473
+ healthCheckPath: backend.healthPath
27474
+ });
27064
27475
  try {
27065
- started = await client.deployBackend(projectId, {
27066
- archivePath: packaged.zipPath,
27067
- runtime: backend.runtime,
27068
- runtimeVersion: backend.runtimeVersion,
27069
- healthCheckPath: backend.healthPath
27070
- });
27476
+ try {
27477
+ started = await upload();
27478
+ } catch (error) {
27479
+ if (!isHostingNotActivated(error) || !client.activateModule)
27480
+ throw error;
27481
+ await claimApiHosting();
27482
+ started = await upload();
27483
+ }
27071
27484
  } catch (error) {
27485
+ if (isHostingNotActivated(error))
27486
+ throw new GatewayDeployError(
27487
+ "GATEWAY_HOSTING_NOT_ACTIVATED",
27488
+ "The project has no API hosting yet, so there is nowhere to run your backend.",
27489
+ "Run `apitogo project create` again: it finishes the same project and claims the hosting, then run `apitogo deploy`."
27490
+ );
27072
27491
  return rejected("backend upload", error);
27073
27492
  }
27074
27493
  if (!started.reused) {
@@ -28747,7 +29166,7 @@ function formatStatus(output4) {
28747
29166
  }
28748
29167
 
28749
29168
  // src/cli/deploy/static-web-config.ts
28750
- import { access as access13, writeFile as writeFile24 } from "node:fs/promises";
29169
+ import { access as access13, writeFile as writeFile25 } from "node:fs/promises";
28751
29170
  import path83 from "node:path";
28752
29171
  var STATIC_WEB_CONFIG_FILE = "staticwebapp.config.json";
28753
29172
  var STATIC_WEB_CONFIG_VERSION = 3;
@@ -28771,7 +29190,7 @@ async function ensureStaticWebConfig(buildDir) {
28771
29190
  await access13(file);
28772
29191
  return false;
28773
29192
  } catch {
28774
- await writeFile24(
29193
+ await writeFile25(
28775
29194
  file,
28776
29195
  `${JSON.stringify(DEFAULT_STATIC_WEB_CONFIG, null, 2)}
28777
29196
  `,
@@ -29117,6 +29536,12 @@ function formatDeploy(output4) {
29117
29536
  // src/cli/deploy/command.ts
29118
29537
  async function deployProject(argv) {
29119
29538
  const json = argv.json === true;
29539
+ const quiet = json || argv.verbose !== true;
29540
+ if (quiet) applyJsonOnlyDeployQuietMode();
29541
+ if (!json && quiet)
29542
+ printDiagnosticsToConsole(
29543
+ "Deploying. This can take a few minutes; add --verbose to see the build log."
29544
+ );
29120
29545
  try {
29121
29546
  const config2 = loadConfig();
29122
29547
  const output4 = await deployV2(
@@ -29127,7 +29552,7 @@ async function deployProject(argv) {
29127
29552
  env: argv.env,
29128
29553
  force: argv.force === true,
29129
29554
  yes: argv.yes === true,
29130
- jsonOnly: json
29555
+ jsonOnly: quiet
29131
29556
  },
29132
29557
  {
29133
29558
  client: new PlatformApiClient(config2),
@@ -29201,6 +29626,10 @@ var deploy_default = {
29201
29626
  type: "boolean",
29202
29627
  describe: "Wait for long-running jobs",
29203
29628
  default: true
29629
+ }).option("verbose", {
29630
+ type: "boolean",
29631
+ describe: "Config version 2: show the full build log (by default only the result of each service is printed)",
29632
+ default: false
29204
29633
  }).option("json", {
29205
29634
  type: "boolean",
29206
29635
  describe: "Print raw JSON result only",
@@ -29217,7 +29646,7 @@ var deploy_default = {
29217
29646
  };
29218
29647
 
29219
29648
  // src/cli/export/handler.ts
29220
- import { mkdir as mkdir19, writeFile as writeFile25 } from "node:fs/promises";
29649
+ import { mkdir as mkdir19, writeFile as writeFile26 } from "node:fs/promises";
29221
29650
  import path86 from "node:path";
29222
29651
  init_invariant();
29223
29652
  var ExportError = class extends ApitogoError {
@@ -29259,7 +29688,7 @@ async function exportOpenApi(args, deps) {
29259
29688
  if (!args.out) return { text };
29260
29689
  const file = path86.resolve(process.cwd(), args.out);
29261
29690
  await mkdir19(path86.dirname(file), { recursive: true });
29262
- await writeFile25(file, text);
29691
+ await writeFile26(file, text);
29263
29692
  return { text, file };
29264
29693
  }
29265
29694
  async function exportCommand(args) {
@@ -29680,9 +30109,9 @@ var plan_default = {
29680
30109
  };
29681
30110
 
29682
30111
  // src/cli/project/create-handler.ts
29683
- init_invariant();
29684
30112
  import path88 from "node:path";
29685
30113
  import open3 from "open";
30114
+ init_invariant();
29686
30115
 
29687
30116
  // src/cli/project/v2.ts
29688
30117
  init_invariant();
@@ -29893,6 +30322,17 @@ async function createLinkedProject(argv, dir) {
29893
30322
  const json = argv.json === true;
29894
30323
  try {
29895
30324
  assertConfigChecksOut(await runCheckV2(dir));
30325
+ let plan2 = argv.plan;
30326
+ if (!plan2 && !json && process.stdin.isTTY && process.stdout.isTTY && !loadConfigV2(dir).resolved.project.id) {
30327
+ const picked = await pickPlatformPlan();
30328
+ if (!picked) {
30329
+ printResultToConsole(
30330
+ "No plan selected. Run `apitogo project create --plan free|starter|professional` when you are ready."
30331
+ );
30332
+ return;
30333
+ }
30334
+ plan2 = picked;
30335
+ }
29896
30336
  const config2 = loadConfig();
29897
30337
  const client = new PlatformApiClient(config2);
29898
30338
  const progress = createConsoleProgressReporter();
@@ -29902,7 +30342,7 @@ async function createLinkedProject(argv, dir) {
29902
30342
  name: argv.name,
29903
30343
  description: argv.description,
29904
30344
  wait: argv.wait !== false,
29905
- plan: argv.plan ?? "free",
30345
+ plan: plan2 ?? "free",
29906
30346
  skipPayment: argv.skipPayment === true,
29907
30347
  open: argv.open !== false,
29908
30348
  // In JSON mode the link is returned and a later run continues; otherwise the command waits for the payment.
@@ -29946,7 +30386,7 @@ async function createLinkedProject(argv, dir) {
29946
30386
  return;
29947
30387
  }
29948
30388
  printDiagnosticsToConsole(
29949
- `Project ${result.projectName} (${result.projectId}) is ready on the ${result.plan ?? "free"} plan${result.modules.length > 0 ? ` with: ${result.modules.join(", ")}` : " (the gateway is created by `apitogo deploy`)"}.`
30389
+ `Project ${result.projectName} (${result.projectId}) is ready on the ${result.plan ?? "free"} plan${result.modules.length > 0 ? ` with: ${result.modules.map((id) => friendlyModuleNames(`module ${id}`)).join(", ")}` : " (the gateway is created by `apitogo deploy`)"}.`
29950
30390
  );
29951
30391
  printResultToConsole(
29952
30392
  "The folder is linked. Run `apitogo deploy` to publish."