batch-exec-cli 1.2.1 → 1.3.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/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # batch-exec
1
+ # batch-exec-cli
2
2
 
3
3
  高效批量命令执行工具,能够遍历目录内所有直接子目录并执行命令。
4
4
 
5
5
  ## 功能特性
6
6
 
7
- - 🚀 高效遍历目标目录的所有直接子目录
7
+ - 🚀 高效遍历目标目录的所有直接子目录, 默认并行执行
8
8
  - 📁 支持绝对路径和相对路径
9
9
  - 🚫 可配置忽略目录(支持 `.gitignore` 风格的模式匹配)
10
10
  - 📊 提供执行摘要和失败目录列表
@@ -17,14 +17,14 @@
17
17
  ## 安装
18
18
 
19
19
  ```bash
20
- npm install -g batch-exec
20
+ npm install -g batch-exec-cli
21
21
  ```
22
22
 
23
23
  或者克隆项目后本地安装:
24
24
 
25
25
  ```bash
26
26
  git clone <repository-url>
27
- cd batch-exec
27
+ cd batch-exec-cli
28
28
  npm install
29
29
  npm link
30
30
  ```
@@ -64,6 +64,7 @@ batch-exec ./repos ls -la
64
64
  | `-s, --skip <文件>` | | 指定忽略文件路径(默认:`./.batchexecignore`) |
65
65
  | `-v, --verbose` | | 显示详细输出 |
66
66
  | `--no-progress` | | 禁用进度条显示 |
67
+ | `--no-parallel` | | 禁用并行执行, 按顺序执行 |
67
68
  | `-h, --help` | | 显示帮助信息 |
68
69
 
69
70
  ### 使用自定义忽略文件
@@ -128,7 +129,7 @@ temp-*
128
129
  你也可以作为库使用:
129
130
 
130
131
  ```javascript
131
- import { batchExecute } from 'batch-exec';
132
+ import { batchExecute } from 'batch-exec-cli';
132
133
 
133
134
  const results = await batchExecute('./my-projects', 'git', ['pull'], {
134
135
  verbose: false,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "batch-exec-cli",
3
- "version": "1.2.1",
4
- "description": "Efficiently iterate through directories and execute commands with progress display",
3
+ "version": "1.3.0",
4
+ "description": "Efficiently iterate through directories and execute commands with progress display and parallel execution",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
7
7
  "bin": {
@@ -11,6 +11,9 @@
11
11
  "test": "echo 'Tests require Node.js >= 18.0.0 with --test support'",
12
12
  "lint": "eslint src/ test/"
13
13
  },
14
+ "files": [
15
+ "src"
16
+ ],
14
17
  "keywords": [
15
18
  "batch-exec",
16
19
  "batchexec",
package/src/cli.js CHANGED
@@ -10,7 +10,7 @@ $.verbose = false;
10
10
 
11
11
  async function main() {
12
12
  const argv = minimist(process.argv.slice(2), {
13
- boolean: ['v', 'verbose', 'h', 'help', 'no-progress'],
13
+ boolean: ['v', 'verbose', 'h', 'help', 'no-progress', 'no-parallel'],
14
14
  string: ['s', 'skip'],
15
15
  alias: {
16
16
  s: 'skip',
@@ -45,6 +45,7 @@ async function main() {
45
45
  console.log(bold('\n🚀 Batch Executor\n'));
46
46
  console.log(`Target directory: ${cyan(targetDir)}`);
47
47
  console.log(`Command: ${yellow(command)} ${args.join(' ')}`);
48
+ console.log(`Parallel mode: ${argv.parallel === false ? red('Disabled') : green('Enabled')}`);
48
49
  if (skipPaths.length > 0) {
49
50
  console.log(`Skipping directories: ${gray(skipPaths.join(', '))}`);
50
51
  }
@@ -55,7 +56,8 @@ async function main() {
55
56
  const results = await batchExecute(targetDir, command, args, {
56
57
  skipPaths,
57
58
  verbose: argv.verbose,
58
- showProgress: argv.progress !== false
59
+ showProgress: argv.progress !== false,
60
+ parallel: argv.parallel !== false
59
61
  });
60
62
 
61
63
  printSummary(results);
@@ -67,7 +69,7 @@ async function main() {
67
69
 
68
70
  function printHelp() {
69
71
  console.log(`
