@jtl-software/create-cloud-app 0.6.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 +152 -112
- package/package.json +9 -9
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 {
|
|
@@ -717,13 +718,14 @@ function environmentFromApi(api) {
|
|
|
717
718
|
if (h.includes("api.jtl-cloud.com")) return "prod";
|
|
718
719
|
return "other";
|
|
719
720
|
}
|
|
720
|
-
function writeFrontendPublicClient(root, authIssuer, publicClientId) {
|
|
721
|
+
function writeFrontendPublicClient(root, authIssuer, publicClientId, apiBase) {
|
|
721
722
|
const frontendDir = detectFrontend(root);
|
|
722
723
|
if (!frontendDir) return null;
|
|
723
724
|
const envFile = path2.join(frontendDir, ".env");
|
|
724
725
|
upsertEnv(envFile, {
|
|
725
726
|
VITE_JTL_CLIENT_ID: publicClientId,
|
|
726
|
-
VITE_JTL_ISSUER: authIssuer
|
|
727
|
+
VITE_JTL_ISSUER: authIssuer,
|
|
728
|
+
...apiBase ? { VITE_JTL_API_BASE: apiBase.replace(/\/$/, "") } : {}
|
|
727
729
|
});
|
|
728
730
|
return envFile;
|
|
729
731
|
}
|
|
@@ -763,19 +765,18 @@ function readExistingCredentials(backend) {
|
|
|
763
765
|
function authBaseFromApi(apiHost) {
|
|
764
766
|
return apiHost.replace(/:\/\/api\./, "://auth.").replace(/\/$/, "");
|
|
765
767
|
}
|
|
766
|
-
function writeBackendCredentials(backend, clientId, clientSecret, issuer, apiBase, appId) {
|
|
767
|
-
const hosts = apiBase ? {
|
|
768
|
-
JTL_API_BASE: apiBase.replace(/\/$/, ""),
|
|
769
|
-
JTL_AUTH_BASE: authBaseFromApi(apiBase)
|
|
770
|
-
} : {};
|
|
768
|
+
function writeBackendCredentials(backend, clientId, clientSecret, issuer, apiBase, appId, authBase) {
|
|
769
|
+
const hosts = apiBase ? { JTL_API_BASE: apiBase.replace(/\/$/, "") } : {};
|
|
771
770
|
if (backend.kind === "node") {
|
|
772
|
-
|
|
771
|
+
const updates = {
|
|
773
772
|
CLIENT_ID: clientId,
|
|
774
|
-
CLIENT_SECRET: clientSecret
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
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);
|
|
779
780
|
return backend.envFile;
|
|
780
781
|
}
|
|
781
782
|
let settings = {};
|
|
@@ -967,7 +968,8 @@ async function listTenantApps(apiBaseUrl, jwt, tenantId, appServicePath = "app-s
|
|
|
967
968
|
clientId: entry.clientId,
|
|
968
969
|
provisioningStatus: entry.activeVersion?.provisioningStatus,
|
|
969
970
|
isInstalled: entry.isInstalled,
|
|
970
|
-
publicClientId: entry.credentials?.publicClient?.clientId
|
|
971
|
+
publicClientId: entry.credentials?.publicClient?.clientId,
|
|
972
|
+
serviceAccountClientId: entry.credentials?.serviceAccount?.clientId
|
|
971
973
|
};
|
|
972
974
|
}).filter((a) => a !== null);
|
|
973
975
|
}
|
|
@@ -1028,34 +1030,53 @@ async function provisionApp(apiBaseUrl, jwt, tenantId, appId, appServicePath = "
|
|
|
1028
1030
|
`provision response missing clientId or clientSecret: ${JSON.stringify(provisioned)}`
|
|
1029
1031
|
);
|
|
1030
1032
|
}
|
|
1033
|
+
const sa = provisioned.credentials?.serviceAccount;
|
|
1031
1034
|
return {
|
|
1032
1035
|
clientId,
|
|
1033
1036
|
clientSecret,
|
|
1034
|
-
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
|
|
1035
1039
|
};
|
|
1036
1040
|
}
|
|
1037
|
-
|
|
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) {
|
|
1038
1057
|
if (provisioned.publicClientId) {
|
|
1039
1058
|
const written = writeFrontendPublicClient(
|
|
1040
1059
|
root,
|
|
1041
1060
|
opts.auth,
|
|
1042
|
-
provisioned.publicClientId
|
|
1061
|
+
provisioned.publicClientId,
|
|
1062
|
+
opts.api
|
|
1043
1063
|
);
|
|
1044
1064
|
if (written) {
|
|
1045
1065
|
p2.log.success(
|
|
1046
|
-
`Wrote VITE_JTL_CLIENT_ID/VITE_JTL_ISSUER to ${pc2.cyan(path2.relative(root, written))}.`
|
|
1066
|
+
`Wrote VITE_JTL_CLIENT_ID/VITE_JTL_ISSUER/VITE_JTL_API_BASE to ${pc2.cyan(path2.relative(root, written))}.`
|
|
1047
1067
|
);
|
|
1048
1068
|
}
|
|
1049
1069
|
}
|
|
1050
1070
|
writeVerifyDemoEnv(root, backend);
|
|
1051
1071
|
if (backend) {
|
|
1072
|
+
const auth = resolveBackendAuth(provisioned, legacy);
|
|
1052
1073
|
await persistCredentials(
|
|
1053
1074
|
backend,
|
|
1054
|
-
|
|
1055
|
-
|
|
1075
|
+
auth.clientId,
|
|
1076
|
+
auth.clientSecret,
|
|
1056
1077
|
root,
|
|
1057
1078
|
opts,
|
|
1058
|
-
{ hub: opts.hub, tenantSlug: tenant.slug, appId, isInstalled }
|
|
1079
|
+
{ hub: opts.hub, tenantSlug: tenant.slug, appId, isInstalled, legacy }
|
|
1059
1080
|
);
|
|
1060
1081
|
return;
|
|
1061
1082
|
}
|
|
@@ -1107,7 +1128,8 @@ async function persistCredentials(backend, clientId, clientSecret, root, opts, c
|
|
|
1107
1128
|
clientSecret,
|
|
1108
1129
|
opts.auth,
|
|
1109
1130
|
opts.api,
|
|
1110
|
-
context.appId
|
|
1131
|
+
context.appId,
|
|
1132
|
+
context.legacy ? authBaseFromApi(opts.api) : void 0
|
|
1111
1133
|
);
|
|
1112
1134
|
printHubLink(
|
|
1113
1135
|
context.hub,
|
|
@@ -1155,11 +1177,18 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1155
1177
|
const manifest = readManifest(path2.join(root, "app.json"));
|
|
1156
1178
|
const summary = summarizeManifest(manifest);
|
|
1157
1179
|
const stack = readScaffoldStack(root);
|
|
1180
|
+
const legacy = stack?.legacy ?? false;
|
|
1158
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
|
+
}
|
|
1159
1187
|
const hasFrontend = stack ? !!stack.frontend : !!auth?.publicClient;
|
|
1160
1188
|
const hasBackend = stack ? !!stack.backend : !!auth?.serviceAccount;
|
|
1161
1189
|
const registerTelemetry = {
|
|
1162
1190
|
environment: environmentFromApi(opts.api),
|
|
1191
|
+
legacy,
|
|
1163
1192
|
has_frontend: hasFrontend,
|
|
1164
1193
|
has_backend: hasBackend,
|
|
1165
1194
|
backend_tech: stack?.backend ?? (hasBackend ? "unknown" : "none"),
|
|
@@ -1168,7 +1197,7 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1168
1197
|
};
|
|
1169
1198
|
for (; ; ) {
|
|
1170
1199
|
const identityJwt = jwt;
|
|
1171
|
-
const apiJwt =
|
|
1200
|
+
const apiJwt = jwt;
|
|
1172
1201
|
spinner4.start("Fetching tenants");
|
|
1173
1202
|
let tenants;
|
|
1174
1203
|
try {
|
|
@@ -1257,9 +1286,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1257
1286
|
}
|
|
1258
1287
|
if (existing.provisioningStatus === "DRAFT") {
|
|
1259
1288
|
spinner4.start("Provisioning app");
|
|
1260
|
-
let
|
|
1289
|
+
let provisioned2;
|
|
1261
1290
|
try {
|
|
1262
|
-
|
|
1291
|
+
provisioned2 = await provisionApp(
|
|
1263
1292
|
opts.api,
|
|
1264
1293
|
apiJwt,
|
|
1265
1294
|
tenant.id,
|
|
@@ -1277,8 +1306,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1277
1306
|
opts,
|
|
1278
1307
|
tenant,
|
|
1279
1308
|
existing.id,
|
|
1280
|
-
|
|
1281
|
-
existing.isInstalled ?? false
|
|
1309
|
+
provisioned2,
|
|
1310
|
+
existing.isInstalled ?? false,
|
|
1311
|
+
legacy
|
|
1282
1312
|
);
|
|
1283
1313
|
return { ...registerTelemetry, is_new_app: false };
|
|
1284
1314
|
}
|
|
@@ -1314,6 +1344,18 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1314
1344
|
);
|
|
1315
1345
|
return { ...registerTelemetry, is_new_app: false };
|
|
1316
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
|
+
}
|
|
1317
1359
|
p2.outro(
|
|
1318
1360
|
pc2.green(
|
|
1319
1361
|
`Manifest updated. Existing ${labels.pair} in ${pc2.cyan(path2.relative(root, envFile))} kept as-is.`
|
|
@@ -1340,17 +1382,15 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1340
1382
|
bail(err.message);
|
|
1341
1383
|
}
|
|
1342
1384
|
spinner4.start("Provisioning app");
|
|
1343
|
-
let
|
|
1344
|
-
let clientSecret;
|
|
1345
|
-
let publicClientId;
|
|
1385
|
+
let provisioned;
|
|
1346
1386
|
try {
|
|
1347
|
-
|
|
1387
|
+
provisioned = await provisionApp(
|
|
1348
1388
|
opts.api,
|
|
1349
1389
|
apiJwt,
|
|
1350
1390
|
tenant.id,
|
|
1351
1391
|
appId,
|
|
1352
1392
|
opts.appServicePath
|
|
1353
|
-
)
|
|
1393
|
+
);
|
|
1354
1394
|
spinner4.stop("Provisioned");
|
|
1355
1395
|
} catch (err) {
|
|
1356
1396
|
spinner4.stop("Failed to provision app");
|
|
@@ -1362,8 +1402,9 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1362
1402
|
opts,
|
|
1363
1403
|
tenant,
|
|
1364
1404
|
appId,
|
|
1365
|
-
|
|
1366
|
-
false
|
|
1405
|
+
provisioned,
|
|
1406
|
+
false,
|
|
1407
|
+
legacy
|
|
1367
1408
|
);
|
|
1368
1409
|
return { ...registerTelemetry, is_new_app: true };
|
|
1369
1410
|
}
|
|
@@ -1544,7 +1585,6 @@ async function listing(argv = [], runOpts = {}) {
|
|
|
1544
1585
|
bail(err.message);
|
|
1545
1586
|
}
|
|
1546
1587
|
const identityJwt = jwt;
|
|
1547
|
-
jwt = apiTokenFor(jwt);
|
|
1548
1588
|
spinner4.start("Fetching tenants");
|
|
1549
1589
|
let tenants;
|
|
1550
1590
|
try {
|
|
@@ -1664,6 +1704,17 @@ function templateTech(template) {
|
|
|
1664
1704
|
);
|
|
1665
1705
|
return m?.[1] ?? template.packageName;
|
|
1666
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.";
|
|
1667
1718
|
function toPascalCase(kebab) {
|
|
1668
1719
|
return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1669
1720
|
}
|
|
@@ -1772,7 +1823,7 @@ function applyManifestAuthentication(appJsonPath, opts) {
|
|
|
1772
1823
|
postLogoutRedirectUris: [`${origin}/`]
|
|
1773
1824
|
};
|
|
1774
1825
|
}
|
|
1775
|
-
if (backend) {
|
|
1826
|
+
if (backend && !opts.legacy) {
|
|
1776
1827
|
authentication.serviceAccount = {
|
|
1777
1828
|
clientCredentials: true,
|
|
1778
1829
|
description: `${opts.appName} backend service account`
|
|
@@ -1801,8 +1852,14 @@ async function scaffold({
|
|
|
1801
1852
|
shared,
|
|
1802
1853
|
frontendPort = DEFAULT_FRONTEND_PORT,
|
|
1803
1854
|
authentication,
|
|
1855
|
+
legacy = false,
|
|
1804
1856
|
targetDir: customTargetDir
|
|
1805
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
|
+
}
|
|
1806
1863
|
const targetDir = customTargetDir ?? path4.resolve(process.cwd(), appName);
|
|
1807
1864
|
const baseReplacements = buildPlaceholders({
|
|
1808
1865
|
appName,
|
|
@@ -1835,12 +1892,37 @@ async function scaffold({
|
|
|
1835
1892
|
backend.meta.type,
|
|
1836
1893
|
appName
|
|
1837
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
|
+
}
|
|
1838
1919
|
}
|
|
1839
1920
|
if (authentication) {
|
|
1840
1921
|
applyManifestAuthentication(path4.join(targetDir, "app.json"), {
|
|
1841
1922
|
appName,
|
|
1842
1923
|
frontendPort,
|
|
1843
|
-
authentication
|
|
1924
|
+
authentication,
|
|
1925
|
+
legacy
|
|
1844
1926
|
});
|
|
1845
1927
|
}
|
|
1846
1928
|
const runtimeVersions = backend?.meta.runtimeVersions ?? {};
|
|
@@ -1872,10 +1954,12 @@ async function scaffold({
|
|
|
1872
1954
|
turbo: "^2.5.5"
|
|
1873
1955
|
},
|
|
1874
1956
|
// The stack the developer picked, recorded so `register` can report it in telemetry without
|
|
1875
|
-
// 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.
|
|
1876
1959
|
jtlCloudApp: {
|
|
1877
1960
|
backend: backend ? templateTech(backend) : null,
|
|
1878
|
-
frontend: frontend ? templateTech(frontend) : null
|
|
1961
|
+
frontend: frontend ? templateTech(frontend) : null,
|
|
1962
|
+
legacy
|
|
1879
1963
|
}
|
|
1880
1964
|
};
|
|
1881
1965
|
fs4.writeFileSync(
|
|
@@ -1922,7 +2006,7 @@ import os2 from "os";
|
|
|
1922
2006
|
import path6 from "path";
|
|
1923
2007
|
import { PostHog } from "posthog-node";
|
|
1924
2008
|
var TOKEN = "phc_SkFAxTIa4RUQxotci1Ftdw7xefZK2Ax0LEIGcl8zuD2";
|
|
1925
|
-
var CLI_VERSION = "0.
|
|
2009
|
+
var CLI_VERSION = "0.8.0";
|
|
1926
2010
|
var client = TOKEN ? new PostHog(TOKEN, { host: "https://e.jtl-cloud.com" }) : null;
|
|
1927
2011
|
function telemetryDisabledByEnv() {
|
|
1928
2012
|
const dnt = process.env.DO_NOT_TRACK;
|
|
@@ -1991,11 +2075,11 @@ async function shutdownPostHog() {
|
|
|
1991
2075
|
|
|
1992
2076
|
// src/index.ts
|
|
1993
2077
|
var NONE = "__none__";
|
|
1994
|
-
function scaffoldChoice(backend, frontend,
|
|
2078
|
+
function scaffoldChoice(backend, frontend, legacy) {
|
|
1995
2079
|
const hasBackend = Boolean(backend);
|
|
1996
2080
|
const hasFrontend = Boolean(frontend);
|
|
1997
2081
|
return {
|
|
1998
|
-
|
|
2082
|
+
legacy,
|
|
1999
2083
|
backend_tech: hasBackend ? templateTech(backend) : "none",
|
|
2000
2084
|
frontend_tech: hasFrontend ? templateTech(frontend) : "none",
|
|
2001
2085
|
has_backend: hasBackend,
|
|
@@ -2020,8 +2104,17 @@ function cancelScaffold() {
|
|
|
2020
2104
|
p4.cancel("Cancelled.");
|
|
2021
2105
|
process.exit(0);
|
|
2022
2106
|
}
|
|
2023
|
-
async function
|
|
2024
|
-
|
|
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
|
+
}
|
|
2025
2118
|
const answers = await p4.group(
|
|
2026
2119
|
{
|
|
2027
2120
|
appName: () => p4.text({
|
|
@@ -2043,7 +2136,7 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2043
2136
|
options: halfSelectOptions(backends, "no backend")
|
|
2044
2137
|
})
|
|
2045
2138
|
},
|
|
2046
|
-
|
|
2139
|
+
{ onCancel: cancelScaffold }
|
|
2047
2140
|
);
|
|
2048
2141
|
const selectedFrontend = frontends.find(
|
|
2049
2142
|
(t) => t.packageName === answers.frontend
|
|
@@ -2055,6 +2148,11 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2055
2148
|
p4.cancel("Pick at least a frontend or a backend.");
|
|
2056
2149
|
process.exit(1);
|
|
2057
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
|
+
}
|
|
2058
2156
|
const s = p4.spinner();
|
|
2059
2157
|
s.start("Scaffolding project");
|
|
2060
2158
|
const frontendPort = selectedFrontend ? await pickFrontendPort() : void 0;
|
|
@@ -2068,7 +2166,8 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2068
2166
|
authentication: {
|
|
2069
2167
|
frontend: Boolean(selectedFrontend),
|
|
2070
2168
|
backend: Boolean(selectedBackend)
|
|
2071
|
-
}
|
|
2169
|
+
},
|
|
2170
|
+
legacy
|
|
2072
2171
|
});
|
|
2073
2172
|
s.stop("Project scaffolded");
|
|
2074
2173
|
if (selectedFrontend) {
|
|
@@ -2077,74 +2176,15 @@ async function runNextScaffold(backends, frontends, shared) {
|
|
|
2077
2176
|
"Frontend login"
|
|
2078
2177
|
);
|
|
2079
2178
|
}
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
console.log();
|
|
2086
|
-
p4.intro(pc4.bgCyan(pc4.black(" create-cloud-app ")));
|
|
2087
|
-
const templates = discoverTemplates();
|
|
2088
|
-
const backends = getTemplatesByType(templates, "backend");
|
|
2089
|
-
const frontends = getTemplatesByType(templates, "frontend");
|
|
2090
|
-
const shared = templates.find((t) => t.meta.type === "shared");
|
|
2091
|
-
if (!backends.length || !frontends.length || !shared) {
|
|
2092
|
-
p4.cancel("No templates found. Ensure template packages are installed.");
|
|
2093
|
-
process.exit(1);
|
|
2094
|
-
}
|
|
2095
|
-
if (process.argv.includes("--next")) {
|
|
2096
|
-
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
|
+
);
|
|
2097
2184
|
}
|
|
2098
|
-
const answers = await p4.group(
|
|
2099
|
-
{
|
|
2100
|
-
appName: () => p4.text({
|
|
2101
|
-
message: "App name",
|
|
2102
|
-
placeholder: "my-jtl-app",
|
|
2103
|
-
validate: (value) => {
|
|
2104
|
-
if (!value) return "App name is required";
|
|
2105
|
-
if (!/^[a-z0-9-]+$/.test(value))
|
|
2106
|
-
return "Use lowercase letters, numbers, and hyphens only";
|
|
2107
|
-
}
|
|
2108
|
-
}),
|
|
2109
|
-
description: () => p4.text({
|
|
2110
|
-
message: "Description",
|
|
2111
|
-
placeholder: "A JTL Platform app"
|
|
2112
|
-
}),
|
|
2113
|
-
backend: () => p4.select({
|
|
2114
|
-
message: "Backend",
|
|
2115
|
-
options: templateSelectOptions(backends)
|
|
2116
|
-
}),
|
|
2117
|
-
frontend: () => p4.select({
|
|
2118
|
-
message: "Frontend",
|
|
2119
|
-
options: templateSelectOptions(frontends)
|
|
2120
|
-
})
|
|
2121
|
-
},
|
|
2122
|
-
{
|
|
2123
|
-
onCancel: () => {
|
|
2124
|
-
p4.cancel("Cancelled.");
|
|
2125
|
-
process.exit(0);
|
|
2126
|
-
}
|
|
2127
|
-
}
|
|
2128
|
-
);
|
|
2129
|
-
const selectedBackend = templates.find(
|
|
2130
|
-
(t) => t.packageName === answers.backend
|
|
2131
|
-
);
|
|
2132
|
-
const selectedFrontend = templates.find(
|
|
2133
|
-
(t) => t.packageName === answers.frontend
|
|
2134
|
-
);
|
|
2135
|
-
const s = p4.spinner();
|
|
2136
|
-
s.start("Scaffolding project");
|
|
2137
|
-
await scaffold({
|
|
2138
|
-
appName: answers.appName,
|
|
2139
|
-
description: answers.description || "A JTL Platform app",
|
|
2140
|
-
backend: selectedBackend,
|
|
2141
|
-
frontend: selectedFrontend,
|
|
2142
|
-
shared
|
|
2143
|
-
});
|
|
2144
|
-
s.stop("Project scaffolded");
|
|
2145
2185
|
printNextSteps(answers.appName);
|
|
2146
2186
|
p4.outro(pc4.green("Done! Happy building."));
|
|
2147
|
-
return scaffoldChoice(selectedBackend, selectedFrontend,
|
|
2187
|
+
return scaffoldChoice(selectedBackend, selectedFrontend, legacy);
|
|
2148
2188
|
}
|
|
2149
2189
|
function printNextSteps(appName) {
|
|
2150
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,19 +18,19 @@
|
|
|
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.
|
|
25
|
-
"@jtl-software/cloud-app-template-shared": "0.
|
|
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
|
+
"@jtl-software/cloud-app-template-shared": "0.2.0",
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
|
-
"posthog-node": "^5.
|
|
27
|
+
"posthog-node": "^5.51.6"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^26.
|
|
30
|
+
"@types/node": "^26.4.0",
|
|
31
31
|
"msw": "^2.15.0",
|
|
32
32
|
"tsup": "^8.5.1",
|
|
33
|
-
"tsx": "^4.23.
|
|
33
|
+
"tsx": "^4.23.13",
|
|
34
34
|
"typescript": "^6.0.3"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|