@jtl-software/create-cloud-app 0.7.0 → 0.8.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 +9 -1
- package/dist/index.js +146 -108
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -40,6 +40,14 @@ npm run register
|
|
|
40
40
|
npm run dev
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
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
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm create @jtl-software/cloud-app@latest -- --legacy
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`npm run register` then writes the app's Ory client credentials, and the backend mints its M2M token against the Ory auth host. Everything else is unchanged: the backend still verifies the incoming app token and reads the tenant id from it.
|
|
50
|
+
|
|
43
51
|
The generated project includes:
|
|
44
52
|
|
|
45
53
|
- A monorepo wired up with [Turborepo](https://turbo.build/)
|
|
@@ -65,7 +73,7 @@ Flags:
|
|
|
65
73
|
| Flag | Description |
|
|
66
74
|
| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
67
75
|
| `--api-host <url>` | API host. Default `https://api.jtl-cloud.com`. |
|
|
68
|
-
| `--auth-host <url>` | OAuth host. Default `https://
|
|
76
|
+
| `--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. |
|
|
69
77
|
| `--hub-host <url>` | Hub host (used in printed links). Default `https://hub.jtl-cloud.com`. |
|
|
70
78
|
| `--client-id <id>` | Override the OAuth client ID. Only needed for custom auth hosts. |
|
|
71
79
|
| `--scope <scope>` | OAuth scope. Default `openid offline_access`. |
|
package/dist/index.js
CHANGED
|
@@ -703,7 +703,8 @@ function readScaffoldStack(root) {
|
|
|
703
703
|
if (meta && typeof meta === "object") {
|
|
704
704
|
return {
|
|
705
705
|
backend: typeof meta.backend === "string" ? meta.backend : null,
|
|
706
|
-
frontend: typeof meta.frontend === "string" ? meta.frontend : null
|
|
706
|
+
frontend: typeof meta.frontend === "string" ? meta.frontend : null,
|
|
707
|
+
legacy: meta.legacy === true
|
|
707
708
|
};
|
|
708
709
|
}
|
|
709
710
|
} catch {
|
|
@@ -764,19 +765,18 @@ function readExistingCredentials(backend) {
|
|
|
764
765
|
function authBaseFromApi(apiHost) {
|
|
765
766
|
return apiHost.replace(/:\/\/api\./, "://auth.").replace(/\/$/, "");
|
|
766
767
|
}
|
|
767
|
-
function writeBackendCredentials(backend, clientId, clientSecret, issuer, apiBase, appId) {
|
|
768
|
-
const hosts = apiBase ? {
|
|
769
|
-
JTL_API_BASE: apiBase.replace(/\/$/, ""),
|
|
770
|
-
JTL_AUTH_BASE: authBaseFromApi(apiBase)
|
|
771
|
-
} : {};
|
|
768
|
+
function writeBackendCredentials(backend, clientId, clientSecret, issuer, apiBase, appId, authBase) {
|
|
769
|
+
const hosts = apiBase ? { JTL_API_BASE: apiBase.replace(/\/$/, "") } : {};
|
|
772
770
|
if (backend.kind === "node") {
|
|
773
|
-
|
|
771
|
+
const updates = {
|
|
774
772
|
CLIENT_ID: clientId,
|
|
775
|
-
CLIENT_SECRET: clientSecret
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
773
|
+
CLIENT_SECRET: clientSecret
|
|
774
|
+
};
|
|
775
|
+
if (issuer) updates.JTL_ISSUER = issuer;
|
|
776
|
+
if (appId) updates.JTL_APP_ID = appId;
|
|
777
|
+
Object.assign(updates, hosts);
|
|
778
|
+
if (authBase) updates.JTL_AUTH_BASE = authBase.replace(/\/$/, "");
|
|
779
|
+
upsertEnv(backend.envFile, updates);
|
|
780
780
|
return backend.envFile;
|
|
781
781
|
}
|
|
782
782
|
let settings = {};
|
|
@@ -968,7 +968,8 @@ async function listTenantApps(apiBaseUrl, jwt, tenantId, appServicePath = "app-s
|
|
|
968
968
|
clientId: entry.clientId,
|
|
969
969
|
provisioningStatus: entry.activeVersion?.provisioningStatus,
|
|
970
970
|
isInstalled: entry.isInstalled,
|
|
971
|
-
publicClientId: entry.credentials?.publicClient?.clientId
|
|
971
|
+
publicClientId: entry.credentials?.publicClient?.clientId,
|
|
972
|
+
serviceAccountClientId: entry.credentials?.serviceAccount?.clientId
|
|
972
973
|
};
|
|
973
974
|
}).filter((a) => a !== null);
|
|
974
975
|
}
|
|
@@ -1029,13 +1030,30 @@ async function provisionApp(apiBaseUrl, jwt, tenantId, appId, appServicePath = "
|
|
|
1029
1030
|
`provision response missing clientId or clientSecret: ${JSON.stringify(provisioned)}`
|
|
1030
1031
|
);
|
|
1031
1032
|
}
|
|
1033
|
+
const sa = provisioned.credentials?.serviceAccount;
|
|
1032
1034
|
return {
|
|
1033
1035
|
clientId,
|
|
1034
1036
|
clientSecret,
|
|
1035
|
-
publicClientId: provisioned.credentials?.publicClient?.clientId
|
|
1037
|
+
publicClientId: provisioned.credentials?.publicClient?.clientId,
|
|
1038
|
+
serviceAccount: sa?.clientId && sa.clientSecret ? { clientId: sa.clientId, clientSecret: sa.clientSecret } : void 0
|
|
1036
1039
|
};
|
|
1037
1040
|
}
|
|
1038
|
-
|
|
1041
|
+
function resolveBackendAuth(provisioned, legacy = false) {
|
|
1042
|
+
if (legacy) {
|
|
1043
|
+
return {
|
|
1044
|
+
clientId: provisioned.clientId,
|
|
1045
|
+
clientSecret: provisioned.clientSecret
|
|
1046
|
+
};
|
|
1047
|
+
}
|
|
1048
|
+
const sa = provisioned.serviceAccount;
|
|
1049
|
+
if (!sa) {
|
|
1050
|
+
throw new Error(
|
|
1051
|
+
"Provision response did not include a service account; the backend cannot authenticate."
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
return { clientId: sa.clientId, clientSecret: sa.clientSecret };
|
|
1055
|
+
}
|
|
1056
|
+
async function applyProvisionResult(root, backend, opts, tenant, appId, provisioned, isInstalled, legacy) {
|
|
1039
1057
|
if (provisioned.publicClientId) {
|
|
1040
1058
|
const written = writeFrontendPublicClient(
|
|
1041
1059
|
root,
|
|
@@ -1051,13 +1069,14 @@ async function applyProvisionResult(root, backend, opts, tenant, appId, provisio
|
|
|
1051
1069
|
}
|
|
1052
1070
|
writeVerifyDemoEnv(root, backend);
|
|
1053
1071
|
if (backend) {
|
|
1072
|
+
const auth = resolveBackendAuth(provisioned, legacy);
|
|
1054
1073
|
await persistCredentials(
|
|
1055
1074
|
backend,
|
|
1056
|
-
|
|
1057
|
-
|
|
1075
|
+
auth.clientId,
|
|
1076
|
+
auth.clientSecret,
|
|
1058
1077
|
root,
|
|
1059
1078
|
opts,
|
|
1060
|
-
{ hub: opts.hub, tenantSlug: tenant.slug, appId, isInstalled }
|
|
1079
|
+
{ hub: opts.hub, tenantSlug: tenant.slug, appId, isInstalled, legacy }
|
|
1061
1080
|
);
|
|
1062
1081
|
return;
|
|
1063
1082
|
}
|
|
@@ -1109,7 +1128,8 @@ async function persistCredentials(backend, clientId, clientSecret, root, opts, c
|
|
|
1109
1128
|
clientSecret,
|
|
1110
1129
|
opts.auth,
|
|
1111
1130
|
opts.api,
|
|
1112
|
-
context.appId
|
|
1131
|
+
context.appId,
|
|
1132
|
+
context.legacy ? authBaseFromApi(opts.api) : void 0
|
|
1113
1133
|
);
|
|
1114
1134
|
printHubLink(
|
|
1115
1135
|
context.hub,
|
|
@@ -1157,11 +1177,18 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1157
1177
|
const manifest = readManifest(path2.join(root, "app.json"));
|
|
1158
1178
|
const summary = summarizeManifest(manifest);
|
|
1159
1179
|
const stack = readScaffoldStack(root);
|
|
1180
|
+
const legacy = stack?.legacy ?? false;
|
|
1160
1181
|
const auth = manifest.authentication;
|
|
1182
|
+
if (!legacy && backend && !auth?.serviceAccount) {
|
|
1183
|
+
bail(
|
|
1184
|
+
"This project has a backend but app.json declares no authentication.serviceAccount. Add it (or re-scaffold) before registering, so the backend gets a Zitadel service account."
|
|
1185
|
+
);
|
|
1186
|
+
}
|
|
1161
1187
|
const hasFrontend = stack ? !!stack.frontend : !!auth?.publicClient;
|
|
1162
1188
|
const hasBackend = stack ? !!stack.backend : !!auth?.serviceAccount;
|
|
1163
1189
|
const registerTelemetry = {
|
|
1164
1190
|
environment: environmentFromApi(opts.api),
|
|
1191
|
+
legacy,
|
|
1165
1192
|
has_frontend: hasFrontend,
|
|
1166
1193
|
has_backend: hasBackend,
|
|
1167
1194
|
backend_tech: stack?.backend ?? (hasBackend ? "unknown" : "none"),
|
|
@@ -1170,7 +1197,7 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1170
1197
|
};
|
|
1171
1198
|
for (; ; ) {
|
|
1172
1199
|
const identityJwt = jwt;
|
|
1173
|
-
const apiJwt =
|
|
1200
|
+
const apiJwt = jwt;
|
|
1174
1201
|
spinner4.start("Fetching tenants");
|
|
1175
1202
|
let tenants;
|
|
1176
1203
|
try {
|
|
@@ -1259,9 +1286,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1259
1286
|
}
|
|
1260
1287
|
if (existing.provisioningStatus === "DRAFT") {
|
|
1261
1288
|
spinner4.start("Provisioning app");
|
|
1262
|
-
let
|
|
1289
|
+
let provisioned2;
|
|
1263
1290
|
try {
|
|
1264
|
-
|
|
1291
|
+
provisioned2 = await provisionApp(
|
|
1265
1292
|
opts.api,
|
|
1266
1293
|
apiJwt,
|
|
1267
1294
|
tenant.id,
|
|
@@ -1279,8 +1306,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1279
1306
|
opts,
|
|
1280
1307
|
tenant,
|
|
1281
1308
|
existing.id,
|
|
1282
|
-
|
|
1283
|
-
existing.isInstalled ?? false
|
|
1309
|
+
provisioned2,
|
|
1310
|
+
existing.isInstalled ?? false,
|
|
1311
|
+
legacy
|
|
1284
1312
|
);
|
|
1285
1313
|
return { ...registerTelemetry, is_new_app: false };
|
|
1286
1314
|
}
|
|
@@ -1316,6 +1344,18 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1316
1344
|
);
|
|
1317
1345
|
return { ...registerTelemetry, is_new_app: false };
|
|
1318
1346
|
}
|
|
1347
|
+
if (!legacy && localCreds.clientId !== existing.serviceAccountClientId) {
|
|
1348
|
+
p2.outro(
|
|
1349
|
+
pc2.yellow(
|
|
1350
|
+
`Manifest updated, but your ${labels.pair} in ${pc2.cyan(path2.relative(root, envFile))} need to be migrated to the new authentication platform. Go to the Partner Portal (${pc2.cyan(opts.partner)}) and follow the migration instructions to get your new ${labels.pair}.`
|
|
1351
|
+
)
|
|
1352
|
+
);
|
|
1353
|
+
return {
|
|
1354
|
+
...registerTelemetry,
|
|
1355
|
+
is_new_app: false,
|
|
1356
|
+
credential_migration_required: true
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1319
1359
|
p2.outro(
|
|
1320
1360
|
pc2.green(
|
|
1321
1361
|
`Manifest updated. Existing ${labels.pair} in ${pc2.cyan(path2.relative(root, envFile))} kept as-is.`
|
|
@@ -1342,17 +1382,15 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1342
1382
|
bail(err.message);
|
|
1343
1383
|
}
|
|
1344
1384
|
spinner4.start("Provisioning app");
|
|
1345
|
-
let
|
|
1346
|
-
let clientSecret;
|
|
1347
|
-
let publicClientId;
|
|
1385
|
+
let provisioned;
|
|
1348
1386
|
try {
|
|
1349
|
-
|
|
1387
|
+
provisioned = await provisionApp(
|
|
1350
1388
|
opts.api,
|
|
1351
1389
|
apiJwt,
|
|
1352
1390
|
tenant.id,
|
|
1353
1391
|
appId,
|
|
1354
1392
|
opts.appServicePath
|
|
1355
|
-
)
|
|
1393
|
+
);
|
|
1356
1394
|
spinner4.stop("Provisioned");
|
|
1357
1395
|
} catch (err) {
|
|
1358
1396
|
spinner4.stop("Failed to provision app");
|
|
@@ -1364,8 +1402,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1364
1402
|
opts,
|
|
1365
1403
|
tenant,
|
|
1366
1404
|
appId,
|
|
1367
|
-
|
|
1368
|
-
false
|
|
1405
|
+
provisioned,
|
|
1406
|
+
false,
|
|
1407
|
+
legacy
|
|
1369
1408
|
);
|
|
1370
1409
|
return { ...registerTelemetry, is_new_app: true };
|
|
1371
1410
|
}
|
|
@@ -1546,7 +1585,6 @@ async function listing(argv = [], runOpts = {}) {
|
|
|
1546
1585
|
bail(err.message);
|
|
1547
1586
|
}
|
|
1548
1587
|
const identityJwt = jwt;
|
|
1549
|
-
jwt = apiTokenFor(jwt);
|
|
1550
1588
|
spinner4.start("Fetching tenants");
|
|
1551
1589
|
let tenants;
|
|
1552
1590
|
try {
|
|
@@ -1666,6 +1704,17 @@ function templateTech(template) {
|
|
|
1666
1704
|
);
|
|
1667
1705
|
return m?.[1] ?? template.packageName;
|
|
1668
1706
|
}
|
|
1707
|
+
var NODE_TOKEN_BLOCK_ZITADEL = `// The service account mints its token at the issuer's token endpoint via the client_credentials
|
|
1708
|
+
// grant. Zitadel rejects a client_credentials request that asks for no scope.
|
|
1709
|
+
const TOKEN_URL = \`\${JTL_ISSUER}/oauth/v2/token\`;
|
|
1710
|
+
const TOKEN_SCOPE = 'openid';`;
|
|
1711
|
+
var NODE_TOKEN_BLOCK_LEGACY = `// Legacy Ory client-credentials M2M: mint the token at the Ory auth host. \`npm run register\` writes
|
|
1712
|
+
// JTL_AUTH_BASE for a --legacy app; the fallback is production. No scope is requested.
|
|
1713
|
+
const AUTH_BASE = (process.env.JTL_AUTH_BASE || 'https://auth.jtl-cloud.com').replace(/\\/$/, '');
|
|
1714
|
+
const TOKEN_URL = \`\${AUTH_BASE}/oauth2/token\`;
|
|
1715
|
+
const TOKEN_SCOPE = '';`;
|
|
1716
|
+
var NODE_ENV_EXAMPLE_ZITADEL = "# The service account mints its token at JTL_ISSUER + /oauth/v2/token; no extra config needed.";
|
|
1717
|
+
var NODE_ENV_EXAMPLE_LEGACY = "# Legacy Ory M2M: the backend mints its token at JTL_AUTH_BASE + /oauth2/token; `npm run register` writes JTL_AUTH_BASE.";
|
|
1669
1718
|
function toPascalCase(kebab) {
|
|
1670
1719
|
return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1671
1720
|
}
|
|
@@ -1774,7 +1823,7 @@ function applyManifestAuthentication(appJsonPath, opts) {
|
|
|
1774
1823
|
postLogoutRedirectUris: [`${origin}/`]
|
|
1775
1824
|
};
|
|
1776
1825
|
}
|
|
1777
|
-
if (backend) {
|
|
1826
|
+
if (backend && !opts.legacy) {
|
|
1778
1827
|
authentication.serviceAccount = {
|
|
1779
1828
|
clientCredentials: true,
|
|
1780
1829
|
description: `${opts.appName} backend service account`
|
|
@@ -1803,8 +1852,14 @@ async function scaffold({
|
|
|
1803
1852
|
shared,
|
|
1804
1853
|
frontendPort = DEFAULT_FRONTEND_PORT,
|
|
1805
1854
|
authentication,
|
|
1855
|
+
legacy = false,
|
|
1806
1856
|
targetDir: customTargetDir
|
|
1807
1857
|
}) {
|
|
1858
|
+
if (legacy && backend && templateTech(backend) !== "node") {
|
|
1859
|
+
throw new Error(
|
|
1860
|
+
`--legacy is currently only supported for the node backend (got "${templateTech(backend)}")`
|
|
1861
|
+
);
|
|
1862
|
+
}
|
|
1808
1863
|
const targetDir = customTargetDir ?? path4.resolve(process.cwd(), appName);
|
|
1809
1864
|
const baseReplacements = buildPlaceholders({
|
|
1810
1865
|
appName,
|
|
@@ -1837,12 +1892,37 @@ async function scaffold({
|
|
|
1837
1892
|
backend.meta.type,
|
|
1838
1893
|
appName
|
|
1839
1894
|
);
|
|
1895
|
+
if (legacy) {
|
|
1896
|
+
const backendIndex = path4.join(backendDest, "src", "index.ts");
|
|
1897
|
+
const src = fs4.readFileSync(backendIndex, "utf-8");
|
|
1898
|
+
if (!src.includes(NODE_TOKEN_BLOCK_ZITADEL)) {
|
|
1899
|
+
throw new Error(
|
|
1900
|
+
"node backend token block not found; the --legacy swap is out of date with the template"
|
|
1901
|
+
);
|
|
1902
|
+
}
|
|
1903
|
+
fs4.writeFileSync(
|
|
1904
|
+
backendIndex,
|
|
1905
|
+
src.replace(NODE_TOKEN_BLOCK_ZITADEL, NODE_TOKEN_BLOCK_LEGACY)
|
|
1906
|
+
);
|
|
1907
|
+
const envExample = path4.join(backendDest, ".env.example");
|
|
1908
|
+
const env = fs4.readFileSync(envExample, "utf-8");
|
|
1909
|
+
if (!env.includes(NODE_ENV_EXAMPLE_ZITADEL)) {
|
|
1910
|
+
throw new Error(
|
|
1911
|
+
"node backend .env.example token line not found; the --legacy swap is out of date with the template"
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
fs4.writeFileSync(
|
|
1915
|
+
envExample,
|
|
1916
|
+
env.replace(NODE_ENV_EXAMPLE_ZITADEL, NODE_ENV_EXAMPLE_LEGACY)
|
|
1917
|
+
);
|
|
1918
|
+
}
|
|
1840
1919
|
}
|
|
1841
1920
|
if (authentication) {
|
|
1842
1921
|
applyManifestAuthentication(path4.join(targetDir, "app.json"), {
|
|
1843
1922
|
appName,
|
|
1844
1923
|
frontendPort,
|
|
1845
|
-
authentication
|
|
1924
|
+
authentication,
|
|
1925
|
+
legacy
|
|
1846
1926
|
});
|
|
1847
1927
|
}
|
|
1848
1928
|
const runtimeVersions = backend?.meta.runtimeVersions ?? {};
|
|
@@ -1874,10 +1954,12 @@ async function scaffold({
|
|
|
1874
1954
|
turbo: "^2.5.5"
|
|
1875
1955
|
},
|
|
1876
1956
|
// The stack the developer picked, recorded so `register` can report it in telemetry without
|
|
1877
|
-
// guessing from the generated files.
|
|
1957
|
+
// guessing from the generated files. `legacy` tells register to persist the Ory M2M client
|
|
1958
|
+
// instead of the Zitadel service account.
|
|
1878
1959
|
jtlCloudApp: {
|
|
1879
1960
|
backend: backend ? templateTech(backend) : null,
|
|
1880
|
-
frontend: frontend ? templateTech(frontend) : null
|
|
1961
|
+
frontend: frontend ? templateTech(frontend) : null,
|
|
1962
|
+
legacy
|
|
1881
1963
|
}
|
|
1882
1964
|
};
|
|
1883
1965
|
fs4.writeFileSync(
|
|
@@ -1924,7 +2006,7 @@ import os2 from "os";
|
|
|
1924
2006
|
import path6 from "path";
|
|
1925
2007
|
import { PostHog } from "posthog-node";
|
|
1926
2008
|
var TOKEN = "phc_SkFAxTIa4RUQxotci1Ftdw7xefZK2Ax0LEIGcl8zuD2";
|
|
1927
|
-
var CLI_VERSION = "0.
|
|
2009
|
+
var CLI_VERSION = "0.8.0";
|
|
1928
2010
|
var client = TOKEN ? new PostHog(TOKEN, { host: "https://e.jtl-cloud.com" }) : null;
|
|
1929
2011
|
function telemetryDisabledByEnv() {
|
|
1930
2012
|
const dnt = process.env.DO_NOT_TRACK;
|
|
@@ -1993,11 +2075,11 @@ async function shutdownPostHog() {
|
|
|
1993
2075
|
|
|
1994
2076
|
// src/index.ts
|
|
1995
2077
|
var NONE = "__none__";
|
|
1996
|
-
function scaffoldChoice(backend, frontend,
|
|
2078
|
+
function scaffoldChoice(backend, frontend, legacy) {
|
|
1997
2079
|
const hasBackend = Boolean(backend);
|
|
1998
2080
|
const hasFrontend = Boolean(frontend);
|
|
1999
2081
|
return {
|
|
2000
|
-
|
|
2082
|
+
legacy,
|
|
2001
2083
|
backend_tech: hasBackend ? templateTech(backend) : "none",
|
|
2002
2084
|
frontend_tech: hasFrontend ? templateTech(frontend) : "none",
|
|
2003
2085
|
has_backend: hasBackend,
|
|
@@ -2022,8 +2104,17 @@ function cancelScaffold() {
|
|
|
2022
2104
|
p4.cancel("Cancelled.");
|
|
2023
2105
|
process.exit(0);
|
|
2024
2106
|
}
|
|
2025
|
-
async function
|
|
2026
|
-
|
|
2107
|
+
async function runScaffold() {
|
|
2108
|
+
console.log();
|
|
2109
|
+
p4.intro(pc4.bgCyan(pc4.black(" create-cloud-app ")));
|
|
2110
|
+
const templates = discoverTemplates();
|
|
2111
|
+
const backends = getTemplatesByType(templates, "backend");
|
|
2112
|
+
const frontends = getTemplatesByType(templates, "frontend");
|
|
2113
|
+
const shared = templates.find((t) => t.meta.type === "shared");
|
|
2114
|
+
if (!backends.length || !frontends.length || !shared) {
|
|
2115
|
+
p4.cancel("No templates found. Ensure template packages are installed.");
|
|
2116
|
+
process.exit(1);
|
|
2117
|
+
}
|
|
2027
2118
|
const answers = await p4.group(
|
|
2028
2119
|
{
|
|
2029
2120
|
appName: () => p4.text({
|
|
@@ -2045,7 +2136,7 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2045
2136
|
options: halfSelectOptions(backends, "no backend")
|
|
2046
2137
|
})
|
|
2047
2138
|
},
|
|
2048
|
-
|
|
2139
|
+
{ onCancel: cancelScaffold }
|
|
2049
2140
|
);
|
|
2050
2141
|
const selectedFrontend = frontends.find(
|
|
2051
2142
|
(t) => t.packageName === answers.frontend
|
|
@@ -2057,6 +2148,11 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2057
2148
|
p4.cancel("Pick at least a frontend or a backend.");
|
|
2058
2149
|
process.exit(1);
|
|
2059
2150
|
}
|
|
2151
|
+
const legacy = process.argv.includes("--legacy");
|
|
2152
|
+
if (legacy && selectedBackend && templateTech(selectedBackend) !== "node") {
|
|
2153
|
+
p4.cancel("--legacy is only supported for the node backend.");
|
|
2154
|
+
process.exit(1);
|
|
2155
|
+
}
|
|
2060
2156
|
const s = p4.spinner();
|
|
2061
2157
|
s.start("Scaffolding project");
|
|
2062
2158
|
const frontendPort = selectedFrontend ? await pickFrontendPort() : void 0;
|
|
@@ -2070,7 +2166,8 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2070
2166
|
authentication: {
|
|
2071
2167
|
frontend: Boolean(selectedFrontend),
|
|
2072
2168
|
backend: Boolean(selectedBackend)
|
|
2073
|
-
}
|
|
2169
|
+
},
|
|
2170
|
+
legacy
|
|
2074
2171
|
});
|
|
2075
2172
|
s.stop("Project scaffolded");
|
|
2076
2173
|
if (selectedFrontend) {
|
|
@@ -2079,74 +2176,15 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2079
2176
|
"Frontend login"
|
|
2080
2177
|
);
|
|
2081
2178
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
console.log();
|
|
2088
|
-
p4.intro(pc4.bgCyan(pc4.black(" create-cloud-app ")));
|
|
2089
|
-
const templates = discoverTemplates();
|
|
2090
|
-
const backends = getTemplatesByType(templates, "backend");
|
|
2091
|
-
const frontends = getTemplatesByType(templates, "frontend");
|
|
2092
|
-
const shared = templates.find((t) => t.meta.type === "shared");
|
|
2093
|
-
if (!backends.length || !frontends.length || !shared) {
|
|
2094
|
-
p4.cancel("No templates found. Ensure template packages are installed.");
|
|
2095
|
-
process.exit(1);
|
|
2096
|
-
}
|
|
2097
|
-
if (process.argv.includes("--next")) {
|
|
2098
|
-
return runNextScaffold(backends, frontends, shared);
|
|
2179
|
+
if (legacy && selectedBackend) {
|
|
2180
|
+
p4.note(
|
|
2181
|
+
"The backend is scaffolded for the legacy Ory M2M flow. `npm run register` will write its Ory client credentials.",
|
|
2182
|
+
"Legacy backend"
|
|
2183
|
+
);
|
|
2099
2184
|
}
|
|
2100
|
-
const answers = await p4.group(
|
|
2101
|
-
{
|
|
2102
|
-
appName: () => p4.text({
|
|
2103
|
-
message: "App name",
|
|
2104
|
-
placeholder: "my-jtl-app",
|
|
2105
|
-
validate: (value) => {
|
|
2106
|
-
if (!value) return "App name is required";
|
|
2107
|
-
if (!/^[a-z0-9-]+$/.test(value))
|
|
2108
|
-
return "Use lowercase letters, numbers, and hyphens only";
|
|
2109
|
-
}
|
|
2110
|
-
}),
|
|
2111
|
-
description: () => p4.text({
|
|
2112
|
-
message: "Description",
|
|
2113
|
-
placeholder: "A JTL Platform app"
|
|
2114
|
-
}),
|
|
2115
|
-
backend: () => p4.select({
|
|
2116
|
-
message: "Backend",
|
|
2117
|
-
options: templateSelectOptions(backends)
|
|
2118
|
-
}),
|
|
2119
|
-
frontend: () => p4.select({
|
|
2120
|
-
message: "Frontend",
|
|
2121
|
-
options: templateSelectOptions(frontends)
|
|
2122
|
-
})
|
|
2123
|
-
},
|
|
2124
|
-
{
|
|
2125
|
-
onCancel: () => {
|
|
2126
|
-
p4.cancel("Cancelled.");
|
|
2127
|
-
process.exit(0);
|
|
2128
|
-
}
|
|
2129
|
-
}
|
|
2130
|
-
);
|
|
2131
|
-
const selectedBackend = templates.find(
|
|
2132
|
-
(t) => t.packageName === answers.backend
|
|
2133
|
-
);
|
|
2134
|
-
const selectedFrontend = templates.find(
|
|
2135
|
-
(t) => t.packageName === answers.frontend
|
|
2136
|
-
);
|
|
2137
|
-
const s = p4.spinner();
|
|
2138
|
-
s.start("Scaffolding project");
|
|
2139
|
-
await scaffold({
|
|
2140
|
-
appName: answers.appName,
|
|
2141
|
-
description: answers.description || "A JTL Platform app",
|
|
2142
|
-
backend: selectedBackend,
|
|
2143
|
-
frontend: selectedFrontend,
|
|
2144
|
-
shared
|
|
2145
|
-
});
|
|
2146
|
-
s.stop("Project scaffolded");
|
|
2147
2185
|
printNextSteps(answers.appName);
|
|
2148
2186
|
p4.outro(pc4.green("Done! Happy building."));
|
|
2149
|
-
return scaffoldChoice(selectedBackend, selectedFrontend,
|
|
2187
|
+
return scaffoldChoice(selectedBackend, selectedFrontend, legacy);
|
|
2150
2188
|
}
|
|
2151
2189
|
function printNextSteps(appName) {
|
|
2152
2190
|
p4.note(
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtl-software/create-cloud-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for scaffolding JTL Platform cloud apps",
|
|
6
6
|
"bin": {
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@clack/prompts": "^1.7.0",
|
|
21
|
-
"@jtl-software/cloud-app-template-backend-dotnet": "0.0
|
|
22
|
-
"@jtl-software/cloud-app-template-backend-node": "0.
|
|
23
|
-
"@jtl-software/cloud-app-template-backend-php": "0.
|
|
24
|
-
"@jtl-software/cloud-app-template-frontend-react": "0.3.
|
|
21
|
+
"@jtl-software/cloud-app-template-backend-dotnet": "0.1.0",
|
|
22
|
+
"@jtl-software/cloud-app-template-backend-node": "0.3.0",
|
|
23
|
+
"@jtl-software/cloud-app-template-backend-php": "0.2.0",
|
|
24
|
+
"@jtl-software/cloud-app-template-frontend-react": "0.3.1",
|
|
25
25
|
"@jtl-software/cloud-app-template-shared": "0.2.0",
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
27
|
"posthog-node": "^5.51.6"
|