mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 11:30:13 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a4ce11bcc | ||
|
|
9c650a4c26 | ||
|
|
b96bcb27ad | ||
|
|
b0e12b2027 | ||
|
|
3b68693177 | ||
|
|
fa36b63fbe | ||
|
|
68db16419d | ||
|
|
19cd10ab7b | ||
|
|
bc0055781e | ||
|
|
8c634c8115 | ||
|
|
ceb9416fab | ||
|
|
c6733814ba | ||
|
|
1b94ae75ca | ||
|
|
3e7092dbd6 | ||
|
|
61f627b125 | ||
|
|
a578d4f910 | ||
|
|
1bca61da40 | ||
|
|
660a7ce4e8 | ||
|
|
ac31ca8659 | ||
|
|
9200336b49 | ||
|
|
c243e29002 | ||
|
|
175db7b8bc | ||
|
|
581e75ee57 | ||
|
|
b625abae68 | ||
|
|
b0bfbb789c | ||
|
|
2ce277c7e5 | ||
|
|
7ef77267d3 | ||
|
|
d34ae3ef7a | ||
|
|
bfa7ed1cf9 | ||
|
|
aa5088cc97 | ||
|
|
0d0465f722 | ||
|
|
360279f765 | ||
|
|
6c73a8abc0 | ||
|
|
55a0dcb443 | ||
|
|
5b3c52bd9b | ||
|
|
a911d953cd | ||
|
|
c52b5a53cd | ||
|
|
c9a43eeeb1 | ||
|
|
69624d7a5d | ||
|
|
4681a743b4 | ||
|
|
e0b3ec74de | ||
|
|
d0b179108a | ||
|
|
10ac3c720e | ||
|
|
7a3b6b1cfb | ||
|
|
dc1d3f6ce5 | ||
|
|
5cfe6823cc | ||
|
|
6cd348433a | ||
|
|
617bb56f5f | ||
|
|
a859b4ff3e | ||
|
|
46fb8407a6 | ||
|
|
c7d18ad95e | ||
|
|
8f31e4f2cd | ||
|
|
d4446186cd | ||
|
|
8fb8af036d | ||
|
|
68492cd932 | ||
|
|
3c1afb92e1 | ||
|
|
abaacf7ee7 | ||
|
|
a22b898c0c | ||
|
|
d64a4ae81d | ||
|
|
ebd81f7cb4 | ||
|
|
f657f633b4 | ||
|
|
fb6411e923 | ||
|
|
a7e646c28b | ||
|
|
98a226f3ff | ||
|
|
49fb5a6a61 | ||
|
|
fe69841622 | ||
|
|
bb968706ad | ||
|
|
849e669147 | ||
|
|
b41e67ee0b | ||
|
|
50a0bbbab1 | ||
|
|
d712925d0f | ||
|
|
b78398680d | ||
|
|
c7f52b1003 | ||
|
|
f008fb9f60 | ||
|
|
76a39add29 | ||
|
|
eb9118d5b1 | ||
|
|
cfdf8730e8 | ||
|
|
6651071580 | ||
|
|
f30f6472f5 | ||
|
|
5ac3b3088d | ||
|
|
acc4d5acba | ||
|
|
1fb43b78e3 |
No files matched your search
@@ -15,7 +15,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
# needs: ci-checks
|
needs: ci-checks
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
uses: mattzcarey/code-review-gpt@v0.1.4-alpha
|
uses: mattzcarey/code-review-gpt@v0.1.4-alpha
|
||||||
with:
|
with:
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
MODEL: 'gpt-4'
|
MODEL: 'gpt-3.5-turbo'
|
||||||
GITHUB_TOKEN: ${{ github.token }}
|
GITHUB_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ jobs:
|
|||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
build:
|
build:
|
||||||
# needs: ci-checks
|
needs: ci-checks
|
||||||
uses: ./.github/workflows/build.yml
|
uses: ./.github/workflows/build.yml
|
||||||
secrets: inherit
|
secrets: inherit
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/icon_totem.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Linux Odyssey</title>
|
<title>Linux Odyssey</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
+3
-1
@@ -20,7 +20,9 @@
|
|||||||
"@linux-odyssey/file-graph": "*",
|
"@linux-odyssey/file-graph": "*",
|
||||||
"@linux-odyssey/utils": "*",
|
"@linux-odyssey/utils": "*",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
"dompurify": "^3.0.6",
|
||||||
"echarts": "^5.4.3",
|
"echarts": "^5.4.3",
|
||||||
|
"marked": "^10.0.0",
|
||||||
"socket.io-client": "^4.7.1",
|
"socket.io-client": "^4.7.1",
|
||||||
"validator": "^13.11.0",
|
"validator": "^13.11.0",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
@@ -36,6 +38,6 @@
|
|||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.3.3",
|
||||||
"vite": "^4.4.0"
|
"vite": "^4.4.12"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 630 KiB |
@@ -89,8 +89,13 @@ const handleChange = () => {
|
|||||||
<div
|
<div
|
||||||
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
|
class="bg-bg border-8 border-bg-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
|
||||||
>
|
>
|
||||||
<h1 class="text-text-primary text-3xl font-black mb-2">{{ title }}</h1>
|
<div class="w-full flex items-center justify-center mb-2 gap-3">
|
||||||
<div class="text-error inline-flex justify-center items-center xl:hidden">
|
<img src="../img/icon_totem.svg" class="h-10" />
|
||||||
|
<h1 class="text-text-primary text-3xl font-black" id="title">
|
||||||
|
{{ title }}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div class="text-error inline-flex justify-center items-center md:hidden">
|
||||||
<font-awesome-icon
|
<font-awesome-icon
|
||||||
:icon="['fas', 'circle-exclamation']"
|
:icon="['fas', 'circle-exclamation']"
|
||||||
class="px-1 text-sm"
|
class="px-1 text-sm"
|
||||||
@@ -163,6 +168,7 @@ const handleChange = () => {
|
|||||||
/>
|
/>
|
||||||
<label
|
<label
|
||||||
class="text-sm font-normal text-text-secondary"
|
class="text-sm font-normal text-text-secondary"
|
||||||
|
id="NameRules"
|
||||||
v-if="type === 'username' || type === 'register'"
|
v-if="type === 'username' || type === 'register'"
|
||||||
>
|
>
|
||||||
<p>可用小寫字母、數字、"_"和"-"</p>
|
<p>可用小寫字母、數字、"_"和"-"</p>
|
||||||
|
|||||||
@@ -78,15 +78,16 @@ onMounted(async () => {
|
|||||||
class="text-border h-full w-full"
|
class="text-border h-full w-full"
|
||||||
/>
|
/>
|
||||||
</button> -->
|
</button> -->
|
||||||
|
<img src="../img/icon_totem.svg" class="h-5" />
|
||||||
<button
|
<button
|
||||||
id="HeaderText"
|
id="GameTitle"
|
||||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
||||||
style="font-size: 2vh"
|
style="font-size: 2vh"
|
||||||
>
|
>
|
||||||
Linux Odyssey
|
Linux Odyssey
|
||||||
</button>
|
</button>
|
||||||
<p
|
<p
|
||||||
id="HeaderText"
|
id="QuestTitle"
|
||||||
class="text-text inline-block font-bold whitespace-nowrap"
|
class="text-text inline-block font-bold whitespace-nowrap"
|
||||||
style="font-size: 2vh"
|
style="font-size: 2vh"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import sessionStore from '../store/session'
|
import sessionStore from '../store/session'
|
||||||
|
import MarkdownText from './MarkdownText.vue'
|
||||||
|
|
||||||
const current = ref(-1)
|
const current = ref(-1)
|
||||||
watch(
|
watch(
|
||||||
@@ -69,7 +70,7 @@ const right = () => {
|
|||||||
:key="hint"
|
:key="hint"
|
||||||
class="text-text font-xl whitespace-pre-wrap"
|
class="text-text font-xl whitespace-pre-wrap"
|
||||||
>
|
>
|
||||||
{{ hint }}
|
<MarkdownText :content="hint" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<br />
|
<br />
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed, defineProps } from 'vue'
|
||||||
|
import markdown2HTML from '../utils/markdown'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
content: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const html = computed(() => {
|
||||||
|
return markdown2HTML(props.content)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="markdown-content" v-html="html"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.markdown-content {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-content :deep(ol) {
|
||||||
|
list-style: decimal;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
.markdown-content :deep(code) {
|
||||||
|
background-color: #282a33;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0.25rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -15,8 +15,8 @@ import { DAG } from '@linux-odyssey/utils'
|
|||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { NodeImage } from '../img/svg.js'
|
import { NodeImage } from '../img/svg.js'
|
||||||
|
|
||||||
const marginX = 200
|
const marginX = 500
|
||||||
const marginY = 65
|
const marginY = 300
|
||||||
const chartContainer = ref(null)
|
const chartContainer = ref(null)
|
||||||
let chartInstance = null
|
let chartInstance = null
|
||||||
const fullwidth = window.screen.width
|
const fullwidth = window.screen.width
|
||||||
@@ -71,9 +71,10 @@ const genOption = (nodes, edges) => ({
|
|||||||
symbol: () => {
|
symbol: () => {
|
||||||
return NodeImage
|
return NodeImage
|
||||||
},
|
},
|
||||||
symbolSize: [fullwidth / 10, fullwidth / 25],
|
symbolSize: [fullwidth / 13, fullwidth / 30],
|
||||||
roam: 'move',
|
roam: 'move',
|
||||||
zoom: 1,
|
zoom: 1.5,
|
||||||
|
center: ['120%', '30%'],
|
||||||
label: {
|
label: {
|
||||||
show: true,
|
show: true,
|
||||||
fontSize: fullwidth / 90,
|
fontSize: fullwidth / 90,
|
||||||
@@ -103,7 +104,7 @@ const genOption = (nodes, edges) => ({
|
|||||||
if (unlocked) {
|
if (unlocked) {
|
||||||
return '#ADADB5'
|
return '#ADADB5'
|
||||||
}
|
}
|
||||||
return '454552'
|
return '#505050'
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,19 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import MarkdownText from './MarkdownText.vue'
|
||||||
import sessionStore from '../store/session'
|
import sessionStore from '../store/session'
|
||||||
|
|
||||||
|
const tasks = computed(() => {
|
||||||
|
return sessionStore.session.tasks.map((task) => {
|
||||||
|
const prefix = task.completed ? '✓' : '➤'
|
||||||
|
const color = task.completed ? 'text-text-primary' : 'text-text'
|
||||||
|
return {
|
||||||
|
...task,
|
||||||
|
class: `${color} markdown-content`,
|
||||||
|
content: `${prefix} ${task.name}`,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -10,22 +24,18 @@ import sessionStore from '../store/session'
|
|||||||
<br />
|
<br />
|
||||||
<div>
|
<div>
|
||||||
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
|
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
|
||||||
<p class="text-text">{{ sessionStore.quest.instruction }}</p>
|
<MarkdownText
|
||||||
|
class="text-text markdown-content"
|
||||||
|
:content="sessionStore.quest.instruction"
|
||||||
|
/>
|
||||||
<br />
|
<br />
|
||||||
<p id="tasks" class="text-text">
|
<p id="tasks" class="text-text">
|
||||||
<!-- Tasks: -->
|
<!-- Tasks: -->
|
||||||
任務:
|
任務:
|
||||||
</p>
|
</p>
|
||||||
<ul v-if="sessionStore.session">
|
<ul v-if="sessionStore.session">
|
||||||
<li v-for="task in sessionStore.session.tasks" :key="task.id">
|
<li v-for="task in tasks" :key="task.id">
|
||||||
<p v-if="task.completed" class="text-text-primary">
|
<MarkdownText :class="task.class" :content="task.content" />
|
||||||
<span class=""> ✓</span>
|
|
||||||
{{ task.name }}
|
|
||||||
</p>
|
|
||||||
<p v-else class="text-text">
|
|
||||||
<span class="">➤</span>
|
|
||||||
{{ task.name }}
|
|
||||||
</p>
|
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -121,6 +121,6 @@ li.discovered {
|
|||||||
}
|
}
|
||||||
|
|
||||||
a.pwd {
|
a.pwd {
|
||||||
box-shadow: 0 0 10px rgba(255, 255, 255, 0.5);
|
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<svg width="638" height="732" viewBox="0 0 638 732" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||||
|
<path d="M0 0H638V732H0V0Z" fill="url(#pattern0)"/>
|
||||||
|
<defs>
|
||||||
|
<pattern id="pattern0" patternContentUnits="objectBoundingBox" width="1" height="1">
|
||||||
|
<use xlink:href="#image0_379_14" transform="matrix(0.00126582 0 0 0.00110327 0 -0.000333748)"/>
|
||||||
|
</pattern>
|
||||||
|
<image id="image0_379_14" width="790" height="907" xlink:href="data:image/png;base64,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 truncated
|
||||||
|
</defs>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 630 KiB |
@@ -0,0 +1,7 @@
|
|||||||
|
import { marked } from 'marked'
|
||||||
|
import DOMPurify from 'dompurify'
|
||||||
|
|
||||||
|
export default function markdown2HTML(markdown) {
|
||||||
|
const html = marked.parse(markdown)
|
||||||
|
return DOMPurify.sanitize(html)
|
||||||
|
}
|
||||||
@@ -13,12 +13,11 @@ class SocketTerminal {
|
|||||||
disableStdin: false, // 是否应禁用输入
|
disableStdin: false, // 是否应禁用输入
|
||||||
// cursorStyle: "underline", //光标样式
|
// cursorStyle: "underline", //光标样式
|
||||||
cursorBlink: true, // 光标闪烁
|
cursorBlink: true, // 光标闪烁
|
||||||
theme: {
|
foreground: '#ECECEC', // 字体
|
||||||
foreground: '#ECECEC', // 字体
|
background: '#000000', // 背景色
|
||||||
background: '#000000', // 背景色
|
cursor: 'help', // 设置光标
|
||||||
cursor: 'help', // 设置光标
|
fontSize: 18,
|
||||||
lineHeight: 20,
|
fontWeight: 'normal',
|
||||||
},
|
|
||||||
})
|
})
|
||||||
this.fitAddon = new FitAddon()
|
this.fitAddon = new FitAddon()
|
||||||
this.term.loadAddon(this.fitAddon)
|
this.term.loadAddon(this.fitAddon)
|
||||||
|
|||||||
@@ -29,4 +29,38 @@ down() {
|
|||||||
docker compose -f docker-compose.testing.yml down
|
docker compose -f docker-compose.testing.yml down
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dump() {
|
||||||
|
# Define service name and dump directory name
|
||||||
|
SERVICE_NAME="db"
|
||||||
|
DUMP_NAME="mongodump-$(date --iso).archive"
|
||||||
|
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
|
||||||
|
|
||||||
|
# Fetch credentials from Docker environment variables
|
||||||
|
MONGO_URL=$(docker compose exec -T backend printenv MONGO_URL)
|
||||||
|
echo mongo uri: $MONGO_URL
|
||||||
|
|
||||||
|
# Dump MongoDB data using credentials
|
||||||
|
docker compose exec -T $SERVICE_NAME sh -c "mongodump --gzip --archive=/tmp/$DUMP_NAME -d odyssey-test" &&
|
||||||
|
|
||||||
|
# Copy the dump from the container to the host
|
||||||
|
# Note: You will need to know the exact container name for this step
|
||||||
|
docker cp ${CONTAINER_ID}:/tmp/$DUMP_NAME . &&
|
||||||
|
|
||||||
|
echo "MongoDB data dumped successfully."
|
||||||
|
}
|
||||||
|
|
||||||
|
restore() {
|
||||||
|
# Define service name and dump directory name
|
||||||
|
SERVICE_NAME="db"
|
||||||
|
DUMP_NAME=$1
|
||||||
|
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
|
||||||
|
|
||||||
|
# Copy the dump from the host to the container
|
||||||
|
docker cp $DUMP_NAME ${CONTAINER_ID}:/tmp &&
|
||||||
|
|
||||||
|
# Dump MongoDB data using credentials
|
||||||
|
docker compose exec -T $SERVICE_NAME sh -c "mongorestore --gzip --archive=/tmp/$DUMP_NAME" &&
|
||||||
|
|
||||||
|
echo "MongoDB data dumped successfully."
|
||||||
|
}
|
||||||
$@
|
$@
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
"@linux-odyssey/models": "*",
|
"@linux-odyssey/models": "*",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"express-basic-auth": "^1.2.1",
|
"express-basic-auth": "^1.2.1",
|
||||||
"express-handlebars": "^7.1.2"
|
"express-handlebars": "^7.1.2",
|
||||||
|
"mongoose": "^8.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
|
|||||||
Whitespace-only changes.
@@ -1,9 +1,24 @@
|
|||||||
|
/* eslint-disable object-shorthand */
|
||||||
|
import Pagination from '../models/pagination.js'
|
||||||
import { errorCommands } from '../models/commands.js'
|
import { errorCommands } from '../models/commands.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export async function commandListController(req, res) {
|
export async function commandListController(req, res) {
|
||||||
const payload = {
|
const { nextKey, order } = req.query
|
||||||
errorCommands: await errorCommands(),
|
const itemsPerPage = 100
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pagination = new Pagination(order, itemsPerPage, nextKey)
|
||||||
|
const payload = await errorCommands(pagination)
|
||||||
|
const newNextKey = payload[payload.length - 1]?._id
|
||||||
|
|
||||||
|
res.render('commands', {
|
||||||
|
errorCommands: payload,
|
||||||
|
nextKey: newNextKey,
|
||||||
|
order: pagination.getOrder(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Error fetching data')
|
||||||
}
|
}
|
||||||
res.render('commands', payload)
|
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,24 @@
|
|||||||
import { userCount } from '../models/users.js'
|
import { userCount } from '../models/users.js'
|
||||||
import { sessionCount, sessionCompleted } from '../models/sessions.js'
|
import { questList } from '../models/quests.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export async function homeController(req, res) {
|
export async function homeController(req, res) {
|
||||||
|
const quests = await questList()
|
||||||
|
const [totalQuests, totalCompleted] = quests.reduce(
|
||||||
|
([total, completed], quest) => [
|
||||||
|
total + quest.started,
|
||||||
|
completed + quest.completed,
|
||||||
|
],
|
||||||
|
[0, 0]
|
||||||
|
)
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
title: 'Linux Odyssey Admin',
|
title: 'Linux Odyssey Admin',
|
||||||
userCount: await userCount(),
|
userCount: await userCount(),
|
||||||
sessionCount: await sessionCount(),
|
totalQuests,
|
||||||
sessionCompleted: await sessionCompleted(),
|
totalCompleted,
|
||||||
}
|
}
|
||||||
payload.completedRate =
|
payload.completedRate =
|
||||||
Math.round((payload.sessionCompleted / payload.sessionCount) * 10000) / 100
|
Math.round((payload.totalCompleted / payload.totalQuests) * 10000) / 100
|
||||||
res.render('home', payload)
|
res.render('home', payload)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { questList } from '../models/quests.js'
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
|
export async function questListController(req, res) {
|
||||||
|
const quests = await questList()
|
||||||
|
quests.forEach((quest) => {
|
||||||
|
quest.failed = quest.started - quest.completed
|
||||||
|
})
|
||||||
|
res.render('quests', { quests })
|
||||||
|
}
|
||||||
@@ -1,8 +1,24 @@
|
|||||||
|
import Pagination from '../models/pagination.js'
|
||||||
import { sessionList, sessionDetail } from '../models/sessions.js'
|
import { sessionList, sessionDetail } from '../models/sessions.js'
|
||||||
|
|
||||||
export async function sessionListController(req, res) {
|
export async function sessionListController(req, res) {
|
||||||
const sessions = await sessionList()
|
const { nextKey, order } = req.query
|
||||||
res.render('sessions', { sessions })
|
const itemsPerPage = 50
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pagination = new Pagination(order, itemsPerPage, nextKey)
|
||||||
|
const sessions = await sessionList(pagination)
|
||||||
|
const newNextKey = sessions[sessions.length - 1]?._id
|
||||||
|
|
||||||
|
res.render('sessions', {
|
||||||
|
sessions,
|
||||||
|
nextKey: newNextKey,
|
||||||
|
order: pagination.getOrder(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
res.status(500).send('Error fetching session data')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sessionDetailController(req, res) {
|
export async function sessionDetailController(req, res) {
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
|
import Pagination from '../models/pagination.js'
|
||||||
import { userList } from '../models/users.js'
|
import { userList } from '../models/users.js'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
|
||||||
export async function userListController(req, res) {
|
export async function userListController(req, res) {
|
||||||
res.render('users', { users: await userList() })
|
const { nextKey, order } = req.query
|
||||||
|
const itemsPerPage = 50
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pagination = new Pagination(order, itemsPerPage, nextKey)
|
||||||
|
const users = await userList(pagination)
|
||||||
|
const newNextKey = users[users.length - 1]?._id
|
||||||
|
|
||||||
|
res.render('users', {
|
||||||
|
users,
|
||||||
|
nextKey: newNextKey,
|
||||||
|
order: pagination.getOrder(),
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
res.status(500).send('Error fetching user data')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +1,44 @@
|
|||||||
import { Command } from '@linux-odyssey/models'
|
import { Command } from '@linux-odyssey/models'
|
||||||
|
|
||||||
// eslint-disable-next-line import/prefer-default-export
|
// eslint-disable-next-line import/prefer-default-export
|
||||||
export async function errorCommands() {
|
export async function errorCommands(pagination) {
|
||||||
const commands = await Command.find({
|
const commands = await Command.aggregate([
|
||||||
$and: [
|
pagination.match('_id'),
|
||||||
{ error: { $exists: true } },
|
{
|
||||||
{ error: { $ne: null } },
|
$match: {
|
||||||
{ error: { $ne: '' } },
|
$and: [
|
||||||
],
|
{ error: { $exists: true } },
|
||||||
})
|
{ error: { $ne: null } },
|
||||||
.populate({
|
{ error: { $ne: '' } },
|
||||||
path: 'session',
|
],
|
||||||
populate: { path: 'user' },
|
},
|
||||||
})
|
},
|
||||||
.exec()
|
{
|
||||||
return commands.map(({ command, error, createdAt, session }) => {
|
$lookup: {
|
||||||
|
from: 'sessions', // 替換成 'session' 對應的集合名稱
|
||||||
|
localField: 'session', // 替換成 'session' 對應的字段名稱
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'session',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $unwind: '$session' },
|
||||||
|
{
|
||||||
|
$lookup: {
|
||||||
|
from: 'users', // 替換成 'user' 對應的集合名稱
|
||||||
|
localField: 'session.user', // 替換成 'user' 對應的字段名稱
|
||||||
|
foreignField: '_id',
|
||||||
|
as: 'session.user',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ $unwind: '$session.user' },
|
||||||
|
pagination.sort('_id'),
|
||||||
|
pagination.limit(),
|
||||||
|
])
|
||||||
|
|
||||||
|
return commands.map(({ _id, command, error, createdAt, session }) => {
|
||||||
const { user, quest } = session
|
const { user, quest } = session
|
||||||
return {
|
return {
|
||||||
|
_id,
|
||||||
command: command.slice(0, 20),
|
command: command.slice(0, 20),
|
||||||
error: error.slice(0, 50),
|
error: error.slice(0, 50),
|
||||||
createdAt: createdAt?.toLocaleString(),
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/* eslint-disable lines-between-class-members */
|
||||||
|
/* eslint-disable class-methods-use-this */
|
||||||
|
// eslint-disable-next-line max-classes-per-file
|
||||||
|
import mongoose from 'mongoose'
|
||||||
|
|
||||||
|
export const DESC = 'desc'
|
||||||
|
export const ASC = 'asc'
|
||||||
|
|
||||||
|
class Asc {
|
||||||
|
getOperator() {
|
||||||
|
return '$gt'
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return ASC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Desc {
|
||||||
|
getOperator() {
|
||||||
|
return '$lt'
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
getName() {
|
||||||
|
return DESC
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class Pagination {
|
||||||
|
nextKey = null
|
||||||
|
itemsPerPage = 50
|
||||||
|
order = null
|
||||||
|
|
||||||
|
constructor(order, itemsPerPage = 50, nextKey = null) {
|
||||||
|
this.itemsPerPage = itemsPerPage
|
||||||
|
if (!order || order === DESC) {
|
||||||
|
this.order = new Desc()
|
||||||
|
} else if (order === ASC) {
|
||||||
|
this.order = new Asc()
|
||||||
|
} else {
|
||||||
|
throw new Error('order must be asc or desc')
|
||||||
|
}
|
||||||
|
if (nextKey) {
|
||||||
|
this.nextKey = new mongoose.Types.ObjectId(nextKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match(field) {
|
||||||
|
const stage = this.nextKey
|
||||||
|
? { [field]: { [this.order.getOperator()]: this.nextKey } }
|
||||||
|
: {}
|
||||||
|
return { $match: stage }
|
||||||
|
}
|
||||||
|
|
||||||
|
sort(field) {
|
||||||
|
return { $sort: { [field]: this.order.getOrder() } }
|
||||||
|
}
|
||||||
|
|
||||||
|
limit() {
|
||||||
|
return { $limit: this.itemsPerPage }
|
||||||
|
}
|
||||||
|
|
||||||
|
getOrder() {
|
||||||
|
return this.order.getName()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* eslint-disable object-shorthand */
|
||||||
|
/* eslint-disable func-names */
|
||||||
|
import { UserProfile } from '@linux-odyssey/models'
|
||||||
|
|
||||||
|
export function questList() {
|
||||||
|
return UserProfile.aggregate([
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
progress: {
|
||||||
|
$objectToArray: '$progress',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$unwind: '$progress',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$progress.v.quest',
|
||||||
|
started: {
|
||||||
|
$sum: 1,
|
||||||
|
},
|
||||||
|
completed: {
|
||||||
|
$sum: {
|
||||||
|
$cond: {
|
||||||
|
if: { $eq: ['$progress.v.completed', true] },
|
||||||
|
then: 1,
|
||||||
|
else: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_id: 1,
|
||||||
|
started: 1,
|
||||||
|
completed: 1,
|
||||||
|
completedRate: {
|
||||||
|
$cond: {
|
||||||
|
if: { $eq: ['$started', 0] },
|
||||||
|
then: 0,
|
||||||
|
else: {
|
||||||
|
$round: [
|
||||||
|
{ $multiply: [{ $divide: ['$completed', '$started'] }, 100] },
|
||||||
|
0,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: {
|
||||||
|
completed: -1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
}
|
||||||
@@ -1,28 +1,5 @@
|
|||||||
import { Command, Session } from '@linux-odyssey/models'
|
import { Command, Session } from '@linux-odyssey/models'
|
||||||
|
|
||||||
export function sessionCount() {
|
|
||||||
return Session.find().count()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function sessionCompleted() {
|
|
||||||
return Session.find({ status: 'finished' }).count()
|
|
||||||
}
|
|
||||||
|
|
||||||
export function averageTimeUsage() {
|
|
||||||
return Session.aggregate([
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$quest',
|
|
||||||
average: {
|
|
||||||
$avg: {
|
|
||||||
$subtract: ['$finishedAt', '$createdAt'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(time) {
|
function formatTime(time) {
|
||||||
const seconds = Math.floor(time / 1000)
|
const seconds = Math.floor(time / 1000)
|
||||||
const minutes = Math.floor(seconds / 60)
|
const minutes = Math.floor(seconds / 60)
|
||||||
@@ -30,8 +7,9 @@ function formatTime(time) {
|
|||||||
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
return `${hours}h ${minutes % 60}m ${seconds % 60}s`
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function sessionList() {
|
export async function sessionList(pagination) {
|
||||||
const sessions = await Session.aggregate([
|
const sessions = await Session.aggregate([
|
||||||
|
pagination.match('_id'),
|
||||||
{
|
{
|
||||||
$lookup: {
|
$lookup: {
|
||||||
from: 'users',
|
from: 'users',
|
||||||
@@ -49,7 +27,10 @@ export async function sessionList() {
|
|||||||
as: 'commands',
|
as: 'commands',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
pagination.sort('_id'),
|
||||||
|
pagination.limit(),
|
||||||
])
|
])
|
||||||
|
|
||||||
return sessions.map(
|
return sessions.map(
|
||||||
({
|
({
|
||||||
_id,
|
_id,
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ export function userCount() {
|
|||||||
return User.find().count()
|
return User.find().count()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function userList() {
|
export async function userList(pagination) {
|
||||||
const users = await Session.aggregate([
|
const users = await Session.aggregate([
|
||||||
|
pagination.match('user'),
|
||||||
{
|
{
|
||||||
$group: {
|
$group: {
|
||||||
_id: '$user',
|
_id: '$user',
|
||||||
@@ -29,11 +30,14 @@ export async function userList() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ $unwind: '$userData' },
|
{ $unwind: '$userData' },
|
||||||
{ $sort: { 'userData.createdAt': 1 } },
|
pagination.sort('userData._id'),
|
||||||
|
pagination.limit(),
|
||||||
])
|
])
|
||||||
|
|
||||||
return users.map(({ userData, count, lastActivityAt }) => {
|
return users.map(({ userData, count, lastActivityAt }) => {
|
||||||
const { username, email, createdAt } = userData
|
const { _id, username, email, createdAt } = userData
|
||||||
return {
|
return {
|
||||||
|
_id,
|
||||||
username,
|
username,
|
||||||
email,
|
email,
|
||||||
createdAt: createdAt?.toLocaleString(),
|
createdAt: createdAt?.toLocaleString(),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { Router } from 'express'
|
|||||||
|
|
||||||
import { homeController } from './controllers/index.js'
|
import { homeController } from './controllers/index.js'
|
||||||
import { userListController } from './controllers/users.js'
|
import { userListController } from './controllers/users.js'
|
||||||
|
import { questListController } from './controllers/quests.js'
|
||||||
import { commandListController } from './controllers/commands.js'
|
import { commandListController } from './controllers/commands.js'
|
||||||
import {
|
import {
|
||||||
sessionDetailController,
|
sessionDetailController,
|
||||||
@@ -12,6 +13,7 @@ const router = Router()
|
|||||||
|
|
||||||
router.get('/', homeController)
|
router.get('/', homeController)
|
||||||
router.get('/users', userListController)
|
router.get('/users', userListController)
|
||||||
|
router.get('/quests', questListController)
|
||||||
router.get('/commands', commandListController)
|
router.get('/commands', commandListController)
|
||||||
router.get('/sessions', sessionListController)
|
router.get('/sessions', sessionListController)
|
||||||
router.get('/sessions/:id', sessionDetailController)
|
router.get('/sessions/:id', sessionDetailController)
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import mongoose from 'mongoose'
|
||||||
|
import Pagination, { ASC, DESC } from '../src/models/pagination.js'
|
||||||
|
|
||||||
|
describe('Pagination', () => {
|
||||||
|
it('should failed when not provide order', () => {
|
||||||
|
expect(new Pagination('').getOrder()).toBe(DESC)
|
||||||
|
expect(() => new Pagination('Hahaha')).toThrow()
|
||||||
|
})
|
||||||
|
it('should return correct limit', () => {
|
||||||
|
const pagination = new Pagination(DESC, 10)
|
||||||
|
|
||||||
|
expect(pagination.limit()).toMatchObject({ $limit: 10 })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('desc should return correct order', async () => {
|
||||||
|
const pagination = new Pagination(DESC, 10)
|
||||||
|
|
||||||
|
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: -1 } })
|
||||||
|
expect(pagination.sort('time')).toMatchObject({ $sort: { time: -1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('asc should return correct order', async () => {
|
||||||
|
const pagination = new Pagination(ASC, 10)
|
||||||
|
|
||||||
|
expect(pagination.sort('_id')).toMatchObject({ $sort: { _id: 1 } })
|
||||||
|
expect(pagination.sort('time')).toMatchObject({ $sort: { time: 1 } })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correct match stage for empty nextKey', async () => {
|
||||||
|
const pagination = new Pagination(DESC, 10)
|
||||||
|
|
||||||
|
expect(pagination.match('_id')).toMatchObject({ $match: {} })
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should return correct match stage for empty nextKey', async () => {
|
||||||
|
const nextKeyStr = '656c7fea28a4c087bfcefe12'
|
||||||
|
const pagination = new Pagination(DESC, 10, nextKeyStr)
|
||||||
|
const nextKey = new mongoose.Types.ObjectId(nextKeyStr)
|
||||||
|
|
||||||
|
expect(pagination.match('_id')).toMatchObject({
|
||||||
|
$match: {
|
||||||
|
_id: {
|
||||||
|
$lt: nextKey,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
<h1>Commands</h1>
|
<h1>Commands</h1>
|
||||||
<div>
|
<div>
|
||||||
<h2>Error commands</h2>
|
<h2>Error commands</h2>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/commands?order=asc'>Ascending</a>
|
||||||
|
<a href='/commands?order=desc'>Descending</a>
|
||||||
|
</div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Command</th>
|
<th>Command</th>
|
||||||
@@ -21,4 +25,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/commands?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,18 +1,18 @@
|
|||||||
<h1>Welcome to {{title}}</h1>
|
<h1>Welcome to {{title}}</h1>
|
||||||
<div>
|
<div>
|
||||||
<h2>Users</h2>
|
<h2>Overview</h2>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Registered Users</th>
|
<th>Registered Users</th>
|
||||||
<td>{{userCount}}</td>
|
<td>{{userCount}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Session Count</th>
|
<th>Total Started Quests</th>
|
||||||
<td>{{sessionCount}}</td>
|
<td>{{totalQuests}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Completed Session</th>
|
<th>Completed Quests</th>
|
||||||
<td>{{sessionCompleted}}</td>
|
<td>{{totalCompleted}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Completed Rate</th>
|
<th>Completed Rate</th>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<ul>
|
<ul>
|
||||||
<li><a href='/'>Home</a></li>
|
<li><a href='/'>Home</a></li>
|
||||||
<li><a href='/users'>Users</a></li>
|
<li><a href='/users'>Users</a></li>
|
||||||
|
<li><a href='/quests'>Quests</a></li>
|
||||||
<li><a href='/commands'>Commands</a></li>
|
<li><a href='/commands'>Commands</a></li>
|
||||||
<li><a href='/sessions'>Sessions</a></li>
|
<li><a href='/sessions'>Sessions</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<h1>Quests</h1>
|
||||||
|
<div>
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Started</th>
|
||||||
|
<th>Completed</th>
|
||||||
|
<th>Failed</th>
|
||||||
|
<th>Completed Rate</th>
|
||||||
|
</tr>
|
||||||
|
{{#each quests}}
|
||||||
|
<tr>
|
||||||
|
<td>{{_id}}</td>
|
||||||
|
<td>{{started}}</td>
|
||||||
|
<td>{{completed}}</td>
|
||||||
|
<td>{{failed}}</td>
|
||||||
|
<td>{{completedRate}} %</td>
|
||||||
|
</tr>
|
||||||
|
{{/each}}
|
||||||
|
</table>
|
||||||
@@ -36,20 +36,20 @@
|
|||||||
|
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Command</th>
|
<th>Stage</th>
|
||||||
<th>PWD</th>
|
<th>PWD</th>
|
||||||
|
<th>Input</th>
|
||||||
<th>Output</th>
|
<th>Output</th>
|
||||||
<th>Error</th>
|
<th>Error</th>
|
||||||
<th>Stage</th>
|
<th>Input Time</th>
|
||||||
<th>Created At</th>
|
|
||||||
</tr>
|
</tr>
|
||||||
{{#each commands}}
|
{{#each commands}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{command}}</td>
|
<td>{{stage}}</td>
|
||||||
<td>{{pwd}}</td>
|
<td>{{pwd}}</td>
|
||||||
|
<td>{{command}}</td>
|
||||||
<td>{{output}}</td>
|
<td>{{output}}</td>
|
||||||
<td>{{error}}</td>
|
<td>{{error}}</td>
|
||||||
<td>{{stage}}</td>
|
|
||||||
<td>{{createdAt}}</td>
|
<td>{{createdAt}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
<h1>Sessions</h1>
|
<h1>Sessions</h1>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/sessions?order=asc'>Ascending</a>
|
||||||
|
<a href='/sessions?order=desc'>Descending</a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>User</th>
|
<th>User</th>
|
||||||
<th>Quest</th>
|
<th>Quest</th>
|
||||||
<th></th>
|
|
||||||
<th>Status</th>
|
<th>Status</th>
|
||||||
<th>Created At</th>
|
<th>Created At</th>
|
||||||
<th>Finished At</th>
|
<th>Finished At</th>
|
||||||
<th>Used Time</th>
|
<th>Used Time</th>
|
||||||
<th>Command Count</th>
|
<th>Command Count</th>
|
||||||
|
<th>Session</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{#each sessions}}
|
{{#each sessions}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{user}}</td>
|
<td>{{user}}</td>
|
||||||
<td>{{quest}}</td>
|
<td>{{quest}}</td>
|
||||||
<td><a href='/sessions/{{_id}}'>Detail</a></td>
|
|
||||||
<td>{{status}}</td>
|
<td>{{status}}</td>
|
||||||
<td>{{createdAt}}</td>
|
<td>{{createdAt}}</td>
|
||||||
<td>{{finishedAt}}</td>
|
<td>{{finishedAt}}</td>
|
||||||
<td>{{usedTime}}</td>
|
<td>{{usedTime}}</td>
|
||||||
<td>{{commandCount}}</td>
|
<td>{{commandCount}}</td>
|
||||||
|
<td><a href='/sessions/{{_id}}'>Detail</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/sessions?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,23 +1,30 @@
|
|||||||
<h1>Users List</h1>
|
<h1>Users List</h1>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/users?order=asc'>Ascending</a>
|
||||||
|
<a href='/users?order=desc'>Descending</a>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<table>
|
<table>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Email</th>
|
|
||||||
<th>Registration Method</th>
|
|
||||||
<th>Registration Time</th>
|
|
||||||
<th>Sessions Count</th>
|
|
||||||
<th>Last Activity</th>
|
<th>Last Activity</th>
|
||||||
|
<th>Sessions Count</th>
|
||||||
|
<th>Email</th>
|
||||||
|
<th>Registration Time</th>
|
||||||
|
<th>Registration Method</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{#each users}}
|
{{#each users}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{username}}</td>
|
<td href='/users/{{_id}}'>{{username}}</td>
|
||||||
<td>{{email}}</td>
|
|
||||||
<td>{{method}}</td>
|
|
||||||
<td>{{createdAt}}</td>
|
|
||||||
<td>{{sessionCount}}</td>
|
|
||||||
<td>{{lastActivityAt}}</td>
|
<td>{{lastActivityAt}}</td>
|
||||||
|
<td>{{sessionCount}}</td>
|
||||||
|
<td>{{email}}</td>
|
||||||
|
<td>{{createdAt}}</td>
|
||||||
|
<td>{{method}}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class='pagination'>
|
||||||
|
<a href='/users?nextKey={{nextKey}}&order={{order}}'>Next</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -48,10 +48,10 @@ async function handleCommand(command) {
|
|||||||
|
|
||||||
async function handleResponse({ responses, hints, end }) {
|
async function handleResponse({ responses, hints, end }) {
|
||||||
if (responses) {
|
if (responses) {
|
||||||
await printResponses(responses, 60)
|
await printResponses(responses, 90)
|
||||||
}
|
}
|
||||||
if (hints) {
|
if (hints) {
|
||||||
await printHints(hints, 60)
|
await printHints(hints, 90)
|
||||||
}
|
}
|
||||||
if (end) {
|
if (end) {
|
||||||
console.log(colorize('Quest completed!', 'blue'))
|
console.log(colorize('Quest completed!', 'blue'))
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ const RETURN_SYMBOL = '\u21B5'
|
|||||||
|
|
||||||
// ANSI color map
|
// ANSI color map
|
||||||
const ANSI_COLORS = {
|
const ANSI_COLORS = {
|
||||||
|
default: 0,
|
||||||
black: 30,
|
black: 30,
|
||||||
red: 31,
|
red: 31,
|
||||||
green: 32,
|
green: 32,
|
||||||
@@ -16,10 +17,13 @@ const ANSI_COLORS = {
|
|||||||
white: 37,
|
white: 37,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function colorCode(color) {
|
||||||
|
return `\x1b[${ANSI_COLORS[color]}m`
|
||||||
|
}
|
||||||
|
|
||||||
function colorize(content, color) {
|
function colorize(content, color) {
|
||||||
const colorCode = ANSI_COLORS[color]
|
if (!color) return content
|
||||||
if (!colorCode) return content
|
return `${colorCode(color)}${content}${colorCode('default')}`
|
||||||
return `\x1b[${colorCode}m${content}\x1b[0m`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
process.stdin.setRawMode(true)
|
process.stdin.setRawMode(true)
|
||||||
@@ -36,8 +40,25 @@ function waitForEnter() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function formatLine(content, color) {
|
||||||
|
let result = colorCode(color)
|
||||||
|
let codeBlock = false
|
||||||
|
for (const c of content) {
|
||||||
|
switch (c) {
|
||||||
|
case '`':
|
||||||
|
result += colorCode(codeBlock ? color : 'green')
|
||||||
|
codeBlock = !codeBlock
|
||||||
|
break
|
||||||
|
|
||||||
|
default:
|
||||||
|
result += c
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result + colorCode('default') + RETURN_SYMBOL
|
||||||
|
}
|
||||||
|
|
||||||
function printLine(content, delay, color) {
|
function printLine(content, delay, color) {
|
||||||
const printContent = `${colorize(content, color)}${RETURN_SYMBOL}`
|
const printContent = formatLine(content, color)
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
let i = 0
|
let i = 0
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
import conditionSchema from './condition.js'
|
||||||
|
import responseSchema from './response.js'
|
||||||
|
import hintSchema from './hint.js'
|
||||||
|
|
||||||
|
const commandResponderSchema = new Schema({
|
||||||
|
condition: {
|
||||||
|
type: conditionSchema,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
responses: {
|
||||||
|
type: [responseSchema],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
commandResponderSchema.add(hintSchema)
|
||||||
|
|
||||||
|
export default commandResponderSchema
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
const fileConditionSchema = new Schema({
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
type: String,
|
||||||
|
exists: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const conditionSchema = new Schema({
|
||||||
|
command: [String],
|
||||||
|
output: [String],
|
||||||
|
error: [String],
|
||||||
|
pwd: [String],
|
||||||
|
files: [fileConditionSchema],
|
||||||
|
})
|
||||||
|
|
||||||
|
conditionSchema.add({
|
||||||
|
or: [conditionSchema],
|
||||||
|
not: conditionSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default conditionSchema
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
import commandResponderSchema from './commandResponder.js'
|
||||||
|
|
||||||
|
const exceptionSchema = new Schema({})
|
||||||
|
|
||||||
|
exceptionSchema.add(commandResponderSchema)
|
||||||
|
|
||||||
|
export default exceptionSchema
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
import exceptionSchema from './exception.js'
|
||||||
|
import requirementSchema from './requirement.js'
|
||||||
|
|
||||||
|
const globalExceptionSchema = new Schema({})
|
||||||
|
|
||||||
|
globalExceptionSchema.add(exceptionSchema)
|
||||||
|
globalExceptionSchema.add(requirementSchema)
|
||||||
|
|
||||||
|
export default globalExceptionSchema
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
const hintSchema = new Schema({
|
||||||
|
hints: {
|
||||||
|
type: [String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
export default hintSchema
|
||||||
@@ -6,6 +6,6 @@
|
|||||||
"license": "GPL",
|
"license": "GPL",
|
||||||
"exports": "./index.js",
|
"exports": "./index.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mongoose": "^7.6.0"
|
"mongoose": "^8.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,98 +1,6 @@
|
|||||||
import { model, Schema } from 'mongoose'
|
import { model, Schema } from 'mongoose'
|
||||||
|
import stageSchema from './stage.js'
|
||||||
const responseSchema = new Schema({
|
import globalExceptionSchema from './globalException.js'
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
enum: ['narrative', 'dialogue'],
|
|
||||||
default: 'dialogue',
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
content: [String],
|
|
||||||
speaker: { type: String, default: 'Ada' },
|
|
||||||
color: String,
|
|
||||||
})
|
|
||||||
|
|
||||||
const fileConditionSchema = new Schema({
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
type: String,
|
|
||||||
exists: {
|
|
||||||
type: Boolean,
|
|
||||||
default: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const conditionSchema = new Schema({
|
|
||||||
command: [String],
|
|
||||||
output: [String],
|
|
||||||
error: [String],
|
|
||||||
pwd: [String],
|
|
||||||
files: [fileConditionSchema],
|
|
||||||
})
|
|
||||||
|
|
||||||
conditionSchema.add({
|
|
||||||
or: [conditionSchema],
|
|
||||||
not: conditionSchema,
|
|
||||||
})
|
|
||||||
|
|
||||||
const exceptionSchema = new Schema({
|
|
||||||
condition: {
|
|
||||||
type: conditionSchema,
|
|
||||||
required: false,
|
|
||||||
},
|
|
||||||
catchAll: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
responses: {
|
|
||||||
type: [responseSchema],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
hints: {
|
|
||||||
type: [String],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const stageSchema = new Schema({
|
|
||||||
id: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
task: {
|
|
||||||
type: String,
|
|
||||||
},
|
|
||||||
requirements: [
|
|
||||||
{
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
repeatable: {
|
|
||||||
type: Boolean,
|
|
||||||
required: true,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
condition: {
|
|
||||||
type: conditionSchema,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
responses: {
|
|
||||||
type: [responseSchema],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
hints: {
|
|
||||||
type: [String],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
exceptions: {
|
|
||||||
type: [exceptionSchema],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const Quest = model(
|
const Quest = model(
|
||||||
'Quest',
|
'Quest',
|
||||||
@@ -126,6 +34,10 @@ const Quest = model(
|
|||||||
type: [stageSchema],
|
type: [stageSchema],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
exceptions: {
|
||||||
|
type: [globalExceptionSchema],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
const requirementSchema = new Schema({
|
||||||
|
requirements: [
|
||||||
|
{
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default requirementSchema
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
|
||||||
|
const responseSchema = new Schema({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: ['narrative', 'dialogue'],
|
||||||
|
default: 'dialogue',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
type: [String],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
speaker: { type: String, default: 'Ada' },
|
||||||
|
color: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
export default responseSchema
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
import commandResponderSchema from './commandResponder.js'
|
||||||
|
import stageExceptionSchema from './stageException.js'
|
||||||
|
import requirementSchema from './requirement.js'
|
||||||
|
|
||||||
|
const stageSchema = new Schema({
|
||||||
|
id: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
task: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
exceptions: {
|
||||||
|
type: [stageExceptionSchema],
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
stageSchema.add(commandResponderSchema)
|
||||||
|
stageSchema.add(requirementSchema)
|
||||||
|
|
||||||
|
export default stageSchema
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Schema } from 'mongoose'
|
||||||
|
import exceptionSchema from './exception.js'
|
||||||
|
|
||||||
|
const stageExceptionSchema = new Schema({
|
||||||
|
catchAll: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
stageExceptionSchema.add(exceptionSchema)
|
||||||
|
|
||||||
|
export default stageExceptionSchema
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
echo start
|
||||||
|
echo example
|
||||||
|
echo How are you
|
||||||
@@ -9,14 +9,6 @@ requirements:
|
|||||||
- get-started
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -73,3 +65,11 @@ stages:
|
|||||||
- 你成功的在終端機上顯示了 `How are you?`。
|
- 你成功的在終端機上顯示了 `How are you?`。
|
||||||
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
- 相信你已經了解如何使用 `echo` 指令來顯示文字了。
|
||||||
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
- 之後如果要在終端機顯示文字,記得可以使用 `echo` 指令。
|
||||||
|
|
||||||
|
exceptions:
|
||||||
|
- repeatable: true
|
||||||
|
condition:
|
||||||
|
command:
|
||||||
|
- ' '
|
||||||
|
hints:
|
||||||
|
- '!! 請勿使用全形空格 !!'
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
echo start
|
||||||
|
cat hello.txt
|
||||||
|
cat world.txt
|
||||||
|
echo continue
|
||||||
|
cp hello.txt hello2.txt
|
||||||
|
cat hello2.txt
|
||||||
|
cp world.txt world2.txt
|
||||||
|
cat world2.txt
|
||||||
|
cp -r notes notes2
|
||||||
|
ls notes2
|
||||||
|
cp hello.txt world.txt
|
||||||
|
cat world.txt
|
||||||
@@ -9,14 +9,6 @@ requirements:
|
|||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
echo start
|
||||||
|
touch hello.txt
|
||||||
|
ls
|
||||||
|
cat hello.txt
|
||||||
|
touch world.txt
|
||||||
|
ls
|
||||||
|
mkdir notes
|
||||||
|
ls
|
||||||
|
cd notes
|
||||||
|
touch note.txt
|
||||||
|
ls
|
||||||
@@ -10,14 +10,6 @@ requirements:
|
|||||||
- read
|
- read
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
@@ -68,6 +60,7 @@ stages:
|
|||||||
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
- ➤ 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
||||||
|
|
||||||
- id: cat
|
- id: cat
|
||||||
|
task: 使用 `cat` 指令來查看 `hello.txt` 檔案的內容
|
||||||
condition:
|
condition:
|
||||||
command:
|
command:
|
||||||
- ^cat hello.txt$
|
- ^cat hello.txt$
|
||||||
@@ -159,7 +152,7 @@ stages:
|
|||||||
- ls-mkdir
|
- ls-mkdir
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- notes
|
- ^/home/commander/notes$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
exceptions:
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- '[\uff21-\uff3a\uff41-\uff5a]'
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! 請使用半形英文字母 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ' '
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! 請使用半形空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
or:
|
||||||
|
- command:
|
||||||
|
- '`'
|
||||||
|
- command:
|
||||||
|
- \'
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! 不需要輸入上下引號 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^echo\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `echo` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cd\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `cd` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^ls\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `ls` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cat\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `cat` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^touch\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `touch` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
not:
|
||||||
|
command:
|
||||||
|
- ^rmdir$
|
||||||
|
command:
|
||||||
|
- ^rm\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `rm` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^mv\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `mv` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^mkdir\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `mkdir` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^rmdir\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `rmdir` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^cp\S$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `cp` 後面要加空格 !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- ^[Ii1]s$
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '!! `ls` 的首字母是小寫的 `L` !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- 嗨
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '嗨~'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- folderl
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '`folder1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
|
||||||
|
|
||||||
|
- condition:
|
||||||
|
command:
|
||||||
|
- filel
|
||||||
|
responses:
|
||||||
|
- type: narrative
|
||||||
|
content:
|
||||||
|
- '`file1` 的 `1` 是數字 `1`,不是小寫的 `L` !!'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
echo start
|
||||||
|
ls
|
||||||
|
cd folder1
|
||||||
|
ls
|
||||||
|
cd ..
|
||||||
|
cd folder2
|
||||||
|
ls
|
||||||
|
cd ..
|
||||||
+10
-25
@@ -10,14 +10,6 @@ requirements:
|
|||||||
- get-started
|
- get-started
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -44,19 +36,12 @@ stages:
|
|||||||
- condition:
|
- condition:
|
||||||
not:
|
not:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
- 請先回到 `/home/commander/` 資料夾
|
- 請先回到 `/home/commander/` 資料夾
|
||||||
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
- 輸入 `cd ~` 來返回 `/home/commander/` 資料夾
|
||||||
- condition:
|
|
||||||
command:
|
|
||||||
- ^[Ii]s$
|
|
||||||
responses:
|
|
||||||
- type: narrative
|
|
||||||
content:
|
|
||||||
- '!! `ls` 的首字母是小寫的 `L`,不是 `I` !!'
|
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你應該能看到資料夾中的內容了。
|
- 你應該能看到資料夾中的內容了。
|
||||||
@@ -78,11 +63,11 @@ stages:
|
|||||||
- ls
|
- ls
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander/folder1
|
- ^/home/commander/folder1$
|
||||||
exceptions:
|
exceptions:
|
||||||
- condition:
|
- condition:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
@@ -90,7 +75,7 @@ stages:
|
|||||||
- condition:
|
- condition:
|
||||||
not:
|
not:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
@@ -113,9 +98,9 @@ stages:
|
|||||||
- cd-folder1
|
- cd-folder1
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander/folder1
|
- ^/home/commander/folder1$
|
||||||
command:
|
command:
|
||||||
- ls
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
- 你現在可以看到 `folder1` 資料夾中有兩的檔案,分別是 `file1.txt` 跟 `file2.txt`。
|
||||||
@@ -131,7 +116,7 @@ stages:
|
|||||||
- ls-folder1
|
- ls-folder1
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你成功的回到上一層資料夾了。
|
- 你成功的回到上一層資料夾了。
|
||||||
@@ -146,7 +131,7 @@ stages:
|
|||||||
- cd-..
|
- cd-..
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander/folder2
|
- ^/home/commander/folder2$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你成功的進入 `folder2` 資料夾了。
|
- 你成功的進入 `folder2` 資料夾了。
|
||||||
@@ -160,7 +145,7 @@ stages:
|
|||||||
- cd-folder2
|
- cd-folder2
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander/folder2
|
- ^/home/commander/folder2$
|
||||||
command:
|
command:
|
||||||
- ls
|
- ls
|
||||||
responses:
|
responses:
|
||||||
@@ -176,7 +161,7 @@ stages:
|
|||||||
- ls-folder2
|
- ls-folder2
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- /home/commander
|
- ^/home/commander$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你成功的回到原本的資料夾了。
|
- 你成功的回到原本的資料夾了。
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
echo start
|
||||||
|
echo hello
|
||||||
|
echo finish
|
||||||
@@ -7,16 +7,9 @@ instruction: |
|
|||||||
|
|
||||||
在畫面的中間可以看到終端機和提示兩個區塊。
|
在畫面的中間可以看到終端機和提示兩個區塊。
|
||||||
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
請你在終端機中的 `commander:~ $` 後面輸入 `echo start` 並按下 Enter 來開始教程。
|
||||||
|
!! 注意:不需要輸入上下引號 !!
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -73,6 +66,7 @@ stages:
|
|||||||
hints:
|
hints:
|
||||||
- ➤ 按下 `Ctrl + C` 能夠中斷指令
|
- ➤ 按下 `Ctrl + C` 能夠中斷指令
|
||||||
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
|
- ➤ 劇情進行時可以按下 `Enter` 來快速跳過
|
||||||
|
- ➤ 輸入 `echo finish` 來結束這一關
|
||||||
|
|
||||||
- id: END
|
- id: END
|
||||||
task: 結束關卡
|
task: 結束關卡
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
echo help{enter}
|
echo help
|
||||||
ls {enter}
|
ls
|
||||||
cat forgotten_scroll.txt{enter}
|
cat forgotten_scroll.txt
|
||||||
echo Hello World!{enter}
|
echo Hello World
|
||||||
@@ -12,9 +12,9 @@ stages:
|
|||||||
task: 輸入 `echo help` 來朗誦咒語
|
task: 輸入 `echo help` 來朗誦咒語
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- echo [Hh]elp
|
- ^echo [Hh]elp$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
@@ -39,9 +39,9 @@ stages:
|
|||||||
- help
|
- help
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ls
|
- ^ls$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
@@ -65,9 +65,9 @@ stages:
|
|||||||
- find-scroll
|
- find-scroll
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- cat forgotten_scroll.txt
|
- ^cat forgotten_scroll.txt$
|
||||||
responses:
|
responses:
|
||||||
- type: dialogue
|
- type: dialogue
|
||||||
speaker: Unix 精靈
|
speaker: Unix 精靈
|
||||||
@@ -83,9 +83,9 @@ stages:
|
|||||||
- read-forgotten-scroll
|
- read-forgotten-scroll
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- '^/home/commander$'
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- echo [Hh]ello\s*[Ww]orld
|
- ^echo [Hh]ello\s*[Ww]orld$
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
echo start
|
||||||
|
echo continue
|
||||||
|
mv hello.txt notes
|
||||||
|
ls
|
||||||
|
ls notes
|
||||||
|
echo continue
|
||||||
|
mv world.txt wow.txt
|
||||||
|
ls
|
||||||
|
mv notes docs
|
||||||
|
ls
|
||||||
|
echo hint
|
||||||
|
mv docs/hello.txt hi.txt
|
||||||
@@ -9,14 +9,6 @@ requirements:
|
|||||||
- create
|
- create
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
echo start
|
||||||
|
ls
|
||||||
|
cd folder1
|
||||||
|
cd ../folder2
|
||||||
|
ls ../folder1
|
||||||
|
pwd
|
||||||
|
ls ~
|
||||||
|
cd ~
|
||||||
+7
-20
@@ -11,14 +11,6 @@ requirements:
|
|||||||
- explore
|
- explore
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -64,23 +56,20 @@ stages:
|
|||||||
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
- ➤ 請輸入 `cd ../folder2` 來進入 `folder2` 資料夾。
|
||||||
|
|
||||||
- id: cd-folder2
|
- id: cd-folder2
|
||||||
task: 用指定方式進入 `folder2` 資料夾
|
task: 進入 `folder2` 資料夾
|
||||||
requirements:
|
requirements:
|
||||||
- cd-folder1
|
- cd-folder1
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander/folder2$
|
- ^/home/commander/folder2$
|
||||||
command:
|
|
||||||
- ^cd \.\./folder2$
|
|
||||||
exceptions:
|
exceptions:
|
||||||
- condition:
|
- condition:
|
||||||
not:
|
pwd:
|
||||||
pwd:
|
- ^/home/commander$
|
||||||
- ^/home/commander/folder1$
|
|
||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
- 請先回到 `folder1` 資料夾,並用指定的方式進入 `folder2` 資料夾。
|
- 輸入 `cd folder2` 來進入 `folder2` 資料夾
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你成功的用相對路徑進入了 `folder2` 資料夾。
|
- 你成功的用相對路徑進入了 `folder2` 資料夾。
|
||||||
@@ -95,10 +84,8 @@ stages:
|
|||||||
requirements:
|
requirements:
|
||||||
- cd-folder2
|
- cd-folder2
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
output:
|
||||||
- ^/home/commander/folder2$
|
- file1.txt
|
||||||
command:
|
|
||||||
- ^ls \.\.\/folder1$
|
|
||||||
exceptions:
|
exceptions:
|
||||||
- condition:
|
- condition:
|
||||||
not:
|
not:
|
||||||
@@ -107,7 +94,7 @@ stages:
|
|||||||
responses:
|
responses:
|
||||||
- type: narrative
|
- type: narrative
|
||||||
content:
|
content:
|
||||||
- 請先回到 `folder2` 資料夾,並用指定的方式查看 `folder1` 資料夾的內容。
|
- 進到 `folder1` 資料夾並用 `ls` 查看內容。
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 很好,你現在知道如何運用相對路徑了。
|
- 很好,你現在知道如何運用相對路徑了。
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
echo start
|
||||||
|
ls
|
||||||
|
cat file1.txt
|
||||||
|
ls folder
|
||||||
|
cat folder/file.txt
|
||||||
+6
-14
@@ -9,14 +9,6 @@ requirements:
|
|||||||
- path
|
- path
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始教程
|
task: 輸入 `echo start` 來開始教程
|
||||||
condition:
|
condition:
|
||||||
@@ -44,19 +36,19 @@ stages:
|
|||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 這個資料夾中有一個檔案和一個資料夾。
|
- 這個資料夾中有一個檔案和一個資料夾。
|
||||||
- 現在你可以使用 `cat` 指令來查看 `file.txt` 的內容。
|
- 現在你可以使用 `cat` 指令來查看 `file1.txt` 的內容。
|
||||||
hints:
|
hints:
|
||||||
- ➤ 請輸入 `cat file.txt` 來查看檔案的內容。
|
- ➤ 請輸入 `cat file1.txt` 來查看檔案的內容。
|
||||||
|
|
||||||
- id: cat-file
|
- id: cat-file1
|
||||||
task: 查看 `file.txt` 的內容
|
task: 查看 `file1.txt` 的內容
|
||||||
requirements:
|
requirements:
|
||||||
- ls-~
|
- ls-~
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
command:
|
command:
|
||||||
- ^cat file.txt$
|
- ^cat file1.txt$
|
||||||
responses:
|
responses:
|
||||||
- content:
|
- content:
|
||||||
- 你應該能看到檔案的內容了。
|
- 你應該能看到檔案的內容了。
|
||||||
@@ -68,7 +60,7 @@ stages:
|
|||||||
- id: ls-folder
|
- id: ls-folder
|
||||||
task: 查看 `folder` 資料夾
|
task: 查看 `folder` 資料夾
|
||||||
requirements:
|
requirements:
|
||||||
- cat-file
|
- cat-file1
|
||||||
condition:
|
condition:
|
||||||
pwd:
|
pwd:
|
||||||
- ^/home/commander$
|
- ^/home/commander$
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
echo start
|
||||||
|
rm hello.txt
|
||||||
|
ls
|
||||||
|
rm world.txt wow.txt
|
||||||
|
ls
|
||||||
|
rmdir empty
|
||||||
|
ls
|
||||||
|
rmdir notes
|
||||||
|
rm notes/note.txt
|
||||||
|
rmdir notes
|
||||||
|
ls trash
|
||||||
|
rm -r trash
|
||||||
|
ls
|
||||||
@@ -13,14 +13,6 @@ requirements:
|
|||||||
- move
|
- move
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
- id: warning-01
|
|
||||||
repeatable: true
|
|
||||||
condition:
|
|
||||||
command:
|
|
||||||
- ' '
|
|
||||||
hints:
|
|
||||||
- '!! 請勿使用全形空格 !!'
|
|
||||||
|
|
||||||
- id: start
|
- id: start
|
||||||
task: 輸入 `echo start` 來開始
|
task: 輸入 `echo start` 來開始
|
||||||
condition:
|
condition:
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@
|
|||||||
"express-validator": "^7.0.1",
|
"express-validator": "^7.0.1",
|
||||||
"jsonwebtoken": "^9.0.1",
|
"jsonwebtoken": "^9.0.1",
|
||||||
"minimist": "^1.2.8",
|
"minimist": "^1.2.8",
|
||||||
"mongoose": "^7.4.0",
|
"mongoose": "^8.0.3",
|
||||||
"node-schedule": "^2.1.1",
|
"node-schedule": "^2.1.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-github2": "^0.1.12",
|
"passport-github2": "^0.1.12",
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ export function createContainer(name, questId) {
|
|||||||
Image: getQuestImage(questId),
|
Image: getQuestImage(questId),
|
||||||
}
|
}
|
||||||
const { hostPwd, mountQuest } = config.docker
|
const { hostPwd, mountQuest } = config.docker
|
||||||
if (!config.isProduction && hostPwd && mountQuest) {
|
if (!config.isProduction && hostPwd && mountQuest === questId) {
|
||||||
logger.debug('Mounting quest folder', mountQuest)
|
logger.info('Mounting quest folder', mountQuest)
|
||||||
option.HostConfig.Binds = [
|
option.HostConfig.Binds = [
|
||||||
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
||||||
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
||||||
|
|||||||
@@ -50,9 +50,18 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async discoverHandler() {
|
async discoverHandler() {
|
||||||
const graph = new FileGraph(this.session.graph)
|
try {
|
||||||
graph.discover(this.params.discover)
|
const graph = new FileGraph(this.session.graph)
|
||||||
this.session.graph = graph
|
graph.discover(this.params.discover)
|
||||||
|
this.session.graph = graph
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Update file graph failed', {
|
||||||
|
error: e.message,
|
||||||
|
session: this.session.id,
|
||||||
|
graph: this.session.graph,
|
||||||
|
discover: this.params.discover,
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async isMatch(condition) {
|
async isMatch(condition) {
|
||||||
@@ -111,22 +120,33 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async run() {
|
async run() {
|
||||||
this.quest = await Quest.findById(this.session.quest)
|
try {
|
||||||
const stages = this.getStages()
|
this.quest = await Quest.findById(this.session.quest)
|
||||||
if (stages.length === 0) {
|
const stages = this.getStages()
|
||||||
logger.warn('stage not found', this.session.tasks)
|
if (stages.length === 0) {
|
||||||
return {}
|
logger.warn('stage not found', { session: this.session.id })
|
||||||
}
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
this.handleEvent()
|
this.handleEvent()
|
||||||
this.handleCommand()
|
this.handleCommand()
|
||||||
|
|
||||||
const matches = await Promise.all(
|
const matches = await Promise.all(
|
||||||
stages.map((stage) => this.isMatch(stage.condition))
|
stages.map((stage) => this.isMatch(stage.condition))
|
||||||
)
|
)
|
||||||
|
|
||||||
const stage = stages.find((_, i) => matches[i])
|
const stage = stages.find((_, i) => matches[i])
|
||||||
if (!stage) {
|
if (stage) {
|
||||||
|
const response = this.execute(stage)
|
||||||
|
|
||||||
|
return {
|
||||||
|
stage: stage.id,
|
||||||
|
end: this.session.status === 'finished',
|
||||||
|
...response,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// stage exception
|
||||||
for (const s of stages) {
|
for (const s of stages) {
|
||||||
// eslint-disable-next-line no-await-in-loop
|
// eslint-disable-next-line no-await-in-loop
|
||||||
const exception = await this.checkException(s)
|
const exception = await this.checkException(s)
|
||||||
@@ -134,14 +154,29 @@ export default class CommandHandler extends SessionHandler {
|
|||||||
return this.executeException(exception)
|
return this.executeException(exception)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {}
|
|
||||||
}
|
|
||||||
const response = this.execute(stage)
|
|
||||||
|
|
||||||
return {
|
// global exception
|
||||||
stage: stage.id,
|
if (this.quest.exceptions.length > 0) {
|
||||||
end: this.session.status === 'finished',
|
const globalExceptions = this.getUnlockedComponents(
|
||||||
...response,
|
this.quest.exceptions
|
||||||
|
)
|
||||||
|
for (const exception of globalExceptions) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
if (await this.isMatch(exception.condition)) {
|
||||||
|
return this.executeException(exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('CommandHandler error', {
|
||||||
|
error: err.message,
|
||||||
|
session: this.session._id,
|
||||||
|
quest: this.quest._id,
|
||||||
|
command: this.commandInput,
|
||||||
|
})
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -19,14 +19,17 @@ export default class SessionHandler {
|
|||||||
return task ? task.completed : false
|
return task ? task.completed : false
|
||||||
}
|
}
|
||||||
|
|
||||||
getStages() {
|
getUnlockedComponents(components) {
|
||||||
return this.quest.stages.filter(
|
return components.filter((component) =>
|
||||||
(stage) =>
|
component.requirements.every((r) => this.isCompleted({ id: r }))
|
||||||
stage.requirements.every((r) => this.isCompleted({ id: r })) &&
|
|
||||||
(stage.repeatable || !this.isCompleted(stage))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getStages() {
|
||||||
|
const unlockedStages = this.getUnlockedComponents(this.quest.stages)
|
||||||
|
return unlockedStages.filter((s) => !this.isCompleted(s))
|
||||||
|
}
|
||||||
|
|
||||||
getNewTasks() {
|
getNewTasks() {
|
||||||
const stages = this.getStages()
|
const stages = this.getStages()
|
||||||
return stages
|
return stages
|
||||||
|
|||||||
+6
-1
@@ -38,7 +38,12 @@ async function main() {
|
|||||||
await setupTest()
|
await setupTest()
|
||||||
}
|
}
|
||||||
|
|
||||||
await loadAndUpdateQuests()
|
try {
|
||||||
|
await loadAndUpdateQuests()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Failed to load quests', err)
|
||||||
|
return
|
||||||
|
}
|
||||||
expiryRemovalScheduler()
|
expiryRemovalScheduler()
|
||||||
|
|
||||||
const file = await fs.readFile('./swagger.yaml', 'utf8')
|
const file = await fs.readFile('./swagger.yaml', 'utf8')
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import winston from 'winston'
|
import winston, { format } from 'winston'
|
||||||
import config from '../config.js'
|
import config from '../config.js'
|
||||||
|
|
||||||
const logger = winston.createLogger({
|
const logger = winston.createLogger({
|
||||||
level: 'info',
|
level: 'info',
|
||||||
format: winston.format.json(),
|
format: format.combine(
|
||||||
|
format.errors({ stack: true }),
|
||||||
|
format.timestamp(),
|
||||||
|
format.json()
|
||||||
|
),
|
||||||
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
|
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
|
||||||
transports: [
|
transports: [
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -7,6 +7,15 @@ import logger from './logger.js'
|
|||||||
|
|
||||||
const questDirectory = path.join(process.cwd(), '..', 'quests')
|
const questDirectory = path.join(process.cwd(), '..', 'quests')
|
||||||
|
|
||||||
|
class QuestValidationError extends Error {
|
||||||
|
constructor(message, questId, error) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'QuestValidationError'
|
||||||
|
this.questId = questId
|
||||||
|
this.error = error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function mapQuests(callback) {
|
async function mapQuests(callback) {
|
||||||
const questNames = await fs.readdir(questDirectory, {
|
const questNames = await fs.readdir(questDirectory, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
@@ -23,12 +32,20 @@ async function mapQuests(callback) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadGlobalExceptions() {
|
||||||
|
const exceptionsPath = path.join(questDirectory, 'exceptions.yml')
|
||||||
|
const body = await fs.readFile(exceptionsPath, 'utf-8')
|
||||||
|
return yaml.parse(body)
|
||||||
|
}
|
||||||
|
|
||||||
export async function loadAndUpdateQuests() {
|
export async function loadAndUpdateQuests() {
|
||||||
await Quest.deleteMany({})
|
await Quest.deleteMany({})
|
||||||
|
const { exceptions } = await loadGlobalExceptions()
|
||||||
|
console.log(exceptions)
|
||||||
return mapQuests(async (id, questPath) => {
|
return mapQuests(async (id, questPath) => {
|
||||||
const files = await fs.readdir(questPath)
|
const files = await fs.readdir(questPath)
|
||||||
if (!files.includes('game.yml')) {
|
if (!files.includes('game.yml')) {
|
||||||
throw new Error(`Quest ${id} is missing game.yml`)
|
throw new QuestValidationError(`Missing game.yml`, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -44,10 +61,12 @@ export async function loadAndUpdateQuests() {
|
|||||||
image,
|
image,
|
||||||
...questData,
|
...questData,
|
||||||
})
|
})
|
||||||
|
quest.exceptions.push(...exceptions)
|
||||||
await quest.save()
|
await quest.save()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Error parsing quest ${id}:`, error)
|
console.error(error)
|
||||||
throw error
|
logger.error(`Error parsing quest`, { id, error })
|
||||||
|
throw new QuestValidationError(`Error parsing quest`, id, error)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ module.exports = defineConfig({
|
|||||||
viewportWidth: 1920,
|
viewportWidth: 1920,
|
||||||
viewportHeight: 1080,
|
viewportHeight: 1080,
|
||||||
pageLoadTimeout: 120000,
|
pageLoadTimeout: 120000,
|
||||||
|
redirectionLimit: 20,
|
||||||
|
defaultCommandTimeout: 10000,
|
||||||
|
retries: { runMode: 5, openMode: 5 },
|
||||||
},
|
},
|
||||||
env: {
|
env: {
|
||||||
defaultAccount: getOrFail('TESTING_USERNAME'),
|
defaultAccount: getOrFail('TESTING_USERNAME'),
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
/// <reference types="cypress" />
|
/// <reference types="cypress" />
|
||||||
export default function checkLoginUI() {
|
export default function checkLoginUI() {
|
||||||
cy.get('h1.text-text-primary')
|
cy.get('#title').should('contain', 'Linux Odyssey').and('be.visible')
|
||||||
.should('contain', 'Linux Odyssey')
|
|
||||||
.and('be.visible')
|
|
||||||
cy.CheckPlaceholder('#password', '密碼', 'Password')
|
cy.CheckPlaceholder('#password', '密碼', 'Password')
|
||||||
}
|
}
|
||||||
describe('example helloworld app', () => {
|
describe('example helloworld app', () => {
|
||||||
@@ -13,7 +11,7 @@ describe('example helloworld app', () => {
|
|||||||
it('Check Login Page Elements-Register', () => {
|
it('Check Login Page Elements-Register', () => {
|
||||||
checkLoginUI()
|
checkLoginUI()
|
||||||
cy.CheckPlaceholder('#username', '帳號名稱', 'Username')
|
cy.CheckPlaceholder('#username', '帳號名稱', 'Username')
|
||||||
cy.get('label.text-text-secondary').should('be.visible') // check the rules is displayed
|
cy.get('#NameRules').should('be.visible') // check the rules is displayed
|
||||||
cy.CheckPlaceholder('#email', '電子郵件', 'Email')
|
cy.CheckPlaceholder('#email', '電子郵件', 'Email')
|
||||||
cy.CheckTextElement(
|
cy.CheckTextElement(
|
||||||
'#CheckRegistered',
|
'#CheckRegistered',
|
||||||
@@ -79,15 +77,23 @@ describe('example helloworld app', () => {
|
|||||||
cy.PrepareForGame()
|
cy.PrepareForGame()
|
||||||
})
|
})
|
||||||
it('Check Header', () => {
|
it('Check Header', () => {
|
||||||
cy.get('#HeaderText').should('contain', 'Linux Odyssey').and('be.visible')
|
cy.get('#GameTitle').should('contain', 'Linux Odyssey').and('be.visible')
|
||||||
|
cy.get('#QuestTitle').should('contain', title).and('be.visible')
|
||||||
|
cy.get('#UsernameText')
|
||||||
|
.should('contain', Cypress.env('defaultAccount'))
|
||||||
|
.and('be.visible')
|
||||||
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
|
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
|
||||||
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
||||||
|
cy.findByRole('button', { name: 'Map' }).should('be.visible')
|
||||||
|
cy.findByRole('button', { name: 'LeaderBoard' }).should('be.visible')
|
||||||
|
cy.findByRole('link', { name: 'Survey' }).should('be.visible')
|
||||||
})
|
})
|
||||||
it('Check QuestInfo', () => {
|
it('Check QuestInfo', () => {
|
||||||
cy.get('#topic').should('contain', title)
|
cy.get('#topic').should('contain', title)
|
||||||
cy.get('#quest').find('p.text-text').should('have.length', 4)
|
cy.get('#topic').should('be.visible')
|
||||||
|
cy.get('#tasks').should('be.visible')
|
||||||
|
cy.get('#quest').find('p.text-text').should('have.length', 2)
|
||||||
cy.CheckTextElement('#tasks', '任務', 'Tasks:')
|
cy.CheckTextElement('#tasks', '任務', 'Tasks:')
|
||||||
cy.getQuestInfo('輸入 `echo help` 來朗誦咒語').should('be.visible')
|
|
||||||
})
|
})
|
||||||
it('Check Terminal', () => {
|
it('Check Terminal', () => {
|
||||||
cy.get('#terminal', { timeout: 20000 }).within(($terminal) => {
|
cy.get('#terminal', { timeout: 20000 }).within(($terminal) => {
|
||||||
@@ -114,6 +120,7 @@ describe('example helloworld app', () => {
|
|||||||
cy.CheckTextElement('#solution', '解答', 'Solution')
|
cy.CheckTextElement('#solution', '解答', 'Solution')
|
||||||
cy.CheckTextElement('#reset', '重來', 'Reset')
|
cy.CheckTextElement('#reset', '重來', 'Reset')
|
||||||
cy.CheckTextElement('#continue', '繼續', 'Continue')
|
cy.CheckTextElement('#continue', '繼續', 'Continue')
|
||||||
|
cy.CheckTextElement('#survey', '問卷', 'Survey')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
describe('Game Play', () => {
|
describe('Game Play', () => {
|
||||||
@@ -126,42 +133,31 @@ describe('example helloworld app', () => {
|
|||||||
.should('contain', '12345')
|
.should('contain', '12345')
|
||||||
.and('contain', 'zsh: command not found: 12345', { timeout: 50000 })
|
.and('contain', 'zsh: command not found: 12345', { timeout: 50000 })
|
||||||
})
|
})
|
||||||
it('Complete the Game(relating UI)', () => {
|
it('Complete the First Quest(relating UI)', () => {
|
||||||
// Stage1
|
cy.checkTaskInit()
|
||||||
cy.log('Stage1')
|
cy.InitTerminal()
|
||||||
cy.typeInCommand('echo start{enter}')
|
cy.typeInCommand('echo start{enter}')
|
||||||
cy.checkPending()
|
cy.checkPending()
|
||||||
cy.waitUntilActive()
|
cy.waitUntilActive()
|
||||||
cy.get('#Lbutton').should('be.visible').and('be.disabled')
|
cy.get('#Lbutton').should('be.visible').and('be.disabled')
|
||||||
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
||||||
cy.checkHint(1, 1)
|
cy.checkHint(1, 1)
|
||||||
cy.getQuestInfo('✓ 輸入 `echo start` 來開始教程').should('be.visible')
|
cy.getQuestInfo('✓ 輸入 echo start 來開始教程').should('be.visible')
|
||||||
cy.getQuestInfo('➤ 在終端機輸入 `echo hello`').should('be.visible')
|
cy.getQuestInfo('➤ 在終端機輸入 echo hello').should('be.visible')
|
||||||
// Stage2
|
|
||||||
cy.log('Stage2')
|
|
||||||
cy.typeInCommand('echo hello{enter}')
|
cy.typeInCommand('echo hello{enter}')
|
||||||
cy.checkPending()
|
cy.checkPending()
|
||||||
cy.waitUntilActive()
|
cy.waitUntilActive()
|
||||||
// cy.get('#tree')
|
|
||||||
// .get('a')
|
|
||||||
// .should('contain', 'forgotten_scroll.txt')
|
|
||||||
// .and('be.visible')
|
|
||||||
cy.get('#Lbutton').should('be.visible').and('be.enabled')
|
cy.get('#Lbutton').should('be.visible').and('be.enabled')
|
||||||
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
cy.get('#Rbutton').should('be.visible').and('be.disabled')
|
||||||
cy.checkHint(2, 2)
|
cy.checkHint(2, 2)
|
||||||
cy.get('#Lbutton').click()
|
cy.get('#Lbutton').click()
|
||||||
cy.get('#Rbutton').should('be.enabled')
|
cy.get('#Rbutton').should('be.enabled')
|
||||||
cy.getQuestInfo('✓ 在終端機輸入 `echo hello`').should('be.visible')
|
cy.getQuestInfo('✓ 在終端機輸入 echo hello').should('be.visible')
|
||||||
cy.getQuestInfo('➤ 結束關卡').should('be.visible')
|
cy.getQuestInfo('➤ 結束關卡').should('be.visible')
|
||||||
// Stage3
|
|
||||||
cy.log('Stage3')
|
|
||||||
cy.typeInCommand('echo finish{enter}')
|
cy.typeInCommand('echo finish{enter}')
|
||||||
cy.checkPending()
|
|
||||||
cy.waitUntilActive()
|
|
||||||
cy.getQuestInfo('✓ 結束關卡').should('be.visible')
|
|
||||||
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
|
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
|
||||||
cy.get('div[class="modal"]').find('p').should('be.visible')
|
cy.get('div[class="modal"]').find('p').should('be.visible')
|
||||||
cy.findByRole('link', { name: '填寫問卷' }).should('be.visible')
|
cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/// <reference types="cypress" />
|
||||||
|
describe('Complete quests with only commands', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.PrepareForGame()
|
||||||
|
})
|
||||||
|
it('Complete quest : get-started', () => {
|
||||||
|
cy.CompleteStageWithCommands('get-started')
|
||||||
|
})
|
||||||
|
it('Complete quest : cast', () => {
|
||||||
|
cy.CompleteStageWithCommands('cast')
|
||||||
|
})
|
||||||
|
it('Complete quest : explore', () => {
|
||||||
|
cy.CompleteStageWithCommands('explore')
|
||||||
|
})
|
||||||
|
it('Complete quest : path', () => {
|
||||||
|
cy.CompleteStageWithCommands('path')
|
||||||
|
})
|
||||||
|
it('Complete quest : read', () => {
|
||||||
|
cy.CompleteStageWithCommands('read')
|
||||||
|
})
|
||||||
|
it('Complete quest : create', () => {
|
||||||
|
cy.CompleteStageWithCommands('create')
|
||||||
|
})
|
||||||
|
it('Complete quest : copy', () => {
|
||||||
|
cy.CompleteStageWithCommands('copy')
|
||||||
|
})
|
||||||
|
it('Complete quest : move', () => {
|
||||||
|
cy.CompleteStageWithCommands('move')
|
||||||
|
})
|
||||||
|
it('Complete quest : remove', () => {
|
||||||
|
cy.CompleteStageWithCommands('remove')
|
||||||
|
})
|
||||||
|
it('Complete quest : helloworld', () => {
|
||||||
|
cy.CompleteStageWithCommands('helloworld')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -22,25 +22,31 @@ Cypress.Commands.add('PrepareForGame', () => {
|
|||||||
cy.url().should('include', '/map')
|
cy.url().should('include', '/map')
|
||||||
cy.visit('/game/get-started')
|
cy.visit('/game/get-started')
|
||||||
cy.url().should('include', '/game/get-started')
|
cy.url().should('include', '/game/get-started')
|
||||||
|
// check task first so the terminal has better possibility loaded
|
||||||
|
cy.checkTaskInit()
|
||||||
cy.get('.xterm-screen', { timeout: 10000 })
|
cy.get('.xterm-screen', { timeout: 10000 })
|
||||||
.as('Terminaltextbox')
|
.as('Terminaltextbox')
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
cy.CheckTextElement('#reset', '重來', 'Reset').click()
|
cy.CheckTextElement('#reset', '重來', 'Reset').click()
|
||||||
cy.typeInCommand('clear{enter}')
|
cy.checkTaskInit()
|
||||||
cy.get('@Terminaltextbox', { timeout: 150000 }).should(
|
cy.InitTerminal()
|
||||||
|
})
|
||||||
|
Cypress.Commands.add('InitTerminal', () => {
|
||||||
|
cy.get('@Terminaltextbox', { timeout: 15000 }).should(
|
||||||
'contain',
|
'contain',
|
||||||
'commander:~ $'
|
'commander:~ $'
|
||||||
)
|
)
|
||||||
|
cy.log('Check terminal init done')
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('typeInCommand', (command) => {
|
Cypress.Commands.add('typeInCommand', (command) => {
|
||||||
cy.get('.xterm-screen', { timeout: 150000 }).type(command)
|
cy.get('.xterm-screen', { timeout: 15000 }).type(command)
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('getQuestInfo', (id) => {
|
Cypress.Commands.add('getQuestInfo', (id) => {
|
||||||
return cy
|
return cy
|
||||||
.get('#quest')
|
.get('#quest')
|
||||||
.find('li')
|
.find('li')
|
||||||
.find('p')
|
.find('p')
|
||||||
.contains(`${id}`, { timeout: 100000 })
|
.contains(`${id}`, { timeout: 10000 })
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('checkHint', (index, total) => {
|
Cypress.Commands.add('checkHint', (index, total) => {
|
||||||
cy.get('#hint')
|
cy.get('#hint')
|
||||||
@@ -48,29 +54,62 @@ Cypress.Commands.add('checkHint', (index, total) => {
|
|||||||
.contains(`${index}/${total}`)
|
.contains(`${index}/${total}`)
|
||||||
.should('be.visible')
|
.should('be.visible')
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('waitUntilActive', () => {
|
Cypress.Commands.add('checkTaskInit', () => {
|
||||||
cy.get('input[id="currentStatus"]', { timeout: 1000000 })
|
cy.get('#tasks').next().children().should('be.visible')
|
||||||
|
})
|
||||||
|
Cypress.Commands.add('waitUntilActive', (last = false) => {
|
||||||
|
cy.get('input[id="currentStatus"]', { timeout: 10000 })
|
||||||
.invoke('val')
|
.invoke('val')
|
||||||
.then((value) => {
|
.then((value) => {
|
||||||
if (value === 'active' || value === 'finished') {
|
if (value === 'active') {
|
||||||
cy.log('story ends')
|
cy.log('story ends')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cy.typeInCommand('{enter}')
|
if (value === 'finished' && last) {
|
||||||
cy.waitUntilActive()
|
cy.log('Quest Finished')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (value === 'pending' && !last) {
|
||||||
|
cy.log(last)
|
||||||
|
cy.typeInCommand('{enter}')
|
||||||
|
cy.waitUntilActive()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('checkPending', () => {
|
Cypress.Commands.add('checkPending', () => {
|
||||||
cy.get('input[id="currentStatus"]', { timeout: 1000000 }).should(
|
cy.get('input[id="currentStatus"]', { timeout: 10000 }).should(
|
||||||
'have.value',
|
'have.value',
|
||||||
'pending'
|
'pending'
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('Complete the Stage (only command)', () => {
|
Cypress.Commands.add('CompleteStageWithCommands', (stagename) => {
|
||||||
cy.log('')
|
cy.visit(`/game/${stagename}`)
|
||||||
|
cy.url().should('include', `/game/${stagename}`, { timeout: 50000 })
|
||||||
|
cy.log(`Completing stage :${stagename}`)
|
||||||
|
cy.checkTaskInit()
|
||||||
|
cy.CheckTextElement('#reset', '重來', 'Reset').click()
|
||||||
|
cy.checkTaskInit()
|
||||||
|
cy.InitTerminal()
|
||||||
|
cy.readFile(`../quests/${stagename}/answer.sh`, 'utf-8').as('answers')
|
||||||
|
cy.get('@answers').then((answers) => {
|
||||||
|
const answerarr = answers.split('\n')
|
||||||
|
for (const element of answerarr) {
|
||||||
|
cy.typeInCommand(`${element}{enter}`)
|
||||||
|
if (answerarr.indexOf(element) + 1 !== answerarr.length) {
|
||||||
|
cy.checkPending()
|
||||||
|
cy.waitUntilActive()
|
||||||
|
}
|
||||||
|
if (answerarr.indexOf(element) + 1 === answerarr.length) {
|
||||||
|
cy.waitUntilActive(true)
|
||||||
|
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
|
||||||
|
cy.get('div[class="modal"]').find('p').should('be.visible')
|
||||||
|
cy.get('#BacktoMap').should('be.visible').and('contain', '回到地圖')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {
|
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {
|
||||||
cy.get(id).within(($element) => {
|
cy.get(id, { timeout: 50000 }).within(($element) => {
|
||||||
if (Cypress.env('isCHVersion')) {
|
if (Cypress.env('isCHVersion')) {
|
||||||
cy.get($element).should('contain', chText).and('be.visible')
|
cy.get($element).should('contain', chText).and('be.visible')
|
||||||
} else {
|
} else {
|
||||||
@@ -89,3 +128,6 @@ Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
Cypress.Commands.add('CheckTreeElement', (element) => {
|
||||||
|
cy.get('#tree').get('a').should('contain', `${element}`).and('be.visible')
|
||||||
|
})
|
||||||
@@ -1756,15 +1756,10 @@ bser@2.1.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
node-int64 "^0.4.0"
|
node-int64 "^0.4.0"
|
||||||
|
|
||||||
bson@^5.4.0:
|
bson@^6.2.0:
|
||||||
version "5.4.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.4.0.tgz#0eea77276d490953ad8616b483298dbff07384c6"
|
resolved "https://registry.yarnpkg.com/bson/-/bson-6.2.0.tgz#4b6acafc266ba18eeee111373c2699304a9ba0a3"
|
||||||
integrity sha512-WRZ5SQI5GfUuKnPTNmAYPiKIof3ORXAF4IRU5UcgmivNIon01rWQlw5RUH954dpu8yGL8T59YShVddIPaU/gFA==
|
integrity sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==
|
||||||
|
|
||||||
bson@^5.5.0:
|
|
||||||
version "5.5.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/bson/-/bson-5.5.0.tgz#a419cc69f368d2def3b8b22ea03ed1c9be40e53f"
|
|
||||||
integrity sha512-B+QB4YmDx9RStKv8LLSl/aVIEV3nYJc3cJNNTK2Cd1TL+7P+cNpw9mAPeCgc5K+j01Dv6sxUzcITXDx7ZU3F0w==
|
|
||||||
|
|
||||||
buffer-crc32@~0.2.3:
|
buffer-crc32@~0.2.3:
|
||||||
version "0.2.13"
|
version "0.2.13"
|
||||||
@@ -2404,6 +2399,11 @@ dom-accessibility-api@^0.5.9:
|
|||||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz#5a7429e6066eb3664d911e33fb0e45de8eb08453"
|
||||||
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
integrity sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==
|
||||||
|
|
||||||
|
dompurify@^3.0.6:
|
||||||
|
version "3.0.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-3.0.6.tgz#925ebd576d54a9531b5d76f0a5bef32548351dae"
|
||||||
|
integrity sha512-ilkD8YEnnGh1zJ240uJsW7AzE+2qpbOUYjacomn3AvJ6J4JhKGSZ2nh4wUIXPZrEPppaCLx5jFe8T89Rk8tQ7w==
|
||||||
|
|
||||||
dotenv@^16.3.1:
|
dotenv@^16.3.1:
|
||||||
version "16.3.1"
|
version "16.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.3.1.tgz#369034de7d7e5b120972693352a3bf112172cc3e"
|
||||||
@@ -3663,11 +3663,6 @@ internal-slot@^1.0.5:
|
|||||||
has "^1.0.3"
|
has "^1.0.3"
|
||||||
side-channel "^1.0.4"
|
side-channel "^1.0.4"
|
||||||
|
|
||||||
ip@^2.0.0:
|
|
||||||
version "2.0.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da"
|
|
||||||
integrity sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==
|
|
||||||
|
|
||||||
ipaddr.js@1.9.1:
|
ipaddr.js@1.9.1:
|
||||||
version "1.9.1"
|
version "1.9.1"
|
||||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||||
@@ -4691,6 +4686,11 @@ makeerror@1.0.12:
|
|||||||
dependencies:
|
dependencies:
|
||||||
tmpl "1.0.5"
|
tmpl "1.0.5"
|
||||||
|
|
||||||
|
marked@^10.0.0:
|
||||||
|
version "10.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/marked/-/marked-10.0.0.tgz#7fe1805bb908433d760e2de0fcc8841a2b2d745c"
|
||||||
|
integrity sha512-YiGcYcWj50YrwBgNzFoYhQ1hT6GmQbFG8SksnYJX1z4BXTHSOrz1GB5/Jm2yQvMg4nN1FHP4M6r03R10KrVUiA==
|
||||||
|
|
||||||
media-typer@0.3.0:
|
media-typer@0.3.0:
|
||||||
version "0.3.0"
|
version "0.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||||
@@ -4823,49 +4823,23 @@ mongodb-connection-string-url@^2.6.0:
|
|||||||
"@types/whatwg-url" "^8.2.1"
|
"@types/whatwg-url" "^8.2.1"
|
||||||
whatwg-url "^11.0.0"
|
whatwg-url "^11.0.0"
|
||||||
|
|
||||||
mongodb@5.7.0:
|
mongodb@6.2.0:
|
||||||
version "5.7.0"
|
version "6.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.7.0.tgz#e16d2fcdfd9f8503ec2d88288392dc3235bb3ecc"
|
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-6.2.0.tgz#2c9dcb3eeaf528ed850e94b3df392de6c6b0d7ab"
|
||||||
integrity sha512-zm82Bq33QbqtxDf58fLWBwTjARK3NSvKYjyz997KSy6hpat0prjeX/kxjbPVyZY60XYPDNETaHkHJI2UCzSLuw==
|
integrity sha512-d7OSuGjGWDZ5usZPqfvb36laQ9CPhnWkAGHT61x5P95p/8nMVeH8asloMwW6GcYFeB0Vj4CB/1wOTDG2RA9BFA==
|
||||||
dependencies:
|
dependencies:
|
||||||
bson "^5.4.0"
|
|
||||||
mongodb-connection-string-url "^2.6.0"
|
|
||||||
socks "^2.7.1"
|
|
||||||
optionalDependencies:
|
|
||||||
saslprep "^1.0.3"
|
|
||||||
|
|
||||||
mongodb@5.9.0:
|
|
||||||
version "5.9.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mongodb/-/mongodb-5.9.0.tgz#5a22065fa8cfaf1d58bf2e3c451cd2c4bfa983a2"
|
|
||||||
integrity sha512-g+GCMHN1CoRUA+wb1Agv0TI4YTSiWr42B5ulkiAfLLHitGK1R+PkSAf3Lr5rPZwi/3F04LiaZEW0Kxro9Fi2TA==
|
|
||||||
dependencies:
|
|
||||||
bson "^5.5.0"
|
|
||||||
mongodb-connection-string-url "^2.6.0"
|
|
||||||
socks "^2.7.1"
|
|
||||||
optionalDependencies:
|
|
||||||
"@mongodb-js/saslprep" "^1.1.0"
|
"@mongodb-js/saslprep" "^1.1.0"
|
||||||
|
bson "^6.2.0"
|
||||||
|
mongodb-connection-string-url "^2.6.0"
|
||||||
|
|
||||||
mongoose@^7.4.0:
|
mongoose@^8.0.3:
|
||||||
version "7.4.0"
|
version "8.0.3"
|
||||||
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.4.0.tgz#3726a8dea0c9e2d54651845cc84c813a6507906a"
|
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-8.0.3.tgz#f3708ba0f40b6dec8f5f1a5d40abd69a80207114"
|
||||||
integrity sha512-oHE1eqodfKzugXRlQxpo+msIea7jPcRoayDuEMr50+bYwM/juA5f+1stjkWlXcg6vo1PdJFVA6DGaKOPLuG5mA==
|
integrity sha512-LJRT0yP4TW14HT4r2RkxqyvoTylMSzWpl5QOeVHTnRggCLQSpkoBdgbUtORFq/mSL2o9cLCPJz+6uzFj25qbHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
bson "^5.4.0"
|
bson "^6.2.0"
|
||||||
kareem "2.5.1"
|
kareem "2.5.1"
|
||||||
mongodb "5.7.0"
|
mongodb "6.2.0"
|
||||||
mpath "0.9.0"
|
|
||||||
mquery "5.0.0"
|
|
||||||
ms "2.1.3"
|
|
||||||
sift "16.0.1"
|
|
||||||
|
|
||||||
mongoose@^7.6.0:
|
|
||||||
version "7.6.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/mongoose/-/mongoose-7.6.0.tgz#3037b754ccdf168385e52d4bcabeb7f5c3b17a5f"
|
|
||||||
integrity sha512-ztQ12rm0BQN5i7LB6xhWX4l9a9w2aa3jEwa/mM2vAutYJRyAwOzcusvKJBULMzFHyUDBOVW15grisexypgMIWA==
|
|
||||||
dependencies:
|
|
||||||
bson "^5.4.0"
|
|
||||||
kareem "2.5.1"
|
|
||||||
mongodb "5.9.0"
|
|
||||||
mpath "0.9.0"
|
mpath "0.9.0"
|
||||||
mquery "5.0.0"
|
mquery "5.0.0"
|
||||||
ms "2.1.3"
|
ms "2.1.3"
|
||||||
@@ -4912,10 +4886,10 @@ nan@^2.17.0:
|
|||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
|
||||||
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
|
||||||
|
|
||||||
nanoid@^3.3.6:
|
nanoid@^3.3.7:
|
||||||
version "3.3.6"
|
version "3.3.7"
|
||||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||||
|
|
||||||
natural-compare@^1.4.0:
|
natural-compare@^1.4.0:
|
||||||
version "1.4.0"
|
version "1.4.0"
|
||||||
@@ -5410,12 +5384,12 @@ postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0:
|
|||||||
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514"
|
||||||
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
|
||||||
|
|
||||||
postcss@^8.1.10, postcss@^8.4.23, postcss@^8.4.25, postcss@^8.4.31:
|
postcss@^8.1.10, postcss@^8.4.23, postcss@^8.4.27, postcss@^8.4.31:
|
||||||
version "8.4.31"
|
version "8.4.32"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.32.tgz#1dac6ac51ab19adb21b8b34fd2d93a86440ef6c9"
|
||||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
integrity sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==
|
||||||
dependencies:
|
dependencies:
|
||||||
nanoid "^3.3.6"
|
nanoid "^3.3.7"
|
||||||
picocolors "^1.0.0"
|
picocolors "^1.0.0"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.0.2"
|
||||||
|
|
||||||
@@ -5691,10 +5665,10 @@ rimraf@^3.0.0, rimraf@^3.0.2:
|
|||||||
dependencies:
|
dependencies:
|
||||||
glob "^7.1.3"
|
glob "^7.1.3"
|
||||||
|
|
||||||
rollup@^3.25.2:
|
rollup@^3.27.1:
|
||||||
version "3.26.2"
|
version "3.29.4"
|
||||||
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.26.2.tgz#2e76a37606cb523fc9fef43e6f59c93f86d95e7c"
|
resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.29.4.tgz#4d70c0f9834146df8705bfb69a9a19c9e1109981"
|
||||||
integrity sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==
|
integrity sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
@@ -5748,13 +5722,6 @@ safe-stable-stringify@^2.3.1:
|
|||||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||||
|
|
||||||
saslprep@^1.0.3:
|
|
||||||
version "1.0.3"
|
|
||||||
resolved "https://registry.yarnpkg.com/saslprep/-/saslprep-1.0.3.tgz#4c02f946b56cf54297e347ba1093e7acac4cf226"
|
|
||||||
integrity sha512-/MY/PEMbk2SuY5sScONwhUDsV2p77Znkb/q3nSVstq/yQzYJOH/Azh29p9oJLsl3LnQwSvZDKagDGBsBwSooag==
|
|
||||||
dependencies:
|
|
||||||
sparse-bitfield "^3.0.3"
|
|
||||||
|
|
||||||
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
semver@^6.0.0, semver@^6.3.0, semver@^6.3.1:
|
||||||
version "6.3.1"
|
version "6.3.1"
|
||||||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||||
@@ -5903,11 +5870,6 @@ slice-ansi@^4.0.0:
|
|||||||
astral-regex "^2.0.0"
|
astral-regex "^2.0.0"
|
||||||
is-fullwidth-code-point "^3.0.0"
|
is-fullwidth-code-point "^3.0.0"
|
||||||
|
|
||||||
smart-buffer@^4.2.0:
|
|
||||||
version "4.2.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
|
|
||||||
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
|
|
||||||
|
|
||||||
socket.io-adapter@~2.5.2:
|
socket.io-adapter@~2.5.2:
|
||||||
version "2.5.2"
|
version "2.5.2"
|
||||||
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
|
resolved "https://registry.yarnpkg.com/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz#5de9477c9182fdc171cd8c8364b9a8894ec75d12"
|
||||||
@@ -5946,14 +5908,6 @@ socket.io@^4.7.1:
|
|||||||
socket.io-adapter "~2.5.2"
|
socket.io-adapter "~2.5.2"
|
||||||
socket.io-parser "~4.2.4"
|
socket.io-parser "~4.2.4"
|
||||||
|
|
||||||
socks@^2.7.1:
|
|
||||||
version "2.7.1"
|
|
||||||
resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
|
|
||||||
integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
|
|
||||||
dependencies:
|
|
||||||
ip "^2.0.0"
|
|
||||||
smart-buffer "^4.2.0"
|
|
||||||
|
|
||||||
sorted-array-functions@^1.3.0:
|
sorted-array-functions@^1.3.0:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
|
resolved "https://registry.yarnpkg.com/sorted-array-functions/-/sorted-array-functions-1.3.0.tgz#8605695563294dffb2c9796d602bd8459f7a0dd5"
|
||||||
@@ -6589,14 +6543,14 @@ verror@1.10.0:
|
|||||||
core-util-is "1.0.2"
|
core-util-is "1.0.2"
|
||||||
extsprintf "^1.2.0"
|
extsprintf "^1.2.0"
|
||||||
|
|
||||||
vite@^4.4.0:
|
vite@^4.4.12:
|
||||||
version "4.4.4"
|
version "4.4.12"
|
||||||
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.4.tgz#b76e6049c0e080cb54e735ad2d18287753752118"
|
resolved "https://registry.yarnpkg.com/vite/-/vite-4.4.12.tgz#e9c355d5a0d8a47afa46cb4bad10820da333da5c"
|
||||||
integrity sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==
|
integrity sha512-KtPlUbWfxzGVul8Nut8Gw2Qe8sBzWY+8QVc5SL8iRFnpnrcoCaNlzO40c1R6hPmcdTwIPEDkq0Y9+27a5tVbdQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild "^0.18.10"
|
esbuild "^0.18.10"
|
||||||
postcss "^8.4.25"
|
postcss "^8.4.27"
|
||||||
rollup "^3.25.2"
|
rollup "^3.27.1"
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
fsevents "~2.3.2"
|
fsevents "~2.3.2"
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user