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
JavaScript
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()