Compare commits

...
238 Commits
Author SHA1 Message Date
Justin Lin f1038e40f4 fix: analytics start path 2026-02-27 20:59:21 +11:00
Justin Lin c5fb65db51 feat: add IMAGE_TAG variables in production compose 2026-02-27 20:59:21 +11:00
Justin Lin d4c67a7d81 fix: forward headers to backend to setup cookies properly 2026-02-27 20:43:53 +11:00
Justin Lin 0a1973f220 Feat terminal service (#196) 2026-02-27 16:06:58 +11:00
Justin Lin 776ef1162a chore: change console log to logger 2026-02-27 16:04:27 +11:00
Justin Lin 5b7bf0794c chore: Move open terminal in new tab to terminal component 2026-02-27 16:02:28 +11:00
Justin Lin ab4d06498a chore: update traefik version 2026-02-27 16:01:43 +11:00
Justin Lin d9fc465731 feat: remove container on finish 2025-05-09 16:46:13 +08:00
Justin Lin dca6861940 fix: remove writng error user terminal on disconnecting 2025-05-09 16:36:53 +08:00
Justin Lin 8161849504 feat: use traefik to proxy in production 2025-05-09 16:36:21 +08:00
Justin Lin 3c2dfcf322 refactor: update Dockerfile paths for analytics service 2025-05-05 19:55:41 +08:00
Justin Lin 0341efdd50 docs: update README with TLDR section and improve development instructions 2025-05-05 19:55:41 +08:00
Justin Lin aba492ff43 fix: reset terminal on connection and improve error handling 2025-05-05 19:55:41 +08:00
Justin Lin c2566cf3dd fix: change dev port of traefik to 8000 2025-05-05 19:55:41 +08:00
Justin Lin 307af61726 refactor: remove generating keypair 2025-05-05 19:55:41 +08:00
Justin Lin 740cbcecb4 refactor: move analytics into services directory 2025-05-05 19:55:40 +08:00
Justin Lin e43cbe26ce refactor: remove deprecated container package 2025-05-05 19:55:40 +08:00
Justin Lin 95c4fb71ae feat: handle command hooks
feat: handle copy

feat: handle remove

feat: handle move and home ~ path

refactor: improve error handling in cmd-hook

feat: handlePwd
2025-05-05 19:55:40 +08:00
Justin Lin d755822908 chore: update go.mod to remove unnecessary indirect dependencies 2025-05-05 19:55:40 +08:00
Justin Lin 372378021d feat: add timout settings for reconnectin websocket 2025-05-05 19:55:40 +08:00
Justin Lin 63e36c1c98 fix: reduce size of quest image by removing unnecessary apt cache 2025-05-05 19:55:40 +08:00
Justin Lin 2f7845e63e fix: write echo start on starting 2025-05-05 19:55:40 +08:00
Justin Lin 156abc044a refactor: clean up unused code 2025-05-05 19:55:40 +08:00
Justin Lin 21ff710f83 fix: cannot backspace in terminal 2025-05-05 19:55:40 +08:00
Justin Lin fb863ba030 fix: make quest image building process cleaner 2025-05-05 19:55:40 +08:00
Justin Lin c7e6d31b9c feat: connect frontend terminal to terminal service
fix: use ReconnectWebSocket to make the connection more stable

fix: typescript error on importing css in vue
2025-05-05 19:55:35 +08:00
Justin Lin 9c9456693d feat: handle dev server proxy by traefik 2025-05-05 19:54:35 +08:00
Justin Lin 5e81db2759 feat: connect terminal service to backend
feat: put terminal service into container

refactor: separate main into terminal and websocket

feat: create zsh using commander in home

feat: pass backendUrl to container

feat: pass token to ws server to cmd-hook

feat: gen terminal access token for frontend

feat: success to connect through traefik

fix: handle invalid jwt

feat: connect to session container via traefik
2025-05-05 19:53:33 +08:00
Justin Lin 0b08a9a5d9 feat: init cmd-hook and move terminal-service to services directory
feat: put cmd-hook in container

feat: cmd-hook request type, auto restart setup

feat: handle ls
2025-05-05 19:51:06 +08:00
Justin Lin 58b6448c3d feat: init terminal-service
feat: make interactive ws terminal
2025-05-05 19:50:34 +08:00
Justin Lin 3b77d91796 Quest: remove unnecessary ls-stages (#195)
* quest: remove ls-stages in create

* feat: prevent throwing errors when failed to copy files

* feat: log new command in dev mode

* quest: copy

* quest: mv

* quest: rm

* quest: execute. Add many exceptions

* feat: pwd discovers parent folders
2025-04-30 15:01:25 +08:00
Justin Lin f0a09ac4d9 Feat filegraph (#194)
* feat: update filegraph on touch command

* feat: update filegraph on mkdir

* feat: add different parsing opts for each command

* refactor: replace axios with fetch. put handleCommand to a new file

* feat: add config for mounting cli in dev stage

* refactor: clean up unused validators

* feat: add command handler for copy files. reuse resolvePath function

* feat: add rm handler, support specific files deletion. Glob not supported yet

* feat: handle discover cases that the folder is actually empty

* feat: handle discover empty folder better and support rmdir

* feat: handle mv command

* refactor: remove unused console logs
2025-04-29 20:40:02 +08:00
Justin Lin a732c1d119 chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates (#193)
Bumps the npm_and_yarn group with 2 updates in the / directory:
[braces](https://github.com/micromatch/braces) and
[nanoid](https://github.com/ai/nanoid).

Updates `braces` from 3.0.2 to 3.0.3
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/micromatch/braces/commit/74b2db2938fad48a2ea54a9c8bf27a37a62c350d"><code>74b2db2</code></a>
3.0.3</li>
<li><a
href="https://github.com/micromatch/braces/commit/88f1429a0f47e1dd3813de35211fc97ffda27f9e"><code>88f1429</code></a>
update eslint. lint, fix unit tests.</li>
<li><a
href="https://github.com/micromatch/braces/commit/415d660c3002d1ab7e63dbf490c9851da80596ff"><code>415d660</code></a>
Snyk js braces 6838727 (<a
href="https://redirect.github.com/micromatch/braces/issues/40">#40</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/190510f79db1adf21d92798b0bb6fccc1f72c9d6"><code>190510f</code></a>
fix tests, skip 1 test in test/braces.expand</li>
<li><a
href="https://github.com/micromatch/braces/commit/716eb9f12d820b145a831ad678618731927e8856"><code>716eb9f</code></a>
readme bump</li>
<li><a
href="https://github.com/micromatch/braces/commit/a5851e57f45c3431a94d83fc565754bc10f5bbc3"><code>a5851e5</code></a>
Merge pull request <a
href="https://redirect.github.com/micromatch/braces/issues/37">#37</a>
from coderaiser/fix/vulnerability</li>
<li><a
href="https://github.com/micromatch/braces/commit/2092bd1fb108d2c59bd62e243b70ad98db961538"><code>2092bd1</code></a>
feature: braces: add maxSymbols (<a
href="https://github.com/micromatch/braces/issues/">https://github.com/micromatch/braces/issues/</a>...</li>
<li><a
href="https://github.com/micromatch/braces/commit/9f5b4cf47329351bcb64287223ffb6ecc9a5e6d3"><code>9f5b4cf</code></a>
fix: vulnerability (<a
href="https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727">https://security.snyk.io/vuln/SNYK-JS-BRACES-6838727</a>)</li>
<li><a
href="https://github.com/micromatch/braces/commit/98414f9f1fabe021736e26836d8306d5de747e0d"><code>98414f9</code></a>
remove funding file</li>
<li><a
href="https://github.com/micromatch/braces/commit/665ab5d561c017a38ba7aafd92cc6655b91d8c14"><code>665ab5d</code></a>
update keepEscaping doc (<a
href="https://redirect.github.com/micromatch/braces/issues/27">#27</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/micromatch/braces/compare/3.0.2...3.0.3">compare
view</a></li>
</ul>
</details>
<br />

Updates `nanoid` from 3.3.7 to 3.3.11
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/ai/nanoid/releases">nanoid's
releases</a>.</em></p>
<blockquote>
<h2>3.3.11</h2>
<ul>
<li>Fixed React Native support.</li>
</ul>
<h2>3.3.10</h2>
<ul>
<li>Fixed React Native support (by <a
href="https://github.com/steida"><code>@​steida</code></a>).</li>
</ul>
<h2>3.3.9</h2>
<ul>
<li>Reduced npm package size.</li>
</ul>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/ai/nanoid/blob/main/CHANGELOG.md">nanoid's
changelog</a>.</em></p>
<blockquote>
<h2>3.3.11</h2>
<ul>
<li>Fixed React Native support.</li>
</ul>
<h2>3.3.10</h2>
<ul>
<li>Fixed React Native support (by <a
href="https://github.com/steida"><code>@​steida</code></a>).</li>
</ul>
<h2>3.3.9</h2>
<ul>
<li>Reduced npm package size.</li>
</ul>
<h2>3.3.8</h2>
<ul>
<li>Fixed a way to break Nano ID by passing non-integer size (by <a
href="https://github.com/myndzi"><code>@​myndzi</code></a>).</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/ai/nanoid/commit/37289ceee51a3194a1f121a1e5d2bbb864076b74"><code>37289ce</code></a>
Release 3.3.11 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/23690b77719ec8043c2509d28c1d74b0e2295b75"><code>23690b7</code></a>
Fix CI</li>
<li><a
href="https://github.com/ai/nanoid/commit/c147962de7f5da3311a0e731030a28f49c5266a3"><code>c147962</code></a>
Fix RN support</li>
<li><a
href="https://github.com/ai/nanoid/commit/a83734e28fa071f51fe3614a5fb891f08a4b91b2"><code>a83734e</code></a>
Move to manually ESM/CJS dual package</li>
<li><a
href="https://github.com/ai/nanoid/commit/bb12e8a6f9c37ebe0b5ff2c697b8f9dcf34c8948"><code>bb12e8a</code></a>
Release 3.3.10 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/8f44264cd724080447f40620974163f1daca4612"><code>8f44264</code></a>
Fix Expo support</li>
<li><a
href="https://github.com/ai/nanoid/commit/adf9b0c05eeeebbbf391c16bbd93da2fc275e235"><code>adf9b0c</code></a>
Release 3.3.9 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/1c6f08825b4f17c4462bd1c19dbc3f1c5626b76f"><code>1c6f088</code></a>
Remove dev file from npm package</li>
<li><a
href="https://github.com/ai/nanoid/commit/3044cd5e73f4cf31795f61f6e6b961c8c0a5c744"><code>3044cd5</code></a>
Release 3.3.8 version</li>
<li><a
href="https://github.com/ai/nanoid/commit/4fe34959c34e5b3573889ed4f24fe91d1d3e7231"><code>4fe3495</code></a>
Update size limit</li>
<li>Additional commits viewable in <a
href="https://github.com/ai/nanoid/compare/3.3.7...3.3.11">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2025-04-28 00:14:54 +08:00
dependabot[bot] da473ceb8f chore(deps): bump the npm_and_yarn group across 1 directory with 2 updates
Bumps the npm_and_yarn group with 2 updates in the / directory: [braces](https://github.com/micromatch/braces) and [nanoid](https://github.com/ai/nanoid).


Updates `braces` from 3.0.2 to 3.0.3
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

Updates `nanoid` from 3.3.7 to 3.3.11
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.7...3.3.11)

---
updated-dependencies:
- dependency-name: braces
  dependency-version: 3.0.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: nanoid
  dependency-version: 3.3.11
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 09:04:42 +00:00
Justin Lin 01d60e259c chore(deps): bump mongoose from 8.1.3 to 8.9.5 (#185)
Bumps [mongoose](https://github.com/Automattic/mongoose) from 8.1.3 to
8.9.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/Automattic/mongoose/releases">mongoose's
releases</a>.</em></p>
<blockquote>
<h1>8.9.5 / 2025-01-13</h1>
<ul>
<li>fix: disallow nested $where in populate match</li>
<li>fix(schema): handle bitwise operators on Int32 <a
href="https://redirect.github.com/Automattic/mongoose/issues/15176">#15176</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15170">#15170</a></li>
</ul>
<h1>8.9.4 / 2025-01-09</h1>
<ul>
<li>fix(document): fix document not applying manual populate when using
a function in schema.options.ref <a
href="https://redirect.github.com/Automattic/mongoose/issues/15138">#15138</a>
<a href="https://github.com/IchirokuXVI">IchirokuXVI</a></li>
<li>fix(model): make Model.validate() static correctly cast document
arrays <a
href="https://redirect.github.com/Automattic/mongoose/issues/15169">#15169</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15164">#15164</a></li>
<li>fix(model): allow passing validateBeforeSave option to bulkSave() to
skip validation <a
href="https://redirect.github.com/Automattic/mongoose/issues/15161">#15161</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15156">#15156</a></li>
<li>fix(schema): allow multiple self-referencing discriminator schemas
using Schema.prototype.discriminator <a
href="https://redirect.github.com/Automattic/mongoose/issues/15142">#15142</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15120">#15120</a></li>
<li>types: avoid BufferToBinary&lt;&gt; wiping lean types when passed to
generic functions <a
href="https://redirect.github.com/Automattic/mongoose/issues/15160">#15160</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15158">#15158</a></li>
<li>docs: fix &lt;code&gt; in header ids <a
href="https://redirect.github.com/Automattic/mongoose/issues/15159">#15159</a></li>
<li>docs: fix header in field-level-encryption.md <a
href="https://redirect.github.com/Automattic/mongoose/issues/15137">#15137</a>
<a href="https://github.com/damieng">damieng</a></li>
</ul>
<h1>8.9.3 / 2024-12-30</h1>
<ul>
<li>fix(schema): make duplicate index error a warning for now to prevent
blocking upgrading <a
href="https://redirect.github.com/Automattic/mongoose/issues/15135">#15135</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15112">#15112</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15109">#15109</a></li>
<li>fix(model): handle document array paths set to non-array values in
Model.castObject() <a
href="https://redirect.github.com/Automattic/mongoose/issues/15124">#15124</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15075">#15075</a></li>
<li>fix(document): avoid using childSchemas.path for compatibility with
pre-Mongoose-8.8 schemas <a
href="https://redirect.github.com/Automattic/mongoose/issues/15131">#15131</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15071">#15071</a></li>
<li>fix(model): avoid throwing unnecessary error if updateOne() returns
null in save() <a
href="https://redirect.github.com/Automattic/mongoose/issues/15126">#15126</a></li>
<li>perf(cursor): clear the stack every time if using populate with
batchSize to avoid stack overflows with large docs <a
href="https://redirect.github.com/Automattic/mongoose/issues/15136">#15136</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/10449">#10449</a></li>
<li>types: make BufferToBinary avoid Document instances <a
href="https://redirect.github.com/Automattic/mongoose/issues/15123">#15123</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15122">#15122</a></li>
<li>types(model+query): avoid stripping out virtuals when calling
populate with paths generic <a
href="https://redirect.github.com/Automattic/mongoose/issues/15132">#15132</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15111">#15111</a></li>
<li>types(schema): add missing removeIndex <a
href="https://redirect.github.com/Automattic/mongoose/issues/15134">#15134</a></li>
<li>types: add cleanIndexes() to IndexManager interface <a
href="https://redirect.github.com/Automattic/mongoose/issues/15127">#15127</a></li>
<li>docs: move search endpoint to netlify <a
href="https://redirect.github.com/Automattic/mongoose/issues/15119">#15119</a></li>
</ul>
<h1>8.9.2 / 2024-12-19</h1>
<ul>
<li>fix(schema): avoid throwing duplicate index error if index spec keys
have different order or index has a custom name <a
href="https://redirect.github.com/Automattic/mongoose/issues/15112">#15112</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15109">#15109</a></li>
<li>fix(map): clean modified subpaths when overwriting values in map of
subdocs <a
href="https://redirect.github.com/Automattic/mongoose/issues/15114">#15114</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15108">#15108</a></li>
<li>fix(aggregate): pull session from transaction local storage for
aggregation cursors <a
href="https://redirect.github.com/Automattic/mongoose/issues/15094">#15094</a>
<a href="https://github.com/IchirokuXVI">IchirokuXVI</a></li>
<li>types: correctly handle union types in BufferToBinary and related
helpers <a
href="https://redirect.github.com/Automattic/mongoose/issues/15103">#15103</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15102">#15102</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15057">#15057</a></li>
<li>types: add UUID to RefType <a
href="https://redirect.github.com/Automattic/mongoose/issues/15115">#15115</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15101">#15101</a></li>
<li>docs: remove link to Mongoose 5.x docs from dropdown <a
href="https://redirect.github.com/Automattic/mongoose/issues/15116">#15116</a></li>
<li>docs(connection+document+model): remove remaining references to
remove(), clarify that deleteOne() does not execute until then() or
exec() <a
href="https://redirect.github.com/Automattic/mongoose/issues/15113">#15113</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15107">#15107</a></li>
</ul>
<h1>8.9.1 / 2024-12-16</h1>
<ul>
<li>fix(connection): remove heartbeat check in load balanced mode <a
href="https://redirect.github.com/Automattic/mongoose/issues/15089">#15089</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15042">#15042</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/14812">#14812</a></li>
<li>fix(discriminator): gather childSchemas when creating discriminator
to ensure $getAllSubdocs() can properly get all subdocs <a
href="https://redirect.github.com/Automattic/mongoose/issues/15099">#15099</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15088">#15088</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15092">#15092</a></li>
<li>fix(model): handle discriminators in castObject() <a
href="https://redirect.github.com/Automattic/mongoose/issues/15096">#15096</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15075">#15075</a></li>
<li>fix(schema): throw error if duplicate index definition using unique
in schema path and subsequent .index() call <a
href="https://redirect.github.com/Automattic/mongoose/issues/15093">#15093</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15056">#15056</a></li>
<li>fix: mark documents that are populated using hydratedPopulatedDocs
option as populated in top-level doc <a
href="https://redirect.github.com/Automattic/mongoose/issues/15080">#15080</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15048">#15048</a></li>
<li>fix(document+schema): improve error message for get() on invalid
path <a
href="https://redirect.github.com/Automattic/mongoose/issues/15098">#15098</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15071">#15071</a></li>
<li>docs: remove more callback doc references &amp; some small other
changes <a
href="https://redirect.github.com/Automattic/mongoose/issues/15095">#15095</a></li>
</ul>
<h1>8.9.0 / 2024-12-13</h1>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md">mongoose's
changelog</a>.</em></p>
<blockquote>
<h1>8.9.5 / 2025-01-13</h1>
<ul>
<li>fix: disallow nested $where in populate match CVE-2025-23061</li>
<li>fix(schema): handle bitwise operators on Int32 <a
href="https://redirect.github.com/Automattic/mongoose/issues/15176">#15176</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15170">#15170</a></li>
</ul>
<h1>7.8.4 / 2025-01-13</h1>
<ul>
<li>fix: disallow nested $where in populate match CVE-2025-23061</li>
</ul>
<h1>6.13.6 / 2025-01-13</h1>
<ul>
<li>fix: disallow nested $where in populate match CVE-2025-23061</li>
</ul>
<h1>8.9.4 / 2025-01-09</h1>
<ul>
<li>fix(document): fix document not applying manual populate when using
a function in schema.options.ref <a
href="https://redirect.github.com/Automattic/mongoose/issues/15138">#15138</a>
<a href="https://github.com/IchirokuXVI">IchirokuXVI</a></li>
<li>fix(model): make Model.validate() static correctly cast document
arrays <a
href="https://redirect.github.com/Automattic/mongoose/issues/15169">#15169</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15164">#15164</a></li>
<li>fix(model): allow passing validateBeforeSave option to bulkSave() to
skip validation <a
href="https://redirect.github.com/Automattic/mongoose/issues/15161">#15161</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15156">#15156</a></li>
<li>fix(schema): allow multiple self-referencing discriminator schemas
using Schema.prototype.discriminator <a
href="https://redirect.github.com/Automattic/mongoose/issues/15142">#15142</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15120">#15120</a></li>
<li>types: avoid BufferToBinary&lt;&gt; wiping lean types when passed to
generic functions <a
href="https://redirect.github.com/Automattic/mongoose/issues/15160">#15160</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15158">#15158</a></li>
<li>docs: fix <code>&lt;code&gt;</code> in header ids <a
href="https://redirect.github.com/Automattic/mongoose/issues/15159">#15159</a></li>
<li>docs: fix header in field-level-encryption.md <a
href="https://redirect.github.com/Automattic/mongoose/issues/15137">#15137</a>
<a href="https://github.com/damieng">damieng</a></li>
</ul>
<h1>8.9.3 / 2024-12-30</h1>
<ul>
<li>fix(schema): make duplicate index error a warning for now to prevent
blocking upgrading <a
href="https://redirect.github.com/Automattic/mongoose/issues/15135">#15135</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15112">#15112</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15109">#15109</a></li>
<li>fix(model): handle document array paths set to non-array values in
Model.castObject() <a
href="https://redirect.github.com/Automattic/mongoose/issues/15124">#15124</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15075">#15075</a></li>
<li>fix(document): avoid using childSchemas.path for compatibility with
pre-Mongoose-8.8 schemas <a
href="https://redirect.github.com/Automattic/mongoose/issues/15131">#15131</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15071">#15071</a></li>
<li>fix(model): avoid throwing unnecessary error if updateOne() returns
null in save() <a
href="https://redirect.github.com/Automattic/mongoose/issues/15126">#15126</a></li>
<li>perf(cursor): clear the stack every time if using populate with
batchSize to avoid stack overflows with large docs <a
href="https://redirect.github.com/Automattic/mongoose/issues/15136">#15136</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/10449">#10449</a></li>
<li>types: make BufferToBinary avoid Document instances <a
href="https://redirect.github.com/Automattic/mongoose/issues/15123">#15123</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15122">#15122</a></li>
<li>types(model+query): avoid stripping out virtuals when calling
populate with paths generic <a
href="https://redirect.github.com/Automattic/mongoose/issues/15132">#15132</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15111">#15111</a></li>
<li>types(schema): add missing removeIndex <a
href="https://redirect.github.com/Automattic/mongoose/issues/15134">#15134</a></li>
<li>types: add cleanIndexes() to IndexManager interface <a
href="https://redirect.github.com/Automattic/mongoose/issues/15127">#15127</a></li>
<li>docs: move search endpoint to netlify <a
href="https://redirect.github.com/Automattic/mongoose/issues/15119">#15119</a></li>
</ul>
<h1>8.9.2 / 2024-12-19</h1>
<ul>
<li>fix(schema): avoid throwing duplicate index error if index spec keys
have different order or index has a custom name <a
href="https://redirect.github.com/Automattic/mongoose/issues/15112">#15112</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15109">#15109</a></li>
<li>fix(map): clean modified subpaths when overwriting values in map of
subdocs <a
href="https://redirect.github.com/Automattic/mongoose/issues/15114">#15114</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15108">#15108</a></li>
<li>fix(aggregate): pull session from transaction local storage for
aggregation cursors <a
href="https://redirect.github.com/Automattic/mongoose/issues/15094">#15094</a>
<a href="https://github.com/IchirokuXVI">IchirokuXVI</a></li>
<li>types: correctly handle union types in BufferToBinary and related
helpers <a
href="https://redirect.github.com/Automattic/mongoose/issues/15103">#15103</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15102">#15102</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15057">#15057</a></li>
<li>types: add UUID to RefType <a
href="https://redirect.github.com/Automattic/mongoose/issues/15115">#15115</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15101">#15101</a></li>
<li>docs: remove link to Mongoose 5.x docs from dropdown <a
href="https://redirect.github.com/Automattic/mongoose/issues/15116">#15116</a></li>
<li>docs(connection+document+model): remove remaining references to
remove(), clarify that deleteOne() does not execute until then() or
exec() <a
href="https://redirect.github.com/Automattic/mongoose/issues/15113">#15113</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15107">#15107</a></li>
</ul>
<h1>8.9.1 / 2024-12-16</h1>
<ul>
<li>fix(connection): remove heartbeat check in load balanced mode <a
href="https://redirect.github.com/Automattic/mongoose/issues/15089">#15089</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15042">#15042</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/14812">#14812</a></li>
<li>fix(discriminator): gather childSchemas when creating discriminator
to ensure $getAllSubdocs() can properly get all subdocs <a
href="https://redirect.github.com/Automattic/mongoose/issues/15099">#15099</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15088">#15088</a>
<a
href="https://redirect.github.com/Automattic/mongoose/issues/15092">#15092</a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/Automattic/mongoose/commit/5af0a109fcf206eb765c02e0fffe54a78ab95174"><code>5af0a10</code></a>
chore: release 8.9.5</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/a42d8f5e3b8a6c2abd7d32c15b0cc5edac928d4e"><code>a42d8f5</code></a>
Merge branch '7.x'</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/73e81abe3ec012f8255d3ca9978594075378faf4"><code>73e81ab</code></a>
chore: release 7.8.4</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/4fe9a904e3c2184ce6d01d9c3c8f3b239616f155"><code>4fe9a90</code></a>
Merge branch '6.x' into 7.x</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/e59e342e5d01bfeee31c5d5c796745315ecf0fa9"><code>e59e342</code></a>
chore: release 6.13.6</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/64a9f9706f2428c49e0cfb8e223065acc645f7bc"><code>64a9f97</code></a>
fix: disallow nested $where in populate match</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/fa33717db8c6ee2331c68241abff7a81b84a70fb"><code>fa33717</code></a>
Merge pull request <a
href="https://redirect.github.com/Automattic/mongoose/issues/15176">#15176</a>
from Automattic/vkarpov15/<a
href="https://redirect.github.com/Automattic/mongoose/issues/15170">gh-15170</a></li>
<li><a
href="https://github.com/Automattic/mongoose/commit/072860237a2b8d968e06d4090a24127de8cdff24"><code>0728602</code></a>
test: make test cast non-boolean value</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/0cad0d71a8667295c6abd4636ea7d7b5cdc164e8"><code>0cad0d7</code></a>
fix(schema): handle bitwise operators on Int32</li>
<li><a
href="https://github.com/Automattic/mongoose/commit/39886fbb57aaf345d01c0294cf2c3d2781898c29"><code>39886fb</code></a>
docs: quick changelog formatting fix</li>
<li>Additional commits viewable in <a
href="https://github.com/Automattic/mongoose/compare/8.1.3...8.9.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=mongoose&package-manager=npm_and_yarn&previous-version=8.1.3&new-version=8.9.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2025-04-27 17:00:44 +08:00
dependabot[bot] 0aeb9fe47e chore(deps): bump mongoose from 8.1.3 to 8.9.5
Bumps [mongoose](https://github.com/Automattic/mongoose) from 8.1.3 to 8.9.5.
- [Release notes](https://github.com/Automattic/mongoose/releases)
- [Changelog](https://github.com/Automattic/mongoose/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Automattic/mongoose/compare/8.1.3...8.9.5)

---
updated-dependencies:
- dependency-name: mongoose
  dependency-version: 8.9.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 08:59:09 +00:00
Justin Lin fd4f3bf8f5 chore(deps): bump dompurify from 3.0.9 to 3.2.4 (#187)
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.0.9 to
3.2.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/cure53/DOMPurify/releases">dompurify's
releases</a>.</em></p>
<blockquote>
<h2>DOMPurify 3.2.4</h2>
<ul>
<li>Fixed a conditional and config dependent mXSS-style <a
href="https://nsysean.github.io/posts/dompurify-323-bypass/">bypass</a>
reported by <a
href="https://github.com/nsysean"><code>@​nsysean</code></a></li>
<li>Added a new feature to allow specific hook removal, thanks <a
href="https://github.com/davecardwell"><code>@​davecardwell</code></a></li>
<li>Added <em>purify.js</em> and <em>purify.min.js</em> to exports,
thanks <a
href="https://github.com/Aetherinox"><code>@​Aetherinox</code></a></li>
<li>Added better logic in case no window object is president, thanks <a
href="https://github.com/yehuya"><code>@​yehuya</code></a></li>
<li>Updated some dependencies called out by dependabot</li>
<li>Updated license files etc to show the correct year</li>
</ul>
<h2>DOMPurify 3.2.3</h2>
<ul>
<li>Fixed two conditional sanitizer bypasses discovered by <a
href="https://github.com/parrot409"><code>@​parrot409</code></a> and <a
href="https://x.com/slonser_"><code>@​Slonser</code></a></li>
<li>Updated the attribute clobbering checks to prevent future bypasses,
thanks <a
href="https://github.com/parrot409"><code>@​parrot409</code></a></li>
</ul>
<h2>DOMPurify 3.2.2</h2>
<ul>
<li>Fixed a possible bypass in case a rather specific config for custom
elements is set, thanks <a
href="https://github.com/yaniv-git"><code>@​yaniv-git</code></a></li>
<li>Fixed several minor issues with the type definitions, thanks again
<a href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Fixed a minor issue with the types reference for trusted types,
thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
<li>Fixed a minor problem with the template detection regex on some
systems, thanks <a
href="https://github.com/svdb99"><code>@​svdb99</code></a></li>
</ul>
<h2>DOMPurify 3.2.1</h2>
<ul>
<li>Fixed several minor issues with the type definitions, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a> <a
href="https://github.com/ghiscoding"><code>@​ghiscoding</code></a> <a
href="https://github.com/asamuzaK"><code>@​asamuzaK</code></a> <a
href="https://github.com/MiniDigger"><code>@​MiniDigger</code></a></li>
<li>Fixed an issue with non-minified dist files and order of imports,
thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a></li>
</ul>
<h2>DOMPurify 3.2.0</h2>
<ul>
<li>Added type declarations, thanks <a
href="https://github.com/reduckted"><code>@​reduckted</code></a> , <a
href="https://github.com/philmayfield"><code>@​philmayfield</code></a>,
<a href="https://github.com/aloisklink"><code>@​aloisklink</code></a>,
<a href="https://github.com/ssi02014"><code>@​ssi02014</code></a> and
others</li>
<li>Fixed a minor issue with the handling of hooks, thanks <a
href="https://github.com/kevin-mizu"><code>@​kevin-mizu</code></a></li>
</ul>
<h2>DOMPurify 3.1.7</h2>
<ul>
<li>Fixed an issue with comment detection and possible bypasses with
specific config settings, thanks <a
href="https://github.com/masatokinugawa"><code>@​masatokinugawa</code></a></li>
<li>Fixed several smaller typos in documentation and test &amp; build
files, thanks <a
href="https://github.com/christianhg"><code>@​christianhg</code></a></li>
<li>Added better support for Angular compiler, thanks <a
href="https://github.com/jeroen1602"><code>@​jeroen1602</code></a></li>
<li>Added several new attributes to HTML and SVG allow-list, thanks <a
href="https://github.com/Gigabyte5671"><code>@​Gigabyte5671</code></a>
and <a href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
<li>Removed the <code>foreignObject</code> element from the list of HTML
entry-points, thanks <a
href="https://github.com/masatokinugawa"><code>@​masatokinugawa</code></a></li>
<li>Bumped several dependencies to be more up to date</li>
</ul>
<h2>DOMPurify 3.1.6</h2>
<ul>
<li>Fixed an issue with the execution logic of attribute hooks to
prevent bypasses, thanks <a
href="https://github.com/kevin-mizu"><code>@​kevin-mizu</code></a></li>
<li>Fixed an issue with element removal leading to uncaught errors
through DOM Clobbering, thanks <a
href="https://github.com/realansgar"><code>@​realansgar</code></a></li>
<li>Fixed a minor problem with the bower file pointing to the wrong dist
path</li>
<li>Fixed several minor typos in docs, comments and comment blocks,
thanks <a
href="https://github.com/Rotzbua"><code>@​Rotzbua</code></a></li>
<li>Updated several development dependencies</li>
</ul>
<h2>DOMPurify 3.1.5</h2>
<ul>
<li>Fixed a minor issue with the dist paths in <code>bower.js</code>,
thanks <a
href="https://github.com/HakumenNC"><code>@​HakumenNC</code></a></li>
<li>Fixed a minor issue with sanitizing HTML coming from copy&amp;paste
Word content, thanks <a
href="https://github.com/kakao-bishop-cho"><code>@​kakao-bishop-cho</code></a></li>
</ul>
<h2>DOMPurify 3.1.4</h2>
<ul>
<li>Fixed an issue with the recently implemented <code>isNaN</code>
checks, thanks <a
href="https://github.com/tulach"><code>@​tulach</code></a></li>
<li>Added several new popover attributes to allow-list, thanks <a
href="https://github.com/Gigabyte5671"><code>@​Gigabyte5671</code></a></li>
<li>Fixed the tests and adjusted the test runner to cover all
branches</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/cure53/DOMPurify/commit/ec29e65f3675077c1a78e6edcab97f797f004a2e"><code>ec29e65</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1062">#1062</a>
from cure53/main</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/1c1b1838625851939d4b86436feeb3e3ccb7dbb6"><code>1c1b183</code></a>
chore: Preparing 3.2.4 release</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/d18ffcb554e0001748865da03ac75dd7829f0f02"><code>d18ffcb</code></a>
fix: Changed the template literal regex to avoid a config-dependent
bypass</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/0d64d2b12f9ecaa28899c60aba0b9ed5072c4d93"><code>0d64d2b</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1060">#1060</a>
from yehuya/initializeTestImprovements</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/9ad79331566e0671788b120cc1f682f4e7f33f44"><code>9ad7933</code></a>
tests: DOMPurify custom window tests improvements</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/72760ca8ee425c3129420390d4c10afe698ff2fb"><code>72760ca</code></a>
Merge pull request <a
href="https://redirect.github.com/cure53/DOMPurify/issues/1059">#1059</a>
from yehuya/fixMissingWindowElement</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/bc72d44b2e3cec00d8ef2427869d03cb668969fa"><code>bc72d44</code></a>
Fix tests</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/363a89dd3a051b72334cd4d0c867616fdc4ee40d"><code>363a89d</code></a>
fix: handle undefined Element in DOMPurify initialization</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/f41b45df18a9666a50c1ad2662cee259230cfef4"><code>f41b45d</code></a>
Update LICENSE</li>
<li><a
href="https://github.com/cure53/DOMPurify/commit/b25bf26d674ff4db9074cef01fc51490cb1369a0"><code>b25bf26</code></a>
Update README.md</li>
<li>Additional commits viewable in <a
href="https://github.com/cure53/DOMPurify/compare/3.0.9...3.2.4">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=dompurify&package-manager=npm_and_yarn&previous-version=3.0.9&new-version=3.2.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2025-04-27 16:57:01 +08:00
dependabot[bot] 39c81e6b6e chore(deps): bump dompurify from 3.0.9 to 3.2.4
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.0.9 to 3.2.4.
- [Release notes](https://github.com/cure53/DOMPurify/releases)
- [Commits](https://github.com/cure53/DOMPurify/compare/3.0.9...3.2.4)

---
updated-dependencies:
- dependency-name: dompurify
  dependency-version: 3.2.4
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 08:54:25 +00:00
Justin Lin ef24270e7e fix: remove the usage of secret in workflows (#192) 2025-04-27 16:51:22 +08:00
Justin Lin 13b90f83c4 chore(deps): bump axios from 1.6.7 to 1.8.2 (#186)
Bumps [axios](https://github.com/axios/axios) from 1.6.7 to 1.8.2.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/releases">axios's
releases</a>.</em></p>
<blockquote>
<h2>Release v1.8.2</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>http-adapter:</strong> add allowAbsoluteUrls to path
building (<a
href="https://redirect.github.com/axios/axios/issues/6810">#6810</a>)
(<a
href="https://github.com/axios/axios/commit/fb8eec214ce7744b5ca787f2c3b8339b2f54b00f">fb8eec2</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/lexcorp16"
title="+1/-1 ([#6810](https://github.com/axios/axios/issues/6810)
)">Fasoro-Joseph Alexander</a></li>
</ul>
<h2>Release v1.8.1</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>utils:</strong> move <code>generateString</code> to platform
utils to avoid importing crypto module into client builds; (<a
href="https://redirect.github.com/axios/axios/issues/6789">#6789</a>)
(<a
href="https://github.com/axios/axios/commit/36a5a620bec0b181451927f13ac85b9888b86cec">36a5a62</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+51/-47
([#6789](https://github.com/axios/axios/issues/6789) )">Dmitriy
Mozgovoy</a></li>
</ul>
<h2>Release v1.8.0</h2>
<h2>Release notes:</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>examples:</strong> application crashed when navigating
examples in browser (<a
href="https://redirect.github.com/axios/axios/issues/5938">#5938</a>)
(<a
href="https://github.com/axios/axios/commit/1260ded634ec101dd5ed05d3b70f8e8f899dba6c">1260ded</a>)</li>
<li>missing word in SUPPORT_QUESTION.yml (<a
href="https://redirect.github.com/axios/axios/issues/6757">#6757</a>)
(<a
href="https://github.com/axios/axios/commit/1f890b13f2c25a016f3c84ae78efb769f244133e">1f890b1</a>)</li>
<li><strong>utils:</strong> replace getRandomValues with crypto module
(<a
href="https://redirect.github.com/axios/axios/issues/6788">#6788</a>)
(<a
href="https://github.com/axios/axios/commit/23a25af0688d1db2c396deb09229d2271cc24f6c">23a25af</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Add config for ignoring absolute URLs (<a
href="https://redirect.github.com/axios/axios/issues/5902">#5902</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6192">#6192</a>)
(<a
href="https://github.com/axios/axios/commit/32c7bcc0f233285ba27dec73a4b1e81fb7a219b3">32c7bcc</a>)</li>
</ul>
<h3>Reverts</h3>
<ul>
<li>Revert &quot;chore: expose fromDataToStream to be consumable (<a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a>)&quot;
(<a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a>)
(<a
href="https://github.com/axios/axios/commit/1317261125e9c419fe9f126867f64d28f9c1efda">1317261</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a> <a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a></li>
</ul>
<h3>BREAKING CHANGES</h3>
<ul>
<li>
<p>code relying on the above will now combine the URLs instead of prefer
request URL</p>
</li>
<li>
<p>feat: add config option for allowing absolute URLs</p>
</li>
<li>
<p>fix: add default value for allowAbsoluteUrls in buildFullPath</p>
</li>
<li>
<p>fix: typo in flow control when setting allowAbsoluteUrls</p>
</li>
</ul>
<h3>Contributors to this release</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/axios/axios/blob/v1.x/CHANGELOG.md">axios's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/axios/axios/compare/v1.8.1...v1.8.2">1.8.2</a>
(2025-03-07)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>http-adapter:</strong> add allowAbsoluteUrls to path
building (<a
href="https://redirect.github.com/axios/axios/issues/6810">#6810</a>)
(<a
href="https://github.com/axios/axios/commit/fb8eec214ce7744b5ca787f2c3b8339b2f54b00f">fb8eec2</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a href="https://github.com/lexcorp16"
title="+1/-1 ([#6810](https://github.com/axios/axios/issues/6810)
)">Fasoro-Joseph Alexander</a></li>
</ul>
<h2><a
href="https://github.com/axios/axios/compare/v1.8.0...v1.8.1">1.8.1</a>
(2025-02-26)</h2>
<h3>Bug Fixes</h3>
<ul>
<li><strong>utils:</strong> move <code>generateString</code> to platform
utils to avoid importing crypto module into client builds; (<a
href="https://redirect.github.com/axios/axios/issues/6789">#6789</a>)
(<a
href="https://github.com/axios/axios/commit/36a5a620bec0b181451927f13ac85b9888b86cec">36a5a62</a>)</li>
</ul>
<h3>Contributors to this release</h3>
<ul>
<li><!-- raw HTML omitted --> <a
href="https://github.com/DigitalBrainJS" title="+51/-47
([#6789](https://github.com/axios/axios/issues/6789) )">Dmitriy
Mozgovoy</a></li>
</ul>
<h1><a
href="https://github.com/axios/axios/compare/v1.7.9...v1.8.0">1.8.0</a>
(2025-02-25)</h1>
<h3>Bug Fixes</h3>
<ul>
<li><strong>examples:</strong> application crashed when navigating
examples in browser (<a
href="https://redirect.github.com/axios/axios/issues/5938">#5938</a>)
(<a
href="https://github.com/axios/axios/commit/1260ded634ec101dd5ed05d3b70f8e8f899dba6c">1260ded</a>)</li>
<li>missing word in SUPPORT_QUESTION.yml (<a
href="https://redirect.github.com/axios/axios/issues/6757">#6757</a>)
(<a
href="https://github.com/axios/axios/commit/1f890b13f2c25a016f3c84ae78efb769f244133e">1f890b1</a>)</li>
<li><strong>utils:</strong> replace getRandomValues with crypto module
(<a
href="https://redirect.github.com/axios/axios/issues/6788">#6788</a>)
(<a
href="https://github.com/axios/axios/commit/23a25af0688d1db2c396deb09229d2271cc24f6c">23a25af</a>)</li>
</ul>
<h3>Features</h3>
<ul>
<li>Add config for ignoring absolute URLs (<a
href="https://redirect.github.com/axios/axios/issues/5902">#5902</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6192">#6192</a>)
(<a
href="https://github.com/axios/axios/commit/32c7bcc0f233285ba27dec73a4b1e81fb7a219b3">32c7bcc</a>)</li>
</ul>
<h3>Reverts</h3>
<ul>
<li>Revert &quot;chore: expose fromDataToStream to be consumable (<a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a>)&quot;
(<a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a>)
(<a
href="https://github.com/axios/axios/commit/1317261125e9c419fe9f126867f64d28f9c1efda">1317261</a>),
closes <a
href="https://redirect.github.com/axios/axios/issues/6731">#6731</a> <a
href="https://redirect.github.com/axios/axios/issues/6732">#6732</a></li>
</ul>
<h3>BREAKING CHANGES</h3>
<ul>
<li>
<p>code relying on the above will now combine the URLs instead of prefer
request URL</p>
</li>
<li>
<p>feat: add config option for allowing absolute URLs</p>
</li>
<li>
<p>fix: add default value for allowAbsoluteUrls in buildFullPath</p>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/axios/axios/commit/a9f7689b0c4b6d68c7f587c3aa376860da509d94"><code>a9f7689</code></a>
chore(release): v1.8.2 (<a
href="https://redirect.github.com/axios/axios/issues/6812">#6812</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/fb8eec214ce7744b5ca787f2c3b8339b2f54b00f"><code>fb8eec2</code></a>
fix(http-adapter): add allowAbsoluteUrls to path building (<a
href="https://redirect.github.com/axios/axios/issues/6810">#6810</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/98120457559e573024862e2925d56295a965ad7e"><code>9812045</code></a>
chore(sponsor): update sponsor block (<a
href="https://redirect.github.com/axios/axios/issues/6804">#6804</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/72acf759373ef4e211d5299818d19e50e08c02f8"><code>72acf75</code></a>
chore(sponsor): update sponsor block (<a
href="https://redirect.github.com/axios/axios/issues/6794">#6794</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/2e64afdff5c41e38284a6fb8312f2745072513a1"><code>2e64afd</code></a>
chore(release): v1.8.1 (<a
href="https://redirect.github.com/axios/axios/issues/6800">#6800</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/36a5a620bec0b181451927f13ac85b9888b86cec"><code>36a5a62</code></a>
fix(utils): move <code>generateString</code> to platform utils to avoid
importing crypto...</li>
<li><a
href="https://github.com/axios/axios/commit/cceb7b1e154fbf294135c93d3f91921643bbe49f"><code>cceb7b1</code></a>
chore(release): v1.8.0 (<a
href="https://redirect.github.com/axios/axios/issues/6795">#6795</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/23a25af0688d1db2c396deb09229d2271cc24f6c"><code>23a25af</code></a>
fix(utils): replace getRandomValues with crypto module (<a
href="https://redirect.github.com/axios/axios/issues/6788">#6788</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/32c7bcc0f233285ba27dec73a4b1e81fb7a219b3"><code>32c7bcc</code></a>
feat: Add config for ignoring absolute URLs (<a
href="https://redirect.github.com/axios/axios/issues/5902">#5902</a>)
(<a
href="https://redirect.github.com/axios/axios/issues/6192">#6192</a>)</li>
<li><a
href="https://github.com/axios/axios/commit/4a3e26cf65bb040b7eb4577d5fd62199b0f3d017"><code>4a3e26c</code></a>
chore(config): adjust rollup config to preserve license header to
minified Ja...</li>
<li>Additional commits viewable in <a
href="https://github.com/axios/axios/compare/v1.6.7...v1.8.2">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=axios&package-manager=npm_and_yarn&previous-version=1.6.7&new-version=1.8.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2025-04-27 16:46:31 +08:00
Justin Lin cb77a3e266 chore(deps): bump @trpc/server from 11.1.0 to 11.1.1 (#188)
Bumps
[@trpc/server](https://github.com/trpc/trpc/tree/HEAD/packages/server)
from 11.1.0 to 11.1.1.
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/trpc/trpc/commit/9c1c753a778ac7a2709b1e81be392231c4657319"><code>9c1c753</code></a>
v11.1.1</li>
<li><a
href="https://github.com/trpc/trpc/commit/9beb26c636d44852e0f407f3d7a82ad54df65b4d"><code>9beb26c</code></a>
Merge commit from fork</li>
<li>See full diff in <a
href="https://github.com/trpc/trpc/commits/v11.1.1/packages/server">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@trpc/server&package-manager=npm_and_yarn&previous-version=11.1.0&new-version=11.1.1)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2025-04-27 16:45:48 +08:00
Justin Lin 61cb36f357 Fix/login limit (By Vincent) (#191)
This PR is originally by @Vincent23412 from #190 . Edited by @lancatlin
2025-04-27 16:43:56 +08:00
Justin Lin 58f3bf7817 fix: use config value instead of hard coded values 2025-04-27 16:41:19 +08:00
Justin Lin ccbc4ff4b0 feat(rate-limiting): implement rate limiting for login attempts and enhance configuration
- Added environment variables for `RATE_LIMIT_MAX_FAILED_ATTEMPTS` and `RATE_LIMIT_LOCKOUT_TIME`
- Integrated rate limiting into the login process with a maximum of 5 failed attempts
- Refactored login attempt recording into the `authRouter`
- Removed deprecated `recordLogin` middleware and adjusted related logic
2025-04-27 16:29:03 +08:00
Justin Lin aef760478c fix: remove the usage of 2025-04-27 16:07:00 +08:00
Vincent23412 bda68ad293 fix path bugs 2025-04-27 16:02:21 +08:00
Vincent23412 af2f1ef707 feat(auth): add LoginAttempt schema and record login attempts with failure rate limit
- Added `LoginAttempt` Mongoose schema to store login history
- Integrated login attempt recording into login flow (success/failure)
- Created `rateLimitByAccount` middleware to block login if 10 failed attempts in 10 minutes
2025-04-27 15:57:34 +08:00
dependabot[bot] 3b4891412e chore(deps): bump @trpc/server from 11.1.0 to 11.1.1
Bumps [@trpc/server](https://github.com/trpc/trpc/tree/HEAD/packages/server) from 11.1.0 to 11.1.1.
- [Release notes](https://github.com/trpc/trpc/releases)
- [Commits](https://github.com/trpc/trpc/commits/v11.1.1/packages/server)

---
updated-dependencies:
- dependency-name: "@trpc/server"
  dependency-version: 11.1.1
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 07:47:14 +00:00
dependabot[bot] ef588414db chore(deps): bump axios from 1.6.7 to 1.8.2
Bumps [axios](https://github.com/axios/axios) from 1.6.7 to 1.8.2.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.6.7...v1.8.2)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.8.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-27 07:47:09 +00:00
Justin Lin bc43184adc chore(deps): bump express from 4.19.2 to 4.20.0 (#163)
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to
4.20.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/releases">express's
releases</a>.</em></p>
<blockquote>
<h2>4.20.0</h2>
<h2>What's Changed</h2>
<h3>Important</h3>
<ul>
<li>IMPORTANT: The default <code>depth</code> level for parsing
URL-encoded data is now <code>32</code> (previously was
<code>Infinity</code>)</li>
<li>Remove link renderization in html while using
<code>res.redirect</code></li>
</ul>
<h3>Other Changes</h3>
<ul>
<li>4.19.2 Staging by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5561">expressjs/express#5561</a></li>
<li>remove duplicate location test for data uri by <a
href="https://github.com/wesleytodd"><code>@​wesleytodd</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5562">expressjs/express#5562</a></li>
<li>feat: document beta releases expectations by <a
href="https://github.com/marco-ippolito"><code>@​marco-ippolito</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5565">expressjs/express#5565</a></li>
<li>Cut down on duplicated CI runs by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5564">expressjs/express#5564</a></li>
<li>Add a Threat Model by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5526">expressjs/express#5526</a></li>
<li>Assign captain of encodeurl by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5579">expressjs/express#5579</a></li>
<li>Nominate jonchurch as repo captain for <code>http-errors</code>,
<code>expressjs.com</code>, <code>morgan</code>, <code>cors</code>,
<code>body-parser</code> by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5587">expressjs/express#5587</a></li>
<li>docs: update Security.md by <a
href="https://github.com/inigomarquinez"><code>@​inigomarquinez</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5590">expressjs/express#5590</a></li>
<li>docs: update triage nomination policy by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5600">expressjs/express#5600</a></li>
<li>Add CodeQL (SAST) by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5433">expressjs/express#5433</a></li>
<li>docs: add UlisesGascon as triage initiative captain by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5605">expressjs/express#5605</a></li>
<li>deps: encodeurl@~2.0.0 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5569">expressjs/express#5569</a></li>
<li>skip QUERY method test by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5628">expressjs/express#5628</a></li>
<li>ignore ETAG query test on 21 and 22, reuse skip util by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5639">expressjs/express#5639</a></li>
<li>add support Node.js@22 in the CI by <a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5627">expressjs/express#5627</a></li>
<li>doc: add table of contents, tc/triager lists to readme by <a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5619">expressjs/express#5619</a></li>
<li>List and sort all projects, add captains by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5653">expressjs/express#5653</a></li>
<li>docs: add <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
as captain for cookie-parser by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5666">expressjs/express#5666</a></li>
<li>✨ bring back query tests for node 21 by <a
href="https://github.com/ctcpip"><code>@​ctcpip</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5690">expressjs/express#5690</a></li>
<li>[v4] Deprecate <code>res.clearCookie</code> accepting
<code>options.maxAge</code> and <code>options.expires</code> by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5672">expressjs/express#5672</a></li>
<li>skip QUERY tests for Node 21 only, still not supported by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5695">expressjs/express#5695</a></li>
<li>📝 update people, add ctcpip to TC by <a
href="https://github.com/ctcpip"><code>@​ctcpip</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5683">expressjs/express#5683</a></li>
<li>remove minor version pinning from ci by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5722">expressjs/express#5722</a></li>
<li>Fix link variable use in attribution section of CODE OF CONDUCT by
<a href="https://github.com/IamLizu"><code>@​IamLizu</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5762">expressjs/express#5762</a></li>
<li>Replace Appveyor windows testing with GHA by <a
href="https://github.com/jonchurch"><code>@​jonchurch</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5599">expressjs/express#5599</a></li>
<li>Add OSSF Scorecard badge by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5436">expressjs/express#5436</a></li>
<li>update scorecard link by <a
href="https://github.com/bjohansebas"><code>@​bjohansebas</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5814">expressjs/express#5814</a></li>
<li>Nominate <a
href="https://github.com/IamLizu"><code>@​IamLizu</code></a> to the
triage team by <a
href="https://github.com/UlisesGascon"><code>@​UlisesGascon</code></a>
in <a
href="https://redirect.github.com/expressjs/express/pull/5836">expressjs/express#5836</a></li>
<li>deps: path-to-regexp@0.1.8 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5603">expressjs/express#5603</a></li>
<li>docs: specify new instructions for <code>question</code> and
<code>discuss</code> by <a
href="https://github.com/IamLizu"><code>@​IamLizu</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5835">expressjs/express#5835</a></li>
<li>4.x: Upgrade <code>merge-descriptors</code> dependency by <a
href="https://github.com/RobinTail"><code>@​RobinTail</code></a> in <a
href="https://redirect.github.com/expressjs/express/pull/5781">expressjs/express#5781</a></li>
<li>path-to-regexp@0.1.10 by <a
href="https://github.com/blakeembrey"><code>@​blakeembrey</code></a> in
<a
href="https://redirect.github.com/expressjs/express/pull/5902">expressjs/express#5902</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/marco-ippolito"><code>@​marco-ippolito</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5565">expressjs/express#5565</a></li>
<li><a
href="https://github.com/inigomarquinez"><code>@​inigomarquinez</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5590">expressjs/express#5590</a></li>
<li><a
href="https://github.com/mertcanaltin"><code>@​mertcanaltin</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5627">expressjs/express#5627</a></li>
<li><a href="https://github.com/ctcpip"><code>@​ctcpip</code></a> made
their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5690">expressjs/express#5690</a></li>
<li><a
href="https://github.com/bjohansebas"><code>@​bjohansebas</code></a>
made their first contribution in <a
href="https://redirect.github.com/expressjs/express/pull/5814">expressjs/express#5814</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/expressjs/express/compare/4.19.1...4.20.0">https://github.com/expressjs/express/compare/4.19.1...4.20.0</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/expressjs/express/blob/master/History.md">express's
changelog</a>.</em></p>
<blockquote>
<h1>4.20.0 / 2024-09-10</h1>
<ul>
<li>deps: serve-static@0.16.0
<ul>
<li>Remove link renderization in html while redirecting</li>
</ul>
</li>
<li>deps: send@0.19.0
<ul>
<li>Remove link renderization in html while redirecting</li>
</ul>
</li>
<li>deps: body-parser@0.6.0
<ul>
<li>add <code>depth</code> option to customize the depth level in the
parser</li>
<li>IMPORTANT: The default <code>depth</code> level for parsing
URL-encoded data is now <code>32</code> (previously was
<code>Infinity</code>)</li>
</ul>
</li>
<li>Remove link renderization in html while using
<code>res.redirect</code></li>
<li>deps: path-to-regexp@0.1.10
<ul>
<li>Adds support for named matching groups in the routes using a
regex</li>
<li>Adds backtracking protection to parameters without regexes
defined</li>
</ul>
</li>
<li>deps: encodeurl@~2.0.0
<ul>
<li>Removes encoding of <code>\</code>, <code>|</code>, and
<code>^</code> to align better with URL spec</li>
</ul>
</li>
<li>Deprecate passing <code>options.maxAge</code> and
<code>options.expires</code> to <code>res.clearCookie</code>
<ul>
<li>Will be ignored in v5, clearCookie will set a cookie with an expires
in the past to instruct clients to delete the cookie</li>
</ul>
</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/expressjs/express/commit/21df421ebc7a5249bb31101da666bbf22adc3f18"><code>21df421</code></a>
4.20.0</li>
<li><a
href="https://github.com/expressjs/express/commit/4c9ddc1c47bf579e55c2fe837d76a952e9fd8959"><code>4c9ddc1</code></a>
feat: upgrade to serve-static@0.16.0</li>
<li><a
href="https://github.com/expressjs/express/commit/9ebe5d500d22cbb2b8aaa73446866b084c747971"><code>9ebe5d5</code></a>
feat: upgrade to send@0.19.0 (<a
href="https://redirect.github.com/expressjs/express/issues/5928">#5928</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/ec4a01b6b8814d7b007f36a3023f4dbafdbc3d09"><code>ec4a01b</code></a>
feat: upgrade to body-parser@1.20.3 (<a
href="https://redirect.github.com/expressjs/express/issues/5926">#5926</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/54271f69b511fea198471e6ff3400ab805d6b553"><code>54271f6</code></a>
fix: don't render redirect values in anchor href</li>
<li><a
href="https://github.com/expressjs/express/commit/125bb742a38cd97938a3932b47cc301e41c31f5d"><code>125bb74</code></a>
path-to-regexp@0.1.10 (<a
href="https://redirect.github.com/expressjs/express/issues/5902">#5902</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/2a980ad16052e53b398c9953fea50e3daa0b495c"><code>2a980ad</code></a>
merge-descriptors@1.0.3 (<a
href="https://redirect.github.com/expressjs/express/issues/5781">#5781</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/a3e7e05e0a435b7b4be25bd38d8d0ca19a773ca9"><code>a3e7e05</code></a>
docs: specify new instructions for <code>question</code> and
<code>discuss</code></li>
<li><a
href="https://github.com/expressjs/express/commit/c5addb9a17c5b4c9fccdd2c04153a30595e03385"><code>c5addb9</code></a>
deps: path-to-regexp@0.1.8 (<a
href="https://redirect.github.com/expressjs/express/issues/5603">#5603</a>)</li>
<li><a
href="https://github.com/expressjs/express/commit/e35380a39d94937e3d0f7119e0efbc7cd69d003f"><code>e35380a</code></a>
docs: add <a
href="https://github.com/IamLizu"><code>@​IamLizu</code></a> to the
triage team (<a
href="https://redirect.github.com/expressjs/express/issues/5836">#5836</a>)</li>
<li>Additional commits viewable in <a
href="https://github.com/expressjs/express/compare/4.19.2...4.20.0">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=express&package-manager=npm_and_yarn&previous-version=4.19.2&new-version=4.20.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/linux-odyssey/linux-odyssey/network/alerts).

</details>
2025-04-27 15:46:10 +08:00
Justin Lin 13fb8737a3 Fix deploy (#183) 2025-04-27 12:38:16 +08:00
Justin Lin eaced63c68 feat: add console logger in production 2025-04-27 12:34:27 +08:00
Justin Lin b64cc6028c feat: add proxy settings to nginx.conf 2025-04-27 12:34:05 +08:00
Justin Lin dbf26bb490 fix: add docker cli into backend image 2025-04-27 12:33:36 +08:00
Justin Lin 698dc368e6 fix: use wrong parsed command body (#182) 2025-04-26 18:48:04 +08:00
Justin LinandCopilot 0a8ef9a37e Update packages/container/cli.js
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-04-26 18:45:35 +08:00
Justin Lin aeed963a75 fix: use wrong parsed command body 2025-04-26 18:43:43 +08:00
Justin Lin 128aae33ec fix: update project root detection and quest directory path (#181) 2025-04-26 18:13:27 +08:00
Justin Lin b86697b6db fix: validate command input in newCommand controller 2025-04-26 18:10:33 +08:00
Justin Lin cb1e395579 refactor: update project root detection and quest directory path 2025-04-26 18:02:49 +08:00
Justin Lin fbe1319183 fix: analytics dockerfile (#180) 2025-04-26 17:30:08 +08:00
Justin Lin 94389d4e1c fix: analytics dockerfile 2025-04-26 17:27:49 +08:00
Justin Lin a66b4f5adb Refactor game/packages/trpc (#179)
- Refactor game core logic in `packages/game`
- Use Zod to parse quests and store in memory directly
- Get quest responses dynamically. Not stored in Session anymore.
- Use tRPC to re-implement some of APIs
- Remove all package.json in `packages`, import them as regular files
- Add JSON schema for quest.yml for better editing experience
- Refine quest files
2025-04-26 17:10:58 +08:00
Justin Lin 9498bb77ef fix: linting 2025-04-26 17:02:47 +08:00
Justin Lin 6fe49c4f38 fix: dockerfile, production env settings 2025-04-26 16:38:32 +08:00
Justin Lin 3985d5d089 fix: server and app build 2025-04-26 13:22:32 +08:00
Justin Lin 253564376d fix: analytics type errors 2025-04-26 13:10:48 +08:00
Justin Lin f7e2b00358 fix: test in game and file-graph 2025-04-26 12:52:12 +08:00
Justin Lin d450613816 clean: remove unused code 2025-04-26 12:48:29 +08:00
Justin Lin 129d2ff49b quest: checkpoint-03 2025-04-26 12:35:20 +08:00
Justin Lin ad0cb3e4b2 quest: checkpoint-02 2025-04-26 12:30:57 +08:00
Justin Lin 66365b106e quest: checkpoint-1 2025-04-26 12:27:17 +08:00
Justin Lin f6ee9f91d4 quest: execute 2025-04-26 12:23:46 +08:00
Justin Lin 67e84d8cf9 quest: rm, fix copy (stages removed by AI) 2025-04-26 12:10:53 +08:00
Justin Lin fda0357a73 feat: add file moving quest with instructions and example files 2025-04-26 11:49:40 +08:00
Justin Lin e9e924165d feat: improve Description page switcher style 2025-04-26 11:48:07 +08:00
Justin Lin 937c5de6b8 fix: terminal overflow 2025-04-26 11:46:36 +08:00
Justin Lin 53c5391f61 quest: copy 2025-04-26 11:27:55 +08:00
Justin Lin 876cae6947 feat: close hint for new responses 2025-04-26 11:27:44 +08:00
Justin Lin beb58e233e quests: tab & create 2025-04-26 11:09:23 +08:00
Justin Lin f7860c6197 fix: update CLIFileExistenceChecker to accept containerId in constructor 2025-04-26 11:09:10 +08:00
Justin Lin 5077f9c677 feat: read quest 2025-04-25 19:57:49 +08:00
Justin Lin 9be288d2de feat: add new path quest with file navigation and update quest structure 2025-04-25 19:55:11 +08:00
Justin Lin cd6f062556 refactor: enhance get-started quest 2025-04-25 12:36:39 +08:00
Justin Lin c61f844e42 feat: add exploration quest with file navigation instructions and example files 2025-04-25 12:01:25 +08:00
Justin Lin 26e79690f8 feat: add pwd handling to session and update related components 2025-04-25 11:33:16 +08:00
Justin Lin fba92dad46 feat: integrate filegraph 2025-04-24 23:30:12 +08:00
Justin Lin b3ed0e646e fix: app import inteface 2025-04-24 23:23:58 +08:00
Justin Lin 87b8ca5d04 wip: replace packages with pure files 2025-04-24 23:20:40 +08:00
Justin Lin 2d95ca382f wip: integrate filegraph into session 2025-04-24 22:05:10 +08:00
Justin Lin cdd22634e0 refactor: replace "folder" with "directory" across file handling components and tests 2025-04-24 21:24:55 +08:00
Justin Lin 7e50c8e06c refactor: remove condition tests and update file node interfaces in game session
TODO: replace "folder" with "directory"
2025-04-24 18:10:47 +08:00
Justin Lin 0685d71299 refactor: remove print.js and simplify response handling in cli.js 2025-04-24 18:08:09 +08:00
Justin Lin 56049a0a29 fix: DAG setLayer logic 2025-04-23 17:58:24 +08:00
Justin Lin 9b5da4e818 feat: enhance game description with collapsible hint section and update hint text format 2025-04-23 17:44:23 +08:00
Justin Lin ab63595fba feat: add JSON schema for quest 2025-04-23 17:40:55 +08:00
Justin Lin 52e9a13645 feat: add isFinished method to GameSession and update commandController to handle session completion 2025-04-23 17:10:51 +08:00
Justin Lin cfbc386c16 refactor: update file-graph package structure and integrate with game session management 2025-04-23 17:04:00 +08:00
Justin Lin 92173bb7e9 refactor: migrate session management to TRPC and remove legacy API endpoints 2025-04-23 16:37:12 +08:00
Justin Lin b4b8f8baee feat: load responses and tasks from existing session 2025-04-23 16:17:10 +08:00
Justin Lin 2fcca0c4b9 refactor: update session management to handle tasks and responses in a single update 2025-04-23 16:03:39 +08:00
Justin Lin 66434ff897 fix: save new created session 2025-04-22 19:06:57 +08:00
Justin Lin a711e08328 refactor: update QuestMap component to use new node structure and simplify DAG class 2025-04-22 18:58:23 +08:00
Justin Lin 1616f62755 wip: migrate backend to new game package 2025-04-21 22:40:11 +08:00
Justin Lin 1d843f7355 refactor: remove unused quest-related schemas and consolidate exports 2025-04-21 21:42:54 +08:00
Justin Lin d2cdd1304a feat: getTasks 2025-04-21 18:31:33 +08:00
Justin Lin 254e4f75ed feat: include responses from Exceptions 2025-04-21 12:31:53 +08:00
Justin Lin 0f0f43b838 refactor: Create a new super class Event and prevent duplicate logic 2025-04-21 12:23:09 +08:00
Justin Lin 2dc40a2254 refactor: add new name "Event" for the set Stage and Exception 2025-04-21 11:58:05 +08:00
Justin Lin 6e804f0ed4 refactor: reuse satisfies function in condition 2025-04-21 11:53:32 +08:00
Justin Lin 3656868b66 feat: catch-all exceptions 2025-04-21 11:45:23 +08:00
Justin Lin cfce5ececa feat: add global exceptions 2025-04-21 11:41:07 +08:00
Justin Lin fb823814e0 feat: add stage exceptions 2025-04-21 11:19:59 +08:00
Justin Lin f7780c3d98 feat: add get responses 2025-04-18 15:17:25 +08:00
Justin Lin dae9e841ca feat: add run command 2025-04-18 15:04:28 +08:00
Justin Lin e622011b21 feat: add quest class and session getActiveStages 2025-04-18 14:54:10 +08:00
Justin Lin 471b781d85 refactor: separate schema to files 2025-04-18 14:33:41 +08:00
Justin Lin 57d8f24930 Merge branch 'game' into refactor-trpc 2025-04-18 14:27:41 +08:00
Justin Lin 6043226142 wip: migrating game handler 2025-04-18 14:27:36 +08:00
Justin Lin 61d1871e95 feat: parse all quest 2025-04-16 19:06:47 +08:00
Justin Lin ed4d04b658 feat: parse quest using zod, basic 2025-04-16 17:37:42 +08:00
Justin Lin 963655c1f7 wip: get active session 2025-04-16 16:58:24 +08:00
Justin Lin 423548804a feat: migreate get user profile to trpc 2025-04-16 16:40:03 +08:00
Justin Lin ac92950177 feat: migrate quest api to trpc 2025-04-16 16:24:29 +08:00
Justin Lin 55a41dd3b5 feat: integrate trpc to app 2025-04-16 16:02:23 +08:00
Justin Lin 529869671c feat: createContext 2025-04-16 15:36:12 +08:00
Justin Lin 6326b84a60 feat: init trpc 2025-04-16 15:27:00 +08:00
Justin Lin 8cc006e3a4 fix: check files using abs path (#175)
the docker user is changed from commander to root due to #168 pr.
Use absolute path instead of relative path to prevent false detection.
2025-02-15 20:55:03 +08:00
Justin Lin d6ae6d818d Fix network (#174) 2025-02-15 20:52:25 +08:00
wancat 6692ae2b9b fix: check files using abs path
(the docker user is changed due to feat-ssh pr)
2025-02-15 20:42:50 +08:00
Justin Lin fab02d03a9 fix: update env samples, use dash only for naming networks 2024-12-27 21:01:31 +08:00
Justin Lin 9e44ebe415 fix: remove unwanted error logging 2024-12-27 20:56:23 +08:00
Justin Lin 019caf58e4 fix: use a different docker network for dev to prevent "interna" settings conflicts 2024-12-27 20:56:05 +08:00
Justin Lin 97ad1a7b50 fix: set mongodb version to 6 (#170) 2024-10-21 13:51:17 +08:00
Justin Lin cb4b9f429a fix: set mongodb version to 6 2024-10-21 13:43:58 +08:00
Justin Lin 5e11d2da11 fix: pass env file to containers directly (#169) 2024-10-21 12:59:56 +08:00
Justin Lin a901ce005c fix: pass env file to containers directly 2024-10-21 12:53:39 +08:00
Justin Lin 4dd2a7cbe1 Feat ssh (#168) 2024-10-21 12:15:43 +08:00
Justin Lin 5cf375905d feat: remove debug logs 2024-10-21 12:12:52 +08:00
Justin Lin d7c5c756f3 feat: add production env sample 2024-10-21 12:11:32 +08:00
Justin Lin 44ecd3b87a fix: mounting quest home into production container 2024-10-21 12:11:24 +08:00
Justin Lin 1888781a47 fix: copy entrypoint into quest container. Disable mounting for entrypoint & public key. Use Env to pass ssh public key into container. 2024-10-21 11:54:32 +08:00
Justin Lin 860b257ea7 doc: update Windows setup guide 2024-10-20 20:06:39 +08:00
Justin Lin ba251fb933 feat: update dump script 2024-10-17 22:08:52 +08:00
Chen, Tai-Lin 84bd2620d8 docs: add windows environment setup document draft 2024-10-16 23:53:58 +08:00
Justin Lin b449d84c54 feat: auto create config folder and player network 2024-10-16 14:13:41 +08:00
Justin Lin 91ebaec22c feat: update document and remove some debug logging 2024-10-14 18:32:02 +08:00
Justin Lin 58354b7cd8 feat: clean up /etc/skel and move ssh key to /etc/ssh/authorized_keys/ 2024-10-14 17:48:05 +08:00
Justin Lin 31301419ad feat: add ssh connection retry 2024-10-14 17:31:35 +08:00
Justin Lin 251139c159 feat: move sshd_config to .include folder 2024-10-14 17:31:23 +08:00
Justin Lin a4ed4cf533 feat: remove unnecessary quest Dockerfiles 2024-10-14 12:40:53 +08:00
Justin Lin 247f03b71a feat: mount quest home to /etc/skel to prevent building images 2024-10-14 12:30:28 +08:00
Justin Lin be22a121c8 feat: use entrypoint to create user account and copy ssh key 2024-10-14 12:06:05 +08:00
Justin Lin 261c6fc029 feat: generate key pair using ssh2 for right public key format 2024-10-14 11:34:53 +08:00
Justin Lin 03b2993b56 feat: load or create key 2024-10-14 11:23:25 +08:00
Justin Lin 2cc348687a feat: generate key pair function 2024-10-14 10:53:15 +08:00
Justin Lin 2a3429106c feat: remove welcome banner 2024-10-13 16:31:17 +08:00
Justin Lin a6720cfe39 feat: connect through ssh 2024-10-13 15:59:01 +08:00
Justin Lin 4c292fd6ba wip: allow connection from docker container 2024-10-13 15:05:20 +08:00
Justin Lin 5ba0021c37 fix: ssh server 2024-10-10 22:43:39 +08:00
Justin Lin 19897e607f wip: connect container through ssh 2024-10-10 20:54:42 +08:00
Justin Lin 08e9191c4b feat: replace alpine with debian slim 2024-10-10 20:54:09 +08:00
Justin Lin aa95eda23c Doc (#167) 2024-10-10 19:52:04 +08:00
Justin Lin 6bc37900d8 doc: update README 2024-10-10 17:41:47 +08:00
Justin Lin 98f18627e5 feat: mount quest 2024-10-10 17:21:38 +08:00
Justin Lin 19c44fbe52 wip: game schema 2024-10-10 16:22:20 +08:00
dependabot[bot] c254d6c948 chore(deps): bump express from 4.19.2 to 4.20.0
Bumps [express](https://github.com/expressjs/express) from 4.19.2 to 4.20.0.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.20.0)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-21 06:11:22 +00:00
Justin Lin e66598f1a7 chore: update dependencies 2024-08-28 14:52:45 +08:00
Justin Lin 2a868ead48 Merge branch 'main' into game 2024-08-28 14:52:13 +08:00
Justin Lin 9b4f50ebbd Formbricks (#157)
add quest feedback and bug report form using Formbricks
2024-08-02 22:07:36 +08:00
wancat 5fd24190d2 feat: change form to production 2024-08-02 21:17:18 +08:00
wancat d2bb079f50 feat: add bug report 2024-08-02 20:54:38 +08:00
wancat d959ab22fe refactor: move open survey to formbricks 2024-08-02 16:49:26 +08:00
wancat 9c8260a411 feat: integrate form after finishing quest 2024-08-02 16:15:06 +08:00
wancat 3c92c9d143 feat: add formbricks 2024-08-02 15:32:47 +08:00
raygur 5208d87bd6 feat: formbricks editing 2024-08-02 14:44:28 +08:00
RayGur 634464065d feat: formbricks edit 2024-08-02 14:44:28 +08:00
RayGur 4c90793fc6 feat: formbricks init 2024-08-02 14:44:28 +08:00
RayGur 169587c54d feat: formbricks setup 2024-08-02 14:41:14 +08:00
Justin Lin 4dd5f94c60 fix: map background (#156)
Put catelog background to public folder
2024-08-02 14:36:38 +08:00
Justin Lin 14bcb3b0d5 Basic checkpoint quests (#155) 2024-08-02 14:35:32 +08:00
Annie Huang 8281a9d882 fix: map background 2024-08-02 13:19:53 +08:00
Chen, Tai-Lin dcf72d046c quest: fix checkpoint-03 2024-08-02 11:12:30 +08:00
Chen, Tai-Lin b73607fb8d quest: fix checkpoint-02 bug 2024-08-02 10:45:31 +08:00
Chen, Tai-Lin b64d707d16 quest: fix checkpoints bug 2024-08-02 10:22:42 +08:00
Chen, Tai-Lin 7dcac306f9 quest: checkpoint03 2024-08-02 02:27:41 +08:00
Chen, Tai-Lin eddbda23d7 quest: fix content in remove 2024-08-02 01:06:05 +08:00
Chen, Tai-Lin 124c5cc401 quest: checkpoint-02 2024-08-02 01:05:38 +08:00
Chen, Tai-Lin 21d08b8156 quest: checkpoint01 2024-08-02 00:28:59 +08:00
Chen, Tai-Lin 3745011504 fix quest content 2024-08-01 21:57:43 +08:00
Justin Lin 3f08452ea4 feat: 119-Quest-Intro (#154)
- Add an introduction for every quest
- Not yet written documents for the introduction, so the contents are
from the information in the game
2024-08-01 20:52:10 +08:00
Annie Huang acfb09c6ca delete redundant code 2024-08-01 19:58:13 +08:00
Annie Huang c2c100e6e6 simplify data usage 2024-08-01 19:55:24 +08:00
Annie Huang 4033eefa0d remove redundant variable 2024-08-01 19:41:55 +08:00
Annie Huang 334b34ef65 Green button for unlocked and completed quest 2024-08-01 19:40:17 +08:00
Annie Huang ac89d521fc remove console log 2024-08-01 19:32:28 +08:00
Annie Huang 60641ba0ad change background loading method 2024-08-01 19:29:01 +08:00
Annie Huang dd67bcb1bd Add: Quest Introduction 2024-08-01 19:11:27 +08:00
Justin Lin d002b21aa8 Feat map (#152) 2024-07-31 20:37:49 +08:00
wancat 033c808c02 chore: remove echarts dependency 2024-07-31 18:25:01 +08:00
wancat 1be4f73e7e refactor: styles of nodes and edges 2024-07-31 18:17:44 +08:00
wancat 9ddfcd2a86 feat: add edges (straight line for now) 2024-07-31 18:11:18 +08:00
wancat ca36f6391a feat: add dragging 2024-07-31 18:00:19 +08:00
wancat 1572b2ea06 feat: rewrite map state handling 2024-07-31 17:54:59 +08:00
wancat 8f6914d46c feat: new map by svg 2024-07-31 17:12:43 +08:00
Justin Lin 517ab43e8c Execute Quest (#150) 2024-07-31 16:37:32 +08:00
Justin Lin 74c49e1df7 Quest tab (#151) 2024-07-31 16:37:17 +08:00
wancat cb0f3cc3b0 fix: hint instruction 2024-07-31 08:46:50 +08:00
Chen, Tai-Lin 5605b27127 modify quests' text 2024-07-30 23:43:23 +08:00
wancat a7368110dd feat: update quest requirements to include tab 2024-07-29 22:19:32 +08:00
wancat bdb7def73a feat: finish tab quest 2024-07-29 22:16:58 +08:00
wancat b74866e50f wip: tab 2024-07-28 15:14:56 +08:00
wancat d577656eb1 feat: allow testing on creating new session 2024-07-28 15:14:45 +08:00
Lin, Hung-Shin 6400aa4a56 Style: Add file/folder icon in file tree (#149) 2024-07-28 12:22:59 +08:00
wancat 887d205ec1 quest: execute 2024-07-26 16:25:38 +08:00
wancat 8597c22b9f wip: execute tutorial content 2024-07-23 18:37:52 +08:00
Chen, Tai-Lin d8c0ee4b20 style: add file/folder icon and modify some style in visualization part 2024-07-23 18:30:57 +08:00
Chen, Tai-Lin 8180e9a0cf delete unused text 2024-07-23 12:15:03 +08:00
Chen, Tai-Lin 6daa11bff7 fix: start buttom languege issue 2024-07-23 11:20:56 +08:00
Chen, Tai-Lin 976dc6363c fix: wrong answer in answer.sh 2024-07-23 10:29:50 +08:00
wancat b449ea455a feat: add Session and stage active check 2024-03-08 01:05:26 +08:00
wancat 5bea2ea78c feat: add stage condition checks 2024-03-08 00:47:50 +08:00
wancat 77fe2d37b3 feat: create stage 2024-03-08 00:38:49 +08:00
wancat 65ad72223b feat: add checkfiles into Condition 2024-03-08 00:24:51 +08:00
wancat d612b2d6af feat: add file matcher 2024-03-08 00:13:38 +08:00
wancat b5e20766a3 refactor: rename paths 2024-03-07 00:50:42 +08:00
wancat e5f5590a67 feat: add "or" and "not" matchers 2024-03-07 00:42:49 +08:00
wancat e83d913852 refactor: separate matchers 2024-03-07 00:25:50 +08:00
wancat 8280f7073d refactor: remove unnecessary patter matcher 2024-03-06 23:57:35 +08:00
wancat 4f3752060d feat: condition matcher 2024-03-06 23:54:03 +08:00
wancat cb26e1dcb0 feat: add output matcher 2024-03-06 23:32:23 +08:00
wancat 9eb08b45f4 feat: add pwdmatcher and update error matcher 2024-03-06 23:28:28 +08:00
wancat 3b4a808131 feat: create pattern and command matcher 2024-03-06 23:06:52 +08:00
wancat a8a19ca902 Merge branch 'ts-project-refs' into game 2024-03-06 22:39:49 +08:00
wancat 6967e230d0 feat: enable swc and ts-node in server dev 2024-02-29 00:33:41 +08:00
wancat 6c111a4475 feat: use babel to replace ts-jest (improve performance) 2024-02-28 23:32:06 +08:00
wancat 15278dfab2 fix: docker 2024-02-28 23:32:06 +08:00
wancat 0644783cc4 feat: use project references to build dependencies efficiently 2024-02-28 23:32:06 +08:00
284 changed files with 10193 additions and 5726 deletions

No files matched your search

+2 -1
View File
@@ -1,4 +1,5 @@
**/node_modules
.git
**/dist
**/tsconfig.tsbuildinfo
**/tsconfig.tsbuildinfo
data
+50
View File
@@ -0,0 +1,50 @@
HOST=localhost
PORT=3000
BASE_URL=https://example.com
SECRET_KEY=YOU_SHOULD_CHANGE_THIS
BACKEND_URL=http://backend:3000
DOCKER_NETWORK=linux-odyssey-players
MONGO_ROOT_PASSWORD=adminpassword
MONGO_USER=commander
MONGO_PASS=commander
MONGO_DB=linux-odyssey
MONGO_URL=mongodb://commander:commander@db:27017/linux-odyssey
# Admin user credentials for analytics
ADMIN_USERNAME=admin
ADMIN_PASSWORD=admin
ADMIN_HOST=0.0.0.0
ADMIN_PORT=3001
# ADMIN_BASE_URL=http://0.0.0.0:3001
# Optional
# CONTAINER_EXPIRY=60 # 60 minutes
# SESSION_MAX_AGE=168 # 7 days
# TRUSTED_PROXIES=1.2.3.4
# valid configurations see isIP & isIPRange in https://github.com/validatorjs/validator.js?tab=readme-ov-file#validators
# RATE_LIMIT_MAX_FAILED_ATTEMPTS=5
# RATE_LIMIT_LOCKOUT_TIME=10
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# GITHUB_CLIENT_ID=
# GITHUB_CLIENT_SECRET=
# SURVEY_URL=
# BUG_REPORT_URL=
# Docker related config
# DOCKER_NETWORK=host
# QUEST_IMAGE=linuxodyssey/quest-base
# DOCKER_PREFIX=linuxodyssey/quest-
# To mount quest folder from host into container, for development purpose
# MOUNT_QUEST=true
# LOG_PATH=
# Testing, create a user with the following credentials, disable rate limiting
# TESTING=true
# TESTING_USERNAME=alex
# TESTING_PASSWORD=Alex1234
+38
View File
@@ -0,0 +1,38 @@
HOST=localhost
PORT=3000
BASE_URL=http://localhost:8000
MONGO_URL=mongodb://localhost:27017/odyssey-test
SECRET_KEY=sample-secret
DOCKER_NETWORK=linux-odyssey-players-dev
# Optional
# BACKEND_URL=http://host.docker.internal:3000
# CONTAINER_EXPIRY=60 # 60 minutes
# SESSION_MAX_AGE=168 # 7 days
# TRUSTED_PROXIES=1.2.3.4
# valid configurations see isIP & isIPRange in https://github.com/validatorjs/validator.js?tab=readme-ov-file#validators
# RATE_LIMIT_MAX_FAILED_ATTEMPTS=5
# RATE_LIMIT_LOCKOUT_TIME=10
# GOOGLE_CLIENT_ID=
# GOOGLE_CLIENT_SECRET=
# GITHUB_CLIENT_ID=
# GITHUB_CLIENT_SECRET=
# SURVEY_URL=
# BUG_REPORT_URL=
# Docker related config
# DOCKER_NETWORK=host
# QUEST_IMAGE=linuxodyssey/quest-base
# DOCKER_PREFIX=linuxodyssey/quest-
# To mount quest folder from host into container, for development purpose
# MOUNT_QUEST=true
# LOG_PATH=
# Testing, create a user with the following credentials, disable rate limiting
# TESTING=true
# TESTING_USERNAME=alex
# TESTING_PASSWORD=Alex1234
+33 -7
View File
@@ -20,9 +20,12 @@ module.exports = {
settings: {
'import/resolver': {
node: {
extensions: ['.js', '.jsx', '.ts', '.tsx'],
extensions: ['.js', '.ts'],
moduleDirectory: ['node_modules', 'packages', 'server/src'],
},
typescript: {
alwaysTryTypes: true,
},
typescript: {},
},
vue: {
version: '3.0', // Explicitly specify Vue 3
@@ -49,7 +52,7 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
'warn',
{
args: 'after-used',
argsIgnorePattern: '^_',
@@ -63,21 +66,44 @@ module.exports = {
// Allow missing file extensions
'import/extensions': [
'warn',
'off',
'ignorePackages',
{
js: 'off',
js: 'never',
jsx: 'never',
ts: 'never',
tsx: 'never',
},
],
'no-unused-vars': 'warn',
'no-useless-constructor': 'off',
'@typescript-eslint/no-useless-constructor': 'error',
'no-empty-function': 'off',
'@typescript-eslint/no-empty-function': 'error',
'no-shadow': 'off',
'@typescript-eslint/no-shadow': 'error',
'no-restricted-imports': ['error', '../*'],
'import/no-relative-packages': 'off',
'no-await-in-loop': 'off',
},
overrides: [
{
files: ['**/tests/**/*.js', '**/tests/**/*.ts', '**/*.test.ts'], // Adjust the pattern to match your test files
files: ['**/tests/**/*.js', '**/tests/**/*.ts', '**/*.test.ts'],
rules: {
'import/extensions': 'off', // Turn off the rule for test files
'import/extensions': 'off',
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['server/**/*.ts'],
rules: {
'import/no-extraneous-dependencies': 'off',
},
},
{
files: ['**/packages/**/*.ts'],
rules: {
'no-restricted-imports': 'off',
'import/no-extraneous-dependencies': 'off',
},
},
+4 -3
View File
@@ -18,9 +18,9 @@ on:
secrets:
DOCKERHUB_USERNAME:
required: true
required: false
DOCKERHUB_TOKEN:
required: true
required: false
jobs:
build-and-push:
@@ -42,6 +42,7 @@ jobs:
type=semver,pattern={{version}}
- name: Login to Docker Hub
if: always()
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@@ -53,5 +54,5 @@ jobs:
context: .
file: ${{ inputs.dockerfilePath }}
target: ${{ inputs.target }}
push: true
push: ${{ secrets.DOCKERHUB_USERNAME != '' }}
tags: ${{ steps.meta.outputs.tags }}
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
uses: ./.github/workflows/build-docker-image.yml
with:
imageName: 'linuxodyssey/analytics'
dockerfilePath: './packages/analytics/Dockerfile'
dockerfilePath: './services/analytics/Dockerfile'
target: prod
secrets: inherit
-9
View File
@@ -2,15 +2,6 @@ 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:
+2 -1
View File
@@ -1,6 +1,7 @@
name: E2E Tests
on: [push, pull_request]
on:
workflow_dispatch:
jobs:
e2e:
-1
View File
@@ -12,7 +12,6 @@ concurrency:
jobs:
ci-checks:
uses: ./.github/workflows/ci-checks.yml
secrets: inherit
build:
needs: ci-checks
-1
View File
@@ -10,7 +10,6 @@ concurrency:
jobs:
ci-checks:
uses: ./.github/workflows/ci-checks.yml
secrets: inherit
build:
uses: ./.github/workflows/build.yml
-1
View File
@@ -12,7 +12,6 @@ concurrency:
jobs:
ci-checks:
uses: ./.github/workflows/ci-checks.yml
secrets: inherit
build:
needs: ci-checks
+9 -1
View File
@@ -1,3 +1,9 @@
config
.quest
tmp
services/terminal-service/terminal-service
bin
# Logs
logs
*.log
@@ -85,6 +91,7 @@ built
.env.test.local
.env.production.local
.env.local
.env.prod
# parcel-bundler cache (https://parceljs.org/)
.cache
@@ -137,4 +144,5 @@ dist
.pnp.*
#screenshots
tests/cypress/screenshots
tests/cypress/screenshots
services/cmd-hook/cmd-hook
+7 -1
View File
@@ -1,5 +1,11 @@
{
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"yaml.schemas": {
"packages/game/quest.json": "quests/**/game.yml"
},
"[go]": {
"editor.defaultFormatter": "golang.go"
}
}
+121 -74
View File
@@ -3,11 +3,24 @@
[![Production](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/production.yml/badge.svg)](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/production.yml)
[![Staging](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/staging.yml/badge.svg)](https://github.com/linux-odyssey/linux-odyssey/actions/workflows/staging.yml)
## TLDR
yarn install
cp .env.sample .env
docker compose up -d
yarn dev
## Requirements
- NodeJS
- NodeJS v20+
- Docker
- Yarn
- Yarn: v1.22
## Known Issues
- MongoDB requires x86 architecture to run for v5+ versions. You can track [this issue](https://github.com/linux-odyssey/linux-odyssey/issues/166) for updates.
Windows user please refer to [docs/windows-setup.md](docs/windows-setup.md) for setup instructions.
## Installation
@@ -15,30 +28,61 @@
cd linux-odyssey
yarn install
## Development
## Project Structures
Run mongoDB first:
1. **app**: frontend
2. **server**: backend
3. **quests**: tutorials scripts for each quest
4. **packages**: other packages
1. **analytics**: admin dashboard to analyze collected data
2. **constants**: some shared config values for app and server
3. **container**: the script run in container to capture users' command
4. **file-graph**: the algorithm to construct and modify file graph, used in both app and server
5. **models**: definitions of MondoDB collections (data schema)
6. **utils**: other utility code shared between packages
7. **game (in the future)**: core logic of game playing, will be extracted from server
## Configuration
The configuration is stored in the `.env` file. You can copy from the sample file.
cp .env.sample .env
## Docker Setup
### Docker compose files explaination
1. **docker-compose.yml**: The one used for development. It sets up MongoDB and builds base quest image.
2. **docker-compose.prod.yml**: The one used for production deployment. It has a Nginx service, a backend, a database, an analytics service, and creates isolated networks for player containers.
3. **docker-compose.testing.yml**: Similar to production config, but sets up an one-time environment for end-to-end testing.
### Common Development Setup
This sets up a development database, and running development server for frontend and backend.
Pull the latest images:
docker compose pull
Start the containers:
docker compose up -d
The db will automatically restarts by Docker.
This will start the db. it will automatically restarts by Docker.
Build up dependencies:
Building quest images:
yarn build:deps
docker compose build base
Run dev server (frontend, backend):
### Build Quest Images in Details
yarn dev
In each quest folder, there is a `game.yml` file, which is the quest definition.
If you want to enable social login, you should have OAuth client token in a `.env` file:
### Build Quest Images
In each quest folder, there is a `game.yml` file, which is the quest definition. And a optional `Dockerfile` to overwrite the base image. If you want to include additional files, put them in a `home` folder. The files in `home` will **not** be copied to the image automatically, you have to copy them in the `Dockerfile`.
If you want to include additional files, put them in a `home` folder. The files in `home` will be copied to the user's home on container creation automatically.
quests/
├── Dockerfile # Base image
├── entrypoint.sh # Entrypoint for the container
├── discover
│ └── game.yml
├── helloworld
@@ -54,14 +98,56 @@ In each quest folder, there is a `game.yml` file, which is the quest definition.
└── home
└── ancient_scroll.txt
An optional `Dockerfile` can overwrite the base image. You can use this to install additional dependencies or modify the environment.
Customized `entrypoint.sh` for each quest is not yet supported.
To build all quests:
yarn build:quests
The base image is built with docker-compose, so you can use `docker compose build base` to rebuild it. Once you rebuild the base image, you have to rebuild all quests.
### Optional: Mounting Host Directory
To update the home content of a quest, simply re-create a new session will do. The home content is mounted and copied on container creation.
To further improve the development experience, we can mount the host directory to the container in the pattern:
/path/to/linux-odyssey/quests/${quest_id}/home:/home/commander
You can edit the files on your host, and the changes will be reflected in the container. This is disabled by default. To enable it, you can set the following environment variables:
MOUNT_QUEST=true
### Optional: Mounting terminal service & cmd-hook
To make the development of terminal services & cmd-hook smoother, we can enable `MOUNT_CLI` to mount the local binaries into container.
MOUNT_CLI
And you can run `air` inside of `services/terminal-service` or `services/cmd-hook` to get auto recompiling.
## Development
Copy `.env.sample` to `.env` and customize it for your needs.
Run dev server (frontend, backend):
yarn dev
To develop analytics dashboard:
yarn analytics
The default username and password is `alex` and `Alex1234`.
## Testing
Unit tests:
yarn test:unit
Run Cypress locally, using the current development containers:
docker compose up -d
@@ -78,76 +164,37 @@ Run Cypress in container, create brand-new containers along with it:
# tearing off the containers
./docker-scripts.sh down
### Deployment
## Deployment
Create production env
cp .env.prod.sample .env.prod
Edit `.env.prod`
BASE_URL=https://yourdomain.com # IMPORTANT to make login work
SECRET_KEY=xxxx # Change it to something safe!
Start the service
# Build the images (only allowed for maintainers)
docker compose -f docker-compose.prod.yml build
docker compose -f docker-compose.prod.yml push
# Server side
docker compose -f docker-compose.prod.yml pull
yarn build:quests
docker compose -f docker-compose.prod.yml up -d
### Necessary Configuration
To enable social login, you should have OAuth client token in a `.env` file:
You can copy from `example.env`
You can copy from `.env.sample`
# .env
BASE_URL=https://example.com
GOOGLE_CLIENT_ID=...
GOOGLE_CLIENT_SECRET=...
**YOU MUST CHANGE THE SECRET_KEY** in `.env` file.
To enable it:
SECRET_KEY=your-secret-key
docker compose --env-file .env -f docker-compose.prod.yml up -d
You can generate a random key by running
## Standalone App (without Docker)
Source: `app/`
# Connect to dev server (https://odyssey.wancat.cc)
yarn app
# Connect to localhost (http://localhost:3000)
yarn app:local
# Connect to anyhost
API_ENDPOINT=http://example.com yarn app:local
## Swagger
docker compose up -d swagger
Open http://localhost:8080 to open Swagger
## CLI terminal client (Docker not required)
yarn cli
Usage: client [options] [command]
Options:
-s, --session <string> Session ID
-c, --create Create a new session
-h, --host <string> Server host (default: "http://localhost:3000")
--help display help for command
Commands:
list List all sessions
connet to recent session:
yarn cli
connect to new session:
yarn cli -c
connect to specific session:
yarn cli -s 64d0de0367459c13004bc83f
connect to other host:
yarn cli -h https://example.com
list sessions:
yarn cli list
yarn setup
+7 -18
View File
@@ -1,31 +1,20 @@
# Base stage
FROM node:20-alpine as base
FROM node:20-alpine AS build
WORKDIR /app
COPY package.json yarn.lock tsconfig.json ./
COPY packages/file-graph/package.json ./packages/file-graph/package.json
COPY packages/utils/package.json ./packages/utils/package.json
COPY packages/constants/package.json ./packages/constants/package.json
COPY packages/models/package.json ./packages/models/package.json
COPY package.json yarn.lock ./
COPY app/package.json ./app/package.json
COPY server/package.json ./server/package.json
RUN yarn install --frozen-lockfile
# Dev stage
FROM base as dev
WORKDIR /app/app
CMD [ "yarn", "dev:docker", "--host", "0.0.0.0" ]
# Build stage
FROM base as build
COPY packages/file-graph ./packages/file-graph
COPY packages/utils ./packages/utils
COPY packages/constants ./packages/constants
COPY packages/models ./packages/models
COPY tsconfig.json ./tsconfig.json
COPY server ./server
COPY packages ./packages
COPY app ./app
RUN yarn workspace app build
# Production stage
FROM nginx:stable-alpine as prod
FROM nginx:stable-alpine AS prod
COPY --from=build /app/app/dist /usr/share/nginx/html
COPY deployment/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
+12 -13
View File
@@ -5,26 +5,24 @@
"scripts": {
"dev": "vite",
"dev:docker": "API_TARGET=http://backend:3000 vite",
"build": "vue-tsc -b && vite build",
"type-check": "vue-tsc -b",
"build": "vue-tsc && vite build",
"type-check": "vue-tsc",
"preview": "vite preview"
},
"dependencies": {
"@formbricks/js": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^6.4.0",
"@fortawesome/free-brands-svg-icons": "^6.4.0",
"@fortawesome/free-regular-svg-icons": "^6.4.0",
"@fortawesome/free-solid-svg-icons": "^6.4.0",
"@fortawesome/vue-fontawesome": "^3.0.3",
"@linux-odyssey/constants": "*",
"@linux-odyssey/file-graph": "*",
"@linux-odyssey/models": "*",
"@linux-odyssey/utils": "*",
"@trpc/client": "^11.1.0",
"@types/dompurify": "^3.0.5",
"axios": "^1.6.0",
"dompurify": "^3.0.6",
"echarts": "^5.4.3",
"axios": "^1.8.2",
"dompurify": "^3.2.4",
"marked": "^10.0.0",
"pinia": "^2.1.7",
"reconnecting-websocket": "^4.4.0",
"socket.io-client": "^4.7.1",
"validator": "^13.11.0",
"vue": "^3.3.4",
@@ -37,13 +35,14 @@
},
"devDependencies": {
"@types/validator": "^13.11.9",
"@vitejs/plugin-vue": "^4.5.2",
"@vitejs/plugin-vue": "^5.2.2",
"@vue/compiler-sfc": "^3.3.13",
"@vue/tsconfig": "^0.7.0",
"autoprefixer": "^10.4.14",
"postcss": "^8.4.31",
"tailwindcss": "^3.3.3",
"typescript": "^5.3.3",
"vite": "^4.5.3",
"vue-tsc": "^2.0.10"
"typescript": "~5.7.2",
"vite": "^6.3.1",
"vue-tsc": "^2.2.8"
}
}
File renamed without changes.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 1.3 MiB

+15 -1
View File
@@ -20,7 +20,21 @@ function nodeClass() {
</script>
<template>
<a href="#" :class="nodeClass()">{{ node.discovered ? node.name : '???' }}</a>
<a href="#" :class="nodeClass()">
<font-awesome-icon
v-if="node.type === 'directory'"
:icon="['far', 'folder']"
class="text-text-primary size-8"
/>
<font-awesome-icon
v-else
:icon="['far', 'file']"
class="text-text-primary size-8"
/>
<br />
{{ node.discovered ? node.name : '???' }}
{{ node.name === '' && node.discovered ? '/' : '' }}
</a>
<ul :class="nodeClass()" v-if="node.children && node.children.length > 0">
<li :class="nodeClass()" v-for="child in node.children" :key="child.path">
<FileNode :node="child" :pwd="pwd" />
+24 -4
View File
@@ -31,11 +31,31 @@ const html = computed(() => {
.markdown-content :deep(ol) {
list-style: decimal;
padding-left: 1em;
line-height: 1.2;
}
.markdown-content :deep(code) {
background-color: #282a33;
.markdown-content :deep(ul) {
list-style: disc;
padding-left: 1em;
line-height: 1.2;
}
.markdown-content :deep(pre) {
background-color: #2e3440;
border-radius: 0.5rem;
padding: 0.5rem;
font-size: 0.9rem;
color: #d8dee9;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
border: 1px solid #4c566a;
line-height: 1.5;
font-family: 'monospace';
}
.markdown-content :deep(code:not(pre code)) {
background-color: #2e3440;
border-radius: 0.25rem;
padding: 0.25rem;
font-size: 0.875rem;
padding: 0.2rem 0.4rem;
font-size: 0.9rem;
color: #d8dee9;
font-family: 'monospace';
}
</style>
+121
View File
@@ -0,0 +1,121 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { ref, defineProps, onMounted } from 'vue'
import MarkdownText from './MarkdownText.vue'
import { trpc } from '../utils/trpc'
const props = defineProps({
questTitle: {
type: String,
required: true,
},
questId: {
type: String,
required: true,
},
questUnlocked: {
type: Boolean,
required: true,
},
})
const emit = defineEmits(['closeIntro'])
const showIntro = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const { questTitle, questId, questUnlocked } = props
const questInstruction = ref('')
const questColor = ref('')
const questTextColor = ref('')
const closeModal = () => {
showIntro.value = false
emit('closeIntro', true)
router.push({ name: 'map' })
}
const handleQuests = (id: string) => {
if (questUnlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
toast.warning('你還沒完成前一個關卡!')
}
}
const handleColour = () => {
if (questUnlocked) {
questColor.value = '#00ff00'
questTextColor.value = '#000000'
} else {
questColor.value = '#505050'
questTextColor.value = '#a0a0a0'
}
}
onMounted(async () => {
const quest = await trpc.quests.getQuestDetail.query(questId)
questInstruction.value = quest.instruction
handleColour()
})
</script>
<template>
<div v-if="showIntro" class="modal">
<div class="modal-content bg-bg-secondary rounded-lg">
<span class="close text-text content-between" @click="closeModal"
>&times;</span
>
<h2
id="QuestTitle"
class="text-2xl text-text mb-5 w-full inline-flex justify-center"
>
{{ questTitle }}
</h2>
<div class="w-full h-3/5 p-8">
<div class="px-5 whitespace-pre-wrap">
<MarkdownText
class="text-text markdown-content"
:content="questInstruction"
/>
</div>
</div>
<a
id="StartAdventure"
target="_blank"
@click="handleQuests(questId)"
:style="{ backgroundColor: questColor, color: questTextColor }"
class="inline-flex justify-center rounded-lg text-base font-black py-3 mt-5 w-full text-lg"
>開始冒險</a
>
</div>
</div>
</template>
<style scoped>
.modal {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1;
}
.modal-content {
position: relative;
margin: 10% auto;
padding: 20px;
width: 60%;
height: 50%;
}
.close {
position: absolute;
top: 0;
right: 10px;
font-size: 30px;
cursor: pointer;
}
.markdown-content {
text-align: left;
}
</style>
+233 -160
View File
@@ -1,181 +1,254 @@
<!-- eslint-disable no-console -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ref, onMounted, watch, computed } from 'vue'
import { useToast } from 'vue-toastification'
import { use, init } from 'echarts/core'
import { GraphChart } from 'echarts/charts'
import { SVGRenderer } from 'echarts/renderers'
import {
TitleComponent,
TooltipComponent,
DataZoomComponent,
} from 'echarts/components'
import { DAG } from '@linux-odyssey/utils'
import api from '../utils/api'
import { NodeImage } from '../img/svg.js'
import { DAG } from '../../../packages/utils'
import { trpc } from '../utils/trpc'
import useUserProfile from '../store/userProfile'
import QuestIntro from './QuestIntro.vue'
const store = useUserProfile()
const marginX = 500
const marginY = 300
const chartContainer = ref(null)
let chartInstance = null
const fullwidth = window.screen.width
use([
GraphChart,
SVGRenderer,
TitleComponent,
TooltipComponent,
DataZoomComponent,
])
async function getQuests() {
try {
const res = await api.get('/quests')
return res.data
} catch (err) {
console.error(err)
throw err
}
}
const genOption = (nodes: any, edges: any) => ({
renderer: 'svg',
tooltip: {},
animationDurationUpdate: 1500,
animationEasingUpdate: 'quinticInOut',
// dataZoom: [
// {
// type: 'slider', // This type means "slider data zoom" which provides a slider bar for zooming
// orient: 'vertical', // This makes the data zoom component vertical
// start: 0, // The starting percentage of the window out of the data extent
// end: 100, // The ending percentage of the window out of the data extent
// show: true,
// },
// ],
series: [
{
type: 'graph',
layout: 'none',
symbol: () => {
return NodeImage
},
symbolSize: [fullwidth / 13, fullwidth / 30],
roam: 'move',
zoom: 1.5,
center: ['120%', '30%'],
label: {
show: true,
fontSize: fullwidth / 90,
},
edgeSymbol: ['pin', 'arrow'],
edgeSymbolSize: [2, 15],
edgeLabel: {
fontSize: 25,
},
data: nodes,
// links: [],
links: edges,
lineStyle: {
width: 3,
color: 'gray',
opacity: 4,
curveness: 0.08,
},
itemStyle: {
color: (params: any) => {
const {
data: { completed, unlocked },
} = params
if (completed) {
return '#00ff00'
}
if (unlocked) {
return '#ADADB5'
}
return '#505050'
},
},
},
],
})
function getOption(quests: any, progress: any) {
const dag = new DAG(quests)
const nodes = dag.getNodes().map((node) => ({
id: node._id,
name: node.title,
y: marginX * node.index - (marginX * dag.getLayer(node._id)) / 2,
x: marginY * node.layer * 2,
completed: progress[node._id]?.completed || false,
unlocked: node.requirements.every((req: any) => progress[req]?.completed),
}))
const lineAppearence = (node: any) => {
try {
const { unlocked } = node
return {
type: unlocked ? 'line' : 'dashed',
color: unlocked ? '#ADADB5' : '#454552',
}
} catch (err) {
console.error(err)
throw err
}
}
const edges = dag.getEdgesArray().map((edge: any) => ({
source: edge[0],
target: edge[1],
lineStyle: lineAppearence(nodes.find((node) => node.id === edge[1])),
}))
return genOption(nodes, edges)
}
const router = useRouter()
const toast = useToast()
function initChart(option: any) {
chartInstance = init(chartContainer.value)
chartInstance.setOption(option)
chartInstance.on('click', (params: any) => {
const {
data: { id, unlocked },
} = params
const svgWidth = ref(window.innerWidth)
const svgHeight = ref(window.innerHeight)
if (unlocked) {
router.push({ name: 'game', params: { questId: id } })
} else {
toast.warning('你還沒完成前一個關卡!')
}
})
const marginX = 120
const marginY = 120
const nodeWidth = 160
const nodeHeight = 50
const opened = ref<Node | null>(null)
type Node = {
id: string
title: string
x: number
y: number
index: number
completed: boolean
unlocked: boolean
}
type Edge = {
source: Node
target: Node
}
const graphData = ref<{ nodes: Node[]; edges: Edge[] }>({
nodes: [],
edges: [],
})
const isDragging = ref(false)
const dragStartX = ref(0)
const dragStartY = ref(0)
const offsetX = ref(0)
const offsetY = ref(0)
function startDrag(event: MouseEvent) {
isDragging.value = true
dragStartX.value = event.clientX - offsetX.value
dragStartY.value = event.clientY - offsetY.value
}
function drag(event: MouseEvent) {
if (!isDragging.value) return
offsetX.value = event.clientX - dragStartX.value
offsetY.value = event.clientY - dragStartY.value
}
function endDrag() {
isDragging.value = false
}
async function computeGraphData() {
if (!store.progress) return
try {
const quests = await trpc.quests.getQuests.query()
const dag = new DAG(quests)
const nodesValues = dag.getNodes()
const nodes = nodesValues.map((node) => ({
id: node.id,
title: node.title,
x: marginX * node.layer * 2,
y:
marginY * node.index -
(marginX * dag.getLayer(node.id)) / 2 +
svgHeight.value / 2,
completed: store.progress[node.id]?.completed || false,
unlocked: node.requirements.every(
(req: string) => store.progress[req]?.completed
),
index: node.index,
}))
const edges = dag
.getEdgesArray()
.map((edge) => ({
source: nodes.find((n) => n.id === edge[0]),
target: nodes.find((n) => n.id === edge[1]),
}))
.filter(
(edge): edge is Edge =>
edge.source !== undefined && edge.target !== undefined
)
graphData.value = { nodes, edges }
} catch (error) {
console.error('Error computing graph data:', error)
toast.error('Failed to load quest data')
}
}
function handleNodeClick(node: Node) {
opened.value = node
}
function closeIntro(close: boolean) {
if (close) {
opened.value = null
}
}
onMounted(async () => {
await store.loadUserProfile()
const quests = await getQuests()
const option = getOption(quests, store.progress)
initChart(option)
await computeGraphData()
})
watch(() => store.progress, computeGraphData, { deep: true })
const curvedPath = computed(() => {
return (source: Node, target: Node) => {
// const dx = target.x - source.x
// const dy = target.y - source.y
// const dr = Math.sqrt(dx * dx + dy * dy)
const dr = 0
const sweep = target.index > source.index ? 0 : 1 // Determines if the arc should curve up or down based on node index
return `M${source.x},${source.y}A${dr},${dr} 0 0,${sweep} ${target.x},${target.y}`
}
})
const nodeStyle = computed(() => {
return (node: Node) => {
if (node.completed) return 'completed'
if (node.unlocked) return 'unlocked'
return 'locked'
}
})
const edgeStyle = computed(() => {
return (edge: Edge) => {
return edge.target.unlocked ? 'unlocked' : 'locked'
}
})
</script>
<template>
<div class="relative bg-black w-full h-full flex">
<img
src="../img/catelogbg.svg"
alt="bg"
class="p-2 stroke-2 scale-y-120 justify-center items-center"
/>
<h1
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
<div
class="flex place-content-center w-full bg-catelogbg bg-cover bg-scroll"
>
踏上你的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>
<h1
class="p-10 absolute w-fit z-2 font-mono flex flax-wrap text-xl"
style="width: 30%; height: 6%; font-size: 3vh; color: #00ff00"
>
踏上你的Linux冒險之旅吧!
</h1>
<svg
:width="svgWidth"
:height="svgHeight"
class="absolute"
@mousedown="startDrag"
@mousemove="drag"
@mouseup="endDrag"
@mouseleave="endDrag"
>
<g :transform="`translate(${offsetX}, ${offsetY})`">
<g
v-for="edge in graphData.edges"
:key="`${edge.source.id}-${edge.target.id}`"
>
<path
:d="curvedPath(edge.source, edge.target)"
:class="['edge', edgeStyle(edge)]"
/>
</g>
<g
v-for="node in graphData.nodes"
:key="node.id"
@click="handleNodeClick(node)"
>
<rect
:x="node.x - nodeWidth / 2"
:y="node.y - nodeHeight / 2"
:width="nodeWidth"
:height="nodeHeight"
rx="10"
ry="10"
:class="['node', nodeStyle(node)]"
/>
<text
:x="node.x"
:y="node.y"
text-anchor="middle"
alignment-baseline="middle"
:class="['node-text', nodeStyle(node)]"
font-size="18"
>
{{ node.title }}
</text>
</g>
</g>
</svg>
<QuestIntro
:questTitle="opened.title"
:questId="opened.id"
:questUnlocked="opened.unlocked"
v-if="opened"
@close-intro="closeIntro"
/>
</div>
</div>
</template>
<style scoped>
svg {
overflow: visible;
}
.node {
&.completed {
fill: #6cf76c;
}
&.unlocked {
fill: #8c8c92;
}
&.locked {
fill: #505050;
}
}
.node-text {
&.completed {
fill: #1d1d1d;
}
&.unlocked {
fill: #ffffff;
}
&.locked {
fill: #a0a0a0;
}
}
.edge {
stroke-width: 3;
fill: none;
&.unlocked {
stroke: #adadb5;
stroke-dasharray: none;
}
&.locked {
stroke: #454552;
stroke-dasharray: 5, 5;
}
}
</style>
+18
View File
@@ -9,9 +9,11 @@ import CompleteAnimation from '../../img/complete-animation.json'
const showModal = ref(true) // Controls whether the modal is visible or not
const router = useRouter()
const toast = useToast()
const closeModal = () => {
showModal.value = false
}
const defineComponent = () => ({
components: {
Vue3Lottie,
@@ -22,6 +24,7 @@ const defineComponent = () => ({
}
},
})
const backtoMap = async () => {
try {
router.push({ name: 'map' })
@@ -67,6 +70,21 @@ setTimeout(() => {
>回到地圖</a
>
</div>
<div style="position: relative; height: 100vh; overflow: auto">
<iframe
src="https://app.formbricks.com/s/clyffdm0c0be2wh6335clle0l"
frameborder="0"
style="
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border: 0;
"
>
</iframe>
</div>
</div>
</template>
+25 -17
View File
@@ -1,8 +1,8 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import useSession from '../../store/session'
import { surveyUrl } from '../../config'
import { useSession } from '../../store/session'
import { openBugReport, openQuestSurvey } from '../../utils/formbricks'
const router = useRouter()
const sessionStore = useSession()
@@ -21,31 +21,39 @@ const continuePlay = async () => {
</script>
<template>
<section
class="h-fit w-fit px-1 py-1 grid grid-cols-4 gap-2 place-content-between"
>
<button id="survey" class="bg-bg-secondary rounded-lg p-2">
<a title="Survey" :href="surveyUrl" target="_blank" class="h-5 w-5">
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline ml-1 md:flex">問卷</p>
</a>
<section class="mt-2 flex flex-row gap-3 justify-center">
<button
id="survey"
class="bg-bg-secondary rounded-lg p-2"
@click="openQuestSurvey"
>
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline md:flex">問卷</p>
</button>
<button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<button
title="Bug Report"
@click="openBugReport"
class="bg-bg-secondary rounded-lg p-2"
>
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
<p class="text-text-primary inline ml-1 md:flex">回報</p>
</button>
<!-- <button disabled id="solution" class="bg-bg-disabled rounded-lg p-2">
<font-awesome-icon
:icon="['far', 'circle-question']"
class="text-text-disabled px-1"
/>
<p class="text-text-disabled inline ml-1 md:flex">解答</p>
</button>
</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 px-1"
/>
<p class="text-text-primary inline ml-1 hidden md:flex">重來</p>
<p class="text-text-primary inline ml-1 md:flex">重來</p>
</button>
<button
id="continue"
@@ -56,7 +64,7 @@ const continuePlay = async () => {
:icon="['far', 'circle-right']"
class="text-text-primary px-1"
/>
<p class="text-text-primary inline ml-1 hidden md:flex">繼續</p>
<p class="text-text-primary inline ml-1 md:flex">繼續</p>
</button>
</section>
</template>
+28 -10
View File
@@ -1,12 +1,13 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import useSession from '../../store/session'
import { useSession } from '../../store/session'
import MarkdownText from '../MarkdownText.vue'
const sessionStore = useSession()
const current = ref(-1)
const hintDetails = ref<HTMLDetailsElement | null>(null)
const length = computed(() => {
return sessionStore.session?.responses.length ?? 0
return sessionStore.session?.responses?.length ?? 0
})
const disabled = computed(() => {
@@ -14,6 +15,7 @@ const disabled = computed(() => {
})
watch(length, () => {
console.log('length update', length.value)
current.value = length.value - 1
})
@@ -28,6 +30,19 @@ const right = () => {
current.value += 1
}
}
const response = computed(() => {
if (!sessionStore.session) return null
if (current.value < 0) return null
if (current.value >= sessionStore.session.responses.length) return null
return sessionStore.session.responses[current.value]
})
watch(response, () => {
if (hintDetails.value) {
hintDetails.value.open = false
}
})
</script>
<template>
@@ -62,27 +77,30 @@ const right = () => {
@click="right"
:disabled="disabled"
>
<font-awesome-icon :icon="['fas', 'arrow-right']" class="text-text" />
<font-awesome-icon
:icon="['fas', 'arrow-right']"
:class="disabled ? 'text-gray-600' : 'text-text'"
/>
</button>
</div>
</div>
<div id="hint" class="bg-bg p-8 overflow-y-auto">
<ul>
<li
v-for="response in sessionStore.session?.responses[current]"
v-if="response"
:key="response.type"
class="text-text font-xl whitespace-pre-wrap"
>
<div v-for="content in response.content" :key="content">
<MarkdownText :content="content" />
</div>
<MarkdownText :content="response.content" />
</li>
<li
v-for="hint in sessionStore.session?.hints[current]"
:key="hint"
v-if="response?.hint"
class="text-text-primary font-xl whitespace-pre-wrap"
>
<MarkdownText :content="hint" />
<details ref="hintDetails">
<summary class="cursor-pointer">提示</summary>
<MarkdownText :content="response.hint" />
</details>
</li>
</ul>
<br />
+3 -3
View File
@@ -1,12 +1,12 @@
<script setup lang="ts">
import { computed } from 'vue'
import MarkdownText from '../MarkdownText.vue'
import useSession from '../../store/session'
import { Task } from '../../types'
import { useSession } from '../../store/session'
const sessionStore = useSession()
const tasks = computed(() => {
return sessionStore.session?.tasks.map((task: Task) => {
console.log('session', sessionStore.session)
return sessionStore.session?.tasks.map((task) => {
const prefix = task.completed ? '✓' : '➤'
const color = task.completed ? 'text-text-primary' : 'text-text'
return {
+1 -1
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { useToast } from 'vue-toastification'
import { computed } from 'vue'
import useSession from '../../store/session'
import { useSession } from '../../store/session'
const toast = useToast()
const sessionStore = useSession()
+32 -11
View File
@@ -1,23 +1,44 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { useTerminal } from '../../store/session'
import { onMounted, onUnmounted, ref } from 'vue'
import { useSession, useTerminal } from '../../store/session'
const sessionStore = useSession()
const terminal = ref<HTMLElement | null>(null)
const term = useTerminal()
onMounted(() => {
const term = useTerminal()
if (!terminal.value) return
term.mount(terminal.value)
window.addEventListener('resize', () => term.resizeScreen())
const handleResize = () => term.resizeScreen()
window.addEventListener('resize', handleResize)
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
})
</script>
<template>
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text p-1" />
<button id="Terminal" class="text-text h-[5%]">終端機</button>
<div
id="terminal"
ref="terminal"
class="bg-bg-secondary h-[calc(100%-5%)] p-0.5"
></div>
<div class="flex flex-col h-full">
<div class="flex items-center gap-2 p-1">
<font-awesome-icon :icon="['fas', 'terminal']" class="text-text" />
<button id="Terminal" class="text-text">終端機</button>
<a
class="text-sm font-normal text-gray-200 ml-10"
:href="`/terminal/${sessionStore.session?.containerName}?token=${sessionStore.session?.token}`"
target="_blank"
>
Open Terminal in New Tab
</a>
</div>
<div
id="terminal"
ref="terminal"
class="bg-bg-secondary flex-grow overflow-hidden px-2"
style="height: calc(100vh - 40px)"
></div>
</div>
</template>
+11 -19
View File
@@ -1,6 +1,6 @@
<script setup lang="ts">
import FileNode from '../FileNode.vue'
import useSession from '../../store/session'
import { useSession } from '../../store/session'
const sessionStore = useSession()
</script>
@@ -48,14 +48,14 @@ const sessionStore = useSession()
position: absolute;
top: 0;
right: 50%;
border-top: 1px solid #00ff00;
border-top: 2px solid #00ff00;
width: 50%;
height: 20px;
}
.tree li::after {
right: auto;
left: 50%;
border-left: 1px solid #00ff00;
border-left: 2px solid #00ff00;
}
.tree li:only-child::after,
@@ -73,7 +73,7 @@ const sessionStore = useSession()
}
.tree li:last-child::before {
border-right: 1px solid #00ff00;
border-right: 2px solid #00ff00;
border-radius: 0 5px 0 0;
-webkit-border-radius: 0 5px 0 0;
-moz-border-radius: 0 5px 0 0;
@@ -89,7 +89,7 @@ const sessionStore = useSession()
position: absolute;
top: 0;
left: 50%;
border-left: 1px solid #00ff00;
border-left: 2px solid #00ff00;
width: 0;
height: 20px;
}
@@ -100,7 +100,7 @@ const sessionStore = useSession()
text-decoration: none;
color: #ffffff;
font-family: arial, verdana, tahoma;
font-size: 11px;
font-size: 12px;
display: inline-block;
border-radius: 5px;
@@ -112,18 +112,10 @@ const sessionStore = useSession()
-moz-transition: all 0.5s;
}
a.discovered {
background: #526b4e;
color: #000;
border: 1px solid #94a0b4;
}
ul.discovered,
li.discovered {
border-color: #94a0b4;
}
a.pwd {
box-shadow: 0 0 40px rgba(255, 255, 0, 20);
.tree li a.pwd {
border: 3px solid #00ff00;
background: #008080;
font-weight: bold;
font-size: 13px;
}
</style>
@@ -3,8 +3,9 @@ import { onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import { logout } from '../../utils/auth'
import { bugReportUrl, surveyUrl } from '../../config'
import useSession from '../../store/session'
import { trpc } from '../../utils/trpc'
import { openBugReport } from '../../utils/formbricks'
import { useSession } from '../../store/session'
import useUserProfile from '../../store/userProfile'
const userStore = useUserProfile()
@@ -25,6 +26,11 @@ const handleLogout = async () => {
}
}
const openSurvey = async () => {
const surveyUrl = await trpc.links.surveyLink.query()
window.open(surveyUrl, '_blank')
}
onMounted(async () => {
try {
await userStore.loadUserProfile()
@@ -60,18 +66,18 @@ onMounted(async () => {
>
{{ userStore.username }}
</p>
<a
<button
title="Survey"
:href="surveyUrl"
target="_blank"
class="md:h-5 md:w-5 px-1.5 w-auto"
@click="openSurvey"
>
<font-awesome-icon
:icon="['fas', 'file-invoice']"
class="text-text-primary inline"
/>
<span class="text-text sm:hidden px-2">Survey</span>
</a>
</button>
<RouterLink
title="LeaderBoard"
to="/leaderboard"
@@ -84,15 +90,14 @@ onMounted(async () => {
<font-awesome-icon :icon="['fas', 'map']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Map</span>
</RouterLink>
<a
<button
title="Bug Report"
:href="bugReportUrl"
target="_blank"
@click="openBugReport"
class="md:h-5 md:w-5 px-1.5 w-auto"
>
<font-awesome-icon :icon="['fas', 'bug']" class="text-text-primary" />
<span class="text-text sm:hidden px-2">Bug Report</span>
</a>
</button>
<button
title="Sign Out"
@click="handleLogout"
+32 -28
View File
@@ -3,28 +3,30 @@ import { library } from '@fortawesome/fontawesome-svg-core'
/* import specific icons */
import {
faBars,
faArrowLeft,
faList,
faGear,
faTriangleExclamation,
faExpand,
faTerminal,
faArrowRotateLeft,
faArrowRightFromBracket,
faArrowRight,
faArrowRightFromBracket,
faArrowRotateLeft,
faBars,
faBug,
faCircleExclamation,
faMap,
faTrophy,
faFileInvoice,
faEnvelope,
faExpand,
faFileInvoice,
faGear,
faList,
faMap,
faPlay,
faTerminal,
faTriangleExclamation,
faTrophy,
} from '@fortawesome/free-solid-svg-icons'
import {
faLightbulb,
faCircleQuestion,
faCircleRight,
faFile,
faFolder,
faLightbulb,
} from '@fortawesome/free-regular-svg-icons'
import {
faFacebook,
@@ -34,27 +36,29 @@ import {
/* add icons to the library */
library.add(
faBars,
faArrowLeft,
faList,
faGear,
faTriangleExclamation,
faExpand,
faTerminal,
faArrowRotateLeft,
faArrowRightFromBracket,
faArrowRight,
faArrowRightFromBracket,
faArrowRotateLeft,
faBars,
faBug,
faMap,
faTrophy,
faCircleExclamation,
faLightbulb,
faEnvelope,
faExpand,
faFileInvoice,
faGear,
faList,
faMap,
faPlay,
faTerminal,
faTriangleExclamation,
faTrophy,
faCircleQuestion,
faCircleRight,
faGithub,
faGoogle,
faFileInvoice,
faFile,
faFolder,
faLightbulb,
faFacebook,
faEnvelope,
faPlay
faGithub,
faGoogle
)
+7 -1
View File
@@ -10,9 +10,9 @@ import Toast, { POSITION } from 'vue-toastification'
/* import Vue Lottie */
import Vue3Lottie from 'vue3-lottie'
import 'vue-toastification/dist/index.css'
import App from './App.vue'
import router from './router'
import formbricks from './utils/formbricks'
const app = createApp(App)
const pinia = createPinia()
@@ -22,6 +22,12 @@ app.component('font-awesome-icon', FontAwesomeIcon)
app.use(pinia)
app.use(router)
router.afterEach(() => {
if (typeof formbricks !== 'undefined') {
formbricks.registerRouteChange()
}
})
const options = {
position: POSITION.TOP_RIGHT,
}
+71 -41
View File
@@ -1,22 +1,40 @@
import { defineStore } from 'pinia'
import { FileGraph, FileObject } from '@linux-odyssey/file-graph'
import { IQuest } from '@linux-odyssey/models'
import { createSession, getActiveSession, getQuest } from '../utils/api'
import {
FileGraph,
type FileGraphUpdateEvent,
} from '../../../packages/file-graph'
import type { IResponse, ITask } from '../../../packages/game'
import type { SessionDetail } from '../../../server/src/routers/sessionRouter'
import Socket from '../utils/socket'
import SocketTerminal from '../utils/terminal'
import { Session, StageResponse } from '../types'
import type { Session } from '../types'
import { trpc } from '../utils/trpc'
const socket = new Socket()
const term = new SocketTerminal(40, 80)
const term = new SocketTerminal()
let hasSetup = false
interface QuestDetailResponse {
id: string
title: string
instruction: string
requirements: string[]
}
interface SessionUpdate {
status: string
responses: IResponse[]
tasks: ITask[]
graphUpdate: FileGraphUpdateEvent
}
interface Store {
session: Session | null
questId: string
quest: IQuest | null
quest: QuestDetailResponse | null
}
const useSession = defineStore('session', {
export const useSession = defineStore('session', {
state: (): Store => ({
session: null,
questId: '',
@@ -24,44 +42,54 @@ const useSession = defineStore('session', {
}),
actions: {
async setQuest(questId: string) {
this.quest = await getQuest(questId)
this.quest = await trpc.quests.getQuestDetail.query(questId)
this.questId = questId
},
async setSession(session: Session) {
this.session = session
this.session.graph = new FileGraph(session.graph)
async setSession(session: SessionDetail) {
this.session = {
...session,
graph: new FileGraph(session.graph),
pwd: '/home/commander',
containerName: session.containerName || '',
}
term.reset()
await socket.connect(session)
await socket.connect(this.session)
await term.connect(
`/terminal/${session.containerName}/ws?token=${session.token}`
)
term.focus()
},
async createSession() {
const session = await createSession(this.questId)
const session = await trpc.session.createSession.mutate({
questId: this.questId,
})
await this.setSession(session)
socket.emit('terminal', 'echo start\n')
term.send('echo start\n')
},
async getActiveSession() {
const session = await getActiveSession(this.questId)
const session = await trpc.session.getActiveSession.query({
questId: this.questId,
})
if (session) {
this.setSession(session)
}
},
updateGraph(event: { discover?: FileObject[]; pwd?: string }) {
newUpdate(update: SessionUpdate) {
if (!this.session) return
if (event.discover) {
this.session.graph.discover(event.discover)
}
if (event.pwd) {
this.session.pwd = event.pwd
}
this.session.responses = update.responses
this.session.tasks = update.tasks
this.session.status = update.status
// if (
// response.status === 'finished' &&
// this.session.status !== 'finished'
// ) {
// this.finish()
// }
// this.session.status = response.status
},
newResponse(response: StageResponse) {
finish() {
if (!this.session) return
this.session.responses.push(response.responses)
this.session.hints.push(response.hints)
if (response.tasks) {
this.session.tasks = response.tasks
}
this.session.status = response.status
this.session.status = 'finished'
},
reset() {
socket.reset()
@@ -70,17 +98,21 @@ const useSession = defineStore('session', {
},
setup() {
if (hasSetup) return
socket.on('terminal', (data: string) => {
term.write(data)
// socket.on('terminal', (data: string) => {
// term.write(data)
// })
// term.onData((data: string) => {
// socket.emit('terminal', data)
// })
socket.on('graph', (event: FileGraphUpdateEvent) => {
if (!this.session) return
this.session.graph.handleEvent(event)
if (event.pwd) {
this.session.pwd = event.pwd
}
})
term.onData((data: string) => {
socket.emit('terminal', data)
})
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
this.updateGraph(event)
})
socket.on('response', (response: StageResponse) => {
this.newResponse(response)
socket.on('update', (update: SessionUpdate) => {
this.newUpdate(update)
})
hasSetup = true
},
@@ -94,5 +126,3 @@ const useSession = defineStore('session', {
export function useTerminal() {
return term
}
export default useSession
+4 -4
View File
@@ -1,5 +1,5 @@
import { defineStore } from 'pinia'
import { getUserProfile } from '../utils/api'
import { trpc } from '../utils/trpc'
interface IProgress {
quest: string
@@ -12,18 +12,18 @@ interface IProgress {
export interface UserProfile {
username: string
email: string
progress: Map<string, IProgress>
progress: Record<string, IProgress>
}
const useUserProfile = defineStore('userProfile', {
state: (): UserProfile => ({
username: '',
email: '',
progress: new Map<string, IProgress>(),
progress: {},
}),
actions: {
async loadUserProfile() {
const res = await getUserProfile()
const res = await trpc.users.getMyProfile.query()
this.$patch(res)
},
async resetUserProfile() {
+6 -6
View File
@@ -1,6 +1,5 @@
import { FileGraph } from '@linux-odyssey/file-graph'
// eslint-disable-next-line import/no-relative-packages
import { IResponse } from '@linux-odyssey/models'
import type { FileGraph } from '../../packages/file-graph'
import type { IResponse, ITask } from '../../packages/game'
export interface Task {
id: number
@@ -20,7 +19,8 @@ export interface Session {
status: string
graph: FileGraph
pwd: string
hints: string[][]
tasks: Task[]
responses: IResponse[][]
tasks: ITask[]
responses: IResponse[]
token: string
containerName: string
}
+4
View File
@@ -0,0 +1,4 @@
declare module '*.css' {
const content: string
export default content
}
-35
View File
@@ -1,8 +1,4 @@
import axios from 'axios'
import { IQuest } from '@linux-odyssey/models'
import { GetSessionsRequest } from '@linux-odyssey/constants'
import { Session } from '../types'
import { UserProfile } from '../store/userProfile'
const api = axios.create({
baseURL: '/api/v1',
@@ -12,34 +8,3 @@ const api = axios.create({
withCredentials: true,
})
export default api
export async function getUserProfile(): Promise<UserProfile> {
const res = await api.get<UserProfile>('/users/me')
return res.data
}
export async function getQuest(id: string): Promise<IQuest> {
const res = await api.get<IQuest>(`/quests/${id}`)
return res.data
}
export async function createSession(questId: string): Promise<Session> {
const res = await api.post<Session>('/sessions', { questId })
return res.data
}
export async function getSessions(
request: GetSessionsRequest
): Promise<Session[]> {
const res = await api.get<Session[]>('/sessions', { params: request })
return res.data
}
export async function getActiveSession(
questId: string
): Promise<Session | null> {
const res = await api.get<Session | null>('/sessions/active', {
params: { questId },
})
return res.data
}
+40
View File
@@ -0,0 +1,40 @@
import formbricks from '@formbricks/js/website'
import { useSession } from '../store/session'
import useUserProfile from '../store/userProfile'
if (typeof window !== 'undefined') {
formbricks.init({
environmentId: 'clzcdqqn0000kadvtqykdo0pc',
apiHost: 'https://app.formbricks.com',
})
}
export function openQuestSurvey() {
const sessionStore = useSession()
const userStore = useUserProfile()
formbricks.track('quest_completed', {
hiddenFields: {
quest: sessionStore.questId,
session: sessionStore.session?._id,
username: userStore.username,
email: userStore.email,
},
})
}
export function openBugReport() {
const sessionStore = useSession()
const userStore = useUserProfile()
formbricks.track('bug_report', {
hiddenFields: {
quest: sessionStore.questId,
session: sessionStore.session?._id,
username: userStore.username,
email: userStore.email,
},
})
}
export default formbricks
+2 -4
View File
@@ -1,5 +1,5 @@
import { io, Socket } from 'socket.io-client'
import { Session } from '../types'
import type { Session } from '../types'
type SocketCallback = (data: any) => void
@@ -23,11 +23,9 @@ class SocketWrapper {
this.socket.on('connect', () => {
console.log('Socket connected to session', session._id)
})
this.socket.once('terminal', () => {
resolve()
})
this.socket.on('connect_error', reject)
this.bindListeners()
})
+59 -23
View File
@@ -1,47 +1,72 @@
import { Terminal } from 'xterm'
import { FitAddon } from 'xterm-addon-fit'
import ReconnectingWebSocket from 'reconnecting-websocket'
import 'xterm/css/xterm.css'
class SocketTerminal {
private term: Terminal
private fitAddon: FitAddon
private socket: ReconnectingWebSocket | null = null
constructor(rows: number, cols: number) {
constructor() {
this.term = new Terminal({
rows, // 行数
cols, // 不指定行数,自动回车后光标从下一行开始
convertEol: true, // 启用时,光标将设置为下一行的开头
// scrollback: 50, //终端中的回滚量
disableStdin: false, // 是否应禁用输入
// cursorStyle: "underline", //光标样式
cursorBlink: true, // 光标闪烁
convertEol: true,
scrollback: 1000,
fontSize: 16,
theme: {
foreground: '#ECECEC', // 字体
background: '#000000', // 背景色
cursor: 'help', // 设置光标
foreground: '#ECECEC',
background: '#000000',
cursor: 'help',
},
fontSize: 18,
fontWeight: 'normal',
})
this.fitAddon = new FitAddon()
this.term.loadAddon(this.fitAddon)
this.fitAddon.fit()
this.term.onData((data) => {
if (this.socket) {
this.socket.send(data)
}
})
}
write(message: string): void {
this.term.write(message)
}
connect(url: string): Promise<void> {
return new Promise((resolve, reject) => {
console.log('Connecting to terminal', url)
this.term.write('\r\n\x1B[1;32mConnecting to terminal\x1B[0m\r\n')
this.socket = new ReconnectingWebSocket(url, [], {
connectionTimeout: 5000,
maxRetries: 10,
})
this.socket.onopen = () => {
console.log('Connected to terminal')
this.term.reset()
this.term.write('\r\n\x1B[1;32mConnected to terminal\x1B[0m\r\n')
resolve()
}
onData(callback: any): void {
this.term.onData(callback)
}
this.socket.onclose = () => {
console.log('Disconnected from terminal')
}
focus(): void {
this.term.focus()
this.socket.onerror = (error) => {
console.error('WebSocket error:', error)
}
this.socket.onmessage = (event) => {
console.log('Received message from terminal')
const data =
typeof event.data === 'string'
? event.data
: new TextDecoder().decode(event.data)
this.term.write(data)
}
})
}
resizeScreen(): void {
this.fitAddon.fit()
try {
this.fitAddon.fit()
} catch (e) {
console.error('Failed to resize terminal:', e)
}
}
mount(reference: HTMLElement): void {
@@ -49,8 +74,19 @@ class SocketTerminal {
this.resizeScreen()
}
send(message: string): void {
if (this.socket) {
this.socket.send(message)
}
}
focus(): void {
this.term.focus()
}
reset(): void {
this.term.reset()
this.socket?.close()
}
}
+16
View File
@@ -0,0 +1,16 @@
import { createTRPCClient, httpBatchLink } from '@trpc/client'
import type { AppRouter } from '../../../server/src/routers'
export const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: '/trpc',
// You can pass any HTTP headers you wish here
// async headers() {
// return {
// authorization: getAuthCookie(),
// }
// },
}),
],
})
+1 -1
View File
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { isValidUsername } from '@linux-odyssey/utils'
import { isValidUsername } from '../../../packages/utils'
import AuthForm from '../components/AuthForm.vue'
import Background from '../components/DynamicBackground.vue'
import { TooManyRequestsError, ValidationError } from '../utils/errors'
+12 -2
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
import useSession from '../store/session'
import { computed, onMounted, watch } from 'vue'
import { useSession } from '../store/session'
import useUserProfile from '../store/userProfile'
import HeaderPart from '../components/header/HeaderPart.vue'
import GameHeaderComponents from '../components/header/GameHeaderComponents.vue'
import DescriptionPart from '../components/game/DescriptionPart.vue'
@@ -10,13 +11,22 @@ import VisualizationPart from '../components/game/VisualizationPart.vue'
import ControlPalette from '../components/game/ControlPalette.vue'
import CompleteModal from '../components/game/CompleteModal.vue'
import StartButton from '../components/game/StartButton.vue'
import { openQuestSurvey } from '../utils/formbricks'
const sessionStore = useSession()
const userStore = useUserProfile()
const completed = computed(() => {
return sessionStore.session?.status === 'finished'
})
watch(completed, (newValue, oldValue) => {
if (newValue && !oldValue) {
console.log('finish', userStore)
openQuestSurvey()
}
})
const props = defineProps({
questId: {
type: String,
+2 -2
View File
@@ -1,7 +1,7 @@
<script setup lang="ts">
import validator from 'validator'
import { isValidUsername } from '@linux-odyssey/utils'
import { passwordPolicy } from '@linux-odyssey/constants'
import { isValidUsername } from '../../../packages/utils'
import { passwordPolicy } from '../../../packages/constants'
import AuthForm from '../components/AuthForm.vue'
import DynamicBackground from '../components/DynamicBackground.vue'
import HeaderPart from '../components/header/HeaderPart.vue'
+3
View File
@@ -24,6 +24,9 @@ export default {
DEFAULT: '#FF0000',
},
},
backgroundImage: {
catelogbg: "url('/catelogbg.svg')",
},
},
},
plugins: [],
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
+3 -26
View File
@@ -1,30 +1,7 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.vue"],
"files": [],
"references": [
{ "path": "../packages/utils" },
{ "path": "../packages/constants" },
{ "path": "../packages/file-graph" },
{ "path": "../packages/models" }
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
+16
View File
@@ -1,11 +1,27 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@game': path.resolve(__dirname, '../packages/game'),
'@models': path.resolve(__dirname, '../packages/models'),
'@utils': path.resolve(__dirname, '../packages/utils'),
'@constants': path.resolve(__dirname, '../packages/constants'),
'@file-graph': path.resolve(__dirname, '../packages/file-graph'),
},
},
server: {
host: '0.0.0.0',
proxy: {
'/trpc': {
target: process.env.API_TARGET || 'http://localhost:3000',
changeOrigin: false,
xfwd: true,
},
'/api': {
target: process.env.API_TARGET || 'http://localhost:3000',
changeOrigin: false,
+1 -18
View File
@@ -1,27 +1,10 @@
server {
listen 80;
error_log /var/log/nginx/error.log debug;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api-docs/ {
proxy_pass http://backend:3000;
}
location /socket.io/ {
proxy_pass http://backend:3000;
proxy_http_version 1.1;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
+53 -32
View File
@@ -1,7 +1,23 @@
version: '3'
services:
traefik:
image: traefik:v3.6.9
ports:
- 8000:8000
- 8080:8080
networks:
- players
- internal
command:
- '--api.insecure=true'
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--entrypoints.web.address=:8000'
- '--entrypoints.web.forwardedHeaders.insecure=true' # trust all X-Forwarded headers
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
app:
image: linuxodyssey/frontend
image: linuxodyssey/frontend:${IMAGE_TAG:-latest}
depends_on:
- backend
restart: always
@@ -10,12 +26,16 @@ services:
dockerfile: app/Dockerfile
target: prod
networks:
internal:
ports:
- 8080:80
- internal
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.app.entrypoints=web'
- 'traefik.http.routers.app.rule=PathPrefix(`/`)'
- 'traefik.http.routers.app.service=app'
- 'traefik.http.services.app.loadbalancer.server.port=80'
backend:
image: linuxodyssey/backend
image: linuxodyssey/backend:${IMAGE_TAG:-latest}
depends_on:
- db
restart: always
@@ -29,21 +49,35 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./logs:/app/logs
- ./config:/app/config
- ./quests:/app/quests:ro
env_file:
- .env
- .env.prod
environment:
- MONGO_URL=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@db:27017/${MONGO_DB}
- DOCKER_NETWORK=linux-odyssey-players
- SECRET_KEY=${SECRET_KEY}
- TRUSTED_PROXIES=${TRUSTED_PROXIES}
- BACKEND_URL=http://backend:3000
- BASE_URL=${BASE_URL}
- HOST_PROJECT_ROOT=${PWD}
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.backend-api.entrypoints=web'
- 'traefik.http.routers.backend-api.rule=PathPrefix(`/api`)'
- 'traefik.http.routers.backend-api.service=backend'
- 'traefik.http.routers.backend-socket.entrypoints=web'
- 'traefik.http.routers.backend-socket.rule=PathPrefix(`/socket.io`)'
- 'traefik.http.routers.backend-socket.service=backend'
- 'traefik.http.routers.backend-trpc.entrypoints=web'
- 'traefik.http.routers.backend-trpc.rule=PathPrefix(`/trpc`)'
- 'traefik.http.routers.backend-trpc.service=backend'
- 'traefik.http.routers.backend-api-docs.entrypoints=web'
- 'traefik.http.routers.backend-api-docs.rule=PathPrefix(`/api-docs`)'
- 'traefik.http.routers.backend-api-docs.service=backend'
- 'traefik.http.services.backend.loadbalancer.server.port=3000'
analytics:
image: linuxodyssey/analytics
image: linuxodyssey/analytics:${IMAGE_TAG:-latest}
build:
context: .
dockerfile: packages/analytics/Dockerfile
dockerfile: services/analytics/Dockerfile
target: prod
restart: always
depends_on:
@@ -52,13 +86,11 @@ services:
- internal
ports:
- '3001:3001'
environment:
- MONGO_URL=mongodb://${MONGO_USER}:${MONGO_PASSWORD}@db:27017/${MONGO_DB}
- ADMIN_USERNAME=${ADMIN_USERNAME}
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
env_file:
- .env.prod
db:
image: mongo
image: mongo:6
restart: always
networks:
- internal
@@ -67,22 +99,11 @@ services:
volumes:
- mongo:/data/db
- ./mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js:ro
environment:
- BASE_URL=${BASE_URL}
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_ROOT_PASSWORD}
- MONGO_USER=${MONGO_USER}
- MONGO_PASS=${MONGO_PASSWORD}
- MONGO_DB=${MONGO_DB}
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
- SURVEY_URL=${SURVEY_URL}
- BUG_REPORT_URL=${BUG_REPORT_URL}
env_file:
- .env.prod
base:
image: linuxodyssey/quest-base
image: linuxodyssey/quest-base:${IMAGE_TAG:-latest}
build:
context: .
dockerfile: ./quests/Dockerfile
+35 -4
View File
@@ -1,7 +1,27 @@
version: '3'
services:
traefik:
image: traefik:v3.6.9
networks:
- players-dev
command:
- '--api.insecure=true'
- '--providers.docker=true'
- '--providers.docker.exposedbydefault=false'
- '--providers.file.directory=/etc/traefik/conf'
- '--providers.file.watch=true'
- '--entrypoints.web.address=:8000'
ports:
- '8000:8000'
- '8080:8080'
extra_hosts:
- 'host.docker.internal:host-gateway'
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik/conf:/etc/traefik/conf
db:
image: mongo
image: mongo:6
restart: always
networks:
- internal
@@ -12,12 +32,23 @@ services:
base:
image: linuxodyssey/quest-base
networks:
- players-dev
build:
context: .
dockerfile: ./quests/Dockerfile
extra_hosts:
- 'host.docker.internal:host-gateway'
labels:
- 'traefik.enable=true'
- 'traefik.http.routers.base.rule=PathPrefix(`/terminal/base`)'
- 'traefik.http.middlewares.base-stripprefix.stripprefix.prefixes=/terminal/base'
- 'traefik.http.routers.base.middlewares=base-stripprefix'
- 'traefik.http.routers.base.entrypoints=web'
- 'traefik.http.services.base.loadbalancer.server.port=9090'
networks:
internal:
players:
name: linux-odyssey-players
internal: true
players-dev:
name: linux-odyssey-players-dev
internal: false
+4 -3
View File
@@ -32,15 +32,16 @@ down() {
dump() {
# Define service name and dump directory name
SERVICE_NAME="db"
COMPOSE_FILE="docker-compose.prod.yml"
DUMP_NAME="mongodump-$(date --iso).archive"
CONTAINER_ID=$(docker compose ps -q $SERVICE_NAME)
CONTAINER_ID=$(docker compose -f $COMPOSE_FILE ps -q $SERVICE_NAME)
# Fetch credentials from Docker environment variables
MONGO_URL=$(docker compose exec -T backend printenv MONGO_URL)
MONGO_URL=$(docker compose -f $COMPOSE_FILE exec -T backend printenv MONGO_URL)
echo mongo uri: $MONGO_URL
# Dump MongoDB data using credentials
docker compose exec -T $SERVICE_NAME sh -c "mongodump --gzip --archive=/tmp/$DUMP_NAME -d odyssey-test" &&
docker compose -f $COMPOSE_FILE exec -T $SERVICE_NAME sh -c "mongodump --gzip --archive=/tmp/$DUMP_NAME --uri=$MONGO_URL" &&
# Copy the dump from the container to the host
# Note: You will need to know the exact container name for this step
+141
View File
@@ -0,0 +1,141 @@
# Windows Environment Setup
## Install WSL and Ubuntu
Please refer to the Official [Setup WSL](https://learn.microsoft.com/en-us/windows/wsl/setup/environment) on Microsoft's website.
Run the following command in PowerShell as Administrator:
wsl --install
Then restart the computer. After restarting, you can open Ubuntu terminal by searching "Ubuntu" in Start menu. You will be prompted to create a new user account. Once you have done that, you can continue with the following steps.
## Setup Environment (In Ubuntu terminal)
1. Update and upgrade
```
sudo apt-get update
sudo apt-get upgrade
```
1. Install Git
```
sudo apt-get install git
```
1. Install Node.js (Version 20.x)
```
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install nodejs
sudo apt install build-essential
sudo npm install -g yarn
```
## Install Docker
(Refer to [get.docker.com](https://get.docker.com/))
To install the latest stable versions of Docker CLI, Docker Engine, and their
dependencies:
1. Download the script
```
curl -fsSL https://get.docker.com -o install-docker.sh
```
1. verify the script's content
```
cat install-docker.sh
```
1. run the script with --dry-run to verify the steps it executes
```
sh install-docker.sh --dry-run
```
**NOTE: the script will recommend you to use Docker Desktop instead, but we need to use Docker inside WSL for the backend to work.** Just ignore it and wait for the installation to finish.
1. run the script either as root, or using sudo to perform the installation.
```
sudo sh install-docker.sh
```
1. Verify the installation
```
docker --version
```
1. Add your user to the docker group
```
sudo usermod -aG docker $USER
```
**NOTE: You need to create a new WSL terminal to apply the changes.**
1. Verify the installation
```
docker ps
```
This should return something like:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
## Run the Project (In Ubuntu terminal)
1. Clone the repository
```
git clone https://github.com/lancatlin/linux-odyssey.git
cd linux-odyssey
```
Optional: To open the project in VSCode, run the following command inside the project folder:
```
code .
```
1. Install dependencies
```
yarn install
```
It is ok if there are some warnings and `gyp: ERR` errors, just verify there is `done` at the end.
1. Copy the environment variables
```
cp .env.sample .env
```
1. Docker setup
```
docker compose pull
docker compose build
docker compose up -d db
```
<!-- Check folder ./config exist and is empty -->
<!-- Check .env file exist -->
1. Run the project
```
yarn build
yarn dev
```
<!-- Check ./config/ssh_key.pub is a file not a folder -->
Enjoy!
+13 -10
View File
@@ -9,6 +9,7 @@
"app",
"server",
"packages/*",
"services/*",
"tests"
],
"scripts": {
@@ -16,7 +17,6 @@
"build": "yarn workspace server build && yarn workspace app build && yarn workspace analytics build",
"dev": "concurrently -n \"App,Server\" -c \"bgBlue.bold,bgGreen.bold\" \"yarn workspace app dev\" \"yarn workspace server dev\"",
"server": "yarn workspace server dev",
"cli": "yarn workspace client start",
"lint": "eslint .",
"format": "prettier --ignore-path .prettierignore --write app server",
"test": "yarn node --experimental-vm-modules --no-warnings $(yarn bin jest)",
@@ -34,26 +34,29 @@
"@jest/globals": "^29.7.0",
"@types/jest": "^29.5.12",
"@types/node": "^20.10.4",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vue/eslint-config-prettier": "^7.1.0",
"babel-jest": "^29.7.0",
"concurrently": "^8.2.2",
"eslint": "^8.44.0",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-prettier": "^9.1.0",
"eslint-import-resolver-typescript": "^3.6.1",
"eslint-plugin-cypress": "^2.15.1",
"eslint-plugin-import": "^2.29.0",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-vue": "^9.15.1",
"globals": "^16.0.0",
"jest": "^29.7.0",
"prettier": "^3.0.0",
"ts-node": "^10.9.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vue-eslint-parser": "^9.4.2",
"zod-to-json-schema": "^3.24.5"
},
"dependencies": {
"typescript-eslint": "^7.4.0"
}
"latest": "^0.2.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}
-19
View File
@@ -1,19 +0,0 @@
FROM node:20-alpine as build
WORKDIR /app
COPY ./package.json yarn.lock tsconfig.json ./
COPY packages/analytics/package.json ./packages/analytics/package.json
COPY packages/models/package.json ./packages/models/package.json
RUN yarn install --frozen-lockfile
ENV HOST=0.0.0.0
ENV PORT=3001
EXPOSE 3001
# Production stage
FROM build as prod
COPY packages/analytics ./packages/analytics
COPY packages/models ./packages/models
ENV NODE_ENV=production
WORKDIR /app/packages/analytics
RUN yarn build
CMD [ "yarn", "start" ]
-6
View File
@@ -1,6 +0,0 @@
{
"watch": ["src", "views"],
"ext": "ts,handlebars,js",
"ignore": ["src/**/*.spec.ts"],
"exec": "node --loader ts-node/esm src/index.ts"
}
-17
View File
@@ -1,17 +0,0 @@
{
"name": "@linux-odyssey/constants",
"type": "module",
"version": "1.0.0",
"private": true,
"main": "dist/index.js",
"exports": "./dist/index.js",
"types": "dist/index.d.ts",
"license": "GPL",
"scripts": {
"build": "tsc",
"dev": "tsc -w"
},
"dependencies": {
"zod": "^3.23.8"
}
}
-9
View File
@@ -1,9 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"composite": true,
"outDir": "./dist"
},
"include": ["*.js", "*.ts"],
"exclude": ["node_modules", "dist"]
}
+1
View File
@@ -0,0 +1 @@
export const toArray = (val: unknown) => (typeof val === 'string' ? [val] : val)
-98
View File
@@ -1,98 +0,0 @@
/* eslint-disable no-restricted-syntax */
const fs = require('fs').promises
const { exit } = require('process')
const minimist = require('minimist')
const axios = require('axios')
const { colorize, printResponses, printHints } = require('./print.js')
const discoverFiles = require('./discover.js')
const {
API_ENDPOINT,
TOKEN,
PWD,
CMD_NAME,
CMD_OUTPUT_FILE,
CMD_ERROR_FILE,
CMD_EXIT_CODE,
} = process.env
const api = axios.create({
baseURL: `${API_ENDPOINT}/api/v1`,
headers: {
Authorization: `Bearer ${TOKEN}`,
},
})
async function readOrNone(file) {
try {
return await fs.readFile(file, 'utf8')
} catch (_) {
return ''
}
}
const commandListeners = {
ls: [discoverFiles],
}
async function handleCommand(command) {
const argv = minimist(command.split(' '))
const name = argv._[0]
const listeners = commandListeners[name]
if (!listeners) return {}
return listeners.reduce(async (result, listener) => {
const res = await listener(argv)
return { ...res, ...result }
}, {})
}
async function handleResponse({ responses, hints, end }) {
if (responses) {
await printResponses(responses, 90)
}
if (hints) {
await printHints(hints, 90)
}
if (end) {
console.log(colorize('Quest completed!', 'blue'))
}
await api.post('/commands/completed')
}
async function main() {
if (!CMD_NAME) exit(0)
const params = await handleCommand(CMD_NAME)
const output = await readOrNone(CMD_OUTPUT_FILE)
const error = await readOrNone(CMD_ERROR_FILE)
const payload = {
command: CMD_NAME,
output,
error,
exit_code: CMD_EXIT_CODE,
pwd: PWD,
params,
}
try {
const res = await api.post('/commands', payload)
if (!res.data || !res.data.responses) return
await handleResponse(res.data)
} catch (err) {
if (err.response) {
console.error(err.response.data)
return
}
console.error(err.message)
}
}
main()
.then(exit)
.catch((err) => {
console.log(
'發生預期外的錯誤,請嘗試重新啟動 Quest,如果問題持續發生,請幫我們填寫右上角的錯誤回報表單。'
)
console.error(err)
exit(1)
})
-49
View File
@@ -1,49 +0,0 @@
const fs = require('fs').promises
const path = require('path')
async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) {
const stats = await fs.stat(inputPath)
const name = path.basename(inputPath)
const discovered = level <= 1 && (hiddenFiles || !name.startsWith('.'))
const files = [
{
path: inputPath,
name,
type: stats.isDirectory() ? 'folder' : 'file',
discovered,
},
]
if (stats.isDirectory() && level < 2) {
try {
let entries = await fs.readdir(inputPath)
if (level > 0) {
entries = entries.slice(0, 5)
}
const result = await Promise.all(
entries.map((entry) =>
collectFilesInfo(path.join(inputPath, entry), level + 1, hiddenFiles)
)
)
files.push(...result.flat())
} catch (_) {
// ignore
}
}
return files
}
async function discoverFiles(argv) {
const targetPath = argv._.length < 2 ? ['.'] : argv._.slice(1)
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 (_) {
return {}
}
}
module.exports = discoverFiles
-14
View File
@@ -1,14 +0,0 @@
{
"name": "container",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"build": "",
"watch": ""
},
"dependencies": {
"axios": "^1.6.0",
"minimist": "^1.2.8"
}
}
-115
View File
@@ -1,115 +0,0 @@
/* eslint-disable no-restricted-syntax */
/* eslint-disable no-await-in-loop */
const readline = require('readline')
const RETURN_SYMBOL = '\u21B5'
// ANSI color map
const ANSI_COLORS = {
default: 0,
black: 30,
red: 31,
green: 32,
yellow: 33,
blue: 34,
magenta: 35,
cyan: 36,
white: 37,
}
function colorCode(color) {
return `\x1b[${ANSI_COLORS[color]}m`
}
function colorize(content, color) {
if (!color) return content
return `${colorCode(color)}${content}${colorCode('default')}`
}
process.stdin.setRawMode(true)
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
terminal: false,
})
function waitForEnter() {
return new Promise((resolve) => {
rl.once('line', resolve)
})
}
function formatLine(content, color) {
let result = colorCode(color)
let codeBlock = false
for (const c of content) {
switch (c) {
case '`':
result += colorCode(codeBlock ? color : 'green')
codeBlock = !codeBlock
break
default:
result += c
}
}
return result + colorCode('default') + RETURN_SYMBOL
}
function printLine(content, delay, color) {
const printContent = formatLine(content, color)
return new Promise((resolve) => {
let i = 0
const timer = setInterval(() => {
process.stdout.write(printContent[i])
i += 1
if (i >= printContent.length) {
clearInterval(timer)
process.stdout.write('\n')
resolve()
}
}, delay)
waitForEnter().then(() => {
clearInterval(timer)
if (i >= printContent.length) return
process.stdout.write(printContent.slice(i))
process.stdout.write('\n')
resolve()
})
})
}
async function printResponses(responses, delay = 100) {
console.log()
console.log(colorize('==============================', 'green'))
for (const response of responses) {
const { type, content, speaker, color } = response
if (type === 'narrative') {
for (const line of content) {
await printLine(line, delay, color || 'cyan')
}
}
if (type === 'dialogue') {
console.log(colorize(`${speaker}:`, 'green'))
for (const line of content) {
await printLine(` ${line}`, delay, color || 'cyan')
}
}
console.log(colorize('------------------------------', 'green'))
console.log()
}
}
async function printHints(hints, delay = 100) {
for (const hint of hints) {
await printLine(hint, delay, 'yellow')
}
}
module.exports = {
printResponses,
printHints,
colorize,
}
File renamed without changes.
@@ -2,25 +2,28 @@ import { basename } from './utils.js'
export interface FileObject {
path: string
type: string
type: 'file' | 'directory'
discovered: boolean
empty?: boolean
}
export default class File implements FileObject {
path: string
name: string
type: string
type: 'file' | 'directory'
discovered: boolean
empty?: boolean
constructor(obj: FileObject) {
this.path = obj.path.trimEnd()
this.name = basename(obj.path)
this.type = obj.type
this.discovered = obj.discovered
this.empty = obj.empty && obj.type === 'directory'
}
isDirectory() {
return this.type === 'folder'
return this.type === 'directory'
}
contains(file: FileObject) {
@@ -1,10 +1,11 @@
import { FileObject } from './file.js'
import FileNode from './fileNode.js'
export interface Event {
export interface FileGraphUpdateEvent {
add?: FileObject[]
remove?: FileObject[]
discover?: FileObject[]
pwd?: string
}
export default class FileGraph extends FileNode {
@@ -20,7 +21,7 @@ export default class FileGraph extends FileNode {
remove(files: FileObject[]) {
// Logic to remove a file node from the graph
// If the file node is a folder, also remove all children
// If the file node is a directory, also remove all children
// If the file node doesn't exist, throw an error
files.forEach((file) => {
@@ -33,6 +34,10 @@ export default class FileGraph extends FileNode {
// Overwrite the file graph with the provided files
// Remove file nodes that don't exist anymore and add new ones
if (eventFiles.length === 0) {
return
}
const newNode = new FileNode(eventFiles[0])
eventFiles.slice(1).forEach((file) => {
@@ -42,19 +47,16 @@ export default class FileGraph extends FileNode {
this.merge(newNode)
}
handleEvent(event: Event) {
handleEvent(event: FileGraphUpdateEvent) {
// eslint-disable-next-line default-case
switch (true) {
case !!event.add:
this.add(event.add)
break
case !!event.remove:
this.remove(event.remove)
break
case !!event.discover:
this.discover(event.discover)
break
// Implement other cases if needed
if (event.add) {
this.add(event.add)
}
if (event.remove) {
this.remove(event.remove)
}
if (event.discover) {
this.discover(event.discover)
}
}
}
@@ -1,15 +1,18 @@
import File, { FileObject } from './file.js'
import {
DuplicateItemError,
ParentNotExistsError,
FileNotExistsError,
} from './errors.js'
import { ParentNotExistsError, FileNotExistsError } from './errors.js'
import { dirname } from './utils.js'
export default class FileNode extends File {
export interface IFileNode {
path: string
type: 'file' | 'directory'
discovered: boolean
children?: IFileNode[]
}
export default class FileNode extends File implements IFileNode {
children: FileNode[]
constructor(file: FileObject | FileNode) {
constructor(file: FileObject | IFileNode) {
super(file)
this.children = []
if ((<FileNode>file).children)
@@ -22,15 +25,24 @@ export default class FileNode extends File {
) {
const file = new FileNode(fileInput)
if (file.path === this.path) {
throw new DuplicateItemError(
`File ${file.path} already exists in the tree: ${this.toString()}`
)
if (!this.discovered) {
this.discovered = file.discovered
}
return
}
if (this.children.some((child) => child.path === file.path)) {
throw new DuplicateItemError(
`File ${file.path} already exists in the tree: ${this.toString()}`
)
let existing = false
for (const child of this.children) {
if (child.path === file.path) {
existing = true
if (!child.discovered) {
child.discovered = file.discovered
}
break
}
}
if (existing) {
return
}
const parentPath = dirname(file.path)
@@ -41,11 +53,11 @@ export default class FileNode extends File {
// Recursively call addChild on the correct child node
const childNode = this.children.find((child) => child.contains(file))
if (childNode) {
childNode.addChild(file)
childNode.addChild(file, { makeParents })
} else if (makeParents) {
const parent = new FileNode({
path: parentPath,
type: 'folder',
type: 'directory',
discovered: false,
children: [file],
})
@@ -88,6 +100,12 @@ export default class FileNode extends File {
// Update the state of this node
this.discovered = this.discovered || fileNode.discovered
// the provided directory is empty, remove all children
if (fileNode.empty) {
this.children = []
return
}
fileNode.children.forEach((child) => {
const existingChild = this.children.find((c) => c.path === child.path)
if (existingChild) {
@@ -133,4 +151,11 @@ export default class FileNode extends File {
.join('')
return `${name}\n${childrenString}`
}
include(filePath: string, type?: 'file' | 'directory'): boolean {
if (this.path === filePath) {
return type ? this.type === type : true
}
return this.children.some((child) => child.include(filePath, type))
}
}
+6
View File
@@ -0,0 +1,6 @@
export { default as File } from './file'
export type { FileObject } from './file'
export { default as FileNode } from './fileNode'
export type { IFileNode } from './fileNode'
export { default as FileGraph } from './fileGraph'
export type { FileGraphUpdateEvent } from './fileGraph'
-14
View File
@@ -1,14 +0,0 @@
{
"name": "@linux-odyssey/file-graph",
"version": "1.0.0",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"exports": "./dist/index.js",
"license": "MIT",
"scripts": {
"build": "tsc",
"dev": "tsc -w",
"test": "yarn build && jest"
}
}
-3
View File
@@ -1,3 +0,0 @@
export { default as File, FileObject } from './file.js'
export { default as FileNode } from './fileNode.js'
export { default as FileGraph, Event } from './fileGraph.js'
+2 -2
View File
@@ -1,10 +1,10 @@
import { describe, test, expect } from '@jest/globals'
import File from '../src/file.js'
import File from '../file.js'
describe('File', () => {
let file: File
beforeEach(() => {
file = new File({ path: '/home/user', type: 'folder', discovered: true })
file = new File({ path: '/home/user', type: 'directory', discovered: true })
})
const filesToCompare = [
+84 -19
View File
@@ -1,6 +1,7 @@
import { describe, test, expect } from '@jest/globals'
import FileGraph from '../src/fileGraph.js'
import FileNode from '../src/fileNode.js'
import FileGraph from '../fileGraph.js'
import FileNode from '../fileNode.js'
import { FileObject } from '../file.js'
describe('FileGraph', () => {
let root: FileNode
@@ -8,7 +9,7 @@ describe('FileGraph', () => {
beforeEach(() => {
root = new FileNode({
path: '/',
type: 'folder',
type: 'directory',
discovered: true,
})
})
@@ -16,7 +17,7 @@ describe('FileGraph', () => {
test('add files to graph', () => {
const fileGraph = new FileGraph(root)
const filesToAdd = [
const filesToAdd: FileObject[] = [
{
path: '/home/user/file1.txt',
type: 'file',
@@ -24,7 +25,12 @@ describe('FileGraph', () => {
},
{
path: '/home/user/folder1',
type: 'folder',
type: 'directory',
discovered: true,
},
{
path: '/home/user/folder2/file2.txt',
type: 'file',
discovered: true,
},
]
@@ -32,7 +38,34 @@ describe('FileGraph', () => {
fileGraph.add(filesToAdd)
expect(fileGraph.children.length).toBe(1)
expect(fileGraph.children[0].children[0].children.length).toBe(2)
expect(fileGraph.children[0].children[0].children.length).toBe(3)
})
test('ignore files that already exist', () => {
const fileGraph = new FileGraph(root)
const filesToAdd: FileObject[] = [
{
path: '/home/user/folder1',
type: 'directory',
discovered: true,
},
{
path: '/home/user/folder1/file1.txt',
type: 'file',
discovered: true,
},
]
fileGraph.add(filesToAdd)
expect(fileGraph.children.length).toBe(1)
expect(fileGraph.children[0].children[0].children.length).toBe(1)
fileGraph.add([filesToAdd[1]])
expect(fileGraph.children.length).toBe(1)
expect(fileGraph.children[0].children[0].children.length).toBe(1)
})
test('remove files from graph', () => {
@@ -41,12 +74,12 @@ describe('FileGraph', () => {
const filesToAdd = [
{
path: '/home/user/folder1',
type: 'folder',
type: 'directory',
discovered: true,
},
{
path: '/home/user/folder2',
type: 'folder',
type: 'directory',
discovered: true,
},
{
@@ -61,22 +94,22 @@ describe('FileGraph', () => {
},
]
fileGraph.add(filesToAdd)
fileGraph.add(filesToAdd as FileObject[])
const filesToRemove = [
{
path: '/home/user/folder1/file1.txt',
type: 'folder',
type: 'directory',
discovered: true,
},
{
path: '/home/user/folder2',
type: 'folder',
type: 'directory',
discovered: true,
},
]
fileGraph.remove(filesToRemove)
fileGraph.remove(filesToRemove as FileObject[])
expect(fileGraph.children[0].children[0].children.length).toBe(1)
expect(fileGraph.children[0].children[0].children[0].name).toBe('folder1')
@@ -90,7 +123,7 @@ describe('FileGraph', () => {
const discoverFiles = [
{
path: '/home/user/folder1',
type: 'folder',
type: 'directory',
discovered: true,
},
{
@@ -110,7 +143,7 @@ describe('FileGraph', () => {
},
]
fileGraph.discover(discoverFiles)
fileGraph.discover(discoverFiles as FileObject[])
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
3
)
@@ -122,12 +155,12 @@ describe('FileGraph', () => {
const filesToAdd = [
{
path: '/home/user/folder1',
type: 'folder',
type: 'directory',
discovered: true,
},
{
path: '/home/user/folder2',
type: 'folder',
type: 'directory',
discovered: true,
},
{
@@ -142,12 +175,12 @@ describe('FileGraph', () => {
},
]
fileGraph.add(filesToAdd)
fileGraph.add(filesToAdd as FileObject[])
const newFiles = [
{
path: '/home/user/folder1',
type: 'folder',
type: 'directory',
discovered: true,
},
{
@@ -167,11 +200,43 @@ describe('FileGraph', () => {
},
]
fileGraph.discover(newFiles)
fileGraph.discover(newFiles as FileObject[])
expect(fileGraph.children[0].children[0].children.length).toBe(2)
expect(fileGraph.children[0].children[0].children[0].children.length).toBe(
3
)
})
test('discover a empty directory which children just being deleted', () => {
const fileGraph = new FileGraph(root)
const discoverFiles = [
{
path: '/home/user/folder1',
type: 'directory',
discovered: true,
},
{
path: '/home/user/folder1/file1.txt',
type: 'file',
discovered: true,
},
]
fileGraph.add(discoverFiles as FileObject[])
expect(fileGraph.include('/home/user/folder1/file1.txt', 'file')).toBe(true)
fileGraph.discover([
{
path: '/home/user/folder1',
type: 'directory',
discovered: true,
empty: true,
},
])
expect(fileGraph.include('/home/user/folder1/file1.txt', 'file')).toBe(
false
)
})
})
+90 -91
View File
@@ -1,7 +1,7 @@
import { describe, test, expect } from '@jest/globals'
import { DuplicateItemError, ParentNotExistsError } from '../src/errors.js'
import FileNode from '../src/fileNode.js'
import { FileObject } from '../src/file.js'
import { ParentNotExistsError } from '../errors.js'
import FileNode, { IFileNode } from '../fileNode.js'
import { FileObject } from '../file.js'
function createNodeFromFiles(files: FileObject[]) {
const node = new FileNode(files[0])
@@ -42,17 +42,16 @@ describe('FileNode', () => {
beforeEach(() => {
rootNode = new FileNode({
path: '/home/rudeus',
name: 'rudeus',
type: 'folder',
type: 'directory',
discovered: true,
})
})
test('should throw error when adding duplicate item', () => {
const child = {
test('should override discovered when adding duplicate item', () => {
const child: FileObject = {
path: '/home/rudeus/test',
type: 'folder',
discovered: true,
type: 'directory',
discovered: false,
}
// Adding the child for the first time
@@ -61,16 +60,16 @@ describe('FileNode', () => {
expect(rootNode.children.length).toBe(1)
// Trying to add the same child again should throw an error
expect(() => {
rootNode.addChild(child)
}).toThrow(DuplicateItemError)
child.discovered = true
rootNode.addChild(child)
expect(rootNode.children[0].discovered).toBe(true)
})
test('should throw error when parent does not exist', () => {
const child = {
const child: FileObject = {
path: '/home/rudeus/alphabet/test',
name: 'test',
type: 'folder',
type: 'directory',
discovered: true,
}
@@ -81,22 +80,19 @@ describe('FileNode', () => {
})
test('adds nested children to the tree', () => {
const files = [
const files: FileObject[] = [
{
path: '/home/rudeus/forgotten_scroll.txt',
name: 'forgotten_scroll.txt',
type: 'file',
discovered: true,
},
{
path: '/home/rudeus/test',
name: 'test',
type: 'folder',
type: 'directory',
discovered: true,
},
{
path: '/home/rudeus/test/a',
name: 'a',
type: 'file',
discovered: false,
},
@@ -111,9 +107,8 @@ describe('FileNode', () => {
})
test('adds nested children to the tree with makeParents option', () => {
const file = {
const file: FileObject = {
path: '/home/rudeus/test/a/b/c/d/e',
name: 'a',
type: 'file',
discovered: false,
}
@@ -129,34 +124,34 @@ describe('FileNode', () => {
})
test('merge-subtree', () => {
const root = {
const root: FileObject = {
path: '/',
type: 'folder',
type: 'directory',
discovered: true,
}
const files1 = [
const files1: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/c', type: 'folder', discovered: true },
{ path: '/b/d', type: 'file', discovered: true },
{ path: '/b/c/f', type: 'file', discovered: true },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/c', type: 'directory' as const, discovered: true },
{ path: '/b/d', type: 'file' as const, discovered: true },
{ path: '/b/c/f', type: 'file' as const, discovered: true },
]
const files2 = [
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/c', type: 'folder', discovered: true },
{ path: '/b/e', type: 'file', discovered: true },
{ path: '/b/c/g', type: 'file', discovered: true },
const files2: FileObject[] = [
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/c', type: 'directory' as const, discovered: true },
{ path: '/b/e', type: 'file' as const, discovered: true },
{ path: '/b/c/g', type: 'file' as const, discovered: true },
]
const expected = [
const expected: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/c', type: 'folder', discovered: true },
{ path: '/b/e', type: 'file', discovered: true },
{ path: '/b/c/g', type: 'file', discovered: true },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/c', type: 'directory' as const, discovered: true },
{ path: '/b/e', type: 'file' as const, discovered: true },
{ path: '/b/c/g', type: 'file' as const, discovered: true },
]
mergeTest(files1, files2, expected)
@@ -166,90 +161,94 @@ describe('FileNode', () => {
// Test if the node to merge is only the middle part of the tree
// (i.e. the node to merge doesn't contains all level of children)
// The original tree should keep the children that are not in the node to merge
const root = {
const root: FileObject = {
path: '/',
type: 'folder',
type: 'directory',
discovered: true,
}
const files1 = [
const files1: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/c', type: 'folder', discovered: true },
{ path: '/b/d', type: 'file', discovered: true },
{ path: '/b/c/f', type: 'file', discovered: true },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/c', type: 'directory' as const, discovered: true },
{ path: '/b/d', type: 'file' as const, discovered: true },
{ path: '/b/c/f', type: 'file' as const, discovered: true },
]
const files2 = [
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/c', type: 'folder', discovered: true },
{ path: '/b/e', type: 'file', discovered: true },
const files2: FileObject[] = [
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/c', type: 'directory' as const, discovered: true },
{ path: '/b/e', type: 'file' as const, discovered: true },
]
const expected = [
const expected: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/c', type: 'folder', discovered: true },
{ path: '/b/e', type: 'file', discovered: true },
{ path: '/b/c/f', type: 'file', discovered: true },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/c', type: 'directory' as const, discovered: true },
{ path: '/b/e', type: 'file' as const, discovered: true },
{ path: '/b/c/f', type: 'file' as const, discovered: true },
]
mergeTest(files1, files2, expected)
})
test('merge-hidden', () => {
const root = {
const root: FileObject = {
path: '/',
type: 'folder',
type: 'directory',
discovered: true,
}
const files1 = [
const files1: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/.b', type: 'file', discovered: false },
{ path: '/c', type: 'folder', discovered: true },
{ path: '/c/.d', type: 'file', discovered: false },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/.b', type: 'file' as const, discovered: false },
{ path: '/c', type: 'directory' as const, discovered: true },
{ path: '/c/.d', type: 'file' as const, discovered: false },
]
const files2 = [
{ path: '/c', type: 'folder', discovered: true },
{ path: '/c/.d', type: 'file', discovered: true },
const files2: FileObject[] = [
{ path: '/c', type: 'directory' as const, discovered: true },
{ path: '/c/.d', type: 'file' as const, discovered: true },
]
const expected = [
const expected: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/.b', type: 'file', discovered: false },
{ path: '/c', type: 'folder', discovered: true },
{ path: '/c/.d', type: 'file', discovered: true },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/.b', type: 'file' as const, discovered: false },
{ path: '/c', type: 'directory' as const, discovered: true },
{ path: '/c/.d', type: 'file' as const, discovered: true },
]
mergeTest(files1, files2, expected)
})
test('remove-child', () => {
const root = {
const root: FileObject = {
path: '/',
type: 'folder',
type: 'directory',
discovered: true,
}
const files = [
const files: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/c', type: 'folder', discovered: true },
{ path: '/b/d', type: 'file', discovered: true },
{ path: '/b/c/f', type: 'file', discovered: true },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/c', type: 'directory' as const, discovered: true },
{ path: '/b/d', type: 'file' as const, discovered: true },
{ path: '/b/c/f', type: 'file' as const, discovered: true },
]
const fileToRemove = { path: '/b/c', type: 'folder', discovered: true }
const fileToRemove: FileObject = {
path: '/b/c',
type: 'directory',
discovered: true,
}
const expected = [
const expected: FileObject[] = [
root,
{ path: '/a', type: 'file', discovered: true },
{ path: '/b', type: 'folder', discovered: true },
{ path: '/b/d', type: 'file', discovered: true },
{ path: '/a', type: 'file' as const, discovered: true },
{ path: '/b', type: 'directory' as const, discovered: true },
{ path: '/b/d', type: 'file' as const, discovered: true },
]
removeChildTest(files, fileToRemove, expected)
@@ -257,9 +256,9 @@ describe('FileNode', () => {
})
test('load nested fileNode', () => {
const data = {
const data: IFileNode = {
path: '/',
type: 'folder',
type: 'directory',
discovered: true,
children: [
{
@@ -269,12 +268,12 @@ test('load nested fileNode', () => {
},
{
path: '/b',
type: 'folder',
type: 'directory',
discovered: true,
children: [
{
path: '/b/c',
type: 'folder',
type: 'directory',
discovered: true,
children: [
{
-10
View File
@@ -1,10 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"composite": true,
"outDir": "./dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
File renamed without changes.
+48
View File
@@ -0,0 +1,48 @@
import { Condition } from './condition/Condition'
import {
ICommand,
ICondition,
IFileExistenceChecker,
IRequirements,
IResponse,
} from './schema'
export class Event {
id: string
private condition: Condition
private requirements: string[]
constructor(
id: string,
condition: ICondition,
private response: IResponse,
requirements?: IRequirements
) {
this.id = id
this.condition = new Condition(condition)
this.requirements = requirements || []
}
async satisfies(
command: ICommand,
checker: IFileExistenceChecker
): Promise<boolean> {
return this.condition.satisfies(command, checker)
}
private checkRequirements(completed: string[]): boolean {
return this.requirements.every((req) => completed.includes(req))
}
private finished(completed: string[]): boolean {
return completed.includes(this.id)
}
active(completed: string[]): boolean {
return !this.finished(completed) && this.checkRequirements(completed)
}
getResponse(): IResponse {
return this.response
}
}
+18
View File
@@ -0,0 +1,18 @@
/* eslint-disable max-classes-per-file */
import { Event } from './Event'
import { IStageException, IGlobalException } from './schema/exception'
export class StageException extends Event {
readonly catchAll: boolean
constructor({ id, condition, response, catchAll }: IStageException) {
super(id, condition, response)
this.catchAll = catchAll
}
}
export class GlobalException extends Event {
constructor({ id, condition, response, requirements }: IGlobalException) {
super(id, condition, response, requirements)
}
}
+93
View File
@@ -0,0 +1,93 @@
/* eslint-disable no-await-in-loop */
import { Event } from './Event'
import { GlobalException } from './Exception'
import {
IQuest,
IFileExistenceChecker,
ICommand,
IResponse,
ITask,
} from './schema'
import { Stage } from './Stage'
export class Quest {
private quest: IQuest
private stages: Stage[]
private exceptions: GlobalException[] = []
private events: Map<string, Event> = new Map()
constructor(
quest: IQuest,
private checker: IFileExistenceChecker
) {
this.quest = quest
this.stages = quest.stages.map((stage) => new Stage(stage))
for (const stage of this.stages) {
this.events.set(stage.id, stage)
for (const exception of stage.getExceptions()) {
this.events.set(exception.id, exception)
}
}
for (const exception of quest.exceptions ?? []) {
const globalException = new GlobalException(exception)
this.exceptions.push(globalException)
this.events.set(globalException.id, globalException)
}
}
getActiveStages(completed: string[]) {
return this.stages.filter((stage) => stage.active(completed))
}
private getActiveExceptions(completed: string[]) {
return this.exceptions.filter((exception) => exception.active(completed))
}
async findSatisfiedEvent(
command: ICommand,
completed: string[]
): Promise<string | null> {
const stages = this.getActiveStages(completed)
let catchAllException: string | null = null
for (const stage of stages) {
const satisfies = await stage.satisfies(command, this.checker)
if (satisfies) {
return stage.id
}
}
for (const exception of stages.flatMap((stage) => stage.getExceptions())) {
if (exception.catchAll && !catchAllException) {
catchAllException = exception.id
} else if (await exception.satisfies(command, this.checker)) {
return exception.id
}
}
for (const exception of this.getActiveExceptions(completed)) {
if (await exception.satisfies(command, this.checker)) {
return exception.id
}
}
return catchAllException
}
getResponses(completed: string[]): IResponse[] {
return completed.map((id) => this.events.get(id)!.getResponse())
}
getTasks(completed: string[]): ITask[] {
const completedSet = new Set(completed)
const completedStages = this.stages
.filter((stage) => completedSet.has(stage.id))
.map((stage) => ({
id: stage.id,
name: stage.name,
completed: true,
}))
const activeStages = this.getActiveStages(completed).map((stage) => ({
id: stage.id,
name: stage.name,
completed: false,
}))
return [...completedStages, ...activeStages]
}
}
+67
View File
@@ -0,0 +1,67 @@
import { FileGraph, IFileNode } from '../file-graph'
import { Quest } from './Quest'
import {
IQuest,
ICommand,
ISession,
IFileExistenceChecker,
IResponse,
ITask,
} from './schema'
export class GameSession {
private completed: string[] = []
private quest: Quest
private graph: FileGraph
constructor(
{ completedEvents, graph }: Partial<ISession>,
quest: IQuest,
checker: IFileExistenceChecker
) {
this.completed = completedEvents || []
this.quest = new Quest(quest, checker)
this.graph = new FileGraph(
graph ?? { path: '/', type: 'directory', discovered: false }
)
}
get completedEvents() {
return this.completed
}
complete(stageId: string) {
this.completed.push(stageId)
}
getActiveStages(): string[] {
return this.quest.getActiveStages(this.completed).map((stage) => stage.id)
}
async runCommand(command: ICommand) {
if (command.params) {
this.graph.handleEvent(command.params)
}
const stageId = await this.quest.findSatisfiedEvent(command, this.completed)
if (stageId) {
this.complete(stageId)
}
return stageId
}
isFinished() {
return this.getActiveStages().length === 0
}
getResponses(): IResponse[] {
return this.quest.getResponses(this.completed)
}
getTasks(): ITask[] {
return this.quest.getTasks(this.completed)
}
getGraph(): IFileNode {
return this.graph
}
}
+26
View File
@@ -0,0 +1,26 @@
import { IStage } from './schema'
import { StageException } from './Exception'
import { Event } from './Event'
export class Stage extends Event {
name: string
private exceptions: StageException[]
constructor({
id,
name,
condition,
requirements,
exceptions,
response,
}: IStage) {
super(id, condition, response, requirements)
this.name = name || ''
this.exceptions =
exceptions?.map((exception) => new StageException(exception)) || []
}
getExceptions(): StageException[] {
return this.exceptions
}
}
+8
View File
@@ -0,0 +1,8 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { zodToJsonSchema } from 'zod-to-json-schema'
import fs from 'fs'
import { questSchema } from './schema'
const jsonSchema = zodToJsonSchema(questSchema)
fs.writeFileSync('quest.json', JSON.stringify(jsonSchema, null, 2))
+13
View File
@@ -0,0 +1,13 @@
import { ICommand } from '../schema'
import { Matcher } from './Matcher.js'
export class CommandMatcher implements Matcher {
private matcher: RegExp
constructor(command: string, flags?: string) {
this.matcher = new RegExp(command, flags)
}
match(command: ICommand): boolean {
return !!command.command && this.matcher.test(command.command)
}
}
+53
View File
@@ -0,0 +1,53 @@
import {
ICommand,
IFileExistenceChecker,
ICondition,
IFileExistenceInput,
} from '../schema'
import { PwdMatcher } from './PwdMatcher.js'
import { CommandMatcher } from './CommandMatcher.js'
import { ErrorMatcher } from './ErrorMatcher.js'
import { Matcher } from './Matcher.js'
import { OutputMatcher } from './OutputMatcher.js'
import { OrMatcher } from './OrMatcher.js'
import { NotMatcher } from './NotMatcher.js'
import { checkFiles } from './FileMatcher.js'
export class Condition {
private files: IFileExistenceInput[] = []
private matchers: Matcher[] = []
constructor({ command, output, error, pwd, or, not, files }: ICondition) {
if (command) this.matchers.push(new CommandMatcher(command))
if (output) this.matchers.push(new OutputMatcher(output))
if (pwd) this.matchers.push(new PwdMatcher(pwd))
if (or) {
const conditions = or.map((condition) => new Condition(condition))
this.matchers.push(new OrMatcher(conditions))
}
if (not) this.matchers.push(new NotMatcher(new Condition(not)))
if (files) this.files = files
this.matchers.push(new ErrorMatcher(error)) // always check unexpected error
}
match(command: ICommand): boolean {
return this.matchers.every((matcher) => {
return matcher.match(command)
})
}
async check(checker: IFileExistenceChecker): Promise<boolean> {
if (this.matchers.length === 0) return true
return checkFiles(checker, this.files)
}
async satisfies(
command: ICommand,
checker: IFileExistenceChecker
): Promise<boolean> {
// Error matcher is always true
if (this.matchers.length === 1 && this.files.length === 0) return false
// eslint-disable-next-line no-return-await
return this.match(command) && (await this.check(checker))
}
}
+18
View File
@@ -0,0 +1,18 @@
import { Matcher } from './Matcher.js'
import { ICommand } from '../schema'
export class ErrorMatcher implements Matcher {
private matcher?: RegExp
constructor(errorPattern?: string) {
if (errorPattern) {
this.matcher = new RegExp(errorPattern)
}
}
match(command: ICommand): boolean {
if (!this.matcher) {
return !command.error
}
return this.matcher.test(command.error || '')
}
}
+11
View File
@@ -0,0 +1,11 @@
import { IFileExistenceInput, IFileExistenceChecker } from '../schema'
export async function checkFiles(
checker: IFileExistenceChecker,
files: IFileExistenceInput[]
) {
const existences = await Promise.all(
files.map((file) => checker.exists(file))
)
return existences.every((exists, index) => exists === files[index].exists)
}
+5
View File
@@ -0,0 +1,5 @@
import { ICommand } from '../schema'
export interface Matcher {
match(command: ICommand): boolean
}
+10
View File
@@ -0,0 +1,10 @@
import { ICommand } from '../schema'
import { Matcher } from './Matcher.js'
export class NotMatcher implements Matcher {
constructor(private condition: Matcher) {}
match(command: ICommand): boolean {
return !this.condition.match(command)
}
}
+10
View File
@@ -0,0 +1,10 @@
import { ICommand } from '../schema'
import { Matcher } from './Matcher.js'
export class OrMatcher implements Matcher {
constructor(private conditions: Matcher[]) {}
match(command: ICommand): boolean {
return this.conditions.some((condition) => condition.match(command))
}
}
+13
View File
@@ -0,0 +1,13 @@
import { ICommand } from '../schema'
import { Matcher } from './Matcher.js'
export class OutputMatcher implements Matcher {
private matcher: RegExp
constructor(pattern: string, args?: string) {
this.matcher = new RegExp(pattern, args)
}
match(command: ICommand): boolean {
return !!command.output && this.matcher.test(command.output.trim())
}
}
+10
View File
@@ -0,0 +1,10 @@
import { ICommand } from '../schema'
import { Matcher } from './Matcher.js'
export class PwdMatcher implements Matcher {
constructor(private pwd: string) {}
match(command: ICommand): boolean {
return command.pwd === this.pwd
}
}
+6
View File
@@ -0,0 +1,6 @@
export * from './schema'
export * from './Quest'
export * from './Session'
export * from './Event'
export * from './Stage'
export * from './Exception'
+159
View File
@@ -0,0 +1,159 @@
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"image": {
"type": "string"
},
"instruction": {
"type": "string"
},
"requirements": {
"type": "array",
"items": {
"type": "string"
},
"default": []
},
"stages": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"pattern": "^[a-zA-Z0-9_-]+$"
},
"name": {
"type": "string"
},
"requirements": {
"$ref": "#/properties/requirements",
"default": []
},
"condition": {
"type": "object",
"properties": {
"command": {
"type": "string"
},
"output": {
"type": "string"
},
"error": {
"type": "string"
},
"pwd": {
"type": "string"
},
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"type": {
"type": "string",
"enum": ["file", "directory"]
},
"exists": {
"type": "boolean"
}
},
"required": ["path", "type", "exists"],
"additionalProperties": false
}
},
"or": {
"type": "array",
"items": {
"$ref": "#/properties/stages/items/properties/condition"
}
},
"not": {
"$ref": "#/properties/stages/items/properties/condition"
}
},
"additionalProperties": false
},
"response": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["narrative", "dialogue"],
"default": "narrative"
},
"content": {
"type": "string"
},
"hint": {
"type": "string"
}
},
"required": ["content"],
"additionalProperties": false
},
"exceptions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"condition": {
"$ref": "#/properties/stages/items/properties/condition"
},
"response": {
"$ref": "#/properties/stages/items/properties/response"
},
"catchAll": {
"type": "boolean",
"default": false
}
},
"required": ["id", "condition", "response"],
"additionalProperties": false
}
}
},
"required": ["id", "condition", "response"],
"additionalProperties": false
}
},
"exceptions": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"$ref": "#/properties/stages/items/properties/exceptions/items/properties/id"
},
"condition": {
"$ref": "#/properties/stages/items/properties/condition"
},
"response": {
"$ref": "#/properties/stages/items/properties/response"
},
"requirements": {
"$ref": "#/properties/requirements",
"default": []
}
},
"required": ["id", "condition", "response"],
"additionalProperties": false
}
}
},
"required": ["id", "title", "image", "instruction", "stages"],
"additionalProperties": false,
"$schema": "http://json-schema.org/draft-07/schema#"
}
+27
View File
@@ -0,0 +1,27 @@
import { z } from 'zod'
const fileSchema = z.object({
path: z.string(),
type: z.enum(['file', 'directory']),
discovered: z.boolean(),
empty: z.boolean().optional(),
})
export const commandParamsSchema = z.object({
discover: fileSchema.array().optional(),
add: fileSchema.array().optional(),
remove: fileSchema.array().optional(),
pwd: z.string().optional(),
})
export type ICommandParams = z.infer<typeof commandParamsSchema>
export const commandSchema = z.object({
command: z.string().optional(),
output: z.string().optional(),
error: z.string().optional(),
pwd: z.string().optional(),
params: commandParamsSchema.optional().nullable(),
})
export type ICommand = z.infer<typeof commandSchema>
+44
View File
@@ -0,0 +1,44 @@
import { z } from 'zod'
export const fileInputSchema = z.object({
path: z.string(),
type: z.enum(['file', 'directory']),
})
export type IFileInput = z.infer<typeof fileInputSchema>
export const fileExistenceInputSchema = fileInputSchema.extend({
exists: z.boolean(),
})
export type IFileExistenceInput = z.infer<typeof fileExistenceInputSchema>
const baseConditionSchema = z.object({
command: z.string().optional(),
output: z.string().optional(),
error: z.string().optional(),
pwd: z.string().optional(),
files: z.array(fileExistenceInputSchema).optional(),
})
export type ICondition = z.infer<typeof baseConditionSchema> & {
or?: ICondition[]
not?: ICondition
}
export const conditionSchema: z.ZodType<ICondition> =
baseConditionSchema.extend({
or: z.lazy(() => z.array(conditionSchema).optional()),
not: z.lazy(() => conditionSchema.optional()),
})
export interface IFileExistenceChecker {
exists(file: IFileInput): Promise<boolean>
}
export class VoidFileExistenceChecker implements IFileExistenceChecker {
// eslint-disable-next-line class-methods-use-this
async exists(): Promise<boolean> {
return false
}
}
+24
View File
@@ -0,0 +1,24 @@
import { z } from 'zod'
import { conditionSchema } from './condition.js'
import { responseSchema } from './response.js'
import { requirementsSchema } from './requirements.js'
export const exceptionSchema = z.object({
id: z.string(),
condition: conditionSchema,
response: responseSchema,
})
export type IException = z.infer<typeof exceptionSchema>
export const stageExceptionSchema = exceptionSchema.extend({
catchAll: z.boolean().default(false),
})
export type IStageException = z.infer<typeof stageExceptionSchema>
export const globalExceptionSchema = exceptionSchema.extend({
requirements: requirementsSchema.default([]),
})
export type IGlobalException = z.infer<typeof globalExceptionSchema>
+20
View File
@@ -0,0 +1,20 @@
import { IFileNode } from '../../file-graph'
export * from './condition.js'
export * from './stage.js'
export * from './requirements.js'
export * from './response.js'
export * from './quest.js'
export * from './exception.js'
export * from './command.js'
export interface ISession {
completedEvents: string[]
graph: IFileNode
}
export interface ITask {
id: string
name: string
completed: boolean
}
+16
View File
@@ -0,0 +1,16 @@
import { z } from 'zod'
import { stageSchema } from './stage.js'
import { requirementsSchema } from './requirements.js'
import { globalExceptionSchema } from './exception.js'
export const questSchema = z.object({
id: z.string(),
title: z.string(),
image: z.string(),
instruction: z.string(),
requirements: requirementsSchema.default([]),
stages: z.array(stageSchema),
exceptions: z.array(globalExceptionSchema).optional(),
})
export type IQuest = z.infer<typeof questSchema>
+5
View File
@@ -0,0 +1,5 @@
import { z } from 'zod'
export const requirementsSchema = z.array(z.string())
export type IRequirements = z.infer<typeof requirementsSchema>
+9
View File
@@ -0,0 +1,9 @@
import { z } from 'zod'
export const responseSchema = z.object({
type: z.enum(['narrative', 'dialogue']).default('narrative'),
content: z.string(),
hint: z.string().optional(),
})
export type IResponse = z.infer<typeof responseSchema>
+16
View File
@@ -0,0 +1,16 @@
import { z } from 'zod'
import { conditionSchema } from './condition.js'
import { responseSchema } from './response.js'
import { stageExceptionSchema } from './exception.js'
import { requirementsSchema } from './requirements.js'
export const stageSchema = z.object({
id: z.string().regex(/^[a-zA-Z0-9_-]+$/),
name: z.string().optional(),
requirements: requirementsSchema.default([]),
condition: conditionSchema,
response: responseSchema,
exceptions: stageExceptionSchema.array().optional(),
})
export type IStage = z.infer<typeof stageSchema>
Loaded 100 of 284 files, more files were not shown because too many files have changed in this diff. Show more