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.
@@ -0,0 +1,250 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { chalk } = require('../utils/colors');
4
+ const { toWinStyle } = require('../utils/pathNormalizer');
5
+
6
+ const KEYS = {
7
+ CTRL_X: '\u0018',
8
+ CTRL_O: '\u000f',
9
+ CTRL_C: '\u0003',
10
+ UP: '\u001b[A',
11
+ DOWN: '\u001b[B',
12
+ RIGHT: '\u001b[C',
13
+ LEFT: '\u001b[D',
14
+ BACKSPACE: '\b',
15
+ BACKSPACE2: '\x7f',
16
+ ENTER: '\r',
17
+ DELETE: '\u001b[3~'
18
+ };
19
+
20
+ module.exports = {
21
+ nano: {
22
+ name: 'nano',
23
+ execute({ args, stdin, stdout }) {
24
+ return new Promise((resolve, reject) => {
25
+ let filePath = args[0];
26
+ let fullPath = null;
27
+ let buffer = [''];
28
+ let cursorX = 0;
29
+ let cursorY = 0;
30
+ let isModified = false;
31
+ let promptMode = false;
32
+ let promptMessage = '';
33
+ let promptCallback = null;
34
+ let statusMessage = '';
35
+
36
+ if (filePath) {
37
+ fullPath = path.resolve(toWinStyle(filePath));
38
+ if (fs.existsSync(fullPath)) {
39
+ try {
40
+ const content = fs.readFileSync(fullPath, 'utf8');
41
+ buffer = content.split(/\r?\n/);
42
+ if (buffer.length === 0) buffer = [''];
43
+ } catch (e) {
44
+ reject(new Error(`nano: ${e.message}`));
45
+ return;
46
+ }
47
+ }
48
+ }
49
+
50
+ const getTerminalSize = () => {
51
+ return {
52
+ columns: stdout.columns || 80,
53
+ rows: stdout.rows || 24
54
+ };
55
+ };
56
+
57
+ const render = () => {
58
+ stdout.write('\x1bc');
59
+ const { columns, rows } = getTerminalSize();
60
+
61
+ const header = ` PENGU NANO 1.0 ${filePath || 'New Buffer'} ${isModified ? '*' : ' '}`;
62
+ stdout.write(chalk.bgBlack.greenBright(header.padEnd(columns)) + '\n');
63
+
64
+ const availableRows = rows - 4;
65
+ for (let i = 0; i < availableRows; i++) {
66
+ if (i < buffer.length) {
67
+ stdout.write(`${buffer[i]}\x1b[K\n`);
68
+ } else {
69
+ stdout.write(`\x1b[K\n`);
70
+ }
71
+ }
72
+
73
+ if (promptMode) {
74
+ stdout.write(`\x1b[K\n${chalk.greenBright(promptMessage)}\n`);
75
+ } else {
76
+ const footerText = statusMessage || `^X Exit ^O Save ^C Cancel`;
77
+ stdout.write(`\x1b[K\n${chalk.bgBlack.greenBright(footerText.padEnd(columns))}\n`);
78
+ }
79
+
80
+ stdout.write(`\x1b[${cursorY + 2};${cursorX + 1}H`);
81
+ };
82
+
83
+ const exitNano = () => {
84
+ if (stdin.isTTY) stdin.setRawMode(false);
85
+ stdin.removeListener('data', onData);
86
+ stdout.write('\x1bc');
87
+ resolve();
88
+ };
89
+
90
+ const saveFile = () => {
91
+ if (!fullPath) {
92
+ promptMode = true;
93
+ promptMessage = 'File Name to Write: ';
94
+ promptCallback = (input) => {
95
+ if (input) {
96
+ filePath = input;
97
+ fullPath = path.resolve(toWinStyle(filePath));
98
+ try {
99
+ fs.writeFileSync(fullPath, buffer.join('\n'));
100
+ isModified = false;
101
+ statusMessage = `[ Wrote ${buffer.length} lines ]`;
102
+ } catch (err) {
103
+ statusMessage = `[ Error saving file: ${err.message} ]`;
104
+ }
105
+ }
106
+ promptMode = false;
107
+ render();
108
+ setTimeout(() => { statusMessage = ''; render(); }, 2000);
109
+ };
110
+ } else {
111
+ try {
112
+ fs.writeFileSync(fullPath, buffer.join('\n'));
113
+ isModified = false;
114
+ statusMessage = `[ Wrote ${buffer.length} lines ]`;
115
+ } catch (err) {
116
+ statusMessage = `[ Error saving file: ${err.message} ]`;
117
+ }
118
+ render();
119
+ setTimeout(() => { statusMessage = ''; render(); }, 2000);
120
+ }
121
+ };
122
+
123
+ if (stdin.isTTY) {
124
+ stdin.setRawMode(true);
125
+ }
126
+
127
+ let promptInputBuffer = '';
128
+
129
+ const onData = (data) => {
130
+ const char = data.toString('utf8');
131
+
132
+ if (promptMode) {
133
+ if (char === KEYS.ENTER) {
134
+ const ans = promptInputBuffer;
135
+ promptInputBuffer = '';
136
+ promptCallback(ans.trim());
137
+ } else if (char === KEYS.CTRL_C) {
138
+ promptMode = false;
139
+ promptInputBuffer = '';
140
+ render();
141
+ } else if (char === KEYS.BACKSPACE || char === KEYS.BACKSPACE2) {
142
+ promptInputBuffer = promptInputBuffer.slice(0, -1);
143
+ promptMessage = promptMessage.substring(0, promptMessage.lastIndexOf(':') + 2) + promptInputBuffer;
144
+ render();
145
+ } else {
146
+ if (char.length > 1 && char.startsWith('\u001b')) return;
147
+ promptInputBuffer += char;
148
+ promptMessage += char;
149
+ render();
150
+ }
151
+ return;
152
+ }
153
+
154
+ if (char === KEYS.CTRL_C) {
155
+ if (isModified) {
156
+ promptMode = true;
157
+ promptMessage = "Save modified buffer? (Y/N): ";
158
+ promptCallback = (ans) => {
159
+ if (ans.toLowerCase() === 'y') {
160
+ saveFile();
161
+ exitNano();
162
+ } else if (ans.toLowerCase() === 'n') {
163
+ exitNano();
164
+ } else {
165
+ promptMode = false;
166
+ render();
167
+ }
168
+ };
169
+ render();
170
+ } else {
171
+ exitNano();
172
+ }
173
+ } else if (char === KEYS.CTRL_X) {
174
+ if (isModified) {
175
+ promptMode = true;
176
+ promptMessage = "Save modified buffer? (Y/N): ";
177
+ promptCallback = (ans) => {
178
+ if (ans.toLowerCase() === 'y') {
179
+ saveFile();
180
+ exitNano();
181
+ } else if (ans.toLowerCase() === 'n') {
182
+ exitNano();
183
+ } else {
184
+ promptMode = false;
185
+ render();
186
+ }
187
+ };
188
+ render();
189
+ } else {
190
+ exitNano();
191
+ }
192
+ } else if (char === KEYS.CTRL_O) {
193
+ saveFile();
194
+ } else if (char === KEYS.UP) {
195
+ if (cursorY > 0) cursorY--;
196
+ cursorX = Math.min(cursorX, buffer[cursorY].length);
197
+ } else if (char === KEYS.DOWN) {
198
+ if (cursorY < buffer.length - 1) cursorY++;
199
+ cursorX = Math.min(cursorX, buffer[cursorY].length);
200
+ } else if (char === KEYS.LEFT) {
201
+ if (cursorX > 0) cursorX--;
202
+ else if (cursorY > 0) {
203
+ cursorY--;
204
+ cursorX = buffer[cursorY].length;
205
+ }
206
+ } else if (char === KEYS.RIGHT) {
207
+ if (cursorX < buffer[cursorY].length) cursorX++;
208
+ else if (cursorY < buffer.length - 1) {
209
+ cursorY++;
210
+ cursorX = 0;
211
+ }
212
+ } else if (char === KEYS.BACKSPACE || char === KEYS.BACKSPACE2) {
213
+ if (cursorX > 0) {
214
+ const line = buffer[cursorY];
215
+ buffer[cursorY] = line.slice(0, cursorX - 1) + line.slice(cursorX);
216
+ cursorX--;
217
+ isModified = true;
218
+ } else if (cursorY > 0) {
219
+ const prevLen = buffer[cursorY - 1].length;
220
+ buffer[cursorY - 1] += buffer[cursorY];
221
+ buffer.splice(cursorY, 1);
222
+ cursorY--;
223
+ cursorX = prevLen;
224
+ isModified = true;
225
+ }
226
+ } else if (char === KEYS.ENTER) {
227
+ const line = buffer[cursorY];
228
+ const nextLine = line.slice(cursorX);
229
+ buffer[cursorY] = line.slice(0, cursorX);
230
+ buffer.splice(cursorY + 1, 0, nextLine);
231
+ cursorY++;
232
+ cursorX = 0;
233
+ isModified = true;
234
+ } else {
235
+ if (char.length > 1 && char.startsWith('\u001b')) return;
236
+
237
+ const line = buffer[cursorY] || '';
238
+ buffer[cursorY] = line.slice(0, cursorX) + char + line.slice(cursorX);
239
+ cursorX += char.length;
240
+ isModified = true;
241
+ }
242
+ render();
243
+ };
244
+
245
+ render();
246
+ stdin.on('data', onData);
247
+ });
248
+ }
249
+ }
250
+ };
@@ -0,0 +1,98 @@
1
+ const fs = require('fs');
2
+ const readline = require('readline');
3
+ const { chalk } = require('../utils/colors');
4
+
5
+ function escapeRegExp(string) {
6
+ return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
8
+
9
+ module.exports = {
10
+ grep: {
11
+ name: 'grep',
12
+ async execute({ args, flags, stdin, stdout }) {
13
+ if (args.length === 0) {
14
+ throw new Error('grep: missing pattern');
15
+ }
16
+
17
+ let pattern = args[0];
18
+ const files = args.slice(1);
19
+
20
+ const isRegex = !flags.F;
21
+ if (!isRegex) {
22
+ pattern = escapeRegExp(pattern);
23
+ }
24
+
25
+ let regex;
26
+ try {
27
+ const regexOpts = flags.i ? 'gi' : 'g';
28
+ regex = new RegExp(pattern, regexOpts);
29
+ } catch (err) {
30
+ throw new Error(`grep: invalid regular expression: ${err.message}`);
31
+ }
32
+
33
+ const useColor = flags.color === 'always' || flags.color === 'auto' || (flags.color !== 'never' && chalk.level > 0);
34
+
35
+ const highlight = (line) => {
36
+ return line.replace(regex, match => chalk.red.bold(match));
37
+ };
38
+
39
+ const processStream = async (inputStream, prefix = '') => {
40
+ const rl = readline.createInterface({
41
+ input: inputStream,
42
+ crlfDelay: Infinity
43
+ });
44
+
45
+ let lineCounter = 1;
46
+ let matchCount = 0;
47
+
48
+ for await (const line of rl) {
49
+ regex.lastIndex = 0;
50
+ const matched = regex.test(line);
51
+ const shouldExclude = !!flags.v;
52
+
53
+ if (matched !== shouldExclude) {
54
+ matchCount++;
55
+ if (!flags.c) {
56
+ let outLine = flags.n ? `${chalk.yellow(lineCounter)}: ` : '';
57
+ outLine += useColor ? highlight(line) : line;
58
+ stdout.write(`${prefix}${outLine}\n`);
59
+ }
60
+ }
61
+ lineCounter++;
62
+ }
63
+
64
+ if (flags.c) {
65
+ stdout.write(`${prefix}${matchCount}\n`);
66
+ }
67
+
68
+ return matchCount > 0;
69
+ };
70
+
71
+ let exitWithSuccess = false;
72
+
73
+ if (files.length === 0) {
74
+ exitWithSuccess = await processStream(stdin);
75
+ } else {
76
+ const showPrefix = files.length > 1;
77
+ for (const file of files) {
78
+ if (!fs.existsSync(file)) {
79
+ stdout.write(`grep: ${file}: No such file or directory\n`);
80
+ continue;
81
+ }
82
+ if (fs.statSync(file).isDirectory()) {
83
+ stdout.write(`grep: ${file}: Is a directory\n`);
84
+ continue;
85
+ }
86
+ const stream = fs.createReadStream(file);
87
+ const pref = showPrefix ? `${chalk.blueBright(file)}: ` : '';
88
+ const hasMatches = await processStream(stream, pref);
89
+ if (hasMatches) {
90
+ exitWithSuccess = true;
91
+ }
92
+ }
93
+ }
94
+
95
+ process.exitCode = exitWithSuccess ? 0 : 1;
96
+ }
97
+ }
98
+ };
@@ -0,0 +1,66 @@
1
+ const os = require('os');
2
+ const path = require('path');
3
+ const { toLinuxStyle, toWinStyle } = require('../utils/pathNormalizer');
4
+
5
+ module.exports = {
6
+ pwd: {
7
+ name: 'pwd',
8
+ execute({ stdout }) {
9
+ stdout.write(toLinuxStyle(process.cwd()) + '\n');
10
+ }
11
+ },
12
+
13
+ cd: {
14
+ name: 'cd',
15
+ execute({ args }) {
16
+ let target = args[0];
17
+ try {
18
+ if (!target) {
19
+ target = os.homedir();
20
+ } else if (target === '~') {
21
+ target = os.homedir();
22
+ } else if (target.startsWith('~/') || target.startsWith('~\\')) {
23
+ target = path.join(os.homedir(), target.slice(2));
24
+ } else {
25
+ target = toWinStyle(target);
26
+ }
27
+ process.chdir(target);
28
+ } catch (err) {
29
+ throw new Error(`cd: ${target || ''}: No such file or directory`);
30
+ }
31
+ }
32
+ },
33
+
34
+ clear: {
35
+ name: 'clear',
36
+ execute() {
37
+ process.stdout.write('\x1bc');
38
+ }
39
+ },
40
+
41
+ echo: {
42
+ name: 'echo',
43
+ execute({ args, flags, stdout }) {
44
+ const output = args.join(' ');
45
+ if (flags.n) {
46
+ stdout.write(output);
47
+ } else {
48
+ stdout.write(output + '\n');
49
+ }
50
+ }
51
+ },
52
+
53
+ whoami: {
54
+ name: 'whoami',
55
+ execute({ stdout }) {
56
+ stdout.write(os.userInfo().username + '\n');
57
+ }
58
+ },
59
+
60
+ hostname: {
61
+ name: 'hostname',
62
+ execute({ stdout }) {
63
+ stdout.write(os.hostname() + '\n');
64
+ }
65
+ }
66
+ };
@@ -0,0 +1,70 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { z } = require('zod');
4
+
5
+ const appDataPath = process.env.APPDATA || (process.platform === 'darwin' ? path.join(process.env.HOME, 'Library/Preferences') : path.join(process.env.HOME, '.local/share'));
6
+ const configDir = path.join(appDataPath, 'pengushell');
7
+ const configFile = path.join(configDir, 'config.json');
8
+
9
+ // Define validation schema using Zod
10
+ const configSchema = z.object({
11
+ useColors: z.boolean().default(true),
12
+ hackerGreenMode: z.boolean().default(true),
13
+ aliases: z.record(z.string()).default({
14
+ "ll": "ls -l",
15
+ "la": "ls -la"
16
+ })
17
+ });
18
+
19
+ const defaultSettings = {
20
+ useColors: true,
21
+ hackerGreenMode: true,
22
+ aliases: {
23
+ "ll": "ls -l",
24
+ "la": "ls -la"
25
+ }
26
+ };
27
+
28
+ function loadConfig() {
29
+ if (!fs.existsSync(configDir)) {
30
+ try {
31
+ fs.mkdirSync(configDir, { recursive: true });
32
+ } catch (err) {
33
+ // Fallback
34
+ }
35
+ }
36
+ if (!fs.existsSync(configFile)) {
37
+ try {
38
+ fs.writeFileSync(configFile, JSON.stringify(defaultSettings, null, 2));
39
+ } catch (err) {
40
+ // Fallback
41
+ }
42
+ return defaultSettings;
43
+ }
44
+ try {
45
+ const data = fs.readFileSync(configFile, 'utf8');
46
+ const parsed = JSON.parse(data);
47
+ return configSchema.parse(parsed);
48
+ } catch (err) {
49
+ return defaultSettings;
50
+ }
51
+ }
52
+
53
+ function saveConfig(settings) {
54
+ try {
55
+ if (!fs.existsSync(configDir)) {
56
+ fs.mkdirSync(configDir, { recursive: true });
57
+ }
58
+ const validated = configSchema.parse(settings);
59
+ fs.writeFileSync(configFile, JSON.stringify(validated, null, 2));
60
+ return true;
61
+ } catch (err) {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ module.exports = {
67
+ loadConfig,
68
+ saveConfig,
69
+ configFile
70
+ };
@@ -0,0 +1,110 @@
1
+ const { getCommand } = require('../registry/commandRegistry');
2
+ const { PassThrough } = require('stream');
3
+ const { spawn } = require('child_process');
4
+
5
+ function runSystemFallback(parsedParams, stdin, stdout, stderr, isSingleCommand) {
6
+ return new Promise((resolve) => {
7
+ const stdioOpt = isSingleCommand
8
+ ? ['inherit', 'inherit', 'inherit']
9
+ : ['pipe', 'pipe', 'inherit'];
10
+
11
+ const child = spawn(parsedParams.command, parsedParams.rawArgs, {
12
+ stdio: stdioOpt,
13
+ shell: true
14
+ });
15
+
16
+ if (!isSingleCommand) {
17
+ if (stdin !== process.stdin && stdin) {
18
+ stdin.pipe(child.stdin);
19
+ }
20
+
21
+ child.stdout.on('data', (data) => {
22
+ stdout.write(data);
23
+ });
24
+ }
25
+
26
+ child.on('close', (code) => {
27
+ if (!isSingleCommand && stdout !== process.stdout && typeof stdout.end === 'function') {
28
+ stdout.end();
29
+ }
30
+ resolve(code);
31
+ });
32
+
33
+ child.on('error', (err) => {
34
+ stderr.write(`pengushell: command not found: ${parsedParams.command}\n`);
35
+ if (!isSingleCommand && stdout !== process.stdout && typeof stdout.end === 'function') {
36
+ stdout.end();
37
+ }
38
+ resolve(1);
39
+ });
40
+ });
41
+ }
42
+
43
+ async function executePipeline(pipeline, processStdin, processStdout) {
44
+ if (!pipeline || pipeline.length === 0) return;
45
+
46
+ if (pipeline.length === 1) {
47
+ const parsedParams = pipeline[0];
48
+ const cmd = getCommand(parsedParams.command);
49
+
50
+ if (!cmd) {
51
+ await runSystemFallback(parsedParams, processStdin, processStdout, processStdout, true);
52
+ return;
53
+ }
54
+
55
+ await cmd.execute({
56
+ args: parsedParams.args,
57
+ rawArgs: parsedParams.rawArgs,
58
+ flags: parsedParams.flags,
59
+ stdin: processStdin,
60
+ stdout: processStdout,
61
+ stderr: processStdout
62
+ });
63
+ return;
64
+ }
65
+
66
+ const streams = [processStdin];
67
+ for (let i = 0; i < pipeline.length - 1; i++) {
68
+ streams.push(new PassThrough());
69
+ }
70
+ streams.push(processStdout);
71
+
72
+ const promises = [];
73
+ for (let i = 0; i < pipeline.length; i++) {
74
+ const parsedParams = pipeline[i];
75
+ const cmd = getCommand(parsedParams.command);
76
+ const cmdStdin = streams[i];
77
+ const cmdStdout = streams[i + 1];
78
+
79
+ if (!cmd) {
80
+ promises.push(runSystemFallback(parsedParams, cmdStdin, cmdStdout, processStdout, false));
81
+ } else {
82
+ promises.push(
83
+ Promise.resolve()
84
+ .then(() => cmd.execute({
85
+ args: parsedParams.args,
86
+ rawArgs: parsedParams.rawArgs,
87
+ flags: parsedParams.flags,
88
+ stdin: cmdStdin,
89
+ stdout: cmdStdout,
90
+ stderr: processStdout
91
+ }))
92
+ .then(() => {
93
+ if (cmdStdout !== processStdout && typeof cmdStdout.end === 'function') {
94
+ cmdStdout.end();
95
+ }
96
+ })
97
+ .catch((err) => {
98
+ if (cmdStdout !== processStdout && typeof cmdStdout.end === 'function') {
99
+ cmdStdout.end();
100
+ }
101
+ throw err;
102
+ })
103
+ );
104
+ }
105
+ }
106
+
107
+ await Promise.all(promises);
108
+ }
109
+
110
+ module.exports = { executePipeline };
@@ -0,0 +1,61 @@
1
+ // A simple lexer to split respecting double quotes, single quotes, and escapes
2
+ function tokenize(input) {
3
+ const tokens = [];
4
+ let currentToken = '';
5
+ let inDoubleQuotes = false;
6
+ let inSingleQuotes = false;
7
+ let escapeNext = false;
8
+
9
+ for (let i = 0; i < input.length; i++) {
10
+ const char = input[i];
11
+
12
+ if (escapeNext) {
13
+ currentToken += char;
14
+ escapeNext = false;
15
+ continue;
16
+ }
17
+
18
+ if (char === '\\' && !inSingleQuotes) {
19
+ escapeNext = true;
20
+ continue;
21
+ }
22
+
23
+ if (char === "'" && !inDoubleQuotes) {
24
+ inSingleQuotes = !inSingleQuotes;
25
+ continue;
26
+ }
27
+
28
+ if (char === '"' && !inSingleQuotes) {
29
+ inDoubleQuotes = !inDoubleQuotes;
30
+ continue;
31
+ }
32
+
33
+ if (char === ' ' || char === '\t') {
34
+ if (inSingleQuotes || inDoubleQuotes) {
35
+ currentToken += char;
36
+ } else if (currentToken.length > 0) {
37
+ tokens.push(currentToken);
38
+ currentToken = '';
39
+ }
40
+ continue;
41
+ }
42
+
43
+ if (char === '|' && !inSingleQuotes && !inDoubleQuotes) {
44
+ if (currentToken.length > 0) tokens.push(currentToken);
45
+ tokens.push('|');
46
+ currentToken = '';
47
+ continue;
48
+ }
49
+
50
+ // append char
51
+ currentToken += char;
52
+ }
53
+
54
+ if (currentToken.length > 0) {
55
+ tokens.push(currentToken);
56
+ }
57
+
58
+ return tokens;
59
+ }
60
+
61
+ module.exports = { tokenize };