From 8a506ce8280ef6c4204e65752276a14b80fd0476 Mon Sep 17 00:00:00 2001 From: Roberto Welzel Filho Date: Fri, 7 Aug 2026 12:48:01 +0200 Subject: [PATCH 1/6] SP-2354: Add export and import bookmarks commands Includes-AI-Code: true Co-authored-by: Cursor --- src/commands/bookmarks/bookmarks-api.ts | 24 ++++ .../bookmarks/bookmarks-command.service.ts | 27 +++++ src/commands/bookmarks/module.ts | 33 ++++++ tests/commands/bookmarks/bookmarks.spec.ts | 104 ++++++++++++++++++ 4 files changed, 188 insertions(+) create mode 100644 src/commands/bookmarks/bookmarks-api.ts create mode 100644 src/commands/bookmarks/bookmarks-command.service.ts create mode 100644 src/commands/bookmarks/module.ts create mode 100644 tests/commands/bookmarks/bookmarks.spec.ts diff --git a/src/commands/bookmarks/bookmarks-api.ts b/src/commands/bookmarks/bookmarks-api.ts new file mode 100644 index 0000000..597bb69 --- /dev/null +++ b/src/commands/bookmarks/bookmarks-api.ts @@ -0,0 +1,24 @@ +import { HttpClient } from "../../core/http/http-client"; +import { Context } from "../../core/command/cli-context"; +import { FatalError } from "../../core/utils/logger"; + +export class BookmarksApi { + + private httpClient: () => HttpClient; + + constructor(context: Context) { + this.httpClient = () => context.httpClient; + } + + public async exportBookmarks(packageKey: string): Promise { + return this.httpClient().get(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/export`).catch(e => { + throw new FatalError(`Problem exporting bookmarks for package ${packageKey}: ${e}`); + }); + } + + public async importBookmarks(packageKey: string, payload: any): Promise { + return this.httpClient().post(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/import`, payload).catch(e => { + throw new FatalError(`Problem importing bookmarks for package ${packageKey}: ${e}`); + }); + } +} diff --git a/src/commands/bookmarks/bookmarks-command.service.ts b/src/commands/bookmarks/bookmarks-command.service.ts new file mode 100644 index 0000000..31d386a --- /dev/null +++ b/src/commands/bookmarks/bookmarks-command.service.ts @@ -0,0 +1,27 @@ +import { Context } from "../../core/command/cli-context"; +import { BookmarksApi } from "./bookmarks-api"; +import { fileService, FileService } from "../../core/utils/file-service"; +import { logger } from "../../core/utils/logger"; + +export class BookmarksCommandService { + + private bookmarksApi: BookmarksApi; + + constructor(context: Context) { + this.bookmarksApi = new BookmarksApi(context); + } + + public async exportBookmarks(packageKey: string, file?: string): Promise { + const exportData = await this.bookmarksApi.exportBookmarks(packageKey); + + const fileName = file ?? `bookmarks-${packageKey}.json`; + fileService.writeToFileWithGivenName(JSON.stringify(exportData, null, 4), fileName); + logger.info(FileService.fileDownloadedMessage + fileName); + } + + public async importBookmarks(packageKey: string, file: string): Promise { + const payload = fileService.readFileToJson(file); + const result = await this.bookmarksApi.importBookmarks(packageKey, payload); + logger.info("Bookmarks imported successfully: " + JSON.stringify(result, null, 4)); + } +} diff --git a/src/commands/bookmarks/module.ts b/src/commands/bookmarks/module.ts new file mode 100644 index 0000000..b95f12b --- /dev/null +++ b/src/commands/bookmarks/module.ts @@ -0,0 +1,33 @@ +import { Configurator, IModule } from "../../core/command/module-handler"; +import { Context } from "../../core/command/cli-context"; +import { Command, OptionValues } from "commander"; +import { BookmarksCommandService } from "./bookmarks-command.service"; + +class Module extends IModule { + + public register(context: Context, configurator: Configurator): void { + const exportCommand = configurator.command("export"); + exportCommand.command("bookmarks") + .description("Export bookmarks for a package") + .requiredOption("--packageKey ", "Key of the package to export bookmarks from") + .option("-f, --file ", "Output file path (defaults to bookmarks-.json)") + .action(this.exportBookmarks); + + const importCommand = configurator.command("import"); + importCommand.command("bookmarks") + .description("Import bookmarks into a package") + .requiredOption("--packageKey ", "Key of the package to import bookmarks into") + .requiredOption("-f, --file ", "Bookmarks JSON file to import") + .action(this.importBookmarks); + } + + private async exportBookmarks(context: Context, command: Command, options: OptionValues): Promise { + await new BookmarksCommandService(context).exportBookmarks(options.packageKey, options.file); + } + + private async importBookmarks(context: Context, command: Command, options: OptionValues): Promise { + await new BookmarksCommandService(context).importBookmarks(options.packageKey, options.file); + } +} + +export = Module; diff --git a/tests/commands/bookmarks/bookmarks.spec.ts b/tests/commands/bookmarks/bookmarks.spec.ts new file mode 100644 index 0000000..52cc0f5 --- /dev/null +++ b/tests/commands/bookmarks/bookmarks.spec.ts @@ -0,0 +1,104 @@ +import { mockAxiosGet, mockAxiosPost, mockedPostRequestBodyByUrl } from "../../utls/http-requests-mock"; +import { BookmarksCommandService } from "../../../src/commands/bookmarks/bookmarks-command.service"; +import { loggingTestTransport } from "../../jest.setup"; +import { FileService } from "../../../src/core/utils/file-service"; +import { testContext } from "../../utls/test-context"; +import { getJsonFromDownloadedFile, writeJsonTempFile } from "../../utls/fs-utils"; + +describe("Export bookmarks", () => { + + const packageKey = "my-package"; + const mockExportResponse = { + packageKey: "my-package", + entries: [ + { + assetKey: "analysis-1", + assetType: "ANALYSIS", + bookmark: { + name: "My Bookmark", + ownerId: "user-123", + sharedByLink: false, + published: true, + }, + preference: { + configuration: "{\"filters\":[]}", + shareable: true, + mode: "PROCESS_ANALYTICS", + userId: "user-123", + }, + }, + ], + }; + + it("Should call export API and write JSON to default file", async () => { + mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/packages/${packageKey}/bookmarks/export`, mockExportResponse); + + await new BookmarksCommandService(testContext).exportBookmarks(packageKey); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); + expect(loggingTestTransport.logMessages[0].message).toContain(`bookmarks-${packageKey}.json`); + + expect(getJsonFromDownloadedFile()).toEqual(mockExportResponse); + }); + + it("Should call export API and write JSON to specified file", async () => { + mockAxiosGet(`https://myTeam.celonis.cloud/package-manager/api/packages/${packageKey}/bookmarks/export`, mockExportResponse); + + await new BookmarksCommandService(testContext).exportBookmarks(packageKey, "custom-output.json"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain("custom-output.json"); + + expect(getJsonFromDownloadedFile()).toEqual(mockExportResponse); + }); +}); + +describe("Import bookmarks", () => { + + const packageKey = "my-package"; + const mockImportPayload = { + entries: [ + { + assetKey: "analysis-1", + assetType: "ANALYSIS", + bookmark: { + name: "My Bookmark", + ownerId: "user-123", + sharedByLink: false, + published: true, + }, + preference: { + configuration: "{\"filters\":[]}", + shareable: true, + mode: "PROCESS_ANALYTICS", + userId: "user-123", + }, + }, + ], + }; + + const mockImportResult = { + packageKey: "my-package", + entries: [ + { + assetKey: "analysis-1", + status: "CREATED", + reason: null, + }, + ], + }; + + it("Should read file and call import API", async () => { + const importUrl = `https://myTeam.celonis.cloud/package-manager/api/packages/${packageKey}/bookmarks/import`; + writeJsonTempFile("bookmarks-import.json", mockImportPayload); + mockAxiosPost(importUrl, mockImportResult); + + await new BookmarksCommandService(testContext).importBookmarks(packageKey, "bookmarks-import.json"); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain("Bookmarks imported successfully"); + + expect(JSON.parse(mockedPostRequestBodyByUrl.get(importUrl))).toEqual(mockImportPayload); + }); +}); From 7db5035708164e6492fe2ed45f4b63241813ffc5 Mon Sep 17 00:00:00 2001 From: Roberto Welzel Filho Date: Fri, 7 Aug 2026 13:03:20 +0200 Subject: [PATCH 2/6] SP-2354: Fix SonarCloud quality gate issues Replace `any` types with proper interfaces for bookmarks API methods, prefix unused `command` parameter with underscore, use template literal for string interpolation, and add error path tests plus module registration tests to reach coverage threshold. Includes-AI-Code: true Co-authored-by: Cursor --- src/commands/bookmarks/bookmarks-api.ts | 5 +- .../bookmarks/bookmarks-command.service.ts | 2 +- .../bookmarks/bookmarks.interfaces.ts | 40 +++++++++ src/commands/bookmarks/module.ts | 4 +- tests/commands/bookmarks/bookmarks.spec.ts | 85 ++++++++++++++++++- 5 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 src/commands/bookmarks/bookmarks.interfaces.ts diff --git a/src/commands/bookmarks/bookmarks-api.ts b/src/commands/bookmarks/bookmarks-api.ts index 597bb69..a24eaed 100644 --- a/src/commands/bookmarks/bookmarks-api.ts +++ b/src/commands/bookmarks/bookmarks-api.ts @@ -1,6 +1,7 @@ import { HttpClient } from "../../core/http/http-client"; import { Context } from "../../core/command/cli-context"; import { FatalError } from "../../core/utils/logger"; +import { BookmarksExport, BookmarksImportRequest, BookmarksImportResult } from "./bookmarks.interfaces"; export class BookmarksApi { @@ -10,13 +11,13 @@ export class BookmarksApi { this.httpClient = () => context.httpClient; } - public async exportBookmarks(packageKey: string): Promise { + public async exportBookmarks(packageKey: string): Promise { return this.httpClient().get(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/export`).catch(e => { throw new FatalError(`Problem exporting bookmarks for package ${packageKey}: ${e}`); }); } - public async importBookmarks(packageKey: string, payload: any): Promise { + public async importBookmarks(packageKey: string, payload: BookmarksImportRequest): Promise { return this.httpClient().post(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/import`, payload).catch(e => { throw new FatalError(`Problem importing bookmarks for package ${packageKey}: ${e}`); }); diff --git a/src/commands/bookmarks/bookmarks-command.service.ts b/src/commands/bookmarks/bookmarks-command.service.ts index 31d386a..383a625 100644 --- a/src/commands/bookmarks/bookmarks-command.service.ts +++ b/src/commands/bookmarks/bookmarks-command.service.ts @@ -22,6 +22,6 @@ export class BookmarksCommandService { public async importBookmarks(packageKey: string, file: string): Promise { const payload = fileService.readFileToJson(file); const result = await this.bookmarksApi.importBookmarks(packageKey, payload); - logger.info("Bookmarks imported successfully: " + JSON.stringify(result, null, 4)); + logger.info(`Bookmarks imported successfully: ${JSON.stringify(result, null, 4)}`); } } diff --git a/src/commands/bookmarks/bookmarks.interfaces.ts b/src/commands/bookmarks/bookmarks.interfaces.ts new file mode 100644 index 0000000..e487944 --- /dev/null +++ b/src/commands/bookmarks/bookmarks.interfaces.ts @@ -0,0 +1,40 @@ +export interface BookmarkEntry { + assetKey: string; + assetType: string; + bookmark: BookmarkDetails; + preference: BookmarkPreference; +} + +export interface BookmarkDetails { + name: string; + ownerId: string; + sharedByLink: boolean; + published: boolean; +} + +export interface BookmarkPreference { + configuration: string; + shareable: boolean; + mode: string; + userId: string; +} + +export interface BookmarksExport { + packageKey: string; + entries: BookmarkEntry[]; +} + +export interface BookmarksImportRequest { + entries: BookmarkEntry[]; +} + +export interface BookmarksImportResultEntry { + assetKey: string; + status: string; + reason: string | null; +} + +export interface BookmarksImportResult { + packageKey: string; + entries: BookmarksImportResultEntry[]; +} diff --git a/src/commands/bookmarks/module.ts b/src/commands/bookmarks/module.ts index b95f12b..54a1beb 100644 --- a/src/commands/bookmarks/module.ts +++ b/src/commands/bookmarks/module.ts @@ -21,11 +21,11 @@ class Module extends IModule { .action(this.importBookmarks); } - private async exportBookmarks(context: Context, command: Command, options: OptionValues): Promise { + private async exportBookmarks(context: Context, _command: Command, options: OptionValues): Promise { await new BookmarksCommandService(context).exportBookmarks(options.packageKey, options.file); } - private async importBookmarks(context: Context, command: Command, options: OptionValues): Promise { + private async importBookmarks(context: Context, _command: Command, options: OptionValues): Promise { await new BookmarksCommandService(context).importBookmarks(options.packageKey, options.file); } } diff --git a/tests/commands/bookmarks/bookmarks.spec.ts b/tests/commands/bookmarks/bookmarks.spec.ts index 52cc0f5..f9ce436 100644 --- a/tests/commands/bookmarks/bookmarks.spec.ts +++ b/tests/commands/bookmarks/bookmarks.spec.ts @@ -1,9 +1,12 @@ -import { mockAxiosGet, mockAxiosPost, mockedPostRequestBodyByUrl } from "../../utls/http-requests-mock"; +import { mockAxiosGet, mockAxiosGetError, mockAxiosPost, mockAxiosPostError, mockedPostRequestBodyByUrl } from "../../utls/http-requests-mock"; import { BookmarksCommandService } from "../../../src/commands/bookmarks/bookmarks-command.service"; import { loggingTestTransport } from "../../jest.setup"; import { FileService } from "../../../src/core/utils/file-service"; +import { FatalError } from "../../../src/core/utils/logger"; import { testContext } from "../../utls/test-context"; import { getJsonFromDownloadedFile, writeJsonTempFile } from "../../utls/fs-utils"; +import { Configurator } from "../../../src/core/command/module-handler"; +import { Command } from "commander"; describe("Export bookmarks", () => { @@ -52,6 +55,15 @@ describe("Export bookmarks", () => { expect(getJsonFromDownloadedFile()).toEqual(mockExportResponse); }); + + it("Should throw FatalError when export API fails", async () => { + mockAxiosGetError(`https://myTeam.celonis.cloud/package-manager/api/packages/${packageKey}/bookmarks/export`, 500, { message: "Internal Server Error" }); + + await expect(new BookmarksCommandService(testContext).exportBookmarks(packageKey)) + .rejects.toThrow(FatalError); + await expect(new BookmarksCommandService(testContext).exportBookmarks(packageKey)) + .rejects.toThrow(/Problem exporting bookmarks for package my-package/); + }); }); describe("Import bookmarks", () => { @@ -101,4 +113,75 @@ describe("Import bookmarks", () => { expect(JSON.parse(mockedPostRequestBodyByUrl.get(importUrl))).toEqual(mockImportPayload); }); + + it("Should throw FatalError when import API fails", async () => { + const importUrl = `https://myTeam.celonis.cloud/package-manager/api/packages/${packageKey}/bookmarks/import`; + writeJsonTempFile("bookmarks-import-err.json", mockImportPayload); + mockAxiosPostError(importUrl, 500, { message: "Internal Server Error" }); + + await expect(new BookmarksCommandService(testContext).importBookmarks(packageKey, "bookmarks-import-err.json")) + .rejects.toThrow(FatalError); + await expect(new BookmarksCommandService(testContext).importBookmarks(packageKey, "bookmarks-import-err.json")) + .rejects.toThrow(/Problem importing bookmarks for package my-package/); + }); +}); + +describe("Bookmarks module registration", () => { + + it("Should register export and import bookmarks commands", () => { + const Module = require("../../../src/commands/bookmarks/module"); + const program = new Command(); + const configurator = new Configurator(program, testContext); + + const moduleInstance = new Module(); + moduleInstance.register(testContext, configurator); + + const exportCmd = program.commands.find(c => c.name() === "export"); + expect(exportCmd).toBeDefined(); + const exportBookmarksCmd = exportCmd.commands.find(c => c.name() === "bookmarks"); + expect(exportBookmarksCmd).toBeDefined(); + expect(exportBookmarksCmd.description()).toBe("Export bookmarks for a package"); + + const importCmd = program.commands.find(c => c.name() === "import"); + expect(importCmd).toBeDefined(); + const importBookmarksCmd = importCmd.commands.find(c => c.name() === "bookmarks"); + expect(importBookmarksCmd).toBeDefined(); + expect(importBookmarksCmd.description()).toBe("Import bookmarks into a package"); + }); + + it("Should execute export bookmarks action", async () => { + const exportUrl = `https://myTeam.celonis.cloud/package-manager/api/packages/test-pkg/bookmarks/export`; + mockAxiosGet(exportUrl, { packageKey: "test-pkg", entries: [] }); + + const Module = require("../../../src/commands/bookmarks/module"); + const program = new Command(); + const configurator = new Configurator(program, testContext); + + const moduleInstance = new Module(); + moduleInstance.register(testContext, configurator); + + await program.parseAsync(["export", "bookmarks", "--packageKey", "test-pkg"], { from: "user" }); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); + }); + + it("Should execute import bookmarks action", async () => { + const importUrl = `https://myTeam.celonis.cloud/package-manager/api/packages/test-pkg/bookmarks/import`; + const payload = { entries: [] }; + writeJsonTempFile("module-test-import.json", payload); + mockAxiosPost(importUrl, { packageKey: "test-pkg", entries: [] }); + + const Module = require("../../../src/commands/bookmarks/module"); + const program = new Command(); + const configurator = new Configurator(program, testContext); + + const moduleInstance = new Module(); + moduleInstance.register(testContext, configurator); + + await program.parseAsync(["import", "bookmarks", "--packageKey", "test-pkg", "-f", "module-test-import.json"], { from: "user" }); + + expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages[0].message).toContain("Bookmarks imported successfully"); + }); }); From 2f75b9ceaba5ebd30b7460ae215479d3548a2e16 Mon Sep 17 00:00:00 2001 From: Roberto Welzel Filho Date: Fri, 7 Aug 2026 13:09:16 +0200 Subject: [PATCH 3/6] SP-2354: Use try/await/catch and preserve error cause Refactor API methods to use idiomatic try/catch with await instead of .catch() chains, and pass the original error as cause to FatalError for proper error chain preservation. Includes-AI-Code: true Co-authored-by: Cursor --- src/commands/bookmarks/bookmarks-api.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/commands/bookmarks/bookmarks-api.ts b/src/commands/bookmarks/bookmarks-api.ts index a24eaed..1e8d5a7 100644 --- a/src/commands/bookmarks/bookmarks-api.ts +++ b/src/commands/bookmarks/bookmarks-api.ts @@ -12,14 +12,18 @@ export class BookmarksApi { } public async exportBookmarks(packageKey: string): Promise { - return this.httpClient().get(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/export`).catch(e => { - throw new FatalError(`Problem exporting bookmarks for package ${packageKey}: ${e}`); - }); + try { + return await this.httpClient().get(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/export`); + } catch (e) { + throw new FatalError(`Problem exporting bookmarks for package ${packageKey}: ${e}`, { cause: e }); + } } public async importBookmarks(packageKey: string, payload: BookmarksImportRequest): Promise { - return this.httpClient().post(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/import`, payload).catch(e => { - throw new FatalError(`Problem importing bookmarks for package ${packageKey}: ${e}`); - }); + try { + return await this.httpClient().post(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/import`, payload); + } catch (e) { + throw new FatalError(`Problem importing bookmarks for package ${packageKey}: ${e}`, { cause: e }); + } } } From 4e39c05f0b9e89bd6427612f3e7f338244240923 Mon Sep 17 00:00:00 2001 From: Roberto Welzel Filho Date: Fri, 7 Aug 2026 13:16:50 +0200 Subject: [PATCH 4/6] SP-2354: Fix remaining SonarCloud issues Mark class members as readonly and use toHaveLength() assertions in tests for better error reporting. Includes-AI-Code: true Co-authored-by: Cursor --- src/commands/bookmarks/bookmarks-api.ts | 2 +- src/commands/bookmarks/bookmarks-command.service.ts | 2 +- tests/commands/bookmarks/bookmarks.spec.ts | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/bookmarks/bookmarks-api.ts b/src/commands/bookmarks/bookmarks-api.ts index 1e8d5a7..23909ad 100644 --- a/src/commands/bookmarks/bookmarks-api.ts +++ b/src/commands/bookmarks/bookmarks-api.ts @@ -5,7 +5,7 @@ import { BookmarksExport, BookmarksImportRequest, BookmarksImportResult } from " export class BookmarksApi { - private httpClient: () => HttpClient; + private readonly httpClient: () => HttpClient; constructor(context: Context) { this.httpClient = () => context.httpClient; diff --git a/src/commands/bookmarks/bookmarks-command.service.ts b/src/commands/bookmarks/bookmarks-command.service.ts index 383a625..3c903f2 100644 --- a/src/commands/bookmarks/bookmarks-command.service.ts +++ b/src/commands/bookmarks/bookmarks-command.service.ts @@ -5,7 +5,7 @@ import { logger } from "../../core/utils/logger"; export class BookmarksCommandService { - private bookmarksApi: BookmarksApi; + private readonly bookmarksApi: BookmarksApi; constructor(context: Context) { this.bookmarksApi = new BookmarksApi(context); diff --git a/tests/commands/bookmarks/bookmarks.spec.ts b/tests/commands/bookmarks/bookmarks.spec.ts index f9ce436..08107ec 100644 --- a/tests/commands/bookmarks/bookmarks.spec.ts +++ b/tests/commands/bookmarks/bookmarks.spec.ts @@ -38,7 +38,7 @@ describe("Export bookmarks", () => { await new BookmarksCommandService(testContext).exportBookmarks(packageKey); - expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages).toHaveLength(1); expect(loggingTestTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); expect(loggingTestTransport.logMessages[0].message).toContain(`bookmarks-${packageKey}.json`); @@ -50,7 +50,7 @@ describe("Export bookmarks", () => { await new BookmarksCommandService(testContext).exportBookmarks(packageKey, "custom-output.json"); - expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages).toHaveLength(1); expect(loggingTestTransport.logMessages[0].message).toContain("custom-output.json"); expect(getJsonFromDownloadedFile()).toEqual(mockExportResponse); @@ -108,7 +108,7 @@ describe("Import bookmarks", () => { await new BookmarksCommandService(testContext).importBookmarks(packageKey, "bookmarks-import.json"); - expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages).toHaveLength(1); expect(loggingTestTransport.logMessages[0].message).toContain("Bookmarks imported successfully"); expect(JSON.parse(mockedPostRequestBodyByUrl.get(importUrl))).toEqual(mockImportPayload); @@ -162,7 +162,7 @@ describe("Bookmarks module registration", () => { await program.parseAsync(["export", "bookmarks", "--packageKey", "test-pkg"], { from: "user" }); - expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages).toHaveLength(1); expect(loggingTestTransport.logMessages[0].message).toContain(FileService.fileDownloadedMessage); }); @@ -181,7 +181,7 @@ describe("Bookmarks module registration", () => { await program.parseAsync(["import", "bookmarks", "--packageKey", "test-pkg", "-f", "module-test-import.json"], { from: "user" }); - expect(loggingTestTransport.logMessages.length).toBe(1); + expect(loggingTestTransport.logMessages).toHaveLength(1); expect(loggingTestTransport.logMessages[0].message).toContain("Bookmarks imported successfully"); }); }); From f733b8b2dc83203dcae68100b2c8d46d6acf4b55 Mon Sep 17 00:00:00 2001 From: Roberto Welzel Filho Date: Fri, 7 Aug 2026 14:08:31 +0200 Subject: [PATCH 5/6] SP-2354: Remove error cause for codebase consistency Drop { cause: e } from FatalError constructors to stay consistent with the pattern used across the rest of the codebase. Includes-AI-Code: true Co-authored-by: Cursor --- src/commands/bookmarks/bookmarks-api.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/bookmarks/bookmarks-api.ts b/src/commands/bookmarks/bookmarks-api.ts index 23909ad..349be47 100644 --- a/src/commands/bookmarks/bookmarks-api.ts +++ b/src/commands/bookmarks/bookmarks-api.ts @@ -15,7 +15,7 @@ export class BookmarksApi { try { return await this.httpClient().get(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/export`); } catch (e) { - throw new FatalError(`Problem exporting bookmarks for package ${packageKey}: ${e}`, { cause: e }); + throw new FatalError(`Problem exporting bookmarks for package ${packageKey}: ${e}`); } } @@ -23,7 +23,7 @@ export class BookmarksApi { try { return await this.httpClient().post(`/package-manager/api/packages/${encodeURIComponent(packageKey)}/bookmarks/import`, payload); } catch (e) { - throw new FatalError(`Problem importing bookmarks for package ${packageKey}: ${e}`, { cause: e }); + throw new FatalError(`Problem importing bookmarks for package ${packageKey}: ${e}`); } } } From 09db219694cb949e2cdfa096eb286fca3c70f9b9 Mon Sep 17 00:00:00 2001 From: Roberto Welzel Filho Date: Fri, 7 Aug 2026 15:16:44 +0200 Subject: [PATCH 6/6] SP-2354: Add documentation, CODEOWNERS, and command graph Includes-AI-Code: true Co-authored-by: Cursor --- .github/CODEOWNERS | 2 ++ docs/command-graph.html | 14 ++++++++++---- docs/user-guide/bookmark-commands.md | 25 +++++++++++++++++++++++++ docs/user-guide/index.md | 1 + mkdocs.yaml | 1 + 5 files changed, 39 insertions(+), 4 deletions(-) create mode 100644 docs/user-guide/bookmark-commands.md diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3f43cfe..4335131 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -14,6 +14,8 @@ /tests/commands/deployment/ @celonis/astro @celonis/studio-platform /src/commands/action-flows/ @celonis/process-automation /tests/commands/action-flows/ @celonis/process-automation +/src/commands/bookmarks/ @celonis/studio-platform +/tests/commands/bookmarks/ @celonis/studio-platform /src/commands/analysis/ @celonis/process-analytics /src/commands/cpm4/ @celonis/cpm4 /src/commands/data-pipeline/ @Dusan-r @IvanGandacov @EktaCelonis @gorasoCelonis diff --git a/docs/command-graph.html b/docs/command-graph.html index ed95d75..68fd253 100644 --- a/docs/command-graph.html +++ b/docs/command-graph.html @@ -155,9 +155,9 @@ { id: "area_analyze", label: "analyze", group: "area", path: "analyze", description: "Analyze action flows and return its dependencies", options: ["-h, --help"] }, { id: "area_export", label: "export", group: "area", path: "export", - description: "Export resource: action flow or data pool", options: ["-h, --help"] }, + description: "Export resource: action flow, data pool, or bookmarks", options: ["-h, --help"] }, { id: "area_import", label: "import", group: "area", path: "import", - description: "Import resource: action flow or data pool", options: ["-h, --help"] }, + description: "Import resource: action flow, data pool, or bookmarks", options: ["-h, --help"] }, { id: "area_pull", label: "pull", group: "area", path: "pull", description: "Pull resource: skill, bookmark, view bookmark, data pool or asset", options: ["-h, --help"] }, { id: "area_push", label: "push", group: "area", path: "push", @@ -211,6 +211,9 @@ { id: "export_data_pool", label: "data-pool", group: "command", path: "export data-pool", description: "Command to export a data pool", options: ["-p, --profile ", "--id ", "--outputToJsonFile", "-h, --help"] }, + { id: "export_bookmarks", label: "bookmarks", group: "command", path: "export bookmarks", + description: "Export bookmarks for a package", + options: ["-p, --profile ", "--packageKey ", "-f, --file ", "-h, --help"] }, // import { id: "import_action_flows", label: "action-flows", group: "command", path: "import action-flows", @@ -219,6 +222,9 @@ { id: "import_data_pools", label: "data-pools", group: "command", path: "import data-pools", description: "Command to batch import multiple data pools with their objects and dependencies", options: ["-p, --profile ", "-f, --jsonFile ", "--outputToJsonFile", "-h, --help"] }, + { id: "import_bookmarks", label: "bookmarks", group: "command", path: "import bookmarks", + description: "Import bookmarks into a package", + options: ["-p, --profile ", "--packageKey ", "-f, --file ", "-h, --help"] }, // pull { id: "pull_skill", label: "skill", group: "command", path: "pull skill", @@ -479,9 +485,9 @@ ["area_analyze","analyze_action_flows"], - ["area_export","export_action_flows"],["area_export","export_data_pool"], + ["area_export","export_action_flows"],["area_export","export_data_pool"],["area_export","export_bookmarks"], - ["area_import","import_action_flows"],["area_import","import_data_pools"], + ["area_import","import_action_flows"],["area_import","import_data_pools"],["area_import","import_bookmarks"], ["area_pull","pull_skill"],["area_pull","pull_bookmarks"],["area_pull","pull_data_pool"], ["area_pull","pull_asset"],["area_pull","pull_package"],["area_pull","pull_view_bookmarks"], diff --git a/docs/user-guide/bookmark-commands.md b/docs/user-guide/bookmark-commands.md new file mode 100644 index 0000000..651ef00 --- /dev/null +++ b/docs/user-guide/bookmark-commands.md @@ -0,0 +1,25 @@ +# Bookmark Commands + +## Export Bookmarks + +Export all bookmarks for a package. The exported file can then be imported into another team using the `import bookmarks` command. + +``` +content-cli export bookmarks -p --packageKey +``` + +By default, the export is saved to `bookmarks-.json` in the current directory. Use `-f` to specify a custom output path: + +``` +content-cli export bookmarks -p --packageKey -f my-bookmarks.json +``` + +## Import Bookmarks + +Import bookmarks into a package from a previously exported JSON file. + +``` +content-cli import bookmarks -p --packageKey -f bookmarks-.json +``` + +The import result is printed to the console showing the status of each entry (CREATED or SKIPPED). diff --git a/docs/user-guide/index.md b/docs/user-guide/index.md index 5eb518f..5fb464b 100644 --- a/docs/user-guide/index.md +++ b/docs/user-guide/index.md @@ -11,3 +11,4 @@ Content CLI organizes its commands into groups by area. Each group covers a spec | [Asset Registry Commands](./asset-registry-commands.md) | Discover registered asset types and their service descriptors | | [Data Pool Commands](./data-pool-commands.md) | Export and import Data Pools with their dependencies | | [Action Flow Commands](./action-flow-commands.md) | Analyze and export/import Action Flows and their dependencies | +| [Bookmark Commands](./bookmark-commands.md) | Export and import bookmarks for packages | diff --git a/mkdocs.yaml b/mkdocs.yaml index 4648ae2..0f5d234 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -19,6 +19,7 @@ nav: - Asset Registry Commands: './user-guide/asset-registry-commands.md' - Data Pool Commands: './user-guide/data-pool-commands.md' - Action Flow Commands: './user-guide/action-flow-commands.md' + - Bookmark Commands: './user-guide/bookmark-commands.md' - Development: - Architecture: './internal-architecture.md' - How to Add a Command: './how-to-add-command.md'