Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
33aa44a
docs(create-app): add design contracts and data model for #37262
fmontes Aug 28, 2026
a6d0fda
test(create-app): add compose verification script and record CLI desi…
fmontes Aug 28, 2026
167db9c
docs(create-app): align design contracts with the corrected 403 diagn…
fmontes Aug 28, 2026
1fec925
docs(create-app): record the D3 decision and correct the D1 recommend…
fmontes Aug 28, 2026
4bd061f
docs(create-app): the CLI ships its own compose file instead of editi…
fmontes Aug 28, 2026
c696da7
docs(create-app): reconcile design contracts with the settled spec de…
fmontes Aug 31, 2026
1ff2338
test(create-app): fix two broken checks and unblock spec module resol…
fmontes Sep 1, 2026
b28da9a
fix(create-app): bundle the compose file so the local stack starts un…
fmontes Sep 1, 2026
3ffa3a3
fix(create-app): a failed UVE call no longer destroys the run (#37262)
fmontes Sep 1, 2026
1bfb00b
fix(create-app): stop the CLI's own side effects from blocking recove…
fmontes Sep 1, 2026
0316a47
fix(create-app): make the wait legible and the readiness signal truth…
fmontes Sep 1, 2026
c281cc9
refactor(create-app): delete the two functions this fix made dead (#3…
fmontes Sep 1, 2026
5b2f288
test(create-app): fix two more defects the first real cold-start run …
fmontes Sep 1, 2026
88ce39f
fix(create-app): write the .env the scaffolded app can actually read …
fmontes Sep 1, 2026
8e05708
fix(create-app): honour the reuse decision and check the right ports …
fmontes Sep 1, 2026
8bfe6e7
fix(create-app): make the port-conflict prompt answerable (#37262)
fmontes Sep 1, 2026
17f3f31
fix(create-app): the port-conflict prompt renders its options (#37262)
fmontes Sep 1, 2026
467fbcf
fix(create-app): write the .env instead of asking the user to (#37262)
fmontes Sep 1, 2026
411143f
fix(create-app): fold the connection details into Next Steps (#37262)
fmontes Sep 1, 2026
ebfd6cf
test(create-app): gate AC-013 on the artifact, not the manifests (#37…
fmontes Sep 1, 2026
f39c0d8
ci(create-app): run both verification gates from `nx test` (#37262)
fmontes Sep 1, 2026
fa551f0
fix(create-app): drop axios for native fetch (#37262)
fmontes Sep 1, 2026
8a9b4f1
docs(create-app): correct the README against what the CLI now does (#…
fmontes Sep 1, 2026
1ff3b4a
style(create-app): satisfy the prettier format-test gate (#37262)
fmontes Sep 1, 2026
59890cb
fix(create-app): four defects the review found in the AC-008 fix (#37…
fmontes Sep 3, 2026
c42dbbb
fix(create-app): verify AC-008 end-to-end and close the gaps it expos…
fmontes Sep 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 99 additions & 16 deletions core-web/libs/sdk/create-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ Beta. Behavior and flags may change.

## Requirements

- Node.js + npm
- Node.js 22.22.3+ and npm
- Git
- Docker (for `--local` or `--starter`)
- Internet access (downloads templates and docker-compose)
- Internet access (downloads templates; pulls Docker images)

## Which SDK Version Should I Use?

Expand Down Expand Up @@ -89,22 +89,27 @@ Flow:
2. Checks dotCMS health at `/api/v1/appconfiguration`.
3. Authenticates (up to 3 attempts).
4. Reads `defaultSite` from `/api/v1/site/defaultSite`.
5. Configures UVE via `/api/v1/apps/dotema-config-v2/{siteId}`.
5. Configures UVE via `/api/v1/apps/dotema-config-v2/{siteId}`. **Optional** — if this fails the
CLI warns, explains how to finish it by hand, and carries on.
6. Scaffolds selected frontend and runs `npm install`.
7. Prints framework-specific env setup instructions.
7. Writes `.env` with your host, site ID and token (see [Your `.env`](#your-env)).

### 2) Local mode (`--local`)

Flow:

1. Validates Docker availability.
2. Validates required ports: `8082`, `8443`, `9200`, `9600`.
3. Downloads docker-compose from dotCMS main repo.
4. Runs `docker compose up -d`.
5. Waits for local health check.
2. Checks the ports this stack publishes: `8082`, `8443` and `8090`. A dotCMS already running on
`8082` is not treated as a conflict — see [If dotCMS is already running](#if-dotcms-is-already-running).
3. Writes the **bundled** `docker-compose.yml` into the project directory (see
[The bundled Docker stack](#the-bundled-docker-stack)).
4. Runs `docker compose up -d --wait`, which blocks until every service reports healthy, streaming
progress and elapsed time so a long first pull is never a silent spinner.
5. Waits for readiness on `/dotmgt/readyz`, falling back to `/api/v1/appconfiguration`.
6. Authenticates with default local credentials (`admin@dotcms.com` / `admin`).
7. Reads `defaultSite`, configures UVE, scaffolds frontend, runs `npm install`.
8. Prints framework-specific env setup instructions.
7. Reads `defaultSite`, configures UVE (optional — a failure warns and continues), scaffolds the
frontend, runs `npm install`.
8. Writes `.env` with your host, site ID and token (see [Your `.env`](#your-env)).

### 3) Starter-only local mode (`--starter <url>`)

Expand All @@ -113,14 +118,85 @@ Flow:
Flow:

1. Same Docker and port checks as local mode.
2. Downloads docker-compose.
2. Writes the bundled `docker-compose.yml`.
3. Rewrites `CUSTOM_STARTER_URL` in `docker-compose.yml`.
4. Also passes `CUSTOM_STARTER_URL` in compose environment at runtime.
5. Starts containers and waits for health check.
6. Skips frontend scaffold and dotCMS frontend settings flow (token, default site lookup, UVE setup).

Use this when your starter is not compatible with the default frontend sample flow.

## The bundled Docker stack

`--local` and `--starter` write a `docker-compose.yml` that **ships inside this package**. It is
no longer downloaded from the `dotCMS/core` repository at run time, so the stack you get is the one
this CLI version was tested against, rather than whatever is currently on `main`.

The stack is `db` (PostgreSQL), `opensearch`, and `dotcms`. `dotcms` starts only after both
dependencies report **healthy**, and carries `restart: unless-stopped`, so it no longer races
Postgres and exit at startup.

### Published ports

| Port | Binding | Purpose |
| --- | --- | --- |
| `8082` | all interfaces | dotCMS HTTP |
| `8443` | all interfaces | dotCMS HTTPS |
| `8090` | **`127.0.0.1` only** | dotCMS management endpoints |

PostgreSQL and OpenSearch publish **no** ports — they are reachable only from inside the compose
network, so running your own Postgres or OpenSearch on the usual ports does not conflict.

> **Why 8090 is loopback-only.** It serves `/dotmgt/livez`, `/dotmgt/readyz`, `/dotmgt/health` and
> `/dotmgt/metrics`, and dotCMS authorizes those purely by the port a request arrives on — there is
> no credential check and no IP allow-list. Binding it to `0.0.0.0` would expose your instance's
> health and metrics to everyone on your network. It is bound to `127.0.0.1` deliberately; do not
> "fix" it to a wildcard.

### Using a different compose file

Set `DOTCMS_COMPOSE_URL` to fetch one from a URL instead of using the bundled file:

```bash
DOTCMS_COMPOSE_URL=https://example.com/my-compose.yml npx @dotcms/create-app my-app --local
```

This is an escape hatch for hotfixes. The file must keep a single-line `CUSTOM_STARTER_URL:` entry
or `--starter` will fail against it.

## If dotCMS is already running

A dotCMS on `8082` from a previous run is not a conflict — the CLI probes it and offers a choice:

```
⚠ Found a dotCMS already running at http://localhost:8082
Docker project "my-app" · Up 8 minutes (healthy)

? How would you like to continue?
❯ Use this instance for my project
Replace it with a clean instance
Cancel
```

**Replace** stops that Docker project and removes its volumes (`docker compose -p <project> down -v`)
before starting fresh. It is offered only when a Compose project owns the port — something started
outside Compose is not the CLI's to destroy.

Reuse requires the instance to pass a readiness check **and** issue a token; anything else on `8082`
is still a hard failure.

In a non-interactive run (CI, or no TTY) the CLI auto-reuses and prints a notice. It never replaces
without being asked.

## Your `.env`

The CLI writes `.env` into your project with the values the scaffolded app reads — `NEXT_PUBLIC_*`
for Next.js, `PUBLIC_*` for Astro. You do not need to copy anything by hand.

An existing `.env` is never overwritten: the CLI prints the values instead so you can merge them.
Angular has no `.env` — it reads a TypeScript `environment` object, so the values are printed for
you to paste into the environment files.

## Examples

Interactive:
Expand Down Expand Up @@ -172,9 +248,10 @@ Docker not available:

Ports already in use:

- macOS/Linux: `lsof -i :8082`
- Windows: `netstat -ano | findstr ":8082"`
- Stop conflicting services or run `docker compose down`.
- If it is a dotCMS from a previous run, the CLI offers to reuse or replace it — see
[If dotCMS is already running](#if-dotcms-is-already-running).
- Otherwise, find the owner: `lsof -i :8082` (macOS/Linux) or
`netstat -ano | findstr ":8082"` (Windows), then stop it or run `docker compose down`.

`zip END header not found` during starter load:

Expand All @@ -186,13 +263,19 @@ Ports already in use:
Build:

```sh
yarn nx build sdk-create-app --skip-nx-cache
pnpm nx build sdk-create-app --skip-nx-cache
```

Lint:

```sh
yarn nx lint sdk-create-app
pnpm nx lint sdk-create-app
```

Verify the package ships correctly (asserts the compose asset reaches `dist/` and the npm tarball):

```sh
pnpm nx verify-package sdk-create-app
```

Dist output:
Expand Down
86 changes: 86 additions & 0 deletions core-web/libs/sdk/create-app/assets/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Bundled with @dotcms/create-app. Written to the project directory by the CLI.
# Implements contracts/compose-service-contract.md C1-C8.
services:
db:
image: pgvector/pgvector:pg18
command: postgres -c 'max_connections=400' -c 'shared_buffers=128MB'
environment:
POSTGRES_USER: 'dotcmsdbuser'
POSTGRES_PASSWORD: 'password'
POSTGRES_DB: 'dotcms'
volumes:
- dbdata:/var/lib/postgresql
networks: [db_net]
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U dotcmsdbuser -d dotcms -h localhost -p 5432']
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped

opensearch:
image: opensearchproject/opensearch:1
environment:
cluster.name: 'elastic-cluster'
discovery.type: 'single-node'
bootstrap.memory_lock: 'true'
OPENSEARCH_JAVA_OPTS: '-Xmx1G'
ulimits:
memlock: { soft: -1, hard: -1 }
nofile: { soft: 65536, hard: 65536 }
volumes:
- opensearch-data:/usr/share/opensearch/data
networks: [opensearch-net]
healthcheck:
test:
[
'CMD-SHELL',
'curl -sk https://localhost:9200 -u admin:admin | grep -q cluster_name'
]
interval: 10s
timeout: 5s
retries: 12
restart: unless-stopped

dotcms:
image: dotcms/dotcms:latest
environment:
CMS_JAVA_OPTS: '-Xmx1g '
LANG: 'C.UTF-8'
TZ: 'UTC'
DB_BASE_URL: 'jdbc:postgresql://db/dotcms'
DB_USERNAME: 'dotcmsdbuser'
DB_PASSWORD: 'password'
DOT_ES_AUTH_BASIC_PASSWORD: 'admin'
DOT_ES_ENDPOINTS: 'https://opensearch:9200'
DOT_INITIAL_ADMIN_PASSWORD: 'admin'
DOT_DOTCMS_CLUSTER_ID: 'dotcms-production'
CUSTOM_STARTER_URL: 'https://repo.dotcms.com/artifactory/libs-release-local/com/dotcms/starter/20260630/starter-20260630.zip'
depends_on:
db:
condition: service_healthy
opensearch:
condition: service_healthy
volumes:
- cms-shared:/data/shared
networks: [db_net, opensearch-net]
healthcheck:
test: ['CMD', 'curl', '-f', 'http://127.0.0.1:8090/dotmgt/livez']
interval: 30s
timeout: 10s
retries: 5
start_period: 180s
restart: unless-stopped
ports:
- '8082:8082'
- '8443:8443'
- '127.0.0.1:8090:8090'

networks:
db_net:
opensearch-net:

volumes:
cms-shared:
dbdata:
opensearch-data:
16 changes: 16 additions & 0 deletions core-web/libs/sdk/create-app/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ export default {
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
},
/*
* This package is `"type": "module"` and every one of its runtime dependencies is
* ESM-only (inquirer, ora, chalk, execa, axios — plus ~50 transitive micro-packages
* such as string-width, restore-cursor, npm-run-path...). Jest runs the specs as
* CommonJS, so anything left untransformed inside node_modules blows up with
* "SyntaxError: Cannot use import statement outside a module" the moment a spec
* imports this CLI's own source.
*
* Other libs in this workspace solve the same problem with a named allow-list
* (see libs/edit-content, apps/dotcms-ui, libs/portlets/dot-agents:
* `node_modules/(?!...y-protocols|lib0|@tiptap...)`). Here the list would need to
* enumerate ~51 packages and would silently rot on every dependency bump, so the
* whole of node_modules is transformed instead. The dependency tree of this CLI is
* small: a cold, uncached run of the full suite costs ~3s.
*/
transformIgnorePatterns: [],
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../../coverage/libs/sdk/create-app'
};
4 changes: 2 additions & 2 deletions core-web/libs/sdk/create-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
},
"files": [
"*.js",
"README.md"
"README.md",
"assets/**"
],
"dependencies": {
"axios": "^1.13.2",
"chalk": "^5.6.2",
"commander": "^14.0.2",
"execa": "^9.6.0",
Expand Down
31 changes: 30 additions & 1 deletion core-web/libs/sdk/create-app/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"inquirer",
"execa",
"cfonts",
"axios",
"fs-extra",
"commander"
],
Expand All @@ -45,6 +44,11 @@
"input": "libs/sdk/create-app",
"glob": "package.json",
"output": "."
},
{
"input": "libs/sdk/create-app/assets",
"glob": "**/*",
"output": "assets"
}
],
"esbuildOptions": {
Expand All @@ -71,6 +75,7 @@
}
},
"test": {
"dependsOn": ["verify-package", "verify-compose-static"],
"options": {
"passWithNoTests": true
},
Expand All @@ -80,6 +85,30 @@
"coverage": true
}
}
},
"verify-package": {
"executor": "nx:run-commands",
"dependsOn": ["build"],
"options": {
"command": "bash libs/sdk/create-app/scripts/verify-package.sh",
"cwd": "."
},
"cache": true,
"inputs": [
"{projectRoot}/assets/**",
"{projectRoot}/package.json",
"{projectRoot}/project.json",
"{projectRoot}/scripts/verify-package.sh"
]
},
"verify-compose-static": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["{projectRoot}/assets/**", "{projectRoot}/scripts/verify-cold-start.sh"],
"options": {
"command": "bash libs/sdk/create-app/scripts/verify-cold-start.sh --static",
"cwd": "."
}
}
}
}
Loading
Loading