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.
@@ -0,0 +1,9 @@
1
+ # Directories to ignore
2
+ node_modules
3
+ dist
4
+ build
5
+ .git
6
+ .idea
7
+ .vscode
8
+ *.tmp
9
+ temp-*
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
+ }
@@ -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
+ });