@lukoweb/apitogo 0.2.42 → 0.2.43

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,178 @@ 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 packageName = (name) => `${name.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "") || "api"}-server`;
8380
+ function fullServerFiles(name) {
8381
+ return {
8382
+ "package.json": `${JSON.stringify(
8383
+ {
8384
+ name: packageName(name),
8385
+ version: "1.0.0",
8386
+ private: true,
8387
+ type: "module",
8388
+ main: "index.js",
8389
+ scripts: { start: "node index.js" },
8390
+ engines: { node: ">=22" }
8391
+ },
8392
+ null,
8393
+ 2
8394
+ )}
8395
+ `,
8396
+ "index.js": `// A small API for ${JSON.stringify(name)}. Every endpoint here is listed in apitogo/apis/openapi.yaml:
8397
+ // add an endpoint in both places, then run \`apitogo deploy\`.
8398
+ import { createServer } from "node:http";
8399
+
8400
+ const items = [
8401
+ { id: "1", name: "First item" },
8402
+ { id: "2", name: "Second item" },
8403
+ { id: "3", name: "Third item" },
8404
+ ];
8405
+
8406
+ // Lets the portal's API page call this server while you run it locally (\`apitogo dev\`). Once deployed, the gateway
8407
+ // answers browsers itself and keeps its own list of allowed sites.
8408
+ const cors = {
8409
+ "access-control-allow-origin": "*",
8410
+ "access-control-allow-methods": "GET, OPTIONS",
8411
+ "access-control-allow-headers": "*",
8412
+ };
8413
+
8414
+ function send(res, status, body) {
8415
+ res.writeHead(status, { "content-type": "application/json", ...cors });
8416
+ res.end(JSON.stringify(body));
8417
+ }
8418
+
8419
+ const server = createServer((req, res) => {
8420
+ const url = new URL(req.url ?? "/", "http://localhost");
8421
+ const itemMatch = url.pathname.match(/^\\/items\\/([^/]+)$/);
8422
+
8423
+ if (req.method === "OPTIONS") {
8424
+ res.writeHead(204, cors);
8425
+ return res.end();
8426
+ }
8427
+ if (req.method !== "GET") return send(res, 405, { error: "Method not allowed" });
8428
+ if (url.pathname === "/health") return send(res, 200, { status: "ok" });
8429
+ if (url.pathname === "/hello") {
8430
+ const who = url.searchParams.get("name") || "world";
8431
+ return send(res, 200, { message: \`Hello, \${who}!\` });
8432
+ }
8433
+ if (url.pathname === "/items") return send(res, 200, items);
8434
+ if (itemMatch) {
8435
+ const item = items.find((candidate) => candidate.id === itemMatch[1]);
8436
+ return item ? send(res, 200, item) : send(res, 404, { error: "Item not found" });
8437
+ }
8438
+ return send(res, 404, { error: "Not found" });
8439
+ });
8440
+
8441
+ const port = Number(process.env.PORT) || 8080;
8442
+ server.listen(port, () => console.log(\`API listening on port \${port}\`));
8443
+ `,
8444
+ "README.md": `# ${name} server
8445
+
8446
+ A small Node.js API with no dependencies, created by \`apitogo create --template full\`.
8447
+
8448
+ - Run it locally: \`npm start\` (port 8080, or \`PORT\`)
8449
+ - Deploy it: \`apitogo deploy\` from the project folder
8450
+
8451
+ Every endpoint in \`index.js\` is also listed in \`apitogo/apis/openapi.yaml\`. Keep the two in step: the gateway only
8452
+ forwards the endpoints the file names, and the API reference of the portal is built from it.
8453
+ `
8454
+ };
8455
+ }
8456
+ var FULL_SPEC = (name) => `openapi: 3.1.0
8457
+ info:
8458
+ title: ${JSON.stringify(name)}
8459
+ version: 1.0.0
8460
+ description: A small sample API. Edit server/index.js and this file together.
8461
+ paths:
8462
+ /health:
8463
+ get:
8464
+ operationId: getHealth
8465
+ summary: Health check
8466
+ responses:
8467
+ "200":
8468
+ description: The service is up.
8469
+ content:
8470
+ application/json:
8471
+ schema:
8472
+ type: object
8473
+ properties:
8474
+ status:
8475
+ type: string
8476
+ example: ok
8477
+ /hello:
8478
+ get:
8479
+ operationId: getHello
8480
+ summary: Say hello
8481
+ parameters:
8482
+ - name: name
8483
+ in: query
8484
+ required: false
8485
+ description: Who to greet.
8486
+ schema:
8487
+ type: string
8488
+ example: developer
8489
+ responses:
8490
+ "200":
8491
+ description: A greeting.
8492
+ content:
8493
+ application/json:
8494
+ schema:
8495
+ type: object
8496
+ properties:
8497
+ message:
8498
+ type: string
8499
+ example: Hello, developer!
8500
+ /items:
8501
+ get:
8502
+ operationId: listItems
8503
+ summary: List items
8504
+ responses:
8505
+ "200":
8506
+ description: All items.
8507
+ content:
8508
+ application/json:
8509
+ schema:
8510
+ type: array
8511
+ items:
8512
+ $ref: "#/components/schemas/Item"
8513
+ /items/{id}:
8514
+ get:
8515
+ operationId: getItem
8516
+ summary: Get one item
8517
+ parameters:
8518
+ - name: id
8519
+ in: path
8520
+ required: true
8521
+ schema:
8522
+ type: string
8523
+ example: "1"
8524
+ responses:
8525
+ "200":
8526
+ description: The item.
8527
+ content:
8528
+ application/json:
8529
+ schema:
8530
+ $ref: "#/components/schemas/Item"
8531
+ "404":
8532
+ description: No item has this id.
8533
+ components:
8534
+ schemas:
8535
+ Item:
8536
+ type: object
8537
+ properties:
8538
+ id:
8539
+ type: string
8540
+ example: "1"
8541
+ name:
8542
+ type: string
8543
+ example: First item
8544
+ `;
8545
+
8340
8546
  // src/config/v2/starter-landing.ts
