@lukoweb/apitogo 0.2.31 → 0.2.35

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 (41) hide show
  1. package/dist/cli/cli.js +440 -153
  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 +0 -4
  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/zip.d.ts +1 -1
  8. package/dist/declarations/cli/platform/tools/infrastructure.d.ts +0 -4
  9. package/dist/declarations/config/effective-redirects.d.ts +6 -0
  10. package/dist/declarations/config/header-navigation-guard.d.ts +2 -0
  11. package/dist/declarations/config/resolve-modules.d.ts +1 -4
  12. package/dist/declarations/config/validators/HeaderNavigationSchema.d.ts +1 -2
  13. package/docs/cli/commands.md +19 -0
  14. package/docs/configuration/gateway.md +52 -0
  15. package/docs/guides/customer-account.md +11 -1
  16. package/docs/mcp/tools.md +101 -102
  17. package/package.json +2 -1
  18. package/src/app/demo.tsx +78 -77
  19. package/src/app/entry.client.tsx +115 -107
  20. package/src/app/entry.server.tsx +164 -160
  21. package/src/app/main.tsx +175 -167
  22. package/src/app/standalone.tsx +61 -60
  23. package/src/config/effective-redirects.ts +31 -0
  24. package/src/config/header-navigation-guard.ts +13 -0
  25. package/src/config/resolve-modules.ts +331 -354
  26. package/src/config/v2/home-redirect.ts +104 -98
  27. package/src/config/v2/index.ts +127 -126
  28. package/src/config/v2/ledger.ts +242 -235
  29. package/src/config/v2/resolve.ts +12 -4
  30. package/src/config/v2/rules.ts +716 -714
  31. package/src/config/v2/services.ts +799 -663
  32. package/src/config/v2/workspace.ts +17 -1
  33. package/src/config/validators/HeaderNavigationSchema.ts +42 -44
  34. package/src/lib/assets/powered-by-icon.png +0 -0
  35. package/src/lib/authentication/providers/dev-portal-auth-pages.tsx +667 -602
  36. package/src/lib/authentication/providers/dev-portal.tsx +476 -448
  37. package/src/lib/components/HeaderNavigation.tsx +176 -176
  38. package/src/lib/components/MobileTopNavigation.tsx +283 -283
  39. package/src/lib/components/navigation/PoweredByZudoku.tsx +42 -39
  40. package/src/vite/plugin-offroot-resolve.ts +68 -67
  41. 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 ?? [
@@ -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
 
@@ -9164,13 +9250,6 @@ var PlatformApiClient = class {
9164
9250
  `/api/v1/projects/${projectId}/infrastructure/provisioning-jobs`
9165
9251
  );
9166
9252
  }
9167
- getDeploymentToken(projectId, regenerate = false) {
9168
- const query = regenerate ? "?regenerate=true" : "";
9169
- return this.request(
9170
- "POST",
9171
- `/api/v1/projects/${projectId}/infrastructure/deployment-token${query}`
9172
- );
9173
- }
9174
9253
  /**
9175
9254
  * Publishes a built portal (.tgz, root folder `dist`). The platform holds the deploy server key and the project's
9176
9255
  * deployment token, so the CLI only needs its own login. `env` falls back to the platform default.
@@ -9843,12 +9922,12 @@ import {
9843
9922
  readFile as readFile12,
9844
9923
  rename as rename6,
9845
9924
  rm as rm8,
9846
- writeFile as writeFile14
9925
+ writeFile as writeFile15
9847
9926
  } from "node:fs/promises";
9848
9927
  import path48 from "node:path";
9849
9928
 
9850
9929
  // src/vite/build.ts
9851
- 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";
9852
9931
  import path46 from "node:path";
9853
9932
  import { build as esbuild } from "esbuild";
9854
9933
  import { build as viteBuild, mergeConfig as mergeConfig3 } from "vite";
@@ -9996,7 +10075,7 @@ import {
9996
10075
  // package.json
9997
10076
  var package_default = {
9998
10077
  name: "@lukoweb/apitogo",
9999
- version: "0.2.31",
10078
+ version: "0.2.35",
10000
10079
  type: "module",
10001
10080
  sideEffects: [
10002
10081
  "**/*.css",
