feat(sdk-cli): dotcms agent setup — one command to connect an IDE to dotCMS - #37416
feat(sdk-cli): dotcms agent setup — one command to connect an IDE to dotCMS#37416fmontes wants to merge 49 commits into
dotcms agent setup — one command to connect an IDE to dotCMS#37416Conversation
Spec-Kit PR 1 of 2 for #37390 — spec only, no implementation. Specifies a `dotcms agent setup` command that collapses the four manual steps needed to connect an AI coding agent to dotCMS (find the admin panel, mint a token, hand-edit an IDE config, install skills) into one command across seven agent targets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…semantics Addresses two gaps found reviewing the spec. Setup proved the token was valid but never that the MCP server actually starts, so a stale package cache or unsupported runtime would produce a green summary and a broken agent. Confirmation now runs by default (FR-024a-e, SC-002a): it launches the server as configured, confirms it reports its tools, and reports a non-start distinctly from a credential failure without rolling back written configs. Writing spans up to seven files and nothing said what happens when one fails after others succeeded. Setup now continues, reports per-target outcomes, and exits non-zero on any failure (FR-020a-d, SC-006a). Also: FR-013 annotated as a structural constraint rather than a testable requirement, FR-023a covers project scope outside version control, concurrent writes documented as a known limitation, and SC-001/SC-002 labelled design intent rather than automated gates. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Records four /speckit-clarify decisions. Folder is now the default scope, not the user account: one dotcms entry per config file, multiple instances via different folders. Because that makes the credential-into-a-repo path the default, FR-023 is strengthened so assume-yes takes the safe answer on the gitignore offer rather than skipping it. status and remove are cut from this release; only agent setup ships. User Story 5 withdrawn, FR-029/030/031 and SC-007/008 retired, and everything that leaned on those commands reworded. The agent sub-command group stays as the seam for adding them later. The written entry references the latest published server rather than pinning a version (FR-020e). The instance address plus one auth mode are the only required inputs; supply both and setup completes without prompting, terminal or not (FR-003i-l). Targets default to every detected editor and scope to the folder, so neither blocks a run. assume-yes and force govern confirmation prompts only and can never suppress a prompt for a missing required input. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No diagnostic mode ships — no verbose flag, no debug output, no log file. Recorded as a deliberate decision rather than an omission, with FR-032a requiring every failure message to be self-sufficient, since "re-run with more detail" is not available as a remedy. Terminology normalized to "token" for the thing minted, supplied, verified and written; a username and password are named as such rather than called "credentials". No behavior change. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The /speckit-plan ADR gate found that ADR-0019 (accepted) requires SDK packages to compare the instance's dotCMS version against their own and warn, fail-open, when the instance is older. dotcms ships from libs/sdk/, is published by the SDK release pipeline, and is date-lockstep versioned, so the requirement applies. FR-005a reuses the response already fetched for the reachability check, so it costs no additional request. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 0 + Phase 1 output of /speckit-plan for #37390. plan.md records the Constitution Check (PASS, three declared TDD exceptions needing sign-off) and the ADR Alignment gate. ADR-0019 is the one binding ADR and produced two conflicts: the unpinned @latest server reference is a justified deviation recorded in Complexity Tracking, and the missing CMS compatibility check is complied with, now FR-005a. research.md resolves ten unknowns. Three change the plan: the nx.json jest include entry is required rather than optional, @dotcms/mcp-server is outside date-lockstep with no release workflow in this repo, and chmod 0600 is a no-op on Windows so FR-021 is POSIX-only and must be reported honestly rather than assumed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The first layout put targets/, verify/, skills/ and asks.ts at the top level beside genuinely shared api/ and utils/. That reads fine at one command and degrades at two: create-app and the dotCLI port would spray files into the same directories with nothing marking which files serve which command. Restructured to shared/ plus commands/<group>/, mirroring what the user types. A second group is a new directory and zero edits to existing files. Adds an explicit one-way dependency rule — groups may import from shared/, never from each other, and shared/ never imports from a group — worth enforcing with ESLint rather than convention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merged main and re-checked create-app. Decision reversed: extract, not copy. http.ts sets Authorization headers and follows redirects -- the exact surface axios was removed from this workspace over, after semgrep found its Node adapter leaks Proxy-Authorization across a redirect to a non-proxied origin. Two copies of that code is where the next such fix lands in one package and not the other. That, not DRY, is the argument. http.ts, fetch-retry.ts, result.ts, the URL helpers and the endpoint constants move to a new internal lib core-web/libs/cli-shared -- no package.json, never published, inlined into both consumers by esbuild. Blast radius is 8 one-line import edits in create-app; its two spec files travel with their code. libs/cli-shared sits outside libs/sdk/ deliberately: the deploy action treats every direct child of libs/sdk/ as a publishable @dotcms/<dirname>. Token minting is NOT extracted -- create-app's getAuthToken returns Result values that are pre-formatted chalk strings, so presentation is entangled with the call. Only the endpoint constants are shared. Extraction lands as its own commit at the head of this PR, gated on create-app's 12 spec files passing with zero edits. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
http.ts sets Authorization headers and follows redirects -- the surface axios was removed from this workspace over, after semgrep found its Node adapter leaks Proxy-Authorization across a redirect to a non-proxied origin. A second copy in the new dotcms CLI is where the next fix of that kind lands in one package and not the other. That, not DRY, is the case for extracting. Generated with nx g @nx/js:library. Named for what it is -- the HTTP layer for talking to a dotCMS instance -- not for the CLIs that are its only consumers today. It sits outside libs/sdk/ deliberately: the deploy action treats every direct child of libs/sdk/ as a publishable @dotcms/<dirname>. package.json is private, so publishing is refused; consumers inline it via esbuild. Moves http.ts, fetch-retry.ts, result.ts and their two spec files out of create-app, adds endpoints.ts so the two CLIs cannot drift on API paths, and repoints 8 imports. Three things this surfaced that the plan had wrong: - Two spec files import the moved code, so the gate is "no test logic changes", not "zero test-file edits". - jest.preset.js does not map tsconfig paths, so create-app needed a moduleNameMapper entry or the alias compiles but never resolves. - enforceBuildableLibDependency forbids a buildable lib importing a non-buildable one, so libs/http needed a tsc build target. Verified: sdk-create-app 132 tests/10 suites, http 34 tests/2 suites, both builds succeed, lint clean, @dotcms/http absent from the bundle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the Nx generator stub. Covers the four modules, the security reason the code is shared rather than copied, and the two wiring steps a consumer must not miss -- the jest moduleNameMapper entry and the buildable-lib constraint -- both of which fail confusingly when skipped. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Removing the throwaway probe alias left a trailing comma after the @dotcms/http entry, making the file invalid JSON. TypeScript and esbuild parse tsconfig leniently, so every build and test still passed and the breakage was invisible -- a strict JSON reader would have failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Minting happens before configurations are written, so a run that mints and then fails leaves a real 365-day token on the instance that was never displayed and never recorded. Re-running mints another, and nothing identifies or revokes them. Accepted rather than mitigated. The alternative that actually recovers the credential is printing it, which contradicts FR-022 -- a rule User Story 3 rates P1. Orphans expire within a year and the failure requires an already-broken environment. Recorded in Assumptions rather than left silent, so a reviewer sees the trade-off instead of rediscovering it later. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Phase 1 and 2 of tasks.md -- scaffolding and types only. No behaviour, so nothing here precedes a TDD gate. Generated with nx g @nx/js:library, then given create-app's proven esbuild config: esm, bundled, runtime deps external, and the #!/usr/bin/env node banner that exists only in the production configuration. The generator named the project "cli". Renamed to "sdk-cli": the SDK release action builds --projects='sdk-*', so a project named "cli" would never be built or published. jest.config.ts carries the moduleNameMapper for @dotcms/http -- this workspace does not map tsconfig paths in jest.preset.js, so without it every suite touching the alias fails on module resolution instead of on the behaviour under test, and the Red gate could not be trusted. eslint.config.mjs enforces the one-way dependency rule: command groups may import shared/, shared/ may not import a command group, and groups may not import each other. Verified: production build emits the shebang, lint clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Writes the User Story 1 tests: URL resolution and reachability, the ADR-0019 compatibility warning, token minting and verification, target registry and per-platform paths, fresh-file JSON writing, the ordering guarantee, the connection check, skills delegation, and the summary. Signature-only stubs accompany them so the failures are assertion failures rather than module-resolution errors -- T024 rejects the latter as an invalid Red. Running Red before requesting approval surfaced four vacuous tests that passed against a throwing stub, because rejects.toThrow() and not.toThrow(/../) are satisfied by any error including "not implemented". They asserted nothing and could never have gone Red. Tightened to assert positively: the message must name the address, the token rejection, or the conflicting options. Red: 50 failed / 0 passed across 8 suites, 0 module-resolution errors. HALTING at T023 -- developer approval of the test set, including explicit sign-off on the three test types plan.md declares cannot be implemented. No implementation code written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
connect.spec.ts: the child/spawn mock was rebuilt in all five it blocks. Hoisted to beforeEach. setup.spec.ts: process.chdir() was wrong, and not only stylistically. chdir is process-global and Jest reuses a worker across spec files, so one suite was moving the working directory out from under the others -- the run reported 50 tests where 55 exist. Five tests were never executing. Replaced with an injected working directory: RunOptions.cwd and configPath(scope, cwd). That removes the global mutation and improves the production design, since folder-scope resolution is now a parameter rather than ambient state. Kept real temp directories rather than switching to a mocked filesystem. "Nothing was written" is the assertion the ordering suite turns on, and only a real empty directory settles it; a mocked fs would prove one API was not called, which a write through any other path slips past. plan.md's Test Strategy said "mocked filesystem" and was wrong -- corrected. Red now: 55 failed / 0 passed across 8 suites, 0 module-resolution errors, 0 vacuous passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Approval (T023) and Red (T024) recorded, then implementation to Green. Adds the shared layer -- named errors, env reading, redaction, instance resolution with the ADR-0019 compatibility warning, token mint/verify over @dotcms/http, and generic merge/write/chmod -- plus the agent group: the seven-target registry, the JSON writer, the connection check, skills delegation, the summary, and the commander wiring. The ordering guarantee is the load-bearing part: nothing touches the filesystem until the token has been verified, and --yes/--force cannot disable that. Two mechanical fixes to the approved tests, changing no assertion. Under ts-jest the node:os and node:child_process namespace objects are non-configurable, so jest.spyOn on them throws "Cannot redefine property"; replaced with module-factory mocks. And the summary now writes via process.stdout rather than console.log, which the workspace lint rule reserves for warn/error. Verified: 55/55 tests, 8 suites; lint clean; production build emits the shebang with @dotcms/http inlined. Smoke-tested the real binary -- conflicting auth and unknown target exit 2, unreachable exits 1, and no file is written on any of them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds 22 tests for User Story 2: JSON merge preserving other servers and unrelated settings verbatim, a similarly-named sibling left alone, replace-not-duplicate, malformed input as a named error with the file untouched, the Codex TOML target round-tripping comments and unrelated tables, overwrite confirmation, and partial-failure semantics. Installs smol-toml (1.8.0) and stubs the TOML target. Only 9 of the 22 are Red. The other 13 passed on arrival because US1's implementation already covered them -- config-file.ts was written with merge semantics and setup.ts with continue-on-error, both nominally US2 scope. Those are regression locks, not Red->Green tests, and the gate note in tasks.md says so rather than presenting a clean Red that is not there. RunOptions gains confirmOverwrite, injected so the FR-017 confirmation is testable without a terminal and setup.ts stays free of prompt mechanics. 77 tests total: 68 passing, 9 failing, 0 module-resolution errors. HALTING at T044. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ran targeted mutants against every behaviour that was implemented before its tests. Four were caught. Two survived with zero extra failures: disabling chmod, and making redact() return the raw secret. Neither was green-on-arrival -- both were entirely untested, and both are security requirements (FR-021/SC-004, FR-022/SC-005). Adds config-file.spec.ts and redact.spec.ts. The permission assertions are POSIX-guarded; the honesty assertion is not, and checks that permissionsApplied tracks the platform capability rather than being hard-coded true. Re-ran both mutants against the new tests: permissions now +3 failures, redaction +2, where both previously survived at +0. 85 tests: 76 passing, 9 failing (the genuine Phase 4 Red). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Implements the Codex writer with smol-toml, round-tripping comments and unrelated tables rather than regenerating the file -- replacing an existing [mcp_servers.dotcms] in place requires knowing where a table begins and ends, which is parsing by another name. Adds hasEntry() as a read-only check so FR-017's confirmation happens before anything is modified rather than as a rollback, and restrictFile() so the JSON and TOML writers share one permission path instead of drifting. setup.ts selects the writer from the registry's `format` field, so the flow still branches on nothing target-specific (FR-013). Mutation-checked the new code: dropping other servers and unrelated tables from the TOML writer takes failures 0 -> 3. 85/85 passing, lint clean, production build succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Folder scope is the default, so a token lands in a working directory on nearly every run. protectFromVersionControl() names every file it went into, offers exclusion, and warns where it cannot help. Three behaviours the tests pin down: - Not a git repository: the files are still named and warned as unprotected, rather than the step being silently skipped (FR-023a). Silence is how a token reaches a public repo. - A repo-root .mcp.json is conventionally COMMITTED, so excluding it is the unusual choice -- warned explicitly rather than quietly gitignored (FR-024). The one place the safe default is wrong for the workflow. - --yes supplies the SAFE answer (exclude) rather than bypassing the prompt, inverting the conventional meaning of -y (FR-023). Red first: all 9 new tests failed, 0 resolution errors. Then mutation-verified -- dropping the no-repo warning, the normally-committed warning, the decline check, or the dedup each takes exactly one test red. 94/94 passing, lint clean, production build succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…060-T072) Red first: 11 new tests, all failing, 0 resolution errors. Two rules that are easy to implement conventionally and wrongly, so both are pinned by tests and mutation-checked: - Prompting is driven by a MISSING REQUIRED INPUT, not by a mode. Supply the url and one auth mode and nothing is asked, terminal or not. A run does not become interactive merely because a terminal exists. - --yes governs CONFIRMATIONS ONLY. The usual reading, "assume defaults for everything", would silently skip a required input. Mutating resolveRequiredInputs to honour --yes that way takes one test red. shared/prompts.ts owns the rules; commands/agent/prompts.ts owns how to ask, behind a PromptPort. That split is what lets the rules be tested without a terminal. Uses inquirer's own prompt module rather than the five @inquirer/* sub-packages -- inquirer is already a declared dependency here and in create-app, and the sub-packages would add install weight for every npx user to no benefit. Fixed an unrealistic mock found during Red: the url prompt returned 'typed-url', which correctly failed validation, so the test died before its assertion. 105/105 passing, lint clean, production build succeeds. Binary verified on four non-interactive failure paths: all exit 2 in under 120ms with no file written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mutation-tested every behaviour implemented before its tests. Six mutants
were caught; two survived.
Hole 1 -- an assertion that could only fail on a platform we never run.
expect(permissionsApplied).toBe(CAN_RESTRICT) is true === true on POSIX,
so a hard-coded true satisfied it; the assertion could only bite on
Windows. Made the capability injectable (writeMerged({ canRestrict })) so
the claim is checked against a platform that cannot restrict, from any
platform.
Hole 2 -- nothing covered FR-003j/FR-010: omitting --agent must configure
every detected editor. Added three tests (detected set is used, empty
detection writes nothing and exits 0, default scope is the folder).
Both mutants now take a test red.
Also recorded a method note: hard-coding a literal scope produced a
COMPILE error, not a failing test -- TypeScript narrowed the type and
rejected the later comparison as provably false. The tell for an invalid
mutant is the collected test TOTAL changing, not the failure count. A
sweep must compare totals.
109/109 passing.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… hole Fourteen more mutants across instance, auth, constants, json-target, connect, skills and ui. Thirteen caught, one survived. Removing the ^https?:// guard failed nothing: new URL() still rejects a bare host, so the existing test passed through that fallback. But new URL() parses ftp://, file:// and javascript: without complaint, so those would have reached fetch and been written into an editor's config with no test objecting. Added four tests -- three bad schemes plus plain http://localhost:8082, since local instances are the common case and must keep working. The mutant now takes 3 tests red. Also redid the compat-warning mutant in a valid form; the first attempt broke compilation. Everything else held, including the ones that are requirements rather than details: token in argv instead of the child environment, child not killed, 404 misclassified as a plain exit, skills invoked per-target, skills failure reported as success, summary saying "ready" after a failed connection, and unverified skills shown as installed. 113/113 passing, lint clean, production build succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
T081 error-message audit, written as a test rather than a one-off check. FR-032a makes "something failed" a defect, since there is no verbose mode to fall back on. Found two raw Error throws that named the target but offered no action -- replaced with NoConfigPathError. InvalidUrlError described the problem without the fix; now imperative. errors.spec.ts asserts every error names its subject, carries an action, is a sentence, leaks no secret, and is classified correctly for exit 1 vs 2. T082 proved no raw fetch error escapes: 12 cases across all three network call sites. T083 surfaced that ora and chalk were declared dependencies and externals that nothing imported -- every npx user installing them for nothing. That was a symptom: the connection check spawns npx and can run a minute on a cold cache with the CLI printing nothing. Added an onProgress port (mirroring promptPort) so setup.ts reports WHAT is happening and the command layer decides HOW -- a spinner on a terminal, plain lines in CI. Both dependencies now earn their place. Extended describeRequestFailure in @dotcms/http for ENOTFOUND, ECONNRESET and the TLS cases; "ENOTFOUND" is not a sentence. Kept them DIAGNOSTIC: the first attempt baked in advice and produced "Host not found - check the address... Check the address and that the instance is running." The caller owns the remedy, and a test now enforces that. T085 points the mcp-server README at the command while keeping the manual steps. Needed create-app's transformIgnorePatterns: [] -- chalk 5 and ora are ESM-only and Jest skips node_modules by default. sdk-cli 163, http 39, create-app 132. Lint clean, build succeeds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cope
The publish step's idempotency guard was
`npm view "@dotcms/${sdk}@${VERSION}"`, deriving the package name from the
directory and hardcoding the scope. That held while every SDK was scoped.
It breaks on `dotcms`, the unscoped CLI: the guard could never match, so
the first publish succeeded and every re-run then tried to publish a
version that already existed and failed the release step -- the exact
stall the guard exists to prevent.
Reads `.name` from each package.json instead, which works for any naming
scheme. Same fix in the version-rewrite step, where sibling dependencies
were repointed under an assumed scope.
Considered replacing the loop with `nx release publish`, which knows each
project's real name. Rejected: it has no already-published semantics, so
it would 403 on a re-run and reintroduce the stall this guard prevents.
Left the examples/* loop alone -- examples import SDK libraries, never the
CLI binary, so the scoped assumption still holds there.
Verified: all five run blocks pass `bash -n`; simulated against a fake
dist containing both a scoped and an unscoped package, where the old
guard looked up a nonexistent @dotcms/cli and the new one finds dotcms;
and confirmed via `nx show projects --projects='sdk-*'` that sdk-cli is in
the set this action builds, with cli the only one of eleven whose npm
name differs from @dotcms/<dir>.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Mirrors create-app's verify-package pattern: nx:run-commands running a
bash script, dependsOn build, wired into test so it cannot be forgotten.
Asserts what unit tests structurally cannot see, since they run against
the source tree and this runs against the artifact npm would upload. Each
check is a defect that actually happened this session:
- shebang present, so `npx dotcms` has an interpreter
- bin points at a file that exists
- @dotcms/http is INLINED -- it is unpublished, so a surviving import
would be unresolvable for every user
- every declared dependency is actually imported (ora and chalk were
declared, externalised and unused: install weight for nothing)
- npm pack includes index.js and README.md
- the package name is still the unscoped `dotcms` the release guard
resolves by reading .name
Verified the verifier: declaring an unused dependency, renaming the
package, stripping the shebang, and leaving @dotcms/http as an import
each turn it red.
Also corrected research.md R1. The shebang trap is narrower than
documented -- defaultConfiguration is production, so a bare `nx build`
does carry it. The trap needs an explicit non-production configuration,
which is why the check asserts the artifact rather than the command.
Final gate: sdk-cli 163, http 39, create-app 132; lint clean on all three.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I stated in a commit message and in the README that @dotcms/http cannot
be published because package.json is private. That is wrong. npm 10's
check is:
if (workspace && manifest.private) throw EPRIVATE
It is gated on `workspace`. It stops `npm publish -w <pkg>`; it does not
stop a direct `cd libs/http && npm publish`, which exits 0 and packs the
tarball. Confirmed against npm's source and by dry-run.
What actually keeps the library unpublished is its LOCATION: the SDK
release action iterates the direct children of core-web/libs/sdk/, and
this sits outside that directory with no nx-release-publish target.
private: true stays as a declaration of intent, but it is not the
mechanism, and saying otherwise was a false assurance.
Since the real guarantee is a directory boundary rather than a flag,
verify-package.sh now asserts it: no package marked private may sit under
libs/sdk/. Planting one there turns the check red -- verified. That moves
the invariant out of my head and into the build.
README corrected. Recorded as R17.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two changes, both from running the CLI by hand. Order. resolveRequiredInputs asked for the address AND the credential in one pass, then checkReachable ran afterwards -- so a developer typed a username and password against an address that was never going to work, and learned it only after the effort. Split resolveInstanceUrl out: the address is resolved and checked against the live instance first, and nothing asks for a credential until the instance is confirmed. Message. "answered (HTTP 404) but is not a dotCMS instance -- /api/v1/appconfiguration did not return a dotCMS configuration. Check the address points at the dotCMS server itself, not a proxy, CDN or site root." explained our method to someone who needs a verdict. Now: "https://example.com is not a valid dotCMS instance. Check the address." A test pins that: the message must not mention the endpoint, the payload shape, proxies or CDNs, and must stay under 120 characters. Three tests cover the ordering directly -- no password prompt when the address is not dotCMS, none when it is unreachable, and the address is always asked for first. 193 tests, lint clean. Verified by hand: --user and --password supplied against example.com never reach authentication, exit 1, nothing written. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1. --skip-mcp silently killed the skills install and the summary. The early return skipped everything downstream, so --skip-mcp alone did nothing and said nothing. It now skips WRITING only: skills are chosen from the selected targets rather than from successful writes, each target is reported as skipped, and the connection check is skipped implicitly since there is no configuration to prove. 2. "ftp://x" suggested "https://ftp://x" -- the suggestion stripped only https?://. Now strips any scheme. 3. Every failed target claimed a permissions problem on macOS. permissionsApplied is false for a failed write because nothing was written, not because the platform refused. Gated on the outcome. 4. TOML comments were dropped. smol-toml does not round-trip them, and Codex's config.toml is hand-maintained. The writer now parses to VALIDATE and splices to WRITE: only our own tables are replaced textually, everything else survives byte-for-byte. A comment immediately before the next table belongs to that table -- the first attempt ate it, and there is a test for exactly that. 5. Pre-existing directories were re-chmodded to 0700, widening a .cursor the developer had set to 0500. mkdir(recursive) reports what it created; only that is restricted now. 6. Env-only auth conflict named flags the developer never typed. The error now names the source actually used. 7. Duplicate --agent inflated the count. Dedup now happens before the count, not inside the write loop. Fixing 1 exposed an eighth: with writing skipped, the version-control step still announced "these files now contain an access token" about files never created. Narrowed to written/replaced. Five of the eight are one shape -- a value computed for one purpose reused where its meaning differs. Recorded as R22. 214 tests. Each fix mutation-checked: reverting it turns its test red. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tead
The comment-preservation rewrite hand-emitted command, args and env
behind a cast:
buildEntry(...) as { command: string; args: string[]; env: ... }
That cast asserts the entry has exactly those keys, so omitting `type`
was not an error -- the compiler had been told the field does not exist.
Every JSON target still wrote type: "stdio"; codex alone stopped. Any
field added to buildEntry later would have vanished the same way, with no
compile error and no failing test.
Fixed structurally rather than by adding the field back. renderEntry now
serializes whatever buildEntry returns via smol-toml's stringify.
Comments are irrelevant for that block because it is generated; the
splice still protects everything outside it, so the file keeps its
annotations and the entry keeps every field.
The guard is a structural assertion, not a field list:
expect(doc.mcp_servers.dotcms).toEqual(buildEntry(codex(), URL, TOKEN))
so a new field is covered the moment it exists. Reverting to the
hand-emitted version turns three tests red -- verified.
Second time today a cast hid a defect the type system had been tracking
(R20 was re-deriving an object from an id). Recorded as R23: where a value
must mirror another, compare it to that value, not to a description of it.
217 tests, lint clean. Verified on disk: comments preserved AND
type = "stdio" present.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ng or missing FR-003b said the conflict error must name the conflicting "options". The conflict can come from environment variables, and reporting --authToken to someone who set DOTCMS_AUTH_TOKEN sends them hunting for a flag they never typed. Now: name the inputs actually used. FR-003c1 is new. Nothing said the skip options are independent, and the implementation read --skip-mcp as "skip everything downstream", so it silently installed no skills and printed no summary. The only permitted implication is FR-024b's: with nothing written there is no configuration for the connection check to prove. FR-016a is new. "Preserves everything else exactly" was read as data only, so the TOML writer re-serialized a parsed document and deleted the developer's comments. For a hand-maintained format that is data loss even though every value survives. Not changed, because the spec was already right and the code was not: FR-021 says "any directory setup CREATES" (the implementation re-chmodded pre-existing ones), FR-023 says "every file it PLACED A TOKEN INTO" (it named files that were only skipped), and FR-024b already covered the connection check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @fmontes's task in 2m 5s —— View job Code Review —
|
| // real dotCMS is anyone asked for a credential: a password typed against a wrong | ||
| // address is wasted effort, and the failure would land after the work rather than | ||
| // before it. | ||
| const url = await resolveInstanceUrl(opts, opts.promptPort); |
There was a problem hiding this comment.
🔴 [P1] setup.ts:95 surface compatibility warning from checkReachable
Current code:
const url = await resolveInstanceUrl(opts, opts.promptPort);
step(`Checking ${url}`);
await checkReachable(url);Problem: Discards InstanceInfo.version; compatibilityWarning() has no production caller.
Fix:
const info = await checkReachable(url);
const warning = compatibilityWarning(info.version, toolVersion);
if (warning) step(warning);There was a problem hiding this comment.
Fixed in c3c2a1c. checkReachable's result is now kept and compared against TOOL_VERSION, surfaced via onWarning (live) and warnings[] (summary). TOOL_VERSION reads package.json at build time — the release pipeline rewrites .version before building, so a literal would report 0.2.0 from a package published as 26.9.x.
|
|
||
| opts.onAuthRetry?.((error as Error).message, attempt, MAX_AUTH_ATTEMPTS); | ||
| // Ask again from scratch: the url is settled, the credential is what was wrong. | ||
| inputs = await resolveRequiredInputs({ url, cwd: opts.cwd }, opts.promptPort); |
There was a problem hiding this comment.
🟡 [P2] setup.ts:135 re-prompt fresh credential on auth retry
Current code:
inputs = await resolveRequiredInputs({ url, cwd: opts.cwd }, opts.promptPort);Problem: Retry re-reads rejected env credential, exhausting attempts without fresh prompt.
Fix:
delete process.env[ENV_KEYS.authToken];
delete process.env[ENV_KEYS.password];
inputs = await resolveRequiredInputs({ url, cwd: opts.cwd }, opts.promptPort);There was a problem hiding this comment.
Fixed in c3c2a1c, though not with delete process.env[...] — mutating the environment would also affect anything else reading it. Instead extracted promptForAuth(port), which consults neither options nor env, and the retry calls that. For a credential the instance just rejected, the only useful source is the human.
| method: 'initialize', | ||
| params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'dotcms', version: '0' } } | ||
| })}\n` | ||
| ); |
There was a problem hiding this comment.
🔴 [P1] connect.ts:109 MCP handshake omits notifications/initialized before tools/list
Current code:
);
child.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' })}\n`);Problem: Missing notifications/initialized notification; tools/list may never be answered.
Fix:
);
child.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' })}\n`);
child.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' })}\n`);The MCP protocol requires the initialized notification before non-initialize requests; the server's own smoke test (apps/mcp-server/src/smoke/server-boot.spec.ts:104-112) sends initialize → notifications/initialized → tools/list. Without it the 60s timer can fire on every otherwise-successful setup, reporting connection: 'failed' and exit code 1 (FR-024a).
There was a problem hiding this comment.
Valid, but deliberately not in this commit — scoped to the two findings above. Tracking as follow-up.
| // before it. | ||
| const url = await resolveInstanceUrl(opts, opts.promptPort); | ||
| step(`Checking ${url}`); | ||
| await checkReachable(url); |
There was a problem hiding this comment.
🔴 [P1] setup.ts:95 discarded checkReachable result leaves FR-005a warning unimplemented
Current code:
const url = await resolveInstanceUrl(opts, opts.promptPort);
step(`Checking ${url}`);
await checkReachable(url);Problem: Return value discarded; compatibilityWarning() has no production caller.
Fix:
const info = await checkReachable(url);
const warning = compatibilityWarning(info.version, toolVersion);
if (warning) step(warning);checkReachable returns InstanceInfo { version } (instance.ts:12,71) and compatibilityWarning (instance.ts:117) is exported and unit-tested for the ADR-0019 / FR-005a warning, but grep finds no non-test caller — the version plumbing is dead code and the developer is never told the instance is older than the tool.
There was a problem hiding this comment.
Duplicate of the finding above — fixed in c3c2a1c.
| if (/Unsupported engine|requires Node|SyntaxError|Unexpected token/i.test(stderr)) { | ||
| return 'runtime-unsupported'; | ||
| } | ||
| return code === null ? 'exited' : 'exited'; |
There was a problem hiding this comment.
🟡 [P2] connect.ts:20 dead ternary returns 'exited' on both branches
Current code:
return code === null ? 'exited' : 'exited';Problem: Both branches return the same value; signal-killed case is unlabeled.
Fix:
return 'exited';Or give the code === null (killed-by-signal) case a distinct label per FR-024c's requirement to name distinguishable failure causes. No functional bug, but the branch is clearly unfinished.
There was a problem hiding this comment.
Valid, but deliberately not in this commit — scoped to the two findings above. Tracking as follow-up.
…-read env **1. The compatibility warning was dead code.** `compatibilityWarning()` existed and was unit-tested, `readVersion()` existed and was unit-tested, and nothing called one with the other. A developer on a dotCMS older than the tool was never told. Same shape as the `entity.version` defect earlier in this branch: the parts were tested, the join was not. `setup.ts` now keeps the `checkReachable` result and compares it against `TOOL_VERSION`, surfacing the notice through `onWarning` and the run's `warnings[]`. Both ends are wired: `agent/index.ts` prints it live, `ui.ts` repeats it in the summary. Writing the producer without the consumer is exactly the bug being fixed here, so the summary render is tested too. `TOOL_VERSION` reads `package.json` at build time rather than a literal — the SDK release pipeline rewrites `.version` to the release tag before building, so a hardcoded value would report `0.2.0` from a package published as `26.9.x`. The import is *named*, not default: a default import makes esbuild inline the entire manifest (dependency list, publishConfig) into the shipped bundle. **2. An auth retry re-read the credential the instance had just rejected.** The retry called `resolveRequiredInputs`, which consults options and the environment before prompting. So a token from `DOTCMS_AUTH_TOKEN` — or a password from `DOTCMS_PASSWORD` — was re-read unchanged and re-submitted until the three attempts ran out, with no prompt ever shown. For env users the retry feature did nothing at all. Extracted `promptForAuth(port)`, which consults neither options nor the environment, and the retry path calls that. For a credential the instance has just refused, the only useful source is the human. Version set to 0.2.0. Both fixes mutation-verified: reverting either turns its own tests red and no others, with the collected total unchanged. 225 tests, lint clean, verify-package green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`nx format:check` (Maven `format-test`) failed the Frontend Unit Tests job on 26 files across this branch. Whitespace only, no behaviour change: prettier collapses single-element JSON arrays onto one line and rewraps a number of long expressions. The pre-commit hook runs `nx format:write` against *staged* files only, so a file formatted at commit time can still drift when a later prettier-relevant edit lands elsewhere — the gate is repo-wide. Verified with a full `format:check`, then re-ran the three affected suites: sdk-cli 225, http 39, sdk-create-app 132. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing it Asked what happens to a folder that already holds other MCP servers, I ran it rather than re-reading the spec: `.cursor/mcp.json` seeded with `github` and `postgres`, `.vscode/mcp.json` with a comment, `.codex/config.toml` with hand-written annotations. No server was lost — but two real problems surfaced. **JSON files were reformatted.** `JSON.stringify(next, null, 2)` rebuilds the document from its parse tree, so values survived and bytes did not: inline arrays exploded, unrelated settings re-indented, the whole file churned. FR-016 and target-configs.md both promise the untouched keys come through byte-for-byte, and on a shared `.mcp.json` this is the difference between a one-entry diff and a whole-file one. **A valid VS Code config was rejected outright.** `.vscode/mcp.json` is JSONC — the same family as `settings.json` and `launch.json` — so a `// comment` made `JSON.parse` throw and the target failed with "is not valid JSON". Nothing was destroyed and the exit code was right, but we told a developer their working config was broken and declined to configure the editor. Trailing commas failed identically. Both have one cause and one fix: do for JSON what `toml-target.ts` already does — parse to validate, splice to write. Parsing is now `jsonc-parser`, so comments and trailing commas are input rather than corruption while a genuine syntax error is still MalformedConfigError (FR-018). `jsonc-parser`'s own `modify` was the obvious tool and the wrong one: with `formattingOptions` it reflows the sibling its insertion point abuts, without them it emits the entry compacted onto one line. Both rewrite bytes we were asked to leave alone. `setProperty` computes the one offset itself, and a new entry adopts the indentation already in the file rather than imposing two spaces. The diff a developer now sees is our entry plus the trailing comma syntax requires. Nothing else. Mutation-verified, three mutants, collected total unchanged at 232: strict `JSON.parse` kills the comment and trailing-comma tests; a fixed two-space indent kills the indentation test; rebuilding the document kills four, including the byte-for-byte one. That last test only bites because it pins a neighbouring line verbatim — an earlier version asserted values and passed against the reflow it was written to catch. Contracts updated to describe what the code does. 232 tests, lint clean, verify-package 8/8, format:check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… charge Four parallel cleanup reviews (reuse, simplification, efficiency, altitude). Everything below was flagged by at least two of them independently; no behaviour changes except where noted. **One writer contract keyed by format.** `writeTomlTarget` returned a bare `string` while the JSON writer returned `WriteResult`, so `setup.ts` hand-assembled the missing half — including `permissionsApplied: CAN_RESTRICT`, a platform constant standing in for an observation the TOML writer had already made and thrown away. For Codex the summary asserted a protection nobody checked. `config-file.spec.ts` even documents that comparing against `CAN_RESTRICT` is the tautology the injected flag exists to avoid; the flow had hardcoded it. Both writers now satisfy `TargetWriter` and `WRITERS[target.format]` selects one, so `format` joins `containerKey`/`entryShape` as registry data the flow never inspects. That removes the `isToml` branch that sat three lines under a comment claiming the flow branches on nothing target-specific, and the `smol-toml` import from the flow module. An eighth editor in a third format is now a registry entry plus a writer, not a third arm here. Behaviour change, in the honest direction: Codex's `permissionsApplied` is the real chmod result. **`shared/` no longer knows a second format exists.** `hasEntry` took an injected `parse` for exactly one caller — the mechanism by which TOML knowledge reached `shared/config-file.ts`, and which left TOML with two disagreeing notions of "is our entry present": object-lookup for the overwrite prompt, line-span for the write. They agreed by luck. Each writer answers for itself now, `hasTomlEntry` backed by the same `findEntrySpan` the write uses. **`.mcp.json` moved into the registry.** It was a basename set inside `gitignore.ts`, a module whose whole point is not knowing editors exist. It is `folderConfigIsCommitted` on the target now. The old unit test only proved a path matched a string; the binding worth pinning is target -> warning, so that is asserted end to end (mutation-verified: dropping the field turns it red). **Dead code, verified before deleting.** `instance.ts`'s `resolveUrl` was a second FR-004 implementation nothing called — and it owned the entire precedence and scheme-rejection suite, so those eight tests were green against code the CLI never ran while the shipped `resolveInstanceUrl` had no precedence coverage at all. Suite moved onto the real function. Also: `writeJsonTarget`'s path-only wrapper (spec-only caller), and `ResolvedInputs.prompted`, written at eight sites and read at none. **Smaller.** `http.ts` defined a third private copy of the success-status rule that `fetch-retry.ts` was written to centralise. `classify()`'s ternary returned `'exited'` from both arms. `connect.ts` re-split and re-parsed the whole buffer per chunk; it now consumes completed frames only. `NoConfigPathError` told developers to re-run with `--project`, a flag that does not exist. `MalformedConfigError` sniffed the format from the file extension when both callers know it. The summary's target column was padded to the longest id that happens to exist today. `warn()` was a byte-identical copy of `fail()`; `canPrompt()` was called twice in a row. Four near-identical `TargetOutcome` literals became one builder. `buildEntry` moved out of `json-target.ts`, which the TOML writer had to import from. Three doc blocks were stranded or inverted by earlier edits — one claimed the file is "parsed rather than text-spliced" above a function in a file that now splices, one sat above the wrong function entirely, and `printBanner` claimed to match create-app's banner, which it does not. Deliberately NOT done, reported instead: ~120 ms of startup spent eagerly loading inquirer/cfonts/ora (real and measured, but small against npx's own overhead and it would scatter `await import()` through the handler); adopting `endpoints.ts` in create-app, which still defines the same two paths twice more; `redact.ts` and `SkillsResult.command`, both unreferenced — those are unimplemented requirements, not cleanup. sdk-cli 234 · http 39 · sdk-create-app 132 · lint clean · verify-package 8/8 · format clean, plus an end-to-end run against a folder holding other MCP servers. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Vitest 4, `@nx/vite` and a registered `@nx/vitest` plugin were already in this workspace, and several SDK libraries run on them — so this follows the house pattern rather than introducing a runner. Each package declares an `nx:run-commands` target running `vitest run`, the same shape `libs/sdk/vue` uses. Both were pinned into `nx.json`'s `@nx/jest/plugin` include list; those two globs are gone. Two pieces of configuration existed only to work around Jest and are simply deleted: - `transformIgnorePatterns: []`. chalk 5 and ora are ESM-only, Jest skips `node_modules` by default, so they arrived untransformed and failed to parse. Vite serves ESM natively. - `ts-jest` with a `module: commonjs` spec tsconfig. Vite transpiles with esbuild. The `@dotcms/http` alias moves from `moduleNameMapper` to `resolve.alias` — a plain alias rather than `vite-tsconfig-paths`, which crawls every tsconfig in the monorepo during @nx/vite's graph inference and has segfaulted the native resolver on CI (the warning is written up in `libs/sdk/analytics/vite.config.mts`). One entry is all this needs. Spec migration was mostly mechanical `jest.*` -> `vi.*`, with three real changes: - `vi.mock` factories are async and take `importOriginal`, so `jest.requireActual` inside a factory becomes `await importOriginal<typeof x>()`. - `test-setup.ts` computed `PROJECT_ROOT` from `__dirname`, a CommonJS global that does not exist in the ESM Vite serves. Now `fileURLToPath(import.meta.url)`. - Two comments explained the module-factory workaround as a ts-jest limitation. The reason is real but not ts-jest's: an ES module namespace object is non-configurable, so `spyOn` on it throws under either runner. Reworded to say what is actually true. `defineConfig` comes from `vitest/config`, not `vite`: in Vitest 4 the `/// <reference types='vitest' />` comment no longer augments Vite's `UserConfig`, so a `test` block does not type-check against `vite`'s export. Both spec tsconfigs now typecheck clean, which they are included in and previously were not exercised for. `verify-package` stays a `dependsOn` of `test`, so the packaging invariants still gate on `nx test` — that is the only place they run. CI invokes `nx affected -t test` with no `--configuration`, so dropping the old `ci` configuration changes nothing. sdk-cli 234 · http 39, both green, lint clean, verify-package 8/8, production build fine, format clean. The repo-pollution backstop was re-verified by planting a forbidden file and confirming the suite fails — `setupFilesAfterEnv` became `setupFiles` and that guard is the one piece of setup whose silence would be indistinguishable from success. http runs in ~125 ms where Jest took seconds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Setup prompting, MCP initialization, secret-file handling, failure reporting, and spec alignment have unresolved correctness and security issues.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds the dotcms agent setup CLI for configuring supported IDE agents, backed by shared HTTP utilities and SDK publishing updates.
Changes:
- Adds agent detection, authentication, secure config merging, skills installation, and MCP verification.
- Extracts reusable HTTP/authentication utilities and migrates
create-app. - Adds extensive tests, packaging checks, documentation, and release-pipeline support.
File summaries
| File | Description |
|---|---|
.github/actions/core-cicd/deployment/deploy-javascript-sdk/action.yml |
Supports unscoped package publishing. |
core-web/apps/mcp-server/README.md |
Documents quick setup command. |
core-web/libs/http/README.md |
Documents shared HTTP library. |
core-web/libs/http/eslint.config.mjs |
Adds lint configuration. |
core-web/libs/http/package.json |
Defines internal HTTP package. |
core-web/libs/http/project.json |
Configures HTTP build and tests. |
core-web/libs/http/src/index.ts |
Exports HTTP APIs. |
core-web/libs/http/src/lib/endpoints.ts |
Centralizes dotCMS endpoints. |
core-web/libs/http/src/lib/fetch-retry.spec.ts |
Tests transport diagnostics. |
core-web/libs/http/src/lib/fetch-retry.ts |
Expands failure descriptions. |
core-web/libs/http/src/lib/http.spec.ts |
Tests fetch client behavior. |
core-web/libs/http/src/lib/http.ts |
Implements shared fetch client. |
core-web/libs/http/src/lib/result.ts |
Adds result primitives. |
core-web/libs/http/tsconfig.json |
Adds TypeScript project references. |
core-web/libs/http/tsconfig.lib.json |
Configures library compilation. |
core-web/libs/http/tsconfig.spec.json |
Configures test compilation. |
core-web/libs/http/vite.config.mts |
Configures Vitest. |
core-web/libs/sdk/cli/README.md |
Documents the new CLI. |
core-web/libs/sdk/cli/eslint.config.mjs |
Enforces dependency boundaries. |
core-web/libs/sdk/cli/package.json |
Defines publishable CLI package. |
core-web/libs/sdk/cli/project.json |
Configures build, test, and publishing. |
core-web/libs/sdk/cli/scripts/verify-package.sh |
Validates publish artifacts. |
core-web/libs/sdk/cli/src/commands/agent/connect.spec.ts |
Tests MCP connection verification. |
core-web/libs/sdk/cli/src/commands/agent/connect.ts |
Launches and probes MCP server. |
core-web/libs/sdk/cli/src/commands/agent/constants.ts |
Defines agent setup constants. |
core-web/libs/sdk/cli/src/commands/agent/gitignore.spec.ts |
Tests repository protection. |
core-web/libs/sdk/cli/src/commands/agent/gitignore.ts |
Adds config files to .gitignore. |
core-web/libs/sdk/cli/src/commands/agent/index.ts |
Registers agent command options. |
core-web/libs/sdk/cli/src/commands/agent/prompts.spec.ts |
Tests prompt compatibility. |
core-web/libs/sdk/cli/src/commands/agent/prompts.ts |
Adapts Inquirer prompts. |
core-web/libs/sdk/cli/src/commands/agent/setup.spec.ts |
Tests complete setup workflow. |
core-web/libs/sdk/cli/src/commands/agent/setup.ts |
Orchestrates agent setup. |
core-web/libs/sdk/cli/src/commands/agent/skills.spec.ts |
Tests skills installation. |
core-web/libs/sdk/cli/src/commands/agent/skills.ts |
Invokes skills installer. |
core-web/libs/sdk/cli/src/commands/agent/targets/entry.ts |
Builds MCP entries. |
core-web/libs/sdk/cli/src/commands/agent/targets/json-target.spec.ts |
Tests JSON target writing. |
core-web/libs/sdk/cli/src/commands/agent/targets/json-target.ts |
Writes JSON configurations. |
core-web/libs/sdk/cli/src/commands/agent/targets/registry.spec.ts |
Tests editor registry. |
core-web/libs/sdk/cli/src/commands/agent/targets/registry.ts |
Describes supported editors. |
core-web/libs/sdk/cli/src/commands/agent/targets/toml-target.spec.ts |
Tests TOML preservation. |
core-web/libs/sdk/cli/src/commands/agent/targets/toml-target.ts |
Writes Codex TOML configuration. |
core-web/libs/sdk/cli/src/commands/agent/targets/types.ts |
Defines target contracts. |
core-web/libs/sdk/cli/src/commands/agent/targets/writers.ts |
Registers format writers. |
core-web/libs/sdk/cli/src/index.ts |
Adds CLI entry point. |
core-web/libs/sdk/cli/src/shared/__fixtures__/appconfiguration.ts |
Adds realistic API fixture. |
core-web/libs/sdk/cli/src/shared/auth.spec.ts |
Tests token authentication. |
core-web/libs/sdk/cli/src/shared/auth.ts |
Mints and verifies tokens. |
core-web/libs/sdk/cli/src/shared/config-file.spec.ts |
Tests safe JSON merging. |
core-web/libs/sdk/cli/src/shared/config-file.ts |
Implements JSONC configuration writes. |
core-web/libs/sdk/cli/src/shared/env.ts |
Centralizes environment access. |
core-web/libs/sdk/cli/src/shared/errors.spec.ts |
Tests diagnostic errors. |
core-web/libs/sdk/cli/src/shared/errors.ts |
Defines CLI errors. |
core-web/libs/sdk/cli/src/shared/instance.spec.ts |
Tests instance validation. |
core-web/libs/sdk/cli/src/shared/instance.ts |
Validates instance and compatibility. |
core-web/libs/sdk/cli/src/shared/prompts.spec.ts |
Tests input resolution. |
core-web/libs/sdk/cli/src/shared/prompts.ts |
Resolves required inputs. |
core-web/libs/sdk/cli/src/shared/redact.spec.ts |
Tests token redaction. |
core-web/libs/sdk/cli/src/shared/redact.ts |
Redacts secrets. |
core-web/libs/sdk/cli/src/shared/types.ts |
Defines setup data types. |
core-web/libs/sdk/cli/src/shared/ui.spec.ts |
Tests summary output. |
core-web/libs/sdk/cli/src/shared/ui.ts |
Renders progress and summaries. |
core-web/libs/sdk/cli/src/shared/version.ts |
Exposes build-time CLI version. |
core-web/libs/sdk/cli/src/test-setup.ts |
Prevents test filesystem leakage. |
core-web/libs/sdk/cli/tsconfig.json |
Adds CLI TypeScript references. |
core-web/libs/sdk/cli/tsconfig.lib.json |
Configures CLI compilation. |
core-web/libs/sdk/cli/tsconfig.spec.json |
Configures CLI test compilation. |
core-web/libs/sdk/cli/vite.config.mts |
Configures CLI Vitest tests. |
core-web/libs/sdk/create-app/jest.config.ts |
Maps shared HTTP imports. |
core-web/libs/sdk/create-app/src/api/index.ts |
Migrates API calls to shared HTTP. |
core-web/libs/sdk/create-app/src/index.ts |
Migrates shared utilities. |
core-web/libs/sdk/create-app/src/utils/index.ts |
Uses shared HTTP and results. |
core-web/libs/sdk/create-app/src/utils/install.spec.ts |
Updates result imports. |
core-web/libs/sdk/create-app/src/utils/install.ts |
Uses shared result type. |
core-web/libs/sdk/create-app/src/utils/readiness.ts |
Uses shared status checks. |
core-web/libs/sdk/create-app/src/uve/configure-uve.spec.ts |
Updates HTTP mocks. |
core-web/libs/sdk/create-app/src/uve/configure-uve.ts |
Uses shared HTTP client. |
core-web/package.json |
Adds parser dependencies. |
core-web/pnpm-lock.yaml |
Locks new dependencies. |
core-web/tsconfig.base.json |
Adds workspace aliases. |
specs/37390-dotcms-agent-setup/contracts/cli-interface.md |
Defines CLI contract. |
specs/37390-dotcms-agent-setup/contracts/target-configs.md |
Defines editor configurations. |
specs/37390-dotcms-agent-setup/data-model.md |
Defines setup data model. |
specs/37390-dotcms-agent-setup/spec.md |
Specifies feature requirements. |
Review details
Files not reviewed (1)
- core-web/pnpm-lock.yaml: Generated file
Suppressed comments (3)
core-web/libs/sdk/cli/src/commands/agent/setup.ts:196
- If no editor is detected in a non-interactive run, this leaves
targetsempty. The command can then run the standalone connection check, return exit 0, and even print “Ready” although no editor was configured. The specified edge case requires a named message directing the user to choose--agent; do not silently treat this as success.
core-web/libs/sdk/cli/src/shared/config-file.ts:223 - The token is written before permissions are restricted. On POSIX, a new file is initially created with
0666 & umask(commonly0644), so other users can read it untilchmodruns, and a crash in between leaves it exposed permanently. Create an owner-only temporary/file descriptor first (mode0600) and then write/rename it.
core-web/libs/http/src/lib/http.ts:114 - The abort timer is cleared before
response.text()consumes the body. A server that sends headers and then stalls can therefore hang both CLIs indefinitely despite the request timeout. Keep the timer active through body consumption.
- Files reviewed: 81/83 changed files
- Comments generated: 11
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| lines: string[], | ||
| containerKey: string | ||
| ): { start: number; end: number } | null { | ||
| const ours = new RegExp(`^\\s*\\[\\s*${containerKey}\\.${ENTRY_KEY}\\s*(\\.[^\\]]+)?\\]`); |
| await fs.writeFile(file, next.endsWith('\n') ? next : `${next}\n`, 'utf8'); | ||
| // Report what chmod actually did. The flow used to assert `CAN_RESTRICT` here, which is a | ||
| // platform constant rather than an observation — the summary claimed a protection nobody | ||
| // had checked. | ||
| return { path: file, permissionsApplied: await restrictFile(file), replacedExisting }; |
| const tree = raw === null ? undefined : parseTree(raw, [], PARSE_OPTIONS); | ||
| if (raw === null || raw.trim() === '' || tree?.type !== 'object') { | ||
| next = `${JSON.stringify({ [args.containerKey]: { [args.entryKey]: args.entry } }, null, 2)}\n`; |
| export function validateUrl(url: string): void { | ||
| if (!/^https?:\/\//i.test(url)) throw new InvalidUrlError(url); | ||
| try { | ||
| new URL(url); |
| } | ||
| })}\n` | ||
| ); | ||
| child.stdin?.write(`${JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list' })}\n`); |
| const detected = await detectTargets(); | ||
| const detectedById = new Map(detected.map((t) => [t.id as string, t])); | ||
| if (opts.promptPort) { | ||
| const chosen = await opts.promptPort.multiSelect( |
| // 7. Skills — non-fatal by design (FR-026). | ||
| if (!opts.skipSkills) { |
| o.skillsInstalled = !skills.ok | ||
| ? 'no' | ||
| : target?.skillsLocationVerified | ||
| ? 'yes' | ||
| : 'unverified'; |
| const result = await confirmConnection({ url, token: token.value }); | ||
| connection = result.ok ? 'ok' : 'failed'; | ||
| if (!result.ok) connectionReason = `${result.cause}: ${result.detail}`; |
| @@ -1,5 +1,6 @@ | |||
| import { httpGet, httpPost } from '@dotcms/http'; | |||
|
dotbot code review:
No new actionable bugs were found in the current changes, but 1 prior unresolved dotbot finding still applies, so the patch remains incorrect. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · meta/muse-spark-1.3 · medium |
|
dotbot code review:
No new actionable bugs were found in the current changes, but 1 prior unresolved dotbot finding still applies, so the patch remains incorrect. Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads. reviewed by dotbot · ~z-ai/glm-latest · medium |
Spec-Kit PR 2 of 2 — implementation. The spec is #37392; its commits are the shared ancestor here and disappear from this diff once it merges.
Fixes #37390
What it solves
Connecting an AI coding agent to dotCMS took four manual steps: find the admin panel, mint an API token, hand-edit whichever config file your editor reads, install the skills separately. Seven editors, no two storing it the same way. The
@dotcms/mcp-serverREADME covered two of them.Three questions — instance, credentials, editors — and the agent is connected.
How it works
Four properties do most of the work:
Nothing is written until the token verifies. A rejected token leaves no file, no directory, no skills install — so a bad credential can't produce seven configs that fail confusingly later.
--yes/--forcecannot disable it.The instance is validated before you're asked for anything.
/api/v1/appconfigurationis fingerprinted for a real dotCMS payload; a proxy or CDN answering 200 is rejected. Typing a password against a wrong address was wasted effort.Merge, never clobber. These files already hold other MCP servers. Only the
dotcmskey is touched; an unparseable file is a named error that leaves it untouched. For Codex the writer parses to validate and splices to write, so hand-written comments survive.The run proves the agent connects. After writing, it launches the configured server and confirms it lists tools. A valid token is not proof the server starts.
Command surface
--urlplus one auth mode are the only required inputs; supply both and it runs unprompted, terminal or not. Targets default to every detected editor, scope to the current folder. The two auth modes are mutually exclusive — passing both is a usage error, not a silent preference.--agent(repeatable) ·-g/--global·--skip-mcp·--skip-skills·--skip-verify·-y·--force. Exit0all succeeded,1a target or the connection check failed,2usage error.Targets: Claude Code, Cursor, VS Code/Copilot, Codex, Antigravity, Devin, OpenCode.
New library:
@dotcms/httpcreate-appand this CLI both authenticate against a dotCMS instance.http.tssetsAuthorizationheaders and follows redirects — the exact surface axios was removed from this workspace over (its Node adapter leakedProxy-Authorizationacross a redirect, #37264). Two copies of that code is where the next such fix lands in one package and not the other. That, not DRY, is the case for extracting it.core-web/libs/http/— internal, never published:http.tshttpGet/httpPostover nativefetch; bearer auth,AbortControllertimeout,HttpErrorcarrying an HTTP status or a transport codefetch-retry.tsdescribeRequestFailure()— turnsENOTFOUNDinto a sentence, kept diagnostic so callers own the remedyresult.tsResult<T,E>endpoints.ts/api/v1paths, so the two CLIs can't driftConsumers:
@dotcms/create-app(moved off its own copies — 8 import edits, its 132 tests pass unchanged) anddotcms. Both bundle it in at build time, so nothing new reaches npm.It sits outside
libs/sdk/deliberately: the SDK release action publishes every direct child of that directory.private: trueis intent, not the mechanism — npm only enforcesprivatefor workspace publishes — soverify-packageasserts the boundary instead.What to review for
shared/vscommands/agent/— the package is organised by command group, not technical layer, becausecreate-appand the dotCLI port fold in as siblings later. An ESLint rule enforces one-way imports.targets/registry.ts— every per-editor difference is data. Adding an eighth editor is one object literal.setup.ts, and its test. It's the thing most worth breaking on purpose to check.Also in here
npm view "@dotcms/${dir}@${version}"— scope hardcoded, name from the directory.dotcmsis unscoped, so the guard could never match: first publish succeeds, every re-run then fails the release. Now reads.name. Of eleven SDK packages, this is the only one that differs.apps/mcp-server/README.mdpoints at the command; the manual steps stay.libs/sdk/cli/**andlibs/http/**added tonx.json's jest include — without it a project gets notesttarget at all.Testing
sdk-cli217 ·http39 ·create-app132 — all green, lint clean, production build emits the shebang.nx run sdk-cli:verify-packageasserts eight packaging invariants against the artifact npm would upload, and runs as part ofnx test. Each check corresponds to a defect that actually happened: a missing shebang,@dotcms/httpsurviving as an unresolvable import, declared-but-unused dependencies, an internal library drifting underlibs/sdk/.Fixes were mutation-verified rather than assumed: reverting one turns its test red. That found three holes where a test existed but could not fail.
Known limitations
agent statusandagent removeare not in this release. Re-running setup replaces a stale entry.chmoddoesn't touch Windows ACLs, and the summary says so rather than implying protection.@latest). Justified deviation from ADR-0019 —@dotcms/mcp-serverisn't date-lockstep, so there's no matched version to pin to.Before merge
dotcms agent setup#37392 approved — this is gated on the spec, not on its mergedotcms@0.0.21download volume before first publish (381/month;^0.0.21pins exactly, so only barenpm i dotcmschanges)quickstart.mdrun, including real editors connecting🤖 Generated with Claude Code