-
-
Notifications
You must be signed in to change notification settings - Fork 35
Expand file tree
/
Copy pathcli.js
More file actions
executable file
·185 lines (163 loc) · 5.2 KB
/
cli.js
File metadata and controls
executable file
·185 lines (163 loc) · 5.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
#!/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;
}
}