كيفية أتمتة إنشاء Cover Letter بامتداد Raycast مخصص (التصدير إلى PDF)

استخدم سكربت Bash وNode.js (docxtemplater) لملء قالب Word بالشركة والمنصب والتاريخ، ثم صدّر إلى PDF عبر Pages، كل ذلك من Raycast.

يجب أن يعمل جهاز MacBook لديك لصالحك وليس العكس. لذلك من الضروري استغلال الإمكانيات المتوفرة بين يديك محلياً عبر أتمتة مهامك وسير العمل المتكررة في أسرع وقت ممكن.

في هذه الحالة، سنُؤتمت إنشاء cover letter مخصصة ترسلها للشركات مع سيرتك الذاتية. بما أن الـ cover letter يُخصّص لشركة ومنصب وتاريخ معيّنين، فإن عملية نسخ الـ cover letter وتعديل تلك الحقول ثم حفظ الملف كـ PDF تصبح طويلة عندما تتقدم لعشرات الوظائف.

نظرة عامة

سننشئ سكربت Bash وسكربت JavaScript لتخصيص الـ cover letter وتوليده كـ PDF. ثم نضيف هذا السكربت إلى Raycast ليُستدعى من واجهتهم الرسومية.

ملاحظة: نضطر لاستخدام سكربت JS لأنني عند تحديث ملف .docx بأدوات مضمنة فقط، إما فشل تصدير الـ PDF أو اختفت كل تنسيقات المستند الأصلية.

متطلبات الإعداد

  • تثبيت Raycast.
  • تثبيت NodeJS.
  • Cover letter بصيغة Word أو Google Docs. إن استخدمت Google Docs، حمّله محلياً كـ Microsoft Word (.docx).

الإعداد

لإبقاء كل شيء منظماً وجاهزاً للعمل لاحقاً، سننشئ مجلداً في المجلد الجذري باسم raycast-scripts/cover-letter ثم ننتقل إليه.

cd ~
mkdir raycast-scripts/cover-letter
cd raycast-scripts/cover-letter

ثم نحتاج تثبيت الحزم المطلوبة للتعديل على ملف docx في الخطوة التالية.

npm i pizzip docxtemplater

إنشاء السكربتات

أنشئ ملفين: cover-letter-local.sh و generate-cover-letter.mjs. اجعل سكربت الشيل قابلاً للتنفيذ:

chmod +x cover-letter-local.sh

1. cover-letter-local.sh

هذا السكربت هو نقطة الدخول التي سيستدعيها Raycast. يستقبل الشركة والمنصب كمعاملات، يحدد موقع قالب DOCX، يشغّل سكربت Node لملء العناصر النائبة، ثم يستخدم Pages (عبر AppleScript) لتصدير المستند المملوء إلى PDF.

#!/bin/bash

# @raycast.schemaVersion 1
# @raycast.title Generate Cover Letter
# @raycast.mode compact
# @raycast.packageName cover-letter
# @raycast.icon 📄
# @raycast.argument1 { "type": "text", "placeholder": "Company (e.g. Acme)" }
# @raycast.argument2 { "type": "text", "placeholder": "Role (e.g. Backend Engineer)" }

set -euo pipefail

COMPANY_NAME="$1"
ROLE_NAME="$2"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
TEMPLATE_DOCX="$SCRIPT_DIR/JobSearch/YOUR_COVER_LETTER_FILE_NAME.docx"
OUTDIR="$HOME/Downloads/Cover Letters"
mkdir -p "$OUTDIR"

NEW_DATE="$(LC_TIME=en_US_POSIX date '+%b %e, %Y' | sed 's/  / /g')"

sanitize() {
  echo "$1" \
    | tr '\n' ' ' \
    | sed -E 's/[\/:*?"<>|]/-/g; s/[[:space:]]+/ /g; s/^[[:space:]]+//; s/[[:space:]]+$//'
}

SAFE_COMPANY="$(sanitize "$COMPANY_NAME")"
BASENAME="YOUR_NAME_${SAFE_COMPANY}_Cover Letter"

