@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 +211 -65
- package/dist/declarations/cli/platform/client/platform-api.d.ts +3 -0
- package/dist/declarations/cli/platform/errors.d.ts +5 -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/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
|
}
|
|
@@ -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
|
|
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
|
|
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
|
|
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)
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
"/",
|