@jtl-software/create-cloud-app 0.8.2 → 0.9.0

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.
Files changed (3) hide show
  1. package/README.md +47 -4
  2. package/dist/index.js +361 -49
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -16,6 +16,30 @@ The CLI has three entry points:
16
16
 
17
17
  Registration and listing are two independent resources on two independent files. `register` must happen first — `listing` pushes against the app that `register` created and will fail with a clear error if it hasn't run yet.
18
18
 
19
+ ### Help and version
20
+
21
+ Every command documents its flags:
22
+
23
+ ```bash
24
+ npx @jtl-software/create-cloud-app --help # overview + scaffold flags
25
+ npx @jtl-software/create-cloud-app help register # same as `register --help`
26
+ npx @jtl-software/create-cloud-app listing --help
27
+ npx @jtl-software/create-cloud-app --version
28
+ ```
29
+
30
+ ### Running without a terminal (AI agents, scripts)
31
+
32
+ Every prompt has a matching flag. When stdin is not a terminal, the CLI never prompts. Instead it exits with code 1 and names the flags to pass. Pass `--yes` to get the same behavior in a terminal.
33
+
34
+ ```bash
35
+ npx @jtl-software/create-cloud-app --name my-app --frontend react --backend node --yes
36
+ cd my-app && npm install
37
+ npm run register -- --yes --tenant my-tenant --new --on-collision overwrite
38
+ npm run listing -- --yes --tenant my-tenant --distribution-type PRIVATE
39
+ ```
40
+
41
+ `register` and `listing` still need a browser sign-in the first time. After that the cached token is reused and refreshed.
42
+
19
43
  ### `npm create @jtl-software/cloud-app@latest`
20
44
 
21
45
  Scaffold a new app interactively.
@@ -28,7 +52,7 @@ You'll be prompted for:
28
52
 
29
53
  - **App name**: directory name, package name, and manifest identifier
30
54
  - **Description**: placed in the listing
31
- - **Backend**: Node.js (Express + TypeScript) or .NET (ASP.NET Core + FastEndpoints)
55
+ - **Backend**: Node.js (Express + TypeScript), .NET (ASP.NET Core + FastEndpoints) or PHP (Slim 4)
32
56
  - **Frontend**: React (Vite + Tailwind + JTL Platform UI)
33
57
 
34
58
  Then:
@@ -40,6 +64,20 @@ npm run register
40
64
  npm run dev
41
65
  ```
42
66
 
67
+ Pass any of these flags to skip the matching prompt:
68
+
69
+ | Flag | Description |
70
+ | ------------------------------------- | ------------------------------------------------------------------ |
71
+ | `--name <app-name>` | App name and directory. Lowercase letters, numbers and hyphens. |
72
+ | `--description <text>` | Short description. Default `A JTL Platform app`. |
73
+ | `--frontend <react\|none>` | Frontend stack, or `none`. |
74
+ | `--backend <node\|dotnet\|php\|none>` | Backend stack, or `none`. Pick at least one frontend or backend. |
75
+ | `--yes`, `-y` | Never prompt. `--name`, `--frontend` and `--backend` are required. |
76
+
77
+ ```bash
78
+ npm create @jtl-software/cloud-app@latest -- --name my-app --frontend react --backend node --yes
79
+ ```
80
+
43
81
  Pass `--legacy` to scaffold the backend for the legacy Ory M2M flow instead of the Zitadel service account (compat testing only, node backend only):
44
82
 
45
83
  ```bash
@@ -75,14 +113,16 @@ Flags:
75
113
  | `--api-host <url>` | API host. Default `https://api.jtl-cloud.com`. |
76
114
  | `--auth-host <url>` | OAuth host. Default `https://id.jtl-cloud.com`. The CLI picks the matching client ID for the known prod/QA/dev hosts. |
77
115
  | `--hub-host <url>` | Hub host (used in printed links). Default `https://hub.jtl-cloud.com`. |
116
+ | `--partner-host <url>` | Partner Portal host (used in printed links). Default `https://partner.jtl-cloud.com`. |
78
117
  | `--client-id <id>` | Override the OAuth client ID. Only needed for custom auth hosts. |
79
- | `--scope <scope>` | OAuth scope. Default `openid offline_access`. |
118
+ | `--scope <scope>` | OAuth scope. The default depends on `--auth-host`. |
80
119
  | `--reauth` | Skip the cached token and open the browser again. Useful after switching accounts. |
