UNPKG

zaincode_terminal

Version:

A Git-like Node.js terminal tool that provides an interactive CLI experience with support for custom commands, colors, and automation. Built with chalk, axios, and node-pty for developers who want a modern, lightweight, and powerful command-line utility.

418 lines (379 loc) 13.5 kB
#!/usr/bin/env node import readline from 'node:readline' import { stdin as input, stdout as output } from 'node:process' import { dirname, join, resolve } from 'node:path' import chalk from 'chalk' import fs from 'fs/promises' import * as fsSync from 'fs' import getAllPaths from './utils/GetAllPaths.js' import commondExtracter from './utils/CommondExtracter.js' import CheckType from './Events/CheckType.js' import { spawn } from 'node:child_process' import axios from 'axios' import FormData from 'form-data' import path from 'path' import { statSync } from 'node:fs' import { fileURLToPath } from 'url' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename) console.log(chalk.green('Hello from ZainCode Terminal 🚀')) // Use process.cwd() for project-related files const projectDir = process.cwd() const zainDir = join(projectDir, '.zain') const indexFile = resolve(zainDir, 'index.json') const configFile = resolve(zainDir, 'config.json') const remoteFile = resolve(zainDir, 'remote.json') const tokenFile = resolve(zainDir, 'token.json') const pkgFile = join(__dirname, 'package.json') readline.emitKeypressEvents(input) if (input.isTTY) input.setRawMode(true) let currentInput = '' let cursorIndex = 0 let history = [] let historyIndex = -1 const prompt = () => { output.write(`PS ${process.cwd()}> `) } const renderLine = () => { const [firstWord, ...rest] = currentInput.split(' ') output.write('\x1B[2K\r') output.write(`PS ${process.cwd()}> ${chalk.yellow(firstWord)} ${rest.join(' ')}`) readline.cursorTo(output, `PS ${process.cwd()}> `.length + cursorIndex) } const safeReadJSON = async (file, fallback = {}) => { try { const data = await fs.readFile(file, 'utf-8') return data.trim() ? JSON.parse(data) : fallback } catch { return fallback } } const keypressHandler = async (str, key) => { try { if (key.sequence === '\r') { if (currentInput.trim()) history.push(currentInput) historyIndex = history.length output.write('\n') await handledCommond(currentInput.trim()) currentInput = '' cursorIndex = 0 } else if (key.ctrl && key.name === 'c') { process.exit() } else if (key.name === 'backspace') { if (cursorIndex > 0) { currentInput = currentInput.slice(0, cursorIndex - 1) + currentInput.slice(cursorIndex) cursorIndex-- } renderLine() } else if (key.name === 'left') { if (cursorIndex > 0) cursorIndex-- renderLine() } else if (key.name === 'right') { if (cursorIndex < currentInput.length) cursorIndex++ renderLine() } else if (key.name === 'up') { if (historyIndex > 0) { historyIndex-- currentInput = history[historyIndex] cursorIndex = currentInput.length } renderLine() } else if (key.name === 'down') { if (historyIndex < history.length - 1) { historyIndex++ currentInput = history[historyIndex] } else { historyIndex = history.length currentInput = '' } cursorIndex = currentInput.length renderLine() } else if (key.name === 'tab') { const parts = currentInput.trim().split(' ') const isCD = parts[0] === 'cd' const partial = parts[parts.length - 1] try { const files = fsSync.readdirSync(process.cwd(), { withFileTypes: true }) let matches = files .filter(f => f.name.startsWith(partial)) .filter(f => (isCD ? f.isDirectory() : true)) .map(f => f.name) if (matches.length === 1) { parts[parts.length - 1] = matches[0] currentInput = parts.join(' ') cursorIndex = currentInput.length renderLine() } else if (matches.length > 1) { output.write('\n' + matches.join(' ') + '\n') renderLine() } } catch (err) { output.write(chalk.red(`Tab completion error: ${err.message}\n`)) } } else { currentInput = currentInput.slice(0, cursorIndex) + str + currentInput.slice(cursorIndex) cursorIndex += str.length renderLine() } } catch (err) { output.write(chalk.red(`\nKeypress error: ${err.message}\n`)) } } const handledCommond = async msg => { try { const cmd = msg.split(' ') if (!cmd[0]) { restorePrompt() return } if (cmd[0] === 'zain') { switch (cmd[1]) { case 'init': await zainInit() break case 'add': if (cmd[2]) await zainAdd(cmd[2]) else output.write(chalk.yellow('Please provide file/folder name\n')) break case 'remote': if (cmd[2] === 'add' && cmd[3] === 'origin') { const fullCommand = msg const url = commondExtracter(fullCommand) if (url) await remoteAddOrigin(url) else output.write(chalk.yellow('Please specify a URL in quotes!\n')) } else { output.write(chalk.yellow('Invalid remote command\n')) } break case 'config': await zainConfig(cmd) break case 'push': await zainPush() break case 'login': await zainLogin() break case '--version': await showVersion() break case '-v': await showVersion() break default: output.write(chalk.yellow('Unknown zain command\n')) } } else { await generalCmd(msg) } } catch (err) { output.write(chalk.red(`Error: ${err.message}\n`)) } finally { restorePrompt() } } const zainInit = async () => { try { await fs.mkdir(zainDir, { recursive: true }) await fs.writeFile(indexFile, JSON.stringify([])) await fs.writeFile(configFile, JSON.stringify({})) await fs.writeFile(remoteFile, JSON.stringify({})) await fs.writeFile(tokenFile, JSON.stringify({})) output.write(chalk.green('Project initialized in .zain\n')) } catch (err) { output.write(chalk.red(`Initialization failed: ${err.message}\n`)) } } const zainAdd = async fileOrFolder => { try { const rootDir = projectDir const excludeDirs = ['node_modules', '.zain', '.git'] if (fileOrFolder === '.') { // Get all files except node_modules & .zain const allPaths = await getAllPaths(rootDir, rootDir, excludeDirs) // Store relative paths instead of absolute const relativePaths = allPaths.map(p => path.relative(rootDir, p)) await fs.writeFile(indexFile, JSON.stringify(relativePaths, null, 2)) output.write(chalk.green('All files added!\n')) } else { const fullPath = join(rootDir, fileOrFolder) const type = await CheckType(fullPath) if (type === 'dir') { const paths = await getAllPaths(fullPath, rootDir, excludeDirs) const relativePaths = paths.map(p => path.relative(rootDir, p)) await fs.writeFile(indexFile, JSON.stringify(relativePaths, null, 2)) output.write(chalk.green('Folder added!\n')) } else if (type === 'file') { const relativePath = path.relative(rootDir, fullPath) if (!relativePath.includes(`node_modules${path.sep}`)) { await fs.writeFile(indexFile, JSON.stringify([relativePath], null, 2)) output.write(chalk.green('File added!\n')) } else { output.write(chalk.yellow('Skipped file inside node_modules\n')) } } else { output.write(chalk.red('Invalid file or directory\n')) } } } catch (err) { output.write(chalk.red(`Add failed: ${err.message}\n`)) } } const remoteAddOrigin = async url => { try { const base = new URL(url).origin const remoteData = { origin: base, push: url } await fs.writeFile(remoteFile, JSON.stringify(remoteData, null, 2)) output.write(chalk.green('Remote URL set successfully!\n')) } catch (err) { output.write(chalk.red(`Failed to set remote: ${err.message}\n`)) } } const zainConfig = async cmd => { try { const parts = cmd[2]?.split('.') || [] const value = commondExtracter(cmd.join(' ')) if (parts[0] !== 'user' || !['name', 'email'].includes(parts[1]) || !value) { output.write(chalk.red('Usage: zain config user.name "Zain"\n')) return } const config = await safeReadJSON(configFile, {}) config.user = config.user || {} config.user[parts[1]] = value await fs.writeFile(configFile, JSON.stringify(config, null, 2)) output.write(chalk.green(`Set user.${parts[1]} = "${value}"\n`)) } catch (err) { output.write(chalk.red(`Config failed: ${err.message}\n`)) } } const generalCmd = async command => { return new Promise(resolve => { const [cmd, ...args] = command.split(' ') if (cmd === 'cd') { try { const newPath = args.join(' ') || process.env.HOME || process.env.USERPROFILE process.chdir(newPath) } catch (err) { output.write(chalk.red(`cd: ${err.message}\n`)) } return resolve() } if (cmd === 'ls' || cmd === 'dir') { try { const files = fsSync.readdirSync(process.cwd(), { withFileTypes: true }) files.forEach(f => { output.write(f.isDirectory() ? chalk.blue(f.name + '/') : f.name) output.write(' ') }) output.write('\n') } catch (err) { output.write(chalk.red(`ls: ${err.message}\n`)) } return resolve() } try { const child = spawn(command, { shell: true, stdio: ['inherit', 'pipe', 'pipe'] }) child.stdout.on('data', d => output.write(`${d}`)) child.stderr.on('data', d => output.write(chalk.red(`${d}`))) child.on('exit', () => resolve()) } catch (err) { output.write(chalk.red(`Command failed: ${err.message}\n`)) resolve() } }) } const zainLogin = async () => { input.removeListener('keypress', keypressHandler) if (input.isTTY) input.setRawMode(false) const rl = readline.createInterface({ input, output }) const ask = (question, hide = false) => { if (!hide) return new Promise(res => rl.question(question, res)) return new Promise(res => { const stdin = process.stdin const onDataHandler = char => { char = char + '' switch (char) { case '\n': case '\r': case '\u0004': stdin.removeListener('data', onDataHandler) break default: process.stdout.clearLine(0) process.stdout.cursorTo(0) process.stdout.write(question + '*'.repeat(rl.line.length)) break } } stdin.on('data', onDataHandler) rl.question(question, answer => { stdin.removeListener('data', onDataHandler) res(answer) }) }) } try { const email = await ask('Email: ') const password = await ask('Password: ', true) rl.close() const { origin } = await safeReadJSON(remoteFile, {}) if (!origin) { output.write(chalk.red('Remote URL not set.\n')) return } const { data } = await axios.post(`${origin}/login`, { email, password }) if (!data.token) throw new Error('No token received') await fs.writeFile(tokenFile, JSON.stringify({ token: data.token })) output.write(chalk.green('Login successful!\n')) } catch (err) { output.write(chalk.red(`Login failed: ${err.message}\n`)) } } const zainPush = async () => { try { const allPaths = await safeReadJSON(indexFile, []) if (!allPaths.length) { output.write(chalk.red('No files to push\n')) return } const remoteConfig = await safeReadJSON(remoteFile, {}) if (!remoteConfig.push) { output.write(chalk.red('Remote URL not set\n')) return } const { token } = await safeReadJSON(tokenFile, {}) if (!token) { output.write(chalk.red('Not logged in\n')) return } const formData = new FormData() allPaths.forEach(filePath => { const stats = statSync(filePath) if (stats.isFile()) { const relativePath = path.relative(projectDir, filePath).replace(/\\/g, '/') // ✅ relative path ko third argument me de rahe hain formData.append('files', fsSync.createReadStream(filePath)) formData.append('paths', relativePath) } }) await axios.post(remoteConfig.push, formData, { headers: { ...formData.getHeaders(), Authorization: `Bearer ${token}` }, }) output.write(chalk.green('Push successful!\n')) } catch (err) { output.write(chalk.red(`Push failed: ${err.message}\n`)) } } const restorePrompt = () => { process.stdin.resume() if (input.isTTY) input.setRawMode(true) readline.emitKeypressEvents(input) input.removeListener('keypress', keypressHandler) input.on('keypress', keypressHandler) prompt() } const showVersion = async () => { const pkf = JSON.parse(await fs.readFile(pkgFile, 'utf-8')) output.write(chalk.green(`${pkf.version}\n`)) } restorePrompt()