8341
8547
  function starterLandingPage(options) {
8342
8548
  const name = JSON.stringify(options.name);
@@ -8523,6 +8729,7 @@ var serviceLabel = (service) => `${SERVICE_LABELS[service]} (${service})`;
8523
8729
  function isServiceName(value) {
8524
8730
  return SERVICES.includes(value);
8525
8731
  }
8732
+ var isFilled = (template) => template === "starter" || template === "full";
8526
8733
  var exists3 = (target) => access3(target).then(
8527
8734
  () => true,
8528
8735
  () => false
@@ -8688,7 +8895,7 @@ async function buildPortal(context) {
8688
8895
  const pages = path14.join(resolved.root, "portal", "pages");
8689
8896
  if (template === "blank")
8690
8897
  await addFile(context, path14.join(pages, ".gitkeep"), "");
8691
- else if (options.template === "starter") {
8898
+ else if (isFilled(options.template)) {
8692
8899
  await addFile(
8693
8900
  context,
8694
8901
  path14.join(pages, "introduction.mdx"),
@@ -8727,21 +8934,30 @@ async function buildPortal(context) {
8727
8934
  ...o.search && !external ? { search: o.search } : {}
8728
8935
  };
8729
8936
  }
8730
- async function createSourceFolder(context, sourcePath) {
8937
+ async function createSourceFolder(context, sourcePath, runtime) {
8731
8938
  const folder = path14.join(context.dir, sourcePath);
8732
8939
  const entries = await readdir3(folder).catch(() => []);
8733
- if (entries.some((name) => name !== ".gitkeep")) return;
8940
+ if (entries.some((name) => name !== ".gitkeep")) return false;
8941
+ if (context.options.template === "full" && runtime === FULL_SERVER_RUNTIME) {
8942
+ for (const [file, content] of Object.entries(
8943
+ fullServerFiles(context.resolved.project.name)
8944
+ ))
8945
+ await addFile(context, path14.join(folder, file), content);
8946
+ return true;
8947
+ }
8734
8948
  if (entries.length === 0)
8735
8949
  await addFile(context, path14.join(folder, ".gitkeep"), "");
8736
8950
  context.notes.push(
8737
8951
  `The folder ${sourcePath}/ is empty: put your backend code there before you deploy.`
8738
8952
  );
8953
+ return false;
8739
8954
  }
8740
8955
  async function buildGateway(context) {
8741
8956
  const { options, dir, resolved } = context;
8742
8957
  const o = options.gateway ?? {};
8743
8958
  const ask4 = options.ask;
8744
- let mode = o.origin ?? (o.url ? "proxy" : o.source || o.runtime ? "managed" : void 0);
8959
+ const full = options.template === "full" && !o.url && o.origin !== "proxy";
8960
+ let mode = o.origin ?? (o.url ? "proxy" : o.source || o.runtime || full ? "managed" : void 0);
8745
8961
  if (!mode && ask4) {
8746
8962
  mode = await ask4.choose("How does the gateway reach your API?", [
8747
8963
  {
@@ -8761,6 +8977,7 @@ async function buildGateway(context) {
8761
8977
  );
8762
8978
  }
8763
8979
  let origin;
8980
+ let sampleServer = false;
8764
8981
  if (mode === "proxy") {
8765
8982
  const url2 = o.url ?? (ask4 ? await ask4.text("https address of your API", {
8766
8983
  validate: httpsProblem
@@ -8769,15 +8986,15 @@ async function buildGateway(context) {
8769
8986
  throw new ServiceEditError("--origin proxy needs --url <https url>.");
8770
8987
  origin = { mode, url: url2 };
8771
8988
  } else {
8772
- const source = o.source ?? (ask4 ? await ask4.text("Folder with your code, relative to the project", {
8989
+ const source = o.source ?? (full ? FULL_SERVER_SOURCE : void 0) ?? (ask4 ? await ask4.text("Folder with your code, relative to the project", {
8773
8990
  default: "server"
8774
8991
  }) : void 0);
8775
- const runtime = o.runtime ?? (ask4 ? await ask4.choose("Runtime of your code", [
8992
+ const runtime = o.runtime ?? (full ? FULL_SERVER_RUNTIME : void 0) ?? (ask4 ? await ask4.choose("Runtime of your code", [
8776
8993
  { value: "nodejs", label: "Node.js" },
8777
8994
  { value: "python", label: "Python" },
8778
8995
  { value: "dotnet", label: ".NET" }
8779
8996
  ]) : void 0);
8780
- const runtimeVersion = o.runtimeVersion !== void 0 && runtime ? runtimeVersionSlug(runtime, o.runtimeVersion) : ask4 && runtime ? await ask4.choose(
8997
+ 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
8998
  "Runtime version",
8782
8999
  RUNTIME_VERSIONS[runtime].map((choice, index) => ({
8783
9000
  value: choice.value,
@@ -8791,14 +9008,15 @@ async function buildGateway(context) {
8791
9008
  );
8792
9009
  }
8793
9010
  const sourcePath = insideProject(dir, source, "Source folder");
9011
+ sampleServer = await createSourceFolder(context, sourcePath, runtime);
8794
9012
  origin = {
8795
9013
  mode,
8796
9014
  source: sourcePath,
8797
9015
  runtime,
8798
9016
  runtimeVersion,
8799
- ...o.exclude?.length ? { exclude: o.exclude } : {}
9017
+ ...o.exclude?.length ? { exclude: o.exclude } : {},
9018
+ ...sampleServer ? { healthPath: FULL_SERVER_HEALTH_PATH } : {}
8800
9019
  };
8801
- await createSourceFolder(context, sourcePath);
8802
9020
  }
8803
9021
  let specs;
8804
9022
  let requestedSpecs = o.spec ?? [];
@@ -8833,7 +9051,7 @@ async function buildGateway(context) {
8833
9051
  await addFile(
8834
9052
  context,
8835
9053
  target,
8836
- options.template === "blank" ? BLANK_SPEC(resolved.project.name) : STARTER_SPEC(resolved.project.name)
9054
+ options.template === "blank" ? BLANK_SPEC(resolved.project.name) : sampleServer ? FULL_SPEC(resolved.project.name) : STARTER_SPEC(resolved.project.name)
8837
9055
  );
8838
9056
  specs = [rel(dir, target)];
8839
9057
  } else {
@@ -8921,7 +9139,7 @@ async function buildIdentity(context) {
8921
9139
  async function buildPlans(context) {
8922
9140
  const { options, dir, resolved } = context;
8923
9141
  const o = options.plans ?? {};
8924
- const template = o.template ?? (options.template === "blank" ? "none" : options.template === "starter" ? "starter" : "free");
9142
+ const template = o.template ?? (options.template === "blank" ? "none" : isFilled(options.template) ? "starter" : "free");
8925
9143
  const billing = o.billing ?? (template === "tiered" ? "stripe" : "none");
8926
9144
  if (template === "tiered" && billing === "none") {
8927
9145
  throw new ServiceEditError(
@@ -10371,7 +10589,7 @@ import {
10371
10589
  // package.json
10372
10590
  var package_default = {
10373
10591
  name: "@lukoweb/apitogo",
10374
- version: "0.2.42",
10592
+ version: "0.2.43",
10375
10593
  type: "module",
10376
10594
  sideEffects: [
10377
10595
  "**/*.css",
@@ -14426,7 +14644,11 @@ function vitePlugin() {
14426
14644
  import { lstatSync, realpathSync } from "node:fs";
14427
14645
  import path40 from "node:path";
14428
14646
  var STATE_INSIDE_CONTENT = /[\\/]content[\\/](?:\.apitogo|apitogo[\\/]\.state)(?:[\\/]|$)/;
14429
- var within = (base, file) => file === base || file.startsWith(base + path40.sep);
14647
+ var within = (base, file) => {
14648
+ const resolvedBase = path40.resolve(base);
14649
+ const resolvedFile = path40.resolve(file);
14650
+ return resolvedFile === resolvedBase || resolvedFile.startsWith(resolvedBase + path40.sep);
14651
+ };
14430
14652
  var unique = (paths) => [...new Set(paths)];
14431
14653
  function isWorkspace(root) {
14432
14654
  try {
@@ -16216,7 +16438,7 @@ function addOptionsFromArguments(service, argv) {
16216
16438
  dir,
16217
16439
  disabled: argv.disabled === true,
16218
16440
  acceptAllDocs: argv.yes === true,
16219
- template: argv.template === "blank" || argv.template === "none" ? "blank" : argv.template === "starter" ? "starter" : void 0,
16441
+ template: argv.template === "blank" || argv.template === "none" ? "blank" : argv.template === "starter" || argv.template === "full" ? argv.template : void 0,
16220
16442
  portal: service === "portal" ? {
16221
16443
  name: argv.name,
16222
16444
  docs: parseDocsFlag(argv.docs),
@@ -16554,8 +16776,15 @@ var add_default = {
16554
16776
  describe: "plans: catalog file (default <root>/plans.json)"
16555
16777
  }).option("template", {
16556
16778
  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"
16779
+ choices: [
16780
+ "blank",
16781
+ "starter",
16782
+ "full",
16783
+ "free",
16784
+ "tiered",
16785
+ "none"
16786
+ ],
16787
+ 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
16788
  }).option("billing", {
16560
16789
  type: "string",
16561
16790
  choices: ["stripe", "none"],
@@ -21341,9 +21570,15 @@ import { promisify } from "node:util";
21341
21570
  var execFileAsync = promisify(execFile);
21342
21571
  function nextStepHint(dir) {
21343
21572
  const linked = loadConfigV2(dir).resolved.project.id;
21344
- return linked ? "Next: apitogo deploy" : "Next: apitogo project create, then apitogo deploy";
21573
+ const cd = path70.resolve(dir) === path70.resolve(process.cwd());
21574
+ const steps = linked ? "apitogo deploy" : "apitogo project create, then apitogo deploy";
21575
+ return cd ? `Next: ${steps}` : `Next: cd ${JSON.stringify(path70.relative(process.cwd(), dir))}, then ${steps}`;
21576
+ }
21577
+ function projectFolderName(name) {
21578
+ return name.trim().replace(/[<>:"/\\|?*\s]+/g, "-").replace(/^[-.]+|[-.]+$/g, "") || "apitogo-project";
21345
21579
  }
21346
21580
  var DONE = "done";
21581
+ var TEMPLATES = ["full", "blank", "starter"];
21347
21582
  var STARTER_SERVICES = [
21348
21583
  "gateway",
21349
21584
  "identity",
@@ -21395,23 +21630,37 @@ async function createProject(argv, deps = {}) {
21395
21630
  if (!json) printDiagnosticsToConsole(line2);
21396
21631
  };
21397
21632
  try {
21398
- const dir = path70.resolve(process.cwd(), argv.dir ?? ".");
21633
+ const base = path70.resolve(process.cwd(), argv.dir ?? ".");
21634
+ let dir = base;
21399
21635
  let requested = parseServicesFlag(argv.services);
21400
21636
  let template = argv.template;
21401
- if (!asker && requested.length === 0) {
21637
+ if (template !== void 0 && !TEMPLATES.includes(template))
21638
+ throw new InitError(
21639
+ `Unknown template: ${template}.`,
21640
+ "Use --template full or --template blank."
21641
+ );
21642
+ const templateServices = template === "starter" || template === "full";
21643
+ if (!asker && requested.length === 0 && !templateServices) {
21402
21644
  throw new InitError(
21403
21645
  "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>`."
21646
+ "Pass --template full, or --services <portal,gateway,identity,plans> (and --template blank|starter|full), or run `apitogo init` and `apitogo add <service>`."
21405
21647
  );
21406
21648
  }
21649
+ const existing = await isConfigV2(base);
21650
+ let name;
21651
+ if (!existing) {
21652
+ name = argv.name?.trim() || (asker ? await asker.text("Project name", {
21653
+ default: await defaultName(base)
21654
+ }) : await defaultName(base));
21655
+ const folder = projectFolderName(name);
21656
+ if (argv.dir === void 0 && folder !== path70.basename(base))
21657
+ dir = path70.join(base, folder);
21658
+ }
21407
21659
  if (await (deps.hasUncommittedChanges ?? gitHasUncommittedChanges)(dir))
21408
21660
  say(
21409
21661
  "This folder has uncommitted changes. Commit first so you can undo this easily."
21410
21662
  );
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));
21663
+ if (name !== void 0 && !await isConfigV2(dir)) {
21415
21664
  const plan2 = await planInit({ dir, name });
21416
21665
  for (const line2 of formatInitPlan(plan2)) say(line2);
21417
21666
  commands.push(`apitogo init --name ${JSON.stringify(name)}`);
@@ -21431,7 +21680,9 @@ async function createProject(argv, deps = {}) {
21431
21680
  return;
21432
21681
  }
21433
21682
  await applyInitPlan(plan2);
21434
- say(`Created ${APITOGO_CONFIG_FILENAME}.`);
21683
+ say(
21684
+ dir === base ? `Created ${APITOGO_CONFIG_FILENAME}.` : `Created ${path70.relative(base, dir)}${path70.sep}${APITOGO_CONFIG_FILENAME}.`
21685
+ );
21435
21686
  } else {
21436
21687
  say(`${APITOGO_CONFIG_FILENAME} exists: continuing from it.`);
21437
21688
  }
@@ -21440,17 +21691,17 @@ async function createProject(argv, deps = {}) {
21440
21691
  "What do you want to start from?",
21441
21692
  [
21442
21693
  {
21443
- value: "starter",
21444
- label: "Starter: every service with examples (home page, sample plans, API reference)"
21694
+ value: "full",
21695
+ label: "Full: every service with examples and a small Node.js backend, ready to deploy"
21445
21696
  },
21446
21697
  { value: "blank", label: "Blank: choose the services one by one" }
21447
21698
  ]
21448
21699
  );
21449
- if (template === "starter" && requested.length === 0)
21700
+ if ((template === "starter" || template === "full") && requested.length === 0)
21450
21701
  requested = STARTER_SERVICES;
21451
21702
  const stepArgs = (service) => ({
21452
21703
  service,
21453
- dir: argv.dir,
21704
+ dir,
21454
21705
  template,
21455
21706
  dryRun: argv.dryRun,
21456
21707
  install: argv.install,
@@ -21531,8 +21782,7 @@ var create_default = {
21531
21782
  command: "create",
21532
21783
  builder: (yargs2) => yargs2.option("dir", {
21533
21784
  type: "string",
21534
- describe: "Project directory",
21535
- default: ".",
21785
+ describe: "Project directory (default: a new folder named after the project)",
21536
21786
  normalize: true,
21537
21787
  hidden: true
21538
21788
  }).option("name", {
@@ -21543,8 +21793,8 @@ var create_default = {
21543
21793
  describe: "Skip the menu and add these services, comma separated (portal, gateway, identity, plans)"
21544
21794
  }).option("template", {
21545
21795
  type: "string",
21546
- choices: ["blank", "starter"],
21547
- describe: "Template for the services added (default: ask)"
21796
+ // `starter` is still accepted for older scripts but no longer offered: `full` replaces it.
21797
+ describe: "Template: full (every service, a sample Node.js backend, ready to deploy) or blank. Default: ask"
21548
21798
  }).option("input", {
21549
21799
  type: "boolean",
21550
21800
  default: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.2.42",
3
+ "version": "0.2.43",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",