Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
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
6 changes: 6 additions & 0 deletions .jest/setup.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { cleanUpTestSpaces } from '@contentful/integration-test-utils'
import { initConfig } from '../test/contentful-config'

// Only run integration-test cleanup when credentials are available.
// Unit tests do not need this and should not fail because of missing tokens.
const hasCredentials = !!process.env.CONTENTFUL_INTEGRATION_TEST_CMA_TOKEN

beforeAll(async () => {
if (!hasCredentials) return
await cleanUpTestSpaces({ threshold: 60 * 1000 })
return initConfig()
})

afterAll(async () => {
if (!hasCredentials) return
return await cleanUpTestSpaces({ threshold: 60 * 1000 })
})
72 changes: 72 additions & 0 deletions 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

Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@

- Get started with Contentful with the `init` command.
- Manage spaces - list, create, delete,...
- Manage content types - get, list, create, update, delete, publish, unpublish.
- Manage entries - get, list, create, update, delete, publish, unpublish, archive, unarchive.
- Manage assets - get, list, upload, update, delete, publish, unpublish.
- Export your space to a JSON file.
- Import your space from a JSON file.
- Execute migration scripts written in the [Contentful Migration DSL](https://github.com/contentful/contentful-migration/blob/main/README.md#reference-documentation)
Expand Down Expand Up @@ -45,6 +48,75 @@ contentful --help
contentful space --help
```

## :package: Content Management Commands

The CLI provides commands for managing content types, entries, and assets directly from the command line. All commands support the following standard options:

| Option | Alias | Description |
|--------|-------|-------------|
| `--space-id` | `-s` | ID of the space to use |
| `--environment-id` | `-e` | ID of the environment (default: `master`) |
| `--management-token` | `--mt` | Contentful management API token |
| `--json` | | Output as JSON |
| `--quiet` | `-q` | Output IDs only (for piping) |
| `--agent-mode` | | Output in TOON format for agent consumption |

### Content Type commands

```sh
contentful content-type get --id <content-type-id>
contentful content-type list
contentful content-type create --name "Blog Post" --fields '[{"id":"title","name":"Title","type":"Symbol"}]'
contentful content-type update --id <id> --name "Updated Name"
contentful content-type delete <id>
contentful content-type publish <id>
contentful content-type unpublish <id>
```

### Entry commands

```sh
contentful entry get <entry-id>
contentful entry list [--content-type <content-type-id>]
contentful entry create --content-type <ct-id> --fields '{"title":{"en-US":"Hello"}}'
contentful entry update <id> --fields '{"title":{"en-US":"Updated"}}'
contentful entry delete <id>
contentful entry publish <id>
contentful entry unpublish <id>
contentful entry archive <id>
contentful entry unarchive <id>
```

### Asset commands

```sh
contentful asset get <asset-id>
contentful asset list
contentful asset upload --file ./image.png --title "My Image"
contentful asset update <id> --title "New Title"
contentful asset delete <id>
contentful asset publish <id>
contentful asset unpublish <id>
```

### Dry-run mode

Commands that create, update, or delete resources support `--dry-run` to preview the operation without making changes:

```sh
contentful entry create --content-type blogPost --fields '{"title":{"en-US":"Test"}}' --dry-run
contentful asset upload --file ./photo.jpg --title "Photo" --dry-run
```

### Piping and scripting

Use `--quiet` to output only IDs, making it easy to pipe into other commands:

```sh
# Unpublish all entries of a content type
contentful entry list --content-type blogPost --quiet | xargs -I{} contentful entry unpublish {}
```

## :books: Documentation

More detailed documentation for every command can be found in the [docs section](https://github.com/contentful/contentful-cli/tree/main/docs).
Expand Down
7 changes: 7 additions & 0 deletions lib/cmds/asset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const command = 'asset'
export const desc = 'Manage assets'
export const builder = function (yargs: any) {
return yargs
.commandDir('asset_cmds')
.demandCommand(4, 'Please specify a sub command.')
}
31 changes: 31 additions & 0 deletions lib/cmds/asset_cmds/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {createCommand} from '../../utils/command-factory'
import {validateId} from '../../utils/validators'

const {command, desc, builder, handler} = createCommand({
command: 'delete <id>',
desc: 'Delete an asset',
feature: 'asset-delete',
usage: 'Usage: contentful asset delete <id> [options]',
needsConfirmation: true,
confirmationMessage: 'Are you sure you want to delete this asset? This action cannot be undone.',
supportsDryRun: true,
handler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
const asset = await client.asset.get({assetId: id})
await client.asset.delete({assetId: id})
return {sys: asset.sys}
},
dryRunHandler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
return client.asset.get({assetId: id})
},
tableFormat: (data) => ({
rows: [
['ID', data.sys.id],
['Status', 'deleted']
]
}),
quietExtractor: (data) => [data.sys.id]
})

export {command, desc, builder, handler}
38 changes: 38 additions & 0 deletions lib/cmds/asset_cmds/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {createCommand} from '../../utils/command-factory'
import {firstLocaleValue} from '../../utils/output'
import {validateId} from '../../utils/validators'

function getAssetStatus(asset: any): string {
if (asset.sys.archivedVersion) return 'archived'
if (asset.sys.publishedVersion) {
if (asset.sys.version > asset.sys.publishedVersion + 1) return 'changed'
return 'published'
}
return 'draft'
}

const {command, desc, builder, handler} = createCommand({
command: 'get <id>',
desc: 'Get a single asset',
feature: 'asset-get',
usage: 'Usage: contentful asset get <id> [options]',
handler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
return client.asset.get({assetId: id})
},
tableFormat: (asset) => ({
rows: [
['ID', asset.sys.id],
['Title', firstLocaleValue(asset.fields?.title) || '-'],
['File Name', firstLocaleValue(asset.fields?.file)?.fileName || '-'],
['URL', firstLocaleValue(asset.fields?.file)?.url || '-'],
['Content Type', firstLocaleValue(asset.fields?.file)?.contentType || '-'],
['Version', String(asset.sys.version)],
['Status', getAssetStatus(asset)],
['Updated At', asset.sys.updatedAt || '-']
]
}),
quietExtractor: (asset) => [asset.sys.id]
})

export {command, desc, builder, handler}
58 changes: 58 additions & 0 deletions lib/cmds/asset_cmds/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import {createCommand} from '../../utils/command-factory'
import {firstLocaleValue} from '../../utils/output'

function getAssetStatus(asset: any): string {
if (asset.sys.archivedVersion) return 'archived'
if (asset.sys.publishedVersion) {
if (asset.sys.version > asset.sys.publishedVersion + 1) return 'changed'
return 'published'
}
return 'draft'
}

const {command, desc, builder, handler} = createCommand({
command: 'list',
desc: 'List assets',
feature: 'asset-list',
usage: 'Usage: contentful asset list [options]',
options: {
query: {
type: 'string',
describe: 'Additional CMA query params'
},
limit: {
alias: 'l',
type: 'number',
describe: 'Results per page (default: 100, max: 1000)'
},
skip: {
type: 'number',
describe: 'Offset for pagination'
}
},
handler: async (client, argv) => {
const query: Record<string, any> = {}
if (argv.limit) query.limit = argv.limit
if (argv.skip) query.skip = argv.skip

return client.asset.getMany({query})
},
tableFormat: (data) => ({
head: ['ID', 'Title', 'File Name', 'Status', 'Updated At'],
rows: data.items.map((asset: any) => {
const title = firstLocaleValue(asset.fields?.title) || '-'
const file = firstLocaleValue(asset.fields?.file)
const fileName = file?.fileName || '-'
return [
asset.sys.id,
title,
fileName,
getAssetStatus(asset),
asset.sys.updatedAt || '-'
]
})
}),
quietExtractor: (data) => data.items.map((a: any) => a.sys.id)
})

export {command, desc, builder, handler}
31 changes: 31 additions & 0 deletions lib/cmds/asset_cmds/publish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {createCommand} from '../../utils/command-factory'
import {firstLocaleValue} from '../../utils/output'
import {validateId} from '../../utils/validators'

const {command, desc, builder, handler} = createCommand({
command: 'publish <id>',
desc: 'Publish an asset',
feature: 'asset-publish',
usage: 'Usage: contentful asset publish <id> [options]',
supportsDryRun: true,
handler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
const asset = await client.asset.get({assetId: id})
return client.asset.publish({assetId: id}, asset)
},
dryRunHandler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
return client.asset.get({assetId: id})
},
tableFormat: (asset) => ({
rows: [
['ID', asset.sys.id],
['Title', firstLocaleValue(asset.fields?.title) || '-'],
['Version', String(asset.sys.version)],
['Published Version', String(asset.sys.publishedVersion || '-')]
]
}),
quietExtractor: (asset) => [asset.sys.id]
})

export {command, desc, builder, handler}
30 changes: 30 additions & 0 deletions lib/cmds/asset_cmds/unpublish.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {createCommand} from '../../utils/command-factory'
import {firstLocaleValue} from '../../utils/output'
import {validateId} from '../../utils/validators'

const {command, desc, builder, handler} = createCommand({
command: 'unpublish <id>',
desc: 'Unpublish an asset',
feature: 'asset-unpublish',
usage: 'Usage: contentful asset unpublish <id> [options]',
supportsDryRun: true,
handler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
const asset = await client.asset.get({assetId: id})
return client.asset.unpublish({assetId: id})
},
dryRunHandler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
return client.asset.get({assetId: id})
},
tableFormat: (asset) => ({
rows: [
['ID', asset.sys.id],
['Title', firstLocaleValue(asset.fields?.title) || '-'],
['Version', String(asset.sys.version)]
]
}),
quietExtractor: (asset) => [asset.sys.id]
})

export {command, desc, builder, handler}
66 changes: 66 additions & 0 deletions lib/cmds/asset_cmds/update.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {createCommand} from '../../utils/command-factory'
import {firstLocaleValue} from '../../utils/output'
import {validateId, validateJsonFields, validatePositiveInt} from '../../utils/validators'

const {command, desc, builder, handler} = createCommand({
command: 'update <id>',
desc: 'Update an asset',
feature: 'asset-update',
usage: 'Usage: contentful asset update <id> --fields <json> --version <n> [options]',
supportsDryRun: true,
options: {
fields: {
type: 'string',
describe: 'JSON object of fields to merge into the asset',
demandOption: true
},
version: {
type: 'number',
describe: 'Current version of the asset (required for optimistic locking)',
demandOption: true
}
},
handler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
const fields = validateJsonFields(argv.fields)
const version = validatePositiveInt(argv.version, '--version')

const asset = await client.asset.get({assetId: id})

if (asset.sys.version !== version) {
throw new Error(
`Version conflict: provided version ${version} does not match current version ${asset.sys.version}`
)
}

asset.fields = {...asset.fields, ...fields}
return client.asset.update({assetId: id}, asset)
},
dryRunHandler: async (client, argv) => {
const id = validateId(argv.id, 'Asset ID')
const fields = validateJsonFields(argv.fields)
const version = validatePositiveInt(argv.version, '--version')

const asset = await client.asset.get({assetId: id})

if (asset.sys.version !== version) {
throw new Error(
`Version conflict: provided version ${version} does not match current version ${asset.sys.version}`
)
}

return {...asset, fields: {...asset.fields, ...fields}}
},
tableFormat: (asset) => ({
rows: [
['ID', asset.sys.id],
['Title', firstLocaleValue(asset.fields?.title) || '-'],
['File Name', firstLocaleValue(asset.fields?.file)?.fileName || '-'],
['Version', String(asset.sys.version)],
['Updated At', asset.sys.updatedAt || '-']
]
}),
quietExtractor: (asset) => [asset.sys.id]
})

export {command, desc, builder, handler}
Loading
Loading