From b804c13ed10eed55d53e39f53f9ee6f043088a7c Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Thu, 4 Jun 2026 19:11:44 +0200 Subject: [PATCH 1/7] SP-874: add 'config package import' command for single-package import Adds the CLI counterpart to the new pacman single-package import API. The command lives under a new 'config package' subgroup, separate from the existing batch 'config import', because the two use different, non-interchangeable archive formats. Supports importing from a package zip (--file) or a directory (--directory, flat-zipped by the CLI), with --overwrite and optional --json output. Config docs and CLI help updated to frame the batch commands as their own batch-specific set. Includes-AI-Code: true Co-authored-by: Cursor --- docs/user-guide/config-commands.md | 117 ++++++++++- .../api/single-package-import-api.ts | 21 ++ .../single-package-import.interfaces.ts | 21 ++ .../configuration-management/module.ts | 29 ++- .../single-package-import.service.ts | 103 ++++++++++ src/core/utils/file-service.ts | 16 ++ .../configuration-management/module.spec.ts | 59 +++++- .../single-package-import.spec.ts | 189 ++++++++++++++++++ tests/core/utils/file.service.spec.ts | 27 +++ 9 files changed, 573 insertions(+), 9 deletions(-) create mode 100644 src/commands/configuration-management/api/single-package-import-api.ts create mode 100644 src/commands/configuration-management/interfaces/single-package-import.interfaces.ts create mode 100644 src/commands/configuration-management/single-package-import.service.ts create mode 100644 tests/commands/configuration-management/single-package-import.spec.ts diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index 697c72a8..d823e27e 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -1,6 +1,36 @@ # Configuration Management Commands -The `config` command group allows you to list, batch export, import packages of different flavors such as Studio and OCDM packages. +The `config` command group manages package configurations. Most of its commands work with a package and its resources — importing a package, working with nodes, versions, or variables, and validating configurations. Separately, it includes a set of **batch commands** built for specific bulk use-cases, such as moving many packages at once between teams and realms. + +The batch commands are **batch-specific**: they use their own archive format that only the batch commands understand, so their artifacts are **not interchangeable** with the package commands (see [Two command families](#two-command-families)). + +- **Package commands** — work with a package and its resources: + - [`config package import`](#package-commands-config-package) — import a package + - [`config nodes …`](#finding-nodes) — work with individual nodes + - [`config versions …`](#package-version) — read and create package versions + - [`config variables list`](#listing--mapping-variables) — read package variables + - [`config validate`](#validate-package-configurations) — validate a package's staging version +- **Batch commands** — bulk multi-package transport for specific use-cases: + - [`config list`](#list-packages), [`config export`](#batch-export-packages), [`config import`](#batch-import-packages), [`config diff`](#diff-local-zip-with-deployed-versionspecific-versionstaging), [`config metadata export`](#batch-export-packages) + +## Two command families + +The batch commands are a **self-contained, batch-specific set**. `config export` produces a multi-package **batch archive** — a top-level `manifest.json`, `variables.json`, `studio.json`, and one nested `_.zip` per package — that is produced and consumed **only** by other batch commands. `config package import`, by contrast, works with a plain **package zip** (a `package.json`, an optional `variables.json`, and a `nodes/` folder). The two formats are **not interchangeable**: + +- An archive from `config export` can be imported with `config import` or inspected with `config diff` — but **not** with `config package import`. +- A package zip used by `config package import` **cannot** be imported with `config import` or diffed with `config diff`. + +| Command | Group | Artifact it reads / writes | +|---|---|---| +| `config package import` | Package commands | Package zip/dir (`package.json`, optional `variables.json`, `nodes/`) | +| `config nodes …` | Package commands | Node JSON payload | +| `config validate`, `config versions …`, `config variables list` | Package commands | — (operate directly on the platform) | +| `config export` | Batch commands | Batch archive (multi-package) | +| `config import` | Batch commands | Batch archive (multi-package) | +| `config diff` | Batch commands | Batch archive (multi-package) | +| `config list`, `config metadata export` | Batch commands | — (JSON list output) | + +**Which should I use?** Reach for the batch commands only for their specific bulk use-cases — moving a set of packages together (for example, a migration between teams). For everything else, use the package commands. ## Permissions @@ -18,7 +48,7 @@ This applies to every command that reads or modifies a single package or its nod - `config variables list` - `config versions get`, `config versions create` - `config validate`, `config diff` -- `config export`, `config import`, `config metadata export` +- `config export`, `config import`, `config package import`, `config metadata export` If the authenticated profile does not have the required permission, the command fails with `Access is Denied`. @@ -26,6 +56,8 @@ If the authenticated profile does not have the required permission, the command ## List Packages +> **Batch command.** Part of the [batch family](#two-command-families) — a bulk listing utility typically used to discover packages before a `config export`. + Packages can be listed using the following command: ```bash @@ -73,6 +105,8 @@ content-cli config list -p --packageKeys key1 ... keyN ## Batch Export Packages +> **Batch command.** Part of the [batch family](#two-command-families). It produces a multi-package **batch archive** that can only be re-imported with [`config import`](#batch-import-packages) or inspected with [`config diff`](#diff-local-zip-with-deployed-versionspecific-versionstaging) — **not** with `config package import`. To work with one package, use [`config package import`](#package-commands-config-package). + Packages can be exported using the following command: ```bash @@ -127,6 +161,8 @@ Inside the nodes directory, a file for each node will be present: ## Batch Import Packages +> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch archive** produced by [`config export`](#batch-export-packages). To import one package from a package zip, use [`config package import`](#package-commands-config-package) instead. + Packages can be imported using the following commands, if importing from a zip file: ```bash @@ -177,6 +213,81 @@ content-cli config import -p -d --validate --overwr `config import --validate` runs the **SCHEMA** layer only. It does **not** run BUSINESS-layer checks (PQL parsing, data-model availability, KPI uniqueness, etc.) or PACKAGE_SETTINGS checks (package dependencies, variables, and flavor-specific package settings). To run those validations, use [`config validate`](#validate-package-configurations) after the import. +## Package Commands (`config package`) + +The `config package` command group works with a package and its contents. It is **not** part of the batch-specific set — it uses the plain package format described below, which is not interchangeable with the batch archive (see [Two command families](#two-command-families)). + +### Import a Package + +`config package import` imports a package from a package zip (or directory). Unlike [`config import`](#batch-import-packages) — which performs a **batch** import and expects the multi-package batch archive (`manifest.json`, a top-level `variables.json`, `studio.json`, and a nested `_.zip` per package) — `config package import` takes a plain, flat package layout and imports it on its own. + +> A zip produced by `config export` is a **batch archive** and cannot be imported with `config package import`. Likewise, a package zip cannot be imported with `config import`. Use the command that matches how the artifact was produced. + +```bash +content-cli config package import -p -f +``` + +Where `-f` is the shorthand for `--file`. You can also point at an unzipped directory with `-d` / `--directory`, in which case the CLI zips it for you before uploading: + +```bash +content-cli config package import -p -d +``` + +`--file` and `--directory` are mutually exclusive — provide exactly one. + +This command requires **edit permission** on the target package, or **create** permission when the package does not yet exist (see [Permissions](#permissions)). + +#### Package Zip Format + +The zip (or directory) must contain a package in the following flat layout: + +```bash +package/ +├─ package.json # package metadata and configuration (key, name, type, flavor, configuration) +├─ variables.json # optional — variable assignments for the package +├─ nodes/ +│ ├─ .json # one file per node +│ ├─ ... +``` + +- `package.json` is the **source of truth for variable declarations**. Every assignment in `variables.json` must reference a variable declared in `package.json`, otherwise the import is rejected. +- `variables.json` is optional. If you do not want to import variable assignments, simply omit the file. + +This is intentionally different from the batch archive: there is no `manifest.json`, no `studio.json`, and no nested per-package zips — just the one package's files at the top level. + +#### Overwriting an Existing Package + +By default the import fails if a package with the same key already exists. Use `--overwrite` to replace it: + +```bash +content-cli config package import -p -f --overwrite +``` + +When overwriting, variable assignments whose key is no longer declared in the imported `package.json` are pruned, keeping declarations and assignments consistent. + +#### Output + +On success, the command prints a summary of the imported package and its nodes to the console: + +```bash +info: Successfully imported package: my-package +info: Name: My Package +info: Flavor: STUDIO +info: Imported 2 node(s). +info: - my-view (VIEW) +info: - my-knowledge-model (KNOWLEDGE_MODEL) +``` + +Use `--json` to write the full import result (imported package and nodes) to a JSON file in the current working directory instead: + +```bash +content-cli config package import -p -f --json +``` + +```bash +info: File downloaded successfully. New filename: 9560f81f-f746-4117-83ee-dd1f614ad624.json +``` + ## Validate Package Configurations The `config validate` command validates the **staging (draft) version** of a package by sending its nodes through one or more validation layers. The command runs against the Pacman validate API and returns a structured report of errors, warnings, and info findings. @@ -832,6 +943,8 @@ content-cli config nodes dependencies list --packageKey --nodeKey < ## Diff local zip with deployed version/specific version/staging +> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch archive** produced by [`config export`](#batch-export-packages); a package zip is not supported here. + To compare local zipped packages with online packages use: ```bash content-cli config diff --file diff --git a/src/commands/configuration-management/api/single-package-import-api.ts b/src/commands/configuration-management/api/single-package-import-api.ts new file mode 100644 index 00000000..2c786ba9 --- /dev/null +++ b/src/commands/configuration-management/api/single-package-import-api.ts @@ -0,0 +1,21 @@ +import * as FormData from "form-data"; +import { HttpClient } from "../../../core/http/http-client"; +import { Context } from "../../../core/command/cli-context"; +import { SinglePackageImportResult } from "../interfaces/single-package-import.interfaces"; + +export class SinglePackageImportApi { + + private httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async importPackage(data: FormData, overwrite: boolean): Promise { + return this.httpClient().postFile( + "/pacman/api/core/staging/packages/import-file", + data, + { overwrite } + ); + } +} diff --git a/src/commands/configuration-management/interfaces/single-package-import.interfaces.ts b/src/commands/configuration-management/interfaces/single-package-import.interfaces.ts new file mode 100644 index 00000000..0c34d529 --- /dev/null +++ b/src/commands/configuration-management/interfaces/single-package-import.interfaces.ts @@ -0,0 +1,21 @@ +import { NodeTransport } from "./node.interfaces"; + +export interface PackageTransport { + id: string; + key: string; + name: string; + type?: string; + version?: string; + flavor?: string; + schemaVersion?: number; + createdBy?: string; + updatedBy?: string; + creationDate?: string; + changeDate?: string; + [key: string]: any; +} + +export interface SinglePackageImportResult { + importedPackage: PackageTransport; + importedNodes: NodeTransport[]; +} diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index bd643f0e..ebbecb90 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -12,13 +12,15 @@ import { NodeDiffService } from "./node-diff.service"; import { NodeDependencyService } from "./node-dependency.service"; import { PackageVersionCommandService } from "./package-version-command.service"; import { PackageValidationService } from "./package-validation.service"; +import { SinglePackageImportService } from "./single-package-import.service"; class Module extends IModule { public register(context: Context, configurator: Configurator): void { - const configCommand = configurator.command("config"); + const configCommand = configurator.command("config") + .description("Manage package configurations. Most commands work with a package and its resources. The batch commands (list, export, import, diff, metadata) are a separate, batch-specific set for bulk multi-package transport: their archive format is only understood by other batch commands and is not interchangeable with the package commands."); configCommand.command("list") - .description("Command to list packages") + .description("[Batch] List packages in the target team. Part of the batch export/import workflow.") .option("--json", "Return response as json type", "") .option("--flavors ", "Lists only packages of the given flavors") .option("--withDependencies", "Include dependencies", "") @@ -31,7 +33,7 @@ class Module extends IModule { .action(this.listPackages); configCommand.command("export") - .description("Command to export package configs") + .description("[Batch] Export one or more packages into a batch archive. The archive only works with the batch commands ('config import' / 'config diff') and cannot be imported with 'config package import'.") .option("--packageKeys ", "Keys of packages to export. Exports the latest deployed version only") .option("--keysByVersion ", "Keys of packages to export by version") .option("--withDependencies", "Include variables and dependencies", "") @@ -45,13 +47,13 @@ class Module extends IModule { metadataCommand .command("export") - .description("Command to show whether packages have unpublished changes") + .description("[Batch] Show whether multiple packages have unpublished changes (bulk metadata export).") .requiredOption("--packageKeys ", "Keys of packages to find the metadata of") .option("--json", "Return response as json type", "") .action(this.batchExportPackagesMetadata); configCommand.command("import") - .description("Command to import package configs") + .description("[Batch] Import packages from a batch archive produced by 'config export'. To import one package, use 'config package import' instead.") .option("--overwrite", "Flag to allow overwriting of packages") .option("--validate", "Validate node configurations before import", false) .option("--gitProfile ", "Git profile which you want to use for the Git operations") @@ -60,6 +62,17 @@ class Module extends IModule { .option("-d, --directory ", "Exported packages directory (relative path)") .action(this.batchImportPackages); + const packageCommand = configCommand.command("package") + .description("Commands for working with a package."); + + packageCommand.command("import") + .description("Import a package from a zip file or directory. Uses the package format, which is not interchangeable with the batch 'config export' / 'config import' archive.") + .option("-f, --file ", "Package zip file (relative path)") + .option("-d, --directory ", "Package directory (relative path)") + .option("--overwrite", "Flag to allow overwriting an existing package with the same key") + .option("--json", "Return the response as a JSON file") + .action(this.importSinglePackage); + configCommand.command("validate") .description("Validate package node configurations") .requiredOption("--packageKey ", "Key of the package to validate") @@ -73,7 +86,7 @@ class Module extends IModule { .action(this.validatePackage); configCommand.command("diff") - .description("Command to diff configs of packages") + .description("[Batch] Diff a local batch archive (from 'config export') against deployed or staging packages.") .option("--hasChanges", "Flag to return only the information if the package has changes without the actual changes") .option("--baseVersion ", "Compare against a given version or STAGING") .option("--json", "Return the response as a JSON file") @@ -256,6 +269,10 @@ class Module extends IModule { await new ConfigCommandService(context).batchImportPackages(options.file, options.directory, options.overwrite, options.gitBranch, options.validate); } + private async importSinglePackage(context: Context, command: Command, options: OptionValues): Promise { + await new SinglePackageImportService(context).importPackage(options.file, options.directory, options.overwrite, options.json); + } + private async diffPackages(context: Context, command: Command, options: OptionValues): Promise { await new ConfigCommandService(context).diffPackages(options.file, options.hasChanges, options.baseVersion, options.json); } diff --git a/src/commands/configuration-management/single-package-import.service.ts b/src/commands/configuration-management/single-package-import.service.ts new file mode 100644 index 00000000..72f78202 --- /dev/null +++ b/src/commands/configuration-management/single-package-import.service.ts @@ -0,0 +1,103 @@ +import { v4 as uuidv4 } from "uuid"; +import * as FormData from "form-data"; +import { Readable } from "stream"; +import * as AdmZip from "adm-zip"; +import * as fs from "fs"; +import { Context } from "../../core/command/cli-context"; +import { fileService, FileService } from "../../core/utils/file-service"; +import { logger } from "../../core/utils/logger"; +import { SinglePackageImportApi } from "./api/single-package-import-api"; +import { SinglePackageImportResult } from "./interfaces/single-package-import.interfaces"; + +export class SinglePackageImportService { + + private static readonly MAX_UNCOMPRESSED_ZIP_SIZE = 4 * 1024 * 1024 * 1024; + + private singlePackageImportApi: SinglePackageImportApi; + + constructor(context: Context) { + this.singlePackageImportApi = new SinglePackageImportApi(context); + } + + public async importPackage(file: string, directory: string, overwrite: boolean, jsonResponse: boolean): Promise { + const resolvedSource = this.resolveSource(file, directory); + + try { + const packageZip = new AdmZip(resolvedSource.zipPath); + const formData = this.buildBodyForImport(packageZip, resolvedSource.zipPath); + const result = await this.singlePackageImportApi.importPackage(formData, overwrite); + this.outputResult(result, jsonResponse); + } finally { + if (resolvedSource.isTemporary) { + fs.rmSync(resolvedSource.zipPath); + } + } + } + + private resolveSource(file: string, directory: string): { zipPath: string; isTemporary: boolean } { + if (file && directory) { + throw new Error("You cannot use both --file and --directory options at the same time. Only one import source can be defined."); + } + if (!file && !directory) { + throw new Error("You must provide either a --file or a --directory option to import a package."); + } + if (file) { + if (fileService.isDirectory(file)) { + throw new Error("The --file option accepts only zip files."); + } + return { zipPath: file, isTemporary: false }; + } + if (!fileService.isDirectory(directory)) { + throw new Error("The --directory option accepts only directories."); + } + return { zipPath: fileService.zipDirectoryAsSinglePackage(directory), isTemporary: true }; + } + + private buildBodyForImport(packageZip: AdmZip, sourcePath: string): FormData { + this.assertUncompressedSizeWithinLimit(packageZip, sourcePath); + + const formData = new FormData(); + formData.append("packageFile", this.getReadableStream(packageZip), { filename: "package.zip" }); + return formData; + } + + private assertUncompressedSizeWithinLimit(packageZip: AdmZip, sourcePath: string): void { + const totalUncompressedBytes = packageZip.getEntries().reduce((sum, entry) => sum + entry.header.size, 0); + if (totalUncompressedBytes > SinglePackageImportService.MAX_UNCOMPRESSED_ZIP_SIZE) { + throw new Error( + `Failed to handle zip file "${sourcePath}": uncompressed size ${(totalUncompressedBytes / (1024 ** 3)).toFixed(2)} GB exceeds the 4 GB limit.` + ); + } + } + + private getReadableStream(packageZip: AdmZip): Readable { + return new Readable({ + read(): void { + this.push(packageZip.toBuffer()); + this.push(null); + }, + }); + } + + private outputResult(result: SinglePackageImportResult, jsonResponse: boolean): void { + if (jsonResponse) { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(result, null, 2), filename); + logger.info(FileService.fileDownloadedMessage + filename); + return; + } + + const importedPackage = result.importedPackage; + const importedNodes = result.importedNodes ?? []; + + logger.info(`Successfully imported package: ${importedPackage.key}`); + logger.info(`Name: ${importedPackage.name}`); + if (importedPackage.flavor) { + logger.info(`Flavor: ${importedPackage.flavor}`); + } + logger.info(`Imported ${importedNodes.length} node(s).`); + importedNodes.forEach(node => { + logger.info(` - ${node.key} (${node.type})`); + }); + } +} diff --git a/src/core/utils/file-service.ts b/src/core/utils/file-service.ts index 2f6ce9ef..cbf232bb 100644 --- a/src/core/utils/file-service.ts +++ b/src/core/utils/file-service.ts @@ -90,6 +90,22 @@ export class FileService { return zipFilePath; } + public zipDirectoryAsSinglePackage(sourceDir: string): string { + if (fs.lstatSync(sourceDir).isSymbolicLink()) { + throw new FatalError("Source directory cannot be a symbolic link."); + } + + const zip = new AdmZip(); + zip.addLocalFolder(sourceDir); + + const tempDir = path.join(os.tmpdir(), "content-cli-imports"); + fs.mkdirSync(tempDir, { recursive: true, mode: FileConstants.DEFAULT_FOLDER_PERMISSIONS }); + const zipFilePath = path.join(tempDir, `single_package_${uuidv4()}.zip`); + zip.writeZip(zipFilePath, () => fs.chmodSync(zipFilePath, FileConstants.DEFAULT_FILE_PERMISSIONS)); + + return zipFilePath; + } + private getSerializedFileContent(data: any): string { return data; diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index fc7f77dd..80ef7eab 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -4,6 +4,7 @@ import { ConfigCommandService } from "../../../src/commands/configuration-manage import { NodeDependencyService } from "../../../src/commands/configuration-management/node-dependency.service"; import { PackageVersionCommandService } from "../../../src/commands/configuration-management/package-version-command.service"; import { NodeDiffService } from "../../../src/commands/configuration-management/node-diff.service"; +import { SinglePackageImportService } from "../../../src/commands/configuration-management/single-package-import.service"; import { testContext } from "../../utls/test-context"; import { createMockConfigurator } from "../../utls/configurator-mock"; @@ -11,6 +12,7 @@ jest.mock("../../../src/commands/configuration-management/config-command.service jest.mock("../../../src/commands/configuration-management/node-dependency.service"); jest.mock("../../../src/commands/configuration-management/node-diff.service"); jest.mock("../../../src/commands/configuration-management/package-version-command.service"); +jest.mock("../../../src/commands/configuration-management/single-package-import.service"); /** Mirrors default values on `config variables list` Commander options (keep in sync with module.ts). */ const variablesListOptionDefaults: OptionValues = { @@ -25,6 +27,7 @@ describe("Configuration Management Module - Action Validations", () => { let mockConfigCommandService: jest.Mocked; let mockNodeDependencyService: jest.Mocked; let mockNodeDiffService: jest.Mocked; + let mockSinglePackageImportService: jest.Mocked; beforeEach(() => { jest.clearAllMocks(); @@ -48,9 +51,14 @@ describe("Configuration Management Module - Action Validations", () => { diffWithFile: jest.fn().mockResolvedValue(undefined), } as any; + mockSinglePackageImportService = { + importPackage: jest.fn().mockResolvedValue(undefined), + } as any; + (ConfigCommandService as jest.MockedClass).mockImplementation(() => mockConfigCommandService); (NodeDependencyService as jest.MockedClass).mockImplementation(() => mockNodeDependencyService); (NodeDiffService as jest.MockedClass).mockImplementation(() => mockNodeDiffService); + (SinglePackageImportService as jest.MockedClass).mockImplementation(() => mockSinglePackageImportService); }); describe("listActivePackages validation", () => { @@ -537,6 +545,55 @@ describe("Configuration Management Module - Action Validations", () => { }); }); + describe("importSinglePackage", () => { + it("should pass file option correctly", async () => { + const options: OptionValues = { + file: "single-package.zip", + }; + + await (module as any).importSinglePackage(testContext, mockCommand, options); + + expect(mockSinglePackageImportService.importPackage).toHaveBeenCalledWith( + "single-package.zip", + undefined, + undefined, + undefined + ); + }); + + it("should pass directory option correctly", async () => { + const options: OptionValues = { + directory: "./single-package-dir", + }; + + await (module as any).importSinglePackage(testContext, mockCommand, options); + + expect(mockSinglePackageImportService.importPackage).toHaveBeenCalledWith( + undefined, + "./single-package-dir", + undefined, + undefined + ); + }); + + it("should pass overwrite and json options correctly", async () => { + const options: OptionValues = { + file: "single-package.zip", + overwrite: true, + json: true, + }; + + await (module as any).importSinglePackage(testContext, mockCommand, options); + + expect(mockSinglePackageImportService.importPackage).toHaveBeenCalledWith( + "single-package.zip", + undefined, + true, + true + ); + }); + }); + describe("listVariables validation", () => { it("should throw when --packageKeys and --keysByVersion are both provided", async () => { const options: OptionValues = { @@ -725,7 +782,7 @@ describe("Configuration Management Module - Action Validations", () => { // Each leaf command terminates the fluent chain with .action(handler). // Keep this count in sync when adding or removing commands in module.ts. - const expectedLeafCommands = 17; + const expectedLeafCommands = 18; expect(mockConfigurator.action).toHaveBeenCalledTimes(expectedLeafCommands); for (const call of mockConfigurator.action.mock.calls) { expect(typeof call[0]).toBe("function"); diff --git a/tests/commands/configuration-management/single-package-import.spec.ts b/tests/commands/configuration-management/single-package-import.spec.ts new file mode 100644 index 00000000..b001534d --- /dev/null +++ b/tests/commands/configuration-management/single-package-import.spec.ts @@ -0,0 +1,189 @@ +import * as path from "path"; +import * as fs from "fs"; +import AdmZip = require("adm-zip"); +import { mockCreateReadStream, mockExistsSync, mockReadFileSync } from "../../utls/fs-mock-utils"; + +jest.mock("adm-zip", () => { + const realAdmZip = jest.requireActual("adm-zip"); + return jest.fn((...args: any[]) => realAdmZip(...args)); +}); + +import { mockAxiosPost, mockedAxiosInstance, mockedPostRequestBodyByUrl } from "../../utls/http-requests-mock"; +import { SinglePackageImportService } from "../../../src/commands/configuration-management/single-package-import.service"; +import { SinglePackageImportResult } from "../../../src/commands/configuration-management/interfaces/single-package-import.interfaces"; +import { testContext } from "../../utls/test-context"; +import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; + +const IMPORT_URL = "https://myTeam.celonis.cloud/pacman/api/core/staging/packages/import-file"; + +function buildSinglePackageZip(): AdmZip { + const zip = new AdmZip(); + zip.addFile("package.json", Buffer.from(JSON.stringify({ + key: "pkg-1", + name: "My Package", + type: "PACKAGE", + flavor: "STUDIO", + schemaVersion: 1, + configuration: { variables: [] }, + }))); + zip.addFile("nodes/", Buffer.alloc(0)); + zip.addFile("nodes/node-1.json", Buffer.from(JSON.stringify({ key: "node-1", type: "VIEW" }))); + return zip; +} + +function buildImportResponse(): SinglePackageImportResult { + return { + importedPackage: { + id: "package-id-1", + key: "pkg-1", + name: "My Package", + flavor: "STUDIO", + }, + importedNodes: [ + { + id: "node-id-1", + key: "node-1", + name: "My View", + packageNodeKey: "pkg-1", + packageNodeId: "package-node-id", + type: "VIEW", + invalidContent: false, + creationDate: "2025-01-01T00:00:00.000Z", + changeDate: "2025-01-01T00:00:00.000Z", + createdBy: "user@celonis.com", + updatedBy: "user@celonis.com", + schemaVersion: 1, + }, + ], + }; +} + +describe("Single package import", () => { + + beforeEach(() => { + mockExistsSync(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it.each([true, false])("Should import a single package from a zip file with overwrite %p", async (overwrite: boolean) => { + const packageZip = buildSinglePackageZip(); + mockReadFileSync(packageZip.toBuffer()); + mockCreateReadStream(packageZip.toBuffer()); + + const importResponse = buildImportResponse(); + mockAxiosPost(IMPORT_URL, importResponse); + + await new SinglePackageImportService(testContext).importPackage("./package.zip", null, overwrite, false); + + expect(mockedAxiosInstance.post).toHaveBeenCalledWith( + IMPORT_URL, + expect.anything(), + expect.objectContaining({ params: { overwrite } }) + ); + expect(loggingTestTransport.logMessages[0].message).toContain("Successfully imported package: pkg-1"); + expect(loggingTestTransport.logMessages[3].message).toContain("Imported 1 node(s)."); + }); + + it("Should import a single package from a directory by zipping it first", async () => { + const packageZip = buildSinglePackageZip(); + const temporaryZipPath = "/tmp/content-cli-imports/single_package_test.zip"; + + jest.spyOn(FileService.prototype, "isDirectory").mockReturnValue(true); + const zipDirectorySpy = jest.spyOn(FileService.prototype, "zipDirectoryAsSinglePackage").mockReturnValue(temporaryZipPath); + mockReadFileSync(packageZip.toBuffer()); + mockCreateReadStream(packageZip.toBuffer()); + + const importResponse = buildImportResponse(); + mockAxiosPost(IMPORT_URL, importResponse); + + await new SinglePackageImportService(testContext).importPackage(null, "./package-dir", true, false); + + expect(zipDirectorySpy).toHaveBeenCalledWith("./package-dir"); + expect(mockedAxiosInstance.post).toHaveBeenCalledWith(IMPORT_URL, expect.anything(), expect.anything()); + expect(fs.rmSync).toHaveBeenCalledWith(temporaryZipPath); + }); + + it("Should write the import result to a json file when jsonResponse is true", async () => { + const packageZip = buildSinglePackageZip(); + mockReadFileSync(packageZip.toBuffer()); + mockCreateReadStream(packageZip.toBuffer()); + + const importResponse = buildImportResponse(); + mockAxiosPost(IMPORT_URL, importResponse); + + await new SinglePackageImportService(testContext).importPackage("./package.zip", null, false, true); + + const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; + expect(mockWriteFileSync).toHaveBeenCalledWith( + path.resolve(process.cwd(), expectedFileName), + JSON.stringify(importResponse, null, 2), + { encoding: "utf-8", mode: 0o600 } + ); + }); + + it("Should pass the overwrite flag to the API", async () => { + const packageZip = buildSinglePackageZip(); + mockReadFileSync(packageZip.toBuffer()); + mockCreateReadStream(packageZip.toBuffer()); + + mockAxiosPost(IMPORT_URL, buildImportResponse()); + + await new SinglePackageImportService(testContext).importPackage("./package.zip", null, true, false); + + expect(mockedPostRequestBodyByUrl.has(IMPORT_URL)).toBe(true); + expect(mockedAxiosInstance.post).toHaveBeenCalledWith( + IMPORT_URL, + expect.anything(), + expect.objectContaining({ params: { overwrite: true } }) + ); + }); + + it("Should throw when both --file and --directory are provided", async () => { + await expect( + new SinglePackageImportService(testContext).importPackage("./package.zip", "./package-dir", false, false) + ).rejects.toThrow("You cannot use both --file and --directory options at the same time. Only one import source can be defined."); + }); + + it("Should throw when neither --file nor --directory is provided", async () => { + await expect( + new SinglePackageImportService(testContext).importPackage(null, null, false, false) + ).rejects.toThrow("You must provide either a --file or a --directory option to import a package."); + }); + + it("Should throw when the --file option points to a directory", async () => { + jest.spyOn(FileService.prototype, "isDirectory").mockReturnValue(true); + + await expect( + new SinglePackageImportService(testContext).importPackage("./package-dir", null, false, false) + ).rejects.toThrow("The --file option accepts only zip files."); + }); + + it("Should throw when the --directory option points to a file", async () => { + jest.spyOn(FileService.prototype, "isDirectory").mockReturnValue(false); + + await expect( + new SinglePackageImportService(testContext).importPackage(null, "./package.zip", false, false) + ).rejects.toThrow("The --directory option accepts only directories."); + }); + + it("Should throw when the uncompressed zip size exceeds the 4 GB limit", async () => { + const packageZip = buildSinglePackageZip(); + mockReadFileSync(packageZip.toBuffer()); + mockCreateReadStream(packageZip.toBuffer()); + + const FIVE_GB = 5 * 1024 * 1024 * 1024; + (AdmZip as unknown as jest.Mock).mockImplementationOnce((...args: any[]) => { + const instance = jest.requireActual("adm-zip")(...args); + instance.getEntries = () => [{ header: { size: FIVE_GB } }]; + return instance; + }); + + await expect( + new SinglePackageImportService(testContext).importPackage("./package.zip", null, false, false) + ).rejects.toThrow('Failed to handle zip file "./package.zip": uncompressed size 5.00 GB exceeds the 4 GB limit.'); + }); +}); diff --git a/tests/core/utils/file.service.spec.ts b/tests/core/utils/file.service.spec.ts index 46e5dc0c..ca11f68a 100644 --- a/tests/core/utils/file.service.spec.ts +++ b/tests/core/utils/file.service.spec.ts @@ -82,4 +82,31 @@ describe("FileService", () => { expect(entries).not.toContain("fileSymlink.txt"); }); }); + + describe("zipDirectoryAsSinglePackage", () => { + test("Should throw error if sourceDir is a symlink", () => { + const targetDir = path.join(tempDir, "realPackageDir"); + fs.mkdirSync(targetDir, 0o700); + + const symlinkDir = path.join(tempDir, "symlinkPackageDir"); + fs.symlinkSync(targetDir, symlinkDir, "dir"); + + expect(() => fileService.zipDirectoryAsSinglePackage(symlinkDir)).toThrow(FatalError); + }); + + test("Should create a flat zip preserving the single package structure", () => { + fs.writeFileSync(path.join(tempDir, "package.json"), JSON.stringify({ key: "pkg-1" })); + const nodesDir = path.join(tempDir, "nodes"); + fs.mkdirSync(nodesDir, 0o700); + fs.writeFileSync(path.join(nodesDir, "node-1.json"), JSON.stringify({ key: "node-1" })); + + const zipPath = fileService.zipDirectoryAsSinglePackage(tempDir); + const zip = new AdmZip(zipPath); + const entries = zip.getEntries().map(e => e.entryName); + + expect(entries).toContain("package.json"); + expect(entries).toContain("nodes/node-1.json"); + expect(entries.some(name => name.endsWith(".zip"))).toBe(false); + }); + }); }); From c01de65f7176c6d9406b7fbb0e7b72ee9ccee099 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Fri, 5 Jun 2026 14:22:51 +0200 Subject: [PATCH 2/7] SP-874: address Sonar code smells (readonly members, node: import prefixes) Mark injected members as readonly and use node: import prefixes for built-in modules. The single-package-import test mocks node:fs explicitly so the temp file cleanup is intercepted (the global fs mock only covers the bare specifier). Includes-AI-Code: true Co-authored-by: Cursor --- .../api/single-package-import-api.ts | 2 +- .../single-package-import.service.ts | 6 +++--- .../configuration-management/single-package-import.spec.ts | 7 ++++++- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/commands/configuration-management/api/single-package-import-api.ts b/src/commands/configuration-management/api/single-package-import-api.ts index 2c786ba9..8afc2376 100644 --- a/src/commands/configuration-management/api/single-package-import-api.ts +++ b/src/commands/configuration-management/api/single-package-import-api.ts @@ -5,7 +5,7 @@ import { SinglePackageImportResult } from "../interfaces/single-package-import.i export class SinglePackageImportApi { - private httpClient: () => HttpClient; + private readonly httpClient: () => HttpClient; constructor(context: Context) { this.httpClient = () => context.httpClient; diff --git a/src/commands/configuration-management/single-package-import.service.ts b/src/commands/configuration-management/single-package-import.service.ts index 72f78202..fb6f8031 100644 --- a/src/commands/configuration-management/single-package-import.service.ts +++ b/src/commands/configuration-management/single-package-import.service.ts @@ -1,8 +1,8 @@ import { v4 as uuidv4 } from "uuid"; import * as FormData from "form-data"; -import { Readable } from "stream"; +import { Readable } from "node:stream"; import * as AdmZip from "adm-zip"; -import * as fs from "fs"; +import * as fs from "node:fs"; import { Context } from "../../core/command/cli-context"; import { fileService, FileService } from "../../core/utils/file-service"; import { logger } from "../../core/utils/logger"; @@ -13,7 +13,7 @@ export class SinglePackageImportService { private static readonly MAX_UNCOMPRESSED_ZIP_SIZE = 4 * 1024 * 1024 * 1024; - private singlePackageImportApi: SinglePackageImportApi; + private readonly singlePackageImportApi: SinglePackageImportApi; constructor(context: Context) { this.singlePackageImportApi = new SinglePackageImportApi(context); diff --git a/tests/commands/configuration-management/single-package-import.spec.ts b/tests/commands/configuration-management/single-package-import.spec.ts index b001534d..5d749730 100644 --- a/tests/commands/configuration-management/single-package-import.spec.ts +++ b/tests/commands/configuration-management/single-package-import.spec.ts @@ -1,8 +1,13 @@ import * as path from "path"; -import * as fs from "fs"; +import * as fs from "node:fs"; import AdmZip = require("adm-zip"); import { mockCreateReadStream, mockExistsSync, mockReadFileSync } from "../../utls/fs-mock-utils"; +// The service imports `node:fs`; the global jest.mock("fs") in jest.setup only +// covers the bare "fs" specifier, so mock the prefixed module explicitly to +// intercept the temp-file cleanup (fs.rmSync). +jest.mock("node:fs"); + jest.mock("adm-zip", () => { const realAdmZip = jest.requireActual("adm-zip"); return jest.fn((...args: any[]) => realAdmZip(...args)); From c3212a567a6874231d51575e88e65d576de6f103 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Fri, 5 Jun 2026 16:29:15 +0200 Subject: [PATCH 3/7] SP-957: move Team-to-Team Copy package commands to 't2tc package' group Bulk T2TC operations (list/export/import/diff) move from the top-level 'config' group to a dedicated 't2tc package' group, and 'config validate' moves to 'config package validate'. The old commands remain as deprecated aliases with runtime deprecation notices pointing to the new ones, so the change is non-breaking. Also adds a staging-focused 'config package list' and updates the docs. Includes-AI-Code: true Co-authored-by: Cursor --- docs/user-guide/agentic-development-guide.md | 12 +- docs/user-guide/config-commands.md | 240 ++++-------------- docs/user-guide/t2tc-commands.md | 211 +++++++++++++++ mkdocs.yaml | 1 + .../config-command.service.ts | 4 + .../configuration-management/module.ts | 90 ++++++- .../configuration-management/module.spec.ts | 51 +++- 7 files changed, 405 insertions(+), 204 deletions(-) create mode 100644 docs/user-guide/t2tc-commands.md diff --git a/docs/user-guide/agentic-development-guide.md b/docs/user-guide/agentic-development-guide.md index 59d80704..b7b8a94b 100644 --- a/docs/user-guide/agentic-development-guide.md +++ b/docs/user-guide/agentic-development-guide.md @@ -45,7 +45,7 @@ content-cli asset-registry examples --assetType BOARD_V2 --json ### 3. Export the target package ```bash -content-cli config export --packageKeys --unzip +content-cli t2tc package export --packageKeys --unzip ``` This gives you the directory structure with `package.json`, `manifest.json`, and existing node JSONs to use as reference. @@ -84,13 +84,13 @@ content-cli asset-registry validate --assetType \ Or validate during import with the `--validate` flag — note that this runs the `SCHEMA` layer only: ```bash -content-cli config import -d --validate --overwrite +content-cli t2tc package import -d --validate --overwrite ``` -To also run business-layer rules (PQL parsing, data-model availability, KPI uniqueness, …) and package-settings checks (dependencies, variables, and flavor-specific package settings), run `config validate` against the just-imported staging version: +To also run business-layer rules (PQL parsing, data-model availability, KPI uniqueness, …) and package-settings checks (dependencies, variables, and flavor-specific package settings), run `config package validate` against the just-imported staging version: ```bash -content-cli config validate --packageKey --layers SCHEMA BUSINESS PACKAGE_SETTINGS +content-cli config package validate --packageKey --layers SCHEMA BUSINESS PACKAGE_SETTINGS ``` If validation returns errors, fix the issues before importing. @@ -98,7 +98,7 @@ If validation returns errors, fix the issues before importing. ### 6. Import ```bash -content-cli config import -d --overwrite +content-cli t2tc package import -d --overwrite ``` This creates a new version in staging (not deployed). To create a brand-new package instead of updating, omit `--overwrite`. @@ -106,7 +106,7 @@ This creates a new version in staging (not deployed). To create a brand-new pack To later export a staging version, use `--keysByVersion`: ```bash -content-cli config export --keysByVersion _ --unzip +content-cli t2tc package export --keysByVersion _ --unzip ``` ## Troubleshooting diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index d823e27e..fce1e75f 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -1,40 +1,32 @@ # Configuration Management Commands -The `config` command group manages package configurations. Most of its commands work with a package and its resources — importing a package, working with nodes, versions, or variables, and validating configurations. Separately, it includes a set of **batch commands** built for specific bulk use-cases, such as moving many packages at once between teams and realms. +The `config` command group manages a package and its resources — importing a single package, working with nodes, versions, or variables, reading metadata, and validating configurations. -The batch commands are **batch-specific**: they use their own archive format that only the batch commands understand, so their artifacts are **not interchangeable** with the package commands (see [Two command families](#two-command-families)). +> **Bulk Team-to-Team Copy commands have moved.** `config list`, `config export`, `config import`, and `config diff` are **deprecated** and now live under the [`t2tc package`](./t2tc-commands.md) command group. `config validate` is deprecated too and has moved to [`config package validate`](#validate-package-configurations-config-package-validate). The deprecated commands still work for now but print a deprecation notice — switch to the new commands at your earliest convenience. -- **Package commands** — work with a package and its resources: - - [`config package import`](#package-commands-config-package) — import a package +- **Package & resource commands** — work with a package and its contents: + - [`config package import`](#package-commands-config-package) — import a single package + - [`config package validate`](#validate-package-configurations-config-package-validate) — validate a package's staging version + - [`config package list`](#list-packages-config-package-list) — list packages (staging by default) - [`config nodes …`](#finding-nodes) — work with individual nodes - [`config versions …`](#package-version) — read and create package versions - [`config variables list`](#listing--mapping-variables) — read package variables - - [`config validate`](#validate-package-configurations) — validate a package's staging version -- **Batch commands** — bulk multi-package transport for specific use-cases: - - [`config list`](#list-packages), [`config export`](#batch-export-packages), [`config import`](#batch-import-packages), [`config diff`](#diff-local-zip-with-deployed-versionspecific-versionstaging), [`config metadata export`](#batch-export-packages) + - `config metadata export` — check whether packages have unpublished changes +- **Team-to-Team Copy (batch) commands** — bulk multi-package transport, now under [`t2tc package`](./t2tc-commands.md): + - [`t2tc package list`](./t2tc-commands.md#list-packages), [`t2tc package export`](./t2tc-commands.md#export-packages), [`t2tc package import`](./t2tc-commands.md#import-packages), [`t2tc package diff`](./t2tc-commands.md#diff-local-zip-with-deployed-versionspecific-versionstaging) -## Two command families +## Package vs. batch archive format -The batch commands are a **self-contained, batch-specific set**. `config export` produces a multi-package **batch archive** — a top-level `manifest.json`, `variables.json`, `studio.json`, and one nested `_.zip` per package — that is produced and consumed **only** by other batch commands. `config package import`, by contrast, works with a plain **package zip** (a `package.json`, an optional `variables.json`, and a `nodes/` folder). The two formats are **not interchangeable**: +`config package import` works with a plain **package zip** (a `package.json`, an optional `variables.json`, and a `nodes/` folder). The Team-to-Team Copy commands instead use a multi-package **batch archive** (a top-level `manifest.json`, `variables.json`, `studio.json`, and a nested `_.zip` per package). The two formats are **not interchangeable**: -- An archive from `config export` can be imported with `config import` or inspected with `config diff` — but **not** with `config package import`. -- A package zip used by `config package import` **cannot** be imported with `config import` or diffed with `config diff`. +- An archive from `t2tc package export` can be imported with `t2tc package import` or inspected with `t2tc package diff` — but **not** with `config package import`. +- A package zip used by `config package import` **cannot** be imported with `t2tc package import` or diffed with `t2tc package diff`. -| Command | Group | Artifact it reads / writes | -|---|---|---| -| `config package import` | Package commands | Package zip/dir (`package.json`, optional `variables.json`, `nodes/`) | -| `config nodes …` | Package commands | Node JSON payload | -| `config validate`, `config versions …`, `config variables list` | Package commands | — (operate directly on the platform) | -| `config export` | Batch commands | Batch archive (multi-package) | -| `config import` | Batch commands | Batch archive (multi-package) | -| `config diff` | Batch commands | Batch archive (multi-package) | -| `config list`, `config metadata export` | Batch commands | — (JSON list output) | - -**Which should I use?** Reach for the batch commands only for their specific bulk use-cases — moving a set of packages together (for example, a migration between teams). For everything else, use the package commands. +For the full Team-to-Team Copy reference, see [T2TC Commands](./t2tc-commands.md). ## Permissions -All `config` commands run against the Pacman API and are subject to the same permission checks the platform applies in the UI. The required permission depends on the **flavor** of the target package: +All `config` commands run against the Pacman API and are subject to the same permission checks the platform applies in the UI. The same permissions apply to the [`t2tc package`](./t2tc-commands.md) commands. The required permission depends on the **flavor** of the target package: | Package flavor | Required permission | |---|---| @@ -47,181 +39,68 @@ This applies to every command that reads or modifies a single package or its nod - `config nodes create`, `config nodes update`, `config nodes archive` - `config variables list` - `config versions get`, `config versions create` -- `config validate`, `config diff` -- `config export`, `config import`, `config package import`, `config metadata export` +- `config package validate`, `config package import`, `config metadata export` +- `t2tc package export`, `t2tc package import`, `t2tc package diff` If the authenticated profile does not have the required permission, the command fails with `Access is Denied`. -`config list` is the one exception: instead of failing, it **filters out packages the profile does not have permission to access**. If a package you expect to see is missing from the list, the most likely cause is missing edit permission on the package (Studio) or on its connected data pool (OCDM). +The listing commands (`config package list` and `t2tc package list`) are the one exception: instead of failing, they **filter out packages the profile does not have permission to access**. If a package you expect to see is missing from the list, the most likely cause is missing edit permission on the package (Studio) or on its connected data pool (OCDM). -## List Packages +## List Packages (`config package list`) -> **Batch command.** Part of the [batch family](#two-command-families) — a bulk listing utility typically used to discover packages before a `config export`. +> The deprecated `config list` command has moved to [`t2tc package list`](./t2tc-commands.md#list-packages) (bulk listing for the Team-to-Team Copy workflow, with `--withDependencies` / `--packageKeys` / `--keysByVersion` filtering). For day-to-day listing, use `config package list` below. `config list` still works but prints a deprecation notice. -Packages can be listed using the following command: +`config package list` lists packages in the target team. **By default it lists staging packages.** ```bash -content-cli config list -p +content-cli config package list -p ``` -The result will be printed in the console containing only the package name and key: +The result is printed in the console containing the package name and key: ```bash info: Package1 - Key: "package-1" ``` -By using the `--json` option, packages can be exported (saved) in an extended form as a json file in the current working directory. +Use `--json` to export the list as a JSON file in the current working directory: ```bash -content-cli config list -p --json +content-cli config package list -p --json ``` -The name of the file will be printed in the console with the following format: - ```bash info: File downloaded successfully. New filename: 9560f81f-f746-4117-83ee-dd1f614ad624.json ``` -By using the `--flavors` option, you can filter which packages to list. The available flavors are: **STUDIO** and **OCDM**. - -To list staging packages instead of deployed packages use the `--staging` option. Please note that this flag is not compatible with the below options. - -### List Packages with Dependencies - -When using the listing command with the `--json` option, two additional options are available: - -- **--withDependencies**: This option will include the dependencies of the packages in the output. - -```bash -content-cli config list -p --withDependencies -``` - -- **--packageKeys**: This option allows you to filter the packages by their keys. You can specify multiple package keys separated by spaces. - -```bash -content-cli config list -p --packageKeys key1 ... keyN -[optional] –-withDependencies -``` - -## Batch Export Packages - -> **Batch command.** Part of the [batch family](#two-command-families). It produces a multi-package **batch archive** that can only be re-imported with [`config import`](#batch-import-packages) or inspected with [`config diff`](#diff-local-zip-with-deployed-versionspecific-versionstaging) — **not** with `config package import`. To work with one package, use [`config package import`](#package-commands-config-package). - -Packages can be exported using the following command: - -```bash -content-cli config export -p --packageKeys key1 ... keyN -``` - -The `--keysByVersion` option can be used to export packages by specific version. You can specify multiple packages with version separated by spaces, in the format of 'packageKey.version'. -The `--withDependencies` option can be used to also export dependencies of the given packages. -The `--unzip` option can be used to unzip the exported packages into the current working directory. - -Depending on the `--unzip` option used, a zip file, or a directory containing the exported packages, will be created in the current working directory containing: - -```bash -exported_package_random_uuid/ -├─ manifest.json -├─ variable.json -├─ studio.json -├─ package_key1-version.zip -├─ ... -├─ package_keyN-version.zip -``` - -### Git Integration for Export - -The following **Git options** are available: - -- `--gitProfile ` – specifies the Git profile to use for exporting directly to a repository. - If not specified, the default profile will be used. -- `--gitBranch ` – specifies the branch in the Github repository where the export will be pushed. - -Example exporting to Git: - -```bash -content-cli config export -p --packageKeys key1 key2 --gitProfile myGitProfile --gitBranch feature-branch -``` - -### Export Directory Structure - -- manifest.json - File which contains the metadata of the exported packages. -- studio.json - File which contains the metadata of the exported packages in a format compatible with Studio. -- variables.json - File which contains the variables of the exported packages. -- exported packages directories - Directories containing the exported package files, each directory is named after the package key and the version. - -Inside each exported package directory, the following files will be present: - -- package.json - File which contains the configuration of the exported package. -- nodes/ - Directory containing the nodes of the exported package. - -Inside the nodes directory, a file for each node will be present: - -- node_key.json - File which contains the configuration of the exported node. - -## Batch Import Packages - -> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch archive** produced by [`config export`](#batch-export-packages). To import one package from a package zip, use [`config package import`](#package-commands-config-package) instead. - -Packages can be imported using the following commands, if importing from a zip file: - -```bash -content-cli config import -p -f -``` - -Where `-f` is the short hand operation for `--file`. -If importing from a directory containing the exported packages, the following command can be used: +Use `--flavors` to filter which packages to list. The available flavors are **STUDIO** and **OCDM**: ```bash -content-cli config import -p -d +content-cli config package list -p --flavors STUDIO ``` -Where `-d` is the shorthand operation for `--directory`. -When packages with the same keys exist in the target team, the --overwrite option can be used for allowing overwriting of those packages. If the package in target environment contains unpublished changes, they are automatically saved under a new version. This allows you to audit, compare, or roll back to your previous state via the version history if needed. +> Need active (deployed) versions, dependency expansion, or `--packageKeys` / `--keysByVersion` filtering? Those belong to the bulk copy workflow — use [`t2tc package list`](./t2tc-commands.md#list-packages). -```bash -content-cli config import -p -f --overwrite -``` +## Bulk Export / Import / Diff (moved to `t2tc package`) -### Git Integration for Import - -The following **Git options** are available: - -- `--gitProfile ` – specifies the Git profile to use for importing directly from a repository. - If not specified, the default profile will be used. -- `--gitBranch ` – specifies the branch in the Github repository from which to import. - -Example importing from Git: - -```bash -content-cli config import -p --gitProfile myGitProfile --gitBranch feature-branch -``` - -Finally, the result of this command will be a list of PostPackageImportData exported as a json file. The file name will be printed with the following message format: - -```bash -info: Config import report file: 9560f81f-f746-4117-83ee-dd1f614ad624.json -``` - -### Validate During Import - -Add `--validate` to `config import` to run validation against each node **before** the import is committed: - -```bash -content-cli config import -p -d --validate --overwrite -``` - -`config import --validate` runs the **SCHEMA** layer only. It does **not** run BUSINESS-layer checks (PQL parsing, data-model availability, KPI uniqueness, etc.) or PACKAGE_SETTINGS checks (package dependencies, variables, and flavor-specific package settings). To run those validations, use [`config validate`](#validate-package-configurations) after the import. +> **Deprecated and moved.** The bulk multi-package commands have moved to the [`t2tc package`](./t2tc-commands.md) group: +> +> | Old (deprecated) | New | +> |---|---| +> | `config export` | [`t2tc package export`](./t2tc-commands.md#export-packages) | +> | `config import` | [`t2tc package import`](./t2tc-commands.md#import-packages) | +> | `config diff` | [`t2tc package diff`](./t2tc-commands.md#diff-local-zip-with-deployed-versionspecific-versionstaging) | +> +> The flags and behaviour are unchanged — only the command path moved. The old commands still work but print a deprecation notice. See [T2TC Commands](./t2tc-commands.md) for the full reference (batch archive format, Git integration, validation during import, and more). To import a **single** package from a package zip, use [`config package import`](#package-commands-config-package). ## Package Commands (`config package`) -The `config package` command group works with a package and its contents. It is **not** part of the batch-specific set — it uses the plain package format described below, which is not interchangeable with the batch archive (see [Two command families](#two-command-families)). +The `config package` command group works with a package and its contents. It uses the plain package format described below, which is not interchangeable with the batch archive used by the Team-to-Team Copy commands (see [Package vs. batch archive format](#package-vs-batch-archive-format)). ### Import a Package -`config package import` imports a package from a package zip (or directory). Unlike [`config import`](#batch-import-packages) — which performs a **batch** import and expects the multi-package batch archive (`manifest.json`, a top-level `variables.json`, `studio.json`, and a nested `_.zip` per package) — `config package import` takes a plain, flat package layout and imports it on its own. +`config package import` imports a package from a package zip (or directory). Unlike [`t2tc package import`](./t2tc-commands.md#import-packages) — which performs a **batch** import and expects the multi-package batch archive (`manifest.json`, a top-level `variables.json`, `studio.json`, and a nested `_.zip` per package) — `config package import` takes a plain, flat package layout and imports it on its own. -> A zip produced by `config export` is a **batch archive** and cannot be imported with `config package import`. Likewise, a package zip cannot be imported with `config import`. Use the command that matches how the artifact was produced. +> A zip produced by `t2tc package export` is a **batch archive** and cannot be imported with `config package import`. Likewise, a package zip cannot be imported with `t2tc package import`. Use the command that matches how the artifact was produced. ```bash content-cli config package import -p -f @@ -288,14 +167,16 @@ content-cli config package import -p -f --json info: File downloaded successfully. New filename: 9560f81f-f746-4117-83ee-dd1f614ad624.json ``` -## Validate Package Configurations +## Validate Package Configurations (`config package validate`) -The `config validate` command validates the **staging (draft) version** of a package by sending its nodes through one or more validation layers. The command runs against the Pacman validate API and returns a structured report of errors, warnings, and info findings. +> **Renamed.** This command moved from `config validate` to `config package validate`. The old `config validate` still works but prints a deprecation notice; switch to `config package validate`. + +The `config package validate` command validates the **staging (draft) version** of a package by sending its nodes through one or more validation layers. The command runs against the Pacman validate API and returns a structured report of errors, warnings, and info findings. This command requires **edit permission** on the target package (see [Permissions](#permissions)). ```bash -content-cli config validate --packageKey +content-cli config package validate --packageKey ``` By default, only the `SCHEMA` layer is run. The console output looks like: @@ -329,7 +210,7 @@ Currently `SCHEMA`, `BUSINESS`, and `PACKAGE_SETTINGS` are the layers accepted b To run all layers: ```bash -content-cli config validate --packageKey --layers SCHEMA BUSINESS PACKAGE_SETTINGS +content-cli config package validate --packageKey --layers SCHEMA BUSINESS PACKAGE_SETTINGS ``` Use `PACKAGE_SETTINGS` when you need to verify that the package's own settings are usable in the destination team before continuing authoring or import work. It reports issues such as missing dependency versions, duplicate dependency or variable keys, blank variable keys/types, missing Studio data model assignments, and OCDM package-settings problems when the corresponding backend validation is enabled. @@ -339,7 +220,7 @@ Use `PACKAGE_SETTINGS` when you need to verify that the package's own settings a By default, every node in the package's staging version is validated. To restrict the scope to a subset of nodes, use `--nodeKeys`: ```bash -content-cli config validate --packageKey --nodeKeys node-key-1 node-key-2 +content-cli config package validate --packageKey --nodeKeys node-key-1 node-key-2 ``` ### Export Validation Report as JSON @@ -347,7 +228,7 @@ content-cli config validate --packageKey --nodeKeys node-key-1 node Use `--json` to write the full validation report to a JSON file in the current working directory instead of printing it to the console: ```bash -content-cli config validate --packageKey --layers SCHEMA BUSINESS PACKAGE_SETTINGS --json +content-cli config package validate --packageKey --layers SCHEMA BUSINESS PACKAGE_SETTINGS --json ``` The filename is printed on success: @@ -885,7 +766,7 @@ To diff a local node JSON file (the compare side) against a version of the node content-cli config nodes diff --packageKey --nodeKey --baseVersion --file ``` -The file must follow the `NodeExportTransport` shape — the format produced by `config export --unzip` under `_/nodes/.json`. `--baseVersion` accepts either `STAGING` or a specific package version. `--file` and `--compareVersion` are mutually exclusive; exactly one must be provided. +The file must follow the `NodeExportTransport` shape — the format produced by `t2tc package export --unzip` under `_/nodes/.json`. `--baseVersion` accepts either `STAGING` or a specific package version. `--file` and `--compareVersion` are mutually exclusive; exactly one must be provided. If no node with the given key exists for the resolved base version, the file is diffed against an empty configuration `{}`. @@ -943,23 +824,4 @@ content-cli config nodes dependencies list --packageKey --nodeKey < ## Diff local zip with deployed version/specific version/staging -> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch archive** produced by [`config export`](#batch-export-packages); a package zip is not supported here. - -To compare local zipped packages with online packages use: -```bash -content-cli config diff --file -``` - -As with other commands, use `--json` to export the diff to a file. -To diff against a specific version use the `--baseVersion` parameter. When omitted it will diff against the current deployed version. -To diff against staging use `--baseVersion STAGING`. - -```bash -content-cli config diff --file --baseVersion -``` - -To diff against the current deployed version and only return whether there are any changes, use the `--hasChanges` flag. - -```bash -content-cli config diff --file --hasChanges -``` \ No newline at end of file +> **Deprecated and moved.** `config diff` has moved to [`t2tc package diff`](./t2tc-commands.md#diff-local-zip-with-deployed-versionspecific-versionstaging). The flags and behaviour are unchanged — only the command path moved. The old command still works but prints a deprecation notice. See [T2TC Commands](./t2tc-commands.md#diff-local-zip-with-deployed-versionspecific-versionstaging) for the full reference. \ No newline at end of file diff --git a/docs/user-guide/t2tc-commands.md b/docs/user-guide/t2tc-commands.md new file mode 100644 index 00000000..115e4fe5 --- /dev/null +++ b/docs/user-guide/t2tc-commands.md @@ -0,0 +1,211 @@ +# Team-to-Team Copy (T2TC) Commands + +The `t2tc package` command group moves whole packages — and, optionally, their dependencies and variables — between teams and realms. These commands power the **Team-to-Team Copy** feature: the T2TC backend drives the copy by exporting from a source team and importing into a target team through this CLI. + +These commands are a **self-contained, batch-specific set**. `t2tc package export` produces a multi-package **batch archive**, and only the other `t2tc package` commands (`import`, `diff`) understand that archive. It is **not** interchangeable with the package format used by [`config package import`](./config-commands.md#package-commands-config-package) (see [Batch archive vs. package format](#batch-archive-vs-package-format)). + +> **Migrating from `config`?** `t2tc package list/export/import/diff` replace the deprecated `config list/export/import/diff` commands. The flags and behaviour are identical — only the command path changed. + +| Old (deprecated) | New | +|---|---| +| `config list` | `t2tc package list` | +| `config export` | `t2tc package export` | +| `config import` | `t2tc package import` | +| `config diff` | `t2tc package diff` | + +## Permissions + +`t2tc package` commands run against the Pacman API and are subject to the same permission checks the platform applies in the UI. The required permission depends on the **flavor** of the target package: + +| Package flavor | Required permission | +|---|---| +| **Studio** (Studio packages and their assets) | **Edit package** permission on the package | +| **OCDM** (OCDM packages and their assets) | **Edit** (admin) permission on the **data pool** the OCDM package is connected to | + +`t2tc package list` is the one exception: instead of failing, it **filters out packages the profile does not have permission to access**. If a package you expect to see is missing from the list, the most likely cause is missing edit permission on the package (Studio) or on its connected data pool (OCDM). + +## Batch archive vs. package format + +`t2tc package export` produces a multi-package **batch archive** — a top-level `manifest.json`, `variables.json`, `studio.json`, and one nested `_.zip` per package — that is produced and consumed **only** by the `t2tc package` commands. [`config package import`](./config-commands.md#package-commands-config-package), by contrast, works with a plain **package zip** (a `package.json`, an optional `variables.json`, and a `nodes/` folder). The two formats are **not interchangeable**: + +- An archive from `t2tc package export` can be imported with `t2tc package import` or inspected with `t2tc package diff` — but **not** with `config package import`. +- A package zip used by `config package import` **cannot** be imported with `t2tc package import` or diffed with `t2tc package diff`. + +Reach for `t2tc package` only for its specific bulk use-case — moving a set of packages together (for example, a migration between teams). To work with a single package, use [`config package import`](./config-commands.md#package-commands-config-package). + +## List Packages + +Packages can be listed using the following command: + +```bash +content-cli t2tc package list -p +``` + +The result will be printed in the console containing only the package name and key: + +```bash +info: Package1 - Key: "package-1" +``` + +By using the `--json` option, packages can be exported (saved) in an extended form as a json file in the current working directory. + +```bash +content-cli t2tc package list -p --json +``` + +The name of the file will be printed in the console with the following format: + +```bash +info: File downloaded successfully. New filename: 9560f81f-f746-4117-83ee-dd1f614ad624.json +``` + +By using the `--flavors` option, you can filter which packages to list. The available flavors are: **STUDIO** and **OCDM**. + +To list staging packages instead of deployed packages use the `--staging` option. Please note that this flag is not compatible with the below options. + +> For day-to-day staging listing outside the copy workflow, prefer [`config package list`](./config-commands.md#list-packages-config-package-list), which lists staging packages by default. + +### List Packages with Dependencies + +When using the listing command with the `--json` option, two additional options are available: + +- **--withDependencies**: This option will include the dependencies of the packages in the output. + +```bash +content-cli t2tc package list -p --withDependencies +``` + +- **--packageKeys**: This option allows you to filter the packages by their keys. You can specify multiple package keys separated by spaces. + +```bash +content-cli t2tc package list -p --packageKeys key1 ... keyN +[optional] --withDependencies +``` + +## Export Packages + +Packages can be exported using the following command: + +```bash +content-cli t2tc package export -p --packageKeys key1 ... keyN +``` + +The `--keysByVersion` option can be used to export packages by specific version. You can specify multiple packages with version separated by spaces, in the format of 'packageKey.version'. +The `--withDependencies` option can be used to also export dependencies of the given packages. +The `--unzip` option can be used to unzip the exported packages into the current working directory. + +Depending on the `--unzip` option used, a zip file, or a directory containing the exported packages, will be created in the current working directory containing: + +```bash +exported_package_random_uuid/ +├─ manifest.json +├─ variable.json +├─ studio.json +├─ package_key1-version.zip +├─ ... +├─ package_keyN-version.zip +``` + +### Git Integration for Export + +The following **Git options** are available: + +- `--gitProfile ` – specifies the Git profile to use for exporting directly to a repository. + If not specified, the default profile will be used. +- `--gitBranch ` – specifies the branch in the Github repository where the export will be pushed. + +Example exporting to Git: + +```bash +content-cli t2tc package export -p --packageKeys key1 key2 --gitProfile myGitProfile --gitBranch feature-branch +``` + +### Export Directory Structure + +- manifest.json - File which contains the metadata of the exported packages. +- studio.json - File which contains the metadata of the exported packages in a format compatible with Studio. +- variables.json - File which contains the variables of the exported packages. +- exported packages directories - Directories containing the exported package files, each directory is named after the package key and the version. + +Inside each exported package directory, the following files will be present: + +- package.json - File which contains the configuration of the exported package. +- nodes/ - Directory containing the nodes of the exported package. + +Inside the nodes directory, a file for each node will be present: + +- node_key.json - File which contains the configuration of the exported node. + +## Import Packages + +Packages can be imported using the following commands, if importing from a zip file: + +```bash +content-cli t2tc package import -p -f +``` + +Where `-f` is the short hand operation for `--file`. +If importing from a directory containing the exported packages, the following command can be used: + +```bash +content-cli t2tc package import -p -d +``` + +Where `-d` is the shorthand operation for `--directory`. +When packages with the same keys exist in the target team, the `--overwrite` option can be used for allowing overwriting of those packages. If the package in target environment contains unpublished changes, they are automatically saved under a new version. This allows you to audit, compare, or roll back to your previous state via the version history if needed. + +```bash +content-cli t2tc package import -p -f --overwrite +``` + +### Git Integration for Import + +The following **Git options** are available: + +- `--gitProfile ` – specifies the Git profile to use for importing directly from a repository. + If not specified, the default profile will be used. +- `--gitBranch ` – specifies the branch in the Github repository from which to import. + +Example importing from Git: + +```bash +content-cli t2tc package import -p --gitProfile myGitProfile --gitBranch feature-branch +``` + +Finally, the result of this command will be a list of PostPackageImportData exported as a json file. The file name will be printed with the following message format: + +```bash +info: Config import report file: 9560f81f-f746-4117-83ee-dd1f614ad624.json +``` + +### Validate During Import + +Add `--validate` to `t2tc package import` to run validation against each node **before** the import is committed: + +```bash +content-cli t2tc package import -p -d --validate --overwrite +``` + +`t2tc package import --validate` runs the **SCHEMA** layer only. It does **not** run BUSINESS-layer checks (PQL parsing, data-model availability, KPI uniqueness, etc.) or PACKAGE_SETTINGS checks (package dependencies, variables, and flavor-specific package settings). To run those validations, use [`config package validate`](./config-commands.md#validate-package-configurations-config-package-validate) after the import. + +## Diff local zip with deployed version/specific version/staging + +To compare local zipped packages with online packages use: + +```bash +content-cli t2tc package diff --file +``` + +As with other commands, use `--json` to export the diff to a file. +To diff against a specific version use the `--baseVersion` parameter. When omitted it will diff against the current deployed version. +To diff against staging use `--baseVersion STAGING`. + +```bash +content-cli t2tc package diff --file --baseVersion +``` + +To diff against the current deployed version and only return whether there are any changes, use the `--hasChanges` flag. + +```bash +content-cli t2tc package diff --file --hasChanges +``` diff --git a/mkdocs.yaml b/mkdocs.yaml index 15cf1760..f0b07a7f 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -13,6 +13,7 @@ nav: - Overview: './user-guide/index.md' - Studio Commands: './user-guide/studio-commands.md' - Config Commands: './user-guide/config-commands.md' + - T2TC Commands: './user-guide/t2tc-commands.md' - Deployment Commands: './user-guide/deployment-commands.md' - Asset Registry Commands: './user-guide/asset-registry-commands.md' - Data Pool Commands: './user-guide/data-pool-commands.md' diff --git a/src/commands/configuration-management/config-command.service.ts b/src/commands/configuration-management/config-command.service.ts index 510d44cd..9f9ec48a 100644 --- a/src/commands/configuration-management/config-command.service.ts +++ b/src/commands/configuration-management/config-command.service.ts @@ -39,6 +39,10 @@ export class ConfigCommandService { } } + public listStagingPackages(jsonResponse: boolean, flavors: string[]): Promise { + return this.batchImportExportService.listStagingPackages(flavors ?? [], false, jsonResponse); + } + public async listVariables( jsonResponse: boolean, keysByVersion: string[], diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index ebbecb90..ca4ebbe7 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -18,9 +18,10 @@ class Module extends IModule { public register(context: Context, configurator: Configurator): void { const configCommand = configurator.command("config") - .description("Manage package configurations. Most commands work with a package and its resources. The batch commands (list, export, import, diff, metadata) are a separate, batch-specific set for bulk multi-package transport: their archive format is only understood by other batch commands and is not interchangeable with the package commands."); + .description("Manage package configurations and their resources (package, nodes, versions, variables, metadata). Note: 'config list/export/import/diff' are deprecated — bulk Team-to-Team Copy operations have moved to the 't2tc package' group, and single-package operations live under 'config package'."); configCommand.command("list") - .description("[Batch] List packages in the target team. Part of the batch export/import workflow.") + .description("[Deprecated] Use 't2tc package list' instead. List packages in the target team.") + .deprecationNotice("'config list' is deprecated and will be removed in a future release. Use 't2tc package list' instead.") .option("--json", "Return response as json type", "") .option("--flavors ", "Lists only packages of the given flavors") .option("--withDependencies", "Include dependencies", "") @@ -33,7 +34,8 @@ class Module extends IModule { .action(this.listPackages); configCommand.command("export") - .description("[Batch] Export one or more packages into a batch archive. The archive only works with the batch commands ('config import' / 'config diff') and cannot be imported with 'config package import'.") + .description("[Deprecated] Use 't2tc package export' instead. Export one or more packages into a Team-to-Team Copy archive.") + .deprecationNotice("'config export' is deprecated and will be removed in a future release. Use 't2tc package export' instead.") .option("--packageKeys ", "Keys of packages to export. Exports the latest deployed version only") .option("--keysByVersion ", "Keys of packages to export by version") .option("--withDependencies", "Include variables and dependencies", "") @@ -47,13 +49,14 @@ class Module extends IModule { metadataCommand .command("export") - .description("[Batch] Show whether multiple packages have unpublished changes (bulk metadata export).") + .description("Show whether multiple packages have unpublished changes (bulk metadata export).") .requiredOption("--packageKeys ", "Keys of packages to find the metadata of") .option("--json", "Return response as json type", "") .action(this.batchExportPackagesMetadata); configCommand.command("import") - .description("[Batch] Import packages from a batch archive produced by 'config export'. To import one package, use 'config package import' instead.") + .description("[Deprecated] Use 't2tc package import' (batch) or 'config package import' (single package) instead. Import packages from a Team-to-Team Copy archive.") + .deprecationNotice("'config import' is deprecated and will be removed in a future release. Use 't2tc package import' (batch archive) or 'config package import' (single package) instead.") .option("--overwrite", "Flag to allow overwriting of packages") .option("--validate", "Validate node configurations before import", false) .option("--gitProfile ", "Git profile which you want to use for the Git operations") @@ -66,14 +69,14 @@ class Module extends IModule { .description("Commands for working with a package."); packageCommand.command("import") - .description("Import a package from a zip file or directory. Uses the package format, which is not interchangeable with the batch 'config export' / 'config import' archive.") + .description("Import a package from a zip file or directory. Uses the package format, which is not interchangeable with the 't2tc package export' / 't2tc package import' batch archive.") .option("-f, --file ", "Package zip file (relative path)") .option("-d, --directory ", "Package directory (relative path)") .option("--overwrite", "Flag to allow overwriting an existing package with the same key") .option("--json", "Return the response as a JSON file") .action(this.importSinglePackage); - configCommand.command("validate") + packageCommand.command("validate") .description("Validate package node configurations") .requiredOption("--packageKey ", "Key of the package to validate") .option( @@ -85,8 +88,28 @@ class Module extends IModule { .option("--json", "Return the response as a JSON file") .action(this.validatePackage); + packageCommand.command("list") + .description("List packages in the target team. Lists staging packages by default.") + .option("--json", "Return response as json type", "") + .option("--flavors ", "Lists only packages of the given flavors") + .action(this.listStagingPackages); + + configCommand.command("validate") + .description("[Deprecated] Use 'config package validate' instead. Validate package node configurations.") + .deprecationNotice("'config validate' is deprecated and will be removed in a future release. Use 'config package validate' instead.") + .requiredOption("--packageKey ", "Key of the package to validate") + .option( + "--layers ", + "Validation layers to run. Allowed values: SCHEMA, BUSINESS, PACKAGE_SETTINGS (can be combined, e.g. --layers SCHEMA BUSINESS PACKAGE_SETTINGS). Defaults to SCHEMA.", + ["SCHEMA"] + ) + .option("--nodeKeys ", "Specific node keys to validate (default: all nodes)") + .option("--json", "Return the response as a JSON file") + .action(this.validatePackage); + configCommand.command("diff") - .description("[Batch] Diff a local batch archive (from 'config export') against deployed or staging packages.") + .description("[Deprecated] Use 't2tc package diff' instead. Diff a local Team-to-Team Copy archive against deployed or staging packages.") + .deprecationNotice("'config diff' is deprecated and will be removed in a future release. Use 't2tc package diff' instead.") .option("--hasChanges", "Flag to return only the information if the package has changes without the actual changes") .option("--baseVersion ", "Compare against a given version or STAGING") .option("--json", "Return the response as a JSON file") @@ -193,6 +216,53 @@ class Module extends IModule { .option("--json", "Return the response as a JSON file") .action(this.listNodeDependencies); + const t2tcCommand = configurator.command("t2tc") + .description("Team-to-Team Copy (T2TC) commands for moving whole packages and their dependencies between teams using the batch transport archive format."); + + const t2tcPackageCommand = t2tcCommand.command("package") + .description("Team-to-Team Copy package commands: list, export, import and diff packages using the batch transport archive."); + + t2tcPackageCommand.command("list") + .description("List packages in the target team. Part of the Team-to-Team Copy export/import workflow.") + .option("--json", "Return response as json type", "") + .option("--flavors ", "Lists only packages of the given flavors") + .option("--withDependencies", "Include dependencies", "") + .option("--packageKeys ", "Lists only active versions of given package keys") + .option("--keysByVersion ", "Lists packages by given key and version [packageKey.version]") + .option("--variableValue ", "Variable value for filtering packages by.") + .option("--variableType ", "Variable type for filtering packages by.") + .option("--branches", "Include branches", false) + .option("--staging", "List staging packages instead", false) + .action(this.listPackages); + + t2tcPackageCommand.command("export") + .description("Export one or more packages into a Team-to-Team Copy archive. The archive is consumed by 't2tc package import' / 't2tc package diff'.") + .option("--packageKeys ", "Keys of packages to export. Exports the latest deployed version only") + .option("--keysByVersion ", "Keys of packages to export by version") + .option("--withDependencies", "Include variables and dependencies", "") + .option("--unzip", "Unzip the exported file", "") + .option("--gitProfile ", "Git profile which you want to use for the Git operations") + .option("--gitBranch ", "Git branch in which you want to push the exported file") + .action(this.batchExportPackages); + + t2tcPackageCommand.command("import") + .description("Import packages from a Team-to-Team Copy archive produced by 't2tc package export'. To import a single standalone package, use 'config package import'.") + .option("--overwrite", "Flag to allow overwriting of packages") + .option("--validate", "Validate node configurations before import", false) + .option("--gitProfile ", "Git profile which you want to use for the Git operations") + .option("--gitBranch ", "Git branch from which you want to pull the exported file and import") + .option("-f, --file ", "Exported packages file (relative path)") + .option("-d, --directory ", "Exported packages directory (relative path)") + .action(this.batchImportPackages); + + t2tcPackageCommand.command("diff") + .description("Diff a local Team-to-Team Copy archive (from 't2tc package export') against deployed or staging packages.") + .option("--hasChanges", "Flag to return only the information if the package has changes without the actual changes") + .option("--baseVersion ", "Compare against a given version or STAGING") + .option("--json", "Return the response as a JSON file") + .requiredOption("-f, --file ", "Exported packages file (relative or absolute path)") + .action(this.diffPackages); + const listCommand = configurator.command("list"); listCommand.command("assignments") .description("Command to list possible variable assignments for a type") @@ -222,6 +292,10 @@ class Module extends IModule { options.staging); } + private async listStagingPackages(context: Context, command: Command, options: OptionValues): Promise { + await new ConfigCommandService(context).listStagingPackages(options.json, options.flavors); + } + private async batchExportPackages(context: Context, command: Command, options: OptionValues): Promise { if ((options.packageKeys && options.keysByVersion) || (!options.packageKeys && !options.keysByVersion)) { throw new Error("Please provide either --packageKeys or --keysByVersion, but not both."); diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index 80ef7eab..03672b75 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -36,6 +36,7 @@ describe("Configuration Management Module - Action Validations", () => { mockConfigCommandService = { listPackages: jest.fn().mockResolvedValue(undefined), + listStagingPackages: jest.fn().mockResolvedValue(undefined), listVariables: jest.fn().mockResolvedValue(undefined), batchExportPackages: jest.fn().mockResolvedValue(undefined), batchImportPackages: jest.fn().mockResolvedValue(undefined), @@ -221,6 +222,38 @@ describe("Configuration Management Module - Action Validations", () => { }); }); + describe("config package list handler", () => { + it("should list staging packages with json and flavors", async () => { + const options: OptionValues = { + json: true, + flavors: ["APP"], + }; + + await (module as any).listStagingPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.listStagingPackages).toHaveBeenCalledWith(true, ["APP"]); + }); + + it("should default json and flavors to undefined when not provided", async () => { + const options: OptionValues = {}; + + await (module as any).listStagingPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.listStagingPackages).toHaveBeenCalledWith(undefined, undefined); + }); + + it("should not pass legacy listPackages options", async () => { + const options: OptionValues = { + flavors: ["APP", "ANALYSIS"], + }; + + await (module as any).listStagingPackages(testContext, mockCommand, options); + + expect(mockConfigCommandService.listStagingPackages).toHaveBeenCalledWith(undefined, ["APP", "ANALYSIS"]); + expect(mockConfigCommandService.listPackages).not.toHaveBeenCalled(); + }); + }); + describe("batchExportPackages validation", () => { describe("packageKeys and keysByVersion validation", () => { it("should throw error when both packageKeys and keysByVersion are provided", async () => { @@ -773,6 +806,8 @@ describe("Configuration Management Module - Action Validations", () => { // Top-level groups attached to the root configurator expect(mockConfigurator.command).toHaveBeenCalledWith("config"); expect(mockConfigurator.command).toHaveBeenCalledWith("list"); + expect(mockConfigurator.command).toHaveBeenCalledWith("t2tc"); + expect(mockConfigurator.command).toHaveBeenCalledWith("package"); }); it("wires an action handler for every leaf subcommand", () => { @@ -782,12 +817,26 @@ describe("Configuration Management Module - Action Validations", () => { // Each leaf command terminates the fluent chain with .action(handler). // Keep this count in sync when adding or removing commands in module.ts. - const expectedLeafCommands = 18; + const expectedLeafCommands = 24; expect(mockConfigurator.action).toHaveBeenCalledTimes(expectedLeafCommands); for (const call of mockConfigurator.action.mock.calls) { expect(typeof call[0]).toBe("function"); } }); + + it("marks the moved config commands as deprecated", () => { + const mockConfigurator = createMockConfigurator(); + + new Module().register(testContext, mockConfigurator); + + // config list/export/import/diff/validate are duplicated under t2tc package / + // config package and the originals carry a deprecation notice. + const expectedDeprecatedCommands = 5; + expect(mockConfigurator.deprecationNotice).toHaveBeenCalledTimes(expectedDeprecatedCommands); + for (const call of mockConfigurator.deprecationNotice.mock.calls) { + expect(typeof call[0]).toBe("string"); + } + }); }); describe("listNodeDependencies", () => { From a459dfcca61ba99b96ba00b771351cf55ae7c895 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Mon, 8 Jun 2026 15:02:02 +0200 Subject: [PATCH 4/7] SP-957: keep agentic dev guide on config export/import commands The agentic development guide is general package-authoring guidance, not Team-to-Team Copy. Revert its export/import examples from 't2tc package' back to the 'config' commands (which still work as deprecated aliases) and keep only the validate example on the new 'config package validate'. The general commands will move to non-t2tc package commands later. Includes-AI-Code: true Co-authored-by: Cursor --- docs/user-guide/agentic-development-guide.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user-guide/agentic-development-guide.md b/docs/user-guide/agentic-development-guide.md index b7b8a94b..f01eb6af 100644 --- a/docs/user-guide/agentic-development-guide.md +++ b/docs/user-guide/agentic-development-guide.md @@ -45,7 +45,7 @@ content-cli asset-registry examples --assetType BOARD_V2 --json ### 3. Export the target package ```bash -content-cli t2tc package export --packageKeys --unzip +content-cli config export --packageKeys --unzip ``` This gives you the directory structure with `package.json`, `manifest.json`, and existing node JSONs to use as reference. @@ -84,7 +84,7 @@ content-cli asset-registry validate --assetType \ Or validate during import with the `--validate` flag — note that this runs the `SCHEMA` layer only: ```bash -content-cli t2tc package import -d --validate --overwrite +content-cli config import -d --validate --overwrite ``` To also run business-layer rules (PQL parsing, data-model availability, KPI uniqueness, …) and package-settings checks (dependencies, variables, and flavor-specific package settings), run `config package validate` against the just-imported staging version: @@ -98,7 +98,7 @@ If validation returns errors, fix the issues before importing. ### 6. Import ```bash -content-cli t2tc package import -d --overwrite +content-cli config import -d --overwrite ``` This creates a new version in staging (not deployed). To create a brand-new package instead of updating, omit `--overwrite`. @@ -106,7 +106,7 @@ This creates a new version in staging (not deployed). To create a brand-new pack To later export a staging version, use `--keysByVersion`: ```bash -content-cli t2tc package export --keysByVersion _ --unzip +content-cli config export --keysByVersion _ --unzip ``` ## Troubleshooting From ec584216301fea4d4f6bbb5dad60cad219241527 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Mon, 8 Jun 2026 17:19:11 +0200 Subject: [PATCH 5/7] SP-957: extract t2tc module and split config batch services by domain Move the Team-to-Team Copy package operations into a self-contained t2tc module (command/package services, package + diff APIs, StudioService and transport constants) so the deprecated config aliases are thin shells. Dismantle the BatchImportExport catch-all in configuration-management by splitting its methods into their proper domains: - staging list -> StagingPackageApi / StagingPackageService - package metadata -> MetadataApi / MetadataService - variables export -> VariableApi - connection-var fix -> connection-variable.helper (shared with t2tc) configuration-management no longer holds any batch export/import code, StudioService, or transport constants; t2tc imports only the variable and package domains plus shared interface types. Includes-AI-Code: true Co-authored-by: Cursor --- .../api/metadata-api.ts | 22 ++ .../api/staging-package-api.ts | 24 ++ .../api/variable-api.ts | 19 ++ .../config-command.service.ts | 74 ------ .../connection-variable.helper.ts | 29 +++ .../metadata.service.ts | 33 +++ .../configuration-management/module.ts | 66 +----- .../staging-package.service.ts | 32 +++ .../variable.service.ts | 14 +- .../api/diff-api.ts | 4 +- .../api/t2tc-package-api.ts} | 35 +-- .../batch-export-import.constants.ts | 2 +- .../diff.service.ts | 2 +- src/commands/t2tc/module.ts | 100 ++++++++ .../studio.service.ts | 31 +-- src/commands/t2tc/t2tc-command.service.ts | 77 +++++++ .../t2tc-package.service.ts} | 71 ++---- .../config-metadata-export.spec.ts | 6 +- .../configuration-management/module.spec.ts | 103 +++++---- tests/commands/t2tc/module.spec.ts | 213 ++++++++++++++++++ .../t2tc-package-diff.spec.ts} | 12 +- .../t2tc-package-export.spec.ts} | 32 +-- .../t2tc-package-import.spec.ts} | 26 +-- .../t2tc-package-list.spec.ts} | 28 +-- 24 files changed, 714 insertions(+), 341 deletions(-) create mode 100644 src/commands/configuration-management/api/metadata-api.ts create mode 100644 src/commands/configuration-management/api/staging-package-api.ts create mode 100644 src/commands/configuration-management/api/variable-api.ts create mode 100644 src/commands/configuration-management/connection-variable.helper.ts create mode 100644 src/commands/configuration-management/metadata.service.ts create mode 100644 src/commands/configuration-management/staging-package.service.ts rename src/commands/{configuration-management => t2tc}/api/diff-api.ts (85%) rename src/commands/{configuration-management/api/batch-import-export-api.ts => t2tc/api/t2tc-package-api.ts} (72%) rename src/commands/{configuration-management/interfaces => t2tc}/batch-export-import.constants.ts (99%) rename src/commands/{configuration-management => t2tc}/diff.service.ts (96%) create mode 100644 src/commands/t2tc/module.ts rename src/commands/{configuration-management => t2tc}/studio.service.ts (92%) create mode 100644 src/commands/t2tc/t2tc-command.service.ts rename src/commands/{configuration-management/batch-import-export.service.ts => t2tc/t2tc-package.service.ts} (75%) create mode 100644 tests/commands/t2tc/module.spec.ts rename tests/commands/{configuration-management/config-diff.spec.ts => t2tc/t2tc-package-diff.spec.ts} (92%) rename tests/commands/{configuration-management/config-export.spec.ts => t2tc/t2tc-package-export.spec.ts} (96%) rename tests/commands/{configuration-management/config-import.spec.ts => t2tc/t2tc-package-import.spec.ts} (93%) rename tests/commands/{configuration-management/config-list.spec.ts => t2tc/t2tc-package-list.spec.ts} (90%) diff --git a/src/commands/configuration-management/api/metadata-api.ts b/src/commands/configuration-management/api/metadata-api.ts new file mode 100644 index 00000000..9c842b9f --- /dev/null +++ b/src/commands/configuration-management/api/metadata-api.ts @@ -0,0 +1,22 @@ +import { PackageMetadataExportTransport } from "../interfaces/package-export.interfaces"; +import { FatalError } from "../../../core/utils/logger"; +import { HttpClient } from "../../../core/http/http-client"; +import { Context } from "../../../core/command/cli-context"; + +export class MetadataApi { + + private readonly httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async exportPackagesMetadata(packageKeys: string[]): Promise { + const queryParams = new URLSearchParams(); + packageKeys.forEach(packageKey => queryParams.append("packageKeys", packageKey)); + + return this.httpClient().get(`/package-manager/api/core/packages/metadata/export?${queryParams.toString()}`).catch(e => { + throw new FatalError(`Problem exporting packages metadata: ${e}`); + }) + } +} diff --git a/src/commands/configuration-management/api/staging-package-api.ts b/src/commands/configuration-management/api/staging-package-api.ts new file mode 100644 index 00000000..2bb7d8c4 --- /dev/null +++ b/src/commands/configuration-management/api/staging-package-api.ts @@ -0,0 +1,24 @@ +import { PackageExportTransport } from "../interfaces/package-export.interfaces"; +import { FatalError } from "../../../core/utils/logger"; +import { HttpClient } from "../../../core/http/http-client"; +import { Context } from "../../../core/command/cli-context"; + +export class StagingPackageApi { + + private readonly httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async findAllStagingPackages(flavors: string[], includeBranches: boolean = false): Promise { + const queryParams = new URLSearchParams(); + + queryParams.set("includeBranches", includeBranches.toString()); + flavors.forEach(flavor => queryParams.append("flavors", flavor)); + + return this.httpClient().get(`/pacman/api/core/staging/packages/export/list?${queryParams.toString()}`).catch(e => { + throw new FatalError(`Problem getting staging packages: ${e}`); + }); + } +} diff --git a/src/commands/configuration-management/api/variable-api.ts b/src/commands/configuration-management/api/variable-api.ts new file mode 100644 index 00000000..2b730632 --- /dev/null +++ b/src/commands/configuration-management/api/variable-api.ts @@ -0,0 +1,19 @@ +import { PackageKeyAndVersionPair, VariableManifestTransport } from "../interfaces/package-export.interfaces"; +import { FatalError } from "../../../core/utils/logger"; +import { HttpClient } from "../../../core/http/http-client"; +import { Context } from "../../../core/command/cli-context"; + +export class VariableApi { + + private readonly httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async findVariablesWithValuesByPackageKeysAndVersion(packagesByKeyAndVersion: PackageKeyAndVersionPair[]): Promise { + return this.httpClient().post("/package-manager/api/core/packages/export/batch/variables-with-assignments", packagesByKeyAndVersion).catch(e => { + throw new FatalError(`Problem exporting package variables: ${e}`); + }) + } +} diff --git a/src/commands/configuration-management/config-command.service.ts b/src/commands/configuration-management/config-command.service.ts index 9f9ec48a..30178581 100644 --- a/src/commands/configuration-management/config-command.service.ts +++ b/src/commands/configuration-management/config-command.service.ts @@ -1,46 +1,12 @@ import { Context } from "../../core/command/cli-context"; -import { BatchImportExportService } from "./batch-import-export.service"; import { VariableService } from "./variable.service"; -import { DiffService } from "./diff.service"; -import { fileService } from "../../core/utils/file-service"; export class ConfigCommandService { - private batchImportExportService: BatchImportExportService; private variableService: VariableService; - private diffService: DiffService; constructor(context: Context) { - this.batchImportExportService = new BatchImportExportService(context); this.variableService = new VariableService(context); - this.diffService = new DiffService(context); - } - - public async listPackages( - jsonResponse: boolean, - flavors: string[], - withDependencies: boolean, - packageKeys: string[], - keysByVersion: string[], - variableValue: string, - variableType: string, - includeBranches: boolean, - staging: boolean): Promise { - if (staging) { - await this.batchImportExportService.listStagingPackages(flavors ?? [], includeBranches, jsonResponse); - } else if (variableValue) { - await this.listPackagesByVariableValue(jsonResponse, flavors, variableValue, variableType, includeBranches); - } else if (jsonResponse) { - await this.batchImportExportService.findAndExportListOfPackages(flavors ?? [], packageKeys ?? [], keysByVersion ?? [], withDependencies, includeBranches); - } else if (keysByVersion) { - await this.batchImportExportService.listPackagesByKeysWithVersion(keysByVersion, withDependencies); - } else { - await this.batchImportExportService.listActivePackages(flavors ?? [], includeBranches); - } - } - - public listStagingPackages(jsonResponse: boolean, flavors: string[]): Promise { - return this.batchImportExportService.listStagingPackages(flavors ?? [], false, jsonResponse); } public async listVariables( @@ -61,44 +27,4 @@ export class ConfigCommandService { await this.variableService.listVariables(keysByVersion, keysByVersionFile); } } - - public batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean): Promise { - return this.batchImportExportService.batchExportPackages(packageKeys, packageKeysByVersion, withDependencies, gitBranch, unzip); - } - - public batchExportPackagesMetadata(packageKeys: string[], jsonResponse: boolean): Promise { - return this.batchImportExportService.batchExportPackagesMetadata(packageKeys, jsonResponse); - } - - public batchImportPackages(file: string, directory: string, overwrite: boolean, gitBranch: string, performValidation: boolean = false): Promise { - if ((directory || file) && gitBranch) { - throw new Error("You cannot use both file/directory and gitBranch options at the same time. Only one import source can be defined."); - } - if (!directory && !file && !gitBranch) { - throw new Error("You must provide either a file/directory or a gitBranch option to import packages."); - } - if (file && directory) { - throw new Error("You cannot use both file and directory options at the same time. Only one import source can be defined."); - } - if (file && fileService.isDirectory(file)) { - throw new Error("The file option accepts only zip files."); - } - if (directory && !fileService.isDirectory(directory)) { - throw new Error("The directory option accepts only directories."); - } - const sourcePath = file ?? directory; - return this.batchImportExportService.batchImportPackages(sourcePath, overwrite, gitBranch, performValidation); - } - - public diffPackages(file: string, hasChanges: boolean, baseVersion: string, jsonResponse: boolean): Promise { - return this.diffService.diffPackages(file, hasChanges, baseVersion, jsonResponse); - } - - private async listPackagesByVariableValue(jsonResponse: boolean, flavors: string[], variableValue: string, variableType: string, includeBranches: boolean): Promise { - if (jsonResponse) { - await this.batchImportExportService.findAndExportListOfActivePackagesByVariableValue(flavors ?? [], variableValue, variableType, includeBranches) - } else { - await this.batchImportExportService.listActivePackagesByVariableValue(flavors ?? [], variableValue, variableType, includeBranches); - } - } } diff --git a/src/commands/configuration-management/connection-variable.helper.ts b/src/commands/configuration-management/connection-variable.helper.ts new file mode 100644 index 00000000..3f1708b2 --- /dev/null +++ b/src/commands/configuration-management/connection-variable.helper.ts @@ -0,0 +1,29 @@ +import { PackageManagerVariableType } from "../studio/interfaces/package-manager.interfaces"; +import { VariableExportTransport, VariableManifestTransport } from "./interfaces/package-export.interfaces"; + +export function fixConnectionVariables(variables: VariableManifestTransport[]): VariableManifestTransport[] { + return variables.map(variableManifest => ({ + ...variableManifest, + variables: variableManifest.variables.map(variable => { + if (variable.type !== PackageManagerVariableType.CONNECTION) { + return variable; + } + + return fixConnectionVariable(variable); + }) + })); +} + +function fixConnectionVariable(variable: VariableExportTransport): VariableExportTransport { + if (!variable.value?.appName) { + return variable; + } + + return { + ...variable, + metadata: { + ...variable.metadata, + appName: variable.value.appName + } + }; +} diff --git a/src/commands/configuration-management/metadata.service.ts b/src/commands/configuration-management/metadata.service.ts new file mode 100644 index 00000000..3d098f75 --- /dev/null +++ b/src/commands/configuration-management/metadata.service.ts @@ -0,0 +1,33 @@ +import { v4 as uuidv4 } from "uuid"; +import { Context } from "../../core/command/cli-context"; +import { PackageMetadataExportTransport } from "./interfaces/package-export.interfaces"; +import { fileService, FileService } from "../../core/utils/file-service"; +import { logger } from "../../core/utils/logger"; +import { MetadataApi } from "./api/metadata-api"; + +export class MetadataService { + + private metadataApi: MetadataApi; + + constructor(context: Context) { + this.metadataApi = new MetadataApi(context); + } + + public async exportPackagesMetadata(packageKeys: string[], jsonResponse: boolean): Promise { + const exportedPackagesMetadata: PackageMetadataExportTransport[] = await this.metadataApi.exportPackagesMetadata(packageKeys); + + if (jsonResponse) { + this.exportListOfPackagesMetadata(exportedPackagesMetadata); + } else { + exportedPackagesMetadata.forEach(pkg => { + logger.info(`${pkg.key} - Has Unpublished Changes: ${pkg.hasUnpublishedChanges}`); + }); + } + } + + private exportListOfPackagesMetadata(packagesMetadata: PackageMetadataExportTransport[]): void { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(packagesMetadata), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } +} diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index ca4ebbe7..a8636276 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -6,6 +6,9 @@ import { Configurator, IModule } from "../../core/command/module-handler"; import { Context } from "../../core/command/cli-context"; import { Command, OptionValues } from "commander"; import { ConfigCommandService } from "./config-command.service"; +import { StagingPackageService } from "./staging-package.service"; +import { MetadataService } from "./metadata.service"; +import { T2tcCommandService } from "../t2tc/t2tc-command.service"; import { VariableCommandService } from "./variable-command.service"; import { NodeService } from "./node.service"; import { NodeDiffService } from "./node-diff.service"; @@ -52,7 +55,7 @@ class Module extends IModule { .description("Show whether multiple packages have unpublished changes (bulk metadata export).") .requiredOption("--packageKeys ", "Keys of packages to find the metadata of") .option("--json", "Return response as json type", "") - .action(this.batchExportPackagesMetadata); + .action(this.exportPackagesMetadata); configCommand.command("import") .description("[Deprecated] Use 't2tc package import' (batch) or 'config package import' (single package) instead. Import packages from a Team-to-Team Copy archive.") @@ -216,53 +219,6 @@ class Module extends IModule { .option("--json", "Return the response as a JSON file") .action(this.listNodeDependencies); - const t2tcCommand = configurator.command("t2tc") - .description("Team-to-Team Copy (T2TC) commands for moving whole packages and their dependencies between teams using the batch transport archive format."); - - const t2tcPackageCommand = t2tcCommand.command("package") - .description("Team-to-Team Copy package commands: list, export, import and diff packages using the batch transport archive."); - - t2tcPackageCommand.command("list") - .description("List packages in the target team. Part of the Team-to-Team Copy export/import workflow.") - .option("--json", "Return response as json type", "") - .option("--flavors ", "Lists only packages of the given flavors") - .option("--withDependencies", "Include dependencies", "") - .option("--packageKeys ", "Lists only active versions of given package keys") - .option("--keysByVersion ", "Lists packages by given key and version [packageKey.version]") - .option("--variableValue ", "Variable value for filtering packages by.") - .option("--variableType ", "Variable type for filtering packages by.") - .option("--branches", "Include branches", false) - .option("--staging", "List staging packages instead", false) - .action(this.listPackages); - - t2tcPackageCommand.command("export") - .description("Export one or more packages into a Team-to-Team Copy archive. The archive is consumed by 't2tc package import' / 't2tc package diff'.") - .option("--packageKeys ", "Keys of packages to export. Exports the latest deployed version only") - .option("--keysByVersion ", "Keys of packages to export by version") - .option("--withDependencies", "Include variables and dependencies", "") - .option("--unzip", "Unzip the exported file", "") - .option("--gitProfile ", "Git profile which you want to use for the Git operations") - .option("--gitBranch ", "Git branch in which you want to push the exported file") - .action(this.batchExportPackages); - - t2tcPackageCommand.command("import") - .description("Import packages from a Team-to-Team Copy archive produced by 't2tc package export'. To import a single standalone package, use 'config package import'.") - .option("--overwrite", "Flag to allow overwriting of packages") - .option("--validate", "Validate node configurations before import", false) - .option("--gitProfile ", "Git profile which you want to use for the Git operations") - .option("--gitBranch ", "Git branch from which you want to pull the exported file and import") - .option("-f, --file ", "Exported packages file (relative path)") - .option("-d, --directory ", "Exported packages directory (relative path)") - .action(this.batchImportPackages); - - t2tcPackageCommand.command("diff") - .description("Diff a local Team-to-Team Copy archive (from 't2tc package export') against deployed or staging packages.") - .option("--hasChanges", "Flag to return only the information if the package has changes without the actual changes") - .option("--baseVersion ", "Compare against a given version or STAGING") - .option("--json", "Return the response as a JSON file") - .requiredOption("-f, --file ", "Exported packages file (relative or absolute path)") - .action(this.diffPackages); - const listCommand = configurator.command("list"); listCommand.command("assignments") .description("Command to list possible variable assignments for a type") @@ -280,7 +236,7 @@ class Module extends IModule { throw new Error("Please provide either --packageKeys or --keysByVersion, but not both."); } - await new ConfigCommandService(context).listPackages( + await new T2tcCommandService(context).listPackages( options.json, options.flavors, options.withDependencies, @@ -293,7 +249,7 @@ class Module extends IModule { } private async listStagingPackages(context: Context, command: Command, options: OptionValues): Promise { - await new ConfigCommandService(context).listStagingPackages(options.json, options.flavors); + await new StagingPackageService(context).listStagingPackages(options.flavors ?? [], false, options.json); } private async batchExportPackages(context: Context, command: Command, options: OptionValues): Promise { @@ -304,11 +260,11 @@ class Module extends IModule { throw new Error("Please specify a branch using --gitBranch when using a Git profile."); } options.withDependencies = options.withDependencies ?? false; - await new ConfigCommandService(context).batchExportPackages(options.packageKeys, options.keysByVersion, options.withDependencies, options.gitBranch, options.unzip); + await new T2tcCommandService(context).batchExportPackages(options.packageKeys, options.keysByVersion, options.withDependencies, options.gitBranch, options.unzip); } - private async batchExportPackagesMetadata(context: Context, command: Command, options: OptionValues): Promise { - await new ConfigCommandService(context).batchExportPackagesMetadata(options.packageKeys, options.json); + private async exportPackagesMetadata(context: Context, command: Command, options: OptionValues): Promise { + await new MetadataService(context).exportPackagesMetadata(options.packageKeys, options.json); } private async getPackageVersion(context: Context, command: Command, options: OptionValues): Promise { @@ -340,7 +296,7 @@ class Module extends IModule { if (options.gitProfile && !options.gitBranch) { throw new Error("Please specify a branch using --gitBranch when using a Git profile."); } - await new ConfigCommandService(context).batchImportPackages(options.file, options.directory, options.overwrite, options.gitBranch, options.validate); + await new T2tcCommandService(context).batchImportPackages(options.file, options.directory, options.overwrite, options.gitBranch, options.validate); } private async importSinglePackage(context: Context, command: Command, options: OptionValues): Promise { @@ -348,7 +304,7 @@ class Module extends IModule { } private async diffPackages(context: Context, command: Command, options: OptionValues): Promise { - await new ConfigCommandService(context).diffPackages(options.file, options.hasChanges, options.baseVersion, options.json); + await new T2tcCommandService(context).diffPackages(options.file, options.hasChanges, options.baseVersion, options.json); } private async validatePackage(context: Context, command: Command, options: OptionValues): Promise { diff --git a/src/commands/configuration-management/staging-package.service.ts b/src/commands/configuration-management/staging-package.service.ts new file mode 100644 index 00000000..16c60ebf --- /dev/null +++ b/src/commands/configuration-management/staging-package.service.ts @@ -0,0 +1,32 @@ +import { v4 as uuidv4 } from "uuid"; +import { Context } from "../../core/command/cli-context"; +import { PackageExportTransport } from "./interfaces/package-export.interfaces"; +import { fileService, FileService } from "../../core/utils/file-service"; +import { logger } from "../../core/utils/logger"; +import { StagingPackageApi } from "./api/staging-package-api"; + +export class StagingPackageService { + + private stagingPackageApi: StagingPackageApi; + + constructor(context: Context) { + this.stagingPackageApi = new StagingPackageApi(context); + } + + public async listStagingPackages(flavors: string[], includeBranches: boolean, jsonResponse: boolean): Promise { + const stagingPackages = await this.stagingPackageApi.findAllStagingPackages(flavors, includeBranches); + if (jsonResponse) { + this.exportListOfPackages(stagingPackages); + } else { + stagingPackages.forEach(pkg => { + logger.info(`${pkg.name} - Key: "${pkg.key}"`); + }); + } + } + + private exportListOfPackages(packages: PackageExportTransport[]): void { + const filename = uuidv4() + ".json"; + fileService.writeToFileWithGivenName(JSON.stringify(packages), filename); + logger.info(FileService.fileDownloadedMessage + filename); + } +} diff --git a/src/commands/configuration-management/variable.service.ts b/src/commands/configuration-management/variable.service.ts index 2b636298..08945abd 100644 --- a/src/commands/configuration-management/variable.service.ts +++ b/src/commands/configuration-management/variable.service.ts @@ -1,26 +1,24 @@ import { v4 as uuidv4 } from "uuid"; import { Context } from "../../core/command/cli-context"; import { FatalError, logger } from "../../core/utils/logger"; -import { StudioService } from "./studio.service"; +import { fixConnectionVariables } from "./connection-variable.helper"; import { FileService, fileService } from "../../core/utils/file-service"; import { PackageKeyAndVersionPair, StagingVariableManifestTransport, VariableManifestTransport } from "./interfaces/package-export.interfaces"; -import { BatchImportExportApi } from "./api/batch-import-export-api"; +import { VariableApi } from "./api/variable-api"; import { URLSearchParams } from "url"; import { VariableAssignmentCandidatesApi } from "./api/variable-assignment-candidates-api"; import { StagingPackageVariablesApi } from "./api/staging-package-variables-api"; export class VariableService { - private batchImportExportApi: BatchImportExportApi; + private variableApi: VariableApi; private variableAssignmentCandidatesApi: VariableAssignmentCandidatesApi; private readonly stagingPackageVariablesApi: StagingPackageVariablesApi; - private studioService: StudioService; constructor(context: Context) { - this.batchImportExportApi = new BatchImportExportApi(context); + this.variableApi = new VariableApi(context); this.variableAssignmentCandidatesApi = new VariableAssignmentCandidatesApi(context); this.stagingPackageVariablesApi = new StagingPackageVariablesApi(context); - this.studioService = new StudioService(context); } public async listVariables(keysByVersion: string[], keysByVersionFile: string): Promise { @@ -74,8 +72,8 @@ export class VariableService { private async getVersionedVariablesByKeyVersionPairs(keysByVersion: string[], keysByVersionFile: string): Promise { const variablesExportRequest: PackageKeyAndVersionPair[] = await this.buildKeyVersionPairs(keysByVersion, keysByVersionFile); - const variableManifests = await this.batchImportExportApi.findVariablesWithValuesByPackageKeysAndVersion(variablesExportRequest); - return this.studioService.fixConnectionVariables(variableManifests); + const variableManifests = await this.variableApi.findVariablesWithValuesByPackageKeysAndVersion(variablesExportRequest); + return fixConnectionVariables(variableManifests); } private async buildKeyVersionPairs(keysByVersion: string[], keysByVersionFile: string): Promise { diff --git a/src/commands/configuration-management/api/diff-api.ts b/src/commands/t2tc/api/diff-api.ts similarity index 85% rename from src/commands/configuration-management/api/diff-api.ts rename to src/commands/t2tc/api/diff-api.ts index e3739e91..1999d4b5 100644 --- a/src/commands/configuration-management/api/diff-api.ts +++ b/src/commands/t2tc/api/diff-api.ts @@ -1,11 +1,11 @@ import * as FormData from "form-data"; import { HttpClient } from "../../../core/http/http-client"; import { Context } from "../../../core/command/cli-context"; -import { PackageDiffMetadata, PackageDiffTransport } from "../interfaces/diff-package.interfaces"; +import { PackageDiffMetadata, PackageDiffTransport } from "../../configuration-management/interfaces/diff-package.interfaces"; export class DiffApi { - private httpClient: () => HttpClient; + private readonly httpClient: () => HttpClient; constructor(context: Context) { this.httpClient = () => context.httpClient; diff --git a/src/commands/configuration-management/api/batch-import-export-api.ts b/src/commands/t2tc/api/t2tc-package-api.ts similarity index 72% rename from src/commands/configuration-management/api/batch-import-export-api.ts rename to src/commands/t2tc/api/t2tc-package-api.ts index 12983ca3..27e4cf0b 100644 --- a/src/commands/configuration-management/api/batch-import-export-api.ts +++ b/src/commands/t2tc/api/t2tc-package-api.ts @@ -1,16 +1,15 @@ import * as FormData from "form-data"; import { PackageExportTransport, - PackageKeyAndVersionPair, PackageMetadataExportTransport, - PostPackageImportData, VariableManifestTransport, -} from "../interfaces/package-export.interfaces"; + PostPackageImportData, +} from "../../configuration-management/interfaces/package-export.interfaces"; import { FatalError } from "../../../core/utils/logger"; import { HttpClient } from "../../../core/http/http-client"; import { Context } from "../../../core/command/cli-context"; -export class BatchImportExportApi { +export class T2tcPackageApi { - private httpClient: () => HttpClient; + private readonly httpClient: () => HttpClient; constructor(context: Context) { this.httpClient = () => context.httpClient; @@ -28,17 +27,6 @@ export class BatchImportExportApi { }); } - public async findAllStagingPackages(flavors: string[], includeBranches: boolean = false): Promise { - const queryParams = new URLSearchParams(); - - queryParams.set("includeBranches", includeBranches.toString()); - flavors.forEach(flavor => queryParams.append("flavors", flavor)); - - return this.httpClient().get(`/pacman/api/core/staging/packages/export/list?${queryParams.toString()}`).catch(e => { - throw new FatalError(`Problem getting staging packages: ${e}`); - }); - } - public async findActivePackagesByVariableValue(flavors: string[], variableValue: string, variableType: string, includeBranches: boolean = false): Promise { const queryParams = new URLSearchParams(); @@ -96,15 +84,6 @@ export class BatchImportExportApi { }); } - public async batchExportPackagesMetadata(packageKeys: string[]): Promise { - const queryParams = new URLSearchParams(); - packageKeys.forEach(packageKey => queryParams.append("packageKeys", packageKey)); - - return this.httpClient().get(`/package-manager/api/core/packages/metadata/export?${queryParams.toString()}`).catch(e => { - throw new FatalError(`Problem exporting packages metadata: ${e}`); - }) - } - public async importPackages(data: FormData, overwrite: boolean, performValidation: boolean): Promise { return this.httpClient().postFile( "/package-manager/api/core/packages/import/batch", @@ -112,10 +91,4 @@ export class BatchImportExportApi { {overwrite, performValidation} ); } - - public async findVariablesWithValuesByPackageKeysAndVersion(packagesByKeyAndVersion: PackageKeyAndVersionPair[]): Promise { - return this.httpClient().post("/package-manager/api/core/packages/export/batch/variables-with-assignments", packagesByKeyAndVersion).catch(e => { - throw new FatalError(`Problem exporting package variables: ${e}`); - }) - } } diff --git a/src/commands/configuration-management/interfaces/batch-export-import.constants.ts b/src/commands/t2tc/batch-export-import.constants.ts similarity index 99% rename from src/commands/configuration-management/interfaces/batch-export-import.constants.ts rename to src/commands/t2tc/batch-export-import.constants.ts index a0c815ea..534543c6 100644 --- a/src/commands/configuration-management/interfaces/batch-export-import.constants.ts +++ b/src/commands/t2tc/batch-export-import.constants.ts @@ -8,4 +8,4 @@ export enum BatchExportImportConstants { JSON_EXTENSION = ".json", NODES_FOLDER_NAME = "nodes/", SCENARIO_NODE = "SCENARIO" -} \ No newline at end of file +} diff --git a/src/commands/configuration-management/diff.service.ts b/src/commands/t2tc/diff.service.ts similarity index 96% rename from src/commands/configuration-management/diff.service.ts rename to src/commands/t2tc/diff.service.ts index d0246bd5..c07f03ba 100644 --- a/src/commands/configuration-management/diff.service.ts +++ b/src/commands/t2tc/diff.service.ts @@ -5,7 +5,7 @@ import {v4 as uuidv4} from "uuid"; import { logger } from "../../core/utils/logger"; import { fileService, FileService } from "../../core/utils/file-service"; import { Context } from "../../core/command/cli-context"; -import { PackageDiffMetadata, PackageDiffTransport } from "./interfaces/diff-package.interfaces"; +import { PackageDiffMetadata, PackageDiffTransport } from "../configuration-management/interfaces/diff-package.interfaces"; import { DiffApi } from "./api/diff-api"; export class DiffService { diff --git a/src/commands/t2tc/module.ts b/src/commands/t2tc/module.ts new file mode 100644 index 00000000..fc3ede07 --- /dev/null +++ b/src/commands/t2tc/module.ts @@ -0,0 +1,100 @@ +import { Configurator, IModule } from "../../core/command/module-handler"; +import { Context } from "../../core/command/cli-context"; +import { Command, OptionValues } from "commander"; +import { T2tcCommandService } from "./t2tc-command.service"; + +class Module extends IModule { + + public register(context: Context, configurator: Configurator): void { + const t2tcCommand = configurator.command("t2tc") + .description("Team-to-Team Copy (T2TC) commands for moving whole packages and their dependencies between teams using the batch transport archive format."); + + const t2tcPackageCommand = t2tcCommand.command("package") + .description("Team-to-Team Copy package commands: list, export, import and diff packages using the batch transport archive."); + + t2tcPackageCommand.command("list") + .description("List packages in the target team. Part of the Team-to-Team Copy export/import workflow.") + .option("--json", "Return response as json type", "") + .option("--flavors ", "Lists only packages of the given flavors") + .option("--withDependencies", "Include dependencies", "") + .option("--packageKeys ", "Lists only active versions of given package keys") + .option("--keysByVersion ", "Lists packages by given key and version [packageKey.version]") + .option("--variableValue ", "Variable value for filtering packages by.") + .option("--variableType ", "Variable type for filtering packages by.") + .option("--branches", "Include branches", false) + .option("--staging", "List staging packages instead", false) + .action(this.listPackages); + + t2tcPackageCommand.command("export") + .description("Export one or more packages into a Team-to-Team Copy archive. The archive is consumed by 't2tc package import' / 't2tc package diff'.") + .option("--packageKeys ", "Keys of packages to export. Exports the latest deployed version only") + .option("--keysByVersion ", "Keys of packages to export by version") + .option("--withDependencies", "Include variables and dependencies", "") + .option("--unzip", "Unzip the exported file", "") + .option("--gitProfile ", "Git profile which you want to use for the Git operations") + .option("--gitBranch ", "Git branch in which you want to push the exported file") + .action(this.batchExportPackages); + + t2tcPackageCommand.command("import") + .description("Import packages from a Team-to-Team Copy archive produced by 't2tc package export'. To import a single standalone package, use 'config package import'.") + .option("--overwrite", "Flag to allow overwriting of packages") + .option("--validate", "Validate node configurations before import", false) + .option("--gitProfile ", "Git profile which you want to use for the Git operations") + .option("--gitBranch ", "Git branch from which you want to pull the exported file and import") + .option("-f, --file ", "Exported packages file (relative path)") + .option("-d, --directory ", "Exported packages directory (relative path)") + .action(this.batchImportPackages); + + t2tcPackageCommand.command("diff") + .description("Diff a local Team-to-Team Copy archive (from 't2tc package export') against deployed or staging packages.") + .option("--hasChanges", "Flag to return only the information if the package has changes without the actual changes") + .option("--baseVersion ", "Compare against a given version or STAGING") + .option("--json", "Return the response as a JSON file") + .requiredOption("-f, --file ", "Exported packages file (relative or absolute path)") + .action(this.diffPackages); + } + + private async listPackages(context: Context, command: Command, options: OptionValues): Promise { + if (options.staging && (options.withDependencies || options.packageKeys || options.keysByVersion || options.variableValue || options.variableType)) { + throw new Error("Staging parameter is not compatible with --withDependencies, --packageKeys, --keysByVersion, --variableValue, --variableType"); + } + if (options.packageKeys && options.keysByVersion) { + throw new Error("Please provide either --packageKeys or --keysByVersion, but not both."); + } + + await new T2tcCommandService(context).listPackages( + options.json, + options.flavors, + options.withDependencies, + options.packageKeys, + options.keysByVersion, + options.variableValue, + options.variableType, + options.branches, + options.staging); + } + + private async batchExportPackages(context: Context, command: Command, options: OptionValues): Promise { + if ((options.packageKeys && options.keysByVersion) || (!options.packageKeys && !options.keysByVersion)) { + throw new Error("Please provide either --packageKeys or --keysByVersion, but not both."); + } + if (options.gitProfile && !options.gitBranch) { + throw new Error("Please specify a branch using --gitBranch when using a Git profile."); + } + options.withDependencies = options.withDependencies ?? false; + await new T2tcCommandService(context).batchExportPackages(options.packageKeys, options.keysByVersion, options.withDependencies, options.gitBranch, options.unzip); + } + + private async batchImportPackages(context: Context, command: Command, options: OptionValues): Promise { + if (options.gitProfile && !options.gitBranch) { + throw new Error("Please specify a branch using --gitBranch when using a Git profile."); + } + await new T2tcCommandService(context).batchImportPackages(options.file, options.directory, options.overwrite, options.gitBranch, options.validate); + } + + private async diffPackages(context: Context, command: Command, options: OptionValues): Promise { + await new T2tcCommandService(context).diffPackages(options.file, options.hasChanges, options.baseVersion, options.json); + } +} + +export = Module; diff --git a/src/commands/configuration-management/studio.service.ts b/src/commands/t2tc/studio.service.ts similarity index 92% rename from src/commands/configuration-management/studio.service.ts rename to src/commands/t2tc/studio.service.ts index b729460b..e99a9db2 100644 --- a/src/commands/configuration-management/studio.service.ts +++ b/src/commands/t2tc/studio.service.ts @@ -6,13 +6,13 @@ import { PackageExportTransport, PackageKeyAndVersionPair, StudioPackageManifest, VariableExportTransport, VariableManifestTransport, -} from "./interfaces/package-export.interfaces"; +} from "../configuration-management/interfaces/package-export.interfaces"; import { ContentNodeTransport, PackageManagerVariableType, PackageWithVariableAssignments, StudioComputeNodeDescriptor, } from "../studio/interfaces/package-manager.interfaces"; -import { BatchExportImportConstants } from "./interfaces/batch-export-import.constants"; +import { BatchExportImportConstants } from "./batch-export-import.constants"; import { SpaceTransport } from "../studio/interfaces/space.interface"; import { parse, stringify } from "../../core/utils/json"; import { Context } from "../../core/command/cli-context"; @@ -60,19 +60,6 @@ export class StudioService { return packagesToExport; } - public fixConnectionVariables(variables: VariableManifestTransport[]): VariableManifestTransport[] { - return variables.map(variableManifest => ({ - ...variableManifest, - variables: variableManifest.variables.map(variable => { - if (variable.type !== PackageManagerVariableType.CONNECTION) { - return variable; - } - - return this.fixConnectionVariable(variable); - }) - })); - } - public async getStudioPackageManifests(studioPackageKeys: string[]): Promise { return Promise.all(studioPackageKeys.map(async packageKey => { const node = await this.studioNodeApi.findOneByKeyAndRootNodeKey(packageKey, packageKey); @@ -142,20 +129,6 @@ export class StudioService { }); } - private fixConnectionVariable(variable: VariableExportTransport): VariableExportTransport { - if (!variable.value?.appName) { - return variable; - } - - return { - ...variable, - metadata: { - ...variable.metadata, - appName: variable.value.appName - } - } - } - private deleteScenarioAssets(packageZip: AdmZip): void { packageZip.getEntries().filter(entry => entry.entryName.startsWith(BatchExportImportConstants.NODES_FOLDER_NAME) && entry.entryName.endsWith(BatchExportImportConstants.JSON_EXTENSION)) .forEach(entry => { diff --git a/src/commands/t2tc/t2tc-command.service.ts b/src/commands/t2tc/t2tc-command.service.ts new file mode 100644 index 00000000..eb75e313 --- /dev/null +++ b/src/commands/t2tc/t2tc-command.service.ts @@ -0,0 +1,77 @@ +import { Context } from "../../core/command/cli-context"; +import { fileService } from "../../core/utils/file-service"; +import { T2tcPackageService } from "./t2tc-package.service"; +import { DiffService } from "./diff.service"; +import { StagingPackageService } from "../configuration-management/staging-package.service"; + +export class T2tcCommandService { + + private t2tcPackageService: T2tcPackageService; + private diffService: DiffService; + private stagingPackageService: StagingPackageService; + + constructor(context: Context) { + this.t2tcPackageService = new T2tcPackageService(context); + this.diffService = new DiffService(context); + this.stagingPackageService = new StagingPackageService(context); + } + + public async listPackages( + jsonResponse: boolean, + flavors: string[], + withDependencies: boolean, + packageKeys: string[], + keysByVersion: string[], + variableValue: string, + variableType: string, + includeBranches: boolean, + staging: boolean): Promise { + if (staging) { + await this.stagingPackageService.listStagingPackages(flavors ?? [], includeBranches, jsonResponse); + } else if (variableValue) { + await this.listPackagesByVariableValue(jsonResponse, flavors, variableValue, variableType, includeBranches); + } else if (jsonResponse) { + await this.t2tcPackageService.findAndExportListOfPackages(flavors ?? [], packageKeys ?? [], keysByVersion ?? [], withDependencies, includeBranches); + } else if (keysByVersion) { + await this.t2tcPackageService.listPackagesByKeysWithVersion(keysByVersion, withDependencies); + } else { + await this.t2tcPackageService.listActivePackages(flavors ?? [], includeBranches); + } + } + + public batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean): Promise { + return this.t2tcPackageService.batchExportPackages(packageKeys, packageKeysByVersion, withDependencies, gitBranch, unzip); + } + + public batchImportPackages(file: string, directory: string, overwrite: boolean, gitBranch: string, performValidation: boolean = false): Promise { + if ((directory || file) && gitBranch) { + throw new Error("You cannot use both file/directory and gitBranch options at the same time. Only one import source can be defined."); + } + if (!directory && !file && !gitBranch) { + throw new Error("You must provide either a file/directory or a gitBranch option to import packages."); + } + if (file && directory) { + throw new Error("You cannot use both file and directory options at the same time. Only one import source can be defined."); + } + if (file && fileService.isDirectory(file)) { + throw new Error("The file option accepts only zip files."); + } + if (directory && !fileService.isDirectory(directory)) { + throw new Error("The directory option accepts only directories."); + } + const sourcePath = file ?? directory; + return this.t2tcPackageService.batchImportPackages(sourcePath, overwrite, gitBranch, performValidation); + } + + public diffPackages(file: string, hasChanges: boolean, baseVersion: string, jsonResponse: boolean): Promise { + return this.diffService.diffPackages(file, hasChanges, baseVersion, jsonResponse); + } + + private async listPackagesByVariableValue(jsonResponse: boolean, flavors: string[], variableValue: string, variableType: string, includeBranches: boolean): Promise { + if (jsonResponse) { + await this.t2tcPackageService.findAndExportListOfActivePackagesByVariableValue(flavors ?? [], variableValue, variableType, includeBranches) + } else { + await this.t2tcPackageService.listActivePackagesByVariableValue(flavors ?? [], variableValue, variableType, includeBranches); + } + } +} diff --git a/src/commands/configuration-management/batch-import-export.service.ts b/src/commands/t2tc/t2tc-package.service.ts similarity index 75% rename from src/commands/configuration-management/batch-import-export.service.ts rename to src/commands/t2tc/t2tc-package.service.ts index 037daf1b..aed32676 100644 --- a/src/commands/configuration-management/batch-import-export.service.ts +++ b/src/commands/t2tc/t2tc-package.service.ts @@ -5,30 +5,34 @@ import * as AdmZip from "adm-zip"; import { Context } from "../../core/command/cli-context"; import { PackageExportTransport, PackageKeyAndVersionPair, - PackageManifestTransport, PackageMetadataExportTransport, + PackageManifestTransport, StudioPackageManifest, VariableManifestTransport, -} from "./interfaces/package-export.interfaces"; -import { BatchExportImportConstants } from "./interfaces/batch-export-import.constants"; +} from "../configuration-management/interfaces/package-export.interfaces"; +import { BatchExportImportConstants } from "./batch-export-import.constants"; import { fileService, FileService } from "../../core/utils/file-service"; import { logger } from "../../core/utils/logger"; import { parse, stringify } from "../../core/utils/json"; import { PackageApi } from "../studio/api/package-api"; -import { BatchImportExportApi } from "./api/batch-import-export-api"; +import { T2tcPackageApi } from "./api/t2tc-package-api"; +import { VariableApi } from "../configuration-management/api/variable-api"; +import { fixConnectionVariables } from "../configuration-management/connection-variable.helper"; import { StudioService } from "./studio.service"; import { GitService } from "../../core/git-profile/git/git.service"; import * as fs from "fs"; import { FileConstants } from "../../core/utils/file.constants"; -export class BatchImportExportService { +export class T2tcPackageService { - private batchImportExportApi: BatchImportExportApi; + private t2tcPackageApi: T2tcPackageApi; + private variableApi: VariableApi; private studioPackageApi: PackageApi; private studioService: StudioService; private gitService: GitService; constructor(context: Context) { - this.batchImportExportApi = new BatchImportExportApi(context); + this.t2tcPackageApi = new T2tcPackageApi(context); + this.variableApi = new VariableApi(context); this.studioPackageApi = new PackageApi(context); this.studioService = new StudioService(context); @@ -36,32 +40,21 @@ export class BatchImportExportService { } public async listActivePackages(flavors: string[], includeBranches: boolean): Promise { - const activePackages = await this.batchImportExportApi.findAllActivePackages(flavors, false, includeBranches); + const activePackages = await this.t2tcPackageApi.findAllActivePackages(flavors, false, includeBranches); activePackages.forEach(pkg => { logger.info(`${pkg.name} - Key: "${pkg.key}"`) }); } - public async listStagingPackages(flavors: string[], includeBranches: boolean, jsonResponse: boolean): Promise { - const stagingPackages = await this.batchImportExportApi.findAllStagingPackages(flavors, includeBranches); - if (jsonResponse) { - this.exportListOfPackages(stagingPackages); - } else { - stagingPackages.forEach(pkg => { - logger.info(`${pkg.name} - Key: "${pkg.key}"`); - }); - } - } - public async findAndExportListOfPackages(flavors: string[], packageKeys: string[], keysByVersion: string[], withDependencies: boolean, includeBranches: boolean): Promise { let packagesToExport: PackageExportTransport[]; if (keysByVersion.length) { - packagesToExport = await this.batchImportExportApi.findPackagesByKeysAndVersion(keysByVersion, withDependencies); + packagesToExport = await this.t2tcPackageApi.findPackagesByKeysAndVersion(keysByVersion, withDependencies); } else if (packageKeys.length) { - packagesToExport = await this.batchImportExportApi.findActivePackagesByKeys(packageKeys, withDependencies); + packagesToExport = await this.t2tcPackageApi.findActivePackagesByKeys(packageKeys, withDependencies); } else { - packagesToExport = await this.batchImportExportApi.findAllActivePackages(flavors, withDependencies, includeBranches); + packagesToExport = await this.t2tcPackageApi.findAllActivePackages(flavors, withDependencies, includeBranches); } packagesToExport = await this.studioService.getExportPackagesWithStudioData(packagesToExport, withDependencies); @@ -70,7 +63,7 @@ export class BatchImportExportService { } public async listPackagesByKeysWithVersion(keysByVersion: string[], withDependencies: boolean): Promise { - const exportedPackages = await this.batchImportExportApi.findPackagesByKeysAndVersion(keysByVersion, withDependencies); + const exportedPackages = await this.t2tcPackageApi.findPackagesByKeysAndVersion(keysByVersion, withDependencies); exportedPackages.forEach(pkg => { logger.info(`${pkg.name} - Key: "${pkg.key}"`); }); @@ -79,9 +72,9 @@ export class BatchImportExportService { public async batchExportPackages(packageKeys: string[], packageKeysByVersion: string[], withDependencies: boolean, gitBranch: string, unzip: boolean): Promise { let exportedPackagesData: Buffer; if (packageKeys) { - exportedPackagesData = await this.batchImportExportApi.exportPackages(packageKeys, withDependencies); + exportedPackagesData = await this.t2tcPackageApi.exportPackages(packageKeys, withDependencies); } else { - exportedPackagesData = await this.batchImportExportApi.exportPackagesByVersions(packageKeysByVersion, withDependencies); + exportedPackagesData = await this.t2tcPackageApi.exportPackagesByVersions(packageKeysByVersion, withDependencies); } const exportedPackagesZip: AdmZip = new AdmZip(exportedPackagesData); @@ -93,7 +86,7 @@ export class BatchImportExportService { const versionsByPackageKey = this.getVersionsByPackageKey(manifest); let exportedVariables = await this.getVersionedVariablesForPackagesWithKeys(versionsByPackageKey); - exportedVariables = this.studioService.fixConnectionVariables(exportedVariables); + exportedVariables = fixConnectionVariables(exportedVariables); exportedPackagesZip.addFile(BatchExportImportConstants.VARIABLES_FILE_NAME, Buffer.from(stringify(exportedVariables), "utf8"), "", FileConstants.DEFAULT_FILE_PERMISSIONS); const studioPackageKeys = manifest.filter(packageManifest => packageManifest.flavor === BatchExportImportConstants.STUDIO) @@ -124,18 +117,6 @@ export class BatchImportExportService { } } - public async batchExportPackagesMetadata(packageKeys: string[], jsonResponse: boolean): Promise { - const exportedPackagesMetadata: PackageMetadataExportTransport[] = await this.batchImportExportApi.batchExportPackagesMetadata(packageKeys); - - if (jsonResponse) { - this.exportListOfPackagesMetadata(exportedPackagesMetadata); - } else { - exportedPackagesMetadata.forEach(pkg => { - logger.info(`${pkg.key} - Has Unpublished Changes: ${pkg.hasUnpublishedChanges}`); - }); - } - } - public async batchImportPackages(sourcePath: string, overwrite: boolean, gitBranch: string, performValidation: boolean = false): Promise { let sourceToBeImported: string; let temporaryGitFolder: string; @@ -157,7 +138,7 @@ export class BatchImportExportService { const existingStudioPackages = await this.studioPackageApi.findAllPackages(); const formData = this.buildBodyForImport(configs, sourceToBeImported, variablesManifests); - const postPackageImportData = await this.batchImportExportApi.importPackages(formData, overwrite, performValidation); + const postPackageImportData = await this.t2tcPackageApi.importPackages(formData, overwrite, performValidation); await this.studioService.processImportedPackages(configs, existingStudioPackages, studioManifests); if (gitBranch) { @@ -171,7 +152,7 @@ export class BatchImportExportService { } public async findAndExportListOfActivePackagesByVariableValue(flavors: string[], variableValue: string, variableType: string, includeBranches: boolean): Promise { - let packagesToExport = await this.batchImportExportApi.findActivePackagesByVariableValue(flavors, variableValue, variableType, includeBranches); + let packagesToExport = await this.t2tcPackageApi.findActivePackagesByVariableValue(flavors, variableValue, variableType, includeBranches); packagesToExport = await this.studioService.getExportPackagesWithStudioData(packagesToExport, false); @@ -179,7 +160,7 @@ export class BatchImportExportService { } public async listActivePackagesByVariableValue(flavors: string[], variableValue: string, variableType: string, includeBranches: boolean) : Promise { - const packagesByVariableValue = await this.batchImportExportApi.findActivePackagesByVariableValue(flavors, variableValue, variableType, includeBranches); + const packagesByVariableValue = await this.t2tcPackageApi.findActivePackagesByVariableValue(flavors, variableValue, variableType, includeBranches); packagesByVariableValue.forEach(pkg => { logger.info(`${pkg.name} - Key: "${pkg.key}"`) }); @@ -211,13 +192,7 @@ export class BatchImportExportService { }) }); - return this.batchImportExportApi.findVariablesWithValuesByPackageKeysAndVersion(variableExportRequest) - } - - private exportListOfPackagesMetadata(packagesMetadata: PackageMetadataExportTransport[]): void { - const filename = uuidv4() + ".json"; - fileService.writeToFileWithGivenName(JSON.stringify(packagesMetadata), filename); - logger.info(FileService.fileDownloadedMessage + filename); + return this.variableApi.findVariablesWithValuesByPackageKeysAndVersion(variableExportRequest) } private buildBodyForImport(configs: AdmZip, sourcePath: string, variablesManifests: VariableManifestTransport[]): FormData { diff --git a/tests/commands/configuration-management/config-metadata-export.spec.ts b/tests/commands/configuration-management/config-metadata-export.spec.ts index 543d84cd..2b6e5f6f 100644 --- a/tests/commands/configuration-management/config-metadata-export.spec.ts +++ b/tests/commands/configuration-management/config-metadata-export.spec.ts @@ -3,7 +3,7 @@ import { mockAxiosGet } from "../../utls/http-requests-mock"; import { PackageMetadataExportTransport } from "../../../src/commands/configuration-management/interfaces/package-export.interfaces"; -import { ConfigCommandService } from "../../../src/commands/configuration-management/config-command.service"; +import { MetadataService } from "../../../src/commands/configuration-management/metadata.service"; import { testContext } from "../../utls/test-context"; import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; import { FileService } from "../../../src/core/utils/file-service"; @@ -28,7 +28,7 @@ describe("Config metadata export", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/metadata/export?packageKeys=package-key-1&packageKeys=package-key-2", response); - await new ConfigCommandService(testContext).batchExportPackagesMetadata(packageKeys, false); + await new MetadataService(testContext).exportPackagesMetadata(packageKeys, false); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain( @@ -49,7 +49,7 @@ describe("Config metadata export", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/metadata/export?packageKeys=package-key-1&packageKeys=package-key-2", response); - await new ConfigCommandService(testContext).batchExportPackagesMetadata(packageKeys, true); + await new MetadataService(testContext).exportPackagesMetadata(packageKeys, true); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; diff --git a/tests/commands/configuration-management/module.spec.ts b/tests/commands/configuration-management/module.spec.ts index 03672b75..e24159cd 100644 --- a/tests/commands/configuration-management/module.spec.ts +++ b/tests/commands/configuration-management/module.spec.ts @@ -1,6 +1,9 @@ import Module = require("../../../src/commands/configuration-management/module"); import { Command, OptionValues } from "commander"; import { ConfigCommandService } from "../../../src/commands/configuration-management/config-command.service"; +import { StagingPackageService } from "../../../src/commands/configuration-management/staging-package.service"; +import { MetadataService } from "../../../src/commands/configuration-management/metadata.service"; +import { T2tcCommandService } from "../../../src/commands/t2tc/t2tc-command.service"; import { NodeDependencyService } from "../../../src/commands/configuration-management/node-dependency.service"; import { PackageVersionCommandService } from "../../../src/commands/configuration-management/package-version-command.service"; import { NodeDiffService } from "../../../src/commands/configuration-management/node-diff.service"; @@ -9,6 +12,9 @@ import { testContext } from "../../utls/test-context"; import { createMockConfigurator } from "../../utls/configurator-mock"; jest.mock("../../../src/commands/configuration-management/config-command.service"); +jest.mock("../../../src/commands/configuration-management/staging-package.service"); +jest.mock("../../../src/commands/configuration-management/metadata.service"); +jest.mock("../../../src/commands/t2tc/t2tc-command.service"); jest.mock("../../../src/commands/configuration-management/node-dependency.service"); jest.mock("../../../src/commands/configuration-management/node-diff.service"); jest.mock("../../../src/commands/configuration-management/package-version-command.service"); @@ -25,6 +31,9 @@ describe("Configuration Management Module - Action Validations", () => { let module: Module; let mockCommand: Command; let mockConfigCommandService: jest.Mocked; + let mockStagingPackageService: jest.Mocked; + let mockMetadataService: jest.Mocked; + let mockT2tcCommandService: jest.Mocked; let mockNodeDependencyService: jest.Mocked; let mockNodeDiffService: jest.Mocked; let mockSinglePackageImportService: jest.Mocked; @@ -35,9 +44,19 @@ describe("Configuration Management Module - Action Validations", () => { mockCommand = {} as Command; mockConfigCommandService = { - listPackages: jest.fn().mockResolvedValue(undefined), - listStagingPackages: jest.fn().mockResolvedValue(undefined), listVariables: jest.fn().mockResolvedValue(undefined), + } as any; + + mockStagingPackageService = { + listStagingPackages: jest.fn().mockResolvedValue(undefined), + } as any; + + mockMetadataService = { + exportPackagesMetadata: jest.fn().mockResolvedValue(undefined), + } as any; + + mockT2tcCommandService = { + listPackages: jest.fn().mockResolvedValue(undefined), batchExportPackages: jest.fn().mockResolvedValue(undefined), batchImportPackages: jest.fn().mockResolvedValue(undefined), diffPackages: jest.fn().mockResolvedValue(undefined), @@ -57,6 +76,9 @@ describe("Configuration Management Module - Action Validations", () => { } as any; (ConfigCommandService as jest.MockedClass).mockImplementation(() => mockConfigCommandService); + (StagingPackageService as jest.MockedClass).mockImplementation(() => mockStagingPackageService); + (MetadataService as jest.MockedClass).mockImplementation(() => mockMetadataService); + (T2tcCommandService as jest.MockedClass).mockImplementation(() => mockT2tcCommandService); (NodeDependencyService as jest.MockedClass).mockImplementation(() => mockNodeDependencyService); (NodeDiffService as jest.MockedClass).mockImplementation(() => mockNodeDiffService); (SinglePackageImportService as jest.MockedClass).mockImplementation(() => mockSinglePackageImportService); @@ -74,7 +96,7 @@ describe("Configuration Management Module - Action Validations", () => { (module as any).listPackages(testContext, mockCommand, options) ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); - expect(mockConfigCommandService.listPackages).not.toHaveBeenCalled(); + expect(mockT2tcCommandService.listPackages).not.toHaveBeenCalled(); }); it("should pass validation when only packageKeys is provided", async () => { @@ -85,7 +107,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).listPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.listPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.listPackages).toHaveBeenCalledWith( true, undefined, undefined, @@ -106,7 +128,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).listPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.listPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.listPackages).toHaveBeenCalledWith( true, undefined, undefined, @@ -128,7 +150,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).listPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.listPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.listPackages).toHaveBeenCalledWith( true, undefined, undefined, @@ -153,7 +175,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).listPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.listPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.listPackages).toHaveBeenCalledWith( true, undefined, undefined, @@ -231,15 +253,15 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).listStagingPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.listStagingPackages).toHaveBeenCalledWith(true, ["APP"]); + expect(mockStagingPackageService.listStagingPackages).toHaveBeenCalledWith(["APP"], false, true); }); - it("should default json and flavors to undefined when not provided", async () => { + it("should default flavors to an empty list and json to undefined when not provided", async () => { const options: OptionValues = {}; await (module as any).listStagingPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.listStagingPackages).toHaveBeenCalledWith(undefined, undefined); + expect(mockStagingPackageService.listStagingPackages).toHaveBeenCalledWith([], false, undefined); }); it("should not pass legacy listPackages options", async () => { @@ -249,8 +271,8 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).listStagingPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.listStagingPackages).toHaveBeenCalledWith(undefined, ["APP", "ANALYSIS"]); - expect(mockConfigCommandService.listPackages).not.toHaveBeenCalled(); + expect(mockStagingPackageService.listStagingPackages).toHaveBeenCalledWith(["APP", "ANALYSIS"], false, undefined); + expect(mockT2tcCommandService.listPackages).not.toHaveBeenCalled(); }); }); @@ -266,7 +288,7 @@ describe("Configuration Management Module - Action Validations", () => { (module as any).batchExportPackages(testContext, mockCommand, options) ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); - expect(mockConfigCommandService.batchExportPackages).not.toHaveBeenCalled(); + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); }); it("should throw error when neither packageKeys nor keysByVersion are provided", async () => { @@ -276,7 +298,7 @@ describe("Configuration Management Module - Action Validations", () => { (module as any).batchExportPackages(testContext, mockCommand, options) ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); - expect(mockConfigCommandService.batchExportPackages).not.toHaveBeenCalled(); + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); }); it("should pass validation when only packageKeys is provided", async () => { @@ -286,7 +308,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( ["package1", "package2"], undefined, false, @@ -302,7 +324,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( undefined, ["package3:v1", "package4:v2"], false, @@ -323,7 +345,7 @@ describe("Configuration Management Module - Action Validations", () => { (module as any).batchExportPackages(testContext, mockCommand, options) ).rejects.toThrow("Please specify a branch using --gitBranch when using a Git profile."); - expect(mockConfigCommandService.batchExportPackages).not.toHaveBeenCalled(); + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); }); it("should pass validation when gitProfile provided with gitBranch option", async () => { @@ -335,7 +357,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( ["package1"], undefined, false, @@ -352,7 +374,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( ["package1"], undefined, false, @@ -368,7 +390,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalled(); + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalled(); }); }); @@ -380,7 +402,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( ["package1"], undefined, false, @@ -397,7 +419,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( ["package1"], undefined, true, @@ -414,7 +436,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchExportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchExportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( ["package1"], undefined, false, @@ -436,7 +458,7 @@ describe("Configuration Management Module - Action Validations", () => { (module as any).batchExportPackages(testContext, mockCommand, options) ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); - expect(mockConfigCommandService.batchExportPackages).not.toHaveBeenCalled(); + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); }); }); }); @@ -453,7 +475,7 @@ describe("Configuration Management Module - Action Validations", () => { (module as any).batchImportPackages(testContext, mockCommand, options) ).rejects.toThrow("Please specify a branch using --gitBranch when using a Git profile."); - expect(mockConfigCommandService.batchImportPackages).not.toHaveBeenCalled(); + expect(mockT2tcCommandService.batchImportPackages).not.toHaveBeenCalled(); }); it("should pass validation when gitProfile is provided with gitBranch option", async () => { @@ -465,7 +487,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchImportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchImportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( "export.zip", undefined, undefined, @@ -482,7 +504,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchImportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchImportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( undefined, "./exported", undefined, @@ -498,7 +520,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchImportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchImportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( "export.zip", undefined, undefined, @@ -516,7 +538,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchImportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchImportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( "my-export.zip", undefined, undefined, @@ -532,7 +554,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchImportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchImportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( undefined, "./my-exports", undefined, @@ -549,7 +571,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchImportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchImportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( "export.zip", undefined, true, @@ -567,7 +589,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).batchImportPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.batchImportPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( undefined, "./exports", true, @@ -803,10 +825,10 @@ describe("Configuration Management Module - Action Validations", () => { expect(() => new Module().register(testContext, mockConfigurator)).not.toThrow(); - // Top-level groups attached to the root configurator + // Top-level groups attached to the root configurator. The 't2tc' group + // lives in its own module (tests/commands/t2tc/module.spec.ts). expect(mockConfigurator.command).toHaveBeenCalledWith("config"); expect(mockConfigurator.command).toHaveBeenCalledWith("list"); - expect(mockConfigurator.command).toHaveBeenCalledWith("t2tc"); expect(mockConfigurator.command).toHaveBeenCalledWith("package"); }); @@ -817,7 +839,8 @@ describe("Configuration Management Module - Action Validations", () => { // Each leaf command terminates the fluent chain with .action(handler). // Keep this count in sync when adding or removing commands in module.ts. - const expectedLeafCommands = 24; + // The 4 't2tc package' leaf commands moved to their own module. + const expectedLeafCommands = 20; expect(mockConfigurator.action).toHaveBeenCalledTimes(expectedLeafCommands); for (const call of mockConfigurator.action.mock.calls) { expect(typeof call[0]).toBe("function"); @@ -921,7 +944,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).diffPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.diffPackages).toHaveBeenCalledWith( "package.zip", undefined, undefined, undefined ); }); @@ -934,7 +957,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).diffPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.diffPackages).toHaveBeenCalledWith( "package.zip", undefined, undefined, true ); }); @@ -947,7 +970,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).diffPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.diffPackages).toHaveBeenCalledWith( "package.zip", true, undefined, undefined ); }); @@ -960,7 +983,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).diffPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.diffPackages).toHaveBeenCalledWith( "package.zip", undefined, "1.0.0", undefined ); }); @@ -974,7 +997,7 @@ describe("Configuration Management Module - Action Validations", () => { await (module as any).diffPackages(testContext, mockCommand, options); - expect(mockConfigCommandService.diffPackages).toHaveBeenCalledWith( + expect(mockT2tcCommandService.diffPackages).toHaveBeenCalledWith( "package.zip", true, "STAGING", undefined ); }); diff --git a/tests/commands/t2tc/module.spec.ts b/tests/commands/t2tc/module.spec.ts new file mode 100644 index 00000000..270adfdb --- /dev/null +++ b/tests/commands/t2tc/module.spec.ts @@ -0,0 +1,213 @@ +import Module = require("../../../src/commands/t2tc/module"); +import { Command, OptionValues } from "commander"; +import { T2tcCommandService } from "../../../src/commands/t2tc/t2tc-command.service"; +import { testContext } from "../../utls/test-context"; +import { createMockConfigurator } from "../../utls/configurator-mock"; + +jest.mock("../../../src/commands/t2tc/t2tc-command.service"); + +describe("T2TC Module", () => { + let module: Module; + let mockCommand: Command; + let mockT2tcCommandService: jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + module = new Module(); + mockCommand = {} as Command; + + mockT2tcCommandService = { + listPackages: jest.fn().mockResolvedValue(undefined), + batchExportPackages: jest.fn().mockResolvedValue(undefined), + batchImportPackages: jest.fn().mockResolvedValue(undefined), + diffPackages: jest.fn().mockResolvedValue(undefined), + } as any; + + (T2tcCommandService as jest.MockedClass).mockImplementation(() => mockT2tcCommandService); + }); + + describe("register", () => { + it("registers the t2tc and package command groups without throwing", () => { + const mockConfigurator = createMockConfigurator(); + + expect(() => new Module().register(testContext, mockConfigurator)).not.toThrow(); + + expect(mockConfigurator.command).toHaveBeenCalledWith("t2tc"); + expect(mockConfigurator.command).toHaveBeenCalledWith("package"); + expect(mockConfigurator.command).toHaveBeenCalledWith("list"); + expect(mockConfigurator.command).toHaveBeenCalledWith("export"); + expect(mockConfigurator.command).toHaveBeenCalledWith("import"); + expect(mockConfigurator.command).toHaveBeenCalledWith("diff"); + }); + + it("wires an action handler for every leaf subcommand", () => { + const mockConfigurator = createMockConfigurator(); + + new Module().register(testContext, mockConfigurator); + + // t2tc package list / export / import / diff + const expectedLeafCommands = 4; + expect(mockConfigurator.action).toHaveBeenCalledTimes(expectedLeafCommands); + for (const call of mockConfigurator.action.mock.calls) { + expect(typeof call[0]).toBe("function"); + } + }); + }); + + describe("listPackages", () => { + it("throws when both --packageKeys and --keysByVersion are provided", async () => { + const options: OptionValues = { + packageKeys: ["package1"], + keysByVersion: ["package2.1.0.0"], + }; + + await expect( + (module as any).listPackages(testContext, mockCommand, options) + ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); + + expect(mockT2tcCommandService.listPackages).not.toHaveBeenCalled(); + }); + + it("throws when --staging is combined with an incompatible filter", async () => { + const options: OptionValues = { + staging: true, + packageKeys: ["package1"], + }; + + await expect( + (module as any).listPackages(testContext, mockCommand, options) + ).rejects.toThrow("Staging parameter is not compatible with"); + + expect(mockT2tcCommandService.listPackages).not.toHaveBeenCalled(); + }); + + it("delegates to T2tcCommandService with the provided options", async () => { + const options: OptionValues = { + json: true, + flavors: ["STUDIO"], + packageKeys: ["package1"], + }; + + await (module as any).listPackages(testContext, mockCommand, options); + + expect(mockT2tcCommandService.listPackages).toHaveBeenCalledWith( + true, + ["STUDIO"], + undefined, + ["package1"], + undefined, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe("batchExportPackages", () => { + it("throws when both --packageKeys and --keysByVersion are provided", async () => { + const options: OptionValues = { + packageKeys: ["package1"], + keysByVersion: ["package2.1.0.0"], + }; + + await expect( + (module as any).batchExportPackages(testContext, mockCommand, options) + ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); + + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); + }); + + it("throws when neither --packageKeys nor --keysByVersion is provided", async () => { + const options: OptionValues = {}; + + await expect( + (module as any).batchExportPackages(testContext, mockCommand, options) + ).rejects.toThrow("Please provide either --packageKeys or --keysByVersion, but not both."); + + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); + }); + + it("throws when a Git profile is provided without a Git branch", async () => { + const options: OptionValues = { + packageKeys: ["package1"], + gitProfile: "my-git-profile", + }; + + await expect( + (module as any).batchExportPackages(testContext, mockCommand, options) + ).rejects.toThrow("Please specify a branch using --gitBranch when using a Git profile."); + + expect(mockT2tcCommandService.batchExportPackages).not.toHaveBeenCalled(); + }); + + it("delegates to T2tcCommandService and defaults withDependencies to false", async () => { + const options: OptionValues = { + packageKeys: ["package1"], + unzip: true, + }; + + await (module as any).batchExportPackages(testContext, mockCommand, options); + + expect(mockT2tcCommandService.batchExportPackages).toHaveBeenCalledWith( + ["package1"], + undefined, + false, + undefined, + true + ); + }); + }); + + describe("batchImportPackages", () => { + it("throws when a Git profile is provided without a Git branch", async () => { + const options: OptionValues = { + gitProfile: "my-git-profile", + }; + + await expect( + (module as any).batchImportPackages(testContext, mockCommand, options) + ).rejects.toThrow("Please specify a branch using --gitBranch when using a Git profile."); + + expect(mockT2tcCommandService.batchImportPackages).not.toHaveBeenCalled(); + }); + + it("delegates to T2tcCommandService with the provided options", async () => { + const options: OptionValues = { + file: "export.zip", + overwrite: true, + validate: true, + }; + + await (module as any).batchImportPackages(testContext, mockCommand, options); + + expect(mockT2tcCommandService.batchImportPackages).toHaveBeenCalledWith( + "export.zip", + undefined, + true, + undefined, + true + ); + }); + }); + + describe("diffPackages", () => { + it("delegates to T2tcCommandService with the provided options", async () => { + const options: OptionValues = { + file: "export.zip", + hasChanges: true, + baseVersion: "STAGING", + json: true, + }; + + await (module as any).diffPackages(testContext, mockCommand, options); + + expect(mockT2tcCommandService.diffPackages).toHaveBeenCalledWith( + "export.zip", + true, + "STAGING", + true + ); + }); + }); +}); diff --git a/tests/commands/configuration-management/config-diff.spec.ts b/tests/commands/t2tc/t2tc-package-diff.spec.ts similarity index 92% rename from tests/commands/configuration-management/config-diff.spec.ts rename to tests/commands/t2tc/t2tc-package-diff.spec.ts index 93f9f604..f7d5a4ef 100644 --- a/tests/commands/configuration-management/config-diff.spec.ts +++ b/tests/commands/t2tc/t2tc-package-diff.spec.ts @@ -9,7 +9,7 @@ import { PackageDiffTransport, } from "../../../src/commands/configuration-management/interfaces/diff-package.interfaces"; import { mockAxiosPost } from "../../utls/http-requests-mock"; -import { ConfigCommandService } from "../../../src/commands/configuration-management/config-command.service"; +import { T2tcCommandService } from "../../../src/commands/t2tc/t2tc-command.service"; import { testContext } from "../../utls/test-context"; import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; import { FileService } from "../../../src/core/utils/file-service"; @@ -81,7 +81,7 @@ describe("Config diff", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration/has-changes", diffResponse); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", true, null, false); + await new T2tcCommandService(testContext).diffPackages("./packages.zip", true, null, false); expect(loggingTestTransport.logMessages.length).toBe(1); expect(loggingTestTransport.logMessages[0].message).toContain( @@ -92,7 +92,7 @@ describe("Config diff", () => { it("Should show diff on terminal with hasChanges set to false and jsonResponse false", async () => { const diffResponse = mockZipDiff("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration"); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, null, false); + await new T2tcCommandService(testContext).diffPackages("./packages.zip", false, null, false); expect(loggingTestTransport.logMessages.length).toBe(1); expect(loggingTestTransport.logMessages[0].message).toContain( @@ -103,7 +103,7 @@ describe("Config diff", () => { it("Should compare with specified version", async () => { const diffResponse = mockZipDiff("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration?baseVersion=1.0.0"); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, "1.0.0", false); + await new T2tcCommandService(testContext).diffPackages("./packages.zip", false, "1.0.0", false); expect(loggingTestTransport.logMessages.length).toBe(1); expect(loggingTestTransport.logMessages[0].message).toContain( @@ -114,7 +114,7 @@ describe("Config diff", () => { it("Should generate a json file with diff info when hasChanges is set to false and jsonResponse is set to true", async () => { const diffResponse = mockZipDiff("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration"); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", false, null, true); + await new T2tcCommandService(testContext).diffPackages("./packages.zip", false, null, true); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -145,7 +145,7 @@ describe("Config diff", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/diff/configuration/has-changes", diffResponse); - await new ConfigCommandService(testContext).diffPackages("./packages.zip", true, null, true); + await new T2tcCommandService(testContext).diffPackages("./packages.zip", true, null, true); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; diff --git a/tests/commands/configuration-management/config-export.spec.ts b/tests/commands/t2tc/t2tc-package-export.spec.ts similarity index 96% rename from tests/commands/configuration-management/config-export.spec.ts rename to tests/commands/t2tc/t2tc-package-export.spec.ts index 58f64b75..1c58008c 100644 --- a/tests/commands/configuration-management/config-export.spec.ts +++ b/tests/commands/t2tc/t2tc-package-export.spec.ts @@ -3,7 +3,7 @@ import AdmZip = require("adm-zip"); import { mockAxiosGet, mockAxiosPost, mockedPostRequestBodyByUrl } from "../../utls/http-requests-mock"; import { BatchExportImportConstants -} from "../../../src/commands/configuration-management/interfaces/batch-export-import.constants"; +} from "../../../src/commands/t2tc/batch-export-import.constants"; import { DependencyTransport, NodeConfiguration, NodeExportTransport, PackageManifestTransport, StudioPackageManifest, VariableManifestTransport, @@ -15,7 +15,7 @@ import { import { loggingTestTransport, mockWriteSync } from "../../jest.setup"; import { FileService } from "../../../src/core/utils/file-service"; import { parse, stringify } from "../../../src/core/utils/json"; -import { ConfigCommandService } from "../../../src/commands/configuration-management/config-command.service"; +import { T2tcCommandService } from "../../../src/commands/t2tc/t2tc-command.service"; import { testContext } from "../../utls/test-context"; import { ConfigUtils } from "../../utls/config-utils"; import { PacmanApiUtils } from "../../utls/pacman-api.utils"; @@ -60,7 +60,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstStudioPackage.key}/variables/runtime-values?appMode=VIEWER`, [firstPackageRuntimeVariable]); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondStudioPackage.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(["key-1", "key-2", "key-3"], undefined, true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(["key-1", "key-2", "key-3"], undefined, true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -122,7 +122,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondStudioPackage.key}/variables/runtime-values?appMode=VIEWER`, []); const branchName = "my-branch"; - await new ConfigCommandService(testContext).batchExportPackages(["key-1", "key-2", "key-3"], undefined, true, branchName, null); + await new T2tcCommandService(testContext).batchExportPackages(["key-1", "key-2", "key-3"], undefined, true, branchName, null); expect(mockGitServicePushToBranch).toHaveBeenCalledTimes(1); expect(mockGitServicePushToBranch).toHaveBeenCalledWith( @@ -160,7 +160,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstStudioPackage.key}/variables/runtime-values?appMode=VIEWER`, [firstPackageRuntimeVariable]); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondStudioPackage.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.1", "key-2.1.0.4", "key-3.1.2.0"], true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.1", "key-2.1.0.4", "key-3.1.2.0"], true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -286,7 +286,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -450,7 +450,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -611,7 +611,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.2", "key-2.1.0.3"], true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.2", "key-2.1.0.3"], true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -703,7 +703,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -745,7 +745,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.2", "key-2.1.0.3"], true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.2", "key-2.1.0.3"], true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -861,7 +861,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(["key-1", "key-2"], undefined, true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -1002,7 +1002,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${secondPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.4", "key-2.1.0.5"], true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.4", "key-2.1.0.5"], true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -1110,7 +1110,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/${firstPackageNode.key}/${firstPackageNode.key}`, {...firstPackageNode, spaceId: "space-1"}); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(["key_with_underscores_1"], undefined, true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(["key_with_underscores_1"], undefined, true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -1209,7 +1209,7 @@ describe("Config export", () => { mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/${firstPackageNode.key}/${firstPackageNode.key}`, {...firstPackageNode, spaceId: "space-1"}); mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/nodes/by-package-key/${firstPackageNode.key}/variables/runtime-values?appMode=VIEWER`, []); - await new ConfigCommandService(testContext).batchExportPackages(undefined, ["key_with_underscores_1.1.0.6"], true, null, null); + await new T2tcCommandService(testContext).batchExportPackages(undefined, ["key_with_underscores_1.1.0.6"], true, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -1252,7 +1252,7 @@ describe("Config export", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/batch?packageKeys=key-1&packageKeys=key-2&packageKeys=key-3&withDependencies=false", exportedPackagesZip.toBuffer()); mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/batch/variables-with-assignments", []); - await new ConfigCommandService(testContext).batchExportPackages(["key-1", "key-2", "key-3"], undefined, false, null, null); + await new T2tcCommandService(testContext).batchExportPackages(["key-1", "key-2", "key-3"], undefined, false, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); @@ -1268,7 +1268,7 @@ describe("Config export", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/versions/export/batch?packageKeysWithVersion=key-1.1.0.3&packageKeysWithVersion=key-2.1.0.4&packageKeysWithVersion=key-3.1.0.5&withDependencies=false", exportedPackagesZip.toBuffer()); mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/batch/variables-with-assignments", []); - await new ConfigCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.3", "key-2.1.0.4", "key-3.1.0.5"], false, null, null); + await new T2tcCommandService(testContext).batchExportPackages(undefined, ["key-1.1.0.3", "key-2.1.0.4", "key-3.1.0.5"], false, null, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; expect(fs.openSync).toHaveBeenCalledWith(expectedFileName, expect.anything(), expect.anything()); diff --git a/tests/commands/configuration-management/config-import.spec.ts b/tests/commands/t2tc/t2tc-package-import.spec.ts similarity index 93% rename from tests/commands/configuration-management/config-import.spec.ts rename to tests/commands/t2tc/t2tc-package-import.spec.ts index 8bb9e80e..f5430f63 100644 --- a/tests/commands/configuration-management/config-import.spec.ts +++ b/tests/commands/t2tc/t2tc-package-import.spec.ts @@ -16,7 +16,7 @@ import { mockedAxiosInstance, mockedPostRequestBodyByUrl, } from "../../utls/http-requests-mock"; -import { ConfigCommandService } from "../../../src/commands/configuration-management/config-command.service"; +import { T2tcCommandService } from "../../../src/commands/t2tc/t2tc-command.service"; import { testContext } from "../../utls/test-context"; import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; import { SpaceTransport } from "../../../src/commands/studio/interfaces/space.interface"; @@ -26,7 +26,7 @@ import { } from "../../../src/commands/studio/interfaces/package-manager.interfaces"; import { BatchExportImportConstants -} from "../../../src/commands/configuration-management/interfaces/batch-export-import.constants"; +} from "../../../src/commands/t2tc/batch-export-import.constants"; import { ConfigUtils } from "../../utls/config-utils"; import { stringify } from "../../../src/core/utils/json"; import { GitService } from "../../../src/core/git-profile/git/git.service"; @@ -62,7 +62,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, overwrite, null); + await new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, overwrite, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); @@ -95,7 +95,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages(null, null, overwrite, branchName); + await new T2tcCommandService(testContext).batchImportPackages(null, null, overwrite, branchName); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); @@ -134,7 +134,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages(null, null, overwrite, branchName); + await new T2tcCommandService(testContext).batchImportPackages(null, null, overwrite, branchName); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); @@ -178,7 +178,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); + await new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); @@ -207,7 +207,7 @@ describe("Config import", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/spaces", [space]); await expect( - new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null) + new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null) ).rejects.toThrow("Provided space ID does not exist."); }) @@ -250,7 +250,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); + await new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); expect(mockedAxiosInstance.put).toHaveBeenCalledWith("https://myTeam.celonis.cloud/package-manager/api/packages/node-id/move/spaceId", expect.anything(), expect.anything()); @@ -287,7 +287,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); + await new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); @@ -334,7 +334,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); + await new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); @@ -358,7 +358,7 @@ describe("Config import", () => { }); await expect( - new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, false, null) + new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, false, null) ).rejects.toThrow('Failed to handle zip file "./export_file.zip": uncompressed size 5.00 GB exceeds the 4 GB limit.'); }) @@ -413,7 +413,7 @@ describe("Config import", () => { mockAxiosPost(assignVariablesUrl, {}); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages", [node]); - await new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); + await new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, true, null); const expectedFileName = loggingTestTransport.logMessages[0].message.split(LOG_MESSAGE)[1]; expect(mockWriteFileSync).toHaveBeenCalledWith(path.resolve(process.cwd(), expectedFileName), JSON.stringify(importResponse), {encoding: "utf-8", mode: 0o600}); @@ -440,7 +440,7 @@ describe("Config import", () => { mockAxiosPost("https://myTeam.celonis.cloud/package-manager/api/core/packages/import/batch", importResponse); - await new ConfigCommandService(testContext).batchImportPackages("./export_file.zip", null, false, null, true); + await new T2tcCommandService(testContext).batchImportPackages("./export_file.zip", null, false, null, true); expect(mockedAxiosInstance.post).toHaveBeenCalledWith( expect.stringContaining("/package-manager/api/core/packages/import/batch"), diff --git a/tests/commands/configuration-management/config-list.spec.ts b/tests/commands/t2tc/t2tc-package-list.spec.ts similarity index 90% rename from tests/commands/configuration-management/config-list.spec.ts rename to tests/commands/t2tc/t2tc-package-list.spec.ts index d09ec265..3af2ec6c 100644 --- a/tests/commands/configuration-management/config-list.spec.ts +++ b/tests/commands/t2tc/t2tc-package-list.spec.ts @@ -1,7 +1,7 @@ import * as path from "path"; import { PacmanApiUtils } from "../../utls/pacman-api.utils"; import { mockAxiosGet } from "../../utls/http-requests-mock"; -import { ConfigCommandService } from "../../../src/commands/configuration-management/config-command.service"; +import { T2tcCommandService } from "../../../src/commands/t2tc/t2tc-command.service"; import { testContext } from "../../utls/test-context"; import { loggingTestTransport, mockWriteFileSync } from "../../jest.setup"; import { @@ -33,7 +33,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list?" + urlParams.toString(), [firstPackage, secondPackage]); - await new ConfigCommandService(testContext).listPackages(false, flavorsArray, false, [], undefined, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(false, flavorsArray, false, [], undefined, null, null, false, false); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); @@ -50,7 +50,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list?withDependencies=false&includeBranches=false", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", [studioPackage]); - await new ConfigCommandService(testContext).listPackages(true, [], false, [], undefined, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(true, [], false, [], undefined, null, null, false, false); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -96,7 +96,7 @@ describe("Config list", () => { }; mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/compute-pools/data-models/details", [dataModelDetailResponse]); - await new ConfigCommandService(testContext).listPackages(true, [], true, [], undefined, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(true, [], true, [], undefined, null, null, false, false); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -121,7 +121,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list-by-keys?packageKeys=key-1&packageKeys=key-2&withDependencies=false", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", [studioPackage]); - await new ConfigCommandService(testContext).listPackages(true, [], false, [firstPackage.key, secondPackage.key], undefined, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(true, [], false, [firstPackage.key, secondPackage.key], undefined, null, null, false, false); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -167,7 +167,7 @@ describe("Config list", () => { }; mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/compute-pools/data-models/details", [dataModelDetailResponse]); - await new ConfigCommandService(testContext).listPackages(true, [], true, [firstPackage.key, secondPackage.key], undefined, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(true, [], true, [firstPackage.key, secondPackage.key], undefined, null, null, false, false); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -191,7 +191,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list-by-variable-value?variableValue=1&includeBranches=false", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", []); - await new ConfigCommandService(testContext).listPackages(false, [], false, [], undefined, "1", null, false, false); + await new T2tcCommandService(testContext).listPackages(false, [], false, [], undefined, "1", null, false, false); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); @@ -205,7 +205,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/core/packages/export/list-by-variable-value?variableValue=1&includeBranches=false", [{...firstPackage}, {...secondPackage}]); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", []); - await new ConfigCommandService(testContext).listPackages(true, [], false, [], undefined, "1", null, false, false); + await new T2tcCommandService(testContext).listPackages(true, [], false, [], undefined, "1", null, false, false); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -225,7 +225,7 @@ describe("Config list", () => { [firstPackage, secondPackage] ); - await new ConfigCommandService(testContext).listPackages(false, [], false, undefined, keysByVersion, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(false, [], false, undefined, keysByVersion, null, null, false, false); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); @@ -242,7 +242,7 @@ describe("Config list", () => { [firstPackage, secondPackage] ); - await new ConfigCommandService(testContext).listPackages(false, [], true, undefined, keysByVersion, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(false, [], true, undefined, keysByVersion, null, null, false, false); expect(loggingTestTransport.logMessages.length).toBe(2); }) @@ -260,7 +260,7 @@ describe("Config list", () => { ); mockAxiosGet("https://myTeam.celonis.cloud/package-manager/api/packages/with-variable-assignments?type=DATA_MODEL", [studioPackage]); - await new ConfigCommandService(testContext).listPackages(true, [], false, [], keysByVersion, null, null, false, false); + await new T2tcCommandService(testContext).listPackages(true, [], false, [], keysByVersion, null, null, false, false); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; @@ -286,7 +286,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/staging/packages/export/list?" + urlParams.toString(), [firstPackage, secondPackage]); - await new ConfigCommandService(testContext).listPackages(false, ["STUDIO"], false, [], undefined, null, null, false, true); + await new T2tcCommandService(testContext).listPackages(false, ["STUDIO"], false, [], undefined, null, null, false, true); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); @@ -302,7 +302,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/staging/packages/export/list?" + urlParams.toString(), [firstPackage, secondPackage]); - await new ConfigCommandService(testContext).listPackages(false, null, false, [], undefined, null, null, false, true); + await new T2tcCommandService(testContext).listPackages(false, null, false, [], undefined, null, null, false, true); expect(loggingTestTransport.logMessages.length).toBe(2); expect(loggingTestTransport.logMessages[0].message).toContain(`${firstPackage.name} - Key: "${firstPackage.key}"`); @@ -319,7 +319,7 @@ describe("Config list", () => { mockAxiosGet("https://myTeam.celonis.cloud/pacman/api/core/staging/packages/export/list?" + urlParams.toString(), [firstPackage, secondPackage]); - await new ConfigCommandService(testContext).listPackages(true, ["STUDIO"], false, [], undefined, null, null, false, true); + await new T2tcCommandService(testContext).listPackages(true, ["STUDIO"], false, [], undefined, null, null, false, true); const expectedFileName = loggingTestTransport.logMessages[0].message.split(FileService.fileDownloadedMessage)[1]; From 2e38590799e0cae726243919dd402519b28e9109 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Mon, 8 Jun 2026 17:23:05 +0200 Subject: [PATCH 6/7] SP-957: add CODEOWNERS entry for t2tc command group Includes-AI-Code: true Co-authored-by: Cursor --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1d39dc37..4bb97347 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -5,6 +5,8 @@ /src/core @celonis/studio-platform /src/commands/configuration-management/ @celonis/astro /tests/commands/configuration-management/ @celonis/astro +/src/commands/t2tc/ @celonis/studio-platform +/tests/commands/t2tc/ @celonis/studio-platform /src/commands/profile/ @celonis/studio-platform /src/commands/asset-registry/ @celonis/astro /tests/commands/asset-registry/ @celonis/astro From 4e040e246983137ea27fd9fede1f94c6a3921b02 Mon Sep 17 00:00:00 2001 From: zgjimhaziri Date: Mon, 8 Jun 2026 17:32:04 +0200 Subject: [PATCH 7/7] SP-874: address PR feedback on wording and package description - Rename "batch archive" to "batch artifact" in config-commands docs - Describe overwritten, undeclared variable assignments as "ignored" - Clarify the 'config package' group description (single package) Includes-AI-Code: true Co-authored-by: Cursor --- docs/user-guide/config-commands.md | 18 +++++++++--------- .../configuration-management/module.ts | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/user-guide/config-commands.md b/docs/user-guide/config-commands.md index d823e27e..52d4efb6 100644 --- a/docs/user-guide/config-commands.md +++ b/docs/user-guide/config-commands.md @@ -15,7 +15,7 @@ The batch commands are **batch-specific**: they use their own archive format tha ## Two command families -The batch commands are a **self-contained, batch-specific set**. `config export` produces a multi-package **batch archive** — a top-level `manifest.json`, `variables.json`, `studio.json`, and one nested `_.zip` per package — that is produced and consumed **only** by other batch commands. `config package import`, by contrast, works with a plain **package zip** (a `package.json`, an optional `variables.json`, and a `nodes/` folder). The two formats are **not interchangeable**: +The batch commands are a **self-contained, batch-specific set**. `config export` produces a multi-package **batch artifact** — a top-level `manifest.json`, `variables.json`, `studio.json`, and one nested `_.zip` per package — that is produced and consumed **only** by other batch commands. `config package import`, by contrast, works with a plain **package zip** (a `package.json`, an optional `variables.json`, and a `nodes/` folder). The two formats are **not interchangeable**: - An archive from `config export` can be imported with `config import` or inspected with `config diff` — but **not** with `config package import`. - A package zip used by `config package import` **cannot** be imported with `config import` or diffed with `config diff`. @@ -105,7 +105,7 @@ content-cli config list -p --packageKeys key1 ... keyN ## Batch Export Packages -> **Batch command.** Part of the [batch family](#two-command-families). It produces a multi-package **batch archive** that can only be re-imported with [`config import`](#batch-import-packages) or inspected with [`config diff`](#diff-local-zip-with-deployed-versionspecific-versionstaging) — **not** with `config package import`. To work with one package, use [`config package import`](#package-commands-config-package). +> **Batch command.** Part of the [batch family](#two-command-families). It produces a multi-package **batch artifact** that can only be re-imported with [`config import`](#batch-import-packages) or inspected with [`config diff`](#diff-local-zip-with-deployed-versionspecific-versionstaging) — **not** with `config package import`. To work with one package, use [`config package import`](#package-commands-config-package). Packages can be exported using the following command: @@ -161,7 +161,7 @@ Inside the nodes directory, a file for each node will be present: ## Batch Import Packages -> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch archive** produced by [`config export`](#batch-export-packages). To import one package from a package zip, use [`config package import`](#package-commands-config-package) instead. +> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch artifact** produced by [`config export`](#batch-export-packages). To import one package from a package zip, use [`config package import`](#package-commands-config-package) instead. Packages can be imported using the following commands, if importing from a zip file: @@ -215,13 +215,13 @@ content-cli config import -p -d --validate --overwr ## Package Commands (`config package`) -The `config package` command group works with a package and its contents. It is **not** part of the batch-specific set — it uses the plain package format described below, which is not interchangeable with the batch archive (see [Two command families](#two-command-families)). +The `config package` command group works with a package and its contents. It is **not** part of the batch-specific set — it uses the plain package format described below, which is not interchangeable with the batch artifact (see [Two command families](#two-command-families)). ### Import a Package -`config package import` imports a package from a package zip (or directory). Unlike [`config import`](#batch-import-packages) — which performs a **batch** import and expects the multi-package batch archive (`manifest.json`, a top-level `variables.json`, `studio.json`, and a nested `_.zip` per package) — `config package import` takes a plain, flat package layout and imports it on its own. +`config package import` imports a package from a package zip (or directory). Unlike [`config import`](#batch-import-packages) — which performs a **batch** import and expects the multi-package batch artifact (`manifest.json`, a top-level `variables.json`, `studio.json`, and a nested `_.zip` per package) — `config package import` takes a plain, flat package layout and imports it on its own. -> A zip produced by `config export` is a **batch archive** and cannot be imported with `config package import`. Likewise, a package zip cannot be imported with `config import`. Use the command that matches how the artifact was produced. +> A zip produced by `config export` is a **batch artifact** and cannot be imported with `config package import`. Likewise, a package zip cannot be imported with `config import`. Use the command that matches how the artifact was produced. ```bash content-cli config package import -p -f @@ -253,7 +253,7 @@ package/ - `package.json` is the **source of truth for variable declarations**. Every assignment in `variables.json` must reference a variable declared in `package.json`, otherwise the import is rejected. - `variables.json` is optional. If you do not want to import variable assignments, simply omit the file. -This is intentionally different from the batch archive: there is no `manifest.json`, no `studio.json`, and no nested per-package zips — just the one package's files at the top level. +This is intentionally different from the batch artifact: there is no `manifest.json`, no `studio.json`, and no nested per-package zips — just the one package's files at the top level. #### Overwriting an Existing Package @@ -263,7 +263,7 @@ By default the import fails if a package with the same key already exists. Use ` content-cli config package import -p -f --overwrite ``` -When overwriting, variable assignments whose key is no longer declared in the imported `package.json` are pruned, keeping declarations and assignments consistent. +When overwriting, variable assignments whose key is no longer declared in the imported `package.json` are ignored, keeping declarations and assignments consistent. #### Output @@ -943,7 +943,7 @@ content-cli config nodes dependencies list --packageKey --nodeKey < ## Diff local zip with deployed version/specific version/staging -> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch archive** produced by [`config export`](#batch-export-packages); a package zip is not supported here. +> **Batch command.** Part of the [batch family](#two-command-families). It expects a multi-package **batch artifact** produced by [`config export`](#batch-export-packages); a package zip is not supported here. To compare local zipped packages with online packages use: ```bash diff --git a/src/commands/configuration-management/module.ts b/src/commands/configuration-management/module.ts index ebbecb90..fd70a6cd 100644 --- a/src/commands/configuration-management/module.ts +++ b/src/commands/configuration-management/module.ts @@ -63,7 +63,7 @@ class Module extends IModule { .action(this.batchImportPackages); const packageCommand = configCommand.command("package") - .description("Commands for working with a package."); + .description("Commands for working with a single package."); packageCommand.command("import") .description("Import a package from a zip file or directory. Uses the package format, which is not interchangeable with the batch 'config export' / 'config import' archive.")