Skip to content

feat(CMA): Add entry, asset, and content-type CRUD commands#3272

Open
Ivo Toby (ivo-toby) wants to merge 11 commits into
contentful:mainfrom
ivo-toby:cli
Open

feat(CMA): Add entry, asset, and content-type CRUD commands#3272
Ivo Toby (ivo-toby) wants to merge 11 commits into
contentful:mainfrom
ivo-toby:cli

Conversation

@ivo-toby
Copy link
Copy Markdown

Summary

Adds a complete CLI command surface for managing entries, assets, and content types via the Contentful Management API. Built on a new declarative createCommand factory that standardizes option handling, output formatting, and error behavior across all commands.

New commands:

  • contentful entry — get, list, create, update, delete, publish, unpublish, archive, unarchive
  • contentful asset — get, list, upload, update, delete, publish, unpublish
  • contentful content-type — get, create, update, delete, publish, unpublish (replaces old JS-based get)

New infrastructure:

  • lib/utils/command-factory.ts — Declarative command builder wiring standard options (--space-id, --environment-id, --management-token, --json, --quiet, --agent-mode, --dry-run, --yes) and error handling
  • lib/utils/output.ts — Unified output dispatch (JSON, table, key-value, quiet/ID-only, TOON for agents)
  • lib/utils/exit-codes.ts — Structured exit codes (1 = client error, 2 = server error)
  • lib/utils/validators.ts — Input validation helpers (ID format, JSON field parsing)
  • lib/utils/toon-encoder.ts + lib/utils/toon.ts — TOON format encoder for agent-mode output

Key design decisions

  • Declarative command config: Each command file declares intent (options, handler, tableFormat, quietExtractor) and the factory wires up yargs, auth, environment resolution, confirmation prompts, dry-run routing, and output formatting.
  • Dry-run support: Mutating commands (create, update, delete, publish, unpublish, upload) support --dry-run to preview operations without side effects.
  • Multiple output modes: Default (table), --json (raw API response), --quiet (IDs only for piping), --agent-mode (TOON format for LLM agent consumption).
  • Optimistic locking: Content-type and entry update commands require --version to prevent accidental overwrites.
  • Confirmation prompts: Destructive commands (delete) require confirmation unless --yes is passed.

Changes

  • 25 new command files across lib/cmds/{asset,entry,content-type}_cmds/
  • 6 new utility modules in lib/utils/
  • Replaced lib/cmds/content-type_cmds/get.js with TypeScript version
  • 22 unit test suites (327 tests) for commands
  • 6 unit test suites for utilities
  • 3 integration test suites
  • Updated README with command documentation

Test plan

  • All 727 unit tests pass (npm test)
  • TypeScript compiles cleanly (npm run tsc)
  • Integration tests pass with proxy fixtures (npm run test:integration)
  • Manual verification: contentful entry list --space-id <id> --json
  • Manual verification: contentful asset upload --file ./test.png --title "Test" --dry-run
  • Verify --quiet piping works: contentful entry list --content-type page -q | head -5

Ivo Toby (ivo-toby) and others added 5 commits May 8, 2026 16:42
…d output

Implement complete CLI command surface for entries (list, get, create,
update, publish, unpublish, archive, unarchive, delete), assets (list,
get, upload, update, publish, unpublish, delete), and content-types
(get, create, update, publish, unpublish, delete).

Built on a declarative command-factory that wires up standard options
(--json, --quiet, --agent-mode, --dry-run), runtime context fallback
for auth/space/environment, and TOON-format output for agent consumption.

Includes unit and integration tests for all commands and utilities.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add spy on process.exit in beforeEach and restore it in afterEach
to mock process termination calls during upload command tests
Add firstLocaleValue to jest mock for output utility across asset command tests to prevent mock-related errors. Replace inline exit spy creation with shared exitSpy variable and simplify test assertions by removing try-catch blocks around process.exit calls.
@wiz-inc-38d59fb8d7
Copy link
Copy Markdown

wiz-inc-38d59fb8d7 Bot commented May 8, 2026

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 3 Medium
Software Management Finding Software Management Findings -
Total 3 Medium

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try using Wiz Code VS Code Extension.

… client

Replace createManagementClient (legacy nested client) with
createPlainClient in command-factory and all command handlers.
This eliminates the deprecation warning from contentful-management v12.

API changes:
- environment.getEntry(id) → client.entry.get({entryId: id})
- environment.getAsset(id) → client.asset.get({assetId: id})
- environment.getContentType(id) → client.contentType.get({contentTypeId: id})
- entity.publish/unpublish/delete/update() → client.<type>.<method>({id}, data)

