@lukoweb/apitogo 0.2.61 → 0.2.65
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 +219 -67
- package/dist/declarations/cli/platform/client/platform-api.d.ts +3 -0
- package/dist/declarations/cli/platform/errors.d.ts +5 -0
- package/dist/declarations/cli/platform/tools/infrastructure.d.ts +1 -0
- package/docs/mcp/setup.md +16 -11
- package/package.json +1 -1
- package/src/config/v2/home-redirect.ts +5 -5
- package/src/config/v2/index.ts +5 -1
- package/src/config/v2/portal-profile.ts +17 -0
- package/src/config/v2/rules.ts +36 -0
- package/src/config/v2/wrapper.ts +8 -4
- package/src/lib/components/Meta.tsx +49 -46
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
|
-
|
|
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
|
|
7024
|
+
const gatewayOn2 = resolved.gateway?.enabled === true;
|
|
6976
7025
|
const identityOn = resolved.identity?.enabled === true;
|
|
6977
|
-
const oidcGateway =
|
|
7026
|
+
const oidcGateway = gatewayOn2 && resolved.gateway?.auth?.mode === "oidc";
|
|
6978
7027
|
const hasIdentitySource = identityOn || oidcGateway;
|
|
6979
|
-
if (identityOn && resolved.portal?.enabled !== true && !
|
|
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 (!
|
|
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
|
|
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 (
|
|
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
|
}
|
|
@@ -8487,10 +8519,13 @@ function buildSiteConfig(resolved, plan2, theme, extras = {}) {
|
|
|
8487
8519
|
}
|
|
8488
8520
|
] : []
|
|
8489
8521
|
];
|
|
8522
|
+
const logo = portal?.logo ? siteLogo(portal) : void 0;
|
|
8490
8523
|
return {
|
|
8524
|
+
// The browser tab's icon is the project's logo; without one, the portal's own icon (API-134).
|
|
8525
|
+
...logo ? { metadata: { favicon: logo.src.light } } : {},
|
|
8491
8526
|
site: {
|
|
8492
8527
|
title: portal?.name ?? resolved.project.name,
|
|
8493
|
-
...
|
|
8528
|
+
...logo ? { logo } : {},
|
|
8494
8529
|
...plan2.preview ? { banner: { message: PREVIEW_BANNER_MESSAGE, color: "caution" } } : {}
|
|
8495
8530
|
},
|
|
8496
8531
|
modules: {
|
|
@@ -10261,6 +10296,17 @@ var PlatformApiError = class extends Error {
|
|
|
10261
10296
|
this.problem = problem;
|
|
10262
10297
|
}
|
|
10263
10298
|
};
|
|
10299
|
+
var ProjectGoneError = class extends PlatformApiError {
|
|
10300
|
+
constructor(projectId) {
|
|
10301
|
+
super(
|
|
10302
|
+
`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>\`.`,
|
|
10303
|
+
410
|
|
10304
|
+
);
|
|
10305
|
+
this.projectId = projectId;
|
|
10306
|
+
this.name = "ProjectGoneError";
|
|
10307
|
+
}
|
|
10308
|
+
code = "PROJECT_GONE";
|
|
10309
|
+
};
|
|
10264
10310
|
function formatProblemDetails(problem) {
|
|
10265
10311
|
const parts = [];
|
|
10266
10312
|
if (problem.title) parts.push(problem.title);
|
|
@@ -10352,6 +10398,11 @@ async function fetchWithRetry(input5, init3, options) {
|
|
|
10352
10398
|
}
|
|
10353
10399
|
|
|
10354
10400
|
// src/cli/platform/client/platform-api.ts
|
|
10401
|
+
function projectIdOfPath(path100) {
|
|
10402
|
+
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(
|
|
10403
|
+
path100
|
|
10404
|
+
)?.[1];
|
|
10405
|
+
}
|
|
10355
10406
|
var MAX_RETRIES = 3;
|
|
10356
10407
|
var UPLOAD_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
10357
10408
|
var PlatformApiClient = class {
|
|
@@ -10387,13 +10438,39 @@ var PlatformApiClient = class {
|
|
|
10387
10438
|
}
|
|
10388
10439
|
);
|
|
10389
10440
|
if (!response.ok) {
|
|
10390
|
-
throw await
|
|
10441
|
+
throw await this.failure(response, path100);
|
|
10391
10442
|
}
|
|
10392
10443
|
if (response.status === 204) {
|
|
10393
10444
|
return void 0;
|
|
10394
10445
|
}
|
|
10395
10446
|
return await response.json();
|
|
10396
10447
|
}
|
|
10448
|
+
/**
|
|
10449
|
+
* The error for a failed answer. A 404 under a project may mean the project itself is gone; a bare "Not Found" hid
|
|
10450
|
+
* that (API-133), so the project is asked once and a missing one becomes {@link ProjectGoneError}.
|
|
10451
|
+
*/
|
|
10452
|
+
async failure(response, path100) {
|
|
10453
|
+
const error = await parseErrorResponse(response);
|
|
10454
|
+
const projectId = response.status === 404 ? projectIdOfPath(path100) : void 0;
|
|
10455
|
+
if (projectId && await this.projectMissing(projectId, path100))
|
|
10456
|
+
return new ProjectGoneError(projectId);
|
|
10457
|
+
return error;
|
|
10458
|
+
}
|
|
10459
|
+
/** Whether the project answers 404 itself. A path that is the project needs no second call. */
|
|
10460
|
+
async projectMissing(projectId, path100) {
|
|
10461
|
+
const projectPath = `/api/v1/projects/${projectId}`;
|
|
10462
|
+
if (path100.replace(/[?#].*$/, "").replace(/\/+$/, "") === projectPath)
|
|
10463
|
+
return true;
|
|
10464
|
+
try {
|
|
10465
|
+
const response = await fetch(this.url(projectPath), {
|
|
10466
|
+
method: "GET",
|
|
10467
|
+
headers: this.headers()
|
|
10468
|
+
});
|
|
10469
|
+
return response.status === 404;
|
|
10470
|
+
} catch {
|
|
10471
|
+
return false;
|
|
10472
|
+
}
|
|
10473
|
+
}
|
|
10397
10474
|
whoami() {
|
|
10398
10475
|
return this.request("GET", "/api/v1/auth/me");
|
|
10399
10476
|
}
|
|
@@ -10501,7 +10578,10 @@ var PlatformApiClient = class {
|
|
|
10501
10578
|
{ method: "POST", maxRetries: 0, retryMutatingOnServerError: false }
|
|
10502
10579
|
);
|
|
10503
10580
|
if (!response.ok) {
|
|
10504
|
-
throw await
|
|
10581
|
+
throw await this.failure(
|
|
10582
|
+
response,
|
|
10583
|
+
`/api/v1/projects/${projectId}/infrastructure/static-web/deployments`
|
|
10584
|
+
);
|
|
10505
10585
|
}
|
|
10506
10586
|
return await response.json();
|
|
10507
10587
|
} catch (error) {
|
|
@@ -10577,7 +10657,10 @@ var PlatformApiClient = class {
|
|
|
10577
10657
|
}
|
|
10578
10658
|
);
|
|
10579
10659
|
if (!response.ok) {
|
|
10580
|
-
throw await
|
|
10660
|
+
throw await this.failure(
|
|
10661
|
+
response,
|
|
10662
|
+
`/api/v1/projects/${projectId}/backend/openapi`
|
|
10663
|
+
);
|
|
10581
10664
|
}
|
|
10582
10665
|
return await response.json();
|
|
10583
10666
|
}
|
|
@@ -10626,7 +10709,11 @@ var PlatformApiClient = class {
|
|
|
10626
10709
|
maxRetries: MAX_RETRIES
|
|
10627
10710
|
}
|
|
10628
10711
|
);
|
|
10629
|
-
if (!response.ok)
|
|
10712
|
+
if (!response.ok)
|
|
10713
|
+
throw await this.failure(
|
|
10714
|
+
response,
|
|
10715
|
+
`/api/v1/projects/${projectId}/gateway/openapi.json`
|
|
10716
|
+
);
|
|
10630
10717
|
return response.text();
|
|
10631
10718
|
}
|
|
10632
10719
|
getGatewayCapabilities(projectId) {
|
|
@@ -10938,7 +11025,10 @@ var PlatformApiClient = class {
|
|
|
10938
11025
|
}
|
|
10939
11026
|
);
|
|
10940
11027
|
if (!response.ok) {
|
|
10941
|
-
throw await
|
|
11028
|
+
throw await this.failure(
|
|
11029
|
+
response,
|
|
11030
|
+
`/api/v1/projects/${projectId}/backend/deployments`
|
|
11031
|
+
);
|
|
10942
11032
|
}
|
|
10943
11033
|
return await response.json();
|
|
10944
11034
|
}
|
|
@@ -11343,7 +11433,7 @@ import {
|
|
|
11343
11433
|
// package.json
|
|
11344
11434
|
var package_default = {
|
|
11345
11435
|
name: "@lukoweb/apitogo",
|
|
11346
|
-
version: "0.2.
|
|
11436
|
+
version: "0.2.65",
|
|
11347
11437
|
type: "module",
|
|
11348
11438
|
sideEffects: [
|
|
11349
11439
|
"**/*.css",
|
|
@@ -23103,6 +23193,9 @@ function waitForJobStepLabel(kind, moduleType) {
|
|
|
23103
23193
|
return "Job";
|
|
23104
23194
|
}
|
|
23105
23195
|
}
|
|
23196
|
+
function deletionFinished(error) {
|
|
23197
|
+
return error instanceof ProjectGoneError || error instanceof PlatformApiError && error.status === 404;
|
|
23198
|
+
}
|
|
23106
23199
|
function projectDeletionNotFoundResult(last, jobId) {
|
|
23107
23200
|
return last ?? {
|
|
23108
23201
|
status: "Completed",
|
|
@@ -23164,7 +23257,7 @@ function pollProvisionJob(poll, options) {
|
|
|
23164
23257
|
stepLabel: options.stepLabel,
|
|
23165
23258
|
progress: options.progress?.child(options.stepLabel),
|
|
23166
23259
|
onPollError: options.kind === "projectDeletion" ? (error, last) => {
|
|
23167
|
-
if (error
|
|
23260
|
+
if (deletionFinished(error)) {
|
|
23168
23261
|
return {
|
|
23169
23262
|
status: "succeeded",
|
|
23170
23263
|
value: projectDeletionNotFoundResult(last, options.jobId),
|
|
@@ -26955,11 +27048,22 @@ function assertConfigChecksOut(output4) {
|
|
|
26955
27048
|
].join("\n")
|
|
26956
27049
|
);
|
|
26957
27050
|
}
|
|
26958
|
-
var
|
|
27051
|
+
var gatewayOn = (config2) => config2.gateway !== void 0 && config2.gateway.enabled !== false;
|
|
27052
|
+
var hostsAnything = (config2) => config2.portal && config2.portal.enabled !== false || gatewayOn(config2);
|
|
26959
27053
|
async function createProjectV2(options, deps) {
|
|
26960
27054
|
const { config: config2, resolved } = loadConfigV2(options.dir);
|
|
26961
27055
|
const linkedProjectId = resolved.project.id;
|
|
26962
|
-
const
|
|
27056
|
+
const externalDocsOnly = portalOnlyLinksExternalDocs(resolved);
|
|
27057
|
+
const modules = modulesForConfig(config2).filter(
|
|
27058
|
+
(module) => module !== "dev-portal" || !externalDocsOnly
|
|
27059
|
+
);
|
|
27060
|
+
if (externalDocsOnly && !gatewayOn(config2)) {
|
|
27061
|
+
throw new ProjectCreateError(
|
|
27062
|
+
"NOTHING_TO_HOST",
|
|
27063
|
+
"No enabled service needs a project: the docs are on an external site and nothing feeds the portal console.",
|
|
27064
|
+
"Enable identity or plans for a console next to your docs (`apitogo add identity`), or a gateway (`apitogo add gateway`)."
|
|
27065
|
+
);
|
|
27066
|
+
}
|
|
26963
27067
|
if (!hostsAnything(config2)) {
|
|
26964
27068
|
throw new ProjectCreateError(
|
|
26965
27069
|
"NOTHING_TO_HOST",
|
|
@@ -28472,6 +28576,16 @@ var IdentityDeployError = class extends ApitogoError {
|
|
|
28472
28576
|
this.name = "IdentityDeployError";
|
|
28473
28577
|
}
|
|
28474
28578
|
};
|
|
28579
|
+
var IDENTITY_NOT_ACTIVE = "IDENTITY_NOT_ACTIVE";
|
|
28580
|
+
async function identityLost(client, projectId) {
|
|
28581
|
+
if (!client.listProjectIdentityConnections) return false;
|
|
28582
|
+
try {
|
|
28583
|
+
await client.listProjectIdentityConnections(projectId);
|
|
28584
|
+
return false;
|
|
28585
|
+
} catch (error) {
|
|
28586
|
+
return error instanceof PlatformApiError && error.status === 409 && error.problem?.code === IDENTITY_NOT_ACTIVE;
|
|
28587
|
+
}
|
|
28588
|
+
}
|
|
28475
28589
|
var IDENTITY_STEPS = ["request", "activate"];
|
|
28476
28590
|
var IDENTITY_CONNECTIONS_STEP = "connections";
|
|
28477
28591
|
var SECRET_PREFIX = "secret:";
|
|
@@ -28671,10 +28785,13 @@ async function deployIdentity(resolved, projectId, deps, options = {}) {
|
|
|
28671
28785
|
return rejected2(error);
|
|
28672
28786
|
}
|
|
28673
28787
|
},
|
|
28674
|
-
// An identity that was removed, or that failed, is not a result worth keeping.
|
|
28788
|
+
// An identity that was removed, or that failed, is not a result worth keeping. Neither is one the platform still
|
|
28789
|
+
// lists as Active while the identity service lost it (API-132): asking again makes it again.
|
|
28675
28790
|
stillValid: async () => {
|
|
28676
28791
|
const found = await client.getProjectIdentity(projectId);
|
|
28677
|
-
|
|
28792
|
+
if (found === null || found.status === "Failed" || found.status === "Released" || found.status === "Releasing")
|
|
28793
|
+
return false;
|
|
28794
|
+
return found.status !== "Active" || !await identityLost(client, projectId);
|
|
28678
28795
|
}
|
|
28679
28796
|
};
|
|
28680
28797
|
const activateStep = {
|
|
@@ -29604,6 +29721,25 @@ async function statusV2(dir, only, options = {}) {
|
|
|
29604
29721
|
const ledger = await readLedger(resolved.state);
|
|
29605
29722
|
const manifest = await readManifest(resolved.state);
|
|
29606
29723
|
const services = [];
|
|
29724
|
+
const manifestSummary = {
|
|
29725
|
+
present: manifest !== void 0,
|
|
29726
|
+
schemaVersion: manifest?.schemaVersion,
|
|
29727
|
+
generatedAt: manifest?.generatedAt
|
|
29728
|
+
};
|
|
29729
|
+
const projectId = resolved.project.id ?? manifest?.project.id;
|
|
29730
|
+
if (options.projectRemote && projectId) {
|
|
29731
|
+
try {
|
|
29732
|
+
await options.projectRemote(projectId);
|
|
29733
|
+
} catch (error) {
|
|
29734
|
+
if (error instanceof ProjectGoneError)
|
|
29735
|
+
return {
|
|
29736
|
+
configPath: resolved.configPath,
|
|
29737
|
+
manifest: manifestSummary,
|
|
29738
|
+
project: { state: "gone", id: projectId, message: error.message },
|
|
29739
|
+
services: []
|
|
29740
|
+
};
|
|
29741
|
+
}
|
|
29742
|
+
}
|
|
29607
29743
|
for (const service of chooseServices(only)) {
|
|
29608
29744
|
const section = sectionOf(resolved, service);
|
|
29609
29745
|
if (!section) {
|
|
@@ -29667,11 +29803,7 @@ async function statusV2(dir, only, options = {}) {
|
|
|
29667
29803
|
}
|
|
29668
29804
|
return {
|
|
29669
29805
|
configPath: resolved.configPath,
|
|
29670
|
-
manifest:
|
|
29671
|
-
present: manifest !== void 0,
|
|
29672
|
-
schemaVersion: manifest?.schemaVersion,
|
|
29673
|
-
generatedAt: manifest?.generatedAt
|
|
29674
|
-
},
|
|
29806
|
+
manifest: manifestSummary,
|
|
29675
29807
|
services
|
|
29676
29808
|
};
|
|
29677
29809
|
}
|
|
@@ -29685,6 +29817,8 @@ function formatPlan(output4, options = {}) {
|
|
|
29685
29817
|
]);
|
|
29686
29818
|
}
|
|
29687
29819
|
function formatStatus(output4) {
|
|
29820
|
+
if (output4.project?.state === "gone")
|
|
29821
|
+
return [`project: gone`, ` ${output4.project.message}`];
|
|
29688
29822
|
return [
|
|
29689
29823
|
output4.manifest.present ? `services.json: version ${output4.manifest.schemaVersion}${output4.manifest.generatedAt ? `, written ${output4.manifest.generatedAt}` : ""}` : "services.json: none yet",
|
|
29690
29824
|
...output4.services.flatMap((svc) => [
|
|
@@ -29837,6 +29971,15 @@ async function deployV2(options, deps) {
|
|
|
29837
29971
|
results.push(await deployPlansService(options, deps, projectId, named));
|
|
29838
29972
|
continue;
|
|
29839
29973
|
}
|
|
29974
|
+
if (portalOnlyLinksExternalDocs(resolved)) {
|
|
29975
|
+
results.push({
|
|
29976
|
+
service: "portal",
|
|
29977
|
+
status: "skipped",
|
|
29978
|
+
reason: "docs are on an external site and no service feeds the console, so there is nothing to host",
|
|
29979
|
+
steps: []
|
|
29980
|
+
});
|
|
29981
|
+
continue;
|
|
29982
|
+
}
|
|
29840
29983
|
results.push(await deployPortal(options, deps, projectId));
|
|
29841
29984
|
}
|
|
29842
29985
|
const gatewayAt = results.findIndex(
|
|
@@ -30609,6 +30752,14 @@ var plan = (argv) => run2(
|
|
|
30609
30752
|
}),
|
|
30610
30753
|
(output4) => formatPlan(output4, { verbose: argv.verbose === true })
|
|
30611
30754
|
);
|
|
30755
|
+
function projectRemote() {
|
|
30756
|
+
try {
|
|
30757
|
+
const client = new PlatformApiClient(loadConfig());
|
|
30758
|
+
return (projectId) => client.getProject(projectId);
|
|
30759
|
+
} catch {
|
|
30760
|
+
return void 0;
|
|
30761
|
+
}
|
|
30762
|
+
}
|
|
30612
30763
|
function gatewayRemote() {
|
|
30613
30764
|
try {
|
|
30614
30765
|
const client = new PlatformApiClient(loadConfig());
|
|
@@ -30666,6 +30817,7 @@ var statusOfProject = (argv) => run2(
|
|
|
30666
30817
|
"status",
|
|
30667
30818
|
argv,
|
|
30668
30819
|
(dir, only) => statusV2(dir, only, {
|
|
30820
|
+
projectRemote: projectRemote(),
|
|
30669
30821
|
hostingRemote: hostingRemote(),
|
|
30670
30822
|
gatewayRemote: gatewayRemote(),
|
|
30671
30823
|
identityRemote: identityRemote(),
|
|
@@ -30787,7 +30939,7 @@ async function switchProject(options, deps) {
|
|
|
30787
30939
|
try {
|
|
30788
30940
|
project = await deps.getProject(projectId);
|
|
30789
30941
|
} catch (error) {
|
|
30790
|
-
if (error instanceof PlatformApiError && (error.status === 404 || error.status === 403))
|
|
30942
|
+
if (error instanceof ProjectGoneError || error instanceof PlatformApiError && (error.status === 404 || error.status === 403))
|
|
30791
30943
|
throw new ProjectUseError(
|
|
30792
30944
|
"PROJECT_NOT_FOUND",
|
|
30793
30945
|
`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;
|
|
@@ -3,6 +3,7 @@ import { type ApitogoConfig } from "../config.js";
|
|
|
3
3
|
import { recentJobMessages, type ProgressReporter } from "../helpers/progress.js";
|
|
4
4
|
export type WaitForJobKind = "module" | "deployment" | "azureInfrastructure" | "projectDeletion" | "devPortalDeploy" | "devPortalBackendUpdate";
|
|
5
5
|
export declare function waitForJobStepLabel(kind: WaitForJobKind, moduleType?: string): string;
|
|
6
|
+
export declare function deletionFinished(error: unknown): boolean;
|
|
6
7
|
export declare function handleActivateModule(client: PlatformApiClient, config: ApitogoConfig, args: {
|
|
7
8
|
projectId?: string;
|
|
8
9
|
moduleType: 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
|
|
162
|
-
terminal. If
|
|
163
|
-
|
|
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
|
@@ -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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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) {
|
package/src/config/v2/index.ts
CHANGED
|
@@ -54,7 +54,11 @@ export {
|
|
|
54
54
|
ConfigV2Schema,
|
|
55
55
|
type Layout,
|
|
56
56
|
} from "./schema.js";
|
|
57
|
-
export {
|
|
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;
|
package/src/config/v2/rules.ts
CHANGED
|
@@ -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
|
"/",
|
package/src/config/v2/wrapper.ts
CHANGED
|
@@ -400,10 +400,13 @@ export function buildSiteConfig(
|
|
|
400
400
|
]
|
|
401
401
|
: []),
|
|
402
402
|
];
|
|
403
|
+
const logo = portal?.logo ? siteLogo(portal) : undefined;
|
|
403
404
|
return {
|
|
405
|
+
// The browser tab's icon is the project's logo; without one, the portal's own icon (API-134).
|
|
406
|
+
...(logo ? { metadata: { favicon: logo.src.light } } : {}),
|
|
404
407
|
site: {
|
|
405
408
|
title: portal?.name ?? resolved.project.name,
|
|
406
|
-
...(
|
|
409
|
+
...(logo ? { logo } : {}),
|
|
407
410
|
...(plan.preview
|
|
408
411
|
? { banner: { message: PREVIEW_BANNER_MESSAGE, color: "caution" } }
|
|
409
412
|
: {}),
|
|
@@ -518,9 +521,10 @@ export const PREVIEW_BANNER_MESSAGE =
|
|
|
518
521
|
* The header logo as the site takes it: a file of the portal's `public/` folder becomes its address on the site, an https
|
|
519
522
|
* address stays as it is (API-129). `check` refuses a file outside `public/`.
|
|
520
523
|
*/
|
|
521
|
-
function siteLogo(
|
|
522
|
-
|
|
523
|
-
|
|
524
|
+
function siteLogo(portal: NonNullable<ResolvedConfigV2["portal"]>): {
|
|
525
|
+
src: { light: string; dark: string };
|
|
526
|
+
alt: string;
|
|
527
|
+
} {
|
|
524
528
|
const logo = portal.logo as NonNullable<typeof portal.logo>;
|
|
525
529
|
const publicDir = path.join(portal.dir, "public");
|
|
526
530
|
const src = (file: string) =>
|
|
@@ -1,46 +1,49 @@
|
|
|
1
|
-
import { Helmet } from "@zudoku/react-helmet-async";
|
|
2
|
-
import type { PropsWithChildren } from "react";
|
|
3
|
-
import { useLocation } from "react-router";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
)}
|
|
28
|
-
{meta?.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
{meta?.
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
{meta?.
|
|
38
|
-
<meta
|
|
39
|
-
)
|
|
40
|
-
{meta?.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
1
|
+
import { Helmet } from "@zudoku/react-helmet-async";
|
|
2
|
+
import type { PropsWithChildren } from "react";
|
|
3
|
+
import { useLocation } from "react-router";
|
|
4
|
+
// A site without its own icon would leave the browser asking for /favicon.ico, a 404 and a console error on every page
|
|
5
|
+
// (API-134). The build publishes this file with a hashed name, so no project has to carry it.
|
|
6
|
+
import defaultFavicon from "../assets/powered-by-icon.png";
|
|
7
|
+
import { joinUrl } from "../util/joinUrl.js";
|
|
8
|
+
import { useZudoku } from "./context/ZudokuContext.js";
|
|
9
|
+
|
|
10
|
+
export const Meta = ({ children }: PropsWithChildren) => {
|
|
11
|
+
const { options } = useZudoku();
|
|
12
|
+
const { metadata: meta } = options;
|
|
13
|
+
const location = useLocation();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<>
|
|
17
|
+
<Helmet titleTemplate={meta?.title} defaultTitle={meta?.defaultTitle}>
|
|
18
|
+
{options.canonicalUrlOrigin && (
|
|
19
|
+
<link
|
|
20
|
+
rel="canonical"
|
|
21
|
+
href={joinUrl(
|
|
22
|
+
options.canonicalUrlOrigin,
|
|
23
|
+
options.basePath,
|
|
24
|
+
location.pathname,
|
|
25
|
+
)}
|
|
26
|
+
/>
|
|
27
|
+
)}
|
|
28
|
+
{meta?.description && (
|
|
29
|
+
<meta name="description" content={meta.description} />
|
|
30
|
+
)}
|
|
31
|
+
<link rel="icon" href={meta?.favicon ?? defaultFavicon} />
|
|
32
|
+
{meta?.generator && <meta name="generator" content={meta.generator} />}
|
|
33
|
+
{meta?.applicationName && (
|
|
34
|
+
<meta name="application-name" content={meta.applicationName} />
|
|
35
|
+
)}
|
|
36
|
+
{meta?.referrer && <meta name="referrer" content={meta.referrer} />}
|
|
37
|
+
{meta?.keywords && meta.keywords.length > 0 && (
|
|
38
|
+
<meta name="keywords" content={meta.keywords.join(", ")} />
|
|
39
|
+
)}
|
|
40
|
+
{meta?.authors?.map((author) => (
|
|
41
|
+
<meta key={author} name="author" content={author} />
|
|
42
|
+
))}
|
|
43
|
+
{meta?.creator && <meta name="creator" content={meta.creator} />}
|
|
44
|
+
{meta?.publisher && <meta name="publisher" content={meta.publisher} />}
|
|
45
|
+
</Helmet>
|
|
46
|
+
{children}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
};
|