@@ -10074,6 +10153,7 @@ var package_default = {
10074
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",
10075
10154
  postbuild: "tsx scripts/check-external-deps.ts",
10076
10155
  typecheck: "tsc --project tsconfig.app.json --noEmit",
10156
+ "test:e2e": "vitest run --config vitest.e2e.config.ts",
10077
10157
  "generate:types": "tsx scripts/generate-types.js && tsx scripts/generate-flat-config.js",
10078
10158
  "build:standalone": "vite build --mode standalone --config vite.standalone.config.ts --log-level=error",
10079
10159
  prepack: "npm run build && tsx scripts/generate-publish-exports.ts",
@@ -12245,13 +12325,13 @@ var NavigationResolver = class {
12245
12325
  init_invariant();
12246
12326
 
12247
12327
  // src/vite/debug.ts
12248
- import { mkdir as mkdir7, writeFile as writeFile8 } from "node:fs/promises";
12328
+ import { mkdir as mkdir7, writeFile as writeFile9 } from "node:fs/promises";
12249
12329
  import path27 from "node:path";
12250
12330
  async function writePluginDebugCode(rootDir, pluginName, code, extension = "js") {
12251
12331
  if (process.env.ZUDOKU_BUILD_DEBUG) {
12252
12332
  const debugDir = path27.join(rootDir, "dist", "debug");
12253
12333
  await mkdir7(debugDir, { recursive: true });
12254
- await writeFile8(
12334
+ await writeFile9(
12255
12335
  path27.join(debugDir, `${pluginName}.${extension}`),
12256
12336
  typeof code === "string" ? code : code.join("\n")
12257
12337
  );
@@ -13108,7 +13188,7 @@ var plugin_local_manifest_default = viteLocalManifestPlugin;
13108
13188
  init_loader();
13109
13189
  init_ProtectedRoutesSchema();
13110
13190
  init_joinUrl();
13111
- import { mkdir as mkdir8, writeFile as writeFile9 } from "node:fs/promises";
13191
+ import { mkdir as mkdir8, writeFile as writeFile10 } from "node:fs/promises";
13112
13192
  import path34 from "node:path";
13113
13193
  import { matchPath } from "react-router";
13114
13194
  var processMarkdownFile = async (filePath) => {
@@ -13220,7 +13300,7 @@ var viteMarkdownExportPlugin = () => {
13220
13300
  const segments = routePath === "/" ? ["index"] : routePath.split("/").filter(Boolean);
13221
13301
  const outputPath = `${path34.join(distDir, ...segments)}.md`;
13222
13302
  await mkdir8(path34.dirname(outputPath), { recursive: true });
13223
- await writeFile9(outputPath, finalMarkdown, "utf-8");
13303
+ await writeFile10(outputPath, finalMarkdown, "utf-8");
13224
13304
  } catch (error) {
13225
13305
  console.warn(`Failed to export markdown for ${routePath}:`, error);
13226
13306
  }
@@ -13230,7 +13310,7 @@ var viteMarkdownExportPlugin = () => {
13230
13310
  config2.__meta.rootDir,
13231
13311
  "node_modules/.apitogo/markdown-info.json"
13232
13312
  );
13233
- await writeFile9(
13313
+ await writeFile10(
13234
13314
  markdownInfoPath,
13235
13315
  JSON.stringify(markdownFileInfos, null, 2),
13236
13316
  "utf-8"
@@ -13858,7 +13938,7 @@ function isBareSpecifier(source) {
13858
13938
  return !source.startsWith("\0") && !source.startsWith(".") && !source.startsWith("/") && !path38.isAbsolute(source);
13859
13939
  }
13860
13940
  function isInsideNodeModules(filePath) {
13861
- return filePath.split(path38.sep).includes("node_modules");
13941
+ return filePath.split(/[\\/]/).includes("node_modules");
13862
13942
  }
13863
13943
  function shouldResolveFromSiteRoot(source, importer, rootDir) {
13864
13944
  if (!importer || !isBareSpecifier(source)) {
@@ -14315,7 +14395,7 @@ ${cssLinks}
14315
14395
  init_package_json();
14316
14396
  init_joinUrl();
14317
14397
  import assert from "node:assert";
14318
- 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";
14319
14399
  import path42 from "node:path";
14320
14400
  var pkgJson = getZudokuPackageJson();
14321
14401
  function generateOutput({
@@ -14376,7 +14456,7 @@ async function writeOutput(dir, {
14376
14456
  const outputDir = process.env.VERCEL ? path42.join(dir, ".vercel/output") : path42.join(dir, "dist/.output");
14377
14457
  await mkdir9(outputDir, { recursive: true });
14378
14458
  const outputFile = path42.join(outputDir, "config.json");
14379
- await writeFile10(outputFile, JSON.stringify(output4, null, 2), "utf-8");
14459
+ await writeFile11(outputFile, JSON.stringify(output4, null, 2), "utf-8");
14380
14460
  if (process.env.VERCEL) {
14381
14461
  console.log("Wrote Vercel output to", outputDir);
14382
14462
  }
@@ -14772,7 +14852,7 @@ var runPrerender = async (options) => {
14772
14852
  });
14773
14853
  const indexHtml = path46.join(clientOutDir, "index.html");
14774
14854
  if (!workerResults.find((r) => r.outputPath === indexHtml)) {
14775
- await writeFile12(indexHtml, html, "utf-8");
14855
+ await writeFile13(indexHtml, html, "utf-8");
14776
14856
  }
14777
14857
  const statusPages = workerResults.flatMap(
14778
14858
  (r) => /400|404|500\.html$/.test(r.outputPath) ? r.outputPath : []
@@ -14797,7 +14877,7 @@ var runPrerender = async (options) => {
14797
14877
  rewrites
14798
14878
  });
14799
14879
  if (ZuploEnv.isZuplo && issuer) {
14800
- await writeFile12(
14880
+ await writeFile13(
14801
14881
  path46.join(dir, DIST_DIR, ".output/zuplo.json"),
14802
14882
  JSON.stringify({ issuer }, null, 2),
14803
14883
  "utf-8"
@@ -14820,7 +14900,7 @@ var bundleSSREntry = async (options) => {
14820
14900
  '"__BASE_PATH__"',
14821
14901
  basePath ? JSON.stringify(basePath) : "undefined"
14822
14902
  );
14823
- await writeFile12(tempEntryPath, entryContent, "utf-8");
14903
+ await writeFile13(tempEntryPath, entryContent, "utf-8");
14824
14904
  try {
14825
14905
  await esbuild({
14826
14906
  entryPoints: [tempEntryPath],
@@ -14843,7 +14923,7 @@ init_package_json();
14843
14923
 
14844
14924
  // src/cli/init/v2-packages.ts
14845
14925
  import { existsSync as existsSync5 } from "node:fs";
14846
- import { readFile as readFile11, writeFile as writeFile13 } from "node:fs/promises";
14926
+ import { readFile as readFile11, writeFile as writeFile14 } from "node:fs/promises";
14847
14927
  import path47 from "node:path";
14848
14928
  var CORE_PACKAGES = [
14849
14929
  "@lukoweb/apitogo",
@@ -14863,7 +14943,7 @@ function packageNameFrom(name) {
14863
14943
  async function ensurePackageJson(dir, name) {
14864
14944
  const file = path47.join(dir, "package.json");
14865
14945
  if (existsSync5(file)) return false;
14866
- await writeFile13(
14946
+ await writeFile14(
14867
14947
  file,
14868
14948
  `${JSON.stringify(
14869
14949
  {
@@ -14946,17 +15026,21 @@ function portalBuildStep(context, options = {}, build2 = runBuild) {
14946
15026
  ),
14947
15027
  run: async () => {
14948
15028
  const originalCwd = process.cwd();
15029
+ const originalNodeEnv = process.env.NODE_ENV;
15030
+ process.env.NODE_ENV = "production";
14949
15031
  process.chdir(root);
14950
15032
  try {
14951
15033
  await build2({ dir: root, jsonOnly: options.jsonOnly });
14952
15034
  } finally {
14953
15035
  process.chdir(originalCwd);
15036
+ if (originalNodeEnv === void 0) delete process.env.NODE_ENV;
15037
+ else process.env.NODE_ENV = originalNodeEnv;
14954
15038
  }
14955
15039
  await rm8(buildDir, { recursive: true, force: true });
14956
15040
  await mkdir12(path48.dirname(buildDir), { recursive: true });
14957
15041
  await rename6(path48.join(root, "dist"), buildDir);
14958
15042
  if (prepared.publishedManifest) {
14959
- await writeFile14(
15043
+ await writeFile15(
14960
15044
  path48.join(buildDir, MANIFEST_FILE),
14961
15045
  `${JSON.stringify(prepared.publishedManifest, null, 2)}
14962
15046
  `
@@ -15970,6 +16054,56 @@ async function installMissingPackages(dir, config2, version, options) {
15970
16054
  return { status: "installed", packages: missing, command: shown };
15971
16055
  }
15972
16056
 
16057
+ // src/cli/services/prompter.ts
16058
+ import { createInterface as createInterface2 } from "node:readline/promises";
16059
+ var terminalQuestion = async (prompt) => {
16060
+ const rl = createInterface2({ input: process.stdin, output: process.stdout });
16061
+ try {
16062
+ return await rl.question(prompt);
16063
+ } finally {
16064
+ rl.close();
16065
+ }
16066
+ };
16067
+ function createAsker(question = terminalQuestion, say = (line2) => console.log(line2)) {
16068
+ return {
16069
+ async text(prompt, options) {
16070
+ for (; ; ) {
16071
+ const suffix = options?.default ? ` [${options.default}]` : "";
16072
+ const answer = (await question(`${prompt}${suffix}: `)).trim();
16073
+ const value = answer || options?.default || "";
16074
+ if (!value) {
16075
+ say("A value is needed.");
16076
+ continue;
16077
+ }
16078
+ const problem = options?.validate?.(value);
16079
+ if (problem) {
16080
+ say(problem);
16081
+ continue;
16082
+ }
16083
+ return value;
16084
+ }
16085
+ },
16086
+ async choose(prompt, choices) {
16087
+ say(prompt);
16088
+ choices.forEach((choice, index) => {
16089
+ say(` ${index + 1}) ${choice.label}`);
16090
+ });
16091
+ for (; ; ) {
16092
+ const answer = (await question(`Choose 1-${choices.length} [1]: `)).trim();
16093
+ const index = Number(answer || "1");
16094
+ const chosen = Number.isInteger(index) && index >= 1 && index <= choices.length ? choices[index - 1] : choices.find((choice) => choice.value === answer);
16095
+ if (chosen) return chosen.value;
16096
+ say(`Pick a number from 1 to ${choices.length}.`);
16097
+ }
16098
+ },
16099
+ async confirm(prompt, defaultYes = false) {
16100
+ const answer = (await question(`${prompt} (${defaultYes ? "Y/n" : "y/N"}) `)).trim().toLowerCase();
16101
+ if (!answer) return defaultYes;
16102
+ return answer === "y" || answer === "yes";
16103
+ }
16104
+ };
16105
+ }
16106
+
15973
16107
  // src/cli/services/handler.ts
15974
16108
  var list = (value) => {
15975
16109
  const items = (Array.isArray(value) ? value : value === void 0 ? [] : [value]).filter(
@@ -16024,15 +16158,6 @@ function addOptionsFromArguments(service, argv) {
16024
16158
  };
16025
16159
  }
16026
16160
  var canPrompt2 = (json) => !json && process.stdin.isTTY === true && process.stdout.isTTY === true;
16027
- async function confirm(question) {
16028
- const rl = createInterface2({ input: process.stdin, output: process.stdout });
16029
- try {
16030
- const answer = (await rl.question(question)).trim().toLowerCase();
16031
- return answer === "y" || answer === "yes";
16032
- } finally {
16033
- rl.close();
16034
- }
16035
- }
16036
16161
  function describe(edit, dir) {
16037
16162
  return {
16038
16163
  service: edit.service,
@@ -16044,6 +16169,7 @@ function describe(edit, dir) {
16044
16169
  }
16045
16170
  async function run(action, argv, plan2, deps = {}) {
16046
16171
  const json = argv.json === true;
16172
+ const asker = argv.yes === true ? void 0 : deps.asker ?? (canPrompt2(json) ? createAsker() : void 0);
16047
16173
  try {
16048
16174
  if (!isServiceName(argv.service)) {
16049
16175
  throw new ServiceEditError(
@@ -16058,7 +16184,28 @@ async function run(action, argv, plan2, deps = {}) {
16058
16184
  "Create one with `apitogo init`."
16059
16185
  );
16060
16186
  }
16061
- const edit = await plan2(argv.service);
16187
+ const service = argv.service;
16188
+ let edit;
16189
+ for (let attempt = 0; edit === void 0; attempt++) {
16190
+ try {
16191
+ edit = await plan2(service, asker);
16192
+ } catch (error) {
16193
+ const missing = action === "add" && error instanceof ServiceEditError ? error.missingServices : [];
16194
+ if (missing.length === 0 || !asker || argv.dryRun || attempt >= 3)
16195
+ throw error;
16196
+ for (const needed2 of missing) {
16197
+ if (!await asker.confirm(
16198
+ `${service} needs ${needed2}. Add ${needed2} now?`,
16199
+ true
16200
+ ))
16201
+ throw error;
16202
+ await addService(
16203
+ { service: needed2, dir: argv.dir, install: argv.install },
16204
+ deps
16205
+ );
16206
+ }
16207
+ }
16208
+ }
16062
16209
  const needed = action === "add" ? missingPackages(dir, packagesForConfig(edit.config)) : [];
16063
16210
  if (!json) {
16064
16211
  for (const line2 of formatServiceEdit(edit))
@@ -16079,13 +16226,13 @@ async function run(action, argv, plan2, deps = {}) {
16079
16226
  return;
16080
16227
  }
16081
16228
  if (argv.yes !== true) {
16082
- if (!canPrompt2(json)) {
16229
+ if (!asker) {
16083
16230
  throw new ServiceEditError(
16084
16231
  "This is not an interactive terminal.",
16085
16232
  "Pass --yes to apply the change, or --dry-run to only see it."
16086
16233
  );
16087
16234
  }
16088
- if (!await confirm("Apply this change? (y/N) ")) {
16235
+ if (!await asker.confirm("Apply this change?")) {
16089
16236
  printResultToConsole("Cancelled. Nothing was written.");
16090
16237
  return;
16091
16238
  }
@@ -16159,7 +16306,10 @@ async function installPackages(dir, edit, argv, deps, json) {
16159
16306
  var addService = (argv, deps = {}) => run(
16160
16307
  "add",
16161
16308
  argv,
16162
- (service) => planAddService(service, addOptionsFromArguments(service, argv)),
16309
+ (service, ask4) => planAddService(service, {
16310
+ ...addOptionsFromArguments(service, argv),
16311
+ ask: ask4
16312
+ }),
16163
16313
  deps
16164
16314
  );
16165
16315
  var removeService = (argv) => run(
@@ -16518,7 +16668,7 @@ function formatDevPortalBackendResponse(projectId, backend) {
16518
16668
  // src/cli/platform/helpers/dev-portal-manifest.ts
16519
16669
  init_gateway_limits();
16520
16670
  import { existsSync as existsSync7 } from "node:fs";
16521
- import { access as access6, mkdir as mkdir13, readFile as readFile17, writeFile as writeFile15 } from "node:fs/promises";
16671
+ import { access as access6, mkdir as mkdir13, readFile as readFile17, writeFile as writeFile16 } from "node:fs/promises";
16522
16672
  import path57 from "node:path";
16523
16673
 
16524
16674
  // src/cli/make-config/json-config.ts
@@ -17838,7 +17988,7 @@ async function writeManifest2(siteDirectory, manifest) {
17838
17988
  ...safeManifest
17839
17989
  } = manifest;
17840
17990
  config2 = applyManifestToConfig(config2, safeManifest);
17841
- await writeFile15(filePath, `${JSON.stringify(config2, null, 2)}
17991
+ await writeFile16(filePath, `${JSON.stringify(config2, null, 2)}
17842
17992
  `, "utf8");
17843
17993
  return filePath;
17844
17994
  }
@@ -17850,7 +18000,7 @@ import {
17850
18000
  mkdir as mkdir14,
17851
18001
  readFile as readFile18,
17852
18002
  unlink,
17853
- writeFile as writeFile16
18003
+ writeFile as writeFile17
17854
18004
  } from "node:fs/promises";
17855
18005
  import path58 from "node:path";
17856
18006
  var ENV_FILENAME = ".env";
@@ -18125,7 +18275,7 @@ async function writeScaffoldEnv(siteDirectory) {
18125
18275
  await access7(envPath);
18126
18276
  } catch {
18127
18277
  await mkdir14(resolvedDirectory, { recursive: true });
18128
- await writeFile16(envPath, createEnvContent(), "utf8");
18278
+ await writeFile17(envPath, createEnvContent(), "utf8");
18129
18279
  filesWritten.push(ENV_FILENAME);
18130
18280
  }
18131
18281
  for (const legacyName of [".env.example"]) {
@@ -18154,7 +18304,7 @@ async function writeScaffoldEnv(siteDirectory) {
18154
18304
  if (!hasEntry) {
18155
18305
  const suffix = gitignore.length === 0 ? "" : gitignore.endsWith("\n") ? "" : "\n";
18156
18306
  if (gitignore.length === 0) {
18157
- await writeFile16(gitignorePath, `${entry}
18307
+ await writeFile17(gitignorePath, `${entry}
18158
18308
  `, "utf8");
18159
18309
  } else {
18160
18310
  await appendFile(gitignorePath, `${suffix}${entry}
@@ -18938,7 +19088,7 @@ var check_default = {
18938
19088
 
18939
19089
  // src/cli/make-config/handler.ts
18940
19090
  import { constants as constants2 } from "node:fs";
18941
- import { access as access12, mkdir as mkdir17, readFile as readFile24, writeFile as writeFile21 } from "node:fs/promises";
19091
+ import { access as access12, mkdir as mkdir17, readFile as readFile24, writeFile as writeFile22 } from "node:fs/promises";
18942
19092
  import path71 from "node:path";
18943
19093
  import { stdin as input2 } from "node:process";
18944
19094
  import { glob as glob8 } from "glob";
@@ -19078,12 +19228,12 @@ function withDefaultIncludedActionKeys(plan2, actionKeys) {
19078
19228
  }
19079
19229
 
19080
19230
  // src/cli/scaffold/dev-portal-config-pages.ts
19081
- import { access as access8, readFile as readFile20, rename as rename7, rm as rm9, writeFile as writeFile17 } from "node:fs/promises";
19231
+ import { access as access8, readFile as readFile20, rename as rename7, rm as rm9, writeFile as writeFile18 } from "node:fs/promises";
19082
19232
  import path63 from "node:path";
19083
19233
  async function writeConfigAtomically(filePath, content) {
19084
19234
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
19085
19235
  try {
19086
- await writeFile17(temporaryPath, content, "utf8");
19236
+ await writeFile18(temporaryPath, content, "utf8");
19087
19237
  await rename7(temporaryPath, filePath);
19088
19238
  } catch (error) {
19089
19239
  await rm9(temporaryPath, { force: true });
@@ -19855,7 +20005,7 @@ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label2) {
19855
20005
  }
19856
20006
 
19857
20007
  // src/cli/scaffold/wire-openapi.ts
19858
- import { access as access10, readFile as readFile22, writeFile as writeFile19 } from "node:fs/promises";
20008
+ import { access as access10, readFile as readFile22, writeFile as writeFile20 } from "node:fs/promises";
19859
20009
  import path68 from "node:path";
19860
20010
 
19861
20011
  // src/cli/platform/helpers/openapi.ts
@@ -20059,7 +20209,7 @@ async function validateOpenApiFile(filePath) {
20059
20209
  }
20060
20210
 
20061
20211
  // src/cli/scaffold/scaffold-dev-portal-site.ts
20062
- import { mkdir as mkdir15, readFile as readFile21, writeFile as writeFile18 } from "node:fs/promises";
20212
+ import { mkdir as mkdir15, readFile as readFile21, writeFile as writeFile19 } from "node:fs/promises";
20063
20213
  import path67 from "node:path";
20064
20214
 
20065
20215
  // src/cli/scaffold/apitogo-npm-packages.ts
@@ -20471,7 +20621,7 @@ async function ensureAccountPlansPage(outputDirectory) {
20471
20621
  return void 0;
20472
20622
  } catch {
20473
20623
  await mkdir15(path67.dirname(pagePath), { recursive: true });
20474
- await writeFile18(pagePath, ACCOUNT_PLANS_PAGE);
20624
+ await writeFile19(pagePath, ACCOUNT_PLANS_PAGE);
20475
20625
  return ACCOUNT_PLANS_PAGE_PATH;
20476
20626
  }
20477
20627
  }
@@ -20482,7 +20632,7 @@ async function ensureDashboardPage(outputDirectory) {
20482
20632
  return void 0;
20483
20633
  } catch {
20484
20634
  await mkdir15(path67.dirname(pagePath), { recursive: true });
20485
- await writeFile18(pagePath, DASHBOARD_PAGE);
20635
+ await writeFile19(pagePath, DASHBOARD_PAGE);
20486
20636
  return DASHBOARD_PAGE_PATH;
20487
20637
  }
20488
20638
  }
@@ -20541,7 +20691,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
20541
20691
  const packageJsonPath = path67.join(outputDirectory, "package.json");
20542
20692
  if (options.patchPackageJson !== false) {
20543
20693
  const packageJson3 = await readFile21(packageJsonPath, "utf8");
20544
- await writeFile18(
20694
+ await writeFile19(
20545
20695
  packageJsonPath,
20546
20696
  rewritePackageJson(packageJson3, {
20547
20697
  siteName: options.siteName,
@@ -20553,7 +20703,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
20553
20703
  }
20554
20704
  const configPath = path67.join(outputDirectory, "apitogo.config.tsx");
20555
20705
  const configContent = await readFile21(configPath, "utf8");
20556
- await writeFile18(
20706
+ await writeFile19(
20557
20707
  configPath,
20558
20708
  patchDashboardPageConfig(
20559
20709
  patchAccountPlansPageConfig(
@@ -20665,7 +20815,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20665
20815
  input: input5,
20666
20816
  apiPath: "api"
20667
20817
  });
20668
- await writeFile19(
20818
+ await writeFile20(
20669
20819
  jsonConfigPath,
20670
20820
  `${JSON.stringify(updated, null, 2)}
20671
20821
  `,
@@ -20676,7 +20826,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20676
20826
  const configContent = await readFile22(tsxConfigPath, "utf8");
20677
20827
  try {
20678
20828
  const patched = patchApitogoConfigOpenApiToFile(configContent, input5);
20679
- await writeFile19(tsxConfigPath, patched);
20829
+ await writeFile20(tsxConfigPath, patched);
20680
20830
  } catch (error) {
20681
20831
  return {
20682
20832
  status: "failed",
@@ -21029,7 +21179,7 @@ export default function ${componentName}() {
21029
21179
  // src/cli/make-config/scaffold-project.ts
21030
21180
  init_package_json();
21031
21181
  import { constants } from "node:fs";
21032
- import { access as access11, mkdir as mkdir16, readFile as readFile23, writeFile as writeFile20 } from "node:fs/promises";
21182
+ import { access as access11, mkdir as mkdir16, readFile as readFile23, writeFile as writeFile21 } from "node:fs/promises";
21033
21183
  import path70 from "node:path";
21034
21184
  import { glob as glob7 } from "glob";
21035
21185
 
@@ -21107,7 +21257,7 @@ async function ensurePackageJson2(dir) {
21107
21257
  typescript: "^5"
21108
21258
  }
21109
21259
  };
21110
- await writeFile20(pkgPath, `${JSON.stringify(pkg2, null, 2)}
21260
+ await writeFile21(pkgPath, `${JSON.stringify(pkg2, null, 2)}
21111
21261
  `, "utf8");
21112
21262
  return true;
21113
21263
  }
@@ -21134,7 +21284,7 @@ async function ensurePackageJson2(dir) {
21134
21284
  for (const [k, v] of Object.entries(baseDeps)) {
21135
21285
  if (!deps[k]) deps[k] = v;
21136
21286
  }
21137
- await writeFile20(pkgPath, `${JSON.stringify(pkg, null, 2)}
21287
+ await writeFile21(pkgPath, `${JSON.stringify(pkg, null, 2)}
21138
21288
  `, "utf8");
21139
21289
  return false;
21140
21290
  }
@@ -21169,7 +21319,7 @@ async function ensureTsconfigJson(dir) {
21169
21319
  "user-panel"
21170
21320
  ]
21171
21321
  };
21172
- await writeFile20(tsPath, `${JSON.stringify(tsconfig, null, 2)}
21322
+ await writeFile21(tsPath, `${JSON.stringify(tsconfig, null, 2)}
21173
21323
  `, "utf8");
21174
21324
  return true;
21175
21325
  }
@@ -21191,17 +21341,17 @@ async function ensurePortalStubFiles(dir) {
21191
21341
  const landingPath = path70.join(dir, "landing", "LandingPage.tsx");
21192
21342
  if (!await fileExists2(landingPath)) {
21193
21343
  await mkdir16(path70.join(dir, "landing"), { recursive: true });
21194
- await writeFile20(landingPath, LANDING_PAGE_SOURCE, "utf8");
21344
+ await writeFile21(landingPath, LANDING_PAGE_SOURCE, "utf8");
21195
21345
  }
21196
21346
  const plansPath = path70.join(dir, "user-panel", "AccountPlansPage.tsx");
21197
21347
  if (!await fileExists2(plansPath)) {
21198
21348
  await mkdir16(path70.join(dir, "user-panel"), { recursive: true });
21199
- await writeFile20(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
21349
+ await writeFile21(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
21200
21350
  }
21201
21351
  const dashboardPath = path70.join(dir, "user-panel", "DashboardPage.tsx");
21202
21352
  if (!await fileExists2(dashboardPath)) {
21203
21353
  await mkdir16(path70.join(dir, "user-panel"), { recursive: true });
21204
- await writeFile20(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
21354
+ await writeFile21(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
21205
21355
  }
21206
21356
  await ensurePublicBrandingFiles(dir);
21207
21357
  }
@@ -21213,11 +21363,11 @@ async function ensurePublicBrandingFiles(dir) {
21213
21363
  if (await fileExists2(dest)) {
21214
21364
  continue;
21215
21365
  }
21216
- await writeFile20(dest, content, "utf8");
21366
+ await writeFile21(dest, content, "utf8");
21217
21367
  }
21218
21368
  const faviconDest = path70.join(destDir, "Icon.png");
21219
21369
  if (!await fileExists2(faviconDest)) {
21220
- await writeFile20(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
21370
+ await writeFile21(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
21221
21371
  }
21222
21372
  }
21223
21373
  async function ensureThinConfigTsx(dir) {
@@ -21225,7 +21375,7 @@ async function ensureThinConfigTsx(dir) {
21225
21375
  if (await fileExists2(tsxPath)) {
21226
21376
  return false;
21227
21377
  }
21228
- await writeFile20(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
21378
+ await writeFile21(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
21229
21379
  return true;
21230
21380
  }
21231
21381
 
@@ -21286,13 +21436,13 @@ async function persistConfig(config2, ctx) {
21286
21436
  force: ctx.force
21287
21437
  })
21288
21438
  };
21289
- await writeFile21(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
21439
+ await writeFile22(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
21290
21440
  `, "utf8");
21291
21441
  const tsxPath = path71.join(ctx.dir, "apitogo.config.tsx");
21292
21442
  if (await fileExists3(tsxPath) && tsxRoutes.length > 0) {
21293
21443
  const tsxSource = await readFile24(tsxPath, "utf8");
21294
21444
  if (isManagedConfigWrapper(tsxSource)) {
21295
- await writeFile21(
21445
+ await writeFile22(
21296
21446
  tsxPath,
21297
21447
  buildThinWrapperWithCustomPages(tsxRoutes),
21298
21448
  "utf8"
@@ -21396,7 +21546,7 @@ async function makeConfig(argv) {
21396
21546
  if (action2.kind === "page") {
21397
21547
  const pagePath = path71.join(dir, action2.page.relativePath);
21398
21548
  await mkdir17(path71.dirname(pagePath), { recursive: true });
21399
- await writeFile21(
21549
+ await writeFile22(
21400
21550
  pagePath,
21401
21551
  buildStarterDocsPageTsx(action2.page.slug, action2.page.title),
21402
21552
  "utf8"
@@ -21505,7 +21655,7 @@ async function makeConfig(argv) {
21505
21655
  argv.pageContent,
21506
21656
  pageTemplate
21507
21657
  );
21508
- await writeFile21(filePath, tsx, "utf8");
21658
+ await writeFile22(filePath, tsx, "utf8");
21509
21659
  pageResult = {
21510
21660
  action: "updated",
21511
21661
  slug,
@@ -21525,7 +21675,7 @@ async function makeConfig(argv) {
21525
21675
  pageTemplate
21526
21676
  );
21527
21677
  await mkdir17(path71.dirname(filePath), { recursive: true });
21528
- await writeFile21(filePath, tsx, "utf8");
21678
+ await writeFile22(filePath, tsx, "utf8");
21529
21679
  const configResult = await wireNewDevPortalPageInConfig(
21530
21680
  dir,
21531
21681
  slug,
@@ -21874,9 +22024,10 @@ var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set([
21874
22024
  "bin",
21875
22025
  "obj"
21876
22026
  ]);
21877
- async function loadGitignore(rootDir) {
22027
+ async function loadGitignore(rootDir, extraExcludes = []) {
21878
22028
  const ig = ignore();
21879
22029
  ig.add(["node_modules/", ".git/", "dist/", "bin/", "obj/"]);
22030
+ if (extraExcludes.length > 0) ig.add([...extraExcludes]);
21880
22031
  const gitignorePath = path72.join(rootDir, ".gitignore");
21881
22032
  try {
21882
22033
  const content = await fs7.readFile(gitignorePath, "utf8");
@@ -21904,13 +22055,13 @@ async function collectFiles(rootDir, relativeDir, ig, files) {
21904
22055
  }
21905
22056
  }
21906
22057
  }
21907
- async function collectPackageableFiles(sourceDirectory) {
22058
+ async function collectPackageableFiles(sourceDirectory, extraExcludes = []) {
21908
22059
  const resolved = path72.resolve(sourceDirectory);
21909
22060
  const stat7 = await fs7.stat(resolved).catch(() => null);
21910
22061
  if (!stat7?.isDirectory()) {
21911
22062
  throw new Error(`sourceDirectory is not a directory: ${resolved}`);
21912
22063
  }
21913
- const ig = await loadGitignore(resolved);
22064
+ const ig = await loadGitignore(resolved, extraExcludes);
21914
22065
  const files = [];
21915
22066
  await collectFiles(resolved, "", ig, files);
21916
22067
  if (files.length === 0) {
@@ -21930,9 +22081,9 @@ async function sha256File(filePath) {
21930
22081
  });
21931
22082
  return hash2.digest("hex");
21932
22083
  }
21933
- async function hashDirectoryContents(rootDirectory) {
22084
+ async function hashDirectoryContents(rootDirectory, extraExcludes = []) {
21934
22085
  const resolved = path73.resolve(rootDirectory);
21935
- const files = (await collectPackageableFiles(resolved)).sort(
22086
+ const files = (await collectPackageableFiles(resolved, extraExcludes)).sort(
21936
22087
  (a, b) => a.localeCompare(b)
21937
22088
  );
21938
22089
  const hash2 = createHash3("sha256");
@@ -22683,7 +22834,7 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
22683
22834
  }
22684
22835
 
22685
22836
  // src/cli/platform/helpers/openapi-gateway-servers.ts
22686
- import { mkdir as mkdir18, readFile as readFile25, writeFile as writeFile22 } from "node:fs/promises";
22837
+ import { mkdir as mkdir18, readFile as readFile25, writeFile as writeFile23 } from "node:fs/promises";
22687
22838
  import path74 from "node:path";
22688
22839
  import YAML3 from "yaml";
22689
22840
  var GATEWAY_URL_ENV_KEY = "APITOGO_GATEWAY_URL";
@@ -22731,7 +22882,7 @@ async function materializePublishOpenApiWithGateway(siteDirectory, sourceOpenApi
22731
22882
  const doc = await readOpenApiDocument(source);
22732
22883
  const rewritten = withGatewayServers(doc, gatewayUrl);
22733
22884
  await mkdir18(path74.dirname(outputPath), { recursive: true });
22734
- await writeFile22(
22885
+ await writeFile23(
22735
22886
  outputPath,
22736
22887
  `${JSON.stringify(rewritten, null, 2)}
22737
22888
  `,
@@ -22757,7 +22908,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22757
22908
  const backend = config2.backend && typeof config2.backend === "object" ? { ...config2.backend } : {};
22758
22909
  backend.openApiPath = relativeInput;
22759
22910
  config2.backend = backend;
22760
- await writeFile22(configPath, `${JSON.stringify(config2, null, 2)}
22911
+ await writeFile23(configPath, `${JSON.stringify(config2, null, 2)}
22761
22912
  `, "utf8");
22762
22913
  return {
22763
22914
  restore: async () => {
@@ -22773,7 +22924,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22773
22924
  } else {
22774
22925
  current.backend = previousBackend;
22775
22926
  }
22776
- await writeFile22(
22927
+ await writeFile23(
22777
22928
  configPath,
22778
22929
  `${JSON.stringify(current, null, 2)}
22779
22930
  `,
@@ -22870,7 +23021,7 @@ function collectManifestPublicActionKeys(gateway) {
22870
23021
  }
22871
23022
 
22872
23023
  // src/cli/platform/helpers/publish-ledger.ts
22873
- import { mkdir as mkdir19, readFile as readFile26, writeFile as writeFile23 } from "node:fs/promises";
23024
+ import { mkdir as mkdir19, readFile as readFile26, writeFile as writeFile24 } from "node:fs/promises";
22874
23025
  import path75 from "node:path";
22875
23026
  var PUBLISH_LEDGER_RELATIVE_PATH = ".apitogo/publish-state.json";
22876
23027
  function publishLedgerPath(siteDirectory) {
@@ -22897,7 +23048,7 @@ async function writePublishLedger(siteDirectory, ledger) {
22897
23048
  version: 1,
22898
23049
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22899
23050
  };
22900
- await writeFile23(filePath, `${JSON.stringify(next, null, 2)}
23051
+ await writeFile24(filePath, `${JSON.stringify(next, null, 2)}
22901
23052
  `, "utf8");
22902
23053
  return filePath;
22903
23054
  }
@@ -23531,9 +23682,9 @@ import { createReadStream as createReadStream2, createWriteStream as createWrite
23531
23682
  import { tmpdir as tmpdir2 } from "node:os";
23532
23683
  import path77 from "node:path";
23533
23684
  import { ZipArchive } from "archiver";
23534
- async function packageBackendDirectory(sourceDirectory) {
23685
+ async function packageBackendDirectory(sourceDirectory, extraExcludes = []) {
23535
23686
  const resolved = path77.resolve(sourceDirectory);
23536
- const files = await collectPackageableFiles(resolved);
23687
+ const files = await collectPackageableFiles(resolved, extraExcludes);
23537
23688
  const zipPath = path77.join(
23538
23689
  tmpdir2(),
23539
23690
  `apitogo-backend-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
@@ -25058,7 +25209,7 @@ async function resolveProjectIdForPublish(client, config2, args, wait, progress,
25058
25209
  import path81 from "node:path";
25059
25210
 
25060
25211
  // src/cli/platform/helpers/dev-portal-site-api-url.ts
25061
- import { readdir as readdir3, readFile as readFile27, writeFile as writeFile24 } from "node:fs/promises";
25212
+ import { readdir as readdir3, readFile as readFile27, writeFile as writeFile25 } from "node:fs/promises";
25062
25213
  import path78 from "node:path";
25063
25214
  var SITE_API_BASE_URL_ENV_KEY = "VITE_APITOGO_AUTHENTICATION_API_BASE_URL";
25064
25215
  var PLACEHOLDER_HOST_MARKERS = [
@@ -25120,13 +25271,13 @@ async function patchSiteApiBaseUrl(sourceDirectory, apiBaseUrl) {
25120
25271
  ${line2}
25121
25272
  ` : `${line2}
25122
25273
  `;
25123
- await writeFile24(envPath, patched);
25274
+ await writeFile25(envPath, patched);
25124
25275
  patchedExisting = true;
25125
25276
  } catch {
25126
25277
  }
25127
25278
  }
25128
25279
  if (!patchedExisting) {
25129
- await writeFile24(path78.join(resolved, ".env"), `${line2}
25280
+ await writeFile25(path78.join(resolved, ".env"), `${line2}
25130
25281
  `);
25131
25282
  }
25132
25283
  }
@@ -27344,19 +27495,11 @@ var GatewayDeployError = class extends ApitogoError {
27344
27495
  };
27345
27496
  var sha256 = (text) => createHash4("sha256").update(text).digest("hex");
27346
27497
  function gatewayNotDeployableReason(gateway) {
27347
- if (gateway.origin.mode === "managed")
27348
- return "a managed origin cannot be deployed from this CLI version yet; use origin.mode proxy";
27349
27498
  if (gateway.specs.length > 1)
27350
27499
  return "the platform takes one OpenAPI file per gateway, but the config lists several";
27351
27500
  return void 0;
27352
27501
  }
27353
27502
  function assertDeployable(gateway) {
27354
- if (gateway.origin.mode === "managed")
27355
- throw new GatewayDeployError(
27356
- "GATEWAY_MANAGED_ORIGIN_UNSUPPORTED",
27357
- "A managed origin cannot be deployed from this CLI version yet.",
27358
- 'Set gateway.origin.mode to "proxy" and point gateway.origin.url at your API.'
27359
- );
27360
27503
  if (gateway.specs.length > 1)
27361
27504
  throw new GatewayDeployError(
27362
27505
  "GATEWAY_MULTIPLE_SPECS",
@@ -27403,6 +27546,37 @@ async function gatewayDeployInputs(resolved, projectId) {
27403
27546
  };
27404
27547
  }
27405
27548
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
27549
+ async function backendDeployInputs(gateway, projectId) {
27550
+ const { origin } = gateway;
27551
+ if (origin.mode !== "managed" || !origin.source || !origin.runtime || !origin.runtimeVersion)
27552
+ return void 0;
27553
+ const runtime = normalizeRuntime(origin.runtime);
27554
+ const runtimeVersion = normalizeRuntimeVersionSlug(
27555
+ runtime,
27556
+ origin.runtimeVersion
27557
+ );
27558
+ const exclude = origin.exclude ?? [];
27559
+ let tree;
27560
+ try {
27561
+ tree = await hashDirectoryContents(origin.source, exclude);
27562
+ } catch (error) {
27563
+ throw new GatewayDeployError(
27564
+ "GATEWAY_BACKEND_UNREADABLE",
27565
+ `Cannot read the backend source ${origin.source}: ${error instanceof Error ? error.message : String(error)}`,
27566
+ "Check gateway.origin.source in the config. It must be a folder with your API's code."
27567
+ );
27568
+ }
27569
+ return {
27570
+ source: origin.source,
27571
+ runtime,
27572
+ runtimeVersion,
27573
+ exclude,
27574
+ healthPath: origin.healthPath,
27575
+ hash: sha256(
27576
+ `${projectId}|${runtime}|${runtimeVersion}|${origin.healthPath ?? ""}|${stableJson(exclude)}|${tree}`
27577
+ )
27578
+ };
27579
+ }
27406
27580
  function describePlatformProblem(error) {
27407
27581
  if (!(error instanceof PlatformApiError) || !error.problem) return [];
27408
27582
  const errors = error.problem.errors;
@@ -27438,11 +27612,73 @@ function rejected(what, error) {
27438
27612
  async function deployGateway(resolved, projectId, deps, options = {}) {
27439
27613
  const { gateway, spec, body, specHash, configHash } = await gatewayDeployInputs(resolved, projectId);
27440
27614
  assertDeployable(gateway);
27615
+ const backend = await backendDeployInputs(gateway, projectId);
27441
27616
  const client = deps.client;
27442
27617
  const sleep5 = deps.sleep ?? defaultSleep;
27443
27618
  const now = deps.now ?? Date.now;
27444
27619
  const timeoutMs = deps.reconcileTimeoutMs ?? 18e4;
27620
+ const backendTimeoutMs = deps.backendTimeoutMs ?? 20 * 6e4;
27445
27621
  const intervalMs = deps.pollIntervalMs ?? 2e3;
27622
+ const backendStep = {
27623
+ name: "backend",
27624
+ hash: () => backend?.hash ?? "",
27625
+ run: async () => {
27626
+ if (!backend) throw new Error("gateway.origin is not a managed origin");
27627
+ const prepared = await prepareBackendPackagingDirectory({
27628
+ sourceDirectory: backend.source,
27629
+ runtime: backend.runtime
27630
+ });
27631
+ let zipPath;
27632
+ try {
27633
+ const packaged = await packageBackendDirectory(
27634
+ prepared.directory,
27635
+ backend.exclude
27636
+ );
27637
+ zipPath = packaged.zipPath;
27638
+ let started;
27639
+ try {
27640
+ started = await client.deployBackend(projectId, {
27641
+ archivePath: packaged.zipPath,
27642
+ runtime: backend.runtime,
27643
+ runtimeVersion: backend.runtimeVersion,
27644
+ healthCheckPath: backend.healthPath
27645
+ });
27646
+ } catch (error) {
27647
+ return rejected("backend upload", error);
27648
+ }
27649
+ if (!started.reused) {
27650
+ const deadline = now() + backendTimeoutMs;
27651
+ for (; ; ) {
27652
+ const state = await client.getDeploymentStatus(
27653
+ projectId,
27654
+ started.deploymentId
27655
+ );
27656
+ if (state.status.toLowerCase() === "healthy") break;
27657
+ if (isTerminalDeploymentStatus(state.status))
27658
+ throw new GatewayDeployError(
27659
+ "GATEWAY_BACKEND_FAILED",
27660
+ `The platform could not deploy your backend (${state.status}): ${state.error ?? state.messages.at(-1)?.message ?? "no reason given"}.`,
27661
+ "Fix the cause and run deploy again; the backend is uploaded again."
27662
+ );
27663
+ if (now() >= deadline)
27664
+ throw new GatewayDeployError(
27665
+ "GATEWAY_BACKEND_TIMEOUT",
27666
+ `Your backend was not ready within ${Math.round(backendTimeoutMs / 6e4)} minutes (state: ${state.status}).`,
27667
+ "Run deploy again to keep waiting."
27668
+ );
27669
+ await sleep5(intervalMs);
27670
+ }
27671
+ }
27672
+ return {
27673
+ deploymentId: started.deploymentId,
27674
+ ...started.artifactContentSha256 ? { artifactSha256: started.artifactContentSha256 } : {}
27675
+ };
27676
+ } finally {
27677
+ if (zipPath) await cleanupZip(zipPath);
27678
+ await prepared.cleanup?.();
27679
+ }
27680
+ }
27681
+ };
27446
27682
  const specStep = {
27447
27683
  name: "spec",
27448
27684
  hash: () => specHash,
@@ -27522,12 +27758,30 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
27522
27758
  };
27523
27759
  }
27524
27760
  };
27525
- const outcomes = await runPipeline(
27526
- resolved.state,
27527
- "gateway",
27528
- [specStep, configStep, reconcileStep],
27529
- { force: options.force }
27530
- );
27761
+ let outcomes;
27762
+ if (backend) {
27763
+ const head = await runPipeline(
27764
+ resolved.state,
27765
+ "gateway",
27766
+ [specStep, configStep],
27767
+ { force: options.force }
27768
+ );
27769
+ const middle = await runPipeline(resolved.state, "gateway", [backendStep], {
27770
+ force: options.force
27771
+ });
27772
+ const changed = [...head, ...middle].some((outcome) => !outcome.skipped);
27773
+ const tail = await runPipeline(resolved.state, "gateway", [reconcileStep], {
27774
+ force: options.force || changed
27775
+ });
27776
+ outcomes = [...head, ...middle, ...tail];
27777
+ } else {
27778
+ outcomes = await runPipeline(
27779
+ resolved.state,
27780
+ "gateway",
27781
+ [specStep, configStep, reconcileStep],
27782
+ { force: options.force }
27783
+ );
27784
+ }
27531
27785
  const reconcile = outcomes.find((outcome) => outcome.step === "reconcile");
27532
27786
  const ran = reconcile !== void 0 && !reconcile.skipped;
27533
27787
  const known = (await readManifest(resolved.state))?.services.gateway;
@@ -28642,7 +28896,7 @@ async function planGatewayService(resolved, ledger, dryRun) {
28642
28896
  details: []
28643
28897
  };
28644
28898
  const details = [
28645
- `origin: proxy ${gateway.origin.url ?? "(no url set)"}`,
28899
+ 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)"}`,
28646
28900
  ...gateway.auth ? [`auth: ${gateway.auth.mode}`] : ["auth: none"],
28647
28901
  ...describePolicies(gateway)
28648
28902
  ];
@@ -28695,8 +28949,27 @@ async function planGatewayService(resolved, ledger, dryRun) {
28695
28949
  );
28696
28950
  }
28697
28951
  }
28952
+ let backendHash;
28953
+ try {
28954
+ backendHash = (await backendDeployInputs(gateway, resolved.project.id ?? ""))?.hash;
28955
+ } catch (error) {
28956
+ return {
28957
+ ...base,
28958
+ action: "blocked",
28959
+ reason: "cannot deploy until this is fixed",
28960
+ details,
28961
+ issues: [
28962
+ {
28963
+ code: "GATEWAY_BACKEND_UNREADABLE",
28964
+ path: "gateway.origin.source",
28965
+ message: error instanceof Error ? error.message : String(error)
28966
+ }
28967
+ ]
28968
+ };
28969
+ }
28698
28970
  const step2 = (name) => stepOf(ledger, "gateway", name);
28699
28971
  const hashes = {
28972
+ backend: backendHash ?? "",
28700
28973
  spec: inputs.specHash,
28701
28974
  config: inputs.configHash,
28702
28975
  reconcile: inputs.configHash
@@ -28712,9 +28985,10 @@ async function planGatewayService(resolved, ledger, dryRun) {
28712
28985
  const lines = [
28713
28986
  `step spec (upload the OpenAPI file): ${state("spec")}`,
28714
28987
  `step config (send the wanted state): ${state("config")}`,
28988
+ ...backendHash ? [`step backend (upload and start your code): ${state("backend")}`] : [],
28715
28989
  `step reconcile (wait until the platform applied it): ${state("reconcile")}`
28716
28990
  ];
28717
- 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;
28991
+ 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;
28718
28992
  if (issues.length > 0)
28719
28993
  return {
28720
28994
  ...base,
@@ -28795,7 +29069,7 @@ function corsLines(platform) {
28795
29069
  ];
28796
29070
  }
28797
29071
  async function gatewayStatus(resolved, ledger, manifest, remote) {
28798
- const steps = ["spec", "config", "reconcile"].map((name) => ({
29072
+ const steps = ["spec", "config", "backend", "reconcile"].map((name) => ({
28799
29073
  name,
28800
29074
  record: stepOf(ledger, "gateway", name)
28801
29075
  }));
@@ -28813,6 +29087,14 @@ async function gatewayStatus(resolved, ledger, manifest, remote) {
28813
29087
  if (remote && projectId) {
28814
29088
  try {
28815
29089
  const platform = await remote(projectId);
29090
+ if (platform.status === "Released") {
29091
+ details.length = 0;
29092
+ details.push(
29093
+ "platform state: Released (the gateway was removed; deploy makes a new one)"
29094
+ );
29095
+ state = "not-deployed";
29096
+ return { service: "gateway", state, details: [...details, ...local] };
29097
+ }
28816
29098
  details.push(
28817
29099
  `platform state: ${platform.status}`,
28818
29100
  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"})`,
@@ -28950,7 +29232,7 @@ function formatStatus(output4) {
28950
29232
  }
28951
29233
 
28952
29234
  // src/cli/deploy/static-web-config.ts
28953
- import { access as access14, writeFile as writeFile25 } from "node:fs/promises";
29235
+ import { access as access14, writeFile as writeFile26 } from "node:fs/promises";
28954
29236
  import path84 from "node:path";
28955
29237
  var STATIC_WEB_CONFIG_FILE = "staticwebapp.config.json";
28956
29238
  var STATIC_WEB_CONFIG_VERSION = 1;
@@ -28969,7 +29251,7 @@ async function ensureStaticWebConfig(buildDir) {
28969
29251
  await access14(file);
28970
29252
  return false;
28971
29253
  } catch {
28972
- await writeFile25(
29254
+ await writeFile26(
28973
29255
  file,
28974
29256
  `${JSON.stringify(DEFAULT_STATIC_WEB_CONFIG, null, 2)}
28975
29257
  `,
@@ -29118,7 +29400,7 @@ async function deployGatewayService(options, deps, projectId, named) {
29118
29400
  return { service: "gateway", status: "skipped", reason: why, steps: [] };
29119
29401
  }
29120
29402
  const client = deps.client;
29121
- if (!client.putGatewaySpec || !client.putGatewayConfig || !client.getGatewayReconciliation || !client.getGatewayStatus || !client.getGatewayCapabilities)
29403
+ if (!client.putGatewaySpec || !client.putGatewayConfig || !client.getGatewayReconciliation || !client.getGatewayStatus || !client.getGatewayCapabilities || gateway?.origin.mode === "managed" && (!client.deployBackend || !client.getDeploymentStatus))
29122
29404
  throw new Error("The platform client has no gateway calls.");
29123
29405
  const result = await deployGateway(
29124
29406
  resolved,
@@ -29366,7 +29648,7 @@ var deploy_default = {
29366
29648
  };
29367
29649
 
29368
29650
  // src/cli/export/handler.ts
29369
- import { mkdir as mkdir20, writeFile as writeFile26 } from "node:fs/promises";
29651
+ import { mkdir as mkdir20, writeFile as writeFile27 } from "node:fs/promises";
29370
29652
  import path87 from "node:path";
29371
29653
  init_invariant();
29372
29654
  var ExportError = class extends ApitogoError {
@@ -29408,7 +29690,7 @@ async function exportOpenApi(args, deps) {
29408
29690
  if (!args.out) return { text };
29409
29691
  const file = path87.resolve(process.cwd(), args.out);
29410
29692
  await mkdir20(path87.dirname(file), { recursive: true });
29411
- await writeFile26(file, text);
29693
+ await writeFile27(file, text);
29412
29694
  return { text, file };
29413
29695
  }
29414
29696
  async function exportCommand(args) {
@@ -29461,13 +29743,13 @@ var export_default = {
29461
29743
  import path91 from "node:path";
29462
29744
 
29463
29745
  // src/cli/scaffold/run-scaffold-discovery.ts
29464
- import { access as access16, readFile as readFile31, writeFile as writeFile28 } from "node:fs/promises";
29746
+ import { access as access16, readFile as readFile31, writeFile as writeFile29 } from "node:fs/promises";
29465
29747
  import path90 from "node:path";
29466
29748
 
29467
29749
  // src/cli/scaffold/detect-backend.ts
29468
29750
  import { readdir as readdir4, readFile as readFile29 } from "node:fs/promises";
29469
29751
  import path88 from "node:path";
29470
- var DEFAULT_RUNTIME_VERSION = {
29752
+ var DEFAULT_RUNTIME_VERSION2 = {
29471
29753
  dotnet: "DOTNET_10",
29472
29754
  nodejs: "NODE_22_LTS",
29473
29755
  python: "PYTHON_312"
@@ -29494,18 +29776,18 @@ function dotnetVersionFromCsproj(content) {
29494
29776
  if (major === "8" || major === "9" || major === "10") {
29495
29777
  return `DOTNET_${major}`;
29496
29778
  }
29497
- return DEFAULT_RUNTIME_VERSION.dotnet;
29779
+ return DEFAULT_RUNTIME_VERSION2.dotnet;
29498
29780
  }
29499
29781
  function nodeVersionFromEngines(engines) {
29500
29782
  const range = engines && typeof engines === "object" && !Array.isArray(engines) ? engines.node : void 0;
29501
29783
  if (typeof range === "string" && /(^|[^\d])24/.test(range)) {
29502
29784
  return "NODE_24_LTS";
29503
29785
  }
29504
- return DEFAULT_RUNTIME_VERSION.nodejs;
29786
+ return DEFAULT_RUNTIME_VERSION2.nodejs;
29505
29787
  }
29506
29788
  function pythonVersionFromText(content) {
29507
29789
  const match = content?.match(/3\.(1[0-4])/);
29508
- return match ? `PYTHON_3${match[1]}` : DEFAULT_RUNTIME_VERSION.python;
29790
+ return match ? `PYTHON_3${match[1]}` : DEFAULT_RUNTIME_VERSION2.python;
29509
29791
  }
29510
29792
  function isRunnableNodePackage(pkg) {
29511
29793
  const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
@@ -29572,7 +29854,7 @@ import {
29572
29854
  readFile as readFile30,
29573
29855
  readlink,
29574
29856
  symlink as symlink2,
29575
- writeFile as writeFile27
29857
+ writeFile as writeFile28
29576
29858
  } from "node:fs/promises";
29577
29859
  import path89 from "node:path";
29578
29860
  var DOCS_DIR_WHITELIST = [
@@ -29809,7 +30091,7 @@ async function applyDocsNavigation(siteDirectory, routes) {
29809
30091
  force: false
29810
30092
  });
29811
30093
  const updated = ensureHeaderDocsNavigation(parsed, routes);
29812
- await writeFile27(configPath, `${JSON.stringify(updated, null, 2)}
30094
+ await writeFile28(configPath, `${JSON.stringify(updated, null, 2)}
29813
30095
  `, "utf8");
29814
30096
  }
29815
30097
  async function discoverAndWireDocs(siteDirectory) {
@@ -29993,7 +30275,7 @@ async function wireOpenApiIntoJsonConfig(siteDirectory, openApiPath) {
29993
30275
  });
29994
30276
  const backend = updated.backend && typeof updated.backend === "object" && !Array.isArray(updated.backend) ? { ...updated.backend } : {};
29995
30277
  updated.backend = { ...backend, openApiPath: input5 };
29996
- await writeFile28(
30278
+ await writeFile29(
29997
30279
  jsonConfigPath,
29998
30280
  `${JSON.stringify(updated, null, 2)}
29999
30281
  `,
@@ -30113,7 +30395,7 @@ async function discoverAndWireBackend(siteDirectory, mode) {
30113
30395
  runtime: detected.runtime,
30114
30396
  runtimeVersion: detected.runtimeVersion
30115
30397
  };
30116
- await writeFile28(
30398
+ await writeFile29(
30117
30399
  jsonConfigPath,
30118
30400
  `${JSON.stringify(siteConfig, null, 2)}
30119
30401
  `,
@@ -30678,7 +30960,12 @@ var ProjectCreateError = class extends ApitogoError {
30678
30960
  }
30679
30961
  };
30680
30962
  function modulesForConfig(config2) {
30681
- return config2.portal && config2.portal.enabled !== false ? ["dev-portal"] : [];
30963
+ const modules = [];
30964
+ if (config2.portal && config2.portal.enabled !== false)
30965
+ modules.push("dev-portal");
30966
+ if (config2.gateway && config2.gateway.enabled !== false && config2.gateway.origin?.mode === "managed")
30967
+ modules.push("apihub");
30968
+ return modules;
30682
30969
  }
30683
30970
  var hostsAnything = (config2) => config2.portal && config2.portal.enabled !== false || config2.gateway && config2.gateway.enabled !== false;
30684
30971
  async function createProjectV2(options, deps) {
@@ -31535,7 +31822,7 @@ async function readStdin(input5 = process.stdin) {
31535
31822
  }
31536
31823
  return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
31537
31824
  }
31538
- async function confirm2(question) {
31825
+ async function confirm(question) {
31539
31826
  const rl = createInterface6({ input: process.stdin, output: process.stdout });
31540
31827
  try {
31541
31828
  const answer = (await rl.question(question)).trim().toLowerCase();
@@ -31651,7 +31938,7 @@ async function secrets(argv) {
31651
31938
  isTerminal: process.stdin.isTTY === true && !json,
31652
31939
  askValue: (prompt) => readHidden(prompt),
31653
31940
  readPiped: () => readStdin(),
31654
- confirm: confirm2
31941
+ confirm
31655
31942
  });
31656
31943
  if (json) {
31657
31944
  console.log(JSON.stringify({ status: "succeeded", ...result }, null, 2));