Bun Spawn -> node child process in tools and seed versioning
This commit is contained in:
@@ -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.
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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],
|
||||||
|
(err, stdout) => {
|
||||||
|
if (err) return resolve(null);
|
||||||
|
const n = parseFloat(stdout.trim());
|
||||||
|
resolve(isNaN(n) ? null : n);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const text = await new Response(proc.stdout).text();
|
});
|
||||||
await proc.exited;
|
|
||||||
const n = parseFloat(text.trim());
|
|
||||||
return isNaN(n) ? null : n;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scanAudioFiles(dir: string): string[] {
|
function scanAudioFiles(dir: string): string[] {
|
||||||
@@ -57,33 +61,24 @@ 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
|
|
||||||
const reader = (proc.stdout as ReadableStream<Uint8Array>).getReader();
|
|
||||||
const decoder = new TextDecoder();
|
|
||||||
let buf = '';
|
let buf = '';
|
||||||
let lastPercent = -1;
|
let lastPercent = -1;
|
||||||
|
let stderrText = '';
|
||||||
|
|
||||||
(async () => {
|
proc.stdout.on('data', (chunk: Buffer) => {
|
||||||
try {
|
buf += chunk.toString();
|
||||||
while (true) {
|
|
||||||
const { done, value } = await reader.read();
|
|
||||||
if (done) break;
|
|
||||||
buf += decoder.decode(value, { stream: true });
|
|
||||||
const lines = buf.split('\n');
|
const lines = buf.split('\n');
|
||||||
buf = lines.pop() ?? '';
|
buf = lines.pop() ?? '';
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
@@ -96,21 +91,25 @@ async function convertFile(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
} 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 (code !== 0) {
|
||||||
if (existsSync(outputFile)) unlinkSync(outputFile);
|
if (existsSync(outputFile)) unlinkSync(outputFile);
|
||||||
return { outputFile, success: false, error: stderrText.trim() || 'ffmpeg failed' };
|
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(
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user