81
- | `--yes`, `-y` | Non-interactive mode. Auto-confirms prompts and fails if any choice is ambiguous (e.g. multiple tenants match). Required for CI. |
120
+ | `--yes`, `-y` | Non-interactive mode. Auto-confirms prompts and fails if any choice is ambiguous (e.g. multiple tenants match). Required for CI and whenever stdin is not a terminal. |
82
121
  | `--tenant <id-or-slug>` | Select a tenant by ID or slug (case-insensitive). |
83
122
  | `--existing-app-id <id>` | Update the app with this ID instead of prompting. Errors if it doesn't match this manifest's technical name. |
84
123
  | `--new` | Force-create a new app even if a matching one exists. Mutually exclusive with `--existing-app-id`. |
85
124
  | `--on-collision <overwrite\|print\|cancel>` | What to do when an existing credentials file would be overwritten during provisioning. `print` writes the secret to stdout instead of the file. Required in `--yes` mode if a collision is possible. |
125
+ | `--app-service-path <path>` | App service path segment, for testing a branch deployment (e.g. `app-service-1317`). Default `app-service`. |
86
126
 
87
127
  Tokens are cached under `~/.config/jtl-cli/tokens.json` (mode `0600`). Override the path with the `JTL_CLI_TOKEN_CACHE_FILE` environment variable.
88
128
 
@@ -96,7 +136,10 @@ npx @jtl-software/create-cloud-app listing
96
136
 
97
137
  Like `register`, it's interactive by default: OAuth login, then a tenant prompt. It also asks two questions about the listing itself — distribution type (`PUBLIC`/`PRIVATE`) and default locale — pre-filled from whatever's already in `listing.json` and written back there once answered, so re-running `listing` later remembers your choice. Unlike `register`, there's no create-vs-update choice or local credential file to write — a listing is always pushed against the app `register` already created, keyed by `technicalName`. If no registered app matches, the command fails with a message telling you to run `register` first.
98
138
 
99
- Flags: `--api-host`, `--auth-host`, `--hub-host`, `--client-id`, `--scope`, `--reauth`, `--yes`/`-y`, `--tenant` — same meaning as the equivalent `register` flags above. There's no `--existing-app-id`, `--new`, or `--on-collision` since listing has nothing to disambiguate or overwrite locally.
139
+ Flags: `--api-host`, `--auth-host`, `--hub-host`, `--partner-host`, `--client-id`, `--scope`, `--reauth`, `--yes`/`-y`, `--tenant`. They mean the same as the `register` flags above. There's no `--existing-app-id`, `--new`, or `--on-collision` since listing has nothing to disambiguate or overwrite locally. Two flags answer the listing questions without a prompt and write the value to `listing.json`:
140
+
141
+ - `--distribution-type <PUBLIC|PRIVATE>`
142
+ - `--default-locale <locale>`, e.g. `de-DE`
100
143
 
101
144
  ## CI usage: keep your deployed app.json and listing.json in sync with the repo
102
145
 
package/dist/index.js CHANGED
@@ -2,15 +2,10 @@
2
2
 
3
3
  // src/index.ts
4
4
  import * as p4 from "@clack/prompts";
5
+ import fs7 from "fs";
6
+ import path7 from "path";
5
7
  import pc4 from "picocolors";
6
8
 
7
- // src/listing.ts
8
- import * as p3 from "@clack/prompts";
9
- import fs3 from "fs";
10
- import path3 from "path";
11
- import { parseArgs as parseArgs2 } from "util";
12
- import pc3 from "picocolors";
13
-
14
9
  // src/platform-client.ts
15
10
  import * as p from "@clack/prompts";
16
11
  import { spawn } from "child_process";
@@ -528,6 +523,12 @@ function resolveCommonOptions(values) {
528
523
  tenant: values.tenant
529
524
  };
530
525
  }
