@lukoweb/apitogo 0.2.61 → 0.2.64

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
@@ -6794,9 +6794,10 @@ function homeRedirect(input5) {
6794
6794
  return void 0;
6795
6795
  const landing = pickLandingRoute(input5.docRoutes, input5.pageOrders);
6796
6796
  if (landing) return { from: "/", to: landing };
6797
- return input5.apiReferencePath ? { from: "/", to: input5.apiReferencePath } : void 0;
6797
+ if (input5.apiReferencePath)
6798
+ return { from: "/", to: input5.apiReferencePath };
6798
6799
  }
6799
- if (input5.externalDocsUrl && !input5.userPanel) {
6800
+ if (!input5.hostsDocs && input5.externalDocsUrl && !input5.userPanel) {
6800
6801
  return { from: "/", to: input5.externalDocsUrl };
6801
6802
  }
6802
6803
  if (input5.userPanel) {
@@ -6805,6 +6806,54 @@ function homeRedirect(input5) {
6805
6806
  return void 0;
6806
6807
  }
6807
6808
 
6809
+ // src/config/v2/portal-profile.ts
6810
+ var NOT_BUILT = {
6811
+ enabled: false,
6812
+ docs: { enabled: false, provider: "apitogo", content: [] },
6813
+ console: { enabled: false },
6814
+ pipelines: { markdown: false, openapi: false, search: "none" },
6815
+ modules: { docs: false, userPanel: false }
6816
+ };
6817
+ function portalOnlyLinksExternalDocs(resolved) {
6818
+ const portal = resolved.portal;
6819
+ if (!portal?.enabled) return false;
6820
+ if (!portal.docs.enabled || portal.docs.provider !== "external") return false;
6821
+ const consoleFed = portal.console.enabled && (resolved.identity?.enabled === true || resolved.plans?.enabled === true);
6822
+ return !consoleFed;
6823
+ }
6824
+ function portalProfile(resolved) {
6825
+ const portal = resolved.portal;
6826
+ if (!portal?.enabled) return NOT_BUILT;
6827
+ const hostsDocs = portal.docs.enabled && portal.docs.provider === "apitogo";
6828
+ const externalDocs = portal.docs.enabled && portal.docs.provider === "external";
6829
+ const gatewayOn2 = resolved.gateway?.enabled === true;
6830
+ return {
6831
+ enabled: true,
6832
+ docs: {
6833
+ enabled: portal.docs.enabled,
6834
+ provider: portal.docs.provider,
6835
+ content: hostsDocs ? portal.docs.content : [],
6836
+ url: externalDocs ? portal.docs.url : void 0
6837
+ },
6838
+ console: { enabled: portal.console.enabled },
6839
+ pipelines: {
6840
+ markdown: hostsDocs,
6841
+ openapi: hostsDocs && gatewayOn2,
6842
+ search: hostsDocs ? portal.search : "none"
6843
+ },
6844
+ modules: { docs: hostsDocs, userPanel: portal.console.enabled },
6845
+ ...externalDocs ? {
6846
+ externalDocs: {
6847
+ url: portal.docs.url,
6848
+ openapiExport: gatewayOn2,
6849
+ cors: portal.console.enabled,
6850
+ consoleLinksInManifest: portal.console.enabled,
6851
+ whiteLabelConsole: portal.console.enabled
6852
+ }
6853
+ } : {}
6854
+ };
6855
+ }
6856
+
6808
6857
  // src/config/v2/tsx-pages.ts
6809
6858
  import { readFile as readFile4 } from "node:fs/promises";
6810
6859
  import path10 from "node:path";
@@ -6972,11 +7021,11 @@ function collectSecretRefs(config2) {
6972
7021
  return found;
6973
7022
  }
6974
7023
  function dependencyRules(config2, resolved, out) {
6975
- const gatewayOn = resolved.gateway?.enabled === true;
7024
+ const gatewayOn2 = resolved.gateway?.enabled === true;
6976
7025
  const identityOn = resolved.identity?.enabled === true;
6977
- const oidcGateway = gatewayOn && resolved.gateway?.auth?.mode === "oidc";
7026
+ const oidcGateway = gatewayOn2 && resolved.gateway?.auth?.mode === "oidc";
6978
7027
  const hasIdentitySource = identityOn || oidcGateway;
6979
- if (identityOn && resolved.portal?.enabled !== true && !gatewayOn && !resolved.project.id) {
7028
+ if (identityOn && resolved.portal?.enabled !== true && !gatewayOn2 && !resolved.project.id) {
6980
7029
  out.error({
6981
7030
  code: "NOTHING_TO_HOST",
6982
7031
  path: "identity",
@@ -6993,7 +7042,7 @@ function dependencyRules(config2, resolved, out) {
6993
7042
  });
6994
7043
  }
6995
7044
  if (resolved.plans?.enabled) {
6996
- if (!gatewayOn) {
7045
+ if (!gatewayOn2) {
6997
7046
  out.error({
6998
7047
  code: "PLANS_GATEWAY_REQUIRED",
6999
7048
  path: "plans",
@@ -7089,6 +7138,14 @@ async function portalRules(config2, resolved, out) {
7089
7138
  hint: "Enable identity or plans, or enable portal.docs."
7090
7139
  });
7091
7140
  }
7141
+ if (portalOnlyLinksExternalDocs(resolved)) {
7142
+ out.warn({
7143
+ code: "PORTAL_NOTHING_TO_HOST",
7144
+ path: "portal",
7145
+ message: "docs are on an external site and no service feeds the console, so there is no portal to host: `project create` takes no website hosting for it and `deploy` skips it.",
7146
+ hint: "Enable identity or plans for a console next to your docs, or remove the portal section."
7147
+ });
7148
+ }
7092
7149
  if (portal.footer && !await isFile(portal.footer))
7093
7150
  out.error({
7094
7151
  code: "PORTAL_FILE_MISSING",
@@ -7142,6 +7199,15 @@ async function docsContentRule(config2, patterns, resolved, out) {
7142
7199
  const shown = config2.portal?.docs?.content ?? [
7143
7200
  path11.relative(resolved.configDir, patterns[0] ?? "")
7144
7201
  ];
7202
+ if (portalHasNothingToShow(resolved)) {
7203
+ out.error({
7204
+ code: "PORTAL_HOME_EMPTY",
7205
+ path: "portal.docs",
7206
+ message: `portal.docs matched 0 files (${shown.join(", ")}), and the portal has no API reference, console pages or home page of its own, so its home page would say "Page not found".`,
7207
+ hint: "Add a .md or .mdx page there (for example introduction.mdx), point docs.content at your existing docs, or enable the gateway with an OpenAPI file for an API reference."
7208
+ });
7209
+ return;
7210
+ }
7145
7211
  out.warn({
7146
7212
  code: "PORTAL_DOCS_EMPTY",
7147
7213
  path: "portal.docs",
@@ -7149,6 +7215,13 @@ async function docsContentRule(config2, patterns, resolved, out) {
7149
7215
  hint: "Add .md, .mdx or .tsx pages, or point docs.content at your existing docs."
7150
7216
  });
7151
7217
  }
7218
+ function portalHasNothingToShow(resolved) {
7219
+ const portal = resolved.portal;
7220
+ if (!portal?.enabled || portal.landing) return false;
7221
+ if (resolved.gateway?.enabled === true) return false;
7222
+ const consoleFed = portal.console.enabled && (resolved.identity?.enabled === true || resolved.plans?.enabled === true);
7223
+ return !consoleFed;
7224
+ }
7152
7225
  var RESERVED_HEADER_ROUTES = [
7153
7226
  "/",
7154
7227
  "/index",
@@ -7618,47 +7691,6 @@ async function checkConfigV2(config2, resolved, context = {}) {
7618
7691
  return { errors, warnings, skipped };
7619
7692
  }
7620
7693
 
7621
- // src/config/v2/portal-profile.ts
7622
- var NOT_BUILT = {
7623
- enabled: false,
7624
- docs: { enabled: false, provider: "apitogo", content: [] },
7625
- console: { enabled: false },
7626
- pipelines: { markdown: false, openapi: false, search: "none" },
7627
- modules: { docs: false, userPanel: false }
7628
- };
7629
- function portalProfile(resolved) {
7630
- const portal = resolved.portal;
7631
- if (!portal?.enabled) return NOT_BUILT;
7632
- const hostsDocs = portal.docs.enabled && portal.docs.provider === "apitogo";
7633
- const externalDocs = portal.docs.enabled && portal.docs.provider === "external";
7634
- const gatewayOn = resolved.gateway?.enabled === true;
7635
- return {
7636
- enabled: true,
7637
- docs: {
7638
- enabled: portal.docs.enabled,
7639
- provider: portal.docs.provider,
7640
- content: hostsDocs ? portal.docs.content : [],
7641
- url: externalDocs ? portal.docs.url : void 0
7642
- },
7643
- console: { enabled: portal.console.enabled },
7644
- pipelines: {
7645
- markdown: hostsDocs,
7646
- openapi: hostsDocs && gatewayOn,
7647
- search: hostsDocs ? portal.search : "none"
7648
- },
7649
- modules: { docs: hostsDocs, userPanel: portal.console.enabled },
7650
- ...externalDocs ? {
7651
- externalDocs: {
7652
- url: portal.docs.url,
7653
- openapiExport: gatewayOn,
7654
- cors: portal.console.enabled,
7655
- consoleLinksInManifest: portal.console.enabled,
7656
- whiteLabelConsole: portal.console.enabled
7657
- }
7658
- } : {}
7659
- };
7660
- }
7661
-
7662
7694
  // src/config/v2/manifest.ts
7663
7695
  import { mkdir as mkdir3, readFile as readFile6, rename, rm, writeFile as writeFile4 } from "node:fs/promises";
7664
7696
  import path12 from "node:path";
@@ -7777,10 +7809,10 @@ function requiredServices(resolved) {
7777
7809
  if (!portal?.enabled) return [];
7778
7810
  const needs = [];
7779
7811
  const consoleOn = portal.console.enabled;
7780
- const gatewayOn = resolved.gateway?.enabled === true;
7812
+ const gatewayOn2 = resolved.gateway?.enabled === true;
7781
7813
  const identityOn = resolved.identity?.enabled === true;
7782
7814
  if (identityOn && (consoleOn || portal.requireAuth)) needs.push("identity");
7783
- if (gatewayOn && (consoleOn || portal.docs.enabled)) needs.push("gateway");
7815
+ if (gatewayOn2 && (consoleOn || portal.docs.enabled)) needs.push("gateway");
7784
7816
  if (resolved.plans?.enabled === true && consoleOn) needs.push("plans");
7785
7817
  return needs;
7786
7818
  }
@@ -10261,6 +10293,17 @@ var PlatformApiError = class extends Error {
10261
10293
  this.problem = problem;
10262
10294
  }
10263
10295
  };
10296
+ var ProjectGoneError = class extends PlatformApiError {
10297
+ constructor(projectId) {
10298
+ super(
10299
+ `The project ${projectId} no longer exists on the platform: it was deleted, or it is not in the account you are signed in to. Run \`apitogo project create\` to make a new project for this folder, or link another one with \`--project-id <id>\`.`,
10300
+ 410
10301
+ );
10302
+ this.projectId = projectId;
10303
+ this.name = "ProjectGoneError";
10304
+ }
10305
+ code = "PROJECT_GONE";
10306
+ };
10264
10307
  function formatProblemDetails(problem) {
10265
10308
  const parts = [];
10266
10309
  if (problem.title) parts.push(problem.title);
@@ -10352,6 +10395,11 @@ async function fetchWithRetry(input5, init3, options) {
10352
10395
  }
10353
10396
 
10354
10397
  // src/cli/platform/client/platform-api.ts
10398
+ function projectIdOfPath(path100) {
10399
+ return /^\/api\/v1\/projects\/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})(?:[/?#]|$)/i.exec(
10400
+ path100
10401
+ )?.[1];
10402
+ }
10355
10403
  var MAX_RETRIES = 3;
10356
10404
  var UPLOAD_TIMEOUT_MS = 10 * 60 * 1e3;
10357
10405
  var PlatformApiClient = class {
@@ -10387,13 +10435,39 @@ var PlatformApiClient = class {
10387
10435
  }
10388
10436
  );
10389
10437
  if (!response.ok) {
10390
- throw await parseErrorResponse(response);
10438
+ throw await this.failure(response, path100);
10391
10439
  }
10392
10440
  if (response.status === 204) {
10393
10441
  return void 0;
10394
10442
  }
10395
10443
  return await response.json();
10396
10444
  }
10445
+ /**
10446
+ * The error for a failed answer. A 404 under a project may mean the project itself is gone; a bare "Not Found" hid
10447
+ * that (API-133), so the project is asked once and a missing one becomes {@link ProjectGoneError}.
10448
+ */
10449
+ async failure(response, path100) {
10450
+ const error = await parseErrorResponse(response);
10451
+ const projectId = response.status === 404 ? projectIdOfPath(path100) : void 0;
10452
+ if (projectId && await this.projectMissing(projectId, path100))
10453
+ return new ProjectGoneError(projectId);
10454
+ return error;
10455
+ }
10456
+ /** Whether the project answers 404 itself. A path that is the project needs no second call. */
10457
+ async projectMissing(projectId, path100) {
10458
+ const projectPath = `/api/v1/projects/${projectId}`;
10459
+ if (path100.replace(/[?#].*$/, "").replace(/\/+$/, "") === projectPath)
10460
+ return true;
10461
+ try {
10462
+ const response = await fetch(this.url(projectPath), {
10463
+ method: "GET",
10464
+ headers: this.headers()
10465
+ });
10466
+ return response.status === 404;
10467
+ } catch {
10468
+ return false;
10469
+ }
10470
+ }
10397
10471
  whoami() {
10398
10472
  return this.request("GET", "/api/v1/auth/me");
10399
10473
  }
@@ -10501,7 +10575,10 @@ var PlatformApiClient = class {
10501
10575
  { method: "POST", maxRetries: 0, retryMutatingOnServerError: false }
10502
10576
  );
10503
10577
  if (!response.ok) {
10504
- throw await parseErrorResponse(response);
10578
+ throw await this.failure(
10579
+ response,
10580
+ `/api/v1/projects/${projectId}/infrastructure/static-web/deployments`
10581
+ );
10505
10582
  }
10506
10583
  return await response.json();
10507
10584
  } catch (error) {
@@ -10577,7 +10654,10 @@ var PlatformApiClient = class {
10577
10654
  }
10578
10655
  );
10579
10656
  if (!response.ok) {
10580
- throw await parseErrorResponse(response);
10657
+ throw await this.failure(
10658
+ response,
10659
+ `/api/v1/projects/${projectId}/backend/openapi`
10660
+ );
10581
10661
  }
10582
10662
  return await response.json();
10583
10663
  }
@@ -10626,7 +10706,11 @@ var PlatformApiClient = class {
10626
10706
  maxRetries: MAX_RETRIES
10627
10707
  }
10628
10708
  );
10629
- if (!response.ok) throw await parseErrorResponse(response);
10709
+ if (!response.ok)
10710
+ throw await this.failure(
10711
+ response,
10712
+ `/api/v1/projects/${projectId}/gateway/openapi.json`
10713
+ );
10630
10714
  return response.text();
10631
10715
  }
10632
10716
  getGatewayCapabilities(projectId) {
@@ -10938,7 +11022,10 @@ var PlatformApiClient = class {
10938
11022
  }
10939
11023
  );
10940
11024
  if (!response.ok) {
10941
- throw await parseErrorResponse(response);
11025
+ throw await this.failure(
11026
+ response,
11027
+ `/api/v1/projects/${projectId}/backend/deployments`
11028
+ );
10942
11029
  }
10943
11030
  return await response.json();
10944
11031
  }
@@ -11343,7 +11430,7 @@ import {
11343
11430
  // package.json
11344
11431
  var package_default = {
11345
11432
  name: "@lukoweb/apitogo",
11346
- version: "0.2.61",
11433
+ version: "0.2.64",
11347
11434
  type: "module",
11348
11435
  sideEffects: [
11349
11436
  "**/*.css",
@@ -26955,11 +27042,22 @@ function assertConfigChecksOut(output4) {
26955
27042
  ].join("\n")
26956
27043
  );
26957
27044
  }
26958
- var hostsAnything = (config2) => config2.portal && config2.portal.enabled !== false || config2.gateway && config2.gateway.enabled !== false;
27045
+ var gatewayOn = (config2) => config2.gateway !== void 0 && config2.gateway.enabled !== false;
27046
+ var hostsAnything = (config2) => config2.portal && config2.portal.enabled !== false || gatewayOn(config2);
26959
27047
  async function createProjectV2(options, deps) {
26960
27048
  const { config: config2, resolved } = loadConfigV2(options.dir);
26961
27049
  const linkedProjectId = resolved.project.id;
26962
- const modules = modulesForConfig(config2);
27050
+ const externalDocsOnly = portalOnlyLinksExternalDocs(resolved);
27051
+ const modules = modulesForConfig(config2).filter(
27052
+ (module) => module !== "dev-portal" || !externalDocsOnly
27053
+ );
27054
+ if (externalDocsOnly && !gatewayOn(config2)) {
27055
+ throw new ProjectCreateError(
27056
+ "NOTHING_TO_HOST",
27057
+ "No enabled service needs a project: the docs are on an external site and nothing feeds the portal console.",
27058
+ "Enable identity or plans for a console next to your docs (`apitogo add identity`), or a gateway (`apitogo add gateway`)."
27059
+ );
27060
+ }
26963
27061
  if (!hostsAnything(config2)) {
26964
27062
  throw new ProjectCreateError(
26965
27063
  "NOTHING_TO_HOST",
@@ -28472,6 +28570,16 @@ var IdentityDeployError = class extends ApitogoError {
28472
28570
  this.name = "IdentityDeployError";
28473
28571
  }
28474
28572
  };
28573
+ var IDENTITY_NOT_ACTIVE = "IDENTITY_NOT_ACTIVE";
28574
+ async function identityLost(client, projectId) {
28575
+ if (!client.listProjectIdentityConnections) return false;
28576
+ try {
28577
+ await client.listProjectIdentityConnections(projectId);
28578
+ return false;
28579
+ } catch (error) {
28580
+ return error instanceof PlatformApiError && error.status === 409 && error.problem?.code === IDENTITY_NOT_ACTIVE;
28581
+ }
28582
+ }
28475
28583
  var IDENTITY_STEPS = ["request", "activate"];
28476
28584
  var IDENTITY_CONNECTIONS_STEP = "connections";
28477
28585
  var SECRET_PREFIX = "secret:";
@@ -28671,10 +28779,13 @@ async function deployIdentity(resolved, projectId, deps, options = {}) {
28671
28779
  return rejected2(error);
28672
28780
  }
28673
28781
  },
28674
- // An identity that was removed, or that failed, is not a result worth keeping.
28782
+ // An identity that was removed, or that failed, is not a result worth keeping. Neither is one the platform still
28783
+ // lists as Active while the identity service lost it (API-132): asking again makes it again.
28675
28784
  stillValid: async () => {
28676
28785
  const found = await client.getProjectIdentity(projectId);
28677
- return found !== null && found.status !== "Failed" && found.status !== "Released" && found.status !== "Releasing";
28786
+ if (found === null || found.status === "Failed" || found.status === "Released" || found.status === "Releasing")
28787
+ return false;
28788
+ return found.status !== "Active" || !await identityLost(client, projectId);
28678
28789
  }
28679
28790
  };
28680
28791
  const activateStep = {
@@ -29604,6 +29715,25 @@ async function statusV2(dir, only, options = {}) {
29604
29715
  const ledger = await readLedger(resolved.state);
29605
29716
  const manifest = await readManifest(resolved.state);
29606
29717
  const services = [];
29718
+ const manifestSummary = {
29719
+ present: manifest !== void 0,
29720
+ schemaVersion: manifest?.schemaVersion,
29721
+ generatedAt: manifest?.generatedAt
29722
+ };
29723
+ const projectId = resolved.project.id ?? manifest?.project.id;
29724
+ if (options.projectRemote && projectId) {
29725
+ try {
29726
+ await options.projectRemote(projectId);
29727
+ } catch (error) {
29728
+ if (error instanceof ProjectGoneError)
29729
+ return {
29730
+ configPath: resolved.configPath,
29731
+ manifest: manifestSummary,
29732
+ project: { state: "gone", id: projectId, message: error.message },
29733
+ services: []
29734
+ };
29735
+ }
29736
+ }
29607
29737
  for (const service of chooseServices(only)) {
29608
29738
  const section = sectionOf(resolved, service);
29609
29739
  if (!section) {
@@ -29667,11 +29797,7 @@ async function statusV2(dir, only, options = {}) {
29667
29797
  }
29668
29798
  return {
29669
29799
  configPath: resolved.configPath,
29670
- manifest: {
29671
- present: manifest !== void 0,
29672
- schemaVersion: manifest?.schemaVersion,
29673
- generatedAt: manifest?.generatedAt
29674
- },
29800
+ manifest: manifestSummary,
29675
29801
  services
29676
29802
  };
29677
29803
  }
@@ -29685,6 +29811,8 @@ function formatPlan(output4, options = {}) {
29685
29811
  ]);
29686
29812
  }
29687
29813
  function formatStatus(output4) {
29814
+ if (output4.project?.state === "gone")
29815
+ return [`project: gone`, ` ${output4.project.message}`];
29688
29816
  return [
29689
29817
  output4.manifest.present ? `services.json: version ${output4.manifest.schemaVersion}${output4.manifest.generatedAt ? `, written ${output4.manifest.generatedAt}` : ""}` : "services.json: none yet",
29690
29818
  ...output4.services.flatMap((svc) => [
@@ -29837,6 +29965,15 @@ async function deployV2(options, deps) {
29837
29965
  results.push(await deployPlansService(options, deps, projectId, named));
29838
29966
  continue;
29839
29967
  }
29968
+ if (portalOnlyLinksExternalDocs(resolved)) {
29969
+ results.push({
29970
+ service: "portal",
29971
+ status: "skipped",
29972
+ reason: "docs are on an external site and no service feeds the console, so there is nothing to host",
29973
+ steps: []
29974
+ });
29975
+ continue;
29976
+ }
29840
29977
  results.push(await deployPortal(options, deps, projectId));
29841
29978
  }
29842
29979
  const gatewayAt = results.findIndex(
@@ -30609,6 +30746,14 @@ var plan = (argv) => run2(
30609
30746
  }),
30610
30747
  (output4) => formatPlan(output4, { verbose: argv.verbose === true })
30611
30748
  );
30749
+ function projectRemote() {
30750
+ try {
30751
+ const client = new PlatformApiClient(loadConfig());
30752
+ return (projectId) => client.getProject(projectId);
30753
+ } catch {
30754
+ return void 0;
30755
+ }
30756
+ }
30612
30757
  function gatewayRemote() {
30613
30758
  try {
30614
30759
  const client = new PlatformApiClient(loadConfig());
@@ -30666,6 +30811,7 @@ var statusOfProject = (argv) => run2(
30666
30811
  "status",
30667
30812
  argv,
30668
30813
  (dir, only) => statusV2(dir, only, {
30814
+ projectRemote: projectRemote(),
30669
30815
  hostingRemote: hostingRemote(),
30670
30816
  gatewayRemote: gatewayRemote(),
30671
30817
  identityRemote: identityRemote(),
@@ -30787,7 +30933,7 @@ async function switchProject(options, deps) {
30787
30933
  try {
30788
30934
  project = await deps.getProject(projectId);
30789
30935
  } catch (error) {
30790
- if (error instanceof PlatformApiError && (error.status === 404 || error.status === 403))
30936
+ if (error instanceof ProjectGoneError || error instanceof PlatformApiError && (error.status === 404 || error.status === 403))
30791
30937
  throw new ProjectUseError(
30792
30938
  "PROJECT_NOT_FOUND",
30793
30939
  `No project ${projectId} is yours.`,
@@ -1,4 +1,5 @@
1
1
  import type { ApitogoConfig } from "../config.js";
2
+ export declare function projectIdOfPath(path: string): string | undefined;
2
3
  export interface TenantDto {
3
4
  id: string;
4
5
  email: string;
@@ -545,6 +546,8 @@ export declare class PlatformApiClient {
545
546
  private url;
546
547
  private headers;
547
548
  private request;
549
+ private failure;
550
+ private projectMissing;
548
551
  whoami(): Promise<TenantDto>;
549
552
  listProjects(): Promise<ProjectDto[]>;
550
553
  getProject(projectId: string): Promise<ProjectDto>;
@@ -11,6 +11,11 @@ export declare class PlatformApiError extends Error {
11
11
  readonly problem?: ProblemDetails;
12
12
  constructor(message: string, status: number, problem?: ProblemDetails);
13
13
  }
14
+ export declare class ProjectGoneError extends PlatformApiError {
15
+ readonly projectId: string;
16
+ readonly code = "PROJECT_GONE";
17
+ constructor(projectId: string);
18
+ }
14
19
  export declare function formatProblemDetails(problem: ProblemDetails): string;
15
20
  export declare function parseErrorResponse(response: Response): Promise<PlatformApiError>;
16
21
  export declare function toolErrorPayload(error: unknown): string;
package/docs/mcp/setup.md CHANGED
@@ -22,9 +22,12 @@ Every host launches the same stdio process:
22
22
  | Field | Value |
23
23
  | ------- | ---------------------------------------------------- |
24
24
  | Command | `npx` |
25
- | Args | `-y`, `@lukoweb/apitogo-mcp` |
25
+ | Args | `-y`, `@lukoweb/apitogo-mcp@latest` |
26
26
  | Env | Optional: `APITOGO_API_KEY_ID`, `APITOGO_API_SECRET` |
27
27
 
28
+ Keep `@latest` in the package name: without it npx can start an older copy it has kept from an
29
+ earlier run, with tools that no longer match this guide.
30
+
28
31
  The tools run the `apitogo` CLI the project installed (`node_modules/@lukoweb/apitogo`), or the
29
32
  latest one through npx in a folder without it. Optional env: `APITOGO_API_KEY_ID` and
30
33
  `APITOGO_API_SECRET` instead of `apitogo login`, `APITOGO_API_URL`, and `APITOGO_CLI_PATH` (a local
@@ -44,7 +47,7 @@ Add to `~/.cursor/mcp.json` (user) or `.cursor/mcp.json` (project):
44
47
  "mcpServers": {
45
48
  "apitogo": {
46
49
  "command": "npx",
47
- "args": ["-y", "@lukoweb/apitogo-mcp"],
50
+ "args": ["-y", "@lukoweb/apitogo-mcp@latest"],
48
51
  "env": {
49
52
  "APITOGO_API_KEY_ID": "apk_...",
50
53
  "APITOGO_API_SECRET": "..."
@@ -70,7 +73,7 @@ Edit the Claude Desktop MCP config (same `mcpServers` shape as Cursor):
70
73
  "mcpServers": {
71
74
  "apitogo": {
72
75
  "command": "npx",
73
- "args": ["-y", "@lukoweb/apitogo-mcp"],
76
+ "args": ["-y", "@lukoweb/apitogo-mcp@latest"],
74
77
  "env": {
75
78
  "APITOGO_API_KEY_ID": "apk_...",
76
79
  "APITOGO_API_SECRET": "..."
@@ -88,7 +91,7 @@ Fully quit and reopen Claude Desktop after saving.
88
91
 
89
92
  ```bash
90
93
  claude mcp add --scope user --env APITOGO_API_KEY_ID=apk_... --env APITOGO_API_SECRET=... \
91
- apitogo -- npx -y @lukoweb/apitogo-mcp
94
+ apitogo -- npx -y @lukoweb/apitogo-mcp@latest
92
95
  ```
93
96
 
94
97
  **Project scope** (committed `.mcp.json` — use `${VAR}` for secrets):
@@ -96,7 +99,7 @@ claude mcp add --scope user --env APITOGO_API_KEY_ID=apk_... --env APITOGO_API_S
96
99
  ```bash
97
100
  claude mcp add --scope project --env APITOGO_API_KEY_ID=${APITOGO_API_KEY_ID} \
98
101
  --env APITOGO_API_SECRET=${APITOGO_API_SECRET} \
99
- apitogo -- npx -y @lukoweb/apitogo-mcp
102
+ apitogo -- npx -y @lukoweb/apitogo-mcp@latest
100
103
  ```
101
104
 
102
105
  Or hand-edit `.mcp.json` in the project root:
@@ -107,7 +110,7 @@ Or hand-edit `.mcp.json` in the project root:
107
110
  "apitogo": {
108
111
  "type": "stdio",
109
112
  "command": "npx",
110
- "args": ["-y", "@lukoweb/apitogo-mcp"],
113
+ "args": ["-y", "@lukoweb/apitogo-mcp@latest"],
111
114
  "env": {
112
115
  "APITOGO_API_KEY_ID": "${APITOGO_API_KEY_ID}",
113
116
  "APITOGO_API_SECRET": "${APITOGO_API_SECRET}"
@@ -128,7 +131,7 @@ Edit `~/.codex/config.toml` (or project `.codex/config.toml` in a trusted projec
128
131
  ```toml
129
132
  [mcp_servers.apitogo]
130
133
  command = "npx"
131
- args = ["-y", "@lukoweb/apitogo-mcp"]
134
+ args = ["-y", "@lukoweb/apitogo-mcp@latest"]
132
135
 
133
136
  [mcp_servers.apitogo.env]
134
137
  APITOGO_API_KEY_ID = "apk_..."
@@ -142,7 +145,7 @@ Restart Codex / open a new session. See [Codex MCP docs](https://developers.open
142
145
 
143
146
  Any client that supports **stdio** MCP can run APItoGo:
144
147
 
145
- 1. Set command `npx`, args `["-y", "@lukoweb/apitogo-mcp"]`
148
+ 1. Set command `npx`, args `["-y", "@lukoweb/apitogo-mcp@latest"]`
146
149
  2. Sign in with `apitogo login`, or pass `APITOGO_API_KEY_ID` and `APITOGO_API_SECRET` in the server
147
150
  env
148
151
  3. Restart the host and confirm tools load
@@ -158,9 +161,11 @@ In a new agent chat:
158
161
  `apitogo_create`, `apitogo_deploy` and `apitogo_status`.
159
162
  2. Ask it for the status of your project; it calls `apitogo_status`.
160
163
 
161
- If the server fails to start: confirm Node 20+ and that `npx -y @lukoweb/apitogo-mcp` works in a
162
- terminal. If a tool says you are not logged in, run `apitogo login` or check the API key id and
163
- secret.
164
+ If the server fails to start: confirm Node 20+ and that `npx -y @lukoweb/apitogo-mcp@latest` works
165
+ in a terminal. If the agent shows many more tools than the ones in [Tools](./tools.md), or asks for
166
+ an API key although you are logged in, the host is running an old copy: add `@latest` as above and
167
+ restart the host. If a tool says you are not logged in, run `apitogo login` or check the API key id
168
+ and secret.
164
169
 
165
170
  ## Next
166
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.61",
3
+ "version": "0.2.64",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -117,7 +117,7 @@ export type HomeRedirectInput = {
117
117
  * "Page not found".
118
118
  *
119
119
  * - Hosted docs without an index page: the first page, so the team's own docs never need an extra file.
120
- * - Hosted docs without any page: the API reference, when there is one.
120
+ * - Hosted docs without any page: the API reference, when there is one, else the console pages as below (API-116).
121
121
  * - External docs and no console: the external docs site, as the portal has nothing else to show.
122
122
  * - No hosted docs but a console: the pricing page when there is one, else the account pages, which lead a signed-out
123
123
  * visitor to sign in. The console has no page of its own at `/`, so the address `deploy` prints showed "Page not
@@ -131,11 +131,11 @@ export function homeRedirect(
131
131
  return undefined;
132
132
  const landing = pickLandingRoute(input.docRoutes, input.pageOrders);
133
133
  if (landing) return { from: "/", to: landing };
134
- return input.apiReferencePath
135
- ? { from: "/", to: input.apiReferencePath }
136
- : undefined;
134
+ if (input.apiReferencePath)
135
+ return { from: "/", to: input.apiReferencePath };
136
+ // No pages and no reference yet: the console pages, when there are any, rather than "Page not found" (API-116).
137
137
  }
138
- if (input.externalDocsUrl && !input.userPanel) {
138
+ if (!input.hostsDocs && input.externalDocsUrl && !input.userPanel) {
139
139
  return { from: "/", to: input.externalDocsUrl };
140
140
  }
141
141
  if (input.userPanel) {
@@ -54,7 +54,11 @@ export {
54
54
  ConfigV2Schema,
55
55
  type Layout,
56
56
  } from "./schema.js";
57
- export { type PortalProfile, portalProfile } from "./portal-profile.js";
57
+ export {
58
+ type PortalProfile,
59
+ portalOnlyLinksExternalDocs,
60
+ portalProfile,
61
+ } from "./portal-profile.js";
58
62
  export {
59
63
  hasCapability,
60
64
  type ManifestServiceName,
@@ -49,6 +49,23 @@ const NOT_BUILT: PortalProfile = {
49
49
  modules: { docs: false, userPanel: false },
50
50
  };
51
51
 
52
+ /**
53
+ * The portal's docs are on an external site and no service (identity or plans) feeds its console, so the portal would be
54
+ * an empty site. `project create` takes no website hosting for it and `deploy` does not build or publish it (API-101: such
55
+ * a portal waited on hosting it never needed, and deploy exited 1).
56
+ */
57
+ export function portalOnlyLinksExternalDocs(
58
+ resolved: Pick<ResolvedConfigV2, "portal" | "identity" | "plans">,
59
+ ): boolean {
60
+ const portal = resolved.portal;
61
+ if (!portal?.enabled) return false;
62
+ if (!portal.docs.enabled || portal.docs.provider !== "external") return false;
63
+ const consoleFed =
64
+ portal.console.enabled &&
65
+ (resolved.identity?.enabled === true || resolved.plans?.enabled === true);
66
+ return !consoleFed;
67
+ }
68
+
52
69
  export function portalProfile(resolved: ResolvedConfigV2): PortalProfile {
53
70
  const portal = resolved.portal;
54
71
  if (!portal?.enabled) return NOT_BUILT;
@@ -5,6 +5,7 @@ import { parse as parseYaml } from "yaml";
5
5
  import { billingNeeded, stripeKeySecretName } from "./billing.js";
6
6
  import type { ConfigIssue } from "./errors.js";
7
7
  import { docRoutes } from "./home-redirect.js";
8
+ import { portalOnlyLinksExternalDocs } from "./portal-profile.js";
8
9
  import { isLogoUrl, type ResolvedConfigV2, toGlobPath } from "./resolve.js";
9
10
  import type { ConfigV2 } from "./schema.js";
10
11
  import { findTsxPages } from "./tsx-pages.js";
@@ -248,6 +249,17 @@ async function portalRules(
248
249
  });
249
250
  }
250
251
 
252
+ // Docs on an external site and nothing for the console: the portal would be an empty site (API-101).
253
+ if (portalOnlyLinksExternalDocs(resolved)) {
254
+ out.warn({
255
+ code: "PORTAL_NOTHING_TO_HOST",
256
+ path: "portal",
257
+ message:
258
+ "docs are on an external site and no service feeds the console, so there is no portal to host: `project create` takes no website hosting for it and `deploy` skips it.",
259
+ hint: "Enable identity or plans for a console next to your docs, or remove the portal section.",
260
+ });
261
+ }
262
+
251
263
  // A missing footer or logo file would fail the build, so check says so first (API-129).
252
264
  if (portal.footer && !(await isFile(portal.footer)))
253
265
  out.error({
@@ -317,6 +329,16 @@ async function docsContentRule(
317
329
  const shown = config.portal?.docs?.content ?? [
318
330
  path.relative(resolved.configDir, patterns[0] ?? ""),
319
331
  ];
332
+ // No pages and nothing else for `/` either: the deployed home page would say "Page not found" (API-116).
333
+ if (portalHasNothingToShow(resolved)) {
334
+ out.error({
335
+ code: "PORTAL_HOME_EMPTY",
336
+ path: "portal.docs",
337
+ message: `portal.docs matched 0 files (${shown.join(", ")}), and the portal has no API reference, console pages or home page of its own, so its home page would say "Page not found".`,
338
+ hint: "Add a .md or .mdx page there (for example introduction.mdx), point docs.content at your existing docs, or enable the gateway with an OpenAPI file for an API reference.",
339
+ });
340
+ return;
341
+ }
320
342
  out.warn({
321
343
  code: "PORTAL_DOCS_EMPTY",
322
344
  path: "portal.docs",
@@ -325,6 +347,20 @@ async function docsContentRule(
325
347
  });
326
348
  }
327
349
 
350
+ /**
351
+ * A portal that hosts docs but has nothing else to show at `/`: no home page of its own, no gateway for an API reference,
352
+ * and no service (identity or plans) for console pages.
353
+ */
354
+ function portalHasNothingToShow(resolved: ResolvedConfigV2): boolean {
355
+ const portal = resolved.portal;
356
+ if (!portal?.enabled || portal.landing) return false;
357
+ if (resolved.gateway?.enabled === true) return false;
358
+ const consoleFed =
359
+ portal.console.enabled &&
360
+ (resolved.identity?.enabled === true || resolved.plans?.enabled === true);
361
+ return !consoleFed;
362
+ }
363
+
328
364
  /** Addresses the portal itself uses, so a header page cannot take them. */
329
365
  const RESERVED_HEADER_ROUTES = [
330
366
  "/",