Add capture() to spawn, add rsync itemize parser
This commit is contained in:
56
lib/itemize.js
Normal file
56
lib/itemize.js
Normal file
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Parse rsync --itemize-changes output into a structured change list.
|
||||
*
|
||||
* rsync itemize format: 11-character code + space + path
|
||||
*
|
||||
* Code structure: YXcstpoguax
|
||||
* Y = update type: > (transfer), * (message/delete), c (local change), . (no update), h (hard link)
|
||||
* X = file type: f (file), d (dir), L (symlink), D (device), S (special)
|
||||
* remaining chars = what changed (size, time, perms, etc.) or '+++++++++' for new
|
||||
*
|
||||
* We care about:
|
||||
* >f... = file transferred (new or modified)
|
||||
* *deleting = file deleted
|
||||
* cd... = directory (ignored for delta purposes)
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {{ status: 'added'|'modified'|'deleted', path: string }} Change
|
||||
*/
|
||||
|
||||
/**
|
||||
* Parse rsync --itemize-changes stdout into a list of file changes.
|
||||
* @param {string} output
|
||||
* @returns {Change[]}
|
||||
*/
|
||||
export function parseItemize(output) {
|
||||
const changes = [];
|
||||
|
||||
for (const raw of output.split('\n')) {
|
||||
const line = raw.trimEnd();
|
||||
if (!line) continue;
|
||||
|
||||
// Deleted files: "*deleting path/to/file"
|
||||
if (line.startsWith('*deleting ')) {
|
||||
const path = line.slice('*deleting '.length).trimStart();
|
||||
// Skip directory deletions (trailing slash)
|
||||
if (!path.endsWith('/')) {
|
||||
changes.push({ status: 'deleted', path });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// File transfers: ">f......... path" (new or modified)
|
||||
if (line.length > 12 && line[0] === '>' && line[1] === 'f') {
|
||||
const code = line.slice(0, 11);
|
||||
const path = line.slice(12);
|
||||
const isNew = code.slice(2) === '+++++++++';
|
||||
changes.push({ status: isNew ? 'added' : 'modified', path });
|
||||
continue;
|
||||
}
|
||||
|
||||
// Everything else (dirs, symlinks, attribute-only changes) — ignore
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
33
lib/spawn.js
33
lib/spawn.js
@@ -4,17 +4,14 @@
|
||||
import { spawn } from 'child_process';
|
||||
|
||||
/**
|
||||
* Spawn a process and stream its output.
|
||||
* Spawn a process and stream its output to stdout/stderr.
|
||||
* @param {string} cmd
|
||||
* @param {string[]} args
|
||||
* @param {{ dryRun?: boolean, label?: string }} opts
|
||||
* @param {{ dryRun?: boolean }} opts
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function run(cmd, args, { dryRun = false, label } = {}) {
|
||||
const display = [cmd, ...args].join(' ');
|
||||
if (label) console.log(`[${label}] ${display}`);
|
||||
else console.log(`$ ${display}`);
|
||||
|
||||
export async function run(cmd, args, { dryRun = false } = {}) {
|
||||
console.log(`$ ${[cmd, ...args].join(' ')}`);
|
||||
if (dryRun) return;
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -26,3 +23,25 @@ export async function run(cmd, args, { dryRun = false, label } = {}) {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Spawn a process and capture stdout as a string.
|
||||
* stderr is inherited (shown to user). Never used in dry-run context.
|
||||
* @param {string} cmd
|
||||
* @param {string[]} args
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
export async function capture(cmd, args) {
|
||||
console.log(`$ ${[cmd, ...args].join(' ')}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, args, { stdio: ['inherit', 'pipe', 'inherit'] });
|
||||
const chunks = [];
|
||||
child.stdout.on('data', chunk => chunks.push(chunk));
|
||||
child.on('error', reject);
|
||||
child.on('close', code => {
|
||||
if (code === 0) resolve(Buffer.concat(chunks).toString('utf8'));
|
||||
else reject(new Error(`${cmd} exited with code ${code}`));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user