TMPDOCX="/tmp/${BASENAME}.docx"
PDFPATH="${OUTDIR}/${BASENAME}.pdf"

if [[ ! -f "$TEMPLATE_DOCX" ]]; then
  echo "DOCX template not found: $TEMPLATE_DOCX"
  exit 1
fi

if [[ ! -d "$SCRIPT_DIR/node_modules" ]]; then
  echo "Run once in this folder: npm install"
  exit 1
fi

NODE=""
if command -v node &>/dev/null; then
  NODE="node"
elif [[ -x "$HOME/.nvm/current/bin/node" ]]; then
  NODE="$HOME/.nvm/current/bin/node"
elif [[ -x /opt/homebrew/bin/node ]]; then
  NODE="/opt/homebrew/bin/node"
elif [[ -x /usr/local/bin/node ]]; then
  NODE="/usr/local/bin/node"
fi
if [[ -z "$NODE" ]]; then
  echo "node not found. Install Node.js or add it to PATH."
  exit 1
fi

"$NODE" "$SCRIPT_DIR/generate-cover-letter.mjs" \
  "$TEMPLATE_DOCX" "$TMPDOCX" "$COMPANY_NAME" "$ROLE_NAME" "$NEW_DATE"

osascript - "$TMPDOCX" "$PDFPATH" <<'APPLESCRIPT'
on run argv
  set docxPosix to item 1 of argv
  set pdfPosix to item 2 of argv
  set docxAlias to (POSIX file docxPosix) as alias
  set pdfFile to POSIX file pdfPosix
  tell application "Pages"
    activate
    open docxAlias
    delay 0.8
    tell front document
      export it to pdfFile as PDF
      close saving no
    end tell
    quit
  end tell
end run
APPLESCRIPT

rm -f "$TMPDOCX"
echo "Saved: $PDFPATH"

ماذا يفعل:

  • بيانات Raycast (تعليقات @raycast.*) تحدد اسم الأمر والأيقونة ومعاملين نصيين بحيث يعرض Raycast حقلي «الشركة» و«المنصب».
  • المسارات: TEMPLATE_DOCX يستخدم YOUR_COVER_LETTER_FILE_NAME.docx داخل مجلد فرعي JobSearch/. استبدل YOUR_COVER_LETTER_FILE_NAME باسم ملف DOCX الفعلي (بدون المسار). تُكتب ملفات PDF في ~/Downloads/Cover Letters.
  • التاريخ: يستخدم التاريخ الحالي بصيغة مثل Feb 11, 2025.
  • Sanitize: ينظّف اسم الشركة لاستخدامه في اسم الملف (يزيل أو يستبدل الأحرف غير المسموحة في أسماء الملفات).
  • التسمية: BASENAME يستخدم YOUR_NAME_ ليكون شكل الملفات مثل YOUR_NAME_Acme_Cover Letter.pdf. استبدل YOUR_NAME_ باسمك أو أحرفك الأولى.
  • البحث عن Node: يجرّب node في PATH ثم المواقع الشائعة (nvm، Homebrew)، حتى يعمل السكربت عند تشغيله من Raycast (حيث قد يكون PATH محدوداً).
  • AppleScript: يفتح ملف DOCX المملوء في Pages، يصدّره إلى PDF، يغلق المستند دون حفظ، ثم يغلق Pages. يُحذف ملف DOCX المؤقت.

2. generate-cover-letter.mjs

سكربت Node هذا يملأ قالب DOCX باستبدال العناصر النائبة النصية داخل XML المستند. لا يستخدم صيغة {COMPANY_NAME}، بل الكلمات COMPANY_NAME و ROLE_NAME و NEW_DATE فقط في ملف Word.

#!/usr/bin/env node
/**
 * Usage: node generate-cover-letter.mjs <template.docx> <output.docx> <company> <role> <date>
 */

import PizZip from "pizzip";
import fs from "fs";
import path from "path";

