Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
48fbf38
docs(sdk-cli): add spec for `dotcms agent setup`
fmontes Sep 3, 2026
4a2c2a3
docs(sdk-cli): verify the agent connects, and define partial-failure …
fmontes Sep 4, 2026
48360e6
docs(sdk-cli): clarify scope, required inputs, and drop status/remove
fmontes Sep 4, 2026
550f8d2
docs(sdk-cli): settle observability and terminology
fmontes Sep 4, 2026
58136d2
docs(sdk-cli): add CMS compatibility warning required by ADR-0019
fmontes Sep 4, 2026
189ed5d
docs(sdk-cli): add implementation plan and design artifacts
fmontes Sep 4, 2026
5e1fa60
docs(sdk-cli): organise the CLI by command group, not by technical layer
fmontes Sep 4, 2026
86d0d24
Merge remote-tracking branch 'origin/main' into fmontes/37390-agent-s…
fmontes Sep 4, 2026
38539da
docs(sdk-cli): extract shared CLI utils rather than copy them
fmontes Sep 4, 2026
ac5875c
refactor(create-app): extract HTTP layer to @dotcms/http
fmontes Sep 4, 2026
fc62993
docs(http): describe what the library is and why it exists
fmontes Sep 4, 2026
4317ccb
fix(core-web): remove trailing comma left in tsconfig.base.json
fmontes Sep 4, 2026
e077053
docs(sdk-cli): accept orphaned tokens as a known risk
fmontes Sep 4, 2026
e44e0ea
Merge branch 'fmontes/37390-dotcms-agent-setup' into fmontes/37390-ag…
fmontes Sep 4, 2026
258f23f
feat(sdk-cli): scaffold the dotcms CLI package (T001-T013)
fmontes Sep 4, 2026
b4d3159
test(sdk-cli): US1 test suite and stubs (T014-T022) -- Red confirmed
fmontes Sep 4, 2026
01294a2
style(sdk-cli): apply eslint import/order fixes
fmontes Sep 4, 2026
04aaa86
test(sdk-cli): address review on connect.spec and setup.spec
fmontes Sep 4, 2026
1323316
feat(sdk-cli): implement User Story 1 (T025-T038) -- 55/55 green
fmontes Sep 4, 2026
2f6f2b0
test(sdk-cli): US2 test suite -- merge safety, TOML, partial failure
fmontes Sep 4, 2026
412489c
test(sdk-cli): cover file permissions and redaction, which had no tests
fmontes Sep 4, 2026
7cb6d8f
feat(sdk-cli): Codex/TOML target and overwrite confirmation (T046-T050)
fmontes Sep 4, 2026
adb69fb
feat(sdk-cli): version-control safety for folder scope (T051-T059)
fmontes Sep 4, 2026
b9ae1f3
feat(sdk-cli): interactive prompting and required-input resolution (T…
fmontes Sep 4, 2026
1fc0bf3
test(sdk-cli): mutation sweep finds and closes two holes (T073-T080)
fmontes Sep 4, 2026
b668f23
test(sdk-cli): sweep the remaining modules, close a scheme-validation…
fmontes Sep 4, 2026
b1a90c7
feat(sdk-cli): polish -- error audit, progress reporting, README pointer
fmontes Sep 4, 2026
daae9df
fix(cicd): resolve each SDK's real npm name instead of assuming the s…
fmontes Sep 4, 2026
8e34f1c
test(sdk-cli): packaging invariants as an nx target, not ad-hoc greps
fmontes Sep 4, 2026
5231b15
fix(http): correct a false claim -- private does not block npm publish
fmontes Sep 4, 2026
f4a758b
feat(sdk-cli): retire the VS Code skills caveat; confirm Antigravity …
fmontes Sep 4, 2026
37ffd98
fix(sdk-cli): stop the tests writing agent config into the repository
fmontes Sep 4, 2026
7632c11
fix(sdk-cli): the auth-mode prompt hung, and the target picker was un…
fmontes Sep 4, 2026
fd54fb2
fix(sdk-cli): retry a rejected credential, and stop the spinner on fa…
fmontes Sep 4, 2026
4039e05
docs(sdk-cli): retry a rejected token, not only rejected credentials
fmontes Sep 4, 2026
d11d31d
Merge branch 'fmontes/37390-dotcms-agent-setup' into fmontes/37390-ag…
fmontes Sep 4, 2026
035b474
fix(sdk-cli): verify the address is really dotCMS, and read the real …
fmontes Sep 4, 2026
b78ece7
docs(sdk-cli): require the address to be a dotCMS instance, not just …
fmontes Sep 4, 2026
a45b8e0
Merge branch 'fmontes/37390-dotcms-agent-setup' into fmontes/37390-ag…
fmontes Sep 4, 2026
97cca4f
fix(sdk-cli): validate the instance before asking for credentials
fmontes Sep 4, 2026
3a58df7
fix(sdk-cli): seven defects from the manual test matrix
fmontes Sep 4, 2026
79245f6
fix(sdk-cli): TOML entry lost type="stdio" -- emit it generically ins…
fmontes Sep 4, 2026
2e6275d
docs(sdk-cli): three requirements that manual testing showed were wro…
fmontes Sep 4, 2026
5e7cefc
Merge branch 'fmontes/37390-dotcms-agent-setup' into fmontes/37390-ag…
fmontes Sep 4, 2026
c3c2a1c
fix(sdk-cli): two PR review findings — dead FR-005a warning, retry re…
fmontes Sep 4, 2026
694f8d4
style(sdk-cli,http): nx format:write — the CI format-test gate
fmontes Sep 4, 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
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,14 @@ runs:
# Pinned exact (no caret): sibling SDK packages always publish in lockstep at
# the same version, so a range only reintroduces the ambiguity ADR-0019 removes.
for dep in "${sdk_packages[@]}"; do
# Same reason as the publish step: resolve the sibling's real name rather than
# assuming the @dotcms/ scope, so an unscoped package is still repointed.
dep_name=$(jq -r '.name' "$dep/package.json" 2>/dev/null || echo "@dotcms/$dep")
for field in dependencies peerDependencies; do
if jq -e ".${field}[\"@dotcms/$dep\"]" "$pkg" >/dev/null 2>&1; then
jq --arg field "$field" --arg dep "@dotcms/$dep" --arg v "$RELEASE_VERSION" \
if jq -e ".${field}[\"$dep_name\"]" "$pkg" >/dev/null 2>&1; then
jq --arg field "$field" --arg dep "$dep_name" --arg v "$RELEASE_VERSION" \
'.[$field][$dep] = $v' "$pkg" > tmp.$$.json && mv tmp.$$.json "$pkg"
echo " ↳ $field @dotcms/$dep -> $RELEASE_VERSION"
echo " ↳ $field $dep_name -> $RELEASE_VERSION"
fi
done
done
Expand Down Expand Up @@ -173,18 +176,27 @@ runs:
PUBLISH_SUCCESS=true

for sdk in $(find . -maxdepth 1 -mindepth 1 -type d -exec basename {} \;); do
echo "📦 Processing @dotcms/${sdk}..."
cd "$sdk"

# The package's OWN name, never "@dotcms/<directory>".
#
# The scope used to be hardcoded here, which silently assumed every SDK is scoped.
# It held until `dotcms` — the unscoped CLI — where the guard below could never
# match: the first publish succeeded, then every re-run tried to publish a version
# that already existed and failed the release step. Reading `.name` makes the guard
# work for any naming scheme, scoped or not.
PKG_NAME=$(jq -r '.name' package.json)
echo "📦 Processing ${PKG_NAME}..."

# Idempotency: the version is fixed to the dotCMS release tag and can never be
# bumped, so if a prior (failed/cancelled) run already published this exact
# version, npm would reject a re-publish with a hard 403 — permanently stalling
# the release. Treat "already published" as success so a re-run can finish the
# remaining packages instead of getting stuck.
if npm view "@dotcms/${sdk}@${RELEASE_VERSION}" version >/dev/null 2>&1; then
echo " ⏭️ @dotcms/${sdk}@${RELEASE_VERSION} already published — skipping (idempotent re-run)"
if npm view "${PKG_NAME}@${RELEASE_VERSION}" version >/dev/null 2>&1; then
echo " ⏭️ ${PKG_NAME}@${RELEASE_VERSION} already published — skipping (idempotent re-run)"
else
echo " 🚀 Publishing: @dotcms/${sdk}@${RELEASE_VERSION} (tag: ${NPM_TAG})"
echo " 🚀 Publishing: ${PKG_NAME}@${RELEASE_VERSION} (tag: ${NPM_TAG})"
if npm publish --access public --tag "${NPM_TAG}"; then
echo " ✅ Published"
else
Expand Down
10 changes: 10 additions & 0 deletions core-web/apps/mcp-server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,16 @@ Get up and running with the dotCMS MCP Server in minutes.

The server runs on both **Node.js** (≥20) and **Bun** — the correct sandbox implementation is selected automatically at runtime.

### The short way

```bash
npx dotcms agent setup
```

One command mints and verifies a token, writes this server into your editor's own configuration, installs the dotCMS skills, and confirms the server starts. It covers Claude Code, Cursor, VS Code (Copilot), Codex, Antigravity, Devin and OpenCode — merging into whatever configuration you already have rather than replacing it.

The manual steps below remain accurate, and are what to reach for when you want to see exactly what is written, or you are configuring an editor the command does not cover.

### Claude Desktop Setup

Add the MCP server to your Claude Desktop configuration file. The configuration file location varies by operating system:
Expand Down
54 changes: 54 additions & 0 deletions core-web/libs/http/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# @dotcms/http

The HTTP layer for talking to a dotCMS instance from Node: one `fetch`-based client, retry
reporting, a `Result` type, and the API paths.

Internal to this workspace. Consumers inline it at build time, so nothing new reaches npm.

What actually keeps it unpublished is its **location**: the SDK release action iterates the
direct children of `core-web/libs/sdk/` and publishes each one, and this library deliberately
sits outside that directory. `private: true` is declared as intent, but it is not the
guarantee — npm only enforces `private` for *workspace* publishes (`npm publish -w`), so a
direct `npm publish` from this folder would still go through.

## What's in it

| Module | |
|---|---|
| `http.ts` | `httpGet` / `httpPost` over native `fetch`. Bearer auth, per-request timeout via `AbortController`, and `HttpError` carrying either an HTTP `status` or a transport `code` (`ECONNREFUSED`, `ETIMEDOUT`). |
| `fetch-retry.ts` | `describeRequestFailure()` turns a failure into a sentence a user can act on; `RetryReport` hands progress to whoever owns the terminal instead of writing over a spinner. |
| `result.ts` | `Result<T, E>` with `Ok` / `Err`, for calls whose failure is expected rather than exceptional. |
| `endpoints.ts` | `DOTCMS_API` paths, in one place so consumers cannot drift apart on them. |

## Why it exists

`@dotcms/create-app` and the `dotcms` CLI both authenticate against a dotCMS instance. This
code sets `Authorization` headers and follows redirects — the exact surface `axios` was
removed from this workspace over, after its Node adapter was found to leak
`Proxy-Authorization` across a redirect to a non-proxied origin (dotCMS/core#37264). A second
copy is where the next fix of that kind lands in one package and not the other.

Native `fetch` is used deliberately: the spec requires stripping `Authorization` on a
cross-origin redirect, which is the protection axios lacked. Requires Node ≥ 22.22.3.

## Using it

```ts
import { httpGet, HttpError, DOTCMS_API, endpoint } from '@dotcms/http';

const { data } = await httpGet(endpoint(url, DOTCMS_API.currentUser), { token });
```

Two things a consumer must wire up, both of which fail confusingly if missed:

- **Jest** — this workspace does not map `tsconfig.base.json` paths in `jest.preset.js`, so add
`moduleNameMapper: { '^@dotcms/http$': '<rootDir>/../../http/src/index.ts' }`. Without it the
alias compiles but never resolves at test time.
- **Buildable libs only** — `enforceBuildableLibDependency` is on workspace-wide, so a library
importing this one needs its own build target.

## Tests

```bash
nx test http
```
3 changes: 3 additions & 0 deletions core-web/libs/http/eslint.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import baseConfig from '../../eslint.config.mjs';

export default [...baseConfig];
10 changes: 10 additions & 0 deletions core-web/libs/http/jest.config.cts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
displayName: 'http',
preset: '../../jest.preset.js',
testEnvironment: 'node',
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
},
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/libs/http'
};
11 changes: 11 additions & 0 deletions core-web/libs/http/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@dotcms/http",
"version": "0.0.1",
"private": true,
"type": "commonjs",
"main": "./src/index.js",
"types": "./src/index.d.ts",
"dependencies": {
"tslib": "^2.3.0"
}
}
20 changes: 20 additions & 0 deletions core-web/libs/http/project.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "http",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "libs/http/src",
"projectType": "library",
"tags": [],
"// targets": "to see all targets run: nx show project http --web",
"targets": {
"build": {
"executor": "@nx/js:tsc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/libs/http",
"main": "libs/http/src/index.ts",
"tsConfig": "libs/http/tsconfig.lib.json",
"assets": ["libs/http/*.md"]
}
}
}
}
4 changes: 4 additions & 0 deletions core-web/libs/http/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './lib/http';
export * from './lib/fetch-retry';
export * from './lib/result';
export * from './lib/endpoints';
21 changes: 21 additions & 0 deletions core-web/libs/http/src/lib/endpoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* dotCMS REST paths, in one place so the CLIs cannot drift apart on them.
*
* `appconfiguration` is used for the reachability probe rather than `/probes/alive`: the probe
* endpoints carry IP ACLs and fail from outside the container (dotCMS/core#34509). The same
* response also carries the instance version, which is what the SDK compatibility warning
* required by ADR-0019 compares against.
*/
export const DOTCMS_API = {
/** Reachability probe, and the source of the instance version. */
appConfiguration: '/api/v1/appconfiguration',
/** Mints an API token. `expirationDays` is sent as a STRING. */
apiToken: '/api/v1/authentication/api-token',
/** Confirms a token actually authorizes, not merely that it was issued. */
currentUser: '/api/v1/users/current'
} as const;

/** Absolute URL for a dotCMS API path against a normalized instance base URL. */
export function endpoint(baseUrl: string, path: (typeof DOTCMS_API)[keyof typeof DOTCMS_API]) {
return `${baseUrl}${path}`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,28 @@ describe('isSuccessStatus — one rule for both callers', () => {
expect(isSuccessStatus(204)).toBe(true);
});
});

