batch-exec-cli 1.2.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/.batchexecignore +9 -0
- package/CHANGELOG.md +11 -0
- package/README.md +143 -0
- package/package.json +39 -0
- package/src/cli.js +124 -0
- package/src/directoryLister.js +31 -0
- package/src/ignoreParser.js +52 -0
- package/src/index.js +89 -0
- package/src/utils/colors.js +209 -0
- package/test/cli.test.js +70 -0
- package/test/directoryLister.test.js +68 -0
- package/test/ignoreParser.test.js +93 -0
- package/test/index.test.js +70 -0
package/.batchexecignore
ADDED
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
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.0 (2026-03-01)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* add colors highlight 240bf16
|
|
11
|
+
* add progress display by using --no-progress 320a372
|
package/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# batch-exec
|
|
2
|
+
|
|
3
|
+
高效批量命令执行工具,能够遍历目录内所有直接子目录并执行命令。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- 🚀 高效遍历目标目录的所有直接子目录
|
|
8
|
+
- 📁 支持绝对路径和相对路径
|
|
9
|
+
- 🚫 可配置忽略目录(支持 `.gitignore` 风格的模式匹配)
|
|
10
|
+
- 📊 提供执行摘要和失败目录列表
|
|
11
|
+
- 🔧 跨平台支持(Windows、macOS、Linux)
|
|
12
|
+
- 💬 详细的 verbose 输出模式
|
|
13
|
+
- 🎨 彩色高亮输出,便于识别目录路径和命令
|
|
14
|
+
- ⏳ 实时进度条显示,带旋转动画和执行时间
|
|
15
|
+
- ✨ 精美的输出格式和摘要展示
|
|
16
|
+
|
|
17
|
+
## 安装
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install -g batch-exec
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
或者克隆项目后本地安装:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
git clone <repository-url>
|
|
27
|
+
cd batch-exec
|
|
28
|
+
npm install
|
|
29
|
+
npm link
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 使用方法
|
|
33
|
+
|
|
34
|
+
### 基本用法
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
batch-exec <目录> <命令> [参数...]
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### 示例
|
|
41
|
+
|
|
42
|
+
在 `./my-projects` 目录下的所有子目录中执行 `git pull`:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
batch-exec ./my-projects git pull
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
在 `./my-projects` 目录下的所有子目录中更新 lodash 依赖:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
batch-exec ./my-projects npm update lodash -S
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
列出所有子目录的内容:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
batch-exec ./repos ls -la
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 选项
|
|
61
|
+
|
|
62
|
+
| 选项 | 别名 | 描述 |
|
|
63
|
+
| ------------------- | ---- | ---------------------------------------------- |
|
|
64
|
+
| `-s, --skip <文件>` | | 指定忽略文件路径(默认:`./.batchexecignore`) |
|
|
65
|
+
| `-v, --verbose` | | 显示详细输出 |
|
|
66
|
+
| `--no-progress` | | 禁用进度条显示 |
|
|
67
|
+
| `-h, --help` | | 显示帮助信息 |
|
|
68
|
+
|
|
69
|
+
### 使用自定义忽略文件
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
batch-exec --skip ./custom-ignore.txt ./repos git status
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 禁用进度条
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
batch-exec --no-progress ./my-projects npm install
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 显示详细输出
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
batch-exec -v ./my-projects git status
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## 输出示例
|
|
88
|
+
|
|
89
|
+
### 普通模式(带进度条)
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
⠋ [████████████████████████████░░] 85% (17/20) [5s]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### 摘要展示
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
═══════════════════════════════════════════════════════════════
|
|
99
|
+
📊 Execution Summary
|
|
100
|
+
═══════════════════════════════════════════════════════════════
|
|
101
|
+
Total directories: 20
|
|
102
|
+
Successful: 18
|
|
103
|
+
Failed: 2
|
|
104
|
+
|
|
105
|
+
❌ Failed directories:
|
|
106
|
+
• project1: Error: Command failed
|
|
107
|
+
• project3: Error: Permission denied
|
|
108
|
+
═══════════════════════════════════════════════════════════════
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## .batchexecignore 文件格式
|
|
112
|
+
|
|
113
|
+
与 `.gitignore` 文件格式完全相同:
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
node_modules
|
|
117
|
+
dist
|
|
118
|
+
build
|
|
119
|
+
.git
|
|
120
|
+
.idea
|
|
121
|
+
.vscode
|
|
122
|
+
*.tmp
|
|
123
|
+
temp-*
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## API 使用
|
|
127
|
+
|
|
128
|
+
你也可以作为库使用:
|
|
129
|
+
|
|
130
|
+
```javascript
|
|
131
|
+
import { batchExecute } from 'batch-exec';
|
|
132
|
+
|
|
133
|
+
const results = await batchExecute('./my-projects', 'git', ['pull'], {
|
|
134
|
+
verbose: false,
|
|
135
|
+
showProgress: true
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
console.log(results);
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 许可证
|
|
142
|
+
|
|
143
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "batch-exec-cli",
|
|
3
|
+
"version": "1.2.0",
|
|
4
|
+
"description": "Efficiently iterate through directories and execute commands with progress display",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"batch-exec": "src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "echo 'Tests require Node.js >= 18.0.0 with --test support'",
|
|
12
|
+
"lint": "eslint src/ test/"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"batch",
|
|
16
|
+
"execute",
|
|
17
|
+
"command",
|
|
18
|
+
"directory",
|
|
19
|
+
"zx",
|
|
20
|
+
"progress",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/chandq/batch-exec.git"
|
|
26
|
+
},
|
|
27
|
+
"author": "",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"minimist": "^1.2.8",
|
|
31
|
+
"zx": "^8.8.5"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"eslint": "^8.57.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=12.17.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import minimist from 'minimist';
|
|
5
|
+
import { $ } from 'zx';
|
|
6
|
+
import { batchExecute, parseIgnoreFile } from './index.js';
|
|
7
|
+
import { cyan, yellow, green, red, gray, bold, dim, magenta, blue } from './utils/colors.js';
|
|
8
|
+
|
|
9
|
+
$.verbose = false;
|
|
10
|
+
|
|
11
|
+
async function main() {
|
|
12
|
+
const argv = minimist(process.argv.slice(2), {
|
|
13
|
+
boolean: ['v', 'verbose', 'h', 'help', 'no-progress'],
|
|
14
|
+
string: ['s', 'skip'],
|
|
15
|
+
alias: {
|
|
16
|
+
s: 'skip',
|
|
17
|
+
v: 'verbose',
|
|
18
|
+
h: 'help'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
if (argv.help) {
|
|
23
|
+
printHelp();
|
|
24
|
+
process.exit(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const [targetDir, command, ...args] = argv._;
|
|
28
|
+
|
|
29
|
+
if (!targetDir || !command) {
|
|
30
|
+
console.error(red('Error: Missing required arguments'));
|
|
31
|
+
printHelp();
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let ignoreFilePath = argv.skip;
|
|
36
|
+
|
|
37
|
+
if (!ignoreFilePath) {
|
|
38
|
+
const defaultIgnorePath = path.join(process.cwd(), '.batchexecignore');
|
|
39
|
+
ignoreFilePath = defaultIgnorePath;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skipPaths = await parseIgnoreFile(ignoreFilePath);
|
|
43
|
+
|
|
44
|
+
if (argv.verbose) {
|
|
45
|
+
console.log(bold('\n🚀 Batch Executor\n'));
|
|
46
|
+
console.log(`Target directory: ${cyan(targetDir)}`);
|
|
47
|
+
console.log(`Command: ${yellow(command)} ${args.join(' ')}`);
|
|
48
|
+
if (skipPaths.length > 0) {
|
|
49
|
+
console.log(`Skipping directories: ${gray(skipPaths.join(', '))}`);
|
|
50
|
+
}
|
|
51
|
+
console.log(gray('----------------------------------------\n'));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const results = await batchExecute(targetDir, command, args, {
|
|
56
|
+
skipPaths,
|
|
57
|
+
verbose: argv.verbose,
|
|
58
|
+
showProgress: argv.progress !== false
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
printSummary(results);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error(red(`\nError: ${error.message}\n`));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function printHelp() {
|
|
69
|
+
console.log(`
|
|
70
|
+
${bold('Batch Executor')} ${dim('v1.1.0')}
|
|
71
|
+
|
|
72
|
+
${cyan('Usage:')} batch-exec [options] <directory> <command> [args...]
|
|
73
|
+
|
|
74
|
+
Efficiently iterate through all direct subdirectories of a directory and execute a command.
|
|
75
|
+
|
|
76
|
+
${blue('Arguments:')}
|
|
77
|
+
${cyan('<directory>')} Target directory (absolute or relative path)
|
|
78
|
+
${yellow('<command>')} Command to execute in each subdirectory
|
|
79
|
+
[args...] Optional arguments for the command
|
|
80
|
+
|
|
81
|
+
${magenta('Options:')}
|
|
82
|
+
-s, --skip <file> Ignore file path (default: ./.batchexecignore)
|
|
83
|
+
-v, --verbose Show verbose output
|
|
84
|
+
--no-progress Disable progress bar
|
|
85
|
+
-h, --help Show this help message
|
|
86
|
+
|
|
87
|
+
${green('Examples:')}
|
|
88
|
+
${green('batch-exec')} ./my-projects git pull
|
|
89
|
+
${green('batch-exec')} ./my-projects npm update lodash -S
|
|
90
|
+
${green('batch-exec')} --skip ./custom-ignore.txt ./repos ls -la
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function printSummary(results) {
|
|
95
|
+
const successCount = results.filter(r => r.success).length;
|
|
96
|
+
const failureCount = results.filter(r => !r.success).length;
|
|
97
|
+
|
|
98
|
+
console.log(bold('\n═══════════════════════════════════════════════════════════════'));
|
|
99
|
+
console.log(bold('📊 Execution Summary'));
|
|
100
|
+
console.log('═══════════════════════════════════════════════════════════════');
|
|
101
|
+
console.log(` Total directories: ${bold(results.length.toString())}`);
|
|
102
|
+
console.log(` Successful: ${green(bold(successCount.toString()))}`);
|
|
103
|
+
console.log(` Failed: ${failureCount > 0 ? red(bold(failureCount.toString())) : '0'}`);
|
|
104
|
+
|
|
105
|
+
if (failureCount > 0) {
|
|
106
|
+
console.log('\n' + red(bold('❌ Failed directories:')));
|
|
107
|
+
results
|
|
108
|
+
.filter(r => !r.success)
|
|
109
|
+
.forEach(r => {
|
|
110
|
+
console.log(` ${cyan('•')} ${cyan(r.directory)}: ${red(r.error)}`);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (successCount > 0 && failureCount === 0) {
|
|
115
|
+
console.log('\n' + green(bold('✅ All operations completed successfully!')));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
console.log('═══════════════════════════════════════════════════════════════\n');
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
main().catch(error => {
|
|
122
|
+
console.error(red('Fatal error:'), error);
|
|
123
|
+
process.exit(1);
|
|
124
|
+
});
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { shouldSkipDirectory } from './ignoreParser.js';
|
|
4
|
+
|
|
5
|
+
export async function listDirectSubdirectories(targetDir, skipPatterns = []) {
|
|
6
|
+
const absoluteTargetDir = path.resolve(targetDir);
|
|
7
|
+
|
|
8
|
+
try {
|
|
9
|
+
const entries = await fs.readdir(absoluteTargetDir, { withFileTypes: true });
|
|
10
|
+
|
|
11
|
+
const subdirs = [];
|
|
12
|
+
|
|
13
|
+
for (const entry of entries) {
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
if (!shouldSkipDirectory(entry.name, skipPatterns)) {
|
|
16
|
+
subdirs.push(entry.name);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return subdirs.sort();
|
|
22
|
+
} catch (error) {
|
|
23
|
+
if (error.code === 'ENOENT') {
|
|
24
|
+
throw new Error(`Directory not found: ${absoluteTargetDir}`);
|
|
25
|
+
}
|
|
26
|
+
if (error.code === 'ENOTDIR') {
|
|
27
|
+
throw new Error(`Not a directory: ${absoluteTargetDir}`);
|
|
28
|
+
}
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
|
|
4
|
+
export async function parseIgnoreFile(ignoreFilePath) {
|
|
5
|
+
if (!ignoreFilePath) {
|
|
6
|
+
return [];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const content = await fs.readFile(ignoreFilePath, 'utf-8');
|
|
11
|
+
const lines = content.split('\n');
|
|
12
|
+
|
|
13
|
+
return lines
|
|
14
|
+
.map(line => line.trim())
|
|
15
|
+
.filter(line => line && !line.startsWith('#'));
|
|
16
|
+
} catch (error) {
|
|
17
|
+
if (error.code === 'ENOENT') {
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function shouldSkipDirectory(dirName, skipPatterns) {
|
|
25
|
+
if (!skipPatterns || skipPatterns.length === 0) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return skipPatterns.some(pattern => {
|
|
30
|
+
if (pattern === dirName) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (pattern.endsWith('/')) {
|
|
35
|
+
const patternWithoutSlash = pattern.slice(0, -1);
|
|
36
|
+
if (patternWithoutSlash === dirName) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (pattern.includes('*')) {
|
|
42
|
+
const regexPattern = pattern
|
|
43
|
+
.replace(/\./g, '\\.')
|
|
44
|
+
.replace(/\*/g, '.*')
|
|
45
|
+
.replace(/\?/g, '.');
|
|
46
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
47
|
+
return regex.test(dirName);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return false;
|
|
51
|
+
});
|
|
52
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import { $, cd } from 'zx';
|
|
3
|
+
import { parseIgnoreFile } from './ignoreParser.js';
|
|
4
|
+
import { listDirectSubdirectories } from './directoryLister.js';
|
|
5
|
+
import { cyan, red, ProgressBar, clearLine } from './utils/colors.js';
|
|
6
|
+
|
|
7
|
+
export { parseIgnoreFile };
|
|
8
|
+
export { listDirectSubdirectories };
|
|
9
|
+
|
|
10
|
+
export async function batchExecute(targetDir, command, args, options = {}) {
|
|
11
|
+
const { skipPaths = [], verbose = false, showProgress = true } = options;
|
|
12
|
+
|
|
13
|
+
const absoluteTargetDir = path.resolve(targetDir);
|
|
14
|
+
|
|
15
|
+
const subdirs = await listDirectSubdirectories(absoluteTargetDir, skipPaths);
|
|
16
|
+
|
|
17
|
+
const results = [];
|
|
18
|
+
let progressBar = null;
|
|
19
|
+
|
|
20
|
+
if (showProgress && subdirs.length > 0) {
|
|
21
|
+
progressBar = new ProgressBar(subdirs.length);
|
|
22
|
+
progressBar.start();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
for (let i = 0; i < subdirs.length; i++) {
|
|
26
|
+
const subdir = subdirs[i];
|
|
27
|
+
const subdirPath = path.join(absoluteTargetDir, subdir);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
if (verbose) {
|
|
31
|
+
console.log(`=== Executing in: ${cyan(subdirPath)} ===`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let result;
|
|
35
|
+
|
|
36
|
+
cd(subdirPath);
|
|
37
|
+
|
|
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
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (progressBar) {
|
|
78
|
+
progressBar.update(i + 1);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (progressBar) {
|
|
83
|
+
progressBar.stop();
|
|
84
|
+
} else if (!verbose) {
|
|
85
|
+
clearLine();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return results;
|
|
89
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
const colors = {
|
|
2
|
+
reset: '\x1b[0m',
|
|
3
|
+
bright: '\x1b[1m',
|
|
4
|
+
dim: '\x1b[2m',
|
|
5
|
+
underscore: '\x1b[4m',
|
|
6
|
+
blink: '\x1b[5m',
|
|
7
|
+
reverse: '\x1b[7m',
|
|
8
|
+
hidden: '\x1b[8m',
|
|
9
|
+
|
|
10
|
+
fg: {
|
|
11
|
+
black: '\x1b[30m',
|
|
12
|
+
red: '\x1b[31m',
|
|
13
|
+
green: '\x1b[32m',
|
|
14
|
+
yellow: '\x1b[33m',
|
|
15
|
+
blue: '\x1b[34m',
|
|
16
|
+
magenta: '\x1b[35m',
|
|
17
|
+
cyan: '\x1b[36m',
|
|
18
|
+
white: '\x1b[37m',
|
|
19
|
+
gray: '\x1b[90m'
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
bg: {
|
|
23
|
+
black: '\x1b[40m',
|
|
24
|
+
red: '\x1b[41m',
|
|
25
|
+
green: '\x1b[42m',
|
|
26
|
+
yellow: '\x1b[43m',
|
|
27
|
+
blue: '\x1b[44m',
|
|
28
|
+
magenta: '\x1b[45m',
|
|
29
|
+
cyan: '\x1b[46m',
|
|
30
|
+
white: '\x1b[47m'
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function wrap(color, text) {
|
|
35
|
+
return `${color}${text}${colors.reset}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function cyan(text) {
|
|
39
|
+
return wrap(colors.fg.cyan, text);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function yellow(text) {
|
|
43
|
+
return wrap(colors.fg.yellow, text);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function green(text) {
|
|
47
|
+
return wrap(colors.fg.green, text);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function red(text) {
|
|
51
|
+
return wrap(colors.fg.red, text);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function gray(text) {
|
|
55
|
+
return wrap(colors.fg.gray, text);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function blue(text) {
|
|
59
|
+
return wrap(colors.fg.blue, text);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function magenta(text) {
|
|
63
|
+
return wrap(colors.fg.magenta, text);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function white(text) {
|
|
67
|
+
return wrap(colors.fg.white, text);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function bold(text) {
|
|
71
|
+
return wrap(colors.bright, text);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function dim(text) {
|
|
75
|
+
return wrap(colors.dim, text);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function underline(text) {
|
|
79
|
+
return wrap(colors.underscore, text);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function bgGreen(text) {
|
|
83
|
+
return wrap(colors.bg.green, text);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function bgRed(text) {
|
|
87
|
+
return wrap(colors.bg.red, text);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function bgYellow(text) {
|
|
91
|
+
return wrap(colors.bg.yellow, text);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function bgCyan(text) {
|
|
95
|
+
return wrap(colors.bg.cyan, text);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
99
|
+
|
|
100
|
+
export class ProgressBar {
|
|
101
|
+
constructor(total, options = {}) {
|
|
102
|
+
this.total = total;
|
|
103
|
+
this.current = 0;
|
|
104
|
+
this.startTime = Date.now();
|
|
105
|
+
this.lastUpdate = 0;
|
|
106
|
+
this.spinnerIndex = 0;
|
|
107
|
+
this.spinnerInterval = null;
|
|
108
|
+
this.options = {
|
|
109
|
+
width: 40,
|
|
110
|
+
showSpinner: true,
|
|
111
|
+
showPercentage: true,
|
|
112
|
+
showCount: true,
|
|
113
|
+
showElapsed: true,
|
|
114
|
+
...options
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
start() {
|
|
119
|
+
if (this.options.showSpinner) {
|
|
120
|
+
this.spinnerInterval = setInterval(() => {
|
|
121
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % spinnerFrames.length;
|
|
122
|
+
this.render(false);
|
|
123
|
+
}, 100);
|
|
124
|
+
}
|
|
125
|
+
this.render(true);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
update(current) {
|
|
129
|
+
this.current = current;
|
|
130
|
+
this.render(true);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
increment() {
|
|
134
|
+
this.current++;
|
|
135
|
+
this.render(true);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
stop() {
|
|
139
|
+
if (this.spinnerInterval) {
|
|
140
|
+
clearInterval(this.spinnerInterval);
|
|
141
|
+
this.spinnerInterval = null;
|
|
142
|
+
}
|
|
143
|
+
this.render(true);
|
|
144
|
+
console.log();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
render(clear = false) {
|
|
148
|
+
const now = Date.now();
|
|
149
|
+
if (!clear && now - this.lastUpdate < 50) return;
|
|
150
|
+
this.lastUpdate = now;
|
|
151
|
+
|
|
152
|
+
const percentage = this.total > 0 ? Math.floor((this.current / this.total) * 100) : 0;
|
|
153
|
+
const filledWidth = this.total > 0 ? Math.floor((this.current / this.total) * this.options.width) : 0;
|
|
154
|
+
const emptyWidth = this.options.width - filledWidth;
|
|
155
|
+
|
|
156
|
+
const bar = '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
|
|
157
|
+
const spinner = this.spinnerInterval ? spinnerFrames[this.spinnerIndex] : '✓';
|
|
158
|
+
|
|
159
|
+
let output = '';
|
|
160
|
+
|
|
161
|
+
if (this.options.showSpinner) {
|
|
162
|
+
output += `${cyan(spinner)} `;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
output += `[${green(bar)}]`;
|
|
166
|
+
|
|
167
|
+
if (this.options.showPercentage) {
|
|
168
|
+
output += ` ${bold(`${percentage}%`)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.options.showCount) {
|
|
172
|
+
output += ` ${dim(`(${this.current}/${this.total})`)}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (this.options.showElapsed) {
|
|
176
|
+
const elapsed = Math.floor((now - this.startTime) / 1000);
|
|
177
|
+
output += ` ${gray(`[${elapsed}s]`)}`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (clear) {
|
|
181
|
+
process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80));
|
|
182
|
+
}
|
|
183
|
+
process.stdout.write('\r' + output);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function clearLine() {
|
|
188
|
+
process.stdout.write('\r' + ' '.repeat(process.stdout.columns || 80) + '\r');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function moveCursorUp(count = 1) {
|
|
192
|
+
process.stdout.write(`\x1b[${count}A`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function moveCursorDown(count = 1) {
|
|
196
|
+
process.stdout.write(`\x1b[${count}B`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function eraseToEnd() {
|
|
200
|
+
process.stdout.write('\x1b[K');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function saveCursor() {
|
|
204
|
+
process.stdout.write('\x1b[s');
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function restoreCursor() {
|
|
208
|
+
process.stdout.write('\x1b[u');
|
|
209
|
+
}
|
package/test/cli.test.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
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
|
+
});
|