├── .nvmrc ├── .eslintignore ├── packages ├── cli │ ├── empty-tsconfig.json │ ├── bin │ │ ├── dev.cmd │ │ ├── run.cmd │ │ ├── run │ │ └── dev │ ├── src │ │ ├── cma-client-node.ts │ │ ├── index.ts │ │ ├── utils │ │ │ ├── environments-diff │ │ │ │ ├── resources │ │ │ │ │ ├── comments.ts │ │ │ │ │ ├── delete-missing-item-types.ts │ │ │ │ │ ├── update-site.ts │ │ │ │ │ ├── delete-missing-fields-and-fieldsets-in-existing-item-types.ts │ │ │ │ │ ├── manage-workflows.ts │ │ │ │ │ ├── manage-upload-filters.ts │ │ │ │ │ ├── create-new-item-types.ts │ │ │ │ │ ├── update-roles.ts │ │ │ │ │ ├── manage-item-type-filters.ts │ │ │ │ │ ├── finalize-item-types.ts │ │ │ │ │ └── create-new-fields-and-fieldsets.ts │ │ │ │ ├── write │ │ │ │ │ ├── comments.ts │ │ │ │ │ └── get-entity-ids-to-be-recreated.ts │ │ │ │ ├── index.ts │ │ │ │ ├── fetch-schema.ts │ │ │ │ └── utils.ts │ │ │ └── find-nearest-file.ts │ │ └── commands │ │ │ ├── environments │ │ │ ├── primary.ts │ │ │ ├── list.ts │ │ │ ├── destroy.ts │ │ │ ├── promote.ts │ │ │ ├── rename.ts │ │ │ └── fork.ts │ │ │ ├── maintenance │ │ │ ├── off.ts │ │ │ └── on.ts │ │ │ ├── profile │ │ │ ├── remove.ts │ │ │ └── set.ts │ │ │ ├── plugins │ │ │ └── available.ts │ │ │ └── schema │ │ │ └── generate.ts │ ├── test │ │ ├── tsconfig.json │ │ ├── helpers │ │ │ └── init.js │ │ └── commands │ │ │ └── help.test.ts │ ├── .mocharc.json │ ├── tsconfig.json │ ├── tsconfig.tsbuildinfo │ └── package.json ├── cli-plugin-wordpress │ ├── .gitignore │ ├── bin │ │ ├── dev.cmd │ │ ├── run.cmd │ │ ├── run │ │ └── dev │ ├── src │ │ ├── index.ts │ │ ├── utils │ │ │ ├── escape-string-regexp.ts │ │ │ ├── build-wp-client.ts │ │ │ └── build-fields.ts │ │ ├── import │ │ │ ├── destroy-dato-schema.ts │ │ │ ├── import-wp-tags.ts │ │ │ ├── import-wp-authors.ts │ │ │ ├── import-wp-assets.ts │ │ │ ├── import-wp-categories.ts │ │ │ ├── base-step.ts │ │ │ ├── import-wp-pages.ts │ │ │ └── import-wp-articles.ts │ │ └── commands │ │ │ └── wordpress │ │ │ └── import.ts │ ├── wp_test_data │ │ ├── 05 │ │ │ ├── 3.png │ │ │ ├── 8.jpg │ │ │ └── beach.mp4 │ │ └── uploads │ │ │ └── 2022 │ │ │ └── 05 │ │ │ ├── 3.png │ │ │ ├── 8.jpg │ │ │ ├── beach.mp4 │ │ │ ├── 3-150x150.png │ │ │ ├── 3-300x270.png │ │ │ ├── 8-150x150.jpg │ │ │ └── 8-300x300.jpg │ ├── test │ │ ├── tsconfig.json │ │ ├── helpers │ │ │ └── init.js │ │ └── commands │ │ │ └── import │ │ │ └── index.test.ts │ ├── tsconfig.json │ ├── tsconfig.tsbuildinfo │ ├── docker-compose.yml │ ├── package.json │ └── README.md ├── cli-utils │ ├── src │ │ ├── oclif.ts │ │ ├── cma-client-node.ts │ │ ├── index.ts │ │ ├── base-command.ts │ │ ├── dato-config-command.ts │ │ ├── config.ts │ │ ├── dato-profile-config-command.ts │ │ └── cma-client-command.ts │ ├── README.md │ ├── tsconfig.json │ └── package.json └── cli-plugin-contentful │ ├── bin │ ├── dev.cmd │ ├── run.cmd │ ├── run │ └── dev │ ├── src │ ├── index.ts │ ├── utils │ │ ├── is-array-with-at-least-one-element.ts │ │ ├── getAll.ts │ │ └── build-contentful-client.ts │ └── import │ │ ├── import-models.ts │ │ ├── add-validations.ts │ │ ├── base-step.ts │ │ ├── import-assets.ts │ │ ├── import-fields.ts │ │ ├── destroy-dato-schema.ts │ │ └── import-records.ts │ ├── tsconfig.json │ ├── tsconfig.tsbuildinfo │ ├── package.json │ └── README.md ├── lerna.json ├── .husky └── pre-commit ├── .vscode └── settings.json ├── .editorconfig ├── .gitignore ├── biome.json ├── LICENSE ├── package.json ├── CLAUDE.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 20 2 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/lib 2 | -------------------------------------------------------------------------------- /packages/cli/empty-tsconfig.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/.gitignore: -------------------------------------------------------------------------------- 1 | src/test.mjs 2 | -------------------------------------------------------------------------------- /packages/cli/bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /packages/cli-utils/src/oclif.ts: -------------------------------------------------------------------------------- 1 | export * from '@oclif/core'; 2 | -------------------------------------------------------------------------------- /packages/cli/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from '@oclif/core'; 2 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/bin/dev.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\dev" %* -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from '@oclif/core'; 2 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/bin/run.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | node "%~dp0\run" %* 4 | -------------------------------------------------------------------------------- /packages/cli-utils/src/cma-client-node.ts: -------------------------------------------------------------------------------- 1 | export * from '@datocms/cma-client-node'; 2 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "version": "3.1.16" 6 | } 7 | -------------------------------------------------------------------------------- /packages/cli/src/cma-client-node.ts: -------------------------------------------------------------------------------- 1 | export * from '@datocms/cli-utils/lib/cma-client-node'; 2 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | ./node_modules/.bin/lint-staged 5 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/05/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/05/3.png -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/05/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/05/8.jpg -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "biome.lsp.bin": "./node_modules/@biomejs/biome/bin/biome" 4 | } 5 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/05/beach.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/05/beach.mp4 -------------------------------------------------------------------------------- /packages/cli/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/3.png -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/8.jpg -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/beach.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/beach.mp4 -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { run } from '@oclif/core'; 2 | export { 3 | generateSchemaTypes, 4 | generateSchemaTypesForMigration, 5 | } from './utils/schema-types-generator'; 6 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig", 3 | "compilerOptions": { 4 | "noEmit": true 5 | }, 6 | "references": [{ "path": ".." }] 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/3-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/3-150x150.png -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/3-300x270.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/3-300x270.png -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/8-150x150.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/8-150x150.jpg -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/8-300x300.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/datocms/cli/HEAD/packages/cli-plugin-wordpress/wp_test_data/uploads/2022/05/8-300x300.jpg -------------------------------------------------------------------------------- /packages/cli/.mocharc.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": ["test/helpers/init.js", "ts-node/register"], 3 | "watch-extensions": ["ts"], 4 | "recursive": true, 5 | "reporter": "spec", 6 | "timeout": 60000 7 | } 8 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core'); 4 | 5 | oclif 6 | .run() 7 | .then(require('@oclif/core/flush')) 8 | .catch(require('@oclif/core/handle')); 9 | -------------------------------------------------------------------------------- /packages/cli-utils/README.md: -------------------------------------------------------------------------------- 1 | # `@datocms/cli-utils` 2 | 3 | > TODO: description 4 | 5 | ## Usage 6 | 7 | ``` 8 | const cliUtils = require('@datocms/cli-utils'); 9 | 10 | // TODO: DEMONSTRATE API! 11 | ``` 12 | -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/resources/comments.ts: -------------------------------------------------------------------------------- 1 | import type { Comment } from '../types'; 2 | 3 | export function buildComment(message: string): Comment { 4 | return { type: 'comment', message }; 5 | } 6 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core'); 4 | 5 | oclif 6 | .run() 7 | .then(require('@oclif/core/flush')) 8 | .catch(require('@oclif/core/handle')); 9 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/src/utils/is-array-with-at-least-one-element.ts: -------------------------------------------------------------------------------- 1 | export default function isArrayWithAtLeastOneElement( 2 | something: T[], 3 | ): something is [T, ...T[]] { 4 | return something.length > 0; 5 | } 6 | -------------------------------------------------------------------------------- /packages/cli/bin/run: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | // eslint-disable-next-line unicorn/prefer-top-level-await 4 | (async () => { 5 | const oclif = await import("@oclif/core"); 6 | await oclif.execute({ dir: __dirname }); 7 | })(); 8 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.md] 11 | trim_trailing_whitespace = false 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *-debug.log 2 | *-error.log 3 | /.nyc_output 4 | /tmp 5 | /yarn.lock 6 | lib 7 | node_modules 8 | oclif.manifest.json 9 | datocms.config.json 10 | migrations 11 | .env 12 | **/.DS_Store/ 13 | **/api-calls/*.log 14 | **/api-calls.log -------------------------------------------------------------------------------- /packages/cli/test/helpers/init.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json'); 3 | process.env.NODE_ENV = 'development'; 4 | 5 | global.oclif = global.oclif || {}; 6 | global.oclif.columns = 80; 7 | -------------------------------------------------------------------------------- /packages/cli/test/commands/help.test.ts: -------------------------------------------------------------------------------- 1 | import { runCommand } from '@oclif/test'; 2 | import { expect } from 'chai'; 3 | 4 | describe('datocms', async () => { 5 | const { stdout } = await runCommand('help'); 6 | expect(stdout).to.contain('plugins'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/test/helpers/init.js: -------------------------------------------------------------------------------- 1 | const path = require('node:path'); 2 | process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json'); 3 | process.env.NODE_ENV = 'development'; 4 | 5 | global.oclif = global.oclif || {}; 6 | global.oclif.columns = 80; 7 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "Node16", 6 | "outDir": "lib", 7 | "rootDir": "src", 8 | "strict": true, 9 | "target": "es2019", 10 | "skipLibCheck": true, 11 | "moduleResolution": "node16" 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli-utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "outDir": "lib", 7 | "rootDir": "src", 8 | "strict": true, 9 | "target": "es2019", 10 | "skipLibCheck": true, 11 | "esModuleInterop": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "esModuleInterop": true, 7 | "outDir": "lib", 8 | "rootDir": "src", 9 | "strict": true, 10 | "target": "es2019", 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "importHelpers": true, 5 | "module": "commonjs", 6 | "esModuleInterop": true, 7 | "outDir": "lib", 8 | "rootDir": "src", 9 | "strict": true, 10 | "target": "es2019", 11 | "skipLibCheck": true 12 | }, 13 | "include": ["src/**/*"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/write/comments.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import type { Comment } from '../types'; 3 | 4 | export function buildCommentNode(comment: Comment): ts.Node { 5 | return ts.factory.createCallExpression( 6 | ts.factory.createIdentifier('console.log'), 7 | undefined, 8 | [ts.factory.createStringLiteral(comment.message)], 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/utils/escape-string-regexp.ts: -------------------------------------------------------------------------------- 1 | export default function convertToRegExp(string: string): string { 2 | // Escape characters with special meaning either inside or outside character sets. 3 | // Use a simple backslash escape when it’s always valid, and a `\xnn` escape when the simpler form would be disallowed by Unicode patterns’ stricter grammar. 4 | return string.replace(/[$()*+.?[\\\]^{|}]/g, '\\$&').replace(/-/g, '\\x2d'); 5 | } 6 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/index.ts","./src/commands/wordpress/import.ts","./src/import/base-step.ts","./src/import/destroy-dato-schema.ts","./src/import/import-wp-articles.ts","./src/import/import-wp-assets.ts","./src/import/import-wp-authors.ts","./src/import/import-wp-categories.ts","./src/import/import-wp-pages.ts","./src/import/import-wp-tags.ts","./src/utils/build-fields.ts","./src/utils/build-wp-client.ts","./src/utils/escape-string-regexp.ts"],"version":"5.7.2"} -------------------------------------------------------------------------------- /packages/cli-utils/src/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv'; 2 | 3 | // Load .env.local and .env (local values take precedence) 4 | dotenv.config({ path: ['.env.local', '.env'], quiet: true }); 5 | 6 | export * as CmaClient from '@datocms/cma-client-node'; 7 | export * as oclif from '@oclif/core'; 8 | export * from './base-command'; 9 | export * from './cma-client-command'; 10 | export * from './config'; 11 | export * from './dato-config-command'; 12 | export * from './dato-profile-config-command'; 13 | -------------------------------------------------------------------------------- /packages/cli/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require("@oclif/core"); 4 | 5 | const path = require("path"); 6 | const project = path.join(__dirname, "..", "tsconfig.json"); 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = "development"; 10 | 11 | require("ts-node").register({ project }); 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle); 18 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core'); 4 | 5 | const path = require('path'); 6 | const project = path.join(__dirname, '..', 'tsconfig.json'); 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = 'development'; 10 | 11 | require('ts-node').register({ project }); 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle); 18 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/bin/dev: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const oclif = require('@oclif/core'); 4 | 5 | const path = require('path'); 6 | const project = path.join(__dirname, '..', 'tsconfig.json'); 7 | 8 | // In dev mode -> use ts-node and dev plugins 9 | process.env.NODE_ENV = 'development'; 10 | 11 | require('ts-node').register({ project }); 12 | 13 | // In dev mode, always show stack traces 14 | oclif.settings.debug = true; 15 | 16 | // Start the CLI 17 | oclif.run().then(oclif.flush).catch(oclif.Errors.handle); 18 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/index.ts","./src/commands/contentful/import.ts","./src/import/add-validations.ts","./src/import/base-step.ts","./src/import/destroy-dato-schema.ts","./src/import/import-assets.ts","./src/import/import-fields.ts","./src/import/import-models.ts","./src/import/import-records.ts","./src/utils/build-contentful-client.ts","./src/utils/getall.ts","./src/utils/is-array-with-at-least-one-element.ts","./src/utils/item-create-helpers.ts","./src/utils/item-type-create-helpers.ts"],"version":"5.7.2"} -------------------------------------------------------------------------------- /packages/cli/src/commands/environments/primary.ts: -------------------------------------------------------------------------------- 1 | import { type CmaClient, CmaClientCommand } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static description = 'Returns the name the primary environment of a project'; 5 | 6 | async run(): Promise { 7 | const environments = await this.client.environments.list(); 8 | const primary = environments.find((e) => e.meta.primary)!; 9 | 10 | this.log(primary.id); 11 | 12 | return primary; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/cli/src/commands/environments/list.ts: -------------------------------------------------------------------------------- 1 | import { type CmaClient, CmaClientCommand } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static aliases = ['environments:index', 'environments:list']; 5 | 6 | static description = 'Lists primary/sandbox environments of a project'; 7 | 8 | async run(): Promise { 9 | const environments = await this.client.environments.list(); 10 | 11 | this.printTable(environments, ['id', 'meta.primary']); 12 | 13 | return environments; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/cli/src/commands/maintenance/off.ts: -------------------------------------------------------------------------------- 1 | import { type CmaClient, CmaClientCommand } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static description = 'Take a project out of maintenance mode'; 5 | 6 | async run(): Promise { 7 | this.startSpinner('Deactivating maintenance mode'); 8 | 9 | try { 10 | const result = await this.client.maintenanceMode.deactivate(); 11 | this.stopSpinner(); 12 | 13 | return result; 14 | } catch (e) { 15 | this.stopSpinnerWithFailure(); 16 | 17 | throw e; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/cli/src/utils/find-nearest-file.ts: -------------------------------------------------------------------------------- 1 | import { access } from 'node:fs/promises'; 2 | import { dirname, join, resolve } from 'node:path'; 3 | 4 | export async function findNearestFile( 5 | fileName: string, 6 | directoryPath: string = resolve(), 7 | ): Promise { 8 | try { 9 | const path = join(directoryPath, fileName); 10 | await access(path); 11 | return path; 12 | } catch { 13 | const parentDirectoryPath = dirname(directoryPath); 14 | 15 | if (parentDirectoryPath === directoryPath) { 16 | throw new Error(`No "${fileName}" file found`); 17 | } 18 | 19 | return findNearestFile(fileName, parentDirectoryPath); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/src/utils/getAll.ts: -------------------------------------------------------------------------------- 1 | import type { BasicQueryOptions, Collection } from 'contentful-management'; 2 | 3 | export async function getAll( 4 | fn: ( 5 | query: BasicQueryOptions, 6 | ) => Promise>, 7 | ): Promise { 8 | let allResources: Resource[] = []; 9 | 10 | const startRequest = await fn({ limit: 1, skip: allResources.length }); 11 | const { total } = startRequest; 12 | 13 | while (allResources.length < total) { 14 | const result = await fn({ limit: 100, skip: allResources.length }); 15 | allResources = [...allResources, ...result.items]; 16 | } 17 | 18 | return allResources; 19 | } 20 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.6.4/schema.json", 3 | "files": { 4 | "ignore": ["*/lib/*.js", "*/lib/*.d.ts", "*/empty-tsconfig.json"] 5 | }, 6 | "linter": { 7 | "enabled": true, 8 | "rules": { 9 | "recommended": true, 10 | "correctness": { 11 | "noUnusedVariables": "warn" 12 | }, 13 | "complexity": { 14 | "noForEach": "off" 15 | }, 16 | "style": { 17 | "noNonNullAssertion": "off" 18 | }, 19 | "suspicious": { 20 | "noExplicitAny": "off" 21 | } 22 | } 23 | }, 24 | "formatter": { 25 | "enabled": true, 26 | "indentStyle": "space" 27 | }, 28 | "javascript": { 29 | "formatter": { 30 | "quoteStyle": "single" 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | db: 5 | platform: linux/amd64 6 | image: mysql:8.0 7 | environment: 8 | MYSQL_ROOT_PASSWORD: somewordpress 9 | MYSQL_DATABASE: wordpress 10 | MYSQL_USER: wordpress 11 | MYSQL_PASSWORD: wordpress 12 | ports: 13 | - '3306:3306' 14 | expose: 15 | - '3306' 16 | volumes: 17 | - './wp_test_data/mysql/:/docker-entrypoint-initdb.d' 18 | wordpress: 19 | platform: linux/amd64 20 | depends_on: 21 | - db 22 | image: wordpress:latest 23 | ports: 24 | - '8081:80' 25 | restart: always 26 | environment: 27 | WORDPRESS_DB_HOST: db:3306 28 | WORDPRESS_DB_USER: wordpress 29 | WORDPRESS_DB_PASSWORD: wordpress 30 | WORDPRESS_DB_NAME: wordpress 31 | WORDPRESS_DEBUG: 1 32 | WP_DEBUG_LOG: 1 33 | volumes: 34 | - './wp_test_data/uploads/:/var/www/html/wp-content/uploads' 35 | -------------------------------------------------------------------------------- /packages/cli-utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datocms/cli-utils", 3 | "version": "3.1.16", 4 | "description": "Utils for DatoCMS CLI", 5 | "author": "Stefano Verna ", 6 | "homepage": "https://github.com/datocms/cli", 7 | "license": "MIT", 8 | "main": "lib/index.js", 9 | "types": "lib/index.d.ts", 10 | "directories": { 11 | "lib": "lib" 12 | }, 13 | "files": [ 14 | "lib", 15 | "src" 16 | ], 17 | "publishConfig": { 18 | "access": "public" 19 | }, 20 | "dependencies": { 21 | "@datocms/cma-client-node": "^5.1.13", 22 | "@oclif/core": "^4", 23 | "@whatwg-node/fetch": "^0.10.10", 24 | "chalk": "^4", 25 | "dotenv": "^17.2.3", 26 | "lodash": "^4.17.21", 27 | "serialize-error": "^8", 28 | "tty-table": "^4.2.3" 29 | }, 30 | "scripts": { 31 | "build": "tsc", 32 | "prebuild": "rimraf lib" 33 | }, 34 | "gitHead": "09f72bd8742b2933271407559d9fbcc235662876", 35 | "devDependencies": { 36 | "@types/lodash": "^4.17.20" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/src/commands/environments/destroy.ts: -------------------------------------------------------------------------------- 1 | import { CmaClient, CmaClientCommand, oclif } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static description = 'Destroys a sandbox environment'; 5 | 6 | static args = { 7 | ENVIRONMENT_ID: oclif.Args.string({ 8 | description: 'The environment to destroy', 9 | required: true, 10 | }), 11 | }; 12 | 13 | async run(): Promise { 14 | const { 15 | args: { ENVIRONMENT_ID: envId }, 16 | } = await this.parse(Command); 17 | 18 | this.startSpinner(`Destroying environment "${envId}"`); 19 | 20 | try { 21 | const result = await this.client.environments.destroy(envId); 22 | 23 | this.stopSpinner(); 24 | 25 | return result; 26 | } catch (e) { 27 | this.stopSpinnerWithFailure(); 28 | 29 | if (e instanceof CmaClient.ApiError && e.findError('NOT_FOUND')) { 30 | this.error(`An environment called "${envId}" does not exist`); 31 | } 32 | 33 | throw e; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/src/commands/environments/promote.ts: -------------------------------------------------------------------------------- 1 | import { CmaClient, CmaClientCommand, oclif } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static description = 'Promotes a sandbox environment to primary'; 5 | 6 | static args = { 7 | ENVIRONMENT_ID: oclif.Args.string({ 8 | description: 'The environment to promote', 9 | required: true, 10 | }), 11 | }; 12 | 13 | async run(): Promise { 14 | const { 15 | args: { ENVIRONMENT_ID: envId }, 16 | } = await this.parse(Command); 17 | 18 | this.startSpinner(`Promoting environment "${envId}"`); 19 | 20 | try { 21 | const result = await this.client.environments.promote(envId); 22 | 23 | this.stopSpinner(); 24 | 25 | return result; 26 | } catch (e) { 27 | this.stopSpinnerWithFailure(); 28 | 29 | if (e instanceof CmaClient.ApiError && e.findError('NOT_FOUND')) { 30 | this.error(`An environment called "${envId}" does not exist!`); 31 | } 32 | 33 | throw e; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/cli/src/commands/environments/rename.ts: -------------------------------------------------------------------------------- 1 | import { type CmaClient, CmaClientCommand, oclif } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static description = 'Renames an environment'; 5 | 6 | static args = { 7 | ENVIRONMENT_ID: oclif.Args.string({ 8 | description: 'The environment to rename', 9 | required: true, 10 | }), 11 | NEW_ENVIRONMENT_ID: oclif.Args.string({ 12 | description: 'The new environment ID', 13 | required: true, 14 | }), 15 | }; 16 | 17 | async run(): Promise { 18 | const { 19 | args: { ENVIRONMENT_ID: oldId, NEW_ENVIRONMENT_ID: newId }, 20 | } = await this.parse(Command); 21 | 22 | this.startSpinner(`Renaming environment "${oldId}" -> "${newId}"`); 23 | 24 | try { 25 | const result = await this.client.environments.rename(oldId, { 26 | id: newId, 27 | }); 28 | 29 | this.stopSpinner(); 30 | 31 | return result; 32 | } catch (e) { 33 | this.stopSpinnerWithFailure(); 34 | 35 | throw e; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Dato SRL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "scripts": { 5 | "prepare": "husky install", 6 | "format": "biome check packages --apply-unsafe && biome format --write packages", 7 | "lint": "biome ci packages", 8 | "build": "lerna run build", 9 | "publish": "lerna run test && npm run build && lerna publish", 10 | "publish-next": "npm run build && lerna publish --dist-tag next" 11 | }, 12 | "devDependencies": { 13 | "@biomejs/biome": "1.6.4", 14 | "@oclif/test": "^4", 15 | "@types/chai": "^4", 16 | "@types/mocha": "^10", 17 | "@types/node": "^22.10.2", 18 | "chai": "^4", 19 | "globby": "^11", 20 | "husky": "^7.0.4", 21 | "lerna": "^4.0.0", 22 | "lint-staged": "^13.1.0", 23 | "mocha": "^10", 24 | "oclif": "^4", 25 | "rimraf": "^3.0.2", 26 | "ts-node": "^10.2.1", 27 | "tslib": "^2.3.1", 28 | "typescript": "5.7.2" 29 | }, 30 | "version": "0.0.1", 31 | "lint-staged": { 32 | "packages/**/*.{js,jsx,ts,tsx}": [ 33 | "biome ci" 34 | ] 35 | }, 36 | "files": [], 37 | "husky": { 38 | "hooks": { 39 | "pre-commit": "./node_modules/.bin/lint-staged" 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/import/destroy-dato-schema.ts: -------------------------------------------------------------------------------- 1 | import type { ListrRendererFactory, ListrTaskWrapper } from 'listr2'; 2 | import type { Context } from '../commands/wordpress/import'; 3 | import BaseStep from './base-step'; 4 | 5 | export default class DestroyDatoSchema extends BaseStep { 6 | async task( 7 | task: ListrTaskWrapper, 8 | ): Promise { 9 | const wpItemTypes = await this.client.itemTypes.list(); 10 | 11 | const itemTypesToDestroy = wpItemTypes.filter((it) => 12 | ['wp_article', 'wp_page', 'wp_author', 'wp_category', 'wp_tag'].includes( 13 | it.api_key, 14 | ), 15 | ); 16 | 17 | for (const itemType of itemTypesToDestroy) { 18 | const confirmed = 19 | this.autoconfirm || 20 | (await task.prompt({ 21 | type: 'Confirm', 22 | message: `A model named "${itemType.api_key}" already exist in the project. Confirm that you want to destroy it?`, 23 | })); 24 | 25 | if (!confirmed) { 26 | throw new Error('Import interrupted by user request'); 27 | } 28 | 29 | await this.client.itemTypes.destroy(itemType); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/utils/build-wp-client.ts: -------------------------------------------------------------------------------- 1 | import WPAPI from 'wpapi'; 2 | 3 | type WpClientType = { 4 | /** A WP-API Basic HTTP Authentication username */ 5 | username: string | undefined; 6 | /** A WP-API Basic HTTP Authentication password */ 7 | password: string | undefined; 8 | /** The URI for a WP-API endpoint */ 9 | apiUrl: string | undefined; 10 | /** A URL within a REST API-enabled WordPress website */ 11 | discoverUrl: string | undefined; 12 | }; 13 | 14 | export async function buildWpClient({ 15 | username, 16 | password, 17 | apiUrl, 18 | discoverUrl, 19 | }: WpClientType): Promise { 20 | if (!(username && password)) { 21 | throw new Error('You need to provide email and password to authenticate!'); 22 | } 23 | 24 | let wpClient: WPAPI; 25 | 26 | if (apiUrl) { 27 | wpClient = new WPAPI({ 28 | endpoint: apiUrl, 29 | username, 30 | password, 31 | }); 32 | } else if (discoverUrl) { 33 | wpClient = await WPAPI.discover(discoverUrl); 34 | await wpClient.auth({ username, password }); 35 | } else { 36 | throw new Error('You need to provide the URl to your WordPress install!'); 37 | } 38 | 39 | return wpClient; 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/commands/profile/remove.ts: -------------------------------------------------------------------------------- 1 | import { DatoConfigCommand, oclif } from '@datocms/cli-utils'; 2 | export default class Command extends DatoConfigCommand { 3 | static description = 'Remove a profile from DatoCMS config file'; 4 | 5 | static args = { 6 | PROFILE_ID: oclif.Args.string({ 7 | description: 'The name of the profile', 8 | required: true, 9 | }), 10 | }; 11 | 12 | async run(): Promise { 13 | const { 14 | args: { PROFILE_ID: profileId }, 15 | } = await this.parse(Command); 16 | 17 | if (!this.datoConfig) { 18 | this.log( 19 | `Config file not present in "${this.datoConfigRelativePath}", skipping operation`, 20 | ); 21 | return; 22 | } 23 | 24 | if (!(profileId in this.datoConfig.profiles)) { 25 | this.log( 26 | `Config file does not contain profile "${profileId}", skipping operation`, 27 | ); 28 | return; 29 | } 30 | 31 | await this.saveDatoConfig({ 32 | ...this.datoConfig, 33 | profiles: Object.fromEntries( 34 | Object.entries(this.datoConfig?.profiles || {}).filter( 35 | ([key]) => key !== profileId, 36 | ), 37 | ), 38 | }); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/commands/plugins/available.ts: -------------------------------------------------------------------------------- 1 | import { BaseCommand } from '@datocms/cli-utils'; 2 | 3 | type AvailablePlugin = { 4 | package: string; 5 | description: string; 6 | }; 7 | 8 | type MaybeInstalledPlugin = AvailablePlugin & { 9 | installed: boolean; 10 | }; 11 | 12 | export default class Command extends BaseCommand { 13 | static description = 'Lists official DatoCMS CLI plugins'; 14 | 15 | static availablePlugins: AvailablePlugin[] = [ 16 | { 17 | package: '@datocms/cli-plugin-wordpress', 18 | description: 'Import a WordPress site into DatoCMS', 19 | }, 20 | { 21 | package: '@datocms/cli-plugin-contentful', 22 | description: 'Import a Contentful site into DatoCMS', 23 | }, 24 | ]; 25 | 26 | async run(): Promise { 27 | const installedPlugins = this.config.plugins; 28 | 29 | const maybeInstalled = Command.availablePlugins.map((availablePlugin) => ({ 30 | ...availablePlugin, 31 | installed: Array.from(installedPlugins.values()).some( 32 | (installedPlugin) => installedPlugin.name === availablePlugin.package, 33 | ), 34 | })); 35 | 36 | this.printTable(maybeInstalled, ['package', 'description', 'installed']); 37 | 38 | return maybeInstalled; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/cli/src/commands/maintenance/on.ts: -------------------------------------------------------------------------------- 1 | import { CmaClient, CmaClientCommand, oclif } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static description = 'Put a project in maintenance mode'; 5 | 6 | static flags = { 7 | force: oclif.Flags.boolean({ 8 | description: 9 | 'Forces the activation of maintenance mode even there are users currently editing records', 10 | }), 11 | }; 12 | 13 | async run(): Promise { 14 | const { flags } = await this.parse(Command); 15 | 16 | this.startSpinner('Activating maintenance mode'); 17 | 18 | try { 19 | const result = await this.client.maintenanceMode.activate({ 20 | force: flags.force, 21 | }); 22 | 23 | this.stopSpinner(); 24 | 25 | return result; 26 | } catch (e) { 27 | this.stopSpinnerWithFailure(); 28 | 29 | if ( 30 | e instanceof CmaClient.ApiError && 31 | e.findError('ACTIVE_EDITING_SESSIONS') 32 | ) { 33 | this.error( 34 | 'Cannot activate maintenance mode as some users are currently editing records', 35 | { 36 | suggestions: ['To proceed anyway, use the --force flag'], 37 | }, 38 | ); 39 | } 40 | 41 | throw e; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/resources/delete-missing-item-types.ts: -------------------------------------------------------------------------------- 1 | import { difference } from 'lodash'; 2 | import type { Command, ItemTypeInfo, Schema } from '../types'; 3 | import { buildItemTypeTitle } from '../utils'; 4 | import { buildComment } from './comments'; 5 | 6 | export function buildDestroyItemTypeClientCommand( 7 | itemTypeSchema: ItemTypeInfo, 8 | ): Command[] { 9 | return [ 10 | buildComment(`Delete ${buildItemTypeTitle(itemTypeSchema.entity)}`), 11 | { 12 | type: 'apiCallClientCommand', 13 | call: 'client.itemTypes.destroy', 14 | arguments: [itemTypeSchema.entity.id, { skip_menu_items_deletion: true }], 15 | }, 16 | ]; 17 | } 18 | 19 | export function deleteMissingItemTypes( 20 | newSchema: Schema, 21 | oldSchema: Schema, 22 | ): Command[] { 23 | const newItemTypeIds = Object.keys(newSchema.itemTypesById); 24 | const oldItemTypeIds = Object.keys(oldSchema.itemTypesById); 25 | 26 | const destroyedItemTypeIds = difference(oldItemTypeIds, newItemTypeIds); 27 | 28 | if (destroyedItemTypeIds.length === 0) { 29 | return []; 30 | } 31 | 32 | return [ 33 | buildComment('Destroy models/block models'), 34 | ...destroyedItemTypeIds.flatMap((itemTypeId) => 35 | buildDestroyItemTypeClientCommand(oldSchema.itemTypesById[itemTypeId]), 36 | ), 37 | ]; 38 | } 39 | -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/resources/update-site.ts: -------------------------------------------------------------------------------- 1 | import type { CmaClient } from '@datocms/cli-utils'; 2 | import { isEqual, omit, pick } from 'lodash'; 3 | import type { Command, Schema } from '../types'; 4 | import { buildComment } from './comments'; 5 | 6 | export function updateSite(newSchema: Schema, oldSchema: Schema): Command[] { 7 | const newSite = newSchema.siteEntity; 8 | const oldSite = oldSchema.siteEntity; 9 | 10 | const attributesToUpdate = omit( 11 | pick( 12 | newSite.attributes, 13 | ( 14 | Object.keys(newSite.attributes) as Array< 15 | keyof CmaClient.RawApiTypes.SiteAttributes 16 | > 17 | ).filter( 18 | (attribute) => 19 | !isEqual( 20 | oldSite.attributes[attribute], 21 | newSite.attributes[attribute], 22 | ), 23 | ), 24 | ), 25 | 'last_data_change_at', 26 | 'global_seo', 27 | 'theme', 28 | ); 29 | 30 | if (Object.keys(attributesToUpdate).length === 0) { 31 | return []; 32 | } 33 | 34 | return [ 35 | buildComment(`Update environment's settings`), 36 | { 37 | type: 'apiCallClientCommand', 38 | call: 'client.site.update', 39 | arguments: [ 40 | { 41 | data: { 42 | type: 'site', 43 | id: newSite.id, 44 | attributes: attributesToUpdate, 45 | }, 46 | }, 47 | ], 48 | }, 49 | ]; 50 | } 51 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/src/import/import-models.ts: -------------------------------------------------------------------------------- 1 | import type { ListrRendererFactory, ListrTaskWrapper } from 'listr2'; 2 | import type { Context } from '../commands/contentful/import'; 3 | import { toItemTypeApiKey } from '../utils/item-type-create-helpers'; 4 | import BaseStep from './base-step'; 5 | 6 | const importModelsLog = 'Import models from Contentful'; 7 | 8 | export default class ImportModels extends BaseStep { 9 | async task( 10 | ctx: Context, 11 | task: ListrTaskWrapper, 12 | ): Promise { 13 | ctx.contentTypeIdToDatoItemType = {}; 14 | 15 | await this.runConcurrentlyOver( 16 | task, 17 | importModelsLog, 18 | ctx.contentTypes, 19 | (contentType) => `Import ${contentType.name} model`, 20 | async (contentType) => { 21 | const contKey = contentType.sys.id; 22 | const itemTypeApiKey = toItemTypeApiKey(contKey); 23 | 24 | const itemTypeAttributes = { 25 | api_key: itemTypeApiKey, 26 | name: contentType.name, 27 | modular_block: false, 28 | ordering_direction: null, 29 | ordering_field: null, 30 | singleton: false, 31 | sortable: false, 32 | tree: false, 33 | draft_mode_active: true, 34 | // Contentful has this option by default 35 | all_locales_required: false, 36 | }; 37 | 38 | const itemType = await this.client.itemTypes.create(itemTypeAttributes); 39 | 40 | ctx.contentTypeIdToDatoItemType[contentType.sys.id] = itemType; 41 | }, 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/utils/build-fields.ts: -------------------------------------------------------------------------------- 1 | import type { CmaClient } from '@datocms/cli-utils'; 2 | import { titleize } from 'inflected'; 3 | 4 | export async function createSlugField( 5 | client: CmaClient.Client, 6 | itemType: CmaClient.ApiTypes.ItemType, 7 | titleFieldId: string, 8 | ): Promise { 9 | return client.fields.create(itemType.id, { 10 | field_type: 'slug', 11 | api_key: 'slug', 12 | label: 'Slug', 13 | validators: { slug_title_field: { title_field_id: titleFieldId } }, 14 | }); 15 | } 16 | 17 | export async function createStringField( 18 | client: CmaClient.Client, 19 | itemType: CmaClient.ApiTypes.ItemType, 20 | apiKey: string, 21 | ): Promise { 22 | return client.fields.create(itemType.id, { 23 | field_type: 'string', 24 | api_key: apiKey, 25 | label: titleize(apiKey), 26 | }); 27 | } 28 | 29 | export async function createTextField( 30 | client: CmaClient.Client, 31 | itemType: CmaClient.ApiTypes.ItemType, 32 | apiKey: string, 33 | ): Promise { 34 | return client.fields.create(itemType.id, { 35 | api_key: apiKey, 36 | label: titleize(apiKey), 37 | field_type: 'text', 38 | appearance: { 39 | editor: 'wysiwyg', 40 | parameters: { 41 | toolbar: [ 42 | 'format', 43 | 'bold', 44 | 'italic', 45 | 'strikethrough', 46 | 'ordered_list', 47 | 'unordered_list', 48 | 'quote', 49 | 'table', 50 | 'link', 51 | 'image', 52 | 'show_source', 53 | ], 54 | }, 55 | addons: [], 56 | }, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /packages/cli-utils/src/base-command.ts: -------------------------------------------------------------------------------- 1 | import { Command, ux } from '@oclif/core'; 2 | import { get } from 'lodash'; 3 | import { serializeError } from 'serialize-error'; 4 | import TtyTable from 'tty-table'; 5 | 6 | export abstract class BaseCommand extends Command { 7 | static flags = {}; 8 | static baseFlags = {}; 9 | 10 | static enableJsonFlag = true; 11 | 12 | protected toErrorJson(err: any): any { 13 | return { error: { message: err.message, ...err } }; 14 | } 15 | 16 | protected startSpinner( 17 | action: string, 18 | status?: string, 19 | opts?: { 20 | stdout?: boolean; 21 | }, 22 | ): void { 23 | if (this.jsonEnabled()) { 24 | return; 25 | } 26 | 27 | ux.action.start(action, status, opts); 28 | } 29 | 30 | protected stopSpinner(message?: string): void { 31 | if (this.jsonEnabled()) { 32 | return; 33 | } 34 | 35 | ux.action.stop(message); 36 | } 37 | 38 | protected stopSpinnerWithFailure(): void { 39 | if (this.jsonEnabled()) { 40 | return; 41 | } 42 | 43 | ux.action.stop('FAILED!'); 44 | } 45 | 46 | protected printTable>( 47 | data: T[], 48 | columns: string[], 49 | ): void { 50 | if (this.jsonEnabled()) { 51 | return; 52 | } 53 | 54 | const table = TtyTable( 55 | columns.map((column) => ({ value: column, alias: column })), 56 | data.map((row) => columns.map((column) => get(row, column))), 57 | ); 58 | 59 | this.log(table.render()); 60 | } 61 | 62 | protected catch( 63 | error: Error & { exitCode?: number | undefined }, 64 | ): Promise { 65 | const serialized = serializeError(error); 66 | 67 | if (!('oclif' in serialized)) { 68 | console.log(); 69 | console.dir(serialized, { depth: null, colors: true }); 70 | console.log(); 71 | } 72 | 73 | throw error; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/cli/src/commands/schema/generate.ts: -------------------------------------------------------------------------------- 1 | import { writeFileSync } from 'node:fs'; 2 | import { resolve } from 'node:path'; 3 | import { CmaClientCommand, oclif } from '@datocms/cli-utils'; 4 | import { generateSchemaTypes } from '../../utils/schema-types-generator'; 5 | 6 | export default class Command extends CmaClientCommand { 7 | static description = 'Generate TypeScript definitions for the schema'; 8 | 9 | static flags = { 10 | ...CmaClientCommand.flags, 11 | environment: oclif.Flags.string({ 12 | char: 'e', 13 | description: 'Environment to generate schema from', 14 | required: false, 15 | }), 16 | 'item-types': oclif.Flags.string({ 17 | char: 't', 18 | description: 19 | 'Comma-separated list of item type API keys to include (includes dependencies)', 20 | required: false, 21 | }), 22 | }; 23 | 24 | static args = { 25 | filename: oclif.Args.string({ 26 | description: 'Output filename for the generated TypeScript definitions', 27 | required: true, 28 | }), 29 | }; 30 | 31 | async run(): Promise { 32 | const { flags, args } = await this.parse(Command); 33 | let { environment } = flags; 34 | const filename = args.filename; 35 | const itemTypesFilter = flags['item-types']; 36 | 37 | if (!environment) { 38 | const environments = await this.client.environments.list(); 39 | const primaryEnv = environments.find((env) => env.meta.primary); 40 | if (primaryEnv) { 41 | environment = primaryEnv.id; 42 | } 43 | } 44 | 45 | const client = await this.buildClient({ environment }); 46 | 47 | const formattedCode = await generateSchemaTypes(client, { 48 | itemTypesFilter, 49 | environment, 50 | }); 51 | 52 | const outputPath = resolve(process.cwd(), filename); 53 | writeFileSync(outputPath, formattedCode, 'utf8'); 54 | 55 | this.log(`Schema types generated at: ${outputPath}`); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/cli-utils/src/dato-config-command.ts: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'node:fs/promises'; 2 | import { relative, resolve } from 'node:path'; 3 | import { Flags } from '@oclif/core'; 4 | import { BaseCommand } from './base-command'; 5 | import { type Config, readConfig } from './config'; 6 | 7 | export abstract class DatoConfigCommand extends BaseCommand { 8 | static baseFlags = { 9 | ...BaseCommand.baseFlags, 10 | 'config-file': Flags.string({ 11 | description: 'Specify a custom config file path', 12 | env: 'DATOCMS_CONFIG_FILE', 13 | default: './datocms.config.json', 14 | helpGroup: 'GLOBAL', 15 | }), 16 | }; 17 | 18 | protected datoConfigPath!: string; 19 | protected datoConfigRelativePath!: string; 20 | protected datoConfig?: Config; 21 | 22 | protected async init(): Promise { 23 | await super.init(); 24 | 25 | const { flags } = await this.parse(this.ctor as typeof DatoConfigCommand); 26 | 27 | this.datoConfigPath = resolve(process.cwd(), flags['config-file']); 28 | 29 | this.datoConfigRelativePath = relative(process.cwd(), this.datoConfigPath); 30 | 31 | this.datoConfig = await readConfig(this.datoConfigPath); 32 | } 33 | 34 | protected requireDatoConfig(): void { 35 | if (!this.datoConfig) { 36 | this.error(`No config file found in "${this.datoConfigRelativePath}"`, { 37 | suggestions: [ 38 | `Configure a local configuration profile with "${this.config.bin} profile:set"`, 39 | ], 40 | }); 41 | } 42 | } 43 | 44 | protected async saveDatoConfig(config: Config): Promise { 45 | this.startSpinner(`Writing "${this.datoConfigRelativePath}"`); 46 | 47 | try { 48 | await writeFile( 49 | this.datoConfigPath, 50 | JSON.stringify(config, null, 2), 51 | 'utf-8', 52 | ); 53 | 54 | this.stopSpinner(); 55 | } catch (e) { 56 | this.stopSpinnerWithFailure(); 57 | 58 | throw e; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/src/import/add-validations.ts: -------------------------------------------------------------------------------- 1 | import type { ContentFields, KeyValueMap } from 'contentful-management'; 2 | import type { ListrRendererFactory, ListrTaskWrapper } from 'listr2'; 3 | import type { Context } from '../commands/contentful/import'; 4 | import contentfulFieldValidatorsToDato from '../utils/item-type-create-helpers'; 5 | import BaseStep from './base-step'; 6 | 7 | const AddValidationsLog = 'Add validations to fields'; 8 | 9 | export default class AddValidations extends BaseStep { 10 | async task( 11 | ctx: Context, 12 | task: ListrTaskWrapper, 13 | ): Promise { 14 | await this.runConcurrentlyOver( 15 | task, 16 | AddValidationsLog, 17 | Object.keys(ctx.contentTypeIdToContentfulFields), 18 | (contentfulContentTypeId) => 19 | `Add validations to ${contentfulContentTypeId}`, 20 | async (contentfulContentTypeId) => { 21 | const contentTypeIdToContentfulFields = 22 | ctx.contentTypeIdToContentfulFields[contentfulContentTypeId]; 23 | 24 | for (const [contentfulFieldId, contentfulField] of Object.entries( 25 | contentTypeIdToContentfulFields, 26 | )) { 27 | const datoField = 28 | ctx.contentTypeIdToDatoFields[contentfulContentTypeId][ 29 | contentfulFieldId 30 | ]; 31 | 32 | if (!datoField) { 33 | throw new Error('Missing field. This should not happen'); 34 | } 35 | 36 | const newValidators = contentfulFieldValidatorsToDato( 37 | contentfulField as ContentFields, 38 | ctx.contentTypeIdToEditorInterface[contentfulContentTypeId], 39 | ctx.contentTypeIdToDatoFields[contentfulContentTypeId], 40 | ); 41 | 42 | await this.client.fields.update(datoField.id, { 43 | validators: { ...datoField.validators, ...newValidators }, 44 | }); 45 | } 46 | }, 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datocms/cli-plugin-wordpress", 3 | "version": "3.1.16", 4 | "description": "DatoCMS CLI plugin to import WordPress sites", 5 | "author": "DatoCMS ", 6 | "homepage": "https://github.com/datocms/cli", 7 | "license": "MIT", 8 | "main": "lib/src/index.js", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/datocms/cli.git", 12 | "directory": "packages/cli-plugin-wordpress" 13 | }, 14 | "files": [ 15 | "/lib", 16 | "/npm-shrinkwrap.json", 17 | "/oclif.manifest.json" 18 | ], 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "dependencies": { 23 | "@datocms/cli-utils": "^3.1.16", 24 | "@oclif/core": "^4", 25 | "async-scheduler": "^1.4.4", 26 | "enquirer": ">= 2.3.0 < 3", 27 | "inflected": "^2.1.0", 28 | "listr2": "^4.0.5", 29 | "wpapi": "^1.2.2" 30 | }, 31 | "oclif": { 32 | "commands": "./lib/commands", 33 | "repositoryPrefix": "<%- repo %>/blob/v<%- version %>/packages/cli-plugin-wordpress/<%- commandPath %>" 34 | }, 35 | "scripts": { 36 | "build": "rm -rf lib && tsc -b", 37 | "postpack": "rm -f oclif.manifest.json", 38 | "prepack": "npm run build && oclif manifest && oclif readme", 39 | "test": "mocha --timeout 200000 --require ts-node/register --forbid-only \"test/**/*.test.ts\"", 40 | "version": "oclif readme && git add README.md" 41 | }, 42 | "engines": { 43 | "node": ">=18.0.0" 44 | }, 45 | "bugs": "https://github.com/datocms/cli/issues", 46 | "keywords": [ 47 | "datocms", 48 | "cli" 49 | ], 50 | "types": "lib/src/index.d.ts", 51 | "gitHead": "09f72bd8742b2933271407559d9fbcc235662876", 52 | "devDependencies": { 53 | "@datocms/dashboard-client": "^5", 54 | "@oclif/test": "^4", 55 | "@types/inflected": "^1.1.29", 56 | "@types/listr": "^0.14.4", 57 | "@types/wpapi": "^1.1.1", 58 | "@whatwg-node/fetch": "^0.10.10", 59 | "oclif": "^4" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/cma-client-node.ts","./src/index.ts","./src/commands/cma/call.ts","./src/commands/environments/destroy.ts","./src/commands/environments/fork.ts","./src/commands/environments/list.ts","./src/commands/environments/primary.ts","./src/commands/environments/promote.ts","./src/commands/environments/rename.ts","./src/commands/maintenance/off.ts","./src/commands/maintenance/on.ts","./src/commands/migrations/new.ts","./src/commands/migrations/run.ts","./src/commands/plugins/available.ts","./src/commands/profile/remove.ts","./src/commands/profile/set.ts","./src/commands/schema/generate.ts","./src/utils/find-nearest-file.ts","./src/utils/schema-types-generator.ts","./src/utils/environments-diff/fetch-schema.ts","./src/utils/environments-diff/index.ts","./src/utils/environments-diff/types.ts","./src/utils/environments-diff/utils.ts","./src/utils/environments-diff/resources/comments.ts","./src/utils/environments-diff/resources/create-new-fields-and-fieldsets.ts","./src/utils/environments-diff/resources/create-new-item-types.ts","./src/utils/environments-diff/resources/delete-missing-fields-and-fieldsets-in-existing-item-types.ts","./src/utils/environments-diff/resources/delete-missing-item-types.ts","./src/utils/environments-diff/resources/finalize-item-types.ts","./src/utils/environments-diff/resources/manage-item-type-filters.ts","./src/utils/environments-diff/resources/manage-menu-items.ts","./src/utils/environments-diff/resources/manage-plugins.ts","./src/utils/environments-diff/resources/manage-schema-menu-items.ts","./src/utils/environments-diff/resources/manage-upload-filters.ts","./src/utils/environments-diff/resources/manage-workflows.ts","./src/utils/environments-diff/resources/update-fields-and-fieldsets.ts","./src/utils/environments-diff/resources/update-roles.ts","./src/utils/environments-diff/resources/update-site.ts","./src/utils/environments-diff/write/api-calls.ts","./src/utils/environments-diff/write/comments.ts","./src/utils/environments-diff/write/get-entity-ids-to-be-recreated.ts","./src/utils/environments-diff/write/index.ts"],"version":"5.7.2"} -------------------------------------------------------------------------------- /packages/cli-utils/src/config.ts: -------------------------------------------------------------------------------- 1 | import { access, readFile } from 'node:fs/promises'; 2 | import { get } from 'lodash'; 3 | import type { LogLevelFlagEnum, LogLevelModeEnum } from '.'; 4 | 5 | export type ProfileConfig = { 6 | baseUrl?: string; 7 | logLevel?: LogLevelFlagEnum; 8 | logMode?: LogLevelModeEnum; 9 | migrations?: { 10 | directory?: string; 11 | modelApiKey?: string; 12 | template?: string; 13 | tsconfig?: string; 14 | }; 15 | }; 16 | 17 | export type Config = { 18 | profiles: Record; 19 | }; 20 | 21 | function isProfileConfig(thing: unknown): thing is ProfileConfig { 22 | if (typeof thing !== 'object' || !thing) { 23 | return false; 24 | } 25 | 26 | for (const key of [ 27 | 'apiToken', 28 | 'baseUrl', 29 | 'logLevel', 30 | 'migrations.directory', 31 | 'migrations.modelApiKey', 32 | 'migrations.template', 33 | 'migrations.tsconfig', 34 | ]) { 35 | const value = get(thing, key); 36 | if (value !== undefined && typeof value !== 'string') { 37 | return false; 38 | } 39 | } 40 | 41 | return true; 42 | } 43 | 44 | function isConfig(thing: unknown): thing is Config { 45 | if (typeof thing !== 'object' || !thing) { 46 | return false; 47 | } 48 | 49 | if (!('profiles' in thing)) { 50 | return false; 51 | } 52 | 53 | const { profiles } = thing as any; 54 | 55 | if (typeof profiles !== 'object' || !profiles) { 56 | return false; 57 | } 58 | 59 | if ( 60 | Object.values(profiles).some( 61 | (profileConfig) => !isProfileConfig(profileConfig), 62 | ) 63 | ) { 64 | return false; 65 | } 66 | 67 | return true; 68 | } 69 | 70 | export async function readConfig( 71 | fullPath: string, 72 | ): Promise { 73 | try { 74 | await access(fullPath); 75 | } catch { 76 | return undefined; 77 | } 78 | 79 | const rawConfig = await readFile(fullPath, 'utf-8'); 80 | 81 | const config = JSON.parse(rawConfig); 82 | 83 | if (!isConfig(config)) { 84 | throw new Error(`Invalid configuration file at "${fullPath}"!`); 85 | } 86 | 87 | return config; 88 | } 89 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datocms/cli-plugin-contentful", 3 | "version": "3.1.16", 4 | "description": "Plugin for DatoCMS CLI to import projects from Contentful to DatoCMS", 5 | "keywords": [ 6 | "contentful", 7 | "datocms", 8 | "import" 9 | ], 10 | "engines": { 11 | "node": ">=18.0.0" 12 | }, 13 | "author": "DatoCMS ", 14 | "homepage": "https://github.com/datocms/cli#readme", 15 | "license": "MIT", 16 | "main": "lib/src/index.js", 17 | "types": "lib/src/index.d.js", 18 | "files": [ 19 | "/lib", 20 | "/npm-shrinkwrap.json", 21 | "/oclif.manifest.json" 22 | ], 23 | "publishConfig": { 24 | "access": "public" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/datocms/cli.git", 29 | "directory": "packages/cli-plugin-contentful" 30 | }, 31 | "scripts": { 32 | "build": "rm -rf lib && tsc -b", 33 | "postpack": "rm -f oclif.manifest.json", 34 | "prepack": "npm run build && oclif manifest && oclif readme", 35 | "test": "mocha --timeout 200000 --require ts-node/register --forbid-only \"test/**/*.test.ts\"", 36 | "version": "oclif readme && git add README.md" 37 | }, 38 | "bugs": { 39 | "url": "https://github.com/datocms/cli/issues" 40 | }, 41 | "dependencies": { 42 | "@datocms/cli-utils": "^3.1.16", 43 | "@oclif/core": "^4", 44 | "@whatwg-node/fetch": "^0.10.10", 45 | "async-scheduler": "^1.4.4", 46 | "contentful-management": "^10.46.4", 47 | "date-fns": "^2.29.3", 48 | "datocms-contentful-to-structured-text": "^2.1.7", 49 | "datocms-structured-text-utils": "^2.0.4", 50 | "enquirer": ">= 2.3.0 < 3", 51 | "humps": "^2.0.1", 52 | "listr2": "^4.0.5", 53 | "lodash": "^4.17.21" 54 | }, 55 | "devDependencies": { 56 | "@datocms/dashboard-client": "^5", 57 | "@oclif/test": "^4", 58 | "@types/humps": "^2.0.1", 59 | "@types/listr": "^0.14.4", 60 | "@types/lodash": "^4.17.20", 61 | "@types/pluralize": "^0.0.29", 62 | "dotenv": "^16.0.2", 63 | "oclif": "^4" 64 | }, 65 | "oclif": { 66 | "commands": "./lib/commands", 67 | "repositoryPrefix": "<%- repo %>/blob/v<%- version %>/packages/cli-plugin-contentful/<%- commandPath %>" 68 | }, 69 | "gitHead": "09f72bd8742b2933271407559d9fbcc235662876" 70 | } 71 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/import/import-wp-tags.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Listr, 3 | type ListrRendererFactory, 4 | type ListrTaskWrapper, 5 | } from 'listr2'; 6 | import type { Context } from '../commands/wordpress/import'; 7 | import { createSlugField, createStringField } from '../utils/build-fields'; 8 | import BaseStep from './base-step'; 9 | 10 | const retrieveTitle = 'Retrieve tags from WordPress'; 11 | const createTitle = 'Import tags to DatoCMS'; 12 | 13 | export default class WpTags extends BaseStep { 14 | task(): Listr { 15 | return new Listr([ 16 | { 17 | title: 'Create DatoCMS model', 18 | task: this.createTagModel.bind(this), 19 | }, 20 | { 21 | title: retrieveTitle, 22 | task: this.retrieveTags.bind(this), 23 | }, 24 | { 25 | title: createTitle, 26 | task: this.createTags.bind(this), 27 | }, 28 | ]); 29 | } 30 | 31 | async createTagModel(ctx: Context): Promise { 32 | const itemType = await this.client.itemTypes.create({ 33 | api_key: 'wp_tag', 34 | name: 'WP Tag', 35 | }); 36 | 37 | await Promise.all([ 38 | createStringField(this.client, itemType, 'name').then((field) => 39 | createSlugField(this.client, itemType, field.id), 40 | ), 41 | ]); 42 | 43 | ctx.datoItemTypes.tag = itemType; 44 | } 45 | 46 | async retrieveTags( 47 | ctx: Context, 48 | task: ListrTaskWrapper, 49 | ): Promise { 50 | ctx.wpTags = await this.fetchAllWpPages( 51 | task, 52 | retrieveTitle, 53 | this.wpClient.tags(), 54 | ); 55 | } 56 | 57 | async createTags( 58 | ctx: Context, 59 | task: ListrTaskWrapper, 60 | ): Promise { 61 | if (!(ctx.wpTags && ctx.datoItemTypes.tag)) { 62 | throw new Error('This should not happen!'); 63 | } 64 | 65 | const wpTags = ctx.wpTags; 66 | const tagItemType = ctx.datoItemTypes.tag; 67 | const tagsMapping: Record = {}; 68 | 69 | await this.runConcurrentlyOver( 70 | task, 71 | createTitle, 72 | wpTags, 73 | (wpTag) => wpTag.name, 74 | async (wpTag) => { 75 | const datoTag = await this.client.items.create({ 76 | item_type: tagItemType, 77 | name: wpTag.name, 78 | slug: wpTag.slug, 79 | }); 80 | 81 | tagsMapping[wpTag.id] = datoTag.id; 82 | }, 83 | ); 84 | 85 | ctx.tagsMapping = tagsMapping; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/write/get-entity-ids-to-be-recreated.ts: -------------------------------------------------------------------------------- 1 | import type * as Types from '../types'; 2 | 3 | export function getEntityIdsToBeRecreated( 4 | commands: Types.Command[], 5 | ): Types.EntityIdsToBeRecreated { 6 | return { 7 | field: commands 8 | .filter( 9 | (command): command is Types.CreateFieldClientCommand => 10 | command.type === 'apiCallClientCommand' && 11 | command.call === 'client.fields.create', 12 | ) 13 | .map((command) => command.oldEnvironmentId), 14 | fieldset: commands 15 | .filter( 16 | (command): command is Types.CreateFieldsetClientCommand => 17 | command.type === 'apiCallClientCommand' && 18 | command.call === 'client.fieldsets.create', 19 | ) 20 | .map((command) => command.oldEnvironmentId), 21 | itemType: commands 22 | .filter( 23 | (command): command is Types.CreateItemTypeClientCommand => 24 | command.type === 'apiCallClientCommand' && 25 | command.call === 'client.itemTypes.create', 26 | ) 27 | .map((command) => command.oldEnvironmentId), 28 | plugin: commands 29 | .filter( 30 | (command): command is Types.CreatePluginClientCommand => 31 | command.type === 'apiCallClientCommand' && 32 | command.call === 'client.plugins.create', 33 | ) 34 | .map((command) => command.oldEnvironmentId), 35 | workflow: commands 36 | .filter( 37 | (command): command is Types.CreateWorkflowClientCommand => 38 | command.type === 'apiCallClientCommand' && 39 | command.call === 'client.workflows.create', 40 | ) 41 | .map((command) => command.oldEnvironmentId), 42 | menuItem: commands 43 | .filter( 44 | (command): command is Types.CreateMenuItemClientCommand => 45 | command.type === 'apiCallClientCommand' && 46 | command.call === 'client.menuItems.create', 47 | ) 48 | .map((command) => command.oldEnvironmentId), 49 | schemaMenuItem: commands 50 | .filter( 51 | (command): command is Types.CreateSchemaMenuItemClientCommand => 52 | command.type === 'apiCallClientCommand' && 53 | command.call === 'client.schemaMenuItems.create', 54 | ) 55 | .map((command) => command.oldEnvironmentId), 56 | itemTypeFilter: commands 57 | .filter( 58 | (command): command is Types.CreateItemTypeFilterClientCommand => 59 | command.type === 'apiCallClientCommand' && 60 | command.call === 'client.itemTypeFilters.create', 61 | ) 62 | .map((command) => command.oldEnvironmentId), 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /packages/cli-utils/src/dato-profile-config-command.ts: -------------------------------------------------------------------------------- 1 | import { Flags } from '@oclif/core'; 2 | import type { ProfileConfig } from './config'; 3 | import { DatoConfigCommand } from './dato-config-command'; 4 | 5 | export abstract class DatoProfileConfigCommand extends DatoConfigCommand { 6 | static baseFlags = { 7 | ...DatoConfigCommand.baseFlags, 8 | profile: Flags.string({ 9 | description: 'Use settings of profile in datocms.config.js', 10 | env: 'DATOCMS_PROFILE', 11 | helpGroup: 'GLOBAL', 12 | }), 13 | }; 14 | 15 | protected datoProfileConfig?: ProfileConfig; 16 | protected profileId!: string; 17 | 18 | protected async init(): Promise { 19 | await super.init(); 20 | 21 | const { flags } = await this.parse( 22 | this.ctor as typeof DatoProfileConfigCommand, 23 | ); 24 | 25 | if (flags.profile) { 26 | if (!this.datoConfig) { 27 | this.error( 28 | `Requested profile "${flags.profile}" but cannot find config file`, 29 | { 30 | suggestions: [ 31 | `Create profile with "${this.config.bin} profile:set ${flags.profile}"`, 32 | ], 33 | }, 34 | ); 35 | } 36 | 37 | if (!(flags.profile in this.datoConfig.profiles)) { 38 | this.error( 39 | `Requested profile "${flags.profile}" is not defined in config file "${this.datoConfigRelativePath}"`, 40 | { 41 | suggestions: [ 42 | `Configure it with "${this.config.bin} profile:set ${flags.profile}"`, 43 | ], 44 | }, 45 | ); 46 | } 47 | } else if ( 48 | this.datoConfig && 49 | Object.keys(this.datoConfig.profiles).length > 1 50 | ) { 51 | this.error( 52 | `Multiple profiles detected in config file "${this.datoConfigRelativePath}"`, 53 | { 54 | suggestions: [ 55 | `Specify which profile to use with the "--profile" flag, or the DATOCMS_PROFILE env variable (we look inside .env.local and .env too)`, 56 | ], 57 | }, 58 | ); 59 | } 60 | 61 | this.profileId = flags.profile || 'default'; 62 | 63 | this.datoProfileConfig = 64 | this.datoConfig && this.profileId in this.datoConfig.profiles 65 | ? this.datoConfig.profiles[this.profileId] 66 | : undefined; 67 | } 68 | 69 | protected requireDatoProfileConfig(): void { 70 | this.requireDatoConfig(); 71 | 72 | if (!this.datoProfileConfig) { 73 | this.error('No profile specified!', { 74 | suggestions: [ 75 | 'Provide the --profile option or specify a DATOCMS_PROFILE env variable', 76 | ], 77 | }); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/import/import-wp-authors.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Listr, 3 | type ListrRendererFactory, 4 | type ListrTaskWrapper, 5 | } from 'listr2'; 6 | import type { Context } from '../commands/wordpress/import'; 7 | import { 8 | createSlugField, 9 | createStringField, 10 | createTextField, 11 | } from '../utils/build-fields'; 12 | import BaseStep from './base-step'; 13 | 14 | const retrieveTitle = 'Retrieve authors from WordPress'; 15 | const createTitle = 'Import authors to DatoCMS'; 16 | export default class WpAuthors extends BaseStep { 17 | task(): Listr { 18 | return new Listr([ 19 | { 20 | title: 'Create DatoCMS model', 21 | task: this.createAuthorModel.bind(this), 22 | }, 23 | { 24 | title: retrieveTitle, 25 | task: this.retrieveAuthors.bind(this), 26 | }, 27 | { 28 | title: createTitle, 29 | task: this.createAuthors.bind(this), 30 | }, 31 | ]); 32 | } 33 | 34 | async createAuthorModel(ctx: Context): Promise { 35 | const itemType = await this.client.itemTypes.create({ 36 | api_key: 'wp_author', 37 | name: 'WP Author', 38 | }); 39 | 40 | await Promise.all([ 41 | createStringField(this.client, itemType, 'name').then((field) => 42 | createSlugField(this.client, itemType, field.id), 43 | ), 44 | createStringField(this.client, itemType, 'url'), 45 | createTextField(this.client, itemType, 'description'), 46 | ]); 47 | 48 | ctx.datoItemTypes.author = itemType; 49 | } 50 | 51 | async retrieveAuthors( 52 | ctx: Context, 53 | task: ListrTaskWrapper, 54 | ): Promise { 55 | ctx.wpAuthors = await this.fetchAllWpPages( 56 | task, 57 | retrieveTitle, 58 | this.wpClient.users(), 59 | ); 60 | } 61 | 62 | async createAuthors( 63 | ctx: Context, 64 | task: ListrTaskWrapper, 65 | ): Promise { 66 | if (!(ctx.wpAuthors && ctx.datoItemTypes.author)) { 67 | throw new Error('This should not happen!'); 68 | } 69 | 70 | const wpAuthors = ctx.wpAuthors; 71 | const authorItemType = ctx.datoItemTypes.author; 72 | const authorsMapping: Record = {}; 73 | 74 | await this.runConcurrentlyOver( 75 | task, 76 | createTitle, 77 | wpAuthors, 78 | (wpAuthor) => wpAuthor.name, 79 | async (wpAuthor) => { 80 | const datoAuthor = await this.client.items.create({ 81 | item_type: authorItemType, 82 | name: wpAuthor.name, 83 | slug: wpAuthor.slug, 84 | url: wpAuthor.url, 85 | description: wpAuthor.description, 86 | }); 87 | 88 | authorsMapping[wpAuthor.id] = datoAuthor.id; 89 | }, 90 | ); 91 | 92 | ctx.authorsMapping = authorsMapping; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /packages/cli/src/commands/environments/fork.ts: -------------------------------------------------------------------------------- 1 | import { CmaClient, CmaClientCommand, oclif } from '@datocms/cli-utils'; 2 | 3 | export default class Command extends CmaClientCommand { 4 | static description = 5 | 'Creates a new sandbox environment by forking an existing one'; 6 | 7 | static args = { 8 | SOURCE_ENVIRONMENT_ID: oclif.Args.string({ 9 | description: 'The environment to copy', 10 | required: true, 11 | }), 12 | NEW_ENVIRONMENT_ID: oclif.Args.string({ 13 | description: 'The name of the new sandbox environment to generate', 14 | required: true, 15 | }), 16 | }; 17 | 18 | static flags = { 19 | fast: oclif.Flags.boolean({ 20 | description: 21 | 'Run a fast fork. A fast fork reduces processing time, but it also prevents writing to the source environment during the process', 22 | }), 23 | force: oclif.Flags.boolean({ 24 | description: 25 | 'Forces the start of a fast fork, even there are users currently editing records in the environment to copy', 26 | dependsOn: ['fast'], 27 | }), 28 | }; 29 | 30 | async run(): Promise { 31 | const { 32 | args: { SOURCE_ENVIRONMENT_ID: srcEnvId, NEW_ENVIRONMENT_ID: newEnvId }, 33 | flags: { fast, force }, 34 | } = await this.parse(Command); 35 | 36 | this.startSpinner( 37 | `Starting a ${ 38 | fast ? 'fast ' : '' 39 | }fork of "${srcEnvId}" called "${newEnvId}"`, 40 | ); 41 | 42 | try { 43 | const sourceEnv = await this.client.environments.find(srcEnvId); 44 | 45 | const environment = await this.client.environments.fork( 46 | sourceEnv.id, 47 | { 48 | id: newEnvId, 49 | }, 50 | { fast, force }, 51 | ); 52 | 53 | this.stopSpinner(); 54 | 55 | return environment; 56 | } catch (e) { 57 | this.stopSpinnerWithFailure(); 58 | 59 | if (e instanceof CmaClient.ApiError && e.findError('NOT_FOUND')) { 60 | this.error(`An environment called "${srcEnvId}" does not exist`); 61 | } 62 | 63 | if ( 64 | e instanceof CmaClient.ApiError && 65 | e.findError('INVALID_FIELD', { 66 | field: 'name', 67 | code: 'VALIDATION_UNIQUENESS', 68 | }) 69 | ) { 70 | this.error(`An environment called "${newEnvId}" already exists`, { 71 | suggestions: [ 72 | `To delete the environment, run "${this.config.bin} environments:destroy ${newEnvId}"`, 73 | ], 74 | }); 75 | } 76 | 77 | if ( 78 | e instanceof CmaClient.ApiError && 79 | e.findError('ACTIVE_EDITING_SESSIONS') 80 | ) { 81 | this.error( 82 | 'Cannot proceed with a fast fork of the environment, as some users are currently editing records', 83 | { 84 | suggestions: ['To proceed anyway, use the --force flag'], 85 | }, 86 | ); 87 | } 88 | 89 | throw e; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@datocms/cli", 3 | "version": "3.1.16", 4 | "description": "CLI to interact with DatoCMS APIs", 5 | "author": "Stefano Verna ", 6 | "bin": { 7 | "datocms": "./bin/run" 8 | }, 9 | "homepage": "https://github.com/datocms/cli", 10 | "license": "MIT", 11 | "main": "lib/index.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/datocms/cli.git", 15 | "directory": "packages/cli" 16 | }, 17 | "files": [ 18 | "/bin", 19 | "/lib", 20 | "/npm-shrinkwrap.json", 21 | "/oclif.manifest.json", 22 | "/empty-tsconfig.json" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "dependencies": { 28 | "@datocms/cli-utils": "^3.1.16", 29 | "@datocms/rest-client-utils": "^1", 30 | "@inquirer/prompts": "^7.8.4", 31 | "@oclif/plugin-autocomplete": "^3", 32 | "@oclif/plugin-help": "^6", 33 | "@oclif/plugin-not-found": "^3", 34 | "@oclif/plugin-plugins": "^5", 35 | "@oclif/plugin-warn-if-update-available": "^3", 36 | "json5": "^2.2.3", 37 | "lodash": "^4.17.21", 38 | "mkdirp": "^1.0.4", 39 | "prettier": "^3.4.2", 40 | "tsx": "^4.19.2", 41 | "typescript": "5.7.2" 42 | }, 43 | "oclif": { 44 | "bin": "datocms", 45 | "dirname": "datocms", 46 | "commands": "./lib/commands", 47 | "plugins": [ 48 | "@oclif/plugin-help", 49 | "@oclif/plugin-plugins", 50 | "@oclif/plugin-autocomplete", 51 | "@oclif/plugin-not-found", 52 | "@oclif/plugin-warn-if-update-available" 53 | ], 54 | "repositoryPrefix": "<%- repo %>/blob/v<%- version %>/packages/cli/<%- commandPath %>", 55 | "topicSeparator": ":", 56 | "topics": { 57 | "cma": { 58 | "description": "Interact with DatoCMS Content Management API" 59 | }, 60 | "maintenance": { 61 | "description": "Enable/disable maintenance mode for a project" 62 | }, 63 | "environments": { 64 | "description": "Manage primary/sandbox environments of a project" 65 | }, 66 | "profile": { 67 | "description": "Manage profiles stored in datocms.config.js file" 68 | }, 69 | "migrations": { 70 | "description": "Manage and run migration scripts" 71 | } 72 | } 73 | }, 74 | "scripts": { 75 | "build": "rm -rf lib && tsc -b", 76 | "postpack": "rm -f oclif.manifest.json", 77 | "prepack": "npm run build && oclif manifest && oclif readme", 78 | "test": "mocha --forbid-only \"test/**/*.test.ts\"", 79 | "version": "oclif readme && git add README.md" 80 | }, 81 | "engines": { 82 | "node": ">=18.0.0" 83 | }, 84 | "bugs": "https://github.com/datocms/cli/issues", 85 | "keywords": [ 86 | "datocms", 87 | "cli" 88 | ], 89 | "types": "lib/index.d.ts", 90 | "gitHead": "09f72bd8742b2933271407559d9fbcc235662876", 91 | "devDependencies": { 92 | "@types/inquirer": "^9.0.9", 93 | "@types/lodash": "^4.17.20", 94 | "@types/mkdirp": "^1.0.2", 95 | "@types/prettier": "^2.7.1", 96 | "oclif": "^4" 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/index.ts: -------------------------------------------------------------------------------- 1 | import type { CmaClient } from '@datocms/cli-utils'; 2 | import { resolveConfig } from 'prettier'; 3 | import { fetchSchema } from './fetch-schema'; 4 | import { createNewFieldsAndFieldsets } from './resources/create-new-fields-and-fieldsets'; 5 | import { createNewItemTypes } from './resources/create-new-item-types'; 6 | import { deleteMissingFieldsAndFieldsetsInExistingItemTypes } from './resources/delete-missing-fields-and-fieldsets-in-existing-item-types'; 7 | import { deleteMissingItemTypes } from './resources/delete-missing-item-types'; 8 | import { finalizeItemTypes } from './resources/finalize-item-types'; 9 | import { manageItemTypeFilters } from './resources/manage-item-type-filters'; 10 | import { manageMenuItems } from './resources/manage-menu-items'; 11 | import { managePlugins } from './resources/manage-plugins'; 12 | import { manageSchemaMenuItems } from './resources/manage-schema-menu-items'; 13 | import { manageUploadFilters } from './resources/manage-upload-filters'; 14 | import { manageWorkflows } from './resources/manage-workflows'; 15 | import { updateFieldsAndFieldsets } from './resources/update-fields-and-fieldsets'; 16 | import { updateRoles } from './resources/update-roles'; 17 | import { updateSite } from './resources/update-site'; 18 | import { write } from './write'; 19 | 20 | export async function diffEnvironments({ 21 | newClient, 22 | newEnvironmentId, 23 | oldClient, 24 | oldEnvironmentId, 25 | migrationFilePath, 26 | format, 27 | }: { 28 | newClient: CmaClient.Client; 29 | newEnvironmentId: string; 30 | oldClient: CmaClient.Client; 31 | oldEnvironmentId: string; 32 | migrationFilePath: string; 33 | format: 'js' | 'ts'; 34 | }) { 35 | const newSchema = await fetchSchema(newClient); 36 | const oldSchema = await fetchSchema(oldClient); 37 | 38 | const { data: roles } = await newClient.roles.rawList(); 39 | 40 | const commands = [ 41 | ...updateSite(newSchema, oldSchema), 42 | ...manageWorkflows(newSchema, oldSchema), 43 | ...managePlugins(newSchema, oldSchema), 44 | ...manageUploadFilters(newSchema, oldSchema), 45 | ...createNewItemTypes(newSchema, oldSchema), 46 | ...createNewFieldsAndFieldsets(newSchema, oldSchema), 47 | ...deleteMissingFieldsAndFieldsetsInExistingItemTypes(newSchema, oldSchema), 48 | ...updateFieldsAndFieldsets(newSchema, oldSchema), 49 | ...deleteMissingItemTypes(newSchema, oldSchema), 50 | ...finalizeItemTypes(newSchema, oldSchema), 51 | ...manageItemTypeFilters(newSchema, oldSchema), 52 | ...manageMenuItems(newSchema, oldSchema), 53 | ...manageSchemaMenuItems(newSchema, oldSchema), 54 | ...updateRoles(roles, newEnvironmentId, oldEnvironmentId), 55 | ]; 56 | 57 | try { 58 | const options = await resolveConfig(migrationFilePath); 59 | return write(commands, { ...options, format, filepath: migrationFilePath }); 60 | } catch { 61 | // .prettierrc of user might not work with our version of prettier, in this case 62 | // fall back to default options 63 | return write(commands, { format, filepath: migrationFilePath }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/src/import/import-wp-assets.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Listr, 3 | type ListrRendererFactory, 4 | type ListrTaskWrapper, 5 | } from 'listr2'; 6 | import type { Context } from '../commands/wordpress/import'; 7 | import BaseStep from './base-step'; 8 | 9 | const retrieveTitle = 'Retrieve assets from WordPress'; 10 | const createTitle = 'Upload assets to DatoCMS'; 11 | export default class WpAssets extends BaseStep { 12 | task(): Listr { 13 | return new Listr([ 14 | { 15 | title: retrieveTitle, 16 | task: this.retrieveAssetsCatalog.bind(this), 17 | }, 18 | { 19 | title: createTitle, 20 | task: this.uploadAssetsToDatoCms.bind(this), 21 | }, 22 | ]); 23 | } 24 | 25 | async retrieveAssetsCatalog( 26 | ctx: Context, 27 | task: ListrTaskWrapper, 28 | ): Promise { 29 | ctx.wpMediaItems = await this.fetchAllWpPages( 30 | task, 31 | retrieveTitle, 32 | this.wpClient.media(), 33 | ); 34 | } 35 | 36 | async uploadAssetsToDatoCms( 37 | ctx: Context, 38 | task: ListrTaskWrapper, 39 | ): Promise { 40 | if (!ctx.wpMediaItems) { 41 | throw new Error('This should not happen'); 42 | } 43 | 44 | const wpAssetIdToDatoId: Record = {}; 45 | const wpAssetUrlToDatoUrl: Record = {}; 46 | 47 | await this.runConcurrentlyOver( 48 | task, 49 | createTitle, 50 | ctx.wpMediaItems, 51 | (wpMediaItem) => wpMediaItem.source_url, 52 | async (wpMediaItem, notify) => { 53 | const upload = await this.client.uploads.createFromUrl({ 54 | url: wpMediaItem.source_url, 55 | skipCreationIfAlreadyExists: true, 56 | onProgress: (info) => { 57 | notify( 58 | `${info.type} ${ 59 | 'payload' in info && 'progress' in info.payload 60 | ? ` (${info.payload.progress}%)` 61 | : '' 62 | }`, 63 | ); 64 | }, 65 | default_field_metadata: { 66 | en: { 67 | title: wpMediaItem.title.rendered, 68 | alt: wpMediaItem.alt_text, 69 | custom_data: {}, 70 | }, 71 | }, 72 | }); 73 | 74 | wpAssetIdToDatoId[wpMediaItem.id] = upload.id; 75 | wpAssetUrlToDatoUrl[wpMediaItem.source_url] = upload.url; 76 | 77 | if (wpMediaItem.media_details?.sizes) { 78 | for (const thumbName of Object.keys( 79 | wpMediaItem.media_details.sizes, 80 | )) { 81 | const { 82 | width, 83 | height, 84 | source_url: sourceUrl, 85 | } = wpMediaItem.media_details.sizes[thumbName]; 86 | 87 | wpAssetUrlToDatoUrl[sourceUrl] = 88 | `${upload.url}?w=${width}&h=${height}&fit=crop`; 89 | } 90 | } 91 | }, 92 | ); 93 | 94 | ctx.wpAssetIdToDatoId = wpAssetIdToDatoId; 95 | ctx.wpAssetUrlToDatoUrl = wpAssetUrlToDatoUrl; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/fetch-schema.ts: -------------------------------------------------------------------------------- 1 | import type { CmaClient } from '@datocms/cli-utils'; 2 | import type { Schema } from './types'; 3 | 4 | export async function fetchSchema(client: CmaClient.Client): Promise { 5 | const [ 6 | siteResponse, 7 | menuItemsResponse, 8 | schemaMenuItemsResponse, 9 | pluginsResponse, 10 | workflowsResponse, 11 | itemTypeFiltersResponse, 12 | uploadFiltersResponse, 13 | ] = await Promise.all([ 14 | client.site.rawFind({ 15 | include: 'item_types,item_types.fields,item_types.fieldsets', 16 | }), 17 | client.menuItems.rawList(), 18 | client.schemaMenuItems.rawList(), 19 | client.plugins.rawList(), 20 | client.workflows.rawList(), 21 | client.itemTypeFilters.rawList(), 22 | client.uploadFilters.rawList(), 23 | ]); 24 | 25 | const includedResources = siteResponse.included || []; 26 | 27 | const allFields = includedResources.filter( 28 | (x): x is CmaClient.RawApiTypes.Field => x.type === 'field', 29 | ); 30 | 31 | const allFieldsets: CmaClient.RawApiTypes.Fieldset[] = 32 | includedResources.filter( 33 | (x): x is CmaClient.RawApiTypes.Fieldset => x.type === 'fieldset', 34 | ); 35 | 36 | return { 37 | siteEntity: siteResponse.data, 38 | itemTypesById: Object.fromEntries( 39 | includedResources 40 | .filter( 41 | (x): x is CmaClient.RawApiTypes.ItemType => x.type === 'item_type', 42 | ) 43 | .map((itemType) => [ 44 | itemType.id, 45 | { 46 | entity: itemType, 47 | fieldsById: Object.fromEntries( 48 | allFields 49 | .filter( 50 | (f) => f.relationships.item_type.data.id === itemType.id, 51 | ) 52 | .map((field) => [field.id, field]), 53 | ), 54 | fieldsetsById: Object.fromEntries( 55 | allFieldsets 56 | .filter( 57 | (f) => f.relationships.item_type.data.id === itemType.id, 58 | ) 59 | .map((fieldset) => [fieldset.id, fieldset]), 60 | ), 61 | }, 62 | ]), 63 | ), 64 | menuItemsById: Object.fromEntries( 65 | menuItemsResponse.data.map((menuItem) => [menuItem.id, menuItem]), 66 | ), 67 | schemaMenuItemsById: Object.fromEntries( 68 | schemaMenuItemsResponse.data.map((schemaMenuItem) => [ 69 | schemaMenuItem.id, 70 | schemaMenuItem, 71 | ]), 72 | ), 73 | pluginsById: Object.fromEntries( 74 | pluginsResponse.data.map((plugin) => [plugin.id, plugin]), 75 | ), 76 | workflowsById: Object.fromEntries( 77 | workflowsResponse.data.map((workflow) => [workflow.id, workflow]), 78 | ), 79 | itemTypeFiltersById: Object.fromEntries( 80 | itemTypeFiltersResponse.data 81 | .filter((itf) => itf.attributes.shared) 82 | .map((itemTypeFilter) => [itemTypeFilter.id, itemTypeFilter]), 83 | ), 84 | uploadFiltersById: Object.fromEntries( 85 | uploadFiltersResponse.data 86 | .filter((itf) => itf.attributes.shared) 87 | .map((uploadFilter) => [uploadFilter.id, uploadFilter]), 88 | ), 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Overview 6 | 7 | This is the DatoCMS CLI - a monorepo containing CLI tools for managing DatoCMS projects, environments, and schemas. It includes: 8 | 9 | - `@datocms/cli`: Main CLI package with environment management, migrations, and maintenance commands 10 | - `@datocms/cli-plugin-wordpress`: WordPress import functionality 11 | - `@datocms/cli-plugin-contentful`: Contentful import functionality 12 | - `@datocms/cli-utils`: Shared utilities and base commands 13 | 14 | ## Architecture 15 | 16 | The codebase uses **Lerna** for monorepo management with packages organized under `packages/`. Each package is built independently using TypeScript. 17 | 18 | ### Key Components 19 | 20 | **CLI Core (`packages/cli/`)**: 21 | - Built on oclif framework for CLI command structure 22 | - Commands organized by topic: `environments`, `migrations`, `maintenance`, `profile` 23 | - Uses `environments-diff` utility for schema synchronization between environments 24 | - Migration system with timestamped files in `migrations/` directory 25 | 26 | **Plugin Architecture**: 27 | - WordPress and Contentful plugins extend base functionality 28 | - Both plugins follow similar patterns with step-based imports and validation 29 | - Base command classes in `cli-utils` provide shared functionality 30 | 31 | **Common Patterns**: 32 | - All packages use TypeScript with strict configuration 33 | - Commands extend base classes from `@datocms/cli-utils` 34 | - API interactions through DatoCMS REST clients 35 | - Step-based processing for complex operations (imports, migrations) 36 | 37 | ## Development Commands 38 | 39 | ```bash 40 | # Initial setup 41 | npm install 42 | lerna bootstrap 43 | npm run build 44 | 45 | # Development workflow 46 | npm run format # Format and fix code with Biome 47 | npm run lint # Check code quality with Biome 48 | npm run build # Build all packages with Lerna 49 | npm run test # Run tests (individual packages) 50 | 51 | # Publishing 52 | npm run publish # Test, build, and publish to npm 53 | npm run publish-next # Publish with next tag 54 | ``` 55 | 56 | ### Individual Package Commands 57 | 58 | Each package supports: 59 | ```bash 60 | cd packages/cli 61 | npm run build # TypeScript compilation 62 | npm run test # Mocha tests 63 | npm run prepack # Build + generate oclif manifest 64 | ``` 65 | 66 | ## Testing 67 | 68 | - Uses **Mocha** with TypeScript support via `ts-node` 69 | - Test files follow pattern `test/**/*.test.ts` 70 | - Individual packages run tests independently 71 | - No unified test runner - each package manages its own tests 72 | 73 | ## Code Quality 74 | 75 | - **Biome** for linting and formatting (configured in `biome.json`) 76 | - **Husky** + **lint-staged** for pre-commit hooks 77 | - TypeScript strict mode enabled 78 | - Uses single quotes, space indentation 79 | - Ignores generated `lib/` directories 80 | 81 | ## Migration System 82 | 83 | The CLI includes a migration system (`packages/cli/migrations/`) for schema changes: 84 | - Timestamped migration files (format: `TIMESTAMP_description.ts`) 85 | - Use `datocms migrations:new` to create new migrations 86 | - Use `datocms migrations:run` to execute pending migrations -------------------------------------------------------------------------------- /packages/cli/src/utils/environments-diff/resources/delete-missing-fields-and-fieldsets-in-existing-item-types.ts: -------------------------------------------------------------------------------- 1 | import type { CmaClient } from '@datocms/cli-utils'; 2 | import { difference, intersection } from 'lodash'; 3 | import type { Command, ItemTypeInfo, Schema } from '../types'; 4 | import { 5 | buildFieldTitle, 6 | buildFieldsetTitle, 7 | buildItemTypeTitle, 8 | } from '../utils'; 9 | import { buildComment } from './comments'; 10 | 11 | export function buildDestroyFieldClientCommand( 12 | field: CmaClient.RawApiTypes.Field, 13 | itemType: CmaClient.RawApiTypes.ItemType, 14 | ): Command[] { 15 | return [ 16 | buildComment( 17 | `Delete ${buildFieldTitle(field)} in ${buildItemTypeTitle(itemType)}`, 18 | ), 19 | { 20 | type: 'apiCallClientCommand', 21 | call: 'client.fields.destroy', 22 | arguments: [field.id], 23 | }, 24 | ]; 25 | } 26 | 27 | export function buildDestroyFieldsetClientCommand( 28 | fieldset: CmaClient.RawApiTypes.Fieldset, 29 | itemType: CmaClient.RawApiTypes.ItemType, 30 | ): Command[] { 31 | return [ 32 | buildComment( 33 | `Delete ${buildFieldsetTitle(fieldset)} in ${buildItemTypeTitle( 34 | itemType, 35 | )}`, 36 | ), 37 | { 38 | type: 'apiCallClientCommand', 39 | call: 'client.fieldsets.destroy', 40 | arguments: [fieldset.id], 41 | }, 42 | ]; 43 | } 44 | 45 | export function deleteMissingFieldsAndFieldsetsInExistingItemType( 46 | newItemTypeSchema: ItemTypeInfo, 47 | oldItemTypeSchema: ItemTypeInfo, 48 | ): Command[] { 49 | const oldFieldIds = Object.keys(oldItemTypeSchema.fieldsById); 50 | const newFieldIds = Object.keys(newItemTypeSchema.fieldsById); 51 | 52 | const deletedFieldIds = difference(oldFieldIds, newFieldIds); 53 | 54 | const oldFieldsetIds = Object.keys(oldItemTypeSchema.fieldsetsById); 55 | const newFieldsetIds = Object.keys(newItemTypeSchema.fieldsetsById); 56 | 57 | const deletedFieldsetsIds = difference(oldFieldsetIds, newFieldsetIds); 58 | 59 | return [ 60 | ...deletedFieldsetsIds.flatMap((fieldsetId) => 61 | buildDestroyFieldsetClientCommand( 62 | oldItemTypeSchema.fieldsetsById[fieldsetId], 63 | newItemTypeSchema.entity, 64 | ), 65 | ), 66 | ...deletedFieldIds.flatMap((fieldId) => 67 | buildDestroyFieldClientCommand( 68 | oldItemTypeSchema.fieldsById[fieldId], 69 | newItemTypeSchema.entity, 70 | ), 71 | ), 72 | ]; 73 | } 74 | 75 | export function deleteMissingFieldsAndFieldsetsInExistingItemTypes( 76 | newSchema: Schema, 77 | oldSchema: Schema, 78 | ): Command[] { 79 | const newItemTypeIds = Object.keys(newSchema.itemTypesById); 80 | const oldItemTypeIds = Object.keys(oldSchema.itemTypesById); 81 | 82 | const keptItemTypeIds = intersection(newItemTypeIds, oldItemTypeIds); 83 | 84 | const destroyCommands = keptItemTypeIds.flatMap((itemTypeId) => 85 | deleteMissingFieldsAndFieldsetsInExistingItemType( 86 | newSchema.itemTypesById[itemTypeId], 87 | oldSchema.itemTypesById[itemTypeId], 88 | ), 89 | ); 90 | 91 | if (destroyCommands.length === 0) { 92 | return []; 93 | } 94 | 95 | return [ 96 | buildComment('Destroy fields in existing models/block models'), 97 | ...destroyCommands, 98 | ]; 99 | } 100 | -------------------------------------------------------------------------------- /packages/cli-plugin-contentful/src/utils/build-contentful-client.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CmaClient, 3 | type LogLevelFlagEnum, 4 | logLevelMap, 5 | } from '@datocms/cli-utils'; 6 | import { 7 | type ClientAPI, 8 | type Environment, 9 | createClient, 10 | } from 'contentful-management'; 11 | 12 | type ContentfulClientType = { 13 | contentfulToken: string | undefined; 14 | contentfulSpaceId: string | undefined; 15 | contentfulEnvironment: string | undefined; 16 | logLevel?: LogLevelFlagEnum; 17 | logFn?: (message: string) => void; 18 | }; 19 | 20 | let requestCount = 1; 21 | 22 | export async function cfEnvironmentApi({ 23 | contentfulToken, 24 | contentfulSpaceId, 25 | contentfulEnvironment = 'master', 26 | logLevel: logLevelString = 'NONE', 27 | logFn: log = () => true, 28 | }: ContentfulClientType): Promise { 29 | if (!(contentfulToken && contentfulSpaceId)) { 30 | throw new Error( 31 | 'You need to provide a read-only Contentful API token and a Contentful space ID!', 32 | ); 33 | } 34 | 35 | const logLevel = logLevelMap[logLevelString]; 36 | 37 | const contentfulClient: ClientAPI = createClient({ 38 | accessToken: contentfulToken, 39 | onBeforeRequest: (requestConfig) => { 40 | const requestId = `CF${requestCount}`; 41 | 42 | requestCount += 1; 43 | 44 | return { ...requestConfig, __requestId: requestId }; 45 | }, 46 | 47 | responseLogger: (response) => { 48 | if (response instanceof Error) { 49 | return; 50 | } 51 | 52 | const { config: request, status, statusText } = response; 53 | const { 54 | url, 55 | method, 56 | __requestId: requestId, 57 | } = request as typeof request & { 58 | __requestId?: number; 59 | }; 60 | 61 | if (logLevel >= CmaClient.LogLevel.BASIC) { 62 | log(`[${requestId}] ${method?.toUpperCase()} ${url}`); 63 | if (logLevel >= CmaClient.LogLevel.BODY_AND_HEADERS) { 64 | for (const [key, value] of Object.entries(request.headers || {})) { 65 | log(`[${requestId}] ${key}: ${value}`); 66 | } 67 | } 68 | if (logLevel >= CmaClient.LogLevel.BODY && request.data) { 69 | log(`[${requestId}] ${JSON.stringify(request.data, null, 2)}`); 70 | } 71 | } 72 | 73 | if (logLevel >= CmaClient.LogLevel.BASIC) { 74 | log(`[${requestId}] Status: ${status} (${statusText})`); 75 | if (logLevel >= CmaClient.LogLevel.BODY_AND_HEADERS) { 76 | for (const [key, value] of Object.entries(response.headers || {})) { 77 | log(`[${requestId}] ${key}: ${value}`); 78 | } 79 | } 80 | if (logLevel >= CmaClient.LogLevel.BODY && response.data) { 81 | log(`[${requestId}] ${JSON.stringify(response.data, null, 2)}`); 82 | } 83 | } 84 | }, 85 | }); 86 | 87 | const contentful = await contentfulClient.getSpace(contentfulSpaceId); 88 | const environments = await contentful.getEnvironments(); 89 | const environment = environments.items.find( 90 | (e) => e.name === contentfulEnvironment, 91 | ); 92 | 93 | if (!environment) { 94 | throw new Error( 95 | `Could not find environment named "${contentfulEnvironment}"!`, 96 | ); 97 | } 98 | 99 | return environment; 100 | } 101 | -------------------------------------------------------------------------------- /packages/cli-plugin-wordpress/README.md: -------------------------------------------------------------------------------- 1 | # DatoCMS WordPress Import CLI 2 | 3 | DatoCMS CLI plugin to import a WordPress site into a DatoCMS project. 4 | 5 | 6 | * [DatoCMS WordPress Import CLI](#datocms-wordpress-import-cli) 7 | * [Usage](#usage) 8 | * [Commands](#commands) 9 | * [Development](#development) 10 | 11 | 12 |

