@lukoweb/apitogo 0.1.138 → 0.1.140

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/cli.js CHANGED
@@ -5809,7 +5809,7 @@ import {
5809
5809
  // package.json
5810
5810
  var package_default = {
5811
5811
  name: "@lukoweb/apitogo",
5812
- version: "0.1.138",
5812
+ version: "0.1.140",
5813
5813
  type: "module",
5814
5814
  sideEffects: [
5815
5815
  "**/*.css",
@@ -16326,7 +16326,7 @@ async function ensureTsconfigJson(dir) {
16326
16326
  return true;
16327
16327
  }
16328
16328
  }
16329
- var LANDING_PAGE_SOURCE = 'import {\n ApiShowcaseSection,\n FeaturesSection,\n HeroSection,\n LandingShell,\n PlansPreviewSection,\n PricingCtaSection,\n StepsSection,\n} from "@lukoweb/apitogo-module-landing";\n\nexport default function LandingPage() {\n return (\n <LandingShell\n title="Test against a real API before you build one"\n description="Sample API gives you realistic endpoints, authentication, rate limits, and webhooks \u2014 a fully working backend for prototypes, demos, and QA."\n >\n <HeroSection\n hero={{\n badge: "SANDBOX API",\n title: "Test against a real API before you build one",\n description:\n "Sample API gives you realistic endpoints, authentication, rate limits, and webhooks \u2014 a fully working backend for prototypes, demos, and QA. No setup, no infrastructure.",\n }}\n primaryAction={{ label: "Start for free", href: "/signup" }}\n secondaryAction={{ label: "Read the docs", href: "/introduction" }}\n codeExamples={[\n {\n filename: "Create a key",\n language: "bash",\n code: `$ curl -X POST https://api.sampleapi.dev/v1/keys\n# sk_live_9f2c7a1e...`,\n },\n {\n filename: "Call an endpoint",\n language: "bash",\n code: `$ curl https://api.sampleapi.dev/v1/users/42 \\\\\n -H "Authorization: Bearer sk_live_9f2c..."\n# { "id": 42, "name": "Priya Shah", "plan": "pro" }`,\n },\n ]}\n />\n <FeaturesSection\n section={{\n label: "Everything a real API has",\n title: "Built for testing, not just browsing",\n description:\n "Realistic data, live auth, rate limits, and webhooks \u2014 so you can ship with confidence.",\n }}\n features={[\n {\n title: "Realistic mock data",\n description:\n "Users, orders, products, and posts with relationships and pagination, not lorem ipsum.",\n icon: "boxes",\n },\n {\n title: "Real auth flow",\n description:\n "Every request checks a live API key, so you can test error handling and expiry.",\n icon: "shield-check",\n },\n {\n title: "Rate limiting",\n description:\n "Per-plan request limits and 429 responses, so you can build and test backoff logic.",\n icon: "gauge",\n },\n {\n title: "Webhooks",\n description:\n "Trigger test events and inspect delivered payloads and retries.",\n icon: "webhook",\n },\n {\n title: "OpenAPI & docs",\n description:\n "A full OpenAPI spec and reference docs for every endpoint.",\n icon: "book-open",\n },\n {\n title: "Usage analytics",\n description:\n "Track requests, latency, and errors per key from your dashboard.",\n icon: "activity",\n },\n ]}\n />\n <StepsSection\n steps={[\n {\n step: "01",\n title: "Create an account",\n description:\n "Sign up and land straight in your dashboard \u2014 no approval queue.",\n },\n {\n step: "02",\n title: "Generate a key",\n description:\n "Every key is scoped and rate-limited to your plan from the moment it\'s created.",\n },\n {\n step: "03",\n title: "Call any endpoint",\n description:\n "Users, orders, products, posts, and webhooks \u2014 all documented, all live.",\n },\n ]}\n />\n {/* Static sample list. To show your real API operations from the dev portal\n backend instead, see https://docs.apitogo.com/docs/customization/landing-page#live-apis */}\n <ApiShowcaseSection\n apiShowcase={{\n title: "Sample endpoints",\n subtitle:\n "A slice of what\'s available \u2014 see the full reference in the docs.",\n browseAllHref: "/api",\n browseAllLabel: "Browse all APIs",\n layout: "list",\n items: [\n {\n name: "List paginated users",\n path: "/v1/users",\n method: "GET",\n summary: "List paginated users",\n },\n {\n name: "Create a mock order",\n path: "/v1/orders",\n method: "POST",\n summary: "Create a mock order",\n },\n {\n name: "Fetch one product",\n path: "/v1/products/{id}",\n method: "GET",\n summary: "Fetch one product",\n },\n {\n name: "List posts with comments",\n path: "/v1/posts",\n method: "GET",\n summary: "List posts with comments",\n },\n {\n name: "Fire a test webhook event",\n path: "/v1/webhooks/test",\n method: "POST",\n summary: "Fire a test webhook event",\n },\n {\n name: "Delete a mock order",\n path: "/v1/orders/{id}",\n method: "DELETE",\n summary: "Delete a mock order",\n },\n ],\n }}\n />\n <PlansPreviewSection\n plans={[\n {\n name: "Free",\n price: "$0",\n unit: "per month",\n blurb: "1,000 requests/mo.",\n },\n {\n name: "Developer",\n price: "$19",\n unit: "per month",\n blurb: "50,000 requests/mo, webhooks included.",\n featured: true,\n },\n {\n name: "Team",\n price: "$79",\n unit: "per month",\n blurb: "500,000 requests/mo, 5 team seats.",\n },\n {\n name: "Enterprise",\n price: "$299",\n unit: "per month",\n blurb: "Unlimited requests, SSO, SLA.",\n },\n ]}\n />\n <PricingCtaSection\n variant="banner"\n pricingCta={{\n title: "Start testing in minutes",\n description:\n "Free tier includes 1,000 requests a month \u2014 no card required.",\n primaryAction: { label: "Start for free", href: "/signup" },\n }}\n />\n </LandingShell>\n );\n}\n';
16329
+ var LANDING_PAGE_SOURCE = 'import {\n ApiShowcaseSection,\n FeaturesSection,\n HeroSection,\n LandingShell,\n PlansPreviewSection,\n PricingCtaSection,\n StepsSection,\n} from "@lukoweb/apitogo-module-landing";\nimport { usePlansPreview } from "@lukoweb/apitogo-plugin-dev-portal-billing";\n\nexport default function LandingPage() {\n // Plans from modules.userPanel.plans.items (or the live dev portal backend).\n const plans = usePlansPreview();\n\n return (\n <LandingShell\n title="Test against a real API before you build one"\n description="Sample API gives you realistic endpoints, authentication, rate limits, and webhooks \u2014 a fully working backend for prototypes, demos, and QA."\n >\n <HeroSection\n hero={{\n badge: "SANDBOX API",\n title: "Test against a real API before you build one",\n description:\n "Sample API gives you realistic endpoints, authentication, rate limits, and webhooks \u2014 a fully working backend for prototypes, demos, and QA. No setup, no infrastructure.",\n }}\n primaryAction={{ label: "Start for free", href: "/signup" }}\n secondaryAction={{ label: "Read the docs", href: "/introduction" }}\n codeExamples={[\n {\n filename: "Create a key",\n language: "bash",\n code: `$ curl -X POST https://api.sampleapi.dev/v1/keys\n# sk_live_9f2c7a1e...`,\n },\n {\n filename: "Call an endpoint",\n language: "bash",\n code: `$ curl https://api.sampleapi.dev/v1/users/42 \\\\\n -H "Authorization: Bearer sk_live_9f2c..."\n# { "id": 42, "name": "Priya Shah", "plan": "pro" }`,\n },\n ]}\n />\n <FeaturesSection\n section={{\n label: "Everything a real API has",\n title: "Built for testing, not just browsing",\n description:\n "Realistic data, live auth, rate limits, and webhooks \u2014 so you can ship with confidence.",\n }}\n features={[\n {\n title: "Realistic mock data",\n description:\n "Users, orders, products, and posts with relationships and pagination, not lorem ipsum.",\n icon: "boxes",\n },\n {\n title: "Real auth flow",\n description:\n "Every request checks a live API key, so you can test error handling and expiry.",\n icon: "shield-check",\n },\n {\n title: "Rate limiting",\n description:\n "Per-plan request limits and 429 responses, so you can build and test backoff logic.",\n icon: "gauge",\n },\n {\n title: "Webhooks",\n description:\n "Trigger test events and inspect delivered payloads and retries.",\n icon: "webhook",\n },\n {\n title: "OpenAPI & docs",\n description:\n "A full OpenAPI spec and reference docs for every endpoint.",\n icon: "book-open",\n },\n {\n title: "Usage analytics",\n description:\n "Track requests, latency, and errors per key from your dashboard.",\n icon: "activity",\n },\n ]}\n />\n <StepsSection\n steps={[\n {\n step: "01",\n title: "Create an account",\n description:\n "Sign up and land straight in your dashboard \u2014 no approval queue.",\n },\n {\n step: "02",\n title: "Generate a key",\n description:\n "Every key is scoped and rate-limited to your plan from the moment it\'s created.",\n },\n {\n step: "03",\n title: "Call any endpoint",\n description:\n "Users, orders, products, posts, and webhooks \u2014 all documented, all live.",\n },\n ]}\n />\n {/* Static sample list. To show your real API operations from the dev portal\n backend instead, see https://docs.apitogo.com/docs/customization/landing-page#live-apis */}\n <ApiShowcaseSection\n apiShowcase={{\n title: "Sample endpoints",\n subtitle:\n "A slice of what\'s available \u2014 see the full reference in the docs.",\n browseAllHref: "/api",\n browseAllLabel: "Browse all APIs",\n layout: "list",\n items: [\n {\n name: "List paginated users",\n path: "/v1/users",\n method: "GET",\n summary: "List paginated users",\n },\n {\n name: "Create a mock order",\n path: "/v1/orders",\n method: "POST",\n summary: "Create a mock order",\n },\n {\n name: "Fetch one product",\n path: "/v1/products/{id}",\n method: "GET",\n summary: "Fetch one product",\n },\n {\n name: "List posts with comments",\n path: "/v1/posts",\n method: "GET",\n summary: "List posts with comments",\n },\n {\n name: "Fire a test webhook event",\n path: "/v1/webhooks/test",\n method: "POST",\n summary: "Fire a test webhook event",\n },\n {\n name: "Delete a mock order",\n path: "/v1/orders/{id}",\n method: "DELETE",\n summary: "Delete a mock order",\n },\n ],\n }}\n />\n <PlansPreviewSection plans={plans} />\n <PricingCtaSection\n variant="banner"\n pricingCta={{\n title: "Start testing in minutes",\n description:\n "Free tier includes 1,000 requests a month \u2014 no card required.",\n primaryAction: { label: "Start for free", href: "/signup" },\n }}\n />\n </LandingShell>\n );\n}\n';
16330
16330
  var ACCOUNT_PLANS_PAGE_SOURCE = `export { AccountPlansPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
16331
16331
  `;
16332
16332
  var DASHBOARD_PAGE_SOURCE = `export { OverviewDashboardPage as default } from "@lukoweb/apitogo-plugin-dev-portal-billing";
@@ -19048,6 +19048,20 @@ async function isBackendDeploymentHealthy(client, projectId) {
19048
19048
  return summary.lastDeploymentStatus?.toLowerCase() === "healthy";
19049
19049
  }
19050
19050
 
19051
+ // src/cli/platform/helpers/project-name.ts
19052
+ function normalizeProjectName(name) {
19053
+ return name.trim().split(/\s+/).filter(Boolean).join(" ").toLowerCase();
19054
+ }
19055
+ function findProjectByNormalizedName(projects, name) {
19056
+ const needle = normalizeProjectName(name);
19057
+ if (!needle) {
19058
+ return void 0;
19059
+ }
19060
+ return projects.find(
19061
+ (project) => normalizeProjectName(project.name) === needle
19062
+ );
19063
+ }
19064
+
19051
19065
  // src/cli/platform/helpers/platform-plan.ts
19052
19066
  var UNSELECTED_TIER = "unselected";
19053
19067
  var FREE_TIER = "free";
@@ -19120,20 +19134,6 @@ function parseToolJsonResult(result) {
19120
19134
  }
19121
19135
  }
19122
19136
 
19123
- // src/cli/platform/helpers/project-name.ts
19124
- function normalizeProjectName(name) {
19125
- return name.trim().split(/\s+/).filter(Boolean).join(" ").toLowerCase();
19126
- }
19127
- function findProjectByNormalizedName(projects, name) {
19128
- const needle = normalizeProjectName(name);
19129
- if (!needle) {
19130
- return void 0;
19131
- }
19132
- return projects.find(
19133
- (project) => normalizeProjectName(project.name) === needle
19134
- );
19135
- }
19136
-
19137
19137
  // src/cli/platform/helpers/platform-plan-catalog.ts
19138
19138
  var PLATFORM_PLAN_CATALOG = [
19139
19139
  {
@@ -20155,6 +20155,27 @@ async function resolveProjectIdForPublish(client, config2, args, wait, progress,
20155
20155
  if (!args.projectName?.trim()) {
20156
20156
  throw new Error("projectName is required when projectId is not provided.");
20157
20157
  }
20158
+ if (!args.reuseExistingByName) {
20159
+ const projectName = args.projectName.trim();
20160
+ const existing = findProjectByNormalizedName(
20161
+ await client.listProjects(),
20162
+ projectName
20163
+ );
20164
+ if (existing) {
20165
+ return {
20166
+ blocked: true,
20167
+ payload: {
20168
+ status: "blocked",
20169
+ reason: "project_name_exists",
20170
+ projectId: existing.id,
20171
+ projectName,
20172
+ userMessage: `The config has no projectId, but a project named "${projectName}" already exists on your account.`,
20173
+ nextStep: "reuse_or_rename_project",
20174
+ hint: "Re-run with a different --project-name to create a new project, or set projectId in the config to deploy to the existing one."
20175
+ }
20176
+ };
20177
+ }
20178
+ }
20158
20179
  await progress?.onStep("Creating project and provisioning modules");
20159
20180
  const bootstrapJson = await handleBootstrapProject(
20160
20181
  client,
@@ -21108,7 +21129,8 @@ async function runApplyFromSiteDirectory(client, config2, args, progress) {
21108
21129
  config2,
21109
21130
  {
21110
21131
  projectId: args.createNewProject ? void 0 : args.projectId ?? manifest.projectId,
21111
- projectName: args.projectName ?? resolveProjectDisplayName(manifest)
21132
+ projectName: args.projectName ?? resolveProjectDisplayName(manifest),
21133
+ reuseExistingByName: args.reuseExistingProject
21112
21134
  },
21113
21135
  wait,
21114
21136
  publishProgress,
@@ -21983,6 +22005,16 @@ function formatBlockedOrFailedResultForHuman(parsed) {
21983
22005
  );
21984
22006
  return lines;
21985
22007
  }
22008
+ if (status2 === "blocked" && parsed.reason === "project_name_exists") {
22009
+ const projectId = typeof parsed.projectId === "string" ? parsed.projectId : void 0;
22010
+ return [
22011
+ typeof parsed.userMessage === "string" ? parsed.userMessage : "Deploy is blocked: a project with this name already exists.",
22012
+ "",
22013
+ "To fix this:",
22014
+ ` Deploy to the existing project: set "projectId"${projectId ? ` to "${projectId}"` : ""} in apitogo.config.json`,
22015
+ " Or create a new project: re-run `apitogo deploy --project-name <new-name>`"
22016
+ ];
22017
+ }
21986
22018
  if (status2 === "blocked" && parsed.reason === "project_not_found") {
21987
22019
  const projectId = typeof parsed.projectId === "string" ? parsed.projectId : void 0;
21988
22020
  const lines = [
@@ -22226,6 +22258,34 @@ async function resolveProjectNotFoundInteractively(payload, defaultProjectName)
22226
22258
  rl.close();
22227
22259
  }
22228
22260
  }
22261
+ async function resolveProjectNameExistsInteractively(payload, defaultProjectName) {
22262
+ const existingName = typeof payload.projectName === "string" ? payload.projectName : defaultProjectName;
22263
+ const projectId = typeof payload.projectId === "string" ? payload.projectId : "";
22264
+ printWarningToConsole(
22265
+ `This site has no projectId, but a project named "${existingName}"${projectId ? ` (${projectId})` : ""} already exists on your account.`
22266
+ );
22267
+ printDiagnosticsToConsole("");
22268
+ const rl = createInterface3({ input: input4, output: output3 });
22269
+ try {
22270
+ const choice = (await rl.question(
22271
+ "[c] Create a new project [u] Use the existing project [x] Cancel (default c): "
22272
+ )).trim().toLowerCase();
22273
+ if (choice === "x" || choice === "cancel") {
22274
+ return null;
22275
+ }
22276
+ if (choice === "u" || choice === "use") {
22277
+ return { action: "reuse" };
22278
+ }
22279
+ for (; ; ) {
22280
+ const nameAnswer = (await rl.question("New project name: ")).trim();
22281
+ if (nameAnswer) {
22282
+ return { action: "create", projectName: nameAnswer };
22283
+ }
22284
+ }
22285
+ } finally {
22286
+ rl.close();
22287
+ }
22288
+ }
22229
22289
 
22230
22290
  // src/cli/apply/handler.ts
22231
22291
  function normalizeOnly(only) {
@@ -22277,6 +22337,7 @@ async function apply(argv) {
22277
22337
  let effectiveProjectId = argv.projectId?.trim();
22278
22338
  let effectiveProjectName = argv.projectName?.trim();
22279
22339
  let createNewProject = false;
22340
+ let reuseExistingProject = false;
22280
22341
  const runOnce = (plan) => runApplyFromSiteDirectory(
22281
22342
  client,
22282
22343
  config2,
@@ -22289,7 +22350,8 @@ async function apply(argv) {
22289
22350
  force: argv.force,
22290
22351
  resumeFromStep: argv.resumeFrom?.trim(),
22291
22352
  wait: argv.wait !== false,
22292
- createNewProject
22353
+ createNewProject,
22354
+ reuseExistingProject
22293
22355
  },
22294
22356
  progress
22295
22357
  );
@@ -22324,6 +22386,30 @@ async function apply(argv) {
22324
22386
  parsed = safeParseResult(result);
22325
22387
  }
22326
22388
  }
22389
+ while (canPromptInteractively && parsed?.status === "blocked" && parsed.reason === "project_name_exists") {
22390
+ const resolution = await resolveProjectNameExistsInteractively(
22391
+ parsed,
22392
+ effectiveProjectName ?? path63.basename(sourceDirectory)
22393
+ );
22394
+ if (!resolution) {
22395
+ break;
22396
+ }
22397
+ if (resolution.action === "reuse") {
22398
+ reuseExistingProject = true;
22399
+ } else {
22400
+ effectiveProjectName = resolution.projectName;
22401
+ createNewProject = true;
22402
+ }
22403
+ try {
22404
+ result = await runOnce(platformPlan);
22405
+ } catch (error) {
22406
+ await printCriticalFailureToConsoleAndExit(
22407
+ error instanceof Error ? error.message : String(error)
22408
+ );
22409
+ return;
22410
+ }
22411
+ parsed = safeParseResult(result);
22412
+ }
22327
22413
  if (canPromptInteractively && parsed?.status === "blocked" && parsed.reason === "platform_plan_unselected") {
22328
22414
  const blockedProjectId = typeof parsed.projectId === "string" ? parsed.projectId : void 0;
22329
22415
  const resolved = await resolvePlatformPlanInteractively(
@@ -27,4 +27,5 @@ export type ResolveProjectIdResult = {
27
27
  export declare function resolveProjectIdForPublish(client: PlatformApiClient, config: ApitogoConfig, args: {
28
28
  projectId?: string;
29
29
  projectName?: string;
30
+ reuseExistingByName?: boolean;
30
31
  }, wait: boolean, progress?: ProgressReporter, timeoutMs?: number, pollIntervalMs?: number): Promise<ResolveProjectIdResult>;
@@ -24,6 +24,7 @@ export interface PublishDevPortalArgs {
24
24
  force?: boolean;
25
25
  resumeFromStep?: PublishStepId;
26
26
  createNewProject?: boolean;
27
+ reuseExistingProject?: boolean;
27
28
  }
28
29
  export type ApplyFromSiteDirectoryArgs = PublishDevPortalArgs;
29
30
  export declare function validateManifestForPublish(manifest: DevPortalLocalManifest, secrets: DevPortalLocalSecrets, emailOptions?: {
@@ -157,3 +157,32 @@ to the gateway if they must not be discoverable.
157
157
  from another domain requires adding it to the backend's allowed origins.
158
158
  - **Empty list on a new portal:** the portal is not activated yet, or the gateway has no release.
159
159
  Deploy your backend, then wait for the next sync.
160
+
161
+ ## Plans preview
162
+
163
+ `PlansPreviewSection` shows the same plans as `/pricing`. The scaffold loads them with the
164
+ `usePlansPreview` hook from the billing plugin, so you only maintain plans in one place —
165
+ `modules.userPanel.plans.items` in `apitogo.config.json`:
166
+
167
+ ```tsx
168
+ import { usePlansPreview } from "@lukoweb/apitogo-plugin-dev-portal-billing";
169
+
170
+ export default function LandingPage() {
171
+ const plans = usePlansPreview();
172
+
173
+ return (
174
+ <LandingShell title="…">
175
+ {/* … */}
176
+ <PlansPreviewSection plans={plans} />
177
+ </LandingShell>
178
+ );
179
+ }
180
+ ```
181
+
182
+ In a live portal the plans come from your dev portal backend (`GET /api/v1/plans`); in local
183
+ preview, and while the live catalog loads, they come from `apitogo.config.json`. Each card shows the
184
+ plan's `displayName`, price and billing period, and its first two feature lines (`features`, or
185
+ lines derived from `limits`). The highlighted card matches the "Popular" plan on `/pricing`.
186
+
187
+ To write the cards by hand instead, pass a static array of `{ name, price, unit, blurb, featured }`
188
+ objects to `plans`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.138",
3
+ "version": "0.1.140",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",