70
- ${bold('Batch Executor')} ${dim('v1.1.0')}
72
+ ${bold('Batch Executor')} ${dim('v1.3.0')}
71
73
 
72
74
  ${cyan('Usage:')} batch-exec [options] <directory> <command> [args...]
73
75
 
@@ -82,12 +84,14 @@ ${magenta('Options:')}
82
84
  -s, --skip <file> Ignore file path (default: ./.batchexecignore)
83
85
  -v, --verbose Show verbose output
84
86
  --no-progress Disable progress bar
87
+ --no-parallel Disable parallel execution (use sequential mode)
85
88
  -h, --help Show this help message
86
89
 
87
90
  ${green('Examples:')}
88
91
  ${green('batch-exec')} ./my-projects git pull
89
92
  ${green('batch-exec')} ./my-projects npm update lodash -S
90
93
  ${green('batch-exec')} --skip ./custom-ignore.txt ./repos ls -la
94
+ ${green('batch-exec')} --no-parallel ./my-projects npm install
91
95
  `);
92
96
  }
93
97
 
package/src/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import path from 'path';
2
- import { $, cd } from 'zx';
2
+ import { $, within, cd } from 'zx';
3
3
  import { parseIgnoreFile } from './ignoreParser.js';
4
4
  import { listDirectSubdirectories } from './directoryLister.js';
5
5
  import { cyan, red, ProgressBar, clearLine } from './utils/colors.js';
@@ -7,8 +7,55 @@ import { cyan, red, ProgressBar, clearLine } from './utils/colors.js';
7
7
  export { parseIgnoreFile };
8
8
  export { listDirectSubdirectories };
9
9
 
10
+ async function executeInDirectory(subdirPath, command, args, verbose) {
11
+ try {
12
+ if (verbose) {
13
+ console.log(`=== Executing in: ${cyan(subdirPath)} ===`);
14
+ }
15
+
16
+ let result;
17
+
18
+ await within(async () => {
19
+ cd(subdirPath);
20
+ if (verbose) {
21
+ result = await $`${command} ${args}`;
22
+ } else {
23
+ result = await $`${command} ${args}`.quiet();
24
+ }
25
+ if (verbose) {
26
+ console.log(`${cyan(subdirPath)}: `, result.stdout);
27
+ if (result.stderr) {
28
+ console.error(`${cyan(subdirPath)}: `, result.stderr);
29
+ }
30
+ }
31
+ });
32
+
33
+ return {
34
+ success: true,
35
+ stdout: result.stdout,
36
+ stderr: result.stderr
37
+ };
38
+ } catch (error) {
39
+ if (verbose) {
40
+ console.error(red(`Error in ${cyan(subdirPath)}: ${error.message}`));
41
+ if (error.stdout) {
42
+ console.log(`${cyan(subdirPath)}: `, error.stdout);
43
+ }
44
+ if (error.stderr) {
45
+ console.error(`${cyan(subdirPath)}: `, error.stderr);
46
+ }
47
+ }
48
+ return {
49
+ success: false,
50
+ error: error.message,
51
+ stdout: error.stdout || '',
52
+ stderr: error.stderr || ''
53
+ };
54
+ }
55
+ }
56
+
10
57
  export async function batchExecute(targetDir, command, args, options = {}) {
11
- const { skipPaths = [], verbose = false, showProgress = true } = options;
58
+ const { skipPaths = [], verbose = false, showProgress = true, parallel = true } = options;
12
59
 
13
60
  const absoluteTargetDir = path.resolve(targetDir);
14
61
 
@@ -22,60 +69,54 @@ export async function batchExecute(targetDir, command, args, options = {}) {
22
69
  progressBar.start();
23
70
  }
24
71
 
25
- for (let i = 0; i < subdirs.length; i++) {
26
- const subdir = subdirs[i];
27
- const subdirPath = path.join(absoluteTargetDir, subdir);
72
+ if (parallel) {
73
+ const promises = subdirs.map(async (subdir, index) => {
74
+ const subdirPath = path.join(absoluteTargetDir, subdir);
75
+ const result = await executeInDirectory(subdirPath, command, args, verbose);
28
76
 
29
- try {
30
- if (verbose) {
31
- console.log(`=== Executing in: ${cyan(subdirPath)} ===`);
77
+ if (progressBar) {
78
+ progressBar.increment();
32
79
  }
33
80
 
34
- let result;
81
+ return { directory: subdir, ...result };
82
+ });
35
83
 
36
- cd(subdirPath);
84
+ const resolvedResults = await Promise.all(promises);
37
85
 
38
- if (verbose) {
39
- result = await $`${command} ${args}`;
40
- } else {
41
- result = await $`${command} ${args}`.quiet();
42
- }
43
-
44
- results.push({
45
- directory: subdir,
46
- success: true,
47
- stdout: result.stdout,
48
- stderr: result.stderr
49
- });
50
-
51
- if (verbose) {
52
- console.log(result.stdout);
53
- if (result.stderr) {
54
- console.error(result.stderr);
55
- }
56
- }
57
- } catch (error) {
58
- results.push({
59
- directory: subdir,
60
- success: false,
61
- error: error.message,
62
- stdout: error.stdout || '',
63
- stderr: error.stderr || ''
64
- });
65
-
66
- if (verbose) {
67
- console.error(red(`Error in ${cyan(subdir)}: ${error.message}`));
68
- if (error.stdout) {
69
- console.log(error.stdout);
70
- }
71
- if (error.stderr) {
72
- console.error(error.stderr);
73
- }
86
+ for (const subdir of subdirs) {
87
+ const result = resolvedResults.find(r => r.directory === subdir);
88
+ if (result) {
89
+ results.push(result);
74
90
  }
75
91
  }
76
-
77
- if (progressBar) {
78
- progressBar.update(i + 1);
92
+ } else {
93
+ for (let i = 0; i < subdirs.length; i++) {
94
+ const subdir = subdirs[i];
95
+ const subdirPath = path.join(absoluteTargetDir, subdir);
96
+ const result = await executeInDirectory(subdirPath, command, args, verbose);
97
+
98
+ results.push({ directory: subdir, ...result });
99
+
100
+ // if (verbose) {
101
+ // if (result.success) {
102
+ // console.log(result.stdout);
103
+ // if (result.stderr) {
104
+ // console.error(result.stderr);
105
+ // }
106
+ // } else {
107
+ // console.error(red(`Error in ${cyan(subdir)}: ${result.error}`));
108
+ // if (result.stdout) {
109
+ // console.log(result.stdout);
110
+ // }
111
+ // if (result.stderr) {
112
+ // console.error(result.stderr);
113
+ // }
114
+ // }
115
+ // }
116
+
117
+ if (progressBar) {
118
+ progressBar.update(i + 1);
119
+ }
79
120
  }
80
121
  }
81
122
 
package/.batchexecignore DELETED
@@ -1,9 +0,0 @@
1
- # Directories to ignore
2
- node_modules
3
- dist
4
- build
5
- .git
6
- .idea
7
- .vscode
8
- *.tmp
9
- temp-*
package/CHANGELOG.md DELETED
@@ -1,13 +0,0 @@
1
- # Changelog
2
-
3
- All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
-
5
- ### [1.2.1](https://github.com/chandq/batch-exec/compare/v1.2.0...v1.2.1) (2026-03-01)
6
-
7
- ## 1.2.0 (2026-03-01)
8
-
9
-
10
- ### Features
11
-
12
- * add colors highlight 240bf16
13
- * add progress display by using --no-progress 320a372
package/test/cli.test.js DELETED
@@ -1,70 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert';
3
- import fs from 'fs/promises';
4
- import path from 'path';
5
- import os from 'os';
6
- import { $ } from 'zx';
7
-
8
- describe('CLI Integration', () => {
9
- let tempDir;
10
- let testProjectsDir;
11
-
12
- beforeEach(async () => {
13
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'batch-exec-cli-test-'));
14
- testProjectsDir = path.join(tempDir, 'test-projects');
15
-
16
- await fs.mkdir(testProjectsDir);
17
- await fs.mkdir(path.join(testProjectsDir, 'project1'));
18
- await fs.mkdir(path.join(testProjectsDir, 'project2'));
19
- await fs.mkdir(path.join(testProjectsDir, 'node_modules'));
20
-
21
- await fs.writeFile(path.join(tempDir, '.batchexecignore'), 'node_modules');
22
- });
23
-
24
- afterEach(async () => {
25
- await fs.rm(tempDir, { recursive: true, force: true });
26
- });
27
-
28
- it('should show help message with --help', async () => {
29
- const result = await $`node ${path.join(process.cwd(), 'src/cli.js')} --help`;
30
- assert(result.stdout.includes('Usage:'));
31
- assert(result.stdout.includes('batch-exec'));
32
- });
33
-
34
- it('should execute command in subdirectories', async () => {
35
- const result = await $`node ${path.join(process.cwd(), 'src/cli.js')} ${testProjectsDir} echo test`;
36
- assert(result.stdout.includes('Summary:'));
37
- assert(result.stdout.includes('Total directories: 2'));
38
- });
39
-
40
- it('should respect .batchexecignore file', async () => {
41
- const result = await $`node ${path.join(process.cwd(), 'src/cli.js')} ${testProjectsDir} pwd`;
42
- assert(result.stdout.includes('Total directories: 2'));
43
- assert(!result.stdout.includes('node_modules'));
44
- });
45
-
46
- it('should work with custom ignore file using --skip', async () => {
47
- const customIgnore = path.join(tempDir, 'custom-ignore');
48
- await fs.writeFile(customIgnore, 'project1\nnode_modules');
49
-
50
- const result = await $`node ${path.join(
51
- process.cwd(),
52
- 'src/cli.js'
53
- )} --skip ${customIgnore} ${testProjectsDir} pwd`;
54
- assert(result.stdout.includes('Total directories: 1'));
55
- assert(!result.stdout.includes('project1'));
56
- });
57
-
58
- it('should show verbose output with --verbose', async () => {
59
- const result = await $`node ${path.join(process.cwd(), 'src/cli.js')} --verbose ${testProjectsDir} echo hello`;
60
- assert(result.stdout.includes('Target directory:'));
61
- assert(result.stdout.includes('Command:'));
62
- });
63
-
64
- it('should fail with error message when missing arguments', async () => {
65
- await assert.rejects($`node ${path.join(process.cwd(), 'src/cli.js')}`, error => {
66
- assert(error.stderr.includes('Missing required arguments'));
67
- return true;
68
- });
69
- });
70
- });
@@ -1,68 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert';
3
- import fs from 'fs/promises';
4
- import path from 'path';
5
- import os from 'os';
6
- import { listDirectSubdirectories } from '../src/directoryLister.js';
7
-
8
- describe('directoryLister', () => {
9
- let tempDir;
10
-
11
- beforeEach(async () => {
12
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'batch-exec-test-'));
13
-
14
- await fs.mkdir(path.join(tempDir, 'dir1'));
15
- await fs.mkdir(path.join(tempDir, 'dir2'));
16
- await fs.mkdir(path.join(tempDir, 'node_modules'));
17
- await fs.mkdir(path.join(tempDir, '.git'));
18
-
19
- await fs.writeFile(path.join(tempDir, 'file1.txt'), 'content');
20
- await fs.writeFile(path.join(tempDir, 'file2.js'), 'content');
21
- });
22
-
23
- afterEach(async () => {
24
- await fs.rm(tempDir, { recursive: true, force: true });
25
- });
26
-
27
- describe('listDirectSubdirectories', () => {
28
- it('should list only direct subdirectories', async () => {
29
- const subdirs = await listDirectSubdirectories(tempDir);
30
- assert.deepStrictEqual(subdirs.sort(), ['.git', 'dir1', 'dir2', 'node_modules'].sort());
31
- });
32
-
33
- it('should skip directories matching patterns', async () => {
34
- const subdirs = await listDirectSubdirectories(tempDir, ['node_modules', '.git']);
35
- assert.deepStrictEqual(subdirs, ['dir1', 'dir2']);
36
- });
37
-
38
- it('should skip directories with wildcard patterns', async () => {
39
- const subdirs = await listDirectSubdirectories(tempDir, ['dir*']);
40
- assert.deepStrictEqual(subdirs.sort(), ['.git', 'node_modules'].sort());
41
- });
42
-
43
- it('should return sorted directory names', async () => {
44
- const subdirs = await listDirectSubdirectories(tempDir);
45
- assert.deepStrictEqual(subdirs, subdirs.slice().sort());
46
- });
47
-
48
- it('should throw error for non-existent directory', async () => {
49
- await assert.rejects(
50
- listDirectSubdirectories(path.join(tempDir, 'non-existent')),
51
- { message: /Directory not found/ }
52
- );
53
- });
54
-
55
- it('should throw error for file path', async () => {
56
- await assert.rejects(
57
- listDirectSubdirectories(path.join(tempDir, 'file1.txt')),
58
- { message: /Not a directory/ }
59
- );
60
- });
61
-
62
- it('should work with relative paths', async () => {
63
- const relativePath = path.relative(process.cwd(), tempDir);
64
- const subdirs = await listDirectSubdirectories(relativePath);
65
- assert.deepStrictEqual(subdirs.sort(), ['.git', 'dir1', 'dir2', 'node_modules'].sort());
66
- });
67
- });
68
- });
@@ -1,93 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert';
3
- import fs from 'fs/promises';
4
- import path from 'path';
5
- import os from 'os';
6
- import { parseIgnoreFile, shouldSkipDirectory } from '../src/ignoreParser.js';
7
-
8
- describe('ignoreParser', () => {
9
- let tempDir;
10
-
11
- beforeEach(async () => {
12
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'batch-exec-test-'));
13
- });
14
-
15
- afterEach(async () => {
16
- await fs.rm(tempDir, { recursive: true, force: true });
17
- });
18
-
19
- describe('parseIgnoreFile', () => {
20
- it('should parse ignore file correctly', async () => {
21
- const ignoreFilePath = path.join(tempDir, '.testignore');
22
- await fs.writeFile(ignoreFilePath, `
23
- # This is a comment
24
- node_modules
25
- dist/
26
- *.tmp
27
- test-*
28
-
29
- # Another comment
30
- build
31
- `.trim());
32
-
33
- const patterns = await parseIgnoreFile(ignoreFilePath);
34
- assert.deepStrictEqual(patterns, [
35
- 'node_modules',
36
- 'dist/',
37
- '*.tmp',
38
- 'test-*',
39
- 'build'
40
- ]);
41
- });
42
-
43
- it('should return empty array for non-existent file', async () => {
44
- const patterns = await parseIgnoreFile(path.join(tempDir, 'non-existent'));
45
- assert.deepStrictEqual(patterns, []);
46
- });
47
-
48
- it('should return empty array for null or undefined', async () => {
49
- const patterns1 = await parseIgnoreFile(null);
50
- const patterns2 = await parseIgnoreFile(undefined);
51
- assert.deepStrictEqual(patterns1, []);
52
- assert.deepStrictEqual(patterns2, []);
53
- });
54
- });
55
-
56
- describe('shouldSkipDirectory', () => {
57
- it('should return false when no patterns provided', () => {
58
- assert.strictEqual(shouldSkipDirectory('dir1', []), false);
59
- assert.strictEqual(shouldSkipDirectory('dir1', null), false);
60
- assert.strictEqual(shouldSkipDirectory('dir1', undefined), false);
61
- });
62
-
63
- it('should match exact directory names', () => {
64
- assert.strictEqual(shouldSkipDirectory('node_modules', ['node_modules']), true);
65
- assert.strictEqual(shouldSkipDirectory('dist', ['node_modules']), false);
66
- });
67
-
68
- it('should match directory names with trailing slash', () => {
69
- assert.strictEqual(shouldSkipDirectory('dist', ['dist/']), true);
70
- assert.strictEqual(shouldSkipDirectory('node_modules', ['dist/']), false);
71
- });
72
-
73
- it('should match wildcard patterns', () => {
74
- assert.strictEqual(shouldSkipDirectory('test-123', ['test-*']), true);
75
- assert.strictEqual(shouldSkipDirectory('test-abc', ['test-*']), true);
76
- assert.strictEqual(shouldSkipDirectory('other', ['test-*']), false);
77
- });
78
-
79
- it('should match extension wildcards', () => {
80
- assert.strictEqual(shouldSkipDirectory('file.tmp', ['*.tmp']), true);
81
- assert.strictEqual(shouldSkipDirectory('test.tmp', ['*.tmp']), true);
82
- assert.strictEqual(shouldSkipDirectory('test.txt', ['*.tmp']), false);
83
- });
84
-
85
- it('should match any of the patterns', () => {
86
- const patterns = ['node_modules', 'dist/', '*.tmp'];
87
- assert.strictEqual(shouldSkipDirectory('node_modules', patterns), true);
88
- assert.strictEqual(shouldSkipDirectory('dist', patterns), true);
89
- assert.strictEqual(shouldSkipDirectory('test.tmp', patterns), true);
90
- assert.strictEqual(shouldSkipDirectory('src', patterns), false);
91
- });
92
- });
93
- });
@@ -1,70 +0,0 @@
1
- import { describe, it, beforeEach, afterEach } from 'node:test';
2
- import assert from 'node:assert';
3
- import fs from 'fs/promises';
4
- import path from 'path';
5
- import os from 'os';
6
- import { batchExecute } from '../src/index.js';
7
-
8
- describe('batchExecute', () => {
9
- let tempDir;
10
-
11
- beforeEach(async () => {
12
- tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'batch-exec-test-'));
13
-
14
- await fs.mkdir(path.join(tempDir, 'dir1'));
15
- await fs.mkdir(path.join(tempDir, 'dir2'));
16
- await fs.mkdir(path.join(tempDir, 'skip-me'));
17
- });
18
-
19
- afterEach(async () => {
20
- await fs.rm(tempDir, { recursive: true, force: true });
21
- });
22
-
23
- it('should execute command in all subdirectories', async () => {
24
- const results = await batchExecute(tempDir, 'pwd', []);
25
-
26
- assert.strictEqual(results.length, 3);
27
-
28
- results.forEach(result => {
29
- assert.strictEqual(result.success, true);
30
- });
31
- });
32
-
33
- it('should skip specified directories', async () => {
34
- const results = await batchExecute(tempDir, 'pwd', [], {
35
- skipPaths: ['skip-me']
36
- });
37
-
38
- assert.strictEqual(results.length, 2);
39
-
40
- const dirs = results.map(r => r.directory);
41
- assert.deepStrictEqual(dirs.sort(), ['dir1', 'dir2'].sort());
42
- });
43
-
44
- it('should capture command output', async () => {
45
- const results = await batchExecute(tempDir, 'echo', ['hello']);
46
-
47
- results.forEach(result => {
48
- assert.strictEqual(result.success, true);
49
- assert(result.stdout.includes('hello'));
50
- });
51
- });
52
-
53
- it('should handle command failures gracefully', async () => {
54
- const results = await batchExecute(tempDir, 'this-command-does-not-exist', []);
55
-
56
- results.forEach(result => {
57
- assert.strictEqual(result.success, false);
58
- assert(result.error);
59
- });
60
- });
61
-
62
- it('should work with multiple arguments', async () => {
63
- const results = await batchExecute(tempDir, 'echo', ['hello', 'world']);
64
-
65
- results.forEach(result => {
66
- assert.strictEqual(result.success, true);
67
- assert(result.stdout.includes('hello world'));
68
- });
69
- });
70
- });