mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-06 03:20:13 +08:00
Merge branch 'quest-file' into quest
This commit is contained in:
commit
956af624c6
69 files changed
+1232
-260
No files matched your search
+1
-1
@@ -17,7 +17,7 @@ module.exports = {
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'no-console': 'warn',
|
||||
'no-debugger': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
|
||||
'import/extensions': 'off',
|
||||
camelcase: ['error', { properties: 'never', ignoreDestructuring: true }],
|
||||
|
||||
@@ -2,6 +2,15 @@ name: CI Checks
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
GOOGLE_CLIENT_ID:
|
||||
required: true
|
||||
GOOGLE_CLIENT_SECRET:
|
||||
required: true
|
||||
GH_CLIENT_ID:
|
||||
required: true
|
||||
GH_CLIENT_SECRET:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
ci:
|
||||
@@ -40,6 +49,6 @@ jobs:
|
||||
env:
|
||||
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
|
||||
GOOGLE_CLIENT_SECRET: ${{ secrets.GOOGLE_CLIENT_SECRET }}
|
||||
GITHUB_CLIENT_ID: ${{ secrets.GITHUB_CLIENT_ID }}
|
||||
GITHUB_CLIENT_SECRET: ${{ secrets.GITHUB_CLIENT_SECRET }}
|
||||
GITHUB_CLIENT_ID: ${{ secrets.GH_CLIENT_ID }}
|
||||
GITHUB_CLIENT_SECRET: ${{ secrets.GH_CLIENT_SECRET }}
|
||||
run: docker-compose -f docker-compose.testing.yml run cypress
|
||||
@@ -12,6 +12,7 @@ concurrency:
|
||||
jobs:
|
||||
ci-checks:
|
||||
uses: ./.github/workflows/ci-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
needs: ci-checks
|
||||
|
||||
@@ -10,6 +10,7 @@ concurrency:
|
||||
jobs:
|
||||
ci-checks:
|
||||
uses: ./.github/workflows/ci-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
code_review:
|
||||
permissions:
|
||||
|
||||
@@ -12,6 +12,7 @@ concurrency:
|
||||
jobs:
|
||||
ci-checks:
|
||||
uses: ./.github/workflows/ci-checks.yml
|
||||
secrets: inherit
|
||||
|
||||
build:
|
||||
needs: ci-checks
|
||||
|
||||
@@ -41,7 +41,7 @@ const hasSocialLogins = computed(() => {
|
||||
})
|
||||
|
||||
const success = () => {
|
||||
router.push({ name: 'game', params: { questId: 'helloworld' } })
|
||||
router.push({ name: 'map' })
|
||||
}
|
||||
|
||||
const error = (err) => {
|
||||
@@ -100,6 +100,7 @@ const handleChange = () => {
|
||||
<form @submit.prevent="handleSubmit()" class="w-full">
|
||||
<div v-if="socialLogin && hasSocialLogins">
|
||||
<a
|
||||
id="GoogleLogin"
|
||||
v-if="availableMethods.google"
|
||||
class="inline-flex justify-center items-center rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
||||
href="/api/v1/auth/google"
|
||||
@@ -108,9 +109,13 @@ const handleChange = () => {
|
||||
:icon="['fab', 'google']"
|
||||
class="text-text-secondary px-3 text-lg"
|
||||
/>
|
||||
<span class="font-medium">Continue with Google</span>
|
||||
<span class="font-medium">
|
||||
<!-- Continue with Google -->
|
||||
以 Google 繼續
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
id="GitHubLogin"
|
||||
v-if="availableMethods.github"
|
||||
class="inline-flex justify-center items-center rounded-lg py-2 mt-3 bg-bg-primary text-text-secondary w-full border-text-secondary border-2"
|
||||
href="/api/v1/auth/github"
|
||||
@@ -119,17 +124,22 @@ const handleChange = () => {
|
||||
:icon="['fab', 'github']"
|
||||
class="text-text-secondary px-3 text-lg"
|
||||
/>
|
||||
<span class="font-medium">Continue with GitHub</span>
|
||||
<span class="font-medium">
|
||||
<!-- Continue with GitHub -->
|
||||
以 GitHub 繼續
|
||||
</span>
|
||||
</a>
|
||||
<p
|
||||
id="or"
|
||||
class="text-text-secondary flex justify-center mt-3"
|
||||
v-if="type === 'login' || type === 'register'"
|
||||
>
|
||||
or
|
||||
<!-- or -->
|
||||
或
|
||||
</p>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<input
|
||||
<!-- <input
|
||||
type="text"
|
||||
id="username"
|
||||
ref="usernameInput"
|
||||
@@ -139,6 +149,17 @@ const handleChange = () => {
|
||||
required
|
||||
@input="handleChange()"
|
||||
autocomplete="username"
|
||||
/> -->
|
||||
<input
|
||||
type="text"
|
||||
id="username"
|
||||
ref="usernameInput"
|
||||
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
|
||||
:placeholder="type === 'login' ? '電子郵件 / 帳號名稱' : '帳號名稱'"
|
||||
v-model="username"
|
||||
required
|
||||
@input="handleChange()"
|
||||
autocomplete="username"
|
||||
/>
|
||||
<label
|
||||
class="text-sm font-normal text-text-secondary"
|
||||
@@ -149,7 +170,7 @@ const handleChange = () => {
|
||||
</label>
|
||||
</div>
|
||||
<div class="mb-6" v-if="type === 'register'">
|
||||
<input
|
||||
<!-- <input
|
||||
type="email"
|
||||
id="email"
|
||||
ref="emailInput"
|
||||
@@ -159,10 +180,21 @@ const handleChange = () => {
|
||||
required
|
||||
@input="handleChange()"
|
||||
autocomplete="email"
|
||||
/> -->
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
ref="emailInput"
|
||||
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
|
||||
placeholder="電子郵件"
|
||||
v-model="email"
|
||||
required
|
||||
@input="handleChange()"
|
||||
autocomplete="email"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-6" v-if="type === 'login' || type === 'register'">
|
||||
<input
|
||||
<!-- <input
|
||||
type="password"
|
||||
id="password"
|
||||
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
|
||||
@@ -170,30 +202,66 @@ const handleChange = () => {
|
||||
v-model="password"
|
||||
required
|
||||
@input="handleChange()"
|
||||
/> -->
|
||||
<input
|
||||
type="password"
|
||||
id="password"
|
||||
class="my-4 bg-bg-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-bg-secondary"
|
||||
placeholder="密碼"
|
||||
v-model="password"
|
||||
required
|
||||
@input="handleChange()"
|
||||
/>
|
||||
</div>
|
||||
<p class="text-error flex justify-center" v-if="errorMessage">
|
||||
<p
|
||||
id="ErrorDisplay"
|
||||
class="text-error flex justify-center"
|
||||
v-if="errorMessage"
|
||||
>
|
||||
{{ errorMessage }}
|
||||
</p>
|
||||
<button
|
||||
id="LogInOrSignUp"
|
||||
class="inline-flex justify-center rounded-lg font-black py-2 bg-text-primary text-bg w-full"
|
||||
type="submit"
|
||||
>
|
||||
<span v-if="type === 'login'">Log In</span>
|
||||
<span v-else>Sign Up</span>
|
||||
<span v-if="type === 'login'">
|
||||
<!-- Log In -->
|
||||
登入
|
||||
</span>
|
||||
<span v-else>
|
||||
<!-- Sign Up -->
|
||||
註冊
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<p class="text-text flex justify-center mt-3">
|
||||
<span v-if="type === 'register'"
|
||||
>Already have an account?
|
||||
<RouterLink class="text-text-primary font-bold" to="/login">
|
||||
<u>Log in</u>
|
||||
<p id="SwitchLoginRegister" class="text-text flex justify-center mt-3">
|
||||
<span id="CheckRegistered" v-if="type === 'register'">
|
||||
<!-- Already have an account? -->
|
||||
已經有帳號了嗎?
|
||||
<RouterLink
|
||||
id="SwitchtoLogin"
|
||||
class="text-text-primary font-bold"
|
||||
to="/login"
|
||||
>
|
||||
<u>
|
||||
<!-- Log in -->
|
||||
登入
|
||||
</u>
|
||||
</RouterLink></span
|
||||
>
|
||||
<span v-else-if="type === 'login'"
|
||||
>Don't have an account?
|
||||
<RouterLink class="text-text-primary font-bold" to="/register">
|
||||
<u>Sign up</u>
|
||||
<span id="CheckRegistered" v-else-if="type === 'login'">
|
||||
<!-- Don't have an account? -->
|
||||
還沒有帳號嗎?
|
||||
<RouterLink
|
||||
id="SwitchtoRegister"
|
||||
class="text-text-primary font-bold"
|
||||
to="/register"
|
||||
>
|
||||
<u>
|
||||
<!-- Sign up -->
|
||||
註冊
|
||||
</u>
|
||||
</RouterLink></span
|
||||
>
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<template>
|
||||
<font-awesome-icon :icon="['fas', 'list']" class="text-text-primary p-1" />
|
||||
<h1 class="inline text-text-primary font-xl">Command Cheatsheets</h1>
|
||||
<h1 id="CommandBook" class="inline text-text-primary font-xl">
|
||||
<!-- Command Cheatsheets -->
|
||||
指令之書
|
||||
</h1>
|
||||
<div id="cheatsheets">
|
||||
<p class="text-text-secondary">--Command List--</p>
|
||||
<p class="text-text-secondary">
|
||||
<!-- --Command List-- -->
|
||||
-- --指令列表-- --
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
@@ -18,14 +18,19 @@ setTimeout(() => {
|
||||
<div v-if="showModal" class="modal">
|
||||
<div class="modal-content bg-bg-secondary rounded-lg">
|
||||
<span class="close text-text" @click="closeModal">×</span>
|
||||
<h2 class="text-2xl text-text mb-5 w-full inline-flex justify-center">
|
||||
Quest Completed!
|
||||
<h2
|
||||
id="QuestCompleted"
|
||||
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
|
||||
>
|
||||
<!-- Quest Completed! -->
|
||||
關卡完成!
|
||||
</h2>
|
||||
<p class="text-base text-text">
|
||||
您已成功完成挑戰,真的非常感謝您的參與!<br />
|
||||
如果您能撥冗填寫這份問卷,我們將不勝感激,這對我們非常重要!
|
||||
</p>
|
||||
<a
|
||||
id="SurveyButton"
|
||||
:href="surveyUrl"
|
||||
target="_blank"
|
||||
class="inline-flex justify-center rounded-lg text-base font-black py-2 mt-3 bg-text-primary w-full"
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
<script setup>
|
||||
import { createSession } from '../store/session'
|
||||
|
||||
const reset = async () => {
|
||||
console.log('resetting...')
|
||||
await createSession()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="h-[10%] p-3 grid grid-cols-3 gap-4 place-content-between">
|
||||
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
|
||||
@@ -14,21 +5,55 @@ const reset = async () => {
|
||||
:icon="['far', 'circle-question']"
|
||||
class="text-text-disabled"
|
||||
/>
|
||||
<p class="text-text-disabled inline ml-1">Solution</p>
|
||||
<p class="text-text-disabled inline ml-1">
|
||||
<!-- Solution -->
|
||||
解答
|
||||
</p>
|
||||
</button>
|
||||
<button id="reset" class="bg-bg-secondary rounded-lg p-2" @click="reset">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'arrow-rotate-left']"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-primary inline ml-1">Reset</p>
|
||||
<p class="text-text-primary inline ml-1">
|
||||
<!-- Reset -->
|
||||
重來
|
||||
</p>
|
||||
</button>
|
||||
<button disabled id="continue" class="bg-bg-disabled rounded-lg p-2">
|
||||
<button
|
||||
id="continue"
|
||||
class="bg-bg-secondary rounded-lg p-2"
|
||||
@click="continuePlay"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['far', 'circle-right']"
|
||||
class="text-text-disabled"
|
||||
class="text-text-primary"
|
||||
/>
|
||||
<p class="text-text-disabled inline ml-1">Continue</p>
|
||||
<p class="text-text-primary inline ml-1">
|
||||
<!-- Continue -->
|
||||
繼續
|
||||
</p>
|
||||
</button>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useToast } from 'vue-toastification'
|
||||
import { createSession } from '../store/session'
|
||||
|
||||
const router = useRouter()
|
||||
const reset = async () => {
|
||||
console.log('resetting...')
|
||||
await createSession()
|
||||
}
|
||||
const continuePlay = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
// useToast().error('Failed to load map')
|
||||
useToast().error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -15,7 +15,17 @@ const handleLogout = async () => {
|
||||
reset()
|
||||
router.push({ name: 'login' })
|
||||
} catch (err) {
|
||||
toast.error('Failed to logout')
|
||||
// toast.error('Failed to logout')
|
||||
toast.error('登出失敗')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
const handleMap = async () => {
|
||||
try {
|
||||
router.push({ name: 'map' })
|
||||
} catch (err) {
|
||||
// toast.error('Failed to load map')
|
||||
toast.error('無法讀取地圖')
|
||||
console.error(err)
|
||||
}
|
||||
}
|
||||
@@ -38,7 +48,9 @@ const handleLogout = async () => {
|
||||
/>
|
||||
</button> -->
|
||||
<p
|
||||
id="HeaderText"
|
||||
class="text-text-primary inline-block font-bold whitespace-nowrap"
|
||||
style="font-size: 2vh"
|
||||
v-if="sessionStore.quest"
|
||||
>
|
||||
{{ sessionStore.quest.title }}
|
||||
@@ -63,6 +75,12 @@ const handleLogout = async () => {
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button> -->
|
||||
<button title="Map" @click="handleMap" class="h-5 w-5">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'map']"
|
||||
class="text-text-primary h-full w-full"
|
||||
/>
|
||||
</button>
|
||||
<a
|
||||
title="Bug Report"
|
||||
:href="bugReportUrl"
|
||||
|
||||
@@ -30,8 +30,11 @@ const right = () => {
|
||||
:icon="['far', 'lightbulb']"
|
||||
class="text-yellow-200 p-2 content-center"
|
||||
/>
|
||||
<h1 class="inline text-text font-xl p-2 m-1">Hint</h1>
|
||||
<div v-if="current !== -1" class="w-full flex justify-end">
|
||||
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">
|
||||
<!-- Hint -->
|
||||
提示
|
||||
</h1>
|
||||
<div v-if="current !== -1" class="flex w-full justify-end items-end">
|
||||
<button
|
||||
class="p-2 m-1 w-1/8"
|
||||
id="Lbutton"
|
||||
|
||||
@@ -14,6 +14,7 @@ const marginX = 100
|
||||
const marginY = 65
|
||||
const chartContainer = ref(null)
|
||||
let chartInstance = null
|
||||
const fullwidth = window.screen.width
|
||||
|
||||
use([GraphChart, SVGRenderer, TitleComponent, TooltipComponent])
|
||||
|
||||
@@ -49,11 +50,11 @@ const genOption = (nodes, edges) => ({
|
||||
symbol: () => {
|
||||
return NodeImage
|
||||
},
|
||||
symbolSize: [200, 100],
|
||||
roam: true,
|
||||
symbolSize: [fullwidth / 10, fullwidth / 20],
|
||||
roam: false,
|
||||
label: {
|
||||
show: true,
|
||||
fontSize: 22,
|
||||
fontSize: fullwidth / 90,
|
||||
},
|
||||
edgeSymbol: ['pin', 'arrow'],
|
||||
edgeSymbolSize: [2, 15],
|
||||
@@ -126,7 +127,8 @@ function initChart(option) {
|
||||
if (unlocked) {
|
||||
router.push({ name: 'game', params: { questId: id } })
|
||||
} else {
|
||||
toast.warning('You have not completed the previous quest yet!')
|
||||
// toast.warning('You have not completed the previous quest yet!')
|
||||
toast.warning('你還沒完成前一個關卡!')
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -147,9 +149,11 @@ onMounted(async () => {
|
||||
class="p-2 stroke-2 scale-y-120 justify-center items-center"
|
||||
/>
|
||||
<h1
|
||||
class="text-text-primary text-xl p-2.5 absolute w-fit z-2 font-mono font-bold flex flax-wrap"
|
||||
class="p-4 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
|
||||
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
|
||||
>
|
||||
Get through your linux journey!
|
||||
<!-- Get through your linux journey! -->
|
||||
踏上你的Linux冒險之旅吧!
|
||||
</h1>
|
||||
<div class="flex flex-wrap absolute w-full h-full z-1">
|
||||
<div ref="chartContainer" class="w-full h-full flex flex-wrap"></div>
|
||||
|
||||
@@ -11,8 +11,11 @@ import sessionStore from '../store/session'
|
||||
<div>
|
||||
<div v-if="sessionStore.quest" class="whitespace-pre-wrap">
|
||||
<p class="text-text">{{ sessionStore.quest.instruction }}</p>
|
||||
<hr class="border-border border-2 my-2" />
|
||||
<p class="text-text">Tasks:</p>
|
||||
<br />
|
||||
<p id="tasks" class="text-text">
|
||||
<!-- Tasks: -->
|
||||
任務:
|
||||
</p>
|
||||
<ul v-if="sessionStore.session">
|
||||
<li v-for="task in sessionStore.session.tasks" :key="task.id">
|
||||
<p v-if="task.completed" class="text-text-primary">
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
<template>
|
||||
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
|
||||
<button id="Terminal" class="text-text h-[5%]">
|
||||
<!-- Terminal -->
|
||||
終端機
|
||||
</button>
|
||||
<div
|
||||
id="terminal"
|
||||
ref="terminal"
|
||||
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
|
||||
></div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { useTerminal } from '../store/session'
|
||||
@@ -10,11 +23,3 @@ onMounted(() => {
|
||||
window.addEventListener('resize', () => term.resizeScreen())
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-[10%] w-full max-h-full">
|
||||
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
|
||||
<button class="text-text h-full">Terminal</button>
|
||||
</div>
|
||||
<div id="terminal" ref="terminal" class="h-[90%] p-2"></div>
|
||||
</template>
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
faArrowRight,
|
||||
faBug,
|
||||
faCircleExclamation,
|
||||
faMap,
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import {
|
||||
faLightbulb,
|
||||
@@ -36,6 +37,7 @@ library.add(
|
||||
faArrowRightFromBracket,
|
||||
faArrowRight,
|
||||
faBug,
|
||||
faMap,
|
||||
faCircleExclamation,
|
||||
faLightbulb,
|
||||
faCircleQuestion,
|
||||
|
||||
+4
-9
@@ -10,11 +10,9 @@ const routes = [
|
||||
{
|
||||
path: '/',
|
||||
redirect: {
|
||||
name: 'game',
|
||||
params: {
|
||||
questId: 'helloworld',
|
||||
},
|
||||
name: 'register',
|
||||
},
|
||||
meta: { requiresGuest: true },
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
@@ -66,13 +64,10 @@ router.beforeEach(async (to, from, next) => {
|
||||
})
|
||||
} else if (to.meta.requiresGuest && loggedIn) {
|
||||
next({
|
||||
name: 'game', // Or any default authenticated user route
|
||||
params: {
|
||||
questId: 'helloworld',
|
||||
},
|
||||
name: 'map',
|
||||
})
|
||||
} else {
|
||||
next() // Always call next() at the end
|
||||
next()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -8,21 +8,24 @@ import { checkUsername, chooseUsername } from '../utils/auth'
|
||||
async function check({ username, error }) {
|
||||
if (username) {
|
||||
if (!isValidUsername(username)) {
|
||||
error('Invalid username.')
|
||||
// error('Invalid username.')
|
||||
error('無效的帳號名稱')
|
||||
return
|
||||
}
|
||||
try {
|
||||
await checkUsername(username)
|
||||
} catch (err) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('Too many requests. Try again in 2 minutes.')
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
error(err.message)
|
||||
return
|
||||
}
|
||||
error('Something went wrong. Please try again later.')
|
||||
// error('Something went wrong. Please try again later.')
|
||||
error('出了點問題,請再試一次。')
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,7 +36,8 @@ async function handleSubmit({ username, success, error }) {
|
||||
success()
|
||||
} catch (err) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('Too many requests. Try again in 2 minutes.')
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
|
||||
@@ -30,13 +30,15 @@ onMounted(async () => {
|
||||
await init(props.questId)
|
||||
} catch (err) {
|
||||
if (err instanceof LoadQuestError) {
|
||||
toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
|
||||
// toast.error(`Failed to load quest: ${err.questId}. Please check the URL.`)
|
||||
toast.error(`無法讀取關卡: ${err.questId},請確認網頁連結。`)
|
||||
return
|
||||
}
|
||||
if (err instanceof LoadSessionError) {
|
||||
toast.error(
|
||||
`Failed to create session for quest: ${err.questId}. Please re-login and try again.`
|
||||
)
|
||||
// toast.error(
|
||||
// `Failed to create session for quest: ${err.questId}. Please re-login and try again.`
|
||||
// )
|
||||
toast.error(`無法建立工作階段: ${err.questId},請重新登入再試一次。`)
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
|
||||
@@ -15,19 +15,23 @@ const handleLogin = async ({ username, password, success, error }) => {
|
||||
if (isSuccess) success()
|
||||
} catch (err) {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('Too many requests. Try again in 2 minutes.')
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
if (err instanceof UnauthorizedError) {
|
||||
error('Wrong username or password.')
|
||||
// error('Wrong username or password.')
|
||||
error('錯誤的帳號名稱或密碼。')
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
error('Invalid username or password.')
|
||||
// error('Invalid username or password.')
|
||||
error('無效的帳號名稱或密碼。')
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
error('Something went wrong.')
|
||||
// error('Something went wrong.')
|
||||
error('出了點問題。')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -7,10 +7,11 @@ import GameHeaderPart from '../components/GameHeaderPart.vue'
|
||||
<div class="h-[6vh] w-full">
|
||||
<GameHeaderPart />
|
||||
<p
|
||||
class="text-text-primary absolute z-1 font-bold inline-block"
|
||||
style="left: 1.2%; top: 1.5%; height: 100%"
|
||||
class="text-text-primary absolute z-1 font-bold inline-block tracking-widest"
|
||||
style="left: 1.2%; top: 1.5%; height: 100%; font-size: 2vh"
|
||||
>
|
||||
Quest Map
|
||||
<!-- Quest Map -->
|
||||
Unix編年史
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col justify-center items-center w-full h-[94vh]">
|
||||
|
||||
@@ -17,11 +17,13 @@ function handleRegister({ username, email, password, success, error }) {
|
||||
.then(success)
|
||||
.catch((err) => {
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('Too many requests. Try again in 2 minutes.')
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
if (err instanceof UnauthorizedError) {
|
||||
error('Invalid username or password.')
|
||||
// error('Invalid username or password.')
|
||||
error('無效的帳號名稱或密碼。')
|
||||
return
|
||||
}
|
||||
if (err instanceof ValidationError) {
|
||||
@@ -29,23 +31,27 @@ function handleRegister({ username, email, password, success, error }) {
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
error('Something went wrong.')
|
||||
// error('Something went wrong.')
|
||||
error('出了點問題。')
|
||||
})
|
||||
}
|
||||
|
||||
async function check({ username, email, password, error }) {
|
||||
if (username && !isValidUsername(username)) {
|
||||
error('Invalid username.')
|
||||
// error('Invalid username.')
|
||||
error('無效的帳號名稱')
|
||||
return
|
||||
}
|
||||
if (email && !validator.isEmail(email)) {
|
||||
error('Invalid email.')
|
||||
// error('Invalid email.')
|
||||
error('無效的電子郵件')
|
||||
return
|
||||
}
|
||||
if (password && !validator.isStrongPassword(password, passwordPolicy)) {
|
||||
error(
|
||||
'Your password must be 8+ characters with at least one number, one upper and one lower case letter.'
|
||||
)
|
||||
// error(
|
||||
// 'Your password must be 8+ characters with at least one number, one upper and one lower case letter.'
|
||||
// )
|
||||
error('密碼必須超過8個字元,至少包含一個數字、一個大寫及一個小寫。')
|
||||
return
|
||||
}
|
||||
try {
|
||||
@@ -56,11 +62,13 @@ async function check({ username, email, password, error }) {
|
||||
return
|
||||
}
|
||||
if (err instanceof TooManyRequestsError) {
|
||||
error('Too many requests. Try again in 2 minutes.')
|
||||
// error('Too many requests. Try again in 2 minutes.')
|
||||
error('太多請求,兩分鐘後再試一次。')
|
||||
return
|
||||
}
|
||||
console.error(err)
|
||||
error('Something went wrong.')
|
||||
// error('Something went wrong.')
|
||||
error('出了點問題。')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,6 +28,7 @@ services:
|
||||
- players
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./logs:/app/logs
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
||||
@@ -41,6 +41,7 @@ services:
|
||||
- players
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- BASE_URL=http://localhost:5173
|
||||
- MONGO_URL=mongodb://user:user-password@db-test:27017/odyssey-test
|
||||
|
||||
+3
-1
@@ -33,6 +33,7 @@ services:
|
||||
- ./server/swagger.yaml:/app/server/swagger.yaml
|
||||
- ./packages:/app/packages
|
||||
- ./quests:/app/quests
|
||||
- ./logs:/app/logs
|
||||
environment:
|
||||
- BASE_URL=http://localhost:5173
|
||||
- MONGO_URL=mongodb://db:27017/odyssey-test
|
||||
@@ -41,7 +42,6 @@ services:
|
||||
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
|
||||
- SURVEY_URL=${SURVEY_URL}
|
||||
- BUG_REPORT_URL=${BUG_REPORT_URL}
|
||||
- HOST_PWD=${PWD}
|
||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
||||
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
||||
@@ -49,6 +49,8 @@ services:
|
||||
- TESTING=true
|
||||
- TESTING_USERNAME=alex
|
||||
- TESTING_PASSWORD=Alex1234
|
||||
- HOST_PWD=${PWD}
|
||||
- MOUNT_QUEST=${MOUNT_QUEST}
|
||||
|
||||
analytics:
|
||||
build:
|
||||
|
||||
@@ -85,4 +85,12 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main().then(exit)
|
||||
main()
|
||||
.then(exit)
|
||||
.catch((err) => {
|
||||
console.log(
|
||||
'發生預期外的錯誤,請嘗試重新啟動 Quest,如果問題持續發生,請幫我們填寫右上角的錯誤回報表單。'
|
||||
)
|
||||
console.error(err)
|
||||
exit(1)
|
||||
})
|
||||
@@ -34,12 +34,16 @@ async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) {
|
||||
|
||||
async function discoverFiles(argv) {
|
||||
const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1)
|
||||
const result = await Promise.all(
|
||||
targetPath
|
||||
.map((p) => path.resolve(process.cwd(), p))
|
||||
.map((p) => collectFilesInfo(p, 0, argv.a || argv.all))
|
||||
)
|
||||
return { discover: result.flat() }
|
||||
try {
|
||||
const result = await Promise.all(
|
||||
targetPath
|
||||
.map((p) => path.resolve(process.cwd(), p))
|
||||
.map((p) => collectFilesInfo(p, 0, argv.a || argv.all))
|
||||
)
|
||||
return { discover: result.flat() }
|
||||
} catch (err) {
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = discoverFiles
|
||||
@@ -3,10 +3,12 @@ import { model, Schema } from 'mongoose'
|
||||
const responseSchema = new Schema({
|
||||
type: {
|
||||
type: String,
|
||||
enum: ['narrative', 'dialogue'],
|
||||
default: 'dialogue',
|
||||
required: true,
|
||||
},
|
||||
content: [String],
|
||||
speaker: String,
|
||||
speaker: { type: String, default: 'Ada' },
|
||||
color: String,
|
||||
})
|
||||
|
||||
|
||||
@@ -35,10 +35,12 @@ const Session = model(
|
||||
user: {
|
||||
type: Schema.ObjectId,
|
||||
ref: 'User',
|
||||
required: true,
|
||||
},
|
||||
quest: {
|
||||
type: String,
|
||||
ref: 'Quest',
|
||||
required: true,
|
||||
},
|
||||
containerId: String,
|
||||
status: {
|
||||
|
||||
+18
-4
@@ -1,6 +1,8 @@
|
||||
title: 複製檔案
|
||||
instruction: |
|
||||
在這個關卡中,你將學習如何複製檔案。
|
||||
在這個關卡中,你會學習到:
|
||||
1. 如何用 `cp` 指令複製檔案。
|
||||
準備好就輸入 `echo start` 來開始吧!
|
||||
|
||||
requirements:
|
||||
- create
|
||||
@@ -170,11 +172,14 @@ stages:
|
||||
- 接下來我們要來學習如何複製資料夾。
|
||||
- 現在目錄下有一個 `notes` 資料夾,裡面有一個 `note.txt` 的檔案。
|
||||
- 我們要能整個資料夾包含裡面的檔案一起複製到 `notes2`。
|
||||
- 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的參數,recursive,代表遞迴地將資料夾中的所有檔案都複製出來。
|
||||
- 跟複製單一檔案不一樣的是,複製資料夾時,我們要加上 `-r` 的選項(recursive)代表將資料夾中的所有檔案都一起複製。
|
||||
- 在使用指令時,如果想要調整指令的行為,可以在指令後面加上選項。
|
||||
- 選項的格式通常是 `-` 加上一個字母,例如 `-r`。
|
||||
- 選項擺放的位置沒有特別的限制,通常會跟在指令的後面,置於檔案名稱的前面。
|
||||
- 請你使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`。
|
||||
hints:
|
||||
- 使用 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
||||
- 記得要加上 `-r` 的參數來複製所有檔案
|
||||
- 記得要加上 `-r` 的選項來複製所有檔案
|
||||
|
||||
- id: cp-notes
|
||||
requirements:
|
||||
@@ -182,7 +187,7 @@ stages:
|
||||
task: 複製 `notes` 資料夾到 `notes2`
|
||||
condition:
|
||||
command:
|
||||
- ^cp -r notes notes2$
|
||||
- ^cp
|
||||
files:
|
||||
- path: notes2
|
||||
type: directory
|
||||
@@ -195,6 +200,15 @@ stages:
|
||||
- 我們打開來確認一下 `notes2` 資料夾的內容吧!
|
||||
hints:
|
||||
- 使用 `ls notes2` 來查看 `notes2` 資料夾的內容
|
||||
exceptions:
|
||||
- condition:
|
||||
error:
|
||||
- 'cp: omitting directory'
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你忘記加上 `-r` 的參數了嗎?
|
||||
- 請輸入 `cp -r notes notes2` 來複製 `notes` 資料夾到 `notes2`
|
||||
|
||||
- id: ls-notes2
|
||||
requirements:
|
||||
|
||||
@@ -1 +1 @@
|
||||
Hello
|
||||
Hello
|
||||
@@ -1 +1 @@
|
||||
Note
|
||||
Note
|
||||
@@ -1 +1 @@
|
||||
World
|
||||
World
|
||||
+20
-2
@@ -1,6 +1,9 @@
|
||||
title: 建立檔案與資料夾
|
||||
instruction: |
|
||||
在這個關卡中,你將學習如何建立檔案與資料夾。
|
||||
在這個關卡中,你會學習到:
|
||||
1. 如何使用 `touch` 建立檔案
|
||||
2. 如何使用 `mkdir` 建立資料夾
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
@@ -47,7 +50,22 @@ stages:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你應該可以看到終端機上顯示了 `hello.txt` 這個檔案。
|
||||
- 我們再來試試建立其他的檔案吧!
|
||||
- 我們來看看 `hello.txt` 檔案的內容吧!
|
||||
- 我們可以使用之前學過的 `cat` 指令來查看檔案內容。
|
||||
hints:
|
||||
- 輸入 `cat hello.txt` 來查看 `hello.txt` 檔案的內容
|
||||
|
||||
- id: cat
|
||||
condition:
|
||||
command:
|
||||
- ^cat hello.txt$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 咦?竟然什麼都沒有!
|
||||
- 沒錯,使用 `touch` 指令建立的檔案是空的。
|
||||
- 我們在未來會學習如何編輯檔案的內容。
|
||||
- 再來試試建立其他的檔案吧!
|
||||
- 請你試著在終端機上建立另一個檔案,檔名為 `world.txt`。
|
||||
hints:
|
||||
- 輸入 `touch world.txt` 來建立一個檔案
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY --chown=${USERNAME} home ${HOME}
|
||||
+214
-1
@@ -1,6 +1,219 @@
|
||||
title: 移動檔案
|
||||
instruction: |
|
||||
移動檔案
|
||||
在這個關卡中,你會學到:
|
||||
1. 如何用 `mv` 移動檔案與資料夾
|
||||
準備好就輸入 `echo start` 來開始吧!
|
||||
|
||||
requirements:
|
||||
- create
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo start$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 今天我們要學習如何移動檔案與資料夾。
|
||||
- 現在資料夾裡有一個 `hello.txt` 跟一個 `world.txt`,以及一個 `notes` 資料夾。
|
||||
- 請先用 `ls` 與 `cat` 來查看它們的內容。
|
||||
- 看完之後,使用 `echo continue` 來繼續吧!
|
||||
hints:
|
||||
- 用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||
- 用 `cat hello.txt` 以及 `cat world.txt` 來查看檔案內容
|
||||
- 完成後輸入 `echo continue` 來繼續
|
||||
|
||||
- id: continue
|
||||
requirements:
|
||||
- start
|
||||
task: 查看檔案內容,完成後輸入 `echo continue` 來繼續
|
||||
condition:
|
||||
command:
|
||||
- ^echo continue$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 現在我們要來學習如何移動檔案與資料夾。
|
||||
- '`mv` 指令是移動 move 的縮寫,可以用來移動檔案與資料夾。'
|
||||
- 使用方式為 `mv [來源] [目的地]`。
|
||||
- 我們先來將 `hello.txt` 移動到 `notes` 資料夾中吧!
|
||||
hints:
|
||||
- 使用 `mv hello.txt notes` 來將 `hello.txt` 移動到 `notes` 資料夾中
|
||||
|
||||
- id: mv-hello
|
||||
requirements:
|
||||
- continue
|
||||
task: 將 `hello.txt` 移動到 `notes` 資料夾中
|
||||
condition:
|
||||
command:
|
||||
- ^mv hello.txt notes$
|
||||
files:
|
||||
- path: notes/hello.txt
|
||||
type: file
|
||||
- path: hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 跟 `ls notes` 來查看一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- 使用 `ls`、`ls notes` 來查看資料夾內的檔案
|
||||
|
||||
- id: ls-no-hello
|
||||
requirements:
|
||||
- mv-hello
|
||||
task: 檢查 `hello.txt` 是否還在資料夾中
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`hello.txt` 已經不在資料夾中了!'
|
||||
|
||||
- id: ls-notes-hello
|
||||
requirements:
|
||||
- mv-hello
|
||||
task: 檢查 `hello.txt` 是否在 `notes` 資料夾中
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls notes$
|
||||
output:
|
||||
- hello.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`hello.txt` 出現在 `notes` 資料夾中了!'
|
||||
|
||||
- id: continue-mv
|
||||
requirements:
|
||||
- ls-no-hello
|
||||
- ls-notes-hello
|
||||
task: 輸入 `echo continue` 來繼續
|
||||
condition:
|
||||
command:
|
||||
- ^echo continue$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`mv` 指令也可以用來更改檔案名稱。'
|
||||
- '只要將目的地改成新的檔案名稱就可以了。'
|
||||
- 現在我們要將 `world.txt` 更名為 `wow.txt`。
|
||||
hints:
|
||||
- 使用 `mv world.txt wow.txt` 來將 `world.txt` 更名為 `wow.txt`
|
||||
|
||||
- id: mv-world
|
||||
requirements:
|
||||
- continue-mv
|
||||
task: 將 `world.txt` 更名為 `wow.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: wow.txt
|
||||
type: file
|
||||
- path: world.txt
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- 使用 `ls` 來查看資料夾內的檔案
|
||||
|
||||
- id: ls-wow
|
||||
requirements:
|
||||
- mv-world
|
||||
task: 檢查 `wow.txt` 是否存在
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
output:
|
||||
- wow.txt
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`world.txt` 現在已經變成 `wow.txt` 了。'
|
||||
- 接下來我們來學習如何移動資料夾吧!
|
||||
- 移動資料夾非常簡單,方法跟移動檔案一樣。
|
||||
- '`mv` 指令會自動將資料夾內的所有檔案一起移動'
|
||||
- 我們先來將 `notes` 資料夾改名叫作 `docs` 吧!
|
||||
hints:
|
||||
- 使用 `mv notes docs` 來將 `notes` 資料夾改名叫作 `docs`
|
||||
|
||||
- id: mv-notes
|
||||
requirements:
|
||||
- ls-wow
|
||||
task: 將 `notes` 資料夾改名叫作 `docs`
|
||||
condition:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: docs
|
||||
type: directory
|
||||
- path: notes
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 我們用 `ls` 來確認一下資料夾內的檔案吧!
|
||||
hints:
|
||||
- 使用 `ls` 來查看資料夾內的檔案
|
||||
|
||||
- id: ls-docs
|
||||
requirements:
|
||||
- mv-notes
|
||||
task: 檢查 `docs` 資料夾是否存在
|
||||
condition:
|
||||
pwd:
|
||||
- ^/home/commander$
|
||||
command:
|
||||
- ^ls$
|
||||
output:
|
||||
- docs
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- '`notes` 資料夾已經變成 `docs` 了。'
|
||||
- 那麼最後來考考你:
|
||||
- 試著將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`。
|
||||
- 如果你不知道怎麼做,可以輸入 `echo hint` 來取得提示。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- ls-docs
|
||||
task: 將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^mv
|
||||
files:
|
||||
- path: hi.txt
|
||||
type: file
|
||||
- path: docs/hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你做得很好!
|
||||
- 你已經學會如何移動檔案與資料夾了!
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^echo hint$
|
||||
responses:
|
||||
- type: narrative
|
||||
content:
|
||||
- 你可以使用 `mv docs/hello.txt hi.txt` 來完成這個任務。
|
||||
hints:
|
||||
- 使用 `mv docs/hello.txt hi.txt` 來將 `docs/hello.txt` 移動到家目錄底下的 `hi.txt`
|
||||
@@ -0,0 +1 @@
|
||||
Hello
|
||||
@@ -0,0 +1 @@
|
||||
Note
|
||||
@@ -0,0 +1 @@
|
||||
World
|
||||
@@ -0,0 +1,3 @@
|
||||
FROM linuxodyssey/quest-base
|
||||
|
||||
COPY --chown=${USERNAME} home ${HOME}
|
||||
+251
-1
@@ -1,7 +1,257 @@
|
||||
title: 刪除檔案與資料夾
|
||||
instruction: |
|
||||
刪除檔案
|
||||
在這個關卡中,你會學到:
|
||||
1. 用 `rm` 指令刪除檔案
|
||||
2. 用 `rmdir` 與資料夾。
|
||||
3. 用 `rm -r` 刪除資料夾與它的所有檔案。
|
||||
現在資料夾裡有一些檔案與資料夾,請你使用 `ls` 跟 `cat` 來查看它們的內容。
|
||||
準備好就輸入 `echo start` 開始吧!
|
||||
|
||||
requirements:
|
||||
- copy
|
||||
- move
|
||||
|
||||
stages:
|
||||
- id: start
|
||||
task: 輸入 `echo start` 來開始
|
||||
condition:
|
||||
command:
|
||||
- ^echo
|
||||
output:
|
||||
- start
|
||||
responses:
|
||||
- content:
|
||||
- 今天我們要學習如何刪除檔案與資料夾。
|
||||
- '首先要來學習 `rm` 指令。`rm` 指令可以用來刪除檔案。使用方式是 `rm [檔案名稱]`。'
|
||||
- '現在請你使用 `rm` 指令刪除 `hello.txt`。'
|
||||
hints:
|
||||
- 使用 `rm hello.txt` 來刪除 `hello.txt`。
|
||||
|
||||
- id: rm-hello
|
||||
requirements:
|
||||
- start
|
||||
task: 使用 `rm` 刪除 `hello.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
||||
|
||||
- id: ls-no-hello
|
||||
requirements:
|
||||
- rm-hello
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls
|
||||
files:
|
||||
- path: hello.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `hello.txt` 已經不見了。'
|
||||
- '`rm` 也可以同時刪除多個檔案,只要把檔案名稱用空白隔開就可以了,也就是 `rm [檔案1] [檔案2] ...`。'
|
||||
- 現在請你使用 `rm` 刪除 `world.txt` 與 `wow.txt`。
|
||||
hints:
|
||||
- 使用 `rm world.txt wow.txt` 來刪除 `world.txt` 與 `wow.txt`。
|
||||
- 檔案名稱之間要用空白隔開。
|
||||
- 名稱的順序不重要。
|
||||
|
||||
- id: rm-world-wow
|
||||
requirements:
|
||||
- ls-no-hello
|
||||
task: 使用 `rm` 刪除 `world.txt` 與 `wow.txt`
|
||||
condition:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: world.txt
|
||||
exists: false
|
||||
- path: wow.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!我們一樣用 `ls` 指令確認一下資料夾內容。'
|
||||
hints:
|
||||
- 使用 `ls` 來查看資料夾內容。
|
||||
|
||||
- id: ls-no-world-wow
|
||||
requirements:
|
||||
- rm-world-wow
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `world.txt` 與 `wow.txt` 已經不見了。'
|
||||
- '現在我們來學習 `rmdir` 指令。`rmdir` 指令可以用來刪除資料夾。使用方式是 `rmdir [資料夾名稱]`。'
|
||||
- '現在請你使用 `rmdir` 指令刪除 `empty` 資料夾。'
|
||||
hints:
|
||||
- 使用 `rmdir empty` 來刪除 `empty` 資料夾。
|
||||
|
||||
- id: rm-empty
|
||||
requirements:
|
||||
- ls-no-world-wow
|
||||
task: 使用 `rmdir` 刪除 `empty` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir
|
||||
files:
|
||||
- path: empty
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令來查看資料夾內容。'
|
||||
|
||||
- id: ls-no-empty
|
||||
requirements:
|
||||
- rm-empty
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '你會發現 `empty` 資料夾已經不見了。'
|
||||
- '現在請你試試看 `rmdir` 指令刪除 `notes` 資料夾。'
|
||||
hints:
|
||||
- 使用 `rmdir notes` 來刪除 `notes` 資料夾。
|
||||
|
||||
- id: rm-notes-failed
|
||||
requirements:
|
||||
- ls-no-empty
|
||||
task: 使用 `rmdir` 刪除 `notes` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir notes$
|
||||
files:
|
||||
- path: notes
|
||||
type: folder
|
||||
exists: true
|
||||
responses:
|
||||
- content:
|
||||
- 指令執行失敗了!
|
||||
- '你會發現 `notes` 資料夾還在。'
|
||||
- '這是因為 `rmdir` 指令只能刪除空的資料夾。'
|
||||
- 我們先來將 `notes` 資料夾的檔案刪除再來試一次。
|
||||
- '現在請你使用 `rm` 指令刪除 `notes` 資料夾內的檔案。'
|
||||
hints:
|
||||
- 使用 `rm notes/[檔案名稱]` 來刪除 `notes` 資料夾內的檔案。
|
||||
- 你可能需要使用 `ls notes` 來查看 `notes` 資料夾內有哪些檔案。
|
||||
|
||||
- id: rm-notes-content
|
||||
requirements:
|
||||
- rm-notes-failed
|
||||
task: 使用 `rm` 刪除 `notes` 資料夾內的檔案
|
||||
condition:
|
||||
command:
|
||||
- ^rm
|
||||
files:
|
||||
- path: notes
|
||||
type: folder
|
||||
exists: true
|
||||
- path: notes/note.txt
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在我們再使用 `rmdir` 指令刪除 `notes` 資料夾。'
|
||||
hints:
|
||||
- 使用 `rmdir notes` 來刪除 `notes` 資料夾。
|
||||
exceptions:
|
||||
- condition:
|
||||
command:
|
||||
- ^rm notes/\[.*\]
|
||||
responses:
|
||||
- content:
|
||||
- '你好像理解錯了。'
|
||||
- 這邊中括號包起來 `[檔案名稱]` 是參數的意思,需要你自己填入檔案的名稱。
|
||||
- 實際打指令的時候,不要打中括號 `[]` 喔!
|
||||
- '例如,如果要刪除 `notes` 資料夾內的 `note.txt` 檔案,指令應該要是 `rm notes/note.txt`。'
|
||||
- 請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!
|
||||
- condition:
|
||||
command:
|
||||
- ^rm
|
||||
error:
|
||||
- 'No such file or directory'
|
||||
responses:
|
||||
- content:
|
||||
- 這個錯誤的意思是找不到檔案
|
||||
- '請先用 `ls notes` 確認 `notes` 資料夾內有哪些檔案,再來試試看吧!'
|
||||
|
||||
- condition:
|
||||
command:
|
||||
- ^ls notes$
|
||||
responses:
|
||||
- content:
|
||||
- '`notes` 資料夾中有著 `note.txt` 檔案。'
|
||||
- 那我們就使用 `rm notes/note.txt` 來刪除 `note.txt` 檔案吧!
|
||||
|
||||
- id: rm-notes
|
||||
requirements:
|
||||
- rm-notes-content
|
||||
task: 使用 `rmdir` 刪除 `notes` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rmdir
|
||||
files:
|
||||
- path: notes
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!`notes` 資料夾已經被刪除了。'
|
||||
- 接下來我們來刪除 `trash` 資料夾。
|
||||
- 先使用 `ls` 指令查看一下 `trash` 資料夾內容。
|
||||
hints:
|
||||
- 使用 `ls trash` 來查看 `trash` 中的檔案。
|
||||
|
||||
- id: ls-trash
|
||||
requirements:
|
||||
- rm-notes
|
||||
task: 使用 `ls` 查看 `trash` 資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls
|
||||
output:
|
||||
- garbage.txt
|
||||
responses:
|
||||
- content:
|
||||
- 天啊!`trash` 資料夾裡有好多檔案,甚至還有其他的資料夾!
|
||||
- 如果今天資料夾中有很多檔案與資料夾,我們不想一個一個刪除,該怎麼辦呢?
|
||||
- 跟 `cp` 的時候一樣,這時候我們可以加上 `-r` 的選項來刪除資料夾與它的所有檔案。
|
||||
- '`-r` 的意思是遞迴(recursive),也就是會把資料夾中的所有檔案都刪除。'
|
||||
- 我們來試試看吧!
|
||||
hints:
|
||||
- 使用 `rm -r trash` 來刪除 `trash` 資料夾。
|
||||
|
||||
- id: rm-trash
|
||||
requirements:
|
||||
- ls-trash
|
||||
task: 使用 `rm -r` 刪除 `trash` 資料夾
|
||||
condition:
|
||||
command:
|
||||
- ^rm -r
|
||||
files:
|
||||
- path: trash
|
||||
exists: false
|
||||
responses:
|
||||
- content:
|
||||
- '很好!現在請你使用 `ls` 指令查看資料夾內容。'
|
||||
hints:
|
||||
- 使用 `ls` 來查看資料夾內容。
|
||||
|
||||
- id: END
|
||||
requirements:
|
||||
- rm-trash
|
||||
task: 使用 `ls` 查看資料夾內容
|
||||
condition:
|
||||
command:
|
||||
- ^ls$
|
||||
responses:
|
||||
- content:
|
||||
- '`trash` 資料夾已經不見了。'
|
||||
- 恭喜你!你已經學會如何刪除檔案與資料夾了!
|
||||
@@ -0,0 +1 @@
|
||||
Hello
|
||||
@@ -0,0 +1 @@
|
||||
Note
|
||||
Whitespace-only changes.
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
World
|
||||
@@ -0,0 +1 @@
|
||||
Wow
|
||||
@@ -36,6 +36,7 @@
|
||||
"socket.io": "^4.7.1",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"validator": "^13.11.0",
|
||||
"winston": "^3.11.0",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { matchedData } from 'express-validator'
|
||||
import { genJWT } from '../../utils/auth.js'
|
||||
import { createUser } from '../../models/userManager.js'
|
||||
import { asyncHandler } from '../../middleware/error.js'
|
||||
import logger from '../../utils/logger.js'
|
||||
|
||||
export const issueToken = asyncHandler(async (req, res) => {
|
||||
const { user } = req
|
||||
@@ -31,7 +32,7 @@ export const register = asyncHandler(async (req, res, next) => {
|
||||
export function logout(req, res) {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
res.status(500).json({
|
||||
message: 'error logging out',
|
||||
})
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Session } from '@linux-odyssey/models'
|
||||
import { getAndStartContainer, attachContainer } from '../containers/docker.js'
|
||||
import SessionMiddleware from '../middleware/session.js'
|
||||
import { genJWT } from '../utils/auth.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
const sessions = new Map()
|
||||
|
||||
@@ -37,7 +38,7 @@ async function connectContainer(socket, next) {
|
||||
// console.log(socket.handshake)
|
||||
const { sessionId } = socket.handshake.query
|
||||
if (!(sessionId && validator.isMongoId(sessionId))) {
|
||||
console.warn('Invalid Session ID.', sessionId)
|
||||
logger.warn('Invalid Session ID.', user.username, sessionId)
|
||||
next(new Error('Invalid Session ID.'))
|
||||
return
|
||||
}
|
||||
@@ -71,8 +72,6 @@ function onConnect(socket) {
|
||||
socket.emit('terminal', chunk.toString())
|
||||
})
|
||||
|
||||
socket.on('message', console.log)
|
||||
|
||||
socket.on('terminal', function incoming(message) {
|
||||
stream.socket.write(message)
|
||||
})
|
||||
@@ -98,6 +97,6 @@ export default (server) => {
|
||||
|
||||
io.on('connection', onConnect)
|
||||
io.on('connect_error', (err) => {
|
||||
console.error(err)
|
||||
logger.error('Socket connect error: ', err)
|
||||
})
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import passwordStrategy from './passwordStrategy.js'
|
||||
import jwtStrategy from './jwtStrategy.js'
|
||||
import googleStrategy from './googleStrategy.js'
|
||||
import githubStrategy from './githubStrategy.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
passport.use(passwordStrategy)
|
||||
passport.use(jwtStrategy)
|
||||
@@ -14,7 +15,7 @@ const enabledMethods = {
|
||||
github: githubStrategy !== null,
|
||||
}
|
||||
|
||||
console.log('Enabled login methods', enabledMethods)
|
||||
logger.info('Enabled login methods', enabledMethods)
|
||||
|
||||
if (enabledMethods.google) {
|
||||
passport.use(googleStrategy)
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { getQuestDockerfiles } from '../utils/quest.js'
|
||||
import { buildQuestImage } from '../containers/docker.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
async function buildImages() {
|
||||
const quests = await getQuestDockerfiles()
|
||||
console.log(`Building ${quests.length} images`)
|
||||
logger.info(`Building ${quests.length} images`)
|
||||
return Promise.all(
|
||||
quests.map(({ id, questPath }) => {
|
||||
console.log(`Building image for ${id}`)
|
||||
logger.info(`Building image for ${id}`)
|
||||
return buildQuestImage(questPath, id)
|
||||
})
|
||||
)
|
||||
@@ -14,11 +15,11 @@ async function buildImages() {
|
||||
|
||||
buildImages()
|
||||
.then((result) => {
|
||||
console.log(result)
|
||||
console.log('Done!')
|
||||
logger.info(result)
|
||||
logger.info('Done!')
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
import connectDB from '@linux-odyssey/models'
|
||||
import { removeExpired } from '../containers/expiryChecker.js'
|
||||
import config from '../config.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
connectDB(config.db)
|
||||
.then(removeExpired)
|
||||
@@ -8,6 +9,6 @@ connectDB(config.db)
|
||||
process.exit(0)
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
import { genSecret } from '../utils/env.js'
|
||||
|
||||
function setup() {
|
||||
|
||||
@@ -25,7 +25,6 @@ const config = {
|
||||
db: get('MONGO_URL', 'mongodb://localhost:27017/odyssey-test'),
|
||||
secret: get('SECRET_KEY', ''),
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
hostPwd: get('HOST_PWD', ''),
|
||||
containerExpiry: get('EXPIRY', 1000 * 60 * 60),
|
||||
sessionMaxAge: get('SESSION_MAX_AGE', 1000 * 60 * 60 * 24 * 7),
|
||||
google: {
|
||||
@@ -45,6 +44,11 @@ const config = {
|
||||
defaultImage: get('QUEST_IMAGE', 'linuxodyssey/quest-base'),
|
||||
imagePrefix: get('DOCKER_PREFIX', 'linuxodyssey/quest-'),
|
||||
mountQuest: get('MOUNT_QUEST', ''),
|
||||
hostPwd: get('HOST_PWD', ''),
|
||||
},
|
||||
|
||||
log: {
|
||||
path: get('LOG_PATH', '../logs'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Docker from 'dockerode'
|
||||
import config, { getQuestImage } from '../config.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
const engine = new Docker()
|
||||
|
||||
@@ -23,17 +24,19 @@ export function createContainer(name, questId) {
|
||||
name,
|
||||
Image: getQuestImage(questId),
|
||||
}
|
||||
if (!config.isProduction && config.hostPwd && config.mountQuest) {
|
||||
option.HostConfig.Bind = [
|
||||
`${config.hostPwd}/quests/${config.mountQuest}/home:/home/commander`,
|
||||
`${config.hostPwd}/packages/container:/usr/local/lib/container`,
|
||||
const { hostPwd, mountQuest } = config.docker
|
||||
if (!config.isProduction && hostPwd && mountQuest) {
|
||||
logger.debug('Mounting quest folder', mountQuest)
|
||||
option.HostConfig.Binds = [
|
||||
`${hostPwd}/quests/${mountQuest}/home:/home/commander`,
|
||||
`${hostPwd}/packages/container:/usr/local/lib/container`,
|
||||
]
|
||||
}
|
||||
return engine.createContainer(option)
|
||||
}
|
||||
|
||||
export async function getAndStartContainer(id) {
|
||||
console.log(`Getting container: ${id}`)
|
||||
logger.debug(`Getting container: ${id}`)
|
||||
const container = engine.getContainer(id)
|
||||
if (!container) {
|
||||
throw new Error(`Container ${id} not found`)
|
||||
@@ -74,11 +77,22 @@ export async function deleteContainer(id) {
|
||||
try {
|
||||
await container.stop()
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
logger.warn('Failed to stop container', id)
|
||||
}
|
||||
await container.remove()
|
||||
}
|
||||
|
||||
function parseJSONOutput(data) {
|
||||
const { stream, error } = JSON.parse(data.toString())
|
||||
if (stream) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(stream)
|
||||
}
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export function buildQuestImage(questPath, questId) {
|
||||
return new Promise((resolve, reject) => {
|
||||
engine.buildImage(
|
||||
@@ -97,23 +111,27 @@ export function buildQuestImage(questPath, questId) {
|
||||
}
|
||||
response.on('data', (data) => {
|
||||
// Process the data (this could be Docker build output)
|
||||
const { stream, error } = JSON.parse(data.toString())
|
||||
if (stream) {
|
||||
console.log(stream)
|
||||
}
|
||||
if (error) {
|
||||
console.error(error)
|
||||
try {
|
||||
data
|
||||
.toString()
|
||||
.split('\n')
|
||||
.map((line) => line.trim())
|
||||
.filter((line) => line !== '')
|
||||
.forEach((line) => {
|
||||
parseJSONOutput(line)
|
||||
})
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
|
||||
response.on('end', () => {
|
||||
console.log(`Build completed for ${questId}`)
|
||||
logger.info(`Build completed for ${questId}`)
|
||||
resolve(questId)
|
||||
})
|
||||
|
||||
response.on('error', (error) => {
|
||||
console.error(`Build failed for ${questId}:`, error)
|
||||
logger.error(`Build failed for ${questId}:`, error)
|
||||
reject(error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,36 +2,47 @@ import schedule from 'node-schedule'
|
||||
import { Session } from '@linux-odyssey/models'
|
||||
import config from '../config.js'
|
||||
import { deleteContainer } from './docker.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
async function disableSession(session) {
|
||||
try {
|
||||
await deleteContainer(session.containerId)
|
||||
} catch (err) {
|
||||
// If the container doesn't exist, we can ignore the error
|
||||
if (err.statusCode !== 404) {
|
||||
throw new Error(
|
||||
`Failed to delete container ${session.containerId} for session ${session._id}`
|
||||
)
|
||||
}
|
||||
}
|
||||
if (session.status === 'active') {
|
||||
session.status = 'inactive'
|
||||
}
|
||||
session.containerId = null
|
||||
try {
|
||||
await session.save()
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to save session ${session._id}\n${err}`)
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeExpired() {
|
||||
// Find all sessions that are containerId is not null and lastActivityAt is
|
||||
// less than the current time minus the expiry time
|
||||
try {
|
||||
console.log('Checking for expired sessions')
|
||||
const sessions = await Session.find({
|
||||
containerId: { $ne: null },
|
||||
lastActivityAt: { $lt: new Date(Date.now() - config.containerExpiry) },
|
||||
})
|
||||
await Promise.all(
|
||||
sessions.map(async (session) => {
|
||||
console.log('Expiring session: ', session._id, session.containerId)
|
||||
try {
|
||||
await deleteContainer(session.containerId)
|
||||
} catch (err) {
|
||||
if (err.statusCode !== 404) {
|
||||
throw err
|
||||
}
|
||||
}
|
||||
if (session.status === 'active') {
|
||||
session.status = 'inactive'
|
||||
}
|
||||
session.containerId = null
|
||||
await session.save()
|
||||
})
|
||||
sessions.map((session) =>
|
||||
disableSession(session).catch((err) => {
|
||||
throw new Error(`Failed to expire session: ${session._id}\n${err}`)
|
||||
})
|
||||
)
|
||||
)
|
||||
console.log('Done checking for expired sessions')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
logger.error('Remove expiring failed:', err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Quest } from '@linux-odyssey/models'
|
||||
import { pushToSession } from '../api/socket.js'
|
||||
import SessionHandler from './sessionHandler.js'
|
||||
import { checkFile } from '../containers/cli.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
// 檢查 pattern 是否符合 input
|
||||
const checkMatch = (pattern, input) => {
|
||||
@@ -114,7 +115,7 @@ export default class CommandHandler extends SessionHandler {
|
||||
this.quest = await Quest.findById(this.session.quest)
|
||||
const stages = this.getStages()
|
||||
if (stages.length === 0) {
|
||||
console.error('stage not found', this.session.tasks)
|
||||
logger.warn('stage not found', this.session.tasks)
|
||||
return {}
|
||||
}
|
||||
|
||||
|
||||
+8
-8
@@ -16,21 +16,21 @@ import { globalRateLimit } from './middleware/rateLimiter.js'
|
||||
import sessionMiddleware from './middleware/session.js'
|
||||
import expiryRemovalScheduler from './containers/expiryChecker.js'
|
||||
import setupTest from './utils/setupTest.js'
|
||||
import logger from './utils/logger.js'
|
||||
|
||||
async function main() {
|
||||
if (!config.secret) {
|
||||
console.warn(
|
||||
'No SECRET_KEY found in .env! To set up a persistent key, please run the setup script:'
|
||||
logger.error(
|
||||
'No SECRET_KEY found in .env! To set up a persistent key, please run `yarn setup`:'
|
||||
)
|
||||
console.warn('yarn setup')
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
try {
|
||||
await connectDB(config.db)
|
||||
console.log('Connected to MongoDB')
|
||||
logger.info('Connected to MongoDB')
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ async function main() {
|
||||
'uniquelocal',
|
||||
...config.trustedProxies,
|
||||
])
|
||||
console.log('Trusted proxies:', config.trustedProxies)
|
||||
logger.info('Trusted proxies:', config.trustedProxies)
|
||||
app.use(globalRateLimit)
|
||||
app.use(sessionMiddleware)
|
||||
app.use(passport.session())
|
||||
@@ -74,8 +74,8 @@ async function main() {
|
||||
app.use(errorHandler)
|
||||
|
||||
server.listen(config.port, config.host, () => {
|
||||
console.log(`Server listening at ${config.baseUrl}`)
|
||||
logger.info(`Server listening at ${config.baseUrl}`)
|
||||
})
|
||||
}
|
||||
|
||||
main().catch((err) => console.log(err))
|
||||
main().catch((err) => logger.error(err))
|
||||
@@ -1,3 +1,5 @@
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
export function asyncHandler(fn) {
|
||||
return (req, res, next) => {
|
||||
fn(req, res, next).catch(next)
|
||||
@@ -6,6 +8,6 @@ export function asyncHandler(fn) {
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export default function errorHandler(err, req, res, next) {
|
||||
console.error(err.stack) // Log error stack trace to the console
|
||||
logger.error(err) // Log error stack trace to the console
|
||||
res.status(500).send({ message: 'Something broke!' }) // Send error response
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Session, Quest, UserProfile } from '@linux-odyssey/models'
|
||||
import { createContainer, deleteContainer } from '../containers/docker.js'
|
||||
import SessionHandler from '../game/sessionHandler.js'
|
||||
import logger from '../utils/logger.js'
|
||||
|
||||
async function deactivateSessions(user, quest) {
|
||||
const sessions = await Session.find({
|
||||
@@ -11,7 +12,7 @@ async function deactivateSessions(user, quest) {
|
||||
return Promise.all(
|
||||
sessions.map((session) => {
|
||||
return deleteContainer(session.containerId)
|
||||
.catch(console.error)
|
||||
.catch((err) => logger.error('Failed to delete container', err))
|
||||
.finally(() => {
|
||||
session.status = 'inactive'
|
||||
session.containerId = null
|
||||
@@ -29,7 +30,7 @@ export async function createNewSession(user, questId) {
|
||||
|
||||
// deactivate all active sessions
|
||||
deactivateSessions(user._id, quest._id).catch((err) => {
|
||||
console.error(err)
|
||||
logger.error(err)
|
||||
})
|
||||
|
||||
const container = await createContainer(
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import winston from 'winston'
|
||||
import config from '../config.js'
|
||||
|
||||
const logger = winston.createLogger({
|
||||
level: 'info',
|
||||
format: winston.format.json(),
|
||||
defaultMeta: { service: 'backend', timestamp: new Date().toISOString() },
|
||||
transports: [
|
||||
//
|
||||
// - Write all logs with importance level of `error` or less to `error.log`
|
||||
// - Write all logs with importance level of `info` or less to `combined.log`
|
||||
//
|
||||
new winston.transports.File({
|
||||
dirname: config.log.path,
|
||||
filename: 'error.log',
|
||||
level: 'error',
|
||||
}),
|
||||
new winston.transports.File({
|
||||
dirname: config.log.path,
|
||||
filename: 'combined.log',
|
||||
}),
|
||||
],
|
||||
})
|
||||
|
||||
//
|
||||
// If we're not in production then log to the `console` with the format:
|
||||
// `${info.level}: ${info.message} JSON.stringify({ ...rest }) `
|
||||
//
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
logger.add(
|
||||
new winston.transports.Console({
|
||||
format: winston.format.simple(),
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export default logger
|
||||
@@ -3,6 +3,7 @@ import path from 'path'
|
||||
import yaml from 'yaml'
|
||||
|
||||
import { Quest } from '@linux-odyssey/models'
|
||||
import logger from './logger.js'
|
||||
|
||||
const questDirectory = path.join(process.cwd(), '..', 'quests')
|
||||
|
||||
@@ -45,7 +46,7 @@ export async function loadAndUpdateQuests() {
|
||||
})
|
||||
await quest.save()
|
||||
} catch (error) {
|
||||
console.error(`Error parsing quest ${id}:`, error.message)
|
||||
logger.error(`Error parsing quest ${id}:`, error)
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { User } from '@linux-odyssey/models'
|
||||
import config from '../config.js'
|
||||
import { createUser } from '../models/userManager.js'
|
||||
import logger from './logger.js'
|
||||
|
||||
export default async function setupTest() {
|
||||
console.log('Setting up test account')
|
||||
logger.info('Setting up test account')
|
||||
if (!(config.testing.username && config.testing.password)) {
|
||||
throw new Error(
|
||||
'No testing account found! Please set TESTING_USERNAME and TESTING_PASSWORD in .env'
|
||||
@@ -13,10 +14,10 @@ export default async function setupTest() {
|
||||
const email = `${username}@example.com`
|
||||
|
||||
if (await User.exists({ username })) {
|
||||
console.log('Testing account already exists')
|
||||
logger.info('Testing account already exists')
|
||||
return
|
||||
}
|
||||
|
||||
await createUser(username, email, { password })
|
||||
console.log('Created testing account')
|
||||
logger.info('Created testing account')
|
||||
}
|
||||
@@ -24,6 +24,7 @@ module.exports = defineConfig({
|
||||
env: {
|
||||
defaultAccount: getOrFail('TESTING_USERNAME'),
|
||||
defaultPassword: getOrFail('TESTING_PASSWORD'),
|
||||
isCHVersion: true,
|
||||
// google_username: process.env.GOOGLE_USERNAME,
|
||||
// google_password: process.env.GOOGLE_PASSWORD,
|
||||
// google_client_id: process.env.GOOGLE_CLIENT_ID,
|
||||
|
||||
@@ -3,9 +3,7 @@ export default function checkLoginUI() {
|
||||
cy.get('h1.text-text-primary')
|
||||
.should('contain', 'Linux Odyssey')
|
||||
.and('be.visible')
|
||||
cy.get('#password')
|
||||
.invoke('attr', 'placeholder')
|
||||
.should('contain', 'Password')
|
||||
cy.CheckPlaceholder('#password', '密碼', 'Password')
|
||||
}
|
||||
describe('example helloworld app', () => {
|
||||
describe('Login tests', () => {
|
||||
@@ -14,51 +12,65 @@ describe('example helloworld app', () => {
|
||||
})
|
||||
it('Check Login Page Elements-Register', () => {
|
||||
checkLoginUI()
|
||||
cy.get('#username')
|
||||
.invoke('attr', 'placeholder')
|
||||
.should('contain', 'Username')
|
||||
cy.CheckPlaceholder('#username', '帳號名稱', 'Username')
|
||||
cy.get('label.text-text-secondary').should('be.visible') // check the rules is displayed
|
||||
cy.get('#email').invoke('attr', 'placeholder').should('contain', 'Email')
|
||||
cy.get('span').contains('Already have an account?').should('be.visible')
|
||||
cy.get('a.text-text-primary')
|
||||
.should('contain', 'Log in')
|
||||
.and('be.visible')
|
||||
cy.CheckPlaceholder('#email', '電子郵件', 'Email')
|
||||
cy.CheckTextElement(
|
||||
'#CheckRegistered',
|
||||
'已經有帳號了嗎?',
|
||||
'Already have an account?'
|
||||
)
|
||||
cy.CheckTextElement('#SwitchtoLogin', '登入', 'Log in')
|
||||
})
|
||||
it('Check Login Page Elements-Login', () => {
|
||||
cy.get('a.text-text-primary')
|
||||
.should('contain', 'Log in')
|
||||
.and('be.visible')
|
||||
.click()
|
||||
cy.get('#SwitchtoLogin').click()
|
||||
checkLoginUI()
|
||||
cy.get('#username')
|
||||
.invoke('attr', 'placeholder')
|
||||
.should('contain', 'Email / Username')
|
||||
cy.findByRole('button', { name: 'Log In' }).should('be.visible')
|
||||
cy.get('span').contains("Don't have an account?").should('be.visible')
|
||||
cy.get('a.text-text-primary')
|
||||
.should('contain', 'Sign up')
|
||||
.and('be.visible')
|
||||
cy.CheckPlaceholder(
|
||||
'#username',
|
||||
'電子郵件 / 帳號名稱',
|
||||
'Email / Username'
|
||||
)
|
||||
cy.get('#LogInOrSignUp').should('be.visible')
|
||||
cy.CheckTextElement(
|
||||
'#CheckRegistered',
|
||||
'還沒有帳號嗎?',
|
||||
"Don't have an account?"
|
||||
)
|
||||
cy.CheckTextElement('#SwitchtoRegister', '註冊', 'Sign up')
|
||||
})
|
||||
it('Check social account UI', () => {
|
||||
cy.get('p').contains('or').should('be.visible')
|
||||
cy.findByRole('link', { name: 'Continue with Google' }).should(
|
||||
'be.visible'
|
||||
cy.CheckTextElement(
|
||||
'#GoogleLogin',
|
||||
'以 Google 繼續',
|
||||
'Continue with Google'
|
||||
)
|
||||
cy.findByRole('link', { name: 'Continue with GitHub' }).should(
|
||||
'be.visible'
|
||||
cy.CheckTextElement('#or', '或', 'or')
|
||||
cy.CheckTextElement(
|
||||
'#GitHubLogin',
|
||||
'以 GitHub 繼續',
|
||||
'Continue with GitHub'
|
||||
)
|
||||
})
|
||||
it('Check login fail', () => {
|
||||
cy.LoginWithPassword('dddd', '123456')
|
||||
cy.get('p').contains('Wrong username or password.').should('be.visible')
|
||||
cy.CheckTextElement(
|
||||
'#ErrorDisplay',
|
||||
'錯誤的帳號名稱或密碼。',
|
||||
'Wrong username or password.'
|
||||
)
|
||||
cy.LoginWithPassword(`ddd\``, '123456')
|
||||
cy.get('p').contains('Invalid username or password.').should('be.visible')
|
||||
cy.CheckTextElement(
|
||||
'#ErrorDisplay',
|
||||
'無效的帳號名稱或密碼。',
|
||||
'Invalid username or password.'
|
||||
)
|
||||
})
|
||||
it('Login-Password Already Register', () => {
|
||||
cy.LoginWithPassword(
|
||||
Cypress.env('defaultAccount'),
|
||||
Cypress.env('defaultPassword')
|
||||
)
|
||||
cy.url().should('include', '/map')
|
||||
})
|
||||
})
|
||||
describe('Game Start Page UI', () => {
|
||||
@@ -66,33 +78,35 @@ describe('example helloworld app', () => {
|
||||
cy.PrepareForGame()
|
||||
})
|
||||
it('Check Header', () => {
|
||||
cy.findByRole('button', { name: 'Hello, Linux World!' }).should(
|
||||
'be.visible'
|
||||
)
|
||||
cy.get('#HeaderText')
|
||||
.should('contain', 'Hello, Linux World!')
|
||||
.and('be.visible')
|
||||
cy.findByRole('link', { name: 'Bug Report' }).should('be.visible')
|
||||
cy.findByRole('button', { name: 'Sign Out' }).should('be.visible')
|
||||
})
|
||||
it('Check QuestInfo', () => {
|
||||
cy.get('#topic').should('contain', 'Hello, Linux World!')
|
||||
cy.get('#quest').find('p.text-text').should('have.length', 3)
|
||||
cy.getQuestInfo('Tasks:').should('be.visible')
|
||||
cy.CheckTextElement('#tasks', '任務', 'Tasks:')
|
||||
cy.getQuestInfo('輸入 `echo help` 來朗誦咒語').should('be.visible')
|
||||
})
|
||||
it('Check Command Cheat Sheet', () => {
|
||||
cy.get('#cmdlist').within(($cmdlist) => {
|
||||
cy.get($cmdlist).should('be.visible')
|
||||
cy.get('svg[data-icon="list"]').should('be.visible')
|
||||
cy.get('h1').should('contain', 'Command Cheatsheets').and('be.visible')
|
||||
cy.get('#cheatsheets')
|
||||
.should('contain', '--Command List--')
|
||||
.and('be.visible')
|
||||
cy.CheckTextElement('#CommandBook', '指令之書', 'Command Cheatsheets')
|
||||
cy.CheckTextElement(
|
||||
'#cheatsheets',
|
||||
'-- --指令列表-- --',
|
||||
'--Command List--'
|
||||
)
|
||||
})
|
||||
})
|
||||
it('Check Terminal', () => {
|
||||
cy.get('#terminal', { timeout: 20000 }).within(($terminal) => {
|
||||
cy.get($terminal).should('be.visible')
|
||||
cy.get('svg[data-icon="terminal"]').should('be.visible')
|
||||
cy.findByRole('button').should('contain', 'Terminal').and('be.visible')
|
||||
cy.CheckTextElement('#Terminal', '終端機', 'Terminal')
|
||||
cy.get('@Terminaltextbox')
|
||||
.should('be.visible')
|
||||
.and('contain', 'commander:~ $')
|
||||
@@ -102,19 +116,17 @@ describe('example helloworld app', () => {
|
||||
cy.get('#hint', { timeout: 20000 }).within(($hint) => {
|
||||
cy.get($hint).should('be.visible')
|
||||
cy.get('svg[data-icon="lightbulb"]').should('be.visible')
|
||||
cy.get('h1').should('contain', 'Hint').and('be.visible')
|
||||
})
|
||||
cy.CheckTextElement('#hint', '提示', 'Hint')
|
||||
})
|
||||
it('Check File TreeChart', () => {
|
||||
cy.get('#tree').should('be.visible')
|
||||
cy.get('#tree').get('a').should('contain', '???').and('be.visible')
|
||||
})
|
||||
it('Check Fucnctional Buttons', () => {
|
||||
cy.findByRole('button', { name: 'Solution' }, { timeout: 30000 }).should(
|
||||
'be.visible'
|
||||
)
|
||||
cy.findByRole('button', { name: 'Reset' }).should('be.visible')
|
||||
cy.findByRole('button', { name: 'Continue' }).should('be.visible')
|
||||
cy.CheckTextElement('#solution', '解答', 'Solution')
|
||||
cy.CheckTextElement('#reset', '重來', 'Reset')
|
||||
cy.CheckTextElement('#continue', '繼續', 'Continue')
|
||||
})
|
||||
})
|
||||
describe('Game Play', () => {
|
||||
@@ -128,7 +140,7 @@ describe('example helloworld app', () => {
|
||||
.should('contain', 'zsh: command not found: 12345')
|
||||
.and('contain', '12345')
|
||||
})
|
||||
it.only('Complete the Game(relating UI)', () => {
|
||||
it('Complete the Game(relating UI)', () => {
|
||||
// get answersheet
|
||||
cy.get('@answers').then((answers) => {
|
||||
const answerarr = answers.split('\n')
|
||||
@@ -172,12 +184,8 @@ describe('example helloworld app', () => {
|
||||
cy.checkPending()
|
||||
cy.waitUntilActive()
|
||||
cy.getQuestInfo('✓ 解除封印').should('be.visible')
|
||||
cy.findByRole(
|
||||
'heading',
|
||||
{ name: 'Quest Completed!' },
|
||||
{ timeout: 20000 }
|
||||
).should('be.visible')
|
||||
// Check survey dialog pop up
|
||||
cy.CheckTextElement('#QuestCompleted', '關卡完成!', 'Quest Completed!')
|
||||
cy.get('div[class="modal"]').find('p').should('be.visible')
|
||||
cy.findByRole('link', { name: '填寫問卷' }).should('be.visible')
|
||||
})
|
||||
|
||||
@@ -5,8 +5,7 @@ Cypress.Commands.add('LoginWithPassword', (username, password) => {
|
||||
cy.visit('/login')
|
||||
cy.get('#username').type(username)
|
||||
cy.get('#password').type(password)
|
||||
cy.findByRole('button', { name: 'Log In' }).click()
|
||||
cy.url().should('include', '/game')
|
||||
cy.get('#LogInOrSignUp').click()
|
||||
})
|
||||
Cypress.Commands.add('PrepareForGame', () => {
|
||||
cy.visit('/')
|
||||
@@ -14,11 +13,12 @@ Cypress.Commands.add('PrepareForGame', () => {
|
||||
Cypress.env('defaultAccount'),
|
||||
Cypress.env('defaultPassword')
|
||||
)
|
||||
cy.url().should('include', '/map')
|
||||
cy.visit('/game/helloworld')
|
||||
cy.get('.xterm-screen', { timeout: 10000 })
|
||||
.as('Terminaltextbox')
|
||||
.should('be.visible')
|
||||
cy.findByRole('button', { name: 'Reset' }).click()
|
||||
cy.CheckTextElement('#reset', '重來', 'Reset').click()
|
||||
cy.typeInCommand('clear{enter}')
|
||||
cy.get('@Terminaltextbox', { timeout: 150000 }).should(
|
||||
'contain',
|
||||
@@ -66,3 +66,23 @@ Cypress.Commands.add('checkPending', () => {
|
||||
Cypress.Commands.add('Complete the Stage (only command)', () => {
|
||||
cy.log('')
|
||||
})
|
||||
Cypress.Commands.add('CheckTextElement', (id, chText, enText) => {
|
||||
cy.get(id).within(($element) => {
|
||||
if (Cypress.env('isCHVersion')) {
|
||||
cy.get($element).should('contain', chText).and('be.visible')
|
||||
} else {
|
||||
cy.get($element).should('contain', enText).and('be.visible')
|
||||
}
|
||||
})
|
||||
})
|
||||
Cypress.Commands.add('CheckPlaceholder', (id, chText, enText) => {
|
||||
cy.get(id)
|
||||
.invoke('attr', 'placeholder')
|
||||
.then(($placeHolder) => {
|
||||
if (Cypress.env('isCHVersion')) {
|
||||
expect($placeHolder).eq(chText)
|
||||
} else {
|
||||
expect($placeHolder).eq(enText)
|
||||
}
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -9,8 +9,8 @@
|
||||
"test:open": "cypress open"
|
||||
},
|
||||
"dependencies": {
|
||||
"cypress": "^13.5.0",
|
||||
"@testing-library/cypress": "^10.0.1",
|
||||
"cypress": "^13.5.1",
|
||||
"dotenv": "^16.3.1"
|
||||
}
|
||||
}
|
||||
@@ -27,7 +27,15 @@
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.22.5"
|
||||
|
||||
"@babel/code-frame@^7.10.4", "@babel/code-frame@^7.22.13":
|
||||
"@babel/code-frame@^7.10.4":
|
||||
version "7.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.23.4.tgz#03ae5af150be94392cb5c7ccd97db5a19a5da6aa"
|
||||
integrity sha512-r1IONyb6Ia+jYR2vvIDhdWdlTGhqbBoFqLTQidzZ4kepUFH15ejXvFHxCVbtl7BOXIudsIubf4E81xeA3h3IXA==
|
||||
dependencies:
|
||||
"@babel/highlight" "^7.23.4"
|
||||
chalk "^2.4.2"
|
||||
|
||||
"@babel/code-frame@^7.22.13":
|
||||
version "7.22.13"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
|
||||
integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
|
||||
@@ -201,6 +209,15 @@
|
||||
chalk "^2.0.0"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/highlight@^7.23.4":
|
||||
version "7.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.23.4.tgz#edaadf4d8232e1a961432db785091207ead0621b"
|
||||
integrity sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==
|
||||
dependencies:
|
||||
"@babel/helper-validator-identifier" "^7.22.20"
|
||||
chalk "^2.4.2"
|
||||
js-tokens "^4.0.0"
|
||||
|
||||
"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.20.15", "@babel/parser@^7.20.7", "@babel/parser@^7.21.3", "@babel/parser@^7.22.5", "@babel/parser@^7.22.7":
|
||||
version "7.22.7"
|
||||
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.22.7.tgz#df8cf085ce92ddbdbf668a7f186ce848c9036cae"
|
||||
@@ -315,9 +332,9 @@
|
||||
"@babel/helper-plugin-utils" "^7.22.5"
|
||||
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.14.6":
|
||||
version "7.23.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
|
||||
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
|
||||
version "7.23.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.4.tgz#36fa1d2b36db873d25ec631dcc4923fdc1cf2e2e"
|
||||
integrity sha512-2Yv65nlWnWlSpe3fXEyX5i7fx5kIKo4Qbcj+hMO0odwaneFjfXw5fdum+4yL20O0QiaHpia0cYQ9xpNMqrBwHg==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
@@ -397,6 +414,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9"
|
||||
integrity sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==
|
||||
|
||||
"@colors/colors@1.6.0", "@colors/colors@^1.6.0":
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.6.0.tgz#ec6cd237440700bc23ca23087f513c75508958b0"
|
||||
integrity sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==
|
||||
|
||||
"@cypress/request@^3.0.0":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@cypress/request/-/request-3.0.1.tgz#72d7d5425236a2413bd3d8bb66d02d9dc3168960"
|
||||
@@ -429,6 +451,15 @@
|
||||
debug "^3.1.0"
|
||||
lodash.once "^4.1.1"
|
||||
|
||||
"@dabh/diagnostics@^2.0.2":
|
||||
version "2.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@dabh/diagnostics/-/diagnostics-2.0.3.tgz#7f7e97ee9a725dffc7808d93668cc984e1dc477a"
|
||||
integrity sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==
|
||||
dependencies:
|
||||
colorspace "1.1.x"
|
||||
enabled "2.0.x"
|
||||
kuler "^2.0.0"
|
||||
|
||||
"@esbuild/android-arm64@0.18.12":
|
||||
version "0.18.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.18.12.tgz#d2b7408f2d2fe6ad93877d90ebb18e0d2648828c"
|
||||
@@ -1078,9 +1109,11 @@
|
||||
integrity sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==
|
||||
|
||||
"@types/node@^18.17.5":
|
||||
version "18.17.19"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.17.19.tgz#80c9b8a89d3648d9e6098f4a7184e03833fee3c5"
|
||||
integrity sha512-+pMhShR3Or5GR0/sp4Da7FnhVmTalWm81M6MkEldbwjETSaPalw138Z4KdpQaistvqQxLB7Cy4xwYdxpbSOs9Q==
|
||||
version "18.18.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-18.18.11.tgz#d43f6b92671eacfdd1fedb7a897388fb8880323f"
|
||||
integrity sha512-c1vku6qnTeujJneYH94/4aq73XrVcsJe35UPyAsSok1ijiKrkRzK+AxQPSpNMUnC03roWBBwJx/9I8V7lQoxmA==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/prettier@^2.1.5":
|
||||
version "2.7.3"
|
||||
@@ -1093,15 +1126,20 @@
|
||||
integrity sha512-0kSuKjAS0TrGLJ0M/+8MaFkGsQhZpB6pxOmvS3K8FYI72K//YmdfoW9X2qPsAKh1mkwxGD5zib9s1FIFed6E8g==
|
||||
|
||||
"@types/sizzle@^2.3.2":
|
||||
version "2.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.3.tgz#ff5e2f1902969d305225a047c8a0fd5c915cebef"
|
||||
integrity sha512-JYM8x9EGF163bEyhdJBpR2QX1R5naCJHC8ucJylJ3w9/CVBaskdQ8WqBf8MmQrd1kRvp/a4TS8HJ+bxzR7ZJYQ==
|
||||
version "2.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/sizzle/-/sizzle-2.3.7.tgz#ab5744eceb30daea3e0c6a9c2d9e1a3817ef7656"
|
||||
integrity sha512-sU5MNKvSFdbT97qLSabU6jxceY9BfYQe9uF+b8E1rdTecpq9ROkkKoiHlblUlhgfaz9bdCByWjAI40Y3mHf1iQ==
|
||||
|
||||
"@types/stack-utils@^2.0.0":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c"
|
||||
integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==
|
||||
|
||||
"@types/triple-beam@^1.3.2":
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/triple-beam/-/triple-beam-1.3.5.tgz#74fef9ffbaa198eb8b588be029f38b00299caa2c"
|
||||
integrity sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==
|
||||
|
||||
"@types/webidl-conversions@*":
|
||||
version "7.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/webidl-conversions/-/webidl-conversions-7.0.0.tgz#2b8e60e33906459219aa587e9d1a612ae994cfe7"
|
||||
@@ -1128,9 +1166,9 @@
|
||||
"@types/yargs-parser" "*"
|
||||
|
||||
"@types/yauzl@^2.9.1":
|
||||
version "2.10.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.0.tgz#b3248295276cf8c6f153ebe6a9aba0c988cb2599"
|
||||
integrity sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==
|
||||
version "2.10.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/yauzl/-/yauzl-2.10.3.tgz#e9b2808b4f109504a03cda958259876f61017999"
|
||||
integrity sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
@@ -1463,9 +1501,14 @@ astral-regex@^2.0.0:
|
||||
integrity sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==
|
||||
|
||||
async@^3.2.0:
|
||||
version "3.2.4"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c"
|
||||
integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
|
||||
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
|
||||
|
||||
async@^3.2.3:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.yarnpkg.com/async/-/async-3.2.5.tgz#ebd52a8fdaf7a2289a24df399f8d8485c8a46b66"
|
||||
integrity sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==
|
||||
|
||||
asynckit@^0.4.0:
|
||||
version "0.4.0"
|
||||
@@ -1930,7 +1973,7 @@ collect-v8-coverage@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz#c0b29bcd33bcd0779a1344c2136051e6afd3d9e9"
|
||||
integrity sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==
|
||||
|
||||
color-convert@^1.9.0:
|
||||
color-convert@^1.9.0, color-convert@^1.9.3:
|
||||
version "1.9.3"
|
||||
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
|
||||
integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==
|
||||
@@ -1949,21 +1992,45 @@ color-name@1.1.3:
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25"
|
||||
integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==
|
||||
|
||||
color-name@~1.1.4:
|
||||
color-name@^1.0.0, color-name@~1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
|
||||
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
|
||||
|
||||
color-string@^1.6.0:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
|
||||
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
|
||||
dependencies:
|
||||
color-name "^1.0.0"
|
||||
simple-swizzle "^0.2.2"
|
||||
|
||||
color-support@^1.1.2:
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
|
||||
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
|
||||
|
||||
color@^3.1.3:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/color/-/color-3.2.1.tgz#3544dc198caf4490c3ecc9a790b54fe9ff45e164"
|
||||
integrity sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==
|
||||
dependencies:
|
||||
color-convert "^1.9.3"
|
||||
color-string "^1.6.0"
|
||||
|
||||
colorette@^2.0.16:
|
||||
version "2.0.20"
|
||||
resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.20.tgz#9eb793e6833067f7235902fcd3b09917a000a95a"
|
||||
integrity sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==
|
||||
|
||||
colorspace@1.1.x:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/colorspace/-/colorspace-1.1.4.tgz#8d442d1186152f60453bf8070cd66eb364e59243"
|
||||
integrity sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==
|
||||
dependencies:
|
||||
color "^3.1.3"
|
||||
text-hex "1.0.x"
|
||||
|
||||
combined-stream@^1.0.6, combined-stream@^1.0.8, combined-stream@~1.0.6:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
|
||||
@@ -2098,10 +2165,10 @@ csstype@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
cypress@^13.5.0:
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.5.0.tgz#8c149074186130972f08b2cdce6ded41f014bacd"
|
||||
integrity sha512-oh6U7h9w8wwHfzNDJQ6wVcAeXu31DlIYlNOBvfd6U4CcB8oe4akawQmH+QJVOMZlM42eBoCne015+svVqdwdRQ==
|
||||
cypress@^13.5.1:
|
||||
version "13.5.1"
|
||||
resolved "https://registry.yarnpkg.com/cypress/-/cypress-13.5.1.tgz#8b19bf0b9f31ea43f78980b2479bd3f25197d5cc"
|
||||
integrity sha512-yqLViT0D/lPI8Kkm7ciF/x/DCK/H/DnogdGyiTnQgX4OVR2aM30PtK+kvklTOD1u3TuItiD9wUQAF8EYWtyZug==
|
||||
dependencies:
|
||||
"@cypress/request" "^3.0.0"
|
||||
"@cypress/xvfb" "^1.2.4"
|
||||
@@ -2400,6 +2467,11 @@ emoji-regex@^9.2.2:
|
||||
resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72"
|
||||
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
|
||||
|
||||
enabled@2.0.x:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/enabled/-/enabled-2.0.0.tgz#f9dd92ec2d6f4bbc0d5d1e64e21d61cd4665e7c2"
|
||||
integrity sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
@@ -3023,6 +3095,11 @@ fd-slicer@~1.1.0:
|
||||
dependencies:
|
||||
pend "~1.2.0"
|
||||
|
||||
fecha@^4.2.0:
|
||||
version "4.2.3"
|
||||
resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.3.tgz#4d9ccdbc61e8629b259fdca67e65891448d569fd"
|
||||
integrity sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==
|
||||
|
||||
figures@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af"
|
||||
@@ -3086,6 +3163,11 @@ flatted@^3.1.0:
|
||||
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787"
|
||||
integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==
|
||||
|
||||
fn.name@1.x.x:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
follow-redirects@^1.15.0:
|
||||
version "1.15.2"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13"
|
||||
@@ -3618,6 +3700,11 @@ is-arrayish@^0.2.1:
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d"
|
||||
integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==
|
||||
|
||||
is-arrayish@^0.3.1:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.3.2.tgz#4574a2ae56f7ab206896fb431eaeed066fdf8f03"
|
||||
integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
|
||||
|
||||
is-bigint@^1.0.1:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3"
|
||||
@@ -4408,6 +4495,11 @@ kruptein@^3.0.0:
|
||||
dependencies:
|
||||
asn1.js "^5.4.1"
|
||||
|
||||
kuler@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/kuler/-/kuler-2.0.0.tgz#e2c570a3800388fb44407e851531c1d670b061b3"
|
||||
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
|
||||
|
||||
lazy-ass@^1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/lazy-ass/-/lazy-ass-1.6.0.tgz#7999655e8646c17f089fdd187d150d3324d54513"
|
||||
@@ -4527,6 +4619,18 @@ log-update@^4.0.0:
|
||||
slice-ansi "^4.0.0"
|
||||
wrap-ansi "^6.2.0"
|
||||
|
||||
logform@^2.3.2, logform@^2.4.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.yarnpkg.com/logform/-/logform-2.6.0.tgz#8c82a983f05d6eaeb2d75e3decae7a768b2bf9b5"
|
||||
integrity sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==
|
||||
dependencies:
|
||||
"@colors/colors" "1.6.0"
|
||||
"@types/triple-beam" "^1.3.2"
|
||||
fecha "^4.2.0"
|
||||
ms "^2.1.1"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
triple-beam "^1.3.0"
|
||||
|
||||
long-timeout@0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/long-timeout/-/long-timeout-0.1.1.tgz#9721d788b47e0bcb5a24c2e2bee1a0da55dab514"
|
||||
@@ -5005,6 +5109,13 @@ once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
dependencies:
|
||||
wrappy "1"
|
||||
|
||||
one-time@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/one-time/-/one-time-1.0.0.tgz#e06bc174aed214ed58edede573b433bbf827cb45"
|
||||
integrity sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==
|
||||
dependencies:
|
||||
fn.name "1.x.x"
|
||||
|
||||
onetime@^5.1.0, onetime@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e"
|
||||
@@ -5622,6 +5733,11 @@ safe-regex-test@^1.0.0:
|
||||
get-intrinsic "^1.1.3"
|
||||
is-regex "^1.1.4"
|
||||
|
||||
safe-stable-stringify@^2.3.1:
|
||||
version "2.4.3"
|
||||
resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886"
|
||||
integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==
|
||||
|
||||
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
@@ -5740,6 +5856,13 @@ signal-exit@^4.0.1:
|
||||
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04"
|
||||
integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
|
||||
|
||||
simple-swizzle@^0.2.2:
|
||||
version "0.2.2"
|
||||
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"
|
||||
integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
|
||||
dependencies:
|
||||
is-arrayish "^0.3.1"
|
||||
|
||||
simple-update-notifier@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz#d70b92bdab7d6d90dfd73931195a30b6e3d7cebb"
|
||||
@@ -5878,9 +6001,9 @@ ssh2@^1.11.0:
|
||||
nan "^2.17.0"
|
||||
|
||||
sshpk@^1.14.1:
|
||||
version "1.17.0"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.17.0.tgz#578082d92d4fe612b13007496e543fa0fbcbe4c5"
|
||||
integrity sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==
|
||||
version "1.18.0"
|
||||
resolved "https://registry.yarnpkg.com/sshpk/-/sshpk-1.18.0.tgz#1663e55cddf4d688b86a46b77f0d5fe363aba028"
|
||||
integrity sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==
|
||||
dependencies:
|
||||
asn1 "~0.2.3"
|
||||
assert-plus "^1.0.0"
|
||||
@@ -5892,6 +6015,11 @@ sshpk@^1.14.1:
|
||||
safer-buffer "^2.0.2"
|
||||
tweetnacl "~0.14.0"
|
||||
|
||||
stack-trace@0.0.x:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/stack-trace/-/stack-trace-0.0.10.tgz#547c70b347e8d32b4e108ea1a2a159e5fdde19c0"
|
||||
integrity sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==
|
||||
|
||||
stack-utils@^2.0.3:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f"
|
||||
@@ -6139,6 +6267,11 @@ test-exclude@^6.0.0:
|
||||
glob "^7.1.4"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
text-hex@1.0.x:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/text-hex/-/text-hex-1.0.0.tgz#69dc9c1b17446ee79a92bf5b884bb4b9127506f5"
|
||||
integrity sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==
|
||||
|
||||
text-table@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
|
||||
@@ -6159,9 +6292,9 @@ thenify-all@^1.0.0:
|
||||
any-promise "^1.0.0"
|
||||
|
||||
throttleit@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c"
|
||||
integrity sha512-rkTVqu6IjfQ/6+uNuuc3sZek4CEYxTJom3IktzgdSxcZqdARuebbA/f4QmAxMQIxqq9ZLEUkSYqvuk1I6VKq4g==
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/throttleit/-/throttleit-1.0.1.tgz#304ec51631c3b770c65c6c6f76938b384000f4d5"
|
||||
integrity sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==
|
||||
|
||||
through@^2.3.8:
|
||||
version "2.3.8"
|
||||
@@ -6231,6 +6364,11 @@ tr46@~0.0.3:
|
||||
resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a"
|
||||
integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
|
||||
|
||||
triple-beam@^1.3.0:
|
||||
version "1.4.1"
|
||||
resolved "https://registry.yarnpkg.com/triple-beam/-/triple-beam-1.4.1.tgz#6fde70271dc6e5d73ca0c3b24e2d92afb7441984"
|
||||
integrity sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==
|
||||
|
||||
ts-interface-checker@^0.1.9:
|
||||
version "0.1.13"
|
||||
resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699"
|
||||
@@ -6355,15 +6493,20 @@ undefsafe@^2.0.5:
|
||||
resolved "https://registry.yarnpkg.com/undefsafe/-/undefsafe-2.0.5.tgz#38733b9327bdcd226db889fb723a6efd162e6e2c"
|
||||
integrity sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==
|
||||
|
||||
undici-types@~5.26.4:
|
||||
version "5.26.5"
|
||||
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617"
|
||||
integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==
|
||||
|
||||
universalify@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0"
|
||||
integrity sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==
|
||||
|
||||
universalify@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d"
|
||||
integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
@@ -6579,6 +6722,32 @@ wide-align@^1.1.2:
|
||||
dependencies:
|
||||
string-width "^1.0.2 || 2 || 3 || 4"
|
||||
|
||||
winston-transport@^4.5.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/winston-transport/-/winston-transport-4.6.0.tgz#f1c1a665ad1b366df72199e27892721832a19e1b"
|
||||
integrity sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==
|
||||
dependencies:
|
||||
logform "^2.3.2"
|
||||
readable-stream "^3.6.0"
|
||||
triple-beam "^1.3.0"
|
||||
|
||||
winston@^3.11.0:
|
||||
version "3.11.0"
|
||||
resolved "https://registry.yarnpkg.com/winston/-/winston-3.11.0.tgz#2d50b0a695a2758bb1c95279f0a88e858163ed91"
|
||||
integrity sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==
|
||||
dependencies:
|
||||
"@colors/colors" "^1.6.0"
|
||||
"@dabh/diagnostics" "^2.0.2"
|
||||
async "^3.2.3"
|
||||
is-stream "^2.0.0"
|
||||
logform "^2.4.0"
|
||||
one-time "^1.0.0"
|
||||
readable-stream "^3.4.0"
|
||||
safe-stable-stringify "^2.3.1"
|
||||
stack-trace "0.0.x"
|
||||
triple-beam "^1.3.0"
|
||||
winston-transport "^4.5.0"
|
||||
|
||||
wordwrap@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
|
||||
|
||||
Reference in new issue
Block a user