@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/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 chalk4 from "chalk";
5
+ import chalk5 from "chalk";
6
6
  import { Command } from "commander";
7
7
  import { execa as execa3 } from "execa";
8
- import fs4 from "fs-extra";
8
+ import fs6 from "fs-extra";
9
9
  import ora from "ora";
10
- import path5 from "path";
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 (axios.isAxiosError(err)) {
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(", ") : "Axios request failed";
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 axios.post(endpoint, payload);
293
+ const res = await httpPost(endpoint, payload);
226
294
  return Ok(res.data.entity.token);
227
295
  } catch (err) {
228
- if (axios.isAxiosError(err)) {
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 axios.get(endpoint, {
262
- headers: { Authorization: `Bearer ${authenticationToken}` }
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
- await fs.emptyDir(targetPath);
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/git/index.ts
609
- import { execa as execa2 } from "execa";
610
- import fs3 from "fs-extra";
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 axios2 from "axios";
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
- async function fetchWithRetry(url, retries = 5, delay = 5e3, requestTimeout = 1e4) {
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 axios2.get(url, {
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
- let errorMsg = "";
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 = axios2.isAxiosError(lastError) && lastError.code === "ECONNREFUSED" ? "Connection Refused" : axios2.isAxiosError(lastError) && (lastError.code === "ETIMEDOUT" || lastError.code === "ECONNABORTED") ? "Timeout" : "Connection Failed";
843
+ const errorType = isHttpError(lastError) && lastError.code === "ECONNREFUSED" ? "Connection Refused" : isHttpError(lastError) && lastError.code === "ETIMEDOUT" ? "Timeout" : "Connection Failed";
650
844
  throw new Error(
651
- chalk3.red(
845
+ chalk4.red(
652
846
  `
653
847
  \u274C Failed to connect to dotCMS after ${retries} attempts (${errorType})
654
848
 
655
849
  `
656
- ) + chalk3.white(`URL: ${url}
657
- `) + chalk3.gray(`Request timeout: ${requestTimeout}ms per attempt
658
- `) + chalk3.gray(
659
- `Total retry window: ~${retries * (delay + requestTimeout) / 1e3}s
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
- ) + chalk3.yellow("Common causes:\n") + chalk3.white(" \u2022 dotCMS is still starting up (may need more time)\n") + chalk3.white(" \u2022 Container crashed or failed to start\n") + chalk3.white(" \u2022 Port conflict (8082 already in use)\n") + chalk3.white(" \u2022 Network/firewall blocking connection\n") + chalk3.white(
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
- ) + chalk3.gray(
860
+ ) + chalk4.gray(
667
861
  "Detailed error history:\n" + errors.map((e) => ` \u2022 ${e}`).join("\n")
668
862
  )
669
863
  );
670
864
  }
671
- console.log(
672
- chalk3.yellow(`\u23F3 dotCMS not ready (attempt ${i + 1}/${retries})`) + chalk3.gray(` - ${errorMsg}`) + chalk3.gray(` - Retrying in ${delay / 1e3}s...`)
673
- );
674
- await new Promise((r) => setTimeout(r, delay));
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 downloadFile(url, dest) {
714
- return new Promise((resolve, reject) => {
715
- const file = fs2.createWriteStream(dest);
716
- https.get(url, (response) => {
717
- if (response.statusCode !== 200) {
718
- return reject(new Error(`Failed to download file: ${response.statusCode}`));
719
- }
720
- response.pipe(file);
721
- file.on("finish", () => file.close(() => resolve()));
722
- }).on("error", (err) => {
723
- fs2.unlink(dest);
724
- reject(err);
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
- siteId,
732
- token
932
+ connection
733
933
  }) {
734
934
  console.log("\n");
735
- console.log(chalk3.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"));
736
- console.log(chalk3.greenBright("\u{1F4CB} Next Steps:\n"));
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
- chalk3.white("1. Navigate to your project:\n") + chalk3.gray(` $ cd ${escapeShellPath(projectPath)}
941
+ chalk4.white("1. Navigate to your project:\n") + chalk4.gray(` $ cd ${escapeShellPath(projectPath)}
739
942
  `)
740
943
  );
741
944
  console.log(
742
- chalk3.white("2. Create your environment file:\n") + chalk3.gray(" $ touch .env\n")
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
- chalk3.white("4. Start your development server:\n") + chalk3.gray(" $ npm run dev\n")
948
+ chalk4.white("3. Open your browser:\n") + chalk4.gray(" \u2192 http://localhost:3000\n")
751
949
  );
752
950
  console.log(
753
- chalk3.white("5. Open your browser:\n") + chalk3.gray(" \u2192 http://localhost:3000\n")
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(chalk3.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"));
760
- console.log(chalk3.blueBright("\u{1F4D6} Documentation: ") + chalk3.white("https://dev.dotcms.com"));
761
- console.log(chalk3.blueBright("\u{1F4AC} Community: ") + chalk3.white("https://community.dotcms.com\n"));
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
- siteId,
767
- token
961
+ connection
768
962
  }) {
769
963
  console.log("\n");
770
- console.log(chalk3.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"));
771
- console.log(chalk3.greenBright("\u{1F4CB} Next Steps:\n"));
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
- chalk3.white("1. Navigate to your project:\n") + chalk3.gray(` $ cd ${escapeShellPath(projectPath)}
970
+ chalk4.white("1. Navigate to your project:\n") + chalk4.gray(` $ cd ${escapeShellPath(projectPath)}
774
971
  `)
775
972
  );
776
973
  console.log(
777
- chalk3.white("2. Create your environment file:\n") + chalk3.gray(" $ touch .env\n")
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
- chalk3.white("4. Start your development server:\n") + chalk3.gray(" $ npm run dev\n")
977
+ chalk4.white("3. Open your browser:\n") + chalk4.gray(" \u2192 http://localhost:3000\n")
786
978
  );
787
979
  console.log(
788
- chalk3.white("5. Open your browser:\n") + chalk3.gray(" \u2192 http://localhost:3000\n")
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(chalk3.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"));
795
- console.log(chalk3.blueBright("\u{1F4D6} Documentation: ") + chalk3.white("https://dev.dotcms.com"));
796
- console.log(chalk3.blueBright("\u{1F4AC} Community: ") + chalk3.white("https://community.dotcms.com\n"));
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(chalk3.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"));
806
- console.log(chalk3.greenBright("\u{1F4CB} Next Steps:\n"));
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
- chalk3.white("1. Navigate to your environments directory:\n") + chalk3.gray(` $ cd ${escapeShellPath(projectPath)}/src/environments
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
- chalk3.white("2. Update the environment files:\n") + chalk3.gray(
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(chalk3.white("3. Add your dotCMS configuration:\n"));
817
- 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"));
818
- console.log(chalk3.white(getEnvVariablesForAngular(urlDotCMSInstance, siteId, token)));
819
- 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"));
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
- chalk3.gray(" \u{1F4A1} Tip: Copy the block above and paste it into both environment files\n")
1010
+ chalk4.gray(" \u{1F4A1} Tip: Copy the block above and paste it into both environment files\n")
822
1011
  );
823
- console.log(chalk3.white("4. Start your development server:\n") + chalk3.gray(" $ ng serve\n"));
1012
+ console.log(chalk4.white("4. Start your development server:\n") + chalk4.gray(" $ ng serve\n"));
824
1013
  console.log(
825
- chalk3.white("5. Open your browser:\n") + chalk3.gray(" \u2192 http://localhost:4200\n")
1014
+ chalk4.white("5. Open your browser:\n") + chalk4.gray(" \u2192 http://localhost:4200\n")
826
1015
  );
827
1016
  console.log(
828
- chalk3.white("6. Edit your page content in dotCMS:\n") + chalk3.gray(` \u2192 ${urlDotCMSInstance}/dotAdmin/#/edit-page?url=/index
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(chalk3.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"));
832
- console.log(chalk3.blueBright("\u{1F4D6} Documentation: ") + chalk3.white("https://dev.dotcms.com"));
833
- console.log(chalk3.blueBright("\u{1F4AC} Community: ") + chalk3.white("https://community.dotcms.com\n"));
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(chalk3.white("Dependencies:"));
889
- dependencies.forEach((item) => lines.push(chalk3.grey(`- ${item}`)));
1095
+ lines.push(chalk4.white("Dependencies:"));
1096
+ dependencies.forEach((item) => lines.push(chalk4.grey(`- ${item}`)));
890
1097
  lines.push("");
891
- lines.push(chalk3.white("Dev Dependencies:"));
892
- devDependencies.forEach((item) => lines.push(chalk3.grey(`- ${item}`)));
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 = chalk3.red("\n\u274C Docker is not available\n\n") + chalk3.white("Docker is required to run dotCMS locally.\n\n") + chalk3.yellow("How to fix:\n") + chalk3.white(" 1. Install Docker Desktop:\n") + chalk3.cyan(" \u2192 https://www.docker.com/products/docker-desktop\n\n") + chalk3.white(" 2. Start Docker Desktop\n") + chalk3.white(
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
- ) + chalk3.white(" 4. Run this command again\n\n") + chalk3.gray("Alternative: Use --url flag to connect to an existing dotCMS instance");
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
- chalk3.yellow(
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 checkPortsAvailability() {
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 requiredPorts) {
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
- if (busyPorts.length > 0) {
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(chalk3.yellow("\n\u26A0\uFE0F No Docker containers found"));
1158
+ diagnostics.push(chalk4.yellow("\n\u26A0\uFE0F No Docker containers found"));
966
1159
  diagnostics.push(
967
- chalk3.white("The docker-compose.yml may not have been started correctly\n")
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(chalk3.cyan("\n\u{1F4CB} Container Status:"));
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} ${chalk3.white(name)}: ${chalk3.gray(status)}`);
1170
+ diagnostics.push(` ${icon} ${chalk4.white(name)}: ${chalk4.gray(status)}`);
978
1171
  if (ports) {
979
- diagnostics.push(` ${chalk3.gray("Ports:")} ${chalk3.white(ports)}`);
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(chalk3.yellow("\n\u{1F50D} Recent logs from problematic containers:\n"));
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(chalk3.white(`
1187
+ diagnostics.push(chalk4.white(`
995
1188
  --- ${name} ---`));
996
- diagnostics.push(chalk3.gray(logs.split("\n").slice(-10).join("\n")));
1189
+ diagnostics.push(chalk4.gray(logs.split("\n").slice(-10).join("\n")));
997
1190
  } catch {
998
- diagnostics.push(chalk3.gray(` Unable to fetch logs for ${name}`));
1191
+ diagnostics.push(chalk4.gray(` Unable to fetch logs for ${name}`));
999
1192
  }
1000
1193
  }
1001
1194
  }
1002
1195
  } catch (error) {
1003
- diagnostics.push(chalk3.red("\n\u274C Failed to get Docker diagnostics"));
1004
- diagnostics.push(chalk3.gray(String(error)));
1005
- }
1006
- diagnostics.push(chalk3.yellow("\n\u{1F4A1} Troubleshooting steps:"));
1007
- diagnostics.push(chalk3.white(" 1. Check if all containers are running:"));
1008
- diagnostics.push(chalk3.gray(" docker ps"));
1009
- diagnostics.push(chalk3.white(" 2. View logs for a specific container:"));
1010
- diagnostics.push(chalk3.gray(" docker logs <container-name>"));
1011
- diagnostics.push(chalk3.white(" 3. Restart the containers:"));
1012
- diagnostics.push(chalk3.gray(" docker compose down && docker compose up -d"));
1013
- diagnostics.push(chalk3.white(" 4. Check if ports 8082, 8443, 9200, and 9600 are available\n"));
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 = path4.join(directory, "examples", `${framework}`);
1411
+ const src = path6.join(directory, "examples", `${framework}`);
1043
1412
  const dest = directory;
1044
- const items = await fs3.readdir(directory);
1413
+ const items = await fs4.readdir(directory);
1045
1414
  for (const item of items) {
1046
1415
  if (item !== "examples") {
1047
- await fs3.remove(path4.join(directory, item));
1416
+ await fs4.remove(path6.join(directory, item));
1048
1417
  }
1049
1418
  }
1050
- await fs3.copy(src, dest, { overwrite: true });
1051
- const allItems = await fs3.readdir(directory);
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 fs3.remove(path4.join(directory, item));
1423
+ await fs4.remove(path6.join(directory, item));
1055
1424
  }
1056
1425
  }
1057
1426
  };
1058
1427
  async function downloadDockerCompose(directory) {
1059
- const dockerUrl = "https://raw.githubusercontent.com/dotCMS/core/main/docker/docker-compose-examples/single-node-demo-site/docker-compose.yml";
1060
- const dockerComposePath = path4.join(directory, "docker-compose.yml");
1061
- await downloadFile(dockerUrl, dockerComposePath);
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 moveDockerComposeOneLevelUp(directory) {
1064
- const sourcePath = path4.join(directory, "docker-compose.yml");
1065
- const targetPath = path4.join(directory, "..", "docker-compose.yml");
1066
- await fs3.rename(sourcePath, targetPath);
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
- async function moveDockerComposeBack(directory) {
1069
- const sourcePath = path4.join(directory, "..", "docker-compose.yml");
1070
- const targetPath = path4.join(directory, "docker-compose.yml");
1071
- await fs3.rename(sourcePath, targetPath);
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
- chalk4.yellow(
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
- chalk4.red(
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
- const setUpUVE2 = await DotCMSApi.setupUVEConfig({
1175
- payload: {
1176
- configuration: {
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
- authenticationToken: dotcmsToken2.val,
1185
- url: emaConfigApiURL
1790
+ projectDirectory: finalDirectory,
1791
+ framework: selectedFramework2
1186
1792
  });
1187
- if (!setUpUVE2.ok) {
1188
- spinner2.fail("Failed to setup UVE configuration in Dotcms.");
1189
- process.exit(1);
1190
- } else {
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(chalk4.white(`\u2705 Project setup complete!`));
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 portsAvailable = await checkPortsAvailability();
1215
- if (!portsAvailable.ok) {
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(portsAvailable.val);
1857
+ console.error(chalk5.red(portOutcome.message));
1218
1858
  process.exit(1);
1219
1859
  }
1220
- spinner.succeed("All required ports are available");
1221
- spinner.start("Downloading Docker Compose configuration...");
1222
- const downloaded = await downloadTheDockerCompose({
1223
- directory: finalDirectory
1224
- });
1225
- if (!downloaded.ok) {
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
- spinner.succeed("Docker Compose configuration downloaded");
1230
- spinner.start("Starting dotCMS containers...");
1231
- const ran = await runDockerCompose({
1232
- directory: finalDirectory,
1233
- starterUrl: options.starter
1234
- });
1235
- if (!ran.ok) {
1236
- spinner.fail("Failed to start Docker containers");
1237
- const errorMessage = ran.val instanceof Error ? ran.val.message : String(ran.val);
1238
- console.error(
1239
- chalk4.red("\n\u274C Docker Compose failed to start\n\n") + chalk4.white("Error details:\n") + chalk4.gray(errorMessage) + "\n\n" + chalk4.yellow("Common solutions:\n") + chalk4.white(" \u2022 Ensure Docker Desktop is running\n") + chalk4.white(" \u2022 Try: ") + chalk4.cyan("docker compose down") + chalk4.white(" then run this command again\n") + chalk4.white(" \u2022 Check Docker logs for more details\n")
1240
- );
1241
- process.exit(1);
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 healthCheckResult = await isDotcmsRunning(
1246
- DOTCMS_HEALTH_API,
1247
- LOCAL_HEALTH_CHECK_RETRIES
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(chalk4.white(`\u2705 Project setup complete!`));
1925
+ console.log(chalk5.white(`\u2705 Project setup complete!`));
1259
1926
  console.log(
1260
- chalk4.gray(
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
- const setUpUVE = await DotCMSApi.setupUVEConfig({
1291
- payload: {
1292
- configuration: {
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
- authenticationToken: dotcmsToken.val
1961
+ projectDirectory: finalDirectory,
1962
+ framework: selectedFramework
1301
1963
  });
1302
- if (!setUpUVE.ok) {
1303
- spinner.fail("Failed to setup UVE configuration in Dotcms.");
1304
- process.exit(1);
1305
- } else {
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
- moveDockerComposeOneLevelUp(finalDirectory);
1309
- await startScaffoldingFrontEnd({ spinner, selectedFramework, finalDirectory });
1310
- moveDockerComposeBack(finalDirectory);
1311
- console.log(chalk4.white(`\u2705 Project setup complete!`));
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" + chalk4.gray("Stack trace:"));
1325
- console.error(chalk4.gray(error.stack || "No stack trace available"));
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(chalk4.red("\u274C An unexpected error occurred"));
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
- chalk4.red(
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(chalk4.red("\u274C Failed to download docker-compose.yml." + JSON.stringify(err)));
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
- await execa3("docker", ["compose", "up", "-d"], { cwd: directory, env });
1374
- await execa3("docker", ["ps"], { cwd: directory });
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
- return Err(err);
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 = path5.join(directory, "docker-compose.yml");
1385
- const composeContents = await fs4.readFile(composePath, "utf-8");
1386
- const updatedContents = composeContents.replace(
1387
- /^(\s*["']?CUSTOM_STARTER_URL["']?\s*:\s*).+$/m,
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 === 200) {
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-200 status");
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
- token,
1421
- siteId,
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
- token,
1448
- siteId,
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
- process.exit(1);
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
- if (!result) {
2184
+ const installReport = reportInstallResult(result);
2185
+ if (installReport.kind === "failed") {
1477
2186
  spinner.fail(
1478
- `Failed to install dependencies. Please check if npm is installed in your system`
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(chalk4.white("\nWelcome to dotCMS CLI"));
1517
- console.log(chalk4.bgGrey.white("\n \u2139\uFE0F Beta: Features may change \n"));
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 {