@lukoweb/apitogo 0.2.42 → 0.2.44

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
@@ -7322,14 +7322,22 @@ function missingManifestServices(resolved, manifest) {
7322
7322
  )
7323
7323
  );
7324
7324
  }
7325
+ var PREVIEW_MANAGED_ORIGIN_URL = "http://localhost:8080";
7326
+ function previewGatewayUrl(resolved) {
7327
+ const gateway = resolved.gateway;
7328
+ if (gateway?.enabled !== true) return void 0;
7329
+ return gateway.origin.mode === "proxy" ? gateway.origin.url : PREVIEW_MANAGED_ORIGIN_URL;
7330
+ }
7325
7331
  function stubManifest(resolved) {
7326
7332
  const portal = resolved.portal;
7327
7333
  const consoleOn = portal?.enabled === true && portal.console.enabled;
7334
+ const gatewayUrl = previewGatewayUrl(resolved);
7328
7335
  return parseManifest({
7329
7336
  schemaVersion: SUPPORTED_MANIFEST_SCHEMA,
7330
7337
  preview: true,
7331
7338
  project: { id: resolved.project.id, name: resolved.project.name },
7332
7339
  services: {
7340
+ ...gatewayUrl ? { gateway: { status: "ready", baseUrl: gatewayUrl } } : {},
7333
7341
  ...consoleOn && resolved.identity?.enabled ? {
7334
7342
  identity: {
7335
7343
  provider: "apitogo",
@@ -7924,6 +7932,17 @@ function planPortal(profile, manifest) {
7924
7932
  skip("console", "portal.console.enabled is false");
7925
7933
  return plan2;
7926
7934
  }
7935
+ if (plan2.preview && identity && !identity.issuer) {
7936
+ plan2.demo = true;
7937
+ plan2.userPanel = true;
7938
+ skip("sign-in", "preview: the demo account signs in");
7939
+ if (plans)
7940
+ plan2.plansPlugin = {
7941
+ billing: "none",
7942
+ links: plan2.apiReference ? { api: `/${API_REFERENCE_PATH}` } : {}
7943
+ };
7944
+ return plan2;
7945
+ }
7927
7946
  if (!identity) {
7928
7947
  skip("sign-in", "services.json has no identity entry");
7929
7948
  } else if (!serviceReady(identity)) {
@@ -8026,22 +8045,26 @@ function buildSiteConfig(resolved, plan2, theme, extras = {}) {
8026
8045
  ]
8027
8046
  } : {},
8028
8047
  ...navigation.length > 0 ? { navigation } : {},
8029
- // The sidebar link above is only seen inside the docs; the header is what every page shows.
8048
+ // The sidebar link above is only seen inside the docs; the header is what every page shows. A header navigation
8049
+ // replaces the docs tab, so the docs get a header link of their own.
8030
8050
  ...plan2.apiReference ? {
8031
8051
  header: {
8032
8052
  navigation: [
8053
+ ...plan2.docs && extras.docsPath ? [{ label: "Documentation", to: extras.docsPath }] : [],
8033
8054
  { label: "API Reference", to: `/${API_REFERENCE_PATH}` }
8034
8055
  ]
8035
8056
  }
8036
8057
  } : {},
8058
+ ...extras.demoPlans?.length ? { plans: extras.demoPlans } : {},
8037
8059
  ...redirects.length > 0 ? { redirects } : {},
8038
8060
  ...extras.protectedRoutes?.length ? { protectedRoutes: extras.protectedRoutes } : {},
8039
8061
  ...theme !== void 0 ? { theme } : {},
8040
- ...plan2.auth ? { authentication: authenticationFor(plan2) } : {},
8062
+ ...plan2.auth || plan2.demo ? { authentication: authenticationFor(plan2) } : {},
8041
8063
  ...plan2.preview ? { preview: true } : {}
8042
8064
  };
8043
8065
  }
8044
8066
  function authenticationFor(plan2) {
8067
+ if (plan2.demo) return { type: "dev-portal", demoMode: true };
8045
8068
  if (plan2.plansPlugin)
8046
8069
  return {
8047
8070
  type: "dev-portal",
@@ -8053,7 +8076,7 @@ function authenticationFor(plan2) {
8053
8076
  const { audience: _audience, ...signIn } = plan2.auth ?? {};
8054
8077
  return { type: "openid", ...signIn };
8055
8078
  }
8056
- var PREVIEW_BANNER_MESSAGE = "Preview mode: sign-in and plans use a stand-in manifest. Run `apitogo deploy` for the real services.";
8079
+ var PREVIEW_BANNER_MESSAGE = "Preview mode: sign in with demo@apitogo.local / password to try the console. Run `apitogo deploy` for the real services.";
8057
8080
  function renderWrapper(plan2) {
8058
8081
  const imports = [
8059
8082
  'import { buildApitogoConfig } from "@lukoweb/apitogo/build-config";',
@@ -8077,6 +8100,15 @@ function renderWrapper(plan2) {
8077
8100
  ""
8078
8101
  ].join("\n");
8079
8102
  }
8103
+ async function readDemoPlans(file) {
8104
+ if (!file) return void 0;
8105
+ try {
8106
+ const parsed = JSON.parse(await readFile6(file, "utf8"));
8107
+ return Array.isArray(parsed.plans) ? parsed.plans : void 0;
8108
+ } catch {
8109
+ return void 0;
8110
+ }
8111
+ }
8080
8112
  async function readTheme(resolved) {
8081
8113
  const file = resolved.portal?.theme;
8082
8114
  if (!file) return void 0;
@@ -8164,6 +8196,8 @@ async function preparePortal(resolved, manifest, mode, options = {}) {
8164
8196
  {
8165
8197
  redirects: home ? [home] : [],
8166
8198
  docsNavigation: docsNavigation(hostedDocRoutes),
8199
+ docsPath: pickLandingRoute(hostedDocRoutes),
8200
+ demoPlans: plan2.demo && plan2.plansPlugin ? await readDemoPlans(resolved.plans?.file) : void 0,
8167
8201
  protectedRoutes: protectedRoutesFor({
8168
8202
  requireAuth: resolved.portal?.requireAuth,
8169
8203
  hasAuth: !!plan2.auth,
@@ -8337,6 +8371,179 @@ import {
8337
8371
  } from "node:fs/promises";
8338
8372
  import path14 from "node:path";
8339
8373
 
8374
+ // src/config/v2/full-server.ts
8375
+ var FULL_SERVER_SOURCE = "server";
8376
+ var FULL_SERVER_RUNTIME = "nodejs";
8377
+ var FULL_SERVER_RUNTIME_VERSION = "NODE_22_LTS";
8378
+ var FULL_SERVER_HEALTH_PATH = "/health";
8379
+ var FULL_SERVER_TIMEOUT_SECONDS = 120;
8380
+ var packageName = (name) => `${name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "api"}-server`;
8381
+ function fullServerFiles(name) {
8382
+ return {
8383
+ "package.json": `${JSON.stringify(
8384
+ {
8385
+ name: packageName(name),
8386
+ version: "1.0.0",
8387
+ private: true,
8388
+ type: "module",
8389
+ main: "index.js",
8390
+ scripts: { start: "node index.js" },
8391
+ engines: { node: ">=22" }
8392
+ },
8393
+ null,
8394
+ 2
8395
+ )}
8396
+ `,
8397
+ "index.js": `// A small API for ${JSON.stringify(name)}. Every endpoint here is listed in apitogo/apis/openapi.yaml:
8398
+ // add an endpoint in both places, then run \`apitogo deploy\`.
8399
+ import { createServer } from "node:http";
8400
+
8401
+ const items = [
8402
+ { id: "1", name: "First item" },
8403
+ { id: "2", name: "Second item" },
8404
+ { id: "3", name: "Third item" },
8405
+ ];
8406
+
8407
+ // Lets the portal's API page call this server while you run it locally (\`apitogo dev\`). Once deployed, the gateway
8408
+ // answers browsers itself and keeps its own list of allowed sites.
8409
+ const cors = {
8410
+ "access-control-allow-origin": "*",
8411
+ "access-control-allow-methods": "GET, OPTIONS",
8412
+ "access-control-allow-headers": "*",
8413
+ };
8414
+
8415
+ function send(res, status, body) {
8416
+ res.writeHead(status, { "content-type": "application/json", ...cors });
8417
+ res.end(JSON.stringify(body));
8418
+ }
8419
+
8420
+ const server = createServer((req, res) => {
8421
+ const url = new URL(req.url ?? "/", "http://localhost");
8422
+ const itemMatch = url.pathname.match(/^\\/items\\/([^/]+)$/);
8423
+
8424
+ if (req.method === "OPTIONS") {
8425
+ res.writeHead(204, cors);
8426
+ return res.end();
8427
+ }
8428
+ if (req.method !== "GET") return send(res, 405, { error: "Method not allowed" });
8429
+ if (url.pathname === "/health") return send(res, 200, { status: "ok" });
8430
+ if (url.pathname === "/hello") {
8431
+ const who = url.searchParams.get("name") || "world";
8432
+ return send(res, 200, { message: \`Hello, \${who}!\` });
8433
+ }
8434
+ if (url.pathname === "/items") return send(res, 200, items);
8435
+ if (itemMatch) {
8436
+ const item = items.find((candidate) => candidate.id === itemMatch[1]);
8437
+ return item ? send(res, 200, item) : send(res, 404, { error: "Item not found" });
8438
+ }
8439
+ return send(res, 404, { error: "Not found" });
8440
+ });
8441
+
8442
+ const port = Number(process.env.PORT) || 8080;
8443
+ server.listen(port, () => console.log(\`API listening on port \${port}\`));
8444
+ `,
8445
+ "README.md": `# ${name} server
8446
+
8447
+ A small Node.js API with no dependencies, created by \`apitogo create --template full\`.
8448
+
8449
+ - Run it locally: \`npm start\` (port 8080, or \`PORT\`)
8450
+ - Deploy it: \`apitogo deploy\` from the project folder
8451
+
8452
+ Every endpoint in \`index.js\` is also listed in \`apitogo/apis/openapi.yaml\`. Keep the two in step: the gateway only
8453
+ forwards the endpoints the file names, and the API reference of the portal is built from it.
8454
+ `
8455
+ };
8456
+ }
8457
+ var FULL_SPEC = (name) => `openapi: 3.1.0
8458
+ info:
8459
+ title: ${JSON.stringify(name)}
8460
+ version: 1.0.0
8461
+ description: A small sample API. Edit server/index.js and this file together.
8462
+ paths:
8463
+ /health:
8464
+ get:
8465
+ operationId: getHealth
8466
+ summary: Health check
8467
+ responses:
8468
+ "200":
8469
+ description: The service is up.
8470
+ content:
8471
+ application/json:
8472
+ schema:
8473
+ type: object
8474
+ properties:
8475
+ status:
8476
+ type: string
8477
+ example: ok
8478
+ /hello:
8479
+ get:
8480
+ operationId: getHello
8481
+ summary: Say hello
8482
+ parameters:
8483
+ - name: name
8484
+ in: query
8485
+ required: false
8486
+ description: Who to greet.
8487
+ schema:
8488
+ type: string
8489
+ example: developer
8490
+ responses:
8491
+ "200":
8492
+ description: A greeting.
8493
+ content:
8494
+ application/json:
8495
+ schema:
8496
+ type: object
8497
+ properties:
8498
+ message:
8499
+ type: string
8500
+ example: Hello, developer!
8501
+ /items:
8502
+ get:
8503
+ operationId: listItems
8504
+ summary: List items
8505
+ responses:
8506
+ "200":
8507
+ description: All items.
8508
+ content:
8509
+ application/json:
8510
+ schema:
8511
+ type: array
8512
+ items:
8513
+ $ref: "#/components/schemas/Item"
8514
+ /items/{id}:
8515
+ get:
8516
+ operationId: getItem
8517
+ summary: Get one item
8518
+ parameters:
8519
+ - name: id
8520
+ in: path
8521
+ required: true
8522
+ schema:
8523
+ type: string
8524
+ example: "1"
8525
+ responses:
8526
+ "200":
8527
+ description: The item.
8528
+ content:
8529
+ application/json:
8530
+ schema:
8531
+ $ref: "#/components/schemas/Item"
8532
+ "404":
8533
+ description: No item has this id.
8534
+ components:
8535
+ schemas:
8536
+ Item:
8537
+ type: object
8538
+ properties:
8539
+ id:
8540
+ type: string
8541
+ example: "1"
8542
+ name:
8543
+ type: string
8544
+ example: First item
8545
+ `;
8546
+
8340
8547
  // src/config/v2/starter-landing.ts
8341
8548
  function starterLandingPage(options) {
8342
8549
  const name = JSON.stringify(options.name);
@@ -8523,6 +8730,7 @@ var serviceLabel = (service) => `${SERVICE_LABELS[service]} (${service})`;
8523
8730
  function isServiceName(value) {
8524
8731
  return SERVICES.includes(value);
8525
8732
  }
8733
+ var isFilled = (template) => template === "starter" || template === "full";
8526
8734
  var exists3 = (target) => access3(target).then(
8527
8735
  () => true,
8528
8736
  () => false
@@ -8688,7 +8896,7 @@ async function buildPortal(context) {
8688
8896
  const pages = path14.join(resolved.root, "portal", "pages");
8689
8897
  if (template === "blank")
8690
8898
  await addFile(context, path14.join(pages, ".gitkeep"), "");
8691
- else if (options.template === "starter") {
8899
+ else if (isFilled(options.template)) {
8692
8900
  await addFile(
8693
8901
  context,
8694
8902
  path14.join(pages, "introduction.mdx"),
@@ -8727,21 +8935,30 @@ async function buildPortal(context) {
8727
8935
  ...o.search && !external ? { search: o.search } : {}
8728
8936
  };
8729
8937
  }
8730
- async function createSourceFolder(context, sourcePath) {
8938
+ async function createSourceFolder(context, sourcePath, runtime) {
8731
8939
  const folder = path14.join(context.dir, sourcePath);
8732
8940
  const entries = await readdir3(folder).catch(() => []);
8733
- if (entries.some((name) => name !== ".gitkeep")) return;
8941
+ if (entries.some((name) => name !== ".gitkeep")) return false;
8942
+ if (context.options.template === "full" && runtime === FULL_SERVER_RUNTIME) {
8943
+ for (const [file, content] of Object.entries(
8944
+ fullServerFiles(context.resolved.project.name)
8945
+ ))
8946
+ await addFile(context, path14.join(folder, file), content);
8947
+ return true;
8948
+ }
8734
8949
  if (entries.length === 0)
8735
8950
  await addFile(context, path14.join(folder, ".gitkeep"), "");
8736
8951
  context.notes.push(
8737
8952
  `The folder ${sourcePath}/ is empty: put your backend code there before you deploy.`
8738
8953
  );
8954
+ return false;
8739
8955
  }
8740
8956
  async function buildGateway(context) {
8741
8957
  const { options, dir, resolved } = context;
8742
8958
  const o = options.gateway ?? {};
8743
8959
  const ask4 = options.ask;
8744
- let mode = o.origin ?? (o.url ? "proxy" : o.source || o.runtime ? "managed" : void 0);
8960
+ const full = options.template === "full" && !o.url && o.origin !== "proxy";
8961
+ let mode = o.origin ?? (o.url ? "proxy" : o.source || o.runtime || full ? "managed" : void 0);
8745
8962
  if (!mode && ask4) {
8746
8963
  mode = await ask4.choose("How does the gateway reach your API?", [
8747
8964
  {
@@ -8761,6 +8978,7 @@ async function buildGateway(context) {
8761
8978
  );
8762
8979
  }
8763
8980
  let origin;
8981
+ let sampleServer = false;
8764
8982
  if (mode === "proxy") {
8765
8983
  const url2 = o.url ?? (ask4 ? await ask4.text("https address of your API", {
8766
8984
  validate: httpsProblem
@@ -8769,15 +8987,15 @@ async function buildGateway(context) {
8769
8987
  throw new ServiceEditError("--origin proxy needs --url <https url>.");
8770
8988
  origin = { mode, url: url2 };
8771
8989
  } else {
8772
- const source = o.source ?? (ask4 ? await ask4.text("Folder with your code, relative to the project", {
8990
+ const source = o.source ?? (full ? FULL_SERVER_SOURCE : void 0) ?? (ask4 ? await ask4.text("Folder with your code, relative to the project", {
8773
8991
  default: "server"
8774
8992
  }) : void 0);
8775
- const runtime = o.runtime ?? (ask4 ? await ask4.choose("Runtime of your code", [
8993
+ const runtime = o.runtime ?? (full ? FULL_SERVER_RUNTIME : void 0) ?? (ask4 ? await ask4.choose("Runtime of your code", [
8776
8994
  { value: "nodejs", label: "Node.js" },
8777
8995
  { value: "python", label: "Python" },
8778
8996
  { value: "dotnet", label: ".NET" }
8779
8997
  ]) : void 0);
8780
- const runtimeVersion = o.runtimeVersion !== void 0 && runtime ? runtimeVersionSlug(runtime, o.runtimeVersion) : ask4 && runtime ? await ask4.choose(
8998
+ const runtimeVersion = o.runtimeVersion !== void 0 && runtime ? runtimeVersionSlug(runtime, o.runtimeVersion) : full && runtime === FULL_SERVER_RUNTIME ? FULL_SERVER_RUNTIME_VERSION : ask4 && runtime ? await ask4.choose(
8781
8999
  "Runtime version",
8782
9000
  RUNTIME_VERSIONS[runtime].map((choice, index) => ({
8783
9001
  value: choice.value,
@@ -8791,14 +9009,17 @@ async function buildGateway(context) {
8791
9009
  );
8792
9010
  }
8793
9011
  const sourcePath = insideProject(dir, source, "Source folder");
9012
+ sampleServer = await createSourceFolder(context, sourcePath, runtime);
8794
9013
  origin = {
8795
9014
  mode,
8796
9015
  source: sourcePath,
8797
9016
  runtime,
8798
9017
  runtimeVersion,
8799
- ...o.exclude?.length ? { exclude: o.exclude } : {}
9018
+ ...o.exclude?.length ? { exclude: o.exclude } : {},
9019
+ ...sampleServer ? { healthPath: FULL_SERVER_HEALTH_PATH } : {},
9020
+ // A full project's API is hosted by the platform and sleeps when idle; the first call must outlast its start.
9021
+ ...full ? { timeoutSeconds: FULL_SERVER_TIMEOUT_SECONDS } : {}
8800
9022
  };
8801
- await createSourceFolder(context, sourcePath);
8802
9023
  }
8803
9024
  let specs;
8804
9025
  let requestedSpecs = o.spec ?? [];
@@ -8833,7 +9054,7 @@ async function buildGateway(context) {
8833
9054
  await addFile(
8834
9055
  context,
8835
9056
  target,
8836
- options.template === "blank" ? BLANK_SPEC(resolved.project.name) : STARTER_SPEC(resolved.project.name)
9057
+ options.template === "blank" ? BLANK_SPEC(resolved.project.name) : sampleServer ? FULL_SPEC(resolved.project.name) : STARTER_SPEC(resolved.project.name)
8837
9058
  );
8838
9059
  specs = [rel(dir, target)];
8839
9060
  } else {
@@ -8921,7 +9142,7 @@ async function buildIdentity(context) {
8921
9142
  async function buildPlans(context) {
8922
9143
  const { options, dir, resolved } = context;
8923
9144
  const o = options.plans ?? {};
8924
- const template = o.template ?? (options.template === "blank" ? "none" : options.template === "starter" ? "starter" : "free");
9145
+ const template = o.template ?? (options.template === "blank" ? "none" : isFilled(options.template) ? "starter" : "free");
8925
9146
  const billing = o.billing ?? (template === "tiered" ? "stripe" : "none");
8926
9147
  if (template === "tiered" && billing === "none") {
8927
9148
  throw new ServiceEditError(
@@ -10371,7 +10592,7 @@ import {
10371
10592
  // package.json
10372
10593
  var package_default = {
10373
10594
  name: "@lukoweb/apitogo",
10374
- version: "0.2.42",
10595
+ version: "0.2.44",
10375
10596
  type: "module",
10376
10597
  sideEffects: [
10377
10598
  "**/*.css",
@@ -14193,6 +14414,8 @@ var viteModulesPlugin = () => {
14193
14414
  code.push(
14194
14415
  `const userPanel = userPanelModule({`,
14195
14416
  ` ...${JSON.stringify(userPanelConfig)},`,
14417
+ // With billing on, the billing plugin owns the plans page and the account side menu.
14418
+ ` billingNav: ${config2.__meta.pricingEnabled === true},`,
14196
14419
  ` dashboard: {`,
14197
14420
  ` ...${JSON.stringify(userPanelConfig.dashboard)},`,
14198
14421
  ` component: config.modules?.userPanel?.dashboard?.component${dashboardPagePath || useBillingDashboard ? " ?? UserPanelDashboardPage" : ""},`,
@@ -14426,7 +14649,11 @@ function vitePlugin() {
14426
14649
  import { lstatSync, realpathSync } from "node:fs";
14427
14650
  import path40 from "node:path";
14428
14651
  var STATE_INSIDE_CONTENT = /[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
14429
- var within = (base, file) => file === base || file.startsWith(base + path40.sep);
14652
+ var within = (base, file) => {
14653
+ const resolvedBase = path40.resolve(base);
14654
+ const resolvedFile = path40.resolve(file);
14655
+ return resolvedFile === resolvedBase || resolvedFile.startsWith(resolvedBase + path40.sep);
14656
+ };
14430
14657
  var unique = (paths) => [...new Set(paths)];
14431
14658
  function isWorkspace(root) {
14432
14659
  try {
@@ -16216,7 +16443,7 @@ function addOptionsFromArguments(service, argv) {
16216
16443
  dir,
16217
16444
  disabled: argv.disabled === true,
16218
16445
  acceptAllDocs: argv.yes === true,
16219
- template: argv.template === "blank" || argv.template === "none" ? "blank" : argv.template === "starter" ? "starter" : void 0,
16446
+ template: argv.template === "blank" || argv.template === "none" ? "blank" : argv.template === "starter" || argv.template === "full" ? argv.template : void 0,
16220
16447
  portal: service === "portal" ? {
16221
16448
  name: argv.name,
16222
16449
  docs: parseDocsFlag(argv.docs),
@@ -16554,8 +16781,15 @@ var add_default = {
16554
16781
  describe: "plans: catalog file (default <root>/plans.json)"
16555
16782
  }).option("template", {
16556
16783
  type: "string",
16557
- choices: ["blank", "starter", "free", "tiered", "none"],
16558
- describe: "Content to create: blank (empty shell) or starter (filled example). Plans also accept free, tiered and none; none is blank"
16784
+ choices: [
16785
+ "blank",
16786
+ "starter",
16787
+ "full",
16788
+ "free",
16789
+ "tiered",
16790
+ "none"
16791
+ ],
16792
+ describe: "Content to create: blank (empty shell), starter (filled example) or full (starter plus a Node.js backend in server/). Plans also accept free, tiered and none; none is blank"
16559
16793
  }).option("billing", {
16560
16794
  type: "string",
16561
16795
  choices: ["stripe", "none"],
@@ -21341,9 +21575,15 @@ import { promisify } from "node:util";
21341
21575
  var execFileAsync = promisify(execFile);
21342
21576
  function nextStepHint(dir) {
21343
21577
  const linked = loadConfigV2(dir).resolved.project.id;
21344
- return linked ? "Next: apitogo deploy" : "Next: apitogo project create, then apitogo deploy";
21578
+ const cd = path70.resolve(dir) === path70.resolve(process.cwd());
21579
+ const steps = linked ? "apitogo deploy" : "apitogo project create, then apitogo deploy";
21580
+ return cd ? `Next: ${steps}` : `Next: cd ${JSON.stringify(path70.relative(process.cwd(), dir))}, then ${steps}`;
21581
+ }
21582
+ function projectFolderName(name) {
21583
+ return name.trim().replace(/[<>:"/\\|?*\s]+/g, "-").replace(/^[-.]+|[-.]+$/g, "") || "apitogo-project";
21345
21584
  }
21346
21585
  var DONE = "done";
21586
+ var TEMPLATES = ["full", "blank", "starter"];
21347
21587
  var STARTER_SERVICES = [
21348
21588
  "gateway",
21349
21589
  "identity",
@@ -21395,23 +21635,37 @@ async function createProject(argv, deps = {}) {
21395
21635
  if (!json) printDiagnosticsToConsole(line2);
21396
21636
  };
21397
21637
  try {
21398
- const dir = path70.resolve(process.cwd(), argv.dir ?? ".");
21638
+ const base = path70.resolve(process.cwd(), argv.dir ?? ".");
21639
+ let dir = base;
21399
21640
  let requested = parseServicesFlag(argv.services);
21400
21641
  let template = argv.template;
21401
- if (!asker && requested.length === 0) {
21642
+ if (template !== void 0 && !TEMPLATES.includes(template))
21643
+ throw new InitError(
21644
+ `Unknown template: ${template}.`,
21645
+ "Use --template full or --template blank."
21646
+ );
21647
+ const templateServices = template === "starter" || template === "full";
21648
+ if (!asker && requested.length === 0 && !templateServices) {
21402
21649
  throw new InitError(
21403
21650
  "create needs a terminal to ask its questions, and none is available.",
21404
- "Pass --services <portal,gateway,identity,plans> (and --template blank|starter), or run `apitogo init` and `apitogo add <service>`."
21651
+ "Pass --template full, or --services <portal,gateway,identity,plans> (and --template blank|starter|full), or run `apitogo init` and `apitogo add <service>`."
21405
21652
  );
21406
21653
  }
21654
+ const existing = await isConfigV2(base);
21655
+ let name;
21656
+ if (!existing) {
21657
+ name = argv.name?.trim() || (asker ? await asker.text("Project name", {
21658
+ default: await defaultName(base)
21659
+ }) : await defaultName(base));
21660
+ const folder = projectFolderName(name);
21661
+ if (argv.dir === void 0 && folder !== path70.basename(base))
21662
+ dir = path70.join(base, folder);
21663
+ }
21407
21664
  if (await (deps.hasUncommittedChanges ?? gitHasUncommittedChanges)(dir))
21408
21665
  say(
21409
21666
  "This folder has uncommitted changes. Commit first so you can undo this easily."
21410
21667
  );
21411
- if (!await isConfigV2(dir)) {
21412
- const name = argv.name?.trim() || (asker ? await asker.text("Project name", {
21413
- default: await defaultName(dir)
21414
- }) : await defaultName(dir));
21668
+ if (name !== void 0 && !await isConfigV2(dir)) {
21415
21669
  const plan2 = await planInit({ dir, name });
21416
21670
  for (const line2 of formatInitPlan(plan2)) say(line2);
21417
21671
  commands.push(`apitogo init --name ${JSON.stringify(name)}`);
@@ -21431,7 +21685,9 @@ async function createProject(argv, deps = {}) {
21431
21685
  return;
21432
21686
  }
21433
21687
  await applyInitPlan(plan2);
21434
- say(`Created ${APITOGO_CONFIG_FILENAME}.`);
21688
+ say(
21689
+ dir === base ? `Created ${APITOGO_CONFIG_FILENAME}.` : `Created ${path70.relative(base, dir)}${path70.sep}${APITOGO_CONFIG_FILENAME}.`
21690
+ );
21435
21691
  } else {
21436
21692
  say(`${APITOGO_CONFIG_FILENAME} exists: continuing from it.`);
21437
21693
  }
@@ -21440,17 +21696,17 @@ async function createProject(argv, deps = {}) {
21440
21696
  "What do you want to start from?",
21441
21697
  [
21442
21698
  {
21443
- value: "starter",
21444
- label: "Starter: every service with examples (home page, sample plans, API reference)"
21699
+ value: "full",
21700
+ label: "Full: every service with examples and a small Node.js backend, ready to deploy"
21445
21701
  },
21446
21702
  { value: "blank", label: "Blank: choose the services one by one" }
21447
21703
  ]
21448
21704
  );
21449
- if (template === "starter" && requested.length === 0)
21705
+ if ((template === "starter" || template === "full") && requested.length === 0)
21450
21706
  requested = STARTER_SERVICES;
21451
21707
  const stepArgs = (service) => ({
21452
21708
  service,
21453
- dir: argv.dir,
21709
+ dir,
21454
21710
  template,
21455
21711
  dryRun: argv.dryRun,
21456
21712
  install: argv.install,
@@ -21531,8 +21787,7 @@ var create_default = {
21531
21787
  command: "create",
21532
21788
  builder: (yargs2) => yargs2.option("dir", {
21533
21789
  type: "string",
21534
- describe: "Project directory",
21535
- default: ".",
21790
+ describe: "Project directory (default: a new folder named after the project)",
21536
21791
  normalize: true,
21537
21792
  hidden: true
21538
21793
  }).option("name", {
@@ -21543,8 +21798,8 @@ var create_default = {
21543
21798
  describe: "Skip the menu and add these services, comma separated (portal, gateway, identity, plans)"
21544
21799
  }).option("template", {
21545
21800
  type: "string",
21546
- choices: ["blank", "starter"],
21547
- describe: "Template for the services added (default: ask)"
21801
+ // `starter` is still accepted for older scripts but no longer offered: `full` replaces it.
21802
+ describe: "Template: full (every service, a sample Node.js backend, ready to deploy) or blank. Default: ask"
21548
21803
  }).option("input", {
21549
21804
  type: "boolean",
21550
21805
  default: true,
@@ -1,8 +1,10 @@
1
1
  import type { UseMutationResult } from "@tanstack/react-query";
2
2
  import type { PlaygroundResult } from "../Playground.js";
3
- export declare const ResultPanel: ({ queryMutation, showLongRunningWarning, onCancel, tip, isFinished, progress, }: {
3
+ export declare const ResultPanel: ({ queryMutation, showLongRunningWarning, isWarmingUp, hasIdentities, onCancel, tip, isFinished, progress, }: {
4
4
  queryMutation: UseMutationResult<PlaygroundResult, Error, any>;
5
5
  showLongRunningWarning?: boolean;
6
+ isWarmingUp?: boolean;
7
+ hasIdentities?: boolean;
6
8
  onCancel?: () => void;
7
9
  isFinished: boolean;
8
10
  progress: number;
@@ -0,0 +1,5 @@
1
+ export declare const WARM_UP_STATUSES: number[];
2
+ export declare function fetchWithWarmUpRetry(request: Request, init: RequestInit, options?: {
3
+ delayMs?: number;
4
+ onRetry?: () => void;
5
+ }): Promise<Response>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.42",
3
+ "version": "0.2.44",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",