@jtl-software/create-cloud-app 0.3.1 → 0.5.2
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/dist/index.js +561 -161
- package/package.json +6 -6
package/dist/index.js
CHANGED
|
@@ -157,7 +157,10 @@ function captureAuthCode(authorizeUrl, expectedState, verifier, options = {}) {
|
|
|
157
157
|
const closeAndSettle = (res, next) => {
|
|
158
158
|
if (settled) return;
|
|
159
159
|
settled = true;
|
|
160
|
-
const finish = () =>
|
|
160
|
+
const finish = () => {
|
|
161
|
+
server.closeAllConnections?.();
|
|
162
|
+
server.close(() => next());
|
|
163
|
+
};
|
|
161
164
|
if (res.writableEnded) {
|
|
162
165
|
finish();
|
|
163
166
|
} else {
|
|
@@ -364,6 +367,9 @@ async function acquireAccessToken(opts) {
|
|
|
364
367
|
authorizeUrl.searchParams.set("state", state);
|
|
365
368
|
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
366
369
|
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
370
|
+
if (reauth) {
|
|
371
|
+
authorizeUrl.searchParams.set("prompt", "select_account");
|
|
372
|
+
}
|
|
367
373
|
spinner4?.start("Waiting for browser login");
|
|
368
374
|
try {
|
|
369
375
|
const { code, verifier } = await captureAuthCode(
|
|
@@ -471,6 +477,10 @@ function tenantsFromClaims(claims) {
|
|
|
471
477
|
}
|
|
472
478
|
return ordered;
|
|
473
479
|
}
|
|
480
|
+
function activeTenantId(claims) {
|
|
481
|
+
const active = claims[TENANT_ID_CLAIM];
|
|
482
|
+
return typeof active === "string" && active ? active : void 0;
|
|
483
|
+
}
|
|
474
484
|
function validateHost(input, flag) {
|
|
475
485
|
let url;
|
|
476
486
|
try {
|
|
@@ -680,6 +690,37 @@ function detectBackend(root) {
|
|
|
680
690
|
}
|
|
681
691
|
return { kind: "node", envFile: path2.join(root, ".env") };
|
|
682
692
|
}
|
|
693
|
+
function detectFrontend(root) {
|
|
694
|
+
const frontendDir = path2.join(root, "packages", "frontend");
|
|
695
|
+
return fs2.existsSync(path2.join(frontendDir, "package.json")) ? frontendDir : null;
|
|
696
|
+
}
|
|
697
|
+
function writeFrontendPublicClient(root, authIssuer, publicClientId) {
|
|
698
|
+
const frontendDir = detectFrontend(root);
|
|
699
|
+
if (!frontendDir) return null;
|
|
700
|
+
const envFile = path2.join(frontendDir, ".env");
|
|
701
|
+
upsertEnv(envFile, {
|
|
702
|
+
VITE_JTL_CLIENT_ID: publicClientId,
|
|
703
|
+
VITE_JTL_ISSUER: authIssuer
|
|
704
|
+
});
|
|
705
|
+
return envFile;
|
|
706
|
+
}
|
|
707
|
+
function readBackendPort(backend) {
|
|
708
|
+
if (backend.kind === "node") {
|
|
709
|
+
for (const file of [backend.envFile, `${backend.envFile}.example`]) {
|
|
710
|
+
const value = readEnvFile(file).PORT;
|
|
711
|
+
if (value && /^\d+$/.test(value)) return Number(value);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
return 3005;
|
|
715
|
+
}
|
|
716
|
+
function writeVerifyDemoEnv(root, backend) {
|
|
717
|
+
if (!backend) return;
|
|
718
|
+
const frontendDir = detectFrontend(root);
|
|
719
|
+
if (!frontendDir) return;
|
|
720
|
+
upsertEnv(path2.join(frontendDir, ".env"), {
|
|
721
|
+
VITE_BACKEND_URL: `http://localhost:${readBackendPort(backend)}`
|
|
722
|
+
});
|
|
723
|
+
}
|
|
683
724
|
function readExistingCredentials(backend) {
|
|
684
725
|
if (backend.kind === "node") {
|
|
685
726
|
const values = readEnvFile(backend.envFile);
|
|
@@ -696,11 +737,12 @@ function readExistingCredentials(backend) {
|
|
|
696
737
|
return {};
|
|
697
738
|
}
|
|
698
739
|
}
|
|
699
|
-
function writeBackendCredentials(backend, clientId, clientSecret) {
|
|
740
|
+
function writeBackendCredentials(backend, clientId, clientSecret, issuer) {
|
|
700
741
|
if (backend.kind === "node") {
|
|
701
742
|
upsertEnv(backend.envFile, {
|
|
702
743
|
CLIENT_ID: clientId,
|
|
703
|
-
CLIENT_SECRET: clientSecret
|
|
744
|
+
CLIENT_SECRET: clientSecret,
|
|
745
|
+
...issuer ? { JTL_ISSUER: issuer } : {}
|
|
704
746
|
});
|
|
705
747
|
return backend.envFile;
|
|
706
748
|
}
|
|
@@ -715,7 +757,8 @@ function writeBackendCredentials(backend, clientId, clientSecret) {
|
|
|
715
757
|
settings.JtlPlatform = {
|
|
716
758
|
...settings.JtlPlatform ?? {},
|
|
717
759
|
ClientId: clientId,
|
|
718
|
-
ClientSecret: clientSecret
|
|
760
|
+
ClientSecret: clientSecret,
|
|
761
|
+
...issuer ? { Issuer: issuer } : {}
|
|
719
762
|
};
|
|
720
763
|
fs2.writeFileSync(
|
|
721
764
|
backend.settingsFile,
|
|
@@ -772,7 +815,8 @@ function resolveOptions(args) {
|
|
|
772
815
|
...COMMON_ARG_OPTIONS,
|
|
773
816
|
"existing-app-id": { type: "string" },
|
|
774
817
|
new: { type: "boolean" },
|
|
775
|
-
"on-collision": { type: "string" }
|
|
818
|
+
"on-collision": { type: "string" },
|
|
819
|
+
"app-service-path": { type: "string" }
|
|
776
820
|
},
|
|
777
821
|
strict: true,
|
|
778
822
|
allowPositionals: false
|
|
@@ -788,11 +832,13 @@ function resolveOptions(args) {
|
|
|
788
832
|
}
|
|
789
833
|
const onCollisionRaw = parsed.values["on-collision"];
|
|
790
834
|
const onCollision = onCollisionRaw ? parseCollisionAction(onCollisionRaw) : void 0;
|
|
835
|
+
const appServicePath = parsed.values["app-service-path"] ?? "app-service";
|
|
791
836
|
return {
|
|
792
837
|
...common,
|
|
793
838
|
existingAppId,
|
|
794
839
|
forceNew,
|
|
795
|
-
onCollision
|
|
840
|
+
onCollision,
|
|
841
|
+
appServicePath
|
|
796
842
|
};
|
|
797
843
|
}
|
|
798
844
|
async function selectExistingOrNew(matches, opts) {
|
|
@@ -847,9 +893,29 @@ async function resolveCollision(targetLabel, credentialPair, opts) {
|
|
|
847
893
|
})
|
|
848
894
|
);
|
|
849
895
|
}
|
|
850
|
-
async function
|
|
896
|
+
async function confirmRegister(opts, message, isUpdate) {
|
|
897
|
+
if (opts.yes) return "proceed";
|
|
898
|
+
return ensureContinue(
|
|
899
|
+
await p2.select({
|
|
900
|
+
message,
|
|
901
|
+
options: [
|
|
902
|
+
{
|
|
903
|
+
value: "proceed",
|
|
904
|
+
label: isUpdate ? "Update the app" : "Create the app"
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
value: "switch-org",
|
|
908
|
+
label: "Use a different organization",
|
|
909
|
+
hint: "sign in again and pick another org"
|
|
910
|
+
},
|
|
911
|
+
{ value: "cancel", label: "Cancel" }
|
|
912
|
+
]
|
|
913
|
+
})
|
|
914
|
+
);
|
|
915
|
+
}
|
|
916
|
+
async function listTenantApps(apiBaseUrl, jwt, tenantId, appServicePath = "app-service") {
|
|
851
917
|
const res = await apiCall(
|
|
852
|
-
`${apiBaseUrl}/
|
|
918
|
+
`${apiBaseUrl}/${appServicePath}/tenants/apps?onlyAppsOfTenant=true&returnAll=true`,
|
|
853
919
|
{
|
|
854
920
|
headers: {
|
|
855
921
|
Authorization: `Bearer ${jwt}`,
|
|
@@ -868,24 +934,28 @@ async function listTenantApps(apiBaseUrl, jwt, tenantId) {
|
|
|
868
934
|
technicalName,
|
|
869
935
|
clientId: entry.clientId,
|
|
870
936
|
provisioningStatus: entry.activeVersion?.provisioningStatus,
|
|
871
|
-
isInstalled: entry.isInstalled
|
|
937
|
+
isInstalled: entry.isInstalled,
|
|
938
|
+
publicClientId: entry.credentials?.publicClient?.clientId
|
|
872
939
|
};
|
|
873
940
|
}).filter((a) => a !== null);
|
|
874
941
|
}
|
|
875
|
-
async function updateAppManifest(apiBaseUrl, jwt, tenantId, appId, manifest) {
|
|
876
|
-
await apiCall(
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
942
|
+
async function updateAppManifest(apiBaseUrl, jwt, tenantId, appId, manifest, appServicePath = "app-service") {
|
|
943
|
+
await apiCall(
|
|
944
|
+
`${apiBaseUrl}/${appServicePath}/store/apps/${appId}/manifest`,
|
|
945
|
+
{
|
|
946
|
+
method: "PUT",
|
|
947
|
+
headers: {
|
|
948
|
+
Authorization: `Bearer ${jwt}`,
|
|
949
|
+
"Content-Type": "application/json",
|
|
950
|
+
"X-Tenant-Id": tenantId
|
|
951
|
+
},
|
|
952
|
+
body: JSON.stringify({ manifest, tcAccepted: true })
|
|
953
|
+
}
|
|
954
|
+
);
|
|
885
955
|
}
|
|
886
|
-
async function createDraftApp(apiBaseUrl, jwt, tenantId, manifest) {
|
|
956
|
+
async function createDraftApp(apiBaseUrl, jwt, tenantId, manifest, appServicePath = "app-service") {
|
|
887
957
|
const created = await apiCall(
|
|
888
|
-
`${apiBaseUrl}/
|
|
958
|
+
`${apiBaseUrl}/${appServicePath}/store/apps`,
|
|
889
959
|
{
|
|
890
960
|
method: "POST",
|
|
891
961
|
headers: {
|
|
@@ -906,9 +976,9 @@ async function createDraftApp(apiBaseUrl, jwt, tenantId, manifest) {
|
|
|
906
976
|
}
|
|
907
977
|
return created.id;
|
|
908
978
|
}
|
|
909
|
-
async function provisionApp(apiBaseUrl, jwt, tenantId, appId) {
|
|
979
|
+
async function provisionApp(apiBaseUrl, jwt, tenantId, appId, appServicePath = "app-service") {
|
|
910
980
|
const provisioned = await apiCall(
|
|
911
|
-
`${apiBaseUrl}/
|
|
981
|
+
`${apiBaseUrl}/${appServicePath}/store/apps/${appId}/provision`,
|
|
912
982
|
{
|
|
913
983
|
method: "PUT",
|
|
914
984
|
headers: {
|
|
@@ -926,7 +996,11 @@ async function provisionApp(apiBaseUrl, jwt, tenantId, appId) {
|
|
|
926
996
|
`provision response missing clientId or clientSecret: ${JSON.stringify(provisioned)}`
|
|
927
997
|
);
|
|
928
998
|
}
|
|
929
|
-
return {
|
|
999
|
+
return {
|
|
1000
|
+
clientId,
|
|
1001
|
+
clientSecret,
|
|
1002
|
+
publicClientId: provisioned.credentials?.publicClient?.clientId
|
|
1003
|
+
};
|
|
930
1004
|
}
|
|
931
1005
|
function credentialLabels(backend) {
|
|
932
1006
|
if (backend.kind === "node") {
|
|
@@ -967,7 +1041,12 @@ async function persistCredentials(backend, clientId, clientSecret, root, opts, c
|
|
|
967
1041
|
p2.outro(pc2.green("Done. Credentials printed above; file left untouched."));
|
|
968
1042
|
return;
|
|
969
1043
|
}
|
|
970
|
-
const written = writeBackendCredentials(
|
|
1044
|
+
const written = writeBackendCredentials(
|
|
1045
|
+
backend,
|
|
1046
|
+
clientId,
|
|
1047
|
+
clientSecret,
|
|
1048
|
+
opts.auth
|
|
1049
|
+
);
|
|
971
1050
|
printHubLink(
|
|
972
1051
|
context.hub,
|
|
973
1052
|
context.tenantSlug,
|
|
@@ -985,15 +1064,16 @@ async function register(argv = [], runOpts = {}) {
|
|
|
985
1064
|
console.log();
|
|
986
1065
|
p2.intro(pc2.bgCyan(pc2.black(" register cloud app ")));
|
|
987
1066
|
const opts = resolveOptions(argv);
|
|
988
|
-
const backend = detectBackend(root);
|
|
1067
|
+
const backend = fs2.existsSync(path2.join(root, "packages", "backend")) ? detectBackend(root) : null;
|
|
989
1068
|
p2.note(
|
|
990
1069
|
[
|
|
991
1070
|
`${pc2.dim("API")} ${opts.api}`,
|
|
1071
|
+
opts.appServicePath !== "app-service" ? `${pc2.dim("App svc")} /${opts.appServicePath}` : null,
|
|
992
1072
|
`${pc2.dim("Auth")} ${opts.auth}`,
|
|
993
1073
|
`${pc2.dim("Hub")} ${opts.hub}`,
|
|
994
1074
|
`${pc2.dim("Client ID")} ${opts.clientId}`,
|
|
995
1075
|
`${pc2.dim("Scope")} ${opts.scope}`
|
|
996
|
-
].join("\n"),
|
|
1076
|
+
].filter(Boolean).join("\n"),
|
|
997
1077
|
"Target"
|
|
998
1078
|
);
|
|
999
1079
|
const spinner4 = p2.spinner();
|
|
@@ -1010,114 +1090,198 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1010
1090
|
} catch (err) {
|
|
1011
1091
|
bail(err.message);
|
|
1012
1092
|
}
|
|
1013
|
-
const identityJwt = jwt;
|
|
1014
|
-
jwt = apiTokenFor(jwt);
|
|
1015
|
-
spinner4.start("Fetching tenants");
|
|
1016
|
-
let tenants;
|
|
1017
|
-
try {
|
|
1018
|
-
tenants = await fetchTenants(opts.api, identityJwt);
|
|
1019
|
-
spinner4.stop(
|
|
1020
|
-
`Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
|
|
1021
|
-
);
|
|
1022
|
-
} catch (err) {
|
|
1023
|
-
spinner4.stop("Failed to fetch tenants");
|
|
1024
|
-
bail(err.message);
|
|
1025
|
-
}
|
|
1026
|
-
const tenant = await selectTenant(tenants, opts);
|
|
1027
1093
|
const manifest = readManifest(path2.join(root, "app.json"));
|
|
1028
1094
|
const summary = summarizeManifest(manifest);
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
spinner4.stop(
|
|
1035
|
-
`Found ${matches.length} app${matches.length === 1 ? "" : "s"} with technical name ${pc2.cyan(summary.technicalName)}`
|
|
1036
|
-
);
|
|
1037
|
-
} catch (err) {
|
|
1038
|
-
spinner4.stop("Failed to look up tenant apps");
|
|
1039
|
-
bail(err.message);
|
|
1040
|
-
}
|
|
1041
|
-
const existing = await selectExistingOrNew(matches, opts);
|
|
1042
|
-
const reviewLines = [
|
|
1043
|
-
`${pc2.dim("Technical name")} ${summary.technicalName}`,
|
|
1044
|
-
`${pc2.dim("Version")} ${summary.version}`,
|
|
1045
|
-
`${pc2.dim("Tenant")} ${tenant.name} (${tenant.slug})`,
|
|
1046
|
-
existing ? `${pc2.dim("Updating")} ${existing.technicalName} (${existing.id}) [${existing.provisioningStatus ?? "?"}]` : `${pc2.dim("Action")} create new`
|
|
1047
|
-
].filter(Boolean);
|
|
1048
|
-
p2.note(reviewLines.join("\n"), "Review");
|
|
1049
|
-
const confirmMessage = existing ? `Update app "${existing.technicalName}" in tenant "${tenant.name}"?` : `Create app "${summary.technicalName}" in tenant "${tenant.name}"?`;
|
|
1050
|
-
const confirm2 = await confirmAction(opts, confirmMessage, existing !== null);
|
|
1051
|
-
if (!confirm2) bail("Cancelled.");
|
|
1052
|
-
if (existing) {
|
|
1053
|
-
spinner4.start("Updating manifest");
|
|
1095
|
+
for (; ; ) {
|
|
1096
|
+
const identityJwt = jwt;
|
|
1097
|
+
const apiJwt = apiTokenFor(jwt);
|
|
1098
|
+
spinner4.start("Fetching tenants");
|
|
1099
|
+
let tenants;
|
|
1054
1100
|
try {
|
|
1055
|
-
await
|
|
1056
|
-
spinner4.stop(
|
|
1101
|
+
tenants = await fetchTenants(opts.api, identityJwt);
|
|
1102
|
+
spinner4.stop(
|
|
1103
|
+
`Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
|
|
1104
|
+
);
|
|
1057
1105
|
} catch (err) {
|
|
1058
|
-
spinner4.stop("Failed to
|
|
1106
|
+
spinner4.stop("Failed to fetch tenants");
|
|
1059
1107
|
bail(err.message);
|
|
1060
1108
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1109
|
+
let tenant;
|
|
1110
|
+
const scopedTenantId = opts.tenant ? void 0 : activeTenantId(decodeJwtClaims(identityJwt));
|
|
1111
|
+
const scopedTenant = scopedTenantId ? tenants.find((t) => t.id === scopedTenantId) : void 0;
|
|
1112
|
+
if (scopedTenant) {
|
|
1113
|
+
tenant = scopedTenant;
|
|
1114
|
+
p2.log.info(
|
|
1115
|
+
`Organization ${pc2.cyan(tenant.name)} ${pc2.dim(`(${tenant.slug})`)} \u2014 selected at login`
|
|
1116
|
+
);
|
|
1117
|
+
} else {
|
|
1118
|
+
tenant = await selectTenant(tenants, opts);
|
|
1119
|
+
}
|
|
1120
|
+
spinner4.start("Looking for apps matching this manifest");
|
|
1121
|
+
let matches;
|
|
1122
|
+
try {
|
|
1123
|
+
const apps = await listTenantApps(
|
|
1124
|
+
opts.api,
|
|
1125
|
+
apiJwt,
|
|
1126
|
+
tenant.id,
|
|
1127
|
+
opts.appServicePath
|
|
1128
|
+
);
|
|
1129
|
+
matches = apps.filter((a) => a.technicalName === summary.technicalName);
|
|
1130
|
+
spinner4.stop(
|
|
1131
|
+
`Found ${matches.length} app${matches.length === 1 ? "" : "s"} with technical name ${pc2.cyan(summary.technicalName)}`
|
|
1132
|
+
);
|
|
1133
|
+
} catch (err) {
|
|
1134
|
+
spinner4.stop("Failed to look up tenant apps");
|
|
1135
|
+
bail(err.message);
|
|
1136
|
+
}
|
|
1137
|
+
const existing = await selectExistingOrNew(matches, opts);
|
|
1138
|
+
const reviewLines = [
|
|
1139
|
+
`${pc2.dim("Technical name")} ${summary.technicalName}`,
|
|
1140
|
+
`${pc2.dim("Version")} ${summary.version}`,
|
|
1141
|
+
`${pc2.dim("Tenant")} ${tenant.name} (${tenant.slug})`,
|
|
1142
|
+
existing ? `${pc2.dim("Updating")} ${existing.technicalName} (${existing.id}) [${existing.provisioningStatus ?? "?"}]` : `${pc2.dim("Action")} create new`
|
|
1143
|
+
].filter(Boolean);
|
|
1144
|
+
p2.note(reviewLines.join("\n"), "Review");
|
|
1145
|
+
const confirmMessage = existing ? `Update app "${existing.technicalName}" in tenant "${tenant.name}"?` : `Create app "${summary.technicalName}" in tenant "${tenant.name}"?`;
|
|
1146
|
+
const decision = await confirmRegister(
|
|
1147
|
+
opts,
|
|
1148
|
+
confirmMessage,
|
|
1149
|
+
existing !== null
|
|
1066
1150
|
);
|
|
1067
|
-
if (
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1151
|
+
if (decision === "cancel") bail("Cancelled.");
|
|
1152
|
+
if (decision === "switch-org") {
|
|
1153
|
+
try {
|
|
1154
|
+
jwt = await acquireAccessToken({
|
|
1155
|
+
auth: opts.auth,
|
|
1156
|
+
clientId: opts.clientId,
|
|
1157
|
+
scope: opts.scope,
|
|
1158
|
+
reauth: true,
|
|
1159
|
+
// forces prompt=select_account so the user re-picks the organization
|
|
1160
|
+
spinner: spinner4,
|
|
1161
|
+
captureOptions: runOpts.captureOptions
|
|
1162
|
+
});
|
|
1163
|
+
} catch (err) {
|
|
1164
|
+
bail(err.message);
|
|
1165
|
+
}
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
if (existing) {
|
|
1169
|
+
spinner4.start("Updating manifest");
|
|
1170
|
+
try {
|
|
1171
|
+
await updateAppManifest(
|
|
1172
|
+
opts.api,
|
|
1173
|
+
apiJwt,
|
|
1174
|
+
tenant.id,
|
|
1175
|
+
existing.id,
|
|
1176
|
+
manifest,
|
|
1177
|
+
opts.appServicePath
|
|
1178
|
+
);
|
|
1179
|
+
spinner4.stop("Manifest updated");
|
|
1180
|
+
} catch (err) {
|
|
1181
|
+
spinner4.stop("Failed to update manifest");
|
|
1182
|
+
bail(err.message);
|
|
1183
|
+
}
|
|
1184
|
+
if (existing.publicClientId) {
|
|
1185
|
+
const written = writeFrontendPublicClient(
|
|
1186
|
+
root,
|
|
1187
|
+
opts.auth,
|
|
1188
|
+
existing.publicClientId
|
|
1189
|
+
);
|
|
1190
|
+
if (written) {
|
|
1191
|
+
p2.log.success(
|
|
1192
|
+
`Wrote VITE_JTL_CLIENT_ID/VITE_JTL_ISSUER to ${pc2.cyan(path2.relative(root, written))}.`
|
|
1193
|
+
);
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
writeVerifyDemoEnv(root, backend);
|
|
1197
|
+
printHubLink(
|
|
1198
|
+
opts.hub,
|
|
1199
|
+
tenant.slug,
|
|
1200
|
+
existing.id,
|
|
1201
|
+
existing.isInstalled ?? false
|
|
1202
|
+
);
|
|
1203
|
+
if (backend && existing.clientId) {
|
|
1204
|
+
const labels = credentialLabels(backend);
|
|
1205
|
+
const envFile = backend.kind === "node" ? backend.envFile : backend.settingsFile;
|
|
1206
|
+
const localCreds = readExistingCredentials(backend);
|
|
1207
|
+
const hasLocalCreds = !!(localCreds.clientId && localCreds.clientSecret);
|
|
1208
|
+
if (!hasLocalCreds) {
|
|
1209
|
+
p2.outro(
|
|
1210
|
+
pc2.yellow(
|
|
1211
|
+
`Manifest updated. Your backend config is missing ${labels.pair} \u2014 they can't be recovered. Re-create the app to get a fresh secret.`
|
|
1212
|
+
)
|
|
1213
|
+
);
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1073
1216
|
p2.outro(
|
|
1074
|
-
pc2.
|
|
1075
|
-
`Manifest updated.
|
|
1217
|
+
pc2.green(
|
|
1218
|
+
`Manifest updated. Existing ${labels.pair} in ${pc2.cyan(path2.relative(root, envFile))} kept as-is.`
|
|
1076
1219
|
)
|
|
1077
1220
|
);
|
|
1078
|
-
|
|
1221
|
+
} else {
|
|
1222
|
+
p2.outro(pc2.green("Manifest updated."));
|
|
1079
1223
|
}
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
spinner4.start("Creating draft app");
|
|
1227
|
+
let appId;
|
|
1228
|
+
try {
|
|
1229
|
+
appId = await createDraftApp(
|
|
1230
|
+
opts.api,
|
|
1231
|
+
apiJwt,
|
|
1232
|
+
tenant.id,
|
|
1233
|
+
manifest,
|
|
1234
|
+
opts.appServicePath
|
|
1084
1235
|
);
|
|
1085
|
-
|
|
1086
|
-
|
|
1236
|
+
spinner4.stop(`Created app ${pc2.cyan(appId)}`);
|
|
1237
|
+
} catch (err) {
|
|
1238
|
+
spinner4.stop("Failed to create app");
|
|
1239
|
+
bail(err.message);
|
|
1240
|
+
}
|
|
1241
|
+
spinner4.start("Provisioning app");
|
|
1242
|
+
let clientId;
|
|
1243
|
+
let clientSecret;
|
|
1244
|
+
let publicClientId;
|
|
1245
|
+
try {
|
|
1246
|
+
({ clientId, clientSecret, publicClientId } = await provisionApp(
|
|
1247
|
+
opts.api,
|
|
1248
|
+
apiJwt,
|
|
1249
|
+
tenant.id,
|
|
1250
|
+
appId,
|
|
1251
|
+
opts.appServicePath
|
|
1252
|
+
));
|
|
1253
|
+
spinner4.stop("Provisioned");
|
|
1254
|
+
} catch (err) {
|
|
1255
|
+
spinner4.stop("Failed to provision app");
|
|
1256
|
+
bail(err.message);
|
|
1257
|
+
}
|
|
1258
|
+
if (publicClientId) {
|
|
1259
|
+
const written = writeFrontendPublicClient(
|
|
1260
|
+
root,
|
|
1261
|
+
opts.auth,
|
|
1262
|
+
publicClientId
|
|
1263
|
+
);
|
|
1264
|
+
if (written) {
|
|
1265
|
+
p2.log.success(
|
|
1266
|
+
`Wrote VITE_JTL_CLIENT_ID/VITE_JTL_ISSUER to ${pc2.cyan(path2.relative(root, written))}.`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
writeVerifyDemoEnv(root, backend);
|
|
1271
|
+
if (backend) {
|
|
1272
|
+
await persistCredentials(backend, clientId, clientSecret, root, opts, {
|
|
1273
|
+
hub: opts.hub,
|
|
1274
|
+
tenantSlug: tenant.slug,
|
|
1275
|
+
appId,
|
|
1276
|
+
// A freshly created app can't be installed yet.
|
|
1277
|
+
isInstalled: false
|
|
1278
|
+
});
|
|
1279
|
+
return;
|
|
1087
1280
|
}
|
|
1281
|
+
printHubLink(opts.hub, tenant.slug, appId, false);
|
|
1282
|
+
p2.outro(pc2.green("Done. App registered."));
|
|
1088
1283
|
return;
|
|
1089
1284
|
}
|
|
1090
|
-
spinner4.start("Creating draft app");
|
|
1091
|
-
let appId;
|
|
1092
|
-
try {
|
|
1093
|
-
appId = await createDraftApp(opts.api, jwt, tenant.id, manifest);
|
|
1094
|
-
spinner4.stop(`Created app ${pc2.cyan(appId)}`);
|
|
1095
|
-
} catch (err) {
|
|
1096
|
-
spinner4.stop("Failed to create app");
|
|
1097
|
-
bail(err.message);
|
|
1098
|
-
}
|
|
1099
|
-
spinner4.start("Provisioning app");
|
|
1100
|
-
let clientId;
|
|
1101
|
-
let clientSecret;
|
|
1102
|
-
try {
|
|
1103
|
-
({ clientId, clientSecret } = await provisionApp(
|
|
1104
|
-
opts.api,
|
|
1105
|
-
jwt,
|
|
1106
|
-
tenant.id,
|
|
1107
|
-
appId
|
|
1108
|
-
));
|
|
1109
|
-
spinner4.stop("Provisioned");
|
|
1110
|
-
} catch (err) {
|
|
1111
|
-
spinner4.stop("Failed to provision app");
|
|
1112
|
-
bail(err.message);
|
|
1113
|
-
}
|
|
1114
|
-
await persistCredentials(backend, clientId, clientSecret, root, opts, {
|
|
1115
|
-
hub: opts.hub,
|
|
1116
|
-
tenantSlug: tenant.slug,
|
|
1117
|
-
appId,
|
|
1118
|
-
// A freshly created app can't be installed yet.
|
|
1119
|
-
isInstalled: false
|
|
1120
|
-
});
|
|
1121
1285
|
}
|
|
1122
1286
|
|
|
1123
1287
|
// src/listing.ts
|
|
@@ -1390,7 +1554,25 @@ async function listing(argv = [], runOpts = {}) {
|
|
|
1390
1554
|
// src/scaffold.ts
|
|
1391
1555
|
import crypto2 from "crypto";
|
|
1392
1556
|
import fs4 from "fs";
|
|
1557
|
+
import net from "net";
|
|
1393
1558
|
import path4 from "path";
|
|
1559
|
+
var DEFAULT_FRONTEND_PORT = 3004;
|
|
1560
|
+
var BACKEND_PORT = 3005;
|
|
1561
|
+
function isPortFree(port) {
|
|
1562
|
+
return new Promise((resolve) => {
|
|
1563
|
+
const server = net.createServer();
|
|
1564
|
+
server.once("error", () => resolve(false));
|
|
1565
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
1566
|
+
server.listen(port, "127.0.0.1");
|
|
1567
|
+
});
|
|
1568
|
+
}
|
|
1569
|
+
async function pickFrontendPort() {
|
|
1570
|
+
for (let port = DEFAULT_FRONTEND_PORT; port <= 3999; port++) {
|
|
1571
|
+
if (port === BACKEND_PORT) continue;
|
|
1572
|
+
if (await isPortFree(port)) return port;
|
|
1573
|
+
}
|
|
1574
|
+
return DEFAULT_FRONTEND_PORT;
|
|
1575
|
+
}
|
|
1394
1576
|
function toPascalCase(kebab) {
|
|
1395
1577
|
return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1396
1578
|
}
|
|
@@ -1412,7 +1594,8 @@ function buildPlaceholders(opts) {
|
|
|
1412
1594
|
"{{APP_NAME_PASCAL}}": toPascalCase(opts.appName),
|
|
1413
1595
|
"{{APP_TECHNICAL_NAME}}": buildTechnicalName(opts.appName),
|
|
1414
1596
|
"{{APP_DESCRIPTION}}": opts.description,
|
|
1415
|
-
"{{APP_DESCRIPTION_SHORT}}": buildShortDescription(opts.description)
|
|
1597
|
+
"{{APP_DESCRIPTION_SHORT}}": buildShortDescription(opts.description),
|
|
1598
|
+
"{{FRONTEND_PORT}}": String(opts.frontendPort)
|
|
1416
1599
|
};
|
|
1417
1600
|
}
|
|
1418
1601
|
function applyPlaceholders(content, replacements) {
|
|
@@ -1448,9 +1631,11 @@ function transformPackageJson(content, appName, templateType, replacements) {
|
|
|
1448
1631
|
const result = applyPlaceholders(JSON.stringify(pkg, null, 2), replacements);
|
|
1449
1632
|
return result + "\n";
|
|
1450
1633
|
}
|
|
1634
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".turbo"]);
|
|
1451
1635
|
function copyDir(src, dest, replacements, templateType, appName) {
|
|
1452
1636
|
fs4.mkdirSync(dest, { recursive: true });
|
|
1453
1637
|
for (const entry of fs4.readdirSync(src, { withFileTypes: true })) {
|
|
1638
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
1454
1639
|
const srcPath = path4.join(src, entry.name);
|
|
1455
1640
|
let destName = entry.name;
|
|
1456
1641
|
if (destName.startsWith("_git")) {
|
|
@@ -1485,40 +1670,87 @@ function buildReplacements(base, template) {
|
|
|
1485
1670
|
}
|
|
1486
1671
|
return { ...base, ...resolved };
|
|
1487
1672
|
}
|
|
1673
|
+
function applyManifestAuthentication(appJsonPath, opts) {
|
|
1674
|
+
const manifest = JSON.parse(fs4.readFileSync(appJsonPath, "utf-8"));
|
|
1675
|
+
const { frontend, backend } = opts.authentication;
|
|
1676
|
+
const authentication = {};
|
|
1677
|
+
if (frontend) {
|
|
1678
|
+
const origin = `http://localhost:${opts.frontendPort}`;
|
|
1679
|
+
authentication.publicClient = {
|
|
1680
|
+
redirectUris: [`${origin}/callback`],
|
|
1681
|
+
postLogoutRedirectUris: [`${origin}/`]
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
if (backend) {
|
|
1685
|
+
authentication.serviceAccount = {
|
|
1686
|
+
clientCredentials: true,
|
|
1687
|
+
description: `${opts.appName} backend service account`
|
|
1688
|
+
};
|
|
1689
|
+
}
|
|
1690
|
+
manifest.authentication = authentication;
|
|
1691
|
+
if (!frontend) {
|
|
1692
|
+
delete manifest.lifecycle;
|
|
1693
|
+
if (manifest.capabilities) {
|
|
1694
|
+
delete manifest.capabilities.hub;
|
|
1695
|
+
if (manifest.capabilities.erp) {
|
|
1696
|
+
delete manifest.capabilities.erp.menuItems;
|
|
1697
|
+
delete manifest.capabilities.erp.pane;
|
|
1698
|
+
}
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
fs4.writeFileSync(appJsonPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
1702
|
+
}
|
|
1488
1703
|
async function scaffold({
|
|
1489
1704
|
appName,
|
|
1490
1705
|
description,
|
|
1491
1706
|
backend,
|
|
1492
1707
|
frontend,
|
|
1493
1708
|
shared,
|
|
1709
|
+
frontendPort = DEFAULT_FRONTEND_PORT,
|
|
1710
|
+
authentication,
|
|
1494
1711
|
targetDir: customTargetDir
|
|
1495
1712
|
}) {
|
|
1496
1713
|
const targetDir = customTargetDir ?? path4.resolve(process.cwd(), appName);
|
|
1497
|
-
const baseReplacements = buildPlaceholders({
|
|
1714
|
+
const baseReplacements = buildPlaceholders({
|
|
1715
|
+
appName,
|
|
1716
|
+
description,
|
|
1717
|
+
frontendPort
|
|
1718
|
+
});
|
|
1498
1719
|
if (fs4.existsSync(targetDir)) {
|
|
1499
1720
|
throw new Error(`Directory "${appName}" already exists`);
|
|
1500
1721
|
}
|
|
1501
1722
|
fs4.mkdirSync(targetDir, { recursive: true });
|
|
1502
1723
|
copyDir(shared.dir, targetDir, baseReplacements);
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1724
|
+
if (frontend) {
|
|
1725
|
+
const frontendDest = path4.join(targetDir, "packages", "frontend");
|
|
1726
|
+
const frontendReplacements = buildReplacements(baseReplacements, frontend);
|
|
1727
|
+
copyDir(
|
|
1728
|
+
frontend.dir,
|
|
1729
|
+
frontendDest,
|
|
1730
|
+
frontendReplacements,
|
|
1731
|
+
frontend.meta.type,
|
|
1732
|
+
appName
|
|
1733
|
+
);
|
|
1734
|
+
}
|
|
1735
|
+
if (backend) {
|
|
1736
|
+
const backendDest = path4.join(targetDir, "packages", "backend");
|
|
1737
|
+
const backendReplacements = buildReplacements(baseReplacements, backend);
|
|
1738
|
+
copyDir(
|
|
1739
|
+
backend.dir,
|
|
1740
|
+
backendDest,
|
|
1741
|
+
backendReplacements,
|
|
1742
|
+
backend.meta.type,
|
|
1743
|
+
appName
|
|
1744
|
+
);
|
|
1745
|
+
}
|
|
1746
|
+
if (authentication) {
|
|
1747
|
+
applyManifestAuthentication(path4.join(targetDir, "app.json"), {
|
|
1748
|
+
appName,
|
|
1749
|
+
frontendPort,
|
|
1750
|
+
authentication
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
const runtimeVersions = backend?.meta.runtimeVersions ?? {};
|
|
1522
1754
|
if (Object.keys(runtimeVersions).length > 0) {
|
|
1523
1755
|
const toolVersions = Object.entries(runtimeVersions).map(([tool, version]) => `${tool} ${version}`).join("\n");
|
|
1524
1756
|
fs4.writeFileSync(
|
|
@@ -1585,14 +1817,163 @@ function getTemplatesByType(templates, type) {
|
|
|
1585
1817
|
}
|
|
1586
1818
|
|
|
1587
1819
|
// src/posthog.ts
|
|
1820
|
+
import { randomUUID } from "crypto";
|
|
1821
|
+
import fs6 from "fs";
|
|
1822
|
+
import os2 from "os";
|
|
1823
|
+
import path6 from "path";
|
|
1588
1824
|
import { PostHog } from "posthog-node";
|
|
1589
1825
|
var TOKEN = "phc_SkFAxTIa4RUQxotci1Ftdw7xefZK2Ax0LEIGcl8zuD2";
|
|
1826
|
+
var CLI_VERSION = "0.5.2";
|
|
1590
1827
|
var client = TOKEN ? new PostHog(TOKEN, { host: "https://e.jtl-cloud.com" }) : null;
|
|
1828
|
+
function telemetryDisabledByEnv() {
|
|
1829
|
+
const dnt = process.env.DO_NOT_TRACK;
|
|
1830
|
+
if (dnt && dnt !== "0" && dnt !== "false") return true;
|
|
1831
|
+
const optOut = process.env.JTL_CLI_TELEMETRY;
|
|
1832
|
+
if (optOut === "0" || optOut === "false") return true;
|
|
1833
|
+
return false;
|
|
1834
|
+
}
|
|
1835
|
+
function telemetryClient() {
|
|
1836
|
+
if (!client || telemetryDisabledByEnv()) return null;
|
|
1837
|
+
return client;
|
|
1838
|
+
}
|
|
1839
|
+
function telemetryIdFile() {
|
|
1840
|
+
return process.env.JTL_CLI_TELEMETRY_ID_FILE ?? path6.join(os2.homedir(), ".config", "jtl-cli", "telemetry.json");
|
|
1841
|
+
}
|
|
1842
|
+
var cachedId;
|
|
1843
|
+
function anonymousId() {
|
|
1844
|
+
if (cachedId) return cachedId;
|
|
1845
|
+
const file = telemetryIdFile();
|
|
1846
|
+
try {
|
|
1847
|
+
const parsed = JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1848
|
+
if (typeof parsed?.anonymous_id === "string" && parsed.anonymous_id) {
|
|
1849
|
+
cachedId = parsed.anonymous_id;
|
|
1850
|
+
return parsed.anonymous_id;
|
|
1851
|
+
}
|
|
1852
|
+
} catch {
|
|
1853
|
+
}
|
|
1854
|
+
const id = randomUUID();
|
|
1855
|
+
try {
|
|
1856
|
+
fs6.mkdirSync(path6.dirname(file), { recursive: true });
|
|
1857
|
+
fs6.writeFileSync(file, JSON.stringify({ anonymous_id: id }, null, 2));
|
|
1858
|
+
} catch {
|
|
1859
|
+
}
|
|
1860
|
+
cachedId = id;
|
|
1861
|
+
return id;
|
|
1862
|
+
}
|
|
1863
|
+
function baseProps() {
|
|
1864
|
+
return {
|
|
1865
|
+
cli_version: CLI_VERSION,
|
|
1866
|
+
node_version: process.version,
|
|
1867
|
+
os: process.platform,
|
|
1868
|
+
arch: process.arch,
|
|
1869
|
+
ci: Boolean(process.env.CI)
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
function captureCommand(command, properties = {}) {
|
|
1873
|
+
const c = telemetryClient();
|
|
1874
|
+
if (!c) return;
|
|
1875
|
+
c.capture({
|
|
1876
|
+
distinctId: anonymousId(),
|
|
1877
|
+
event: "cli_command",
|
|
1878
|
+
properties: { command, ...baseProps(), ...properties }
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
function captureError(error, properties = {}) {
|
|
1882
|
+
const c = telemetryClient();
|
|
1883
|
+
if (!c) return;
|
|
1884
|
+
try {
|
|
1885
|
+
c.captureException(error, anonymousId(), { ...baseProps(), ...properties });
|
|
1886
|
+
} catch {
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1591
1889
|
async function shutdownPostHog() {
|
|
1592
1890
|
await client?.shutdown();
|
|
1593
1891
|
}
|
|
1594
1892
|
|
|
1595
1893
|
// src/index.ts
|
|
1894
|
+
var NONE = "__none__";
|
|
1895
|
+
function templateSelectOptions(templates) {
|
|
1896
|
+
return templates.map((t) => ({
|
|
1897
|
+
value: t.packageName,
|
|
1898
|
+
label: t.meta.label ?? t.packageName,
|
|
1899
|
+
hint: t.meta.hint
|
|
1900
|
+
}));
|
|
1901
|
+
}
|
|
1902
|
+
function halfSelectOptions(templates, noneHint) {
|
|
1903
|
+
return [
|
|
1904
|
+
...templateSelectOptions(templates),
|
|
1905
|
+
{ value: NONE, label: "None", hint: noneHint }
|
|
1906
|
+
];
|
|
1907
|
+
}
|
|
1908
|
+
function cancelScaffold() {
|
|
1909
|
+
p4.cancel("Cancelled.");
|
|
1910
|
+
process.exit(0);
|
|
1911
|
+
}
|
|
1912
|
+
async function runNextScaffold(backends, frontends, shared) {
|
|
1913
|
+
const cancelOpts = { onCancel: cancelScaffold };
|
|
1914
|
+
const answers = await p4.group(
|
|
1915
|
+
{
|
|
1916
|
+
appName: () => p4.text({
|
|
1917
|
+
message: "App name",
|
|
1918
|
+
placeholder: "my-jtl-app",
|
|
1919
|
+
validate: (value) => {
|
|
1920
|
+
if (!value) return "App name is required";
|
|
1921
|
+
if (!/^[a-z0-9-]+$/.test(value))
|
|
1922
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
1923
|
+
}
|
|
1924
|
+
}),
|
|
1925
|
+
description: () => p4.text({ message: "Description", placeholder: "A JTL Platform app" }),
|
|
1926
|
+
frontend: () => p4.select({
|
|
1927
|
+
message: "Frontend (adds a public client for browser login)",
|
|
1928
|
+
options: halfSelectOptions(frontends, "no frontend")
|
|
1929
|
+
}),
|
|
1930
|
+
backend: () => p4.select({
|
|
1931
|
+
message: "Backend (adds a service account for the JTL API)",
|
|
1932
|
+
options: halfSelectOptions(backends, "no backend")
|
|
1933
|
+
})
|
|
1934
|
+
},
|
|
1935
|
+
cancelOpts
|
|
1936
|
+
);
|
|
1937
|
+
const selectedFrontend = frontends.find(
|
|
1938
|
+
(t) => t.packageName === answers.frontend
|
|
1939
|
+
);
|
|
1940
|
+
const selectedBackend = backends.find(
|
|
1941
|
+
(t) => t.packageName === answers.backend
|
|
1942
|
+
);
|
|
1943
|
+
if (!selectedFrontend && !selectedBackend) {
|
|
1944
|
+
p4.cancel("Pick at least a frontend or a backend.");
|
|
1945
|
+
process.exit(1);
|
|
1946
|
+
}
|
|
1947
|
+
const s = p4.spinner();
|
|
1948
|
+
s.start("Scaffolding project");
|
|
1949
|
+
const frontendPort = selectedFrontend ? await pickFrontendPort() : void 0;
|
|
1950
|
+
await scaffold({
|
|
1951
|
+
appName: answers.appName,
|
|
1952
|
+
description: answers.description || "A JTL Platform app",
|
|
1953
|
+
backend: selectedBackend,
|
|
1954
|
+
frontend: selectedFrontend,
|
|
1955
|
+
shared,
|
|
1956
|
+
frontendPort,
|
|
1957
|
+
authentication: {
|
|
1958
|
+
frontend: Boolean(selectedFrontend),
|
|
1959
|
+
backend: Boolean(selectedBackend)
|
|
1960
|
+
}
|
|
1961
|
+
});
|
|
1962
|
+
s.stop("Project scaffolded");
|
|
1963
|
+
if (selectedFrontend) {
|
|
1964
|
+
p4.note(
|
|
1965
|
+
`The frontend dev server runs on ${pc4.cyan(`http://localhost:${frontendPort}`)}, and its ${pc4.cyan("/callback")} route is registered as the public client's redirect URI.`,
|
|
1966
|
+
"Frontend login"
|
|
1967
|
+
);
|
|
1968
|
+
}
|
|
1969
|
+
printNextSteps(answers.appName);
|
|
1970
|
+
p4.outro(pc4.green("Done! Happy building."));
|
|
1971
|
+
return {
|
|
1972
|
+
next: true,
|
|
1973
|
+
frontend: selectedFrontend ? selectedFrontend.meta.label ?? selectedFrontend.packageName : "none",
|
|
1974
|
+
backend: selectedBackend ? selectedBackend.meta.label ?? selectedBackend.packageName : "none"
|
|
1975
|
+
};
|
|
1976
|
+
}
|
|
1596
1977
|
async function runScaffold() {
|
|
1597
1978
|
console.log();
|
|
1598
1979
|
p4.intro(pc4.bgCyan(pc4.black(" create-cloud-app ")));
|
|
@@ -1604,6 +1985,9 @@ async function runScaffold() {
|
|
|
1604
1985
|
p4.cancel("No templates found. Ensure template packages are installed.");
|
|
1605
1986
|
process.exit(1);
|
|
1606
1987
|
}
|
|
1988
|
+
if (process.argv.includes("--next")) {
|
|
1989
|
+
return runNextScaffold(backends, frontends, shared);
|
|
1990
|
+
}
|
|
1607
1991
|
const answers = await p4.group(
|
|
1608
1992
|
{
|
|
1609
1993
|
appName: () => p4.text({
|
|
@@ -1621,19 +2005,11 @@ async function runScaffold() {
|
|
|
1621
2005
|
}),
|
|
1622
2006
|
backend: () => p4.select({
|
|
1623
2007
|
message: "Backend",
|
|
1624
|
-
options: backends
|
|
1625
|
-
value: t.packageName,
|
|
1626
|
-
label: t.meta.label ?? t.packageName,
|
|
1627
|
-
hint: t.meta.hint
|
|
1628
|
-
}))
|
|
2008
|
+
options: templateSelectOptions(backends)
|
|
1629
2009
|
}),
|
|
1630
2010
|
frontend: () => p4.select({
|
|
1631
2011
|
message: "Frontend",
|
|
1632
|
-
options: frontends
|
|
1633
|
-
value: t.packageName,
|
|
1634
|
-
label: t.meta.label ?? t.packageName,
|
|
1635
|
-
hint: t.meta.hint
|
|
1636
|
-
}))
|
|
2012
|
+
options: templateSelectOptions(frontends)
|
|
1637
2013
|
})
|
|
1638
2014
|
},
|
|
1639
2015
|
{
|
|
@@ -1659,9 +2035,17 @@ async function runScaffold() {
|
|
|
1659
2035
|
shared
|
|
1660
2036
|
});
|
|
1661
2037
|
s.stop("Project scaffolded");
|
|
2038
|
+
printNextSteps(answers.appName);
|
|
2039
|
+
p4.outro(pc4.green("Done! Happy building."));
|
|
2040
|
+
return {
|
|
2041
|
+
backend: selectedBackend.meta.label ?? selectedBackend.packageName,
|
|
2042
|
+
frontend: selectedFrontend.meta.label ?? selectedFrontend.packageName
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function printNextSteps(appName) {
|
|
1662
2046
|
p4.note(
|
|
1663
2047
|
[
|
|
1664
|
-
`${pc4.cyan("cd")} ${
|
|
2048
|
+
`${pc4.cyan("cd")} ${appName}`,
|
|
1665
2049
|
`${pc4.cyan("npm install")}`,
|
|
1666
2050
|
`${pc4.cyan("npm run register")}`,
|
|
1667
2051
|
`${pc4.cyan("npm run dev")}`,
|
|
@@ -1669,18 +2053,34 @@ async function runScaffold() {
|
|
|
1669
2053
|
].join("\n"),
|
|
1670
2054
|
"Next steps"
|
|
1671
2055
|
);
|
|
1672
|
-
p4.outro(pc4.green("Done! Happy building."));
|
|
1673
2056
|
}
|
|
1674
2057
|
async function main() {
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
2058
|
+
const command = process.argv[2] === "register" ? "register" : process.argv[2] === "listing" ? "listing" : "scaffold";
|
|
2059
|
+
const startedAt = Date.now();
|
|
2060
|
+
try {
|
|
2061
|
+
let detail = {};
|
|
2062
|
+
if (command === "register") {
|
|
2063
|
+
await register(process.argv.slice(3));
|
|
2064
|
+
} else if (command === "listing") {
|
|
2065
|
+
await listing(process.argv.slice(3));
|
|
2066
|
+
} else {
|
|
2067
|
+
detail = await runScaffold();
|
|
2068
|
+
}
|
|
2069
|
+
captureCommand(command, {
|
|
2070
|
+
success: true,
|
|
2071
|
+
duration_ms: Date.now() - startedAt,
|
|
2072
|
+
...detail
|
|
2073
|
+
});
|
|
2074
|
+
} catch (err) {
|
|
2075
|
+
const bail2 = err instanceof BailError;
|
|
2076
|
+
captureCommand(command, {
|
|
2077
|
+
success: false,
|
|
2078
|
+
duration_ms: Date.now() - startedAt,
|
|
2079
|
+
outcome: bail2 ? "bailed" : "error"
|
|
2080
|
+
});
|
|
2081
|
+
if (!bail2) captureError(err, { command });
|
|
2082
|
+
throw err;
|
|
1682
2083
|
}
|
|
1683
|
-
await runScaffold();
|
|
1684
2084
|
}
|
|
1685
2085
|
main().then(() => shutdownPostHog()).catch(async (err) => {
|
|
1686
2086
|
await shutdownPostHog();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtl-software/create-cloud-app",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for scaffolding JTL Platform cloud apps",
|
|
6
6
|
"bin": {
|
|
@@ -18,11 +18,11 @@
|
|
|
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.0.
|
|
23
|
-
"@jtl-software/cloud-app-template-backend-php": "0.1.
|
|
24
|
-
"@jtl-software/cloud-app-template-frontend-react": "0.
|
|
25
|
-
"@jtl-software/cloud-app-template-shared": "0.1.
|
|
21
|
+
"@jtl-software/cloud-app-template-backend-dotnet": "0.0.13",
|
|
22
|
+
"@jtl-software/cloud-app-template-backend-node": "0.0.16",
|
|
23
|
+
"@jtl-software/cloud-app-template-backend-php": "0.1.3",
|
|
24
|
+
"@jtl-software/cloud-app-template-frontend-react": "0.1.2",
|
|
25
|
+
"@jtl-software/cloud-app-template-shared": "0.1.2",
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
27
|
"posthog-node": "^5.48.1"
|
|
28
28
|
},
|