From 214675d0a0e2eb6643e253faab90109045ff1088 Mon Sep 17 00:00:00 2001 From: Kjell Tore Guttormsen Date: Fri, 10 Apr 2026 15:08:42 +0200 Subject: [PATCH] =?UTF-8?q?feat(linkedin):=20add=20clipboard-helper.mjs=20?= =?UTF-8?q?=E2=80=94=20cross-platform=20clipboard=20utility?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TDD: 13 tests written first, then implementation. Exports copyToClipboard(text) and clipboardAvailable() — never throws. Supports darwin (pbcopy), win32 (clip), linux (xclip/xsel fallback). Dual standalone/import mode following personalization-score.mjs pattern. Co-Authored-By: Claude Opus 4.6 --- .../__tests__/clipboard-helper.test.mjs | 86 +++++++++++++++ .../hooks/scripts/clipboard-helper.mjs | 102 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 plugins/linkedin-thought-leadership/hooks/scripts/__tests__/clipboard-helper.test.mjs create mode 100644 plugins/linkedin-thought-leadership/hooks/scripts/clipboard-helper.mjs diff --git a/plugins/linkedin-thought-leadership/hooks/scripts/__tests__/clipboard-helper.test.mjs b/plugins/linkedin-thought-leadership/hooks/scripts/__tests__/clipboard-helper.test.mjs new file mode 100644 index 0000000..3407b4e --- /dev/null +++ b/plugins/linkedin-thought-leadership/hooks/scripts/__tests__/clipboard-helper.test.mjs @@ -0,0 +1,86 @@ +import { describe, test } from 'node:test'; +import assert from 'node:assert/strict'; +import { clipboardAvailable, copyToClipboard } from '../clipboard-helper.mjs'; + +describe('clipboardAvailable', () => { + test('returns object with available and platform fields', () => { + const result = clipboardAvailable(); + assert.equal(typeof result.available, 'boolean'); + assert.equal(typeof result.platform, 'string'); + }); + + test('returns available: true on macOS (darwin)', () => { + if (process.platform !== 'darwin') return; + const result = clipboardAvailable(); + assert.equal(result.available, true); + assert.equal(result.platform, 'darwin'); + }); + + test('returns a recognized platform string', () => { + const result = clipboardAvailable(); + assert.ok( + ['darwin', 'win32', 'linux'].includes(result.platform), + `Unexpected platform: ${result.platform}` + ); + }); +}); + +describe('copyToClipboard', () => { + test('returns object with success and platform fields', () => { + const result = copyToClipboard('test clipboard text'); + assert.equal(typeof result.success, 'boolean'); + assert.equal(typeof result.platform, 'string'); + }); + + test('copies text successfully on macOS', () => { + if (process.platform !== 'darwin') return; + const result = copyToClipboard('clipboard-helper test 2026'); + assert.equal(result.success, true); + assert.equal(result.platform, 'darwin'); + }); + + test('handles empty string input gracefully', () => { + const result = copyToClipboard(''); + assert.equal(result.success, true); + assert.equal(typeof result.platform, 'string'); + }); + + test('handles multiline text', () => { + const multiline = 'Line 1\nLine 2\nLine 3'; + const result = copyToClipboard(multiline); + assert.equal(result.success, true); + }); + + test('handles special characters (quotes, ampersands, backticks)', () => { + const special = 'He said "hello" & she said \'goodbye\' `code` $VAR'; + const result = copyToClipboard(special); + assert.equal(result.success, true); + }); + + test('handles unicode/emoji text', () => { + const unicode = '🚀 Thought leadership → impact'; + const result = copyToClipboard(unicode); + assert.equal(result.success, true); + }); + + test('never throws — always returns a result object', () => { + assert.doesNotThrow(() => copyToClipboard(null)); + assert.doesNotThrow(() => copyToClipboard(undefined)); + assert.doesNotThrow(() => copyToClipboard(123)); + }); + + test('returns success: false for non-string input', () => { + const result = copyToClipboard(null); + assert.equal(result.success, false); + }); +}); + +describe('module exports', () => { + test('exports clipboardAvailable as a function', () => { + assert.equal(typeof clipboardAvailable, 'function'); + }); + + test('exports copyToClipboard as a function', () => { + assert.equal(typeof copyToClipboard, 'function'); + }); +}); diff --git a/plugins/linkedin-thought-leadership/hooks/scripts/clipboard-helper.mjs b/plugins/linkedin-thought-leadership/hooks/scripts/clipboard-helper.mjs new file mode 100644 index 0000000..82a0e9e --- /dev/null +++ b/plugins/linkedin-thought-leadership/hooks/scripts/clipboard-helper.mjs @@ -0,0 +1,102 @@ +#!/usr/bin/env node +// Cross-platform clipboard helper for linkedin-thought-leadership plugin +// Copies text to system clipboard using platform-native commands. +// Standalone: reads stdin and copies it. Import: export { copyToClipboard, clipboardAvailable } + +import { execSync } from 'node:child_process'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const PLATFORM_COMMANDS = { + darwin: { copy: 'pbcopy', check: 'which pbcopy' }, + win32: { copy: 'clip', check: 'where clip' }, + linux: { copy: 'xclip -selection clipboard', check: 'which xclip' }, +}; + +const LINUX_FALLBACK = { copy: 'xsel --clipboard --input', check: 'which xsel' }; + +/** + * Check if clipboard is available on this platform. + * @returns {{ available: boolean, platform: string }} + */ +export function clipboardAvailable() { + const platform = process.platform; + const commands = PLATFORM_COMMANDS[platform]; + + if (!commands) { + return { available: false, platform }; + } + + try { + execSync(commands.check, { stdio: 'ignore' }); + return { available: true, platform }; + } catch { + // Linux fallback: try xsel if xclip not found + if (platform === 'linux') { + try { + execSync(LINUX_FALLBACK.check, { stdio: 'ignore' }); + return { available: true, platform }; + } catch { + return { available: false, platform }; + } + } + return { available: false, platform }; + } +} + +/** + * Copy text to the system clipboard. + * Never throws — always returns a result object. + * @param {string} text - The text to copy + * @returns {{ success: boolean, platform: string }} + */ +export function copyToClipboard(text) { + const platform = process.platform; + + if (typeof text !== 'string') { + return { success: false, platform }; + } + + const commands = PLATFORM_COMMANDS[platform]; + if (!commands) { + return { success: false, platform }; + } + + // Determine which copy command to use + let copyCmd = commands.copy; + if (platform === 'linux') { + try { + execSync(commands.check, { stdio: 'ignore' }); + } catch { + try { + execSync(LINUX_FALLBACK.check, { stdio: 'ignore' }); + copyCmd = LINUX_FALLBACK.copy; + } catch { + return { success: false, platform }; + } + } + } + + try { + execSync(copyCmd, { input: text, stdio: ['pipe', 'ignore', 'ignore'] }); + return { success: true, platform }; + } catch { + return { success: false, platform }; + } +} + +// Standalone execution: read stdin and copy +if (process.argv[1] === fileURLToPath(import.meta.url)) { + let input = ''; + process.stdin.setEncoding('utf-8'); + process.stdin.on('data', (chunk) => { input += chunk; }); + process.stdin.on('end', () => { + const result = copyToClipboard(input); + if (result.success) { + process.stdout.write('COPIED\n'); + } else { + process.stdout.write(`FAILED:${result.platform}\n`); + process.exitCode = 1; + } + }); +}