يجب أن يعمل جهاز 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, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
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
- اكتب الـ cover letter في Word (أو Google Docs ثم File → Download → Microsoft Word (.docx)).
- حيث تريد اسم الشركة، اكتب COMPANY_NAME (بدون أقواس معقوفة).
- حيث تريد المسمى الوظيفي، اكتب ROLE_NAME.
- حيث تريد التاريخ، اكتب NEW_DATE.
- احفظ الملف وضعه حيث يتوقعه
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
- افتح Raycast → Settings → Extensions.
- انقر Add Script Directory واختر
~/raycast-scripts/cover-letter(أو المجلد الذي فيهcover-letter-local.shوgenerate-cover-letter.mjs). - سيكتشف Raycast السكربت من تعليقات
@raycast.*. يفترض أن ترى Generate Cover Letter تحت الحزمة cover-letter. - شغّله: أدخل الشركة والمنصب واضغط Enter. السكربت يولّد DOCX المملوء، يصدّره إلى PDF عبر Pages، ويحفظه في
~/Downloads/Cover Letters.
أصبح لديك تدفق واحد: الشركة + المنصب في Raycast → cover letter PDF جاهز على جهازك، دون بحث واستبدال أو تصدير يدوي.
أتمنى أن تحصل على الوظيفة!