Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions packages/cli/src/bin/linkedin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7282,6 +7282,79 @@ async function runProfilePrepareUpdatePublicProfile(
}
}

async function runProfilePrepareUploadPhoto(
input: {
profileName: string;
filePath: string;
operatorNote?: string;
},
cdpUrl?: string,
): Promise<void> {
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<void> {
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 {
Expand Down Expand Up @@ -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>", "Path to the local image file")
.option("-p, --profile <profile>", "Profile name", "default")
.option(
"--operator-note <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>", "Path to the local image file")
.option("-p, --profile <profile>", "Profile name", "default")
.option(
"--operator-note <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(
Expand Down
80 changes: 80 additions & 0 deletions packages/cli/test/profileCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
},
Expand Down Expand Up @@ -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",
Expand Down