From 41a12c9aa897d16b49e793506be15f44a3df566e Mon Sep 17 00:00:00 2001 From: wancat Date: Fri, 11 Aug 2023 16:31:17 +0800 Subject: [PATCH 01/12] feat: collect 2 level files info when ls * change cli dir to /usr/local/lib/cli * refactor cli to multiple files * add handleCommand and listeners pattern --- packages/cli/cli.js | 102 +++++++-------------------- packages/cli/discover.js | 38 ++++++++++ packages/cli/package.json | 4 +- packages/cli/print.js | 82 +++++++++++++++++++++ packages/cli/watch.js | 6 ++ server/quests/helloworld/Dockerfile | 2 +- server/quests/helloworld/home/.zshrc | 2 +- server/src/utils/docker.js | 2 +- 8 files changed, 157 insertions(+), 81 deletions(-) create mode 100644 packages/cli/discover.js create mode 100644 packages/cli/print.js create mode 100644 packages/cli/watch.js diff --git a/packages/cli/cli.js b/packages/cli/cli.js index 39d5ecd..5b2c546 100644 --- a/packages/cli/cli.js +++ b/packages/cli/cli.js @@ -1,82 +1,8 @@ -/* eslint-disable no-await-in-loop */ /* eslint-disable no-restricted-syntax */ const fs = require('fs').promises const { exit } = require('process') -const readline = require('readline') - -const RETURN_SYMBOL = '\u21B5' - -// ANSI color map -const ANSI_COLORS = { - black: 30, - red: 31, - green: 32, - yellow: 33, - blue: 34, - magenta: 35, - cyan: 36, - white: 37, -} - -function colorize(content, color) { - const colorCode = ANSI_COLORS[color] - if (!colorCode) return content - return `\x1b[${colorCode}m${content}\x1b[0m` -} - -process.stdin.setRawMode(true) - -const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout, - terminal: false, -}) - -function waitForEnter() { - return new Promise((resolve) => { - rl.once('line', resolve) - }) -} - -function printLine(content, delay, color) { - const printContent = `${colorize(content, color)}${RETURN_SYMBOL}` - return new Promise((resolve) => { - let i = 0 - const timer = setInterval(() => { - process.stdout.write(printContent[i]) - i += 1 - if (i >= printContent.length) { - clearInterval(timer) - waitForEnter().then(resolve) - } - }, delay) - - waitForEnter().then(() => { - clearInterval(timer) - process.stdout.write(printContent.slice(i)) - process.stdout.write('\n') - resolve() - }) - }) -} - -async function printResponses(responses, delay = 100) { - for (const response of responses) { - const { type, content, speaker, color } = response - if (type === 'narrative') { - for (const line of content) { - await printLine(line, delay, color || 'white') - } - } - if (type === 'dialogue') { - console.log(`${speaker}:`) - for (const line of content) { - await printLine(`\t${line}`, delay, color || 'green') - } - } - } - rl.close() -} +const { colorize, printResponses } = require('./print.js') +const discoverFiles = require('./discover.js') async function readOrNone(file) { try { @@ -86,6 +12,25 @@ async function readOrNone(file) { } } +function parseCommand(command) { + const [name, ...args] = command.trim().split(' ') + return { name, args } +} + +const commandListeners = { + ls: [discoverFiles], +} + +async function handleCommand(command) { + const { name, args } = parseCommand(command) + const listeners = commandListeners[name] + if (!listeners) return {} + return listeners.reduce(async (result, listener) => { + const res = await listener(name, args) + return { ...res, ...result } + }, {}) +} + async function main() { const { API_ENDPOINT, @@ -98,6 +43,8 @@ async function main() { } = process.env if (!CMD_NAME) exit(0) + const additionalData = await handleCommand(CMD_NAME) + const output = await readOrNone(CMD_OUTPUT_FILE) const error = await readOrNone(CMD_ERROR_FILE) const payload = { @@ -107,6 +54,7 @@ async function main() { exit_code: CMD_EXIT_CODE, token: TOKEN, pwd: PWD, + ...additionalData, } try { const res = await fetch(`${API_ENDPOINT}/api/v1/commands`, { @@ -133,4 +81,4 @@ async function main() { } } -main().then(exit).catch(console.error) +main().then(exit) diff --git a/packages/cli/discover.js b/packages/cli/discover.js new file mode 100644 index 0000000..06d9d28 --- /dev/null +++ b/packages/cli/discover.js @@ -0,0 +1,38 @@ +const fs = require('fs').promises +const path = require('path') + +async function collectFilesInfo(directoryPath, level = 0) { + const entries = await fs.readdir(directoryPath, { withFileTypes: true }) + + const promises = entries.map(async (entry) => { + const filePath = path.join(directoryPath, entry.name) + const isDirectory = entry.isDirectory() + const discovered = level === 0 && !entry.name.startsWith('.') + + const file = { + path: filePath, + name: entry.name, + type: isDirectory ? 'folder' : 'file', + discovered, + } + + if (isDirectory && level < 2) { + // Get up to two levels deep + const subFilesInfo = await collectFilesInfo(filePath, level + 1) + return [file, ...subFilesInfo] + } + + return file + }) + + const results = await Promise.all(promises) + return results.flat() +} + +async function discoverFiles(name, args) { + let targetPath = args[0] || '.' + targetPath = path.resolve(process.cwd(), targetPath) + return { discover: await collectFilesInfo(targetPath) } +} + +module.exports = discoverFiles diff --git a/packages/cli/package.json b/packages/cli/package.json index d8c4598..0bbe6b2 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -3,5 +3,7 @@ "version": "1.0.0", "main": "index.js", "license": "MIT", - "dependencies": {} + "dependencies": { + "chokidar": "^3.5.3" + } } diff --git a/packages/cli/print.js b/packages/cli/print.js new file mode 100644 index 0000000..76a77a5 --- /dev/null +++ b/packages/cli/print.js @@ -0,0 +1,82 @@ +/* eslint-disable no-restricted-syntax */ +/* eslint-disable no-await-in-loop */ +const readline = require('readline') + +const RETURN_SYMBOL = '\u21B5' + +// ANSI color map +const ANSI_COLORS = { + black: 30, + red: 31, + green: 32, + yellow: 33, + blue: 34, + magenta: 35, + cyan: 36, + white: 37, +} + +function colorize(content, color) { + const colorCode = ANSI_COLORS[color] + if (!colorCode) return content + return `\x1b[${colorCode}m${content}\x1b[0m` +} + +process.stdin.setRawMode(true) + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + terminal: false, +}) + +function waitForEnter() { + return new Promise((resolve) => { + rl.once('line', resolve) + }) +} + +function printLine(content, delay, color) { + const printContent = `${colorize(content, color)}${RETURN_SYMBOL}` + return new Promise((resolve) => { + let i = 0 + const timer = setInterval(() => { + process.stdout.write(printContent[i]) + i += 1 + if (i >= printContent.length) { + clearInterval(timer) + waitForEnter().then(resolve) + } + }, delay) + + waitForEnter().then(() => { + clearInterval(timer) + process.stdout.write(printContent.slice(i)) + process.stdout.write('\n') + resolve() + }) + }) +} + +async function printResponses(responses, delay = 100) { + for (const response of responses) { + const { type, content, speaker, color } = response + if (type === 'narrative') { + for (const line of content) { + await printLine(line, delay, color || 'white') + } + } + if (type === 'dialogue') { + console.log(`${speaker}:`) + for (const line of content) { + await printLine(`\t${line}`, delay, color || 'green') + } + } + } + rl.close() +} + +module.exports = { + printResponses, + colorize, +} diff --git a/packages/cli/watch.js b/packages/cli/watch.js new file mode 100644 index 0000000..8288150 --- /dev/null +++ b/packages/cli/watch.js @@ -0,0 +1,6 @@ +const chokidar = require('chokidar') + +// One-liner for current directory +chokidar.watch('.').on('all', (event, path) => { + console.log(event, path) +}) diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile index 8a8942d..9606231 100644 --- a/server/quests/helloworld/Dockerfile +++ b/server/quests/helloworld/Dockerfile @@ -14,6 +14,6 @@ WORKDIR /home/rudeus COPY ./server/quests/helloworld/home /home/rudeus/ -COPY ./packages/cli/cli.js /usr/local/bin/cli.js +COPY ./packages/cli /usr/local/lib/cli CMD [ "/bin/zsh" ] diff --git a/server/quests/helloworld/home/.zshrc b/server/quests/helloworld/home/.zshrc index d8275fd..29c9f3e 100644 --- a/server/quests/helloworld/home/.zshrc +++ b/server/quests/helloworld/home/.zshrc @@ -29,7 +29,7 @@ precmd() { 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 + node /usr/local/lib/cli/cli.js rm /tmp/cmd.out /tmp/cmd.error 2>/dev/null # cleanup } diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index 730a6e1..fc9bbe4 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -18,7 +18,7 @@ const containerOptions = { ? [] : [ `${projectPath}/server/quests/helloworld/home:/home/rudeus`, - `${projectPath}/packages/cli/cli.js:/usr/local/bin/cli.js`, + `${projectPath}/packages/cli:/usr/local/lib/cli`, ], }, } From 6ace18475b58480b80dbdfb099c63b5bbd7f75bb Mon Sep 17 00:00:00 2001 From: wancat Date: Fri, 11 Aug 2023 17:22:54 +0800 Subject: [PATCH 02/12] refactor: move client to sperate packages, rename cli to container --- package.json | 2 +- .../src/client.js => packages/client/index.js | 14 +++++++-- packages/client/package.json | 14 +++++++++ packages/{cli => container}/cli.js | 0 packages/{cli => container}/discover.js | 0 packages/{cli => container}/package.json | 2 +- packages/{cli => container}/print.js | 0 packages/{cli => container}/watch.js | 0 server/package.json | 2 -- server/quests/helloworld/Dockerfile | 2 +- server/quests/helloworld/home/.zshrc | 2 +- .../src/api/controllers/commandController.js | 3 +- server/src/api/socket.js | 6 ++-- server/src/utils/docker.js | 2 +- yarn.lock | 30 +++++++++---------- 15 files changed, 51 insertions(+), 28 deletions(-) rename server/src/client.js => packages/client/index.js (90%) create mode 100644 packages/client/package.json rename packages/{cli => container}/cli.js (100%) rename packages/{cli => container}/discover.js (100%) rename packages/{cli => container}/package.json (83%) rename packages/{cli => container}/print.js (100%) rename packages/{cli => container}/watch.js (100%) diff --git a/package.json b/package.json index a0fc4ba..02f9ac1 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "scripts": { "app": "yarn workspace app dev", "server": "yarn workspace server dev", - "cli": "yarn workspace server cli", + "cli": "yarn workspace client start", "lint": "eslint --ext .js,.vue .", "format": "prettier --ignore-path .prettierignore --write app server" }, diff --git a/server/src/client.js b/packages/client/index.js similarity index 90% rename from server/src/client.js rename to packages/client/index.js index 557b88d..ff5b78f 100644 --- a/server/src/client.js +++ b/packages/client/index.js @@ -2,8 +2,14 @@ import { stdin, stdout, exit } from 'process' import { io } from 'socket.io-client' import { program } from 'commander' -import { get } from './utils/env.js' +function get(key, defaultValue) { + const value = process.env[key] + if (value != null) { + return value + } + return defaultValue +} stdin.setRawMode(true) stdin.resume() stdin.setEncoding('utf8') @@ -60,7 +66,9 @@ async function connect(sessionId) { console.log('Connected to the server.') }) - socket.on('message', function incoming(data) { + socket.on('message', console.log) + + socket.on('terminal', function incoming(data) { stdout.write(data) }) @@ -74,7 +82,7 @@ async function connect(sessionId) { socket.close() exit() } else { - socket.send(key) + socket.emit('terminal', key) } }) } diff --git a/packages/client/package.json b/packages/client/package.json new file mode 100644 index 0000000..968cf60 --- /dev/null +++ b/packages/client/package.json @@ -0,0 +1,14 @@ +{ + "name": "client", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "type": "module", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "commander": "^11.0.0", + "socket.io-client": "^4.7.2" + } +} diff --git a/packages/cli/cli.js b/packages/container/cli.js similarity index 100% rename from packages/cli/cli.js rename to packages/container/cli.js diff --git a/packages/cli/discover.js b/packages/container/discover.js similarity index 100% rename from packages/cli/discover.js rename to packages/container/discover.js diff --git a/packages/cli/package.json b/packages/container/package.json similarity index 83% rename from packages/cli/package.json rename to packages/container/package.json index 0bbe6b2..f6089d0 100644 --- a/packages/cli/package.json +++ b/packages/container/package.json @@ -1,5 +1,5 @@ { - "name": "cli", + "name": "container", "version": "1.0.0", "main": "index.js", "license": "MIT", diff --git a/packages/cli/print.js b/packages/container/print.js similarity index 100% rename from packages/cli/print.js rename to packages/container/print.js diff --git a/packages/cli/watch.js b/packages/container/watch.js similarity index 100% rename from packages/cli/watch.js rename to packages/container/watch.js diff --git a/server/package.json b/server/package.json index 6a40895..f3eb8e2 100644 --- a/server/package.json +++ b/server/package.json @@ -14,10 +14,8 @@ "dockerode": "^3.3.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.1", - "marked": "^5.1.1", "mongoose": "^7.4.0", "socket.io": "^4.7.1", - "socket.io-client": "^4.7.1", "swagger-ui-express": "^5.0.0", "yaml": "^2.3.1" }, diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile index 9606231..86b5ae6 100644 --- a/server/quests/helloworld/Dockerfile +++ b/server/quests/helloworld/Dockerfile @@ -14,6 +14,6 @@ WORKDIR /home/rudeus COPY ./server/quests/helloworld/home /home/rudeus/ -COPY ./packages/cli /usr/local/lib/cli +COPY ./packages/container /usr/local/lib/container CMD [ "/bin/zsh" ] diff --git a/server/quests/helloworld/home/.zshrc b/server/quests/helloworld/home/.zshrc index 29c9f3e..5ac25a5 100644 --- a/server/quests/helloworld/home/.zshrc +++ b/server/quests/helloworld/home/.zshrc @@ -29,7 +29,7 @@ precmd() { export CMD_ERROR_FILE=/tmp/cmd.error # Now run your Node.js script with the result as an argument - node /usr/local/lib/cli/cli.js + node /usr/local/lib/container/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 add1214..22b64fa 100644 --- a/server/src/api/controllers/commandController.js +++ b/server/src/api/controllers/commandController.js @@ -6,7 +6,8 @@ import commandHandler from '../../game/commandHandler.js' export async function newCommand(req, res) { console.log('new command:', req.body) - const { token, command, pwd, output, error } = req.body + const { token, command, pwd, output, error, ...additionalData } = req.body + console.log('additionalData:', additionalData) if (!command) { res.status(400).json({ message: 'command is required' }) diff --git a/server/src/api/socket.js b/server/src/api/socket.js index d1f33e5..df0a9fb 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -50,10 +50,12 @@ export default (server) => { const stream = await attachContainer(container, { token }) stream.on('data', (chunk) => { - socket.send(chunk.toString()) + socket.emit('terminal', chunk.toString()) }) - socket.on('message', function incoming(message) { + socket.on('message', console.log) + + socket.on('terminal', function incoming(message) { stream.write(message) }) diff --git a/server/src/utils/docker.js b/server/src/utils/docker.js index fc9bbe4..af6c634 100644 --- a/server/src/utils/docker.js +++ b/server/src/utils/docker.js @@ -18,7 +18,7 @@ const containerOptions = { ? [] : [ `${projectPath}/server/quests/helloworld/home:/home/rudeus`, - `${projectPath}/packages/cli:/usr/local/lib/cli`, + `${projectPath}/packages/container:/usr/local/lib/container`, ], }, } diff --git a/yarn.lock b/yarn.lock index 47e89d5..e66d6dc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1811,14 +1811,14 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -engine.io-client@~6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.1.tgz#1735fb8ae3bae5ae13115e18d2f484daf005dd9c" - integrity sha512-hE5wKXH8Ru4L19MbM1GgYV/2Qo54JSMh1rlJbfpa40bEWkCKNo3ol2eOtGmowcr+ysgbI7+SGL+by42Q3pt/Ng== +engine.io-client@~6.5.2: + version "6.5.2" + resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.5.2.tgz#8709e22c291d4297ae80318d3c8baeae71f0e002" + integrity sha512-CQZqbrpEYnrpGqC07a9dJDz4gePZUgTPMU3NKJPSeQOyw27Tst4Pl3FemKoFGAlHzgZmKjoRmiJvbWfhCXUlIg== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.1" - engine.io-parser "~5.1.0" + engine.io-parser "~5.2.1" ws "~8.11.0" xmlhttprequest-ssl "~2.0.0" @@ -1827,6 +1827,11 @@ engine.io-parser@~5.1.0: resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.1.0.tgz#d593d6372d7f79212df48f807b8cace1ea1cb1b8" integrity sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w== +engine.io-parser@~5.2.1: + version "5.2.1" + resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.2.1.tgz#9f213c77512ff1a6cc0c7a86108a7ffceb16fcfb" + integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ== + engine.io@~6.5.0: version "6.5.1" resolved "https://registry.yarnpkg.com/engine.io/-/engine.io-6.5.1.tgz#59725f8593ccc891abb47f1efcdc52a089525a56" @@ -3432,11 +3437,6 @@ makeerror@1.0.12: dependencies: tmpl "1.0.5" -marked@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/marked/-/marked-5.1.1.tgz#40b3963bb9da225314f746d5012ba7e34942f636" - integrity sha512-bTmmGdEINWmOMDjnPWDxGPQ4qkDLeYorpYbEtFOXzOruTwUE671q4Guiuchn4N8h/v6NGd7916kXsm3Iz4iUSg== - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -4269,14 +4269,14 @@ socket.io-adapter@~2.5.2: dependencies: ws "~8.11.0" -socket.io-client@^4.7.1: - version "4.7.1" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.1.tgz#48e5f703abe4fb0402182bcf9c06b7820fb3453b" - integrity sha512-Qk3Xj8ekbnzKu3faejo4wk2MzXA029XppiXtTF/PkbTg+fcwaTw1PlDrTrrrU4mKoYC4dvlApOnSeyLCKwek2w== +socket.io-client@^4.7.2: + version "4.7.2" + resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.7.2.tgz#f2f13f68058bd4e40f94f2a1541f275157ff2c08" + integrity sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w== dependencies: "@socket.io/component-emitter" "~3.1.0" debug "~4.3.2" - engine.io-client "~6.5.1" + engine.io-client "~6.5.2" socket.io-parser "~4.2.4" socket.io-parser@~4.2.4: From 34d5ace6646722f62ae5f8b0dd4dd4d401f4a71f Mon Sep 17 00:00:00 2001 From: wancat Date: Fri, 11 Aug 2023 18:24:43 +0800 Subject: [PATCH 03/12] feat: use minimist to refactor cli --- packages/container/cli.js | 12 +++--- packages/container/discover.js | 60 ++++++++++++++--------------- packages/container/package.json | 3 +- server/quests/helloworld/Dockerfile | 12 +++++- yarn.lock | 2 +- 5 files changed, 48 insertions(+), 41 deletions(-) diff --git a/packages/container/cli.js b/packages/container/cli.js index 5b2c546..2dfcf0c 100644 --- a/packages/container/cli.js +++ b/packages/container/cli.js @@ -1,6 +1,7 @@ /* eslint-disable no-restricted-syntax */ const fs = require('fs').promises const { exit } = require('process') +const minimist = require('minimist') const { colorize, printResponses } = require('./print.js') const discoverFiles = require('./discover.js') @@ -12,21 +13,18 @@ async function readOrNone(file) { } } -function parseCommand(command) { - const [name, ...args] = command.trim().split(' ') - return { name, args } -} - const commandListeners = { ls: [discoverFiles], } async function handleCommand(command) { - const { name, args } = parseCommand(command) + const argv = minimist(command.split(' ')) + console.log(argv) + const name = argv._[0] const listeners = commandListeners[name] if (!listeners) return {} return listeners.reduce(async (result, listener) => { - const res = await listener(name, args) + const res = await listener(argv) return { ...res, ...result } }, {}) } diff --git a/packages/container/discover.js b/packages/container/discover.js index 06d9d28..4d71a84 100644 --- a/packages/container/discover.js +++ b/packages/container/discover.js @@ -1,38 +1,38 @@ const fs = require('fs').promises const path = require('path') -async function collectFilesInfo(directoryPath, level = 0) { - const entries = await fs.readdir(directoryPath, { withFileTypes: true }) - - const promises = entries.map(async (entry) => { - const filePath = path.join(directoryPath, entry.name) - const isDirectory = entry.isDirectory() - const discovered = level === 0 && !entry.name.startsWith('.') - - const file = { - path: filePath, - name: entry.name, - type: isDirectory ? 'folder' : 'file', - discovered, - } - - if (isDirectory && level < 2) { - // Get up to two levels deep - const subFilesInfo = await collectFilesInfo(filePath, level + 1) - return [file, ...subFilesInfo] - } - - return file - }) - - const results = await Promise.all(promises) - return results.flat() +async function collectFilesInfo(inputPath, level = 0) { + const stats = await fs.stat(inputPath) + const discovered = level === 0 + const name = path.basename(inputPath) + const file = { + path: inputPath, + name, + type: stats.isDirectory() ? 'folder' : 'file', + discovered, + } + if (stats.isFile()) { + return [file] + } + if (stats.isDirectory()) { + const entries = await fs.readdir(inputPath) + const result = await Promise.all( + entries.map((entry) => + collectFilesInfo(path.join(inputPath, entry), level + 1) + ) + ) + return [file, ...result.flat()] + } + return [] } -async function discoverFiles(name, args) { - let targetPath = args[0] || '.' - targetPath = path.resolve(process.cwd(), targetPath) - return { discover: await collectFilesInfo(targetPath) } +async function discoverFiles(argv) { + const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1) + console.log(targetPath) + const result = await Promise.all( + targetPath.map((p) => path.resolve(process.cwd(), p)).map(collectFilesInfo) + ) + return { discover: result.flat() } } module.exports = discoverFiles diff --git a/packages/container/package.json b/packages/container/package.json index f6089d0..2c6bc8a 100644 --- a/packages/container/package.json +++ b/packages/container/package.json @@ -4,6 +4,7 @@ "main": "index.js", "license": "MIT", "dependencies": { - "chokidar": "^3.5.3" + "chokidar": "^3.5.3", + "minimist": "^1.2.8" } } diff --git a/server/quests/helloworld/Dockerfile b/server/quests/helloworld/Dockerfile index 86b5ae6..cd7b038 100644 --- a/server/quests/helloworld/Dockerfile +++ b/server/quests/helloworld/Dockerfile @@ -6,6 +6,16 @@ RUN apt-get update && apt-get install -y \ zsh \ jq +WORKDIR /usr/local/lib/container + +COPY ./packages/container/package.json ./yarn.lock ./ + +RUN yarn install --modules-folder /usr/src/node_modules + +ENV NODE_PATH=/usr/src/node_modules + +COPY ./packages/container /usr/local/lib/container + RUN adduser --disabled-password --gecos '' --shell /bin/zsh rudeus USER rudeus @@ -14,6 +24,4 @@ WORKDIR /home/rudeus COPY ./server/quests/helloworld/home /home/rudeus/ -COPY ./packages/container /usr/local/lib/container - CMD [ "/bin/zsh" ] diff --git a/yarn.lock b/yarn.lock index e66d6dc..264332c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3509,7 +3509,7 @@ minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: dependencies: brace-expansion "^1.1.7" -minimist@^1.2.0, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.6, minimist@^1.2.8: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== From f43fa1b5efe9761b69c8e46e519d4252b0545665 Mon Sep 17 00:00:00 2001 From: wancat Date: Fri, 11 Aug 2023 18:58:26 +0800 Subject: [PATCH 04/12] fix: limit 2 level to only 5 children --- packages/container/cli.js | 1 - packages/container/discover.js | 41 ++++++++++++++++------------ server/quests/helloworld/home/.zshrc | 4 +-- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/packages/container/cli.js b/packages/container/cli.js index 2dfcf0c..fac7eba 100644 --- a/packages/container/cli.js +++ b/packages/container/cli.js @@ -19,7 +19,6 @@ const commandListeners = { async function handleCommand(command) { const argv = minimist(command.split(' ')) - console.log(argv) const name = argv._[0] const listeners = commandListeners[name] if (!listeners) return {} diff --git a/packages/container/discover.js b/packages/container/discover.js index 4d71a84..59c6ca7 100644 --- a/packages/container/discover.js +++ b/packages/container/discover.js @@ -5,30 +5,35 @@ async function collectFilesInfo(inputPath, level = 0) { const stats = await fs.stat(inputPath) const discovered = level === 0 const name = path.basename(inputPath) - const file = { - path: inputPath, - name, - type: stats.isDirectory() ? 'folder' : 'file', - discovered, - } - if (stats.isFile()) { - return [file] - } - if (stats.isDirectory()) { - const entries = await fs.readdir(inputPath) - const result = await Promise.all( - entries.map((entry) => - collectFilesInfo(path.join(inputPath, entry), level + 1) + const files = [ + { + path: inputPath, + name, + type: stats.isDirectory() ? 'folder' : 'file', + discovered, + }, + ] + if (stats.isDirectory() && level < 2) { + try { + let entries = await fs.readdir(inputPath) + if (level > 0) { + entries = entries.slice(0, 5) + } + const result = await Promise.all( + entries.map((entry) => + collectFilesInfo(path.join(inputPath, entry), level + 1) + ) ) - ) - return [file, ...result.flat()] + files.push(...result.flat()) + } catch (err) { + // ignore + } } - return [] + return files } async function discoverFiles(argv) { const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1) - console.log(targetPath) const result = await Promise.all( targetPath.map((p) => path.resolve(process.cwd(), p)).map(collectFilesInfo) ) diff --git a/server/quests/helloworld/home/.zshrc b/server/quests/helloworld/home/.zshrc index 5ac25a5..eacba97 100644 --- a/server/quests/helloworld/home/.zshrc +++ b/server/quests/helloworld/home/.zshrc @@ -29,8 +29,6 @@ precmd() { export CMD_ERROR_FILE=/tmp/cmd.error # Now run your Node.js script with the result as an argument - node /usr/local/lib/container/cli.js - - rm /tmp/cmd.out /tmp/cmd.error 2>/dev/null # cleanup + node /usr/local/lib/container/cli.js; rm /tmp/cmd.out /tmp/cmd.error 2>/dev/null } From c0186c6a30d3c4a784bb88461d482fb4f1cd12c5 Mon Sep 17 00:00:00 2001 From: wancat Date: Fri, 11 Aug 2023 19:05:34 +0800 Subject: [PATCH 05/12] feat: show hiddenFiles --- packages/container/discover.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/container/discover.js b/packages/container/discover.js index 59c6ca7..0f0fc13 100644 --- a/packages/container/discover.js +++ b/packages/container/discover.js @@ -1,10 +1,10 @@ const fs = require('fs').promises const path = require('path') -async function collectFilesInfo(inputPath, level = 0) { +async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) { const stats = await fs.stat(inputPath) - const discovered = level === 0 const name = path.basename(inputPath) + const discovered = level <= 1 && (hiddenFiles || !name.startsWith('.')) const files = [ { path: inputPath, @@ -21,7 +21,7 @@ async function collectFilesInfo(inputPath, level = 0) { } const result = await Promise.all( entries.map((entry) => - collectFilesInfo(path.join(inputPath, entry), level + 1) + collectFilesInfo(path.join(inputPath, entry), level + 1, hiddenFiles) ) ) files.push(...result.flat()) @@ -33,9 +33,12 @@ async function collectFilesInfo(inputPath, level = 0) { } async function discoverFiles(argv) { + console.log(argv) const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1) const result = await Promise.all( - targetPath.map((p) => path.resolve(process.cwd(), p)).map(collectFilesInfo) + targetPath + .map((p) => path.resolve(process.cwd(), p)) + .map((p) => collectFilesInfo(p, 0, argv.a || argv.all)) ) return { discover: result.flat() } } From 096b7a77148abd3051bed57b90e5258662c6f97c Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 14 Aug 2023 19:39:30 +0800 Subject: [PATCH 06/12] feat: push discover event to frontend --- packages/client/index.js | 5 +++++ packages/container/discover.js | 1 - .../src/api/controllers/commandController.js | 2 +- server/src/api/socket.js | 18 ++++++++++++++++++ server/src/game/commandHandler.js | 10 +++++++++- 5 files changed, 33 insertions(+), 3 deletions(-) diff --git a/packages/client/index.js b/packages/client/index.js index ff5b78f..5363631 100644 --- a/packages/client/index.js +++ b/packages/client/index.js @@ -72,6 +72,11 @@ async function connect(sessionId) { stdout.write(data) }) + socket.on('graph', (data) => { + console.log('receive graph:') + console.log(data) + }) + socket.on('close', function close() { console.log('Disconnected from the server.') exit() diff --git a/packages/container/discover.js b/packages/container/discover.js index 0f0fc13..781a58d 100644 --- a/packages/container/discover.js +++ b/packages/container/discover.js @@ -33,7 +33,6 @@ async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) { } async function discoverFiles(argv) { - console.log(argv) const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1) const result = await Promise.all( targetPath diff --git a/server/src/api/controllers/commandController.js b/server/src/api/controllers/commandController.js index 22b64fa..c37121d 100644 --- a/server/src/api/controllers/commandController.js +++ b/server/src/api/controllers/commandController.js @@ -40,7 +40,7 @@ export async function newCommand(req, res) { await c.save() - const response = await commandHandler(session, c) + const response = await commandHandler(session, c, additionalData) console.log(response) res.status(201).json(response) diff --git a/server/src/api/socket.js b/server/src/api/socket.js index df0a9fb..f2c6aa6 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -3,6 +3,20 @@ import { getOrCreateContainer, attachContainer } from '../utils/docker.js' import Session from '../models/session.js' import { defaultUser, genSessionJWT } from '../utils/auth.js' +const sessions = new Map() + +function listenToSession(sessionId, event, callback) { + sessions.set((sessionId, event), callback) +} + +export function pushToSession(sessionId, event, data) { + console.log(sessions, sessionId, event, data) + const callback = sessions.get((sessionId, event)) + if (callback) { + callback(data) + } +} + export default (server) => { const io = new Server(server) @@ -63,6 +77,10 @@ export default (server) => { console.log('Disconnected from the client.') }) + listenToSession(session.id, 'graph', (data) => { + socket.emit('graph', data) + }) + socket.send(`${container.id}\n`) }) } diff --git a/server/src/game/commandHandler.js b/server/src/game/commandHandler.js index 4dd0df5..d24eb33 100644 --- a/server/src/game/commandHandler.js +++ b/server/src/game/commandHandler.js @@ -1,4 +1,5 @@ import Quest from '../models/quest.js' +import { pushToSession } from '../api/socket.js' // 檢查 pattern 是否符合 input const checkMatch = (pattern, input) => { @@ -9,7 +10,7 @@ const checkMatch = (pattern, input) => { }) } -const commandHandler = async (session, commandInput) => { +async function commandHandler(session, commandInput, additionalData) { const quest = await Quest.findById(session.quest) const stage = quest.stages.find((s) => s.id === session.progress) if (!stage) { @@ -17,6 +18,13 @@ const commandHandler = async (session, commandInput) => { return {} } + if ('discover' in additionalData) { + pushToSession(session.id, 'graph', { + pwd: commandInput.pwd, + discover: additionalData.discover, + }) + } + const commandMatch = checkMatch(stage.condition.command, commandInput.command) const outputMatch = checkMatch(stage.condition.output, commandInput.output) const errorMatch = checkMatch(stage.condition.error, commandInput.output) From f08ee333a7903cfe3ff964e82a442b78db22349d Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 14 Aug 2023 19:50:44 +0800 Subject: [PATCH 07/12] feat: push new pwd when 'cd' --- server/package.json | 1 + server/src/api/socket.js | 11 ++++++----- server/src/game/commandHandler.js | 5 +++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/server/package.json b/server/package.json index f3eb8e2..9895bae 100644 --- a/server/package.json +++ b/server/package.json @@ -14,6 +14,7 @@ "dockerode": "^3.3.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.1", + "minimist": "^1.2.8", "mongoose": "^7.4.0", "socket.io": "^4.7.1", "swagger-ui-express": "^5.0.0", diff --git a/server/src/api/socket.js b/server/src/api/socket.js index f2c6aa6..7e167ae 100644 --- a/server/src/api/socket.js +++ b/server/src/api/socket.js @@ -6,14 +6,15 @@ import { defaultUser, genSessionJWT } from '../utils/auth.js' const sessions = new Map() function listenToSession(sessionId, event, callback) { - sessions.set((sessionId, event), callback) + const callbacks = sessions.get((sessionId, event)) || [] + sessions.set((sessionId, event), callbacks.concat(callback)) } export function pushToSession(sessionId, event, data) { - console.log(sessions, sessionId, event, data) - const callback = sessions.get((sessionId, event)) - if (callback) { - callback(data) + console.log('Push:', sessionId, event) + const callbacks = sessions.get((sessionId, event)) + if (callbacks) { + callbacks.forEach((callback) => callback(data)) } } diff --git a/server/src/game/commandHandler.js b/server/src/game/commandHandler.js index d24eb33..b467b5c 100644 --- a/server/src/game/commandHandler.js +++ b/server/src/game/commandHandler.js @@ -1,3 +1,4 @@ +import minimist from 'minimist' import Quest from '../models/quest.js' import { pushToSession } from '../api/socket.js' @@ -18,11 +19,15 @@ async function commandHandler(session, commandInput, additionalData) { return {} } + const argv = minimist(commandInput.command.split(' ')) + if ('discover' in additionalData) { pushToSession(session.id, 'graph', { pwd: commandInput.pwd, discover: additionalData.discover, }) + } else if (argv._[0] === 'cd') { + pushToSession(session.id, 'graph', { pwd: commandInput.pwd }) } const commandMatch = checkMatch(stage.condition.command, commandInput.command) From c3e735b6273db2393fa3b587d1f230ee98c8f2b7 Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 14 Aug 2023 19:51:00 +0800 Subject: [PATCH 08/12] fix: unset variable after execution --- server/quests/helloworld/home/.zshrc | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/quests/helloworld/home/.zshrc b/server/quests/helloworld/home/.zshrc index eacba97..92044b5 100644 --- a/server/quests/helloworld/home/.zshrc +++ b/server/quests/helloworld/home/.zshrc @@ -30,5 +30,13 @@ precmd() { # Now run your Node.js script with the result as an argument node /usr/local/lib/container/cli.js; rm /tmp/cmd.out /tmp/cmd.error 2>/dev/null + + # Reset variables + unset CMD_START_TIME + unset CMD_END_TIME + unset CMD_EXIT_CODE + unset CMD_OUTPUT_FILE + unset CMD_ERROR_FILE + unset CMD_NAME } From c2480044848d48071bd34035170e9734ac28ff1d Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 21 Aug 2023 20:24:23 +0800 Subject: [PATCH 09/12] feat: add Vite proxy, change terminal to other channel --- app/src/App.vue | 1 + app/src/components/TerminalPart.vue | 4 ++-- app/src/utils/api.js | 2 +- app/src/utils/socket.js | 4 +--- app/vite.config.js | 8 ++++++-- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/app/src/App.vue b/app/src/App.vue index 58f4090..8bade64 100644 --- a/app/src/App.vue +++ b/app/src/App.vue @@ -19,6 +19,7 @@ onMounted(async () => { socket.on('disconnect', function close() { console.log('Disconnected from the server.') }) + socket.on('message', console.log) }) diff --git a/app/src/components/TerminalPart.vue b/app/src/components/TerminalPart.vue index 2b61bf5..bdedd4d 100644 --- a/app/src/components/TerminalPart.vue +++ b/app/src/components/TerminalPart.vue @@ -55,11 +55,11 @@ onMounted(() => { term.open(terminal.value) term.focus() resizeScreen() - socket.on('message', (message) => { + socket.on('terminal', (message) => { term.write(message) }) term.onData((key) => { - socket.send(key) + socket.emit('terminal', key) }) }) diff --git a/app/src/utils/api.js b/app/src/utils/api.js index 61b68da..f598681 100644 --- a/app/src/utils/api.js +++ b/app/src/utils/api.js @@ -1,7 +1,7 @@ import axios from 'axios' const api = axios.create({ - baseURL: 'https://odyssey.wancat.cc/api/v1', + baseURL: '/api', }) export default api diff --git a/app/src/utils/socket.js b/app/src/utils/socket.js index 96c66e3..69050ca 100644 --- a/app/src/utils/socket.js +++ b/app/src/utils/socket.js @@ -1,7 +1,5 @@ import io from 'socket.io-client' -const host = 'wss://odyssey.wancat.cc' - class SocketWrapper { constructor() { this.socket = null @@ -14,7 +12,7 @@ class SocketWrapper { this.socket.disconnect() } - this.socket = io(host, { + this.socket = io('', { query: { session_id: session._id, }, diff --git a/app/vite.config.js b/app/vite.config.js index cf94bdc..c3d6440 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -7,8 +7,12 @@ export default defineConfig({ server: { proxy: { '/api': { - // target: 'http://localhost:3000', // the server url - target: 'http://odyssey.wancat.cc ', + target: 'http://localhost:3000/api/v1', + rewrite: (path) => path.replace(/^\/api/, ''), // Optional: remove "/api" prefix from the path + }, + '/socket.io': { + target: 'ws://localhost:3000', + // target: 'wss://odyssey.wancat.cc', ws: true, }, }, From d86aa5bcc1b90902b55a5063dc070c6ef13df154 Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 21 Aug 2023 20:59:35 +0800 Subject: [PATCH 10/12] fix: use environment to select API_ENDPOINT --- app/package.json | 3 ++- app/vite.config.js | 9 +++++---- package.json | 1 + 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app/package.json b/app/package.json index de08c19..bd2350a 100644 --- a/app/package.json +++ b/app/package.json @@ -4,7 +4,8 @@ "version": "0.0.0", "type": "module", "scripts": { - "dev": "vite", + "dev": "API_TARGET=https://odyssey.wancat.cc vite", + "dev:local": "vite", "build": "vite build", "preview": "vite preview" }, diff --git a/app/vite.config.js b/app/vite.config.js index c3d6440..2683207 100644 --- a/app/vite.config.js +++ b/app/vite.config.js @@ -7,12 +7,13 @@ export default defineConfig({ server: { proxy: { '/api': { - target: 'http://localhost:3000/api/v1', - rewrite: (path) => path.replace(/^\/api/, ''), // Optional: remove "/api" prefix from the path + target: `${process.env.API_TARGET || 'http://localhost:3000'}/api/v1`, + changeOrigin: true, + rewrite: (path) => path.replace(/^\/api/, ''), }, '/socket.io': { - target: 'ws://localhost:3000', - // target: 'wss://odyssey.wancat.cc', + target: process.env.API_TARGET || 'ws://localhost:3000', + changeOrigin: true, ws: true, }, }, diff --git a/package.json b/package.json index 02f9ac1..6ecf712 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "scripts": { "app": "yarn workspace app dev", + "app:local": "yarn workspace app dev:local", "server": "yarn workspace server dev", "cli": "yarn workspace client start", "lint": "eslint --ext .js,.vue .", From aba582a737c04681e004fe28f2fec982df7d1d0d Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 21 Aug 2023 21:01:18 +0800 Subject: [PATCH 11/12] doc: update README to add app and app:local instruction --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 122d011..58467aa 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,11 @@ docker compose -f docker-compose.build.yml push Source: `app/` ``` +# Connect to dev server (https://odyssey.wancat.cc) yarn app -# Open http://localhost:5173/ + +# Connect to localhost (http://localhost:3000) +yarn app:local ``` ## Swagger From 9facfa31d09dc8a6af6ad8fe69396212d42aca25 Mon Sep 17 00:00:00 2001 From: wancat Date: Mon, 21 Aug 2023 22:11:38 +0800 Subject: [PATCH 12/12] fix: remove unused experiment file --- packages/container/watch.js | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 packages/container/watch.js diff --git a/packages/container/watch.js b/packages/container/watch.js deleted file mode 100644 index 8288150..0000000 --- a/packages/container/watch.js +++ /dev/null @@ -1,6 +0,0 @@ -const chokidar = require('chokidar') - -// One-liner for current directory -chokidar.watch('.').on('all', (event, path) => { - console.log(event, path) -})