@jtl-software/create-cloud-app 0.3.0 → 0.5.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/dist/index.js +531 -158
- package/package.json +9 -9
package/dist/index.js
CHANGED
|
@@ -46,7 +46,6 @@ function defaultScopeFor(authBaseUrl) {
|
|
|
46
46
|
const scopes = ["openid", "profile", "email", "offline_access"];
|
|
47
47
|
if (projectId) {
|
|
48
48
|
scopes.push(`urn:zitadel:iam:org:project:id:${projectId}:aud`);
|
|
49
|
-
scopes.push("urn:zitadel:iam:org:project:id:zitadel:aud");
|
|
50
49
|
}
|
|
51
50
|
return scopes.join(" ");
|
|
52
51
|
}
|
|
@@ -158,7 +157,10 @@ function captureAuthCode(authorizeUrl, expectedState, verifier, options = {}) {
|
|
|
158
157
|
const closeAndSettle = (res, next) => {
|
|
159
158
|
if (settled) return;
|
|
160
159
|
settled = true;
|
|
161
|
-
const finish = () =>
|
|
160
|
+
const finish = () => {
|
|
161
|
+
server.closeAllConnections?.();
|
|
162
|
+
server.close(() => next());
|
|
163
|
+
};
|
|
162
164
|
if (res.writableEnded) {
|
|
163
165
|
finish();
|
|
164
166
|
} else {
|
|
@@ -365,6 +367,9 @@ async function acquireAccessToken(opts) {
|
|
|
365
367
|
authorizeUrl.searchParams.set("state", state);
|
|
366
368
|
authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
|
|
367
369
|
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
370
|
+
if (reauth) {
|
|
371
|
+
authorizeUrl.searchParams.set("prompt", "select_account");
|
|
372
|
+
}
|
|
368
373
|
spinner4?.start("Waiting for browser login");
|
|
369
374
|
try {
|
|
370
375
|
const { code, verifier } = await captureAuthCode(
|
|
@@ -472,6 +477,10 @@ function tenantsFromClaims(claims) {
|
|
|
472
477
|
}
|
|
473
478
|
return ordered;
|
|
474
479
|
}
|
|
480
|
+
function activeTenantId(claims) {
|
|
481
|
+
const active = claims[TENANT_ID_CLAIM];
|
|
482
|
+
return typeof active === "string" && active ? active : void 0;
|
|
483
|
+
}
|
|
475
484
|
function validateHost(input, flag) {
|
|
476
485
|
let url;
|
|
477
486
|
try {
|
|
@@ -681,6 +690,20 @@ function detectBackend(root) {
|
|
|
681
690
|
}
|
|
682
691
|
return { kind: "node", envFile: path2.join(root, ".env") };
|
|
683
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
|
+
}
|
|
684
707
|
function readExistingCredentials(backend) {
|
|
685
708
|
if (backend.kind === "node") {
|
|
686
709
|
const values = readEnvFile(backend.envFile);
|
|
@@ -773,7 +796,8 @@ function resolveOptions(args) {
|
|
|
773
796
|
...COMMON_ARG_OPTIONS,
|
|
774
797
|
"existing-app-id": { type: "string" },
|
|
775
798
|
new: { type: "boolean" },
|
|
776
|
-
"on-collision": { type: "string" }
|
|
799
|
+
"on-collision": { type: "string" },
|
|
800
|
+
"app-service-path": { type: "string" }
|
|
777
801
|
},
|
|
778
802
|
strict: true,
|
|
779
803
|
allowPositionals: false
|
|
@@ -789,11 +813,13 @@ function resolveOptions(args) {
|
|
|
789
813
|
}
|
|
790
814
|
const onCollisionRaw = parsed.values["on-collision"];
|
|
791
815
|
const onCollision = onCollisionRaw ? parseCollisionAction(onCollisionRaw) : void 0;
|
|
816
|
+
const appServicePath = parsed.values["app-service-path"] ?? "app-service";
|
|
792
817
|
return {
|
|
793
818
|
...common,
|
|
794
819
|
existingAppId,
|
|
795
820
|
forceNew,
|
|
796
|
-
onCollision
|
|
821
|
+
onCollision,
|
|
822
|
+
appServicePath
|
|
797
823
|
};
|
|
798
824
|
}
|
|
799
825
|
async function selectExistingOrNew(matches, opts) {
|
|
@@ -848,9 +874,29 @@ async function resolveCollision(targetLabel, credentialPair, opts) {
|
|
|
848
874
|
})
|
|
849
875
|
);
|
|
850
876
|
}
|
|
851
|
-
async function
|
|
877
|
+
async function confirmRegister(opts, message, isUpdate) {
|
|
878
|
+
if (opts.yes) return "proceed";
|
|
879
|
+
return ensureContinue(
|
|
880
|
+
await p2.select({
|
|
881
|
+
message,
|
|
882
|
+
options: [
|
|
883
|
+
{
|
|
884
|
+
value: "proceed",
|
|
885
|
+
label: isUpdate ? "Update the app" : "Create the app"
|
|
886
|
+
},
|
|
887
|
+
{
|
|
888
|
+
value: "switch-org",
|
|
889
|
+
label: "Use a different organization",
|
|
890
|
+
hint: "sign in again and pick another org"
|
|
891
|
+
},
|
|
892
|
+
{ value: "cancel", label: "Cancel" }
|
|
893
|
+
]
|
|
894
|
+
})
|
|
895
|
+
);
|
|
896
|
+
}
|
|
897
|
+
async function listTenantApps(apiBaseUrl, jwt, tenantId, appServicePath = "app-service") {
|
|
852
898
|
const res = await apiCall(
|
|
853
|
-
`${apiBaseUrl}/
|
|
899
|
+
`${apiBaseUrl}/${appServicePath}/tenants/apps?onlyAppsOfTenant=true&returnAll=true`,
|
|
854
900
|
{
|
|
855
901
|
headers: {
|
|
856
902
|
Authorization: `Bearer ${jwt}`,
|
|
@@ -869,24 +915,28 @@ async function listTenantApps(apiBaseUrl, jwt, tenantId) {
|
|
|
869
915
|
technicalName,
|
|
870
916
|
clientId: entry.clientId,
|
|
871
917
|
provisioningStatus: entry.activeVersion?.provisioningStatus,
|
|
872
|
-
isInstalled: entry.isInstalled
|
|
918
|
+
isInstalled: entry.isInstalled,
|
|
919
|
+
publicClientId: entry.credentials?.publicClient?.clientId
|
|
873
920
|
};
|
|
874
921
|
}).filter((a) => a !== null);
|
|
875
922
|
}
|
|
876
|
-
async function updateAppManifest(apiBaseUrl, jwt, tenantId, appId, manifest) {
|
|
877
|
-
await apiCall(
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
923
|
+
async function updateAppManifest(apiBaseUrl, jwt, tenantId, appId, manifest, appServicePath = "app-service") {
|
|
924
|
+
await apiCall(
|
|
925
|
+
`${apiBaseUrl}/${appServicePath}/store/apps/${appId}/manifest`,
|
|
926
|
+
{
|
|
927
|
+
method: "PUT",
|
|
928
|
+
headers: {
|
|
929
|
+
Authorization: `Bearer ${jwt}`,
|
|
930
|
+
"Content-Type": "application/json",
|
|
931
|
+
"X-Tenant-Id": tenantId
|
|
932
|
+
},
|
|
933
|
+
body: JSON.stringify({ manifest, tcAccepted: true })
|
|
934
|
+
}
|
|
935
|
+
);
|
|
886
936
|
}
|
|
887
|
-
async function createDraftApp(apiBaseUrl, jwt, tenantId, manifest) {
|
|
937
|
+
async function createDraftApp(apiBaseUrl, jwt, tenantId, manifest, appServicePath = "app-service") {
|
|
888
938
|
const created = await apiCall(
|
|
889
|
-
`${apiBaseUrl}/
|
|
939
|
+
`${apiBaseUrl}/${appServicePath}/store/apps`,
|
|
890
940
|
{
|
|
891
941
|
method: "POST",
|
|
892
942
|
headers: {
|
|
@@ -907,9 +957,9 @@ async function createDraftApp(apiBaseUrl, jwt, tenantId, manifest) {
|
|
|
907
957
|
}
|
|
908
958
|
return created.id;
|
|
909
959
|
}
|
|
910
|
-
async function provisionApp(apiBaseUrl, jwt, tenantId, appId) {
|
|
960
|
+
async function provisionApp(apiBaseUrl, jwt, tenantId, appId, appServicePath = "app-service") {
|
|
911
961
|
const provisioned = await apiCall(
|
|
912
|
-
`${apiBaseUrl}/
|
|
962
|
+
`${apiBaseUrl}/${appServicePath}/store/apps/${appId}/provision`,
|
|
913
963
|
{
|
|
914
964
|
method: "PUT",
|
|
915
965
|
headers: {
|
|
@@ -927,7 +977,11 @@ async function provisionApp(apiBaseUrl, jwt, tenantId, appId) {
|
|
|
927
977
|
`provision response missing clientId or clientSecret: ${JSON.stringify(provisioned)}`
|
|
928
978
|
);
|
|
929
979
|
}
|
|
930
|
-
return {
|
|
980
|
+
return {
|
|
981
|
+
clientId,
|
|
982
|
+
clientSecret,
|
|
983
|
+
publicClientId: provisioned.credentials?.publicClient?.clientId
|
|
984
|
+
};
|
|
931
985
|
}
|
|
932
986
|
function credentialLabels(backend) {
|
|
933
987
|
if (backend.kind === "node") {
|
|
@@ -986,15 +1040,16 @@ async function register(argv = [], runOpts = {}) {
|
|
|
986
1040
|
console.log();
|
|
987
1041
|
p2.intro(pc2.bgCyan(pc2.black(" register cloud app ")));
|
|
988
1042
|
const opts = resolveOptions(argv);
|
|
989
|
-
const backend = detectBackend(root);
|
|
1043
|
+
const backend = fs2.existsSync(path2.join(root, "packages", "backend")) ? detectBackend(root) : null;
|
|
990
1044
|
p2.note(
|
|
991
1045
|
[
|
|
992
1046
|
`${pc2.dim("API")} ${opts.api}`,
|
|
1047
|
+
opts.appServicePath !== "app-service" ? `${pc2.dim("App svc")} /${opts.appServicePath}` : null,
|
|
993
1048
|
`${pc2.dim("Auth")} ${opts.auth}`,
|
|
994
1049
|
`${pc2.dim("Hub")} ${opts.hub}`,
|
|
995
1050
|
`${pc2.dim("Client ID")} ${opts.clientId}`,
|
|
996
1051
|
`${pc2.dim("Scope")} ${opts.scope}`
|
|
997
|
-
].join("\n"),
|
|
1052
|
+
].filter(Boolean).join("\n"),
|
|
998
1053
|
"Target"
|
|
999
1054
|
);
|
|
1000
1055
|
const spinner4 = p2.spinner();
|
|
@@ -1011,114 +1066,196 @@ async function register(argv = [], runOpts = {}) {
|
|
|
1011
1066
|
} catch (err) {
|
|
1012
1067
|
bail(err.message);
|
|
1013
1068
|
}
|
|
1014
|
-
const identityJwt = jwt;
|
|
1015
|
-
jwt = apiTokenFor(jwt);
|
|
1016
|
-
spinner4.start("Fetching tenants");
|
|
1017
|
-
let tenants;
|
|
1018
|
-
try {
|
|
1019
|
-
tenants = await fetchTenants(opts.api, identityJwt);
|
|
1020
|
-
spinner4.stop(
|
|
1021
|
-
`Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
|
|
1022
|
-
);
|
|
1023
|
-
} catch (err) {
|
|
1024
|
-
spinner4.stop("Failed to fetch tenants");
|
|
1025
|
-
bail(err.message);
|
|
1026
|
-
}
|
|
1027
|
-
const tenant = await selectTenant(tenants, opts);
|
|
1028
1069
|
const manifest = readManifest(path2.join(root, "app.json"));
|
|
1029
1070
|
const summary = summarizeManifest(manifest);
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
spinner4.stop(
|
|
1036
|
-
`Found ${matches.length} app${matches.length === 1 ? "" : "s"} with technical name ${pc2.cyan(summary.technicalName)}`
|
|
1037
|
-
);
|
|
1038
|
-
} catch (err) {
|
|
1039
|
-
spinner4.stop("Failed to look up tenant apps");
|
|
1040
|
-
bail(err.message);
|
|
1041
|
-
}
|
|
1042
|
-
const existing = await selectExistingOrNew(matches, opts);
|
|
1043
|
-
const reviewLines = [
|
|
1044
|
-
`${pc2.dim("Technical name")} ${summary.technicalName}`,
|
|
1045
|
-
`${pc2.dim("Version")} ${summary.version}`,
|
|
1046
|
-
`${pc2.dim("Tenant")} ${tenant.name} (${tenant.slug})`,
|
|
1047
|
-
existing ? `${pc2.dim("Updating")} ${existing.technicalName} (${existing.id}) [${existing.provisioningStatus ?? "?"}]` : `${pc2.dim("Action")} create new`
|
|
1048
|
-
].filter(Boolean);
|
|
1049
|
-
p2.note(reviewLines.join("\n"), "Review");
|
|
1050
|
-
const confirmMessage = existing ? `Update app "${existing.technicalName}" in tenant "${tenant.name}"?` : `Create app "${summary.technicalName}" in tenant "${tenant.name}"?`;
|
|
1051
|
-
const confirm2 = await confirmAction(opts, confirmMessage, existing !== null);
|
|
1052
|
-
if (!confirm2) bail("Cancelled.");
|
|
1053
|
-
if (existing) {
|
|
1054
|
-
spinner4.start("Updating manifest");
|
|
1071
|
+
for (; ; ) {
|
|
1072
|
+
const identityJwt = jwt;
|
|
1073
|
+
const apiJwt = apiTokenFor(jwt);
|
|
1074
|
+
spinner4.start("Fetching tenants");
|
|
1075
|
+
let tenants;
|
|
1055
1076
|
try {
|
|
1056
|
-
await
|
|
1057
|
-
spinner4.stop(
|
|
1077
|
+
tenants = await fetchTenants(opts.api, identityJwt);
|
|
1078
|
+
spinner4.stop(
|
|
1079
|
+
`Found ${tenants.length} tenant${tenants.length === 1 ? "" : "s"}`
|
|
1080
|
+
);
|
|
1058
1081
|
} catch (err) {
|
|
1059
|
-
spinner4.stop("Failed to
|
|
1082
|
+
spinner4.stop("Failed to fetch tenants");
|
|
1060
1083
|
bail(err.message);
|
|
1061
1084
|
}
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1085
|
+
let tenant;
|
|
1086
|
+
const scopedTenantId = opts.tenant ? void 0 : activeTenantId(decodeJwtClaims(identityJwt));
|
|
1087
|
+
const scopedTenant = scopedTenantId ? tenants.find((t) => t.id === scopedTenantId) : void 0;
|
|
1088
|
+
if (scopedTenant) {
|
|
1089
|
+
tenant = scopedTenant;
|
|
1090
|
+
p2.log.info(
|
|
1091
|
+
`Organization ${pc2.cyan(tenant.name)} ${pc2.dim(`(${tenant.slug})`)} \u2014 selected at login`
|
|
1092
|
+
);
|
|
1093
|
+
} else {
|
|
1094
|
+
tenant = await selectTenant(tenants, opts);
|
|
1095
|
+
}
|
|
1096
|
+
spinner4.start("Looking for apps matching this manifest");
|
|
1097
|
+
let matches;
|
|
1098
|
+
try {
|
|
1099
|
+
const apps = await listTenantApps(
|
|
1100
|
+
opts.api,
|
|
1101
|
+
apiJwt,
|
|
1102
|
+
tenant.id,
|
|
1103
|
+
opts.appServicePath
|
|
1104
|
+
);
|
|
1105
|
+
matches = apps.filter((a) => a.technicalName === summary.technicalName);
|
|
1106
|
+
spinner4.stop(
|
|
1107
|
+
`Found ${matches.length} app${matches.length === 1 ? "" : "s"} with technical name ${pc2.cyan(summary.technicalName)}`
|
|
1108
|
+
);
|
|
1109
|
+
} catch (err) {
|
|
1110
|
+
spinner4.stop("Failed to look up tenant apps");
|
|
1111
|
+
bail(err.message);
|
|
1112
|
+
}
|
|
1113
|
+
const existing = await selectExistingOrNew(matches, opts);
|
|
1114
|
+
const reviewLines = [
|
|
1115
|
+
`${pc2.dim("Technical name")} ${summary.technicalName}`,
|
|
1116
|
+
`${pc2.dim("Version")} ${summary.version}`,
|
|
1117
|
+
`${pc2.dim("Tenant")} ${tenant.name} (${tenant.slug})`,
|
|
1118
|
+
existing ? `${pc2.dim("Updating")} ${existing.technicalName} (${existing.id}) [${existing.provisioningStatus ?? "?"}]` : `${pc2.dim("Action")} create new`
|
|
1119
|
+
].filter(Boolean);
|
|
1120
|
+
p2.note(reviewLines.join("\n"), "Review");
|
|
1121
|
+
const confirmMessage = existing ? `Update app "${existing.technicalName}" in tenant "${tenant.name}"?` : `Create app "${summary.technicalName}" in tenant "${tenant.name}"?`;
|
|
1122
|
+
const decision = await confirmRegister(
|
|
1123
|
+
opts,
|
|
1124
|
+
confirmMessage,
|
|
1125
|
+
existing !== null
|
|
1067
1126
|
);
|
|
1068
|
-
if (
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1127
|
+
if (decision === "cancel") bail("Cancelled.");
|
|
1128
|
+
if (decision === "switch-org") {
|
|
1129
|
+
try {
|
|
1130
|
+
jwt = await acquireAccessToken({
|
|
1131
|
+
auth: opts.auth,
|
|
1132
|
+
clientId: opts.clientId,
|
|
1133
|
+
scope: opts.scope,
|
|
1134
|
+
reauth: true,
|
|
1135
|
+
// forces prompt=select_account so the user re-picks the organization
|
|
1136
|
+
spinner: spinner4,
|
|
1137
|
+
captureOptions: runOpts.captureOptions
|
|
1138
|
+
});
|
|
1139
|
+
} catch (err) {
|
|
1140
|
+
bail(err.message);
|
|
1141
|
+
}
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
if (existing) {
|
|
1145
|
+
spinner4.start("Updating manifest");
|
|
1146
|
+
try {
|
|
1147
|
+
await updateAppManifest(
|
|
1148
|
+
opts.api,
|
|
1149
|
+
apiJwt,
|
|
1150
|
+
tenant.id,
|
|
1151
|
+
existing.id,
|
|
1152
|
+
manifest,
|
|
1153
|
+
opts.appServicePath
|
|
1154
|
+
);
|
|
1155
|
+
spinner4.stop("Manifest updated");
|
|
1156
|
+
} catch (err) {
|
|
1157
|
+
spinner4.stop("Failed to update manifest");
|
|
1158
|
+
bail(err.message);
|
|
1159
|
+
}
|
|
1160
|
+
if (existing.publicClientId) {
|
|
1161
|
+
const written = writeFrontendPublicClient(
|
|
1162
|
+
root,
|
|
1163
|
+
opts.auth,
|
|
1164
|
+
existing.publicClientId
|
|
1165
|
+
);
|
|
1166
|
+
if (written) {
|
|
1167
|
+
p2.log.success(
|
|
1168
|
+
`Wrote VITE_JTL_CLIENT_ID/VITE_JTL_ISSUER to ${pc2.cyan(path2.relative(root, written))}.`
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
printHubLink(
|
|
1173
|
+
opts.hub,
|
|
1174
|
+
tenant.slug,
|
|
1175
|
+
existing.id,
|
|
1176
|
+
existing.isInstalled ?? false
|
|
1177
|
+
);
|
|
1178
|
+
if (backend && existing.clientId) {
|
|
1179
|
+
const labels = credentialLabels(backend);
|
|
1180
|
+
const envFile = backend.kind === "node" ? backend.envFile : backend.settingsFile;
|
|
1181
|
+
const localCreds = readExistingCredentials(backend);
|
|
1182
|
+
const hasLocalCreds = !!(localCreds.clientId && localCreds.clientSecret);
|
|
1183
|
+
if (!hasLocalCreds) {
|
|
1184
|
+
p2.outro(
|
|
1185
|
+
pc2.yellow(
|
|
1186
|
+
`Manifest updated. Your backend config is missing ${labels.pair} \u2014 they can't be recovered. Re-create the app to get a fresh secret.`
|
|
1187
|
+
)
|
|
1188
|
+
);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1074
1191
|
p2.outro(
|
|
1075
|
-
pc2.
|
|
1076
|
-
`Manifest updated.
|
|
1192
|
+
pc2.green(
|
|
1193
|
+
`Manifest updated. Existing ${labels.pair} in ${pc2.cyan(path2.relative(root, envFile))} kept as-is.`
|
|
1077
1194
|
)
|
|
1078
1195
|
);
|
|
1079
|
-
|
|
1196
|
+
} else {
|
|
1197
|
+
p2.outro(pc2.green("Manifest updated."));
|
|
1080
1198
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1199
|
+
return;
|
|
1200
|
+
}
|
|
1201
|
+
spinner4.start("Creating draft app");
|
|
1202
|
+
let appId;
|
|
1203
|
+
try {
|
|
1204
|
+
appId = await createDraftApp(
|
|
1205
|
+
opts.api,
|
|
1206
|
+
apiJwt,
|
|
1207
|
+
tenant.id,
|
|
1208
|
+
manifest,
|
|
1209
|
+
opts.appServicePath
|
|
1085
1210
|
);
|
|
1086
|
-
|
|
1087
|
-
|
|
1211
|
+
spinner4.stop(`Created app ${pc2.cyan(appId)}`);
|
|
1212
|
+
} catch (err) {
|
|
1213
|
+
spinner4.stop("Failed to create app");
|
|
1214
|
+
bail(err.message);
|
|
1215
|
+
}
|
|
1216
|
+
spinner4.start("Provisioning app");
|
|
1217
|
+
let clientId;
|
|
1218
|
+
let clientSecret;
|
|
1219
|
+
let publicClientId;
|
|
1220
|
+
try {
|
|
1221
|
+
({ clientId, clientSecret, publicClientId } = await provisionApp(
|
|
1222
|
+
opts.api,
|
|
1223
|
+
apiJwt,
|
|
1224
|
+
tenant.id,
|
|
1225
|
+
appId,
|
|
1226
|
+
opts.appServicePath
|
|
1227
|
+
));
|
|
1228
|
+
spinner4.stop("Provisioned");
|
|
1229
|
+
} catch (err) {
|
|
1230
|
+
spinner4.stop("Failed to provision app");
|
|
1231
|
+
bail(err.message);
|
|
1232
|
+
}
|
|
1233
|
+
if (publicClientId) {
|
|
1234
|
+
const written = writeFrontendPublicClient(
|
|
1235
|
+
root,
|
|
1236
|
+
opts.auth,
|
|
1237
|
+
publicClientId
|
|
1238
|
+
);
|
|
1239
|
+
if (written) {
|
|
1240
|
+
p2.log.success(
|
|
1241
|
+
`Wrote VITE_JTL_CLIENT_ID/VITE_JTL_ISSUER to ${pc2.cyan(path2.relative(root, written))}.`
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1088
1244
|
}
|
|
1245
|
+
if (backend) {
|
|
1246
|
+
await persistCredentials(backend, clientId, clientSecret, root, opts, {
|
|
1247
|
+
hub: opts.hub,
|
|
1248
|
+
tenantSlug: tenant.slug,
|
|
1249
|
+
appId,
|
|
1250
|
+
// A freshly created app can't be installed yet.
|
|
1251
|
+
isInstalled: false
|
|
1252
|
+
});
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
printHubLink(opts.hub, tenant.slug, appId, false);
|
|
1256
|
+
p2.outro(pc2.green("Done. App registered."));
|
|
1089
1257
|
return;
|
|
1090
1258
|
}
|
|
1091
|
-
spinner4.start("Creating draft app");
|
|
1092
|
-
let appId;
|
|
1093
|
-
try {
|
|
1094
|
-
appId = await createDraftApp(opts.api, jwt, tenant.id, manifest);
|
|
1095
|
-
spinner4.stop(`Created app ${pc2.cyan(appId)}`);
|
|
1096
|
-
} catch (err) {
|
|
1097
|
-
spinner4.stop("Failed to create app");
|
|
1098
|
-
bail(err.message);
|
|
1099
|
-
}
|
|
1100
|
-
spinner4.start("Provisioning app");
|
|
1101
|
-
let clientId;
|
|
1102
|
-
let clientSecret;
|
|
1103
|
-
try {
|
|
1104
|
-
({ clientId, clientSecret } = await provisionApp(
|
|
1105
|
-
opts.api,
|
|
1106
|
-
jwt,
|
|
1107
|
-
tenant.id,
|
|
1108
|
-
appId
|
|
1109
|
-
));
|
|
1110
|
-
spinner4.stop("Provisioned");
|
|
1111
|
-
} catch (err) {
|
|
1112
|
-
spinner4.stop("Failed to provision app");
|
|
1113
|
-
bail(err.message);
|
|
1114
|
-
}
|
|
1115
|
-
await persistCredentials(backend, clientId, clientSecret, root, opts, {
|
|
1116
|
-
hub: opts.hub,
|
|
1117
|
-
tenantSlug: tenant.slug,
|
|
1118
|
-
appId,
|
|
1119
|
-
// A freshly created app can't be installed yet.
|
|
1120
|
-
isInstalled: false
|
|
1121
|
-
});
|
|
1122
1259
|
}
|
|
1123
1260
|
|
|
1124
1261
|
// src/listing.ts
|
|
@@ -1391,7 +1528,25 @@ async function listing(argv = [], runOpts = {}) {
|
|
|
1391
1528
|
// src/scaffold.ts
|
|
1392
1529
|
import crypto2 from "crypto";
|
|
1393
1530
|
import fs4 from "fs";
|
|
1531
|
+
import net from "net";
|
|
1394
1532
|
import path4 from "path";
|
|
1533
|
+
var DEFAULT_FRONTEND_PORT = 3004;
|
|
1534
|
+
var BACKEND_PORT = 3005;
|
|
1535
|
+
function isPortFree(port) {
|
|
1536
|
+
return new Promise((resolve) => {
|
|
1537
|
+
const server = net.createServer();
|
|
1538
|
+
server.once("error", () => resolve(false));
|
|
1539
|
+
server.once("listening", () => server.close(() => resolve(true)));
|
|
1540
|
+
server.listen(port, "127.0.0.1");
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
async function pickFrontendPort() {
|
|
1544
|
+
for (let port = DEFAULT_FRONTEND_PORT; port <= 3999; port++) {
|
|
1545
|
+
if (port === BACKEND_PORT) continue;
|
|
1546
|
+
if (await isPortFree(port)) return port;
|
|
1547
|
+
}
|
|
1548
|
+
return DEFAULT_FRONTEND_PORT;
|
|
1549
|
+
}
|
|
1395
1550
|
function toPascalCase(kebab) {
|
|
1396
1551
|
return kebab.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
1397
1552
|
}
|
|
@@ -1413,7 +1568,8 @@ function buildPlaceholders(opts) {
|
|
|
1413
1568
|
"{{APP_NAME_PASCAL}}": toPascalCase(opts.appName),
|
|
1414
1569
|
"{{APP_TECHNICAL_NAME}}": buildTechnicalName(opts.appName),
|
|
1415
1570
|
"{{APP_DESCRIPTION}}": opts.description,
|
|
1416
|
-
"{{APP_DESCRIPTION_SHORT}}": buildShortDescription(opts.description)
|
|
1571
|
+
"{{APP_DESCRIPTION_SHORT}}": buildShortDescription(opts.description),
|
|
1572
|
+
"{{FRONTEND_PORT}}": String(opts.frontendPort)
|
|
1417
1573
|
};
|
|
1418
1574
|
}
|
|
1419
1575
|
function applyPlaceholders(content, replacements) {
|
|
@@ -1449,9 +1605,11 @@ function transformPackageJson(content, appName, templateType, replacements) {
|
|
|
1449
1605
|
const result = applyPlaceholders(JSON.stringify(pkg, null, 2), replacements);
|
|
1450
1606
|
return result + "\n";
|
|
1451
1607
|
}
|
|
1608
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", "dist", ".turbo"]);
|
|
1452
1609
|
function copyDir(src, dest, replacements, templateType, appName) {
|
|
1453
1610
|
fs4.mkdirSync(dest, { recursive: true });
|
|
1454
1611
|
for (const entry of fs4.readdirSync(src, { withFileTypes: true })) {
|
|
1612
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
|
|
1455
1613
|
const srcPath = path4.join(src, entry.name);
|
|
1456
1614
|
let destName = entry.name;
|
|
1457
1615
|
if (destName.startsWith("_git")) {
|
|
@@ -1486,40 +1644,87 @@ function buildReplacements(base, template) {
|
|
|
1486
1644
|
}
|
|
1487
1645
|
return { ...base, ...resolved };
|
|
1488
1646
|
}
|
|
1647
|
+
function applyManifestAuthentication(appJsonPath, opts) {
|
|
1648
|
+
const manifest = JSON.parse(fs4.readFileSync(appJsonPath, "utf-8"));
|
|
1649
|
+
const { frontend, backend } = opts.authentication;
|
|
1650
|
+
const authentication = {};
|
|
1651
|
+
if (frontend) {
|
|
1652
|
+
const origin = `http://localhost:${opts.frontendPort}`;
|
|
1653
|
+
authentication.publicClient = {
|
|
1654
|
+
redirectUris: [`${origin}/callback`],
|
|
1655
|
+
postLogoutRedirectUris: [`${origin}/`]
|
|
1656
|
+
};
|
|
1657
|
+
}
|
|
1658
|
+
if (backend) {
|
|
1659
|
+
authentication.serviceAccount = {
|
|
1660
|
+
clientCredentials: true,
|
|
1661
|
+
description: `${opts.appName} backend service account`
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
manifest.authentication = authentication;
|
|
1665
|
+
if (!frontend) {
|
|
1666
|
+
delete manifest.lifecycle;
|
|
1667
|
+
if (manifest.capabilities) {
|
|
1668
|
+
delete manifest.capabilities.hub;
|
|
1669
|
+
if (manifest.capabilities.erp) {
|
|
1670
|
+
delete manifest.capabilities.erp.menuItems;
|
|
1671
|
+
delete manifest.capabilities.erp.pane;
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
fs4.writeFileSync(appJsonPath, JSON.stringify(manifest, null, 2) + "\n");
|
|
1676
|
+
}
|
|
1489
1677
|
async function scaffold({
|
|
1490
1678
|
appName,
|
|
1491
1679
|
description,
|
|
1492
1680
|
backend,
|
|
1493
1681
|
frontend,
|
|
1494
1682
|
shared,
|
|
1683
|
+
frontendPort = DEFAULT_FRONTEND_PORT,
|
|
1684
|
+
authentication,
|
|
1495
1685
|
targetDir: customTargetDir
|
|
1496
1686
|
}) {
|
|
1497
1687
|
const targetDir = customTargetDir ?? path4.resolve(process.cwd(), appName);
|
|
1498
|
-
const baseReplacements = buildPlaceholders({
|
|
1688
|
+
const baseReplacements = buildPlaceholders({
|
|
1689
|
+
appName,
|
|
1690
|
+
description,
|
|
1691
|
+
frontendPort
|
|
1692
|
+
});
|
|
1499
1693
|
if (fs4.existsSync(targetDir)) {
|
|
1500
1694
|
throw new Error(`Directory "${appName}" already exists`);
|
|
1501
1695
|
}
|
|
1502
1696
|
fs4.mkdirSync(targetDir, { recursive: true });
|
|
1503
1697
|
copyDir(shared.dir, targetDir, baseReplacements);
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1698
|
+
if (frontend) {
|
|
1699
|
+
const frontendDest = path4.join(targetDir, "packages", "frontend");
|
|
1700
|
+
const frontendReplacements = buildReplacements(baseReplacements, frontend);
|
|
1701
|
+
copyDir(
|
|
1702
|
+
frontend.dir,
|
|
1703
|
+
frontendDest,
|
|
1704
|
+
frontendReplacements,
|
|
1705
|
+
frontend.meta.type,
|
|
1706
|
+
appName
|
|
1707
|
+
);
|
|
1708
|
+
}
|
|
1709
|
+
if (backend) {
|
|
1710
|
+
const backendDest = path4.join(targetDir, "packages", "backend");
|
|
1711
|
+
const backendReplacements = buildReplacements(baseReplacements, backend);
|
|
1712
|
+
copyDir(
|
|
1713
|
+
backend.dir,
|
|
1714
|
+
backendDest,
|
|
1715
|
+
backendReplacements,
|
|
1716
|
+
backend.meta.type,
|
|
1717
|
+
appName
|
|
1718
|
+
);
|
|
1719
|
+
}
|
|
1720
|
+
if (authentication) {
|
|
1721
|
+
applyManifestAuthentication(path4.join(targetDir, "app.json"), {
|
|
1722
|
+
appName,
|
|
1723
|
+
frontendPort,
|
|
1724
|
+
authentication
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
const runtimeVersions = backend?.meta.runtimeVersions ?? {};
|
|
1523
1728
|
if (Object.keys(runtimeVersions).length > 0) {
|
|
1524
1729
|
const toolVersions = Object.entries(runtimeVersions).map(([tool, version]) => `${tool} ${version}`).join("\n");
|
|
1525
1730
|
fs4.writeFileSync(
|
|
@@ -1586,14 +1791,163 @@ function getTemplatesByType(templates, type) {
|
|
|
1586
1791
|
}
|
|
1587
1792
|
|
|
1588
1793
|
// src/posthog.ts
|
|
1794
|
+
import { randomUUID } from "crypto";
|
|
1795
|
+
import fs6 from "fs";
|
|
1796
|
+
import os2 from "os";
|
|
1797
|
+
import path6 from "path";
|
|
1589
1798
|
import { PostHog } from "posthog-node";
|
|
1590
1799
|
var TOKEN = "phc_SkFAxTIa4RUQxotci1Ftdw7xefZK2Ax0LEIGcl8zuD2";
|
|
1800
|
+
var CLI_VERSION = "0.5.1";
|
|
1591
1801
|
var client = TOKEN ? new PostHog(TOKEN, { host: "https://e.jtl-cloud.com" }) : null;
|
|
1802
|
+
function telemetryDisabledByEnv() {
|
|
1803
|
+
const dnt = process.env.DO_NOT_TRACK;
|
|
1804
|
+
if (dnt && dnt !== "0" && dnt !== "false") return true;
|
|
1805
|
+
const optOut = process.env.JTL_CLI_TELEMETRY;
|
|
1806
|
+
if (optOut === "0" || optOut === "false") return true;
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
function telemetryClient() {
|
|
1810
|
+
if (!client || telemetryDisabledByEnv()) return null;
|
|
1811
|
+
return client;
|
|
1812
|
+
}
|
|
1813
|
+
function telemetryIdFile() {
|
|
1814
|
+
return process.env.JTL_CLI_TELEMETRY_ID_FILE ?? path6.join(os2.homedir(), ".config", "jtl-cli", "telemetry.json");
|
|
1815
|
+
}
|
|
1816
|
+
var cachedId;
|
|
1817
|
+
function anonymousId() {
|
|
1818
|
+
if (cachedId) return cachedId;
|
|
1819
|
+
const file = telemetryIdFile();
|
|
1820
|
+
try {
|
|
1821
|
+
const parsed = JSON.parse(fs6.readFileSync(file, "utf8"));
|
|
1822
|
+
if (typeof parsed?.anonymous_id === "string" && parsed.anonymous_id) {
|
|
1823
|
+
cachedId = parsed.anonymous_id;
|
|
1824
|
+
return parsed.anonymous_id;
|
|
1825
|
+
}
|
|
1826
|
+
} catch {
|
|
1827
|
+
}
|
|
1828
|
+
const id = randomUUID();
|
|
1829
|
+
try {
|
|
1830
|
+
fs6.mkdirSync(path6.dirname(file), { recursive: true });
|
|
1831
|
+
fs6.writeFileSync(file, JSON.stringify({ anonymous_id: id }, null, 2));
|
|
1832
|
+
} catch {
|
|
1833
|
+
}
|
|
1834
|
+
cachedId = id;
|
|
1835
|
+
return id;
|
|
1836
|
+
}
|
|
1837
|
+
function baseProps() {
|
|
1838
|
+
return {
|
|
1839
|
+
cli_version: CLI_VERSION,
|
|
1840
|
+
node_version: process.version,
|
|
1841
|
+
os: process.platform,
|
|
1842
|
+
arch: process.arch,
|
|
1843
|
+
ci: Boolean(process.env.CI)
|
|
1844
|
+
};
|
|
1845
|
+
}
|
|
1846
|
+
function captureCommand(command, properties = {}) {
|
|
1847
|
+
const c = telemetryClient();
|
|
1848
|
+
if (!c) return;
|
|
1849
|
+
c.capture({
|
|
1850
|
+
distinctId: anonymousId(),
|
|
1851
|
+
event: "cli_command",
|
|
1852
|
+
properties: { command, ...baseProps(), ...properties }
|
|
1853
|
+
});
|
|
1854
|
+
}
|
|
1855
|
+
function captureError(error, properties = {}) {
|
|
1856
|
+
const c = telemetryClient();
|
|
1857
|
+
if (!c) return;
|
|
1858
|
+
try {
|
|
1859
|
+
c.captureException(error, anonymousId(), { ...baseProps(), ...properties });
|
|
1860
|
+
} catch {
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1592
1863
|
async function shutdownPostHog() {
|
|
1593
1864
|
await client?.shutdown();
|
|
1594
1865
|
}
|
|
1595
1866
|
|
|
1596
1867
|
// src/index.ts
|
|
1868
|
+
var NONE = "__none__";
|
|
1869
|
+
function templateSelectOptions(templates) {
|
|
1870
|
+
return templates.map((t) => ({
|
|
1871
|
+
value: t.packageName,
|
|
1872
|
+
label: t.meta.label ?? t.packageName,
|
|
1873
|
+
hint: t.meta.hint
|
|
1874
|
+
}));
|
|
1875
|
+
}
|
|
1876
|
+
function halfSelectOptions(templates, noneHint) {
|
|
1877
|
+
return [
|
|
1878
|
+
...templateSelectOptions(templates),
|
|
1879
|
+
{ value: NONE, label: "None", hint: noneHint }
|
|
1880
|
+
];
|
|
1881
|
+
}
|
|
1882
|
+
function cancelScaffold() {
|
|
1883
|
+
p4.cancel("Cancelled.");
|
|
1884
|
+
process.exit(0);
|
|
1885
|
+
}
|
|
1886
|
+
async function runNextScaffold(backends, frontends, shared) {
|
|
1887
|
+
const cancelOpts = { onCancel: cancelScaffold };
|
|
1888
|
+
const answers = await p4.group(
|
|
1889
|
+
{
|
|
1890
|
+
appName: () => p4.text({
|
|
1891
|
+
message: "App name",
|
|
1892
|
+
placeholder: "my-jtl-app",
|
|
1893
|
+
validate: (value) => {
|
|
1894
|
+
if (!value) return "App name is required";
|
|
1895
|
+
if (!/^[a-z0-9-]+$/.test(value))
|
|
1896
|
+
return "Use lowercase letters, numbers, and hyphens only";
|
|
1897
|
+
}
|
|
1898
|
+
}),
|
|
1899
|
+
description: () => p4.text({ message: "Description", placeholder: "A JTL Platform app" }),
|
|
1900
|
+
frontend: () => p4.select({
|
|
1901
|
+
message: "Frontend (adds a public client for browser login)",
|
|
1902
|
+
options: halfSelectOptions(frontends, "no frontend")
|
|
1903
|
+
}),
|
|
1904
|
+
backend: () => p4.select({
|
|
1905
|
+
message: "Backend (adds a service account for the JTL API)",
|
|
1906
|
+
options: halfSelectOptions(backends, "no backend")
|
|
1907
|
+
})
|
|
1908
|
+
},
|
|
1909
|
+
cancelOpts
|
|
1910
|
+
);
|
|
1911
|
+
const selectedFrontend = frontends.find(
|
|
1912
|
+
(t) => t.packageName === answers.frontend
|
|
1913
|
+
);
|
|
1914
|
+
const selectedBackend = backends.find(
|
|
1915
|
+
(t) => t.packageName === answers.backend
|
|
1916
|
+
);
|
|
1917
|
+
if (!selectedFrontend && !selectedBackend) {
|
|
1918
|
+
p4.cancel("Pick at least a frontend or a backend.");
|
|
1919
|
+
process.exit(1);
|
|
1920
|
+
}
|
|
1921
|
+
const s = p4.spinner();
|
|
1922
|
+
s.start("Scaffolding project");
|
|
1923
|
+
const frontendPort = selectedFrontend ? await pickFrontendPort() : void 0;
|
|
1924
|
+
await scaffold({
|
|
1925
|
+
appName: answers.appName,
|
|
1926
|
+
description: answers.description || "A JTL Platform app",
|
|
1927
|
+
backend: selectedBackend,
|
|
1928
|
+
frontend: selectedFrontend,
|
|
1929
|
+
shared,
|
|
1930
|
+
frontendPort,
|
|
1931
|
+
authentication: {
|
|
1932
|
+
frontend: Boolean(selectedFrontend),
|
|
1933
|
+
backend: Boolean(selectedBackend)
|
|
1934
|
+
}
|
|
1935
|
+
});
|
|
1936
|
+
s.stop("Project scaffolded");
|
|
1937
|
+
if (selectedFrontend) {
|
|
1938
|
+
p4.note(
|
|
1939
|
+
`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.`,
|
|
1940
|
+
"Frontend login"
|
|
1941
|
+
);
|
|
1942
|
+
}
|
|
1943
|
+
printNextSteps(answers.appName);
|
|
1944
|
+
p4.outro(pc4.green("Done! Happy building."));
|
|
1945
|
+
return {
|
|
1946
|
+
next: true,
|
|
1947
|
+
frontend: selectedFrontend ? selectedFrontend.meta.label ?? selectedFrontend.packageName : "none",
|
|
1948
|
+
backend: selectedBackend ? selectedBackend.meta.label ?? selectedBackend.packageName : "none"
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1597
1951
|
async function runScaffold() {
|
|
1598
1952
|
console.log();
|
|
1599
1953
|
p4.intro(pc4.bgCyan(pc4.black(" create-cloud-app ")));
|
|
@@ -1605,6 +1959,9 @@ async function runScaffold() {
|
|
|
1605
1959
|
p4.cancel("No templates found. Ensure template packages are installed.");
|
|
1606
1960
|
process.exit(1);
|
|
1607
1961
|
}
|
|
1962
|
+
if (process.argv.includes("--next")) {
|
|
1963
|
+
return runNextScaffold(backends, frontends, shared);
|
|
1964
|
+
}
|
|
1608
1965
|
const answers = await p4.group(
|
|
1609
1966
|
{
|
|
1610
1967
|
appName: () => p4.text({
|
|
@@ -1622,19 +1979,11 @@ async function runScaffold() {
|
|
|
1622
1979
|
}),
|
|
1623
1980
|
backend: () => p4.select({
|
|
1624
1981
|
message: "Backend",
|
|
1625
|
-
options: backends
|
|
1626
|
-
value: t.packageName,
|
|
1627
|
-
label: t.meta.label ?? t.packageName,
|
|
1628
|
-
hint: t.meta.hint
|
|
1629
|
-
}))
|
|
1982
|
+
options: templateSelectOptions(backends)
|
|
1630
1983
|
}),
|
|
1631
1984
|
frontend: () => p4.select({
|
|
1632
1985
|
message: "Frontend",
|
|
1633
|
-
options: frontends
|
|
1634
|
-
value: t.packageName,
|
|
1635
|
-
label: t.meta.label ?? t.packageName,
|
|
1636
|
-
hint: t.meta.hint
|
|
1637
|
-
}))
|
|
1986
|
+
options: templateSelectOptions(frontends)
|
|
1638
1987
|
})
|
|
1639
1988
|
},
|
|
1640
1989
|
{
|
|
@@ -1660,9 +2009,17 @@ async function runScaffold() {
|
|
|
1660
2009
|
shared
|
|
1661
2010
|
});
|
|
1662
2011
|
s.stop("Project scaffolded");
|
|
2012
|
+
printNextSteps(answers.appName);
|
|
2013
|
+
p4.outro(pc4.green("Done! Happy building."));
|
|
2014
|
+
return {
|
|
2015
|
+
backend: selectedBackend.meta.label ?? selectedBackend.packageName,
|
|
2016
|
+
frontend: selectedFrontend.meta.label ?? selectedFrontend.packageName
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function printNextSteps(appName) {
|
|
1663
2020
|
p4.note(
|
|
1664
2021
|
[
|
|
1665
|
-
`${pc4.cyan("cd")} ${
|
|
2022
|
+
`${pc4.cyan("cd")} ${appName}`,
|
|
1666
2023
|
`${pc4.cyan("npm install")}`,
|
|
1667
2024
|
`${pc4.cyan("npm run register")}`,
|
|
1668
2025
|
`${pc4.cyan("npm run dev")}`,
|
|
@@ -1670,18 +2027,34 @@ async function runScaffold() {
|
|
|
1670
2027
|
].join("\n"),
|
|
1671
2028
|
"Next steps"
|
|
1672
2029
|
);
|
|
1673
|
-
p4.outro(pc4.green("Done! Happy building."));
|
|
1674
2030
|
}
|
|
1675
2031
|
async function main() {
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
2032
|
+
const command = process.argv[2] === "register" ? "register" : process.argv[2] === "listing" ? "listing" : "scaffold";
|
|
2033
|
+
const startedAt = Date.now();
|
|
2034
|
+
try {
|
|
2035
|
+
let detail = {};
|
|
2036
|
+
if (command === "register") {
|
|
2037
|
+
await register(process.argv.slice(3));
|
|
2038
|
+
} else if (command === "listing") {
|
|
2039
|
+
await listing(process.argv.slice(3));
|
|
2040
|
+
} else {
|
|
2041
|
+
detail = await runScaffold();
|
|
2042
|
+
}
|
|
2043
|
+
captureCommand(command, {
|
|
2044
|
+
success: true,
|
|
2045
|
+
duration_ms: Date.now() - startedAt,
|
|
2046
|
+
...detail
|
|
2047
|
+
});
|
|
2048
|
+
} catch (err) {
|
|
2049
|
+
const bail2 = err instanceof BailError;
|
|
2050
|
+
captureCommand(command, {
|
|
2051
|
+
success: false,
|
|
2052
|
+
duration_ms: Date.now() - startedAt,
|
|
2053
|
+
outcome: bail2 ? "bailed" : "error"
|
|
2054
|
+
});
|
|
2055
|
+
if (!bail2) captureError(err, { command });
|
|
2056
|
+
throw err;
|
|
1683
2057
|
}
|
|
1684
|
-
await runScaffold();
|
|
1685
2058
|
}
|
|
1686
2059
|
main().then(() => shutdownPostHog()).catch(async (err) => {
|
|
1687
2060
|
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.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "CLI tool for scaffolding JTL Platform cloud apps",
|
|
6
6
|
"bin": {
|
|
@@ -17,20 +17,20 @@
|
|
|
17
17
|
"dev": "tsx src/index.ts"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@clack/prompts": "^1.
|
|
20
|
+
"@clack/prompts": "^1.7.0",
|
|
21
21
|
"@jtl-software/cloud-app-template-backend-dotnet": "0.0.12",
|
|
22
22
|
"@jtl-software/cloud-app-template-backend-node": "0.0.15",
|
|
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.
|
|
23
|
+
"@jtl-software/cloud-app-template-backend-php": "0.1.2",
|
|
24
|
+
"@jtl-software/cloud-app-template-frontend-react": "0.1.1",
|
|
25
|
+
"@jtl-software/cloud-app-template-shared": "0.1.2",
|
|
26
26
|
"picocolors": "^1.1.1",
|
|
27
|
-
"posthog-node": "^5.
|
|
27
|
+
"posthog-node": "^5.48.1"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@types/node": "^26.
|
|
31
|
-
"msw": "^2.
|
|
30
|
+
"@types/node": "^26.2.0",
|
|
31
|
+
"msw": "^2.15.0",
|
|
32
32
|
"tsup": "^8.5.1",
|
|
33
|
-
"tsx": "^4.
|
|
33
|
+
"tsx": "^4.23.12",
|
|
34
34
|
"typescript": "^6.0.3"
|
|
35
35
|
},
|
|
36
36
|
"publishConfig": {
|