@lukoweb/apitogo 0.1.81 → 0.1.83

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
@@ -5633,7 +5633,7 @@ import {
5633
5633
  // package.json
5634
5634
  var package_default = {
5635
5635
  name: "@lukoweb/apitogo",
5636
- version: "0.1.81",
5636
+ version: "0.1.83",
5637
5637
  type: "module",
5638
5638
  sideEffects: [
5639
5639
  "**/*.css",
@@ -7454,13 +7454,13 @@ var generateCode = (schema2, filePath) => {
7454
7454
 
7455
7455
  // src/vite/api/SchemaManager.ts
7456
7456
  var FALLBACK_VERSION = "default";
7457
- var parseInputString = (input2) => {
7458
- const idx = input2.indexOf("?");
7457
+ var parseInputString = (input3) => {
7458
+ const idx = input3.indexOf("?");
7459
7459
  if (idx === -1)
7460
- return { filePath: input2, params: {} };
7460
+ return { filePath: input3, params: {} };
7461
7461
  return {
7462
- filePath: input2.slice(0, idx),
7463
- params: Object.fromEntries(new URLSearchParams(input2.slice(idx)))
7462
+ filePath: input3.slice(0, idx),
7463
+ params: Object.fromEntries(new URLSearchParams(input3.slice(idx)))
7464
7464
  };
7465
7465
  };
7466
7466
  var canonicalParams = (params) => Object.entries(params).sort(([a], [b]) => a.localeCompare(b)).map(([k, v]) => `${k}=${v}`).join("&");
@@ -7496,8 +7496,8 @@ var SchemaManager = class {
7496
7496
  ...processors
7497
7497
  ];
7498
7498
  }
7499
- getPathForFile = (input2, params) => {
7500
- const filePath = path11.resolve(this.config.__meta.rootDir, input2);
7499
+ getPathForFile = (input3, params) => {
7500
+ const filePath = path11.resolve(this.config.__meta.rootDir, input3);
7501
7501
  const apis = ensureArray(this.config.apis ?? []);
7502
7502
  for (const apiConfig of apis) {
7503
7503
  if (!apiConfig || apiConfig.type !== "file" || !apiConfig.path) continue;
@@ -7507,12 +7507,12 @@ var SchemaManager = class {
7507
7507
  if (match) return apiConfig.path;
7508
7508
  }
7509
7509
  };
7510
- processSchema = async (input2) => {
7511
- const filePath = path11.resolve(this.config.__meta.rootDir, input2.input);
7512
- const params = input2.params;
7513
- const configuredPath = this.getPathForFile(input2.input, params);
7510
+ processSchema = async (input3) => {
7511
+ const filePath = path11.resolve(this.config.__meta.rootDir, input3.input);
7512
+ const params = input3.params;
7513
+ const configuredPath = this.getPathForFile(input3.input, params);
7514
7514
  if (!configuredPath) {
7515
- console.warn(`No path found for file ${input2.input}`);
7515
+ console.warn(`No path found for file ${input3.input}`);
7516
7516
  return;
7517
7517
  }
7518
7518
  const parser = new $RefParser2();
@@ -7619,20 +7619,20 @@ var SchemaManager = class {
7619
7619
  if (apiConfig.type !== "file" || !apiConfig.path) continue;
7620
7620
  const inputs = normalizeInputs(apiConfig.input);
7621
7621
  if (inputs.length === 0) throw new Error("No schema found");
7622
- this.processedSchemas[apiConfig.path] = inputs.map((input2) => ({
7622
+ this.processedSchemas[apiConfig.path] = inputs.map((input3) => ({
7623
7623
  schema: {},
7624
7624
  version: "",
7625
- path: input2.path ?? "",
7626
- label: input2.label,
7627
- inputPath: path11.resolve(this.config.__meta.rootDir, input2.input),
7628
- params: input2.params,
7625
+ path: input3.path ?? "",
7626
+ label: input3.label,
7627
+ inputPath: path11.resolve(this.config.__meta.rootDir, input3.input),
7628
+ params: input3.params,
7629
7629
  importKey: "",
7630
7630
  downloadUrl: "",
7631
7631
  processedJsonPath: "",
7632
7632
  processedTime: 0
7633
7633
  }));
7634
7634
  const results = await Promise.allSettled(
7635
- inputs.map((input2) => this.processSchema(input2))
7635
+ inputs.map((input3) => this.processSchema(input3))
7636
7636
  );
7637
7637
  const errors = results.flatMap(
7638
7638
  (r) => r.status === "rejected" ? r.reason : []
@@ -7711,7 +7711,7 @@ import { readFile } from "node:fs/promises";
7711
7711
  import matter from "gray-matter";
7712
7712
  import { parse, stringify as stringify2 } from "yaml";
7713
7713
  var yaml = {
7714
- parse: (input2) => parse(input2) ?? {},
7714
+ parse: (input3) => parse(input3) ?? {},
7715
7715
  stringify: (obj) => stringify2(obj)
7716
7716
  };
7717
7717
  var readFrontmatter = async (filePath) => {
@@ -10520,6 +10520,7 @@ var build_default = {
10520
10520
  import { constants as constants2 } from "node:fs";
10521
10521
  import { access as access3, mkdir as mkdir7, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
10522
10522
  import path36 from "node:path";
10523
+ import { stdin as input2 } from "node:process";
10523
10524
  import { glob as glob5 } from "glob";
10524
10525
 
10525
10526
  // src/cli/make-config/interactive.ts
@@ -10783,12 +10784,12 @@ function mergeDocumentationNavigation(navigation, docRoutePaths, options) {
10783
10784
  }
10784
10785
  return [documentation, ...rest];
10785
10786
  }
10786
- function applyOpenApiToJsonConfig(config2, input2) {
10787
+ function applyOpenApiToJsonConfig(config2, input3) {
10787
10788
  const next = { ...config2 };
10788
10789
  next.apis = [
10789
10790
  {
10790
10791
  type: "file",
10791
- input: input2,
10792
+ input: input3,
10792
10793
  path: "api"
10793
10794
  }
10794
10795
  ];
@@ -10831,15 +10832,15 @@ function buildLimitsJson(rateLimitCalls, monthlyQuota) {
10831
10832
  quota: { calls: monthlyQuota, renewalPeriodDays: 30 }
10832
10833
  });
10833
10834
  }
10834
- function buildPlanItemFromWizard(input2) {
10835
- const sku = input2.sku ?? slugifyPlanSku(input2.displayName);
10835
+ function buildPlanItemFromWizard(input3) {
10836
+ const sku = input3.sku ?? slugifyPlanSku(input3.displayName);
10836
10837
  const item = {
10837
10838
  sku,
10838
- displayName: input2.displayName,
10839
- priceAmount: input2.priceAmount,
10840
- limitsJson: buildLimitsJson(input2.rateLimitCalls, input2.monthlyQuota)
10839
+ displayName: input3.displayName,
10840
+ priceAmount: input3.priceAmount,
10841
+ limitsJson: buildLimitsJson(input3.rateLimitCalls, input3.monthlyQuota)
10841
10842
  };
10842
- if (input2.priceAmount > 0) {
10843
+ if (input3.priceAmount > 0) {
10843
10844
  item.priceCurrency = "usd";
10844
10845
  item.billingPeriod = "month";
10845
10846
  }
@@ -10870,6 +10871,45 @@ function applyPlansToJsonConfig(config2, planItems) {
10870
10871
  modules.userPanel = userPanel;
10871
10872
  return { ...config2, modules };
10872
10873
  }
10874
+ var BACKEND_RUNTIMES = ["dotnet", "nodejs", "python"];
10875
+ var RUNTIME_VERSIONS_BY_RUNTIME = {
10876
+ nodejs: ["NODE_22_LTS", "NODE_24_LTS"],
10877
+ python: [
10878
+ "PYTHON_310",
10879
+ "PYTHON_311",
10880
+ "PYTHON_312",
10881
+ "PYTHON_313",
10882
+ "PYTHON_314"
10883
+ ],
10884
+ dotnet: ["DOTNET_8", "DOTNET_9", "DOTNET_10"]
10885
+ };
10886
+ function parseBackendRuntime(raw) {
10887
+ const value = raw.trim().toLowerCase();
10888
+ if (value === "node" || value === "nodejs") {
10889
+ return "nodejs";
10890
+ }
10891
+ if (value === "dotnet" || value === "python") {
10892
+ return value;
10893
+ }
10894
+ return null;
10895
+ }
10896
+ function parseRuntimeVersion(runtime, raw) {
10897
+ const value = raw.trim().toUpperCase();
10898
+ const allowed = RUNTIME_VERSIONS_BY_RUNTIME[runtime];
10899
+ return allowed.includes(value) ? value : null;
10900
+ }
10901
+ function applyBackendToJsonConfig(config2, backend) {
10902
+ const existing = isPlainObject2(config2.backend) ? config2.backend : {};
10903
+ return {
10904
+ ...config2,
10905
+ backend: {
10906
+ ...existing,
10907
+ sourceDirectory: backend.sourceDirectory,
10908
+ runtime: backend.runtime,
10909
+ runtimeVersion: backend.runtimeVersion
10910
+ }
10911
+ };
10912
+ }
10873
10913
  function toRoutePath(relativeFile, pagesDir) {
10874
10914
  const relativeNoExt = relativeFile.replace(/\.(mdx?|tsx|jsx)$/i, "");
10875
10915
  const escaped = pagesDir.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -10897,6 +10937,17 @@ export default buildApitogoConfig({
10897
10937
  function isThinConfigWrapper(source) {
10898
10938
  return /export\s+default\s+buildApitogoConfig\s*\(/.test(source);
10899
10939
  }
10940
+ function isManagedConfigWrapper(source) {
10941
+ if (isThinConfigWrapper(source)) {
10942
+ return true;
10943
+ }
10944
+ return /from\s+["']\.\/apitogo\.config\.json["']/.test(source) && /buildApitogoConfig\s*\(/.test(source) && /type:\s*["']custom-page["']/.test(source);
10945
+ }
10946
+ function labelFromRoutePath(routePath) {
10947
+ const slug = routePath.replace(/^\/+/, "");
10948
+ const leaf = slug.split("/").filter(Boolean).pop() ?? slug;
10949
+ return titleFromDirectoryName(leaf);
10950
+ }
10900
10951
  function buildThinWrapperWithCustomPages(tsxRoutePaths) {
10901
10952
  if (tsxRoutePaths.length === 0) {
10902
10953
  return THIN_WRAPPER_SOURCE;
@@ -10909,9 +10960,11 @@ function buildThinWrapperWithCustomPages(tsxRoutePaths) {
10909
10960
  const customPages = tsxRoutePaths.map((routePath) => {
10910
10961
  const slug = routePath.replace(/^\//, "");
10911
10962
  const name = pageComponentName(slug);
10963
+ const label = labelFromRoutePath(routePath);
10912
10964
  return ` {
10913
10965
  type: "custom-page",
10914
10966
  path: ${JSON.stringify(routePath)},
10967
+ label: ${JSON.stringify(label)},
10915
10968
  element: <${name} />,
10916
10969
  },`;
10917
10970
  }).join("\n");
@@ -10933,13 +10986,17 @@ export default {
10933
10986
  label: ${JSON.stringify(DOCUMENTATION_NAV_LABEL)},
10934
10987
  items: [
10935
10988
  ${customPages}
10936
- ...(((config.navigation ?? []).find(
10989
+ ...((((config.navigation ?? []).find(
10937
10990
  (item) =>
10938
10991
  typeof item === "object" &&
10939
10992
  item !== null &&
10940
10993
  "label" in item &&
10941
10994
  item.label === ${JSON.stringify(DOCUMENTATION_NAV_LABEL)},
10942
- ) as { items?: unknown[] } | undefined)?.items ?? []),
10995
+ ) as { items?: unknown[] } | undefined)?.items ?? []).filter(
10996
+ (item) =>
10997
+ typeof item !== "string" ||
10998
+ !${JSON.stringify(tsxRoutePaths)}.includes(item),
10999
+ )),
10943
11000
  ],
10944
11001
  },
10945
11002
  ...(config.navigation ?? []).filter(
@@ -11022,45 +11079,95 @@ var parseLoopChoice = (raw) => {
11022
11079
  if (!value || value === "done") {
11023
11080
  return "done";
11024
11081
  }
11025
- if (value === "openapi" || value === "page" || value === "plan") {
11082
+ if (value === "backend" || value === "openapi" || value === "page" || value === "plan") {
11026
11083
  return value;
11027
11084
  }
11028
11085
  return null;
11029
11086
  };
11030
- async function promptMakeConfigInteractive(defaults) {
11087
+ async function promptMakeConfigInteractive(options) {
11031
11088
  const rl = createInterface({ input, output });
11032
- const customPages = [];
11033
- const planItems = [];
11034
11089
  let hasPaidPlans = false;
11035
- let openApiPath;
11036
11090
  try {
11037
- const title = await ask(rl, "Site title", defaults.title);
11038
11091
  while (true) {
11039
11092
  const choiceRaw = await ask(
11040
11093
  rl,
11041
- "Add OpenAPI, a custom page, or a plan? (openapi / page / plan / done)",
11094
+ "What do you want to configure? (backend / openapi / page / plan / done)",
11042
11095
  "done"
11043
11096
  );
11044
11097
  const choice = parseLoopChoice(choiceRaw);
11045
11098
  if (!choice) {
11046
- printDiagnosticsToConsole("Choose openapi, page, plan, or done.");
11099
+ printDiagnosticsToConsole(
11100
+ "Choose backend, openapi, page, plan, or done."
11101
+ );
11047
11102
  continue;
11048
11103
  }
11049
11104
  if (choice === "done") {
11050
11105
  break;
11051
11106
  }
11107
+ if (choice === "backend") {
11108
+ const sourceDirectory = await ask(
11109
+ rl,
11110
+ "Backend source directory (relative to portal)",
11111
+ ".."
11112
+ );
11113
+ if (!sourceDirectory.trim()) {
11114
+ printDiagnosticsToConsole("Backend source directory is required.");
11115
+ continue;
11116
+ }
11117
+ let runtime = null;
11118
+ while (!runtime) {
11119
+ const runtimeRaw = await ask(
11120
+ rl,
11121
+ `Runtime (${BACKEND_RUNTIMES.join(" / ")})`,
11122
+ "nodejs"
11123
+ );
11124
+ runtime = parseBackendRuntime(runtimeRaw);
11125
+ if (!runtime) {
11126
+ printDiagnosticsToConsole(
11127
+ `Choose one of: ${BACKEND_RUNTIMES.join(", ")} (node \u2192 nodejs).`
11128
+ );
11129
+ }
11130
+ }
11131
+ const versions = RUNTIME_VERSIONS_BY_RUNTIME[runtime];
11132
+ let runtimeVersion = null;
11133
+ while (!runtimeVersion) {
11134
+ const versionRaw = await ask(
11135
+ rl,
11136
+ `Runtime version (${versions.join(" / ")})`,
11137
+ versions[0]
11138
+ );
11139
+ runtimeVersion = parseRuntimeVersion(runtime, versionRaw);
11140
+ if (!runtimeVersion) {
11141
+ printDiagnosticsToConsole(
11142
+ `Choose one of: ${versions.join(", ")}.`
11143
+ );
11144
+ }
11145
+ }
11146
+ const backend = {
11147
+ sourceDirectory: sourceDirectory.trim(),
11148
+ runtime,
11149
+ runtimeVersion
11150
+ };
11151
+ await options.onApplied({ kind: "backend", backend });
11152
+ printDiagnosticsToConsole(
11153
+ `Backend saved: ${backend.runtime} ${backend.runtimeVersion} (sourceDirectory: ${backend.sourceDirectory}).`
11154
+ );
11155
+ continue;
11156
+ }
11052
11157
  if (choice === "openapi") {
11053
11158
  const entered = await ask(
11054
11159
  rl,
11055
11160
  "OpenAPI file path (leave empty to skip)",
11056
- defaults.discoveredOpenApi ?? ""
11161
+ options.discoveredOpenApi ?? ""
11057
11162
  );
11058
- openApiPath = entered ? entered : null;
11059
- if (openApiPath) {
11060
- printDiagnosticsToConsole(
11061
- `OpenAPI will be linked as a relative path in apitogo.config.json.`
11062
- );
11163
+ if (!entered) {
11164
+ printDiagnosticsToConsole("OpenAPI skipped.");
11165
+ continue;
11063
11166
  }
11167
+ await options.onApplied({ kind: "openapi", openApiPath: entered });
11168
+ printDiagnosticsToConsole(
11169
+ `OpenAPI linked as a relative path in apitogo.config.json.`
11170
+ );
11064
11171
  continue;
11065
11172
  }
11066
11173
  if (choice === "page") {
@@ -11079,14 +11186,14 @@ async function promptMakeConfigInteractive(defaults) {
11079
11186
  continue;
11080
11187
  }
11081
11188
  const routePath = `/${slug}`;
11082
- const relativePath = path34.posix.join(defaults.pagesDir, `${slug}.tsx`);
11189
+ const relativePath = path34.posix.join(options.pagesDir, `${slug}.tsx`);
11083
11190
  const page = {
11084
11191
  slug,
11085
11192
  title: pageTitle.trim(),
11086
11193
  routePath,
11087
11194
  relativePath
11088
11195
  };
11089
- customPages.push(page);
11196
+ await options.onApplied({ kind: "page", page });
11090
11197
  printPageAddedHint(relativePath, routePath);
11091
11198
  continue;
11092
11199
  }
@@ -11116,25 +11223,15 @@ async function promptMakeConfigInteractive(defaults) {
11116
11223
  rateLimitCalls,
11117
11224
  monthlyQuota
11118
11225
  });
11119
- planItems.push(planItem);
11120
- if (priceAmount > 0) {
11226
+ const paid = priceAmount > 0;
11227
+ if (paid) {
11121
11228
  hasPaidPlans = true;
11122
11229
  printPaidPlanPlatformWarning();
11123
11230
  }
11231
+ await options.onApplied({ kind: "plan", planItem, paid });
11124
11232
  printPlanAddedHint(displayName.trim());
11125
11233
  }
11126
- return {
11127
- title,
11128
- site: { title },
11129
- authentication: {
11130
- enabled: true,
11131
- type: "dev-portal"
11132
- },
11133
- openApiPath,
11134
- customPages,
11135
- planItems,
11136
- hasPaidPlans
11137
- };
11234
+ return { hasPaidPlans };
11138
11235
  } finally {
11139
11236
  rl.close();
11140
11237
  }
@@ -11414,11 +11511,66 @@ var readJsonConfig = async (filePath) => {
11414
11511
  }
11415
11512
  return parsed;
11416
11513
  };
11514
+ function shouldRunInteractive(argv) {
11515
+ if (argv.interactive === true) {
11516
+ return true;
11517
+ }
11518
+ if (argv.interactive === false) {
11519
+ return false;
11520
+ }
11521
+ const hasPatchFlags = argv.title !== void 0 || argv.site !== void 0 || argv.theme !== void 0 || argv.authentication !== void 0 || argv.force === true;
11522
+ if (hasPatchFlags) {
11523
+ return false;
11524
+ }
11525
+ return input2.isTTY === true;
11526
+ }
11527
+ async function persistConfig(config2, ctx) {
11528
+ const pageFiles = await glob5(`${ctx.pagesDir}/**/*.{md,mdx,tsx,jsx}`, {
11529
+ cwd: ctx.dir,
11530
+ ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
11531
+ posix: true
11532
+ });
11533
+ const markdownFiles = pageFiles.filter((file) => /\.mdx?$/i.test(file));
11534
+ const tsxFiles = pageFiles.filter((file) => /\.(tsx|jsx)$/i.test(file));
11535
+ const markdownRoutes = markdownFiles.map(
11536
+ (file) => toRoutePath(file, ctx.pagesDir)
11537
+ );
11538
+ const tsxRoutes = tsxFiles.map((file) => toRoutePath(file, ctx.pagesDir));
11539
+ const docRoutes = [.../* @__PURE__ */ new Set([...markdownRoutes, ...tsxRoutes])];
11540
+ if (docRoutes.length === 0) {
11541
+ printDiagnosticsToConsole(
11542
+ `No docs pages under '${ctx.pagesDir}/' \u2014 skipping documentation nav scan.`
11543
+ );
11544
+ }
11545
+ const next = {
11546
+ ...config2,
11547
+ navigation: mergeDocumentationNavigation(
11548
+ config2.navigation,
11549
+ docRoutes,
11550
+ { force: ctx.force }
11551
+ )
11552
+ };
11553
+ await writeFile7(ctx.jsonPath, `${JSON.stringify(next, null, 2)}
11554
+ `, "utf8");
11555
+ const tsxPath = path36.join(ctx.dir, "apitogo.config.tsx");
11556
+ if (await fileExists3(tsxPath) && tsxRoutes.length > 0) {
11557
+ const tsxSource = await readFile5(tsxPath, "utf8");
11558
+ if (isManagedConfigWrapper(tsxSource)) {
11559
+ await writeFile7(
11560
+ tsxPath,
11561
+ buildThinWrapperWithCustomPages(tsxRoutes),
11562
+ "utf8"
11563
+ );
11564
+ }
11565
+ }
11566
+ return next;
11567
+ }
11417
11568
  async function makeConfig(argv) {
11418
11569
  const dir = path36.resolve(process.cwd(), argv.dir);
11419
11570
  const pagesDir = toPosixPath2(
11420
11571
  argv.pagesDir.replace(/^[./]+/, "").replace(/\/+$/, "")
11421
11572
  );
11573
+ const interactive = shouldRunInteractive(argv);
11422
11574
  try {
11423
11575
  const discoveredOpenApi = await findOpenApiSpecPath(dir);
11424
11576
  const createdPackageJson = await ensurePackageJson(dir);
@@ -11455,79 +11607,71 @@ async function makeConfig(argv) {
11455
11607
  authentication: parseJsonObjectFlag(argv.authentication, "authentication"),
11456
11608
  title: argv.title
11457
11609
  };
11458
- let openApiOverride;
11610
+ const persistCtx = {
11611
+ dir,
11612
+ pagesDir,
11613
+ jsonPath,
11614
+ force: argv.force === true
11615
+ };
11459
11616
  let interactiveHasPaidPlans = false;
11460
- if (argv.interactive) {
11461
- const site = isPlainObject2(config2.site) ? config2.site : {};
11617
+ if (interactive) {
11618
+ config2 = await persistConfig(config2, persistCtx);
11462
11619
  const prompted = await promptMakeConfigInteractive({
11463
- title: flagPatches.title ?? (typeof site.title === "string" ? site.title : titleFromDirectoryName(path36.basename(dir))),
11464
11620
  discoveredOpenApi,
11465
- pagesDir
11621
+ pagesDir,
11622
+ onApplied: async (action2) => {
11623
+ if (action2.kind === "backend") {
11624
+ config2 = applyBackendToJsonConfig(config2, action2.backend);
11625
+ config2 = await persistConfig(config2, persistCtx);
11626
+ return;
11627
+ }
11628
+ if (action2.kind === "openapi") {
11629
+ const openApiInput = toRelativeOpenApiInput(
11630
+ dir,
11631
+ action2.openApiPath
11632
+ );
11633
+ config2 = applyOpenApiToJsonConfig(config2, openApiInput);
11634
+ config2 = await persistConfig(config2, persistCtx);
11635
+ return;
11636
+ }
11637
+ if (action2.kind === "page") {
11638
+ const pagePath = path36.join(dir, action2.page.relativePath);
11639
+ await mkdir7(path36.dirname(pagePath), { recursive: true });
11640
+ await writeFile7(
11641
+ pagePath,
11642
+ buildStarterDocsPageTsx(action2.page.slug, action2.page.title),
11643
+ "utf8"
11644
+ );
11645
+ config2 = await persistConfig(config2, persistCtx);
11646
+ return;
11647
+ }
11648
+ if (action2.kind === "plan") {
11649
+ config2 = applyPlansToJsonConfig(config2, [action2.planItem]);
11650
+ config2 = await persistConfig(config2, persistCtx);
11651
+ }
11652
+ }
11466
11653
  });
11467
- openApiOverride = prompted.openApiPath;
11468
11654
  interactiveHasPaidPlans = prompted.hasPaidPlans;
11469
- config2 = applyConfigPatches(config2, prompted);
11470
- config2 = applyPlansToJsonConfig(config2, prompted.planItems);
11471
- for (const page of prompted.customPages) {
11472
- const pagePath = path36.join(dir, page.relativePath);
11473
- await mkdir7(path36.dirname(pagePath), { recursive: true });
11474
- await writeFile7(
11475
- pagePath,
11476
- buildStarterDocsPageTsx(page.slug, page.title),
11477
- "utf8"
11478
- );
11479
- }
11480
- }
11481
- config2 = applyConfigPatches(config2, flagPatches);
11482
- const pageFiles = await glob5(`${pagesDir}/**/*.{md,mdx,tsx,jsx}`, {
11483
- cwd: dir,
11484
- ignore: ["**/node_modules/**", "**/.git/**", "**/dist/**"],
11485
- posix: true
11486
- });
11487
- const markdownFiles = pageFiles.filter((file) => /\.mdx?$/i.test(file));
11488
- const tsxFiles = pageFiles.filter((file) => /\.(tsx|jsx)$/i.test(file));
11489
- const markdownRoutes = markdownFiles.map(
11490
- (file) => toRoutePath(file, pagesDir)
11491
- );
11492
- const tsxRoutes = tsxFiles.map((file) => toRoutePath(file, pagesDir));
11493
- if (markdownRoutes.length === 0 && tsxRoutes.length === 0) {
11494
- printDiagnosticsToConsole(
11495
- `No docs pages under '${pagesDir}/' \u2014 skipping documentation nav scan.`
11496
- );
11497
- }
11498
- config2.navigation = mergeDocumentationNavigation(
11499
- config2.navigation,
11500
- markdownRoutes,
11501
- { force: argv.force === true }
11502
- );
11503
- const openApiRel = openApiOverride === void 0 ? discoveredOpenApi : openApiOverride;
11504
- if (openApiRel) {
11505
- const openApiInput = argv.interactive ? toRelativeOpenApiInput(dir, openApiRel) : openapiInputForConfig(openApiRel);
11506
- config2 = applyOpenApiToJsonConfig(config2, openApiInput);
11507
- }
11508
- await writeFile7(jsonPath, `${JSON.stringify(config2, null, 2)}
11509
- `, "utf8");
11510
- const tsxPath = path36.join(dir, "apitogo.config.tsx");
11511
- if (await fileExists3(tsxPath) && tsxRoutes.length > 0) {
11512
- const tsxSource = await readFile5(tsxPath, "utf8");
11513
- if (isThinConfigWrapper(tsxSource)) {
11514
- await writeFile7(
11515
- tsxPath,
11516
- buildThinWrapperWithCustomPages(tsxRoutes),
11517
- "utf8"
11655
+ } else {
11656
+ config2 = applyConfigPatches(config2, flagPatches);
11657
+ if (discoveredOpenApi) {
11658
+ config2 = applyOpenApiToJsonConfig(
11659
+ config2,
11660
+ openapiInputForConfig(discoveredOpenApi)
11518
11661
  );
11519
11662
  }
11663
+ config2 = await persistConfig(config2, persistCtx);
11520
11664
  }
11521
11665
  const action = createdJson ? "Initialized" : "Updated";
11522
11666
  printResultToConsole(
11523
- `${action} ${APITOGO_JSON_FILENAME} (site/auth/navigation/OpenAPI/plans as applicable; redirects unchanged).`
11667
+ `${action} ${APITOGO_JSON_FILENAME}.`
11524
11668
  );
11525
11669
  if (createdPackageJson) {
11526
11670
  printDiagnosticsToConsole(
11527
11671
  "Next: run npm install (or pnpm install / yarn) in this directory, then npm run dev."
11528
11672
  );
11529
11673
  }
11530
- if (argv.interactive) {
11674
+ if (interactive) {
11531
11675
  printInteractiveWrapUp(interactiveHasPaidPlans);
11532
11676
  }
11533
11677
  } catch (error) {
@@ -11539,7 +11683,7 @@ async function makeConfig(argv) {
11539
11683
 
11540
11684
  // src/cli/cmds/config.ts
11541
11685
  var config_default = {
11542
- desc: "Create or update apitogo.config.json from pages, OpenAPI, and optional site/theme/auth flags",
11686
+ desc: "Create or update apitogo.config.json (interactive on TTY: backend, OpenAPI, pages, plans)",
11543
11687
  command: "config",
11544
11688
  aliases: ["make-config"],
11545
11689
  builder: (yargs2) => yargs2.option("dir", {
@@ -11555,8 +11699,7 @@ var config_default = {
11555
11699
  }).option("interactive", {
11556
11700
  alias: "i",
11557
11701
  type: "boolean",
11558
- describe: "Interactive wizard: site title, OpenAPI path, custom pages, and gateway plans",
11559
- default: false
11702
+ describe: "Interactive wizard (default on TTY). Apply backend, OpenAPI, pages, and plans immediately. Use --no-interactive for CI/flags-only."
11560
11703
  }).option("site", {
11561
11704
  type: "string",
11562
11705
  describe: "JSON object merged into site"
@@ -12005,10 +12148,36 @@ var dev_default = {
12005
12148
  };
12006
12149
 
12007
12150
  // src/cli/init/handler.ts
12151
+ import { mkdir as mkdir8, readdir, stat as stat4 } from "node:fs/promises";
12008
12152
  import path40 from "node:path";
12153
+ var IGNORED_TARGET_ENTRIES = /* @__PURE__ */ new Set([".DS_Store", "Thumbs.db"]);
12154
+ async function ensureEmptyTargetDir(dir) {
12155
+ try {
12156
+ const existing = await stat4(dir);
12157
+ if (!existing.isDirectory()) {
12158
+ throw new Error(`${dir} exists and is not a directory.`);
12159
+ }
12160
+ } catch (err) {
12161
+ const code = err.code;
12162
+ if (code !== "ENOENT") {
12163
+ throw err;
12164
+ }
12165
+ await mkdir8(dir, { recursive: true });
12166
+ return;
12167
+ }
12168
+ const entries = (await readdir(dir)).filter(
12169
+ (name) => !IGNORED_TARGET_ENTRIES.has(name)
12170
+ );
12171
+ if (entries.length > 0) {
12172
+ throw new Error(
12173
+ `Directory '${dir}' is not empty. Choose a new directory or remove existing files.`
12174
+ );
12175
+ }
12176
+ }
12009
12177
  async function init2(argv) {
12010
12178
  const dir = path40.resolve(process.cwd(), argv.dir ?? ".");
12011
12179
  try {
12180
+ await ensureEmptyTargetDir(dir);
12012
12181
  const createdPackageJson = await ensurePackageJson(dir);
12013
12182
  const createdTsconfig = await ensureTsconfigJson(dir);
12014
12183
  await ensurePortalStubFiles(dir);
@@ -12023,10 +12192,11 @@ async function init2(argv) {
12023
12192
  dir,
12024
12193
  pagesDir: "pages",
12025
12194
  title: argv.name,
12026
- force: false
12195
+ force: false,
12196
+ interactive: false
12027
12197
  });
12028
12198
  printResultToConsole(
12029
- `Portal scaffolded in ${dir}. Next: apitogo config -i, apitogo dev, then apitogo apply.`
12199
+ `Portal scaffolded in ${dir}. Next: apitogo config, apitogo dev, then apitogo apply.`
12030
12200
  );
12031
12201
  } catch (error) {
12032
12202
  await printCriticalFailureToConsoleAndExit(
@@ -12,7 +12,7 @@ Commands are exposed by the **`apitogo`** binary (`@lukoweb/apitogo`). Platform
12
12
  | Command | Role |
13
13
  | ------- | ---- |
14
14
  | `init` | Scaffold portal locally (no platform calls) |
15
- | `config` / `make-config` | Write or update `apitogo.config.json` (interactive: OpenAPI, pages, plans) |
15
+ | `config` / `make-config` | Write or update `apitogo.config.json` (interactive: backend, OpenAPI, pages, plans) |
16
16
  | `dev` | Local preview server |
17
17
  | `build` | Production build (`--preview` to serve locally) |
18
18
  | `apply` | Publish config to platform (apihub + portal) |
@@ -24,7 +24,7 @@ Commands are exposed by the **`apitogo`** binary (`@lukoweb/apitogo`). Platform
24
24
  | Command | Role |
25
25
  | ------- | ---- |
26
26
  | `init [dir]` | Create `package.json`, stubs, and initial `apitogo.config.json` |
27
- | `config` | Merge pages nav, wire OpenAPI, patch site/theme/auth; `-i` wizard for OpenAPI, custom pages, plans |
27
+ | `config` | Interactive on TTY: backend, OpenAPI, pages, plans (apply each immediately); flags for site/theme/auth |
28
28
 
29
29
  `npm create @lukoweb/apitogo@latest` is an interactive alternative to `init` for new folders.
30
30
 
@@ -14,7 +14,7 @@ local files, preview with `apitogo dev`, and publish to APItoGo hosting with `ap
14
14
  ## What you get
15
15
 
16
16
  - Scaffold a local portal site (`npm create @lukoweb/apitogo@latest` or `apitogo init`)
17
- - Shape OpenAPI, plans, docs pages, and site title via `apitogo config -i` and file edits
17
+ - Shape backend, OpenAPI, plans, and docs pages via `apitogo config` and file edits
18
18
  - Preview locally (`apitogo dev`)
19
19
  - Publish to APItoGo hosting (`apitogo apply`) with platform plans **Free**, **Starter**, or
20
20
  **Professional**
@@ -47,10 +47,11 @@ Common CLI shape tasks:
47
47
 
48
48
  | Change | How |
49
49
  | ----------------------------------- | ------------------------------------------------ |
50
- | OpenAPI + API Reference nav | `apitogo config` or `apitogo config -i` (openapi) |
51
- | Custom docs pages | `apitogo config -i` (page) or add under `pages/` |
52
- | Gateway / API plans | `apitogo config -i` (plan) or edit manifest |
53
- | Site title | `apitogo config -i` or `--title` |
50
+ | Backend runtime | `apitogo config` (backend) |
51
+ | OpenAPI + API Reference nav | `apitogo config` (openapi) |
52
+ | Custom docs pages | `apitogo config` (page) or add under `pages/` |
53
+ | Gateway / API plans | `apitogo config` (plan) or edit manifest |
54
+ | Site title | `--title` or edit `apitogo.config.json` |
54
55
  | Theme, fonts, branding, projectId | Edit `apitogo.config.json` |
55
56
 
56
57
  Home page content often lives in `landing/LandingPage.tsx`. Site footer can live under
package/docs/cli/setup.md CHANGED
@@ -28,7 +28,7 @@ need a global install — scaffold commands add it to your project and `npm inst
28
28
  ```bash
29
29
  npm create @lukoweb/apitogo@latest
30
30
  cd your-project-name
31
- npx apitogo config -i # interactive: title, OpenAPI, pages, plans
31
+ npx apitogo config # interactive: backend, OpenAPI, pages, plans
32
32
  npm run dev
33
33
  ```
34
34
 
@@ -40,7 +40,7 @@ Non-interactive example:
40
40
  ```bash
41
41
  npm create @lukoweb/apitogo@latest my-portal -- --yes --typescript
42
42
  cd my-portal
43
- npx apitogo config -i
43
+ npx apitogo config
44
44
  ```
45
45
 
46
46
  ### Scaffold into an existing folder
@@ -52,18 +52,18 @@ before other `apitogo` commands:
52
52
  npx @lukoweb/apitogo init ./dev-portal
53
53
  cd ./dev-portal
54
54
  npm install
55
- npx apitogo config -i
55
+ npx apitogo config
56
56
  npm run dev
57
57
  ```
58
58
 
59
- `apitogo init` is local only — it does not call the platform. After `config -i`, run `npm run dev`
59
+ `apitogo init` is local only — it does not call the platform. After `config`, run `npm run dev`
60
60
  to preview, then `apitogo apply` when ready to go live.
61
61
 
62
62
  ### How to run `apitogo`
63
63
 
64
64
  | Situation | Command |
65
65
  | --------- | ------- |
66
- | After `npm create` or `npm install` in the project | `npx apitogo <command>` (e.g. `npx apitogo config -i`) |
66
+ | After `npm create` or `npm install` in the project | `npx apitogo <command>` (e.g. `npx apitogo config`) |
67
67
  | Same, if `node_modules/.bin` is on your PATH | `apitogo <command>` |
68
68
  | One-off, no project yet | `npx @lukoweb/apitogo <command>` |
69
69
  | Optional global install | `npm install -g @lukoweb/apitogo` then `apitogo <command>` anywhere |
package/docs/cli/shape.md CHANGED
@@ -18,7 +18,7 @@ Requires the **`apitogo`** CLI (`@lukoweb/apitogo`). If you have not installed i
18
18
  ```bash
19
19
  npm create @lukoweb/apitogo@latest my-portal -- --yes
20
20
  cd my-portal
21
- npx apitogo config -i # title, OpenAPI, pages, plans
21
+ npx apitogo config # backend, OpenAPI, pages, plans (apply each immediately)
22
22
  npm run dev # http://localhost:3000
23
23
  ```
24
24
 
@@ -28,18 +28,20 @@ Or scaffold into an existing folder (`npm install` is required after `init`):
28
28
  npx @lukoweb/apitogo init ./dev-portal --name "Books API"
29
29
  cd ./dev-portal
30
30
  npm install
31
- npx apitogo config -i
31
+ npx apitogo config
32
32
  npm run dev
33
33
  ```
34
34
 
35
35
  `apitogo init` does **not** call the platform. Run `apitogo apply` when you are ready to go live.
36
36
 
37
- ## Interactive config (`apitogo config -i`)
37
+ ## Interactive config (`apitogo config`)
38
38
 
39
- After **site title**, the wizard loops until you type `done`:
39
+ On a TTY, bare `apitogo config` opens the wizard. Each choice is written immediately; the loop
40
+ continues until you type `done`:
40
41
 
41
42
  | Choice | What it does |
42
43
  | ------ | ------------ |
44
+ | `backend` | Set `backend.sourceDirectory`, `runtime`, and platform `runtimeVersion` |
43
45
  | `openapi` | Link an OpenAPI file by **relative path** in `apitogo.config.json` |
44
46
  | `page` | Create `pages/<slug>.tsx` and wire it in navigation |
45
47
  | `plan` | Add a gateway plan (title, price, rate limit, monthly quota) |
@@ -64,14 +66,14 @@ stubs when missing. Scans `pages/` for navigation and wires OpenAPI when a spec
64
66
  npx apitogo config
65
67
  npx apitogo config --title "Books API"
66
68
  npx apitogo config --theme '{"light":{"primary":"#111111"}}'
67
- npx apitogo config -i
69
+ npx apitogo config --no-interactive
68
70
  ```
69
71
 
70
72
  Common flags:
71
73
 
72
74
  | Flag | Purpose |
73
75
  | ---- | ------- |
74
- | `--interactive` / `-i` | Wizard: title, OpenAPI, custom pages, plans |
76
+ | `--interactive` / `-i` | Force wizard (default on TTY). `--no-interactive` skips it |
75
77
  | `--title` | Set `site.title` |
76
78
  | `--theme`, `--site`, `--authentication` | Deep-merge JSON into config |
77
79
  | `--pages-dir` | Folder to scan (default: `pages`) |
@@ -112,7 +114,7 @@ If you already have `pages/` and an OpenAPI spec:
112
114
  ```bash
113
115
  npx @lukoweb/apitogo init .
114
116
  npm install
115
- npx apitogo config -i
117
+ npx apitogo config
116
118
  npm run dev
117
119
  ```
118
120
 
@@ -30,7 +30,7 @@ Backend API URL (`apitogo apply` writes this to `.env` after the portal backend
30
30
  VITE_APITOGO_AUTHENTICATION_API_BASE_URL=https://your-dev-portal-api.example.com
31
31
  ```
32
32
 
33
- You do not need to enter this URL in `apitogo config -i`. Or set `authentication.apiBaseUrl` in JSON /
33
+ You do not need to enter this URL in `apitogo config`. Or set `authentication.apiBaseUrl` in JSON /
34
34
  `APITOGO_CONFIG_AUTHENTICATION__API_BASE_URL` in `.env` manually.
35
35
 
36
36
  ### Local demo mode
@@ -10,7 +10,8 @@ description:
10
10
  The **`apitogo config`** (alias: `make-config`) command writes **`apitogo.config.json`**. It can scaffold a portal
11
11
  project (package.json, tsconfig, landing and user-panel stubs, thin `apitogo.config.tsx`) when those
12
12
  files are missing, merge **site / theme / authentication** from flags, run an **interactive wizard**
13
- with `-i`, scan `pages/` for navigation, and wire OpenAPI plus an **API Reference** link.
13
+ on a TTY (apply each change immediately), scan `pages/` for navigation, and wire OpenAPI plus an
14
+ **API Reference** link.
14
15
 
15
16
  Landing and user panel are **always enabled** on new configs. **Authentication is enabled by default**
16
17
  on new scaffolds. The command does **not** change `redirects`.
@@ -34,31 +35,37 @@ Run config from the project root with `npx apitogo config` (or `apitogo config`
34
35
  **`apitogo.config.tsx`** wrapper only if that file does not already exist (existing TSX is left
35
36
  alone).
36
37
  3. **Patches site, theme, and auth** from `--site`, `--theme`, `--authentication`, `--title`, or
37
- `--interactive`.
38
+ the interactive wizard.
38
39
  4. **Merges `navigation`** from `.md` / `.mdx` / `.tsx` files under your pages directory into a
39
40
  Documentation category when those files exist. **Missing or empty `pages/` is fine** (common for
40
41
  portal-only sites) — existing navigation is left alone. Other nav items are kept. Use
41
42
  **`--force`** to rebuild Documentation items from disk when pages are present (API Reference is
42
43
  still kept when OpenAPI is wired).
43
- 5. **Wires OpenAPI** when a spec is found (non-interactive) or when you enter a path in `-i`:
44
+ 5. **Wires OpenAPI** when a spec is found (non-interactive) or when you enter a path in the wizard:
44
45
  `apis: [{ type: "file", input, path: "api" }]` and a top-level **API Reference** link to `/api` if
45
46
  that link is missing. Interactive mode stores a **relative file path** in `apis[0].input` (for
46
47
  example `./openapi.yaml` or `../api/openapi.yaml`).
47
- 6. **Interactive only** — can scaffold **custom pages** under `pages/` and append **gateway plans**
48
- to `modules.userPanel.plans.items`.
48
+ 6. **Interactive only** — can set **backend** (`sourceDirectory`, `runtime`, `runtimeVersion`),
49
+ scaffold **custom pages** under `pages/`, and append **gateway plans** to
50
+ `modules.userPanel.plans.items`. Each choice is written immediately.
49
51
 
50
- ## Interactive wizard (`-i`)
52
+ ## Interactive wizard
53
+
54
+ On a TTY, bare `apitogo config` opens the wizard (no `-i` required). `-i` still works as an alias.
55
+ Use `--no-interactive` (or pass flags like `--title`) for CI / non-TTY.
51
56
 
52
57
  ```bash
53
- npx apitogo config -i
58
+ npx apitogo config
54
59
  ```
55
60
 
56
- 1. **Site title**
57
- 2. **Loop** until you type `done`:
61
+ 1. **Loop** until you type `done` (default):
62
+ - **`backend`** — source directory, runtime (`dotnet` / `nodejs` / `python`), platform
63
+ `runtimeVersion` (writes `backend` in `apitogo.config.json` immediately)
58
64
  - **`openapi`** — path to your OpenAPI file (relative link in config; empty skips)
59
- - **`page`** — page title + URL path (writes `pages/<slug>.tsx` and wires navigation)
60
- - **`plan`** — plan title, price (USD/month), rate limit (calls/min), monthly quota
61
- 3. **Wrap-up** — links to [docs.apitogo.com](https://docs.apitogo.com) for further configuration
65
+ - **`page`** — page title + URL path (writes `pages/<slug>.tsx` and wires navigation immediately)
66
+ - **`plan`** — plan title, price (USD/month), rate limit (calls/min), monthly quota (writes
67
+ immediately)
68
+ 2. **Wrap-up** — links to [docs.apitogo.com](https://docs.apitogo.com) for further configuration
62
69
 
63
70
  The wizard does **not** ask for fonts, colors, logo alt text, or the auth API base URL. Authentication
64
71
  stays **enabled**; `apitogo apply` writes `VITE_APITOGO_AUTHENTICATION_API_BASE_URL` to `.env` after
@@ -83,8 +90,8 @@ npx apitogo config
83
90
  | ------------------------- | --------------------------------------------------------------------------- |
84
91
  | `--pages-dir <dir>` | Folder to scan for `.md` / `.mdx` / `.tsx` (default: `pages`) |
85
92
  | `--dir <path>` | Project root (default: current directory) |
86
- | `--interactive` / `-i` | Wizard: site title, OpenAPI path, custom pages, gateway plans |
87
- | `--title <string>` | Set `site.title` |
93
+ | `--interactive` / `-i` | Force the wizard (default on TTY). Use `--no-interactive` to skip. |
94
+ | `--title <string>` | Set `site.title` (non-interactive) |
88
95
  | `--site <json>` | Deep-merge a JSON object into `site` |
89
96
  | `--theme <json>` | Deep-merge a JSON object into `theme` |
90
97
  | `--authentication <json>` | Deep-merge a JSON object into `authentication` (secrets stay in `.env`) |
@@ -93,9 +100,10 @@ npx apitogo config
93
100
  Examples:
94
101
 
95
102
  ```bash
103
+ npx apitogo config
96
104
  npx apitogo config --title "Books API"
97
105
  npx apitogo config --theme '{"light":{"primary":"#111111"}}'
98
- npx apitogo config -i
106
+ npx apitogo config --no-interactive
99
107
  ```
100
108
 
101
109
  Theme and fonts are edited in `apitogo.config.json` or via MCP — not in the interactive wizard. See
@@ -57,7 +57,7 @@ metadata, with optional `.env` overrides for secrets and local tweaks.
57
57
  - **Site name / logo:** only `site.title` and `site.logo` (platform branding is derived at publish).
58
58
  - **OpenAPI path:** `backend.openApiPath` is derived from `apis[0].input`.
59
59
  - **Billing / account plans:** use `modules.userPanel.plans.enabled` and store the plan catalog in
60
- `modules.userPanel.plans.items`. Add plans with `apitogo config -i` or edit the manifest by hand.
60
+ `modules.userPanel.plans.items`. Add plans with `apitogo config` or edit the manifest by hand.
61
61
  See [Gateway plans](./plans.md). Legacy root `plans` is migrated on load.
62
62
  - **Docs / API keys:** configure under `modules.docs` and `modules.userPanel.apiKeys` only.
63
63
 
@@ -160,7 +160,7 @@ Put navigation in `apitogo.config.json`, or keep large trees in a separate modul
160
160
  ## Related
161
161
 
162
162
  - [Manifest (`apitogo.config.json`)](./manifest.md)
163
- - [Make config (CLI)](./make-config.mdx) — interactive wizard (`apitogo config -i`)
163
+ - [Make config (CLI)](./make-config.mdx) — interactive wizard (`apitogo config`)
164
164
  - [Gateway plans](./plans.md)
165
165
  - [Authentication](./authentication.md)
166
166
  - [MCP overview](../mcp/overview.md)
@@ -14,12 +14,12 @@ Store the catalog under `modules.userPanel.plans.items` in `apitogo.config.json`
14
14
 
15
15
  ## Shape with the CLI
16
16
 
17
- `apitogo config -i` can add plans interactively (title, price, rate limit, monthly quota). You can
18
- also edit the manifest by hand or use MCP `update_dev_portal_manifest`.
17
+ `apitogo config` can add plans interactively (title, price, rate limit, monthly quota). Each plan is
18
+ written immediately. You can also edit the manifest by hand or use MCP `update_dev_portal_manifest`.
19
19
 
20
20
  ```bash
21
- npx apitogo config -i
22
- # loop: plan → enter title, price, policies → done
21
+ npx apitogo config
22
+ # choose plan → enter title, price, policies → done when finished
23
23
  ```
24
24
 
25
25
  ## Canonical shape
@@ -13,8 +13,8 @@ Start by creating the page you want to add.
13
13
 
14
14
  ## CLI scaffold
15
15
 
16
- `apitogo config -i` can create a starter page under `pages/` and wire it into navigation. Choose
17
- `page` in the wizard loop, then edit the generated `.tsx` file.
16
+ `apitogo config` can create a starter page under `pages/` and wire it into navigation immediately.
17
+ Choose `page` in the wizard loop, then edit the generated `.tsx` file.
18
18
 
19
19
  Predefined UI components (Callout, Card, Button, Heading, and more) are documented under
20
20
  [/docs/components](/docs/components).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lukoweb/apitogo",
3
- "version": "0.1.81",
3
+ "version": "0.1.83",
4
4
  "type": "module",
5
5
  "sideEffects": [
6
6
  "**/*.css",