@jtl-software/create-cloud-app 0.7.0 → 0.8.1
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 +148 -109
- 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 = {};
|
|
@@ -791,7 +791,8 @@ function writeBackendCredentials(backend, clientId, clientSecret, issuer, apiBas
|
|
|
791
791
|
...settings.JtlPlatform ?? {},
|
|
792
792
|
ClientId: clientId,
|
|
793
793
|
ClientSecret: clientSecret,
|
|
794
|
-
...issuer ? { Issuer: issuer } : {}
|
|
794
|
+
...issuer ? { Issuer: issuer } : {},
|
|
795
|
+
...appId ? { AppId: appId } : {}
|
|
795
796
|
};
|
|
796
797
|
fs2.writeFileSync(
|
|
797
798
|
backend.settingsFile,
|
|
@@ -968,7 +969,8 @@ async function listTenantApps(apiBaseUrl, jwt, tenantId, appServicePath = "app-s
|
|
|
968
969
|
clientId: entry.clientId,
|
|
969
970
|
provisioningStatus: entry.activeVersion?.provisioningStatus,
|
|
970
971
|
isInstalled: entry.isInstalled,
|
|
971
|
-
publicClientId: entry.credentials?.publicClient?.clientId
|
|
972
|
+
publicClientId: entry.credentials?.publicClient?.clientId,
|
|
973
|
+
serviceAccountClientId: entry.credentials?.serviceAccount?.clientId
|
|
972
974
|
};
|
|
973
975
|
}).filter((a) => a !== null);
|
|
974
976
|
}
|
|
@@ -1029,13 +1031,30 @@ async function provisionApp(apiBaseUrl, jwt, tenantId, appId, appServicePath = "
|
|
|
1029
1031
|
`provision response missing clientId or clientSecret: ${JSON.stringify(provisioned)}`
|
|
1030
1032
|
);
|
|
1031
1033
|
}
|
|
1034
|
+
const sa = provisioned.credentials?.serviceAccount;
|
|
1032
1035
|
return {
|
|
1033
1036
|
clientId,
|
|
1034
1037
|
clientSecret,
|
|
1035
|
-
publicClientId: provisioned.credentials?.publicClient?.clientId
|
|
1038
|
+
publicClientId: provisioned.credentials?.publicClient?.clientId,
|
|
1039
|
+
serviceAccount: sa?.clientId && sa.clientSecret ? { clientId: sa.clientId, clientSecret: sa.clientSecret } : void 0
|
|
1036
1040
|
};
|
|
1037
1041
|
}
|
|
1038
|
-
|
|
1042
|
+
function resolveBackendAuth(provisioned, legacy = false) {
|
|
1043
|
+
if (legacy) {
|
|
1044
|
+
return {
|
|
1045
|
+
clientId: provisioned.clientId,
|
|
1046
|
+
clientSecret: provisioned.clientSecret
|
|
1047
|
+
};
|
|
1048
|
+
}
|
|
1049
|
+
const sa = provisioned.serviceAccount;
|
|
1050
|
+
if (!sa) {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
"Provision response did not include a service account; the backend cannot authenticate."
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
return { clientId: sa.clientId, clientSecret: sa.clientSecret };
|
|
1056
|
+
}
|
|
1057
|
+
async function applyProvisionResult(root, backend, opts, tenant, appId, provisioned, isInstalled, legacy) {
|
|
1039
1058
|
if (provisioned.publicClientId) {
|
|
1040
1059
|
const written = writeFrontendPublicClient(
|
|
1041
1060
|
root,
|
|
@@ -1051,13 +1070,14 @@ async function applyProvisionResult(root, backend, opts, tenant, appId, provisio
|
|
|
1051
1070
|
}
|
|
1052
1071
|
writeVerifyDemoEnv(root, backend);
|
|
1053
1072
|
if (backend) {
|
|
1073
|
+
const auth = resolveBackendAuth(provisioned, legacy);
|
|
1054
1074
|
await persistCredentials(
|
|
1055
1075
|
backend,
|
|
1056
|
-
|
|
1057
|
-
|
|
1076
|
+
auth.clientId,
|
|
1077
|
+
auth.clientSecret,
|
|
1058
1078
|
root,
|
|
1059
1079
|
opts,
|
|
1060
|
-
{ hub: opts.hub, tenantSlug: tenant.slug, appId, isInstalled }
|
|
1080
|
+
{ hub: opts.hub, tenantSlug: tenant.slug, appId, isInstalled, legacy }
|
|
1061
1081
|
);
|
|
1062
1082
|
return;
|
|
1063
1083
|
}
|
|
@@ -1109,7 +1129,8 @@ async function persistCredentials(backend, clientId, clientSecret, root, opts, c
|
|
|
1109
1129
|
clientSecret,
|
|
1110
1130
|
opts.auth,
|
|
1111
1131
|
opts.api,
|
|
1112
|
-
context.appId
|
|
1132
|
+
context.appId,
|
|
1133
|
+
context.legacy ? authBaseFromApi(opts.api) : void 0
|
|
1113
1134
|
);
|
|
1114
1135
|
printHubLink(
|
|
1115
1136
|
context.hub,
|
|
@@ -1157,11 +1178,18 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1157
1178
|
const manifest = readManifest(path2.join(root, "app.json"));
|
|
1158
1179
|
const summary = summarizeManifest(manifest);
|
|
1159
1180
|
const stack = readScaffoldStack(root);
|
|
1181
|
+
const legacy = stack?.legacy ?? false;
|
|
1160
1182
|
const auth = manifest.authentication;
|
|
1183
|
+
if (!legacy && backend && !auth?.serviceAccount) {
|
|
1184
|
+
bail(
|
|
1185
|
+
"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."
|
|
1186
|
+
);
|
|
1187
|
+
}
|
|
1161
1188
|
const hasFrontend = stack ? !!stack.frontend : !!auth?.publicClient;
|
|
1162
1189
|
const hasBackend = stack ? !!stack.backend : !!auth?.serviceAccount;
|
|
1163
1190
|
const registerTelemetry = {
|
|
1164
1191
|
environment: environmentFromApi(opts.api),
|
|
1192
|
+
legacy,
|
|
1165
1193
|
has_frontend: hasFrontend,
|
|
1166
1194
|
has_backend: hasBackend,
|
|
1167
1195
|
backend_tech: stack?.backend ?? (hasBackend ? "unknown" : "none"),
|
|
@@ -1170,7 +1198,7 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1170
1198
|
};
|
|
1171
1199
|
for (; ; ) {
|
|
1172
1200
|
const identityJwt = jwt;
|
|
1173
|
-
const apiJwt =
|
|
1201
|
+
const apiJwt = jwt;
|
|
1174
1202
|
spinner4.start("Fetching tenants");
|
|
1175
1203
|
let tenants;
|
|
1176
1204
|
try {
|
|
@@ -1259,9 +1287,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1259
1287
|
}
|
|
1260
1288
|
if (existing.provisioningStatus === "DRAFT") {
|
|
1261
1289
|
spinner4.start("Provisioning app");
|
|
1262
|
-
let
|
|
1290
|
+
let provisioned2;
|
|
1263
1291
|
try {
|
|
1264
|
-
|
|
1292
|
+
provisioned2 = await provisionApp(
|
|
1265
1293
|
opts.api,
|
|
1266
1294
|
apiJwt,
|
|
1267
1295
|
tenant.id,
|
|
@@ -1279,8 +1307,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1279
1307
|
opts,
|
|
1280
1308
|
tenant,
|
|
1281
1309
|
existing.id,
|
|
1282
|
-
|
|
1283
|
-
existing.isInstalled ?? false
|
|
1310
|
+
provisioned2,
|
|
1311
|
+
existing.isInstalled ?? false,
|
|
1312
|
+
legacy
|
|
1284
1313
|
);
|
|
1285
1314
|
return { ...registerTelemetry, is_new_app: false };
|
|
1286
1315
|
}
|
|
@@ -1316,6 +1345,18 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1316
1345
|
);
|
|
1317
1346
|
return { ...registerTelemetry, is_new_app: false };
|
|
1318
1347
|
}
|
|
1348
|
+
if (!legacy && localCreds.clientId !== existing.serviceAccountClientId) {
|
|
1349
|
+
p2.outro(
|
|
1350
|
+
pc2.yellow(
|
|
1351
|
+
`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}.`
|
|
1352
|
+
)
|
|
1353
|
+
);
|
|
1354
|
+
return {
|
|
1355
|
+
...registerTelemetry,
|
|
1356
|
+
is_new_app: false,
|
|
1357
|
+
credential_migration_required: true
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1319
1360
|
p2.outro(
|
|
1320
1361
|
pc2.green(
|
|
1321
1362
|
`Manifest updated. Existing ${labels.pair} in ${pc2.cyan(path2.relative(root, envFile))} kept as-is.`
|
|
@@ -1342,17 +1383,15 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1342
1383
|
bail(err.message);
|
|
1343
1384
|
}
|
|
1344
1385
|
spinner4.start("Provisioning app");
|
|
1345
|
-
let
|
|
1346
|
-
let clientSecret;
|
|
1347
|
-
let publicClientId;
|
|
1386
|
+
let provisioned;
|
|
1348
1387
|
try {
|
|
1349
|
-
|
|
1388
|
+
provisioned = await provisionApp(
|
|
1350
1389
|
opts.api,
|
|
1351
1390
|
apiJwt,
|
|
1352
1391
|
tenant.id,
|
|
1353
1392
|
appId,
|
|
1354
1393
|
opts.appServicePath
|
|
1355
|
-
)
|
|
1394
|
+
);
|
|
1356
1395
|
spinner4.stop("Provisioned");
|
|
1357
1396
|
} catch (err) {
|
|
1358
1397
|
spinner4.stop("Failed to provision app");
|
|
@@ -1364,8 +1403,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1364
1403
|
opts,
|
|
1365
1404
|
tenant,
|
|
1366
1405
|
appId,
|
|
1367
|
-
|
|
1368
|
-
false
|
|
1406
|
+
provisioned,
|
|
1407
|
+
false,
|
|
1408
|
+
legacy
|
|
1369
1409
|
);
|
|
1370
1410
|
return { ...registerTelemetry, is_new_app: true };
|
|
1371
1411
|
}
|
|
@@ -1546,7 +1586,6 @@ async function listing(argv = [], runOpts = {}) {
|
|
|
1546
1586
|
bail(err.message);
|
|
1547
1587
|
}
|
|
1548
1588
|
const identityJwt = jwt;
|
|
1549
|
-
jwt = apiTokenFor(jwt);
|
|
1550
1589
|
spinner4.start("Fetching tenants");
|
|
1551
1590
|
let tenants;
|
|
1552
1591
|
try {
|
|
@@ -1666,6 +1705,17 @@ function templateTech(template) {
|
|
|
1666
1705
|
);
|
|
1667
1706
|
return m?.[1] ?? template.packageName;
|
|
1668
1707
|
}
|
|
1708
|
+
var NODE_TOKEN_BLOCK_ZITADEL = `// The service account mints its token at the issuer's token endpoint via the client_credentials
|
|
1709
|
+
// grant. Zitadel rejects a client_credentials request that asks for no scope.
|
|
1710
|
+
const TOKEN_URL = \`\${JTL_ISSUER}/oauth/v2/token\`;
|
|
1711
|
+
const TOKEN_SCOPE = 'openid';`;
|
|
1712
|
+
var NODE_TOKEN_BLOCK_LEGACY = `// Legacy Ory client-credentials M2M: mint the token at the Ory auth host. \`npm run register\` writes
|
|
1713
|
+
// JTL_AUTH_BASE for a --legacy app; the fallback is production. No scope is requested.
|
|
1714
|
+
const AUTH_BASE = (process.env.JTL_AUTH_BASE || 'https://auth.jtl-cloud.com').replace(/\\/$/, '');
|
|
1715
|
+
const TOKEN_URL = \`\${AUTH_BASE}/oauth2/token\`;
|
|
1716
|
+
const TOKEN_SCOPE = '';`;
|
|
1717
|
+
var NODE_ENV_EXAMPLE_ZITADEL = "# The service account mints its token at JTL_ISSUER + /oauth/v2/token; no extra config needed.";
|
|
1718
|
+
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
1719
|
function toPascalCase(kebab) {
|
|
1670
1720
|
return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1671
1721
|
}
|
|
@@ -1774,7 +1824,7 @@ function applyManifestAuthentication(appJsonPath, opts) {
|
|
|
1774
1824
|
postLogoutRedirectUris: [`${origin}/`]
|
|
1775
1825
|
};
|
|
1776
1826
|
}
|
|
1777
|
-
if (backend) {
|
|
1827
|
+
if (backend && !opts.legacy) {
|
|
1778
1828
|
authentication.serviceAccount = {
|
|
1779
1829
|
clientCredentials: true,
|
|
1780
1830
|
description: `${opts.appName} backend service account`
|
|
@@ -1803,8 +1853,14 @@ async function scaffold({
|
|
|
1803
1853
|
shared,
|
|
1804
1854
|
frontendPort = DEFAULT_FRONTEND_PORT,
|
|
1805
1855
|
authentication,
|
|
1856
|
+
legacy = false,
|
|
1806
1857
|
targetDir: customTargetDir
|
|
1807
1858
|
}) {
|
|
1859
|
+
if (legacy && backend && templateTech(backend) !== "node") {
|
|
1860
|
+
throw new Error(
|
|
1861
|
+
`--legacy is currently only supported for the node backend (got "${templateTech(backend)}")`
|
|
1862
|
+
);
|
|
1863
|
+
}
|
|
1808
1864
|
const targetDir = customTargetDir ?? path4.resolve(process.cwd(), appName);
|
|
1809
1865
|
const baseReplacements = buildPlaceholders({
|
|
1810
1866
|
appName,
|
|
@@ -1837,12 +1893,37 @@ async function scaffold({
|
|
|
1837
1893
|
backend.meta.type,
|
|
1838
1894
|
appName
|
|
1839
1895
|
);
|
|
1896
|
+
if (legacy) {
|
|
1897
|
+
const backendIndex = path4.join(backendDest, "src", "index.ts");
|
|
1898
|
+
const src = fs4.readFileSync(backendIndex, "utf-8");
|
|
1899
|
+
if (!src.includes(NODE_TOKEN_BLOCK_ZITADEL)) {
|
|
1900
|
+
throw new Error(
|
|
1901
|
+
"node backend token block not found; the --legacy swap is out of date with the template"
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
fs4.writeFileSync(
|
|
1905
|
+
backendIndex,
|
|
1906
|
+
src.replace(NODE_TOKEN_BLOCK_ZITADEL, NODE_TOKEN_BLOCK_LEGACY)
|
|
1907
|
+
);
|
|
1908
|
+
const envExample = path4.join(backendDest, ".env.example");
|
|
1909
|
+
const env = fs4.readFileSync(envExample, "utf-8");
|
|
1910
|
+
if (!env.includes(NODE_ENV_EXAMPLE_ZITADEL)) {
|
|
1911
|
+
throw new Error(
|
|
1912
|
+
"node backend .env.example token line not found; the --legacy swap is out of date with the template"
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
fs4.writeFileSync(
|
|
1916
|
+
envExample,
|
|
1917
|
+
env.replace(NODE_ENV_EXAMPLE_ZITADEL, NODE_ENV_EXAMPLE_LEGACY)
|
|
1918
|
+
);
|
|
1919
|
+
}
|
|
1840
1920
|
}
|
|
1841
1921
|
if (authentication) {
|
|
1842
1922
|
applyManifestAuthentication(path4.join(targetDir, "app.json"), {
|
|
1843
1923
|
appName,
|
|
1844
1924
|
frontendPort,
|
|
1845
|
-
authentication
|
|
1925
|
+
authentication,
|
|
1926
|
+
legacy
|
|
1846
1927
|
});
|
|
1847
1928
|
}
|
|
1848
1929
|
const runtimeVersions = backend?.meta.runtimeVersions ?? {};
|
|
@@ -1874,10 +1955,12 @@ async function scaffold({
|
|
|
1874
1955
|
turbo: "^2.5.5"
|
|
1875
1956
|
},
|
|
1876
1957
|
// The stack the developer picked, recorded so `register` can report it in telemetry without
|
|
1877
|
-
// guessing from the generated files.
|
|
1958
|
+
// guessing from the generated files. `legacy` tells register to persist the Ory M2M client
|
|
1959
|
+
// instead of the Zitadel service account.
|
|
1878
1960
|
jtlCloudApp: {
|
|
1879
1961
|
backend: backend ? templateTech(backend) : null,
|
|
1880
|
-
frontend: frontend ? templateTech(frontend) : null
|
|
1962
|
+
frontend: frontend ? templateTech(frontend) : null,
|
|
1963
|
+
legacy
|
|
1881
1964
|
}
|
|
1882
1965
|
};
|
|
1883
1966
|
fs4.writeFileSync(
|
|
@@ -1924,7 +2007,7 @@ import os2 from "os";
|
|
|
1924
2007
|
import path6 from "path";
|
|
1925
2008
|
import { PostHog } from "posthog-node";
|
|
1926
2009
|
var TOKEN = "phc_SkFAxTIa4RUQxotci1Ftdw7xefZK2Ax0LEIGcl8zuD2";
|
|
1927
|
-
var CLI_VERSION = "0.
|
|
2010
|
+
var CLI_VERSION = "0.8.1";
|
|
1928
2011
|
var client = TOKEN ? new PostHog(TOKEN, { host: "https://e.jtl-cloud.com" }) : null;
|
|
1929
2012
|
function telemetryDisabledByEnv() {
|
|
1930
2013
|
const dnt = process.env.DO_NOT_TRACK;
|
|
@@ -1993,11 +2076,11 @@ async function shutdownPostHog() {
|
|
|
1993
2076
|
|
|
1994
2077
|
// src/index.ts
|
|
1995
2078
|
var NONE = "__none__";
|
|
1996
|
-
function scaffoldChoice(backend, frontend,
|
|
2079
|
+
function scaffoldChoice(backend, frontend, legacy) {
|
|
1997
2080
|
const hasBackend = Boolean(backend);
|
|
1998
2081
|
const hasFrontend = Boolean(frontend);
|
|
1999
2082
|
return {
|
|
2000
|
-
|
|
2083
|
+
legacy,
|
|
2001
2084
|
backend_tech: hasBackend ? templateTech(backend) : "none",
|
|
2002
2085
|
frontend_tech: hasFrontend ? templateTech(frontend) : "none",
|
|
2003
2086
|
has_backend: hasBackend,
|
|
@@ -2022,8 +2105,17 @@ function cancelScaffold() {
|
|
|
2022
2105
|
p4.cancel("Cancelled.");
|
|
2023
2106
|
process.exit(0);
|
|
2024
2107
|
}
|
|
2025
|
-
async function
|
|
2026
|
-
|
|
2108
|
+
async function runScaffold() {
|
|
2109
|
+
console.log();
|
|
2110
|
+
p4.intro(pc4.bgCyan(pc4.black(" create-cloud-app ")));
|
|
2111
|
+
const templates = discoverTemplates();
|
|
2112
|
+
const backends = getTemplatesByType(templates, "backend");
|
|
2113
|
+
const frontends = getTemplatesByType(templates, "frontend");
|
|
2114
|
+
const shared = templates.find((t) => t.meta.type === "shared");
|
|
2115
|
+
if (!backends.length || !frontends.length || !shared) {
|
|
2116
|
+
p4.cancel("No templates found. Ensure template packages are installed.");
|
|
2117
|
+
process.exit(1);
|
|
2118
|
+
}
|
|
2027
2119
|
const answers = await p4.group(
|
|
2028
2120
|
{
|
|
2029
2121
|
appName: () => p4.text({
|
|
@@ -2045,7 +2137,7 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2045
2137
|
options: halfSelectOptions(backends, "no backend")
|
|
2046
2138
|
})
|
|
2047
2139
|
},
|
|
2048
|
-
|
|
2140
|
+
{ onCancel: cancelScaffold }
|
|
2049
2141
|
);
|
|
2050
2142
|
const selectedFrontend = frontends.find(
|
|
2051
2143
|
(t) => t.packageName === answers.frontend
|
|
@@ -2057,6 +2149,11 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2057
2149
|
p4.cancel("Pick at least a frontend or a backend.");
|
|
2058
2150
|
process.exit(1);
|
|
2059
2151
|
}
|
|
2152
|
+
const legacy = process.argv.includes("--legacy");
|
|
2153
|
+
if (legacy && selectedBackend && templateTech(selectedBackend) !== "node") {
|
|
2154
|
+
p4.cancel("--legacy is only supported for the node backend.");
|
|
2155
|
+
process.exit(1);
|
|
2156
|
+
}
|
|
2060
2157
|
const s = p4.spinner();
|
|
2061
2158
|
s.start("Scaffolding project");
|
|
2062
2159
|
const frontendPort = selectedFrontend ? await pickFrontendPort() : void 0;
|
|
@@ -2070,7 +2167,8 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2070
2167
|
authentication: {
|
|
2071
2168
|
frontend: Boolean(selectedFrontend),
|
|
2072
2169
|
backend: Boolean(selectedBackend)
|
|
2073
|
-
}
|
|
2170
|
+
},
|
|
2171
|
+
legacy
|
|
2074
2172
|
});
|
|
2075
2173
|
s.stop("Project scaffolded");
|
|
2076
2174
|
if (selectedFrontend) {
|
|
@@ -2079,74 +2177,15 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2079
2177
|
"Frontend login"
|
|
2080
2178
|
);
|
|
2081
2179
|
}
|
|
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);
|
|
2180
|
+
if (legacy && selectedBackend) {
|
|
2181
|
+
p4.note(
|
|
2182
|
+
"The backend is scaffolded for the legacy Ory M2M flow. `npm run register` will write its Ory client credentials.",
|
|
2183
|
+
"Legacy backend"
|
|
2184
|
+
);
|
|
2099
2185
|
}
|
|
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
2186
|
printNextSteps(answers.appName);
|
|
2148
2187
|
p4.outro(pc4.green("Done! Happy building."));
|
|
2149
|
-
return scaffoldChoice(selectedBackend, selectedFrontend,
|
|
2188
|
+
return scaffoldChoice(selectedBackend, selectedFrontend, legacy);
|
|
2150
2189
|
}
|
|
2151
2190
|
function printNextSteps(appName) {
|
|
2152
2191
|
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.1",
|
|
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.2.0",
|
|
22
|
+
"@jtl-software/cloud-app-template-backend-node": "0.3.0",
|
|
23
|
+
"@jtl-software/cloud-app-template-backend-php": "0.3.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"
|