pengushell 1.0.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 +0 -0
- package/README.md +195 -0
- package/bin/pengushell.js +5 -0
- package/package.json +39 -0
- package/src/commands/filesystem.js +266 -0
- package/src/commands/nano.js +250 -0
- package/src/commands/search.js +98 -0
- package/src/commands/system.js +66 -0
- package/src/config/defaults.js +70 -0
- package/src/core/executor.js +110 -0
- package/src/core/lexer.js +61 -0
- package/src/core/parser.js +160 -0
- package/src/core/repl.js +148 -0
- package/src/index.js +142 -0
- package/src/registry/commandRegistry.js +24 -0
- package/src/utils/colors.js +29 -0
- package/src/utils/pathNormalizer.js +56 -0
package/LICENSE
ADDED
|
File without changes
|
package/README.md
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# 🐧 PenguShell (pengushell)
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
[](https://www.npmjs.com/package/pengushell)
|
|
5
|
+
|
|
6
|
+
A lightweight compatibility layer for Windows PowerShell that allows developers to run typical Linux/Unix commands natively. Written entirely in Node.js, PenguShell requires no external Linux binaries, WSL, or virtual machines, making it the perfect developer-experience enhancement tool on Windows.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🚀 Features
|
|
11
|
+
|
|
12
|
+
- 🟢 **Native Node.js Implementation**: Completely portable cross-platform JavaScript under the hood.
|
|
13
|
+
- 🔀 **Pipes & Streams**: Full standard stream pipe (`|`) support to chain commands (e.g. `cat file.log | grep Error`).
|
|
14
|
+
- 📁 **Smart Path Conversion**: Automatic translation between Windows paths (e.g., `C:\Users\...`) and Linux style path syntax (e.g., `/c/users/...`).
|
|
15
|
+
- ⚙️ **Recursive Alias Engine**: Expand shortcuts dynamically (e.g. `ll` to `ls -l`, `la` to `ls -la`) with built-in loop/cycle detection.
|
|
16
|
+
- 🛠️ **Raw Mode Text Editor**: Built-in `nano` (MVP clone) with cursor navigation, modifications tracking, and safe file saving.
|
|
17
|
+
- 🎨 **Hacker Green UI**: Highly readable, premium retro terminal styling powered by `chalk` with user toggling.
|
|
18
|
+
- 🔍 **POSIX-Grade Grep**: Highly optimized searching, regular expressions support, matching highlights, line numbers, counting, and inversion.
|
|
19
|
+
- ⌨️ **Tab Autocomplete**: Interactive command and file/folder path completions in the REPL.
|
|
20
|
+
- 📜 **Persistent Session History**: Commands logged to `%APPDATA%/pengushell/history.log` to persist shell history across sessions.
|
|
21
|
+
- 📂 **Wildcard Globbing**: Native pattern expansion (`*` and `?` wildcard operators) integrated directly into the parser.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
To use PenguShell globally anywhere in your terminal, install it via NPM:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# Install globally
|
|
31
|
+
npm install -g pengushell
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Alternatively, you can run it directly using `npx`:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx pengushell
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## 🎮 Usage
|
|
43
|
+
|
|
44
|
+
PenguShell can be run in two modes:
|
|
45
|
+
|
|
46
|
+
### 1. Interactive REPL Mode
|
|
47
|
+
Running `pengushell` with no arguments starts an interactive session:
|
|
48
|
+
```bash
|
|
49
|
+
pengushell
|
|
50
|
+
```
|
|
51
|
+
**Example output:**
|
|
52
|
+
```
|
|
53
|
+
username@hostname:~$ ls -l
|
|
54
|
+
drw-rw-rw- 1 user group 0 30/05/2026, 17:40:00 bin
|
|
55
|
+
-rw-rw-rw- 1 user group 726 30/05/2026, 17:40:00 package.json
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 2. Direct Execution Mode
|
|
59
|
+
Run a one-off compatibility command or pipeline and exit with standard codes:
|
|
60
|
+
```bash
|
|
61
|
+
pengushell ls -la
|
|
62
|
+
pengushell "cat package.json | grep chalk"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 3. Extended Shell Features
|
|
66
|
+
PenguShell provides several built-in desktop terminal experiences natively:
|
|
67
|
+
|
|
68
|
+
* **Native Wildcard Globbing**: Use standard pattern matching (`*` and `?` operators) for file parameters:
|
|
69
|
+
```bash
|
|
70
|
+
# List all files ending in .json
|
|
71
|
+
pengushell ls *.json
|
|
72
|
+
|
|
73
|
+
# Find references to "chalk" in any package file
|
|
74
|
+
pengushell "grep chalk package*.json"
|
|
75
|
+
```
|
|
76
|
+
* **Command & Path Autocomplete**: In interactive REPL mode, press `Tab` once to view matching commands or double-tab to complete directory and file paths:
|
|
77
|
+
```bash
|
|
78
|
+
user@hostname:~$ cd s[Tab]
|
|
79
|
+
# Completes automatically to:
|
|
80
|
+
user@hostname:~$ cd src/
|
|
81
|
+
```
|
|
82
|
+
* **Persistent Command History**: Command navigation is saved automatically. Hit the `Up` and `Down` arrow keys in the REPL to browse commands run in past terminal sessions. Logs are saved inside `%APPDATA%/pengushell/history.log`.
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## 🛠️ Commands Reference
|
|
87
|
+
|
|
88
|
+
| Command | Options | Description |
|
|
89
|
+
| :--- | :--- | :--- |
|
|
90
|
+
| **`ls`** | `-a`, `-l` | Lists directory contents with permissions, owner, sizes, and timestamps |
|
|
91
|
+
| **`cd`** | `[path]` | Changes the current working directory (supports `~` home directory) |
|
|
92
|
+
| **`pwd`** | None | Prints the current directory in Unix format |
|
|
93
|
+
| **`cat`** | `-n` | Concatenates and displays files (reads from standard input if empty) |
|
|
94
|
+
| **`mkdir`**| `-p` | Creates directories (ignores existing directories with `-p`) |
|
|
95
|
+
| **`rm`** | `-r`, `-f` | Removes files and directories recursively and/or forcefully |
|
|
96
|
+
| **`cp`** | `-r` | Copies files and directories recursively |
|
|
97
|
+
| **`mv`** | None | Moves files and directories (handles cross-drive `EXDEV` copy-and-delete fallbacks) |
|
|
98
|
+
| **`touch`**| None | Creates new files or updates access and modification timestamps |
|
|
99
|
+
| **`grep`** | `-i`, `-v`, `-c`, `-n`, `-F` | Searches text streams/files using regular expressions or literals |
|
|
100
|
+
| **`nano`** | `[file]` | MVP text editor with raw terminal inputs, save (`^O`), and exit (`^X`) |
|
|
101
|
+
| **`clear`**| None | Resets the terminal screen output |
|
|
102
|
+
| **`echo`** | `-n` | Prints text (omits trailing newline with `-n`) |
|
|
103
|
+
| **`whoami`**| None | Outputs the current logged-in user name |
|
|
104
|
+
| **`hostname`**| None | Prints the system host name |
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## ⚙️ Configuration
|
|
109
|
+
|
|
110
|
+
PenguShell generates a config file at:
|
|
111
|
+
- **Windows**: `%APPDATA%/pengushell/config.json`
|
|
112
|
+
- **macOS/Linux**: `~/.config/pengushell/config.json`
|
|
113
|
+
|
|
114
|
+
### Interactive Configuration
|
|
115
|
+
You can manage settings interactively:
|
|
116
|
+
```bash
|
|
117
|
+
pengushell config
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### CLI Config Options
|
|
121
|
+
```bash
|
|
122
|
+
# View configuration settings and path
|
|
123
|
+
pengushell config list
|
|
124
|
+
|
|
125
|
+
# View only the config file location
|
|
126
|
+
pengushell config path
|
|
127
|
+
|
|
128
|
+
# Modify settings directly
|
|
129
|
+
pengushell config set useColors false
|
|
130
|
+
pengushell config set hackerGreenMode true
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## 💻 Development & Testing
|
|
136
|
+
|
|
137
|
+
### Setup
|
|
138
|
+
Clone the repository, install dependencies, and run:
|
|
139
|
+
```bash
|
|
140
|
+
git clone https://github.com/aneesh-srivastava-11/PenguShell.git
|
|
141
|
+
cd PenguShell
|
|
142
|
+
npm install
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Run Tests
|
|
146
|
+
PenguShell includes a comprehensive test suite targeting lexing, parsing, configs, and command outputs:
|
|
147
|
+
```bash
|
|
148
|
+
npm test
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Local CLI Linking
|
|
152
|
+
To link the command globally for local testing:
|
|
153
|
+
```bash
|
|
154
|
+
npm link
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 📤 Publishing Instructions
|
|
160
|
+
|
|
161
|
+
To publish your own version of `pengushell` to the NPM registry, execute:
|
|
162
|
+
|
|
163
|
+
1. **Log in to NPM**:
|
|
164
|
+
```bash
|
|
165
|
+
npm login
|
|
166
|
+
```
|
|
167
|
+
2. **Ensure Tests Pass**:
|
|
168
|
+
```bash
|
|
169
|
+
npm test
|
|
170
|
+
```
|
|
171
|
+
3. **Bump Version** (Select major, minor, or patch):
|
|
172
|
+
```bash
|
|
173
|
+
npm version patch
|
|
174
|
+
```
|
|
175
|
+
4. **Publish Package**:
|
|
176
|
+
```bash
|
|
177
|
+
npm publish --access public
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## 🤝 Contributing
|
|
183
|
+
|
|
184
|
+
Contributions are welcome! Please follow these guidelines:
|
|
185
|
+
1. Fork the repository.
|
|
186
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`).
|
|
187
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`).
|
|
188
|
+
4. Push to the branch (`git push origin feature/amazing-feature`).
|
|
189
|
+
5. Open a Pull Request.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## 📄 License
|
|
194
|
+
|
|
195
|
+
Distributed under the MIT License. See `LICENSE` for more information.
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pengushell",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A professional developer-grade Linux command compatibility layer for Windows PowerShell.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"pengushell": "bin/pengushell.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node tests/run.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"shell",
|
|
15
|
+
"powershell",
|
|
16
|
+
"windows",
|
|
17
|
+
"linux",
|
|
18
|
+
"compatibility",
|
|
19
|
+
"bash-on-windows"
|
|
20
|
+
],
|
|
21
|
+
"author": "Developer",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=16.7.0"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"chalk": "^4.1.2",
|
|
28
|
+
"commander": "^11.1.0",
|
|
29
|
+
"ora": "^5.4.1",
|
|
30
|
+
"prompts": "^2.4.2",
|
|
31
|
+
"zod": "^3.23.8"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"bin",
|
|
35
|
+
"src",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
]
|
|
39
|
+
}
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
const { toLinuxStyle, toWinStyle } = require('../utils/pathNormalizer');
|
|
5
|
+
const { colorizePath, chalk } = require('../utils/colors');
|
|
6
|
+
|
|
7
|
+
function resolvePath(targetPath) {
|
|
8
|
+
if (!targetPath) return process.cwd();
|
|
9
|
+
return path.resolve(toWinStyle(targetPath));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function getUnixPermissions(stat) {
|
|
13
|
+
const isDir = stat.isDirectory();
|
|
14
|
+
let perms = isDir ? 'd' : '-';
|
|
15
|
+
|
|
16
|
+
const mode = stat.mode;
|
|
17
|
+
perms += (mode & 0o400) ? 'r' : '-';
|
|
18
|
+
perms += (mode & 0o200) ? 'w' : '-';
|
|
19
|
+
perms += (mode & 0o100) ? 'x' : '-';
|
|
20
|
+
perms += (mode & 0o040) ? 'r' : '-';
|
|
21
|
+
perms += (mode & 0o020) ? 'w' : '-';
|
|
22
|
+
perms += (mode & 0o010) ? 'x' : '-';
|
|
23
|
+
perms += (mode & 0o004) ? 'r' : '-';
|
|
24
|
+
perms += (mode & 0o002) ? 'w' : '-';
|
|
25
|
+
perms += (mode & 0o001) ? 'x' : '-';
|
|
26
|
+
|
|
27
|
+
return perms;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
ls: {
|
|
32
|
+
name: 'ls',
|
|
33
|
+
execute({ args, flags, stdout }) {
|
|
34
|
+
const targets = args.length > 0 ? args : ['.'];
|
|
35
|
+
const results = [];
|
|
36
|
+
|
|
37
|
+
for (const arg of targets) {
|
|
38
|
+
const target = resolvePath(arg);
|
|
39
|
+
if (!fs.existsSync(target)) {
|
|
40
|
+
throw new Error(`ls: cannot access '${arg}': No such file or directory`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const stat = fs.statSync(target);
|
|
44
|
+
if (!stat.isDirectory()) {
|
|
45
|
+
if (flags.l) {
|
|
46
|
+
const size = stat.size.toString().padStart(8);
|
|
47
|
+
const perms = getUnixPermissions(stat);
|
|
48
|
+
const nameColored = chalk.green(path.basename(target));
|
|
49
|
+
results.push(`${perms} 1 user group ${size} ${stat.mtime.toLocaleString()} ${nameColored}`);
|
|
50
|
+
} else {
|
|
51
|
+
results.push(chalk.green(path.basename(target)));
|
|
52
|
+
}
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let entries = fs.readdirSync(target);
|
|
57
|
+
if (!flags.a) {
|
|
58
|
+
entries = entries.filter(e => !e.startsWith('.'));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const header = targets.length > 1 ? `${arg}:\n` : '';
|
|
62
|
+
const listOutput = [];
|
|
63
|
+
|
|
64
|
+
if (flags.l) {
|
|
65
|
+
for (const e of entries) {
|
|
66
|
+
const p = path.join(target, e);
|
|
67
|
+
let s;
|
|
68
|
+
try {
|
|
69
|
+
s = fs.statSync(p);
|
|
70
|
+
} catch (err) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const isDir = s.isDirectory();
|
|
74
|
+
const size = s.size.toString().padStart(8);
|
|
75
|
+
const perms = getUnixPermissions(s);
|
|
76
|
+
const nameColored = isDir ? colorizePath(e) : chalk.green(e);
|
|
77
|
+
listOutput.push(`${perms} 1 user group ${size} ${s.mtime.toLocaleString()} ${nameColored}`);
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
const coloredEntries = entries.map(e => {
|
|
81
|
+
const p = path.join(target, e);
|
|
82
|
+
let isDir = false;
|
|
83
|
+
try {
|
|
84
|
+
isDir = fs.statSync(p).isDirectory();
|
|
85
|
+
} catch (err) {}
|
|
86
|
+
return isDir ? colorizePath(e) : chalk.green(e);
|
|
87
|
+
});
|
|
88
|
+
listOutput.push(coloredEntries.join(' '));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
results.push(header + listOutput.join('\n'));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
stdout.write(results.join('\n\n') + '\n');
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
cat: {
|
|
99
|
+
name: 'cat',
|
|
100
|
+
execute({ args, flags, stdin, stdout }) {
|
|
101
|
+
if (args.length === 0) {
|
|
102
|
+
return new Promise((resolve) => {
|
|
103
|
+
const rl = readline.createInterface({
|
|
104
|
+
input: stdin,
|
|
105
|
+
crlfDelay: Infinity
|
|
106
|
+
});
|
|
107
|
+
let lineIdx = 1;
|
|
108
|
+
rl.on('line', (line) => {
|
|
109
|
+
if (flags.n) {
|
|
110
|
+
stdout.write(` ${lineIdx++} ${line}\n`);
|
|
111
|
+
} else {
|
|
112
|
+
stdout.write(line + '\n');
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
rl.on('close', () => {
|
|
116
|
+
resolve();
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const f of args) {
|
|
122
|
+
const target = resolvePath(f);
|
|
123
|
+
if (!fs.existsSync(target)) {
|
|
124
|
+
throw new Error(`cat: ${f}: No such file or directory`);
|
|
125
|
+
}
|
|
126
|
+
if (fs.statSync(target).isDirectory()) {
|
|
127
|
+
throw new Error(`cat: ${f}: Is a directory`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const content = fs.readFileSync(target, 'utf8');
|
|
131
|
+
if (flags.n) {
|
|
132
|
+
const lines = content.split(/\r?\n/);
|
|
133
|
+
lines.forEach((l, idx) => stdout.write(` ${idx + 1} ${l}\n`));
|
|
134
|
+
} else {
|
|
135
|
+
stdout.write(content + '\n');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
mkdir: {
|
|
142
|
+
name: 'mkdir',
|
|
143
|
+
execute({ args, flags }) {
|
|
144
|
+
if (args.length === 0) throw new Error(`mkdir: missing operand`);
|
|
145
|
+
for (const target of args) {
|
|
146
|
+
const fullPath = resolvePath(target);
|
|
147
|
+
try {
|
|
148
|
+
fs.mkdirSync(fullPath, { recursive: !!flags.p });
|
|
149
|
+
} catch (err) {
|
|
150
|
+
if (err.code === 'EEXIST' && flags.p) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
throw new Error(`mkdir: cannot create directory '${target}': ${err.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
rm: {
|
|
160
|
+
name: 'rm',
|
|
161
|
+
execute({ args, flags }) {
|
|
162
|
+
if (args.length === 0) throw new Error(`rm: missing operand`);
|
|
163
|
+
for (const target of args) {
|
|
164
|
+
const fullPath = resolvePath(target);
|
|
165
|
+
if (!fs.existsSync(fullPath)) {
|
|
166
|
+
if (flags.f) continue;
|
|
167
|
+
throw new Error(`rm: cannot remove '${target}': No such file or directory`);
|
|
168
|
+
}
|
|
169
|
+
const st = fs.statSync(fullPath);
|
|
170
|
+
if (st.isDirectory()) {
|
|
171
|
+
if (!flags.r && !flags.R) {
|
|
172
|
+
throw new Error(`rm: cannot remove '${target}': Is a directory`);
|
|
173
|
+
}
|
|
174
|
+
fs.rmSync(fullPath, { recursive: true, force: !!flags.f });
|
|
175
|
+
} else {
|
|
176
|
+
fs.rmSync(fullPath, { force: !!flags.f });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
touch: {
|
|
183
|
+
name: 'touch',
|
|
184
|
+
execute({ args }) {
|
|
185
|
+
if (args.length === 0) throw new Error(`touch: missing file operand`);
|
|
186
|
+
for (const target of args) {
|
|
187
|
+
const fullPath = resolvePath(target);
|
|
188
|
+
const time = new Date();
|
|
189
|
+
try {
|
|
190
|
+
if (fs.existsSync(fullPath)) {
|
|
191
|
+
fs.utimesSync(fullPath, time, time);
|
|
192
|
+
} else {
|
|
193
|
+
const parentDir = path.dirname(fullPath);
|
|
194
|
+
if (!fs.existsSync(parentDir)) {
|
|
195
|
+
throw new Error(`No such file or directory`);
|
|
196
|
+
}
|
|
197
|
+
fs.closeSync(fs.openSync(fullPath, 'w'));
|
|
198
|
+
}
|
|
199
|
+
} catch (err) {
|
|
200
|
+
throw new Error(`touch: cannot touch '${target}': ${err.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
},
|
|
205
|
+
|
|
206
|
+
cp: {
|
|
207
|
+
name: 'cp',
|
|
208
|
+
execute({ args, flags }) {
|
|
209
|
+
if (args.length < 2) throw new Error(`cp: missing file operand`);
|
|
210
|
+
const dest = resolvePath(args[args.length - 1]);
|
|
211
|
+
const sources = args.slice(0, args.length - 1);
|
|
212
|
+
|
|
213
|
+
const isDestDir = fs.existsSync(dest) && fs.statSync(dest).isDirectory();
|
|
214
|
+
if (sources.length > 1 && !isDestDir) {
|
|
215
|
+
throw new Error(`cp: target '${args[args.length - 1]}' is not a directory`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
for (const src of sources) {
|
|
219
|
+
const srcPath = resolvePath(src);
|
|
220
|
+
if (!fs.existsSync(srcPath)) {
|
|
221
|
+
throw new Error(`cp: cannot stat '${src}': No such file or directory`);
|
|
222
|
+
}
|
|
223
|
+
const st = fs.statSync(srcPath);
|
|
224
|
+
const destPath = isDestDir ? path.join(dest, path.basename(srcPath)) : dest;
|
|
225
|
+
if (st.isDirectory()) {
|
|
226
|
+
if (!flags.r && !flags.R) throw new Error(`cp: -r not specified; omitting directory '${src}'`);
|
|
227
|
+
fs.cpSync(srcPath, destPath, { recursive: true });
|
|
228
|
+
} else {
|
|
229
|
+
fs.copyFileSync(srcPath, destPath);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
mv: {
|
|
236
|
+
name: 'mv',
|
|
237
|
+
execute({ args }) {
|
|
238
|
+
if (args.length < 2) throw new Error(`mv: missing file operand`);
|
|
239
|
+
const dest = resolvePath(args[args.length - 1]);
|
|
240
|
+
const sources = args.slice(0, args.length - 1);
|
|
241
|
+
|
|
242
|
+
const isDestDir = fs.existsSync(dest) && fs.statSync(dest).isDirectory();
|
|
243
|
+
if (sources.length > 1 && !isDestDir) {
|
|
244
|
+
throw new Error(`mv: target '${args[args.length - 1]}' is not a directory`);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const src of sources) {
|
|
248
|
+
const srcPath = resolvePath(src);
|
|
249
|
+
if (!fs.existsSync(srcPath)) {
|
|
250
|
+
throw new Error(`mv: cannot stat '${src}': No such file or directory`);
|
|
251
|
+
}
|
|
252
|
+
const destPath = isDestDir ? path.join(dest, path.basename(srcPath)) : dest;
|
|
253
|
+
try {
|
|
254
|
+
fs.renameSync(srcPath, destPath);
|
|
255
|
+
} catch (err) {
|
|
256
|
+
if (err.code === 'EXDEV') {
|
|
257
|
+
fs.cpSync(srcPath, destPath, { recursive: true });
|
|
258
|
+
fs.rmSync(srcPath, { recursive: true, force: true });
|
|
259
|
+
} else {
|
|
260
|
+
throw new Error(`mv: cannot move '${src}' to '${destPath}': ${err.message}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
};
|