@lukoweb/apitogo 0.2.30 → 0.2.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/dist/cli/cli.js +794 -248
  2. package/dist/declarations/app/entry.server.d.ts +2 -2
  3. package/dist/declarations/app/main.d.ts +3 -1
  4. package/dist/declarations/cli/platform/client/platform-api.d.ts +16 -5
  5. package/dist/declarations/cli/platform/helpers/deploy-artifact-hash.d.ts +1 -1
  6. package/dist/declarations/cli/platform/helpers/package-files.d.ts +1 -1
  7. package/dist/declarations/cli/platform/helpers/platform-plan.d.ts +1 -1
  8. package/dist/declarations/cli/platform/helpers/zip.d.ts +1 -1
  9. package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
  10. package/dist/declarations/config/effective-redirects.d.ts +6 -0
  11. package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
  12. package/dist/declarations/config/resolve-modules.d.ts +1 -4
  13. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
  14. package/docs/cli/commands.md +151 -75
  15. package/docs/cli/go-live.md +135 -123
  16. package/docs/cli/walkthrough.md +40 -1
  17. package/docs/configuration/gateway.md +52 -0
  18. package/docs/guides/customer-account.md +33 -2
  19. package/docs/mcp/tools.md +101 -102
  20. package/package.json +2 -1
  21. package/src/app/demo.tsx +78 -77
  22. package/src/app/entry.client.tsx +115 -107
  23. package/src/app/entry.server.tsx +164 -160
  24. package/src/app/main.tsx +175 -167
  25. package/src/app/standalone.tsx +61 -60
  26. package/src/config/effective-redirects.ts +31 -0
  27. package/src/config/header-navigation-guard.ts +13 -0
  28. package/src/config/resolve-modules.ts +331 -354
  29. package/src/config/v2/home-redirect.ts +104 -98
  30. package/src/config/v2/index.ts +127 -126
  31. package/src/config/v2/ledger.ts +242 -235
  32. package/src/config/v2/resolve.ts +12 -4
  33. package/src/config/v2/rules.ts +716 -714
  34. package/src/config/v2/services.ts +799 -663
  35. package/src/config/v2/workspace.ts +17 -1
  36. package/src/config/validators/HeaderNavigationSchema.ts +42 -44
  37. package/src/lib/assets/powered-by-icon.png +0 -0
  38. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
  39. package/src/lib/authentication/providers/dev-portal.tsx +476 -448
  40. package/src/lib/components/HeaderNavigation.tsx +176 -176
  41. package/src/lib/components/MobileTopNavigation.tsx +283 -283
  42. package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
  43. package/src/vite/plugin-offroot-resolve.ts +68 -67
  44. package/src/vite/prerender/prerender.ts +242 -242
package/dist/cli/cli.js CHANGED
@@ -5267,7 +5267,7 @@ var llms_exports = {};
5267
5267
  __export(llms_exports, {
5268
5268
  generateLlmsTxtFiles: () => generateLlmsTxtFiles
5269
5269
  });
5270
- import { writeFile as writeFile11 } from "node:fs/promises";
5270
+ import { writeFile as writeFile12 } from "node:fs/promises";
5271
5271
  import path44 from "node:path";
5272
5272
  import colors10 from "picocolors";
