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,160 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { tokenize } = require('./lexer');
4
+ const { loadConfig } = require('../config/defaults');
5
+ const { toWinStyle } = require('../utils/pathNormalizer');
6
+
7
+ function expandGlob(arg, cwd = process.cwd()) {
8
+ if (!arg.includes('*') && !arg.includes('?')) {
9
+ return [arg];
10
+ }
11
+
12
+ let normalized = arg.replace(/\\/g, '/');
13
+ let isDriveMapped = false;
14
+
15
+ if (normalized.match(/^\/[a-zA-Z]\//) || normalized.match(/^\/[a-zA-Z]$/)) {
16
+ cwd = toWinStyle(normalized.substring(0, 3));
17
+ normalized = normalized.substring(3);
18
+ isDriveMapped = true;
19
+ }
20
+
21
+ const parts = normalized.split('/').filter(p => p !== '');
22
+ let currentDirs = [cwd];
23
+
24
+ if (path.isAbsolute(arg) && !isDriveMapped) {
25
+ currentDirs = [path.parse(arg).root];
26
+ }
27
+
28
+ for (let i = 0; i < parts.length; i++) {
29
+ const part = parts[i];
30
+ if (part === '.' || part === '..') {
31
+ currentDirs = currentDirs.map(d => path.resolve(d, part));
32
+ continue;
33
+ }
34
+
35
+ if (!part.includes('*') && !part.includes('?')) {
36
+ currentDirs = currentDirs
37
+ .map(d => path.join(d, part))
38
+ .filter(p => fs.existsSync(p));
39
+ continue;
40
+ }
41
+
42
+ const regexStr = '^' + part
43
+ .replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&')
44
+ .replace(/\*/g, '.*')
45
+ .replace(/\?/g, '.') + '$';
46
+ const regex = new RegExp(regexStr, 'i');
47
+
48
+ const nextDirs = [];
49
+ for (const dir of currentDirs) {
50
+ if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) continue;
51
+ try {
52
+ const entries = fs.readdirSync(dir);
53
+ for (const entry of entries) {
54
+ if (regex.test(entry)) {
55
+ nextDirs.push(path.join(dir, entry));
56
+ }
57
+ }
58
+ } catch (err) {}
59
+ }
60
+ currentDirs = nextDirs;
61
+ }
62
+
63
+ if (currentDirs.length === 0) {
64
+ return [arg];
65
+ }
66
+
67
+ return currentDirs.map(p => {
68
+ if (path.isAbsolute(arg)) return p;
69
+ const rel = path.relative(process.cwd(), p);
70
+ return rel || '.';
71
+ });
72
+ }
73
+
74
+ function expandAliases(tokens, aliases, expanded = new Set()) {
75
+ const result = [];
76
+ let isCommandStart = true;
77
+
78
+ for (let i = 0; i < tokens.length; i++) {
79
+ const token = tokens[i];
80
+ if (token === '|') {
81
+ result.push(token);
82
+ isCommandStart = true;
83
+ continue;
84
+ }
85
+
86
+ if (isCommandStart) {
87
+ isCommandStart = false;
88
+ if (aliases[token] && !expanded.has(token)) {
89
+ expanded.add(token);
90
+ const aliasTokens = tokenize(aliases[token]);
91
+ const expandedAlias = expandAliases(aliasTokens, aliases, new Set(expanded));
92
+ result.push(...expandedAlias);
93
+ continue;
94
+ }
95
+ }
96
+
97
+ result.push(token);
98
+ }
99
+ return result;
100
+ }
101
+
102
+ function parsePipeline(input) {
103
+ let tokens = tokenize(input);
104
+ if (tokens.length === 0) return [];
105
+
106
+ const config = loadConfig();
107
+ if (config && config.aliases) {
108
+ tokens = expandAliases(tokens, config.aliases);
109
+ }
110
+
111
+ const pipeline = [];
112
+ let currentCmd = null;
113
+
114
+ for (const token of tokens) {
115
+ if (token === '|') {
116
+ if (currentCmd) {
117
+ pipeline.push(currentCmd);
118
+ currentCmd = null;
119
+ }
120
+ continue;
121
+ }
122
+
123
+ if (!currentCmd) {
124
+ currentCmd = {
125
+ command: token,
126
+ args: [],
127
+ rawArgs: [],
128
+ flags: {}
129
+ };
130
+ continue;
131
+ }
132
+
133
+ if (token.startsWith('--')) {
134
+ currentCmd.rawArgs.push(token);
135
+ const parts = token.slice(2).split('=');
136
+ const flagName = parts[0];
137
+ const flagValue = parts.length > 1 ? parts.slice(1).join('=') : true;
138
+ currentCmd.flags[flagName] = flagValue;
139
+ } else if (token.startsWith('-') && token !== '-' && token !== '--') {
140
+ currentCmd.rawArgs.push(token);
141
+ const flags = token.slice(1);
142
+ for (const f of flags) {
143
+ currentCmd.flags[f] = true;
144
+ }
145
+ } else {
146
+ // Positional argument - expand wildcards
147
+ const expanded = expandGlob(token);
148
+ currentCmd.args.push(...expanded);
149
+ currentCmd.rawArgs.push(...expanded);
150
+ }
151
+ }
152
+
153
+ if (currentCmd) {
154
+ pipeline.push(currentCmd);
155
+ }
156
+
157
+ return pipeline;
158
+ }
159
+
160
+ module.exports = { parsePipeline };
@@ -0,0 +1,148 @@
1
+ const readline = require('readline');
2
+ const os = require('os');
3
+ const path = require('path');
4
+ const fs = require('fs');
5
+ const { hackerGreen, errorMsg, chalk } = require('../utils/colors');
6
+ const { toLinuxStyle, toWinStyle } = require('../utils/pathNormalizer');
7
+ const { parsePipeline } = require('./parser');
8
+ const { executePipeline } = require('./executor');
9
+ const { configFile } = require('../config/defaults');
10
+
11
+ const configDir = path.dirname(configFile);
12
+ const historyFile = path.join(configDir, 'history.log');
13
+
14
+ function getPrompt() {
15
+ const username = os.userInfo().username;
16
+ const hostname = os.hostname();
17
+ const rawCwd = process.cwd();
18
+ const homeDir = os.homedir();
19
+
20
+ let linuxCwd = toLinuxStyle(rawCwd);
21
+ const linuxHome = toLinuxStyle(homeDir);
22
+
23
+ if (linuxCwd.startsWith(linuxHome)) {
24
+ linuxCwd = linuxCwd.replace(linuxHome, '~');
25
+ }
26
+
27
+ const promptText = `${username}@${hostname}:${linuxCwd}$ `;
28
+ return hackerGreen(promptText);
29
+ }
30
+
31
+ // Tab completion function
32
+ function completer(line) {
33
+ const builtins = [
34
+ 'ls', 'cd', 'pwd', 'clear', 'echo', 'whoami',
35
+ 'hostname', 'cat', 'mkdir', 'rm', 'cp', 'mv',
36
+ 'touch', 'grep', 'nano', 'config', 'exit'
37
+ ];
38
+
39
+ const parts = line.split(/\s+/);
40
+
41
+ // 1. Autocomplete commands if typing the first word
42
+ if (parts.length <= 1) {
43
+ const hits = builtins.filter(c => c.startsWith(line));
44
+ return [hits.length ? hits : builtins, line];
45
+ }
46
+
47
+ // 2. Autocomplete files/folders for parameters
48
+ const lastArg = parts[parts.length - 1];
49
+ let dir = '.';
50
+ let prefix = lastArg;
51
+
52
+ if (lastArg.includes('/') || lastArg.includes('\\')) {
53
+ const normalized = lastArg.replace(/\\/g, '/');
54
+ const slashIndex = normalized.lastIndexOf('/');
55
+ dir = lastArg.substring(0, slashIndex);
56
+ prefix = lastArg.substring(slashIndex + 1);
57
+ if (dir === '') dir = '/';
58
+ }
59
+
60
+ try {
61
+ const resolvedDir = path.resolve(toWinStyle(dir));
62
+ if (fs.existsSync(resolvedDir) && fs.statSync(resolvedDir).isDirectory()) {
63
+ const entries = fs.readdirSync(resolvedDir);
64
+ const hits = entries
65
+ .filter(f => f.startsWith(prefix))
66
+ .map(f => {
67
+ const joined = path.join(dir, f).replace(/\\/g, '/');
68
+ try {
69
+ if (fs.statSync(path.join(resolvedDir, f)).isDirectory()) {
70
+ return joined + '/'; // Directory trailer
71
+ }
72
+ } catch (e) {}
73
+ return joined;
74
+ });
75
+
76
+ return [hits, lastArg];
77
+ }
78
+ } catch (e) {
79
+ // Fail silently, returning empty completions
80
+ }
81
+
82
+ return [[], lastArg];
83
+ }
84
+
85
+ function startRepl() {
86
+ const rl = readline.createInterface({
87
+ input: process.stdin,
88
+ output: process.stdout,
89
+ prompt: getPrompt(),
90
+ completer: completer, // Enable autocomplete
91
+ historySize: 1000
92
+ });
93
+
94
+ // Seed history from file
95
+ if (fs.existsSync(historyFile)) {
96
+ try {
97
+ const historyLines = fs.readFileSync(historyFile, 'utf8')
98
+ .split(/\r?\n/)
99
+ .filter(l => l.trim().length > 0)
100
+ .reverse(); // Readline expects newest commands first
101
+ rl.history = historyLines;
102
+ } catch (err) {}
103
+ }
104
+
105
+ console.log(hackerGreen('PenguShell v1.0.0 - Windows Compatibility Layer'));
106
+ console.log(hackerGreen('Type "exit" to quit. (Tab-completion and history enabled)'));
107
+
108
+ rl.prompt();
109
+
110
+ rl.on('line', async (line) => {
111
+ line = line.trim();
112
+ if (line === 'exit') {
113
+ rl.close();
114
+ return;
115
+ }
116
+
117
+ if (line.length > 0) {
118
+ // Append to persistent history log
119
+ try {
120
+ fs.appendFileSync(historyFile, line + '\n');
121
+ } catch (err) {}
122
+
123
+ try {
124
+ rl.pause();
125
+ const pipeline = parsePipeline(line);
126
+ await executePipeline(pipeline, process.stdin, process.stdout);
127
+ } catch (err) {
128
+ console.error(errorMsg(`pengushell: ${err.message}`));
129
+ } finally {
130
+ rl.resume();
131
+ }
132
+ }
133
+
134
+ rl.setPrompt(getPrompt());
135
+ rl.prompt();
136
+ }).on('close', () => {
137
+ console.log(hackerGreen('exit'));
138
+ process.exit(0);
139
+ });
140
+
141
+ rl.on('SIGINT', () => {
142
+ rl.write('^C\n');
143
+ rl.setPrompt(getPrompt());
144
+ rl.prompt();
145
+ });
146
+ }
147
+
148
+ module.exports = { startRepl };
package/src/index.js ADDED
@@ -0,0 +1,142 @@
1
+ const { loadConfig, saveConfig, configFile } = require('./config/defaults');
2
+ const { startRepl } = require('./core/repl');
3
+ const { parsePipeline } = require('./core/parser');
4
+ const { executePipeline } = require('./core/executor');
5
+ const { chalk, errorMsg, hackerGreen } = require('./utils/colors');
6
+ const prompts = require('prompts');
7
+
8
+ function printHelp() {
9
+ console.log(hackerGreen('PenguShell - Windows PowerShell Compatibility Layer'));
10
+ console.log('\nUsage:');
11
+ console.log(' pengushell Start interactive shell (REPL)');
12
+ console.log(' pengushell <command> [args] Execute command directly and exit');
13
+ console.log(' pengushell config [action] Manage configuration');
14
+ console.log('\nConfig Actions:');
15
+ console.log(' interactive Start interactive config manager (default)');
16
+ console.log(' list / show Display current configuration settings');
17
+ console.log(' path Print the path of the config file');
18
+ console.log(' set <key> <value> Update a specific configuration value');
19
+ console.log('\nOptions:');
20
+ console.log(' -h, --help Show this help message');
21
+ console.log(' -v, --version Show version info');
22
+ console.log('\nBuilt-in Commands:');
23
+ console.log(' ls, pwd, cd, clear, echo, whoami, hostname, cat, mkdir, rm, cp, mv, touch, grep, nano');
24
+ }
25
+
26
+ async function interactiveConfig() {
27
+ const current = loadConfig();
28
+ const response = await prompts([
29
+ {
30
+ type: 'toggle',
31
+ name: 'useColors',
32
+ message: 'Enable colorized output?',
33
+ initial: current.useColors,
34
+ active: 'yes',
35
+ inactive: 'no'
36
+ },
37
+ {
38
+ type: 'toggle',
39
+ name: 'hackerGreenMode',
40
+ message: 'Enable hacker green terminal styling?',
41
+ initial: current.hackerGreenMode,
42
+ active: 'yes',
43
+ inactive: 'no'
44
+ }
45
+ ]);
46
+
47
+ if (response.useColors !== undefined && response.hackerGreenMode !== undefined) {
48
+ const updated = {
49
+ ...current,
50
+ useColors: response.useColors,
51
+ hackerGreenMode: response.hackerGreenMode
52
+ };
53
+ saveConfig(updated);
54
+ console.log(chalk.green('✔ Configuration saved successfully!'));
55
+ } else {
56
+ console.log(chalk.yellow('Configuration changes cancelled.'));
57
+ }
58
+ }
59
+
60
+ async function main() {
61
+ const args = process.argv.slice(2);
62
+
63
+ // 1. Version Check
64
+ if (args.includes('-v') || args.includes('--version')) {
65
+ console.log(`PenguShell v${require('../package.json').version}`);
66
+ process.exit(0);
67
+ }
68
+
69
+ // 2. Help Check
70
+ if (args.includes('-h') || args.includes('--help')) {
71
+ printHelp();
72
+ process.exit(0);
73
+ }
74
+
75
+ // 3. Config Subcommands
76
+ if (args[0] === 'config') {
77
+ const configAction = args[1];
78
+ if (!configAction || configAction === 'interactive') {
79
+ await interactiveConfig();
80
+ } else if (configAction === 'list' || configAction === 'show') {
81
+ const current = loadConfig();
82
+ console.log(chalk.cyan(`Config file: ${configFile}`));
83
+ console.log(JSON.stringify(current, null, 2));
84
+ } else if (configAction === 'path') {
85
+ console.log(configFile);
86
+ } else if (configAction === 'set') {
87
+ const key = args[2];
88
+ const val = args[3];
89
+ if (!key || val === undefined) {
90
+ console.error(errorMsg('Usage: pengushell config set <key> <value>'));
91
+ process.exit(1);
92
+ }
93
+ const current = loadConfig();
94
+ let typedVal = val;
95
+ if (val === 'true') typedVal = true;
96
+ if (val === 'false') typedVal = false;
97
+
98
+ current[key] = typedVal;
99
+ if (saveConfig(current)) {
100
+ console.log(chalk.green(`✔ Saved ${key} = ${typedVal}`));
101
+ } else {
102
+ console.error(errorMsg('Failed to save config.'));
103
+ process.exit(1);
104
+ }
105
+ } else {
106
+ console.error(errorMsg(`Unknown config command: ${configAction}`));
107
+ console.log('Available config actions: interactive, list, path, set');
108
+ process.exit(1);
109
+ }
110
+ process.exit(0);
111
+ }
112
+
113
+ // 4. Start REPL Mode
114
+ if (args.length === 0) {
115
+ startRepl();
116
+ return;
117
+ }
118
+
119
+ // 5. Direct Execution Mode (e.g. pengushell ls -la)
120
+ let cmdString;
121
+ if (args.length === 1) {
122
+ cmdString = args[0];
123
+ } else {
124
+ cmdString = args.map(arg => {
125
+ if (arg.includes(' ') || arg.includes('|') || arg.includes('>') || arg.includes('<')) {
126
+ return `"${arg.replace(/"/g, '\\"')}"`;
127
+ }
128
+ return arg;
129
+ }).join(' ');
130
+ }
131
+
132
+ try {
133
+ const pipeline = parsePipeline(cmdString);
134
+ await executePipeline(pipeline, process.stdin, process.stdout);
135
+ process.exit(process.exitCode !== undefined ? process.exitCode : 0);
136
+ } catch (err) {
137
+ console.error(errorMsg(`pengushell: ${err.message}`));
138
+ process.exit(1);
139
+ }
140
+ }
141
+
142
+ module.exports = { main };
@@ -0,0 +1,24 @@
1
+ const systemCmds = require('../commands/system');
2
+ const fsCmds = require('../commands/filesystem');
3
+ const searchCmds = require('../commands/search');
4
+ const nanoCmd = require('../commands/nano');
5
+
6
+ const registry = {
7
+ ...systemCmds,
8
+ ...fsCmds,
9
+ ...searchCmds,
10
+ ...nanoCmd
11
+ };
12
+
13
+ function getCommand(name) {
14
+ return registry[name] || null;
15
+ }
16
+
17
+ function registerCommand(name, cmdObj) {
18
+ registry[name] = cmdObj;
19
+ }
20
+
21
+ module.exports = {
22
+ getCommand,
23
+ registerCommand
24
+ };
@@ -0,0 +1,29 @@
1
+ const chalk = require('chalk');
2
+ const { loadConfig } = require('../config/defaults');
3
+
4
+ const config = loadConfig();
5
+ if (!config.useColors) {
6
+ chalk.level = 0;
7
+ }
8
+
9
+ function hackerGreen(text) {
10
+ if (config.hackerGreenMode) {
11
+ return chalk.bgBlack.greenBright(text);
12
+ }
13
+ return chalk.greenBright(text);
14
+ }
15
+
16
+ function colorizePath(pathStr) {
17
+ return chalk.blueBright(pathStr);
18
+ }
19
+
20
+ function errorMsg(text) {
21
+ return chalk.red(text);
22
+ }
23
+
24
+ module.exports = {
25
+ hackerGreen,
26
+ colorizePath,
27
+ errorMsg,
28
+ chalk
29
+ };
@@ -0,0 +1,56 @@
1
+ const path = require('path');
2
+
3
+ // Convert C:\Users\name to /c/Users/name
4
+ function toLinuxStyle(winPath) {
5
+ if (!winPath) return '/';
6
+
7
+ // Normalize slashes
8
+ let linuxPath = winPath.replace(/\\/g, '/');
9
+
10
+ // Handle drive letter C:/ -> /c/
11
+ const driveMatch = linuxPath.match(/^([a-zA-Z]):\/(.*)$/);
12
+ if (driveMatch) {
13
+ const drive = driveMatch[1].toLowerCase();
14
+ const rest = driveMatch[2];
15
+ linuxPath = `/${drive}/${rest}`;
16
+ } else {
17
+ const driveMatchRoot = linuxPath.match(/^([a-zA-Z]):$/);
18
+ if(driveMatchRoot) {
19
+ linuxPath = `/${driveMatchRoot[1].toLowerCase()}`;
20
+ }
21
+ }
22
+
23
+ // Remove trailing slash unless it's just root
24
+ if (linuxPath.length > 1 && linuxPath.endsWith('/')) {
25
+ linuxPath = linuxPath.slice(0, -1);
26
+ }
27
+
28
+ return linuxPath;
29
+ }
30
+
31
+ // Convert /c/Users/name to C:\Users\name
32
+ function toWinStyle(linuxPath) {
33
+ if (!linuxPath) return process.cwd();
34
+
35
+ // Convert /c/ to C:\
36
+ const driveMatch = linuxPath.match(/^\/([a-zA-Z])(?:\/(.*))?$/);
37
+ if (driveMatch) {
38
+ const drive = driveMatch[1].toUpperCase();
39
+ const rest = driveMatch[2] || '';
40
+ return path.normalize(`${drive}:\\${rest}`);
41
+ }
42
+
43
+ // If it's just / return current drive root (assuming C:\ as default)
44
+ if (linuxPath === '/') {
45
+ const currentDrive = path.parse(process.cwd()).root;
46
+ return currentDrive;
47
+ }
48
+
49
+ // Otherwise, probably relative, just normalize
50
+ return path.normalize(linuxPath);
51
+ }
52
+
53
+ module.exports = {
54
+ toLinuxStyle,
55
+ toWinStyle
56
+ };