diff --git a/packages/cli/cli.js b/packages/cli/cli.js index f42d495..b170f09 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -1,5 +1,10 @@ +/* eslint-disable no-restricted-syntax */ +const fs = require('fs').promises +const { exit } = require('process') const readline = require('readline') +const RETURN_SYMBOL = '\u21B5' + process.stdin.setRawMode(true) const rl = readline.createInterface({ @@ -8,13 +13,6 @@ const rl = readline.createInterface({ terminal: false, }) -async function printResponses(responses, delay = 100) { - for (const response of responses) { - await printResponse(response, delay) - } - rl.close() -} - function waitForEnter() { return new Promise((resolve) => { rl.once('line', resolve) @@ -35,18 +33,48 @@ function printResponse(response, delay) { waitForEnter().then(() => { clearInterval(timer) - process.stdout.write(response.slice(i) + '\n') + process.stdout.write(response.slice(i)) + process.stdout.write('\n') resolve() }) }) } -async function request() { - const { API_ENDPOINT, TOKEN, PWD, COMMAND, OUTPUT, EXIT_CODE } = process.env +async function printResponses(responses, delay = 100) { + for (const response of responses) { + // eslint-disable-next-line no-await-in-loop + await printResponse(response + RETURN_SYMBOL, delay) + } + rl.close() +} + +async function readOrNone(file) { + try { + return await fs.readFile(file, 'utf8') + } catch (err) { + return '' + } +} + +async function main() { + const { + API_ENDPOINT, + TOKEN, + PWD, + CMD_NAME, + CMD_OUTPUT_FILE, + CMD_ERROR_FILE, + CMD_EXIT_CODE, + } = process.env + if (!CMD_NAME) exit(0) + + const output = await readOrNone(CMD_OUTPUT_FILE) + const error = await readOrNone(CMD_ERROR_FILE) const payload = { - command: COMMAND, - output: OUTPUT, - exit_code: EXIT_CODE, + command: CMD_NAME, + output, + error, + exit_code: CMD_EXIT_CODE, token: TOKEN, pwd: PWD, } @@ -67,4 +95,4 @@ async function request() { } } -request() +main() diff --git a/packages/cli/index.js b/packages/cli/index.js deleted file mode 100644 index 1e87ff1..0000000 --- a/packages/cli/index.js +++ /dev/null @@ -1,50 +0,0 @@ -import { stdin, stdout, exit } from 'process' -import os from 'os' -import pty from 'node-pty' - -stdin.setRawMode(true) -stdin.resume() -stdin.setEncoding('utf8') - -// Create a new pseudoterminal -const shell = os.platform() === 'win32' ? 'powershell.exe' : 'bash' -const ptyProcess = pty.spawn(shell, [], { - name: 'xterm-color', - cols: 80, - rows: 30, - cwd: process.env.HOME, - env: process.env, -}) - -// Listen for output data -ptyProcess.on('data', function (data) { - // Intercept and process the output here - stdout.write(data) -}) - -// Write a command to the pseudoterminal -ptyProcess.write('ls\r') - -let commandBuffer = '' - -stdin.on('data', (key) => { - if (key === '\u0004') { - exit() - } else if (key === '\r') { - // User pressed enter, log and execute the command - console.log('User command: ', commandBuffer) - commandBuffer = '' - } else if (key === '\u007F') { - // User pressed backspace, remove the last character from the buffer - commandBuffer = commandBuffer.slice(0, -1) - } else { - // Add the typed character to the command buffer - commandBuffer += key - } - ptyProcess.write(key) -}) - -// Listen for exit event -ptyProcess.on('exit', function (exitCode) { - console.log('Process exited with code: ', exitCode) -}) diff --git a/packages/cli/package.json b/packages/cli/package.json index 85f8223..d8c4598 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,8 +3,5 @@ "version": "1.0.0", "main": "index.js", "license": "MIT", - "type": "module", - "dependencies": { - "node-pty": "^1.0.0" - } + "dependencies": {} } diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile index 40265a8..8a8942d 100644 --- a/server/quests/helloworld/Dockerfile +++ b/server/quests/helloworld/Dockerfile @@ -3,9 +3,10 @@ FROM node:latest RUN apt-get update && apt-get install -y \ curl \ vim \ + zsh \ jq -RUN adduser --disabled-password --gecos '' rudeus +RUN adduser --disabled-password --gecos '' --shell /bin/zsh rudeus USER rudeus @@ -15,4 +16,4 @@ COPY ./server/quests/helloworld/home /home/rudeus/ COPY ./packages/cli/cli.js /usr/local/bin/cli.js -CMD [ "/bin/bash" ] +CMD [ "/bin/zsh" ] diff --git a/server/quests/helloworld/home/.bashrc b/server/quests/helloworld/home/.bashrc deleted file mode 100644 index 39e6094..0000000 --- a/server/quests/helloworld/home/.bashrc +++ /dev/null @@ -1,24 +0,0 @@ -# This function will be called before each command is executed -preexec() { - [ -n "$COMP_LINE" ] && return # do nothing if completing - [ "$BASH_COMMAND" = "$PROMPT_COMMAND" ] && return # do nothing if completing - export COMMAND="$BASH_COMMAND" - - if [[ $COMMAND =~ ^(vim|top|less|more|nano) ]]; then - node /usr/local/bin/cli.js - return - fi - - # Execute the command, capture output and exit code - export OUTPUT=$( { eval $COMMAND 2>&1; echo $? > /tmp/exit_code; }) - export EXIT_CODE=$(cat /tmp/exit_code) - - echo $OUTPUT - # Send command to the API - node /usr/local/bin/cli.js - - BASH_COMMAND="" -} - -# Install the trap -trap 'preexec' DEBUG diff --git a/server/quests/helloworld/home/.zshrc b/server/quests/helloworld/home/.zshrc new file mode 100644 index 0000000..9369e80 --- /dev/null +++ b/server/quests/helloworld/home/.zshrc @@ -0,0 +1,36 @@ +PROMPT='%n@%m:%~%# ' +unsetopt CASE_GLOB +setopt CHASE_LINKS +emulate bash + +typeset -A cmd + +preexec() { + export CMD_START_TIME=$(date +%s%N) # capture start time + export CMD_NAME=$1 # capture command + + # A list of interactive commands + interactive_commands=("vim" "nano" "top" "ssh" "less" "zsh" "bash") + + # Check if the command is in the list of interactive commands + if [[ " ${interactive_commands[@]} " =~ " $1 " ]]; then + # If it is, just execute it normally + eval $1 + else + # If it's not, execute it and redirect its output to a file + eval $1 > /tmp/cmd.out 2>/tmp/cmd.error + fi +} + +precmd() { + export CMD_END_TIME=$(date +%s%N) # capture end time + export CMD_EXIT_CODE=$? # capture exit status + export CMD_OUTPUT_FILE=/tmp/cmd.out # read command output from file + export CMD_ERROR_FILE=/tmp/cmd.error + + # Now run your Node.js script with the result as an argument + node /usr/local/bin/cli.js + + rm /tmp/cmd.out /tmp/cmd.error 2>/dev/null # cleanup +} + diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js index a886d23..7cfbb0c 100644 --- a/server/src/api/controllers/commandController.js +++ b/server/src/api/controllers/commandController.js @@ -7,6 +7,11 @@ export async function newCommand(req, res) { console.log('new command:', req.body) const { token, command, pwd, exit_code, output } = req.body + if (!command) { + res.status(400).json({ message: 'command is required' }) + return + } + let sessionId try { const decoded = await verifySessionJWT(token) @@ -33,5 +38,7 @@ export async function newCommand(req, res) { await c.save() - res.status(201).end() + res.status(201).json({ + responses: ['Hello, world!', 'How are you?'], + }) } diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index cf7ee87..730a6e1 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -11,7 +11,6 @@ const containerOptions = { AttachStdout: true, AttachStderr: true, Tty: true, - Cmd: ['/bin/bash'], OpenStdin: true, StdinOnce: false, HostConfig: { @@ -59,7 +58,7 @@ export async function attachContainer(container, { token }) { AttachStdin: true, AttachStdout: true, AttachStderr: true, - Cmd: ['/bin/bash'], + Cmd: ['/bin/zsh'], Tty: true, Env: [`TOKEN=${token}`, `API_ENDPOINT=http://app:3000`], }) diff --git a/server/src/utils/quest.js b/server/src/utils/quest.js index 73f55bc..6ff8a40 100644 --- a/server/src/utils/quest.js +++ b/server/src/utils/quest.js @@ -17,6 +17,11 @@ export default async function loadAndUpdateQuests() { const quests = Promise.all( questNames.map(async (id) => { + // Check if id is directory + const stat = await fs.stat(path.join(questDirectory, id)) + if (!stat.isDirectory()) { + return null + } const fullPath = path.join(questDirectory, id, 'README.md') try { diff --git a/yarn.lock b/yarn.lock index 88291a4..2a3ac1c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3612,13 +3612,6 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== -node-pty@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-1.0.0.tgz#7daafc0aca1c4ca3de15c61330373af4af5861fd" - integrity sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA== - dependencies: - nan "^2.17.0" - node-releases@^2.0.12: version "2.0.13" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.13.tgz#d5ed1627c23e3461e819b02e57b75e4899b1c81d"