13 | 14 | 15 | 16 |

17 | 18 | # Usage 19 | 20 | ```sh-session 21 | $ npm install -g @datocms/cli 22 | $ datocms plugins:install @datocms/cli-plugin-wordpress 23 | $ datocms wordpress:import --help 24 | ``` 25 | 26 | # Commands 27 | 28 | 29 | * [`@datocms/cli-plugin-wordpress wordpress:import`](#datocmscli-plugin-wordpress-wordpressimport) 30 | 31 | ## `@datocms/cli-plugin-wordpress wordpress:import` 32 | 33 | Imports a WordPress site into a DatoCMS project 34 | 35 | ``` 36 | USAGE 37 | $ @datocms/cli-plugin-wordpress wordpress:import --wp-username --wp-password [--json] [--config-file 38 | ] [--profile ] [--api-token ] [--log-level NONE|BASIC|BODY|BODY_AND_HEADERS] [--log-mode 39 | stdout|file|directory] [--wp-json-api-url | --wp-url ] [--autoconfirm] [--ignore-errors] 40 | [--concurrency ] 41 | 42 | FLAGS 43 | --autoconfirm Automatically enters the affirmative response to all confirmation prompts, enabling the 44 | command to execute without waiting for user confirmation. Forces the destroy of existing 45 | "wp_*" models. 46 | --concurrency= [default: 15] Maximum number of operations to be run concurrently 47 | --ignore-errors Try to ignore errors encountered during import 48 | --wp-json-api-url= The endpoint for your WordPress install (ex. https://www.wordpress-website.com/wp-json) 49 | --wp-password= (required) WordPress password 50 | --wp-url= A URL within a WordPress REST API-enabled site (ex. https://www.wordpress-website.com) 51 | --wp-username= (required) WordPress username 52 | 53 | GLOBAL FLAGS 54 | --api-token= Specify a custom API key to access a DatoCMS project 55 | --config-file= [default: ./datocms.config.json] Specify a custom config file path 56 | --json Format output as json. 57 | --log-level=