@jtl-software/create-cloud-app 0.8.3 → 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.
- package/README.md +47 -4
- package/dist/index.js +361 -49
- package/package.json +1 -1
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)
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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:
|
|
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:
|
|
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: () =>
|
|
2181
|
-
|
|
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
|
-
|
|
2200
|
-
process.exit(1);
|
|
2475
|
+
bail("Pick at least a frontend or a backend.");
|
|
2201
2476
|
}
|
|
2202
|
-
const legacy =
|
|
2477
|
+
const legacy = args.legacy;
|
|
2203
2478
|
if (legacy && selectedBackend && templateTech(selectedBackend) !== "node") {
|
|
2204
|
-
|
|
2205
|
-
|
|
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
|
|
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
|
|
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(
|
|
2570
|
+
detail = await register(argv.slice(1));
|
|
2259
2571
|
} else if (command === "listing") {
|
|
2260
|
-
await listing(
|
|
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,
|