describe('describeRequestFailure names every transport cause we can distinguish', () => {
/**
* A bare code like "ENOTFOUND" is not a sentence. `@dotcms/create-app` shows it mid-wait,
* and the `dotcms` CLI has no verbose mode to fall back on, so the mapping has to be here.
*/
it.each([
['ENOTFOUND', /host not found/i],
['ECONNRESET', /reset/i],
['CERT_HAS_EXPIRED', /certificate/i],
['DEPTH_ZERO_SELF_SIGNED_CERT', /self-signed/i]
])('%s reads as a sentence', (code, expected) => {
const message = describeRequestFailure(new HttpError('x', { status: null, code }));
expect(message).toMatch(expected);
expect(message).not.toBe(code);
expect(message.length).toBeGreaterThan(15);
});

it('stays diagnostic — the caller owns the remedy, so advice is not duplicated', () => {
for (const code of ['ENOTFOUND', 'ECONNRESET', 'CERT_HAS_EXPIRED']) {
const message = describeRequestFailure(new HttpError('x', { status: null, code }));
expect(message).not.toMatch(/\bcheck\b|\btry\b|\bverify\b/i);
}
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,31 @@ export function describeRequestFailure(error: unknown): string {
return 'Connection timeout - service too slow or not responding';
}

// Without these the raw code reaches the user, and "ENOTFOUND" is not a sentence.
//
// These stay DIAGNOSTIC — what went wrong, not what to do about it. Callers add the
// remedy, and they know their own context; baking advice in here produced
// "Host not found - check the address... Check the address and that the instance is
// running." in the dotcms CLI.
if (error.code === 'ENOTFOUND') {
return 'Host not found (DNS lookup failed)';
}

if (error.code === 'ECONNRESET') {
return 'Connection reset by the server';
}

if (error.code === 'CERT_HAS_EXPIRED') {
return 'TLS certificate has expired';
}

if (
error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT' ||
error.code === 'SELF_SIGNED_CERT_IN_CHAIN'
) {
return 'TLS certificate is self-signed and not trusted';
}

if (error.response) {
return `HTTP ${error.response.status}: ${error.response.statusText}`;
}
Expand Down
23 changes: 23 additions & 0 deletions core-web/libs/http/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"module": "commonjs",
"forceConsistentCasingInFileNames": true,
"strict": true,
"importHelpers": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noPropertyAccessFromIndexSignature": true
},
"files": [],
"include": [],
"references": [
{
"path": "./tsconfig.lib.json"
},
{
"path": "./tsconfig.spec.json"
}
]
}
10 changes: 10 additions & 0 deletions core-web/libs/http/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"declaration": true,
"types": ["node"]
},
"include": ["src/**/*.ts"],
"exclude": ["jest.config.ts", "jest.config.cts", "src/**/*.spec.ts", "src/**/*.test.ts"]
}
16 changes: 16 additions & 0 deletions core-web/libs/http/tsconfig.spec.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/out-tsc",
"module": "commonjs",
"moduleResolution": "bundler",
"types": ["jest", "node"]
},
"include": [
"jest.config.ts",
"jest.config.cts",
"src/**/*.test.ts",
"src/**/*.spec.ts",
"src/**/*.d.ts"
]
}
54 changes: 54 additions & 0 deletions core-web/libs/sdk/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# dotcms

