Compare commits

..
287 Commits
Author SHA1 Message Date
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
Lin, Hung-Shin 8d0a210f0e Fix current quests (#144) 2024-07-03 08:15:18 +08:00
wancat c38104d043 fix: typo 2024-07-03 08:09:57 +08:00
wancat 83e563ab56 fix: requirement in get-started 2024-07-03 08:05:58 +08:00
wancat fa2059814c fix: increase line height in description part 2024-07-03 08:05:40 +08:00
Chen, Tai-Lin f9fa5a2102 fix: update quest [remove] 2024-07-03 01:57:39 +08:00
Chen, Tai-Lin 8ed6acbcc1 fix: update quest [move] 2024-07-03 01:47:59 +08:00
Chen, Tai-Lin d2452c91a6 fix: update quest [copy] 2024-07-03 01:42:37 +08:00
Chen, Tai-Lin 5bac9ce099 style: revise description part 2024-07-03 01:25:34 +08:00
Chen, Tai-Lin 2aaff0f7bc fix: quests exceptions 2024-07-03 01:10:59 +08:00
Chen, Tai-Lin 96ab39437b fix: quest error [remove] 2024-07-03 01:02:15 +08:00
Chen, Tai-Lin d6b606e807 fix: update quest [get-started] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin b792992391 fix: update quest [explore] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin 922100d8da fix: update quest [cast] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin 52597e8c5e fix: update quest [explore] 2024-07-03 01:01:30 +08:00
Chen, Tai-Lin a6c160ac0e fix: update quest [cast] 2024-07-03 01:00:33 +08:00
Chen, Tai-Lin 3859403b56 fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 01:00:17 +08:00
Chen, Tai-Lin 2a29b0b8ac fix: update quest [read] 2024-07-03 00:57:33 +08:00
Chen, Tai-Lin aaa97e0dca fix: update quest [path] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 3a28af0a67 fix: update quest [move] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 551d502845 fix: update quest [helloworld] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 3f0191112d fix: update quest [get-started] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin d565709a59 fix: update quest [explore] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 435a426f5b fix: update quest [remove] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 377afb3078 fix: update quest [create] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 9d26951978 fix: update quest [cast] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin db3b00f8ee fix: update quest [copy] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin c09c4ff9e0 fix: quests exceptions error 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin b3b374b3cd fix: update quest [explore] 2024-07-03 00:53:56 +08:00
Chen, Tai-Lin 5ca187651d fix: update quest answer file [get-started] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin f2ca4a26ac fix: update quest's exceptions 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 462f17a779 fix: update quest [cast] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3deabb25a9 fix: update quest [get-started] 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3dad7ddccb fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 25888a1465 fix: modify component name and title (HintPart=>DescriptionPart) 2024-07-03 00:53:55 +08:00
Chen, Tai-Lin 3559814f55 fix: quest error [remove] 2024-07-03 00:44:30 +08:00
Chen, Tai-Lin 8142ff816c Merge branch 'fix-current-quests' of https://github.com/lancatlin/linux-odyssey into fix-current-quests 2024-07-03 00:34:39 +08:00
Chen, Tai-Lin 9c160253c9 fix: update quest [read] 2024-07-03 00:34:33 +08:00
Chen, Tai-Lin 80e1a40b6e fix: update quest [path] 2024-07-03 00:34:33 +08:00
Chen, Tai-Lin 2b70f3c7cd fix: update quest [move] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 2e29919ad9 fix: update quest [helloworld] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin ca79042a3b fix: update quest [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin c8d2320bf5 fix: update quest [explore] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 441909efd5 fix: update quest [remove] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 90e3fc2bd0 fix: update quest [create] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 598141d735 fix: update quest [cast] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin e11432be45 fix: update quest [copy] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 2c1fe7bb96 fix: quests exceptions error 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 8c54bfe192 fix: update quest [explore] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin d292195302 fix: update quest answer file [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 6bbf70a258 fix: update quest's exceptions 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 526b856de7 fix: update quest [cast] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 1a20ffb295 fix: update quest [get-started] 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 4efa9b8831 fix: revise quest content [get-started] (1 to be fixed) 2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 3944e91d0c fix: modify component name and title (HintPart=>DescriptionPart) 2024-07-03 00:34:32 +08:00
wancat c30b4a7c6e feat: add isGuest migration script 2024-07-03 00:34:32 +08:00
wancat 14cd16b611 feat: add isGuest field in user model 2024-07-03 00:34:32 +08:00
wancat b07b60f6bd feat: add register guest button 2024-07-03 00:34:32 +08:00
wancat 9d1bed7543 feat: add register-guest api 2024-07-03 00:34:32 +08:00
wancat 715352f77c feat: install date-fns 2024-07-03 00:34:32 +08:00
Annie Huang c4550a395a fix map view 2024-07-03 00:34:32 +08:00
dependabot[bot] 5795cb16b8 build(deps): bump tar from 6.2.0 to 6.2.1
Bumps [tar](https://github.com/isaacs/node-tar) from 6.2.0 to 6.2.1.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v6.2.0...v6.2.1)

---
updated-dependencies:
- dependency-name: tar
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-07-03 00:34:32 +08:00
Chen, Tai-Lin 691a6dcb25 fix: update quest [read] 2024-07-03 00:32:04 +08:00
Chen, Tai-Lin 979ea72f3a fix: update quest [path] 2024-07-03 00:31:57 +08:00
Chen, Tai-Lin f9e40edb8a fix: update quest [move] 2024-07-03 00:31:46 +08:00
Chen, Tai-Lin 276bfee5ed fix: update quest [helloworld] 2024-07-03 00:31:40 +08:00
Chen, Tai-Lin f03c359f89 fix: update quest [get-started] 2024-07-03 00:31:30 +08:00
Chen, Tai-Lin 7db08a5cda fix: update quest [explore] 2024-07-03 00:31:20 +08:00
Chen, Tai-Lin b19f0da678 fix: update quest [remove] 2024-07-03 00:31:10 +08:00
Chen, Tai-Lin c87eef233c fix: update quest [create] 2024-07-03 00:30:59 +08:00
Chen, Tai-Lin dabc40eb1e fix: update quest [cast] 2024-07-03 00:30:44 +08:00
Chen, Tai-Lin 0efe2235f8 fix: update quest [copy] 2024-07-03 00:30:37 +08:00
Chen, Tai-Lin 9d45993d0d fix: quests exceptions error 2024-07-02 22:32:00 +08:00
Chen, Tai-Lin 79f9cb3e95 fix: update quest [explore] 2024-07-02 02:09:09 +08:00
Chen, Tai-Lin 47c7ff04a9 fix: update quest answer file [get-started] 2024-07-02 00:49:42 +08:00
Chen, Tai-Lin f831f4d4e5 fix: update quest's exceptions 2024-07-01 23:39:58 +08:00
Chen, Tai-Lin 29799faa79 fix: update quest [cast] 2024-07-01 23:10:46 +08:00
Chen, Tai-Lin 2727c9f4bb fix: update quest [get-started] 2024-07-01 22:38:42 +08:00
Chen, Tai-Lin 9ed726d597 fix: revise quest content [get-started] (1 to be fixed) 2024-06-28 02:25:34 +08:00
Chen, Tai-Lin fcb52b9d9e fix: modify component name and title (HintPart=>DescriptionPart) 2024-06-28 02:14:22 +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
258 changed files with 9514 additions and 5565 deletions

No files matched your search

+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:5173
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 }}
-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
+4
View File
@@ -1,3 +1,6 @@
config
.quest
# Logs
logs
*.log
@@ -85,6 +88,7 @@ built
.env.test.local
.env.production.local
.env.local
.env.prod
# parcel-bundler cache (https://parceljs.org/)
.cache
+4 -1
View File
@@ -1,5 +1,8 @@
{
"editor.tabSize": 2,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"yaml.schemas": {
"packages/game/quest.json": "quests/**/game.yml"
}
}
+112 -74
View File
@@ -5,9 +5,15 @@
## 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 +21,66 @@
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
The backend will use a SSH key pair to login to the player's container. The private key is on the host, and the public key is copied to the player's container.
The key pair is stored in `./config/ssh_key` and `./config/ssh_key.pub`. If it doesn't exist, the backend will generate a new pair and store them.
## Docker Setup
### Docker compose files explaination
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
yarn build:quests
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 +96,49 @@ 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 improve the development experience, we can mount the host directory to the container in the pattern:
/path/to/linux-odyssey/quests/${quest_id}/home:/home/commander
/path/to/linux-odyssey/packages/container:/usr/local/lib/container
You can edit the files on your host, and the changes will be reflected in the container. This is disabled by default. To enable it, you can set the following environment variables:
MOUNT_QUEST=true
## Development
Copy `.env.sample` to `.env` and customize it for your needs.
Run dev server (frontend, backend):
yarn dev
**IMPORTANT NOTE**: only the app and server have hot-reloading, other packages require manually running `yarn dev` in each package.
To develop analytics dashboard:
yarn analytics
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 +155,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
+6 -17
View File
@@ -1,26 +1,15 @@
# 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
+11 -13
View File
@@ -5,24 +5,21 @@
"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",
"socket.io-client": "^4.7.1",
@@ -37,13 +34,14 @@
},
"devDependencies": {
"@types/validator": "^13.11.9",
"@vitejs/plugin-vue": "^4.5.2",
"@vue/compiler-sfc": "^3.3.13",
"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"
"@vitejs/plugin-vue": "^5.2.2",
"@vue/tsconfig": "^0.7.0",
"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" />
+27 -4
View File
@@ -25,14 +25,37 @@ const html = computed(() => {
line-height: 1;
}
.markdown-content :deep(p) {
line-height: 1.5;
}
.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>
+24 -16
View File
@@ -2,7 +2,7 @@
import { useRouter } from 'vue-router'
import { useToast } from 'vue-toastification'
import useSession from '../../store/session'
import { surveyUrl } from '../../config'
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>
@@ -5,8 +5,9 @@ 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>
@@ -37,7 +52,7 @@ const right = () => {
:icon="['far', 'lightbulb']"
class="text-yellow-200 p-2 content-center"
/>
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">提示 {{ length }}</h1>
<h1 class="inline text-text w-1/2 font-xl p-2 m-1">說明</h1>
<div v-if="current !== -1" class="flex w-full justify-end items-end">
<button
class="p-2 m-1 w-1/8"
@@ -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 />
+2 -2
View File
@@ -2,11 +2,11 @@
import { computed } from 'vue'
import MarkdownText from '../MarkdownText.vue'
import useSession from '../../store/session'
import { Task } from '../../types'
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 {
+21 -10
View File
@@ -1,23 +1,34 @@
<script setup lang="ts">
import { onMounted, ref } from 'vue'
import { onMounted, onUnmounted, ref } from 'vue'
import { useTerminal } from '../../store/session'
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>
</div>
<div
id="terminal"
ref="terminal"
class="bg-bg-secondary flex-grow overflow-hidden px-2"
style="height: calc(100vh - 40px)"
></div>
</div>
</template>
+10 -18
View File
@@ -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,7 +3,8 @@ 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 { trpc } from '../../utils/trpc'
import { openBugReport } from '../../utils/formbricks'
import useSession from '../../store/session'
import useUserProfile from '../../store/userProfile'
@@ -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,
}
+59 -31
View File
@@ -1,19 +1,37 @@
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', {
@@ -24,44 +42,50 @@ 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',
}
term.reset()
await socket.connect(session)
await socket.connect(this.session)
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')
},
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()
@@ -76,11 +100,15 @@ const useSession = defineStore('session', {
term.onData((data: string) => {
socket.emit('terminal', data)
})
socket.on('graph', (event: { discover: FileObject[]; pwd: string }) => {
this.updateGraph(event)
socket.on('graph', (event: FileGraphUpdateEvent) => {
if (!this.session) return
this.session.graph.handleEvent(event)
if (event.pwd) {
this.session.pwd = event.pwd
}
})
socket.on('response', (response: StageResponse) => {
this.newResponse(response)
socket.on('update', (update: SessionUpdate) => {
this.newUpdate(update)
})
hasSetup = true
},
+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() {
+4 -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,6 @@ export interface Session {
status: string
graph: FileGraph
pwd: string
hints: string[][]
tasks: Task[]
responses: IResponse[][]
tasks: ITask[]
responses: IResponse[]
}
-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
+1 -1
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
+21 -24
View File
@@ -6,33 +6,39 @@ class SocketTerminal {
private term: Terminal
private fitAddon: FitAddon
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()
}
resizeScreen(): void {
try {
this.fitAddon.fit()
} catch (e) {
console.error('Failed to resize terminal:', e)
}
}
mount(reference: HTMLElement): void {
this.term.open(reference)
this.resizeScreen()
}
write(message: string): void {
this.term.write(message)
}
onData(callback: any): void {
onData(callback: (data: string) => void): void {
this.term.onData(callback)
}
@@ -40,15 +46,6 @@ class SocketTerminal {
this.term.focus()
}
resizeScreen(): void {
this.fitAddon.fit()
}
mount(reference: HTMLElement): void {
this.term.open(reference)
this.resizeScreen()
}
reset(): void {
this.term.reset()
}
+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'
+13 -3
View File
@@ -1,22 +1,32 @@
<script setup lang="ts">
import { computed, onMounted } from 'vue'
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 HintPart from '../components/game/HintPart.vue'
import DescriptionPart from '../components/game/DescriptionPart.vue'
import QuestPart from '../components/game/QuestPart.vue'
import TerminalPart from '../components/game/TerminalPart.vue'
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,
@@ -62,7 +72,7 @@ onMounted(async () => {
<div class="bg-bg h-full rounded-lg w-1/2">
<StartButton :questId="$props.questId" />
<section id="hint" class="h-3/5">
<HintPart />
<DescriptionPart />
</section>
<hr class="border-border border" />
<section id="terminal" class="h-2/5 overflow-hidden">
+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"]
}
+15
View File
@@ -1,11 +1,26 @@
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: {
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,
+24
View File
@@ -1,5 +1,6 @@
server {
listen 80;
error_log /var/log/nginx/error.log debug;
location / {
root /usr/share/nginx/html;
@@ -10,10 +11,29 @@ server {
location /api/ {
proxy_pass http://backend:3000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
location /trpc/ {
proxy_pass http://backend:3000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
location /api-docs/ {
proxy_pass http://backend:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /socket.io/ {
@@ -23,5 +43,9 @@ server {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_buffering off;
}
}
+10 -25
View File
@@ -1,4 +1,3 @@
version: '3'
services:
app:
image: linuxodyssey/frontend
@@ -10,7 +9,7 @@ services:
dockerfile: app/Dockerfile
target: prod
networks:
internal:
- internal
ports:
- 8080:80
@@ -29,15 +28,14 @@ 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}
analytics:
image: linuxodyssey/analytics
@@ -52,13 +50,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,19 +63,8 @@ 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
+8 -4
View File
@@ -1,7 +1,7 @@
version: '3'
services:
db:
image: mongo
image: mongo:6
restart: always
networks:
- internal
@@ -12,12 +12,16 @@ services:
base:
image: linuxodyssey/quest-base
networks:
- players-dev
build:
context: .
dockerfile: ./quests/Dockerfile
extra_hosts:
- 'host.docker.internal:host-gateway'
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!
+12 -10
View File
@@ -16,7 +16,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 +33,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"
}
+15 -8
View File
@@ -1,19 +1,26 @@
FROM node:20-alpine as build
WORKDIR /app
COPY ./package.json yarn.lock tsconfig.json ./
COPY ./package.json yarn.lock ./
COPY packages/analytics/package.json ./packages/analytics/package.json
COPY packages/models/package.json ./packages/models/package.json
RUN yarn install --frozen-lockfile
COPY tsconfig.json ./tsconfig.json
COPY packages ./packages
RUN yarn workspace analytics build
# Production stage
FROM node:20-alpine as prod
WORKDIR /app
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/packages/analytics ./packages/analytics
ENV NODE_ENV=production
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"
}
+7 -10
View File
@@ -3,30 +3,27 @@
"version": "1.0.0",
"main": "src/index.js",
"private": true,
"type": "module",
"license": "GPL",
"scripts": {
"start": "node dist/index.js",
"build": "tsc -b -f",
"watch": "tsc -b --watch",
"dev": "nodemon"
"start": "node dist/analytics/src/index.js",
"build": "tsc",
"type-check": "tsc --noEmit",
"dev": "tsx watch src/index.ts"
},
"dependencies": {
"@linux-odyssey/models": "*",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express": "^4.20.0",
"express-basic-auth": "^1.2.1",
"express-handlebars": "^7.1.2",
"mongoose": "^8.0.3"
"mongoose": "^8.9.5"
},
"devDependencies": {
"@swc/core": "^1.4.2",
"@swc/helpers": "^0.5.6",
"@types/express": "^4.17.21",
"@types/node": "^20.11.20",
"nodemon": "^3.1.0",
"regenerator-runtime": "^0.14.1",
"ts-node": "^10.9.2",
"tsx": "^4.19.3",
"typescript": "^5.3.3"
}
}
+6 -4
View File
@@ -21,14 +21,16 @@ interface Config {
}
const config: Config = {
host: process.env.HOST || 'localhost',
port: process.env.PORT ? parseInt(process.env.PORT, 10) : 3001,
host: process.env.ADMIN_HOST || 'localhost',
port: process.env.ADMIN_PORT ? parseInt(process.env.ADMIN_PORT, 10) : 3001,
db: process.env.MONGO_URL || 'mongodb://localhost:27017/odyssey-test',
username: getOrFailed('ADMIN_USERNAME'),
password: getOrFailed('ADMIN_PASSWORD'),
baseUrl:
process.env.BASE_URL ||
`http://${process.env.HOST || 'localhost'}:${process.env.PORT ? parseInt(process.env.PORT, 10) : 3001}`,
process.env.ADMIN_BASE_URL ||
`http://${process.env.ADMIN_HOST || 'localhost'}:${
process.env.ADMIN_PORT ? parseInt(process.env.ADMIN_PORT, 10) : 3001
}`,
}
export default config
@@ -1,6 +1,6 @@
import type { Request, Response } from 'express'
import Pagination from '../models/pagination.js'
import { errorCommands } from '../models/commands.js'
import Pagination from '../models/pagination'
import { errorCommands } from '../models/commands'
export async function commandListController(
req: Request,
+2 -2
View File
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'
import { userCount } from '../models/users.js'
import { questList } from '../models/quests.js'
import { userCount } from '../models/users'
import { questList } from '../models/quests'
export async function homeController(req: Request, res: Response) {
const quests = await questList()
+1 -1
View File
@@ -1,5 +1,5 @@
import { Request, Response } from 'express'
import { questList } from '../models/quests.js'
import { questList } from '../models/quests'
export async function questListController(req: Request, res: Response) {
const quests = await questList()
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'
import Pagination from '../models/pagination.js'
import { sessionList, sessionDetail } from '../models/sessions.js'
import Pagination from '../models/pagination'
import { sessionList, sessionDetail } from '../models/sessions'
export async function sessionListController(req: Request, res: Response) {
const { nextKey, order } = req.query as { nextKey: string; order: string }
+2 -2
View File
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'
import mongoose from 'mongoose'
import Pagination from '../models/pagination.js'
import { userList, userDetail, idToUser } from '../models/users.js'
import Pagination from '../models/pagination'
import { userList, userDetail, idToUser } from '../models/users'
export async function userListController(req: Request, res: Response) {
const { nextKey, order } = req.query as { nextKey: string; order: string }
+3 -3
View File
@@ -1,9 +1,9 @@
import connectDB from '@linux-odyssey/models'
import express from 'express'
import { engine } from 'express-handlebars'
import basicAuth from 'express-basic-auth'
import config from './config.js'
import router from './router.js'
import { connectDB } from '../../models'
import config from './config'
import router from './router'
async function main() {
await connectDB(config.db)
+2 -2
View File
@@ -1,5 +1,5 @@
import { Command } from '@linux-odyssey/models'
import Pagination from './pagination.js'
import { Command } from '../../../models'
import Pagination from './pagination'
// eslint-disable-next-line import/prefer-default-export
export async function errorCommands(pagination: Pagination) {
+1 -1
View File
@@ -1,5 +1,5 @@
/* eslint-disable class-methods-use-this */
// eslint-disable-next-line max-classes-per-file
/* eslint-disable max-classes-per-file */
import mongoose, { Types } from 'mongoose'
export const DESC: string = 'desc'
+10 -4
View File
@@ -1,8 +1,14 @@
/* eslint-disable object-shorthand */
/* eslint-disable func-names */
import { UserProfile } from '@linux-odyssey/models'
import { UserProfile } from '../../../models'
export function questList() {
export interface QuestProgress {
_id: string
started: number
completed: number
completedRate: number
failed: number
}
export function questList(): Promise<QuestProgress[]> {
return UserProfile.aggregate([
{
$project: {
+2 -2
View File
@@ -1,5 +1,5 @@
import { Command, Session } from '@linux-odyssey/models'
import Pagination from './pagination.js'
import { Command, Session } from '../../../models'
import Pagination from './pagination'
import {
CommandObject,
SessionDetail,
+10 -6
View File
@@ -1,7 +1,7 @@
import { Session, User } from '@linux-odyssey/models'
import { Types } from 'mongoose'
import Pagination from './pagination.js'
import { IUser, UserSessionDetail } from '../interface.js'
import { HydratedDocument, Types } from 'mongoose'
import { Session, User, IUser, ISession } from '../../../models'
import Pagination from './pagination'
import { UserSessionDetail } from '../interface'
function loginMethods(user: IUser): string[] {
const methods: string[] = []
@@ -23,7 +23,11 @@ export function userCount() {
}
export async function userList(pagination: Pagination) {
const users = await Session.aggregate([
const users: {
userData: HydratedDocument<IUser>
count: number
lastActivityAt: Date
}[] = await Session.aggregate([
pagination.match('user'),
{
$group: {
@@ -62,7 +66,7 @@ export async function userList(pagination: Pagination) {
export async function userDetail(
id: Types.ObjectId
): Promise<UserSessionDetail[]> {
const sessions = await Session.find({
const sessions: HydratedDocument<ISession>[] = await Session.find({
user: id,
})
return sessions.map((session): UserSessionDetail => {
+5 -8
View File
@@ -1,16 +1,13 @@
import { Router } from 'express'
import { homeController } from './controllers/index.js'
import {
userListController,
userDetailController,
} from './controllers/users.js'
import { questListController } from './controllers/quests.js'
import { commandListController } from './controllers/commands.js'
import { homeController } from './controllers/index'
import { userListController, userDetailController } from './controllers/users'
import { questListController } from './controllers/quests'
import { commandListController } from './controllers/commands'
import {
sessionDetailController,
sessionListController,
} from './controllers/sessions.js'
} from './controllers/sessions'
const router = Router()
+1 -3
View File
@@ -1,11 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "./dist"
},
"include": ["src"],
"references": [{ "path": "../models" }],
"include": ["src", "../models"],
"ts-node": {
"swc": true
}
-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)
+20 -49
View File
@@ -1,10 +1,6 @@
/* 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 handleCommand = require('./handleCommand.js')
const {
API_ENDPOINT,
@@ -16,13 +12,6 @@ const {
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')
@@ -31,34 +20,6 @@ async function readOrNone(file) {
}
}
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)
@@ -75,15 +36,21 @@ async function main() {
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
const response = await fetch(`${API_ENDPOINT}/api/v1/commands`, {
method: 'POST',
headers: {
Authorization: `Bearer ${TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
})
if (!response.ok) {
console.error(`Failed to send command: ${response.statusText}`)
const responseBody = await response.text()
console.error(`Response Body: ${responseBody}`)
}
console.error(err.message)
} catch (e) {
console.error('Error occurred while sending command:', e)
}
}
@@ -93,6 +60,10 @@ main()
console.log(
'發生預期外的錯誤,請嘗試重新啟動 Quest,如果問題持續發生,請幫我們填寫右上角的錯誤回報表單。'
)
console.error(err)
if (err.response && err.response.data) {
console.log(err.response.data)
} else {
console.log(err)
}
exit(1)
})
+36 -16
View File
@@ -1,21 +1,33 @@
const fs = require('fs').promises
const path = require('path')
const { resolvePath } = require('./utils')
async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) {
const stats = await fs.stat(inputPath)
let stats
try {
stats = await fs.stat(inputPath)
} catch (e) {
if (e.code === 'ENOENT') {
return []
}
throw e
}
const name = path.basename(inputPath)
const discovered = level <= 1 && (hiddenFiles || !name.startsWith('.'))
const files = [
{
path: inputPath,
name,
type: stats.isDirectory() ? 'folder' : 'file',
discovered,
},
]
const self = {
path: inputPath,
name,
type: stats.isDirectory() ? 'directory' : 'file',
discovered,
}
const files = [self]
if (stats.isDirectory() && level < 2) {
try {
let entries = await fs.readdir(inputPath)
if (entries.length === 0) {
self.empty = true
return files
}
if (level > 0) {
entries = entries.slice(0, 5)
}
@@ -35,15 +47,23 @@ async function collectFilesInfo(inputPath, level = 0, hiddenFiles = false) {
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() }
const result = (
await Promise.all(
targetPath
.map((p) => resolvePath(p))
.map((p) => collectFilesInfo(p, 0, argv.a || argv.all))
)
).flat()
if (result.length === 0) {
return {}
}
return { discover: result }
} catch (_) {
return {}
}
}
module.exports = discoverFiles
module.exports = {
discoverFiles,
collectFilesInfo,
}
+174
View File
@@ -0,0 +1,174 @@
const fs = require('fs').promises
const path = require('path')
const minimist = require('minimist')
const { discoverFiles, collectFilesInfo } = require('./discover')
const { resolvePath } = require('./utils')
const commandListeners = {
ls: {
opts: {
boolean: ['a', 'l', 'h'],
},
handler: discoverFiles,
},
cd: {
handler: changeDirectory,
},
touch: {
handler: createFiles,
},
mkdir: {
opts: {
boolean: ['p'],
},
handler: createFiles,
},
cp: {
opts: {
boolean: ['r'],
},
handler: copyFiles,
},
rm: {
opts: {
boolean: ['r', 'f', 'v'],
},
handler: removeFiles,
},
rmdir: {
opts: {
boolean: ['p', 'v'],
},
handler: removeFiles,
},
mv: {
handler: moveFiles,
},
pwd: {
handler: showCurrentPath,
},
}
async function handleCommand(command) {
const name = command.split(' ')[0]
const listener = commandListeners[name]
if (!listener) return {}
const argv = minimist(command.split(' '), listener.opts)
return listener.handler(argv)
}
function changeDirectory() {
return {
pwd: process.cwd(),
}
}
async function createFiles(argv) {
const { _ } = argv
const files = _.slice(1)
const results = []
for (const file of files) {
const filePath = resolvePath(file)
try {
const exists = await fs.stat(filePath)
if (exists) {
results.push({
path: filePath,
type: exists.isFile() ? 'file' : 'directory',
discovered: true,
})
}
} catch (err) {
if (err.code !== 'ENOENT') {
throw err
}
}
}
return {
add: results,
}
}
async function copyFiles(argv) {
const { _ } = argv
const files = _.slice(1)
if (files.length === 0) {
return undefined
}
const dest = files.pop()
const destPath = resolvePath(dest)
return {
discover: await collectFilesInfo(destPath, 0, true),
}
}
async function removeFiles(argv) {
const files = argv._.slice(1)
const results = []
for (const file of files) {
const filePath = resolvePath(file)
try {
await fs.access(filePath, fs.constants.F_OK)
} catch {
results.push({
path: filePath,
type: 'file',
discovered: false,
})
}
}
return {
remove: results,
}
}
async function moveFiles(argv) {
const { _ } = argv
const sources = _.slice(1)
if (sources.length === 0) {
return undefined
}
const dest = sources.pop()
const results = {
add: [],
remove: [],
}
// check source
for (const source of sources) {
const sourcePath = resolvePath(source)
try {
await fs.access(sourcePath, fs.constants.F_OK)
} catch {
results.remove.push({
path: sourcePath,
type: 'file',
discovered: false,
})
}
}
// check dest
const destPath = resolvePath(dest)
results.add = await collectFilesInfo(destPath, 0, true)
return results
}
function showCurrentPath() {
const pwd = process.cwd()
const dirs = pwd.split('/')
let currentPath = '/'
const result = []
for (const dir of dirs) {
currentPath = path.join(currentPath, dir)
result.push({
path: currentPath,
type: 'directory',
discovered: true,
})
}
return {
add: result,
}
}
module.exports = handleCommand
-1
View File
@@ -8,7 +8,6 @@
"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,
}
+10
View File
@@ -0,0 +1,10 @@
const os = require('os')
const path = require('path')
function resolvePath(p) {
return path.resolve(process.cwd(), p.replace('~', os.homedir()))
}
module.exports = {
resolvePath,
}
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)
}
}
Loaded 100 of 258 files, more files were not shown because too many files have changed in this diff. Show more