#!/usr/bin/env node import { readFileSync } from 'node:fs' import { resolve } from 'node:path' import process from 'node:process' function printUsage() { console.log(`Usage: node scripts/rc-request.mjs [options] Options: -X, --method HTTP method to use (default: POST) -b, --base Base URL (default: http://localhost:5572) -q, --query Query parameter to append (repeatable) -d, --data JSON payload string --data-file Path to file containing JSON payload -h, --help Show this help message Examples: node scripts/rc-request.mjs /rc/noop node scripts/rc-request.mjs -q _async=true /job/list node scripts/rc-request.mjs -d '{"fs":"rct:","remote":""}' /operations/list`) } function parseArgs(argv) { const args = { baseUrl: process.env.RCLONE_RC_BASE_URL || 'http://localhost:5572', method: 'POST', path: null, query: [], data: null, } for (let i = 0; i < argv.length; i += 1) { const value = argv[i] switch (value) { case '-h': case '--help': { printUsage() process.exit(0) } case '-X': case '--method': { const next = argv[++i] if (!next) { throw new Error('Expected HTTP method after --method') } args.method = next.toUpperCase() break } case '-b': case '--base': { const next = argv[++i] if (!next) { throw new Error('Expected URL after --base') } args.baseUrl = next break } case '-q': case '--query': { const next = argv[++i] if (!next) { throw new Error('Expected key=value after --query') } args.query.push(next) break } case '-d': case '--data': { const next = argv[++i] if (next === undefined) { throw new Error('Expected JSON string after --data') } args.data = next break } case '--data-file': { const next = argv[++i] if (!next) { throw new Error('Expected file path after --data-file') } args.data = readFileSync(resolve(next), 'utf8') break } default: { if (args.path) { throw new Error(`Unexpected argument: ${value}`) } args.path = value break } } } if (!args.path) { throw new Error('Missing path argument (e.g. /rc/noop)') } return args } function ensurePath(path) { if (!path.startsWith('/')) { return `/${path}` } return path } function buildUrl(baseUrl, path, queryEntries) { const normalizedBase = baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/` const url = new URL(ensurePath(path), normalizedBase) for (const entry of queryEntries) { const [key, ...valueParts] = entry.split('=') if (!key) { throw new Error(`Invalid query entry "${entry}", expected key=value`) } const value = valueParts.length > 0 ? valueParts.join('=') : '' url.searchParams.append(key, value) } return url } function parseJsonMaybe(input) { if (input === null || input === undefined) { return null } const trimmed = input.trim() if (!trimmed) { return null } try { return JSON.parse(trimmed) } catch (error) { throw new Error(`Failed to parse JSON payload: ${error?.message || error}`) } } async function main() { let args try { args = parseArgs(process.argv.slice(2)) } catch (error) { console.error(`[rc-request] ${error.message}`) printUsage() process.exit(1) } const url = buildUrl(args.baseUrl, args.path, args.query) const payload = parseJsonMaybe(args.data) const headers = new Headers() const requestInit = { method: args.method, headers, } if (payload !== null) { headers.set('Content-Type', 'application/json') requestInit.body = JSON.stringify(payload) } const startedAt = Date.now() let response try { response = await fetch(url, requestInit) } catch (error) { console.error(`[rc-request] Request failed: ${error?.message || error}`) process.exit(1) } const durationMs = Date.now() - startedAt const text = await response.text() let parsedBody = null if (text) { try { parsedBody = JSON.parse(text) } catch { parsedBody = text } } const result = { url: url.toString(), method: args.method, status: response.status, statusText: response.statusText, durationMs, headers: Object.fromEntries(response.headers.entries()), body: parsedBody, } console.log(JSON.stringify(result, null, 2)) if (!response.ok) { process.exitCode = 1 } } main().catch((error) => { console.error(`[rc-request] Unexpected error: ${error?.message || error}`) process.exit(1) })