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 ADDED
File without changes
package/README.md ADDED
@@ -0,0 +1,195 @@
1
+ # 🐧 PenguShell (pengushell)
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![NPM Version](https://img.shields.io/npm/v/pengushell.svg)](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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { main } = require('../src/index');
4
+
5
+ main();
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
+ };