@dotcms/create-app 26.9.3-1 → 26.9.3-1-next.2649
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -16
- package/assets/docker-compose.yml +86 -0
- package/index.js +1017 -309
- package/package.json +3 -3
package/index.js
CHANGED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
// libs/sdk/create-app/src/index.ts
|
|
4
4
|
import cfonts from "cfonts";
|
|
5
|
-
import
|
|
5
|
+
import chalk5 from "chalk";
|
|
6
6
|
import { Command } from "commander";
|
|
7
7
|
import { execa as execa3 } from "execa";
|
|
8
|
-
import
|
|
8
|
+
import fs6 from "fs-extra";
|
|
9
9
|
import ora from "ora";
|
|
10
|
-
import
|
|
10
|
+
import path8 from "path";
|
|
11
11
|
|
|
12
12
|
// libs/sdk/create-app/src/api/index.ts
|
|
13
|
-
import axios from "axios";
|
|
14
13
|
import chalk from "chalk";
|
|
15
14
|
|
|
16
15
|
// libs/sdk/create-app/src/constants/index.ts
|
|
@@ -170,11 +169,6 @@ var FailedToDownloadDockerComposeError = class extends Error {
|
|
|
170
169
|
super(`Failed to download the docker compose file`);
|
|
171
170
|
}
|
|
172
171
|
};
|
|
173
|
-
var FailedToSetUpUVEConfig = class extends Error {
|
|
174
|
-
constructor() {
|
|
175
|
-
super(`Failed to set up UVE configuration in DotCMS.`);
|
|
176
|
-
}
|
|
177
|
-
};
|
|
178
172
|
var FailedToGetDefaultSiteError = class extends Error {
|
|
179
173
|
constructor() {
|
|
180
174
|
super(`Failed to get default site identifier from DotCMS.`);
|
|
@@ -189,16 +183,90 @@ function Err(val) {
|
|
|
189
183
|
return { ok: false, val };
|
|
190
184
|
}
|
|
191
185
|
|
|
186
|
+
// libs/sdk/create-app/src/utils/http.ts
|
|
187
|
+
var HttpError = class extends Error {
|
|
188
|
+
constructor(message, init) {
|
|
189
|
+
super(message);
|
|
190
|
+
this.name = "HttpError";
|
|
191
|
+
this.status = init.status ?? null;
|
|
192
|
+
this.code = init.code;
|
|
193
|
+
if (typeof init.status === "number") {
|
|
194
|
+
this.response = { status: init.status, statusText: init.statusText ?? "" };
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
function isHttpError(error) {
|
|
199
|
+
return error instanceof HttpError;
|
|
200
|
+
}
|
|
201
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
202
|
+
function isSuccess(status) {
|
|
203
|
+
return status >= 200 && status < 300;
|
|
204
|
+
}
|
|
205
|
+
async function readBody(response) {
|
|
206
|
+
const text = await response.text().catch(() => "");
|
|
207
|
+
if (!text) {
|
|
208
|
+
return void 0;
|
|
209
|
+
}
|
|
210
|
+
try {
|
|
211
|
+
return JSON.parse(text);
|
|
212
|
+
} catch {
|
|
213
|
+
return text;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
async function request(url, init, { token, timeoutMs = DEFAULT_TIMEOUT_MS, acceptAnyStatus = false }) {
|
|
217
|
+
const controller = new AbortController();
|
|
218
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
219
|
+
const headers = new Headers(init.headers);
|
|
220
|
+
if (token) {
|
|
221
|
+
headers.set("Authorization", `Bearer ${token}`);
|
|
222
|
+
}
|
|
223
|
+
let response;
|
|
224
|
+
try {
|
|
225
|
+
response = await fetch(url, { ...init, headers, signal: controller.signal });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
const aborted = error?.name === "AbortError";
|
|
228
|
+
const cause = error?.cause;
|
|
229
|
+
throw new HttpError(
|
|
230
|
+
aborted ? `Request to ${url} timed out after ${timeoutMs}ms` : `Request to ${url} failed: ${error?.message ?? String(error)}`,
|
|
231
|
+
{ status: null, code: aborted ? "ETIMEDOUT" : cause?.code }
|
|
232
|
+
);
|
|
233
|
+
} finally {
|
|
234
|
+
clearTimeout(timer);
|
|
235
|
+
}
|
|
236
|
+
const data = await readBody(response);
|
|
237
|
+
if (!isSuccess(response.status) && !acceptAnyStatus) {
|
|
238
|
+
throw new HttpError(`Request failed with status code ${response.status}`, {
|
|
239
|
+
status: response.status,
|
|
240
|
+
statusText: response.statusText
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return { status: response.status, data };
|
|
244
|
+
}
|
|
245
|
+
function httpGet(url, options = {}) {
|
|
246
|
+
return request(url, { method: "GET" }, options);
|
|
247
|
+
}
|
|
248
|
+
function httpPost(url, body, options = {}) {
|
|
249
|
+
return request(
|
|
250
|
+
url,
|
|
251
|
+
{
|
|
252
|
+
method: "POST",
|
|
253
|
+
body: JSON.stringify(body),
|
|
254
|
+
headers: { "Content-Type": "application/json" }
|
|
255
|
+
},
|
|
256
|
+
options
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
192
260
|
// libs/sdk/create-app/src/api/index.ts
|
|
193
261
|
function getSafeErrorDetails(err) {
|
|
194
|
-
if (
|
|
262
|
+
if (isHttpError(err)) {
|
|
195
263
|
const details = [
|
|
196
264
|
err.response?.status ? `status=${err.response.status}` : null,
|
|
197
265
|
err.response?.statusText ? `statusText=${err.response.statusText}` : null,
|
|
198
266
|
err.code ? `code=${err.code}` : null,
|
|
199
267
|
err.message ? `message=${err.message}` : null
|
|
200
268
|
].filter(Boolean);
|
|
201
|
-
return details.length > 0 ? details.join(", ") : "
|
|
269
|
+
return details.length > 0 ? details.join(", ") : "HTTP request failed";
|
|
202
270
|
}
|
|
203
271
|
if (err instanceof Error) {
|
|
204
272
|
return err.message;
|
|
@@ -222,10 +290,10 @@ var DotCMSApi = class {
|
|
|
222
290
|
}) {
|
|
223
291
|
const endpoint = url || this.defaultTokenApi;
|
|
224
292
|
try {
|
|
225
|
-
const res = await
|
|
293
|
+
const res = await httpPost(endpoint, payload);
|
|
226
294
|
return Ok(res.data.entity.token);
|
|
227
295
|
} catch (err) {
|
|
228
|
-
if (
|
|
296
|
+
if (isHttpError(err)) {
|
|
229
297
|
if (err.response?.status === 401) {
|
|
230
298
|
return Err(
|
|
231
299
|
chalk.red("\n\u274C Authentication failed\n\n") + chalk.white("Invalid username or password.\n\n") + chalk.yellow("Please check your credentials and try again:\n") + chalk.white(" \u2022 Verify your username is correct\n") + chalk.white(" \u2022 Ensure your password is correct\n") + chalk.white(" \u2022 Check if your account is active\n")
|
|
@@ -258,8 +326,8 @@ var DotCMSApi = class {
|
|
|
258
326
|
}) {
|
|
259
327
|
try {
|
|
260
328
|
const endpoint = (url || this.defaultSiteApi) + "defaultSite";
|
|
261
|
-
const res = await
|
|
262
|
-
|
|
329
|
+
const res = await httpGet(endpoint, {
|
|
330
|
+
token: authenticationToken
|
|
263
331
|
});
|
|
264
332
|
return Ok(res.data);
|
|
265
333
|
} catch (err) {
|
|
@@ -267,27 +335,10 @@ var DotCMSApi = class {
|
|
|
267
335
|
return Err(new FailedToGetDefaultSiteError());
|
|
268
336
|
}
|
|
269
337
|
}
|
|
270
|
-
/** Setup UVE Config */
|
|
271
|
-
static async setupUVEConfig({
|
|
272
|
-
payload,
|
|
273
|
-
siteId,
|
|
274
|
-
authenticationToken,
|
|
275
|
-
url
|
|
276
|
-
}) {
|
|
277
|
-
try {
|
|
278
|
-
const endpoint = (url || this.defaultUveConfigApi) + siteId;
|
|
279
|
-
const res = await axios.post(endpoint, payload, {
|
|
280
|
-
headers: { Authorization: `Bearer ${authenticationToken}` }
|
|
281
|
-
});
|
|
282
|
-
return Ok(res.data.entity);
|
|
283
|
-
} catch (err) {
|
|
284
|
-
console.error(`failed to setup UVE config: ${getSafeErrorDetails(err)}`);
|
|
285
|
-
return Err(new FailedToSetUpUVEConfig());
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
338
|
};
|
|
289
339
|
|
|
290
340
|
// libs/sdk/create-app/src/asks.ts
|
|
341
|
+
import chalk3 from "chalk";
|
|
291
342
|
import fs from "fs-extra";
|
|
292
343
|
import inquirer from "inquirer";
|
|
293
344
|
import path2 from "path";
|
|
@@ -589,11 +640,13 @@ async function prepareDirectory(basePath, projectName) {
|
|
|
589
640
|
if (files.length === 0) {
|
|
590
641
|
return targetPath;
|
|
591
642
|
}
|
|
643
|
+
const composePath = path2.join(targetPath, "docker-compose.yml");
|
|
644
|
+
const hasComposeFile = fs.existsSync(composePath);
|
|
592
645
|
const ans = await inquirer.prompt([
|
|
593
646
|
{
|
|
594
647
|
type: "confirm",
|
|
595
648
|
name: "confirm",
|
|
596
|
-
message: `\u26A0\uFE0F Directory "${targetPath}" is not empty. All files inside will be deleted. Continue?`,
|
|
649
|
+
message: hasComposeFile ? `\u26A0\uFE0F Directory "${targetPath}" contains a docker-compose.yml from a previous run. Everything EXCEPT that file will be deleted. Continue?` : `\u26A0\uFE0F Directory "${targetPath}" is not empty. All files inside will be deleted. Continue?`,
|
|
597
650
|
default: false
|
|
598
651
|
}
|
|
599
652
|
]);
|
|
@@ -601,77 +654,221 @@ async function prepareDirectory(basePath, projectName) {
|
|
|
601
654
|
console.log("\u274C Operation cancelled.");
|
|
602
655
|
process.exit(1);
|
|
603
656
|
}
|
|
604
|
-
|
|
657
|
+
if (hasComposeFile) {
|
|
658
|
+
const preserved = await fs.readFile(composePath);
|
|
659
|
+
await fs.emptyDir(targetPath);
|
|
660
|
+
await fs.writeFile(composePath, preserved);
|
|
661
|
+
} else {
|
|
662
|
+
await fs.emptyDir(targetPath);
|
|
663
|
+
}
|
|
605
664
|
return targetPath;
|
|
606
665
|
}
|
|
666
|
+
async function askPortConflictAction({
|
|
667
|
+
description,
|
|
668
|
+
canReplace
|
|
669
|
+
}) {
|
|
670
|
+
console.log(
|
|
671
|
+
"\n" + chalk3.yellow("\u26A0 Found a dotCMS already running at ") + chalk3.cyan("http://localhost:8082") + "\n" + chalk3.gray(` ${description}`) + "\n"
|
|
672
|
+
);
|
|
673
|
+
const choices = [
|
|
674
|
+
{
|
|
675
|
+
name: "Use this instance for my project",
|
|
676
|
+
value: "reuse",
|
|
677
|
+
description: "Fastest. Keeps its existing content."
|
|
678
|
+
}
|
|
679
|
+
];
|
|
680
|
+
if (canReplace) {
|
|
681
|
+
choices.push({
|
|
682
|
+
name: "Replace it with a clean instance",
|
|
683
|
+
value: "replace",
|
|
684
|
+
description: "Stops it and DELETES its data, then starts fresh."
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
choices.push({
|
|
688
|
+
name: "Cancel",
|
|
689
|
+
value: "cancel",
|
|
690
|
+
description: "Change nothing and exit."
|
|
691
|
+
});
|
|
692
|
+
const { action } = await inquirer.prompt([
|
|
693
|
+
// `select`, NOT `list`. Inquirer 13 is built on @inquirer/prompts, where the type is
|
|
694
|
+
// `select`; `list` is the inquirer 8/9 name and is not registered, so the message renders
|
|
695
|
+
// and the choices silently do not. Every other prompt in this file already uses `select`.
|
|
696
|
+
{ type: "select", name: "action", message: "How would you like to continue?", choices }
|
|
697
|
+
]);
|
|
698
|
+
return action;
|
|
699
|
+
}
|
|
607
700
|
|
|
608
|
-
// libs/sdk/create-app/src/
|
|
609
|
-
import
|
|
610
|
-
import
|
|
611
|
-
import path4 from "path";
|
|
701
|
+
// libs/sdk/create-app/src/exit-state.ts
|
|
702
|
+
import fs2 from "node:fs";
|
|
703
|
+
import path4 from "node:path";
|
|
612
704
|
|
|
613
705
|
// libs/sdk/create-app/src/utils/index.ts
|
|
614
|
-
import
|
|
615
|
-
import chalk3 from "chalk";
|
|
706
|
+
import chalk4 from "chalk";
|
|
616
707
|
import { execa } from "execa";
|
|
617
|
-
import fs2 from "fs-extra";
|
|
618
|
-
import https from "https";
|
|
619
708
|
import net from "net";
|
|
620
709
|
import path3 from "path";
|
|
621
|
-
|
|
710
|
+
|
|
711
|
+
// libs/sdk/create-app/src/utils/fetch-retry.ts
|
|
712
|
+
function isSuccessStatus(status) {
|
|
713
|
+
return status >= 200 && status < 300;
|
|
714
|
+
}
|
|
715
|
+
function describeRequestFailure(error) {
|
|
716
|
+
if (isHttpError(error)) {
|
|
717
|
+
if (error.code === "ECONNREFUSED") {
|
|
718
|
+
return "Connection refused - service not accepting connections yet";
|
|
719
|
+
}
|
|
720
|
+
if (error.code === "ETIMEDOUT") {
|
|
721
|
+
return "Connection timeout - service too slow or not responding";
|
|
722
|
+
}
|
|
723
|
+
if (error.response) {
|
|
724
|
+
return `HTTP ${error.response.status}: ${error.response.statusText}`;
|
|
725
|
+
}
|
|
726
|
+
return error.code || error.message;
|
|
727
|
+
}
|
|
728
|
+
if (error instanceof Error) {
|
|
729
|
+
return error.message;
|
|
730
|
+
}
|
|
731
|
+
return String(error);
|
|
732
|
+
}
|
|
733
|
+
function formatRetryReport({
|
|
734
|
+
attempt,
|
|
735
|
+
totalAttempts,
|
|
736
|
+
reason,
|
|
737
|
+
nextDelayMs
|
|
738
|
+
}) {
|
|
739
|
+
return `dotCMS not ready (attempt ${attempt}/${totalAttempts}) - ${reason} - retrying in ${Math.round(nextDelayMs / 1e3)}s`;
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// libs/sdk/create-app/src/utils/ports.ts
|
|
743
|
+
var DOTCMS_HTTP_PORT = 8082;
|
|
744
|
+
var REQUIRED_PORTS = [
|
|
745
|
+
{ port: 8082, service: "dotCMS HTTP" },
|
|
746
|
+
{ port: 8443, service: "dotCMS HTTPS" },
|
|
747
|
+
{ port: 8090, service: "dotCMS management" }
|
|
748
|
+
];
|
|
749
|
+
function listPorts(busyPorts) {
|
|
750
|
+
return busyPorts.map(({ port, service }) => ` \u2022 Port ${port} (${service})`).join("\n");
|
|
751
|
+
}
|
|
752
|
+
function abortMessage(busyPorts, detail) {
|
|
753
|
+
return {
|
|
754
|
+
kind: "abort",
|
|
755
|
+
message: [
|
|
756
|
+
"Required ports are already in use:",
|
|
757
|
+
listPorts(busyPorts),
|
|
758
|
+
"",
|
|
759
|
+
detail,
|
|
760
|
+
"",
|
|
761
|
+
"Stop whatever is holding them, or stop an existing stack with:",
|
|
762
|
+
" docker compose down"
|
|
763
|
+
].join("\n")
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
async function resolvePortConflict(options) {
|
|
767
|
+
const { busyPorts, isInteractive, host, probeInstance, askAction, notify, owner } = options;
|
|
768
|
+
if (busyPorts.length === 0) {
|
|
769
|
+
return { kind: "free" };
|
|
770
|
+
}
|
|
771
|
+
const ours = new Set(REQUIRED_PORTS.map(({ port }) => port));
|
|
772
|
+
const looksLikeOurStack = busyPorts.every(({ port }) => ours.has(port)) && busyPorts.some(({ port }) => port === DOTCMS_HTTP_PORT);
|
|
773
|
+
if (!looksLikeOurStack) {
|
|
774
|
+
return abortMessage(
|
|
775
|
+
busyPorts,
|
|
776
|
+
"These are not ports a previous run of this CLI would be holding on its own."
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
const reusable = await probeInstance();
|
|
780
|
+
if (!reusable) {
|
|
781
|
+
return abortMessage(
|
|
782
|
+
busyPorts,
|
|
783
|
+
`Something is listening on ${DOTCMS_HTTP_PORT}, but it did not answer as a usable dotCMS.`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
if (!isInteractive) {
|
|
787
|
+
notify(
|
|
788
|
+
`dotCMS is already running on ${DOTCMS_HTTP_PORT} \u2014 reusing it (non-interactive run).`
|
|
789
|
+
);
|
|
790
|
+
return { kind: "reuse", host };
|
|
791
|
+
}
|
|
792
|
+
const canReplace = Boolean(owner?.project);
|
|
793
|
+
const action = await askAction({
|
|
794
|
+
description: owner?.description ?? `something on port ${DOTCMS_HTTP_PORT}`,
|
|
795
|
+
canReplace
|
|
796
|
+
});
|
|
797
|
+
if (action === "replace" && owner?.project) {
|
|
798
|
+
return { kind: "replace", project: owner.project };
|
|
799
|
+
}
|
|
800
|
+
if (action === "cancel") {
|
|
801
|
+
return {
|
|
802
|
+
kind: "abort",
|
|
803
|
+
message: `Left the dotCMS already running on ${DOTCMS_HTTP_PORT} untouched, as requested.`
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
return { kind: "reuse", host };
|
|
807
|
+
}
|
|
808
|
+
async function describePortOwner(port, run) {
|
|
809
|
+
try {
|
|
810
|
+
const { stdout } = await run("docker", [
|
|
811
|
+
"ps",
|
|
812
|
+
"--filter",
|
|
813
|
+
`publish=${port}`,
|
|
814
|
+
"--format",
|
|
815
|
+
'{{.Label "com.docker.compose.project"}} {{.Status}} {{.Names}}'
|
|
816
|
+
]);
|
|
817
|
+
const line = stdout.trim().split("\n").filter(Boolean)[0];
|
|
818
|
+
if (!line) {
|
|
819
|
+
return void 0;
|
|
820
|
+
}
|
|
821
|
+
const [project, status, name] = line.split(" ");
|
|
822
|
+
return {
|
|
823
|
+
project: project || void 0,
|
|
824
|
+
description: project ? `Docker project "${project}" \xB7 ${status}` : `container ${name} \xB7 ${status} \xB7 not managed by Docker Compose`
|
|
825
|
+
};
|
|
826
|
+
} catch {
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
// libs/sdk/create-app/src/utils/index.ts
|
|
832
|
+
async function fetchWithRetry(url, retries = 5, delay2 = 5e3, requestTimeout = 1e4, onRetry) {
|
|
622
833
|
const errors = [];
|
|
623
834
|
let lastError;
|
|
624
835
|
for (let i = 0; i < retries; i++) {
|
|
625
836
|
try {
|
|
626
|
-
return await
|
|
627
|
-
timeout: requestTimeout,
|
|
628
|
-
// Accept any 2xx status code as success (health endpoints may return 200, 201, 204, etc.)
|
|
629
|
-
validateStatus: (status) => status >= 200 && status < 300
|
|
630
|
-
});
|
|
837
|
+
return await httpGet(url, { timeoutMs: requestTimeout });
|
|
631
838
|
} catch (err) {
|
|
632
839
|
lastError = err;
|
|
633
|
-
|
|
634
|
-
if (axios2.isAxiosError(err)) {
|
|
635
|
-
if (err.code === "ECONNREFUSED") {
|
|
636
|
-
errorMsg = "Connection refused - service not accepting connections";
|
|
637
|
-
} else if (err.code === "ETIMEDOUT" || err.code === "ECONNABORTED") {
|
|
638
|
-
errorMsg = "Connection timeout - service too slow or not responding";
|
|
639
|
-
} else if (err.response) {
|
|
640
|
-
errorMsg = `HTTP ${err.response.status}: ${err.response.statusText}`;
|
|
641
|
-
} else {
|
|
642
|
-
errorMsg = err.code || err.message;
|
|
643
|
-
}
|
|
644
|
-
} else {
|
|
645
|
-
errorMsg = String(err);
|
|
646
|
-
}
|
|
840
|
+
const errorMsg = describeRequestFailure(err);
|
|
647
841
|
errors.push(`Attempt ${i + 1}: ${errorMsg}`);
|
|
648
842
|
if (i === retries - 1) {
|
|
649
|
-
const errorType =
|
|
843
|
+
const errorType = isHttpError(lastError) && lastError.code === "ECONNREFUSED" ? "Connection Refused" : isHttpError(lastError) && lastError.code === "ETIMEDOUT" ? "Timeout" : "Connection Failed";
|
|
650
844
|
throw new Error(
|
|
651
|
-
|
|
845
|
+
chalk4.red(
|
|
652
846
|
`
|
|
653
847
|
\u274C Failed to connect to dotCMS after ${retries} attempts (${errorType})
|
|
654
848
|
|
|
655
849
|
`
|
|
656
|
-
) +
|
|
657
|
-
`) +
|
|
658
|
-
`) +
|
|
659
|
-
`Total retry window: ~${retries * (
|
|
850
|
+
) + chalk4.white(`URL: ${url}
|
|
851
|
+
`) + chalk4.gray(`Request timeout: ${requestTimeout}ms per attempt
|
|
852
|
+
`) + chalk4.gray(
|
|
853
|
+
`Total retry window: ~${retries * (delay2 + requestTimeout) / 1e3}s
|
|
660
854
|
|
|
661
855
|
`
|
|
662
|
-
) +
|
|
856
|
+
) + chalk4.yellow("Common causes:\n") + chalk4.white(" \u2022 dotCMS is still starting up (may need more time)\n") + chalk4.white(" \u2022 Container crashed or failed to start\n") + chalk4.white(" \u2022 Port conflict (8082 already in use)\n") + chalk4.white(" \u2022 Network/firewall blocking connection\n") + chalk4.white(
|
|
663
857
|
` \u2022 Request timeout too short (current: ${requestTimeout}ms)
|
|
664
858
|
|
|
665
859
|
`
|
|
666
|
-
) +
|
|
860
|
+
) + chalk4.gray(
|
|
667
861
|
"Detailed error history:\n" + errors.map((e) => ` \u2022 ${e}`).join("\n")
|
|
668
862
|
)
|
|
669
863
|
);
|
|
670
864
|
}
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
865
|
+
onRetry?.({
|
|
866
|
+
attempt: i + 1,
|
|
867
|
+
totalAttempts: retries,
|
|
868
|
+
reason: errorMsg,
|
|
869
|
+
nextDelayMs: delay2
|
|
870
|
+
});
|
|
871
|
+
await new Promise((r) => setTimeout(r, delay2));
|
|
675
872
|
}
|
|
676
873
|
}
|
|
677
874
|
}
|
|
@@ -710,90 +907,82 @@ function getDotcmsApisByBaseUrl(baseUrl) {
|
|
|
710
907
|
DOTCMS_SITE_API: `${baseUrl}/api/v1/site/`
|
|
711
908
|
};
|
|
712
909
|
}
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
910
|
+
function renderConnectionSummary(report) {
|
|
911
|
+
if (report.wroteEnv && report.filename) {
|
|
912
|
+
console.log(
|
|
913
|
+
chalk4.green(` \u2714 Your dotCMS credentials are already in ${report.filename}
|
|
914
|
+
`) + chalk4.gray(` host : ${report.host}
|
|
915
|
+
`) + chalk4.gray(` site id : ${report.siteId}
|
|
916
|
+
`)
|
|
917
|
+
);
|
|
918
|
+
return;
|
|
919
|
+
}
|
|
920
|
+
console.log(
|
|
921
|
+
chalk4.white(
|
|
922
|
+
report.filename ? ` Add these to your ${report.filename}:
|
|
923
|
+
` : " Configuration for your project:\n"
|
|
924
|
+
) + chalk4.gray(
|
|
925
|
+
report.contents.trimEnd().split("\n").map((l) => ` ${l}`).join("\n")
|
|
926
|
+
) + "\n"
|
|
927
|
+
);
|
|
727
928
|
}
|
|
728
929
|
function finalStepsForNextjs({
|
|
729
930
|
projectPath,
|
|
730
931
|
urlDotCMSInstance,
|
|
731
|
-
|
|
732
|
-
token
|
|
932
|
+
connection
|
|
733
933
|
}) {
|
|
734
934
|
console.log("\n");
|
|
735
|
-
console.log(
|
|
736
|
-
console.log(
|
|
935
|
+
console.log(chalk4.white("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
|
|
936
|
+
console.log(chalk4.greenBright("\u{1F4CB} Next Steps:\n"));
|
|
937
|
+
if (connection) {
|
|
938
|
+
renderConnectionSummary(connection);
|
|
939
|
+
}
|
|
737
940
|
console.log(
|
|
738
|
-
|
|
941
|
+
chalk4.white("1. Navigate to your project:\n") + chalk4.gray(` $ cd ${escapeShellPath(projectPath)}
|
|
739
942
|
`)
|
|
740
943
|
);
|
|
741
944
|
console.log(
|
|
742
|
-
|
|
945
|
+
chalk4.white("2. Start your development server:\n") + chalk4.gray(" $ npm run dev\n")
|
|
743
946
|
);
|
|
744
|
-
console.log(chalk3.white("3. Add your dotCMS configuration to ") + chalk3.green(".env") + ":\n");
|
|
745
|
-
console.log(chalk3.white("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
746
|
-
console.log(chalk3.white(getEnvVariablesForNextJS(urlDotCMSInstance, siteId, token)));
|
|
747
|
-
console.log(chalk3.white("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
748
|
-
console.log(chalk3.gray(" \u{1F4A1} Tip: Copy the block above and paste into your .env file\n"));
|
|
749
947
|
console.log(
|
|
750
|
-
|
|
948
|
+
chalk4.white("3. Open your browser:\n") + chalk4.gray(" \u2192 http://localhost:3000\n")
|
|
751
949
|
);
|
|
752
950
|
console.log(
|
|
753
|
-
|
|
754
|
-
);
|
|
755
|
-
console.log(
|
|
756
|
-
chalk3.white("6. Edit your page content in dotCMS:\n") + chalk3.gray(` \u2192 ${urlDotCMSInstance}/dotAdmin/#/edit-page?url=/index
|
|
951
|
+
chalk4.white("4. Edit your page content in dotCMS:\n") + chalk4.gray(` \u2192 ${urlDotCMSInstance}/dotAdmin/#/edit-page?url=/index
|
|
757
952
|
`)
|
|
758
953
|
);
|
|
759
|
-
console.log(
|
|
760
|
-
console.log(
|
|
761
|
-
console.log(
|
|
954
|
+
console.log(chalk4.white("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
|
|
955
|
+
console.log(chalk4.blueBright("\u{1F4D6} Documentation: ") + chalk4.white("https://dev.dotcms.com"));
|
|
956
|
+
console.log(chalk4.blueBright("\u{1F4AC} Community: ") + chalk4.white("https://community.dotcms.com\n"));
|
|
762
957
|
}
|
|
763
958
|
function finalStepsForAstro({
|
|
764
959
|
projectPath,
|
|
765
960
|
urlDotCMSInstance,
|
|
766
|
-
|
|
767
|
-
token
|
|
961
|
+
connection
|
|
768
962
|
}) {
|
|
769
963
|
console.log("\n");
|
|
770
|
-
console.log(
|
|
771
|
-
console.log(
|
|
964
|
+
console.log(chalk4.white("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
|
|
965
|
+
console.log(chalk4.greenBright("\u{1F4CB} Next Steps:\n"));
|
|
966
|
+
if (connection) {
|
|
967
|
+
renderConnectionSummary(connection);
|
|
968
|
+
}
|
|
772
969
|
console.log(
|
|
773
|
-
|
|
970
|
+
chalk4.white("1. Navigate to your project:\n") + chalk4.gray(` $ cd ${escapeShellPath(projectPath)}
|
|
774
971
|
`)
|
|
775
972
|
);
|
|
776
973
|
console.log(
|
|
777
|
-
|
|
974
|
+
chalk4.white("2. Start your development server:\n") + chalk4.gray(" $ npm run dev\n")
|
|
778
975
|
);
|
|
779
|
-
console.log(chalk3.white("3. Add your dotCMS configuration to ") + chalk3.green(".env") + ":\n");
|
|
780
|
-
console.log(chalk3.white("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
781
|
-
console.log(chalk3.white(getEnvVariablesForAstro(urlDotCMSInstance, siteId, token)));
|
|
782
|
-
console.log(chalk3.white("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
783
|
-
console.log(chalk3.gray(" \u{1F4A1} Tip: Copy the block above and paste into your .env file\n"));
|
|
784
976
|
console.log(
|
|
785
|
-
|
|
977
|
+
chalk4.white("3. Open your browser:\n") + chalk4.gray(" \u2192 http://localhost:3000\n")
|
|
786
978
|
);
|
|
787
979
|
console.log(
|
|
788
|
-
|
|
789
|
-
);
|
|
790
|
-
console.log(
|
|
791
|
-
chalk3.white("6. Edit your page content in dotCMS:\n") + chalk3.gray(` \u2192 ${urlDotCMSInstance}/dotAdmin/#/edit-page?url=/index
|
|
980
|
+
chalk4.white("4. Edit your page content in dotCMS:\n") + chalk4.gray(` \u2192 ${urlDotCMSInstance}/dotAdmin/#/edit-page?url=/index
|
|
792
981
|
`)
|
|
793
982
|
);
|
|
794
|
-
console.log(
|
|
795
|
-
console.log(
|
|
796
|
-
console.log(
|
|
983
|
+
console.log(chalk4.white("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
|
|
984
|
+
console.log(chalk4.blueBright("\u{1F4D6} Documentation: ") + chalk4.white("https://dev.dotcms.com"));
|
|
985
|
+
console.log(chalk4.blueBright("\u{1F4AC} Community: ") + chalk4.white("https://community.dotcms.com\n"));
|
|
797
986
|
}
|
|
798
987
|
function finalStepsForAngularAndAngularSSR({
|
|
799
988
|
projectPath,
|
|
@@ -802,35 +991,53 @@ function finalStepsForAngularAndAngularSSR({
|
|
|
802
991
|
token
|
|
803
992
|
}) {
|
|
804
993
|
console.log("\n");
|
|
805
|
-
console.log(
|
|
806
|
-
console.log(
|
|
994
|
+
console.log(chalk4.white("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
|
|
995
|
+
console.log(chalk4.greenBright("\u{1F4CB} Next Steps:\n"));
|
|
807
996
|
console.log(
|
|
808
|
-
|
|
997
|
+
chalk4.white("1. Navigate to your environments directory:\n") + chalk4.gray(` $ cd ${escapeShellPath(projectPath)}/src/environments
|
|
809
998
|
`)
|
|
810
999
|
);
|
|
811
1000
|
console.log(
|
|
812
|
-
|
|
1001
|
+
chalk4.white("2. Update the environment files:\n") + chalk4.gray(
|
|
813
1002
|
" Replace the contents of the following files:\n \u2022 environment.ts\n \u2022 environment.development.ts\n\n"
|
|
814
1003
|
)
|
|
815
1004
|
);
|
|
816
|
-
console.log(
|
|
817
|
-
console.log(
|
|
818
|
-
console.log(
|
|
819
|
-
console.log(
|
|
1005
|
+
console.log(chalk4.white("3. Add your dotCMS configuration:\n"));
|
|
1006
|
+
console.log(chalk4.white("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
1007
|
+
console.log(chalk4.white(getEnvVariablesForAngular(urlDotCMSInstance, siteId, token)));
|
|
1008
|
+
console.log(chalk4.white("\n\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n"));
|
|
820
1009
|
console.log(
|
|
821
|
-
|
|
1010
|
+
chalk4.gray(" \u{1F4A1} Tip: Copy the block above and paste it into both environment files\n")
|
|
822
1011
|
);
|
|
823
|
-
console.log(
|
|
1012
|
+
console.log(chalk4.white("4. Start your development server:\n") + chalk4.gray(" $ ng serve\n"));
|
|
824
1013
|
console.log(
|
|
825
|
-
|
|
1014
|
+
chalk4.white("5. Open your browser:\n") + chalk4.gray(" \u2192 http://localhost:4200\n")
|
|
826
1015
|
);
|
|
827
1016
|
console.log(
|
|
828
|
-
|
|
1017
|
+
chalk4.white("6. Edit your page content in dotCMS:\n") + chalk4.gray(` \u2192 ${urlDotCMSInstance}/dotAdmin/#/edit-page?url=/index
|
|
829
1018
|
`)
|
|
830
1019
|
);
|
|
831
|
-
console.log(
|
|
832
|
-
console.log(
|
|
833
|
-
console.log(
|
|
1020
|
+
console.log(chalk4.white("\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\n"));
|
|
1021
|
+
console.log(chalk4.blueBright("\u{1F4D6} Documentation: ") + chalk4.white("https://dev.dotcms.com"));
|
|
1022
|
+
console.log(chalk4.blueBright("\u{1F4AC} Community: ") + chalk4.white("https://community.dotcms.com\n"));
|
|
1023
|
+
}
|
|
1024
|
+
function getEnvFileSpec(framework, host, siteId, token) {
|
|
1025
|
+
if (framework === "astro") {
|
|
1026
|
+
return {
|
|
1027
|
+
filename: ".env",
|
|
1028
|
+
contents: dedentEnv(getEnvVariablesForAstro(host, siteId, token))
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
if (framework === "angular" || framework === "angular-ssr") {
|
|
1032
|
+
return {
|
|
1033
|
+
filename: null,
|
|
1034
|
+
contents: dedentEnv(getEnvVariablesForAngular(host, siteId, token))
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
return { filename: ".env", contents: dedentEnv(getEnvVariablesForNextJS(host, siteId, token)) };
|
|
1038
|
+
}
|
|
1039
|
+
function dedentEnv(block) {
|
|
1040
|
+
return block.split("\n").map((line) => line.trim()).filter(Boolean).join("\n") + "\n";
|
|
834
1041
|
}
|
|
835
1042
|
function getEnvVariablesForNextJS(host, siteId, token) {
|
|
836
1043
|
return `
|
|
@@ -885,11 +1092,11 @@ function displayDependencies(selectedFrameWork) {
|
|
|
885
1092
|
}
|
|
886
1093
|
function formatDependencies(dependencies, devDependencies) {
|
|
887
1094
|
const lines = [];
|
|
888
|
-
lines.push(
|
|
889
|
-
dependencies.forEach((item) => lines.push(
|
|
1095
|
+
lines.push(chalk4.white("Dependencies:"));
|
|
1096
|
+
dependencies.forEach((item) => lines.push(chalk4.grey(`- ${item}`)));
|
|
890
1097
|
lines.push("");
|
|
891
|
-
lines.push(
|
|
892
|
-
devDependencies.forEach((item) => lines.push(
|
|
1098
|
+
lines.push(chalk4.white("Dev Dependencies:"));
|
|
1099
|
+
devDependencies.forEach((item) => lines.push(chalk4.grey(`- ${item}`)));
|
|
893
1100
|
return lines.join("\n");
|
|
894
1101
|
}
|
|
895
1102
|
async function checkDockerAvailability() {
|
|
@@ -897,9 +1104,9 @@ async function checkDockerAvailability() {
|
|
|
897
1104
|
await execa("docker", ["info"]);
|
|
898
1105
|
return Ok(true);
|
|
899
1106
|
} catch {
|
|
900
|
-
const errorMsg =
|
|
1107
|
+
const errorMsg = chalk4.red("\n\u274C Docker is not available\n\n") + chalk4.white("Docker is required to run dotCMS locally.\n\n") + chalk4.yellow("How to fix:\n") + chalk4.white(" 1. Install Docker Desktop:\n") + chalk4.cyan(" \u2192 https://www.docker.com/products/docker-desktop\n\n") + chalk4.white(" 2. Start Docker Desktop\n") + chalk4.white(
|
|
901
1108
|
" 3. Wait for Docker to be running (check the Docker icon in your system tray)\n"
|
|
902
|
-
) +
|
|
1109
|
+
) + chalk4.white(" 4. Run this command again\n\n") + chalk4.gray("Alternative: Use --url flag to connect to an existing dotCMS instance");
|
|
903
1110
|
return Err(errorMsg);
|
|
904
1111
|
}
|
|
905
1112
|
}
|
|
@@ -911,7 +1118,7 @@ function isPortAvailable(port) {
|
|
|
911
1118
|
resolve(false);
|
|
912
1119
|
} else {
|
|
913
1120
|
console.warn(
|
|
914
|
-
|
|
1121
|
+
chalk4.yellow(
|
|
915
1122
|
`Warning: Unexpected error while checking port ${port}: ${err.message}`
|
|
916
1123
|
)
|
|
917
1124
|
);
|
|
@@ -925,29 +1132,15 @@ function isPortAvailable(port) {
|
|
|
925
1132
|
server.listen(port, "0.0.0.0");
|
|
926
1133
|
});
|
|
927
1134
|
}
|
|
928
|
-
async function
|
|
929
|
-
const requiredPorts = [
|
|
930
|
-
{ port: 8082, service: "dotCMS HTTP" },
|
|
931
|
-
{ port: 8443, service: "dotCMS HTTPS" },
|
|
932
|
-
{ port: 9200, service: "Elasticsearch HTTP" },
|
|
933
|
-
{ port: 9600, service: "Elasticsearch Transport" }
|
|
934
|
-
];
|
|
1135
|
+
async function findBusyPorts() {
|
|
935
1136
|
const busyPorts = [];
|
|
936
|
-
for (const { port, service } of
|
|
1137
|
+
for (const { port, service } of REQUIRED_PORTS) {
|
|
937
1138
|
const available = await isPortAvailable(port);
|
|
938
1139
|
if (!available) {
|
|
939
1140
|
busyPorts.push({ port, service });
|
|
940
1141
|
}
|
|
941
1142
|
}
|
|
942
|
-
|
|
943
|
-
const errorMsg = chalk3.red("\n\u274C Required ports are already in use\n\n") + chalk3.white("The following ports are busy:\n") + busyPorts.map(
|
|
944
|
-
({ port, service }) => chalk3.yellow(` \u2022 Port ${port}`) + chalk3.gray(` (${service})`)
|
|
945
|
-
).join("\n") + "\n\n" + chalk3.yellow("How to fix:\n") + chalk3.white(" 1. Stop services using these ports:\n") + chalk3.gray(" \u2022 Check what's using the ports: ") + chalk3.cyan(
|
|
946
|
-
process.platform === "win32" ? `netstat -ano | findstr ":<port>"` : `lsof -i :<port>`
|
|
947
|
-
) + "\n" + chalk3.gray(" \u2022 Stop the conflicting service\n\n") + chalk3.white(" 2. Or stop existing dotCMS containers:\n") + chalk3.cyan(" $ docker compose down\n\n") + chalk3.white(" 3. Run this command again\n\n") + chalk3.gray("Alternative: Use --url flag to connect to an existing dotCMS instance");
|
|
948
|
-
return Err(errorMsg);
|
|
949
|
-
}
|
|
950
|
-
return Ok(true);
|
|
1143
|
+
return busyPorts;
|
|
951
1144
|
}
|
|
952
1145
|
async function getDockerDiagnostics(directory) {
|
|
953
1146
|
const diagnostics = [];
|
|
@@ -962,28 +1155,28 @@ async function getDockerDiagnostics(directory) {
|
|
|
962
1155
|
{ cwd: directory }
|
|
963
1156
|
);
|
|
964
1157
|
if (!psOutput.trim()) {
|
|
965
|
-
diagnostics.push(
|
|
1158
|
+
diagnostics.push(chalk4.yellow("\n\u26A0\uFE0F No Docker containers found"));
|
|
966
1159
|
diagnostics.push(
|
|
967
|
-
|
|
1160
|
+
chalk4.white("The docker-compose.yml may not have been started correctly\n")
|
|
968
1161
|
);
|
|
969
1162
|
return diagnostics.join("\n");
|
|
970
1163
|
}
|
|
971
|
-
diagnostics.push(
|
|
1164
|
+
diagnostics.push(chalk4.cyan("\n\u{1F4CB} Container Status:"));
|
|
972
1165
|
const containers = psOutput.trim().split("\n");
|
|
973
1166
|
for (const container of containers) {
|
|
974
1167
|
const [name, status, ports] = container.split(" ");
|
|
975
1168
|
const isHealthy = status.includes("Up") && !status.includes("unhealthy");
|
|
976
1169
|
const icon = isHealthy ? "\u2705" : "\u274C";
|
|
977
|
-
diagnostics.push(` ${icon} ${
|
|
1170
|
+
diagnostics.push(` ${icon} ${chalk4.white(name)}: ${chalk4.gray(status)}`);
|
|
978
1171
|
if (ports) {
|
|
979
|
-
diagnostics.push(` ${
|
|
1172
|
+
diagnostics.push(` ${chalk4.gray("Ports:")} ${chalk4.white(ports)}`);
|
|
980
1173
|
}
|
|
981
1174
|
}
|
|
982
1175
|
const unhealthyContainers = containers.filter(
|
|
983
1176
|
(c) => !c.includes("Up") || c.includes("unhealthy") || c.includes("Exited")
|
|
984
1177
|
);
|
|
985
1178
|
if (unhealthyContainers.length > 0) {
|
|
986
|
-
diagnostics.push(
|
|
1179
|
+
diagnostics.push(chalk4.yellow("\n\u{1F50D} Recent logs from problematic containers:\n"));
|
|
987
1180
|
for (const container of unhealthyContainers) {
|
|
988
1181
|
const name = container.split(" ")[0];
|
|
989
1182
|
try {
|
|
@@ -991,26 +1184,26 @@ async function getDockerDiagnostics(directory) {
|
|
|
991
1184
|
cwd: directory,
|
|
992
1185
|
reject: false
|
|
993
1186
|
});
|
|
994
|
-
diagnostics.push(
|
|
1187
|
+
diagnostics.push(chalk4.white(`
|
|
995
1188
|
--- ${name} ---`));
|
|
996
|
-
diagnostics.push(
|
|
1189
|
+
diagnostics.push(chalk4.gray(logs.split("\n").slice(-10).join("\n")));
|
|
997
1190
|
} catch {
|
|
998
|
-
diagnostics.push(
|
|
1191
|
+
diagnostics.push(chalk4.gray(` Unable to fetch logs for ${name}`));
|
|
999
1192
|
}
|
|
1000
1193
|
}
|
|
1001
1194
|
}
|
|
1002
1195
|
} catch (error) {
|
|
1003
|
-
diagnostics.push(
|
|
1004
|
-
diagnostics.push(
|
|
1005
|
-
}
|
|
1006
|
-
diagnostics.push(
|
|
1007
|
-
diagnostics.push(
|
|
1008
|
-
diagnostics.push(
|
|
1009
|
-
diagnostics.push(
|
|
1010
|
-
diagnostics.push(
|
|
1011
|
-
diagnostics.push(
|
|
1012
|
-
diagnostics.push(
|
|
1013
|
-
diagnostics.push(
|
|
1196
|
+
diagnostics.push(chalk4.red("\n\u274C Failed to get Docker diagnostics"));
|
|
1197
|
+
diagnostics.push(chalk4.gray(String(error)));
|
|
1198
|
+
}
|
|
1199
|
+
diagnostics.push(chalk4.yellow("\n\u{1F4A1} Troubleshooting steps:"));
|
|
1200
|
+
diagnostics.push(chalk4.white(" 1. Check if all containers are running:"));
|
|
1201
|
+
diagnostics.push(chalk4.gray(" docker ps"));
|
|
1202
|
+
diagnostics.push(chalk4.white(" 2. View logs for a specific container:"));
|
|
1203
|
+
diagnostics.push(chalk4.gray(" docker logs <container-name>"));
|
|
1204
|
+
diagnostics.push(chalk4.white(" 3. Restart the containers:"));
|
|
1205
|
+
diagnostics.push(chalk4.gray(" docker compose down && docker compose up -d"));
|
|
1206
|
+
diagnostics.push(chalk4.white(" 4. Check if ports 8082, 8443, and 8090 are available\n"));
|
|
1014
1207
|
return diagnostics.join("\n");
|
|
1015
1208
|
}
|
|
1016
1209
|
function getDisplayPath(targetPath, cwd) {
|
|
@@ -1022,6 +1215,182 @@ function getDisplayPath(targetPath, cwd) {
|
|
|
1022
1215
|
return relativePath.startsWith(".") ? relativePath : `./${relativePath}`;
|
|
1023
1216
|
}
|
|
1024
1217
|
|
|
1218
|
+
// libs/sdk/create-app/src/exit-state.ts
|
|
1219
|
+
var recorded = {};
|
|
1220
|
+
var handler = null;
|
|
1221
|
+
var reported = false;
|
|
1222
|
+
function recordRecoverableState(state) {
|
|
1223
|
+
recorded = { ...recorded, ...state };
|
|
1224
|
+
}
|
|
1225
|
+
function hasRecoverableState(state) {
|
|
1226
|
+
return Boolean(state.host && state.token && state.siteId);
|
|
1227
|
+
}
|
|
1228
|
+
function envFileFor(state) {
|
|
1229
|
+
return getEnvFileSpec(state.framework, state.host, state.siteId, state.token);
|
|
1230
|
+
}
|
|
1231
|
+
function flushRecoverableState() {
|
|
1232
|
+
if (!hasRecoverableState(recorded)) {
|
|
1233
|
+
return null;
|
|
1234
|
+
}
|
|
1235
|
+
const state = recorded;
|
|
1236
|
+
const envFile = envFileFor(state);
|
|
1237
|
+
let wroteEnv = false;
|
|
1238
|
+
if (state.projectDirectory && envFile.filename) {
|
|
1239
|
+
const envPath = path4.join(state.projectDirectory, envFile.filename);
|
|
1240
|
+
if (!fs2.existsSync(envPath)) {
|
|
1241
|
+
try {
|
|
1242
|
+
fs2.writeFileSync(
|
|
1243
|
+
envPath,
|
|
1244
|
+
`# Written by @dotcms/create-app so this run is never lost.
|
|
1245
|
+
${envFile.contents}`,
|
|
1246
|
+
"utf8"
|
|
1247
|
+
);
|
|
1248
|
+
wroteEnv = true;
|
|
1249
|
+
} catch {
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
reported = true;
|
|
1254
|
+
return {
|
|
1255
|
+
wroteEnv,
|
|
1256
|
+
filename: envFile.filename,
|
|
1257
|
+
host: state.host,
|
|
1258
|
+
siteId: state.siteId,
|
|
1259
|
+
token: state.token,
|
|
1260
|
+
contents: envFile.contents
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function emit() {
|
|
1264
|
+
const report = flushRecoverableState();
|
|
1265
|
+
if (!report) {
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
if (report.wroteEnv) {
|
|
1269
|
+
console.log(
|
|
1270
|
+
[
|
|
1271
|
+
"",
|
|
1272
|
+
`Wrote ${report.filename} with your dotCMS connection details.`,
|
|
1273
|
+
` host : ${report.host}`,
|
|
1274
|
+
` site id : ${report.siteId}`,
|
|
1275
|
+
` token : stored in ${report.filename}`
|
|
1276
|
+
].join("\n")
|
|
1277
|
+
);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
console.log(
|
|
1281
|
+
[
|
|
1282
|
+
"",
|
|
1283
|
+
"dotCMS connection details for this run:",
|
|
1284
|
+
` host : ${report.host}`,
|
|
1285
|
+
` site id : ${report.siteId}`,
|
|
1286
|
+
` token : ${report.token}`,
|
|
1287
|
+
"",
|
|
1288
|
+
report.filename ? `Add these to your ${report.filename}:` : "Configuration for your project:",
|
|
1289
|
+
...report.contents.trimEnd().split("\n")
|
|
1290
|
+
].join("\n")
|
|
1291
|
+
);
|
|
1292
|
+
}
|
|
1293
|
+
function installExitStateHandler() {
|
|
1294
|
+
if (handler) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
handler = () => {
|
|
1298
|
+
if (reported) {
|
|
1299
|
+
return;
|
|
1300
|
+
}
|
|
1301
|
+
emit();
|
|
1302
|
+
};
|
|
1303
|
+
process.on("exit", handler);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
// libs/sdk/create-app/src/git/index.ts
|
|
1307
|
+
import { execa as execa2 } from "execa";
|
|
1308
|
+
import fs4 from "fs-extra";
|
|
1309
|
+
import path6 from "path";
|
|
1310
|
+
|
|
1311
|
+
// libs/sdk/create-app/src/compose/compose-source.ts
|
|
1312
|
+
import fs3 from "fs-extra";
|
|
1313
|
+
import path5 from "path";
|
|
1314
|
+
var COMPOSE_URL_ENV_VAR = "DOTCMS_COMPOSE_URL";
|
|
1315
|
+
var ASSET_RELATIVE_PATH = path5.join("assets", "docker-compose.yml");
|
|
1316
|
+
var REMOTE_READ_TIMEOUT_MS = 15e3;
|
|
1317
|
+
function currentModuleDir() {
|
|
1318
|
+
if (typeof __dirname === "string") {
|
|
1319
|
+
return __dirname;
|
|
1320
|
+
}
|
|
1321
|
+
const entryPoint = process.argv[1];
|
|
1322
|
+
if (typeof entryPoint === "string" && entryPoint.length > 0) {
|
|
1323
|
+
return path5.dirname(entryPoint);
|
|
1324
|
+
}
|
|
1325
|
+
return process.cwd();
|
|
1326
|
+
}
|
|
1327
|
+
function resolveBundledAssetPath() {
|
|
1328
|
+
let dir = currentModuleDir();
|
|
1329
|
+
let fallback = path5.resolve(dir, ASSET_RELATIVE_PATH);
|
|
1330
|
+
for (; ; ) {
|
|
1331
|
+
const candidate = path5.join(dir, ASSET_RELATIVE_PATH);
|
|
1332
|
+
if (fs3.existsSync(candidate)) {
|
|
1333
|
+
return candidate;
|
|
1334
|
+
}
|
|
1335
|
+
if (fs3.existsSync(path5.join(dir, "package.json"))) {
|
|
1336
|
+
fallback = candidate;
|
|
1337
|
+
}
|
|
1338
|
+
const parent = path5.dirname(dir);
|
|
1339
|
+
if (parent === dir) {
|
|
1340
|
+
return fallback;
|
|
1341
|
+
}
|
|
1342
|
+
dir = parent;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
function createBundledSource() {
|
|
1346
|
+
const assetPath = resolveBundledAssetPath();
|
|
1347
|
+
return {
|
|
1348
|
+
kind: "bundled",
|
|
1349
|
+
path: assetPath,
|
|
1350
|
+
describe: `bundled compose file (${assetPath})`,
|
|
1351
|
+
read: () => fs3.readFile(assetPath, "utf8")
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1354
|
+
function createRemoteSource(url) {
|
|
1355
|
+
return {
|
|
1356
|
+
kind: "remote",
|
|
1357
|
+
url,
|
|
1358
|
+
describe: `remote compose file from ${COMPOSE_URL_ENV_VAR}`,
|
|
1359
|
+
read: async () => {
|
|
1360
|
+
const controller = new AbortController();
|
|
1361
|
+
const timer = setTimeout(() => controller.abort(), REMOTE_READ_TIMEOUT_MS);
|
|
1362
|
+
try {
|
|
1363
|
+
const response = await fetch(url, {
|
|
1364
|
+
signal: controller.signal,
|
|
1365
|
+
redirect: "follow"
|
|
1366
|
+
});
|
|
1367
|
+
if (!response.ok) {
|
|
1368
|
+
throw new Error(
|
|
1369
|
+
`Failed to download compose file from ${url}: ${response.status} ${response.statusText}`
|
|
1370
|
+
);
|
|
1371
|
+
}
|
|
1372
|
+
return await response.text();
|
|
1373
|
+
} catch (error) {
|
|
1374
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
1375
|
+
throw new Error(
|
|
1376
|
+
`Timed out after ${REMOTE_READ_TIMEOUT_MS}ms downloading compose file from ${url}`
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
throw error;
|
|
1380
|
+
} finally {
|
|
1381
|
+
clearTimeout(timer);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
function resolveComposeSource() {
|
|
1387
|
+
const override = process.env[COMPOSE_URL_ENV_VAR];
|
|
1388
|
+
if (override) {
|
|
1389
|
+
return createRemoteSource(override);
|
|
1390
|
+
}
|
|
1391
|
+
return createBundledSource();
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1025
1394
|
// libs/sdk/create-app/src/git/index.ts
|
|
1026
1395
|
var cloneFrontEndSample = async ({
|
|
1027
1396
|
framework,
|
|
@@ -1039,39 +1408,280 @@ var cloneFrontEndSample = async ({
|
|
|
1039
1408
|
cwd: directory
|
|
1040
1409
|
// stdio: 'inherit'
|
|
1041
1410
|
});
|
|
1042
|
-
const src =
|
|
1411
|
+
const src = path6.join(directory, "examples", `${framework}`);
|
|
1043
1412
|
const dest = directory;
|
|
1044
|
-
const items = await
|
|
1413
|
+
const items = await fs4.readdir(directory);
|
|
1045
1414
|
for (const item of items) {
|
|
1046
1415
|
if (item !== "examples") {
|
|
1047
|
-
await
|
|
1416
|
+
await fs4.remove(path6.join(directory, item));
|
|
1048
1417
|
}
|
|
1049
1418
|
}
|
|
1050
|
-
await
|
|
1051
|
-
const allItems = await
|
|
1419
|
+
await fs4.copy(src, dest, { overwrite: true });
|
|
1420
|
+
const allItems = await fs4.readdir(directory);
|
|
1052
1421
|
for (const item of allItems) {
|
|
1053
1422
|
if (item === "examples") {
|
|
1054
|
-
await
|
|
1423
|
+
await fs4.remove(path6.join(directory, item));
|
|
1055
1424
|
}
|
|
1056
1425
|
}
|
|
1057
1426
|
};
|
|
1058
1427
|
async function downloadDockerCompose(directory) {
|
|
1059
|
-
const
|
|
1060
|
-
const dockerComposePath =
|
|
1061
|
-
await
|
|
1428
|
+
const source = resolveComposeSource();
|
|
1429
|
+
const dockerComposePath = path6.join(directory, "docker-compose.yml");
|
|
1430
|
+
const contents = await source.read();
|
|
1431
|
+
await fs4.writeFile(dockerComposePath, contents);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// libs/sdk/create-app/src/utils/compose-move.ts
|
|
1435
|
+
import fs5 from "fs-extra";
|
|
1436
|
+
import os from "node:os";
|
|
1437
|
+
import path7 from "node:path";
|
|
1438
|
+
var COMPOSE_FILE = "docker-compose.yml";
|
|
1439
|
+
async function withComposeFileMovedAside(directory, action) {
|
|
1440
|
+
const inProject = path7.join(directory, COMPOSE_FILE);
|
|
1441
|
+
const moved = fs5.existsSync(inProject);
|
|
1442
|
+
if (!moved) {
|
|
1443
|
+
return await action();
|
|
1444
|
+
}
|
|
1445
|
+
const holdingDir = await fs5.mkdtemp(path7.join(os.tmpdir(), "dotcms-create-app-compose-"));
|
|
1446
|
+
const asideNext = path7.join(holdingDir, COMPOSE_FILE);
|
|
1447
|
+
await fs5.move(inProject, asideNext);
|
|
1448
|
+
try {
|
|
1449
|
+
return await action();
|
|
1450
|
+
} finally {
|
|
1451
|
+
if (fs5.existsSync(asideNext)) {
|
|
1452
|
+
await fs5.move(asideNext, inProject, { overwrite: true });
|
|
1453
|
+
}
|
|
1454
|
+
await fs5.remove(holdingDir);
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// libs/sdk/create-app/src/utils/install.ts
|
|
1459
|
+
function describe(value) {
|
|
1460
|
+
if (value instanceof Error) {
|
|
1461
|
+
return value.message;
|
|
1462
|
+
}
|
|
1463
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1464
|
+
}
|
|
1465
|
+
function reportInstallResult(result) {
|
|
1466
|
+
if (result.ok) {
|
|
1467
|
+
return { kind: "installed" };
|
|
1468
|
+
}
|
|
1469
|
+
return { kind: "failed", reason: describe(result.val) };
|
|
1470
|
+
}
|
|
1471
|
+
|
|
1472
|
+
// libs/sdk/create-app/src/utils/readiness.ts
|
|
1473
|
+
var STILL_STARTING = 503;
|
|
1474
|
+
async function probeReadiness({
|
|
1475
|
+
readyzUrl,
|
|
1476
|
+
fallbackUrl,
|
|
1477
|
+
get
|
|
1478
|
+
}) {
|
|
1479
|
+
let readyzStatus = null;
|
|
1480
|
+
try {
|
|
1481
|
+
const { status } = await get(readyzUrl);
|
|
1482
|
+
readyzStatus = status;
|
|
1483
|
+
if (isSuccessStatus(status)) {
|
|
1484
|
+
return { kind: "ready" };
|
|
1485
|
+
}
|
|
1486
|
+
if (status === STILL_STARTING) {
|
|
1487
|
+
return { kind: "not-ready", detail: "dotCMS is still starting up" };
|
|
1488
|
+
}
|
|
1489
|
+
} catch {
|
|
1490
|
+
}
|
|
1491
|
+
try {
|
|
1492
|
+
const { status } = await get(fallbackUrl);
|
|
1493
|
+
if (isSuccessStatus(status)) {
|
|
1494
|
+
return { kind: "ready" };
|
|
1495
|
+
}
|
|
1496
|
+
return {
|
|
1497
|
+
kind: "not-ready",
|
|
1498
|
+
detail: `${readyzUrl} answered ${readyzStatus ?? "nothing"} and ${fallbackUrl} answered ${status}`
|
|
1499
|
+
};
|
|
1500
|
+
} catch (error) {
|
|
1501
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
1502
|
+
return {
|
|
1503
|
+
kind: "not-ready",
|
|
1504
|
+
detail: `neither ${readyzUrl} nor ${fallbackUrl} could be reached (${reason})`
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1062
1507
|
}
|
|
1063
|
-
async function
|
|
1064
|
-
const
|
|
1065
|
-
|
|
1066
|
-
|
|
1508
|
+
async function waitForReadiness(options) {
|
|
1509
|
+
const { attempts, delayMs, onAttempt, ...probe } = options;
|
|
1510
|
+
let last = { kind: "not-ready", detail: "not probed yet" };
|
|
1511
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
1512
|
+
last = await probeReadiness(probe);
|
|
1513
|
+
if (last.kind === "ready") {
|
|
1514
|
+
return last;
|
|
1515
|
+
}
|
|
1516
|
+
onAttempt?.(attempt, attempts, last.detail);
|
|
1517
|
+
if (attempt < attempts) {
|
|
1518
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
return last;
|
|
1067
1522
|
}
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1523
|
+
|
|
1524
|
+
// libs/sdk/create-app/src/utils/starter-url.ts
|
|
1525
|
+
var CUSTOM_STARTER_URL_LINE = /^(\s*["']?CUSTOM_STARTER_URL["']?\s*:\s*).+$/m;
|
|
1526
|
+
function applyStarterUrl(composeContents, starterUrl) {
|
|
1527
|
+
if (!CUSTOM_STARTER_URL_LINE.test(composeContents)) {
|
|
1528
|
+
throw new Error(
|
|
1529
|
+
"CUSTOM_STARTER_URL entry not found in docker-compose.yml. Unable to apply --starter value."
|
|
1530
|
+
);
|
|
1531
|
+
}
|
|
1532
|
+
return composeContents.replace(
|
|
1533
|
+
CUSTOM_STARTER_URL_LINE,
|
|
1534
|
+
(_match, prefix) => `${prefix}"${starterUrl}"`
|
|
1535
|
+
);
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
// libs/sdk/create-app/src/uve/configure-uve.ts
|
|
1539
|
+
var UVE_APP_KEY = "dotema-config-v2";
|
|
1540
|
+
var HEADLESS_UVE_GUIDE = "https://dev.dotcms.com/docs/author/pages-and-visual-editing/universal-visual-editor/uve-headless-config";
|
|
1541
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
1542
|
+
var DEFAULT_RETRY_DELAY_MS = 2e3;
|
|
1543
|
+
function uveResourceUrl(host, siteId) {
|
|
1544
|
+
return `${host.replace(/\/+$/, "")}/api/v1/apps/${UVE_APP_KEY}/${siteId}`;
|
|
1545
|
+
}
|
|
1546
|
+
function statusOf(error) {
|
|
1547
|
+
const response = error?.response;
|
|
1548
|
+
return typeof response?.status === "number" ? response.status : null;
|
|
1549
|
+
}
|
|
1550
|
+
function reasonFor(status) {
|
|
1551
|
+
if (status === null) {
|
|
1552
|
+
return "unreachable";
|
|
1553
|
+
}
|
|
1554
|
+
if (status === 403) {
|
|
1555
|
+
return "forbidden";
|
|
1556
|
+
}
|
|
1557
|
+
if (status >= 500) {
|
|
1558
|
+
return "server-error";
|
|
1559
|
+
}
|
|
1560
|
+
return "unknown";
|
|
1561
|
+
}
|
|
1562
|
+
function forbiddenMessage(mode, siteId) {
|
|
1563
|
+
if (mode === "local") {
|
|
1564
|
+
return [
|
|
1565
|
+
"The Universal Visual Editor could not be configured: the instance rejected the request (403).",
|
|
1566
|
+
"",
|
|
1567
|
+
"This local instance is unrecoverable. Its first boot was interrupted, so the starter",
|
|
1568
|
+
"import never wrote the permission rows for the demo site, and a restart does not repair",
|
|
1569
|
+
"them. Configuring the editor by hand would fail the same way.",
|
|
1570
|
+
"",
|
|
1571
|
+
"Recreate the instance from scratch:",
|
|
1572
|
+
" docker compose down -v && docker compose up -d --wait",
|
|
1573
|
+
"",
|
|
1574
|
+
"Tracked as dotCMS issue #37268."
|
|
1575
|
+
].join("\n");
|
|
1576
|
+
}
|
|
1577
|
+
return [
|
|
1578
|
+
"The Universal Visual Editor could not be configured: the instance rejected the request (403).",
|
|
1579
|
+
"",
|
|
1580
|
+
"The API token does not have permission to write app configuration on the target site.",
|
|
1581
|
+
` site id : ${siteId}`,
|
|
1582
|
+
` app key : ${UVE_APP_KEY}`,
|
|
1583
|
+
"",
|
|
1584
|
+
"Check that the token belongs to a user who can administer that site, then finish the",
|
|
1585
|
+
"setup by hand:",
|
|
1586
|
+
` ${HEADLESS_UVE_GUIDE}`
|
|
1587
|
+
].join("\n");
|
|
1588
|
+
}
|
|
1589
|
+
function genericMessage(mode, siteId, detail, { withGuide }) {
|
|
1590
|
+
const lines = [
|
|
1591
|
+
`The Universal Visual Editor could not be configured: ${detail}`,
|
|
1592
|
+
"",
|
|
1593
|
+
"Your project is unaffected and setup will continue. To finish the editor configuration",
|
|
1594
|
+
"later, use these values:",
|
|
1595
|
+
` site id : ${siteId}`,
|
|
1596
|
+
` app key : ${UVE_APP_KEY}`
|
|
1597
|
+
];
|
|
1598
|
+
if (withGuide) {
|
|
1599
|
+
lines.push("", ` ${HEADLESS_UVE_GUIDE}`);
|
|
1600
|
+
}
|
|
1601
|
+
return lines.join("\n");
|
|
1602
|
+
}
|
|
1603
|
+
function delay(ms) {
|
|
1604
|
+
return ms > 0 ? new Promise((resolve) => setTimeout(resolve, ms)) : Promise.resolve();
|
|
1605
|
+
}
|
|
1606
|
+
async function configureUVE(options) {
|
|
1607
|
+
const {
|
|
1608
|
+
host,
|
|
1609
|
+
siteId,
|
|
1610
|
+
token,
|
|
1611
|
+
mode,
|
|
1612
|
+
frontendUrl,
|
|
1613
|
+
maxRetries = DEFAULT_MAX_RETRIES,
|
|
1614
|
+
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
|
1615
|
+
report
|
|
1616
|
+
} = options;
|
|
1617
|
+
const url = uveResourceUrl(host, siteId);
|
|
1618
|
+
const notify = (message2) => report?.(message2);
|
|
1619
|
+
try {
|
|
1620
|
+
await httpGet(url, { token });
|
|
1621
|
+
} catch (error) {
|
|
1622
|
+
const status = statusOf(error);
|
|
1623
|
+
const reason = reasonFor(status);
|
|
1624
|
+
const message2 = reason === "forbidden" ? forbiddenMessage(mode, siteId) : genericMessage(
|
|
1625
|
+
mode,
|
|
1626
|
+
siteId,
|
|
1627
|
+
status === null ? "the instance could not be reached." : `the instance answered ${status} when the current configuration was read.`,
|
|
1628
|
+
{ withGuide: true }
|
|
1629
|
+
);
|
|
1630
|
+
notify(message2);
|
|
1631
|
+
return { kind: "failed", phase: "probe", reason, status, message: message2 };
|
|
1632
|
+
}
|
|
1633
|
+
const payload = {
|
|
1634
|
+
configuration: {
|
|
1635
|
+
hidden: false,
|
|
1636
|
+
// The endpoint expects the serialized UVE config object, not a bare URL. Building
|
|
1637
|
+
// it here rather than at the call sites is the point of this module owning the
|
|
1638
|
+
// operation — a caller passing the raw origin would be accepted with a 200 and
|
|
1639
|
+
// silently leave the editor misconfigured.
|
|
1640
|
+
value: getUVEConfigValue(frontendUrl)
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
let lastStatus = null;
|
|
1644
|
+
for (let attempt = 1; attempt <= Math.max(1, maxRetries); attempt++) {
|
|
1645
|
+
try {
|
|
1646
|
+
await httpPost(url, payload, { token });
|
|
1647
|
+
return { kind: "configured" };
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
const status = statusOf(error);
|
|
1650
|
+
lastStatus = status;
|
|
1651
|
+
const retryable = status !== null && status >= 500 && attempt < Math.max(1, maxRetries);
|
|
1652
|
+
if (!retryable) {
|
|
1653
|
+
const reason = reasonFor(status);
|
|
1654
|
+
const message2 = reason === "forbidden" ? forbiddenMessage(mode, siteId) : genericMessage(
|
|
1655
|
+
mode,
|
|
1656
|
+
siteId,
|
|
1657
|
+
status === null ? "the instance could not be reached." : `the instance answered ${status}.`,
|
|
1658
|
+
{ withGuide: true }
|
|
1659
|
+
);
|
|
1660
|
+
notify(message2);
|
|
1661
|
+
return { kind: "failed", phase: "write", reason, status, message: message2 };
|
|
1662
|
+
}
|
|
1663
|
+
notify(`dotCMS answered ${status}; retrying (${attempt}/${maxRetries})`);
|
|
1664
|
+
await delay(retryDelayMs);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const message = genericMessage(mode, siteId, `the instance answered ${lastStatus}.`, {
|
|
1668
|
+
withGuide: true
|
|
1669
|
+
});
|
|
1670
|
+
return {
|
|
1671
|
+
kind: "failed",
|
|
1672
|
+
phase: "write",
|
|
1673
|
+
reason: reasonFor(lastStatus),
|
|
1674
|
+
status: lastStatus,
|
|
1675
|
+
message
|
|
1676
|
+
};
|
|
1072
1677
|
}
|
|
1073
1678
|
|
|
1074
1679
|
// libs/sdk/create-app/src/index.ts
|
|
1680
|
+
var COMPOSE_WAIT_TIMEOUT_SECONDS = 600;
|
|
1681
|
+
var PROGRESS_TICK_MS = 2e3;
|
|
1682
|
+
var LOCAL_DOTCMS_HOST = "http://localhost:8082";
|
|
1683
|
+
var LOCAL_MANAGEMENT_HOST = "http://127.0.0.1:8090";
|
|
1684
|
+
installExitStateHandler();
|
|
1075
1685
|
var program = new Command();
|
|
1076
1686
|
program.name("create-dotcms-app").description("dotCMS CLI for creating applications").version("0.1.0-beta");
|
|
1077
1687
|
program.argument("[projectName]", "Name of the project").option("-f, --framework <framework>", "Framework to use [nextjs,astro,angular,angular-ssr]").option("-d, --directory <path>", "Project directory").option("--local", "Use local dotCMS instance using docker").option("--url <url>", "DotCMS instance url (skip in case of local)").option("-u, --username <username>", "DotCMS instance username (skip in case of local)").option("-p, --password <password>", "DotCMS instance password (skip in case of local)").option(
|
|
@@ -1097,13 +1707,15 @@ program.argument("[projectName]", "Name of the project").option("-f, --framework
|
|
|
1097
1707
|
validateUrl(urlInput);
|
|
1098
1708
|
const urlDotcmsInstance = normalizeUrl(urlInput);
|
|
1099
1709
|
const healthApiURL = getDotcmsApisByBaseUrl(urlDotcmsInstance).DOTCMS_HEALTH_API;
|
|
1100
|
-
const emaConfigApiURL = getDotcmsApisByBaseUrl(urlDotcmsInstance).DOTCMS_EMA_CONFIG_API;
|
|
1101
1710
|
const siteApiURL = getDotcmsApisByBaseUrl(urlDotcmsInstance).DOTCMS_SITE_API;
|
|
1102
1711
|
const tokenApiUrl = getDotcmsApisByBaseUrl(urlDotcmsInstance).DOTCMS_TOKEN_API;
|
|
1103
1712
|
const spinner2 = ora(`\u23F3 Connecting to dotCMS...`).start();
|
|
1104
1713
|
const healthCheckResult2 = await isDotcmsRunning(
|
|
1105
1714
|
healthApiURL,
|
|
1106
|
-
CLOUD_HEALTH_CHECK_RETRIES
|
|
1715
|
+
CLOUD_HEALTH_CHECK_RETRIES,
|
|
1716
|
+
(report) => {
|
|
1717
|
+
spinner2.text = formatRetryReport(report);
|
|
1718
|
+
}
|
|
1107
1719
|
);
|
|
1108
1720
|
if (!healthCheckResult2.ok) {
|
|
1109
1721
|
spinner2.fail(
|
|
@@ -1137,7 +1749,7 @@ program.argument("[projectName]", "Name of the project").option("-f, --framework
|
|
|
1137
1749
|
console.error(dotcmsToken2.val);
|
|
1138
1750
|
if (authAttempts < MAX_AUTH_ATTEMPTS) {
|
|
1139
1751
|
console.log(
|
|
1140
|
-
|
|
1752
|
+
chalk5.yellow(
|
|
1141
1753
|
`
|
|
1142
1754
|
Attempt ${authAttempts}/${MAX_AUTH_ATTEMPTS} - Please try again
|
|
1143
1755
|
`
|
|
@@ -1145,7 +1757,7 @@ Attempt ${authAttempts}/${MAX_AUTH_ATTEMPTS} - Please try again
|
|
|
1145
1757
|
);
|
|
1146
1758
|
} else {
|
|
1147
1759
|
console.log(
|
|
1148
|
-
|
|
1760
|
+
chalk5.red(
|
|
1149
1761
|
`
|
|
1150
1762
|
Maximum authentication attempts (${MAX_AUTH_ATTEMPTS}) reached. Exiting.
|
|
1151
1763
|
`
|
|
@@ -1171,27 +1783,29 @@ Maximum authentication attempts (${MAX_AUTH_ATTEMPTS}) reached. Exiting.
|
|
|
1171
1783
|
);
|
|
1172
1784
|
}
|
|
1173
1785
|
const selectedFramework2 = validatedFramework ?? await askFramework();
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
hidden: false,
|
|
1178
|
-
value: getUVEConfigValue(
|
|
1179
|
-
`http://localhost:${getPortByFramework(selectedFramework2)}`
|
|
1180
|
-
)
|
|
1181
|
-
}
|
|
1182
|
-
},
|
|
1786
|
+
recordRecoverableState({
|
|
1787
|
+
host: urlDotcmsInstance,
|
|
1788
|
+
token: dotcmsToken2.val,
|
|
1183
1789
|
siteId: defaultSite2.val.entity.identifier,
|
|
1184
|
-
|
|
1185
|
-
|
|
1790
|
+
projectDirectory: finalDirectory,
|
|
1791
|
+
framework: selectedFramework2
|
|
1186
1792
|
});
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1793
|
+
const uveOutcome2 = await configureUVE({
|
|
1794
|
+
host: urlDotcmsInstance,
|
|
1795
|
+
siteId: defaultSite2.val.entity.identifier,
|
|
1796
|
+
token: dotcmsToken2.val,
|
|
1797
|
+
mode: "remote",
|
|
1798
|
+
frontendUrl: `http://localhost:${getPortByFramework(selectedFramework2)}`,
|
|
1799
|
+
report: (message) => spinner2.info(message)
|
|
1800
|
+
});
|
|
1801
|
+
if (uveOutcome2.kind === "configured") {
|
|
1191
1802
|
spinner2.succeed(`Configured the Universal Visual Editor`);
|
|
1803
|
+
} else {
|
|
1804
|
+
spinner2.warn("Skipped Universal Visual Editor configuration.");
|
|
1805
|
+
console.log(chalk5.yellow(uveOutcome2.message));
|
|
1192
1806
|
}
|
|
1193
1807
|
await startScaffoldingFrontEnd({ spinner: spinner2, selectedFramework: selectedFramework2, finalDirectory });
|
|
1194
|
-
console.log(
|
|
1808
|
+
console.log(chalk5.white(`\u2705 Project setup complete!`));
|
|
1195
1809
|
const relativePath2 = getDisplayPath(finalDirectory, process.cwd());
|
|
1196
1810
|
displayFinalSteps({
|
|
1197
1811
|
host: urlDotcmsInstance,
|
|
@@ -1211,41 +1825,94 @@ Maximum authentication attempts (${MAX_AUTH_ATTEMPTS}) reached. Exiting.
|
|
|
1211
1825
|
}
|
|
1212
1826
|
spinner.succeed("Docker is available");
|
|
1213
1827
|
spinner.start("Checking port availability...");
|
|
1214
|
-
const
|
|
1215
|
-
|
|
1828
|
+
const busyPorts = await findBusyPorts();
|
|
1829
|
+
const portOutcome = await resolvePortConflict({
|
|
1830
|
+
busyPorts,
|
|
1831
|
+
isInteractive: Boolean(process.stdout.isTTY) && !process.env.CI,
|
|
1832
|
+
host: LOCAL_DOTCMS_HOST,
|
|
1833
|
+
probeInstance: async () => {
|
|
1834
|
+
const running = await isDotcmsRunning(void 0, 1);
|
|
1835
|
+
if (!running.ok) {
|
|
1836
|
+
return false;
|
|
1837
|
+
}
|
|
1838
|
+
const probeToken = await DotCMSApi.getAuthToken({
|
|
1839
|
+
payload: {
|
|
1840
|
+
user: DOTCMS_USER.username,
|
|
1841
|
+
password: DOTCMS_USER.password,
|
|
1842
|
+
expirationDays: "1",
|
|
1843
|
+
label: "create-app reuse probe"
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
return probeToken.ok;
|
|
1847
|
+
},
|
|
1848
|
+
owner: await describePortOwner(8082, (cmd, args) => execa3(cmd, args)),
|
|
1849
|
+
askAction: (context) => {
|
|
1850
|
+
spinner.stop();
|
|
1851
|
+
return askPortConflictAction(context);
|
|
1852
|
+
},
|
|
1853
|
+
notify: (message) => spinner.info(message)
|
|
1854
|
+
});
|
|
1855
|
+
if (portOutcome.kind === "abort") {
|
|
1216
1856
|
spinner.fail("Required ports are busy");
|
|
1217
|
-
console.error(
|
|
1857
|
+
console.error(chalk5.red(portOutcome.message));
|
|
1218
1858
|
process.exit(1);
|
|
1219
1859
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
spinner.fail("Failed to download Docker Compose file.");
|
|
1227
|
-
process.exit(1);
|
|
1860
|
+
if (portOutcome.kind === "replace") {
|
|
1861
|
+
spinner.start(`Removing the existing "${portOutcome.project}" stack...`);
|
|
1862
|
+
await execa3("docker", ["compose", "-p", portOutcome.project, "down", "-v"], {
|
|
1863
|
+
reject: false
|
|
1864
|
+
});
|
|
1865
|
+
spinner.succeed(`Removed the existing "${portOutcome.project}" stack`);
|
|
1228
1866
|
}
|
|
1229
|
-
|
|
1230
|
-
spinner.
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1867
|
+
const reusingExistingInstance = portOutcome.kind === "reuse";
|
|
1868
|
+
spinner.succeed(
|
|
1869
|
+
reusingExistingInstance ? "Reusing the dotCMS already running on 8082" : "All required ports are available"
|
|
1870
|
+
);
|
|
1871
|
+
if (!reusingExistingInstance) {
|
|
1872
|
+
spinner.start("Downloading Docker Compose configuration...");
|
|
1873
|
+
const downloaded = await downloadTheDockerCompose({
|
|
1874
|
+
directory: finalDirectory
|
|
1875
|
+
});
|
|
1876
|
+
if (!downloaded.ok) {
|
|
1877
|
+
spinner.fail("Failed to download Docker Compose file.");
|
|
1878
|
+
process.exit(1);
|
|
1879
|
+
}
|
|
1880
|
+
spinner.succeed("Docker Compose configuration downloaded");
|
|
1881
|
+
spinner.start("Starting dotCMS containers...");
|
|
1882
|
+
const ran = await runDockerCompose({
|
|
1883
|
+
directory: finalDirectory,
|
|
1884
|
+
starterUrl: options.starter,
|
|
1885
|
+
onProgress: (message) => {
|
|
1886
|
+
spinner.text = message;
|
|
1887
|
+
}
|
|
1888
|
+
});
|
|
1889
|
+
if (!ran.ok) {
|
|
1890
|
+
spinner.fail("Failed to start Docker containers");
|
|
1891
|
+
const errorMessage = ran.val instanceof Error ? ran.val.message : String(ran.val);
|
|
1892
|
+
console.error(
|
|
1893
|
+
chalk5.red("\n\u274C Docker Compose failed to start\n\n") + chalk5.white("Error details:\n") + chalk5.gray(errorMessage) + "\n\n" + chalk5.yellow("Common solutions:\n") + chalk5.white(" \u2022 Ensure Docker Desktop is running\n") + chalk5.white(" \u2022 Try: ") + chalk5.cyan("docker compose down") + chalk5.white(" then run this command again\n") + chalk5.white(" \u2022 Check Docker logs for more details\n")
|
|
1894
|
+
);
|
|
1895
|
+
process.exit(1);
|
|
1896
|
+
}
|
|
1897
|
+
spinner.succeed("dotCMS containers started successfully.");
|
|
1242
1898
|
}
|
|
1243
|
-
spinner.succeed("dotCMS containers started successfully.");
|
|
1244
1899
|
spinner.start("Verifying if dotCMS is running...");
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1900
|
+
const readiness = await waitForReadiness({
|
|
1901
|
+
readyzUrl: `${LOCAL_MANAGEMENT_HOST}/dotmgt/readyz`,
|
|
1902
|
+
fallbackUrl: DOTCMS_HEALTH_API,
|
|
1903
|
+
get: (url) => httpGet(url, { timeoutMs: 1e4, acceptAnyStatus: true }),
|
|
1904
|
+
attempts: LOCAL_HEALTH_CHECK_RETRIES,
|
|
1905
|
+
delayMs: 5e3,
|
|
1906
|
+
onAttempt: (attempt, attempts, detail) => {
|
|
1907
|
+
spinner.text = formatRetryReport({
|
|
1908
|
+
attempt,
|
|
1909
|
+
totalAttempts: attempts,
|
|
1910
|
+
reason: detail,
|
|
1911
|
+
nextDelayMs: 5e3
|
|
1912
|
+
});
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
const healthCheckResult = readiness.kind === "ready" ? Ok(true) : Err(readiness.detail);
|
|
1249
1916
|
if (!healthCheckResult.ok) {
|
|
1250
1917
|
spinner.fail("dotCMS failed to start properly");
|
|
1251
1918
|
console.error(healthCheckResult.val);
|
|
@@ -1255,9 +1922,9 @@ Maximum authentication attempts (${MAX_AUTH_ATTEMPTS}) reached. Exiting.
|
|
|
1255
1922
|
spinner.succeed("dotCMS is running locally at http://localhost:8082");
|
|
1256
1923
|
spinner.succeed("Default credentials: admin@dotcms.com / admin");
|
|
1257
1924
|
if (starterOnlyMode) {
|
|
1258
|
-
console.log(
|
|
1925
|
+
console.log(chalk5.white(`\u2705 Project setup complete!`));
|
|
1259
1926
|
console.log(
|
|
1260
|
-
|
|
1927
|
+
chalk5.gray(
|
|
1261
1928
|
"Skipped frontend scaffolding and dotCMS UVE setup because --starter was provided."
|
|
1262
1929
|
)
|
|
1263
1930
|
);
|
|
@@ -1287,28 +1954,32 @@ Maximum authentication attempts (${MAX_AUTH_ATTEMPTS}) reached. Exiting.
|
|
|
1287
1954
|
} else {
|
|
1288
1955
|
spinner.succeed(`Retrieved default site (${defaultSite.val.entity.identifier})`);
|
|
1289
1956
|
}
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
hidden: false,
|
|
1294
|
-
value: getUVEConfigValue(
|
|
1295
|
-
`http://localhost:${getPortByFramework(selectedFramework)}`
|
|
1296
|
-
)
|
|
1297
|
-
}
|
|
1298
|
-
},
|
|
1957
|
+
recordRecoverableState({
|
|
1958
|
+
host: LOCAL_DOTCMS_HOST,
|
|
1959
|
+
token: dotcmsToken.val,
|
|
1299
1960
|
siteId: defaultSite.val.entity.identifier,
|
|
1300
|
-
|
|
1961
|
+
projectDirectory: finalDirectory,
|
|
1962
|
+
framework: selectedFramework
|
|
1301
1963
|
});
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1964
|
+
const uveOutcome = await configureUVE({
|
|
1965
|
+
host: LOCAL_DOTCMS_HOST,
|
|
1966
|
+
siteId: defaultSite.val.entity.identifier,
|
|
1967
|
+
token: dotcmsToken.val,
|
|
1968
|
+
mode: "local",
|
|
1969
|
+
frontendUrl: `http://localhost:${getPortByFramework(selectedFramework)}`,
|
|
1970
|
+
report: (message) => spinner.info(message)
|
|
1971
|
+
});
|
|
1972
|
+
if (uveOutcome.kind === "configured") {
|
|
1306
1973
|
spinner.succeed(`Configured the Universal Visual Editor`);
|
|
1974
|
+
} else {
|
|
1975
|
+
spinner.warn("Skipped Universal Visual Editor configuration.");
|
|
1976
|
+
console.log(chalk5.yellow(uveOutcome.message));
|
|
1307
1977
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1978
|
+
await withComposeFileMovedAside(
|
|
1979
|
+
finalDirectory,
|
|
1980
|
+
() => startScaffoldingFrontEnd({ spinner, selectedFramework, finalDirectory })
|
|
1981
|
+
);
|
|
1982
|
+
console.log(chalk5.white(`\u2705 Project setup complete!`));
|
|
1312
1983
|
const relativePath = getDisplayPath(finalDirectory, process.cwd());
|
|
1313
1984
|
displayFinalSteps({
|
|
1314
1985
|
host: "http://localhost:8082",
|
|
@@ -1321,11 +1992,11 @@ Maximum authentication attempts (${MAX_AUTH_ATTEMPTS}) reached. Exiting.
|
|
|
1321
1992
|
if (error instanceof Error) {
|
|
1322
1993
|
console.error(error.message);
|
|
1323
1994
|
if (process.env.DEBUG) {
|
|
1324
|
-
console.error("\n" +
|
|
1325
|
-
console.error(
|
|
1995
|
+
console.error("\n" + chalk5.gray("Stack trace:"));
|
|
1996
|
+
console.error(chalk5.gray(error.stack || "No stack trace available"));
|
|
1326
1997
|
}
|
|
1327
1998
|
} else {
|
|
1328
|
-
console.error(
|
|
1999
|
+
console.error(chalk5.red("\u274C An unexpected error occurred"));
|
|
1329
2000
|
console.error(String(error));
|
|
1330
2001
|
}
|
|
1331
2002
|
process.exit(1);
|
|
@@ -1343,7 +2014,7 @@ async function scaffoldFrontendProject({
|
|
|
1343
2014
|
return Ok(void 0);
|
|
1344
2015
|
} catch (err) {
|
|
1345
2016
|
console.log(
|
|
1346
|
-
|
|
2017
|
+
chalk5.red(
|
|
1347
2018
|
`\u274C Failed to create ${framework} project. Please check git installation and network connection.` + JSON.stringify(err)
|
|
1348
2019
|
)
|
|
1349
2020
|
);
|
|
@@ -1357,50 +2028,88 @@ async function downloadTheDockerCompose({
|
|
|
1357
2028
|
await downloadDockerCompose(directory);
|
|
1358
2029
|
return Ok(void 0);
|
|
1359
2030
|
} catch (err) {
|
|
1360
|
-
console.log(
|
|
2031
|
+
console.log(chalk5.red("\u274C Failed to download docker-compose.yml." + JSON.stringify(err)));
|
|
1361
2032
|
return Err(new FailedToDownloadDockerComposeError());
|
|
1362
2033
|
}
|
|
1363
2034
|
}
|
|
1364
2035
|
async function runDockerCompose({
|
|
1365
2036
|
directory,
|
|
1366
|
-
starterUrl
|
|
2037
|
+
starterUrl,
|
|
2038
|
+
onProgress
|
|
1367
2039
|
}) {
|
|
1368
2040
|
try {
|
|
1369
2041
|
if (starterUrl) {
|
|
1370
2042
|
await updateDockerComposeStarterUrl({ directory, starterUrl });
|
|
1371
2043
|
}
|
|
1372
2044
|
const env = starterUrl ? { ...process.env, CUSTOM_STARTER_URL: starterUrl } : process.env;
|
|
1373
|
-
|
|
1374
|
-
|
|
2045
|
+
const subprocess = execa3(
|
|
2046
|
+
"docker",
|
|
2047
|
+
[
|
|
2048
|
+
"compose",
|
|
2049
|
+
"up",
|
|
2050
|
+
"-d",
|
|
2051
|
+
"--wait",
|
|
2052
|
+
"--wait-timeout",
|
|
2053
|
+
String(COMPOSE_WAIT_TIMEOUT_SECONDS)
|
|
2054
|
+
],
|
|
2055
|
+
{ cwd: directory, env }
|
|
2056
|
+
);
|
|
2057
|
+
let lastLine = "starting containers";
|
|
2058
|
+
const startedAt = Date.now();
|
|
2059
|
+
const absorb = (chunk) => {
|
|
2060
|
+
const line = String(chunk).split("\n").map((part) => part.trim()).filter(Boolean).pop();
|
|
2061
|
+
if (line) {
|
|
2062
|
+
lastLine = line;
|
|
2063
|
+
}
|
|
2064
|
+
};
|
|
2065
|
+
subprocess.stdout?.on("data", absorb);
|
|
2066
|
+
subprocess.stderr?.on("data", absorb);
|
|
2067
|
+
const ticker = setInterval(() => {
|
|
2068
|
+
const elapsed = Math.round((Date.now() - startedAt) / 1e3);
|
|
2069
|
+
onProgress?.(`${lastLine} (${elapsed}s elapsed)`);
|
|
2070
|
+
}, PROGRESS_TICK_MS);
|
|
2071
|
+
try {
|
|
2072
|
+
await subprocess;
|
|
2073
|
+
} finally {
|
|
2074
|
+
clearInterval(ticker);
|
|
2075
|
+
}
|
|
1375
2076
|
return Ok(void 0);
|
|
1376
2077
|
} catch (err) {
|
|
1377
|
-
|
|
2078
|
+
const detail = await describeComposeState(directory);
|
|
2079
|
+
return Err(new Error(`${err.message}${detail}`));
|
|
2080
|
+
}
|
|
2081
|
+
}
|
|
2082
|
+
async function describeComposeState(directory) {
|
|
2083
|
+
try {
|
|
2084
|
+
const { stdout } = await execa3(
|
|
2085
|
+
"docker",
|
|
2086
|
+
["compose", "ps", "--format", "{{.Service}}: {{.State}} {{.Status}}"],
|
|
2087
|
+
{ cwd: directory }
|
|
2088
|
+
);
|
|
2089
|
+
return stdout.trim() ? `
|
|
2090
|
+
|
|
2091
|
+
Container state:
|
|
2092
|
+
${stdout.trim()}` : "";
|
|
2093
|
+
} catch {
|
|
2094
|
+
return "";
|
|
1378
2095
|
}
|
|
1379
2096
|
}
|
|
1380
2097
|
async function updateDockerComposeStarterUrl({
|
|
1381
2098
|
directory,
|
|
1382
2099
|
starterUrl
|
|
1383
2100
|
}) {
|
|
1384
|
-
const composePath =
|
|
1385
|
-
const composeContents = await
|
|
1386
|
-
const updatedContents = composeContents
|
|
1387
|
-
|
|
1388
|
-
`$1"${starterUrl}"`
|
|
1389
|
-
);
|
|
1390
|
-
if (updatedContents === composeContents) {
|
|
1391
|
-
throw new Error(
|
|
1392
|
-
"CUSTOM_STARTER_URL entry not found in docker-compose.yml. Unable to apply --starter value."
|
|
1393
|
-
);
|
|
1394
|
-
}
|
|
1395
|
-
await fs4.writeFile(composePath, updatedContents);
|
|
2101
|
+
const composePath = path8.join(directory, "docker-compose.yml");
|
|
2102
|
+
const composeContents = await fs6.readFile(composePath, "utf-8");
|
|
2103
|
+
const updatedContents = applyStarterUrl(composeContents, starterUrl);
|
|
2104
|
+
await fs6.writeFile(composePath, updatedContents);
|
|
1396
2105
|
}
|
|
1397
|
-
async function isDotcmsRunning(url, retries = 60) {
|
|
2106
|
+
async function isDotcmsRunning(url, retries = 60, onRetry) {
|
|
1398
2107
|
try {
|
|
1399
|
-
const res = await fetchWithRetry(url ?? DOTCMS_HEALTH_API, retries, 5e3);
|
|
1400
|
-
if (res && res.status
|
|
2108
|
+
const res = await fetchWithRetry(url ?? DOTCMS_HEALTH_API, retries, 5e3, 1e4, onRetry);
|
|
2109
|
+
if (res && isSuccessStatus(res.status)) {
|
|
1401
2110
|
return Ok(true);
|
|
1402
2111
|
}
|
|
1403
|
-
return Err("dotCMS health check returned non-
|
|
2112
|
+
return Err("dotCMS health check returned a non-success status");
|
|
1404
2113
|
} catch (error) {
|
|
1405
2114
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1406
2115
|
return Err(errorMessage);
|
|
@@ -1413,13 +2122,13 @@ function displayFinalSteps({
|
|
|
1413
2122
|
siteId,
|
|
1414
2123
|
host
|
|
1415
2124
|
}) {
|
|
2125
|
+
const connection = flushRecoverableState();
|
|
1416
2126
|
switch (selectedFramework) {
|
|
1417
2127
|
case "nextjs": {
|
|
1418
2128
|
finalStepsForNextjs({
|
|
1419
2129
|
projectPath: relativePath,
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
urlDotCMSInstance: host
|
|
2130
|
+
urlDotCMSInstance: host,
|
|
2131
|
+
connection
|
|
1423
2132
|
});
|
|
1424
2133
|
break;
|
|
1425
2134
|
}
|
|
@@ -1444,9 +2153,8 @@ function displayFinalSteps({
|
|
|
1444
2153
|
case "astro": {
|
|
1445
2154
|
finalStepsForAstro({
|
|
1446
2155
|
projectPath: relativePath,
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
urlDotCMSInstance: host
|
|
2156
|
+
urlDotCMSInstance: host,
|
|
2157
|
+
connection
|
|
1450
2158
|
});
|
|
1451
2159
|
break;
|
|
1452
2160
|
}
|
|
@@ -1464,7 +2172,7 @@ async function startScaffoldingFrontEnd({
|
|
|
1464
2172
|
});
|
|
1465
2173
|
if (!created.ok) {
|
|
1466
2174
|
spinner.fail(`Failed to scaffold frontend project (${selectedFramework}).`);
|
|
1467
|
-
|
|
2175
|
+
throw created.val;
|
|
1468
2176
|
}
|
|
1469
2177
|
spinner.succeed(`Frontend project (${selectedFramework}) scaffolded successfully.`);
|
|
1470
2178
|
spinner.start(
|
|
@@ -1473,11 +2181,11 @@ async function startScaffoldingFrontEnd({
|
|
|
1473
2181
|
${displayDependencies(selectedFramework)}`
|
|
1474
2182
|
);
|
|
1475
2183
|
const result = await installDependenciesForProject(finalDirectory);
|
|
1476
|
-
|
|
2184
|
+
const installReport = reportInstallResult(result);
|
|
2185
|
+
if (installReport.kind === "failed") {
|
|
1477
2186
|
spinner.fail(
|
|
1478
|
-
`Failed to install dependencies.
|
|
2187
|
+
`Failed to install dependencies (${installReport.reason}). Check that npm is installed and on your PATH.`
|
|
1479
2188
|
);
|
|
1480
|
-
process.exit(1);
|
|
1481
2189
|
} else {
|
|
1482
2190
|
spinner.succeed(`Dependencies installed`);
|
|
1483
2191
|
}
|
|
@@ -1513,8 +2221,8 @@ function printWelcomeScreen() {
|
|
|
1513
2221
|
env: "node"
|
|
1514
2222
|
// define the environment cfonts is being executed in
|
|
1515
2223
|
});
|
|
1516
|
-
console.log(
|
|
1517
|
-
console.log(
|
|
2224
|
+
console.log(chalk5.white("\nWelcome to dotCMS CLI"));
|
|
2225
|
+
console.log(chalk5.bgGrey.white("\n \u2139\uFE0F Beta: Features may change \n"));
|
|
1518
2226
|
}
|
|
1519
2227
|
createApp();
|
|
1520
2228
|
export {
|