@lukoweb/apitogo 0.1.119 → 0.1.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/cli.js CHANGED
@@ -5802,7 +5802,7 @@ import {
5802
5802
  // package.json
5803
5803
  var package_default = {
5804
5804
  name: "@lukoweb/apitogo",
5805
- version: "0.1.119",
5805
+ version: "0.1.120",
5806
5806
  type: "module",
5807
5807
  sideEffects: [
5808
5808
  "**/*.css",
@@ -11432,9 +11432,7 @@ function loadConfig() {
11432
11432
  apiSecret = stored?.apiSecret;
11433
11433
  }
11434
11434
  if (!apiKeyId || !apiSecret) {
11435
- throw new Error(
11436
- "No credentials found. Run `apitogo login`, or set APITOGO_API_KEY_ID and APITOGO_API_SECRET for CI."
11437
- );
11435
+ throw new Error("No credentials found. Run `apitogo login`.");
11438
11436
  }
11439
11437
  const defaultProjectId = process.env.APITOGO_DEFAULT_PROJECT_ID?.trim() || void 0;
11440
11438
  const devPortalDeployUrl = process.env.APITOGO_DEV_PORTAL_DEPLOY_URL?.trim() || process.env.APITOGO_DOC_DEPLOY_URL?.trim() || DEFAULT_DEV_PORTAL_DEPLOY_URL;
@@ -11558,6 +11556,7 @@ function formatDevPortalBackendResponse(projectId, backend) {
11558
11556
  }
11559
11557
 
11560
11558
  // src/cli/platform/helpers/dev-portal-manifest.ts
11559
+ init_gateway_limits();
11561
11560
  import { existsSync as existsSync4 } from "node:fs";
11562
11561
  import { access, mkdir as mkdir6, readFile as readFile6, writeFile as writeFile7 } from "node:fs/promises";
11563
11562
  import path36 from "node:path";
@@ -11613,7 +11612,6 @@ function readLandingFromConfig(config2) {
11613
11612
  }
11614
11613
 
11615
11614
  // src/cli/platform/helpers/dev-portal-manifest.ts
11616
- init_gateway_limits();
11617
11615
  var APITOGO_CONFIG_FILENAME2 = "apitogo.config.json";
11618
11616
  var PLACEHOLDER_DEV_PORTAL_API_URL = "https://dp-example.azurecontainerapps.io";
11619
11617
  function isPlanFree(plan) {
@@ -11810,7 +11808,9 @@ function applyManifestToConfig(config2, manifest) {
11810
11808
  for (const key of STORED_MANIFEST_KEYS) {
11811
11809
  const value = manifest[key];
11812
11810
  if (value !== void 0) {
11813
- next[key] = key === "gateway" ? gatewayForConfig(value) : value;
11811
+ next[key] = key === "gateway" ? gatewayForConfig(
11812
+ value
11813
+ ) : value;
11814
11814
  } else {
11815
11815
  delete next[key];
11816
11816
  }
@@ -11824,7 +11824,9 @@ function applyManifestToConfig(config2, manifest) {
11824
11824
  delete next.plans;
11825
11825
  delete next.pricing;
11826
11826
  if (next.authentication && typeof next.authentication === "object") {
11827
- const authentication = { ...next.authentication };
11827
+ const authentication = {
11828
+ ...next.authentication
11829
+ };
11828
11830
  delete authentication.gateway;
11829
11831
  next.authentication = authentication;
11830
11832
  }
@@ -11863,7 +11865,7 @@ var DEFAULT_PLANS = [
11863
11865
  displayName: "Free",
11864
11866
  priceAmount: 0,
11865
11867
  limits: {
11866
- rateLimit: { calls: 10, renewalPeriodSeconds: 60 }
11868
+ rateLimit: { calls: 3, renewalPeriodSeconds: 60 }
11867
11869
  },
11868
11870
  includedActionKeys: []
11869
11871
  },
@@ -12034,7 +12036,9 @@ function resolveManifestNextStep(manifest, secrets) {
12034
12036
  if (!secrets) {
12035
12037
  return "Continue shaping the site in the running local preview. For publish, save auth secrets (OIDC provider secrets, and Stripe if paid plans) in .env using APITOGO_CONFIG_AUTHENTICATION__* keys \u2014 never in chat. Native auth email is always platform-managed.";
12036
12038
  }
12037
- const secretFields = prerequisites.missingFields.filter((field) => field !== "plans");
12039
+ const secretFields = prerequisites.missingFields.filter(
12040
+ (field) => field !== "plans"
12041
+ );
12038
12042
  if (secretFields.length > 0) {
12039
12043
  return `Publish prerequisites incomplete in .env: ${secretFields.join(", ")}.`;
12040
12044
  }
@@ -12055,7 +12059,10 @@ function applyScaffoldManifestPolicy(manifest, options) {
12055
12059
  const result = {
12056
12060
  ...manifest,
12057
12061
  siteName: title,
12058
- branding: { ...manifest.branding, title: manifest.branding?.title ?? title }
12062
+ branding: {
12063
+ ...manifest.branding,
12064
+ title: manifest.branding?.title ?? title
12065
+ }
12059
12066
  };
12060
12067
  if (options.includeDefaultPlans) {
12061
12068
  result.plans = mergePlansBySku2(result.plans, DEFAULT_PLANS);
@@ -12096,7 +12103,10 @@ function resolveBackendOpenApiPath(openApiPath, options) {
12096
12103
  return siteResolved;
12097
12104
  }
12098
12105
  function toSiteRelativeOpenApiInput(siteDirectory, openApiAbsolutePath) {
12099
- const relative = path36.relative(path36.resolve(siteDirectory), path36.resolve(openApiAbsolutePath));
12106
+ const relative = path36.relative(
12107
+ path36.resolve(siteDirectory),
12108
+ path36.resolve(openApiAbsolutePath)
12109
+ );
12100
12110
  const posix = relative.split(path36.sep).join("/");
12101
12111
  if (!posix || posix === ".") {
12102
12112
  return "./";
@@ -12154,7 +12164,11 @@ async function writeManifest(siteDirectory, manifest) {
12154
12164
  const raw = await readFile6(filePath, "utf8");
12155
12165
  config2 = JSON.parse(raw);
12156
12166
  }
12157
- const { oidc: _oidc, stripe: _stripe, ...safeManifest } = manifest;
12167
+ const {
12168
+ oidc: _oidc,
12169
+ stripe: _stripe,
12170
+ ...safeManifest
12171
+ } = manifest;
12158
12172
  config2 = applyManifestToConfig(config2, safeManifest);
12159
12173
  await writeFile7(filePath, `${JSON.stringify(config2, null, 2)}
12160
12174
  `, "utf8");
@@ -15133,7 +15147,7 @@ function createDefaultApitogoJsonConfig(options) {
15133
15147
  priceAmount: 0,
15134
15148
  billingPeriod: "month",
15135
15149
  limits: {
15136
- rateLimit: { calls: 10, renewalPeriodSeconds: 1 },
15150
+ rateLimit: { calls: 3, renewalPeriodSeconds: 60 },
15137
15151
  quota: { calls: 1e3, renewalPeriodDays: 30 }
15138
15152
  }
15139
15153
  },
@@ -16658,138 +16672,394 @@ async function reportPollProgress(progress, stepLabel, value) {
16658
16672
  // src/cli/platform/tools/dev-portal-publish.ts
16659
16673
  init_gateway_limits();
16660
16674
 
16661
- // src/cli/platform/helpers/gateway-plan-sync.ts
16662
- init_gateway_limits();
16675
+ // src/cli/platform/helpers/deploy-artifact-hash.ts
16676
+ import { createHash as createHash2 } from "node:crypto";
16677
+ import { createReadStream, promises as fs8 } from "node:fs";
16678
+ import path54 from "node:path";
16663
16679
 
16664
- // src/cli/platform/tools/gateway.ts
16665
- var DEFAULT_BASELINE_LIMITS_JSON = JSON.stringify({
16666
- rateLimit: { calls: 100, renewalPeriodSeconds: 60 },
16667
- quota: { calls: 1e4, renewalPeriodDays: 1 }
16668
- });
16669
- var DEFAULT_ENTRA_WAIT_TIMEOUT_MS = 12e4;
16670
- var DEFAULT_ENTRA_POLL_INTERVAL_MS = 5e3;
16671
- function sameStringList(a, b) {
16672
- const left = [...a ?? []].map((value) => value.trim()).filter(Boolean).sort();
16673
- const right = [...b ?? []].map((value) => value.trim()).filter(Boolean).sort();
16674
- if (left.length !== right.length) return false;
16675
- return left.every((value, index) => value === right[index]);
16676
- }
16677
- function isPlatformManagedAuthMode(authMode) {
16678
- return authMode.trim().toLowerCase() === "platformmanagedentra";
16679
- }
16680
- function arraysEquivalentForAuth(existing, desired) {
16681
- if (existing.authMode.toLowerCase() !== desired.authMode.toLowerCase()) {
16682
- return false;
16683
- }
16684
- if (isPlatformManagedAuthMode(desired.authMode)) {
16685
- return true;
16686
- }
16687
- return (existing.oidcMetadataUrl ?? "").trim() === (desired.oidcMetadataUrl ?? "").trim() && sameStringList(existing.oidcAllowedIssuers, desired.oidcAllowedIssuers ?? void 0) && sameStringList(existing.oidcAudiences, desired.oidcAudiences ?? void 0);
16688
- }
16689
- async function ensureGatewayAuthConfigured(client, projectId, desired) {
16690
- let existing = null;
16680
+ // src/cli/platform/helpers/package-files.ts
16681
+ import { promises as fs7 } from "node:fs";
16682
+ import path53 from "node:path";
16683
+ import ignore from "ignore";
16684
+ var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set([
16685
+ "node_modules",
16686
+ ".git",
16687
+ ".DS_Store",
16688
+ "dist",
16689
+ "bin",
16690
+ "obj"
16691
+ ]);
16692
+ async function loadGitignore(rootDir) {
16693
+ const ig = ignore();
16694
+ ig.add(["node_modules/", ".git/", "dist/", "bin/", "obj/"]);
16695
+ const gitignorePath = path53.join(rootDir, ".gitignore");
16691
16696
  try {
16692
- existing = await client.getGatewayAuth(projectId);
16693
- } catch (error) {
16694
- if (!isNotFoundError(error)) {
16695
- throw error;
16696
- }
16697
- }
16698
- if (existing && arraysEquivalentForAuth(existing, desired) && (!isPlatformManagedAuthMode(desired.authMode) || existing.platformEntraProvisionStatus?.toLowerCase() === "ready")) {
16699
- return { auth: existing, created: false, updated: false, skipped: true };
16697
+ const content = await fs7.readFile(gitignorePath, "utf8");
16698
+ ig.add(content);
16699
+ } catch {
16700
16700
  }
16701
- const auth = await client.updateGatewayAuth(projectId, desired);
16702
- return {
16703
- auth,
16704
- created: existing === null,
16705
- updated: existing !== null,
16706
- skipped: false
16707
- };
16701
+ return ig;
16708
16702
  }
16709
- async function waitForGatewayEntraProvision(client, projectId, options) {
16710
- const timeoutMs = options?.timeoutMs ?? DEFAULT_ENTRA_WAIT_TIMEOUT_MS;
16711
- const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_ENTRA_POLL_INTERVAL_MS;
16712
- const started = Date.now();
16713
- while (true) {
16714
- const auth = await client.getGatewayAuth(projectId);
16715
- if (!isPlatformManagedAuthMode(auth.authMode)) {
16716
- return { auth, status: "not_applicable" };
16717
- }
16718
- const status2 = auth.platformEntraProvisionStatus?.toLowerCase() ?? "";
16719
- if (status2 === "ready") {
16720
- return { auth, status: "ready" };
16703
+ async function collectFiles(rootDir, relativeDir, ig, files) {
16704
+ const absDir = path53.join(rootDir, relativeDir);
16705
+ const entries = await fs7.readdir(absDir, { withFileTypes: true });
16706
+ for (const entry of entries) {
16707
+ const rel = relativeDir ? path53.join(relativeDir, entry.name) : entry.name;
16708
+ const relPosix = rel.split(path53.sep).join("/");
16709
+ if (DEFAULT_EXCLUDES.has(entry.name) && !relativeDir) {
16710
+ continue;
16721
16711
  }
16722
- if (status2 === "failed") {
16723
- return { auth, status: "failed" };
16712
+ if (ig.ignores(relPosix)) {
16713
+ continue;
16724
16714
  }
16725
- if (Date.now() - started >= timeoutMs) {
16726
- return { auth, status: "timeout" };
16715
+ if (entry.isDirectory()) {
16716
+ await collectFiles(rootDir, rel, ig, files);
16717
+ } else if (entry.isFile()) {
16718
+ files.push(rel);
16727
16719
  }
16728
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
16729
16720
  }
16730
16721
  }
16731
- function normalizeActionKey(actionKey) {
16732
- const trimmed = actionKey.trim();
16733
- if (trimmed.includes(":")) {
16734
- const [method2, ...pathParts] = trimmed.split(":");
16735
- const path73 = pathParts.join(":").trim();
16736
- return `${method2.toLowerCase()}:${path73.startsWith("/") ? path73 : `/${path73}`}`;
16722
+ async function collectPackageableFiles(sourceDirectory) {
16723
+ const resolved = path53.resolve(sourceDirectory);
16724
+ const stat4 = await fs7.stat(resolved).catch(() => null);
16725
+ if (!stat4?.isDirectory()) {
16726
+ throw new Error(`sourceDirectory is not a directory: ${resolved}`);
16737
16727
  }
16738
- const spaceIndex = trimmed.indexOf(" ");
16739
- if (spaceIndex <= 0) {
16740
- throw new Error(
16741
- "actionKey must be in format 'get:/path' or 'GET /path' (method plus path)."
16742
- );
16728
+ const ig = await loadGitignore(resolved);
16729
+ const files = [];
16730
+ await collectFiles(resolved, "", ig, files);
16731
+ if (files.length === 0) {
16732
+ throw new Error(`No files to package in ${resolved}`);
16743
16733
  }
16744
- const method = trimmed.slice(0, spaceIndex).trim().toLowerCase();
16745
- const path72 = trimmed.slice(spaceIndex + 1).trim();
16746
- return `${method}:${path72.startsWith("/") ? path72 : `/${path72}`}`;
16734
+ return files.sort();
16747
16735
  }
16748
- async function applyGatewayPublicRoutes(client, projectId, routes, options) {
16749
- const applied = [];
16750
- const unresolved = [];
16751
- const warnings = [];
16752
- if (!routes.length) {
16753
- return { applied, unresolved, warnings };
16736
+
16737
+ // src/cli/platform/helpers/deploy-artifact-hash.ts
16738
+ async function sha256File(filePath) {
16739
+ const hash2 = createHash2("sha256");
16740
+ await new Promise((resolve, reject) => {
16741
+ const stream = createReadStream(filePath);
16742
+ stream.on("data", (chunk) => hash2.update(chunk));
16743
+ stream.on("error", reject);
16744
+ stream.on("end", () => resolve());
16745
+ });
16746
+ return hash2.digest("hex");
16747
+ }
16748
+ async function hashDirectoryContents(rootDirectory) {
16749
+ const resolved = path54.resolve(rootDirectory);
16750
+ const files = (await collectPackageableFiles(resolved)).sort((a, b) => a.localeCompare(b));
16751
+ const hash2 = createHash2("sha256");
16752
+ for (const rel of files) {
16753
+ hash2.update(rel);
16754
+ hash2.update("\0");
16755
+ hash2.update(await fs8.readFile(path54.join(resolved, rel)));
16756
+ hash2.update("\0");
16754
16757
  }
16755
- let knownKeys = null;
16756
- if (options?.knownActions) {
16757
- knownKeys = new Set(options.knownActions.map((action) => action.actionKey.toLowerCase()));
16758
- } else {
16758
+ return hash2.digest("hex");
16759
+ }
16760
+ async function hashDevPortalSiteSources(siteDirectory, options) {
16761
+ const resolved = path54.resolve(siteDirectory);
16762
+ const hash2 = createHash2("sha256");
16763
+ const candidates = [
16764
+ "apitogo.config.json",
16765
+ "apitogo.config.ts",
16766
+ "package.json",
16767
+ "zudoku.config.ts",
16768
+ "zudoku.config.tsx"
16769
+ ];
16770
+ for (const name of candidates) {
16771
+ const full = path54.join(resolved, name);
16759
16772
  try {
16760
- const listed = await listAllGatewayActions(client, projectId);
16761
- if (listed.length > 0) {
16762
- knownKeys = new Set(listed.map((action) => action.actionKey.toLowerCase()));
16763
- }
16773
+ hash2.update(name);
16774
+ hash2.update("\0");
16775
+ hash2.update(await fs8.readFile(full));
16776
+ hash2.update("\0");
16764
16777
  } catch {
16765
- knownKeys = null;
16766
16778
  }
16767
16779
  }
16768
- for (const route of routes) {
16769
- let actionKey;
16780
+ for (const dir of ["pages", "landing", "src"]) {
16781
+ const full = path54.join(resolved, dir);
16770
16782
  try {
16771
- const trimmed = route.trim();
16772
- actionKey = trimmed.startsWith("/") ? normalizeActionKey(`GET ${trimmed}`) : normalizeActionKey(trimmed);
16773
- } catch (error) {
16774
- unresolved.push({
16775
- route,
16776
- error: error instanceof Error ? error.message : String(error)
16777
- });
16778
- continue;
16779
- }
16780
- if (knownKeys && !knownKeys.has(actionKey.toLowerCase())) {
16781
- const message = `Action '${actionKey}' not found in gateway actions.`;
16782
- unresolved.push({ route, actionKey, error: message });
16783
- warnings.push(message);
16784
- continue;
16783
+ const st = await fs8.stat(full);
16784
+ if (!st.isDirectory()) continue;
16785
+ hash2.update(dir);
16786
+ hash2.update("\0");
16787
+ hash2.update(await hashDirectoryContents(full));
16788
+ hash2.update("\0");
16789
+ } catch {
16785
16790
  }
16791
+ }
16792
+ const gatewayUrl = options?.gatewayUrl?.trim().replace(/\/+$/, "");
16793
+ if (gatewayUrl) {
16794
+ hash2.update("gatewayUrl");
16795
+ hash2.update("\0");
16796
+ hash2.update(gatewayUrl);
16797
+ hash2.update("\0");
16798
+ }
16799
+ return hash2.digest("hex");
16800
+ }
16801
+
16802
+ // src/cli/platform/helpers/dev-portal-plan-sync.ts
16803
+ var PLANS_TIMEOUT_MS = 3e4;
16804
+ var DEFAULT_RETRIES = 5;
16805
+ var DEFAULT_RETRY_DELAY_MS = 5e3;
16806
+ async function syncDevPortalPlanCatalog(options) {
16807
+ const fetchImpl = options.fetchImpl ?? fetch;
16808
+ const retries = Math.max(1, options.retries ?? DEFAULT_RETRIES);
16809
+ const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
16810
+ const base = options.backendApiBaseUrl.replace(/\/$/, "");
16811
+ const url2 = `${base}/api/v1/plans?refresh=true&force=true`;
16812
+ let lastError;
16813
+ for (let attempt = 1; attempt <= retries; attempt++) {
16814
+ const controller = new AbortController();
16815
+ const timeout = setTimeout(() => controller.abort(), PLANS_TIMEOUT_MS);
16786
16816
  try {
16787
- const action = await client.setGatewayActionPublic(projectId, actionKey, true);
16788
- applied.push(action);
16817
+ const response = await fetchImpl(url2, {
16818
+ method: "GET",
16819
+ headers: { Accept: "application/json" },
16820
+ signal: controller.signal
16821
+ });
16822
+ if (!response.ok) {
16823
+ throw new Error(`GET /api/v1/plans?refresh=true&force=true returned ${response.status}.`);
16824
+ }
16825
+ const catalog = await response.json();
16826
+ if (options.expectedPlanCount !== void 0 && catalog.plans.length < options.expectedPlanCount) {
16827
+ throw new Error(
16828
+ `Plan catalog has ${catalog.plans.length} plan(s); expected at least ${options.expectedPlanCount}.`
16829
+ );
16830
+ }
16831
+ return catalog;
16789
16832
  } catch (error) {
16790
- const message = error instanceof Error ? error.message : String(error);
16791
- unresolved.push({ route, actionKey, error: message });
16792
- warnings.push(`Failed to set public for '${actionKey}': ${message}`);
16833
+ lastError = error instanceof Error && error.name === "AbortError" ? new Error("GET /api/v1/plans?refresh=true&force=true timed out.") : error instanceof Error ? error : new Error(String(error));
16834
+ if (attempt < retries) {
16835
+ await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
16836
+ }
16837
+ } finally {
16838
+ clearTimeout(timeout);
16839
+ }
16840
+ }
16841
+ throw lastError ?? new Error("Dev portal plan catalog sync failed.");
16842
+ }
16843
+
16844
+ // src/cli/platform/helpers/dev-portal-settings-kinds.ts
16845
+ function isHttpUrl(value) {
16846
+ return /^https?:\/\//i.test(value.trim());
16847
+ }
16848
+ function resolveLogoSettings(logoUrl) {
16849
+ const trimmed = logoUrl?.trim();
16850
+ if (!trimmed) {
16851
+ return { logoKind: "RepoRelative" };
16852
+ }
16853
+ if (isHttpUrl(trimmed)) {
16854
+ return { logoKind: "AbsoluteUrl", logoValue: trimmed };
16855
+ }
16856
+ return { logoKind: "RepoRelative", logoValue: trimmed };
16857
+ }
16858
+ function resolveOpenApiSettings(openApiPath) {
16859
+ const trimmed = openApiPath?.trim();
16860
+ if (!trimmed) {
16861
+ return { openApiSourceKind: "RepoRelative" };
16862
+ }
16863
+ if (isHttpUrl(trimmed)) {
16864
+ return { openApiSourceKind: "ExternalUrl", openApiValue: trimmed };
16865
+ }
16866
+ return { openApiSourceKind: "RepoRelative", openApiValue: trimmed };
16867
+ }
16868
+
16869
+ // src/cli/platform/helpers/dev-portal-urls.ts
16870
+ function normalizeHostname(hostname) {
16871
+ return hostname.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
16872
+ }
16873
+ function resolveSwaHostname(project) {
16874
+ const siteUrl = project.siteUrl?.trim();
16875
+ if (siteUrl) {
16876
+ try {
16877
+ return normalizeHostname(new URL(siteUrl).hostname);
16878
+ } catch {
16879
+ return normalizeHostname(siteUrl);
16880
+ }
16881
+ }
16882
+ const custom = project.userCustomDomain?.trim();
16883
+ if (custom) {
16884
+ return normalizeHostname(custom);
16885
+ }
16886
+ const defaultHost = project.azureStaticWebDefaultHostname?.trim();
16887
+ if (defaultHost) {
16888
+ return normalizeHostname(defaultHost);
16889
+ }
16890
+ return null;
16891
+ }
16892
+ function resolveSwaHostnameWithFallback(project, platformSiteUrl) {
16893
+ return resolveSwaHostname(project) ?? (platformSiteUrl?.trim() ? normalizeHostname(platformSiteUrl) : null);
16894
+ }
16895
+ function resolveSwaUrl(project) {
16896
+ const hostname = resolveSwaHostname(project);
16897
+ if (!hostname) {
16898
+ return null;
16899
+ }
16900
+ return `https://${hostname}`;
16901
+ }
16902
+ function resolveDevPortalUrls(project, backendFqdn, providerIds = [], platformSiteUrl) {
16903
+ const hostname = resolveSwaHostnameWithFallback(project, platformSiteUrl);
16904
+ const swaUrl = hostname ? `https://${hostname}` : null;
16905
+ if (!swaUrl) {
16906
+ throw new Error(
16907
+ "Static Web App hostname is not available. Ensure the dev-portal module is provisioned."
16908
+ );
16909
+ }
16910
+ const backendApiBaseUrl = toApiBaseUrl(backendFqdn);
16911
+ if (!backendApiBaseUrl) {
16912
+ throw new Error("Dev portal backend FQDN is missing.");
16913
+ }
16914
+ const swaOrigin = new URL(swaUrl).origin;
16915
+ const backendOrigin = new URL(backendApiBaseUrl).origin;
16916
+ const oidcRedirectUris = providerIds.map((providerId) => ({
16917
+ providerId,
16918
+ redirectUri: `${backendOrigin}/signin-oidc/${providerId}`
16919
+ }));
16920
+ return {
16921
+ swaUrl,
16922
+ swaOrigin,
16923
+ stripeSuccessUrl: `${swaOrigin}/account/subscriptions`,
16924
+ stripeCancelUrl: `${swaOrigin}/pricing`,
16925
+ oidcRedirectUris,
16926
+ stripeWebhookUrl: `${backendOrigin}/api/v1/webhooks/stripe`,
16927
+ backendApiBaseUrl
16928
+ };
16929
+ }
16930
+
16931
+ // src/cli/platform/helpers/gateway-plan-sync.ts
16932
+ init_gateway_limits();
16933
+
16934
+ // src/cli/platform/tools/gateway.ts
16935
+ var DEFAULT_BASELINE_LIMITS_JSON = JSON.stringify({
16936
+ rateLimit: { calls: 100, renewalPeriodSeconds: 60 },
16937
+ quota: { calls: 1e4, renewalPeriodDays: 1 }
16938
+ });
16939
+ var DEFAULT_ENTRA_WAIT_TIMEOUT_MS = 12e4;
16940
+ var DEFAULT_ENTRA_POLL_INTERVAL_MS = 5e3;
16941
+ function sameStringList(a, b) {
16942
+ const left = [...a ?? []].map((value) => value.trim()).filter(Boolean).sort();
16943
+ const right = [...b ?? []].map((value) => value.trim()).filter(Boolean).sort();
16944
+ if (left.length !== right.length) return false;
16945
+ return left.every((value, index) => value === right[index]);
16946
+ }
16947
+ function isPlatformManagedAuthMode(authMode) {
16948
+ return authMode.trim().toLowerCase() === "platformmanagedentra";
16949
+ }
16950
+ function arraysEquivalentForAuth(existing, desired) {
16951
+ if (existing.authMode.toLowerCase() !== desired.authMode.toLowerCase()) {
16952
+ return false;
16953
+ }
16954
+ if (isPlatformManagedAuthMode(desired.authMode)) {
16955
+ return true;
16956
+ }
16957
+ return (existing.oidcMetadataUrl ?? "").trim() === (desired.oidcMetadataUrl ?? "").trim() && sameStringList(existing.oidcAllowedIssuers, desired.oidcAllowedIssuers ?? void 0) && sameStringList(existing.oidcAudiences, desired.oidcAudiences ?? void 0);
16958
+ }
16959
+ async function ensureGatewayAuthConfigured(client, projectId, desired) {
16960
+ let existing = null;
16961
+ try {
16962
+ existing = await client.getGatewayAuth(projectId);
16963
+ } catch (error) {
16964
+ if (!isNotFoundError(error)) {
16965
+ throw error;
16966
+ }
16967
+ }
16968
+ if (existing && arraysEquivalentForAuth(existing, desired) && (!isPlatformManagedAuthMode(desired.authMode) || existing.platformEntraProvisionStatus?.toLowerCase() === "ready")) {
16969
+ return { auth: existing, created: false, updated: false, skipped: true };
16970
+ }
16971
+ const auth = await client.updateGatewayAuth(projectId, desired);
16972
+ return {
16973
+ auth,
16974
+ created: existing === null,
16975
+ updated: existing !== null,
16976
+ skipped: false
16977
+ };
16978
+ }
16979
+ async function waitForGatewayEntraProvision(client, projectId, options) {
16980
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_ENTRA_WAIT_TIMEOUT_MS;
16981
+ const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_ENTRA_POLL_INTERVAL_MS;
16982
+ const started = Date.now();
16983
+ while (true) {
16984
+ const auth = await client.getGatewayAuth(projectId);
16985
+ if (!isPlatformManagedAuthMode(auth.authMode)) {
16986
+ return { auth, status: "not_applicable" };
16987
+ }
16988
+ const status2 = auth.platformEntraProvisionStatus?.toLowerCase() ?? "";
16989
+ if (status2 === "ready") {
16990
+ return { auth, status: "ready" };
16991
+ }
16992
+ if (status2 === "failed") {
16993
+ return { auth, status: "failed" };
16994
+ }
16995
+ if (Date.now() - started >= timeoutMs) {
16996
+ return { auth, status: "timeout" };
16997
+ }
16998
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
16999
+ }
17000
+ }
17001
+ function normalizeActionKey(actionKey) {
17002
+ const trimmed = actionKey.trim();
17003
+ if (trimmed.includes(":")) {
17004
+ const [method2, ...pathParts] = trimmed.split(":");
17005
+ const path73 = pathParts.join(":").trim();
17006
+ return `${method2.toLowerCase()}:${path73.startsWith("/") ? path73 : `/${path73}`}`;
17007
+ }
17008
+ const spaceIndex = trimmed.indexOf(" ");
17009
+ if (spaceIndex <= 0) {
17010
+ throw new Error(
17011
+ "actionKey must be in format 'get:/path' or 'GET /path' (method plus path)."
17012
+ );
17013
+ }
17014
+ const method = trimmed.slice(0, spaceIndex).trim().toLowerCase();
17015
+ const path72 = trimmed.slice(spaceIndex + 1).trim();
17016
+ return `${method}:${path72.startsWith("/") ? path72 : `/${path72}`}`;
17017
+ }
17018
+ async function applyGatewayPublicRoutes(client, projectId, routes, options) {
17019
+ const applied = [];
17020
+ const unresolved = [];
17021
+ const warnings = [];
17022
+ if (!routes.length) {
17023
+ return { applied, unresolved, warnings };
17024
+ }
17025
+ let knownKeys = null;
17026
+ if (options?.knownActions) {
17027
+ knownKeys = new Set(options.knownActions.map((action) => action.actionKey.toLowerCase()));
17028
+ } else {
17029
+ try {
17030
+ const listed = await listAllGatewayActions(client, projectId);
17031
+ if (listed.length > 0) {
17032
+ knownKeys = new Set(listed.map((action) => action.actionKey.toLowerCase()));
17033
+ }
17034
+ } catch {
17035
+ knownKeys = null;
17036
+ }
17037
+ }
17038
+ for (const route of routes) {
17039
+ let actionKey;
17040
+ try {
17041
+ const trimmed = route.trim();
17042
+ actionKey = trimmed.startsWith("/") ? normalizeActionKey(`GET ${trimmed}`) : normalizeActionKey(trimmed);
17043
+ } catch (error) {
17044
+ unresolved.push({
17045
+ route,
17046
+ error: error instanceof Error ? error.message : String(error)
17047
+ });
17048
+ continue;
17049
+ }
17050
+ if (knownKeys && !knownKeys.has(actionKey.toLowerCase())) {
17051
+ const message = `Action '${actionKey}' not found in gateway actions.`;
17052
+ unresolved.push({ route, actionKey, error: message });
17053
+ warnings.push(message);
17054
+ continue;
17055
+ }
17056
+ try {
17057
+ const action = await client.setGatewayActionPublic(projectId, actionKey, true);
17058
+ applied.push(action);
17059
+ } catch (error) {
17060
+ const message = error instanceof Error ? error.message : String(error);
17061
+ unresolved.push({ route, actionKey, error: message });
17062
+ warnings.push(`Failed to set public for '${actionKey}': ${message}`);
16793
17063
  }
16794
17064
  }
16795
17065
  return { applied, unresolved, warnings };
@@ -17163,358 +17433,240 @@ async function upsertGatewayPlansFromManifest(client, config2, projectId, plans,
17163
17433
  return results;
17164
17434
  }
17165
17435
 
17166
- // src/cli/platform/helpers/dev-portal-plan-sync.ts
17167
- var PLANS_TIMEOUT_MS = 3e4;
17168
- var DEFAULT_RETRIES = 5;
17169
- var DEFAULT_RETRY_DELAY_MS = 5e3;
17170
- async function syncDevPortalPlanCatalog(options) {
17171
- const fetchImpl = options.fetchImpl ?? fetch;
17172
- const retries = Math.max(1, options.retries ?? DEFAULT_RETRIES);
17173
- const retryDelayMs = options.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
17174
- const base = options.backendApiBaseUrl.replace(/\/$/, "");
17175
- const url2 = `${base}/api/v1/plans?refresh=true&force=true`;
17176
- let lastError;
17177
- for (let attempt = 1; attempt <= retries; attempt++) {
17178
- const controller = new AbortController();
17179
- const timeout = setTimeout(() => controller.abort(), PLANS_TIMEOUT_MS);
17180
- try {
17181
- const response = await fetchImpl(url2, {
17182
- method: "GET",
17183
- headers: { Accept: "application/json" },
17184
- signal: controller.signal
17185
- });
17186
- if (!response.ok) {
17187
- throw new Error(`GET /api/v1/plans?refresh=true&force=true returned ${response.status}.`);
17188
- }
17189
- const catalog = await response.json();
17190
- if (options.expectedPlanCount !== void 0 && catalog.plans.length < options.expectedPlanCount) {
17191
- throw new Error(
17192
- `Plan catalog has ${catalog.plans.length} plan(s); expected at least ${options.expectedPlanCount}.`
17193
- );
17194
- }
17195
- return catalog;
17196
- } catch (error) {
17197
- lastError = error instanceof Error && error.name === "AbortError" ? new Error("GET /api/v1/plans?refresh=true&force=true timed out.") : error instanceof Error ? error : new Error(String(error));
17198
- if (attempt < retries) {
17199
- await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
17200
- }
17201
- } finally {
17202
- clearTimeout(timeout);
17203
- }
17204
- }
17205
- throw lastError ?? new Error("Dev portal plan catalog sync failed.");
17206
- }
17207
-
17208
- // src/cli/platform/helpers/dev-portal-urls.ts
17209
- function normalizeHostname(hostname) {
17210
- return hostname.trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
17211
- }
17212
- function resolveSwaHostname(project) {
17213
- const siteUrl = project.siteUrl?.trim();
17214
- if (siteUrl) {
17215
- try {
17216
- return normalizeHostname(new URL(siteUrl).hostname);
17217
- } catch {
17218
- return normalizeHostname(siteUrl);
17219
- }
17220
- }
17221
- const custom = project.userCustomDomain?.trim();
17222
- if (custom) {
17223
- return normalizeHostname(custom);
17224
- }
17225
- const defaultHost = project.azureStaticWebDefaultHostname?.trim();
17226
- if (defaultHost) {
17227
- return normalizeHostname(defaultHost);
17228
- }
17229
- return null;
17230
- }
17231
- function resolveSwaHostnameWithFallback(project, platformSiteUrl) {
17232
- return resolveSwaHostname(project) ?? (platformSiteUrl?.trim() ? normalizeHostname(platformSiteUrl) : null);
17233
- }
17234
- function resolveSwaUrl(project) {
17235
- const hostname = resolveSwaHostname(project);
17236
- if (!hostname) {
17237
- return null;
17238
- }
17239
- return `https://${hostname}`;
17436
+ // src/cli/platform/helpers/openapi-gateway-servers.ts
17437
+ import { mkdir as mkdir11, readFile as readFile15, writeFile as writeFile14 } from "node:fs/promises";
17438
+ import path55 from "node:path";
17439
+ import YAML3 from "yaml";
17440
+ var GATEWAY_URL_ENV_KEY = "APITOGO_GATEWAY_URL";
17441
+ var PUBLISH_OPENAPI_RELATIVE = ".apitogo/openapi-publish.json";
17442
+ function normalizeGatewayUrl(gatewayUrl) {
17443
+ return gatewayUrl.trim().replace(/\/+$/, "");
17240
17444
  }
17241
- function resolveDevPortalUrls(project, backendFqdn, providerIds = [], platformSiteUrl) {
17242
- const hostname = resolveSwaHostnameWithFallback(project, platformSiteUrl);
17243
- const swaUrl = hostname ? `https://${hostname}` : null;
17244
- if (!swaUrl) {
17245
- throw new Error(
17246
- "Static Web App hostname is not available. Ensure the dev-portal module is provisioned."
17247
- );
17248
- }
17249
- const backendApiBaseUrl = toApiBaseUrl(backendFqdn);
17250
- if (!backendApiBaseUrl) {
17251
- throw new Error("Dev portal backend FQDN is missing.");
17252
- }
17253
- const swaOrigin = new URL(swaUrl).origin;
17254
- const backendOrigin = new URL(backendApiBaseUrl).origin;
17255
- const oidcRedirectUris = providerIds.map((providerId) => ({
17256
- providerId,
17257
- redirectUri: `${backendOrigin}/signin-oidc/${providerId}`
17258
- }));
17445
+ function withGatewayServers(doc, gatewayUrl) {
17446
+ const url2 = normalizeGatewayUrl(gatewayUrl);
17259
17447
  return {
17260
- swaUrl,
17261
- swaOrigin,
17262
- stripeSuccessUrl: `${swaOrigin}/account/subscriptions`,
17263
- stripeCancelUrl: `${swaOrigin}/pricing`,
17264
- oidcRedirectUris,
17265
- stripeWebhookUrl: `${backendOrigin}/api/v1/webhooks/stripe`,
17266
- backendApiBaseUrl
17448
+ ...doc,
17449
+ servers: [{ url: url2, description: "API gateway" }]
17267
17450
  };
17268
17451
  }
17269
-
17270
- // src/cli/platform/helpers/status.ts
17271
- function normalizeInfrastructureStatus(platformStatus) {
17272
- switch (platformStatus.toLowerCase()) {
17273
- case "completed":
17274
- case "completed_with_warnings":
17275
- return "succeeded";
17276
- case "in_progress":
17277
- return "running";
17278
- case "failed":
17279
- return "failed";
17280
- case "pending":
17281
- case "none":
17282
- default:
17283
- return "pending";
17452
+ async function resolveProjectGatewayUrl(client, projectId) {
17453
+ const project = await client.getProject(projectId);
17454
+ const fromProject = project.gatewayUrl?.trim();
17455
+ if (fromProject) {
17456
+ return normalizeGatewayUrl(fromProject);
17284
17457
  }
17285
- }
17286
- function normalizeDeploymentStatus(platformStatus) {
17287
- switch (platformStatus.toLowerCase()) {
17288
- case "healthy":
17289
- return "succeeded";
17290
- case "failed":
17291
- case "cancelled":
17292
- return "failed";
17293
- case "queued":
17294
- case "validating":
17295
- case "building":
17296
- case "deploying":
17297
- return "running";
17298
- default:
17299
- return "pending";
17458
+ try {
17459
+ const summary = await client.getBackendSummary(projectId);
17460
+ const fromBackend = summary.backendUrl?.trim();
17461
+ if (fromBackend) {
17462
+ return normalizeGatewayUrl(fromBackend);
17463
+ }
17464
+ } catch {
17300
17465
  }
17466
+ return null;
17301
17467
  }
17302
- function isTerminalInfrastructureStatus(platformStatus) {
17303
- const s = platformStatus.toLowerCase();
17304
- return s === "completed" || s === "completed_with_warnings" || s === "failed";
17305
- }
17306
- function isTerminalDeploymentStatus(platformStatus) {
17307
- const s = platformStatus.toLowerCase();
17308
- return s === "healthy" || s === "failed" || s === "cancelled";
17468
+ async function readOpenApiDocument(filePath) {
17469
+ const content = await readFile15(filePath, "utf8");
17470
+ const ext = path55.extname(filePath).toLowerCase();
17471
+ const parsed = ext === ".json" ? JSON.parse(content) : YAML3.parse(content);
17472
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
17473
+ throw new Error(`OpenAPI document must be an object: ${filePath}`);
17474
+ }
17475
+ return parsed;
17309
17476
  }
17310
- var DEFAULT_POLL_INTERVAL_MS = 5e3;
17311
- var DEFAULT_POLL_TIMEOUT_MS = 15 * 60 * 1e3;
17312
-
17313
- // src/cli/platform/helpers/poll.ts
17314
- function sleep2(ms) {
17315
- return new Promise((resolve) => setTimeout(resolve, ms));
17477
+ async function materializePublishOpenApiWithGateway(siteDirectory, sourceOpenApiPath, gatewayUrl) {
17478
+ const site = path55.resolve(siteDirectory);
17479
+ const source = path55.resolve(sourceOpenApiPath);
17480
+ const relativeInput = PUBLISH_OPENAPI_RELATIVE;
17481
+ const outputPath = path55.join(site, ...relativeInput.split("/"));
17482
+ const doc = await readOpenApiDocument(source);
17483
+ const rewritten = withGatewayServers(doc, gatewayUrl);
17484
+ await mkdir11(path55.dirname(outputPath), { recursive: true });
17485
+ await writeFile14(outputPath, `${JSON.stringify(rewritten, null, 2)}
17486
+ `, "utf8");
17487
+ return { outputPath, relativeInput };
17316
17488
  }
17317
- async function pollUntilTerminal(options) {
17318
- const timeoutMs = options.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
17319
- const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
17320
- const deadline = Date.now() + timeoutMs;
17321
- const stepLabel = options.stepLabel ?? "Job";
17322
- let last;
17323
- while (Date.now() <= deadline) {
17324
- if (options.progress?.isCancelled()) {
17325
- if (!last) {
17326
- throw new Error("Operation cancelled before first poll result.");
17489
+ async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
17490
+ const configPath = path55.join(path55.resolve(siteDirectory), APITOGO_CONFIG_FILENAME2);
17491
+ const raw = await readFile15(configPath, "utf8");
17492
+ const config2 = JSON.parse(raw);
17493
+ const previousApis = config2.apis;
17494
+ const previousBackend = config2.backend;
17495
+ const apis = Array.isArray(config2.apis) ? [...config2.apis] : [];
17496
+ const first = apis[0] && typeof apis[0] === "object" ? { ...apis[0] } : { type: "file", path: "api" };
17497
+ first.type = first.type ?? "file";
17498
+ first.input = relativeInput;
17499
+ first.path = first.path ?? "api";
17500
+ config2.apis = [first, ...apis.slice(1)];
17501
+ const backend = config2.backend && typeof config2.backend === "object" ? { ...config2.backend } : {};
17502
+ backend.openApiPath = relativeInput;
17503
+ config2.backend = backend;
17504
+ await writeFile14(configPath, `${JSON.stringify(config2, null, 2)}
17505
+ `, "utf8");
17506
+ return {
17507
+ restore: async () => {
17508
+ const currentRaw = await readFile15(configPath, "utf8");
17509
+ const current = JSON.parse(currentRaw);
17510
+ if (previousApis === void 0) {
17511
+ delete current.apis;
17512
+ } else {
17513
+ current.apis = previousApis;
17327
17514
  }
17328
- return {
17329
- status: options.getStatus(last),
17330
- value: last,
17331
- timedOut: true,
17332
- cancelled: true
17333
- };
17334
- }
17335
- try {
17336
- last = await options.poll();
17337
- } catch (error) {
17338
- const handled = options.onPollError?.(error, last);
17339
- if (handled) {
17340
- return handled;
17515
+ if (previousBackend === void 0) {
17516
+ delete current.backend;
17517
+ } else {
17518
+ current.backend = previousBackend;
17341
17519
  }
17342
- throw error;
17343
- }
17344
- options.onPoll?.(last);
17345
- if (options.progress && isPollableJobValue(last)) {
17346
- await reportPollProgress(options.progress, stepLabel, last);
17347
- }
17348
- if (options.isTerminal(last)) {
17349
- return {
17350
- status: options.getStatus(last),
17351
- value: last,
17352
- timedOut: false
17353
- };
17520
+ await writeFile14(configPath, `${JSON.stringify(current, null, 2)}
17521
+ `, "utf8");
17354
17522
  }
17355
- if (Date.now() + intervalMs > deadline) {
17356
- break;
17523
+ };
17524
+ }
17525
+ async function prepareOpenApiServersForGatewayBuild(options) {
17526
+ const siteDirectory = path55.resolve(options.siteDirectory);
17527
+ let openApiPath = resolveBackendOpenApiPath(options.manifest?.backend?.openApiPath, {
17528
+ siteDirectory,
17529
+ backendSourceDirectory: options.manifest?.backend?.sourceDirectory
17530
+ });
17531
+ if (!openApiPath) {
17532
+ try {
17533
+ const raw = await readFile15(path55.join(siteDirectory, APITOGO_CONFIG_FILENAME2), "utf8");
17534
+ const config2 = JSON.parse(raw);
17535
+ const input5 = Array.isArray(config2.apis) ? config2.apis[0]?.input?.trim() : void 0;
17536
+ openApiPath = resolveBackendOpenApiPath(input5, { siteDirectory });
17537
+ } catch {
17357
17538
  }
17358
- await sleep2(intervalMs);
17359
17539
  }
17360
- if (!last) {
17361
- throw new Error("Polling produced no results.");
17540
+ if (!openApiPath) {
17541
+ return null;
17362
17542
  }
17363
- return {
17364
- status: options.getStatus(last),
17365
- value: last,
17366
- timedOut: true
17367
- };
17368
- }
17369
- function isPollableJobValue(value) {
17370
- return typeof value === "object" && value !== null && "status" in value;
17543
+ const { outputPath, relativeInput } = await materializePublishOpenApiWithGateway(
17544
+ siteDirectory,
17545
+ openApiPath,
17546
+ options.gatewayUrl
17547
+ );
17548
+ const { restore } = await pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput);
17549
+ return { relativeInput, outputPath, restore };
17371
17550
  }
17372
17551
 
17373
- // src/cli/platform/helpers/warm-pool.ts
17374
- var WARM_POOL_CAPACITY_TEMPORARILY_UNAVAILABLE = "WARM_POOL_CAPACITY_TEMPORARILY_UNAVAILABLE";
17375
- var COLD_MODULE_INFRA_RESOURCE_TYPES = /* @__PURE__ */ new Set([
17376
- "AzureProjectInfrastructure",
17377
- "DevPortalInfrastructure",
17378
- "BackendInfrastructure"
17379
- ]);
17380
- var WARM_POOL_CREATE_RETRY_BUDGET_MS = 10 * 60 * 1e3;
17381
- function sleep3(ms) {
17382
- return new Promise((resolve) => setTimeout(resolve, ms));
17383
- }
17384
- function warmPoolRetryDelayMs() {
17385
- return 6e4 + Math.floor(Math.random() * 30001);
17386
- }
17387
- function isWarmPoolCapacityUnavailableError(error) {
17388
- if (!(error instanceof PlatformApiError)) {
17389
- return false;
17552
+ // src/cli/platform/helpers/public-routes.ts
17553
+ function normalizeActionKeyFormat(actionKey) {
17554
+ const trimmed = actionKey.trim();
17555
+ if (trimmed.includes(":")) {
17556
+ const [method2, ...pathParts] = trimmed.split(":");
17557
+ const path73 = pathParts.join(":").trim();
17558
+ return `${method2.toLowerCase()}:${path73.startsWith("/") ? path73 : `/${path73}`}`;
17390
17559
  }
17391
- if (error.status !== 503) {
17392
- return false;
17560
+ const spaceIndex = trimmed.indexOf(" ");
17561
+ if (spaceIndex <= 0) {
17562
+ throw new Error(
17563
+ "actionKey must be in format 'get:/path' or 'GET /path' (method plus path)."
17564
+ );
17393
17565
  }
17394
- const code = error.problem?.code;
17395
- return code === WARM_POOL_CAPACITY_TEMPORARILY_UNAVAILABLE;
17396
- }
17397
- function isWarmPoolInfrastructureReady(project) {
17398
- return project.infrastructureStatus?.toLowerCase() === "ready";
17399
- }
17400
- function isWarmPoolInfrastructureFailed(project) {
17401
- return project.infrastructureStatus?.toLowerCase() === "failed";
17566
+ const method = trimmed.slice(0, spaceIndex).trim().toLowerCase();
17567
+ const path72 = trimmed.slice(spaceIndex + 1).trim();
17568
+ return `${method}:${path72.startsWith("/") ? path72 : `/${path72}`}`;
17402
17569
  }
17403
- function isWarmPoolInfrastructureTerminal(project) {
17404
- const status2 = project.infrastructureStatus?.toLowerCase() ?? "";
17405
- return status2 === "ready" || status2 === "failed" || status2 === "deleting";
17570
+ function normalizePublicRoute(route) {
17571
+ const trimmed = route.trim();
17572
+ if (!trimmed) {
17573
+ throw new Error("public route must be a non-empty string");
17574
+ }
17575
+ if (trimmed.startsWith("/")) {
17576
+ return normalizeActionKeyFormat(`GET ${trimmed}`);
17577
+ }
17578
+ return normalizeActionKeyFormat(trimmed);
17406
17579
  }
17407
- function shouldSkipColdModuleInfrastructureWait(project, infraStatus) {
17408
- if (isWarmPoolInfrastructureReady(project)) {
17409
- return true;
17580
+ function collectManifestPublicActionKeys(gateway) {
17581
+ if (!gateway) {
17582
+ return [];
17410
17583
  }
17411
- const resourceType = infraStatus?.resourceType;
17412
- if (resourceType === "WarmPoolBinding") {
17413
- return true;
17584
+ const keys = /* @__PURE__ */ new Set();
17585
+ for (const route of gateway.publicRoutes ?? []) {
17586
+ try {
17587
+ keys.add(normalizePublicRoute(route));
17588
+ } catch {
17589
+ }
17414
17590
  }
17415
- if (resourceType && COLD_MODULE_INFRA_RESOURCE_TYPES.has(resourceType)) {
17416
- return false;
17591
+ if (gateway.publicHealthActionKey?.trim()) {
17592
+ try {
17593
+ keys.add(normalizePublicRoute(gateway.publicHealthActionKey));
17594
+ } catch {
17595
+ }
17417
17596
  }
17418
- return false;
17597
+ return [...keys];
17598
+ }
17599
+
17600
+ // src/cli/platform/helpers/publish-ledger.ts
17601
+ import { mkdir as mkdir12, readFile as readFile16, writeFile as writeFile15 } from "node:fs/promises";
17602
+ import path56 from "node:path";
17603
+ var PUBLISH_LEDGER_RELATIVE_PATH = ".apitogo/publish-state.json";
17604
+ function publishLedgerPath(siteDirectory) {
17605
+ return path56.join(path56.resolve(siteDirectory), PUBLISH_LEDGER_RELATIVE_PATH);
17419
17606
  }
17420
- function resolvePortalBackendFqdn(project) {
17421
- const portalApiUrl = project.portalApiUrl?.trim();
17422
- if (!portalApiUrl) {
17423
- return null;
17424
- }
17607
+ async function readPublishLedger(siteDirectory) {
17608
+ const filePath = publishLedgerPath(siteDirectory);
17425
17609
  try {
17426
- return new URL(portalApiUrl).hostname;
17610
+ const raw = await readFile16(filePath, "utf8");
17611
+ const parsed = JSON.parse(raw);
17612
+ if (parsed?.version !== 1 || !Array.isArray(parsed.steps)) {
17613
+ return null;
17614
+ }
17615
+ return parsed;
17427
17616
  } catch {
17428
17617
  return null;
17429
17618
  }
17430
17619
  }
17431
- var RESERVED_ENDPOINT_URL_AGENT_INSTRUCTION = "Do not share siteUrl, portalApiUrl, gatewayUrl, or any warm-pool hostnames with the user yet \u2014 those addresses are reserved infrastructure and show Azure's default page until the site or backend is actually deployed. Share live URLs only after publish_dev_portal, deploy_backend, or verify_dev_portal_live succeeds.";
17432
- function omitReservedEndpointUrls(project) {
17433
- const {
17434
- siteUrl: _siteUrl,
17435
- portalApiUrl: _portalApiUrl,
17436
- gatewayUrl: _gatewayUrl,
17437
- ...rest
17438
- } = project;
17439
- return rest;
17440
- }
17441
- async function waitForWarmPoolBinding(client, projectId, options) {
17442
- const poll = await pollUntilTerminal({
17443
- poll: () => client.getProject(projectId),
17444
- isTerminal: (project2) => isWarmPoolInfrastructureTerminal(project2),
17445
- getStatus: (project2) => {
17446
- if (isWarmPoolInfrastructureFailed(project2)) {
17447
- return "failed";
17448
- }
17449
- if (isWarmPoolInfrastructureReady(project2)) {
17450
- return "succeeded";
17451
- }
17452
- return "running";
17453
- },
17454
- timeoutMs: options?.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
17455
- intervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
17456
- stepLabel: "Warm pool binding",
17457
- progress: options?.progress
17458
- });
17459
- const project = poll.value;
17460
- let status2;
17461
- if (poll.timedOut) {
17462
- status2 = "running";
17463
- } else if (isWarmPoolInfrastructureFailed(project)) {
17464
- status2 = "failed";
17465
- } else if (isWarmPoolInfrastructureReady(project)) {
17466
- status2 = "succeeded";
17467
- } else {
17468
- status2 = "running";
17469
- }
17470
- return {
17471
- status: status2,
17472
- timedOut: poll.timedOut,
17473
- project
17620
+ async function writePublishLedger(siteDirectory, ledger) {
17621
+ const filePath = publishLedgerPath(siteDirectory);
17622
+ await mkdir12(path56.dirname(filePath), { recursive: true });
17623
+ const next = {
17624
+ ...ledger,
17625
+ version: 1,
17626
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17474
17627
  };
17628
+ await writeFile15(filePath, `${JSON.stringify(next, null, 2)}
17629
+ `, "utf8");
17630
+ return filePath;
17475
17631
  }
17476
- async function createProjectWithWarmPoolRetry(client, body, options) {
17477
- const maxRetryMs = options?.maxRetryMs ?? WARM_POOL_CREATE_RETRY_BUDGET_MS;
17478
- const startedAt = Date.now();
17479
- while (true) {
17480
- try {
17481
- return await client.createProject(body);
17482
- } catch (error) {
17483
- if (!isWarmPoolCapacityUnavailableError(error)) {
17484
- throw error;
17485
- }
17486
- if (Date.now() - startedAt >= maxRetryMs) {
17487
- throw error;
17488
- }
17489
- await sleep3(warmPoolRetryDelayMs());
17632
+ function resolveResumeStepFromLedger(ledger, orderedSteps) {
17633
+ if (!ledger?.steps?.length) {
17634
+ return void 0;
17635
+ }
17636
+ const byStep = new Map(ledger.steps.map((step) => [step.step, step]));
17637
+ for (const step of orderedSteps) {
17638
+ const record = byStep.get(step);
17639
+ if (!record || record.status !== "succeeded" && record.status !== "skipped") {
17640
+ return step;
17490
17641
  }
17491
17642
  }
17643
+ return void 0;
17492
17644
  }
17493
-
17494
- // src/cli/platform/helpers/dev-portal-settings-kinds.ts
17495
- function isHttpUrl(value) {
17496
- return /^https?:\/\//i.test(value.trim());
17497
- }
17498
- function resolveLogoSettings(logoUrl) {
17499
- const trimmed = logoUrl?.trim();
17500
- if (!trimmed) {
17501
- return { logoKind: "RepoRelative" };
17502
- }
17503
- if (isHttpUrl(trimmed)) {
17504
- return { logoKind: "AbsoluteUrl", logoValue: trimmed };
17505
- }
17506
- return { logoKind: "RepoRelative", logoValue: trimmed };
17645
+ function createEmptyPublishLedger(projectId) {
17646
+ return {
17647
+ version: 1,
17648
+ projectId,
17649
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
17650
+ steps: []
17651
+ };
17507
17652
  }
17508
- function resolveOpenApiSettings(openApiPath) {
17509
- const trimmed = openApiPath?.trim();
17510
- if (!trimmed) {
17511
- return { openApiSourceKind: "RepoRelative" };
17653
+ var StepTimer = class {
17654
+ startedAt;
17655
+ startedAtIso;
17656
+ constructor() {
17657
+ this.startedAt = Date.now();
17658
+ this.startedAtIso = new Date(this.startedAt).toISOString();
17512
17659
  }
17513
- if (isHttpUrl(trimmed)) {
17514
- return { openApiSourceKind: "ExternalUrl", openApiValue: trimmed };
17660
+ finish(detail) {
17661
+ const finishedAt = Date.now();
17662
+ return {
17663
+ startedAt: this.startedAtIso,
17664
+ finishedAt: new Date(finishedAt).toISOString(),
17665
+ durationMs: finishedAt - this.startedAt,
17666
+ detail
17667
+ };
17515
17668
  }
17516
- return { openApiSourceKind: "RepoRelative", openApiValue: trimmed };
17517
- }
17669
+ };
17518
17670
 
17519
17671
  // src/cli/platform/helpers/validate-dev-portal-readiness.ts
17520
17672
  import { lookup as lookup2 } from "node:dns/promises";
@@ -17752,343 +17904,253 @@ async function validateDevPortalReadiness(client, projectId, options) {
17752
17904
  oidcRedirectUri: urls.oidcRedirectUris[0]?.redirectUri ?? `${urls.backendApiBaseUrl}/signin-oidc`,
17753
17905
  stripeWebhookUrl: urls.stripeWebhookUrl,
17754
17906
  stripeWebhookEvents: [...STRIPE_WEBHOOK_EVENTS],
17755
- stripeSuccessUrl: urls.stripeSuccessUrl,
17756
- stripeCancelUrl: urls.stripeCancelUrl
17757
- }
17758
- };
17759
- }
17760
-
17761
- // src/cli/platform/helpers/publish-ledger.ts
17762
- import { mkdir as mkdir11, readFile as readFile15, writeFile as writeFile14 } from "node:fs/promises";
17763
- import path53 from "node:path";
17764
- var PUBLISH_LEDGER_RELATIVE_PATH = ".apitogo/publish-state.json";
17765
- function publishLedgerPath(siteDirectory) {
17766
- return path53.join(path53.resolve(siteDirectory), PUBLISH_LEDGER_RELATIVE_PATH);
17767
- }
17768
- async function readPublishLedger(siteDirectory) {
17769
- const filePath = publishLedgerPath(siteDirectory);
17770
- try {
17771
- const raw = await readFile15(filePath, "utf8");
17772
- const parsed = JSON.parse(raw);
17773
- if (parsed?.version !== 1 || !Array.isArray(parsed.steps)) {
17774
- return null;
17775
- }
17776
- return parsed;
17777
- } catch {
17778
- return null;
17779
- }
17780
- }
17781
- async function writePublishLedger(siteDirectory, ledger) {
17782
- const filePath = publishLedgerPath(siteDirectory);
17783
- await mkdir11(path53.dirname(filePath), { recursive: true });
17784
- const next = {
17785
- ...ledger,
17786
- version: 1,
17787
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
17788
- };
17789
- await writeFile14(filePath, `${JSON.stringify(next, null, 2)}
17790
- `, "utf8");
17791
- return filePath;
17792
- }
17793
- function resolveResumeStepFromLedger(ledger, orderedSteps) {
17794
- if (!ledger?.steps?.length) {
17795
- return void 0;
17796
- }
17797
- const byStep = new Map(ledger.steps.map((step) => [step.step, step]));
17798
- for (const step of orderedSteps) {
17799
- const record = byStep.get(step);
17800
- if (!record || record.status !== "succeeded" && record.status !== "skipped") {
17801
- return step;
17802
- }
17803
- }
17804
- return void 0;
17805
- }
17806
- function createEmptyPublishLedger(projectId) {
17807
- return {
17808
- version: 1,
17809
- projectId,
17810
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
17811
- steps: []
17812
- };
17813
- }
17814
- var StepTimer = class {
17815
- startedAt;
17816
- startedAtIso;
17817
- constructor() {
17818
- this.startedAt = Date.now();
17819
- this.startedAtIso = new Date(this.startedAt).toISOString();
17820
- }
17821
- finish(detail) {
17822
- const finishedAt = Date.now();
17823
- return {
17824
- startedAt: this.startedAtIso,
17825
- finishedAt: new Date(finishedAt).toISOString(),
17826
- durationMs: finishedAt - this.startedAt,
17827
- detail
17828
- };
17829
- }
17830
- };
17831
-
17832
- // src/cli/platform/helpers/wait-for-gateway-actions.ts
17833
- async function waitForGatewayActionsRegistered(client, projectId, options) {
17834
- const minActionCount = options?.minActionCount ?? 1;
17835
- const timeoutMs = options?.timeoutMs ?? 18e4;
17836
- const pollIntervalMs = options?.pollIntervalMs ?? 5e3;
17837
- const started = Date.now();
17838
- while (true) {
17839
- const page = await client.listGatewayActions(projectId, 1, 1);
17840
- const actionCount = page.totalCount;
17841
- if (actionCount >= minActionCount) {
17842
- return { actionCount, timedOut: false };
17843
- }
17844
- if (Date.now() - started >= timeoutMs) {
17845
- return { actionCount, timedOut: true };
17846
- }
17847
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
17848
- }
17849
- }
17850
-
17851
- // src/cli/platform/helpers/deploy-artifact-hash.ts
17852
- import { createHash as createHash2 } from "node:crypto";
17853
- import { createReadStream, promises as fs8 } from "node:fs";
17854
- import path55 from "node:path";
17855
-
17856
- // src/cli/platform/helpers/package-files.ts
17857
- import { promises as fs7 } from "node:fs";
17858
- import path54 from "node:path";
17859
- import ignore from "ignore";
17860
- var DEFAULT_EXCLUDES = /* @__PURE__ */ new Set([
17861
- "node_modules",
17862
- ".git",
17863
- ".DS_Store",
17864
- "dist",
17865
- "bin",
17866
- "obj"
17867
- ]);
17868
- async function loadGitignore(rootDir) {
17869
- const ig = ignore();
17870
- ig.add(["node_modules/", ".git/", "dist/", "bin/", "obj/"]);
17871
- const gitignorePath = path54.join(rootDir, ".gitignore");
17872
- try {
17873
- const content = await fs7.readFile(gitignorePath, "utf8");
17874
- ig.add(content);
17875
- } catch {
17876
- }
17877
- return ig;
17878
- }
17879
- async function collectFiles(rootDir, relativeDir, ig, files) {
17880
- const absDir = path54.join(rootDir, relativeDir);
17881
- const entries = await fs7.readdir(absDir, { withFileTypes: true });
17882
- for (const entry of entries) {
17883
- const rel = relativeDir ? path54.join(relativeDir, entry.name) : entry.name;
17884
- const relPosix = rel.split(path54.sep).join("/");
17885
- if (DEFAULT_EXCLUDES.has(entry.name) && !relativeDir) {
17886
- continue;
17907
+ stripeSuccessUrl: urls.stripeSuccessUrl,
17908
+ stripeCancelUrl: urls.stripeCancelUrl
17887
17909
  }
17888
- if (ig.ignores(relPosix)) {
17889
- continue;
17910
+ };
17911
+ }
17912
+
17913
+ // src/cli/platform/helpers/wait-for-gateway-actions.ts
17914
+ async function waitForGatewayActionsRegistered(client, projectId, options) {
17915
+ const minActionCount = options?.minActionCount ?? 1;
17916
+ const timeoutMs = options?.timeoutMs ?? 18e4;
17917
+ const pollIntervalMs = options?.pollIntervalMs ?? 5e3;
17918
+ const started = Date.now();
17919
+ while (true) {
17920
+ const page = await client.listGatewayActions(projectId, 1, 1);
17921
+ const actionCount = page.totalCount;
17922
+ if (actionCount >= minActionCount) {
17923
+ return { actionCount, timedOut: false };
17890
17924
  }
17891
- if (entry.isDirectory()) {
17892
- await collectFiles(rootDir, rel, ig, files);
17893
- } else if (entry.isFile()) {
17894
- files.push(rel);
17925
+ if (Date.now() - started >= timeoutMs) {
17926
+ return { actionCount, timedOut: true };
17895
17927
  }
17928
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
17896
17929
  }
17897
17930
  }
17898
- async function collectPackageableFiles(sourceDirectory) {
17899
- const resolved = path54.resolve(sourceDirectory);
17900
- const stat4 = await fs7.stat(resolved).catch(() => null);
17901
- if (!stat4?.isDirectory()) {
17902
- throw new Error(`sourceDirectory is not a directory: ${resolved}`);
17931
+
17932
+ // src/cli/platform/helpers/status.ts
17933
+ function normalizeInfrastructureStatus(platformStatus) {
17934
+ switch (platformStatus.toLowerCase()) {
17935
+ case "completed":
17936
+ case "completed_with_warnings":
17937
+ return "succeeded";
17938
+ case "in_progress":
17939
+ return "running";
17940
+ case "failed":
17941
+ return "failed";
17942
+ case "pending":
17943
+ case "none":
17944
+ default:
17945
+ return "pending";
17903
17946
  }
17904
- const ig = await loadGitignore(resolved);
17905
- const files = [];
17906
- await collectFiles(resolved, "", ig, files);
17907
- if (files.length === 0) {
17908
- throw new Error(`No files to package in ${resolved}`);
17947
+ }
17948
+ function normalizeDeploymentStatus(platformStatus) {
17949
+ switch (platformStatus.toLowerCase()) {
17950
+ case "healthy":
17951
+ return "succeeded";
17952
+ case "failed":
17953
+ case "cancelled":
17954
+ return "failed";
17955
+ case "queued":
17956
+ case "validating":
17957
+ case "building":
17958
+ case "deploying":
17959
+ return "running";
17960
+ default:
17961
+ return "pending";
17909
17962
  }
17910
- return files.sort();
17911
17963
  }
17912
-
17913
- // src/cli/platform/helpers/deploy-artifact-hash.ts
17914
- async function sha256File(filePath) {
17915
- const hash2 = createHash2("sha256");
17916
- await new Promise((resolve, reject) => {
17917
- const stream = createReadStream(filePath);
17918
- stream.on("data", (chunk) => hash2.update(chunk));
17919
- stream.on("error", reject);
17920
- stream.on("end", () => resolve());
17921
- });
17922
- return hash2.digest("hex");
17964
+ function isTerminalInfrastructureStatus(platformStatus) {
17965
+ const s = platformStatus.toLowerCase();
17966
+ return s === "completed" || s === "completed_with_warnings" || s === "failed";
17923
17967
  }
17924
- async function hashDirectoryContents(rootDirectory) {
17925
- const resolved = path55.resolve(rootDirectory);
17926
- const files = (await collectPackageableFiles(resolved)).sort((a, b) => a.localeCompare(b));
17927
- const hash2 = createHash2("sha256");
17928
- for (const rel of files) {
17929
- hash2.update(rel);
17930
- hash2.update("\0");
17931
- hash2.update(await fs8.readFile(path55.join(resolved, rel)));
17932
- hash2.update("\0");
17933
- }
17934
- return hash2.digest("hex");
17968
+ function isTerminalDeploymentStatus(platformStatus) {
17969
+ const s = platformStatus.toLowerCase();
17970
+ return s === "healthy" || s === "failed" || s === "cancelled";
17935
17971
  }
17936
- async function hashDevPortalSiteSources(siteDirectory, options) {
17937
- const resolved = path55.resolve(siteDirectory);
17938
- const hash2 = createHash2("sha256");
17939
- const candidates = [
17940
- "apitogo.config.json",
17941
- "apitogo.config.ts",
17942
- "package.json",
17943
- "zudoku.config.ts",
17944
- "zudoku.config.tsx"
17945
- ];
17946
- for (const name of candidates) {
17947
- const full = path55.join(resolved, name);
17948
- try {
17949
- hash2.update(name);
17950
- hash2.update("\0");
17951
- hash2.update(await fs8.readFile(full));
17952
- hash2.update("\0");
17953
- } catch {
17972
+ var DEFAULT_POLL_INTERVAL_MS = 5e3;
17973
+ var DEFAULT_POLL_TIMEOUT_MS = 15 * 60 * 1e3;
17974
+
17975
+ // src/cli/platform/helpers/poll.ts
17976
+ function sleep2(ms) {
17977
+ return new Promise((resolve) => setTimeout(resolve, ms));
17978
+ }
17979
+ async function pollUntilTerminal(options) {
17980
+ const timeoutMs = options.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS;
17981
+ const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
17982
+ const deadline = Date.now() + timeoutMs;
17983
+ const stepLabel = options.stepLabel ?? "Job";
17984
+ let last;
17985
+ while (Date.now() <= deadline) {
17986
+ if (options.progress?.isCancelled()) {
17987
+ if (!last) {
17988
+ throw new Error("Operation cancelled before first poll result.");
17989
+ }
17990
+ return {
17991
+ status: options.getStatus(last),
17992
+ value: last,
17993
+ timedOut: true,
17994
+ cancelled: true
17995
+ };
17954
17996
  }
17955
- }
17956
- for (const dir of ["pages", "landing", "src"]) {
17957
- const full = path55.join(resolved, dir);
17958
17997
  try {
17959
- const st = await fs8.stat(full);
17960
- if (!st.isDirectory()) continue;
17961
- hash2.update(dir);
17962
- hash2.update("\0");
17963
- hash2.update(await hashDirectoryContents(full));
17964
- hash2.update("\0");
17965
- } catch {
17998
+ last = await options.poll();
17999
+ } catch (error) {
18000
+ const handled = options.onPollError?.(error, last);
18001
+ if (handled) {
18002
+ return handled;
18003
+ }
18004
+ throw error;
18005
+ }
18006
+ options.onPoll?.(last);
18007
+ if (options.progress && isPollableJobValue(last)) {
18008
+ await reportPollProgress(options.progress, stepLabel, last);
18009
+ }
18010
+ if (options.isTerminal(last)) {
18011
+ return {
18012
+ status: options.getStatus(last),
18013
+ value: last,
18014
+ timedOut: false
18015
+ };
18016
+ }
18017
+ if (Date.now() + intervalMs > deadline) {
18018
+ break;
17966
18019
  }
18020
+ await sleep2(intervalMs);
17967
18021
  }
17968
- const gatewayUrl = options?.gatewayUrl?.trim().replace(/\/+$/, "");
17969
- if (gatewayUrl) {
17970
- hash2.update("gatewayUrl");
17971
- hash2.update("\0");
17972
- hash2.update(gatewayUrl);
17973
- hash2.update("\0");
18022
+ if (!last) {
18023
+ throw new Error("Polling produced no results.");
17974
18024
  }
17975
- return hash2.digest("hex");
18025
+ return {
18026
+ status: options.getStatus(last),
18027
+ value: last,
18028
+ timedOut: true
18029
+ };
18030
+ }
18031
+ function isPollableJobValue(value) {
18032
+ return typeof value === "object" && value !== null && "status" in value;
17976
18033
  }
17977
18034
 
17978
- // src/cli/platform/helpers/openapi-gateway-servers.ts
17979
- import { mkdir as mkdir12, readFile as readFile16, writeFile as writeFile15 } from "node:fs/promises";
17980
- import path56 from "node:path";
17981
- import YAML3 from "yaml";
17982
- var GATEWAY_URL_ENV_KEY = "APITOGO_GATEWAY_URL";
17983
- var PUBLISH_OPENAPI_RELATIVE = ".apitogo/openapi-publish.json";
17984
- function normalizeGatewayUrl(gatewayUrl) {
17985
- return gatewayUrl.trim().replace(/\/+$/, "");
18035
+ // src/cli/platform/helpers/warm-pool.ts
18036
+ var WARM_POOL_CAPACITY_TEMPORARILY_UNAVAILABLE = "WARM_POOL_CAPACITY_TEMPORARILY_UNAVAILABLE";
18037
+ var COLD_MODULE_INFRA_RESOURCE_TYPES = /* @__PURE__ */ new Set([
18038
+ "AzureProjectInfrastructure",
18039
+ "DevPortalInfrastructure",
18040
+ "BackendInfrastructure"
18041
+ ]);
18042
+ var WARM_POOL_CREATE_RETRY_BUDGET_MS = 10 * 60 * 1e3;
18043
+ function sleep3(ms) {
18044
+ return new Promise((resolve) => setTimeout(resolve, ms));
17986
18045
  }
17987
- function withGatewayServers(doc, gatewayUrl) {
17988
- const url2 = normalizeGatewayUrl(gatewayUrl);
17989
- return {
17990
- ...doc,
17991
- servers: [{ url: url2, description: "API gateway" }]
17992
- };
18046
+ function warmPoolRetryDelayMs() {
18047
+ return 6e4 + Math.floor(Math.random() * 30001);
17993
18048
  }
17994
- async function resolveProjectGatewayUrl(client, projectId) {
17995
- const project = await client.getProject(projectId);
17996
- const fromProject = project.gatewayUrl?.trim();
17997
- if (fromProject) {
17998
- return normalizeGatewayUrl(fromProject);
18049
+ function isWarmPoolCapacityUnavailableError(error) {
18050
+ if (!(error instanceof PlatformApiError)) {
18051
+ return false;
17999
18052
  }
18000
- try {
18001
- const summary = await client.getBackendSummary(projectId);
18002
- const fromBackend = summary.backendUrl?.trim();
18003
- if (fromBackend) {
18004
- return normalizeGatewayUrl(fromBackend);
18005
- }
18006
- } catch {
18053
+ if (error.status !== 503) {
18054
+ return false;
18007
18055
  }
18008
- return null;
18056
+ const code = error.problem?.code;
18057
+ return code === WARM_POOL_CAPACITY_TEMPORARILY_UNAVAILABLE;
18009
18058
  }
18010
- async function readOpenApiDocument(filePath) {
18011
- const content = await readFile16(filePath, "utf8");
18012
- const ext = path56.extname(filePath).toLowerCase();
18013
- const parsed = ext === ".json" ? JSON.parse(content) : YAML3.parse(content);
18014
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
18015
- throw new Error(`OpenAPI document must be an object: ${filePath}`);
18059
+ function isWarmPoolInfrastructureReady(project) {
18060
+ return project.infrastructureStatus?.toLowerCase() === "ready";
18061
+ }
18062
+ function isWarmPoolInfrastructureFailed(project) {
18063
+ return project.infrastructureStatus?.toLowerCase() === "failed";
18064
+ }
18065
+ function isWarmPoolInfrastructureTerminal(project) {
18066
+ const status2 = project.infrastructureStatus?.toLowerCase() ?? "";
18067
+ return status2 === "ready" || status2 === "failed" || status2 === "deleting";
18068
+ }
18069
+ function shouldSkipColdModuleInfrastructureWait(project, infraStatus) {
18070
+ if (isWarmPoolInfrastructureReady(project)) {
18071
+ return true;
18072
+ }
18073
+ const resourceType = infraStatus?.resourceType;
18074
+ if (resourceType === "WarmPoolBinding") {
18075
+ return true;
18076
+ }
18077
+ if (resourceType && COLD_MODULE_INFRA_RESOURCE_TYPES.has(resourceType)) {
18078
+ return false;
18079
+ }
18080
+ return false;
18081
+ }
18082
+ function resolvePortalBackendFqdn(project) {
18083
+ const portalApiUrl = project.portalApiUrl?.trim();
18084
+ if (!portalApiUrl) {
18085
+ return null;
18086
+ }
18087
+ try {
18088
+ return new URL(portalApiUrl).hostname;
18089
+ } catch {
18090
+ return null;
18016
18091
  }
18017
- return parsed;
18018
18092
  }
18019
- async function materializePublishOpenApiWithGateway(siteDirectory, sourceOpenApiPath, gatewayUrl) {
18020
- const site = path56.resolve(siteDirectory);
18021
- const source = path56.resolve(sourceOpenApiPath);
18022
- const relativeInput = PUBLISH_OPENAPI_RELATIVE;
18023
- const outputPath = path56.join(site, ...relativeInput.split("/"));
18024
- const doc = await readOpenApiDocument(source);
18025
- const rewritten = withGatewayServers(doc, gatewayUrl);
18026
- await mkdir12(path56.dirname(outputPath), { recursive: true });
18027
- await writeFile15(outputPath, `${JSON.stringify(rewritten, null, 2)}
18028
- `, "utf8");
18029
- return { outputPath, relativeInput };
18093
+ var RESERVED_ENDPOINT_URL_AGENT_INSTRUCTION = "Do not share siteUrl, portalApiUrl, gatewayUrl, or any warm-pool hostnames with the user yet \u2014 those addresses are reserved infrastructure and show Azure's default page until the site or backend is actually deployed. Share live URLs only after publish_dev_portal, deploy_backend, or verify_dev_portal_live succeeds.";
18094
+ function omitReservedEndpointUrls(project) {
18095
+ const {
18096
+ siteUrl: _siteUrl,
18097
+ portalApiUrl: _portalApiUrl,
18098
+ gatewayUrl: _gatewayUrl,
18099
+ ...rest
18100
+ } = project;
18101
+ return rest;
18030
18102
  }
18031
- async function pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput) {
18032
- const configPath = path56.join(path56.resolve(siteDirectory), APITOGO_CONFIG_FILENAME2);
18033
- const raw = await readFile16(configPath, "utf8");
18034
- const config2 = JSON.parse(raw);
18035
- const previousApis = config2.apis;
18036
- const previousBackend = config2.backend;
18037
- const apis = Array.isArray(config2.apis) ? [...config2.apis] : [];
18038
- const first = apis[0] && typeof apis[0] === "object" ? { ...apis[0] } : { type: "file", path: "api" };
18039
- first.type = first.type ?? "file";
18040
- first.input = relativeInput;
18041
- first.path = first.path ?? "api";
18042
- config2.apis = [first, ...apis.slice(1)];
18043
- const backend = config2.backend && typeof config2.backend === "object" ? { ...config2.backend } : {};
18044
- backend.openApiPath = relativeInput;
18045
- config2.backend = backend;
18046
- await writeFile15(configPath, `${JSON.stringify(config2, null, 2)}
18047
- `, "utf8");
18048
- return {
18049
- restore: async () => {
18050
- const currentRaw = await readFile16(configPath, "utf8");
18051
- const current = JSON.parse(currentRaw);
18052
- if (previousApis === void 0) {
18053
- delete current.apis;
18054
- } else {
18055
- current.apis = previousApis;
18103
+ async function waitForWarmPoolBinding(client, projectId, options) {
18104
+ const poll = await pollUntilTerminal({
18105
+ poll: () => client.getProject(projectId),
18106
+ isTerminal: (project2) => isWarmPoolInfrastructureTerminal(project2),
18107
+ getStatus: (project2) => {
18108
+ if (isWarmPoolInfrastructureFailed(project2)) {
18109
+ return "failed";
18056
18110
  }
18057
- if (previousBackend === void 0) {
18058
- delete current.backend;
18059
- } else {
18060
- current.backend = previousBackend;
18111
+ if (isWarmPoolInfrastructureReady(project2)) {
18112
+ return "succeeded";
18061
18113
  }
18062
- await writeFile15(configPath, `${JSON.stringify(current, null, 2)}
18063
- `, "utf8");
18064
- }
18114
+ return "running";
18115
+ },
18116
+ timeoutMs: options?.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
18117
+ intervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
18118
+ stepLabel: "Warm pool binding",
18119
+ progress: options?.progress
18120
+ });
18121
+ const project = poll.value;
18122
+ let status2;
18123
+ if (poll.timedOut) {
18124
+ status2 = "running";
18125
+ } else if (isWarmPoolInfrastructureFailed(project)) {
18126
+ status2 = "failed";
18127
+ } else if (isWarmPoolInfrastructureReady(project)) {
18128
+ status2 = "succeeded";
18129
+ } else {
18130
+ status2 = "running";
18131
+ }
18132
+ return {
18133
+ status: status2,
18134
+ timedOut: poll.timedOut,
18135
+ project
18065
18136
  };
18066
18137
  }
18067
- async function prepareOpenApiServersForGatewayBuild(options) {
18068
- const siteDirectory = path56.resolve(options.siteDirectory);
18069
- let openApiPath = resolveBackendOpenApiPath(options.manifest?.backend?.openApiPath, {
18070
- siteDirectory,
18071
- backendSourceDirectory: options.manifest?.backend?.sourceDirectory
18072
- });
18073
- if (!openApiPath) {
18138
+ async function createProjectWithWarmPoolRetry(client, body, options) {
18139
+ const maxRetryMs = options?.maxRetryMs ?? WARM_POOL_CREATE_RETRY_BUDGET_MS;
18140
+ const startedAt = Date.now();
18141
+ while (true) {
18074
18142
  try {
18075
- const raw = await readFile16(path56.join(siteDirectory, APITOGO_CONFIG_FILENAME2), "utf8");
18076
- const config2 = JSON.parse(raw);
18077
- const input5 = Array.isArray(config2.apis) ? config2.apis[0]?.input?.trim() : void 0;
18078
- openApiPath = resolveBackendOpenApiPath(input5, { siteDirectory });
18079
- } catch {
18143
+ return await client.createProject(body);
18144
+ } catch (error) {
18145
+ if (!isWarmPoolCapacityUnavailableError(error)) {
18146
+ throw error;
18147
+ }
18148
+ if (Date.now() - startedAt >= maxRetryMs) {
18149
+ throw error;
18150
+ }
18151
+ await sleep3(warmPoolRetryDelayMs());
18080
18152
  }
18081
18153
  }
18082
- if (!openApiPath) {
18083
- return null;
18084
- }
18085
- const { outputPath, relativeInput } = await materializePublishOpenApiWithGateway(
18086
- siteDirectory,
18087
- openApiPath,
18088
- options.gatewayUrl
18089
- );
18090
- const { restore } = await pointSiteApisAtPublishOpenApi(siteDirectory, relativeInput);
18091
- return { relativeInput, outputPath, restore };
18092
18154
  }
18093
18155
 
18094
18156
  // src/cli/platform/helpers/backend-packaging.ts
@@ -19506,7 +19568,9 @@ function normalizeModuleType2(type) {
19506
19568
  var READY_MODULE_STATUSES2 = /* @__PURE__ */ new Set(["provisioned", "configured", "started"]);
19507
19569
  function isModuleProvisioned(project, moduleType) {
19508
19570
  const normalized = normalizeModuleType2(moduleType);
19509
- const module = project.modules.find((m) => normalizeModuleType2(m.type) === normalized);
19571
+ const module = project.modules.find(
19572
+ (m) => normalizeModuleType2(m.type) === normalized
19573
+ );
19510
19574
  if (!module?.status) {
19511
19575
  return false;
19512
19576
  }
@@ -19528,10 +19592,14 @@ async function ensureModuleProvisioned(client, config2, projectId, moduleType, w
19528
19592
  }
19529
19593
  await progress?.onStep(`Activating module ${moduleType}`);
19530
19594
  try {
19531
- await handleActivateModule(client, { ...config2, defaultProjectId: projectId }, {
19532
- projectId,
19533
- moduleType
19534
- });
19595
+ await handleActivateModule(
19596
+ client,
19597
+ { ...config2, defaultProjectId: projectId },
19598
+ {
19599
+ projectId,
19600
+ moduleType
19601
+ }
19602
+ );
19535
19603
  } catch (error) {
19536
19604
  if (isAlreadyProvisionedError(error)) {
19537
19605
  return;
@@ -19567,7 +19635,9 @@ async function ensureModuleProvisioned(client, config2, projectId, moduleType, w
19567
19635
  }
19568
19636
  async function ensureModulesProvisioned(client, config2, projectId, moduleTypes, wait, timeoutMs, pollIntervalMs, progress) {
19569
19637
  const project = await client.getProject(projectId);
19570
- const pending = moduleTypes.filter((moduleType) => !isModuleProvisioned(project, moduleType));
19638
+ const pending = moduleTypes.filter(
19639
+ (moduleType) => !isModuleProvisioned(project, moduleType)
19640
+ );
19571
19641
  if (pending.length === 0) {
19572
19642
  return;
19573
19643
  }
@@ -19612,213 +19682,68 @@ async function resolveProjectIdForPublish(client, config2, args, wait, progress,
19612
19682
  return {
19613
19683
  blocked: true,
19614
19684
  payload: {
19615
- status: "blocked",
19616
- reason: "project_not_found",
19617
- projectId,
19618
- userMessage: `Project ${projectId} was not found. It may have been deleted, or it belongs to a different account than the one you're currently logged in as.`,
19619
- nextStep: "create_or_fix_project"
19620
- }
19621
- };
19622
- }
19623
- throw error;
19624
- }
19625
- if (!isPlatformPlanUnselected(project.planTier)) {
19626
- await ensureModulesProvisioned(
19627
- client,
19628
- config2,
19629
- projectId,
19630
- ["apihub", "dev-portal"],
19631
- wait,
19632
- timeoutMs,
19633
- pollIntervalMs,
19634
- progress
19635
- );
19636
- }
19637
- completedSteps.push({ step: "resolve_project", status: "succeeded", detail: { projectId } });
19638
- return { blocked: false, projectId, completedSteps };
19639
- }
19640
- if (!args.projectName?.trim()) {
19641
- throw new Error("projectName is required when projectId is not provided.");
19642
- }
19643
- await progress?.onStep("Creating project and provisioning modules");
19644
- const bootstrapJson = await handleBootstrapProject(
19645
- client,
19646
- config2,
19647
- {
19648
- name: args.projectName.trim(),
19649
- modules: ["apihub", "dev-portal"],
19650
- selectFreePlan: false,
19651
- configureGatewayBaseline: false,
19652
- wait,
19653
- timeoutMs,
19654
- pollIntervalMs
19655
- },
19656
- progress
19657
- );
19658
- const bootstrap = parseHandlerResult(bootstrapJson, "bootstrap_project");
19659
- completedSteps.push({ step: "bootstrap_project", status: "succeeded", detail: bootstrap });
19660
- return { blocked: false, projectId: bootstrap.projectId, completedSteps };
19661
- }
19662
-
19663
- // src/cli/platform/tools/dev-portal.ts
19664
- import { promises as fs11 } from "node:fs";
19665
- import path61 from "node:path";
19666
-
19667
- // src/cli/platform/helpers/tarball.ts
19668
- import { createReadStream as createReadStream3, createWriteStream as createWriteStream3, promises as fs10 } from "node:fs";
19669
- import { tmpdir as tmpdir3 } from "node:os";
19670
- import path59 from "node:path";
19671
- import { TarArchive } from "archiver";
19672
- var DEV_PORTAL_DEPLOY_ARCHIVE_ROOT = "dist";
19673
- function normalizeArchiveRoot(archiveRoot) {
19674
- const trimmed = archiveRoot.trim().replace(/^\/+|\/+$/g, "");
19675
- if (!trimmed) {
19676
- throw new Error("archiveRoot must not be empty.");
19677
- }
19678
- return `${trimmed}/`;
19679
- }
19680
- async function packageDirectoryAsTarGz(sourceDirectory, options) {
19681
- const resolved = path59.resolve(sourceDirectory);
19682
- const files = await collectPackageableFiles(resolved);
19683
- const entryPrefix = options?.archiveRoot ? normalizeArchiveRoot(options.archiveRoot) : "";
19684
- const archivePath = path59.join(
19685
- tmpdir3(),
19686
- `apitogo-docs-${Date.now()}-${Math.random().toString(36).slice(2)}.tgz`
19687
- );
19688
- await new Promise((resolve, reject) => {
19689
- const output4 = createWriteStream3(archivePath);
19690
- const archive = new TarArchive({ gzip: true, gzipOptions: { level: 6 } });
19691
- output4.on("close", () => resolve());
19692
- archive.on("error", reject);
19693
- output4.on("error", reject);
19694
- archive.pipe(output4);
19695
- for (const rel of files) {
19696
- const entryName = entryPrefix + rel.split(path59.sep).join("/");
19697
- archive.append(createReadStream3(path59.join(resolved, rel)), { name: entryName });
19698
- }
19699
- void archive.finalize();
19700
- });
19701
- const { size } = await fs10.stat(archivePath);
19702
- return { archivePath, byteSize: size };
19703
- }
19704
- async function cleanupArchive(archivePath) {
19705
- await fs10.unlink(archivePath).catch(() => void 0);
19706
- }
19707
- function isTarGzPath(filePath) {
19708
- const normalized = filePath.toLowerCase();
19709
- return normalized.endsWith(".tgz") || normalized.endsWith(".tar.gz");
19710
- }
19711
-
19712
- // src/cli/platform/helpers/dev-portal-backend-job.ts
19713
- var DEV_PORTAL_BACKEND_UPDATE_RESOURCE_TYPE = "DevPortalBackendUpdate";
19714
- var ACTIVE_JOB_STATUSES = /* @__PURE__ */ new Set(["Pending", "InProgress"]);
19715
- var RECENT_COMPLETED_WINDOW_MS = 3e4;
19716
- var BACKEND_UPDATE_JOB_APPEARANCE_TIMEOUT_MS = 3e4;
19717
- function isBackendUpdateJob(job) {
19718
- return job.resourceType === DEV_PORTAL_BACKEND_UPDATE_RESOURCE_TYPE;
19719
- }
19720
- function isActiveBackendUpdateJob(job) {
19721
- return isBackendUpdateJob(job) && ACTIVE_JOB_STATUSES.has(job.status);
19722
- }
19723
- function isRecentlyCompletedBackendUpdateJob(job) {
19724
- if (!isBackendUpdateJob(job) || job.status !== "Completed" || !job.completedAt) {
19725
- return false;
19726
- }
19727
- const completedAtMs = Date.parse(job.completedAt);
19728
- if (Number.isNaN(completedAtMs)) {
19729
- return false;
19730
- }
19731
- return Date.now() - completedAtMs <= RECENT_COMPLETED_WINDOW_MS;
19732
- }
19733
- function sortJobsNewestFirst(jobs) {
19734
- return [...jobs].sort(
19735
- (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)
19736
- );
19737
- }
19738
- function findBackendUpdateJob(jobs, options) {
19739
- const sorted = sortJobsNewestFirst(jobs);
19740
- if (options?.jobId) {
19741
- return sorted.find((job) => job.id === options.jobId) ?? null;
19742
- }
19743
- const active = sorted.find(isActiveBackendUpdateJob);
19744
- if (active) {
19745
- return active;
19746
- }
19747
- const baseline = options?.baselineJobIds;
19748
- if (baseline) {
19749
- const newlyCompleted = sorted.find(
19750
- (job) => isBackendUpdateJob(job) && !baseline.has(job.id) && (ACTIVE_JOB_STATUSES.has(job.status) || job.status === "Completed")
19751
- );
19752
- if (newlyCompleted) {
19753
- return newlyCompleted;
19685
+ status: "blocked",
19686
+ reason: "project_not_found",
19687
+ projectId,
19688
+ userMessage: `Project ${projectId} was not found. It may have been deleted, or it belongs to a different account than the one you're currently logged in as.`,
19689
+ nextStep: "create_or_fix_project"
19690
+ }
19691
+ };
19692
+ }
19693
+ throw error;
19754
19694
  }
19755
- }
19756
- const recentlyCompleted = sorted.find(isRecentlyCompletedBackendUpdateJob);
19757
- if (recentlyCompleted) {
19758
- return recentlyCompleted;
19759
- }
19760
- return null;
19761
- }
19762
- async function discoverBackendUpdateJob(options) {
19763
- if (options.jobId) {
19764
- const result = await options.client.listProvisioningJobs(options.projectId);
19765
- const job = findBackendUpdateJob(result.jobs, { jobId: options.jobId });
19766
- if (!job) {
19767
- throw new Error(`DevPortalBackendUpdate job not found: ${options.jobId}`);
19695
+ if (!isPlatformPlanUnselected(project.planTier)) {
19696
+ await ensureModulesProvisioned(
19697
+ client,
19698
+ config2,
19699
+ projectId,
19700
+ ["apihub", "dev-portal"],
19701
+ wait,
19702
+ timeoutMs,
19703
+ pollIntervalMs,
19704
+ progress
19705
+ );
19768
19706
  }
19769
- return { job, timedOut: false };
19770
- }
19771
- const pollOnce = async () => {
19772
- const result = await options.client.listProvisioningJobs(options.projectId);
19773
- return findBackendUpdateJob(result.jobs, {
19774
- baselineJobIds: options.baselineJobIds
19707
+ completedSteps.push({
19708
+ step: "resolve_project",
19709
+ status: "succeeded",
19710
+ detail: { projectId }
19775
19711
  });
19776
- };
19777
- const first = await pollOnce();
19778
- if (first) {
19779
- return { job: first, timedOut: false };
19712
+ return { blocked: false, projectId, completedSteps };
19780
19713
  }
19781
- if (!options.baselineJobIds) {
19782
- return { job: null, timedOut: false };
19714
+ if (!args.projectName?.trim()) {
19715
+ throw new Error("projectName is required when projectId is not provided.");
19783
19716
  }
19784
- const appearanceTimeoutMs = Math.min(
19785
- options.timeoutMs ?? BACKEND_UPDATE_JOB_APPEARANCE_TIMEOUT_MS,
19786
- BACKEND_UPDATE_JOB_APPEARANCE_TIMEOUT_MS
19717
+ await progress?.onStep("Creating project and provisioning modules");
19718
+ const bootstrapJson = await handleBootstrapProject(
19719
+ client,
19720
+ config2,
19721
+ {
19722
+ name: args.projectName.trim(),
19723
+ modules: ["apihub", "dev-portal"],
19724
+ selectFreePlan: false,
19725
+ configureGatewayBaseline: false,
19726
+ wait,
19727
+ timeoutMs,
19728
+ pollIntervalMs
19729
+ },
19730
+ progress
19787
19731
  );
19788
- const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
19789
- const deadline = Date.now() + appearanceTimeoutMs;
19790
- while (Date.now() <= deadline) {
19791
- const job = await pollOnce();
19792
- if (job) {
19793
- return { job, timedOut: false };
19794
- }
19795
- if (Date.now() + pollIntervalMs > deadline) {
19796
- break;
19797
- }
19798
- await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
19799
- }
19800
- return { job: null, timedOut: false };
19801
- }
19802
- async function pollBackendUpdateJobStatus(client, projectId, jobId, options) {
19803
- return pollUntilTerminal({
19804
- poll: () => client.getDevPortalDeployStatus(projectId, jobId),
19805
- isTerminal: (value) => isTerminalInfrastructureStatus(value.status),
19806
- getStatus: (value) => normalizeInfrastructureStatus(value.status),
19807
- timeoutMs: options?.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
19808
- intervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
19809
- stepLabel: "Dev portal backend update"
19732
+ const bootstrap = parseHandlerResult(bootstrapJson, "bootstrap_project");
19733
+ completedSteps.push({
19734
+ step: "bootstrap_project",
19735
+ status: "succeeded",
19736
+ detail: bootstrap
19810
19737
  });
19738
+ return { blocked: false, projectId: bootstrap.projectId, completedSteps };
19811
19739
  }
19812
- async function snapshotBackendUpdateJobIds(client, projectId) {
19813
- const result = await client.listProvisioningJobs(projectId);
19814
- return new Set(
19815
- result.jobs.filter(isBackendUpdateJob).map((job) => job.id)
19816
- );
19817
- }
19740
+
19741
+ // src/cli/platform/tools/dev-portal-site-build.ts
19742
+ import path62 from "node:path";
19818
19743
 
19819
19744
  // src/cli/platform/helpers/dev-portal-site-api-url.ts
19820
19745
  import { readdir as readdir2, readFile as readFile17, writeFile as writeFile16 } from "node:fs/promises";
19821
- import path60 from "node:path";
19746
+ import path59 from "node:path";
19822
19747
  var SITE_API_BASE_URL_ENV_KEY = "VITE_APITOGO_AUTHENTICATION_API_BASE_URL";
19823
19748
  var PLACEHOLDER_HOST_MARKERS = [
19824
19749
  "dp-example.azurecontainerapps.io",
@@ -19836,10 +19761,10 @@ function isPlaceholderDevPortalApiUrl(apiBaseUrl) {
19836
19761
  return PLACEHOLDER_HOST_MARKERS.some((marker) => normalized.includes(marker));
19837
19762
  }
19838
19763
  async function readSiteApiBaseUrl(sourceDirectory) {
19839
- const resolved = path60.resolve(sourceDirectory);
19764
+ const resolved = path59.resolve(sourceDirectory);
19840
19765
  for (const envFile of [".env", ".env.production"]) {
19841
19766
  try {
19842
- const content = await readFile17(path60.join(resolved, envFile), "utf8");
19767
+ const content = await readFile17(path59.join(resolved, envFile), "utf8");
19843
19768
  const lines = [...content.matchAll(ENV_LINE_GLOBAL_RE)];
19844
19769
  if (lines.length === 0) {
19845
19770
  continue;
@@ -19864,11 +19789,11 @@ async function readSiteApiBaseUrl(sourceDirectory) {
19864
19789
  return null;
19865
19790
  }
19866
19791
  async function patchSiteApiBaseUrl(sourceDirectory, apiBaseUrl) {
19867
- const resolved = path60.resolve(sourceDirectory);
19792
+ const resolved = path59.resolve(sourceDirectory);
19868
19793
  const line = `${SITE_API_BASE_URL_ENV_KEY}=${apiBaseUrl}`;
19869
19794
  let patchedExisting = false;
19870
19795
  for (const envFile of [".env.production", ".env"]) {
19871
- const envPath = path60.join(resolved, envFile);
19796
+ const envPath = path59.join(resolved, envFile);
19872
19797
  try {
19873
19798
  const content = await readFile17(envPath, "utf8");
19874
19799
  const withoutAssignments = content.replace(ENV_LINE_GLOBAL_RE, "").replace(/\n{3,}/g, "\n\n").trimEnd();
@@ -19882,7 +19807,7 @@ ${line}
19882
19807
  }
19883
19808
  }
19884
19809
  if (!patchedExisting) {
19885
- await writeFile16(path60.join(resolved, ".env"), `${line}
19810
+ await writeFile16(path59.join(resolved, ".env"), `${line}
19886
19811
  `);
19887
19812
  }
19888
19813
  }
@@ -19890,7 +19815,7 @@ function tryProjectId(explicit, manifestProjectId, config2) {
19890
19815
  return explicit?.trim() || manifestProjectId?.trim() || config2.defaultProjectId?.trim() || void 0;
19891
19816
  }
19892
19817
  async function ensureLiveSiteApiBaseUrlForBuild(_client, config2, args, resolveFromProject) {
19893
- const sourceDirectory = path60.resolve(args.sourceDirectory);
19818
+ const sourceDirectory = path59.resolve(args.sourceDirectory);
19894
19819
  const explicit = args.apiBaseUrl?.trim();
19895
19820
  if (explicit && !isPlaceholderDevPortalApiUrl(explicit)) {
19896
19821
  await patchSiteApiBaseUrl(sourceDirectory, explicit);
@@ -19914,63 +19839,219 @@ async function ensureLiveSiteApiBaseUrlForBuild(_client, config2, args, resolveF
19914
19839
  `Resolved portal API URL is still a placeholder (${resolved}). Ensure the dev-portal backend is provisioned.`
19915
19840
  );
19916
19841
  }
19917
- const normalized = resolved.replace(/\/+$/, "");
19918
- await patchSiteApiBaseUrl(sourceDirectory, normalized);
19919
- return normalized;
19842
+ const normalized = resolved.replace(/\/+$/, "");
19843
+ await patchSiteApiBaseUrl(sourceDirectory, normalized);
19844
+ return normalized;
19845
+ }
19846
+ async function* walkFiles(root) {
19847
+ let entries;
19848
+ try {
19849
+ entries = await readdir2(root, { withFileTypes: true });
19850
+ } catch {
19851
+ return;
19852
+ }
19853
+ for (const entry of entries) {
19854
+ const full = path59.join(root, entry.name);
19855
+ if (entry.isDirectory()) {
19856
+ if (entry.name === "node_modules" || entry.name === ".git") {
19857
+ continue;
19858
+ }
19859
+ yield* walkFiles(full);
19860
+ } else if (entry.isFile() && /\.(js|mjs|cjs|html|map)$/i.test(entry.name)) {
19861
+ yield full;
19862
+ }
19863
+ }
19864
+ }
19865
+ async function assertDeployableDistHasLiveApiBaseUrl(distDirectory, expectedApiBaseUrl) {
19866
+ const dist = path59.resolve(distDirectory);
19867
+ const siteRoot = path59.dirname(dist);
19868
+ const fromEnv = expectedApiBaseUrl?.trim() || await readSiteApiBaseUrl(siteRoot) || await readSiteApiBaseUrl(dist);
19869
+ if (!fromEnv || isPlaceholderDevPortalApiUrl(fromEnv)) {
19870
+ throw new Error(
19871
+ `Refusing to deploy: no live ${SITE_API_BASE_URL_ENV_KEY}. Run build_dev_portal_site first (it patches .env automatically) or set the live portal URL.`
19872
+ );
19873
+ }
19874
+ const needle = fromEnv.replace(/\/+$/, "");
19875
+ let found = false;
19876
+ let foundPlaceholder = false;
19877
+ for await (const file of walkFiles(dist)) {
19878
+ const content = await readFile17(file, "utf8");
19879
+ if (content.includes(needle)) {
19880
+ found = true;
19881
+ }
19882
+ if (PLACEHOLDER_HOST_MARKERS.some((marker) => content.includes(marker))) {
19883
+ foundPlaceholder = true;
19884
+ }
19885
+ if (found && foundPlaceholder) {
19886
+ break;
19887
+ }
19888
+ }
19889
+ if (foundPlaceholder && !found) {
19890
+ throw new Error(
19891
+ "Refusing to deploy: built site still contains a placeholder portal API URL. Rebuild with build_dev_portal_site."
19892
+ );
19893
+ }
19894
+ if (!found) {
19895
+ throw new Error(
19896
+ `Refusing to deploy: dist was not built with ${SITE_API_BASE_URL_ENV_KEY}=${needle}. Run build_dev_portal_site so the live URL is baked into the bundle.`
19897
+ );
19898
+ }
19899
+ }
19900
+
19901
+ // src/cli/platform/tools/dev-portal.ts
19902
+ import { promises as fs11 } from "node:fs";
19903
+ import path61 from "node:path";
19904
+
19905
+ // src/cli/platform/helpers/tarball.ts
19906
+ import { createReadStream as createReadStream3, createWriteStream as createWriteStream3, promises as fs10 } from "node:fs";
19907
+ import { tmpdir as tmpdir3 } from "node:os";
19908
+ import path60 from "node:path";
19909
+ import { TarArchive } from "archiver";
19910
+ var DEV_PORTAL_DEPLOY_ARCHIVE_ROOT = "dist";
19911
+ function normalizeArchiveRoot(archiveRoot) {
19912
+ const trimmed = archiveRoot.trim().replace(/^\/+|\/+$/g, "");
19913
+ if (!trimmed) {
19914
+ throw new Error("archiveRoot must not be empty.");
19915
+ }
19916
+ return `${trimmed}/`;
19917
+ }
19918
+ async function packageDirectoryAsTarGz(sourceDirectory, options) {
19919
+ const resolved = path60.resolve(sourceDirectory);
19920
+ const files = await collectPackageableFiles(resolved);
19921
+ const entryPrefix = options?.archiveRoot ? normalizeArchiveRoot(options.archiveRoot) : "";
19922
+ const archivePath = path60.join(
19923
+ tmpdir3(),
19924
+ `apitogo-docs-${Date.now()}-${Math.random().toString(36).slice(2)}.tgz`
19925
+ );
19926
+ await new Promise((resolve, reject) => {
19927
+ const output4 = createWriteStream3(archivePath);
19928
+ const archive = new TarArchive({ gzip: true, gzipOptions: { level: 6 } });
19929
+ output4.on("close", () => resolve());
19930
+ archive.on("error", reject);
19931
+ output4.on("error", reject);
19932
+ archive.pipe(output4);
19933
+ for (const rel of files) {
19934
+ const entryName = entryPrefix + rel.split(path60.sep).join("/");
19935
+ archive.append(createReadStream3(path60.join(resolved, rel)), { name: entryName });
19936
+ }
19937
+ void archive.finalize();
19938
+ });
19939
+ const { size } = await fs10.stat(archivePath);
19940
+ return { archivePath, byteSize: size };
19941
+ }
19942
+ async function cleanupArchive(archivePath) {
19943
+ await fs10.unlink(archivePath).catch(() => void 0);
19944
+ }
19945
+ function isTarGzPath(filePath) {
19946
+ const normalized = filePath.toLowerCase();
19947
+ return normalized.endsWith(".tgz") || normalized.endsWith(".tar.gz");
19948
+ }
19949
+
19950
+ // src/cli/platform/helpers/dev-portal-backend-job.ts
19951
+ var DEV_PORTAL_BACKEND_UPDATE_RESOURCE_TYPE = "DevPortalBackendUpdate";
19952
+ var ACTIVE_JOB_STATUSES = /* @__PURE__ */ new Set(["Pending", "InProgress"]);
19953
+ var RECENT_COMPLETED_WINDOW_MS = 3e4;
19954
+ var BACKEND_UPDATE_JOB_APPEARANCE_TIMEOUT_MS = 3e4;
19955
+ function isBackendUpdateJob(job) {
19956
+ return job.resourceType === DEV_PORTAL_BACKEND_UPDATE_RESOURCE_TYPE;
19957
+ }
19958
+ function isActiveBackendUpdateJob(job) {
19959
+ return isBackendUpdateJob(job) && ACTIVE_JOB_STATUSES.has(job.status);
19960
+ }
19961
+ function isRecentlyCompletedBackendUpdateJob(job) {
19962
+ if (!isBackendUpdateJob(job) || job.status !== "Completed" || !job.completedAt) {
19963
+ return false;
19964
+ }
19965
+ const completedAtMs = Date.parse(job.completedAt);
19966
+ if (Number.isNaN(completedAtMs)) {
19967
+ return false;
19968
+ }
19969
+ return Date.now() - completedAtMs <= RECENT_COMPLETED_WINDOW_MS;
19970
+ }
19971
+ function sortJobsNewestFirst(jobs) {
19972
+ return [...jobs].sort(
19973
+ (a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt)
19974
+ );
19975
+ }
19976
+ function findBackendUpdateJob(jobs, options) {
19977
+ const sorted = sortJobsNewestFirst(jobs);
19978
+ if (options?.jobId) {
19979
+ return sorted.find((job) => job.id === options.jobId) ?? null;
19980
+ }
19981
+ const active = sorted.find(isActiveBackendUpdateJob);
19982
+ if (active) {
19983
+ return active;
19984
+ }
19985
+ const baseline = options?.baselineJobIds;
19986
+ if (baseline) {
19987
+ const newlyCompleted = sorted.find(
19988
+ (job) => isBackendUpdateJob(job) && !baseline.has(job.id) && (ACTIVE_JOB_STATUSES.has(job.status) || job.status === "Completed")
19989
+ );
19990
+ if (newlyCompleted) {
19991
+ return newlyCompleted;
19992
+ }
19993
+ }
19994
+ const recentlyCompleted = sorted.find(isRecentlyCompletedBackendUpdateJob);
19995
+ if (recentlyCompleted) {
19996
+ return recentlyCompleted;
19997
+ }
19998
+ return null;
19920
19999
  }
19921
- async function* walkFiles(root) {
19922
- let entries;
19923
- try {
19924
- entries = await readdir2(root, { withFileTypes: true });
19925
- } catch {
19926
- return;
19927
- }
19928
- for (const entry of entries) {
19929
- const full = path60.join(root, entry.name);
19930
- if (entry.isDirectory()) {
19931
- if (entry.name === "node_modules" || entry.name === ".git") {
19932
- continue;
19933
- }
19934
- yield* walkFiles(full);
19935
- } else if (entry.isFile() && /\.(js|mjs|cjs|html|map)$/i.test(entry.name)) {
19936
- yield full;
20000
+ async function discoverBackendUpdateJob(options) {
20001
+ if (options.jobId) {
20002
+ const result = await options.client.listProvisioningJobs(options.projectId);
20003
+ const job = findBackendUpdateJob(result.jobs, { jobId: options.jobId });
20004
+ if (!job) {
20005
+ throw new Error(`DevPortalBackendUpdate job not found: ${options.jobId}`);
19937
20006
  }
20007
+ return { job, timedOut: false };
19938
20008
  }
19939
- }
19940
- async function assertDeployableDistHasLiveApiBaseUrl(distDirectory, expectedApiBaseUrl) {
19941
- const dist = path60.resolve(distDirectory);
19942
- const siteRoot = path60.dirname(dist);
19943
- const fromEnv = expectedApiBaseUrl?.trim() || await readSiteApiBaseUrl(siteRoot) || await readSiteApiBaseUrl(dist);
19944
- if (!fromEnv || isPlaceholderDevPortalApiUrl(fromEnv)) {
19945
- throw new Error(
19946
- `Refusing to deploy: no live ${SITE_API_BASE_URL_ENV_KEY}. Run build_dev_portal_site first (it patches .env automatically) or set the live portal URL.`
19947
- );
20009
+ const pollOnce = async () => {
20010
+ const result = await options.client.listProvisioningJobs(options.projectId);
20011
+ return findBackendUpdateJob(result.jobs, {
20012
+ baselineJobIds: options.baselineJobIds
20013
+ });
20014
+ };
20015
+ const first = await pollOnce();
20016
+ if (first) {
20017
+ return { job: first, timedOut: false };
19948
20018
  }
19949
- const needle = fromEnv.replace(/\/+$/, "");
19950
- let found = false;
19951
- let foundPlaceholder = false;
19952
- for await (const file of walkFiles(dist)) {
19953
- const content = await readFile17(file, "utf8");
19954
- if (content.includes(needle)) {
19955
- found = true;
19956
- }
19957
- if (PLACEHOLDER_HOST_MARKERS.some((marker) => content.includes(marker))) {
19958
- foundPlaceholder = true;
20019
+ if (!options.baselineJobIds) {
20020
+ return { job: null, timedOut: false };
20021
+ }
20022
+ const appearanceTimeoutMs = Math.min(
20023
+ options.timeoutMs ?? BACKEND_UPDATE_JOB_APPEARANCE_TIMEOUT_MS,
20024
+ BACKEND_UPDATE_JOB_APPEARANCE_TIMEOUT_MS
20025
+ );
20026
+ const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
20027
+ const deadline = Date.now() + appearanceTimeoutMs;
20028
+ while (Date.now() <= deadline) {
20029
+ const job = await pollOnce();
20030
+ if (job) {
20031
+ return { job, timedOut: false };
19959
20032
  }
19960
- if (found && foundPlaceholder) {
20033
+ if (Date.now() + pollIntervalMs > deadline) {
19961
20034
  break;
19962
20035
  }
20036
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
19963
20037
  }
19964
- if (foundPlaceholder && !found) {
19965
- throw new Error(
19966
- "Refusing to deploy: built site still contains a placeholder portal API URL. Rebuild with build_dev_portal_site."
19967
- );
19968
- }
19969
- if (!found) {
19970
- throw new Error(
19971
- `Refusing to deploy: dist was not built with ${SITE_API_BASE_URL_ENV_KEY}=${needle}. Run build_dev_portal_site so the live URL is baked into the bundle.`
19972
- );
19973
- }
20038
+ return { job: null, timedOut: false };
20039
+ }
20040
+ async function pollBackendUpdateJobStatus(client, projectId, jobId, options) {
20041
+ return pollUntilTerminal({
20042
+ poll: () => client.getDevPortalDeployStatus(projectId, jobId),
20043
+ isTerminal: (value) => isTerminalInfrastructureStatus(value.status),
20044
+ getStatus: (value) => normalizeInfrastructureStatus(value.status),
20045
+ timeoutMs: options?.timeoutMs ?? DEFAULT_POLL_TIMEOUT_MS,
20046
+ intervalMs: options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS,
20047
+ stepLabel: "Dev portal backend update"
20048
+ });
20049
+ }
20050
+ async function snapshotBackendUpdateJobIds(client, projectId) {
20051
+ const result = await client.listProvisioningJobs(projectId);
20052
+ return new Set(
20053
+ result.jobs.filter(isBackendUpdateJob).map((job) => job.id)
20054
+ );
19974
20055
  }
19975
20056
 
19976
20057
  // src/cli/platform/tools/dev-portal.ts
@@ -20262,7 +20343,6 @@ async function handleDeployDevPortal(client, config2, args, progress) {
20262
20343
  }
20263
20344
 
20264
20345
  // src/cli/platform/tools/dev-portal-site-build.ts
20265
- import path62 from "node:path";
20266
20346
  async function handleBuildDevPortalSite(client, config2, args, progress) {
20267
20347
  const buildProgress = progress?.child("Build dev portal site");
20268
20348
  const sourceDirectory = path62.resolve(args.sourceDirectory);
@@ -20342,54 +20422,6 @@ async function handleBuildDevPortalSite(client, config2, args, progress) {
20342
20422
  }
20343
20423
  }
20344
20424
 
20345
- // src/cli/platform/helpers/public-routes.ts
20346
- function normalizeActionKeyFormat(actionKey) {
20347
- const trimmed = actionKey.trim();
20348
- if (trimmed.includes(":")) {
20349
- const [method2, ...pathParts] = trimmed.split(":");
20350
- const path73 = pathParts.join(":").trim();
20351
- return `${method2.toLowerCase()}:${path73.startsWith("/") ? path73 : `/${path73}`}`;
20352
- }
20353
- const spaceIndex = trimmed.indexOf(" ");
20354
- if (spaceIndex <= 0) {
20355
- throw new Error(
20356
- "actionKey must be in format 'get:/path' or 'GET /path' (method plus path)."
20357
- );
20358
- }
20359
- const method = trimmed.slice(0, spaceIndex).trim().toLowerCase();
20360
- const path72 = trimmed.slice(spaceIndex + 1).trim();
20361
- return `${method}:${path72.startsWith("/") ? path72 : `/${path72}`}`;
20362
- }
20363
- function normalizePublicRoute(route) {
20364
- const trimmed = route.trim();
20365
- if (!trimmed) {
20366
- throw new Error("public route must be a non-empty string");
20367
- }
20368
- if (trimmed.startsWith("/")) {
20369
- return normalizeActionKeyFormat(`GET ${trimmed}`);
20370
- }
20371
- return normalizeActionKeyFormat(trimmed);
20372
- }
20373
- function collectManifestPublicActionKeys(gateway) {
20374
- if (!gateway) {
20375
- return [];
20376
- }
20377
- const keys = /* @__PURE__ */ new Set();
20378
- for (const route of gateway.publicRoutes ?? []) {
20379
- try {
20380
- keys.add(normalizePublicRoute(route));
20381
- } catch {
20382
- }
20383
- }
20384
- if (gateway.publicHealthActionKey?.trim()) {
20385
- try {
20386
- keys.add(normalizePublicRoute(gateway.publicHealthActionKey));
20387
- } catch {
20388
- }
20389
- }
20390
- return [...keys];
20391
- }
20392
-
20393
20425
  // src/cli/platform/tools/dev-portal-publish.ts
20394
20426
  var PUBLISH_STEPS = [
20395
20427
  "resolve_project",
@@ -20420,10 +20452,7 @@ var PUBLISH_STEPS_BY_RESOURCE = {
20420
20452
  "update_dev_portal_settings",
20421
20453
  "sync_dev_portal_plan_catalog"
20422
20454
  ],
20423
- "portal-frontend": [
20424
- "build_dev_portal_site",
20425
- "deploy_dev_portal"
20426
- ]
20455
+ "portal-frontend": ["build_dev_portal_site", "deploy_dev_portal"]
20427
20456
  };
20428
20457
  function parseApplyResources(raw) {
20429
20458
  if (raw === void 0) {
@@ -20467,7 +20496,9 @@ function shouldRunPublishStep(step, resumeFromStep, completedSteps) {
20467
20496
  return true;
20468
20497
  }
20469
20498
  if (stepIndex < resumeIndex) {
20470
- return completedSteps.some((record) => record.step === step && record.status === "succeeded");
20499
+ return completedSteps.some(
20500
+ (record) => record.step === step && record.status === "succeeded"
20501
+ );
20471
20502
  }
20472
20503
  return true;
20473
20504
  }
@@ -20500,10 +20531,16 @@ async function shouldSkipBackendRedeploy(client, projectId, manifest, ledger, ex
20500
20531
  const remoteSha = summary.artifactContentSha256?.toLowerCase() ?? null;
20501
20532
  const ledgerSha = ledger.backendArtifactHash?.toLowerCase() ?? null;
20502
20533
  if (remoteSha && ledgerSha && remoteSha === ledgerSha) {
20503
- return { skip: true, reason: "healthy backend matches ledger artifact hash" };
20534
+ return {
20535
+ skip: true,
20536
+ reason: "healthy backend matches ledger artifact hash"
20537
+ };
20504
20538
  }
20505
20539
  if (manifest.backend?.deployed && remoteSha && ledgerSha && remoteSha === ledgerSha) {
20506
- return { skip: true, reason: "backend marked deployed with matching hash" };
20540
+ return {
20541
+ skip: true,
20542
+ reason: "backend marked deployed with matching hash"
20543
+ };
20507
20544
  }
20508
20545
  } catch {
20509
20546
  }
@@ -20527,7 +20564,9 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20527
20564
  const wait = args.wait !== false;
20528
20565
  const resources = resolveApplyResources(args.only);
20529
20566
  const forceBackendRedeploy = Boolean(args.force && resources.has("apihub"));
20530
- const forceSiteRedeploy = Boolean(args.force && resources.has("portal-frontend"));
20567
+ const forceSiteRedeploy = Boolean(
20568
+ args.force && resources.has("portal-frontend")
20569
+ );
20531
20570
  const includeApihub = resources.has("apihub");
20532
20571
  const includePortalBackend = resources.has("portal-backend");
20533
20572
  const includePortalFrontend = resources.has("portal-frontend");
@@ -20558,22 +20597,37 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20558
20597
  version: 1,
20559
20598
  projectId: projectId || ledger.projectId,
20560
20599
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
20561
- lastSuccessfulStep: [...timedSteps].reverse().find((step) => step.status === "succeeded" || step.status === "skipped")?.step ?? ledger.lastSuccessfulStep,
20600
+ lastSuccessfulStep: [...timedSteps].reverse().find(
20601
+ (step) => step.status === "succeeded" || step.status === "skipped"
20602
+ )?.step ?? ledger.lastSuccessfulStep,
20562
20603
  steps: timedSteps.length > 0 ? timedSteps : ledger.steps
20563
20604
  };
20564
20605
  await writePublishLedger(args.sourceDirectory, ledger);
20565
20606
  };
20566
20607
  try {
20567
- const { manifest: loadedManifest, secrets } = await resolvePublishConfig(args.sourceDirectory);
20608
+ const { manifest: loadedManifest, secrets } = await resolvePublishConfig(
20609
+ args.sourceDirectory
20610
+ );
20568
20611
  const projectIdHint = args.projectId ?? loadedManifest.projectId;
20569
- const emailOptions = await resolveEmailOptionsForPublish(client, projectIdHint);
20570
- let manifest = validateManifestForPublish(loadedManifest, secrets, emailOptions);
20612
+ const emailOptions = await resolveEmailOptionsForPublish(
20613
+ client,
20614
+ projectIdHint
20615
+ );
20616
+ const manifest = validateManifestForPublish(
20617
+ loadedManifest,
20618
+ secrets,
20619
+ emailOptions
20620
+ );
20571
20621
  await writeManifest(args.sourceDirectory, manifest);
20572
20622
  const stripeRequired = manifestRequiresStripe(manifest.plans);
20573
20623
  const ledgerResume = resolveResumeStepFromLedger(ledger, PUBLISH_STEPS);
20574
20624
  const effectiveResumeFromStep = args.resumeFromStep ?? ledgerResume;
20575
20625
  let publicRouteWarnings = [];
20576
- if (args.createNewProject || shouldRunPublishStep("resolve_project", effectiveResumeFromStep, completedSteps)) {
20626
+ if (args.createNewProject || shouldRunPublishStep(
20627
+ "resolve_project",
20628
+ effectiveResumeFromStep,
20629
+ completedSteps
20630
+ )) {
20577
20631
  const timer = new StepTimer();
20578
20632
  const resolved = await resolveProjectIdForPublish(
20579
20633
  client,
@@ -20654,7 +20708,11 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20654
20708
  if (ensured.status !== "succeeded") {
20655
20709
  return ensureJson;
20656
20710
  }
20657
- completedSteps.push({ step: "ensure_platform_plan", status: "succeeded", detail: ensured });
20711
+ completedSteps.push({
20712
+ step: "ensure_platform_plan",
20713
+ status: "succeeded",
20714
+ detail: ensured
20715
+ });
20658
20716
  }
20659
20717
  const planGate = await assertPlatformPlanReadyForPublish(client, projectId);
20660
20718
  if (planGate.blocked) {
@@ -20723,7 +20781,12 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20723
20781
  detail: { reason: "resource not selected" }
20724
20782
  });
20725
20783
  }
20726
- if (shouldRunResourceStep("upsert_gateway_plans", resources, effectiveResumeFromStep, completedSteps)) {
20784
+ if (shouldRunResourceStep(
20785
+ "upsert_gateway_plans",
20786
+ resources,
20787
+ effectiveResumeFromStep,
20788
+ completedSteps
20789
+ )) {
20727
20790
  await publishProgress?.onStep("Creating gateway plans");
20728
20791
  const timer = new StepTimer();
20729
20792
  const planResults = await upsertGatewayPlansFromManifest(
@@ -20733,7 +20796,11 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20733
20796
  manifest.plans,
20734
20797
  { deferActionKeys: true }
20735
20798
  );
20736
- recordStep("upsert_gateway_plans", "succeeded", timer.finish(planResults));
20799
+ recordStep(
20800
+ "upsert_gateway_plans",
20801
+ "succeeded",
20802
+ timer.finish(planResults)
20803
+ );
20737
20804
  } else {
20738
20805
  recordStep("upsert_gateway_plans", "skipped", new StepTimer().finish());
20739
20806
  }
@@ -20750,7 +20817,9 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20750
20817
  const project = await client.getProject(projectId);
20751
20818
  await publishProgress?.onStep("Loading backend FQDN");
20752
20819
  let backendFqdn = resolvePortalBackendFqdn(project);
20753
- const backendJson = await handleGetDevPortalBackend(client, config2, { projectId });
20820
+ const backendJson = await handleGetDevPortalBackend(client, config2, {
20821
+ projectId
20822
+ });
20754
20823
  const backendResult = JSON.parse(backendJson);
20755
20824
  backendFqdn = backendFqdn ?? backendResult.backend?.fqdn?.trim() ?? null;
20756
20825
  const platformSiteUrl = backendResult.backend?.platformSiteUrl?.trim() ?? null;
@@ -20766,7 +20835,12 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20766
20835
  );
20767
20836
  const urls = backendFqdn ? resolveDevPortalUrls(project, backendFqdn, providerIds, platformSiteUrl) : null;
20768
20837
  const backendApiBaseUrl = backendFqdn ? `https://${backendFqdn}` : "";
20769
- if (shouldRunResourceStep("update_dev_portal_settings", resources, effectiveResumeFromStep, completedSteps)) {
20838
+ if (shouldRunResourceStep(
20839
+ "update_dev_portal_settings",
20840
+ resources,
20841
+ effectiveResumeFromStep,
20842
+ completedSteps
20843
+ )) {
20770
20844
  await publishProgress?.onStep("Updating dev portal settings");
20771
20845
  const timer = new StepTimer();
20772
20846
  const openApiPath = manifest.backend?.openApiPath;
@@ -20785,7 +20859,9 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20785
20859
  openApiSourceKind: openApi.openApiSourceKind,
20786
20860
  openApiValue: openApi.openApiValue,
20787
20861
  authConfigJson: JSON.stringify(buildAuthConfigJson(manifest)),
20788
- authSecretsJson: JSON.stringify(buildAuthSecretsJson(secrets, emailOptions)),
20862
+ authSecretsJson: JSON.stringify(
20863
+ buildAuthSecretsJson(secrets, emailOptions)
20864
+ ),
20789
20865
  stripeSecretKey: stripeRequired ? secrets.stripe.secretKey : void 0,
20790
20866
  stripeWebhookSecret: stripeRequired ? secrets.stripe.webhookSecret : void 0,
20791
20867
  stripeSuccessUrl: stripeRequired ? urls.stripeSuccessUrl : void 0,
@@ -20799,9 +20875,18 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20799
20875
  parseHandlerResult(settingsJson, "update_dev_portal_settings");
20800
20876
  recordStep("update_dev_portal_settings", "succeeded", timer.finish());
20801
20877
  } else {
20802
- recordStep("update_dev_portal_settings", "skipped", new StepTimer().finish());
20878
+ recordStep(
20879
+ "update_dev_portal_settings",
20880
+ "skipped",
20881
+ new StepTimer().finish()
20882
+ );
20803
20883
  }
20804
- if (shouldRunResourceStep("configure_gateway_baseline", resources, effectiveResumeFromStep, completedSteps)) {
20884
+ if (shouldRunResourceStep(
20885
+ "configure_gateway_baseline",
20886
+ resources,
20887
+ effectiveResumeFromStep,
20888
+ completedSteps
20889
+ )) {
20805
20890
  await publishProgress?.onStep("Configuring gateway baseline");
20806
20891
  const timer = new StepTimer();
20807
20892
  const gateway = manifest.gateway ?? {};
@@ -20835,14 +20920,21 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20835
20920
  { ...config2, defaultProjectId: projectId },
20836
20921
  baselineArgs
20837
20922
  );
20838
- const baseline = parseHandlerResult(baselineJson, "configure_gateway_baseline");
20923
+ const baseline = parseHandlerResult(
20924
+ baselineJson,
20925
+ "configure_gateway_baseline"
20926
+ );
20839
20927
  recordStep(
20840
20928
  "configure_gateway_baseline",
20841
20929
  baseline.status === "pending" ? "pending" : "succeeded",
20842
20930
  timer.finish(baseline)
20843
20931
  );
20844
20932
  } else {
20845
- recordStep("configure_gateway_baseline", "skipped", new StepTimer().finish());
20933
+ recordStep(
20934
+ "configure_gateway_baseline",
20935
+ "skipped",
20936
+ new StepTimer().finish()
20937
+ );
20846
20938
  }
20847
20939
  const skipBackendDecision = await shouldSkipBackendRedeploy(
20848
20940
  client,
@@ -20858,13 +20950,21 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20858
20950
  const hasBackendSource = Boolean(backendSourceDirectory);
20859
20951
  let backendDeploySucceeded = false;
20860
20952
  let backendDeployFailed = false;
20861
- if (shouldRunResourceStep("deploy_backend", resources, effectiveResumeFromStep, completedSteps) && backendConfig && backendSourceDirectory && !skipBackend) {
20953
+ if (shouldRunResourceStep(
20954
+ "deploy_backend",
20955
+ resources,
20956
+ effectiveResumeFromStep,
20957
+ completedSteps
20958
+ ) && backendConfig && backendSourceDirectory && !skipBackend) {
20862
20959
  await publishProgress?.onStep("Deploying API backend");
20863
20960
  const timer = new StepTimer();
20864
- const resolvedOpenApiPath = resolveBackendOpenApiPath(backendConfig.openApiPath, {
20865
- siteDirectory: args.sourceDirectory,
20866
- backendSourceDirectory
20867
- });
20961
+ const resolvedOpenApiPath = resolveBackendOpenApiPath(
20962
+ backendConfig.openApiPath,
20963
+ {
20964
+ siteDirectory: args.sourceDirectory,
20965
+ backendSourceDirectory
20966
+ }
20967
+ );
20868
20968
  try {
20869
20969
  const deployJson = await handleDeployBackend(
20870
20970
  client,
@@ -20926,14 +21026,25 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20926
21026
  }
20927
21027
  }
20928
21028
  const shouldSyncPlanActions = hasBackendSource && !backendDeployFailed && (backendDeploySucceeded || manifest.backend?.deployed === true);
20929
- if (shouldRunResourceStep("sync_gateway_plan_actions", resources, effectiveResumeFromStep, completedSteps) && shouldSyncPlanActions) {
20930
- await publishProgress?.onStep("Waiting for OpenAPI actions then syncing plan membership");
21029
+ if (shouldRunResourceStep(
21030
+ "sync_gateway_plan_actions",
21031
+ resources,
21032
+ effectiveResumeFromStep,
21033
+ completedSteps
21034
+ ) && shouldSyncPlanActions) {
21035
+ await publishProgress?.onStep(
21036
+ "Waiting for OpenAPI actions then syncing plan membership"
21037
+ );
20931
21038
  const timer = new StepTimer();
20932
- const actionWait = await waitForGatewayActionsRegistered(client, projectId, {
20933
- minActionCount: 1,
20934
- timeoutMs: args.timeoutMs ?? 18e4,
20935
- pollIntervalMs: args.pollIntervalMs ?? 5e3
20936
- });
21039
+ const actionWait = await waitForGatewayActionsRegistered(
21040
+ client,
21041
+ projectId,
21042
+ {
21043
+ minActionCount: 1,
21044
+ timeoutMs: args.timeoutMs ?? 18e4,
21045
+ pollIntervalMs: args.pollIntervalMs ?? 5e3
21046
+ }
21047
+ );
20937
21048
  if (actionWait.timedOut && actionWait.actionCount < 1) {
20938
21049
  throw new BootstrapStepError(
20939
21050
  "sync_gateway_plan_actions",
@@ -20951,9 +21062,17 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20951
21062
  recordStep(
20952
21063
  "sync_gateway_plan_actions",
20953
21064
  "succeeded",
20954
- timer.finish({ actionCount: actionWait.actionCount, results: actionSyncResults })
21065
+ timer.finish({
21066
+ actionCount: actionWait.actionCount,
21067
+ results: actionSyncResults
21068
+ })
20955
21069
  );
20956
- } else if (shouldRunResourceStep("sync_gateway_plan_actions", resources, effectiveResumeFromStep, completedSteps)) {
21070
+ } else if (shouldRunResourceStep(
21071
+ "sync_gateway_plan_actions",
21072
+ resources,
21073
+ effectiveResumeFromStep,
21074
+ completedSteps
21075
+ )) {
20957
21076
  recordStep(
20958
21077
  "sync_gateway_plan_actions",
20959
21078
  "skipped",
@@ -20962,15 +21081,26 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20962
21081
  })
20963
21082
  );
20964
21083
  } else {
20965
- recordStep("sync_gateway_plan_actions", "skipped", new StepTimer().finish());
21084
+ recordStep(
21085
+ "sync_gateway_plan_actions",
21086
+ "skipped",
21087
+ new StepTimer().finish()
21088
+ );
20966
21089
  }
20967
21090
  const publicActionKeys = collectManifestPublicActionKeys(manifest.gateway);
20968
- if (shouldRunResourceStep("apply_public_routes", resources, effectiveResumeFromStep, completedSteps)) {
21091
+ if (shouldRunResourceStep(
21092
+ "apply_public_routes",
21093
+ resources,
21094
+ effectiveResumeFromStep,
21095
+ completedSteps
21096
+ )) {
20969
21097
  if (publicActionKeys.length === 0) {
20970
21098
  recordStep(
20971
21099
  "apply_public_routes",
20972
21100
  "skipped",
20973
- new StepTimer().finish({ reason: "no publicRoutes or publicHealthActionKey in manifest" })
21101
+ new StepTimer().finish({
21102
+ reason: "no publicRoutes or publicHealthActionKey in manifest"
21103
+ })
20974
21104
  );
20975
21105
  } else if (!shouldSyncPlanActions && !(manifest.backend?.deployed === true)) {
20976
21106
  recordStep(
@@ -20987,12 +21117,20 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
20987
21117
  } else {
20988
21118
  await publishProgress?.onStep("Applying public gateway routes");
20989
21119
  const timer = new StepTimer();
20990
- const actionWait = await waitForGatewayActionsRegistered(client, projectId, {
20991
- minActionCount: 1,
20992
- timeoutMs: args.timeoutMs ?? 12e4,
20993
- pollIntervalMs: args.pollIntervalMs ?? 5e3
20994
- });
20995
- const result = await applyGatewayPublicRoutes(client, projectId, publicActionKeys);
21120
+ const actionWait = await waitForGatewayActionsRegistered(
21121
+ client,
21122
+ projectId,
21123
+ {
21124
+ minActionCount: 1,
21125
+ timeoutMs: args.timeoutMs ?? 12e4,
21126
+ pollIntervalMs: args.pollIntervalMs ?? 5e3
21127
+ }
21128
+ );
21129
+ const result = await applyGatewayPublicRoutes(
21130
+ client,
21131
+ projectId,
21132
+ publicActionKeys
21133
+ );
20996
21134
  publicRouteWarnings = result.warnings;
20997
21135
  recordStep(
20998
21136
  "apply_public_routes",
@@ -21008,7 +21146,12 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
21008
21146
  } else {
21009
21147
  recordStep("apply_public_routes", "skipped", new StepTimer().finish());
21010
21148
  }
21011
- if (shouldRunResourceStep("sync_dev_portal_plan_catalog", resources, effectiveResumeFromStep, completedSteps)) {
21149
+ if (shouldRunResourceStep(
21150
+ "sync_dev_portal_plan_catalog",
21151
+ resources,
21152
+ effectiveResumeFromStep,
21153
+ completedSteps
21154
+ )) {
21012
21155
  await publishProgress?.onStep("Syncing dev portal plan catalog");
21013
21156
  const timer = new StepTimer();
21014
21157
  const expectedPlanCount = manifest.plans.length;
@@ -21026,29 +21169,52 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
21026
21169
  })
21027
21170
  );
21028
21171
  } else {
21029
- recordStep("sync_dev_portal_plan_catalog", "skipped", new StepTimer().finish());
21172
+ recordStep(
21173
+ "sync_dev_portal_plan_catalog",
21174
+ "skipped",
21175
+ new StepTimer().finish()
21176
+ );
21030
21177
  }
21031
- let gatewayUrl = await resolveProjectGatewayUrl(client, projectId);
21032
- const siteSourceHash = await hashDevPortalSiteSources(args.sourceDirectory, {
21033
- gatewayUrl
21034
- });
21178
+ let gatewayUrl = await resolveProjectGatewayUrl(
21179
+ client,
21180
+ projectId
21181
+ );
21182
+ const siteSourceHash = await hashDevPortalSiteSources(
21183
+ args.sourceDirectory,
21184
+ {
21185
+ gatewayUrl
21186
+ }
21187
+ );
21035
21188
  const skipSiteDeploy = !forceSiteRedeploy && Boolean(ledger.siteArtifactHash) && ledger.siteArtifactHash === siteSourceHash;
21036
- if (shouldRunResourceStep("build_dev_portal_site", resources, effectiveResumeFromStep, completedSteps)) {
21189
+ if (shouldRunResourceStep(
21190
+ "build_dev_portal_site",
21191
+ resources,
21192
+ effectiveResumeFromStep,
21193
+ completedSteps
21194
+ )) {
21037
21195
  if (skipSiteDeploy) {
21038
21196
  recordStep(
21039
21197
  "build_dev_portal_site",
21040
21198
  "skipped",
21041
- new StepTimer().finish({ reason: "unchanged site artifact hash", siteSourceHash })
21199
+ new StepTimer().finish({
21200
+ reason: "unchanged site artifact hash",
21201
+ siteSourceHash
21202
+ })
21042
21203
  );
21043
21204
  recordStep(
21044
21205
  "deploy_dev_portal",
21045
21206
  "skipped",
21046
- new StepTimer().finish({ reason: "unchanged site artifact hash", siteSourceHash })
21207
+ new StepTimer().finish({
21208
+ reason: "unchanged site artifact hash",
21209
+ siteSourceHash
21210
+ })
21047
21211
  );
21048
21212
  } else {
21049
21213
  await publishProgress?.onStep("Validating site config before build");
21050
21214
  const timer = new StepTimer();
21051
- const preBuildValidation = await validateDevPortalSiteConfig(args.sourceDirectory);
21215
+ const preBuildValidation = await validateDevPortalSiteConfig(
21216
+ args.sourceDirectory
21217
+ );
21052
21218
  if (!preBuildValidation.valid) {
21053
21219
  throw new BootstrapStepError(
21054
21220
  "build_dev_portal_site",
@@ -21069,12 +21235,18 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
21069
21235
  },
21070
21236
  publishProgress
21071
21237
  );
21072
- const buildResult = parseHandlerResult(
21073
- buildJson,
21074
- "build_dev_portal_site"
21238
+ const buildResult = parseHandlerResult(buildJson, "build_dev_portal_site");
21239
+ recordStep(
21240
+ "build_dev_portal_site",
21241
+ "succeeded",
21242
+ timer.finish(buildResult)
21075
21243
  );
21076
- recordStep("build_dev_portal_site", "succeeded", timer.finish(buildResult));
21077
- if (shouldRunResourceStep("deploy_dev_portal", resources, effectiveResumeFromStep, completedSteps)) {
21244
+ if (shouldRunResourceStep(
21245
+ "deploy_dev_portal",
21246
+ resources,
21247
+ effectiveResumeFromStep,
21248
+ completedSteps
21249
+ )) {
21078
21250
  await publishProgress?.onStep("Deploying dev portal site");
21079
21251
  const deployTimer = new StepTimer();
21080
21252
  const deployPortalJson = await handleDeployDevPortal(
@@ -21088,10 +21260,19 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
21088
21260
  );
21089
21261
  parseHandlerResult(deployPortalJson, "deploy_dev_portal");
21090
21262
  ledger.siteArtifactHash = siteSourceHash;
21091
- recordStep("deploy_dev_portal", "succeeded", deployTimer.finish({ siteSourceHash }));
21263
+ recordStep(
21264
+ "deploy_dev_portal",
21265
+ "succeeded",
21266
+ deployTimer.finish({ siteSourceHash })
21267
+ );
21092
21268
  }
21093
21269
  }
21094
- } else if (shouldRunResourceStep("deploy_dev_portal", resources, effectiveResumeFromStep, completedSteps)) {
21270
+ } else if (shouldRunResourceStep(
21271
+ "deploy_dev_portal",
21272
+ resources,
21273
+ effectiveResumeFromStep,
21274
+ completedSteps
21275
+ )) {
21095
21276
  recordStep("build_dev_portal_site", "skipped", new StepTimer().finish());
21096
21277
  recordStep("deploy_dev_portal", "skipped", new StepTimer().finish());
21097
21278
  }
@@ -21116,12 +21297,7 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
21116
21297
  }
21117
21298
  };
21118
21299
  gatewayUrl = refreshedProject.gatewayUrl?.trim().replace(/\/+$/, "") || gatewayUrl || await resolveProjectGatewayUrl(client, projectId);
21119
- const swaUrl = backendFqdn ? resolveDevPortalUrls(
21120
- refreshedProject,
21121
- backendFqdn,
21122
- [],
21123
- platformSiteUrl
21124
- ).swaUrl : null;
21300
+ const swaUrl = backendFqdn ? resolveDevPortalUrls(refreshedProject, backendFqdn, [], platformSiteUrl).swaUrl : null;
21125
21301
  const manualSteps = stripeRequired ? postValidation.manualSteps : {
21126
21302
  oidcRedirectUri: postValidation.manualSteps.oidcRedirectUri
21127
21303
  };
@@ -21209,7 +21385,9 @@ async function handleVerifyDevPortalLive(client, config2, args) {
21209
21385
  }
21210
21386
  projectId = resolveProjectId(projectId, config2);
21211
21387
  if (!backendFqdn) {
21212
- const backendJson = await handleGetDevPortalBackend(client, config2, { projectId });
21388
+ const backendJson = await handleGetDevPortalBackend(client, config2, {
21389
+ projectId
21390
+ });
21213
21391
  const backendResult = JSON.parse(backendJson);
21214
21392
  backendFqdn = backendResult.backend?.fqdn?.trim();
21215
21393
  }
@@ -21225,7 +21403,9 @@ async function handleVerifyDevPortalLive(client, config2, args) {
21225
21403
  postDeploy: true
21226
21404
  });
21227
21405
  for (let attempt = 1; attempt < retries && !validation.passed; attempt++) {
21228
- const plansCheck = validation.checks.find((check2) => check2.name === "plans_endpoint");
21406
+ const plansCheck = validation.checks.find(
21407
+ (check2) => check2.name === "plans_endpoint"
21408
+ );
21229
21409
  if (plansCheck?.passed) {
21230
21410
  break;
21231
21411
  }
@@ -21259,76 +21439,6 @@ async function handleVerifyDevPortalLive(client, config2, args) {
21259
21439
  );
21260
21440
  }
21261
21441
 
21262
- // src/cli/apply/human-output.ts
21263
- function formatNonSuccessResultForHuman(parsed) {
21264
- const status2 = parsed.status;
21265
- if (status2 === "blocked" && parsed.reason === "platform_plan_unselected") {
21266
- const lines = [
21267
- "Deploy is blocked: this project doesn't have an API ToGo platform hosting plan yet."
21268
- ];
21269
- if (typeof parsed.freeProjectsQuotaHint === "string") {
21270
- lines.push(parsed.freeProjectsQuotaHint);
21271
- }
21272
- lines.push(
21273
- "Pick a plan and re-run:",
21274
- " apitogo deploy --platform-plan free",
21275
- " apitogo deploy --platform-plan starter",
21276
- " apitogo deploy --platform-plan professional",
21277
- "Run `apitogo deploy --json` to see full plan details (pricing, limits) as data."
21278
- );
21279
- return lines;
21280
- }
21281
- if (status2 === "blocked" && parsed.reason === "project_not_found") {
21282
- const projectId = typeof parsed.projectId === "string" ? parsed.projectId : void 0;
21283
- const lines = [
21284
- typeof parsed.userMessage === "string" ? parsed.userMessage : `Deploy is blocked: project${projectId ? ` ${projectId}` : ""} was not found.`
21285
- ];
21286
- lines.push(
21287
- "This usually means the project was deleted, or it belongs to a different account than the one you're logged in as.",
21288
- "",
21289
- "To fix this:",
21290
- " Run `apitogo login` and confirm you're using the right account, then re-run `apitogo deploy`",
21291
- ' Or create a new project: remove "projectId" from apitogo.config.json and re-run `apitogo deploy`'
21292
- );
21293
- return lines;
21294
- }
21295
- if (status2 === "blocked" && parsed.reason === "paid_gateway_plans_require_starter") {
21296
- const lines = [];
21297
- lines.push(
21298
- typeof parsed.userMessage === "string" ? parsed.userMessage : "Deploy is blocked: your Free platform plan can't host paid API plans."
21299
- );
21300
- lines.push(
21301
- "Choose one:",
21302
- " apitogo deploy --platform-plan starter # or professional, to keep the paid plans",
21303
- " Edit apitogo.config.json to remove those plans or set their price to 0, then re-run apitogo deploy"
21304
- );
21305
- return lines;
21306
- }
21307
- if (status2 === "blocked") {
21308
- return [
21309
- typeof parsed.userMessage === "string" ? parsed.userMessage : `Deploy is blocked${typeof parsed.reason === "string" ? `: ${parsed.reason}` : "."}`
21310
- ];
21311
- }
21312
- if (status2 === "failed") {
21313
- const lines = [
21314
- `Deploy failed${typeof parsed.failedStep === "string" ? ` at step "${parsed.failedStep}"` : ""}.`
21315
- ];
21316
- if (typeof parsed.error === "string") {
21317
- lines.push(parsed.error);
21318
- }
21319
- if (typeof parsed.recoveryHint === "string") {
21320
- lines.push(parsed.recoveryHint);
21321
- }
21322
- if (typeof parsed.failedStep === "string") {
21323
- lines.push(
21324
- `To retry from there: apitogo deploy --resume-from ${parsed.failedStep}`
21325
- );
21326
- }
21327
- return lines;
21328
- }
21329
- return [];
21330
- }
21331
-
21332
21442
  // src/cli/apply/platform-plan-prompt.ts
21333
21443
  import { stdin as input3, stdout as output2 } from "node:process";
21334
21444
  import { createInterface as createInterface2 } from "node:readline/promises";
@@ -21593,7 +21703,7 @@ async function apply(argv) {
21593
21703
  if (argv.json) {
21594
21704
  console.log(result);
21595
21705
  } else {
21596
- const humanLines = parsed ? formatNonSuccessResultForHuman(parsed) : [];
21706
+ const humanLines = parsed ? formatResultForHuman(parsed) : [];
21597
21707
  if (humanLines.length > 0) {
21598
21708
  for (const line of humanLines) {
21599
21709
  console.log(line);