@lukoweb/apitogo 0.2.57 → 0.2.59

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/docs/mcp/tools.md CHANGED
@@ -1,101 +1,57 @@
1
1
  ---
2
2
  title: MCP tools
3
- description: APItoGo MCP tool catalog by category and hand-edit exceptions.
3
+ description: Every tool of the APItoGo MCP server and the CLI command it runs.
4
4
  sidebar_icon: wrench
5
5
  ---
6
6
 
7
- Tools are exposed by the APItoGo MCP server. Prefer **`apply_product_intent`** for create / shape /
8
- go-live. Use granular tools for advanced edits, debugging, or CI.
9
-
10
- ## Lifecycle (prefer these)
11
-
12
- | Tool | Role |
13
- | ------------------------------------ | ---------------------------------------------- |
14
- | `apply_product_intent` | Shape or goLive from a product intent |
15
- | `create_dev_portal` | Scaffold site (+ optional project bootstrap) |
16
- | `check_dev_portal_publish_readiness` | Pre-publish checklist |
17
- | `publish_dev_portal` | Publish portal + plans (+ optional backend) |
18
- | `verify_dev_portal_live` | Post-publish verification |
19
- | `bootstrap_dev_portal` | CI one-shot provision + deploy |
20
- | `bootstrap_project` | Create platform project only (plan unselected) |
21
- | `teardown_project` | Tear down a project |
22
-
23
- ## Site shaping
24
-
25
- | Tool | Role |
26
- | ---------------------------------- | ------------------------------------------------------------------------- |
27
- | `update_dev_portal_manifest` | Plans, branding, backend, projectId |
28
- | `set_dev_portal_openapi` | OpenAPI / API reference |
29
- | `set_dev_portal_theme` | Colors and fonts |
30
- | `add_dev_portal_page` | Add a TSX docs page (site components; `template`: docs\|guide\|marketing) |
31
- | `update_dev_portal_page` | Edit a docs page |
32
- | `scaffold_dev_portal_landing_page` | Scaffold missing landing file |
33
- | `get_dev_portal_landing_guide` | Landing component catalog |
34
- | `preview_dev_portal_site` | Start / restart local preview |
35
- | `build_dev_portal_site` | Production build |
36
-
37
- Hosted page CRUD also exists (`list/create/delete/rename/reorder_dev_portal_pages`) for the platform
38
- API surface.
39
-
40
- ## Projects and platform billing
41
-
42
- | Tool | Role |
43
- | ------------------------------------------- | ------------------------------------- |
44
- | `whoami`, `list_projects`, `get_project`, … | Tenant / project CRUD |
45
- | `list_platform_plans` | Free / Starter / Professional + quota |
46
- | `ensure_platform_plan` | Select or checkout platform plan |
47
- | `wait_for_platform_plan` | Poll until active |
48
- | `get_subscription` | Current subscription |
49
- | `select_free_plan` | Free hosting |
50
- | `create_checkout_session` | Stripe checkout for paid hosting |
51
-
52
- ## Backend and OpenAPI
53
-
54
- | Tool | Role |
55
- | ----------------------- | ------------------------ |
56
- | `validate_openapi` | Validate OpenAPI |
57
- | `update_openapi` | Update stored OpenAPI |
58
- | `package_backend` | Package backend artifact |
59
- | `deploy_backend` | Deploy API hub |
60
- | `get_backend_summary` | Backend status |
61
- | `get_deployment_status` | Deployment job status |
62
- | `get_gateway_readiness` | Gateway readiness |
63
-
64
- ## Gateway
65
-
66
- | Tool | Role |
67
- | ------------------------------------------------------------------ | ------------------------ |
68
- | `list/get/create/update/patch_gateway_plan` | Plan CRUD |
69
- | `add_actions_to_gateway_plan` / `remove_actions_from_gateway_plan` | Attach ops |
70
- | `set_action_public` | Public routes |
71
- | `upsert_entitlement` | Entitlements |
72
- | `configure_gateway_baseline` | Baseline policies |
73
- | `get/update_gateway_settings` | Settings |
74
- | `get/update_gateway_auth` | Gateway auth |
75
- | `sync_gateway_plans_from_manifest` | Post-publish action sync |
76
- | `reorder_gateway_plans_from_manifest` | Plan order |
77
-
78
- ## Infrastructure and deploy
79
-
80
- | Tool | Role |
81
- | ------------------------------------------------------ | ---------------- |
82
- | `activate_module` | Activate modules |
83
- | `get_infrastructure_status` / Azure variants | Infra status |
84
- | `list_provisioning_jobs`, `wait_for_job` | Jobs |
85
- | `get/set/delete_custom_domain` | Custom domain |
86
- | `get_dev_portal_deploy_config`, `deploy_dev_portal`, … | SWA deploy |
87
-
88
- ## Hand-edit exceptions
89
-
90
- | Change | Where |
91
- | ----------------- | -------------------------------------------------------- |
92
- | Home page content | `landing/LandingPage.tsx` |
93
- | Site footer | `apitogo.config.json` → `modules.landing.content.footer` |
94
-
95
- There is **no** `update_dev_portal_landing` tool for arbitrary home content.
96
-
97
- ## Related
98
-
99
- - [Overview](./overview.md)
100
- - [Shape](./shape.md)
101
- - [Go live](./go-live.md)
7
+ Each tool runs one `apitogo` command with `--json` in the project folder and answers with its JSON.
8
+ Every tool takes `directory`: the project folder, absolute or relative to where the server runs
9
+ (default: that folder).
10
+
11
+ ## Make and shape a project
12
+
13
+ | Tool | Runs | What it does |
14
+ | ---------------- | -------------------------- | ----------------------------------------------------------------------- |
15
+ | `apitogo_create` | `apitogo create` | A new project folder; `template` `full` or `blank`, optional `services` |
16
+ | `apitogo_init` | `apitogo init` | Prepares an existing folder: `apitogo.config.json` and `apitogo/` only |
17
+ | `apitogo_add` | `apitogo add <service>` | Adds `portal`, `gateway`, `identity` or `plans`; settings in `options` |
18
+ | `apitogo_remove` | `apitogo remove <service>` | Takes a service out of the config; `deleteFiles` also deletes its files |
19
+ | `apitogo_config` | `apitogo config` | Portal name, OpenAPI, docs pages, plans, or a `patch` of the config |
20
+
21
+ `options` of `apitogo_add` and `apitogo_config` are the command's flags in camelCase, for example
22
+ `{"planSku":"pro","planPrice":"19"}` for `--plan-sku pro --plan-price 19`. A flag the command does
23
+ not have is refused before anything runs. `true` and `false` are `--flag` and `--no-flag`; a list
24
+ repeats the flag.
25
+
26
+ ## Check, preview, deploy
27
+
28
+ | Tool | Runs | What it does |
29
+ | ---------------- | ---------------- | ------------------------------------------------------------ |
30
+ | `apitogo_check` | `apitogo check` | Checks the config and its files; `offline`, `validateBuild` |
31
+ | `apitogo_plan` | `apitogo plan` | What a deploy would do for each service; changes nothing |
32
+ | `apitogo_deploy` | `apitogo deploy` | Deploys what changed; `only`, `force`, `confirmIrreversible` |
33
+ | `apitogo_status` | `apitogo status` | Each service's state and addresses; `skipLive` is faster |
34
+
35
+ ## Platform project
36
+
37
+ | Tool | Runs | What it does |
38
+ | ------------------------ | -------------------------- | ---------------------------------------------------------------------------------------------- |
39
+ | `apitogo_project_create` | `apitogo project create` | Creates the project; `plan` free, starter or professional |
40
+ | `apitogo_project_use` | `apitogo project use <id>` | Points the folder at another existing project |
41
+ | `apitogo_secrets_list` | `apitogo secrets list` | The names of the project's secrets, never their values |
42
+ | `apitogo_secrets_remove` | `apitogo secrets rm` | Removes a secret |
43
+ | `apitogo_teardown` | `apitogo teardown` | Deletes the project, or with `only` its gateway, identity or plans; needs `confirmProjectName` |
44
+
45
+ ## Not a tool, on purpose
46
+
47
+ | Command | Why |
48
+ | --------------------- | --------------------------------------------------------- |
49
+ | `apitogo login` | Opens a browser to sign you in; run it once in a terminal |
50
+ | `apitogo secrets set` | A secret's value must not pass through the agent |
51
+ | `apitogo run` | The local preview keeps running; start it yourself |
52
+
53
+ ## Safety hints
54
+
55
+ Tools carry MCP hints so hosts can ask before running them: `apitogo_check`, `apitogo_plan`,
56
+ `apitogo_status` and `apitogo_secrets_list` are read-only; `apitogo_remove`,
57
+ `apitogo_secrets_remove` and `apitogo_teardown` are marked destructive.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.57",
3
+ "version": "0.2.59",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",
@@ -39,11 +39,13 @@ export type GatewayConfigBody = {
39
39
  export type DerivedCorsSite = { origin: string; source: "portal" | "docs" };
40
40
 
41
41
  /**
42
- * The browser origins of this project's own sites: the portal (from `services.json`) and an external docs site
43
- * (from the config). Only the origin is kept, never a path. Sorted, so the same sites always give the same body.
42
+ * The browser origins of this project's own sites: the portal and its custom domain (from `services.json`) and an
43
+ * external docs site (from the config). Only the origin is kept, never a path. Sorted, so the same sites always give the same body.
44
44
  */
45
45
  export function derivedCorsSites(sites: {
46
46
  portalUrl?: string;
47
+ /** The portal's custom domain, once it is ready (API-130). */
48
+ portalCustomDomain?: string;
47
49
  docsUrl?: string;
48
50
  }): DerivedCorsSite[] {
49
51
  const originOf = (url?: string) => {
@@ -58,6 +60,7 @@ export function derivedCorsSites(sites: {
58
60
  const found = [
59
61
  { origin: originOf(sites.docsUrl), source: "docs" as const },
60
62
  { origin: originOf(sites.portalUrl), source: "portal" as const },
63
+ { origin: originOf(sites.portalCustomDomain), source: "portal" as const },
61
64
  ].flatMap((site) => (site.origin ? [{ ...site, origin: site.origin }] : []));
62
65
  const seen = new Set<string>();
63
66
  return found
@@ -70,6 +70,7 @@ export {
70
70
  withGatewayEntry,
71
71
  withIdentityEntry,
72
72
  withPlansEntry,
73
+ withPortalCustomDomain,
73
74
  withPortalEntry,
74
75
  writeManifest,
75
76
  } from "./manifest.js";
@@ -29,6 +29,8 @@ const ServiceManifestSchema = z.looseObject({
29
29
  portal: z
30
30
  .looseObject({
31
31
  url: z.string().optional(),
32
+ /** The portal's custom domain once it is ready, such as `https://docs.acme.com` (API-130). */
33
+ customDomain: z.string().optional(),
32
34
  docs: z
33
35
  .looseObject({ provider: z.string(), url: z.string().optional() })
34
36
  .optional(),
@@ -291,6 +293,29 @@ export function withPortalEntry(
291
293
  };
292
294
  }
293
295
 
296
+ /**
297
+ * Records the portal's custom domain (an https origin), or removes it when undefined (API-130). The gateway lets the
298
+ * browser call it from there, and `plan` reads it from here, so it agrees with deploy without asking the platform.
299
+ * Returns undefined when nothing changes, so an unchanged deploy leaves the file (and the portal's build hash) alone.
300
+ */
301
+ export function withPortalCustomDomain(
302
+ existing: ServiceManifest | undefined,
303
+ customDomain: string | undefined,
304
+ ): ServiceManifest | undefined {
305
+ if (!existing) return undefined;
306
+ const portal = existing.services.portal;
307
+ if (portal?.customDomain === customDomain) return undefined;
308
+ if (!portal && customDomain === undefined) return undefined;
309
+ const { customDomain: _previous, ...rest } = portal ?? {};
310
+ return {
311
+ ...existing,
312
+ services: {
313
+ ...existing.services,
314
+ portal: customDomain === undefined ? rest : { ...rest, customDomain },
315
+ },
316
+ };
317
+ }
318
+
294
319
  /**
295
320
  * Adds the gateway entry a gateway deploy learned. Only public values: the base URL and what the platform says the
296
321
  * gateway can do. Entries of other services are kept as they are.
@@ -169,7 +169,11 @@ export function resolveConfigV2(
169
169
  }
170
170
  : {
171
171
  light: logoSource(configDir, portal.logo.light),
172
- dark: logoSource(configDir, portal.logo.dark),
172
+ // Without a dark image, the light one is used in both color schemes.
173
+ dark: logoSource(
174
+ configDir,
175
+ portal.logo.dark ?? portal.logo.light,
176
+ ),
173
177
  ...(portal.logo.alt ? { alt: portal.logo.alt } : {}),
174
178
  },
175
179
  }
@@ -49,12 +49,25 @@ export const PortalSchema = z.strictObject({
49
49
  landing: path.optional(),
50
50
  /** The footer: a React component file shown under every page except the console's (API-129). */
51
51
  footer: path.optional(),
52
- /** The logo in the portal header: one image, or one per color scheme (API-129). */
52
+ /**
53
+ * The logo in the portal header: one image, or one per color scheme (API-129). `dark` is optional; without it the
54
+ * light image is used in both.
55
+ */
53
56
  logo: z
54
- .union([
55
- path,
56
- z.strictObject({ light: path, dark: path, alt: z.string().optional() }),
57
- ])
57
+ .union(
58
+ [
59
+ path,
60
+ z.strictObject({
61
+ light: path,
62
+ dark: path.optional(),
63
+ alt: z.string().optional(),
64
+ }),
65
+ ],
66
+ {
67
+ error:
68
+ 'must be an image path or https address, or { "light": "...", "dark": "...", "alt": "..." } (dark and alt are optional)',
69
+ },
70
+ )
58
71
  .optional(),
59
72
  requireAuth: z
60
73
  .union([z.boolean(), z.strictObject({ exclude: pathList })], {
@@ -1,76 +1,89 @@
1
- import type { RouteObject } from "react-router";
2
- import { joinUrl } from "../../lib/util/joinUrl.js";
3
-
4
- const resolveRoutePath = (path: string): string | undefined => {
5
- const segments = path.split("/");
6
- if (segments.some((s) => s.startsWith(":") && !s.endsWith("?"))) {
7
- return undefined;
8
- }
9
- return segments.filter((s) => !s.startsWith(":")).join("/") || undefined;
10
- };
11
-
12
- const isSkipped = (path: string) => path.includes("*") || /^\d+$/.test(path);
13
-
14
- export type RouteRewrite = { source: string; destination: string };
15
-
16
- type ResolvedRoute = {
17
- path: string;
18
- hasStrippedParams: boolean;
19
- children: ResolvedRoute[];
20
- };
21
-
22
- const resolveRoutes = (
23
- routes: RouteObject[],
24
- parentPath = "",
25
- ): ResolvedRoute[] =>
26
- routes.flatMap((route) => {
27
- if (route.path && isSkipped(route.path)) return [];
28
-
29
- const routePath = route.path ? resolveRoutePath(route.path) : undefined;
30
- if (route.path && !routePath) return [];
31
-
32
- const fullPath = routePath
33
- ? routePath.startsWith("/")
34
- ? routePath
35
- : joinUrl(parentPath, routePath)
36
- : parentPath;
37
-
38
- const hasStrippedParams =
39
- route.path?.split("/").some((s) => s.startsWith(":")) ?? false;
40
-
41
- return [
42
- {
43
- path: fullPath,
44
- hasStrippedParams,
45
- children: resolveRoutes(route.children ?? [], fullPath),
46
- },
47
- ];
48
- });
49
-
50
- const collectPaths = (
51
- resolved: ResolvedRoute[],
52
- parentPath: string,
53
- ): string[] =>
54
- resolved.flatMap((r) => [
55
- ...(r.path !== parentPath ? [r.path] : []),
56
- ...collectPaths(r.children, r.path),
57
- ]);
58
-
59
- const collectRewrites = (resolved: ResolvedRoute[]): RouteRewrite[] =>
60
- resolved.flatMap((r) => [
61
- ...(r.hasStrippedParams && r.path
62
- ? [
63
- {
64
- source: `${r.path}/(.+)`,
65
- destination: r.path === "/" ? "/index.html" : `${r.path}.html`,
66
- },
67
- ]
68
- : []),
69
- ...collectRewrites(r.children),
70
- ]);
71
-
72
- export const routesToPaths = (routes: RouteObject[]): string[] =>
73
- collectPaths(resolveRoutes(routes), "");
74
-
75
- export const routesToRewrites = (routes: RouteObject[]): RouteRewrite[] =>
76
- collectRewrites(resolveRoutes(routes));
1
+ import type { RouteObject } from "react-router";
2
+ import { joinUrl } from "../../lib/util/joinUrl.js";
3
+
4
+ const resolveRoutePath = (path: string): string | undefined => {
5
+ const segments = path.split("/");
6
+ if (segments.some((s) => s.startsWith(":") && !s.endsWith("?"))) {
7
+ return undefined;
8
+ }
9
+ return segments.filter((s) => !s.startsWith(":")).join("/") || undefined;
10
+ };
11
+
12
+ const isSkipped = (path: string) => path.includes("*") || /^\d+$/.test(path);
13
+
14
+ export type RouteRewrite = { source: string; destination: string };
15
+
16
+ type ResolvedRoute = {
17
+ path: string;
18
+ hasStrippedParams: boolean;
19
+ children: ResolvedRoute[];
20
+ };
21
+
22
+ const resolveRoutes = (
23
+ routes: RouteObject[],
24
+ parentPath = "",
25
+ ): ResolvedRoute[] =>
26
+ routes.flatMap((route) => {
27
+ if (route.path && isSkipped(route.path)) return [];
28
+
29
+ const routePath = route.path ? resolveRoutePath(route.path) : undefined;
30
+ if (route.path && !routePath) return [];
31
+
32
+ const fullPath = routePath
33
+ ? routePath.startsWith("/")
34
+ ? routePath
35
+ : joinUrl(parentPath, routePath)
36
+ : parentPath;
37
+
38
+ const hasStrippedParams =
39
+ route.path?.split("/").some((s) => s.startsWith(":")) ?? false;
40
+
41
+ return [
42
+ {
43
+ path: fullPath,
44
+ hasStrippedParams,
45
+ children: resolveRoutes(route.children ?? [], fullPath),
46
+ },
47
+ ];
48
+ });
49
+
50
+ const collectPaths = (
51
+ resolved: ResolvedRoute[],
52
+ parentPath: string,
53
+ ): string[] =>
54
+ resolved.flatMap((r) => [
55
+ ...(r.path !== parentPath ? [r.path] : []),
56
+ ...collectPaths(r.children, r.path),
57
+ ]);
58
+
59
+ /**
60
+ * The file a prerendered page is written to. `/` is `index.html`; any other page is `<path>.html`, except a page whose
61
+ * last segment is `index`: `/index.html` is the home page's file, and a docs page at `/index` (kept off `/` when the
62
+ * portal has a home page) overwrote it, so the home page was served as the docs page until the browser took over
63
+ * (API-130). Such a page gets a folder of its own instead.
64
+ */
65
+ export const prerenderFileFor = (urlPath: string): string =>
66
+ urlPath === "/"
67
+ ? "/index.html"
68
+ : urlPath.endsWith("/index")
69
+ ? `${urlPath}/index.html`
70
+ : `${urlPath}.html`;
71
+
72
+ const collectRewrites = (resolved: ResolvedRoute[]): RouteRewrite[] =>
73
+ resolved.flatMap((r) => [
74
+ ...(r.hasStrippedParams && r.path
75
+ ? [
76
+ {
77
+ source: `${r.path}/(.+)`,
78
+ destination: prerenderFileFor(r.path),
79
+ },
80
+ ]
81
+ : []),
82
+ ...collectRewrites(r.children),
83
+ ]);
84
+
85
+ export const routesToPaths = (routes: RouteObject[]): string[] =>
86
+ collectPaths(resolveRoutes(routes), "");
87
+
88
+ export const routesToRewrites = (routes: RouteObject[]): RouteRewrite[] =>
89
+ collectRewrites(resolveRoutes(routes));
@@ -7,6 +7,7 @@ import type { ZudokuConfig } from "../../config/validators/ZudokuConfig.js";
7
7
  import { runPluginTransformConfig } from "../../lib/core/transform-config.js";
8
8
  import { joinUrl } from "../../lib/util/joinUrl.js";
9
9
  import type { WorkerResult } from "./prerender.js";
10
+ import { prerenderFileFor } from "./utils.js";
10
11
 
11
12
  type EntryServer = typeof import("../../app/entry.server.js");
12
13
 
@@ -47,7 +48,7 @@ const routes = server.getRoutesByConfig(config);
47
48
  const { basePath } = config;
48
49
 
49
50
  const renderPage = async ({ urlPath }: WorkerData): Promise<WorkerResult> => {
50
- const filename = urlPath === "/" ? "/index.html" : `${urlPath}.html`;
51
+ const filename = prerenderFileFor(urlPath);
51
52
  const pathname = joinUrl(basePath, urlPath);
52
53
  const url = joinUrl("http://localhost", pathname);
53
54
  const outputPath = path.join(distDir, filename);