@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.
Files changed (3) hide show
  1. package/README.md +9 -1
  2. package/dist/index.js +152 -112
  3. 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://auth.jtl-cloud.com`. The CLI picks the matching client ID for the known prod/QA/dev hosts. |
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
- upsertEnv(backend.envFile, {
771
+ const updates = {
773
772
  CLIENT_ID: clientId,
774
- CLIENT_SECRET: clientSecret,
775
- ...issuer ? { JTL_ISSUER: issuer } : {},
776
- ...appId ? { JTL_APP_ID: appId } : {},
777
- ...hosts
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
- async function applyProvisionResult(root, backend, opts, tenant, appId, provisioned, isInstalled) {
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
- provisioned.clientId,
1055
- provisioned.clientSecret,
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 = apiTokenFor(jwt);
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 provisioned;
1289
+ let provisioned2;
1261
1290
  try {
1262
- provisioned = await provisionApp(
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
- provisioned,
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 clientId;
1344
- let clientSecret;
1345
- let publicClientId;
1385
+ let provisioned;
1346
1386
  try {
1347
- ({ clientId, clientSecret, publicClientId } = await provisionApp(
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
- { clientId, clientSecret, publicClientId },
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.6.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, next) {
2078
+ function scaffoldChoice(backend, frontend, legacy) {
1995
2079
  const hasBackend = Boolean(backend);
1996
2080
  const hasFrontend = Boolean(frontend);
1997
2081
  return {
1998
- next,
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 runNextScaffold(backends, frontends, shared) {
2024
- const cancelOpts = { onCancel: cancelScaffold };
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
- cancelOpts
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
- printNextSteps(answers.appName);
2081
- p4.outro(pc4.green("Done! Happy building."));
2082
- return scaffoldChoice(selectedBackend, selectedFrontend, true);
2083
- }
2084
- async function runScaffold() {
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, false);
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.6.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.13",
22
- "@jtl-software/cloud-app-template-backend-node": "0.2.0",
23
- "@jtl-software/cloud-app-template-backend-php": "0.1.3",
24
- "@jtl-software/cloud-app-template-frontend-react": "0.2.1",
25
- "@jtl-software/cloud-app-template-shared": "0.1.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
+ "@jtl-software/cloud-app-template-shared": "0.2.0",
26
26
  "picocolors": "^1.1.1",
27
- "posthog-node": "^5.49.1"
27
+ "posthog-node": "^5.51.6"
28
28
  },
29
29
  "devDependencies": {
30
- "@types/node": "^26.2.0",
30
+ "@types/node": "^26.4.0",
31
31
  "msw": "^2.15.0",
32
32
  "tsup": "^8.5.1",
33
- "tsx": "^4.23.12",
33
+ "tsx": "^4.23.13",
34
34
  "typescript": "^6.0.3"
35
35
  },
36
36
  "publishConfig": {