@wpelevator/wp-release 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +316 -2
- package/dist/bin/wp-release.d.ts +2 -0
- package/dist/bin/wp-release.js +3 -0
- package/dist/checksums.d.ts +138 -0
- package/dist/checksums.js +216 -0
- package/dist/cli.d.ts +42 -0
- package/dist/cli.js +163 -0
- package/dist/commands/args.d.ts +81 -0
- package/dist/commands/args.js +116 -0
- package/dist/commands/checksums.d.ts +34 -0
- package/dist/commands/checksums.js +76 -0
- package/dist/commands/command.d.ts +63 -0
- package/dist/commands/command.js +33 -0
- package/dist/commands/deploy-svn.d.ts +74 -0
- package/dist/commands/deploy-svn.js +103 -0
- package/dist/commands/verify.d.ts +27 -0
- package/dist/commands/verify.js +60 -0
- package/dist/commands/version-check.d.ts +38 -0
- package/dist/commands/version-check.js +80 -0
- package/dist/commands/zip.d.ts +41 -0
- package/dist/commands/zip.js +138 -0
- package/dist/deploy-options.d.ts +136 -0
- package/dist/deploy-options.js +168 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.js +16 -0
- package/dist/file-selection.d.ts +78 -0
- package/dist/file-selection.js +133 -0
- package/dist/filename.d.ts +17 -0
- package/dist/filename.js +23 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +15 -0
- package/dist/logger.d.ts +42 -0
- package/dist/logger.js +56 -0
- package/dist/package-meta.d.ts +112 -0
- package/dist/package-meta.js +222 -0
- package/dist/svn-deploy.d.ts +208 -0
- package/dist/svn-deploy.js +485 -0
- package/dist/svn.d.ts +209 -0
- package/dist/svn.js +298 -0
- package/dist/sync.d.ts +32 -0
- package/dist/sync.js +90 -0
- package/dist/unzip.d.ts +45 -0
- package/dist/unzip.js +121 -0
- package/dist/version-check.d.ts +75 -0
- package/dist/version-check.js +193 -0
- package/dist/zip.d.ts +32 -0
- package/dist/zip.js +53 -0
- package/package.json +61 -5
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { createReadStream } from 'node:fs';
|
|
2
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { ValidationError } from "./errors.js";
|
|
6
|
+
import { isErrorCode, selectFiles } from "./file-selection.js";
|
|
7
|
+
export const CHECKSUMS_EXTENSION = '.checksums.json';
|
|
8
|
+
/**
|
|
9
|
+
* Hash algorithms in each file entry. MD5 is kept for compatibility with the
|
|
10
|
+
* WordPress.org checksums format; verification prefers SHA-256.
|
|
11
|
+
*/
|
|
12
|
+
export const CHECKSUM_ALGORITHMS = ['md5', 'sha256'];
|
|
13
|
+
const PACKAGE_TYPES = ['plugin', 'theme'];
|
|
14
|
+
/**
|
|
15
|
+
* Hash a file with every checksum algorithm in a single read.
|
|
16
|
+
*
|
|
17
|
+
* @param path File path.
|
|
18
|
+
* @return Hex digests keyed by algorithm.
|
|
19
|
+
*/
|
|
20
|
+
export async function hashFile(path) {
|
|
21
|
+
const md5 = createHash('md5');
|
|
22
|
+
const sha256 = createHash('sha256');
|
|
23
|
+
for await (const chunk of createReadStream(path)) {
|
|
24
|
+
md5.update(chunk);
|
|
25
|
+
sha256.update(chunk);
|
|
26
|
+
}
|
|
27
|
+
return { md5: md5.digest('hex'), sha256: sha256.digest('hex') };
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Hash files one at a time, so that large plugins don't run out of file
|
|
31
|
+
* descriptors.
|
|
32
|
+
*
|
|
33
|
+
* @param sourceDir Directory the paths are relative to.
|
|
34
|
+
* @param files Relative file paths.
|
|
35
|
+
* @return Digests keyed by relative path, in the order of `files`.
|
|
36
|
+
*/
|
|
37
|
+
export async function hashFiles(sourceDir, files) {
|
|
38
|
+
const hashes = {};
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
hashes[file] = await hashFile(join(sourceDir, file));
|
|
41
|
+
}
|
|
42
|
+
return hashes;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Create a checksums manifest for the selected files of a directory.
|
|
46
|
+
*
|
|
47
|
+
* The manifest uses the format of the WordPress.org plugin checksums API
|
|
48
|
+
* (downloads.wordpress.org/plugin-checksums/{slug}/{version}.json), with
|
|
49
|
+
* paths relative to the plugin or theme folder:
|
|
50
|
+
*
|
|
51
|
+
* { "plugin": "my-plugin", "version": "1.2.0", "files": { "my-plugin.php": { "md5": "…", "sha256": "…" } } }
|
|
52
|
+
*
|
|
53
|
+
* Themes use a "theme" key instead of "plugin". The version is left out when
|
|
54
|
+
* it's unknown.
|
|
55
|
+
*
|
|
56
|
+
* @param input Directory, file rules and package details.
|
|
57
|
+
* @return Checksums manifest.
|
|
58
|
+
*/
|
|
59
|
+
export async function createChecksums(input) {
|
|
60
|
+
const { sourceDir, rules, slug, version = null, type = 'plugin' } = input;
|
|
61
|
+
if (!isPackageType(type)) {
|
|
62
|
+
throw new ValidationError(`Unknown type "${type}", use ${PACKAGE_TYPES.join(' or ')}.`);
|
|
63
|
+
}
|
|
64
|
+
const files = await selectFiles(sourceDir, rules);
|
|
65
|
+
// Keep the slug and version before the files in the JSON.
|
|
66
|
+
return {
|
|
67
|
+
...('theme' === type ? { theme: slug } : { plugin: slug }),
|
|
68
|
+
...(version ? { version } : {}),
|
|
69
|
+
files: await hashFiles(sourceDir, files),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function isPackageType(type) {
|
|
73
|
+
return PACKAGE_TYPES.includes(type);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Write a checksums manifest as JSON.
|
|
77
|
+
*
|
|
78
|
+
* @param path Target file path.
|
|
79
|
+
* @param manifest Manifest from createChecksums().
|
|
80
|
+
* @param force Overwrite an existing file.
|
|
81
|
+
* @throws {ValidationError} If the file exists and force isn't set.
|
|
82
|
+
*/
|
|
83
|
+
export async function writeChecksums(path, manifest, force = false) {
|
|
84
|
+
try {
|
|
85
|
+
await writeFile(path, JSON.stringify(manifest, null, '\t') + '\n', {
|
|
86
|
+
flag: force ? 'w' : 'wx',
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (isErrorCode(error, 'EEXIST')) {
|
|
91
|
+
throw new ValidationError(`${path} already exists, pass --force to overwrite it.`);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Read and validate a checksums manifest. Accepts manifests from
|
|
98
|
+
* createChecksums() and the WordPress.org checksums API, where a hash can
|
|
99
|
+
* also be a list of accepted values.
|
|
100
|
+
*
|
|
101
|
+
* @param path Manifest file path.
|
|
102
|
+
* @return Checksums manifest.
|
|
103
|
+
* @throws {ValidationError} If the file isn't a valid manifest.
|
|
104
|
+
*/
|
|
105
|
+
export async function readChecksums(path) {
|
|
106
|
+
const contents = await readFile(path, 'utf8').catch((error) => {
|
|
107
|
+
throw isErrorCode(error, 'ENOENT')
|
|
108
|
+
? new ValidationError(`Checksums file ${path} doesn't exist.`)
|
|
109
|
+
: error;
|
|
110
|
+
});
|
|
111
|
+
return parseChecksums(contents, path);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse and validate checksums manifest JSON.
|
|
115
|
+
*
|
|
116
|
+
* @param contents Manifest JSON.
|
|
117
|
+
* @param source Name of the manifest for error messages.
|
|
118
|
+
* @return Checksums manifest.
|
|
119
|
+
* @throws {ValidationError} If the JSON isn't a valid manifest.
|
|
120
|
+
*/
|
|
121
|
+
export function parseChecksums(contents, source = 'checksums') {
|
|
122
|
+
const manifest = parseJson(contents, source);
|
|
123
|
+
const files = isRecord(manifest) ? manifest.files : undefined;
|
|
124
|
+
if (!isRecord(manifest) || !isRecord(files)) {
|
|
125
|
+
throw new ValidationError(`${source} has no "files" object.`);
|
|
126
|
+
}
|
|
127
|
+
const invalidPaths = Object.keys(files).filter((path) => !isValidPath(path));
|
|
128
|
+
const invalidHashes = Object.entries(files)
|
|
129
|
+
.filter(([, hashes]) => !isValidEntry(hashes))
|
|
130
|
+
.map(([path]) => path);
|
|
131
|
+
if (invalidPaths.length) {
|
|
132
|
+
throw new ValidationError(`${source} has paths outside the package folder: ${invalidPaths.join(', ')}.`);
|
|
133
|
+
}
|
|
134
|
+
if (invalidHashes.length) {
|
|
135
|
+
throw new ValidationError(`${source} has no md5 or sha256 hash for ${invalidHashes.join(', ')}.`);
|
|
136
|
+
}
|
|
137
|
+
return manifest;
|
|
138
|
+
}
|
|
139
|
+
function isRecord(value) {
|
|
140
|
+
return (Boolean(value) &&
|
|
141
|
+
'object' === typeof value &&
|
|
142
|
+
!Array.isArray(value));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Check that a manifest path is relative to the package folder, with forward
|
|
146
|
+
* slashes and without "." or ".." segments.
|
|
147
|
+
*
|
|
148
|
+
* @param path Manifest path.
|
|
149
|
+
* @return Whether the path is valid.
|
|
150
|
+
*/
|
|
151
|
+
function isValidPath(path) {
|
|
152
|
+
const segments = path.split('/');
|
|
153
|
+
return (!path.includes('\\') &&
|
|
154
|
+
segments.every((segment) => segment && '.' !== segment && '..' !== segment));
|
|
155
|
+
}
|
|
156
|
+
function parseJson(contents, source) {
|
|
157
|
+
try {
|
|
158
|
+
return JSON.parse(contents);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw new ValidationError(`${source} isn't valid JSON: ${error.message}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
function isValidEntry(hashes) {
|
|
165
|
+
return (isRecord(hashes) &&
|
|
166
|
+
CHECKSUM_ALGORITHMS.some((algorithm) => [hashes[algorithm]]
|
|
167
|
+
.flat()
|
|
168
|
+
.some((hash) => 'string' === typeof hash && hash.length > 0)));
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Compare expected file hashes with actual ones. A file matches when its
|
|
172
|
+
* SHA-256 is one of the expected values, or its MD5 for manifests without
|
|
173
|
+
* SHA-256.
|
|
174
|
+
*
|
|
175
|
+
* @param expected Expected hashes keyed by relative path.
|
|
176
|
+
* @param actual Actual hashes keyed by relative path.
|
|
177
|
+
* @return Sorted lists of matched, changed, missing and extra paths, and whether all match.
|
|
178
|
+
*/
|
|
179
|
+
export function compareChecksums(expected, actual) {
|
|
180
|
+
const expectedPaths = Object.keys(expected).sort();
|
|
181
|
+
const present = expectedPaths.filter((path) => Object.hasOwn(actual, path));
|
|
182
|
+
const isMatch = (path) => matchesChecksum(expected[path], actual[path]);
|
|
183
|
+
const changed = present.filter((path) => !isMatch(path));
|
|
184
|
+
const missing = expectedPaths.filter((path) => !Object.hasOwn(actual, path));
|
|
185
|
+
const extra = Object.keys(actual)
|
|
186
|
+
.filter((path) => !Object.hasOwn(expected, path))
|
|
187
|
+
.sort();
|
|
188
|
+
return {
|
|
189
|
+
valid: 0 === changed.length + missing.length + extra.length,
|
|
190
|
+
matched: present.filter(isMatch),
|
|
191
|
+
changed,
|
|
192
|
+
missing,
|
|
193
|
+
extra,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function matchesChecksum(expected, actual) {
|
|
197
|
+
const algorithm = expected?.sha256 ? 'sha256' : 'md5';
|
|
198
|
+
return [expected?.[algorithm]]
|
|
199
|
+
.flat()
|
|
200
|
+
.map((hash) => String(hash).toLowerCase())
|
|
201
|
+
.includes(actual?.[algorithm] ?? '');
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Verify the files of a directory against a checksums manifest. Paths that
|
|
205
|
+
* the rules ignore are skipped on both sides, so files created after the
|
|
206
|
+
* install (such as caches) can be excluded.
|
|
207
|
+
*
|
|
208
|
+
* @param input Directory, manifest and file rules.
|
|
209
|
+
* @return Result from compareChecksums().
|
|
210
|
+
*/
|
|
211
|
+
export async function verifyChecksums(input) {
|
|
212
|
+
const { sourceDir, manifest, rules } = input;
|
|
213
|
+
const expected = Object.fromEntries(Object.entries(manifest.files).filter(([path]) => !rules.ignores(path)));
|
|
214
|
+
const files = await selectFiles(sourceDir, rules);
|
|
215
|
+
return compareChecksums(expected, await hashFiles(sourceDir, files));
|
|
216
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OutputStream } from './logger.ts';
|
|
2
|
+
import type { Command } from './commands/command.ts';
|
|
3
|
+
export interface CliIo {
|
|
4
|
+
/** Stream for regular output. */
|
|
5
|
+
stdout?: OutputStream;
|
|
6
|
+
/** Stream for errors. */
|
|
7
|
+
stderr?: OutputStream;
|
|
8
|
+
/** Working directory that relative paths resolve against. */
|
|
9
|
+
cwd?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare const EXIT_SUCCESS = 0;
|
|
12
|
+
export declare const EXIT_FAILURE = 1;
|
|
13
|
+
export declare const EXIT_INVALID = 2;
|
|
14
|
+
export declare const commands: Command[];
|
|
15
|
+
/**
|
|
16
|
+
* Get the top-level help text listing all commands.
|
|
17
|
+
*
|
|
18
|
+
* @return Help text.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getHelp(): string;
|
|
21
|
+
/**
|
|
22
|
+
* Get the help text of a single command.
|
|
23
|
+
*
|
|
24
|
+
* @param command Command definition.
|
|
25
|
+
* @return Help text.
|
|
26
|
+
*/
|
|
27
|
+
export declare function getCommandHelp(command: Command): string;
|
|
28
|
+
/**
|
|
29
|
+
* Find the command named by the first argument.
|
|
30
|
+
*
|
|
31
|
+
* @param args CLI arguments.
|
|
32
|
+
* @return Command definition.
|
|
33
|
+
*/
|
|
34
|
+
export declare function findCommand(args: string[]): Command | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Run the CLI.
|
|
37
|
+
*
|
|
38
|
+
* @param args CLI arguments without the node and script paths.
|
|
39
|
+
* @param io Output streams and working directory.
|
|
40
|
+
* @return Exit code.
|
|
41
|
+
*/
|
|
42
|
+
export declare function run(args: string[], io?: CliIo): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import zip from "./commands/zip.js";
|
|
3
|
+
import checksums from "./commands/checksums.js";
|
|
4
|
+
import verify from "./commands/verify.js";
|
|
5
|
+
import versionCheck from "./commands/version-check.js";
|
|
6
|
+
import deploySvn from "./commands/deploy-svn.js";
|
|
7
|
+
import { Logger } from "./logger.js";
|
|
8
|
+
import { getBoolean } from "./commands/command.js";
|
|
9
|
+
export const EXIT_SUCCESS = 0;
|
|
10
|
+
export const EXIT_FAILURE = 1;
|
|
11
|
+
export const EXIT_INVALID = 2;
|
|
12
|
+
export const commands = [
|
|
13
|
+
zip,
|
|
14
|
+
checksums,
|
|
15
|
+
verify,
|
|
16
|
+
versionCheck,
|
|
17
|
+
deploySvn,
|
|
18
|
+
];
|
|
19
|
+
const globalArgs = {
|
|
20
|
+
help: { type: 'boolean', short: 'h' },
|
|
21
|
+
verbose: { type: 'boolean', short: 'v' },
|
|
22
|
+
quiet: { type: 'boolean', short: 'q' },
|
|
23
|
+
ci: { type: 'boolean' },
|
|
24
|
+
json: { type: 'boolean' },
|
|
25
|
+
};
|
|
26
|
+
const globalOptions = `Global options:
|
|
27
|
+
-h, --help Show help for the command
|
|
28
|
+
-v, --verbose Print more output
|
|
29
|
+
-q, --quiet Print errors only
|
|
30
|
+
--ci Use GitHub Actions annotations (default when GITHUB_ACTIONS=true)
|
|
31
|
+
--json Print the result as JSON`;
|
|
32
|
+
/**
|
|
33
|
+
* Get the top-level help text listing all commands.
|
|
34
|
+
*
|
|
35
|
+
* @return Help text.
|
|
36
|
+
*/
|
|
37
|
+
export function getHelp() {
|
|
38
|
+
const width = Math.max(...commands.map((command) => command.name.length));
|
|
39
|
+
const list = commands
|
|
40
|
+
.map((command) => ` ${command.name.padEnd(width)} ${command.summary}`)
|
|
41
|
+
.join('\n');
|
|
42
|
+
return `Usage: wp-release <command> [options]
|
|
43
|
+
|
|
44
|
+
Package, checksum, verify and version-check WordPress plugins and themes, and deploy plugins to WordPress.org.
|
|
45
|
+
|
|
46
|
+
Commands:
|
|
47
|
+
${list}
|
|
48
|
+
|
|
49
|
+
${globalOptions}
|
|
50
|
+
|
|
51
|
+
Run "wp-release <command> --help" for command options.`;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the help text of a single command.
|
|
55
|
+
*
|
|
56
|
+
* @param command Command definition.
|
|
57
|
+
* @return Help text.
|
|
58
|
+
*/
|
|
59
|
+
export function getCommandHelp(command) {
|
|
60
|
+
return `Usage: wp-release ${command.usage}
|
|
61
|
+
|
|
62
|
+
${command.summary}
|
|
63
|
+
|
|
64
|
+
${command.help}
|
|
65
|
+
|
|
66
|
+
${globalOptions}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Find the command named by the first argument.
|
|
70
|
+
*
|
|
71
|
+
* @param args CLI arguments.
|
|
72
|
+
* @return Command definition.
|
|
73
|
+
*/
|
|
74
|
+
export function findCommand(args) {
|
|
75
|
+
return commands.find((command) => command.name === args[0]);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Get the exit code for an error thrown while running a command.
|
|
79
|
+
*
|
|
80
|
+
* @param error Thrown error.
|
|
81
|
+
* @return Exit code.
|
|
82
|
+
*/
|
|
83
|
+
function getErrorExitCode(error) {
|
|
84
|
+
const { exitCode, code } = (error ?? {});
|
|
85
|
+
if ('number' === typeof exitCode && exitCode) {
|
|
86
|
+
return exitCode;
|
|
87
|
+
}
|
|
88
|
+
if (String(code).startsWith('ERR_PARSE_ARGS')) {
|
|
89
|
+
return EXIT_INVALID;
|
|
90
|
+
}
|
|
91
|
+
return EXIT_FAILURE;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Parse the arguments of a command and run it.
|
|
95
|
+
*
|
|
96
|
+
* The command's run() gets the parsed values and positionals, the working
|
|
97
|
+
* directory and a logger, and returns the exit code with a result that is
|
|
98
|
+
* printed with --json. With --json, regular output is suppressed so that
|
|
99
|
+
* stdout stays valid JSON.
|
|
100
|
+
*
|
|
101
|
+
* @param command Command definition with options and run().
|
|
102
|
+
* @param args Arguments after the command name.
|
|
103
|
+
* @param io Output streams and working directory.
|
|
104
|
+
* @return Exit code.
|
|
105
|
+
*/
|
|
106
|
+
async function runCommand(command, args, io) {
|
|
107
|
+
const { stdout, stderr, cwd } = io;
|
|
108
|
+
let logger = new Logger({ stdout, stderr });
|
|
109
|
+
try {
|
|
110
|
+
const { values, positionals } = parseArgs({
|
|
111
|
+
args,
|
|
112
|
+
options: { ...globalArgs, ...command.options },
|
|
113
|
+
allowPositionals: true,
|
|
114
|
+
});
|
|
115
|
+
logger = new Logger({
|
|
116
|
+
ci: getBoolean(values, 'ci'),
|
|
117
|
+
quiet: getBoolean(values, 'quiet') || getBoolean(values, 'json'),
|
|
118
|
+
verbose: getBoolean(values, 'verbose'),
|
|
119
|
+
stdout,
|
|
120
|
+
stderr,
|
|
121
|
+
});
|
|
122
|
+
const { exitCode, result } = await command.run({
|
|
123
|
+
values,
|
|
124
|
+
positionals,
|
|
125
|
+
cwd,
|
|
126
|
+
logger,
|
|
127
|
+
});
|
|
128
|
+
if (getBoolean(values, 'json')) {
|
|
129
|
+
stdout.write(JSON.stringify(result, null, '\t') + '\n');
|
|
130
|
+
}
|
|
131
|
+
return exitCode;
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
logger.error(error instanceof Error ? error.message : String(error));
|
|
135
|
+
return getErrorExitCode(error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Run the CLI.
|
|
140
|
+
*
|
|
141
|
+
* @param args CLI arguments without the node and script paths.
|
|
142
|
+
* @param io Output streams and working directory.
|
|
143
|
+
* @return Exit code.
|
|
144
|
+
*/
|
|
145
|
+
export async function run(args, io = {}) {
|
|
146
|
+
const { stdout = process.stdout, stderr = process.stderr, cwd = process.cwd(), } = io;
|
|
147
|
+
const wantsHelp = args.includes('--help') || args.includes('-h');
|
|
148
|
+
const command = findCommand(args);
|
|
149
|
+
if (command && wantsHelp) {
|
|
150
|
+
stdout.write(getCommandHelp(command) + '\n');
|
|
151
|
+
return EXIT_SUCCESS;
|
|
152
|
+
}
|
|
153
|
+
if (command) {
|
|
154
|
+
return runCommand(command, args.slice(1), { stdout, stderr, cwd });
|
|
155
|
+
}
|
|
156
|
+
if (0 === args.length || wantsHelp || 'help' === args[0]) {
|
|
157
|
+
stdout.write(getHelp() + '\n');
|
|
158
|
+
return EXIT_SUCCESS;
|
|
159
|
+
}
|
|
160
|
+
stderr.write(`Unknown command "${args[0]}".\n\n`);
|
|
161
|
+
stderr.write(getHelp() + '\n');
|
|
162
|
+
return EXIT_INVALID;
|
|
163
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ParseArgsOptionsConfig } from 'node:util';
|
|
2
|
+
import type { FileRules } from '../file-selection.ts';
|
|
3
|
+
import type { ParsedValues } from './command.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Options shared by the commands that select files with .distignore and
|
|
6
|
+
* --exclude, in node:util parseArgs() format.
|
|
7
|
+
*/
|
|
8
|
+
export declare const fileSelectionArgs: ParseArgsOptionsConfig;
|
|
9
|
+
/**
|
|
10
|
+
* Map the parsed file selection flags to resolveFileSelection() input.
|
|
11
|
+
*
|
|
12
|
+
* @param values Parsed CLI values.
|
|
13
|
+
* @return The distignore and exclude input.
|
|
14
|
+
*/
|
|
15
|
+
export declare function getFileSelectionInput(values: ParsedValues): {
|
|
16
|
+
distignore: string | false | undefined;
|
|
17
|
+
exclude: string[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Check that a command got no more positional arguments than it accepts.
|
|
21
|
+
*
|
|
22
|
+
* @param positionals Positional arguments.
|
|
23
|
+
* @param max Number of accepted arguments.
|
|
24
|
+
* @return The positional arguments.
|
|
25
|
+
* @throws {ValidationError} With unexpected arguments.
|
|
26
|
+
*/
|
|
27
|
+
export declare function checkPositionals(positionals: string[], max: number): string[];
|
|
28
|
+
/**
|
|
29
|
+
* Check that a path is an existing directory.
|
|
30
|
+
*
|
|
31
|
+
* @param path Directory path.
|
|
32
|
+
* @throws {ValidationError} If the path isn't a directory.
|
|
33
|
+
*/
|
|
34
|
+
export declare function checkDirectory(path: string): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Check if a path is an existing directory.
|
|
37
|
+
*
|
|
38
|
+
* @param path Path to check.
|
|
39
|
+
* @return Whether the path is a directory.
|
|
40
|
+
*/
|
|
41
|
+
export declare function isDirectory(path: string): Promise<boolean>;
|
|
42
|
+
/**
|
|
43
|
+
* Ignore a file that the command writes or reads, such as the checksums
|
|
44
|
+
* manifest, when it's inside the selected directory.
|
|
45
|
+
*
|
|
46
|
+
* @param rules Matcher from loadFileRules().
|
|
47
|
+
* @param sourceDir Selected directory.
|
|
48
|
+
* @param path Absolute file path.
|
|
49
|
+
* @return The same matcher.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ignoreOwnFile(rules: FileRules, sourceDir: string, path: string): FileRules;
|
|
52
|
+
/**
|
|
53
|
+
* Option for the source directory, which can also be the first positional
|
|
54
|
+
* argument.
|
|
55
|
+
*/
|
|
56
|
+
export declare const sourceDirArgs: ParseArgsOptionsConfig;
|
|
57
|
+
/**
|
|
58
|
+
* Get the source directory from the positional argument or --source-dir.
|
|
59
|
+
*
|
|
60
|
+
* @param values Parsed CLI values.
|
|
61
|
+
* @param positionals Positional arguments.
|
|
62
|
+
* @return The source directory argument, default ".".
|
|
63
|
+
* @throws {ValidationError} With extra arguments, or both forms set to different paths.
|
|
64
|
+
*/
|
|
65
|
+
export declare function getSourceDirArg(values: ParsedValues, positionals: string[]): string;
|
|
66
|
+
export interface ResolveOutputInput {
|
|
67
|
+
/** Working directory. */
|
|
68
|
+
cwd: string;
|
|
69
|
+
/** The --output file path. */
|
|
70
|
+
output?: string;
|
|
71
|
+
/** Default filename in the working directory. */
|
|
72
|
+
filename: string;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve the path of the file that a command writes: --output, or the
|
|
76
|
+
* default filename in the working directory.
|
|
77
|
+
*
|
|
78
|
+
* @param input Working directory, --output value and default filename.
|
|
79
|
+
* @return Absolute file path.
|
|
80
|
+
*/
|
|
81
|
+
export declare function resolveOutput(input: ResolveOutputInput): string;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { stat } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'node:path';
|
|
3
|
+
import { ValidationError } from "../errors.js";
|
|
4
|
+
import { getBoolean, getString, getStrings } from "./command.js";
|
|
5
|
+
/**
|
|
6
|
+
* Options shared by the commands that select files with .distignore and
|
|
7
|
+
* --exclude, in node:util parseArgs() format.
|
|
8
|
+
*/
|
|
9
|
+
export const fileSelectionArgs = {
|
|
10
|
+
distignore: { type: 'string' },
|
|
11
|
+
'no-distignore': { type: 'boolean' },
|
|
12
|
+
exclude: { type: 'string', multiple: true },
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Map the parsed file selection flags to resolveFileSelection() input.
|
|
16
|
+
*
|
|
17
|
+
* @param values Parsed CLI values.
|
|
18
|
+
* @return The distignore and exclude input.
|
|
19
|
+
*/
|
|
20
|
+
export function getFileSelectionInput(values) {
|
|
21
|
+
return {
|
|
22
|
+
distignore: getBoolean(values, 'no-distignore')
|
|
23
|
+
? false
|
|
24
|
+
: getString(values, 'distignore'),
|
|
25
|
+
exclude: getStrings(values, 'exclude'),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Check that a command got no more positional arguments than it accepts.
|
|
30
|
+
*
|
|
31
|
+
* @param positionals Positional arguments.
|
|
32
|
+
* @param max Number of accepted arguments.
|
|
33
|
+
* @return The positional arguments.
|
|
34
|
+
* @throws {ValidationError} With unexpected arguments.
|
|
35
|
+
*/
|
|
36
|
+
export function checkPositionals(positionals, max) {
|
|
37
|
+
if (positionals.length > max) {
|
|
38
|
+
throw new ValidationError(`Unexpected argument "${positionals[max]}".`);
|
|
39
|
+
}
|
|
40
|
+
return positionals;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Check that a path is an existing directory.
|
|
44
|
+
*
|
|
45
|
+
* @param path Directory path.
|
|
46
|
+
* @throws {ValidationError} If the path isn't a directory.
|
|
47
|
+
*/
|
|
48
|
+
export async function checkDirectory(path) {
|
|
49
|
+
if (!(await isDirectory(path))) {
|
|
50
|
+
throw new ValidationError(`${path} is not a directory.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Check if a path is an existing directory.
|
|
55
|
+
*
|
|
56
|
+
* @param path Path to check.
|
|
57
|
+
* @return Whether the path is a directory.
|
|
58
|
+
*/
|
|
59
|
+
export async function isDirectory(path) {
|
|
60
|
+
const stats = await stat(path).catch(() => null);
|
|
61
|
+
return Boolean(stats?.isDirectory());
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Ignore a file that the command writes or reads, such as the checksums
|
|
65
|
+
* manifest, when it's inside the selected directory.
|
|
66
|
+
*
|
|
67
|
+
* @param rules Matcher from loadFileRules().
|
|
68
|
+
* @param sourceDir Selected directory.
|
|
69
|
+
* @param path Absolute file path.
|
|
70
|
+
* @return The same matcher.
|
|
71
|
+
*/
|
|
72
|
+
export function ignoreOwnFile(rules, sourceDir, path) {
|
|
73
|
+
const relativePath = relative(sourceDir, path).split('\\').join('/');
|
|
74
|
+
const isInside = relativePath &&
|
|
75
|
+
'..' !== relativePath &&
|
|
76
|
+
!relativePath.startsWith('../') &&
|
|
77
|
+
!isAbsolute(relativePath);
|
|
78
|
+
if (isInside) {
|
|
79
|
+
// Escape the gitignore wildcards so that the pattern matches only this file.
|
|
80
|
+
rules.add(`/${relativePath.replace(/[\\*?[\]!#]/g, '\\$&')}`);
|
|
81
|
+
}
|
|
82
|
+
return rules;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Option for the source directory, which can also be the first positional
|
|
86
|
+
* argument.
|
|
87
|
+
*/
|
|
88
|
+
export const sourceDirArgs = {
|
|
89
|
+
'source-dir': { type: 'string' },
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Get the source directory from the positional argument or --source-dir.
|
|
93
|
+
*
|
|
94
|
+
* @param values Parsed CLI values.
|
|
95
|
+
* @param positionals Positional arguments.
|
|
96
|
+
* @return The source directory argument, default ".".
|
|
97
|
+
* @throws {ValidationError} With extra arguments, or both forms set to different paths.
|
|
98
|
+
*/
|
|
99
|
+
export function getSourceDirArg(values, positionals) {
|
|
100
|
+
const [positional] = checkPositionals(positionals, 1);
|
|
101
|
+
const option = getString(values, 'source-dir');
|
|
102
|
+
if (positional && option && positional !== option) {
|
|
103
|
+
throw new ValidationError(`Pass the source directory either as an argument or with --source-dir, got "${positional}" and "${option}".`);
|
|
104
|
+
}
|
|
105
|
+
return positional ?? option ?? '.';
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Resolve the path of the file that a command writes: --output, or the
|
|
109
|
+
* default filename in the working directory.
|
|
110
|
+
*
|
|
111
|
+
* @param input Working directory, --output value and default filename.
|
|
112
|
+
* @return Absolute file path.
|
|
113
|
+
*/
|
|
114
|
+
export function resolveOutput(input) {
|
|
115
|
+
return resolve(input.cwd, input.output ?? input.filename);
|
|
116
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
declare const _default: {
|
|
2
|
+
name: string;
|
|
3
|
+
usage: string;
|
|
4
|
+
summary: string;
|
|
5
|
+
help: string;
|
|
6
|
+
options: {
|
|
7
|
+
force: {
|
|
8
|
+
type: "boolean";
|
|
9
|
+
};
|
|
10
|
+
slug: {
|
|
11
|
+
type: "string";
|
|
12
|
+
};
|
|
13
|
+
version: {
|
|
14
|
+
type: "string";
|
|
15
|
+
};
|
|
16
|
+
type: {
|
|
17
|
+
type: "string";
|
|
18
|
+
};
|
|
19
|
+
'filename-format': {
|
|
20
|
+
type: "string";
|
|
21
|
+
};
|
|
22
|
+
output: {
|
|
23
|
+
type: "string";
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
run({ values, positionals, cwd, logger }: import("./command.ts").CommandContext): Promise<{
|
|
27
|
+
exitCode: number;
|
|
28
|
+
result: {
|
|
29
|
+
checksumsPath: string;
|
|
30
|
+
files: number;
|
|
31
|
+
};
|
|
32
|
+
}>;
|
|
33
|
+
};
|
|
34
|
+
export default _default;
|