cpy-cli
Version:
186 lines (163 loc) • 5.33 kB
JavaScript
#!/usr/bin/env node
import path from 'node:path';
import fs from 'node:fs';
import process from 'node:process';
import os from 'node:os';
import meow from 'meow';
import cpy from 'cpy';
import {isDynamicPattern} from 'globby';
function isDirectory(filePath) {
try {
return fs.statSync(filePath).isDirectory();
} catch {
return false;
}
}
const cli = meow(`
Usage
$ cpy <source …> <destination>
Options
--no-overwrite Don't overwrite the destination
--ignore-existing Skip files that already exist at the destination
--update Only overwrite if the source is newer, or if sizes differ with the same modification time
--cwd=<dir> Working directory for files
--base=<mode> Base mode for destination paths: cwd or pattern
--rename=<filename> Rename all <source> filenames to <filename>. Supports string templates.
--dot Allow patterns to match entries that begin with a period (.)
--flat Flatten directory structure. All copied files will be put in the same directory.
--dry-run List files that would be copied without actually copying
--concurrency Number of files being copied concurrently
<source> can contain globs if quoted
Errors if no files match, similar to cp.
--update is ignored when --no-overwrite or --ignore-existing is set.
If the source is a single file and the destination is not an existing directory, it will be treated as a file-to-file copy (like cp).
Examples
Copy all .png files in src folder into dist except src/goat.png
$ cpy 'src/*.png' '!src/goat.png' dist
Copy all files inside src folder into dist and preserve path structure
$ cpy . '../dist/' --cwd=src
Copy a single file to a specific filename
$ cpy .env.development .env
Copy all .png files in the src folder to dist and prefix the image filenames
$ cpy 'src/*.png' dist --cwd=src --rename=hi-{{basename}}
Copy only when the source is newer, or if sizes differ with the same modification time
$ cpy src dist --update
`, {
importMeta: import.meta,
flags: {
overwrite: {
type: 'boolean',
default: true,
},
ignoreExisting: {
type: 'boolean',
default: false,
},
update: {
type: 'boolean',
default: false,
},
cwd: {
type: 'string',
default: process.cwd(),
},
base: {
type: 'string',
},
rename: {
type: 'string',
},
dot: {
type: 'boolean',
default: false,
},
flat: {
type: 'boolean',
default: false,
},
dryRun: {
type: 'boolean',
default: false,
},
concurrency: {
type: 'number',
default: (os.cpus().length > 0 ? os.cpus().length : 1) * 2,
},
},
});
try {
const {rename} = cli.flags;
const stringTemplate = '{{basename}}';
if (rename?.includes(stringTemplate)) {
cli.flags.rename = (source, destination) => {
destination.name = rename.replaceAll(stringTemplate, source.nameWithoutExtension) + (source.extension ? `.${source.extension}` : '');
};
}
const copyFiles = [];
let destination = cli.input.pop();
const sourcePatterns = cli.input.filter(pattern => !pattern.startsWith('!'));
const hasDestination = typeof destination === 'string';
const hasTrailingSeparator = hasDestination && /[\\/]$/.test(destination);
const sourcePatternForDynamicCheck = sourcePatterns.length === 1 && process.platform === 'win32' ? sourcePatterns[0].replaceAll('\\', '/') : sourcePatterns[0];
const isFileToFileCopy = sourcePatterns.length === 1
&& hasDestination
&& !isDynamicPattern(sourcePatternForDynamicCheck)
&& !isDirectory(path.resolve(cli.flags.cwd, sourcePatterns[0]))
&& !hasTrailingSeparator
&& !isDirectory(path.resolve(cli.flags.cwd, destination));
if (isFileToFileCopy) {
const destinationFilename = path.basename(destination);
cli.flags.rename = (source, destination) => {
destination.name = destinationFilename;
};
cli.flags.flat = true;
destination = path.dirname(destination);
}
const shouldIgnoreExisting = cli.flags.ignoreExisting;
const shouldUseUpdate = cli.flags.update && cli.flags.overwrite && !shouldIgnoreExisting;
let hasMatchedFiles = false;
let filter;
if (shouldUseUpdate || shouldIgnoreExisting) {
filter = () => {
hasMatchedFiles = true;
return true;
};
}
const cpyOptions = {
cwd: cli.flags.cwd,
base: cli.flags.base,
rename: cli.flags.rename,
overwrite: cli.flags.overwrite,
ignoreExisting: shouldIgnoreExisting,
dot: cli.flags.dot,
flat: cli.flags.flat,
concurrency: cli.flags.concurrency,
};
const files = await cpy(cli.input, destination, {
...cpyOptions,
dryRun: cli.flags.dryRun,
update: shouldUseUpdate,
filter,
onProgress({sourcePath, destinationPath}) {
if (cli.flags.dryRun && sourcePath !== '' && destinationPath !== '') {
copyFiles.push({sourcePath, destinationPath});
}
},
});
if (files.length === 0 && !hasMatchedFiles) {
console.error('No files matched the given patterns');
process.exit(1);
}
if (cli.flags.dryRun) {
for (const {sourcePath, destinationPath} of copyFiles) {
console.log(`${path.relative(process.cwd(), sourcePath)} → ${path.relative(process.cwd(), destinationPath)}`);
}
}
} catch (error) {
if (error.name === 'CpyError') {
console.error(error.message);
process.exit(1);
} else {
throw error;
}
}