@lukoweb/apitogo 0.2.31 → 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 (41) hide show
  1. package/dist/cli/cli.js +436 -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.34",
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
  {
@@ -14956,7 +15036,7 @@ function portalBuildStep(context, options = {}, build2 = runBuild) {
14956
15036
  await mkdir12(path48.dirname(buildDir), { recursive: true });
14957
15037
  await rename6(path48.join(root, "dist"), buildDir);
14958
15038
  if (prepared.publishedManifest) {
14959
- await writeFile14(
15039
+ await writeFile15(
14960
15040
  path48.join(buildDir, MANIFEST_FILE),
14961
15041
  `${JSON.stringify(prepared.publishedManifest, null, 2)}
14962
15042
  `
@@ -15970,6 +16050,56 @@ async function installMissingPackages(dir, config2, version, options) {
15970
16050
  return { status: "installed", packages: missing, command: shown };
15971
16051
  }
15972
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
+
15973
16103
  // src/cli/services/handler.ts
15974
16104
  var list = (value) => {
15975
16105
  const items = (Array.isArray(value) ? value : value === void 0 ? [] : [value]).filter(
@@ -16024,15 +16154,6 @@ function addOptionsFromArguments(service, argv) {
16024
16154
  };
16025
16155
  }
16026
16156
  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
16157
  function describe(edit, dir) {
16037
16158
  return {
16038
16159
  service: edit.service,
@@ -16044,6 +16165,7 @@ function describe(edit, dir) {
16044
16165
  }
16045
16166
  async function run(action, argv, plan2, deps = {}) {
16046
16167
  const json = argv.json === true;
16168
+ const asker = argv.yes === true ? void 0 : deps.asker ?? (canPrompt2(json) ? createAsker() : void 0);
16047
16169
  try {
16048
16170
  if (!isServiceName(argv.service)) {
16049
16171
  throw new ServiceEditError(
@@ -16058,7 +16180,28 @@ async function run(action, argv, plan2, deps = {}) {
16058
16180
  "Create one with `apitogo init`."
16059
16181
  );
16060
16182
  }
16061
- 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
+ }
16062
16205
  const needed = action === "add" ? missingPackages(dir, packagesForConfig(edit.config)) : [];
16063
16206
  if (!json) {
16064
16207
  for (const line2 of formatServiceEdit(edit))
@@ -16079,13 +16222,13 @@ async function run(action, argv, plan2, deps = {}) {
16079
16222
  return;
16080
16223
  }
16081
16224
  if (argv.yes !== true) {
16082
- if (!canPrompt2(json)) {
16225
+ if (!asker) {
16083
16226
  throw new ServiceEditError(
16084
16227
  "This is not an interactive terminal.",
16085
16228
  "Pass --yes to apply the change, or --dry-run to only see it."
16086
16229
  );
16087
16230
  }
16088
- if (!await confirm("Apply this change? (y/N) ")) {
16231
+ if (!await asker.confirm("Apply this change?")) {
16089
16232
  printResultToConsole("Cancelled. Nothing was written.");
16090
16233
  return;
16091
16234
  }
@@ -16159,7 +16302,10 @@ async function installPackages(dir, edit, argv, deps, json) {
16159
16302
  var addService = (argv, deps = {}) => run(
16160
16303
  "add",
16161
16304
  argv,
16162
- (service) => planAddService(service, addOptionsFromArguments(service, argv)),
16305
+ (service, ask4) => planAddService(service, {
16306
+ ...addOptionsFromArguments(service, argv),
16307
+ ask: ask4
16308
+ }),
16163
16309
  deps
16164
16310
  );
16165
16311
  var removeService = (argv) => run(
@@ -16518,7 +16664,7 @@ function formatDevPortalBackendResponse(projectId, backend) {
16518
16664
  // src/cli/platform/helpers/dev-portal-manifest.ts
16519
16665
  init_gateway_limits();
16520
16666
  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";
16667
+ import { access as access6, mkdir as mkdir13, readFile as readFile17, writeFile as writeFile16 } from "node:fs/promises";
16522
16668
  import path57 from "node:path";
16523
16669
 
16524
16670
  // src/cli/make-config/json-config.ts
@@ -17838,7 +17984,7 @@ async function writeManifest2(siteDirectory, manifest) {
17838
17984
  ...safeManifest
17839
17985
  } = manifest;
17840
17986
  config2 = applyManifestToConfig(config2, safeManifest);
17841
- await writeFile15(filePath, `${JSON.stringify(config2, null, 2)}
17987
+ await writeFile16(filePath, `${JSON.stringify(config2, null, 2)}
17842
17988
  `, "utf8");
17843
17989
  return filePath;
17844
17990
  }
@@ -17850,7 +17996,7 @@ import {
17850
17996
  mkdir as mkdir14,
17851
17997
  readFile as readFile18,
17852
17998
  unlink,
17853
- writeFile as writeFile16
17999
+ writeFile as writeFile17
17854
18000
  } from "node:fs/promises";
17855
18001
  import path58 from "node:path";
17856
18002
  var ENV_FILENAME = ".env";
@@ -18125,7 +18271,7 @@ async function writeScaffoldEnv(siteDirectory) {
18125
18271
  await access7(envPath);
18126
18272
  } catch {
18127
18273
  await mkdir14(resolvedDirectory, { recursive: true });
18128
- await writeFile16(envPath, createEnvContent(), "utf8");
18274
+ await writeFile17(envPath, createEnvContent(), "utf8");
18129
18275
  filesWritten.push(ENV_FILENAME);
18130
18276
  }
18131
18277
  for (const legacyName of [".env.example"]) {
@@ -18154,7 +18300,7 @@ async function writeScaffoldEnv(siteDirectory) {
18154
18300
  if (!hasEntry) {
18155
18301
  const suffix = gitignore.length === 0 ? "" : gitignore.endsWith("\n") ? "" : "\n";
18156
18302
  if (gitignore.length === 0) {
18157
- await writeFile16(gitignorePath, `${entry}
18303
+ await writeFile17(gitignorePath, `${entry}
18158
18304
  `, "utf8");
18159
18305
  } else {
18160
18306
  await appendFile(gitignorePath, `${suffix}${entry}
@@ -18938,7 +19084,7 @@ var check_default = {
18938
19084
 
18939
19085
  // src/cli/make-config/handler.ts
18940
19086
  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";
19087
+ import { access as access12, mkdir as mkdir17, readFile as readFile24, writeFile as writeFile22 } from "node:fs/promises";
18942
19088
  import path71 from "node:path";
18943
19089
  import { stdin as input2 } from "node:process";
18944
19090
  import { glob as glob8 } from "glob";
@@ -19078,12 +19224,12 @@ function withDefaultIncludedActionKeys(plan2, actionKeys) {
19078
19224
  }
19079
19225
 
19080
19226
  // 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";
19227
+ import { access as access8, readFile as readFile20, rename as rename7, rm as rm9, writeFile as writeFile18 } from "node:fs/promises";
19082
19228
  import path63 from "node:path";
19083
19229
  async function writeConfigAtomically(filePath, content) {
19084
19230
  const temporaryPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
19085
19231
  try {
19086
- await writeFile17(temporaryPath, content, "utf8");
19232
+ await writeFile18(temporaryPath, content, "utf8");
19087
19233
  await rename7(temporaryPath, filePath);
19088
19234
  } catch (error) {
19089
19235
  await rm9(temporaryPath, { force: true });
@@ -19855,7 +20001,7 @@ async function wireNewDevPortalPageInConfig(sourceDirectory, slug, label2) {
19855
20001
  }
19856
20002
 
19857
20003
  // src/cli/scaffold/wire-openapi.ts
19858
- 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";
19859
20005
  import path68 from "node:path";
19860
20006
 
19861
20007
  // src/cli/platform/helpers/openapi.ts
@@ -20059,7 +20205,7 @@ async function validateOpenApiFile(filePath) {
20059
20205
  }
20060
20206
 
20061
20207
  // src/cli/scaffold/scaffold-dev-portal-site.ts
20062
- 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";
20063
20209
  import path67 from "node:path";
20064
20210
 
20065
20211
  // src/cli/scaffold/apitogo-npm-packages.ts
@@ -20471,7 +20617,7 @@ async function ensureAccountPlansPage(outputDirectory) {
20471
20617
  return void 0;
20472
20618
  } catch {
20473
20619
  await mkdir15(path67.dirname(pagePath), { recursive: true });
20474
- await writeFile18(pagePath, ACCOUNT_PLANS_PAGE);
20620
+ await writeFile19(pagePath, ACCOUNT_PLANS_PAGE);
20475
20621
  return ACCOUNT_PLANS_PAGE_PATH;
20476
20622
  }
20477
20623
  }
@@ -20482,7 +20628,7 @@ async function ensureDashboardPage(outputDirectory) {
20482
20628
  return void 0;
20483
20629
  } catch {
20484
20630
  await mkdir15(path67.dirname(pagePath), { recursive: true });
20485
- await writeFile18(pagePath, DASHBOARD_PAGE);
20631
+ await writeFile19(pagePath, DASHBOARD_PAGE);
20486
20632
  return DASHBOARD_PAGE_PATH;
20487
20633
  }
20488
20634
  }
@@ -20541,7 +20687,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
20541
20687
  const packageJsonPath = path67.join(outputDirectory, "package.json");
20542
20688
  if (options.patchPackageJson !== false) {
20543
20689
  const packageJson3 = await readFile21(packageJsonPath, "utf8");
20544
- await writeFile18(
20690
+ await writeFile19(
20545
20691
  packageJsonPath,
20546
20692
  rewritePackageJson(packageJson3, {
20547
20693
  siteName: options.siteName,
@@ -20553,7 +20699,7 @@ async function patchScaffoldedSite(outputDirectory, options) {
20553
20699
  }
20554
20700
  const configPath = path67.join(outputDirectory, "apitogo.config.tsx");
20555
20701
  const configContent = await readFile21(configPath, "utf8");
20556
- await writeFile18(
20702
+ await writeFile19(
20557
20703
  configPath,
20558
20704
  patchDashboardPageConfig(
20559
20705
  patchAccountPlansPageConfig(
@@ -20665,7 +20811,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20665
20811
  input: input5,
20666
20812
  apiPath: "api"
20667
20813
  });
20668
- await writeFile19(
20814
+ await writeFile20(
20669
20815
  jsonConfigPath,
20670
20816
  `${JSON.stringify(updated, null, 2)}
20671
20817
  `,
@@ -20676,7 +20822,7 @@ async function wireDevPortalOpenApi(sourceDirectory, openApiPath) {
20676
20822
  const configContent = await readFile22(tsxConfigPath, "utf8");
20677
20823
  try {
20678
20824
  const patched = patchApitogoConfigOpenApiToFile(configContent, input5);
20679
- await writeFile19(tsxConfigPath, patched);
20825
+ await writeFile20(tsxConfigPath, patched);
20680
20826
  } catch (error) {
20681
20827
  return {
20682
20828
  status: "failed",
@@ -21029,7 +21175,7 @@ export default function ${componentName}() {
21029
21175
  // src/cli/make-config/scaffold-project.ts
21030
21176
  init_package_json();
21031
21177
  import { constants } from "node:fs";
21032
- 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";
21033
21179
  import path70 from "node:path";
21034
21180
  import { glob as glob7 } from "glob";
21035
21181
 
@@ -21107,7 +21253,7 @@ async function ensurePackageJson2(dir) {
21107
21253
  typescript: "^5"
21108
21254
  }
21109
21255
  };
21110
- await writeFile20(pkgPath, `${JSON.stringify(pkg2, null, 2)}
21256
+ await writeFile21(pkgPath, `${JSON.stringify(pkg2, null, 2)}
21111
21257
  `, "utf8");
21112
21258
  return true;
21113
21259
  }
@@ -21134,7 +21280,7 @@ async function ensurePackageJson2(dir) {
21134
21280
  for (const [k, v] of Object.entries(baseDeps)) {
21135
21281
  if (!deps[k]) deps[k] = v;
21136
21282
  }
21137
- await writeFile20(pkgPath, `${JSON.stringify(pkg, null, 2)}
21283
+ await writeFile21(pkgPath, `${JSON.stringify(pkg, null, 2)}
21138
21284
  `, "utf8");
21139
21285
  return false;
21140
21286
  }
@@ -21169,7 +21315,7 @@ async function ensureTsconfigJson(dir) {
21169
21315
  "user-panel"
21170
21316
  ]
21171
21317
  };
21172
- await writeFile20(tsPath, `${JSON.stringify(tsconfig, null, 2)}
21318
+ await writeFile21(tsPath, `${JSON.stringify(tsconfig, null, 2)}
21173
21319
  `, "utf8");
21174
21320
  return true;
21175
21321
  }
@@ -21191,17 +21337,17 @@ async function ensurePortalStubFiles(dir) {
21191
21337
  const landingPath = path70.join(dir, "landing", "LandingPage.tsx");
21192
21338
  if (!await fileExists2(landingPath)) {
21193
21339
  await mkdir16(path70.join(dir, "landing"), { recursive: true });
21194
- await writeFile20(landingPath, LANDING_PAGE_SOURCE, "utf8");
21340
+ await writeFile21(landingPath, LANDING_PAGE_SOURCE, "utf8");
21195
21341
  }
21196
21342
  const plansPath = path70.join(dir, "user-panel", "AccountPlansPage.tsx");
21197
21343
  if (!await fileExists2(plansPath)) {
21198
21344
  await mkdir16(path70.join(dir, "user-panel"), { recursive: true });
21199
- await writeFile20(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
21345
+ await writeFile21(plansPath, ACCOUNT_PLANS_PAGE_SOURCE, "utf8");
21200
21346
  }
21201
21347
  const dashboardPath = path70.join(dir, "user-panel", "DashboardPage.tsx");
21202
21348
  if (!await fileExists2(dashboardPath)) {
21203
21349
  await mkdir16(path70.join(dir, "user-panel"), { recursive: true });
21204
- await writeFile20(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
21350
+ await writeFile21(dashboardPath, DASHBOARD_PAGE_SOURCE, "utf8");
21205
21351
  }
21206
21352
  await ensurePublicBrandingFiles(dir);
21207
21353
  }
@@ -21213,11 +21359,11 @@ async function ensurePublicBrandingFiles(dir) {
21213
21359
  if (await fileExists2(dest)) {
21214
21360
  continue;
21215
21361
  }
21216
- await writeFile20(dest, content, "utf8");
21362
+ await writeFile21(dest, content, "utf8");
21217
21363
  }
21218
21364
  const faviconDest = path70.join(destDir, "Icon.png");
21219
21365
  if (!await fileExists2(faviconDest)) {
21220
- await writeFile20(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
21366
+ await writeFile21(faviconDest, Buffer.from(FAVICON_PNG_BASE64, "base64"));
21221
21367
  }
21222
21368
  }
21223
21369
  async function ensureThinConfigTsx(dir) {
@@ -21225,7 +21371,7 @@ async function ensureThinConfigTsx(dir) {
21225
21371
  if (await fileExists2(tsxPath)) {
21226
21372
  return false;
21227
21373
  }
21228
- await writeFile20(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
21374
+ await writeFile21(tsxPath, THIN_WRAPPER_SOURCE, "utf8");
21229
21375
  return true;
21230
21376
  }
21231
21377
 
@@ -21286,13 +21432,13 @@ async function persistConfig(config2, ctx) {
21286
21432
  force: ctx.force
21287
21433
  })
21288
21434
  };
21289
- await writeFile21(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
21435
+ await writeFile22(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
21290
21436
  `, "utf8");
21291
21437
  const tsxPath = path71.join(ctx.dir, "apitogo.config.tsx");
21292
21438
  if (await fileExists3(tsxPath) && tsxRoutes.length > 0) {
21293
21439
  const tsxSource = await readFile24(tsxPath, "utf8");
21294
21440
  if (isManagedConfigWrapper(tsxSource)) {
21295
- await writeFile21(
21441
+ await writeFile22(
21296
21442
  tsxPath,
21297
21443
  buildThinWrapperWithCustomPages(tsxRoutes),
21298
21444
  "utf8"
@@ -21396,7 +21542,7 @@ async function makeConfig(argv) {
21396
21542
  if (action2.kind === "page") {
21397
21543
  const pagePath = path71.join(dir, action2.page.relativePath);
21398
21544
  await mkdir17(path71.dirname(pagePath), { recursive: true });
21399
- await writeFile21(
21545
+ await writeFile22(
21400
21546
  pagePath,
21401
21547
  buildStarterDocsPageTsx(action2.page.slug, action2.page.title),
21402
21548
  "utf8"
@@ -21505,7 +21651,7 @@ async function makeConfig(argv) {
21505
21651
  argv.pageContent,
21506
21652
  pageTemplate
21507
21653
  );
21508
- await writeFile21(filePath, tsx, "utf8");
21654
+ await writeFile22(filePath, tsx, "utf8");
21509
21655
  pageResult = {
21510
21656
  action: "updated",
21511
21657
  slug,
@@ -21525,7 +21671,7 @@ async function makeConfig(argv) {
21525
21671
  pageTemplate
21526
21672
  );
21527
21673
  await mkdir17(path71.dirname(filePath), { recursive: true });
21528
- await writeFile21(filePath, tsx, "utf8");
21674
+ await writeFile22(filePath, tsx, "utf8");
21529
21675
  const configResult = await wireNewDevPortalPageInConfig(
21530
21676
  dir,
21531
21677
  slug,
@@ -21874,9 +22020,10 @@ var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set([
21874
22020
  "bin",
21875
22021
  "obj"
21876
22022
  ]);
21877
- async function loadGitignore(rootDir) {
22023
+ async function loadGitignore(rootDir, extraExcludes = []) {
21878
22024
  const ig = ignore();
21879
22025
  ig.add(["node_modules/", ".git/", "dist/", "bin/", "obj/"]);
22026
+ if (extraExcludes.length > 0) ig.add([...extraExcludes]);
21880
22027
  const gitignorePath = path72.join(rootDir, ".gitignore");
21881
22028
  try {
21882
22029
  const content = await fs7.readFile(gitignorePath, "utf8");
@@ -21904,13 +22051,13 @@ async function collectFiles(rootDir, relativeDir, ig, files) {
21904
22051
  }
21905
22052
  }
21906
22053
  }
21907
- async function collectPackageableFiles(sourceDirectory) {
22054
+ async function collectPackageableFiles(sourceDirectory, extraExcludes = []) {
21908
22055
  const resolved = path72.resolve(sourceDirectory);
21909
22056
  const stat7 = await fs7.stat(resolved).catch(() => null);
21910
22057
  if (!stat7?.isDirectory()) {
21911
22058
  throw new Error(`sourceDirectory is not a directory: ${resolved}`);
21912
22059
  }
21913
- const ig = await loadGitignore(resolved);
22060
+ const ig = await loadGitignore(resolved, extraExcludes);
21914
22061
  const files = [];
21915
22062
  await collectFiles(resolved, "", ig, files);
21916
22063
  if (files.length === 0) {
@@ -21930,9 +22077,9 @@ async function sha256File(filePath) {
21930
22077
  });
21931
22078
  return hash2.digest("hex");
21932
22079
  }
21933
- async function hashDirectoryContents(rootDirectory) {
22080
+ async function hashDirectoryContents(rootDirectory, extraExcludes = []) {
21934
22081
  const resolved = path73.resolve(rootDirectory);
21935
- const files = (await collectPackageableFiles(resolved)).sort(
22082
+ const files = (await collectPackageableFiles(resolved, extraExcludes)).sort(
21936
22083
  (a, b) => a.localeCompare(b)
21937
22084
  );
21938
22085
  const hash2 = createHash3("sha256");
@@ -22683,7 +22830,7 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
22683
22830
  }
22684
22831
 
22685
22832
  // src/cli/platform/helpers/openapi-gateway-servers.ts
22686
- 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";
22687
22834
  import path74 from "node:path";
22688
22835
  import YAML3 from "yaml";
22689
22836
  var GATEWAY_URL_ENV_KEY = "APITOGO_GATEWAY_URL";
@@ -22731,7 +22878,7 @@ async function materializePublishOpenApiWithGateway(siteDirectory, sourceOpenApi
22731
22878
  const doc = await readOpenApiDocument(source);
22732
22879
  const rewritten = withGatewayServers(doc, gatewayUrl);
22733
22880
  await mkdir18(path74.dirname(outputPath), { recursive: true });
22734
- await writeFile22(
22881
+ await writeFile23(
22735
22882
  outputPath,
22736
22883
  `${JSON.stringify(rewritten, null, 2)}
22737
22884
  `,
@@ -22757,7 +22904,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22757
22904
  const backend = config2.backend && typeof config2.backend === "object" ? { ...config2.backend } : {};
22758
22905
  backend.openApiPath = relativeInput;
22759
22906
  config2.backend = backend;
22760
- await writeFile22(configPath, `${JSON.stringify(config2, null, 2)}
22907
+ await writeFile23(configPath, `${JSON.stringify(config2, null, 2)}
22761
22908
  `, "utf8");
22762
22909
  return {
22763
22910
  restore: async () => {
@@ -22773,7 +22920,7 @@ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
22773
22920
  } else {
22774
22921
  current.backend = previousBackend;
22775
22922
  }
22776
- await writeFile22(
22923
+ await writeFile23(
22777
22924
  configPath,
22778
22925
  `${JSON.stringify(current, null, 2)}
22779
22926
  `,
@@ -22870,7 +23017,7 @@ function collectManifestPublicActionKeys(gateway) {
22870
23017
  }
22871
23018
 
22872
23019
  // src/cli/platform/helpers/publish-ledger.ts
22873
- 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";
22874
23021
  import path75 from "node:path";
22875
23022
  var PUBLISH_LEDGER_RELATIVE_PATH = ".apitogo/publish-state.json";
22876
23023
  function publishLedgerPath(siteDirectory) {
@@ -22897,7 +23044,7 @@ async function writePublishLedger(siteDirectory, ledger) {
22897
23044
  version: 1,
22898
23045
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
22899
23046
  };
22900
- await writeFile23(filePath, `${JSON.stringify(next, null, 2)}
23047
+ await writeFile24(filePath, `${JSON.stringify(next, null, 2)}
22901
23048
  `, "utf8");
22902
23049
  return filePath;
22903
23050
  }
@@ -23531,9 +23678,9 @@ import { createReadStream as createReadStream2, createWriteStream as createWrite
23531
23678
  import { tmpdir as tmpdir2 } from "node:os";
23532
23679
  import path77 from "node:path";
23533
23680
  import { ZipArchive } from "archiver";
23534
- async function packageBackendDirectory(sourceDirectory) {
23681
+ async function packageBackendDirectory(sourceDirectory, extraExcludes = []) {
23535
23682
  const resolved = path77.resolve(sourceDirectory);
23536
- const files = await collectPackageableFiles(resolved);
23683
+ const files = await collectPackageableFiles(resolved, extraExcludes);
23537
23684
  const zipPath = path77.join(
23538
23685
  tmpdir2(),
23539
23686
  `apitogo-backend-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`
@@ -25058,7 +25205,7 @@ async function resolveProjectIdForPublish(client, config2, args, wait, progress,
25058
25205
  import path81 from "node:path";
25059
25206
 
25060
25207
  // 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";
25208
+ import { readdir as readdir3, readFile as readFile27, writeFile as writeFile25 } from "node:fs/promises";
25062
25209
  import path78 from "node:path";
25063
25210
  var SITE_API_BASE_URL_ENV_KEY = "VITE_APITOGO_AUTHENTICATION_API_BASE_URL";
25064
25211
  var PLACEHOLDER_HOST_MARKERS = [
@@ -25120,13 +25267,13 @@ async function patchSiteApiBaseUrl(sourceDirectory, apiBaseUrl) {
25120
25267
  ${line2}
25121
25268
  ` : `${line2}
25122
25269
  `;
25123
- await writeFile24(envPath, patched);
25270
+ await writeFile25(envPath, patched);
25124
25271
  patchedExisting = true;
25125
25272
  } catch {
25126
25273
  }
25127
25274
  }
25128
25275
  if (!patchedExisting) {
25129
- await writeFile24(path78.join(resolved, ".env"), `${line2}
25276
+ await writeFile25(path78.join(resolved, ".env"), `${line2}
25130
25277
  `);
25131
25278
  }
25132
25279
  }
@@ -27344,19 +27491,11 @@ var GatewayDeployError = class extends ApitogoError {
27344
27491
  };
27345
27492
  var sha256 = (text) => createHash4("sha256").update(text).digest("hex");
27346
27493
  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
27494
  if (gateway.specs.length > 1)
27350
27495
  return "the platform takes one OpenAPI file per gateway, but the config lists several";
27351
27496
  return void 0;
27352
27497
  }
27353
27498
  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
27499
  if (gateway.specs.length > 1)
27361
27500
  throw new GatewayDeployError(
27362
27501
  "GATEWAY_MULTIPLE_SPECS",
@@ -27403,6 +27542,37 @@ async function gatewayDeployInputs(resolved, projectId) {
27403
27542
  };
27404
27543
  }
27405
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
+ }
27406
27576
  function describePlatformProblem(error) {
27407
27577
  if (!(error instanceof PlatformApiError) || !error.problem) return [];
27408
27578
  const errors = error.problem.errors;
@@ -27438,11 +27608,73 @@ function rejected(what, error) {
27438
27608
  async function deployGateway(resolved, projectId, deps, options = {}) {
27439
27609
  const { gateway, spec, body, specHash, configHash } = await gatewayDeployInputs(resolved, projectId);
27440
27610
  assertDeployable(gateway);
27611
+ const backend = await backendDeployInputs(gateway, projectId);
27441
27612
  const client = deps.client;
27442
27613
  const sleep5 = deps.sleep ?? defaultSleep;
27443
27614
  const now = deps.now ?? Date.now;
27444
27615
  const timeoutMs = deps.reconcileTimeoutMs ?? 18e4;
27616
+ const backendTimeoutMs = deps.backendTimeoutMs ?? 20 * 6e4;
27445
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
+ };
27446
27678
  const specStep = {
27447
27679
  name: "spec",
27448
27680
  hash: () => specHash,
@@ -27522,12 +27754,30 @@ async function deployGateway(resolved, projectId, deps, options = {}) {
27522
27754
  };
27523
27755
  }
27524
27756
  };
27525
- const outcomes = await runPipeline(
27526
- resolved.state,
27527
- "gateway",
27528
- [specStep, configStep, reconcileStep],
27529
- { force: options.force }
27530
- );
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
+ }
27531
27781
  const reconcile = outcomes.find((outcome) => outcome.step === "reconcile");
27532
27782
  const ran = reconcile !== void 0 && !reconcile.skipped;
27533
27783
  const known = (await readManifest(resolved.state))?.services.gateway;
@@ -28642,7 +28892,7 @@ async function planGatewayService(resolved, ledger, dryRun) {
28642
28892
  details: []
28643
28893
  };
28644
28894
  const details = [
28645
- `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)"}`,
28646
28896
  ...gateway.auth ? [`auth: ${gateway.auth.mode}`] : ["auth: none"],
28647
28897
  ...describePolicies(gateway)
28648
28898
  ];
@@ -28695,8 +28945,27 @@ async function planGatewayService(resolved, ledger, dryRun) {
28695
28945
  );
28696
28946
  }
28697
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
+ }
28698
28966
  const step2 = (name) => stepOf(ledger, "gateway", name);
28699
28967
  const hashes = {
28968
+ backend: backendHash ?? "",
28700
28969
  spec: inputs.specHash,
28701
28970
  config: inputs.configHash,
28702
28971
  reconcile: inputs.configHash
@@ -28712,9 +28981,10 @@ async function planGatewayService(resolved, ledger, dryRun) {
28712
28981
  const lines = [
28713
28982
  `step spec (upload the OpenAPI file): ${state("spec")}`,
28714
28983
  `step config (send the wanted state): ${state("config")}`,
28984
+ ...backendHash ? [`step backend (upload and start your code): ${state("backend")}`] : [],
28715
28985
  `step reconcile (wait until the platform applied it): ${state("reconcile")}`
28716
28986
  ];
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;
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;
28718
28988
  if (issues.length > 0)
28719
28989
  return {
28720
28990
  ...base,
@@ -28795,7 +29065,7 @@ function corsLines(platform) {
28795
29065
  ];
28796
29066
  }
28797
29067
  async function gatewayStatus(resolved, ledger, manifest, remote) {
28798
- const steps = ["spec", "config", "reconcile"].map((name) => ({
29068
+ const steps = ["spec", "config", "backend", "reconcile"].map((name) => ({
28799
29069
  name,
28800
29070
  record: stepOf(ledger, "gateway", name)
28801
29071
  }));
@@ -28813,6 +29083,14 @@ async function gatewayStatus(resolved, ledger, manifest, remote) {
28813
29083
  if (remote && projectId) {
28814
29084
  try {
28815
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
+ }
28816
29094
  details.push(
28817
29095
  `platform state: ${platform.status}`,
28818
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"})`,
@@ -28950,7 +29228,7 @@ function formatStatus(output4) {
28950
29228
  }
28951
29229
 
28952
29230
  // src/cli/deploy/static-web-config.ts
28953
- import { access as access14, writeFile as writeFile25 } from "node:fs/promises";
29231
+ import { access as access14, writeFile as writeFile26 } from "node:fs/promises";
28954
29232
  import path84 from "node:path";
28955
29233
  var STATIC_WEB_CONFIG_FILE = "staticwebapp.config.json";
28956
29234
  var STATIC_WEB_CONFIG_VERSION = 1;
@@ -28969,7 +29247,7 @@ async function ensureStaticWebConfig(buildDir) {
28969
29247
  await access14(file);
28970
29248
  return false;
28971
29249
  } catch {
28972
- await writeFile25(
29250
+ await writeFile26(
28973
29251
  file,
28974
29252
  `${JSON.stringify(DEFAULT_STATIC_WEB_CONFIG, null, 2)}
28975
29253
  `,
@@ -29118,7 +29396,7 @@ async function deployGatewayService(options, deps, projectId, named) {
29118
29396
  return { service: "gateway", status: "skipped", reason: why, steps: [] };
29119
29397
  }
29120
29398
  const client = deps.client;
29121
- 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))
29122
29400
  throw new Error("The platform client has no gateway calls.");
29123
29401
  const result = await deployGateway(
29124
29402
  resolved,
@@ -29366,7 +29644,7 @@ var deploy_default = {
29366
29644
  };
29367
29645
 
29368
29646
  // src/cli/export/handler.ts
29369
- import { mkdir as mkdir20, writeFile as writeFile26 } from "node:fs/promises";
29647
+ import { mkdir as mkdir20, writeFile as writeFile27 } from "node:fs/promises";
29370
29648
  import path87 from "node:path";
29371
29649
  init_invariant();
29372
29650
  var ExportError = class extends ApitogoError {
@@ -29408,7 +29686,7 @@ async function exportOpenApi(args, deps) {
29408
29686
  if (!args.out) return { text };
29409
29687
  const file = path87.resolve(process.cwd(), args.out);
29410
29688
  await mkdir20(path87.dirname(file), { recursive: true });
29411
- await writeFile26(file, text);
29689
+ await writeFile27(file, text);
29412
29690
  return { text, file };
29413
29691
  }
29414
29692
  async function exportCommand(args) {
@@ -29461,13 +29739,13 @@ var export_default = {
29461
29739
  import path91 from "node:path";
29462
29740
 
29463
29741
  // src/cli/scaffold/run-scaffold-discovery.ts
29464
- 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";
29465
29743
  import path90 from "node:path";
29466
29744
 
29467
29745
  // src/cli/scaffold/detect-backend.ts
29468
29746
  import { readdir as readdir4, readFile as readFile29 } from "node:fs/promises";
29469
29747
  import path88 from "node:path";
29470
- var DEFAULT_RUNTIME_VERSION = {
29748
+ var DEFAULT_RUNTIME_VERSION2 = {
29471
29749
  dotnet: "DOTNET_10",
29472
29750
  nodejs: "NODE_22_LTS",
29473
29751
  python: "PYTHON_312"
@@ -29494,18 +29772,18 @@ function dotnetVersionFromCsproj(content) {
29494
29772
  if (major === "8" || major === "9" || major === "10") {
29495
29773
  return `DOTNET_${major}`;
29496
29774
  }
29497
- return DEFAULT_RUNTIME_VERSION.dotnet;
29775
+ return DEFAULT_RUNTIME_VERSION2.dotnet;
29498
29776
  }
29499
29777
  function nodeVersionFromEngines(engines) {
29500
29778
  const range = engines && typeof engines === "object" && !Array.isArray(engines) ? engines.node : void 0;
29501
29779
  if (typeof range === "string" && /(^|[^\d])24/.test(range)) {
29502
29780
  return "NODE_24_LTS";
29503
29781
  }
29504
- return DEFAULT_RUNTIME_VERSION.nodejs;
29782
+ return DEFAULT_RUNTIME_VERSION2.nodejs;
29505
29783
  }
29506
29784
  function pythonVersionFromText(content) {
29507
29785
  const match = content?.match(/3\.(1[0-4])/);
29508
- return match ? `PYTHON_3${match[1]}` : DEFAULT_RUNTIME_VERSION.python;
29786
+ return match ? `PYTHON_3${match[1]}` : DEFAULT_RUNTIME_VERSION2.python;
29509
29787
  }
29510
29788
  function isRunnableNodePackage(pkg) {
29511
29789
  const scripts = pkg.scripts && typeof pkg.scripts === "object" ? pkg.scripts : {};
@@ -29572,7 +29850,7 @@ import {
29572
29850
  readFile as readFile30,
29573
29851
  readlink,
29574
29852
  symlink as symlink2,
29575
- writeFile as writeFile27
29853
+ writeFile as writeFile28
29576
29854
  } from "node:fs/promises";
29577
29855
  import path89 from "node:path";
29578
29856
  var DOCS_DIR_WHITELIST = [
@@ -29809,7 +30087,7 @@ async function applyDocsNavigation(siteDirectory, routes) {
29809
30087
  force: false
29810
30088
  });
29811
30089
  const updated = ensureHeaderDocsNavigation(parsed, routes);
29812
- await writeFile27(configPath, `${JSON.stringify(updated, null, 2)}
30090
+ await writeFile28(configPath, `${JSON.stringify(updated, null, 2)}
29813
30091
  `, "utf8");
29814
30092
  }
29815
30093
  async function discoverAndWireDocs(siteDirectory) {
@@ -29993,7 +30271,7 @@ async function wireOpenApiIntoJsonConfig(siteDirectory, openApiPath) {
29993
30271
  });
29994
30272
  const backend = updated.backend && typeof updated.backend === "object" && !Array.isArray(updated.backend) ? { ...updated.backend } : {};
29995
30273
  updated.backend = { ...backend, openApiPath: input5 };
29996
- await writeFile28(
30274
+ await writeFile29(
29997
30275
  jsonConfigPath,
29998
30276
  `${JSON.stringify(updated, null, 2)}
29999
30277
  `,
@@ -30113,7 +30391,7 @@ async function discoverAndWireBackend(siteDirectory, mode) {
30113
30391
  runtime: detected.runtime,
30114
30392
  runtimeVersion: detected.runtimeVersion
30115
30393
  };
30116
- await writeFile28(
30394
+ await writeFile29(
30117
30395
  jsonConfigPath,
30118
30396
  `${JSON.stringify(siteConfig, null, 2)}
30119
30397
  `,
@@ -30678,7 +30956,12 @@ var ProjectCreateError = class extends ApitogoError {
30678
30956
  }
30679
30957
  };
30680
30958
  function modulesForConfig(config2) {
30681
- 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;
30682
30965
  }
30683
30966
  var hostsAnything = (config2) => config2.portal && config2.portal.enabled !== false || config2.gateway && config2.gateway.enabled !== false;
30684
30967
  async function createProjectV2(options, deps) {
@@ -31535,7 +31818,7 @@ async function readStdin(input5 = process.stdin) {
31535
31818
  }
31536
31819
  return Buffer.concat(chunks).toString("utf8").replace(/\r?\n$/, "");
31537
31820
  }
31538
- async function confirm2(question) {
31821
+ async function confirm(question) {
31539
31822
  const rl = createInterface6({ input: process.stdin, output: process.stdout });
31540
31823
  try {
31541
31824
  const answer = (await rl.question(question)).trim().toLowerCase();
@@ -31651,7 +31934,7 @@ async function secrets(argv) {
31651
31934
  isTerminal: process.stdin.isTTY === true && !json,
31652
31935
  askValue: (prompt) => readHidden(prompt),
31653
31936
  readPiped: () => readStdin(),
31654
- confirm: confirm2
31937
+ confirm
31655
31938
  });
31656
31939
  if (json) {
31657
31940
  console.log(JSON.stringify({ status: "succeeded", ...result }, null, 2));