diff --git a/packages/cli/src/bin/linkedin.ts b/packages/cli/src/bin/linkedin.ts index 2fd1992d..e91bc043 100644 --- a/packages/cli/src/bin/linkedin.ts +++ b/packages/cli/src/bin/linkedin.ts @@ -7282,6 +7282,79 @@ async function runProfilePrepareUpdatePublicProfile( } } +async function runProfilePrepareUploadPhoto( + input: { + profileName: string; + filePath: string; + operatorNote?: string; + }, + cdpUrl?: string, +): Promise { + const runtime = createRuntime(cdpUrl); + + try { + runtime.logger.log("info", "cli.profile.prepare_upload_photo.start", { + profileName: input.profileName, + filePath: input.filePath, + }); + + const prepared = await runtime.profile.prepareUploadPhoto({ + profileName: input.profileName, + filePath: input.filePath, + ...(input.operatorNote ? { operatorNote: input.operatorNote } : {}), + }); + + runtime.logger.log("info", "cli.profile.prepare_upload_photo.done", { + profileName: input.profileName, + preparedActionId: prepared.preparedActionId, + }); + + printPrepareResult({ + run_id: runtime.runId, + profile_name: input.profileName, + ...prepared, + }); + } finally { + runtime.close(); + } +} + +async function runProfilePrepareUploadBanner( + input: { + profileName: string; + filePath: string; + operatorNote?: string; + }, + cdpUrl?: string, +): Promise { + const runtime = createRuntime(cdpUrl); + + try { + runtime.logger.log("info", "cli.profile.prepare_upload_banner.start", { + profileName: input.profileName, + filePath: input.filePath, + }); + + const prepared = await runtime.profile.prepareUploadBanner({ + profileName: input.profileName, + filePath: input.filePath, + ...(input.operatorNote ? { operatorNote: input.operatorNote } : {}), + }); + + runtime.logger.log("info", "cli.profile.prepare_upload_banner.done", { + profileName: input.profileName, + preparedActionId: prepared.preparedActionId, + }); + + printPrepareResult({ + run_id: runtime.runId, + profile_name: input.profileName, + ...prepared, + }); + } finally { + runtime.close(); + } +} function summarizeProfileSeedUnsupportedFields( unsupportedFields: readonly ProfileSeedUnsupportedField[], ): string { @@ -13254,6 +13327,62 @@ export function createCliProgram(): Command { }, ); + profileCommand + .command("prepare-upload-photo") + .description("Prepare to upload a LinkedIn profile photo (two-phase)") + .requiredOption("--file ", "Path to the local image file") + .option("-p, --profile ", "Profile name", "default") + .option( + "--operator-note ", + "Optional note attached to the prepared action", + ) + .action( + async (options: { + file: string; + operatorNote?: string; + profile: string; + }) => { + await runProfilePrepareUploadPhoto( + { + profileName: options.profile, + filePath: options.file, + ...(options.operatorNote + ? { operatorNote: options.operatorNote } + : {}), + }, + readCdpUrl(), + ); + }, + ); + + profileCommand + .command("prepare-upload-banner") + .description("Prepare to upload a LinkedIn profile banner (two-phase)") + .requiredOption("--file ", "Path to the local image file") + .option("-p, --profile ", "Profile name", "default") + .option( + "--operator-note ", + "Optional note attached to the prepared action", + ) + .action( + async (options: { + file: string; + operatorNote?: string; + profile: string; + }) => { + await runProfilePrepareUploadBanner( + { + profileName: options.profile, + filePath: options.file, + ...(options.operatorNote + ? { operatorNote: options.operatorNote } + : {}), + }, + readCdpUrl(), + ); + }, + ); + profileCommand .command("apply-spec") .description( diff --git a/packages/cli/test/profileCli.test.ts b/packages/cli/test/profileCli.test.ts index 4fefae1c..92e25411 100644 --- a/packages/cli/test/profileCli.test.ts +++ b/packages/cli/test/profileCli.test.ts @@ -10,6 +10,8 @@ const profileCliMocks = vi.hoisted(() => ({ loggerLog: vi.fn(), prepareUpdatePublicProfile: vi.fn(), prepareUpdateSettings: vi.fn(), + prepareUploadBanner: vi.fn(), + prepareUploadPhoto: vi.fn(), prepareRemoveSectionItem: vi.fn(), prepareUpdateIntro: vi.fn(), prepareUpsertSectionItem: vi.fn(), @@ -51,6 +53,8 @@ describe("CLI profile commands", () => { prepareUpdatePublicProfile: profileCliMocks.prepareUpdatePublicProfile, prepareUpdateSettings: profileCliMocks.prepareUpdateSettings, prepareUpdateIntro: profileCliMocks.prepareUpdateIntro, + prepareUploadBanner: profileCliMocks.prepareUploadBanner, + prepareUploadPhoto: profileCliMocks.prepareUploadPhoto, prepareUpsertSectionItem: profileCliMocks.prepareUpsertSectionItem, prepareRemoveSectionItem: profileCliMocks.prepareRemoveSectionItem }, @@ -108,6 +112,18 @@ describe("CLI profile commands", () => { expiresAtMs: 1, preview: { summary: "Update public profile" } }); + profileCliMocks.prepareUploadPhoto.mockResolvedValue({ + preparedActionId: "pa_upload_photo", + confirmToken: "ct_upload_photo", + expiresAtMs: 1, + preview: { summary: "Upload LinkedIn profile photo" } + }); + profileCliMocks.prepareUploadBanner.mockResolvedValue({ + preparedActionId: "pa_upload_banner", + confirmToken: "ct_upload_banner", + expiresAtMs: 1, + preview: { summary: "Upload LinkedIn profile banner" } + }); profileCliMocks.prepareUpsertSectionItem.mockReturnValue({ preparedActionId: "pa_about", confirmToken: "ct_about", @@ -240,6 +256,56 @@ describe("CLI profile commands", () => { }); }); + it("prepares a profile photo upload", async () => { + await runCli([ + "node", + "linkedin", + "profile", + "prepare-upload-photo", + "--profile", + "smoke", + "--file", + "photo.jpg" + ]); + + const output = JSON.parse(stdoutChunks.join("\n")) as { + confirmToken: string; + preview: { summary: string }; + }; + + expect(output.confirmToken).toBe("ct_upload_photo"); + expect(output.preview.summary).toBe("Upload LinkedIn profile photo"); + expect(profileCliMocks.prepareUploadPhoto).toHaveBeenCalledWith({ + profileName: "smoke", + filePath: "photo.jpg" + }); + }); + + it("prepares a profile banner upload", async () => { + await runCli([ + "node", + "linkedin", + "profile", + "prepare-upload-banner", + "--profile", + "smoke", + "--file", + "banner.jpg" + ]); + + const output = JSON.parse(stdoutChunks.join("\n")) as { + confirmToken: string; + preview: { summary: string }; + }; + + expect(output.confirmToken).toBe("ct_upload_banner"); + expect(output.preview.summary).toBe("Upload LinkedIn profile banner"); + expect(profileCliMocks.prepareUploadBanner).toHaveBeenCalledWith({ + profileName: "smoke", + filePath: "banner.jpg" + }); + }); + it("applies a profile seed spec and reports unsupported fields when partial mode is enabled", async () => { const specPath = path.join(tempDir, "profile-spec.json"); await writeFile( @@ -385,6 +451,8 @@ describe("CLI profile apply-spec --continue-on-error", () => { prepareUpdatePublicProfile: profileCliMocks.prepareUpdatePublicProfile, prepareUpdateSettings: profileCliMocks.prepareUpdateSettings, prepareUpdateIntro: profileCliMocks.prepareUpdateIntro, + prepareUploadBanner: profileCliMocks.prepareUploadBanner, + prepareUploadPhoto: profileCliMocks.prepareUploadPhoto, prepareUpsertSectionItem: profileCliMocks.prepareUpsertSectionItem, prepareRemoveSectionItem: profileCliMocks.prepareRemoveSectionItem }, @@ -444,6 +512,18 @@ describe("CLI profile apply-spec --continue-on-error", () => { expiresAtMs: 1, preview: { summary: "Update public profile" } }); + profileCliMocks.prepareUploadPhoto.mockResolvedValue({ + preparedActionId: "pa_upload_photo", + confirmToken: "ct_upload_photo", + expiresAtMs: 1, + preview: { summary: "Upload LinkedIn profile photo" } + }); + profileCliMocks.prepareUploadBanner.mockResolvedValue({ + preparedActionId: "pa_upload_banner", + confirmToken: "ct_upload_banner", + expiresAtMs: 1, + preview: { summary: "Upload LinkedIn profile banner" } + }); profileCliMocks.prepareUpsertSectionItem.mockReturnValue({ preparedActionId: "pa_about", confirmToken: "ct_about",