feat(linkedin): add clipboard-helper.mjs — cross-platform clipboard utility
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 <noreply@anthropic.com>
This commit is contained in:
parent
2c33e9cc64
commit
214675d0a0
2 changed files with 188 additions and 0 deletions
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue