#!/usr/bin/env bun import { execFileSync, spawnSync } from "child_process"; import * as fs from "fs"; import * as path from "path"; import { parseArgs } from "util"; import { randomUUID } from "crypto"; // Path validation to prevent command injection const DANGEROUS_PATH_CHARS = /[;&|`$(){}[\]<>\\!*?'"]/; function validatePath(inputPath: string): void { if (DANGEROUS_PATH_CHARS.test(inputPath)) { throw new Error(`Invalid characters in path: ${inputPath}. Paths cannot contain shell metacharacters.`); } // Resolve to absolute path and check for path traversal const resolved = path.resolve(inputPath); if (resolved.includes("..")) { throw new Error(`Path traversal detected in: ${inputPath}`); } } // Constants const SKILL_NAME = "extract-frames"; const SESSION_ID = randomUUID().slice(0, 8); const SKILLS_OUTPUT_DIR = process.env.SKILLS_OUTPUT_DIR || path.join(process.cwd(), ".skills"); const LOGS_DIR = path.join(SKILLS_OUTPUT_DIR, "logs", SKILL_NAME); const SESSION_TIMESTAMP = new Date().toISOString().replace(/[:.]/g, "_").slice(0, 19); // Ensure directories exist function ensureDir(dir: string) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } // Logger function log(message: string, level: "info" | "error" | "success" | "debug" = "info") { ensureDir(LOGS_DIR); const timestamp = new Date().toISOString(); const logFile = path.join(LOGS_DIR, `log_${SESSION_TIMESTAMP}_${SESSION_ID}.log`); const logEntry = `[${timestamp}] [${level.toUpperCase()}] ${message}\n`; fs.appendFileSync(logFile, logEntry); const prefix = level === "error" ? "❌" : level === "success" ? "✅" : level === "debug" ? "🔍" : "â„šī¸"; if (level === "error") { console.error(`${prefix} ${message}`); } else if (level !== "debug") { console.log(`${prefix} ${message}`); } } // Types interface ExtractOptions { output: string; interval: number | null; fps: number | null; timestamps: string | null; frames: string | null; start: number; end: number | null; duration: number | null; first: boolean; last: boolean; scene: boolean; sceneThreshold: number; format: "jpg" | "png" | "webp"; quality: number; width: number | null; height: number | null; scale: number; naming: string; padDigits: number; } // Parse command line arguments const { values, positionals } = parseArgs({ args: process.argv.slice(2), options: { output: { type: "string", short: "o", default: "./frames/" }, interval: { type: "string" }, fps: { type: "string" }, timestamps: { type: "string" }, frames: { type: "string" }, start: { type: "string", default: "0" }, end: { type: "string" }, duration: { type: "string" }, first: { type: "boolean", default: false }, last: { type: "boolean", default: false }, scene: { type: "boolean", default: false }, "scene-threshold": { type: "string", default: "0.3" }, format: { type: "string", default: "jpg" }, quality: { type: "string", default: "90" }, width: { type: "string" }, height: { type: "string" }, scale: { type: "string", default: "1.0" }, naming: { type: "string", default: "frame-{n}" }, "pad-digits": { type: "string", default: "5" }, help: { type: "boolean", short: "h" }, }, allowPositionals: true, }); // Show help if (values.help || positionals.length === 0) { console.log(` Extract Frames - Extract frames from videos Usage: skills run extract-frames --