526
+ function requireTerminalOrYes(opts, isTTY = Boolean(process.stdin.isTTY)) {
527
+ if (opts.yes || isTTY) return;
528
+ bail(
529
+ "no terminal to prompt in. Pass --yes (plus --tenant and the command's other flags) to run without prompts. Run with --help for the flags."
530
+ );
531
+ }
531
532
  function ensureContinue(value) {
532
533
  if (p.isCancel(value)) {
533
534
  bail("Cancelled.");
@@ -605,6 +606,191 @@ function printHubLink(hubHost, tenantSlug, appId, isInstalled) {
605
606
  }
606
607
  }
607
608
 
609
+ // src/help.ts
610
+ var BIN = "create-cloud-app";
611
+ var GLOBAL_FLAGS = [
612
+ { flag: "-h, --help", description: "Show help for the command." },
613
+ { flag: "-v, --version", description: "Print the CLI version." }
614
+ ];
615
+ var COMMON_FLAGS = [
616
+ {
617
+ flag: "-y, --yes",
618
+ description: "Non-interactive: auto-confirm, and fail with a hint instead of prompting when a choice is ambiguous. Required when stdin is not a terminal."
619
+ },
620
+ {
621
+ flag: "--tenant <id|slug>",
622
+ description: "Tenant to use (case-insensitive slug). Needed with --yes when the account has more than one tenant."
623
+ },
624
+ {
625
+ flag: "--reauth",
626
+ description: "Ignore the cached token and sign in again in the browser."
627
+ },
628
+ {
629
+ flag: "--api-host <url>",
630
+ description: `API host. Default ${DEFAULT_API_HOST}.`
631
+ },
632
+ {
633
+ flag: "--auth-host <url>",
634
+ description: `OAuth host. Default ${DEFAULT_AUTH_HOST}.`
635
+ },
636
+ {
637
+ flag: "--hub-host <url>",
638
+ description: `Hub host for printed links. Default ${DEFAULT_HUB_HOST}.`
639
+ },
640
+ {
641
+ flag: "--partner-host <url>",
642
+ description: `Partner Portal host for printed links. Default ${DEFAULT_PARTNER_HOST}.`
643
+ },
644
+ {
645
+ flag: "--client-id <id>",
646
+ description: "OAuth client id. Only needed for an unknown --auth-host."
647
+ },
648
+ {
649
+ flag: "--scope <scope>",
650
+ description: "OAuth scope. Default depends on --auth-host."
651
+ }
652
+ ];
653
+ var AUTH_NOTE = "Sign-in opens a browser once; the token is cached in ~/.config/jtl-cli/tokens.json (override with JTL_CLI_TOKEN_CACHE_FILE).";
654
+ var COMMANDS = {
655
+ scaffold: {
656
+ name: "scaffold",
657
+ usage: `${BIN} [options]`,
658
+ summary: "Scaffold a new app project (default when no command is given).",
659
+ description: "Creates ./<name> with app.json, listing.json and the chosen frontend/backend. Prompts for any value not passed as a flag. With --yes, or when stdin is not a terminal, it never prompts: --name, --frontend and --backend are then required.",
660
+ flags: [
661
+ {
662
+ flag: "--name <app-name>",
663
+ description: "App name and directory. Lowercase letters, numbers and hyphens."
664
+ },
665
+ {
666
+ flag: "--description <text>",
667
+ description: 'Short description. Default "A JTL Platform app".'
668
+ },
669
+ {
670
+ flag: "--frontend <react|none>",
671
+ description: "Frontend stack, or none."
672
+ },
673
+ {
674
+ flag: "--backend <node|dotnet|php|none>",
675
+ description: "Backend stack, or none. Pick at least one of frontend and backend."
676
+ },
677
+ {
678
+ flag: "--legacy",
679
+ description: "Scaffold the node backend for the legacy Ory M2M flow (compat testing only)."
680
+ },
681
+ {
682
+ flag: "-y, --yes",
683
+ description: "Never prompt; fail on missing flags instead."
684
+ }
685
+ ],
686
+ examples: [
687
+ `${BIN} --name my-app --frontend react --backend node --yes`,
688
+ `npm create @jtl-software/cloud-app@latest -- --name my-app --frontend react --backend none --yes`
689
+ ]
690
+ },
691
+ register: {
692
+ name: "register",
693
+ usage: `${BIN} register [options]`,
694
+ summary: "Register or update app.json with the JTL Cloud and write credentials.",
695
+ description: `Run from the project root. Reads app.json, creates or updates the app in the chosen tenant, and writes its credentials to the backend and frontend config. ${AUTH_NOTE}`,
696
+ flags: [
697
+ ...COMMON_FLAGS,
698
+ {
699
+ flag: "--existing-app-id <id>",
700
+ description: "Update this app instead of asking. Must match app.json's technicalName."
701
+ },
702
+ {
703
+ flag: "--new",
704
+ description: "Create a new app even if one matches. Excludes --existing-app-id."
705
+ },
706
+ {
707
+ flag: "--on-collision <overwrite|print|cancel>",
708
+ description: "What to do when a credentials file already has values. Needed with --yes when that can happen."
709
+ },
710
+ {
711
+ flag: "--app-service-path <path>",
712
+ description: "App service path segment, for branch deployments. Default app-service."
713
+ }
714
+ ],
715
+ examples: [
716
+ `${BIN} register --yes --tenant my-tenant --new --on-collision overwrite`,
717
+ `${BIN} register --yes --tenant my-tenant --existing-app-id <app-id>`
718
+ ]
719
+ },
720
+ listing: {
721
+ name: "listing",
722
+ usage: `${BIN} listing [options]`,
723
+ summary: "Push listing.json as the app's store listing.",
724
+ description: `Run from the project root after register. Reads listing.json and the technicalName from app.json. With --yes, the distribution type and default locale are taken from listing.json (or the flags below). ${AUTH_NOTE}`,
725
+ flags: [
726
+ ...COMMON_FLAGS,
727
+ {
728
+ flag: "--distribution-type <PUBLIC|PRIVATE>",
729
+ description: "Set distributionType in listing.json instead of asking."
730
+ },
731
+ {
732
+ flag: "--default-locale <locale>",
733
+ description: "Set defaultLocale in listing.json instead of asking, e.g. de-DE."
734
+ }
735
+ ],
736
+ examples: [
737
+ `${BIN} listing --yes --tenant my-tenant --distribution-type PRIVATE`
738
+ ]
739
+ }
740
+ };
741
+ function flagTable(flags) {
742
+ const width = Math.max(...flags.map((f) => f.flag.length));
743
+ return flags.map((f) => ` ${f.flag.padEnd(width)} ${f.description}`);
744
+ }
745
+ function renderOverview(version) {
746
+ const commands = Object.values(COMMANDS);
747
+ const width = Math.max(...commands.map((c) => c.name.length));
748
+ return [
749
+ `${BIN} ${version}: create, register and publish JTL Platform cloud apps.`,
750
+ "",
751
+ "Usage:",
752
+ ` ${BIN} [command] [options]`,
753
+ "",
754
+ "Commands:",
755
+ ...commands.map(
756
+ (c) => ` ${(c.name === "scaffold" ? "(none)" : c.name).padEnd(width)} ${c.summary}`
757
+ ),
758
+ ` ${"help".padEnd(width)} Show help for a command, e.g. \`${BIN} help register\`.`,
759
+ "",
760
+ "Options:",
761
+ ...flagTable(GLOBAL_FLAGS),
762
+ "",
763
+ `Run \`${BIN} <command> --help\` for the command's flags.`,
764
+ "Every prompt has a matching flag. register and listing still need a one-time browser sign-in.",
765
+ ""
766
+ ].join("\n");
767
+ }
768
+ function renderCommandHelp(name) {
769
+ const c = COMMANDS[name];
770
+ return [
771
+ c.summary,
772
+ "",
773
+ "Usage:",
774
+ ` ${c.usage}`,
775
+ "",
776
+ c.description,
777
+ "",
778
+ "Options:",
779
+ ...flagTable([...c.flags, GLOBAL_FLAGS[0]]),
780
+ "",
781
+ "Examples:",
782
+ ...c.examples.map((e) => ` ${e}`),
783
+ ""
784
+ ].join("\n");
785
+ }
786
+
787
+ // src/listing.ts
788
+ import * as p3 from "@clack/prompts";
789
+ import fs3 from "fs";
790
+ import path3 from "path";
791
+ import { parseArgs as parseArgs2 } from "util";
792
+ import pc3 from "picocolors";
793
+
608
794
  // src/register.ts
609
795
  import * as p2 from "@clack/prompts";
610
796
  import fs2 from "fs";
@@ -884,23 +1070,24 @@ function parseCollisionAction(input) {
884
1070
  `--on-collision must be one of: overwrite, print, cancel (got "${input}")`
885
1071
  );
886
1072
  }