const [templatePath, outputPath, companyName, roleName, newDate] =
  process.argv.slice(2);

const TEMPLATE = templatePath || process.env.TEMPLATE_PATH;
const OUTPUT = outputPath || process.env.OUTPUT_PATH;
const COMPANY = companyName || process.env.COMPANY_NAME || "";
const ROLE = roleName || process.env.ROLE_NAME || "";
const DATE = newDate || process.env.NEW_DATE || "";

if (!TEMPLATE || !OUTPUT) {
  console.error(
    "Usage: node generate-cover-letter.mjs <template.docx> <output.docx> <company> <role> <date>",
  );
  process.exit(1);
}

const templateAbs = path.resolve(TEMPLATE);
const outputAbs = path.resolve(OUTPUT);

if (!fs.existsSync(templateAbs)) {
  console.error("Template not found:", templateAbs);
  process.exit(1);
}

const content = fs.readFileSync(templateAbs, "binary");
const zip = new PizZip(content);

const docPath = "word/document.xml";
const docFile = zip.files[docPath];
if (!docFile) {
  console.error("DOCX invalid: missing word/document.xml");
  process.exit(1);
}

let xml = docFile.asText();
const replacements = [
  ["COMPANY_NAME", COMPANY],
  ["ROLE_NAME", ROLE],
  ["NEW_DATE", DATE],
];

for (const [placeholder, value] of replacements) {
  const escaped = String(value)
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
  xml = xml.split(placeholder).join(escaped);
}

zip.file(docPath, xml);

const buf = zip.generate({ type: "nodebuffer" });
fs.writeFileSync(outputAbs, buf);

console.log(outputAbs);

ماذا يفعل:

  • يقرأ DOCX كأرشيف ZIP، يعدّل word/document.xml ويكتب DOCX جديداً. كل التنسيق (الأنماط، الخطوط، التخطيط) يُحفظ لأن ما يتغيّر هو العقد النصية فقط.
  • يستبدل COMPANY_NAME و ROLE_NAME و NEW_DATE بالقيم المُمرَّرة (أو من المتغيرات البيئية). القيم تُهرب لـ XML حتى لا تكسر أحرف مثل & أو < الملف.

3. تجهيز قالب DOCX

  1. اكتب الـ cover letter في Word (أو Google Docs ثم File → Download → Microsoft Word (.docx)).
  2. حيث تريد اسم الشركة، اكتب COMPANY_NAME (بدون أقواس معقوفة).
  3. حيث تريد المسمى الوظيفي، اكتب ROLE_NAME.
  4. حيث تريد التاريخ، اكتب NEW_DATE.
  5. احفظ الملف وضعه حيث يتوقعه cover-letter-local.sh، مثلاً raycast-scripts/cover-letter/JobSearch/YOUR_COVER_LETTER_FILE_NAME.docx. في السكربت استبدل YOUR_COVER_LETTER_FILE_NAME باسم ملف DOCX الفعلي و YOUR_NAME_ باسمك (أو أحرفك الأولى) لتحصل على أسماء الملفات المصدرة التي تريدها.

4. إضافة السكربت إلى Raycast

  1. افتح Raycast → Settings → Extensions.
  2. انقر Add Script Directory واختر ~/raycast-scripts/cover-letter (أو المجلد الذي فيه cover-letter-local.sh و generate-cover-letter.mjs).
  3. سيكتشف Raycast السكربت من تعليقات @raycast.*. يفترض أن ترى Generate Cover Letter تحت الحزمة cover-letter.
  4. شغّله: أدخل الشركة والمنصب واضغط Enter. السكربت يولّد DOCX المملوء، يصدّره إلى PDF عبر Pages، ويحفظه في ~/Downloads/Cover Letters.

أصبح لديك تدفق واحد: الشركة + المنصب في Raycast → cover letter PDF جاهز على جهازك، دون بحث واستبدال أو تصدير يدوي.

أتمنى أن تحصل على الوظيفة!