5273
5273
  async function generateLlmsTxtFiles({
@@ -5303,7 +5303,7 @@ async function generateLlmsTxtFiles({
5303
5303
  }
5304
5304
  }
5305
5305
  const llmsTxt2 = llmsTxtParts.join("\n");
5306
- await writeFile11(path44.join(baseOutputDir, "llms.txt"), llmsTxt2, "utf-8");
5306
+ await writeFile12(path44.join(baseOutputDir, "llms.txt"), llmsTxt2, "utf-8");
5307
5307
  if (process.env.APITOGO_JSON_ONLY !== "1") {
5308
5308
  console.log(colors10.blue("\u2713 generated llms.txt"));
5309
5309
  }
@@ -5330,7 +5330,7 @@ ${info.content}
5330
5330
  `);
5331
5331
  }
5332
5332
  const llmsFull = llmsFullParts.join("\n");
5333
- await writeFile11(
5333
+ await writeFile12(
5334
5334
  path44.join(baseOutputDir, "llms-full.txt"),
5335
5335
  llmsFull,
5336
5336
  "utf-8"
@@ -5811,7 +5811,6 @@ import yargs from "yargs/yargs";
5811
5811
 
5812
5812
  // src/cli/services/handler.ts
5813
5813
  import path53 from "node:path";
5814
- import { createInterface as createInterface2 } from "node:readline/promises";
5815
5814
 
5816
5815
  // src/config/v2/errors.ts
5817
5816
  init_invariant();
@@ -6540,8 +6539,11 @@ import path8 from "node:path";
6540
6539
 
6541
6540
  // src/config/v2/resolve.ts
6542
6541
  import path7 from "node:path";
6542
+ function toGlobPath(value) {
6543
+ return value.split(path7.sep).join("/");
6544
+ }
6543
6545
  function fromConfig(configDir, value) {
6544
- return value.startsWith("!") ? `!${path7.resolve(configDir, value.slice(1))}` : path7.resolve(configDir, value);
6546
+ return value.startsWith("!") ? `!${toGlobPath(path7.resolve(configDir, value.slice(1)))}` : toGlobPath(path7.resolve(configDir, value));
6545
6547
  }
6546
6548
  function resolveConfigV2(config2, configPath) {
6547
6549
  const absoluteConfigPath = path7.resolve(configPath);
@@ -6569,7 +6571,7 @@ function resolveConfigV2(config2, configPath) {
6569
6571
  docs: {
6570
6572
  enabled: portal.docs?.enabled ?? true,
6571
6573
  provider,
6572
- content: provider === "external" ? [] : portal.docs?.content ? portal.docs.content.map((glob9) => fromConfig(configDir, glob9)) : [path7.join(dir, "pages", "**", "*.{md,mdx}")],
6574
+ content: provider === "external" ? [] : portal.docs?.content ? portal.docs.content.map((glob9) => fromConfig(configDir, glob9)) : [toGlobPath(path7.join(dir, "pages", "**", "*.{md,mdx}"))],
6573
6575
  url: portal.docs?.url
6574
6576
  },
6575
6577
  console: { enabled: portal.console?.enabled ?? true },
@@ -6815,8 +6817,9 @@ async function portalRules(config2, resolved, out) {
6815
6817
  }
6816
6818
  }
6817
6819
  async function docsContentRule(config2, patterns, resolved, out) {
6818
- const include = patterns.filter((pattern) => !pattern.startsWith("!"));
6819
- const ignore2 = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
6820
+ const globs = patterns.map(toGlobPath);
6821
+ const include = globs.filter((pattern) => !pattern.startsWith("!"));
6822
+ const ignore2 = globs.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
6820
6823
  const matches = include.length > 0 ? await glob(include, { nodir: true, ignore: ignore2, absolute: true }) : [];
6821
6824
  if (matches.length > 0) return;
6822
6825
  const shown = config2.portal?.docs?.content ?? [
@@ -7092,7 +7095,7 @@ function identityRules(config2, resolved, out) {
7092
7095
  );
7093
7096
  }
7094
7097
  }
7095
- for (const [list2, label] of [
7098
+ for (const [list2, label2] of [
7096
7099
  [identity.connections, "connections"],
7097
7100
  [identity.clients, "clients"]
7098
7101
  ]) {
@@ -7101,8 +7104,8 @@ function identityRules(config2, resolved, out) {
7101
7104
  if (seen.has(item.id)) {
7102
7105
  out.error({
7103
7106
  code: "IDENTITY_DUPLICATE_ID",
7104
- path: `identity.${label}[${index}].id`,
7105
- message: `the id "${item.id}" is used more than once in identity.${label}.`
7107
+ path: `identity.${label2}[${index}].id`,
7108
+ message: `the id "${item.id}" is used more than once in identity.${label2}.`
7106
7109
  });
7107
7110
  }
7108
7111
  seen.add(item.id);
@@ -7835,8 +7838,9 @@ var readFrontmatter = async (filePath) => {
7835
7838
  // src/config/v2/home-redirect.ts
7836
7839
  var IGNORED = ["**/node_modules/**", "**/dist/**", "**/.git/**"];
7837
7840
  async function docRoutes(patterns, options = {}) {
7838
- const excluded = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
7839
- const included = patterns.filter((pattern) => !pattern.startsWith("!"));
7841
+ const globs = patterns.map(toGlobPath);
7842
+ const excluded = globs.filter((pattern) => pattern.startsWith("!")).map((pattern) => pattern.slice(1));
7843
+ const included = globs.filter((pattern) => !pattern.startsWith("!"));
7840
7844
  const routes = await Promise.all(
7841
7845
  included.map(async (pattern) => {
7842
7846
  const files = await glob2(pattern, {
@@ -7852,7 +7856,7 @@ async function docRoutes(patterns, options = {}) {
7852
7856
  }))
7853
7857
  )).filter(({ draft }) => !draft).map(({ file }) => file);
7854
7858
  return published.map(
7855
- (file) => `/${path11.posix.relative(parent, file).replace(/\.mdx?$/, "")}`
7859
+ (file) => `/${path11.posix.relative(parent, file.replace(/^\/\/\?\//, "")).replace(/\.mdx?$/, "")}`
7856
7860
  );
7857
7861
  })
7858
7862
  );
@@ -8223,7 +8227,7 @@ async function preparePortal(resolved, manifest, mode, options = {}) {
8223
8227
 
8224
8228
  // src/config/v2/workspace.ts
8225
8229
  import { createHash as createHash2 } from "node:crypto";
8226
- import { access as access2, lstat, readFile as readFile7, rm as rm3 } from "node:fs/promises";
8230
+ import { access as access2, lstat, readFile as readFile7, rm as rm3, writeFile as writeFile6 } from "node:fs/promises";
8227
8231
  import path13 from "node:path";
8228
8232
  import { glob as glob3 } from "glob";
8229
8233
 
@@ -8256,9 +8260,17 @@ async function relink(target, link) {
8256
8260
  await rm3(link, { recursive: true, force: true });
8257
8261
  await linkDirectory(target, link);
8258
8262
  }
8263
+ var GENERATED_PACKAGE_JSON = '{\n "private": true,\n "type": "module"\n}\n';
8264
+ async function writeGeneratedPackageJson(root) {
8265
+ const file = path13.join(root, "package.json");
8266
+ const current = await readFile7(file, "utf8").catch(() => void 0);
8267
+ if (current !== GENERATED_PACKAGE_JSON)
8268
+ await writeFile6(file, GENERATED_PACKAGE_JSON, "utf8");
8269
+ }
8259
8270
  async function materializeWorkspace(resolved, prepared) {
8260
8271
  const root = path13.dirname(prepared.wrapperPath);
8261
8272
  await ensureStateDir(resolved.state);
8273
+ await writeGeneratedPackageJson(root);
8262
8274
  await relink(resolved.configDir, path13.join(root, CONTENT_LINK));
8263
8275
  const publicDir = resolved.portal && path13.join(resolved.portal.dir, "public");
8264
8276
  const publicLink = path13.join(root, "public");
@@ -8326,15 +8338,22 @@ async function portalArtifactHash(resolved, manifestText, cliVersion, wrapper =
8326
8338
 
8327
8339
  // src/config/v2/services.ts
8328
8340
  init_invariant();
8329
- import { access as access3, mkdir as mkdir5, rename as rename3, rm as rm4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
8341
+ import { access as access3, mkdir as mkdir5, rename as rename3, rm as rm4, stat as stat3, writeFile as writeFile7 } from "node:fs/promises";
8330
8342
  import path14 from "node:path";
8331
8343
  var SERVICES = ["portal", "gateway", "identity", "plans"];
8332
8344
  var ServiceEditError = class extends ApitogoError {
8333
- constructor(message, developerHint) {
8345
+ constructor(message, developerHint, missingServices = []) {
8334
8346
  super(message, { developerHint, title: "Cannot change the services" });
8347
+ this.missingServices = missingServices;
8335
8348
  this.name = "ServiceEditError";
8336
8349
  }
8337
8350
  };
8351
+ var httpsProblem = (value) => /^https:\/\/\S+$/i.test(value) ? void 0 : "The address must start with https://";
8352
+ var DEFAULT_RUNTIME_VERSION = {
8353
+ nodejs: "NODE_22_LTS",
8354
+ python: "PYTHON_312",
8355
+ dotnet: "DOTNET_8"
8356
+ };
8338
8357
  function isServiceName(value) {
8339
8358
  return SERVICES.includes(value);
8340
8359
  }
@@ -8430,7 +8449,10 @@ async function buildPortal(context) {
8430
8449
  if (!external && o.docsUrl) {
8431
8450
  throw new ServiceEditError("--docs-url needs --docs-provider external.");
8432
8451
  }
8433
- if (external && !o.docsUrl) {
8452
+ const docsUrl = o.docsUrl ?? (external && options.ask ? await options.ask.text("HTTPS address of your docs site", {
8453
+ validate: httpsProblem
8454
+ }) : void 0);
8455
+ if (external && !docsUrl) {
8434
8456
  throw new ServiceEditError(
8435
8457
  "--docs-provider external needs --docs-url <https url>."
8436
8458
  );
@@ -8444,6 +8466,13 @@ async function buildPortal(context) {
8444
8466
  const candidates = await detectExistingDocs(dir);
8445
8467
  if (candidates.length === 1 || candidates.length > 1 && options.acceptAllDocs) {
8446
8468
  content = candidates.map((candidate) => candidate.glob);
8469
+ } else if (candidates.length > 1 && options.ask) {
8470
+ const places = candidates.map((c) => c.path).join(", ");
8471
+ if (await options.ask.confirm(
8472
+ `Several places with documentation were found (${places}). Include all of them?`,
8473
+ true
8474
+ ))
8475
+ content = candidates.map((candidate) => candidate.glob);
8447
8476
  } else if (candidates.length > 1) {
8448
8477
  throw new ServiceEditError(
8449
8478
  `Several places with documentation were found (${candidates.map((c) => c.path).join(", ")}).`,
@@ -8462,7 +8491,7 @@ async function buildPortal(context) {
8462
8491
  return {
8463
8492
  ...options.disabled ? { enabled: false } : {},
8464
8493
  name,
8465
- ...external ? { docs: { provider: "external", url: o.docsUrl } } : content.length > 0 ? { docs: { content } } : {},
8494
+ ...external ? { docs: { provider: "external", url: docsUrl } } : content.length > 0 ? { docs: { content } } : {},
8466
8495
  ...o.noConsole ? { console: { enabled: false } } : {},
8467
8496
  ...o.requireAuth ? { requireAuth: true } : {},
8468
8497
  ...o.search && !external ? { search: o.search } : {}
@@ -8471,7 +8500,20 @@ async function buildPortal(context) {
8471
8500
  async function buildGateway(context) {
8472
8501
  const { options, dir, resolved } = context;
8473
8502
  const o = options.gateway ?? {};
8474
- const mode = o.origin ?? (o.url ? "proxy" : o.source || o.runtime ? "managed" : void 0);
8503
+ const ask4 = options.ask;
8504
+ let mode = o.origin ?? (o.url ? "proxy" : o.source || o.runtime ? "managed" : void 0);
8505
+ if (!mode && ask4) {
8506
+ mode = await ask4.choose("How does the gateway reach your API?", [
8507
+ {
8508
+ value: "proxy",
8509
+ label: "My API runs somewhere else: the gateway forwards calls to its https address"
8510
+ },
8511
+ {
8512
+ value: "managed",
8513
+ label: "The platform runs my code (nodejs, python or dotnet)"
8514
+ }
8515
+ ]);
8516
+ }
8475
8517
  if (!mode) {
8476
8518
  throw new ServiceEditError(
8477
8519
  "The gateway needs an origin: say how it reaches your API.",
@@ -8480,11 +8522,25 @@ async function buildGateway(context) {
8480
8522
  }
8481
8523
  let origin;
8482
8524
  if (mode === "proxy") {
8483
- if (!o.url)
8525
+ const url2 = o.url ?? (ask4 ? await ask4.text("https address of your API", {
8526
+ validate: httpsProblem
8527
+ }) : void 0);
8528
+ if (!url2)
8484
8529
  throw new ServiceEditError("--origin proxy needs --url <https url>.");
8485
- origin = { mode, url: o.url };
8530
+ origin = { mode, url: url2 };
8486
8531
  } else {
8487
- if (!o.source || !o.runtime || !o.runtimeVersion) {
8532
+ const source = o.source ?? (ask4 ? await ask4.text("Folder with your code, relative to the project", {
8533
+ default: "server"
8534
+ }) : void 0);
8535
+ const runtime = o.runtime ?? (ask4 ? await ask4.choose("Runtime of your code", [
8536
+ { value: "nodejs", label: "Node.js" },
8537
+ { value: "python", label: "Python" },
8538
+ { value: "dotnet", label: ".NET" }
8539
+ ]) : void 0);
8540
+ const runtimeVersion = o.runtimeVersion ?? (ask4 && runtime ? await ask4.text("Runtime version", {
8541
+ default: DEFAULT_RUNTIME_VERSION[runtime]
8542
+ }) : void 0);
8543
+ if (!source || !runtime || !runtimeVersion) {
8488
8544
  throw new ServiceEditError(
8489
8545
  "--origin managed needs --source, --runtime and --runtime-version.",
8490
8546
  "For example: --source server --runtime nodejs --runtime-version NODE_22_LTS."
@@ -8492,16 +8548,29 @@ async function buildGateway(context) {
8492
8548
  }
8493
8549
  origin = {
8494
8550
  mode,
8495
- source: insideProject(dir, o.source, "Source folder"),
8496
- runtime: o.runtime,
8497
- runtimeVersion: o.runtimeVersion,
8551
+ source: insideProject(dir, source, "Source folder"),
8552
+ runtime,
8553
+ runtimeVersion,
8498
8554
  ...o.exclude?.length ? { exclude: o.exclude } : {}
8499
8555
  };
8500
8556
  }
8501
8557
  let specs;
8502
- if (o.spec?.length) {
8558
+ let requestedSpecs = o.spec ?? [];
8559
+ let createSpec = o.createSpec === true;
8560
+ if (requestedSpecs.length === 0 && !createSpec && ask4) {
8561
+ const answer = await ask4.choose("OpenAPI file of your API", [
8562
+ { value: "create", label: "Start from a small file the command creates" },
8563
+ { value: "existing", label: "Use a file I already have" }
8564
+ ]);
8565
+ if (answer === "create") createSpec = true;
8566
+ else
8567
+ requestedSpecs = [
8568
+ await ask4.text("Path of your OpenAPI file, relative to the project")
8569
+ ];
8570
+ }
8571
+ if (requestedSpecs.length > 0) {
8503
8572
  specs = [];
8504
- for (const requested of o.spec) {
8573
+ for (const requested of requestedSpecs) {
8505
8574
  const relative = insideProject(dir, requested, "OpenAPI file");
8506
8575
  if (!await stat3(path14.join(dir, relative)).then(
8507
8576
  (s) => s.isFile(),
@@ -8513,7 +8582,7 @@ async function buildGateway(context) {
8513
8582
  }
8514
8583
  specs.push(relative);
8515
8584
  }
8516
- } else if (o.createSpec) {
8585
+ } else if (createSpec) {
8517
8586
  const target = path14.join(resolved.apisDir, "openapi.yaml");
8518
8587
  await addFile(context, target, STARTER_SPEC(resolved.project.name));
8519
8588
  specs = [rel(dir, target)];
@@ -8545,12 +8614,15 @@ async function buildGateway(context) {
8545
8614
  ...Object.keys(policies).length > 0 ? { policies } : {}
8546
8615
  };
8547
8616
  }
8548
- function buildIdentity(context) {
8617
+ async function buildIdentity(context) {
8549
8618
  const { options } = context;
8550
8619
  const o = options.identity ?? {};
8551
8620
  const provider = o.provider ?? "apitogo";
8552
8621
  if (provider === "oidc") {
8553
- if (!o.issuer)
8622
+ const issuer = o.issuer ?? (options.ask ? await options.ask.text("https address of your OIDC issuer", {
8623
+ validate: httpsProblem
8624
+ }) : void 0);
8625
+ if (!issuer)
8554
8626
  throw new ServiceEditError("--provider oidc needs --issuer <https url>.");
8555
8627
  if (o.connection || o.native === false) {
8556
8628
  throw new ServiceEditError(
@@ -8560,7 +8632,7 @@ function buildIdentity(context) {
8560
8632
  return {
8561
8633
  ...options.disabled ? { enabled: false } : {},
8562
8634
  provider,
8563
- issuer: o.issuer,
8635
+ issuer,
8564
8636
  ...o.clientId ? { clientId: o.clientId } : {}
8565
8637
  };
8566
8638
  }
@@ -8634,6 +8706,15 @@ async function buildPlans(context) {
8634
8706
  function reparse(configPath, raw) {
8635
8707
  return parseConfigV2(JSON.parse(JSON.stringify(raw)), configPath);
8636
8708
  }
8709
+ function missingServicesOf(issues) {
8710
+ const missing = /* @__PURE__ */ new Set();
8711
+ for (const { code } of issues) {
8712
+ if (code === "PLANS_GATEWAY_REQUIRED") missing.add("gateway");
8713
+ if (code === "PLANS_IDENTITY_REQUIRED" || code === "PORTAL_CAPABILITY_MISSING")
8714
+ missing.add("identity");
8715
+ }
8716
+ return [...missing];
8717
+ }
8637
8718
  function dependencyProblems(config2, configPath) {
8638
8719
  return checkDependencies(config2, resolveConfigV2(config2, configPath));
8639
8720
  }
@@ -8655,7 +8736,7 @@ async function planAddService(service, options) {
8655
8736
  keep: [],
8656
8737
  notes: []
8657
8738
  };
8658
- const section = service === "portal" ? await buildPortal(context) : service === "gateway" ? await buildGateway(context) : service === "identity" ? buildIdentity(context) : await buildPlans(context);
8739
+ const section = service === "portal" ? await buildPortal(context) : service === "gateway" ? await buildGateway(context) : service === "identity" ? await buildIdentity(context) : await buildPlans(context);
8659
8740
  const next = reparse(resolved.configPath, { ...config2, [service]: section });
8660
8741
  if (!options.disabled) {
8661
8742
  const before = new Set(
@@ -8669,7 +8750,8 @@ async function planAddService(service, options) {
8669
8750
  if (added.length > 0) {
8670
8751
  throw new ServiceEditError(
8671
8752
  added.map((issue4) => issue4.message).join(" "),
8672
- added.map((issue4) => issue4.hint).filter(Boolean).join(" ") || "Add the missing service first."
8753
+ added.map((issue4) => issue4.hint).filter(Boolean).join(" ") || "Add the missing service first.",
8754
+ missingServicesOf(added)
8673
8755
  );
8674
8756
  }
8675
8757
  }
@@ -8727,7 +8809,7 @@ async function planRemoveService(service, options) {
8727
8809
  async function writeConfig(configPath, config2) {
8728
8810
  const temporary = `${configPath}.${process.pid}.tmp`;
8729
8811
  try {
8730
- await writeFile6(temporary, `${JSON.stringify(config2, null, 2)}
8812
+ await writeFile7(temporary, `${JSON.stringify(config2, null, 2)}
8731
8813
  `, "utf8");
8732
8814
  await rename3(temporary, configPath);
8733
8815
  } catch (error) {
@@ -8740,7 +8822,7 @@ async function applyServiceEdit(edit) {
8740
8822
  for (const file of edit.files) {
8741
8823
  await mkdir5(path14.dirname(file.path), { recursive: true });
8742
8824
  try {
8743
- await writeFile6(file.path, file.content, { flag: "wx" });
8825
+ await writeFile7(file.path, file.content, { flag: "wx" });
8744
8826
  created.push(file.path);
8745
8827
  } catch (error) {
8746
8828
  if (error.code !== "EEXIST") throw error;
@@ -8788,7 +8870,7 @@ async function clearProjectId(dir, projectId) {
8788
8870
 
8789
8871
  // src/config/v2/ledger.ts
8790
8872
  init_invariant();
8791
- import { mkdir as mkdir6, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
8873
+ import { mkdir as mkdir6, readFile as readFile8, rename as rename4, rm as rm5, writeFile as writeFile8 } from "node:fs/promises";
8792
8874
  import path15 from "node:path";
8793
8875
  var LEDGER_VERSION = 1;
8794
8876
  var LedgerError = class extends ApitogoError {
@@ -8847,7 +8929,7 @@ async function writeLedger(state, ledger) {
8847
8929
  await mkdir6(path15.dirname(state.file), { recursive: true });
8848
8930
  const temporary = `${state.file}.${process.pid}.tmp`;
8849
8931
  try {
8850
- await writeFile7(temporary, `${JSON.stringify(ledger, null, 2)}
8932
+ await writeFile8(temporary, `${JSON.stringify(ledger, null, 2)}
8851
8933
  `, "utf8");
8852
8934
  await rename4(temporary, state.file);
8853
8935
  } catch (error) {
@@ -8928,6 +9010,10 @@ async function clearServices(state, services) {
8928
9010
  );
8929
9011
  if (Object.keys(remaining).length === Object.keys(ledger.services).length)
8930
9012
  return;
9013
+ if (Object.keys(remaining).length === 0) {
9014
+ await rm5(state.file, { force: true });
9015
+ return;
9016
+ }
8931
9017
  await writeLedger(state, { ...ledger, services: remaining });
8932
9018
  }
8933
9019
 
@@ -9133,7 +9219,9 @@ var PlatformApiClient = class {
9133
9219
  headers: { "Content-Type": "application/json" },
9134
9220
  body: JSON.stringify({
9135
9221
  planSlug: body.planSlug,
9136
- currency: body.currency ?? null
9222
+ currency: body.currency ?? null,
9223
+ // Only a platform admin may skip the payment; the platform refuses it for anyone else.
9224
+ ...body.skipPayment ? { skipPayment: true } : {}
9137
9225
  })
9138
9226
  }
9139
9227
  );
@@ -9162,13 +9250,6 @@ var PlatformApiClient = class {
9162
9250
  `/api/v1/projects/${projectId}/infrastructure/provisioning-jobs`
9163
9251
  );
9164
9252
  }
9165
- getDeploymentToken(projectId, regenerate = false) {
9166
- const query = regenerate ? "?regenerate=true" : "";
9167
- return this.request(
9168
- "POST",
9169
- `/api/v1/projects/${projectId}/infrastructure/deployment-token${query}`
9170
- );
9171
- }
9172
9253
  /**
9173
9254
  * Publishes a built portal (.tgz, root folder `dist`). The platform holds the deploy server key and the project's
9174
9255
  * deployment token, so the CLI only needs its own login. `env` falls back to the platform default.
@@ -9431,6 +9512,20 @@ var PlatformApiClient = class {
9431
9512
  );
9432
9513
  return result ?? null;
9433
9514
  }
9515
+ /**
9516
+ * Gives the plans backend the tenant's own Stripe settings. `apiKey` and `webhookSecret` are `secret:NAME`
9517
+ * references to project secrets, never values. Queues a job that updates the running backend.
9518
+ */
9519
+ putDevPortalBilling(projectId, body) {
9520
+ return this.request(
9521
+ "PUT",
9522
+ `/api/v1/projects/${projectId}/dev-portal/billing`,
9523
+ {
9524
+ headers: { "Content-Type": "application/json" },
9525
+ body: JSON.stringify(body)
9526
+ }
9527
+ );
9528
+ }
9434
9529
  /** Applies the project's identity to its plans backend as the sign-in provider. Queues a job. */
9435
9530
  linkDevPortalIdentity(projectId) {
9436
9531
  return this.request(
@@ -9827,12 +9922,12 @@ import {
9827
9922
  readFile as readFile12,
9828
9923
  rename as rename6,
9829
9924
  rm as rm8,
9830
- writeFile as writeFile14
9925
+ writeFile as writeFile15
9831
9926
  } from "node:fs/promises";
9832
9927
  import path48 from "node:path";
9833
9928
 
9834
9929
  // src/vite/build.ts
9835
- import { mkdir as mkdir11, readFile as readFile10, rename as rename5, rm as rm7, writeFile as writeFile12 } from "node:fs/promises";
9930
+ import { mkdir as mkdir11, readFile as readFile10, rename as rename5, rm as rm7, writeFile as writeFile13 } from "node:fs/promises";
9836
9931
  import path46 from "node:path";
9837
9932
  import { build as esbuild } from "esbuild";
9838
9933
  import { build as viteBuild, mergeConfig as mergeConfig3 } from "vite";
@@ -9980,7 +10075,7 @@ import {
9980
10075
  // package.json
9981
10076
  var package_default = {
9982
10077
  name: "@lukoweb/apitogo",
9983
- version: "0.2.30",
10078
+ version: "0.2.34",
9984
10079
  type: "module",
9985
10080
  sideEffects: [
9986
10081
  "**/*.css",
@@ -10058,6 +10153,7 @@ var package_default = {
10058
10153
  build: "esbuild src/cli/cli.ts src/vite/prerender/worker.ts --outdir=dist/cli --entry-names=[name] --bundle --format=esm --packages=external --target=node20 --platform=node --log-level=error",
10059
10154
  postbuild: "tsx scripts/check-external-deps.ts",
10060
10155
  typecheck: "tsc --project tsconfig.app.json --noEmit",
10156
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
10061
10157
  "generate:types": "tsx scripts/generate-types.js && tsx scripts/generate-flat-config.js",
10062
10158
  "build:standalone": "vite build --mode standalone --config vite.standalone.config.ts --log-level=error",
10063
10159
  prepack: "npm run build && tsx scripts/generate-publish-exports.ts",
@@ -12090,8 +12186,8 @@ var extractRichH1 = (content) => {
12090
12186
  if (!h1) return void 0;
12091
12187
  const children = h1.children;
12092
12188
  const hasJsx = children.some(isMdxJsxElement);
12093
- const label = mdastToString(h1).trim();
12094
- return hasJsx ? { label, rich: children } : { label };
12189
+ const label2 = mdastToString(h1).trim();
12190
+ return hasJsx ? { label: label2, rich: children } : { label: label2 };
12095
12191
  } catch {
12096
12192
  return void 0;
12097
12193
  }
@@ -12152,12 +12248,12 @@ var NavigationResolver = class {
12152
12248
  return void 0;
12153
12249
  }
12154
12250
  const richH1 = extractRichH1(content);
12155
- const label = data.navigation_label ?? data.sidebar_label ?? data.title ?? richH1?.label ?? extractTitleFromContent(content) ?? filePath;
12251
+ const label2 = data.navigation_label ?? data.sidebar_label ?? data.title ?? richH1?.label ?? extractTitleFromContent(content) ?? filePath;
12156
12252
  const icon = data.navigation_icon ?? data.sidebar_icon;
12157
12253
  const doc = {
12158
12254
  type: "doc",
12159
12255
  file: filePath,
12160
- label,
12256
+ label: label2,
12161
12257
  icon,
12162
12258
  display: data.navigation_display,
12163
12259
  categoryLabel,
@@ -12229,13 +12325,13 @@ var NavigationResolver = class {
12229
12325
  init_invariant();
12230
12326
 
12231
12327
  // src/vite/debug.ts
12232
- import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
12328
+ import { mkdir as mkdir7, writeFile as writeFile9 } from "node:fs/promises";
12233
12329
  import path27 from "node:path";
12234
12330
  async function writePluginDebugCode(rootDir, pluginName, code, extension = "js") {
12235
12331
  if (process.env.ZUDOKU_BUILD_DEBUG) {
12236
12332
  const debugDir = path27.join(rootDir, "dist", "debug");
12237
12333
  await mkdir7(debugDir, { recursive: true });
12238
- await writeFile8(
12334
+ await writeFile9(
12239
12335
  path27.join(debugDir, `${pluginName}.${extension}`),
12240
12336
  typeof code === "string" ? code : code.join("\n")
12241
12337
  );
@@ -12543,8 +12639,8 @@ var viteApiPlugin = async () => {
12543
12639
  return acc;
12544
12640
  }, /* @__PURE__ */ new Map());
12545
12641
  const categoryList = Array.from(categories.entries()).map(
12546
- ([label, tags]) => ({
12547
- label,
12642
+ ([label2, tags]) => ({
12643
+ label: label2,
12548
12644
  tags: Array.from(tags)
12549
12645
  })
12550
12646
  );
@@ -13092,7 +13188,7 @@ var plugin_local_manifest_default = viteLocalManifestPlugin;
13092
13188
  init_loader();
13093
13189
  init_ProtectedRoutesSchema();
13094
13190
  init_joinUrl();
13095
- import { mkdir as mkdir8, writeFile as writeFile9 } from "node:fs/promises";
13191
+ import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
13096
13192
  import path34 from "node:path";
13097
13193
  import { matchPath } from "react-router";
13098
13194
  var processMarkdownFile = async (filePath) => {
@@ -13204,7 +13300,7 @@ var viteMarkdownExportPlugin = () => {
13204
13300
  const segments = routePath === "/" ? ["index"] : routePath.split("/").filter(Boolean);
13205
13301
  const outputPath = `${path34.join(distDir, ...segments)}.md`;
13206
13302
  await mkdir8(path34.dirname(outputPath), { recursive: true });
13207
- await writeFile9(outputPath, finalMarkdown, "utf-8");
13303
+ await writeFile10(outputPath, finalMarkdown, "utf-8");
13208
13304
  } catch (error) {
13209
13305
  console.warn(`Failed to export markdown for ${routePath}:`, error);
13210
13306
  }
@@ -13214,7 +13310,7 @@ var viteMarkdownExportPlugin = () => {
13214
13310
  config2.__meta.rootDir,
13215
13311
  "node_modules/.apitogo/markdown-info.json"
13216
13312
  );
13217
- await writeFile9(
13313
+ await writeFile10(
13218
13314
  markdownInfoPath,
13219
13315
  JSON.stringify(markdownFileInfos, null, 2),
13220
13316
  "utf-8"
@@ -13842,7 +13938,7 @@ function isBareSpecifier(source) {
13842
13938
  return !source.startsWith("\0") && !source.startsWith(".") && !source.startsWith("/") && !path38.isAbsolute(source);
13843
13939
  }
13844
13940
  function isInsideNodeModules(filePath) {
13845
- return filePath.split(path38.sep).includes("node_modules");
13941
+ return filePath.split(/[\\/]/).includes("node_modules");
13846
13942
  }
13847
13943
  function shouldResolveFromSiteRoot(source, importer, rootDir) {
13848
13944
  if (!importer || !isBareSpecifier(source)) {
@@ -14299,7 +14395,7 @@ ${cssLinks}
14299
14395
  init_package_json();
14300
14396
  init_joinUrl();
14301
14397
  import assert from "node:assert";
14302
- import { cp, mkdir as mkdir9, writeFile as writeFile10 } from "node:fs/promises";
14398
+ import { cp, mkdir as mkdir9, writeFile as writeFile11 } from "node:fs/promises";
14303
14399
  import path42 from "node:path";
14304
14400
  var pkgJson = getZudokuPackageJson();
14305
14401
  function generateOutput({
@@ -14360,7 +14456,7 @@ async function writeOutput(dir, {
14360
14456
  const outputDir = process.env.VERCEL ? path42.join(dir, ".vercel/output") : path42.join(dir, "dist/.output");
14361
14457
  await mkdir9(outputDir, { recursive: true });
14362
14458
  const outputFile = path42.join(outputDir, "config.json");
14363
- await writeFile10(outputFile, JSON.stringify(output4, null, 2), "utf-8");
14459
+ await writeFile11(outputFile, JSON.stringify(output4, null, 2), "utf-8");
14364
14460
  if (process.env.VERCEL) {
14365
14461
  console.log("Wrote Vercel output to", outputDir);
14366
14462
  }
@@ -14756,7 +14852,7 @@ var runPrerender = async (options) => {
14756
14852
  });
14757
14853
  const indexHtml = path46.join(clientOutDir, "index.html");
14758
14854
  if (!workerResults.find((r) => r.outputPath === indexHtml)) {
14759
- await writeFile12(indexHtml, html, "utf-8");
14855
+ await writeFile13(indexHtml, html, "utf-8");
14760
14856
  }
14761
14857
  const statusPages = workerResults.flatMap(
14762
14858
  (r) => /400|404|500\.html$/.test(r.outputPath) ? r.outputPath : []
@@ -14781,7 +14877,7 @@ var runPrerender = async (options) => {
14781
14877
  rewrites
14782
14878
  });
14783
14879
  if (ZuploEnv.isZuplo && issuer) {
14784
- await writeFile12(
14880
+ await writeFile13(
14785
14881
  path46.join(dir, DIST_DIR, ".output/zuplo.json"),
14786
14882
  JSON.stringify({ issuer }, null, 2),
14787
14883
  "utf-8"
@@ -14804,7 +14900,7 @@ var bundleSSREntry = async (options) => {
14804
14900
  '"__BASE_PATH__"',
14805
14901
  basePath ? JSON.stringify(basePath) : "undefined"
14806
14902
  );
14807
- await writeFile12(tempEntryPath, entryContent, "utf-8");
14903
+ await writeFile13(tempEntryPath, entryContent, "utf-8");
14808
14904
  try {
14809
14905
  await esbuild({
14810
14906
  entryPoints: [tempEntryPath],
@@ -14827,7 +14923,7 @@ init_package_json();
14827
14923
 
14828
14924
  // src/cli/init/v2-packages.ts
14829
14925
  import { existsSync as existsSync5 } from "node:fs";
14830
- import { readFile as readFile11, writeFile as writeFile13 } from "node:fs/promises";
14926
+ import { readFile as readFile11, writeFile as writeFile14 } from "node:fs/promises";
14831
14927
  import path47 from "node:path";
14832
14928
  var CORE_PACKAGES = [
14833
14929
  "@lukoweb/apitogo",
@@ -14847,7 +14943,7 @@ function packageNameFrom(name) {
14847
14943
  async function ensurePackageJson(dir, name) {
14848
14944
  const file = path47.join(dir, "package.json");
14849
14945
  if (existsSync5(file)) return false;
14850
- await writeFile13(
14946
+ await writeFile14(
14851
14947
  file,
14852
14948
  `${JSON.stringify(
14853
14949
  {
@@ -14940,7 +15036,7 @@ function portalBuildStep(context, options = {}, build2 = runBuild) {
14940
15036
  await mkdir12(path48.dirname(buildDir), { recursive: true });
14941
15037
  await rename6(path48.join(root, "dist"), buildDir);
14942
15038
  if (prepared.publishedManifest) {
14943
- await writeFile14(
15039
+ await writeFile15(
14944
15040
  path48.join(buildDir, MANIFEST_FILE),
14945
15041
  `${JSON.stringify(prepared.publishedManifest, null, 2)}
14946
15042
  `
@@ -15137,14 +15233,14 @@ async function readPlansFile(file) {
15137
15233
  return parsed.data.plans;
15138
15234
  }
15139
15235
  var BILLING_PERIOD = { month: "monthly", year: "annual" };
15140
- function toPlatformPlans(plans) {
15236
+ function toPlatformPlans(plans, options = {}) {
15141
15237
  return plans.map((plan2, index) => {
15142
15238
  const paid = plan2.pricingModel !== "custom" && (plan2.priceAmount ?? 0) > 0;
15143
- if (paid)
15239
+ if (paid && !options.billing)
15144
15240
  throw new PlansFileError(
15145
15241
  "PLANS_PAID_UNSUPPORTED",
15146
- `The plan "${plan2.sku}" has a price, and deploying paid plans is not available in this CLI version yet.`,
15147
- "Remove the plan, or set priceAmount to 0 (or pricingModel to custom for a contact-us plan)."
15242
+ `The plan "${plan2.sku}" has a price, and plans.billing.provider is not stripe, so nobody could pay for it.`,
15243
+ "Set plans.billing.provider to stripe (with plans.billing.apiKey), or set priceAmount to 0 (or pricingModel to custom for a contact-us plan)."
15148
15244
  );
15149
15245
  const limits = plan2.limits ? {
15150
15246
  ...plan2.limits.rateLimit ? {
@@ -15233,7 +15329,11 @@ async function plansFileProblem(resolved) {
15233
15329
  const plans = resolved.plans;
15234
15330
  if (!plans?.enabled) return { wanted: [] };
15235
15331
  try {
15236
- return { wanted: toPlatformPlans(await readPlansFile(plans.file)) };
15332
+ return {
15333
+ wanted: toPlatformPlans(await readPlansFile(plans.file), {
15334
+ billing: plans.billing.provider === "stripe"
15335
+ })
15336
+ };
15237
15337
  } catch (error) {
15238
15338
  if (!(error instanceof PlansFileError)) throw error;
15239
15339
  return {
@@ -15483,7 +15583,7 @@ async function runCheckV2(dir, args = {}, deps = {}) {
15483
15583
  ...report
15484
15584
  };
15485
15585
  }
15486
- var line = (label, issue4) => `${label}: ${formatConfigIssue(issue4)}`;
15586
+ var line = (label2, issue4) => `${label2}: ${formatConfigIssue(issue4)}`;
15487
15587
  function formatCheckV2(output4) {
15488
15588
  const lines = [
15489
15589
  ...output4.errors.map((issue4) => line("error", issue4)),
@@ -15950,6 +16050,56 @@ async function installMissingPackages(dir, config2, version, options) {
15950
16050
  return { status: "installed", packages: missing, command: shown };
15951
16051
  }
15952
16052
 
16053
+ // src/cli/services/prompter.ts
16054
+ import { createInterface as createInterface2 } from "node:readline/promises";
16055
+ var terminalQuestion = async (prompt) => {
16056
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
16057
+ try {
16058
+ return await rl.question(prompt);
16059
+ } finally {
16060
+ rl.close();
16061
+ }
16062
+ };
16063
+ function createAsker(question = terminalQuestion, say = (line2) => console.log(line2)) {
16064
+ return {
16065
+ async text(prompt, options) {
16066
+ for (; ; ) {
16067
+ const suffix = options?.default ? ` [${options.default}]` : "";
16068
+ const answer = (await question(`${prompt}${suffix}: `)).trim();
16069
+ const value = answer || options?.default || "";
16070
+ if (!value) {
16071
+ say("A value is needed.");
16072
+ continue;
16073
+ }
16074
+ const problem = options?.validate?.(value);
16075
+ if (problem) {
16076
+ say(problem);
16077
+ continue;
16078
+ }
16079
+ return value;
16080
+ }
16081
+ },
16082
+ async choose(prompt, choices) {
16083
+ say(prompt);
16084
+ choices.forEach((choice, index) => {
16085
+ say(` ${index + 1}) ${choice.label}`);
16086
+ });
16087
+ for (; ; ) {
16088
+ const answer = (await question(`Choose 1-${choices.length} [1]: `)).trim();
16089
+ const index = Number(answer || "1");
16090
+ const chosen = Number.isInteger(index) && index >= 1 && index <= choices.length ? choices[index - 1] : choices.find((choice) => choice.value === answer);
16091
+ if (chosen) return chosen.value;
16092
+ say(`Pick a number from 1 to ${choices.length}.`);
16093
+ }
16094
+ },
16095
+ async confirm(prompt, defaultYes = false) {
16096
+ const answer = (await question(`${prompt} (${defaultYes ? "Y/n" : "y/N"}) `)).trim().toLowerCase();
16097
+ if (!answer) return defaultYes;
16098
+ return answer === "y" || answer === "yes";
16099
+ }
16100
+ };
16101
+ }
16102
+
15953
16103
  // src/cli/services/handler.ts
15954
16104
  var list = (value) => {
15955
16105
  const items = (Array.isArray(value) ? value : value === void 0 ? [] : [value]).filter(
@@ -16004,15 +16154,6 @@ function addOptionsFromArguments(service, argv) {
16004
16154
  };
16005
16155
  }
16006
16156
  var canPrompt2 = (json) => !json && process.stdin.isTTY === true && process.stdout.isTTY === true;
16007
- async function confirm(question) {
16008
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
16009
- try {
16010
- const answer = (await rl.question(question)).trim().toLowerCase();
16011
- return answer === "y" || answer === "yes";
16012
- } finally {
16013
- rl.close();
16014
- }
16015
- }
16016
16157
  function describe(edit, dir) {
16017
16158
  return {
16018
16159
  service: edit.service,
@@ -16024,6 +16165,7 @@ function describe(edit, dir) {
16024
16165
  }
16025
16166
  async function run(action, argv, plan2, deps = {}) {
16026
16167
  const json = argv.json === true;
16168
+ const asker = argv.yes === true ? void 0 : deps.asker ?? (canPrompt2(json) ? createAsker() : void 0);
16027
16169
  try {
16028
16170
  if (!isServiceName(argv.service)) {
16029
16171
  throw new ServiceEditError(
@@ -16038,7 +16180,28 @@ async function run(action, argv, plan2, deps = {}) {
16038
16180
  "Create one with `apitogo init`."
16039
16181
  );
16040
16182
  }
16041
- const edit = await plan2(argv.service);
16183
+ const service = argv.service;
16184
+ let edit;
16185
+ for (let attempt = 0; edit === void 0; attempt++) {
16186
+ try {
16187
+ edit = await plan2(service, asker);
16188
+ } catch (error) {
16189
+ const missing = action === "add" && error instanceof ServiceEditError ? error.missingServices : [];
16190
+ if (missing.length === 0 || !asker || argv.dryRun || attempt >= 3)
16191
+ throw error;
16192
+ for (const needed2 of missing) {
16193
+ if (!await asker.confirm(
16194
+ `${service} needs ${needed2}. Add ${needed2} now?`,
16195
+ true
16196
+ ))
16197
+ throw error;
16198
+ await addService(
16199
+ { service: needed2, dir: argv.dir, install: argv.install },
16200
+ deps
16201
+ );
16202
+ }
16203
+ }
16204
+ }
16042
16205
  const needed = action === "add" ? missingPackages(dir, packagesForConfig(edit.config)) : [];
16043
16206
  if (!json) {
16044
16207
  for (const line2 of formatServiceEdit(edit))
@@ -16059,13 +16222,13 @@ async function run(action, argv, plan2, deps = {}) {
16059
16222
  return;
16060
16223
  }
16061
16224
  if (argv.yes !== true) {
16062
- if (!canPrompt2(json)) {
16225
+ if (!asker) {
16063
16226
  throw new ServiceEditError(
16064
16227
  "This is not an interactive terminal.",
16065
16228
  "Pass --yes to apply the change, or --dry-run to only see it."
16066
16229
  );
16067
16230
  }
16068
- if (!await confirm("Apply this change? (y/N) ")) {
16231
+ if (!await asker.confirm("Apply this change?")) {
16069
16232
  printResultToConsole("Cancelled. Nothing was written.");
16070
16233
  return;
16071
16234
  }
@@ -16139,7 +16302,10 @@ async function installPackages(dir, edit, argv, deps, json) {
16139
16302
  var addService = (argv, deps = {}) => run(
16140
16303
  "add",
16141
16304
  argv,
16142
- (service) => planAddService(service, addOptionsFromArguments(service, argv)),
16305
+ (service, ask4) => planAddService(service, {
16306
+ ...addOptionsFromArguments(service, argv),
16307
+ ask: ask4
16308
+ }),
16143
16309
  deps
16144
16310
  );
16145
16311
  var removeService = (argv) => run(
@@ -16498,7 +16664,7 @@ function formatDevPortalBackendResponse(projectId, backend) {
16498
16664
  // src/cli/platform/helpers/dev-portal-manifest.ts
16499
16665
  init_gateway_limits();
16500
16666
  import { existsSync as existsSync7 } from "node:fs";
16501
- import { access as access6, mkdir as mkdir13, readFile as readFile17, writeFile as writeFile15 } from "node:fs/promises";
16667
+ import { access as access6, mkdir as mkdir13, readFile as readFile17, writeFile as writeFile16 } from "node:fs/promises";
16502
16668
  import path57 from "node:path";
16503
16669
 
16504
16670
  // src/cli/make-config/json-config.ts
@@ -17126,11 +17292,11 @@ function buildThinWrapperWithCustomPages(tsxRoutePaths) {
17126
17292
  const customPages = tsxRoutePaths.map((routePath) => {
17127
17293
  const slug = routePath.replace(/^\//, "");
17128
17294
  const name = pageComponentName(slug);
17129
- const label = labelFromRoutePath(routePath);
17295
+ const label2 = labelFromRoutePath(routePath);
17130
17296
  return ` {
17131
17297
  type: "custom-page",
17132
17298
  path: ${JSON.stringify(routePath)},
17133
- label: ${JSON.stringify(label)},
17299
+ label: ${JSON.stringify(label2)},
17134
17300
  element: <${name} />,
17135
17301
  },`;
17136
17302
  }).join("\n");
@@ -17213,7 +17379,7 @@ function ensureHeaderDocsNavigation(config2, docRoutes2) {
17213
17379
  }
17214
17380
  return { ...config2, header: { ...header, navigation } };
17215
17381
  }
17216
- function ensureHeaderApiReferenceNavigation(config2, to, label = HEADER_API_REFERENCE_LABEL) {
17382
+ function ensureHeaderApiReferenceNavigation(config2, to, label2 = HEADER_API_REFERENCE_LABEL) {
17217
17383
  const current = readHeaderNavigation(config2);
17218
17384
  if (!current) return config2;
17219
17385
  const { header, navigation } = current;
@@ -17221,7 +17387,7 @@ function ensureHeaderApiReferenceNavigation(config2, to, label = HEADER_API_REFE
17221
17387
  return config2;
17222
17388
  }
17223
17389
  const docsIndex = navigation.findIndex(isDocsItem);
17224
- const item = { label, to };
17390
+ const item = { label: label2, to };
17225
17391
  if (docsIndex >= 0) {
17226
17392
  navigation.splice(docsIndex + 1, 0, item);
17227
17393
  } else {
@@ -17818,7 +17984,7 @@ async function writeManifest2(siteDirectory, manifest) {
17818
17984
  ...safeManifest
17819
17985
  } = manifest;
17820
17986
  config2 = applyManifestToConfig(config2, safeManifest);
17821
- await writeFile15(filePath, `${JSON.stringify(config2, null, 2)}
17987
+ await writeFile16(filePath, `${JSON.stringify(config2, null, 2)}
17822
17988
  `, "utf8");
17823
17989
  return filePath;
17824
17990
  }
@@ -17830,7 +17996,7 @@ import {
17830
17996
  mkdir as mkdir14,
17831
17997
  readFile as readFile18,
17832
17998
  unlink,
17833
- writeFile as writeFile16
17999
+ writeFile as writeFile17
17834
18000
  } from "node:fs/promises";
17835
18001
  import path58 from "node:path";
17836
18002
  var ENV_FILENAME = ".env";
@@ -18105,7 +18271,7 @@ async function writeScaffoldEnv(siteDirectory) {
18105
18271
  await access7(envPath);
18106
18272
  } catch {
18107
18273
  await mkdir14(resolvedDirectory, { recursive: true });
18108
- await writeFile16(envPath, createEnvContent(), "utf8");
18274
+ await writeFile17(envPath, createEnvContent(), "utf8");
18109
18275
  filesWritten.push(ENV_FILENAME);
18110
18276
  }
18111
18277
  for (const legacyName of [".env.example"]) {
@@ -18134,7 +18300,7 @@ async function writeScaffoldEnv(siteDirectory) {
18134
18300
  if (!hasEntry) {
18135
18301
  const suffix = gitignore.length === 0 ? "" : gitignore.endsWith("\n") ? "" : "\n";
18136
18302
  if (gitignore.length === 0) {
18137
- await writeFile16(gitignorePath, `${entry}
18303
+ await writeFile17(gitignorePath, `${entry}
18138
18304
  `, "utf8");
18139
18305
  } else {
18140
18306
  await appendFile(gitignorePath, `${suffix}${entry}
@@ -18918,7 +19084,7 @@ var check_default = {
18918
19084
 
18919
19085
  // src/cli/make-config/handler.ts
18920
19086
  import { constants as constants2 } from "node:fs";
18921
- import { access as access12, mkdir as mkdir17, readFile as readFile24, writeFile as writeFile21 } from "node:fs/promises";
19087
+ import { access as access12, mkdir as mkdir17, readFile as readFile24, writeFile as writeFile22 } from "node:fs/promises";
18922
19088
  import path71 from "node:path";
18923
19089
  import { stdin as input2 } from "node:process";
18924
19090
  import { glob as glob8 } from "glob";
@@ -19058,12 +19224,12 @@ function withDefaultIncludedActionKeys(plan2, actionKeys) {
19058
19224
  }
19059
19225
 
19060
19226
  // src/cli/scaffold/dev-portal-config-pages.ts
19061
- import { access as access8, readFile as readFile20, rename as rename7, rm as rm9, writeFile as writeFile17 } from "node:fs/promises";
19227
+ import { access as access8, readFile as readFile20, rename as rename7, rm as rm9, writeFile as writeFile18 } from "node:fs/promises";
19062
19228
  import path63 from "node:path";
19063
19229
  async function writeConfigAtomically(filePath, content) {
19064
19230
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
19065
19231
  try {
19066
- await writeFile17(temporaryPath, content, "utf8");
19232
+ await writeFile18(temporaryPath, content, "utf8");
19067
19233
  await rename7(temporaryPath, filePath);
19068
19234
  } catch (error) {
19069
19235
  await rm9(temporaryPath, { force: true });
@@ -19555,10 +19721,10 @@ ${importStatement}${content.slice(lastImportEnd)}`;
19555
19721
  return `${importStatement}
19556
19722
  ${content}`;
19557
19723
  }
19558
- function formatCustomPageNavigationEntry(slug, label, indent) {
19724
+ function formatCustomPageNavigationEntry(slug, label2, indent) {
19559
19725
  const componentName = pageComponentName2(slug);
19560
19726
  const inner = `${indent} `;
19561
- const labelLine = label?.trim() ? `${inner}label: ${JSON.stringify(label.trim())},
19727
+ const labelLine = label2?.trim() ? `${inner}label: ${JSON.stringify(label2.trim())},
19562
19728
  ` : "";
19563
19729
  return `${indent}{
19564
19730
  ${inner}type: "custom-page",
@@ -19566,21 +19732,21 @@ ${inner}path: "/${normalizePageSlug2(slug)}",
19566
19732
  ` + labelLine + `${inner}element: <${componentName} />,
19567
19733
  ${indent}},`;
19568
19734
  }
19569
- function formatDocumentationCategoryWithPage(slug, label, indent) {
19735
+ function formatDocumentationCategoryWithPage(slug, label2, indent) {
19570
19736
  const inner = `${indent} `;
19571
19737
  const itemIndent = `${inner} `;
19572
19738
  return `${indent}{
19573
19739
  ${inner}type: "category",
19574
19740
  ${inner}label: ${JSON.stringify(DOCUMENTATION_NAV_LABEL2)},
19575
19741
  ${inner}items: [
19576
- ${formatCustomPageNavigationEntry(slug, label, itemIndent)}
19742
+ ${formatCustomPageNavigationEntry(slug, label2, itemIndent)}
19577
19743
  ${inner}],
19578
19744
  ${indent}},`;
19579
19745
  }
19580
- function findMatchingArrayClose(content, open3) {
19746
+ function findMatchingArrayClose(content, open4) {
19581
19747
  let depth = 0;
19582
19748
  let inString = null;
19583
- for (let i = open3; i < content.length; i++) {
19749
+ for (let i = open4; i < content.length; i++) {
19584
19750
  const char = content[i];
19585
19751
  if (inString) {
19586
19752
  if (char === "\\") {
@@ -19619,8 +19785,8 @@ function findDocumentationCategoryItemsRange(content, from, to) {
19619
19785
  if (!itemsMatch) {
19620
19786
  return null;
19621
19787
  }
19622
- const open3 = afterLabel + itemsMatch.index + itemsMatch[0].length - 1;
19623
- const close3 = findMatchingArrayClose(content, open3);
19788
+ const open4 = afterLabel + itemsMatch.index + itemsMatch[0].length - 1;
19789
+ const close3 = findMatchingArrayClose(content, open4);
19624
19790
  if (close3 === null || close3 > to) {
19625
19791
  return null;
19626
19792
  }
@@ -19631,7 +19797,7 @@ function findDocumentationCategoryItemsRange(content, from, to) {
19631
19797
  const indentMatch = /^[ \t]*/.exec(content.slice(lineStart));
19632
19798
  const itemsIndent = indentMatch?.[0] ?? "";
19633
19799
  const entryIndent = `${itemsIndent} `;
19634
- return { open: open3, close: close3, indent: entryIndent };
19800
+ return { open: open4, close: close3, indent: entryIndent };
19635
19801
  }
19636
19802
  function appendEntryToArrayRange(content, range, entry) {
19637
19803
  const before = content.slice(0, range.close);
@@ -19664,7 +19830,7 @@ ${normalizedEntry}
19664
19830
  ${range.indent}${trimmedInner}
19665
19831
  ${closeIndent}${content.slice(range.close)}`;
19666
19832
  }
19667
- function appendCustomPageToDirectBuildExport(content, slug, label) {
19833
+ function appendCustomPageToDirectBuildExport(content, slug, label2) {
19668
19834
  const exportPrefix = "export default ";
19669
19835
  const callPrefix = "buildApitogoConfig(";
19670
19836
  const exportIndex = content.indexOf(`${exportPrefix}${callPrefix}`);
@@ -19672,11 +19838,11 @@ function appendCustomPageToDirectBuildExport(content, slug, label) {
19672
19838
  return null;
19673
19839
  }
19674
19840
  const callStart = exportIndex + exportPrefix.length;
19675
- const open3 = callStart + callPrefix.length - 1;
19841
+ const open4 = callStart + callPrefix.length - 1;
19676
19842
  let depth = 0;
19677
19843
  let inString = null;
19678
19844
  let close3 = -1;
19679
- for (let i = open3; i < content.length; i++) {
19845
+ for (let i = open4; i < content.length; i++) {
19680
19846
  const char = content[i];
19681
19847
  if (inString) {
19682
19848
  if (char === "\\") {
@@ -19711,7 +19877,7 @@ function appendCustomPageToDirectBuildExport(content, slug, label) {
19711
19877
  replaceEnd += 1;
19712
19878
  }
19713
19879
  const buildCall = content.slice(callStart, close3 + 1);
19714
- const category = formatDocumentationCategoryWithPage(slug, label, " ");
19880
+ const category = formatDocumentationCategoryWithPage(slug, label2, " ");
19715
19881
  const replacement = `const config = ${buildCall};
19716
19882
 
19717
19883
  export default {
@@ -19737,10 +19903,10 @@ function findNavigationArrayRange(content) {
19737
19903
  return null;
19738
19904
  }
19739
19905
  const indent = match[1] ?? "";
19740
- const open3 = match.index + match[0].length - 1;
19906
+ const open4 = match.index + match[0].length - 1;
19741
19907
  let depth = 0;
19742
19908
  let inString = null;
19743
- for (let i = open3; i < content.length; i++) {
19909
+ for (let i = open4; i < content.length; i++) {
19744
19910
  const char = content[i];
19745
19911
  if (inString) {
19746
19912
  if (char === "\\") {
@@ -19759,13 +19925,13 @@ function findNavigationArrayRange(content) {
19759
19925
  } else if (char === "]") {
19760
19926
  depth -= 1;
19761
19927
  if (depth === 0) {
19762
- return { open: open3, close: i, indent };
19928
+ return { open: open4, close: i, indent };
19763
19929
  }
19764
19930
  }
19765
19931
  }
19766
19932
  return null;
19767
19933
  }
19768
- function appendCustomPageToNavigation(content, slug, label, _options) {
19934
+ function appendCustomPageToNavigation(content, slug, label2, _options) {
19769
19935
  const pagePath = `"/${normalizePageSlug2(slug)}"`;
19770
19936
  if (content.includes(`path: ${pagePath}`)) {
19771
19937
  return content;
@@ -19773,7 +19939,7 @@ function appendCustomPageToNavigation(content, slug, label, _options) {
19773
19939
  const rewrittenBuildExport = appendCustomPageToDirectBuildExport(
19774
19940
  content,
19775
19941
  slug,
19776
- label
19942
+ label2
19777
19943
  );
19778
19944
  if (rewrittenBuildExport) {
19779
19945
  return rewrittenBuildExport;
@@ -19790,14 +19956,14 @@ function appendCustomPageToNavigation(content, slug, label, _options) {
19790
19956
  if (docsItems) {
19791
19957
  const entry = formatCustomPageNavigationEntry(
19792
19958
  slug,
19793
- label,
19959
+ label2,
19794
19960
  docsItems.indent
19795
19961
  );
19796
19962
  return appendEntryToArrayRange(content, docsItems, entry);
19797
19963
  }
19798
19964
  const category = formatDocumentationCategoryWithPage(
19799
19965
  slug,
19800
- label,
19966
+ label2,
19801
19967
  `${range.indent} `
19802
19968
  );
19803
19969
  return prependEntryToArrayRange(
@@ -19806,11 +19972,11 @@ function appendCustomPageToNavigation(content, slug, label, _options) {
19806
19972
  category
19807
19973
  );
19808
19974
  }
19809
- function patchApitogoConfigForNewPage(content, slug, label, options) {
19975
+ function patchApitogoConfigForNewPage(content, slug, label2, options) {
19810
19976
  const withNavigation = appendCustomPageToNavigation(
19811
19977
  content,
19812
19978
  slug,
19813
- label,
19979
+ label2,
19814
19980
  options
19815
19981
  );
19816
19982
  if (withNavigation === content) {
@@ -19818,13 +19984,13 @@ function patchApitogoConfigForNewPage(content, slug, label, options) {
19818
19984
  }
19819
19985
  return ensurePageImport(withNavigation, pageComponentName2(slug), slug);
19820
19986
  }
19821
- async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label) {
19987
+ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label2) {
19822
19988
  const configPath = await resolveApitogoConfigPath(sourceDirectory);
19823
19989
  if (!configPath) {
19824
19990
  return { updated: false };
19825
19991
  }
19826
19992
  const original = await readFile20(configPath, "utf8");
19827
- const patched = patchApitogoConfigForNewPage(original, slug, label, {
19993
+ const patched = patchApitogoConfigForNewPage(original, slug, label2, {
19828
19994
  typescript: configPath.endsWith(".tsx")
19829
19995
  });
19830
19996
  if (patched === original) {
@@ -19835,7 +20001,7 @@ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label) {
19835
20001
  }
19836
20002
 
19837
20003
  // src/cli/scaffold/wire-openapi.ts
19838
- import { access as access10, readFile as readFile22, writeFile as writeFile19 } from "node:fs/promises";
20004
+ import { access as access10, readFile as readFile22, writeFile as writeFile20 } from "node:fs/promises";
19839
20005
  import path68 from "node:path";
19840
20006
 
19841
20007
  // src/cli/platform/helpers/openapi.ts
@@ -20039,7 +20205,7 @@ async function validateOpenApiFile(filePath) {
20039
20205
  }
20040
20206
 
20041
20207
  // src/cli/scaffold/scaffold-dev-portal-site.ts
20042
- import { mkdir as mkdir15, readFile as readFile21, writeFile as writeFile18 } from "node:fs/promises";
20208
+ import { mkdir as mkdir15, readFile as readFile21, writeFile as writeFile19 } from "node:fs/promises";
20043
20209
  import path67 from "node:path";
20044
20210
 
20045
20211
  // src/cli/scaffold/apitogo-npm-packages.ts
@@ -20451,7 +20617,7 @@ async function ensureAccountPlansPage(outputDirectory) {
20451
20617
  return void 0;
20452
20618
  } catch {
20453
20619
  await mkdir15(path67.dirname(pagePath), { recursive: true });
20454
- await writeFile18(pagePath, ACCOUNT_PLANS_PAGE);
20620
+ await writeFile19(pagePath, ACCOUNT_PLANS_PAGE);
20455
20621
  return ACCOUNT_PLANS_PAGE_PATH;
20456
20622
  }
20457
20623
  }
@@ -20462,7 +20628,7 @@ async function ensureDashboardPage(outputDirectory) {
20462
20628
  return void 0;
20463
20629
  } catch {
20464
20630
  await mkdir15(path67.dirname(pagePath), { recursive: true });
20465
- await writeFile18(pagePath, DASHBOARD_PAGE);
20631
+ await writeFile19(pagePath, DASHBOARD_PAGE);
20466
20632
  return DASHBOARD_PAGE_PATH;
20467
20633
  }
20468
20634
  }
@@ -20521,7 +20687,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
20521
20687
  const packageJsonPath = path67.join(outputDirectory, "package.json");
20522
20688
  if (options.patchPackageJson !== false) {
20523
20689
  const packageJson3 = await readFile21(packageJsonPath, "utf8");
20524
- await writeFile18(
20690
+ await writeFile19(
20525
20691
  packageJsonPath,
20526
20692
  rewritePackageJson(packageJson3, {
20527
20693
  siteName: options.siteName,
@@ -20533,7 +20699,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
20533
20699
  }
20534
20700
  const configPath = path67.join(outputDirectory, "apitogo.config.tsx");
20535
20701
  const configContent = await readFile21(configPath, "utf8");
20536
- await writeFile18(
20702
+ await writeFile19(
20537
20703
  configPath,
20538
20704
  patchDashboardPageConfig(
20539
20705
  patchAccountPlansPageConfig(
@@ -20645,7 +20811,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20645
20811
  input: input5,
20646
20812
  apiPath: "api"
20647
20813
  });
20648
- await writeFile19(
20814
+ await writeFile20(
20649
20815
  jsonConfigPath,
20650
20816
  `${JSON.stringify(updated, null, 2)}
20651
20817
  `,
@@ -20656,7 +20822,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20656
20822
  const configContent = await readFile22(tsxConfigPath, "utf8");
20657
20823
  try {
20658
20824
  const patched = patchApitogoConfigOpenApiToFile(configContent, input5);
20659
- await writeFile19(tsxConfigPath, patched);
20825
+ await writeFile20(tsxConfigPath, patched);
20660
20826
  } catch (error) {
20661
20827
  return {
20662
20828
  status: "failed",
@@ -21009,7 +21175,7 @@ export default function ${componentName}() {
21009
21175
  // src/cli/make-config/scaffold-project.ts
21010
21176
  init_package_json();
21011
21177
  import { constants } from "node:fs";
21012
- import { access as access11, mkdir as mkdir16, readFile as readFile23, writeFile as writeFile20 } from "node:fs/promises";
21178
+ import { access as access11, mkdir as mkdir16, readFile as readFile23, writeFile as writeFile21 } from "node:fs/promises";
21013
21179
  import path70 from "node:path";
21014
21180
  import { glob as glob7 } from "glob";
21015
21181
 
@@ -21087,7 +21253,7 @@ async function ensurePackageJson2(dir) {
21087
21253
  typescript: "^5"
21088
21254
  }
21089
21255
  };
21090
- await writeFile20(pkgPath, `${JSON.stringify(pkg2, null, 2)}
21256
+ await writeFile21(pkgPath, `${JSON.stringify(pkg2, null, 2)}
21091
21257
  `, "utf8");
21092
21258
  return true;
21093
21259
  }
@@ -21114,7 +21280,7 @@ async function ensurePackageJson2(dir) {
21114
21280
  for (const [k, v] of Object.entries(baseDeps)) {
21115
21281
  if (!deps[k]) deps[k] = v;
21116
21282
  }
21117
- await writeFile20(pkgPath, `${JSON.stringify(pkg, null, 2)}
21283
+ await writeFile21(pkgPath, `${JSON.stringify(pkg, null, 2)}
21118
21284
  `, "utf8");
21119
21285
  return false;
21120
21286
  }
@@ -21149,7 +21315,7 @@ async function ensureTsconfigJson(dir) {
21149
21315
  "user-panel"
21150
21316
  ]
21151
21317
  };
21152
- await writeFile20(tsPath, `${JSON.stringify(tsconfig, null, 2)}
21318
+ await writeFile21(tsPath, `${JSON.stringify(tsconfig, null, 2)}
21153
21319
  `, "utf8");
21154
21320
  return true;
21155
21321
  }
@@ -21171,17 +21337,17 @@ async function ensurePortalStubFiles(dir) {
21171
21337
  const landingPath = path70.join(dir, "landing", "LandingPage.tsx");
21172
21338
  if (!await fileExists2(landingPath)) {
21173
21339
  await mkdir16(path70.join(dir, "landing"), { recursive: true });
21174
- await writeFile20(landingPath, LANDING_PAGE_SOURCE, "utf8");
21340
+ await writeFile21(landingPath, LANDING_PAGE_SOURCE, "utf8");
21175
21341
  }
21176
21342
  const plansPath = path70.join(dir, "user-panel", "AccountPlansPage.tsx");
21177
21343
  if (!await fileExists2(plansPath)) {
21178
21344
  await mkdir16(path70.join(dir, "user-panel"), { recursive: true });
21179
- await writeFile20(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
21345
+ await writeFile21(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
21180
21346
  }
21181
21347
  const dashboardPath = path70.join(dir, "user-panel", "DashboardPage.tsx");
21182
21348
  if (!await fileExists2(dashboardPath)) {
21183
21349
  await mkdir16(path70.join(dir, "user-panel"), { recursive: true });
21184
- await writeFile20(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
21350
+ await writeFile21(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
21185
21351
  }
21186
21352
  await ensurePublicBrandingFiles(dir);
21187
21353
  }
@@ -21193,11 +21359,11 @@ async function ensurePublicBrandingFiles(dir) {
21193
21359
  if (await fileExists2(dest)) {
21194
21360
  continue;
21195
21361
  }
21196
- await writeFile20(dest, content, "utf8");
21362
+ await writeFile21(dest, content, "utf8");
21197
21363
  }
21198
21364
  const faviconDest = path70.join(destDir, "Icon.png");
21199
21365
  if (!await fileExists2(faviconDest)) {
21200
- await writeFile20(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
21366
+ await writeFile21(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
21201
21367
  }
21202
21368
  }
21203
21369
  async function ensureThinConfigTsx(dir) {
@@ -21205,7 +21371,7 @@ async function ensureThinConfigTsx(dir) {
21205
21371
  if (await fileExists2(tsxPath)) {
21206
21372
  return false;
21207
21373
  }
21208
- await writeFile20(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
21374
+ await writeFile21(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
21209
21375
  return true;
21210
21376
  }
21211
21377
 
@@ -21266,13 +21432,13 @@ async function persistConfig(config2, ctx) {
21266
21432
  force: ctx.force
21267
21433
  })
21268
21434
  };
21269
- await writeFile21(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
21435
+ await writeFile22(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
21270
21436
  `, "utf8");
21271
21437
  const tsxPath = path71.join(ctx.dir, "apitogo.config.tsx");
21272
21438
  if (await fileExists3(tsxPath) && tsxRoutes.length > 0) {
21273
21439
  const tsxSource = await readFile24(tsxPath, "utf8");
21274
21440
  if (isManagedConfigWrapper(tsxSource)) {
21275
- await writeFile21(
21441
+ await writeFile22(
21276
21442
  tsxPath,
21277
21443
  buildThinWrapperWithCustomPages(tsxRoutes),
21278
21444
  "utf8"
@@ -21376,7 +21542,7 @@ async function makeConfig(argv) {
21376
21542
  if (action2.kind === "page") {
21377
21543
  const pagePath = path71.join(dir, action2.page.relativePath);
21378
21544
  await mkdir17(path71.dirname(pagePath), { recursive: true });
21379
- await writeFile21(
21545
+ await writeFile22(
21380
21546
  pagePath,
21381
21547
  buildStarterDocsPageTsx(action2.page.slug, action2.page.title),
21382
21548
  "utf8"
@@ -21485,7 +21651,7 @@ async function makeConfig(argv) {
21485
21651
  argv.pageContent,
21486
21652
  pageTemplate
21487
21653
  );
21488
- await writeFile21(filePath, tsx, "utf8");
21654
+ await writeFile22(filePath, tsx, "utf8");
21489
21655
  pageResult = {
21490
21656
  action: "updated",
21491
21657
  slug,
@@ -21505,7 +21671,7 @@ async function makeConfig(argv) {
21505
21671
  pageTemplate
21506
21672
  );
21507
21673
  await mkdir17(path71.dirname(filePath), { recursive: true });
21508
- await writeFile21(filePath, tsx, "utf8");
21674
+ await writeFile22(filePath, tsx, "utf8");
21509
21675
  const configResult = await wireNewDevPortalPageInConfig(
21510
21676
  dir,
21511
21677
  slug,
@@ -21805,8 +21971,8 @@ function createScopedConsoleProgressReporter(stream, stepLabel, shared) {
21805
21971
  }
21806
21972
  },
21807
21973
  isCancelled: () => false,
21808
- child(label) {
21809
- return createScopedConsoleProgressReporter(stream, label, shared);
21974
+ child(label2) {
21975
+ return createScopedConsoleProgressReporter(stream, label2, shared);
21810
21976
  }
21811
21977
  };
21812
21978
  const baseOnNewMessages = reporter.onNewMessages.bind(reporter);
@@ -21854,9 +22020,10 @@ var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set([
21854
22020
  "bin",
21855
22021
  "obj"
21856
22022
  ]);
21857
- async function loadGitignore(rootDir) {
22023
+ async function loadGitignore(rootDir, extraExcludes = []) {
21858
22024
  const ig = ignore();
21859
22025
  ig.add(["node_modules/", ".git/", "dist/", "bin/", "obj/"]);
22026
+ if (extraExcludes.length > 0) ig.add([...extraExcludes]);
21860
22027
  const gitignorePath = path72.join(rootDir, ".gitignore");
21861
22028
  try {
21862
22029
  const content = await fs7.readFile(gitignorePath, "utf8");
@@ -21884,13 +22051,13 @@ async function collectFiles(rootDir, relativeDir, ig, files) {
21884
22051
  }
21885
22052
  }
21886
22053
  }
21887
- async function collectPackageableFiles(sourceDirectory) {
22054
+ async function collectPackageableFiles(sourceDirectory, extraExcludes = []) {
21888
22055
  const resolved = path72.resolve(sourceDirectory);
21889
22056
  const stat7 = await fs7.stat(resolved).catch(() => null);
21890
22057
  if (!stat7?.isDirectory()) {
21891
22058
  throw new Error(`sourceDirectory is not a directory: ${resolved}`);
21892
22059
  }
21893
- const ig = await loadGitignore(resolved);
22060
+ const ig = await loadGitignore(resolved, extraExcludes);
21894
22061
  const files = [];
21895
22062
  await collectFiles(resolved, "", ig, files);
21896
22063
  if (files.length === 0) {
@@ -21910,9 +22077,9 @@ async function sha256File(filePath) {
21910
22077
  });
21911
22078
  return hash2.digest("hex");
21912
22079
  }
21913
- async function hashDirectoryContents(rootDirectory) {
22080
+ async function hashDirectoryContents(rootDirectory, extraExcludes = []) {
21914
22081
  const resolved = path73.resolve(rootDirectory);
21915
- const files = (await collectPackageableFiles(resolved)).sort(
22082
+ const files = (await collectPackageableFiles(resolved, extraExcludes)).sort(
21916
22083
  (a, b) => a.localeCompare(b)
21917
22084
  );
21918
22085
  const hash2 = createHash3("sha256");
@@ -22663,7 +22830,7 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
22663
22830
  }
22664
22831
 
22665
22832
  // src/cli/platform/helpers/openapi-gateway-servers.ts
22666
- import { mkdir as mkdir18, readFile as readFile25, writeFile as writeFile22 } from "node:fs/promises";
22833
+ import { mkdir as mkdir18, readFile as readFile25, writeFile as writeFile23 } from "node:fs/promises";
22667
22834
  import path74 from "node:path";
22668
22835
  import YAML3 from "yaml";
22669
22836
  var GATEWAY_URL_ENV_KEY = "APITOGO_GATEWAY_URL";
@@ -22711,7 +22878,7 @@ async function materializePublishOpenApiWithGateway(siteDirectory, sourceOpenApi
22711
22878
  const doc = await readOpenApiDocument(source);
22712
22879
  const rewritten = withGatewayServers(doc, gatewayUrl);
22713
22880
  await mkdir18(path74.dirname(outputPath), { recursive: true });
22714
- await writeFile22(
22881
+ await writeFile23(
22715
22882
  outputPath,
22716
22883
  `${JSON.stringify(rewritten, null, 2)}
22717
22884
  `,
@@ -22737,7 +22904,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22737
22904
  const backend = config2.backend && typeof config2.backend === "object" ? { ...config2.backend } : {};
22738
22905
  backend.openApiPath = relativeInput;
22739
22906
  config2.backend = backend;
22740
- await writeFile22(configPath, `${JSON.stringify(config2, null, 2)}
22907
+ await writeFile23(configPath, `${JSON.stringify(config2, null, 2)}
22741
22908
  `, "utf8");
22742
22909
  return {
22743
22910
  restore: async () => {
@@ -22753,7 +22920,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22753
22920
  } else {
22754
22921
  current.backend = previousBackend;
22755
22922
  }
22756
- await writeFile22(
22923
+ await writeFile23(
22757
22924
  configPath,
22758
22925
  `${JSON.stringify(current, null, 2)}
22759
22926
  `,
@@ -22850,7 +23017,7 @@ function collectManifestPublicActionKeys(gateway) {
22850
23017
  }
22851
23018
 
22852
23019
  // src/cli/platform/helpers/publish-ledger.ts
22853
- import { mkdir as mkdir19, readFile as readFile26, writeFile as writeFile23 } from "node:fs/promises";
23020
+ import { mkdir as mkdir19, readFile as readFile26, writeFile as writeFile24 } from "node:fs/promises";
22854
23021
  import path75 from "node:path";
22855
23022
  var PUBLISH_LEDGER_RELATIVE_PATH = ".apitogo/publish-state.json";
22856
23023
  function publishLedgerPath(siteDirectory) {
@@ -22877,7 +23044,7 @@ async function writePublishLedger(siteDirectory, ledger) {
22877
23044
  version: 1,
22878
23045
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22879
23046
  };
22880
- await writeFile23(filePath, `${JSON.stringify(next, null, 2)}
23047
+ await writeFile24(filePath, `${JSON.stringify(next, null, 2)}
22881
23048
  `, "utf8");
22882
23049
  return filePath;
22883
23050
  }
@@ -23511,9 +23678,9 @@ import { createReadStream as createReadStream2, createWriteStream as createWrite
23511
23678
  import { tmpdir as tmpdir2 } from "node:os";
23512
23679
  import path77 from "node:path";
23513
23680
  import { ZipArchive } from "archiver";
23514
- async function packageBackendDirectory(sourceDirectory) {
23681
+ async function packageBackendDirectory(sourceDirectory, extraExcludes = []) {
23515
23682
  const resolved = path77.resolve(sourceDirectory);
23516
- const files = await collectPackageableFiles(resolved);
23683
+ const files = await collectPackageableFiles(resolved, extraExcludes);
23517
23684
  const zipPath = path77.join(
23518
23685
  tmpdir2(),
23519
23686
  `apitogo-backend-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
@@ -25038,7 +25205,7 @@ async function resolveProjectIdForPublish(client, config2, args, wait, progress,
25038
25205
  import path81 from "node:path";
25039
25206
 
25040
25207
  // src/cli/platform/helpers/dev-portal-site-api-url.ts
25041
- import { readdir as readdir3, readFile as readFile27, writeFile as writeFile24 } from "node:fs/promises";
25208
+ import { readdir as readdir3, readFile as readFile27, writeFile as writeFile25 } from "node:fs/promises";
25042
25209
  import path78 from "node:path";
25043
25210
  var SITE_API_BASE_URL_ENV_KEY = "VITE_APITOGO_AUTHENTICATION_API_BASE_URL";
25044
25211
  var PLACEHOLDER_HOST_MARKERS = [
@@ -25100,13 +25267,13 @@ async function patchSiteApiBaseUrl(sourceDirectory, apiBaseUrl) {
25100
25267
  ${line2}
25101
25268
  ` : `${line2}
25102
25269
  `;
25103
- await writeFile24(envPath, patched);
25270
+ await writeFile25(envPath, patched);
25104
25271
  patchedExisting = true;
25105
25272
  } catch {
25106
25273
  }
25107
25274
  }
25108
25275
  if (!patchedExisting) {
25109
- await writeFile24(path78.join(resolved, ".env"), `${line2}
25276
+ await writeFile25(path78.join(resolved, ".env"), `${line2}
25110
25277
  `);
25111
25278
  }
25112
25279
  }
@@ -27324,19 +27491,11 @@ var GatewayDeployError = class extends ApitogoError {
27324
27491
  };
27325
27492
  var sha256 = (text) => createHash4("sha256").update(text).digest("hex");
27326
27493
  function gatewayNotDeployableReason(gateway) {
27327
- if (gateway.origin.mode === "managed")
27328
- return "a managed origin cannot be deployed from this CLI version yet; use origin.mode proxy";
27329
27494
  if (gateway.specs.length > 1)
27330
27495
  return "the platform takes one OpenAPI file per gateway, but the config lists several";
27331
27496
  return void 0;
27332
27497
  }
27333
27498
  function assertDeployable(gateway) {
27334
- if (gateway.origin.mode === "managed")
27335
- throw new GatewayDeployError(
27336
- "GATEWAY_MANAGED_ORIGIN_UNSUPPORTED",
27337
- "A managed origin cannot be deployed from this CLI version yet.",
27338
- 'Set gateway.origin.mode to "proxy" and point gateway.origin.url at your API.'
27339
- );
27340
27499
  if (gateway.specs.length > 1)
27341
27500
  throw new GatewayDeployError(
27342
27501
  "GATEWAY_MULTIPLE_SPECS",
@@ -27383,6 +27542,37 @@ async function gatewayDeployInputs(resolved, projectId) {
27383
27542
  };
27384
27543
  }
27385
27544
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27545
+ async function backendDeployInputs(gateway, projectId) {
27546
+ const { origin } = gateway;
27547
+ if (origin.mode !== "managed" || !origin.source || !origin.runtime || !origin.runtimeVersion)
27548
+ return void 0;
27549
+ const runtime = normalizeRuntime(origin.runtime);
27550
+ const runtimeVersion = normalizeRuntimeVersionSlug(
27551
+ runtime,
27552
+ origin.runtimeVersion
27553
+ );
27554
+ const exclude = origin.exclude ?? [];
27555
+ let tree;
27556
+ try {
27557
+ tree = await hashDirectoryContents(origin.source, exclude);
27558
+ } catch (error) {
27559
+ throw new GatewayDeployError(
27560
+ "GATEWAY_BACKEND_UNREADABLE",
27561
+ `Cannot read the backend source ${origin.source}: ${error instanceof Error ? error.message : String(error)}`,
27562
+ "Check gateway.origin.source in the config. It must be a folder with your API's code."
27563
+ );
27564
+ }
27565
+ return {
27566
+ source: origin.source,
27567
+ runtime,
27568
+ runtimeVersion,
27569
+ exclude,
27570
+ healthPath: origin.healthPath,
27571
+ hash: sha256(
27572
+ `${projectId}|${runtime}|${runtimeVersion}|${origin.healthPath ?? ""}|${stableJson(exclude)}|${tree}`
27573
+ )
27574
+ };
27575
+ }
27386
27576
  function describePlatformProblem(error) {
27387
27577
  if (!(error instanceof PlatformApiError) || !error.problem) return [];
27388
27578
  const errors = error.problem.errors;
@@ -27418,11 +27608,73 @@ function rejected(what, error) {
27418
27608
  async function deployGateway(resolved, projectId, deps, options = {}) {
27419
27609
  const { gateway, spec, body, specHash, configHash } = await gatewayDeployInputs(resolved, projectId);
27420
27610
  assertDeployable(gateway);
27611
+ const backend = await backendDeployInputs(gateway, projectId);
27421
27612
  const client = deps.client;
27422
27613
  const sleep5 = deps.sleep ?? defaultSleep;
27423
27614
  const now = deps.now ?? Date.now;
27424
27615
  const timeoutMs = deps.reconcileTimeoutMs ?? 18e4;
27616
+ const backendTimeoutMs = deps.backendTimeoutMs ?? 20 * 6e4;
27425
27617
  const intervalMs = deps.pollIntervalMs ?? 2e3;
27618
+ const backendStep = {
27619
+ name: "backend",
27620
+ hash: () => backend?.hash ?? "",
27621
+ run: async () => {
27622
+ if (!backend) throw new Error("gateway.origin is not a managed origin");
27623
+ const prepared = await prepareBackendPackagingDirectory({
27624
+ sourceDirectory: backend.source,
27625
+ runtime: backend.runtime
27626
+ });
27627
+ let zipPath;
27628
+ try {
27629
+ const packaged = await packageBackendDirectory(
27630
+ prepared.directory,
27631
+ backend.exclude
27632
+ );
27633
+ zipPath = packaged.zipPath;
27634
+ let started;
27635
+ try {
27636
+ started = await client.deployBackend(projectId, {
27637
+ archivePath: packaged.zipPath,
27638
+ runtime: backend.runtime,
27639
+ runtimeVersion: backend.runtimeVersion,
27640
+ healthCheckPath: backend.healthPath
27641
+ });
27642
+ } catch (error) {
27643
+ return rejected("backend upload", error);
27644
+ }
27645
+ if (!started.reused) {
27646
+ const deadline = now() + backendTimeoutMs;
27647
+ for (; ; ) {
27648
+ const state = await client.getDeploymentStatus(
27649
+ projectId,
27650
+ started.deploymentId
27651
+ );
27652
+ if (state.status.toLowerCase() === "healthy") break;
27653
+ if (isTerminalDeploymentStatus(state.status))
27654
+ throw new GatewayDeployError(
27655
+ "GATEWAY_BACKEND_FAILED",
27656
+ `The platform could not deploy your backend (${state.status}): ${state.error ?? state.messages.at(-1)?.message ?? "no reason given"}.`,
27657
+ "Fix the cause and run deploy again; the backend is uploaded again."
27658
+ );
27659
+ if (now() >= deadline)
27660
+ throw new GatewayDeployError(
27661
+ "GATEWAY_BACKEND_TIMEOUT",
27662
+ `Your backend was not ready within ${Math.round(backendTimeoutMs / 6e4)} minutes (state: ${state.status}).`,
27663
+ "Run deploy again to keep waiting."
27664
+ );
27665
+ await sleep5(intervalMs);
27666
+ }
27667
+ }
27668
+ return {
27669
+ deploymentId: started.deploymentId,
27670
+ ...started.artifactContentSha256 ? { artifactSha256: started.artifactContentSha256 } : {}
27671
+ };
27672
+ } finally {
27673
+ if (zipPath) await cleanupZip(zipPath);
27674
+ await prepared.cleanup?.();
27675
+ }
27676
+ }
27677
+ };
27426
27678
  const specStep = {
27427
27679
  name: "spec",
27428
27680
  hash: () => specHash,
@@ -27502,12 +27754,30 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
27502
27754
  };
27503
27755
  }
27504
27756
  };
27505
- const outcomes = await runPipeline(
27506
- resolved.state,
27507
- "gateway",
27508
- [specStep, configStep, reconcileStep],
27509
- { force: options.force }
27510
- );
27757
+ let outcomes;
27758
+ if (backend) {
27759
+ const head = await runPipeline(
27760
+ resolved.state,
27761
+ "gateway",
27762
+ [specStep, configStep],
27763
+ { force: options.force }
27764
+ );
27765
+ const middle = await runPipeline(resolved.state, "gateway", [backendStep], {
27766
+ force: options.force
27767
+ });
27768
+ const changed = [...head, ...middle].some((outcome) => !outcome.skipped);
27769
+ const tail = await runPipeline(resolved.state, "gateway", [reconcileStep], {
27770
+ force: options.force || changed
27771
+ });
27772
+ outcomes = [...head, ...middle, ...tail];
27773
+ } else {
27774
+ outcomes = await runPipeline(
27775
+ resolved.state,
27776
+ "gateway",
27777
+ [specStep, configStep, reconcileStep],
27778
+ { force: options.force }
27779
+ );
27780
+ }
27511
27781
  const reconcile = outcomes.find((outcome) => outcome.step === "reconcile");
27512
27782
  const ran = reconcile !== void 0 && !reconcile.skipped;
27513
27783
  const known = (await readManifest(resolved.state))?.services.gateway;
@@ -27904,15 +28174,41 @@ var PlansDeployError = class extends ApitogoError {
27904
28174
  this.name = "PlansDeployError";
27905
28175
  }
27906
28176
  };
27907
- var PLANS_STEPS = ["catalog", "backend", "identity-link"];
28177
+ var PLANS_STEPS = [
28178
+ "catalog",
28179
+ "backend",
28180
+ "billing",
28181
+ "identity-link"
28182
+ ];
27908
28183
  var defaultSleep3 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27909
28184
  var BACKEND_MODULE = "dev-portal";
27910
28185
  var PLANS_CAPABILITIES = { catalog: true, keys: true, usage: true };
28186
+ var STRIPE_WEBHOOK_EVENTS2 = [
28187
+ "checkout.session.completed",
28188
+ "customer.subscription.updated",
28189
+ "customer.subscription.deleted",
28190
+ "invoice.payment_failed"
28191
+ ];
28192
+ function billingNotes(billing) {
28193
+ const notes = [
28194
+ billing.mode === "live" ? "Stripe: LIVE mode. Customers pay with real money." : "Stripe: test mode. Use Stripe's test cards; no real money moves."
28195
+ ];
28196
+ notes.push(
28197
+ billing.hasWebhookSecret ? `Stripe webhook endpoint (add it in your Stripe account if it is not there yet): ${billing.webhookUrl}` : `A payment does not activate a plan until Stripe can confirm it. In your Stripe account add the webhook endpoint ${billing.webhookUrl} (events: ${STRIPE_WEBHOOK_EVENTS2.join(", ")}), store its signing secret with "apitogo secrets set STRIPE_WEBHOOK", set plans.billing.webhookSecret to "secret:STRIPE_WEBHOOK", and deploy again.`
28198
+ );
28199
+ return notes;
28200
+ }
27911
28201
  async function plansInputs(resolved, projectId) {
27912
28202
  const plans = resolved.plans;
27913
28203
  if (!plans) throw new Error("plans is not in the config.");
27914
- const wanted = toPlatformPlans(await readPlansFile(plans.file));
28204
+ const billing = plans.billing;
28205
+ const wanted = toPlatformPlans(await readPlansFile(plans.file), {
28206
+ billing: billing.provider === "stripe"
28207
+ });
27915
28208
  const manifest = await readManifest(resolved.state);
28209
+ const ledger = await readLedger(resolved.state);
28210
+ const billingOn = billing.provider === "stripe";
28211
+ const billingStep = billingOn || stepOf(ledger, "plans", "billing") !== void 0;
27916
28212
  const identity = manifest?.services.identity;
27917
28213
  const ownIdentity = Boolean(
27918
28214
  identity?.provider === "apitogo" && identity.issuer
@@ -27923,6 +28219,16 @@ async function plansInputs(resolved, projectId) {
27923
28219
  ownIdentity,
27924
28220
  catalogHash: stableJson({ projectId, plans: wanted }),
27925
28221
  backendHash: stableJson({ projectId }),
28222
+ billingStep,
28223
+ // Only the references to the project's secrets: a rotated value is applied with --force.
28224
+ billingHash: stableJson({
28225
+ projectId,
28226
+ provider: billing.provider,
28227
+ apiKey: billingOn ? billing.apiKey : void 0,
28228
+ webhookSecret: billingOn ? billing.webhookSecret : void 0,
28229
+ successUrl: billingOn ? billing.successUrl : void 0,
28230
+ cancelUrl: billingOn ? billing.cancelUrl : void 0
28231
+ }),
27926
28232
  linkHash: stableJson({
27927
28233
  projectId,
27928
28234
  issuer: identity?.issuer,
@@ -27959,6 +28265,17 @@ function refused(error) {
27959
28265
  }
27960
28266
  throw error;
27961
28267
  }
28268
+ function billingRefused(error) {
28269
+ if (error instanceof PlatformApiError && error.status === 400) {
28270
+ const lines = describePlatformProblem(error);
28271
+ throw new PlansDeployError(
28272
+ "PLANS_BILLING_REJECTED",
28273
+ `The platform refused the billing settings: ${lines.join(" ") || error.message}`,
28274
+ "Check plans.billing in the config and the project's secrets (apitogo secrets list). The Stripe key starts with sk_ or rk_, the webhook secret with whsec_."
28275
+ );
28276
+ }
28277
+ throw error;
28278
+ }
27962
28279
  async function deployPlans(resolved, projectId, deps, options = {}) {
27963
28280
  const plans = resolved.plans;
27964
28281
  if (!plans) throw new Error("plans is not in the config.");
@@ -27968,7 +28285,16 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
27968
28285
  const intervalMs = deps.pollIntervalMs ?? 2e3;
27969
28286
  const backendTimeoutMs = deps.backendTimeoutMs ?? 9e5;
27970
28287
  const linkTimeoutMs = deps.linkTimeoutMs ?? 6e5;
27971
- const { wanted, manifest, ownIdentity, catalogHash, backendHash, linkHash } = await plansInputs(resolved, projectId);
28288
+ const {
28289
+ wanted,
28290
+ manifest,
28291
+ ownIdentity,
28292
+ billingStep,
28293
+ billingHash,
28294
+ catalogHash,
28295
+ backendHash,
28296
+ linkHash
28297
+ } = await plansInputs(resolved, projectId);
27972
28298
  const catalogStep = {
27973
28299
  name: "catalog",
27974
28300
  hash: () => catalogHash,
@@ -28055,35 +28381,73 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
28055
28381
  },
28056
28382
  stillValid: async () => Boolean(await backendOf())
28057
28383
  };
28384
+ const waitForBackendUpdate = async (jobId, what) => {
28385
+ const deadline = now() + linkTimeoutMs;
28386
+ for (; ; ) {
28387
+ const job = (await client.listProvisioningJobs(projectId)).jobs.find(
28388
+ (item) => item.id === jobId
28389
+ );
28390
+ const status2 = job?.status.toLowerCase();
28391
+ if (status2?.startsWith("completed")) return { jobId };
28392
+ if (status2 === "failed")
28393
+ throw new PlansDeployError(
28394
+ `${what.code}_FAILED`,
28395
+ `Applying ${what.text} to the plans backend failed: ${job?.errorMessage ?? "no reason given"}.`,
28396
+ "Run deploy again; it is applied again."
28397
+ );
28398
+ if (now() >= deadline)
28399
+ throw new PlansDeployError(
28400
+ `${what.code}_TIMEOUT`,
28401
+ `Applying ${what.text} did not finish within ${Math.round(linkTimeoutMs / 1e3)} seconds (state: ${status2 ?? "unknown"}).`,
28402
+ "Run deploy again to keep waiting."
28403
+ );
28404
+ await sleep5(intervalMs);
28405
+ }
28406
+ };
28407
+ let billingNote;
28408
+ const billingStepDef = {
28409
+ name: "billing",
28410
+ hash: () => billingHash,
28411
+ run: async () => {
28412
+ const body = plans.billing.provider === "stripe" ? {
28413
+ provider: "stripe",
28414
+ apiKey: plans.billing.apiKey,
28415
+ webhookSecret: plans.billing.webhookSecret,
28416
+ successUrl: plans.billing.successUrl,
28417
+ cancelUrl: plans.billing.cancelUrl
28418
+ } : { provider: "none" };
28419
+ let queued;
28420
+ try {
28421
+ queued = await client.putDevPortalBilling(projectId, body);
28422
+ } catch (error) {
28423
+ billingRefused(error);
28424
+ }
28425
+ const done = await waitForBackendUpdate(queued.jobId, {
28426
+ code: "PLANS_BILLING",
28427
+ text: "the billing settings"
28428
+ });
28429
+ if (plans.billing.provider === "stripe")
28430
+ billingNote = billingNotes(queued);
28431
+ return { ...done, mode: queued.mode };
28432
+ }
28433
+ };
28058
28434
  const linkStep = {
28059
28435
  name: "identity-link",
28060
28436
  hash: () => linkHash,
28061
28437
  run: async () => {
28062
28438
  const queued = await client.linkDevPortalIdentity(projectId);
28063
- const deadline = now() + linkTimeoutMs;
28064
- for (; ; ) {
28065
- const job = (await client.listProvisioningJobs(projectId)).jobs.find(
28066
- (item) => item.id === queued.jobId
28067
- );
28068
- const status2 = job?.status.toLowerCase();
28069
- if (status2?.startsWith("completed")) return { jobId: queued.jobId };
28070
- if (status2 === "failed")
28071
- throw new PlansDeployError(
28072
- "PLANS_LINK_FAILED",
28073
- `Applying the identity to the plans backend failed: ${job?.errorMessage ?? "no reason given"}.`,
28074
- "Run deploy again; the link is applied again."
28075
- );
28076
- if (now() >= deadline)
28077
- throw new PlansDeployError(
28078
- "PLANS_LINK_TIMEOUT",
28079
- `Applying the identity did not finish within ${Math.round(linkTimeoutMs / 1e3)} seconds (state: ${status2 ?? "unknown"}).`,
28080
- "Run deploy again to keep waiting."
28081
- );
28082
- await sleep5(intervalMs);
28083
- }
28439
+ return waitForBackendUpdate(queued.jobId, {
28440
+ code: "PLANS_LINK",
28441
+ text: "the identity"
28442
+ });
28084
28443
  }
28085
28444
  };
28086
- const steps = [catalogStep, backendStep, ...ownIdentity ? [linkStep] : []];
28445
+ const steps = [
28446
+ catalogStep,
28447
+ backendStep,
28448
+ ...billingStep ? [billingStepDef] : [],
28449
+ ...ownIdentity ? [linkStep] : []
28450
+ ];
28087
28451
  const outcomes = await runPipeline(
28088
28452
  resolved.state,
28089
28453
  "plans",
@@ -28108,7 +28472,7 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
28108
28472
  apiBaseUrl,
28109
28473
  billing: plans.billing.provider,
28110
28474
  catalogRevision: catalog && !catalog.skipped ? previous + 1 : Math.max(previous, 1),
28111
- capabilities: PLANS_CAPABILITIES
28475
+ capabilities: plans.billing.provider === "stripe" ? { ...PLANS_CAPABILITIES, checkout: true } : PLANS_CAPABILITIES
28112
28476
  })
28113
28477
  );
28114
28478
  }
@@ -28119,7 +28483,8 @@ async function deployPlans(resolved, projectId, deps, options = {}) {
28119
28483
  step: outcome.step,
28120
28484
  skipped: outcome.skipped
28121
28485
  })),
28122
- apiBaseUrl
28486
+ apiBaseUrl,
28487
+ ...billingNote ? { notes: billingNote } : {}
28123
28488
  };
28124
28489
  }
28125
28490
 
@@ -28238,14 +28603,18 @@ async function planPlansService(resolved, ledger) {
28238
28603
  const hashes = {
28239
28604
  catalog: projectId ? inputs.catalogHash : void 0,
28240
28605
  backend: projectId ? inputs.backendHash : void 0,
28606
+ billing: projectId ? inputs.billingHash : void 0,
28241
28607
  "identity-link": projectId ? inputs.linkHash : void 0
28242
28608
  };
28243
28609
  const steps = PLANS_STEPS.filter(
28244
- (name) => name !== "identity-link" || inputs.ownIdentity
28610
+ (name) => (name !== "identity-link" || inputs.ownIdentity) && (name !== "billing" || inputs.billingStep)
28245
28611
  );
28246
28612
  const details = [
28247
28613
  ...inputs.wanted.length === 0 ? ["plans: none in the file"] : inputs.wanted.map((plan2) => `plan ${plan2.sku}: ${plan2.displayName}`),
28248
28614
  inputs.ownIdentity ? "sign-in: the project's identity will be applied to the plans backend" : "sign-in: no identity of the platform deployed, so nothing is applied to the plans backend",
28615
+ ...inputs.billingStep ? [
28616
+ resolved.plans?.billing.provider === "stripe" ? "billing: the Stripe key and webhook secret of the project's secrets will be applied to the plans backend" : "billing: switched off, so the stored Stripe settings will be removed from the plans backend"
28617
+ ] : [],
28249
28618
  ...steps.map(
28250
28619
  (name) => `step ${name}: ${stepState(stepOf(ledger, "plans", name), hashes[name])}`
28251
28620
  )
@@ -28486,7 +28855,7 @@ function describePolicies(gateway) {
28486
28855
  function dryRunLines(dryRun) {
28487
28856
  const changes = dryRun.changes;
28488
28857
  if (!changes) return [];
28489
- const list2 = (label, items) => items.length > 0 ? [` ${label}: ${items.join(", ")}`] : [];
28858
+ const list2 = (label2, items) => items.length > 0 ? [` ${label2}: ${items.join(", ")}`] : [];
28490
28859
  if (changes.wouldCreate)
28491
28860
  return [
28492
28861
  "platform dry run: the gateway does not exist yet; everything would be created",
@@ -28523,7 +28892,7 @@ async function planGatewayService(resolved, ledger, dryRun) {
28523
28892
  details: []
28524
28893
  };
28525
28894
  const details = [
28526
- `origin: proxy ${gateway.origin.url ?? "(no url set)"}`,
28895
+ gateway.origin.mode === "managed" ? `origin: managed (${gateway.origin.runtime ?? "no runtime"} from ${gateway.origin.source ?? "no source folder"})` : `origin: proxy ${gateway.origin.url ?? "(no url set)"}`,
28527
28896
  ...gateway.auth ? [`auth: ${gateway.auth.mode}`] : ["auth: none"],
28528
28897
  ...describePolicies(gateway)
28529
28898
  ];
@@ -28576,8 +28945,27 @@ async function planGatewayService(resolved, ledger, dryRun) {
28576
28945
  );
28577
28946
  }
28578
28947
  }
28948
+ let backendHash;
28949
+ try {
28950
+ backendHash = (await backendDeployInputs(gateway, resolved.project.id ?? ""))?.hash;
28951
+ } catch (error) {
28952
+ return {
28953
+ ...base,
28954
+ action: "blocked",
28955
+ reason: "cannot deploy until this is fixed",
28956
+ details,
28957
+ issues: [
28958
+ {
28959
+ code: "GATEWAY_BACKEND_UNREADABLE",
28960
+ path: "gateway.origin.source",
28961
+ message: error instanceof Error ? error.message : String(error)
28962
+ }
28963
+ ]
28964
+ };
28965
+ }
28579
28966
  const step2 = (name) => stepOf(ledger, "gateway", name);
28580
28967
  const hashes = {
28968
+ backend: backendHash ?? "",
28581
28969
  spec: inputs.specHash,
28582
28970
  config: inputs.configHash,
28583
28971
  reconcile: inputs.configHash
@@ -28593,9 +28981,10 @@ async function planGatewayService(resolved, ledger, dryRun) {
28593
28981
  const lines = [
28594
28982
  `step spec (upload the OpenAPI file): ${state("spec")}`,
28595
28983
  `step config (send the wanted state): ${state("config")}`,
28984
+ ...backendHash ? [`step backend (upload and start your code): ${state("backend")}`] : [],
28596
28985
  `step reconcile (wait until the platform applied it): ${state("reconcile")}`
28597
28986
  ];
28598
- const allDone = step2("spec")?.status === "succeeded" && step2("spec")?.hash === hashes.spec && step2("config")?.status === "succeeded" && step2("config")?.hash === hashes.config && step2("reconcile")?.status === "succeeded" && step2("reconcile")?.hash === hashes.reconcile;
28987
+ const allDone = (!backendHash || step2("backend")?.status === "succeeded" && step2("backend")?.hash === hashes.backend) && step2("spec")?.status === "succeeded" && step2("spec")?.hash === hashes.spec && step2("config")?.status === "succeeded" && step2("config")?.hash === hashes.config && step2("reconcile")?.status === "succeeded" && step2("reconcile")?.hash === hashes.reconcile;
28599
28988
  if (issues.length > 0)
28600
28989
  return {
28601
28990
  ...base,
@@ -28676,7 +29065,7 @@ function corsLines(platform) {
28676
29065
  ];
28677
29066
  }
28678
29067
  async function gatewayStatus(resolved, ledger, manifest, remote) {
28679
- const steps = ["spec", "config", "reconcile"].map((name) => ({
29068
+ const steps = ["spec", "config", "backend", "reconcile"].map((name) => ({
28680
29069
  name,
28681
29070
  record: stepOf(ledger, "gateway", name)
28682
29071
  }));
@@ -28694,6 +29083,14 @@ async function gatewayStatus(resolved, ledger, manifest, remote) {
28694
29083
  if (remote && projectId) {
28695
29084
  try {
28696
29085
  const platform = await remote(projectId);
29086
+ if (platform.status === "Released") {
29087
+ details.length = 0;
29088
+ details.push(
29089
+ "platform state: Released (the gateway was removed; deploy makes a new one)"
29090
+ );
29091
+ state = "not-deployed";
29092
+ return { service: "gateway", state, details: [...details, ...local] };
29093
+ }
28697
29094
  details.push(
28698
29095
  `platform state: ${platform.status}`,
28699
29096
  platform.inSync ? "drift: none (the real gateway matches the config)" : `drift: the real gateway does not match the config yet (wanted ${platform.desiredHash?.slice(0, 12) ?? "none"}, real ${platform.observedHash?.slice(0, 12) ?? "none"})`,
@@ -28831,7 +29228,7 @@ function formatStatus(output4) {
28831
29228
  }
28832
29229
 
28833
29230
  // src/cli/deploy/static-web-config.ts
28834
- import { access as access14, writeFile as writeFile25 } from "node:fs/promises";
29231
+ import { access as access14, writeFile as writeFile26 } from "node:fs/promises";
28835
29232
  import path84 from "node:path";
28836
29233
  var STATIC_WEB_CONFIG_FILE = "staticwebapp.config.json";
28837
29234
  var STATIC_WEB_CONFIG_VERSION = 1;
@@ -28850,7 +29247,7 @@ async function ensureStaticWebConfig(buildDir) {
28850
29247
  await access14(file);
28851
29248
  return false;
28852
29249
  } catch {
28853
- await writeFile25(
29250
+ await writeFile26(
28854
29251
  file,
28855
29252
  `${JSON.stringify(DEFAULT_STATIC_WEB_CONFIG, null, 2)}
28856
29253
  `,
@@ -28999,7 +29396,7 @@ async function deployGatewayService(options, deps, projectId, named) {
28999
29396
  return { service: "gateway", status: "skipped", reason: why, steps: [] };
29000
29397
  }
29001
29398
  const client = deps.client;
29002
- if (!client.putGatewaySpec || !client.putGatewayConfig || !client.getGatewayReconciliation || !client.getGatewayStatus || !client.getGatewayCapabilities)
29399
+ if (!client.putGatewaySpec || !client.putGatewayConfig || !client.getGatewayReconciliation || !client.getGatewayStatus || !client.getGatewayCapabilities || gateway?.origin.mode === "managed" && (!client.deployBackend || !client.getDeploymentStatus))
29003
29400
  throw new Error("The platform client has no gateway calls.");
29004
29401
  const result = await deployGateway(
29005
29402
  resolved,
@@ -29074,7 +29471,8 @@ async function deployPlansService(options, deps, projectId, named) {
29074
29471
  service: "plans",
29075
29472
  status: result.status,
29076
29473
  steps: result.steps,
29077
- url: result.apiBaseUrl
29474
+ url: result.apiBaseUrl,
29475
+ ...result.notes ? { notes: result.notes } : {}
29078
29476
  };
29079
29477
  }
29080
29478
  async function deployPortal(options, deps, projectId) {
@@ -29208,10 +29606,10 @@ var deploy_default = {
29208
29606
  describe: "Override project GUID"
29209
29607
  }).option("project-name", {
29210
29608
  type: "string",
29211
- describe: "Display name when creating a project"
29609
+ describe: "Version 1: display name when creating a project. Version 2 ignores it: the project is made by `apitogo project create` and named by project.name"
29212
29610
  }).option("platform-plan", {
29213
29611
  type: "string",
29214
- describe: "Platform hosting tier",
29612
+ describe: "Version 1: platform hosting tier. Version 2 ignores it: a project made by `apitogo project create` is on the Free plan",
29215
29613
  choices: ["free", "starter", "professional"]
29216
29614
  }).option("only", {
29217
29615
  type: "string",
@@ -29246,7 +29644,7 @@ var deploy_default = {
29246
29644
  };
29247
29645
 
29248
29646
  // src/cli/export/handler.ts
29249
- import { mkdir as mkdir20, writeFile as writeFile26 } from "node:fs/promises";
29647
+ import { mkdir as mkdir20, writeFile as writeFile27 } from "node:fs/promises";
29250
29648
  import path87 from "node:path";
29251
29649
  init_invariant();
29252
29650
  var ExportError = class extends ApitogoError {
@@ -29288,7 +29686,7 @@ async function exportOpenApi(args, deps) {
29288
29686
  if (!args.out) return { text };
29289
29687
  const file = path87.resolve(process.cwd(), args.out);
29290
29688
  await mkdir20(path87.dirname(file), { recursive: true });
29291
- await writeFile26(file, text);
29689
+ await writeFile27(file, text);
29292
29690
  return { text, file };
29293
29691
  }
29294
29692
  async function exportCommand(args) {
@@ -29341,13 +29739,13 @@ var export_default = {
29341
29739
  import path91 from "node:path";
29342
29740
 
29343
29741
  // src/cli/scaffold/run-scaffold-discovery.ts
29344
- import { access as access16, readFile as readFile31, writeFile as writeFile28 } from "node:fs/promises";
29742
+ import { access as access16, readFile as readFile31, writeFile as writeFile29 } from "node:fs/promises";
29345
29743
  import path90 from "node:path";
29346
29744
 
29347
29745
  // src/cli/scaffold/detect-backend.ts
29348
29746
  import { readdir as readdir4, readFile as readFile29 } from "node:fs/promises";
29349
29747
  import path88 from "node:path";
29350
- var DEFAULT_RUNTIME_VERSION = {
29748
+ var DEFAULT_RUNTIME_VERSION2 = {
29351
29749
  dotnet: "DOTNET_10",
29352
29750
  nodejs: "NODE_22_LTS",
29353
29751
  python: "PYTHON_312"
@@ -29374,18 +29772,18 @@ function dotnetVersionFromCsproj(content) {
29374
29772
  if (major === "8" || major === "9" || major === "10") {
29375
29773
  return `DOTNET_${major}`;
29376
29774
  }
29377
- return DEFAULT_RUNTIME_VERSION.dotnet;
29775
+ return DEFAULT_RUNTIME_VERSION2.dotnet;
29378
29776
  }
29379
29777
  function nodeVersionFromEngines(engines) {
29380
29778
  const range = engines && typeof engines === "object" && !Array.isArray(engines) ? engines.node : void 0;
29381
29779
  if (typeof range === "string" && /(^|[^\d])24/.test(range)) {
29382
29780
  return "NODE_24_LTS";
29383
29781
  }
29384
- return DEFAULT_RUNTIME_VERSION.nodejs;
29782
+ return DEFAULT_RUNTIME_VERSION2.nodejs;
29385
29783
  }
29386
29784
  function pythonVersionFromText(content) {
29387
29785
  const match = content?.match(/3\.(1[0-4])/);
29388
- return match ? `PYTHON_3${match[1]}` : DEFAULT_RUNTIME_VERSION.python;
29786
+ return match ? `PYTHON_3${match[1]}` : DEFAULT_RUNTIME_VERSION2.python;
29389
29787
  }
29390
29788
  function isRunnableNodePackage(pkg) {
29391
29789
  const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
@@ -29452,7 +29850,7 @@ import {
29452
29850
  readFile as readFile30,
29453
29851
  readlink,
29454
29852
  symlink as symlink2,
29455
- writeFile as writeFile27
29853
+ writeFile as writeFile28
29456
29854
  } from "node:fs/promises";
29457
29855
  import path89 from "node:path";
29458
29856
  var DOCS_DIR_WHITELIST = [
@@ -29689,7 +30087,7 @@ async function applyDocsNavigation(siteDirectory, routes) {
29689
30087
  force: false
29690
30088
  });
29691
30089
  const updated = ensureHeaderDocsNavigation(parsed, routes);
29692
- await writeFile27(configPath, `${JSON.stringify(updated, null, 2)}
30090
+ await writeFile28(configPath, `${JSON.stringify(updated, null, 2)}
29693
30091
  `, "utf8");
29694
30092
  }
29695
30093
  async function discoverAndWireDocs(siteDirectory) {
@@ -29873,7 +30271,7 @@ async function wireOpenApiIntoJsonConfig(siteDirectory, openApiPath) {
29873
30271
  });
29874
30272
  const backend = updated.backend && typeof updated.backend === "object" && !Array.isArray(updated.backend) ? { ...updated.backend } : {};
29875
30273
  updated.backend = { ...backend, openApiPath: input5 };
29876
- await writeFile28(
30274
+ await writeFile29(
29877
30275
  jsonConfigPath,
29878
30276
  `${JSON.stringify(updated, null, 2)}
29879
30277
  `,
@@ -29993,7 +30391,7 @@ async function discoverAndWireBackend(siteDirectory, mode) {
29993
30391
  runtime: detected.runtime,
29994
30392
  runtimeVersion: detected.runtimeVersion
29995
30393
  };
29996
- await writeFile28(
30394
+ await writeFile29(
29997
30395
  jsonConfigPath,
29998
30396
  `${JSON.stringify(siteConfig, null, 2)}
29999
30397
  `,
@@ -30546,6 +30944,7 @@ var plan_default = {
30546
30944
  // src/cli/project/create-handler.ts
30547
30945
  init_invariant();
30548
30946
  import path93 from "node:path";
30947
+ import open3 from "open";
30549
30948
 
30550
30949
  // src/cli/project/v2.ts
30551
30950
  init_invariant();
@@ -30557,7 +30956,12 @@ var ProjectCreateError = class extends ApitogoError {
30557
30956
  }
30558
30957
  };
30559
30958
  function modulesForConfig(config2) {
30560
- return config2.portal && config2.portal.enabled !== false ? ["dev-portal"] : [];
30959
+ const modules = [];
30960
+ if (config2.portal && config2.portal.enabled !== false)
30961
+ modules.push("dev-portal");
30962
+ if (config2.gateway && config2.gateway.enabled !== false && config2.gateway.origin?.mode === "managed")
30963
+ modules.push("apihub");
30964
+ return modules;
30561
30965
  }
30562
30966
  var hostsAnything = (config2) => config2.portal && config2.portal.enabled !== false || config2.gateway && config2.gateway.enabled !== false;
30563
30967
  async function createProjectV2(options, deps) {
@@ -30572,15 +30976,52 @@ async function createProjectV2(options, deps) {
30572
30976
  );
30573
30977
  }
30574
30978
  const name = options.name?.trim() || resolved.project.name;
30979
+ const plan2 = options.plan ?? "free";
30980
+ const onProjectResolved = async (projectId) => {
30981
+ await setProjectId(options.dir, projectId);
30982
+ };
30983
+ let continueWith = linkedProjectId;
30984
+ if (plan2 !== "free") {
30985
+ const first = parse4(
30986
+ await deps.bootstrap({
30987
+ name,
30988
+ description: options.description,
30989
+ modules,
30990
+ wait: false,
30991
+ projectId: linkedProjectId,
30992
+ onProjectResolved,
30993
+ paidPlan: true
30994
+ })
30995
+ );
30996
+ assertCreated(first);
30997
+ const paid = await settlePaidPlan(
30998
+ first.projectId,
30999
+ plan2,
31000
+ options,
31001
+ deps
31002
+ );
31003
+ if (paid.status === "awaiting_checkout") {
31004
+ await setProjectId(options.dir, first.projectId);
31005
+ return {
31006
+ projectId: first.projectId,
31007
+ projectName: name,
31008
+ modules,
31009
+ resumed: first.resumed === true,
31010
+ plan: plan2,
31011
+ status: "awaiting_checkout",
31012
+ checkoutUrl: paid.url
31013
+ };
31014
+ }
31015
+ continueWith = first.projectId;
31016
+ }
30575
31017
  const raw = await deps.bootstrap({
30576
31018
  name,
30577
31019
  description: options.description,
30578
31020
  modules,
30579
31021
  wait: options.wait !== false,
30580
- projectId: linkedProjectId,
30581
- onProjectResolved: async (projectId) => {
30582
- await setProjectId(options.dir, projectId);
30583
- }
31022
+ projectId: continueWith,
31023
+ onProjectResolved,
31024
+ ...plan2 !== "free" ? { paidPlan: true } : {}
30584
31025
  });
30585
31026
  const outcome = parse4(raw);
30586
31027
  if (outcome.requiresPlatformPlanSelection) {
@@ -30590,13 +31031,7 @@ async function createProjectV2(options, deps) {
30590
31031
  "Pick another name with --name, or choose a plan for that project in the web app."
30591
31032
  );
30592
31033
  }
30593
- if (outcome.status === "failed" || outcome.status === "blocked" || !outcome.projectId) {
30594
- throw new ProjectCreateError(
30595
- "CREATE_FAILED",
30596
- `The platform could not create the project: ${outcome.message ?? outcome.status ?? "no answer"}.`,
30597
- "Nothing was written to this folder. Try again, or check `apitogo status`."
30598
- );
30599
- }
31034
+ assertCreated(outcome);
30600
31035
  if (outcome.status === "running" || outcome.status === "pending") {
30601
31036
  await setProjectId(options.dir, outcome.projectId);
30602
31037
  throw new ProjectCreateError(
@@ -30610,9 +31045,82 @@ async function createProjectV2(options, deps) {
30610
31045
  projectId: outcome.projectId,
30611
31046
  projectName: name,
30612
31047
  modules,
30613
- resumed: outcome.resumed === true
31048
+ resumed: outcome.resumed === true,
31049
+ ...plan2 !== "free" ? { plan: plan2 } : {}
30614
31050
  };
30615
31051
  }
31052
+ function assertCreated(outcome) {
31053
+ if (outcome.status === "failed" || outcome.status === "blocked" || !outcome.projectId) {
31054
+ throw new ProjectCreateError(
31055
+ "CREATE_FAILED",
31056
+ `The platform could not create the project: ${outcome.message ?? outcome.status ?? "no answer"}.`,
31057
+ "Nothing was written to this folder. Try again, or check `apitogo status`."
31058
+ );
31059
+ }
31060
+ }
31061
+ var defaultSleep4 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
31062
+ var label = (plan2) => plan2.charAt(0).toUpperCase() + plan2.slice(1);
31063
+ async function settlePaidPlan(projectId, plan2, options, deps) {
31064
+ const api = deps.platform;
31065
+ if (!api) throw new Error("The platform client has no plan calls.");
31066
+ const sleep5 = api.sleep ?? defaultSleep4;
31067
+ const now = api.now ?? Date.now;
31068
+ const subscription = await api.getSubscription(projectId);
31069
+ const tier = subscription.planTier.toLowerCase();
31070
+ const onThisPlan = tier === plan2;
31071
+ if (onThisPlan && isPaidPlanActive(subscription)) return { status: "paid" };
31072
+ if (!isPlatformPlanUnselected(subscription.planTier) && !onThisPlan) {
31073
+ throw new ProjectCreateError(
31074
+ "PLAN_CHANGE_UNSUPPORTED",
31075
+ `This project is already on the ${label(tier)} plan, so it cannot be created on ${label(plan2)}.`,
31076
+ "Moving a project to another plan is not supported yet. Create a new project with --plan in a new folder, or change the plan in the web app."
31077
+ );
31078
+ }
31079
+ if (!onThisPlan) {
31080
+ let session;
31081
+ try {
31082
+ session = await api.createCheckoutSession(projectId, {
31083
+ planSlug: plan2,
31084
+ ...options.skipPayment ? { skipPayment: true } : {}
31085
+ });
31086
+ } catch (error) {
31087
+ if (options.skipPayment && error instanceof PlatformApiError && error.status === 403)
31088
+ throw new ProjectCreateError(
31089
+ "SKIP_PAYMENT_REFUSED",
31090
+ "Only a platform admin may skip the payment.",
31091
+ "Run it without --skip-payment to pay through the platform's checkout."
31092
+ );
31093
+ throw error;
31094
+ }
31095
+ if (session.granted) return { status: "paid" };
31096
+ if (!session.url)
31097
+ throw new ProjectCreateError(
31098
+ "CREATE_FAILED",
31099
+ "The platform gave no checkout link.",
31100
+ "Run the command again, or choose the plan in the web app."
31101
+ );
31102
+ api.say?.(`Pay for the ${label(plan2)} plan here: ${session.url}`);
31103
+ if (options.waitForPayment === false)
31104
+ return { status: "awaiting_checkout", url: session.url };
31105
+ if (options.open !== false)
31106
+ await api.openUrl?.(session.url).catch(() => {
31107
+ });
31108
+ }
31109
+ const timeoutMs = options.paymentTimeoutMs ?? 9e5;
31110
+ const deadline = now() + timeoutMs;
31111
+ for (; ; ) {
31112
+ const current = await api.getSubscription(projectId);
31113
+ if (current.planTier.toLowerCase() === plan2 && isPaidPlanActive(current))
31114
+ return { status: "paid" };
31115
+ if (now() >= deadline)
31116
+ throw new ProjectCreateError(
31117
+ "CHECKOUT_PENDING",
31118
+ `The payment for the ${label(plan2)} plan is not confirmed yet.`,
31119
+ `Finish paying with the link above, then run \`apitogo project create --plan ${plan2}\` again; it continues the same project (project.id ${projectId}).`
31120
+ );
31121
+ await sleep5(api.pollIntervalMs ?? 3e3);
31122
+ }
31123
+ }
30616
31124
  function parse4(raw) {
30617
31125
  try {
30618
31126
  return JSON.parse(raw);
@@ -30641,9 +31149,22 @@ async function createLinkedProject(argv, dir) {
30641
31149
  dir,
30642
31150
  name: argv.name,
30643
31151
  description: argv.description,
30644
- wait: argv.wait !== false
31152
+ wait: argv.wait !== false,
31153
+ plan: argv.plan ?? "free",
31154
+ skipPayment: argv.skipPayment === true,
31155
+ open: argv.open !== false,
31156
+ // In JSON mode the link is returned and a later run continues; otherwise the command waits for the payment.
31157
+ waitForPayment: !json
30645
31158
  },
30646
31159
  {
31160
+ platform: {
31161
+ getSubscription: (projectId) => client.getSubscription(projectId),
31162
+ createCheckoutSession: (projectId, body) => client.createCheckoutSession(projectId, body),
31163
+ openUrl: async (url2) => {
31164
+ await open3(url2);
31165
+ },
31166
+ ...json ? {} : { say: (message) => printDiagnosticsToConsole(message) }
31167
+ },
30647
31168
  bootstrap: (args) => handleBootstrapProject(
30648
31169
  client,
30649
31170
  config2,
@@ -30651,7 +31172,7 @@ async function createLinkedProject(argv, dir) {
30651
31172
  name: args.name,
30652
31173
  description: args.description,
30653
31174
  modules: args.modules,
30654
- selectFreePlan: true,
31175
+ selectFreePlan: args.paidPlan !== true,
30655
31176
  wait: args.wait,
30656
31177
  configureGatewayBaseline: false,
30657
31178
  projectId: args.projectId,
@@ -30666,8 +31187,14 @@ async function createLinkedProject(argv, dir) {
30666
31187
  console.log(JSON.stringify({ status: "succeeded", ...result }, null, 2));
30667
31188
  return;
30668
31189
  }
31190
+ if (result.status === "awaiting_checkout") {
31191
+ printResultToConsole(
31192
+ `Project ${result.projectName} (${result.projectId}) is created and linked. Pay for the ${result.plan} plan at ${result.checkoutUrl}, then run \`apitogo project create --plan ${result.plan}\` again to continue.`
31193
+ );
31194
+ return;
31195
+ }
30669
31196
  printDiagnosticsToConsole(
30670
- `Project ${result.projectName} (${result.projectId}) is ready on the free plan${result.modules.length > 0 ? ` with: ${result.modules.join(", ")}` : " (the gateway is created by `apitogo deploy`)"}.`
31197
+ `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`)"}.`
30671
31198
  );
30672
31199
  printResultToConsole(
30673
31200
  "The folder is linked. Run `apitogo deploy` to publish."
@@ -30697,6 +31224,12 @@ async function createProject(argv) {
30697
31224
  await createLinkedProject(argv, dir);
30698
31225
  return;
30699
31226
  }
31227
+ if (argv.plan && argv.plan !== "free") {
31228
+ await printCriticalFailureToConsoleAndExit(
31229
+ "--plan is for config version 2 folders. For a version 1 project use `apitogo deploy --platform-plan`."
31230
+ );
31231
+ return;
31232
+ }
30700
31233
  const name = argv.name?.trim();
30701
31234
  if (!name) {
30702
31235
  await printCriticalFailureToConsoleAndExit(
@@ -30763,11 +31296,24 @@ var project_create_default = {
30763
31296
  describe: "Optional project description"
30764
31297
  }).option("modules", {
30765
31298
  type: "string",
30766
- describe: "Comma-separated modules to activate (default: apihub)",
31299
+ describe: "Version 1 only: comma-separated modules to activate (default: apihub). Version 2 takes them from the config (the portal needs dev-portal) and ignores this option",
30767
31300
  default: "apihub"
31301
+ }).option("plan", {
31302
+ type: "string",
31303
+ choices: ["free", "starter", "professional"],
31304
+ describe: "Version 2: the platform plan of the new project (default: free). Starter and Professional are paid through the platform's own checkout, and a project needs one of them to sell paid plans"
31305
+ }).option("open", {
31306
+ type: "boolean",
31307
+ describe: "Open the checkout page in the browser for a paid plan (use --no-open to only print the link)",
31308
+ default: true
31309
+ }).option("skip-payment", {
31310
+ type: "boolean",
31311
+ describe: "Platform admins only: give the paid plan without payment. The platform refuses it for anyone else",
31312
+ default: false,
31313
+ hidden: true
30768
31314
  }).option("wait", {
30769
31315
  type: "boolean",
30770
- describe: "Wait for warm pool when a platform plan is already selected",
31316
+ describe: "Wait until the hosting from the warm pool is ready before returning, once a platform plan is selected (always so in version 2). Use --no-wait to return sooner",
30771
31317
  default: true
30772
31318
  }).option("json", {
30773
31319
  type: "boolean",
@@ -31272,7 +31818,7 @@ async function readStdin(input5 = process.stdin) {
31272
31818
  }
31273
31819
  return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
31274
31820
  }
31275
- async function confirm2(question) {
31821
+ async function confirm(question) {
31276
31822
  const rl = createInterface6({ input: process.stdin, output: process.stdout });
31277
31823
  try {
31278
31824
  const answer = (await rl.question(question)).trim().toLowerCase();
@@ -31388,7 +31934,7 @@ async function secrets(argv) {
31388
31934
  isTerminal: process.stdin.isTTY === true && !json,
31389
31935
  askValue: (prompt) => readHidden(prompt),
31390
31936
  readPiped: () => readStdin(),
31391
- confirm: confirm2
31937
+ confirm
31392
31938
  });
31393
31939
  if (json) {
31394
31940
  console.log(JSON.stringify({ status: "succeeded", ...result }, null, 2));