diff --git a/.dockerignore b/.dockerignore
index 2b9daea..a23c257 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,4 +1,5 @@
**/node_modules
.git
**/dist
-**/tsconfig.tsbuildinfo
\ No newline at end of file
+**/tsconfig.tsbuildinfo
+data
\ No newline at end of file
diff --git a/.env.sample b/.env.sample
index 16a135b..73df773 100644
--- a/.env.sample
+++ b/.env.sample
@@ -1,6 +1,6 @@
HOST=localhost
PORT=3000
-BASE_URL=http://localhost:5173
+BASE_URL=http://localhost:8000
MONGO_URL=mongodb://localhost:27017/odyssey-test
SECRET_KEY=sample-secret
DOCKER_NETWORK=linux-odyssey-players-dev
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 7a9bf3e..ef704eb 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -24,7 +24,7 @@ jobs:
uses: ./.github/workflows/build-docker-image.yml
with:
imageName: 'linuxodyssey/analytics'
- dockerfilePath: './packages/analytics/Dockerfile'
+ dockerfilePath: './services/analytics/Dockerfile'
target: prod
secrets: inherit
diff --git a/.gitignore b/.gitignore
index b831f99..0c32b4b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
config
.quest
+tmp
+services/terminal-service/terminal-service
+bin
# Logs
logs
@@ -141,4 +144,5 @@ dist
.pnp.*
#screenshots
-tests/cypress/screenshots
\ No newline at end of file
+tests/cypress/screenshots
+services/cmd-hook/cmd-hook
diff --git a/.vscode/settings.json b/.vscode/settings.json
index fd4883f..1aa2f62 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -4,5 +4,8 @@
"editor.formatOnSave": true,
"yaml.schemas": {
"packages/game/quest.json": "quests/**/game.yml"
+ },
+ "[go]": {
+ "editor.defaultFormatter": "golang.go"
}
}
diff --git a/README.md b/README.md
index 32114ca..7643e06 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,13 @@
[](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/production.yml)
[](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/staging.yml)
+## TLDR
+
+ yarn install
+ cp .env.sample .env
+ docker compose up -d
+ yarn dev
+
## Requirements
- NodeJS v20+
@@ -41,10 +48,6 @@ The configuration is stored in the `.env` file. You can copy from the sample fil
cp .env.sample .env
-The backend will use a SSH key pair to login to the player's container. The private key is on the host, and the public key is copied to the player's container.
-
-The key pair is stored in `./config/ssh_key` and `./config/ssh_key.pub`. If it doesn't exist, the backend will generate a new pair and store them.
-
## Docker Setup
### Docker compose files explaination
@@ -70,7 +73,6 @@ This will start the db. it will automatically restarts by Docker.
Building quest images:
docker compose build base
- yarn build:quests
### Build Quest Images in Details
@@ -108,15 +110,24 @@ The base image is built with docker-compose, so you can use `docker compose buil
### Optional: Mounting Host Directory
-To improve the development experience, we can mount the host directory to the container in the pattern:
+To update the home content of a quest, simply re-create a new session will do. The home content is mounted and copied on container creation.
+
+To further improve the development experience, we can mount the host directory to the container in the pattern:
/path/to/linux-odyssey/quests/${quest_id}/home:/home/commander
- /path/to/linux-odyssey/packages/container:/usr/local/lib/container
You can edit the files on your host, and the changes will be reflected in the container. This is disabled by default. To enable it, you can set the following environment variables:
MOUNT_QUEST=true
+### Optional: Mounting terminal service & cmd-hook
+
+To make the development of terminal services & cmd-hook smoother, we can enable `MOUNT_CLI` to mount the local binaries into container.
+
+ MOUNT_CLI
+
+And you can run `air` inside of `services/terminal-service` or `services/cmd-hook` to get auto recompiling.
+
## Development
Copy `.env.sample` to `.env` and customize it for your needs.
@@ -125,8 +136,6 @@ Run dev server (frontend, backend):
yarn dev
-**IMPORTANT NOTE**: only the app and server have hot-reloading, other packages require manually running `yarn dev` in each package.
-
To develop analytics dashboard:
yarn analytics
diff --git a/app/Dockerfile b/app/Dockerfile
index c3be3df..3c3e03b 100644
--- a/app/Dockerfile
+++ b/app/Dockerfile
@@ -1,5 +1,5 @@
# Base stage
-FROM node:20-alpine as build
+FROM node:20-alpine AS build
WORKDIR /app
COPY package.json yarn.lock ./
@@ -14,7 +14,7 @@ COPY app ./app
RUN yarn workspace app build
# Production stage
-FROM nginx:stable-alpine as prod
+FROM nginx:stable-alpine AS prod
COPY --from=build /app/app/dist /usr/share/nginx/html
COPY deployment/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
diff --git a/app/package.json b/app/package.json
index 7fdc36f..c44e96f 100644
--- a/app/package.json
+++ b/app/package.json
@@ -22,6 +22,7 @@
"dompurify": "^3.2.4",
"marked": "^10.0.0",
"pinia": "^2.1.7",
+ "reconnecting-websocket": "^4.4.0",
"socket.io-client": "^4.7.1",
"validator": "^13.11.0",
"vue": "^3.3.4",
@@ -34,12 +35,12 @@
},
"devDependencies": {
"@types/validator": "^13.11.9",
+ "@vitejs/plugin-vue": "^5.2.2",
"@vue/compiler-sfc": "^3.3.13",
+ "@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
- "@vitejs/plugin-vue": "^5.2.2",
- "@vue/tsconfig": "^0.7.0",
"typescript": "~5.7.2",
"vite": "^6.3.1",
"vue-tsc": "^2.2.8"
diff --git a/app/src/components/game/ControlPalette.vue b/app/src/components/game/ControlPalette.vue
index 8d82084..f5dd047 100644
--- a/app/src/components/game/ControlPalette.vue
+++ b/app/src/components/game/ControlPalette.vue
@@ -1,7 +1,7 @@
diff --git a/app/src/components/header/GameHeaderComponents.vue b/app/src/components/header/GameHeaderComponents.vue
index 1987895..38a8006 100644
--- a/app/src/components/header/GameHeaderComponents.vue
+++ b/app/src/components/header/GameHeaderComponents.vue
@@ -5,7 +5,7 @@ import { useToast } from 'vue-toastification'
import { logout } from '../../utils/auth'
import { trpc } from '../../utils/trpc'
import { openBugReport } from '../../utils/formbricks'
-import useSession from '../../store/session'
+import { useSession } from '../../store/session'
import useUserProfile from '../../store/userProfile'
const userStore = useUserProfile()
diff --git a/app/src/store/session.ts b/app/src/store/session.ts
index 75a35d8..87e3f2d 100644
--- a/app/src/store/session.ts
+++ b/app/src/store/session.ts
@@ -34,7 +34,7 @@ interface Store {
quest: QuestDetailResponse | null
}
-const useSession = defineStore('session', {
+export const useSession = defineStore('session', {
state: (): Store => ({
session: null,
questId: '',
@@ -50,9 +50,13 @@ const useSession = defineStore('session', {
...session,
graph: new FileGraph(session.graph),
pwd: '/home/commander',
+ containerName: session.containerName || '',
}
term.reset()
await socket.connect(this.session)
+ await term.connect(
+ `/terminal/${session.containerName}/ws?token=${session.token}`
+ )
term.focus()
},
async createSession() {
@@ -60,7 +64,7 @@ const useSession = defineStore('session', {
questId: this.questId,
})
await this.setSession(session)
- socket.emit('terminal', 'echo start\n')
+ term.send('echo start\n')
},
async getActiveSession() {
const session = await trpc.session.getActiveSession.query({
@@ -94,12 +98,12 @@ const useSession = defineStore('session', {
},
setup() {
if (hasSetup) return
- socket.on('terminal', (data: string) => {
- term.write(data)
- })
- term.onData((data: string) => {
- socket.emit('terminal', data)
- })
+ // socket.on('terminal', (data: string) => {
+ // term.write(data)
+ // })
+ // term.onData((data: string) => {
+ // socket.emit('terminal', data)
+ // })
socket.on('graph', (event: FileGraphUpdateEvent) => {
if (!this.session) return
this.session.graph.handleEvent(event)
@@ -122,5 +126,3 @@ const useSession = defineStore('session', {
export function useTerminal() {
return term
}
-
-export default useSession
diff --git a/app/src/types.ts b/app/src/types.ts
index 72192f7..a057d56 100644
--- a/app/src/types.ts
+++ b/app/src/types.ts
@@ -21,4 +21,6 @@ export interface Session {
pwd: string
tasks: ITask[]
responses: IResponse[]
+ token: string
+ containerName: string
}
diff --git a/app/src/types/css.d.ts b/app/src/types/css.d.ts
new file mode 100644
index 0000000..8be70df
--- /dev/null
+++ b/app/src/types/css.d.ts
@@ -0,0 +1,4 @@
+declare module '*.css' {
+ const content: string
+ export default content
+}
diff --git a/app/src/utils/formbricks.ts b/app/src/utils/formbricks.ts
index 761a489..b7f4caa 100644
--- a/app/src/utils/formbricks.ts
+++ b/app/src/utils/formbricks.ts
@@ -1,5 +1,5 @@
import formbricks from '@formbricks/js/website'
-import useSession from '../store/session'
+import { useSession } from '../store/session'
import useUserProfile from '../store/userProfile'
if (typeof window !== 'undefined') {
diff --git a/app/src/utils/socket.ts b/app/src/utils/socket.ts
index e520533..48666b7 100644
--- a/app/src/utils/socket.ts
+++ b/app/src/utils/socket.ts
@@ -23,11 +23,9 @@ class SocketWrapper {
this.socket.on('connect', () => {
console.log('Socket connected to session', session._id)
- })
-
- this.socket.once('terminal', () => {
resolve()
})
+
this.socket.on('connect_error', reject)
this.bindListeners()
})
diff --git a/app/src/utils/terminal.ts b/app/src/utils/terminal.ts
index b384844..ec2ef91 100644
--- a/app/src/utils/terminal.ts
+++ b/app/src/utils/terminal.ts
@@ -1,10 +1,12 @@
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
+import ReconnectingWebSocket from 'reconnecting-websocket'
import 'xterm/css/xterm.css'
class SocketTerminal {
private term: Terminal
private fitAddon: FitAddon
+ private socket: ReconnectingWebSocket | null = null
constructor() {
this.term = new Terminal({
@@ -19,6 +21,44 @@ class SocketTerminal {
})
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
+ this.term.onData((data) => {
+ if (this.socket) {
+ this.socket.send(data)
+ }
+ })
+ }
+
+ connect(url: string): Promise {
+ return new Promise((resolve, reject) => {
+ console.log('Connecting to terminal', url)
+ this.term.write('\r\n\x1B[1;32mConnecting to terminal\x1B[0m\r\n')
+ this.socket = new ReconnectingWebSocket(url, [], {
+ connectionTimeout: 5000,
+ maxRetries: 10,
+ })
+ this.socket.onopen = () => {
+ console.log('Connected to terminal')
+ this.term.reset()
+ this.term.write('\r\n\x1B[1;32mConnected to terminal\x1B[0m\r\n')
+ resolve()
+ }
+
+ this.socket.onclose = () => {
+ console.log('Disconnected from terminal')
+ }
+
+ this.socket.onerror = (error) => {
+ console.error('WebSocket error:', error)
+ }
+ this.socket.onmessage = (event) => {
+ console.log('Received message from terminal')
+ const data =
+ typeof event.data === 'string'
+ ? event.data
+ : new TextDecoder().decode(event.data)
+ this.term.write(data)
+ }
+ })
}
resizeScreen(): void {
@@ -34,12 +74,10 @@ class SocketTerminal {
this.resizeScreen()
}
- write(message: string): void {
- this.term.write(message)
- }
-
- onData(callback: (data: string) => void): void {
- this.term.onData(callback)
+ send(message: string): void {
+ if (this.socket) {
+ this.socket.send(message)
+ }
}
focus(): void {
@@ -48,6 +86,7 @@ class SocketTerminal {
reset(): void {
this.term.reset()
+ this.socket?.close()
}
}
diff --git a/app/src/views/GamePage.vue b/app/src/views/GamePage.vue
index ad7cc2b..f281126 100644
--- a/app/src/views/GamePage.vue
+++ b/app/src/views/GamePage.vue
@@ -1,6 +1,6 @@
+
+
+