All 329 command tests and 38 command-factory tests updated and passing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ivo-toby Ivo Toby (ivo-toby) marked this pull request as ready for review May 8, 2026 16:10
@ivo-toby Ivo Toby (ivo-toby) requested a review from a team as a code owner May 8, 2026 16:10
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5258d3dac1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "Codex (@codex) review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "Codex (@codex) address that feedback".

Comment thread lib/utils/command-factory.ts
Comment thread lib/cmds/asset_cmds/upload.ts
Ivo Toby (ivo-toby) and others added 3 commits May 8, 2026 19:56
The old list.js was still using createManagementClient (nested/legacy
client), causing the deprecation warning. Replaced with a TypeScript
version using createPlainClient and the standard createCommand factory.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Makes commands more discoverable by showing concrete usage examples in
help text, useful for both humans and LLM-driven workflows.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Address PR review P1 feedback:

1. Remove `default: 'master'` from --environment-id yargs option. The
   middleware already resolves active environment from user config and
   falls back to master — the yargs default made omission
   indistinguishable from explicit use, overriding the configured
   active environment.

2. Fetch the space's default locale dynamically in asset upload instead
   of hardcoding en-US. Spaces without en-US would fail on upload when
   --locale was omitted.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread lib/cmds/content-type_cmds/unpublish.ts Outdated
}
},
handler: async (client, argv) => {
return client.contentType.unpublish({contentTypeId: argv.id})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

today I learned that content types can be unpublished :)

Comment thread README.md
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also add these new commands to the ./docs/README.md https://github.com/contentful/contentful-cli/blob/main/docs/README.md

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As well as existing ./docs/content-type

// eslint-disable-next-line @typescript-eslint/no-var-requires
const { createPlainClient } = require('./contentful-clients') as {
createPlainClient: (
params: {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use type ClientOptions to reduce risk of drift between CMA and CLI client initialization?

/** Command description shown in help */
desc: string
/** SDK tracking feature string, e.g. 'entry-list' */
feature: string
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feature, another today I learned. I'm not sure that's going to any datadog dashboard that anyone is reading/depending upon, but good to know about.

Comment thread lib/utils/output.ts
flags: OutputFlags,
options: OutputOptions<TData>
): void {
if (flags.json) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any risk of circular json references? Might be worth it to wrap in a try/catch

Comment thread lib/utils/toon-encoder.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be in our codebase? This seems more like it should be a dependency.

Comment thread lib/utils/toon-encoder.ts
// Core encode generators
// ---------------------------------------------------------------------------

function* encodeJsonValue(
Copy link
Copy Markdown
Contributor

@ethan-ozelius-contentful ethan ozelius (ethan-ozelius-contentful) May 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Frantically searching MDN for what function\* does. Flashback to C++ pointers.

Comment thread lib/utils/exit-codes.ts
export const EXIT_CLIENT_ERROR = 1
export const EXIT_SERVER_ERROR = 2

interface ErrorWithStatus {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add message/details? Need to check if CF-stacks or some other shared types already solved this problem.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would love it if these types could be imported from CF-stacks or a similar source of truth. Even the CMA, which is already a dep.

import { confirmation } from './actions'
import { warning } from './log'

interface ErrorWithName {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question/optional: Add to other standarized errors in exit-codes.ts


for (const item of items) {
const revoked = item?.revokedAt
; (revoked) // no-op to satisfy linter if unused
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yikes.

const entry = await client.entry.get({ entryId: id })
return client.entry.publish({ entryId: id }, entry)
},
dryRunHandler: async (client, argv) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this would require some more significant backend changes, but it would REALLY take the cli to the next level if we could run the content type field validations in a dry run before actually attempting to publish.

A LOT of the time I see customers that are attempting to publish an entry with 422 field validation errors and the script/cmd fails silently 👎

handler: async (client, argv) => {
const id = validateId(argv.id, 'Entry ID')
const entry = await client.entry.get({ entryId: id })
return client.entry.publish({ entryId: id }, entry)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Today the CMA does not check for unpublished references before publishing, but it'd be sweet if it did

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks really good, left a TON of comments, sorry for the wall of text. Thanks for fixing the countless linting issues.

@ethan-ozelius-contentful
Copy link
Copy Markdown
Contributor

One last thing, should we add archive/unarchive for assets?

Screenshot 2026-05-27 at 9 33 42 AM

@ethan-ozelius-contentful
Copy link
Copy Markdown
Contributor

One last, last thing, that should probably be handled in a follow up, but I don't want to forget it: locale based publishing should maybe be added? I don't think it's available to all customers, so that would complicate things, but let's discuss.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants