Bun Spawn -> node child process in tools and seed versioning

This commit is contained in:
2026-02-25 02:15:54 +00:00
parent 21fde3d989
commit f2da4c0300
8 changed files with 107 additions and 74 deletions
+9 -6
View File
@@ -1,7 +1,7 @@
--- ---
name: Convert To MP3 name: Convert To MP3
description: Convert audio files to MP3 320kbps, preserving metadata. description: Convert audio files to MP3 320kbps, preserving metadata.
version: 2 version: 3
author: pastilhas author: pastilhas
tags: tags:
- audio - audio
@@ -38,10 +38,13 @@ inputs:
Convert audio files to MP3 320kbps, preserving metadata. Convert audio files to MP3 320kbps, preserving metadata.
## Important
- Do NOT explore, list, or inspect the target path before converting. The tool handles everything internally — file discovery, format detection, and error reporting.
- Do NOT use bash, ls, or any other tool. Only use `convert_audio_to_mp3`.
- Call the tool exactly once, then report the result. Nothing else.
## Steps ## Steps
1. Determine whether `file_path` points to a single audio file or a directory. 1. If `file_path` has an audio extension (flac, wav, ogg, etc.), call `convert_audio_to_mp3(mode="single", path=file_path)`. Otherwise call `convert_audio_to_mp3(mode="batch", path=file_path)`.
2. Call `convert_audio_to_mp3` with the appropriate mode: 2. Print the tool's output as the final report. Do not add extra commentary.
- Single file: `mode="single"`, `path=file_path`
- Directory: `mode="batch"`, `path=file_path`
3. Report the result to the user.
+1
View File
@@ -2,6 +2,7 @@
name: convert_audio_to_mp3 name: convert_audio_to_mp3
label: Convert Audio to MP3 label: Convert Audio to MP3
description: Convert audio files to MP3 320kbps using ffmpeg, preserving metadata. Supports single file conversion with real-time percentage progress, and batch conversion of an entire artist directory with per-file progress. Use when the user wants to convert FLAC, WAV, OGG, or other audio formats to MP3. description: Convert audio files to MP3 320kbps using ffmpeg, preserving metadata. Supports single file conversion with real-time percentage progress, and batch conversion of an entire artist directory with per-file progress. Use when the user wants to convert FLAC, WAV, OGG, or other audio formats to MP3.
version: 2
language: typescript language: typescript
inputs: inputs:
mode: mode:
+47 -48
View File
@@ -1,5 +1,6 @@
import { readdirSync, existsSync, unlinkSync, statSync } from 'node:fs'; import { readdirSync, existsSync, unlinkSync, statSync } from 'node:fs';
import { join, extname, basename } from 'node:path'; import { join, extname, basename } from 'node:path';
import { spawn, execFile } from 'node:child_process';
const AUDIO_EXTS = new Set([ const AUDIO_EXTS = new Set([
'flac', 'wav', 'ogg', 'wma', 'aac', 'm4a', 'opus', 'flac', 'wav', 'ogg', 'wma', 'aac', 'm4a', 'opus',
@@ -13,14 +14,17 @@ function update(onUpdate: OnUpdate | undefined, text: string): void {
} }
async function getDuration(filePath: string): Promise<number | null> { async function getDuration(filePath: string): Promise<number | null> {
const proc = Bun.spawn( return new Promise((resolve) => {
['ffprobe', '-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath], execFile(
{ stdout: 'pipe', stderr: 'pipe' }, 'ffprobe',
); ['-v', 'quiet', '-show_entries', 'format=duration', '-of', 'csv=p=0', filePath],
const text = await new Response(proc.stdout).text(); (err, stdout) => {
await proc.exited; if (err) return resolve(null);
const n = parseFloat(text.trim()); const n = parseFloat(stdout.trim());
return isNaN(n) ? null : n; resolve(isNaN(n) ? null : n);
},
);
});
} }
function scanAudioFiles(dir: string): string[] { function scanAudioFiles(dir: string): string[] {
@@ -57,60 +61,55 @@ async function convertFile(
return { outputFile, success: true, skipped: true }; return { outputFile, success: true, skipped: true };
} }
const proc = Bun.spawn( return new Promise((resolve) => {
[ const proc = spawn('ffmpeg', [
'ffmpeg',
'-i', inputFile, '-i', inputFile,
'-progress', 'pipe:1', // progress data → stdout '-progress', 'pipe:1',
'-nostats', '-nostats',
'-loglevel', 'error', // only errors → stderr '-loglevel', 'error',
'-codec:a', 'libmp3lame', '-codec:a', 'libmp3lame',
'-b:a', '320k', '-b:a', '320k',
outputFile, outputFile,
'-y', '-y',
], ], { stdio: ['ignore', 'pipe', 'pipe'] });
{ stdin: 'ignore', stdout: 'pipe', stderr: 'pipe' },
);
// Parse -progress output from stdout for real-time percentage let buf = '';
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader(); let lastPercent = -1;
const decoder = new TextDecoder(); let stderrText = '';
let buf = '';
let lastPercent = -1;
(async () => { proc.stdout.on('data', (chunk: Buffer) => {
try { buf += chunk.toString();
while (true) { const lines = buf.split('\n');
const { done, value } = await reader.read(); buf = lines.pop() ?? '';
if (done) break; for (const line of lines) {
buf += decoder.decode(value, { stream: true }); const m = line.match(/^out_time_us=(\d+)$/);
const lines = buf.split('\n'); if (m && duration) {
buf = lines.pop() ?? ''; const pct = Math.min(100, Math.round((parseInt(m[1]!, 10) / 1_000_000 / duration) * 100));
for (const line of lines) { if (pct >= lastPercent + 5) {
const m = line.match(/^out_time_us=(\d+)$/); lastPercent = pct;
if (m && duration) { update(onUpdate, `${label} ${pct}%`);
const pct = Math.min(100, Math.round((parseInt(m[1]!, 10) / 1_000_000 / duration) * 100));
if (pct >= lastPercent + 5) {
lastPercent = pct;
update(onUpdate, `${label} ${pct}%`);
}
} }
} }
} }
} catch { });
// stream closed
}
})();
const stderrText = await new Response(proc.stderr).text(); proc.stderr.on('data', (chunk: Buffer) => {
await proc.exited; stderrText += chunk.toString();
});
if (proc.exitCode !== 0) { proc.on('close', (code) => {
if (existsSync(outputFile)) unlinkSync(outputFile); if (code !== 0) {
return { outputFile, success: false, error: stderrText.trim() || 'ffmpeg failed' }; if (existsSync(outputFile)) unlinkSync(outputFile);
} resolve({ outputFile, success: false, error: stderrText.trim() || 'ffmpeg failed' });
} else {
resolve({ outputFile, success: true });
}
});
return { outputFile, success: true }; proc.on('error', (err) => {
resolve({ outputFile, success: false, error: err.message });
});
});
} }
export async function execute( export async function execute(
+10 -5
View File
@@ -1,6 +1,7 @@
import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { SEED_PATH, DATA_PATH } from './data-path'; import { SEED_PATH, DATA_PATH } from './data-path';
import { parseSeedVersion } from './sync-version';
const SEED_PROCESSES_DIR = join(SEED_PATH, 'processes'); const SEED_PROCESSES_DIR = join(SEED_PATH, 'processes');
const GLOBAL_PROCESSES_DIR = join(DATA_PATH, 'processes'); const GLOBAL_PROCESSES_DIR = join(DATA_PATH, 'processes');
@@ -16,14 +17,18 @@ export function syncSeedProcesses(): void {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
const seedProcessDir = join(SEED_PROCESSES_DIR, entry.name); const seedProcessDir = join(SEED_PROCESSES_DIR, entry.name);
const processFile = join(seedProcessDir, 'PROCESS.md'); const seedFile = join(seedProcessDir, 'PROCESS.md');
if (!existsSync(processFile)) continue; if (!existsSync(seedFile)) continue;
const targetDir = join(GLOBAL_PROCESSES_DIR, entry.name); const targetDir = join(GLOBAL_PROCESSES_DIR, entry.name);
const targetFile = join(targetDir, 'PROCESS.md');
if (existsSync(targetDir)) { if (existsSync(targetDir)) {
// Process already exists in DATA_PATH — skip to preserve user edits const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8'));
continue; const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0;
if (seedVersion <= targetVersion) continue;
rmSync(targetDir, { recursive: true });
console.log(`[processes] Updating seed process: ${entry.name} (v${targetVersion} → v${seedVersion})`);
} }
cpSync(seedProcessDir, targetDir, { recursive: true }); cpSync(seedProcessDir, targetDir, { recursive: true });
+10 -5
View File
@@ -1,6 +1,7 @@
import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { SEED_PATH, DATA_PATH } from './data-path'; import { SEED_PATH, DATA_PATH } from './data-path';
import { parseSeedVersion } from './sync-version';
const SEED_SKILLS_DIR = join(SEED_PATH, 'skills'); const SEED_SKILLS_DIR = join(SEED_PATH, 'skills');
const GLOBAL_SKILLS_DIR = join(DATA_PATH, 'skills'); const GLOBAL_SKILLS_DIR = join(DATA_PATH, 'skills');
@@ -16,14 +17,18 @@ export function syncSeedSkills(): void {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
const seedSkillDir = join(SEED_SKILLS_DIR, entry.name); const seedSkillDir = join(SEED_SKILLS_DIR, entry.name);
const skillFile = join(seedSkillDir, 'SKILL.md'); const seedFile = join(seedSkillDir, 'SKILL.md');
if (!existsSync(skillFile)) continue; if (!existsSync(seedFile)) continue;
const targetDir = join(GLOBAL_SKILLS_DIR, entry.name); const targetDir = join(GLOBAL_SKILLS_DIR, entry.name);
const targetFile = join(targetDir, 'SKILL.md');
if (existsSync(targetDir)) { if (existsSync(targetDir)) {
// Skill already exists in DATA_PATH — skip to preserve user edits const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8'));
continue; const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0;
if (seedVersion <= targetVersion) continue;
rmSync(targetDir, { recursive: true });
console.log(`[skills] Updating seed skill: ${entry.name} (v${targetVersion} → v${seedVersion})`);
} }
cpSync(seedSkillDir, targetDir, { recursive: true }); cpSync(seedSkillDir, targetDir, { recursive: true });
+10 -5
View File
@@ -1,6 +1,7 @@
import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { SEED_PATH, DATA_PATH } from './data-path'; import { SEED_PATH, DATA_PATH } from './data-path';
import { parseSeedVersion } from './sync-version';
const SEED_TASKS_DIR = join(SEED_PATH, 'tasks'); const SEED_TASKS_DIR = join(SEED_PATH, 'tasks');
const GLOBAL_TASKS_DIR = join(DATA_PATH, 'tasks'); const GLOBAL_TASKS_DIR = join(DATA_PATH, 'tasks');
@@ -16,14 +17,18 @@ export function syncSeedTasks(): void {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
const seedTaskDir = join(SEED_TASKS_DIR, entry.name); const seedTaskDir = join(SEED_TASKS_DIR, entry.name);
const taskFile = join(seedTaskDir, 'TASK.md'); const seedFile = join(seedTaskDir, 'TASK.md');
if (!existsSync(taskFile)) continue; if (!existsSync(seedFile)) continue;
const targetDir = join(GLOBAL_TASKS_DIR, entry.name); const targetDir = join(GLOBAL_TASKS_DIR, entry.name);
const targetFile = join(targetDir, 'TASK.md');
if (existsSync(targetDir)) { if (existsSync(targetDir)) {
// Task already exists in DATA_PATH — skip to preserve user edits const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8'));
continue; const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0;
if (seedVersion <= targetVersion) continue;
rmSync(targetDir, { recursive: true });
console.log(`[tasks] Updating seed task: ${entry.name} (v${targetVersion} → v${seedVersion})`);
} }
cpSync(seedTaskDir, targetDir, { recursive: true }); cpSync(seedTaskDir, targetDir, { recursive: true });
+10 -5
View File
@@ -1,6 +1,7 @@
import { readdirSync, existsSync, mkdirSync, cpSync } from 'node:fs'; import { readdirSync, readFileSync, existsSync, mkdirSync, cpSync, rmSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { SEED_PATH, DATA_PATH } from './data-path'; import { SEED_PATH, DATA_PATH } from './data-path';
import { parseSeedVersion } from './sync-version';
const SEED_TOOLS_DIR = join(SEED_PATH, 'tools'); const SEED_TOOLS_DIR = join(SEED_PATH, 'tools');
const GLOBAL_TOOLS_DIR = join(DATA_PATH, 'tools'); const GLOBAL_TOOLS_DIR = join(DATA_PATH, 'tools');
@@ -16,14 +17,18 @@ export function syncSeedTools(): void {
if (!entry.isDirectory()) continue; if (!entry.isDirectory()) continue;
const seedToolDir = join(SEED_TOOLS_DIR, entry.name); const seedToolDir = join(SEED_TOOLS_DIR, entry.name);
const toolFile = join(seedToolDir, 'TOOL.md'); const seedFile = join(seedToolDir, 'TOOL.md');
if (!existsSync(toolFile)) continue; if (!existsSync(seedFile)) continue;
const targetDir = join(GLOBAL_TOOLS_DIR, entry.name); const targetDir = join(GLOBAL_TOOLS_DIR, entry.name);
const targetFile = join(targetDir, 'TOOL.md');
if (existsSync(targetDir)) { if (existsSync(targetDir)) {
// Tool already exists in DATA_PATH — skip to preserve user edits const seedVersion = parseSeedVersion(readFileSync(seedFile, 'utf-8'));
continue; const targetVersion = existsSync(targetFile) ? parseSeedVersion(readFileSync(targetFile, 'utf-8')) : 0;
if (seedVersion <= targetVersion) continue;
rmSync(targetDir, { recursive: true });
console.log(`[tools] Updating seed tool: ${entry.name} (v${targetVersion} → v${seedVersion})`);
} }
cpSync(seedToolDir, targetDir, { recursive: true }); cpSync(seedToolDir, targetDir, { recursive: true });
+10
View File
@@ -0,0 +1,10 @@
/**
* Extract the `version` field from YAML frontmatter.
* Returns 0 if no version is found.
*/
export function parseSeedVersion(content: string): number {
const match = content.match(/^---\n([\s\S]*?)\n---/);
if (!match) return 0;
const versionMatch = match[1]!.match(/^version:\s*(\d+)/m);
return versionMatch ? parseInt(versionMatch[1]!, 10) : 0;
}