mirror of
https://github.com/tiennm99/miti99.git
synced 2026-10-03 09:13:41 +00:00
refactor(skills): convert shell scripts to Node.js
Replace bash scripts with cross-platform Node.js equivalents for mt-add-post and mt-add-tags skills. Update SKILL.md references.
This commit is contained in:
1 parent
17222538a5
commit
134e3a5ff4
8 files changed
+248
-184
No files matched your search
@@ -30,7 +30,7 @@ This skill manages the complete workflow for adding newsletter posts to a Hugo b
|
||||
|
||||
For each URL, run the bundled script:
|
||||
```bash
|
||||
.claude/skills/mt-add-post/scripts/prepare_url.sh "<url>"
|
||||
node .claude/skills/mt-add-post/scripts/prepare-url.js "<url>"
|
||||
```
|
||||
|
||||
The script handles:
|
||||
@@ -53,7 +53,7 @@ Check if `content/post/YYYY/MM/DD/index.md` exists:
|
||||
|
||||
Run the bundled script:
|
||||
```bash
|
||||
.claude/skills/mt-add-post/scripts/find_newsletter_number.sh
|
||||
node .claude/skills/mt-add-post/scripts/find-newsletter-number.js
|
||||
```
|
||||
|
||||
This searches backwards from current date to find the most recent newsletter and returns the next number.
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
// Find the most recent newsletter number and return the next one
|
||||
// Usage: node find-newsletter-number.js
|
||||
// Outputs: The next newsletter number
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../../../..");
|
||||
const CONTENT_DIR = path.join(PROJECT_ROOT, "content", "post");
|
||||
|
||||
// Extract newsletter number from file content
|
||||
function extractNewsletterNumber(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const match = content.match(/Newsletter\s*#(\d+)/);
|
||||
return match ? parseInt(match[1], 10) : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Get current date in Asia/Ho_Chi_Minh timezone
|
||||
function getCurrentDate() {
|
||||
const now = new Date();
|
||||
const formatter = new Intl.DateTimeFormat("en-CA", {
|
||||
timeZone: "Asia/Ho_Chi_Minh",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
});
|
||||
const parts = formatter.formatToParts(now);
|
||||
return {
|
||||
year: parts.find((p) => p.type === "year").value,
|
||||
month: parts.find((p) => p.type === "month").value,
|
||||
day: parts.find((p) => p.type === "day").value,
|
||||
};
|
||||
}
|
||||
|
||||
// Scan all year/month/day directories for newsletter posts
|
||||
function findMostRecentNewsletter() {
|
||||
let maxNumber = 0;
|
||||
|
||||
// List year directories, sorted descending
|
||||
let years;
|
||||
try {
|
||||
years = fs.readdirSync(CONTENT_DIR)
|
||||
.filter((d) => /^\d{4}$/.test(d))
|
||||
.sort((a, b) => b.localeCompare(a));
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const year of years) {
|
||||
const yearDir = path.join(CONTENT_DIR, year);
|
||||
let months;
|
||||
try {
|
||||
months = fs.readdirSync(yearDir)
|
||||
.filter((d) => /^\d{2}$/.test(d))
|
||||
.sort((a, b) => b.localeCompare(a));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const month of months) {
|
||||
const monthDir = path.join(yearDir, month);
|
||||
let days;
|
||||
try {
|
||||
days = fs.readdirSync(monthDir)
|
||||
.filter((d) => /^\d{2}$/.test(d))
|
||||
.sort((a, b) => b.localeCompare(a));
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const day of days) {
|
||||
const filePath = path.join(monthDir, day, "index.md");
|
||||
const num = extractNewsletterNumber(filePath);
|
||||
if (num > maxNumber) maxNumber = num;
|
||||
}
|
||||
}
|
||||
|
||||
// Early exit: if we found a newsletter in this year, no need to go further back
|
||||
if (maxNumber > 0) break;
|
||||
}
|
||||
|
||||
return maxNumber;
|
||||
}
|
||||
|
||||
const mostRecent = findMostRecentNewsletter();
|
||||
console.log(mostRecent + 1);
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Find the most recent newsletter number and return the next one
|
||||
# Usage: find_newsletter_number.sh
|
||||
# Outputs: The next newsletter number
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Get project root
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||
CONTENT_DIR="$PROJECT_ROOT/content/post"
|
||||
|
||||
# Get current date in YYYY/MM/DD format
|
||||
CURRENT_YEAR=$(TZ='Asia/Ho_Chi_Minh' date +%Y)
|
||||
CURRENT_MONTH=$(TZ='Asia/Ho_Chi_Minh' date +%m)
|
||||
CURRENT_DAY=$(TZ='Asia/Ho_Chi_Minh' date +%d)
|
||||
|
||||
# Function to find newsletter number in a file
|
||||
extract_newsletter_number() {
|
||||
local file="$1"
|
||||
# Look for "Newsletter #N" in front matter title or content
|
||||
grep -oP 'Newsletter\s*#\K\d+' "$file" 2>/dev/null | head -1 || echo "0"
|
||||
}
|
||||
|
||||
# Function to search for newsletters in date order
|
||||
find_most_recent_newsletter() {
|
||||
local max_number=0
|
||||
|
||||
# Current month, going backwards by day
|
||||
for day in {31..1}; do
|
||||
local day_dir=$(printf "%02d" "$day")
|
||||
local check_path="$CONTENT_DIR/$CURRENT_YEAR/$CURRENT_MONTH/$day_dir/index.md"
|
||||
if [[ -f "$check_path" ]]; then
|
||||
local num=$(extract_newsletter_number "$check_path")
|
||||
if ((num > max_number)); then
|
||||
max_number=$num
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
# Previous months in current year
|
||||
for month in {12..1}; do
|
||||
if [[ $month -ge $CURRENT_MONTH ]]; then
|
||||
continue
|
||||
fi
|
||||
local month_dir=$(printf "%02d" "$month")
|
||||
for day in {31..1}; do
|
||||
local day_dir=$(printf "%02d" "$day")
|
||||
local check_path="$CONTENT_DIR/$CURRENT_YEAR/$month_dir/$day_dir/index.md"
|
||||
if [[ -f "$check_path" ]]; then
|
||||
local num=$(extract_newsletter_number "$check_path")
|
||||
if ((num > max_number)); then
|
||||
max_number=$num
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
# Previous years (check last 5 years)
|
||||
for year in $(seq $((CURRENT_YEAR - 1)) -1 $((CURRENT_YEAR - 5))); do
|
||||
for month in {12..1}; do
|
||||
local month_dir=$(printf "%02d" "$month")
|
||||
for day in {31..1}; do
|
||||
local day_dir=$(printf "%02d" "$day")
|
||||
local check_path="$CONTENT_DIR/$year/$month_dir/$day_dir/index.md"
|
||||
if [[ -f "$check_path" ]]; then
|
||||
local num=$(extract_newsletter_number "$check_path")
|
||||
if ((num > max_number)); then
|
||||
max_number=$num
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done
|
||||
done
|
||||
|
||||
echo $max_number
|
||||
}
|
||||
|
||||
# Find most recent and increment
|
||||
MOST_RECENT=$(find_most_recent_newsletter)
|
||||
NEXT_NUMBER=$((MOST_RECENT + 1))
|
||||
|
||||
echo "$NEXT_NUMBER"
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env node
|
||||
// URL preparation script for mt-add-post skill
|
||||
// Usage: node prepare-url.js "<url>"
|
||||
// Outputs: JSON with status, clean_url, type, accessible, duplicate
|
||||
|
||||
const { execSync } = require("child_process");
|
||||
const path = require("path");
|
||||
|
||||
const url = process.argv[2];
|
||||
if (!url) {
|
||||
console.error("Usage: node prepare-url.js <url>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../../../..");
|
||||
|
||||
// Remove common tracking parameters
|
||||
function cleanUrl(rawUrl) {
|
||||
try {
|
||||
const parsed = new URL(rawUrl);
|
||||
const trackingParams = [
|
||||
"utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content",
|
||||
"fbclid", "gclid", "msclkid", "mc_eid",
|
||||
];
|
||||
trackingParams.forEach((p) => parsed.searchParams.delete(p));
|
||||
return parsed.toString();
|
||||
} catch {
|
||||
// If URL parsing fails, do basic string cleanup
|
||||
return rawUrl.replace(/[?&](utm_[^&]*|fbclid|gclid|msclkid|mc_eid)[^&]*/g, "")
|
||||
.replace(/[?&]$/, "");
|
||||
}
|
||||
}
|
||||
|
||||
// Check if URL is accessible (returns HTTP status code)
|
||||
async function checkAccessibility(targetUrl) {
|
||||
try {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 10000);
|
||||
const res = await fetch(targetUrl, {
|
||||
method: "HEAD",
|
||||
redirect: "follow",
|
||||
signal: controller.signal,
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
return res.status.toString();
|
||||
} catch {
|
||||
return "000";
|
||||
}
|
||||
}
|
||||
|
||||
// Check if URL already exists in project content
|
||||
function checkDuplicate(targetUrl) {
|
||||
try {
|
||||
const contentDir = path.join(PROJECT_ROOT, "content");
|
||||
execSync(`grep -rF "${targetUrl}" "${contentDir}"`, { stdio: "pipe" });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Classify URL type by extension
|
||||
function classifyUrl(targetUrl) {
|
||||
const lower = targetUrl.toLowerCase();
|
||||
if (/\.(png|jpg|jpeg|gif|webp|svg)(\?.*)?$/.test(lower)) return "image";
|
||||
if (/\.(mp4|webm|mov|avi)(\?.*)?$/.test(lower)) return "video";
|
||||
if (/\.(pdf|doc|docx|xls|xlsx)(\?.*)?$/.test(lower)) return "document";
|
||||
return "article";
|
||||
}
|
||||
|
||||
// Main
|
||||
async function main() {
|
||||
const cleanedUrl = cleanUrl(url);
|
||||
const httpStatus = await checkAccessibility(cleanedUrl);
|
||||
const accessible = httpStatus === "200";
|
||||
const duplicate = checkDuplicate(cleanedUrl);
|
||||
const type = classifyUrl(cleanedUrl);
|
||||
|
||||
console.log(JSON.stringify({
|
||||
original_url: url,
|
||||
clean_url: cleanedUrl,
|
||||
http_status: httpStatus,
|
||||
accessible,
|
||||
duplicate,
|
||||
type,
|
||||
}, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -1,78 +0,0 @@
|
||||
#!/bin/bash
|
||||
# URL preparation script for miti-add-post skill
|
||||
# Usage: prepare_url.sh "<url>"
|
||||
# Outputs: JSON with status, clean_url, type, accessible, duplicate
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
URL="$1"
|
||||
|
||||
# Remove tracking parameters
|
||||
clean_url() {
|
||||
local url="$1"
|
||||
# Remove common tracking parameters
|
||||
url=$(echo "$url" | sed -E 's/[?&](utm_[^&]*|fbclid|gclid|msclkid|mc_eid)[^&]*//g')
|
||||
# Clean up trailing ? or &
|
||||
url=$(echo "$url" | sed -E 's/[?&]$//')
|
||||
echo "$url"
|
||||
}
|
||||
|
||||
# Check if URL is accessible (returns HTTP status code)
|
||||
check_accessibility() {
|
||||
local url="$1"
|
||||
# Use curl to get HTTP status only
|
||||
curl -s -o /dev/null -w "%{http_code}" -L --max-time 10 "$url" 2>/dev/null || echo "000"
|
||||
}
|
||||
|
||||
# Check if URL is duplicate in project
|
||||
check_duplicate() {
|
||||
local url="$1"
|
||||
local project_root="$2"
|
||||
if grep -rF "$url" "$project_root/content" 2>/dev/null; then
|
||||
echo "true"
|
||||
else
|
||||
echo "false"
|
||||
fi
|
||||
}
|
||||
|
||||
# Classify URL type
|
||||
classify_url() {
|
||||
local url="$1"
|
||||
local lower_url=$(echo "$url" | tr '[:upper:]' '[:lower:]')
|
||||
|
||||
# Check for direct assets
|
||||
if echo "$lower_url" | grep -qE '\.(png|jpg|jpeg|gif|webp|svg)(\?.*)?$'; then
|
||||
echo "image"
|
||||
elif echo "$lower_url" | grep -qE '\.(mp4|webm|mov|avi)(\?.*)?$'; then
|
||||
echo "video"
|
||||
elif echo "$lower_url" | grep -qE '\.(pdf|doc|docx|xls|xlsx)(\?.*)?$'; then
|
||||
echo "document"
|
||||
else
|
||||
echo "article"
|
||||
fi
|
||||
}
|
||||
|
||||
# Main execution
|
||||
CLEAN_URL=$(clean_url "$URL")
|
||||
HTTP_STATUS=$(check_accessibility "$CLEAN_URL")
|
||||
IS_ACCESSIBLE="false"
|
||||
if [ "$HTTP_STATUS" = "200" ]; then
|
||||
IS_ACCESSIBLE="true"
|
||||
fi
|
||||
|
||||
# Get project root (assumes we're in .claude/skills/miti-add-post/scripts)
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||
IS_DUPLICATE=$(check_duplicate "$CLEAN_URL" "$PROJECT_ROOT")
|
||||
URL_TYPE=$(classify_url "$CLEAN_URL")
|
||||
|
||||
# Output JSON
|
||||
cat <<EOF
|
||||
{
|
||||
"original_url": "$URL",
|
||||
"clean_url": "$CLEAN_URL",
|
||||
"http_status": "$HTTP_STATUS",
|
||||
"accessible": $IS_ACCESSIBLE,
|
||||
"duplicate": $IS_DUPLICATE,
|
||||
"type": "$URL_TYPE"
|
||||
}
|
||||
EOF
|
||||
@@ -49,7 +49,7 @@ Read the full post body. Identify:
|
||||
|
||||
Before generating, run:
|
||||
```bash
|
||||
grep -rh "^tags:" content/post/ | tr ',' '\n' | grep -oP '(?<=")[^"]+(?=")' | sort | uniq -c | sort -rn | head -40
|
||||
node .claude/skills/mt-add-tags/scripts/list-existing-tags.js
|
||||
```
|
||||
When a proposed tag matches an existing one case-insensitively, use the existing casing.
|
||||
-->
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env node
|
||||
// List existing tags in the repo ranked by frequency
|
||||
// Usage: node list-existing-tags.js
|
||||
// Outputs: tag count and name, sorted most-used first (top 40)
|
||||
//
|
||||
// NOTE: This script is NOT currently used by the mt-add-tags skill.
|
||||
// Tag normalization is disabled until existing posts have standardized tags.
|
||||
// To enable, uncomment step 4a in SKILL.md.
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../../../..");
|
||||
const CONTENT_DIR = path.join(PROJECT_ROOT, "content", "post");
|
||||
|
||||
// Recursively find all index.md files
|
||||
function findIndexFiles(dir) {
|
||||
const results = [];
|
||||
try {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
results.push(...findIndexFiles(fullPath));
|
||||
} else if (entry.name === "index.md") {
|
||||
results.push(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// skip unreadable directories
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Extract tags from frontmatter
|
||||
function extractTags(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, "utf-8");
|
||||
const match = content.match(/^tags:\s*\[([^\]]*)\]/m);
|
||||
if (!match) return [];
|
||||
// Extract quoted strings from the tags array
|
||||
return [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Count tag frequency
|
||||
const tagCounts = new Map();
|
||||
const files = findIndexFiles(CONTENT_DIR);
|
||||
|
||||
for (const file of files) {
|
||||
for (const tag of extractTags(file)) {
|
||||
tagCounts.set(tag, (tagCounts.get(tag) || 0) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by frequency descending, take top 40
|
||||
const sorted = [...tagCounts.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 40);
|
||||
|
||||
for (const [tag, count] of sorted) {
|
||||
console.log(`${String(count).padStart(6)} ${tag}`);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#!/bin/bash
|
||||
# List existing tags in the repo ranked by frequency.
|
||||
# Usage: list_existing_tags.sh
|
||||
# Outputs: tag count and name, sorted most-used first (top 40)
|
||||
#
|
||||
# NOTE: This script is NOT currently used by the mt-add-tags skill.
|
||||
# Tag normalization is disabled until existing posts have standardized tags.
|
||||
# To enable, uncomment step 4a in SKILL.md.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../.." && pwd)"
|
||||
CONTENT_DIR="$PROJECT_ROOT/content/post"
|
||||
|
||||
grep -rh "^tags:" "$CONTENT_DIR" \
|
||||
| tr ',' '\n' \
|
||||
| grep -oP '(?<=")[^"]+(?=")' \
|
||||
| sort \
|
||||
| uniq -c \
|
||||
| sort -rn \
|
||||
| head -40
|
||||
Reference in new issue
Block a user