One command to connect an AI coding agent to a dotCMS instance.

```bash
npx dotcms agent setup
```

It asks where your instance is, how to authenticate, and which editors to wire up — then mints
and verifies a token, writes the dotCMS MCP server into each editor's own config file, installs
the dotCMS skills, and launches the server to confirm it responds before reporting success.

Replaces four manual steps: find the admin panel, mint a token by hand, hand-edit whichever
config file your editor reads, install the skills separately.

## Supported editors

Claude Code · Cursor · VS Code (Copilot) · Codex · Antigravity · Devin · OpenCode

Installed editors are detected and used by default; `--agent <id>` selects specific ones.

## Authentication

Two mutually exclusive modes. Passing both is a usage error, not a silent preference.

```bash
# mint a token from credentials
npx dotcms agent setup --url https://demo.dotcms.com --user admin@dotcms.com --password '…'

# or use a token you already have
npx dotcms agent setup --url https://demo.dotcms.com --authToken '…'
```

The URL plus one auth mode are the only required inputs — supply both and the command runs
without prompting, whether or not a terminal is attached.

> Passing a secret as a flag makes it visible in the process list and shell history. Prefer
> `DOTCMS_PASSWORD` / `DOTCMS_AUTH_TOKEN`, or let it prompt.

## Scope

Writes to the **current folder** by default, so each project can point at its own instance.
`-g/--global` writes to your user account instead. Folder scope offers to add the files it
touched to `.gitignore`, because they contain a token.

## Where the token goes

Only into the editor config files listed in the summary, restricted to your user where the
platform supports it. It is never logged in full, never passed as a process argument, and a
password is never written anywhere.

## Status

Not yet released. See `specs/37390-dotcms-agent-setup/` for the specification.
Loading
Loading