Skip to content
Open
Show file tree
Hide file tree
Changes from all 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 .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ module.exports = {
'@typescript-eslint/ban-ts-comment': ['warn']
},
overrides: [
{
files: ['**/*.js'],
rules: {
'@typescript-eslint/no-var-requires': 'off'
}
},
{
files: ['**.test.js'],
rules: {
Expand Down
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
2 changes: 1 addition & 1 deletion lib/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import yargs from 'yargs'
import { log } from './utils/log'
import { copyright } from './utils/copyright'
import { buildContext, getCommand, assertContext } from './utils/middlewares'
const { version } = require('../package.json')
import { version } from '../package.json'

yargs
.usage('\nUsage: contentful <cmd> [args]')
Expand Down
9 changes: 9 additions & 0 deletions lib/cmds/asset.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Argv } from 'yargs'

export const command = 'asset'
export const desc = 'Manage assets'
export const builder = function (yargs: Argv): Argv {
return yargs
.commandDir('asset_cmds')
.demandCommand(4, 'Please specify a sub command.')
}
42 changes: 42 additions & 0 deletions lib/cmds/asset_cmds/delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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]',
examples: [
[
'contentful asset delete 3wtvPBbBjiMKqKGFI0MeCu',
'Delete (prompts for confirmation)'
],
[
'contentful asset delete 3wtvPBbBjiMKqKGFI0MeCu --yes',
'Delete without confirmation'
]
],
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 }
55 changes: 55 additions & 0 deletions lib/cmds/asset_cmds/get.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { createCommand } from '../../utils/command-factory'
import { firstLocaleValue } from '../../utils/output'
import { validateId } from '../../utils/validators'
import type { AssetFileField, AssetLike } from '../../utils/contentful-types'

function getAssetStatus(asset: AssetLike): 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]',
examples: [
[
'contentful asset get 3wtvPBbBjiMKqKGFI0MeCu',
'Get asset details as a table'
],
[
'contentful asset get 3wtvPBbBjiMKqKGFI0MeCu --json',
'Get full asset JSON (includes file URL, metadata)'
]
],
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<AssetFileField>(asset.fields?.file)?.fileName || '-'
],
['URL', firstLocaleValue<AssetFileField>(asset.fields?.file)?.url || '-'],
[
'Content Type',
firstLocaleValue<AssetFileField>(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 }
68 changes: 68 additions & 0 deletions lib/cmds/asset_cmds/list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { createCommand } from '../../utils/command-factory'
import { firstLocaleValue } from '../../utils/output'
import type {
AssetFileField,
AssetLike,
QueryParams
} from '../../utils/contentful-types'

function getAssetStatus(asset: AssetLike): 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]',
examples: [
['contentful asset list', 'List all assets as a table'],
['contentful asset list --json --limit 5', 'Get first 5 assets as JSON'],
['contentful asset list --quiet', 'Output only asset IDs (one per line)']
],
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: QueryParams = {}
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: AssetLike) => {
const title = firstLocaleValue(asset.fields?.title) || '-'
const file = firstLocaleValue<AssetFileField>(asset.fields?.file)
const fileName = file?.fileName || '-'
return [
asset.sys.id,
title,
fileName,
getAssetStatus(asset),
asset.sys.updatedAt || '-'
]
})
}),
quietExtractor: data => data.items.map((a: AssetLike) => 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')
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 }
Loading
Loading