1073
+ var REGISTER_ARG_OPTIONS = {
1074
+ ...COMMON_ARG_OPTIONS,
1075
+ "existing-app-id": { type: "string" },
1076
+ new: { type: "boolean" },
1077
+ "on-collision": { type: "string" },
1078
+ "app-service-path": { type: "string" }
1079
+ };
887
1080
  function resolveOptions(args) {
888
1081
  let parsed;
889
1082
  try {
890
1083
  parsed = parseArgs({
891
1084
  args,
892
- options: {
893
- ...COMMON_ARG_OPTIONS,
894
- "existing-app-id": { type: "string" },
895
- new: { type: "boolean" },
896
- "on-collision": { type: "string" },
897
- "app-service-path": { type: "string" }
898
- },
1085
+ options: REGISTER_ARG_OPTIONS,
899
1086
  strict: true,
900
1087
  allowPositionals: false
901
1088
  });
902
1089
  } catch (err) {
903
- bail(err.message);
1090
+ bail(`${err.message}. Run with --help for usage.`);
904
1091
  }
905
1092
  const common = resolveCommonOptions(parsed.values);
906
1093
  const existingAppId = parsed.values["existing-app-id"];
@@ -1199,6 +1386,7 @@ async function register(argv = [], runOpts = {}) {
1199
1386
  console.log();
1200
1387
  p2.intro(pc2.bgCyan(pc2.black(" register cloud app ")));
1201
1388
  const opts = resolveOptions(argv);
1389
+ requireTerminalOrYes(opts);
1202
1390
  const backend = detectBackend(root);
1203
1391
  p2.note(
1204
1392
  [
@@ -1517,19 +1705,36 @@ async function resolveAppId(apiBaseUrl, jwt, tenantId, technicalName) {
1517
1705
  }
1518
1706
  return matches[0].id;
1519
1707
  }
1708
+ var LISTING_ARG_OPTIONS = {
1709
+ ...COMMON_ARG_OPTIONS,
1710
+ "distribution-type": { type: "string" },
1711
+ "default-locale": { type: "string" }
1712
+ };
1520
1713
  function resolveListingOptions(args) {
1521
1714
  let parsed;
1522
1715
  try {
1523
1716
  parsed = parseArgs2({
1524
1717
  args,
1525
- options: { ...COMMON_ARG_OPTIONS },
1718
+ options: LISTING_ARG_OPTIONS,
1526
1719
  strict: true,
1527
1720
  allowPositionals: false
1528
1721
  });
1529
1722
  } catch (err) {
1530
- bail(err.message);
1723
+ bail(`${err.message}. Run with --help for usage.`);
1724
+ }
1725
+ const distributionType = parsed.values["distribution-type"]?.toUpperCase();
1726
+ if (distributionType !== void 0 && distributionType !== "PUBLIC" && distributionType !== "PRIVATE") {
1727
+ bail(
1728
+ `--distribution-type must be PUBLIC or PRIVATE (got "${parsed.values["distribution-type"]}")`
1729
+ );
1531
1730
  }
1532
- return resolveCommonOptions(parsed.values);
1731
+ const defaultLocale = parsed.values["default-locale"];
1732
+ if (defaultLocale === "") bail("--default-locale must not be empty");
1733
+ return {
1734
+ ...resolveCommonOptions(parsed.values),
1735
+ distributionType,
1736
+ defaultLocale
1737
+ };
1533
1738
  }
1534
1739
  async function createAppListing(apiBaseUrl, jwt, tenantId, appId, listing2) {
1535
1740
  const created = await apiCall(
@@ -1610,6 +1815,7 @@ async function listing(argv = [], runOpts = {}) {
1610
1815
  console.log();
1611
1816
  p3.intro(pc3.bgCyan(pc3.black(" push app listing ")));
1612
1817
  const opts = resolveListingOptions(argv);
1818
+ requireTerminalOrYes(opts);
1613
1819
  const technicalName = readTechnicalName(root);
1614
1820
  const listingData = readListing(listingPath);
1615
1821
  p3.note(
@@ -1648,14 +1854,8 @@ async function listing(argv = [], runOpts = {}) {
1648
1854
  bail(err.message);
1649
1855
  }
1650
1856
  const tenant = await selectTenant(tenants, opts);
1651
- listingData.distributionType = await resolveDistributionType(
1652
- listingData.distributionType,
1653
- opts
1654
- );
1655
- listingData.defaultLocale = await resolveDefaultLocale(
1656
- listingData.defaultLocale,
1657
- opts
1658
- );
1857
+ listingData.distributionType = opts.distributionType ?? await resolveDistributionType(listingData.distributionType, opts);
1858
+ listingData.defaultLocale = opts.defaultLocale ?? await resolveDefaultLocale(listingData.defaultLocale, opts);
1659
1859
  writeListing(listingPath, listingData);
1660
1860
  const summary = summarizeListing(listingData);
1661
1861
  if (listingData.distributionType === "PUBLIC" && !listingData.pricing?.subscriptionTiers?.length) {
@@ -2019,6 +2219,82 @@ async function scaffold({
2019
2219
  );
2020
2220
  }
2021
2221
 
2222
+ // src/scaffold-args.ts
2223
+ import { parseArgs as parseArgs3 } from "util";
2224
+ var SCAFFOLD_ARG_OPTIONS = {
2225
+ name: { type: "string" },
2226
+ description: { type: "string" },
2227
+ frontend: { type: "string" },
2228
+ backend: { type: "string" },
2229
+ legacy: { type: "boolean" },
2230
+ yes: { type: "boolean", short: "y" }
2231
+ };
2232
+ var NONE = "none";
2233
+ function validateAppName(value) {
2234
+ if (!value) return "App name is required";
2235
+ if (!/^[a-z0-9-]+$/.test(value))
2236
+ return "Use lowercase letters, numbers, and hyphens only";
2237
+ }
2238
+ function stackChoices(templates) {
2239
+ return [...templates.map(templateTech), NONE];
2240
+ }
2241
+ function pickTemplate(flag, value, templates) {
2242
+ if (value === void 0) return void 0;
2243
+ if (value === NONE) return null;
2244
+ const match = templates.find((t) => templateTech(t) === value);
2245
+ if (!match) {
2246
+ bail(
2247
+ `--${flag} must be one of: ${stackChoices(templates).join(", ")} (got "${value}")`
2248
+ );
2249
+ }
2250
+ return match;
2251
+ }
2252
+ function resolveScaffoldArgs(args, templates, isTTY = Boolean(process.stdin.isTTY)) {
2253
+ let parsed;
2254
+ try {
2255
+ parsed = parseArgs3({
2256
+ args,
2257
+ options: SCAFFOLD_ARG_OPTIONS,
2258
+ strict: true,
2259
+ allowPositionals: false
2260
+ });
2261
+ } catch (err) {
2262
+ bail(`${err.message}. Run with --help for usage.`);
2263
+ }
2264
+ const v = parsed.values;
2265
+ if (v.name !== void 0) {
2266
+ const invalid = validateAppName(v.name);
2267
+ if (invalid) bail(`--name: ${invalid}`);
2268
+ }
2269
+ const result = {
2270
+ name: v.name,
2271
+ description: v.description,
2272
+ frontend: pickTemplate("frontend", v.frontend, templates.frontends),
2273
+ backend: pickTemplate("backend", v.backend, templates.backends),
2274
+ legacy: v.legacy ?? false,
2275
+ nonInteractive: Boolean(v.yes) || !isTTY
2276
+ };
2277
+ if (result.nonInteractive) {
2278
+ const missing = [
2279
+ result.name === void 0 && "--name <app-name>",
2280
+ result.frontend === void 0 && `--frontend <${stackChoices(templates.frontends).join("|")}>`,
2281
+ result.backend === void 0 && `--backend <${stackChoices(templates.backends).join("|")}>`
2282
+ ].filter(Boolean);
2283
+ if (missing.length) {
2284
+ bail(
2285
+ `missing ${missing.join(", ")}. Without a terminal (or with --yes) these must be passed as flags.`
2286
+ );
2287
+ }
2288
+ }
2289
+ if (result.frontend === null && result.backend === null) {
2290
+ bail("Pick at least a frontend or a backend.");
2291
+ }
2292
+ if (result.legacy && result.backend && templateTech(result.backend) !== "node") {
2293
+ bail("--legacy is only supported for the node backend.");
2294
+ }
2295
+ return result;
2296
+ }
2297
+
2022
2298
  // src/templates.ts
2023
2299
  import fs5 from "fs";
2024
2300
  import path5 from "path";
@@ -2057,7 +2333,7 @@ import os2 from "os";
2057
2333
  import path6 from "path";
2058
2334
  import { PostHog } from "posthog-node";
2059
2335
  var TOKEN = "phc_SkFAxTIa4RUQxotci1Ftdw7xefZK2Ax0LEIGcl8zuD2";
2060
- var CLI_VERSION = "0.8.2";
2336
+ var CLI_VERSION = "0.9.0";
2061
2337
  var client = TOKEN ? new PostHog(TOKEN, { host: "https://e.jtl-cloud.com" }) : null;
2062
2338
  function telemetryDisabledByEnv() {
2063
2339
  const dnt = process.env.DO_NOT_TRACK;
@@ -2125,7 +2401,7 @@ async function shutdownPostHog() {
2125
2401
  }
2126
2402
 
2127
2403
  // src/index.ts
2128
- var NONE = "__none__";
2404
+ var NONE2 = "__none__";
2129
2405
  function scaffoldChoice(backend, frontend, legacy) {
2130
2406
  const hasBackend = Boolean(backend);
2131
2407
  const hasFrontend = Boolean(frontend);
@@ -2148,14 +2424,14 @@ function templateSelectOptions(templates) {
2148
2424
  function halfSelectOptions(templates, noneHint) {
2149
2425
  return [
2150
2426
  ...templateSelectOptions(templates),
2151
- { value: NONE, label: "None", hint: noneHint }
2427
+ { value: NONE2, label: "None", hint: noneHint }
2152
2428
  ];
2153
2429
  }
2154
2430
  function cancelScaffold() {
2155
2431
  p4.cancel("Cancelled.");
2156
2432
  process.exit(0);
2157
2433
  }
2158
- async function runScaffold() {
2434
+ async function runScaffold(argv) {
2159
2435
  console.log();
2160
2436
  p4.intro(pc4.bgCyan(pc4.black(" create-cloud-app ")));
2161
2437
  const templates = discoverTemplates();
@@ -2166,23 +2442,23 @@ async function runScaffold() {
2166
2442
  p4.cancel("No templates found. Ensure template packages are installed.");
2167
2443
  process.exit(1);
2168
2444
  }
2445
+ const args = resolveScaffoldArgs(argv, { frontends, backends });
2169
2446
  const answers = await p4.group(
2170
2447
  {
2171
- appName: () => p4.text({
2448
+ appName: () => args.name !== void 0 ? Promise.resolve(args.name) : p4.text({
2172
2449
  message: "App name",
2173
2450
  placeholder: "my-jtl-app",
2174
- validate: (value) => {
2175
- if (!value) return "App name is required";
2176
- if (!/^[a-z0-9-]+$/.test(value))
2177
- return "Use lowercase letters, numbers, and hyphens only";
2178
- }
2451
+ validate: validateAppName
2179
2452
  }),
2180
- description: () => p4.text({ message: "Description", placeholder: "A JTL Platform app" }),
2181
- frontend: () => p4.select({
2453
+ description: () => args.description !== void 0 || args.nonInteractive ? Promise.resolve(args.description ?? "") : p4.text({
2454
+ message: "Description",
2455
+ placeholder: "A JTL Platform app"
2456
+ }),
2457
+ frontend: () => args.frontend !== void 0 ? Promise.resolve(args.frontend?.packageName ?? NONE2) : p4.select({
2182
2458
  message: "Frontend (adds a public client for browser login)",
2183
2459
  options: halfSelectOptions(frontends, "no frontend")
2184
2460
  }),
2185
- backend: () => p4.select({
2461
+ backend: () => args.backend !== void 0 ? Promise.resolve(args.backend?.packageName ?? NONE2) : p4.select({
2186
2462
  message: "Backend (adds a service account for the JTL API)",
2187
2463
  options: halfSelectOptions(backends, "no backend")
2188
2464
  })
@@ -2196,13 +2472,14 @@ async function runScaffold() {
2196
2472
  (t) => t.packageName === answers.backend
2197
2473
  );
2198
2474
  if (!selectedFrontend && !selectedBackend) {
2199
- p4.cancel("Pick at least a frontend or a backend.");
2200
- process.exit(1);
2475
+ bail("Pick at least a frontend or a backend.");
2201
2476
  }
2202
- const legacy = process.argv.includes("--legacy");
2477
+ const legacy = args.legacy;
2203
2478
  if (legacy && selectedBackend && templateTech(selectedBackend) !== "node") {
2204
- p4.cancel("--legacy is only supported for the node backend.");
2205
- process.exit(1);
2479
+ bail("--legacy is only supported for the node backend.");
2480
+ }
2481
+ if (fs7.existsSync(path7.resolve(process.cwd(), answers.appName))) {
2482
+ bail(`Directory "${answers.appName}" already exists.`);
2206
2483
  }
2207
2484
  const s = p4.spinner();
2208
2485
  s.start("Scaffolding project");
@@ -2235,7 +2512,10 @@ async function runScaffold() {
2235
2512
  }
2236
2513
  printNextSteps(answers.appName);
2237
2514
  p4.outro(pc4.green("Done! Happy building."));
2238
- return scaffoldChoice(selectedBackend, selectedFrontend, legacy);
2515
+ return {
2516
+ ...scaffoldChoice(selectedBackend, selectedFrontend, legacy),
2517
+ non_interactive: args.nonInteractive
2518
+ };
2239
2519
  }
2240
2520
  function printNextSteps(appName) {
2241
2521
  p4.note(
@@ -2249,17 +2529,49 @@ function printNextSteps(appName) {
2249
2529
  "Next steps"
2250
2530
  );
2251
2531
  }
2532
+ var SUBCOMMANDS = ["register", "listing"];
2533
+ function handleMeta(argv) {
2534
+ const [first, second] = argv;
2535
+ if (first === "-v" || first === "--version") {
2536
+ console.log(CLI_VERSION);
2537
+ return true;
2538
+ }
2539
+ if (first === "help") {
2540
+ if (second === void 0) console.log(renderOverview(CLI_VERSION));
2541
+ else if (second === "scaffold" || isSubcommand(second))
2542
+ console.log(renderCommandHelp(second));
2543
+ else
2544
+ bail(`unknown command "${second}". Run \`${BIN} --help\` for the list.`);
2545
+ return true;
2546
+ }
2547
+ if (first !== void 0 && !first.startsWith("-") && !isSubcommand(first)) {
2548
+ bail(`unknown command "${first}". Run \`${BIN} --help\` for the list.`);
2549
+ }
2550
+ if (argv.includes("-h") || argv.includes("--help")) {
2551
+ console.log(
2552
+ isSubcommand(first) ? renderCommandHelp(first) : `${renderOverview(CLI_VERSION)}
2553
+ ${renderCommandHelp("scaffold")}`
2554
+ );
2555
+ return true;
2556
+ }
2557
+ return false;
2558
+ }
2559
+ function isSubcommand(arg) {
2560
+ return SUBCOMMANDS.includes(arg);
2561
+ }
2252
2562
  async function main() {
2253
- const command = process.argv[2] === "register" ? "register" : process.argv[2] === "listing" ? "listing" : "scaffold";
2563
+ const argv = process.argv.slice(2);
2564
+ if (handleMeta(argv)) return;
2565
+ const command = isSubcommand(argv[0]) ? argv[0] : "scaffold";
2254
2566
  const startedAt = Date.now();
2255
2567
  try {
2256
2568
  let detail = {};
2257
2569
  if (command === "register") {
2258
- detail = await register(process.argv.slice(3));
2570
+ detail = await register(argv.slice(1));
2259
2571
  } else if (command === "listing") {
2260
- await listing(process.argv.slice(3));
2572
+ await listing(argv.slice(1));
2261
2573
  } else {
2262
- detail = await runScaffold();
2574
+ detail = await runScaffold(argv);
2263
2575
  }
2264
2576
  captureCommand(command, {
2265
2577
  success: true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jtl-software/create-cloud-app",
3
- "version": "0.8.2",
3
+ "version": "0.9.0",
4
4
  "type": "module",
5
5
  "description": "CLI tool for scaffolding JTL Platform cloud apps",
6
6
  "bin": {
@@ -19,9 +19,9 @@
19
19
  "dependencies": {
20
20
  "@clack/prompts": "^1.7.0",
21
21
  "@jtl-software/cloud-app-template-backend-dotnet": "0.2.0",
22
- "@jtl-software/cloud-app-template-backend-node": "0.3.0",
22
+ "@jtl-software/cloud-app-template-backend-node": "0.3.1",
23
23
  "@jtl-software/cloud-app-template-backend-php": "0.3.0",
24
- "@jtl-software/cloud-app-template-frontend-react": "0.3.1",
24
+ "@jtl-software/cloud-app-template-frontend-react": "0.3.2",
25
25
  "@jtl-software/cloud-app-template-shared": "0.2.0",
26
26
  "picocolors": "^1.1.1",
27
27
  "posthog-node": "^5.51.6"