Compare commits
68 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c318bd301a | ||
|
|
86a2b2fda0 | ||
|
|
8a8b7319d5 | ||
|
|
2428de23ee | ||
|
|
7ec889e759 | ||
|
|
9d009c40dd | ||
|
|
663d63bde0 | ||
|
|
6b83dcbf8f | ||
|
|
72e925fb6f | ||
|
|
c299e95bc6 | ||
|
|
2e40605d59 | ||
|
|
3455a2ca6c | ||
|
|
6fe858d86a | ||
|
|
7cc161c828 | ||
|
|
e14bf2bfa0 | ||
|
|
34672414c3 | ||
|
|
f7557bcc0f | ||
|
|
e4b6cdfb18 | ||
|
|
8cd3352017 | ||
|
|
c4ec6a1445 | ||
|
|
b3117c27f2 | ||
|
|
54ba4dbb0b | ||
|
|
a744415eb2 | ||
|
|
55f936fee9 | ||
|
|
d9e44e2574 | ||
|
|
52d4313156 | ||
|
|
c2b531e968 | ||
|
|
ab7d617886 | ||
|
|
b247b12c3f | ||
|
|
c26bc209eb | ||
|
|
ad1872e7cd | ||
|
|
29c7704e0b | ||
|
|
e41e40c5bf | ||
|
|
31e08d231d | ||
|
|
45b8dd972b | ||
|
|
4e72673373 | ||
|
|
92c3dd9232 | ||
|
|
2c5ab3ea7d | ||
|
|
ffa279f3df | ||
|
|
89dddfbb23 | ||
|
|
cfedcd724c | ||
|
|
fd28eaad06 | ||
|
|
8ce06facbd | ||
|
|
1a9db31c77 | ||
|
|
ac70b063b8 | ||
|
|
d0019c2c9b | ||
|
|
4c68408f1e | ||
|
|
1072505219 | ||
|
|
07fc555324 | ||
|
|
50fb29038a | ||
|
|
3785c623f8 | ||
|
|
14be3017c5 | ||
|
|
978bab0b81 | ||
|
|
d9d7f62d81 | ||
|
|
11e0613165 | ||
|
|
77c3d2bbb8 | ||
|
|
784a67afff | ||
|
|
19a092dd31 | ||
|
|
4c2f74949b | ||
|
|
a3fc3782ef | ||
|
|
cdc93f032a | ||
|
|
7dd15ed064 | ||
|
|
d169434157 | ||
|
|
6efed72bf2 | ||
|
|
15db311801 | ||
|
|
82d509129b | ||
|
|
939d838e35 | ||
|
|
6de4ce58c4 |
2
.github/workflows/deploy-site.yaml
vendored
2
.github/workflows/deploy-site.yaml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
|
--secret-access-key ${{secrets.AWS_DEPLOYMENT_USER_SECRET_ACCESS_KEY}} \
|
||||||
--region us-east-1 \
|
--region us-east-1 \
|
||||||
&& \
|
&& \
|
||||||
echo "::set-env name=SESSION_NAME::${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)"
|
echo "SESSION_NAME=${{github.actor}}-${{github.event_name}}-$(echo ${{github.sha}} | cut -c1-8)" >> $GITHUB_ENV
|
||||||
working-directory: aws
|
working-directory: aws
|
||||||
-
|
-
|
||||||
name: "Infrastructure: Deploy IAM stack"
|
name: "Infrastructure: Deploy IAM stack"
|
||||||
|
|||||||
2
.github/workflows/quality-checks.yaml
vendored
2
.github/workflows/quality-checks.yaml
vendored
@@ -1,6 +1,6 @@
|
|||||||
name: Quality checks
|
name: Quality checks
|
||||||
|
|
||||||
on: push
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
|
|||||||
2
.github/workflows/security-checks.yaml
vendored
2
.github/workflows/security-checks.yaml
vendored
@@ -2,6 +2,8 @@ name: Security checks
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
pull_request:
|
||||||
|
paths: [ '/package.json', '/package-lock.json' ] # Allow PRs to be green if they do not introduce dependency change
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0'
|
- cron: '0 0 * * 0'
|
||||||
|
|
||||||
|
|||||||
7
.github/workflows/test.yaml
vendored
7
.github/workflows/test.yaml
vendored
@@ -1,10 +1,13 @@
|
|||||||
name: Test
|
name: Test
|
||||||
|
|
||||||
on: push
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
run-tests:
|
run-tests:
|
||||||
runs-on: ubuntu-latest
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos, ubuntu, windows]
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,5 +1,5 @@
|
|||||||
node_modules
|
node_modules
|
||||||
/dist
|
dist/
|
||||||
.vs
|
.vs
|
||||||
.vscode
|
.vscode
|
||||||
#Electron-builder output
|
#Electron-builder output
|
||||||
|
|||||||
79
CHANGELOG.md
79
CHANGELOG.md
@@ -1,5 +1,84 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.8.2 (2020-12-26)
|
||||||
|
|
||||||
|
* replace ampersand in "Movies & TV app" with "and" to prevent batch file from misinterpreting it (#45) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/52d4313156d2dcbc508b7271e7d9dfd45723d7bc)
|
||||||
|
* update dependencies to latest #46 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d9e44e25744e5d0aa01b8fc0f0af74c48027aea3)
|
||||||
|
* fix type assignment error after typescript upgrade | [commit](https://github.com/undergroundwires/privacy.sexy/commit/55f936fee9f86757f63fa8952d89711feb247e5b)
|
||||||
|
* correct typos (#48) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a744415eb2ab65ee4f519f863fdd6a43953377bb)
|
||||||
|
* in ci/cd, do not run security checks if PRs do not change dependencies #48 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/54ba4dbb0bf8f08f9479f8facb2e12c786c1bc51)
|
||||||
|
* rename app launch tracking tweak to make it more clear #44 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b3117c27f283c2d5a25fd94021a9f628a272cda6)
|
||||||
|
* refactor capabilities to use a shared function #41 #47 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c4ec6a1445d2fd5eb923c97b54aee01e272e13a8)
|
||||||
|
* rename "disable" to "uninstall" for removing capabilities #47 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8cd3352017f9dc85f8efcd7b450d90f555d3e92e)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.1...0.8.2)
|
||||||
|
|
||||||
|
## 0.8.1 (2020-11-16)
|
||||||
|
|
||||||
|
* refactor removing bloatware to use functions #41 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ffa279f3dfe51db564f0a3859543eb212170e173)
|
||||||
|
* fix reinstalling store apps by searching appx for all users | [commit](https://github.com/undergroundwires/privacy.sexy/commit/2c5ab3ea7da159cfb9fbfbbb7cdd28afbee965ea)
|
||||||
|
* fix clearing jump lists causing os to break and user pin removal #37 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/92c3dd923257ac940eab6cbab858698ed55a09b7)
|
||||||
|
* fix reinstalling store apps by searching appx for all users | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4e7267337301fe4a0480ba0603218fca25c2d096)
|
||||||
|
* refactor unused imports | [commit](https://github.com/undergroundwires/privacy.sexy/commit/45b8dd972b1edf9e263858c23b27e7a1d2e07077)
|
||||||
|
* fix not being able to uninstall system apps | [commit](https://github.com/undergroundwires/privacy.sexy/commit/31e08d231d52e2a691400468b7c599c142a29448)
|
||||||
|
* fix wrong app names caused by wrong Microsoft docs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/e41e40c5bf01e2971d3054fcd3a48f8465a96622)
|
||||||
|
* unrecommend some system apps and document more | [commit](https://github.com/undergroundwires/privacy.sexy/commit/29c7704e0bd38f6e9923cde84accb569b02d2dd6)
|
||||||
|
* fix not being able to rename paths including brackets | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ad1872e7cd4ad7ef9facf33fadfa8c6a55065dd3)
|
||||||
|
* fix errors when file already exists | [commit](https://github.com/undergroundwires/privacy.sexy/commit/c26bc209eb167aa71cad10b7f3ea02d0dedd97b0)
|
||||||
|
* move Microsoft.Appconnector to right category | [commit](https://github.com/undergroundwires/privacy.sexy/commit/b247b12c3f009aab4350e33f4779fd193e570050)
|
||||||
|
* replace deprecated github ::set-env command | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ab7d617886a65fe4f3c2daa929168e5678ccae60)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.8.0...0.8.1)
|
||||||
|
|
||||||
|
## 0.8.0 (2020-11-01)
|
||||||
|
|
||||||
|
* add support for different recommendation levels: strict and standard | [commit](https://github.com/undergroundwires/privacy.sexy/commit/14be3017c55ed5e0d9bdecb63fcc4e1131e79ab0)
|
||||||
|
* Add GroupMe and Spotify removal option (#34) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3785c623f837b182d82fa383dfe7709722a67248)
|
||||||
|
* switch places of download and copy buttons | [commit](https://github.com/undergroundwires/privacy.sexy/commit/50fb29038ae19b17ec006093db02cf1e568d53c3)
|
||||||
|
* change "download" button to "save" on desktop | [commit](https://github.com/undergroundwires/privacy.sexy/commit/07fc555324d8bf4fa3594a9701daaa124a873153)
|
||||||
|
* show icons on cards during indeterminate and fully selected states | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1072505219edc47d82a91f148d1f310f32869fea)
|
||||||
|
* add scripts to increase cryptography, enable camera notifications and remove todo app (#36) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c68408f1ec339dc8d39c7ab044f825a7f7185cb)
|
||||||
|
* update recommendations to be safer and consistent | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d0019c2c9b1eea620e2e8e02b586903ce62b80e3)
|
||||||
|
* rework disabling metadata retrieval | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ac70b063b8a15bc528256185792939685be6b36f)
|
||||||
|
* add all dist folders in gitignore because of files auto-generated by vscode | [commit](https://github.com/undergroundwires/privacy.sexy/commit/1a9db31c7778c3269a71c0bd9665827efda70a02)
|
||||||
|
* add support for shared functions #41 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/8ce06facbd54184402a4b1af3c7303e64db85b8a)
|
||||||
|
* hide scrollbars on code area when not overflowing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/fd28eaad061c75ea1aa7e0f0d60ea37a7e52f8c4)
|
||||||
|
* update screenshot | [commit](https://github.com/undergroundwires/privacy.sexy/commit/cfedcd724cad7708b30c7390a7bca3b6313b6726)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.6...0.8.0)
|
||||||
|
|
||||||
|
## 0.7.6 (2020-10-18)
|
||||||
|
|
||||||
|
* add docs for default0 pointing to github discussion (#30) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a3fc3782efd346b4c99d2a0b40df2eb0229f5b36)
|
||||||
|
* add robots.txt to explicitly allow indexing | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4c2f74949b0758d33049bdfa4f0124a28958f8ea)
|
||||||
|
* add more reversibility | [commit](https://github.com/undergroundwires/privacy.sexy/commit/19a092dd31fb3588277f1ab3120b409d98506752)
|
||||||
|
* refactor to read more from package.json | [commit](https://github.com/undergroundwires/privacy.sexy/commit/784a67afff681bc19147d03c947de0e165d97e87)
|
||||||
|
* simplify "why" section | [commit](https://github.com/undergroundwires/privacy.sexy/commit/77c3d2bbb8d13db86bb82ed0b5cbeaacfdea3db9)
|
||||||
|
* update dependencies to latest | [commit](https://github.com/undergroundwires/privacy.sexy/commit/11e06131655398db08faeeacff62062e46e0dddd)
|
||||||
|
* run tests on all operating systems: macos, ubuntu, windows | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d9d7f62d81d4d8f95104d33211e82641884d711f)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.5...0.7.6)
|
||||||
|
|
||||||
|
## 0.7.5 (2020-09-14)
|
||||||
|
|
||||||
|
* fix reverting (reinstalling) capabilities not working | [commit](https://github.com/undergroundwires/privacy.sexy/commit/939d838e3535bb1c9b00c8ea9dacb735ae41d700)
|
||||||
|
* fix tests and checks are not running on PRs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/82d509129b4e4a5df4b84786a0d6842a7d26e888)
|
||||||
|
* fix the recycling bin option (#32) | [commit](https://github.com/undergroundwires/privacy.sexy/commit/15db3118012a172a2191a2afad57084a65b34642)
|
||||||
|
* fix rendering issue in older edge/IE | [commit](https://github.com/undergroundwires/privacy.sexy/commit/6efed72bf25c2ddf0901caab7f22966ca13cd47a)
|
||||||
|
* fix pasting in search bar after page load showing no results | [commit](https://github.com/undergroundwires/privacy.sexy/commit/d1694341578288eeaf8b80caf9296a38d76789f0)
|
||||||
|
* fix typo | [commit](https://github.com/undergroundwires/privacy.sexy/commit/7dd15ed06433e0e6583ab0fa46a683ce6554bbea)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.4...0.7.5)
|
||||||
|
|
||||||
|
## 0.7.4 (2020-09-12)
|
||||||
|
|
||||||
|
* fix checked checkbox has blue border | [commit](https://github.com/undergroundwires/privacy.sexy/commit/4ae385b7fcea9014a68442714b7d99e2ee7df7d0)
|
||||||
|
* fix spectre protection getting single lined #31 | [commit](https://github.com/undergroundwires/privacy.sexy/commit/22b23a9ece446c7f9abd4ede293051eb616ad50a)
|
||||||
|
* fix missing reg value in denying app access to account | [commit](https://github.com/undergroundwires/privacy.sexy/commit/3c13a9e837e06e097450b31d7eb0c0e6bf20cefb)
|
||||||
|
* fix wrong path in clear all firefox user profile settings | [commit](https://github.com/undergroundwires/privacy.sexy/commit/ee66196d9a60f27d17ae7f62d02b4f119a47e6e0)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.7.3...0.7.4)
|
||||||
|
|
||||||
## 0.7.3 (2020-09-12)
|
## 0.7.3 (2020-09-12)
|
||||||
|
|
||||||
* fix vscode settings file override and add more configs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00)
|
* fix vscode settings file override and add more configs | [commit](https://github.com/undergroundwires/privacy.sexy/commit/a0d61728ead04b4455437f85820121a848db9e00)
|
||||||
|
|||||||
@@ -25,19 +25,13 @@
|
|||||||
|
|
||||||
## Guidelines
|
## Guidelines
|
||||||
|
|
||||||
### Extend scripts
|
|
||||||
|
|
||||||
- Create a [pull request](#Pull-Request-Process) for [application.yaml](./src/application/application.yaml)
|
|
||||||
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
|
|
||||||
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.
|
|
||||||
|
|
||||||
### Handle the state in presentation layer
|
### Handle the state in presentation layer
|
||||||
|
|
||||||
- There are two types of components:
|
- There are two types of components:
|
||||||
- **Stateless**, extends `Vue`
|
- **Stateless**, extends `Vue`
|
||||||
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
|
- **Stateful**, extends [`StatefulVue`](./src/presentation/StatefulVue.ts)
|
||||||
- The source of truth for the state lies in application layer (`./src/application/`) and must be updated from the views if they're mutating the state
|
- The source of truth for the state lies in application layer ([`./src/application/`](src/application/)) and must be updated from the views if they're mutating the state
|
||||||
- They mutate or/and reacts to changes in [application state](src/application/State/ApplicationState.ts).
|
- They mutate or/and react to state changes in [ApplicationContext](src/application/Context/ApplicationContext.ts).
|
||||||
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
|
- You can react by getting the state and listening to it and update the view accordingly in [`mounted()`](https://vuejs.org/v2/api/#mounted) method.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
38
README.md
38
README.md
@@ -1,6 +1,6 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy
|
||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
[](./CONTRIBUTING.md)
|
[](./CONTRIBUTING.md)
|
||||||
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
[](https://lgtm.com/projects/g/undergroundwires/privacy.sexy/context:javascript)
|
||||||
@@ -15,26 +15,28 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
- Online version: [https://privacy.sexy](https://privacy.sexy)
|
||||||
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.3/privacy.sexy-Setup-0.7.3.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.3/privacy.sexy-0.7.3.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.7.3/privacy.sexy-0.7.3.dmg)
|
- or download latest desktop version for [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.2/privacy.sexy-Setup-0.8.2.exe), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.2/privacy.sexy-0.8.2.AppImage), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.8.2/privacy.sexy-0.8.2.dmg)
|
||||||
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
- 💡 Come back regularly to apply latest version for stronger privacy and security.
|
||||||
|
|
||||||
[](https://privacy.sexy)
|
[](https://privacy.sexy)
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
- You don't need to run any compiled software that has access to your system, just run the generated scripts.
|
- Rich tweak pool to harden security & privacy of the OS and other software on it
|
||||||
- Have full visibility into what the tweaks do as you enable them.
|
- Free (both free as in beer and free as in speech)
|
||||||
- Ability to revert applied scripts
|
- No need to run any compiled software that has access to your system, just run the generated scripts
|
||||||
|
- Have full visibility into what the tweaks do as you enable them
|
||||||
|
- Ability to revert (undo) applied scripts
|
||||||
|
- Everything is transparent: both application and its infrastructure are open-source and automated
|
||||||
- Easily extendable
|
- Easily extendable
|
||||||
- Everything is open-sourced including both application and infrastructure
|
|
||||||
- Fully automated CI/CD pipeline using GitHub actions
|
|
||||||
- to AWS for provisioning serverless infrastructure
|
|
||||||
- for building and sharing the desktop applications
|
|
||||||
|
|
||||||
## Extend scripts
|
## Extend scripts
|
||||||
|
|
||||||
- Fork it & add more scripts in [application.yaml](src/application/application.yaml) and send a pull request 👌
|
1. Fork the repository
|
||||||
- 📖 More: [extend scripts | CONTRIBUTING.md](./CONTRIBUTING.md#extend-scripts)
|
2. Add more scripts in respective script collection in [collections](src/application/collections/) folder.
|
||||||
|
- 📖 If you're unsure about the syntax you can refer to the [collection files | documentation](docs/collection-files.md).
|
||||||
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
3. Send a pull request 👌
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
@@ -49,8 +51,8 @@
|
|||||||
- Development: `npm run serve` to compile & hot-reload for development.
|
- Development: `npm run serve` to compile & hot-reload for development.
|
||||||
- Production: `npm run build` to prepare files for distribution.
|
- Production: `npm run build` to prepare files for distribution.
|
||||||
- Or run using Docker:
|
- Or run using Docker:
|
||||||
1. Build: `docker build -t undergroundwires/privacy.sexy:0.7.3 .`
|
1. Build: `docker build -t undergroundwires/privacy.sexy:0.8.2 .`
|
||||||
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.7.3 undergroundwires/privacy.sexy:0.7.3`
|
2. Run: `docker run -it -p 8080:80 --rm --name privacy.sexy-0.8.2 undergroundwires/privacy.sexy:0.8.2`
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -65,9 +67,13 @@
|
|||||||
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
- Desktop application is created using [Electron](https://www.electronjs.org/).
|
||||||
- Event driven as in components simply listens to events from the state and act accordingly.
|
- Event driven as in components simply listens to events from the state and act accordingly.
|
||||||
- **Application Layer**
|
- **Application Layer**
|
||||||
- Keeps the application state
|
- Keeps the application state using [state pattern](https://en.wikipedia.org/wiki/State_pattern)
|
||||||
- The [state](src/application/State/ApplicationState.ts) is a mutable singleton & event producer.
|
- [ApplicationContext](src/application/Context/ApplicationContext.ts)
|
||||||
- The application is defined & controlled in a [single YAML file](src/application/application.yaml) (see [Data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming))
|
- Holds the [CategoryCollectionState](src/application/Context/State/CategoryCollectionState.ts)] for each OS
|
||||||
|
- Same instance is shared throughout the application
|
||||||
|
- The scripts are defined and controlled in [yaml files](src/application/collections/) per OS
|
||||||
|
- Uses [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming)
|
||||||
|
- 📖 See [extend scripts](#extend-scripts) to read about how to extend them.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
161
docs/collection-files.md
Normal file
161
docs/collection-files.md
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# Collection files
|
||||||
|
|
||||||
|
- privacy.sexy is a data-driven application where it reads the necessary OS-specific logic from yaml files in [`application/collections`](./../src/application/collections/)
|
||||||
|
- 💡 Best practices
|
||||||
|
- If you repeat yourself, try to utilize [YAML-defined functions](#Function)
|
||||||
|
- Always try to add documentation and a way to revert a tweak in [scripts](#Script)
|
||||||
|
- 📖 Types in code: [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts)
|
||||||
|
|
||||||
|
## Objects
|
||||||
|
|
||||||
|
### `Collection`
|
||||||
|
|
||||||
|
- A collection simply defines:
|
||||||
|
- different categories and their scripts in a tree structure
|
||||||
|
- OS specific details
|
||||||
|
- Also allows defining common [function](#Function)s to be used throughout the collection if you'd like different scripts to share same code.
|
||||||
|
|
||||||
|
#### `Collection` syntax
|
||||||
|
|
||||||
|
- `os:` *`string`* (**required**)
|
||||||
|
- Operating system that the [Collection](#collection) is written for.
|
||||||
|
- 📖 See [OperatingSystem.ts](./../src/domain/OperatingSystem.ts) enumeration for allowed values.
|
||||||
|
- `actions: [` ***[`Category`](#Category)*** `, ... ]` **(required)**
|
||||||
|
- Each [category](#category) is rendered as different cards in card presentation.
|
||||||
|
- ❗ A [Collection](#collection) must consist of at least one category.
|
||||||
|
- `functions: [` ***[`Function`](#Function)*** `, ... ]`
|
||||||
|
- Functions are optionally defined to re-use the same code throughout different scripts.
|
||||||
|
- `scripting:` ***[`ScriptingDefinition`](#ScriptingDefinition)*** **(required)**
|
||||||
|
- Defines the scripting language that the code of other action uses.
|
||||||
|
|
||||||
|
### `Category`
|
||||||
|
|
||||||
|
- Category has a parent that has tree-like structure where it can have subcategories or subscripts.
|
||||||
|
- It's a logical grouping of different scripts and other categories.
|
||||||
|
|
||||||
|
#### `Category` syntax
|
||||||
|
|
||||||
|
- `category:` *`string`* (**required**)
|
||||||
|
- Name of the category
|
||||||
|
- ❗ Must be unique throughout the [Collection](#collection)
|
||||||
|
- `children: [` ***[`Category`](#Category)*** `|` [***`Script`***](#Script) `, ... ]` (**required**)
|
||||||
|
- ❗ Category must consist of at least one subcategory or script.
|
||||||
|
- Children can be combination of scripts and subcategories.
|
||||||
|
|
||||||
|
### `Script`
|
||||||
|
|
||||||
|
- Script represents a single tweak.
|
||||||
|
- A script must include either:
|
||||||
|
- A `code` and `revertCode`
|
||||||
|
- Or `call` to call YAML-defined functions
|
||||||
|
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
|
||||||
|
|
||||||
|
#### `Script` syntax
|
||||||
|
|
||||||
|
- `name`: *`string`* (**required**)
|
||||||
|
- Name of the script
|
||||||
|
- ❗ Must be unique throughout the [Collection](#collection)
|
||||||
|
- E.g. `Disable targeted ads`
|
||||||
|
- `code`: *`string`* (may be **required**)
|
||||||
|
- Batch file commands that will be executed
|
||||||
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
|
- ❗ If not defined `call` must be defined, do not define if `call` is defined.
|
||||||
|
- `revertCode`: `string`
|
||||||
|
- Code that'll undo the change done by `code` property.
|
||||||
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
|
- ❗ Do not define if `call` is defined.
|
||||||
|
- `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**)
|
||||||
|
- A shared function or sequence of functions to call (called in order)
|
||||||
|
- ❗ If not defined `code` must be defined
|
||||||
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
|
- Single documentation URL or list of URLs for those who wants to learn more about the script
|
||||||
|
- E.g. `https://docs.microsoft.com/en-us/windows-server/`
|
||||||
|
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
|
||||||
|
- If not defined then the script will not be recommended
|
||||||
|
- If defined it can be either
|
||||||
|
- `standard`: Only non-breaking scripts without limiting OS functionality
|
||||||
|
- `strict`: Scripts that can break certain functionality in favor of privacy and security
|
||||||
|
|
||||||
|
### `FunctionCall`
|
||||||
|
|
||||||
|
- Describes a single call to a function by optionally providing values to its parameters.
|
||||||
|
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
||||||
|
|
||||||
|
#### `FunctionCall` syntax
|
||||||
|
|
||||||
|
- `function`: *`string`* (**required**)
|
||||||
|
- Name of the function to call.
|
||||||
|
- ❗ Function with same name must defined in `functions` property of [Collection](#collection)
|
||||||
|
- `parameters`: `[ parameterName:` *`parameterValue`*`, ... ]`
|
||||||
|
- Defines key value dictionary for each parameter and its value
|
||||||
|
- E.g.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
parameters:
|
||||||
|
userDefinedParameterName: parameterValue
|
||||||
|
# ...
|
||||||
|
appName: Microsoft.WindowsFeedbackHub
|
||||||
|
```
|
||||||
|
|
||||||
|
### `Function`
|
||||||
|
|
||||||
|
- Functions allow re-usable code throughout the defined scripts.
|
||||||
|
- Functions are templates compiled by privacy.sexy and uses special expressions.
|
||||||
|
- Expressions are defined inside mustaches (double brackets, `{{` and `}}`)
|
||||||
|
- 👀 See [parameter substitution](#parameter-substitution) for an example usage
|
||||||
|
|
||||||
|
#### Parameter substitution
|
||||||
|
|
||||||
|
A simple function example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
function: EchoArgument
|
||||||
|
parameters: [ 'argument' ]
|
||||||
|
code: Hello {{ $argument }} !
|
||||||
|
```
|
||||||
|
|
||||||
|
It would print "Hello world" if it's called in a [script](#script) as following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
script: Echo script
|
||||||
|
call:
|
||||||
|
function: EchoArgument
|
||||||
|
parameters:
|
||||||
|
argument: World
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `Function` syntax
|
||||||
|
|
||||||
|
- `name`: *`string`* (**required**)
|
||||||
|
- Name of the function that scripts will use.
|
||||||
|
- Convention is to use camelCase, and be verbs.
|
||||||
|
- E.g. `uninstallStoreApp`
|
||||||
|
- ❗ Function names must be unique
|
||||||
|
- `parameters`: `[` *`string`* `, ... ]`
|
||||||
|
- Name of the parameters that the function has.
|
||||||
|
- Parameter values are provided by a [Script](#script) through a [FunctionCall](#FunctionCall)
|
||||||
|
- Parameter names must be defined to be used in expressions such as [parameter substitution](#parameter-substitution)
|
||||||
|
- ❗ Parameter names must be unique
|
||||||
|
`code`: *`string`* (**required**)
|
||||||
|
- Batch file commands that will be executed
|
||||||
|
- 💡 If defined, best practice to also define `revertCode`
|
||||||
|
- `revertCode`: *`string`*
|
||||||
|
- Code that'll undo the change done by `code` property.
|
||||||
|
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
|
||||||
|
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
|
||||||
|
|
||||||
|
### `ScriptingDefinition`
|
||||||
|
|
||||||
|
- Defines global properties for scripting that's used throughout its parent [Collection](#collection).
|
||||||
|
|
||||||
|
#### `ScriptingDefinition` syntax
|
||||||
|
|
||||||
|
- `language:` *`string`* (**required**)
|
||||||
|
- 📖 See [ScriptingLanguage.ts](./../src/domain/ScriptingLanguage.ts) enumeration for allowed values.
|
||||||
|
- `startCode:` *`string`* (**required**)
|
||||||
|
- Code that'll be inserted on top of user created script.
|
||||||
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
|
- `endCode:` *`string`* (**required**)
|
||||||
|
- Code that'll be inserted at the end of user created script.
|
||||||
|
- Global variables such as `$homepage`, `$version`, `$date` can be used using [parameter substitution](#parameter-substitution) code syntax such as `Welcome to {{ $homepage }}!`
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 53 KiB |
2939
package-lock.json
generated
2939
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
61
package.json
61
package.json
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.7.3",
|
"version": "0.8.2",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
||||||
|
"homepage": "https://privacy.sexy",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/undergroundwires/privacy.sexy.git"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
"build": "vue-cli-service build",
|
"build": "vue-cli-service build",
|
||||||
@@ -21,45 +26,45 @@
|
|||||||
},
|
},
|
||||||
"main": "background.js",
|
"main": "background.js",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fortawesome/fontawesome-svg-core": "^1.2.30",
|
"@fortawesome/fontawesome-svg-core": "^1.2.32",
|
||||||
"@fortawesome/free-brands-svg-icons": "^5.14.0",
|
"@fortawesome/free-brands-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/free-regular-svg-icons": "^5.14.0",
|
"@fortawesome/free-regular-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/free-solid-svg-icons": "^5.14.0",
|
"@fortawesome/free-solid-svg-icons": "^5.15.1",
|
||||||
"@fortawesome/vue-fontawesome": "^0.1.10",
|
"@fortawesome/vue-fontawesome": "^2.0.2",
|
||||||
"ace-builds": "^1.4.12",
|
"ace-builds": "^1.4.12",
|
||||||
"file-saver": "^2.0.2",
|
"file-saver": "^2.0.5",
|
||||||
"inversify": "^5.0.1",
|
"inversify": "^5.0.5",
|
||||||
"liquor-tree": "^0.2.70",
|
"liquor-tree": "^0.2.70",
|
||||||
"v-tooltip": "2.0.2",
|
"v-tooltip": "2.0.2",
|
||||||
"vue": "^2.6.12",
|
"vue": "^2.6.12",
|
||||||
"vue-class-component": "^7.2.5",
|
"vue-class-component": "^7.2.6",
|
||||||
"vue-js-modal": "^2.0.0-rc.6",
|
"vue-js-modal": "^2.0.0-rc.6",
|
||||||
"vue-property-decorator": "^9.0.0"
|
"vue-property-decorator": "^9.1.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/ace": "0.0.43",
|
"@types/ace": "0.0.44",
|
||||||
"@types/chai": "^4.2.12",
|
"@types/chai": "^4.2.14",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/mocha": "^8.0.3",
|
"@types/mocha": "^8.2.0",
|
||||||
"@vue/cli-plugin-typescript": "^4.5.4",
|
"@vue/cli-plugin-typescript": "^4.5.9",
|
||||||
"@vue/cli-plugin-unit-mocha": "^4.5.4",
|
"@vue/cli-plugin-unit-mocha": "^4.5.9",
|
||||||
"@vue/cli-service": "^4.5.4",
|
"@vue/cli-service": "^4.5.9",
|
||||||
"@vue/test-utils": "1.0.4",
|
"@vue/test-utils": "1.1.2",
|
||||||
"chai": "^4.2.0",
|
"chai": "^4.2.0",
|
||||||
"electron": "^10.1.0",
|
"electron": "^11.1.0",
|
||||||
"electron-devtools-installer": "^3.1.1",
|
"electron-devtools-installer": "^3.1.1",
|
||||||
"electron-log": "^4.2.4",
|
"electron-log": "^4.3.1",
|
||||||
"electron-updater": "^4.3.4",
|
"electron-updater": "^4.3.5",
|
||||||
"js-yaml-loader": "^1.2.2",
|
"js-yaml-loader": "^1.2.2",
|
||||||
"markdownlint-cli": "^0.23.2",
|
"markdownlint-cli": "^0.26.0",
|
||||||
"remark-cli": "^8.0.1",
|
"remark-cli": "^9.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^3.0.1",
|
"remark-preset-lint-consistent": "^4.0.0",
|
||||||
"remark-validate-links": "^10.0.2",
|
"remark-validate-links": "^10.0.2",
|
||||||
"sass": "^1.26.10",
|
"sass": "^1.30.0",
|
||||||
"sass-loader": "^10.0.1",
|
"sass-loader": "^10.1.0",
|
||||||
"typescript": "^4.0.2",
|
"typescript": "^4.1.3",
|
||||||
"vue-cli-plugin-electron-builder": "^2.0.0-rc.4",
|
"vue-cli-plugin-electron-builder": "^2.0.0-rc.5",
|
||||||
"vue-template-compiler": "^2.6.12",
|
"vue-template-compiler": "^2.6.12",
|
||||||
"yaml-lint": "^1.2.4"
|
"yaml-lint": "^1.2.4"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows</title>
|
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title>
|
||||||
<meta name="robots" content="index,follow" />
|
<meta name="robots" content="index,follow" />
|
||||||
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
|||||||
2
public/robots.txt
Normal file
2
public/robots.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
@@ -16,7 +16,7 @@ import { Component, Vue } from 'vue-property-decorator';
|
|||||||
import TheHeader from '@/presentation/TheHeader.vue';
|
import TheHeader from '@/presentation/TheHeader.vue';
|
||||||
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
import TheFooter from '@/presentation/TheFooter/TheFooter.vue';
|
||||||
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
import TheCodeArea from '@/presentation/TheCodeArea.vue';
|
||||||
import TheCodeButtons from '@/presentation/TheCodeButtons.vue';
|
import TheCodeButtons from '@/presentation/CodeButtons/TheCodeButtons.vue';
|
||||||
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
import TheSearchBar from '@/presentation/TheSearchBar.vue';
|
||||||
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
import TheScripts from '@/presentation/Scripts/TheScripts.vue';
|
||||||
|
|
||||||
|
|||||||
43
src/application/Common/Enum.ts
Normal file
43
src/application/Common/Enum.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||||
|
type EnumType = number | string;
|
||||||
|
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
||||||
|
|
||||||
|
export interface IEnumParser<TEnum> {
|
||||||
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
|
}
|
||||||
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
||||||
|
return {
|
||||||
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
value: string,
|
||||||
|
enumName: string,
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`undefined ${enumName}`);
|
||||||
|
}
|
||||||
|
if (typeof value !== 'string') {
|
||||||
|
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||||
|
}
|
||||||
|
const casedValue = getEnumNames(enumVariable)
|
||||||
|
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
||||||
|
if (!casedValue) {
|
||||||
|
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||||
|
}
|
||||||
|
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
||||||
|
return Object
|
||||||
|
.values(enumVariable)
|
||||||
|
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
|
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
||||||
|
return getEnumNames(enumVariable)
|
||||||
|
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||||
|
}
|
||||||
71
src/application/Context/ApplicationContext.ts
Normal file
71
src/application/Context/ApplicationContext.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
|
||||||
|
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
|
|
||||||
|
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
|
||||||
|
|
||||||
|
export class ApplicationContext implements IApplicationContext {
|
||||||
|
public readonly contextChanged = new Signal<IApplicationContextChangedEvent>();
|
||||||
|
public collection: ICategoryCollection;
|
||||||
|
public currentOs: OperatingSystem;
|
||||||
|
|
||||||
|
public get state(): ICategoryCollectionState {
|
||||||
|
return this.states[this.collection.os];
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly states: StateMachine;
|
||||||
|
public constructor(
|
||||||
|
public readonly app: IApplication,
|
||||||
|
initialContext: OperatingSystem) {
|
||||||
|
validateApp(app);
|
||||||
|
validateOs(initialContext);
|
||||||
|
this.states = initializeStates(app);
|
||||||
|
this.changeContext(initialContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
public changeContext(os: OperatingSystem): void {
|
||||||
|
if (this.currentOs === os) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.collection = this.app.getCollection(os);
|
||||||
|
if (!this.collection) {
|
||||||
|
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
||||||
|
}
|
||||||
|
const event: IApplicationContextChangedEvent = {
|
||||||
|
newState: this.states[os],
|
||||||
|
oldState: this.states[this.currentOs],
|
||||||
|
};
|
||||||
|
this.contextChanged.notify(event);
|
||||||
|
this.currentOs = os;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateApp(app: IApplication) {
|
||||||
|
if (!app) {
|
||||||
|
throw new Error('undefined app');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateOs(os: OperatingSystem) {
|
||||||
|
if (os === undefined) {
|
||||||
|
throw new Error('undefined os');
|
||||||
|
}
|
||||||
|
if (os === OperatingSystem.Unknown) {
|
||||||
|
throw new Error('unknown os');
|
||||||
|
}
|
||||||
|
if (!(os in OperatingSystem)) {
|
||||||
|
throw new Error(`os "${os}" is out of range`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
|
for (const collection of app.collections) {
|
||||||
|
machine[collection.os] = new CategoryCollectionState(collection);
|
||||||
|
}
|
||||||
|
return machine;
|
||||||
|
}
|
||||||
31
src/application/Context/ApplicationContextProvider.ts
Normal file
31
src/application/Context/ApplicationContextProvider.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { ApplicationContext } from './ApplicationContext';
|
||||||
|
import { IApplicationContext } from '@/application/Context/IApplicationContext';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { Environment } from '../Environment/Environment';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { IEnvironment } from '../Environment/IEnvironment';
|
||||||
|
import { parseApplication } from '../Parser/ApplicationParser';
|
||||||
|
|
||||||
|
export type ApplicationParserType = () => IApplication;
|
||||||
|
const ApplicationParser: ApplicationParserType = parseApplication;
|
||||||
|
|
||||||
|
export function buildContext(
|
||||||
|
parser = ApplicationParser,
|
||||||
|
environment = Environment.CurrentEnvironment): IApplicationContext {
|
||||||
|
const app = parser();
|
||||||
|
const os = getInitialOs(app, environment);
|
||||||
|
return new ApplicationContext(app, os);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
|
||||||
|
const currentOs = environment.os;
|
||||||
|
const supportedOsList = app.getSupportedOsList();
|
||||||
|
if (supportedOsList.includes(currentOs)) {
|
||||||
|
return currentOs;
|
||||||
|
}
|
||||||
|
supportedOsList.sort((os1, os2) => {
|
||||||
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
|
return getPriority(os2) - getPriority(os1);
|
||||||
|
});
|
||||||
|
return supportedOsList[0];
|
||||||
|
}
|
||||||
16
src/application/Context/IApplicationContext.ts
Normal file
16
src/application/Context/IApplicationContext.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
|
||||||
|
export interface IApplicationContext {
|
||||||
|
readonly app: IApplication;
|
||||||
|
readonly state: ICategoryCollectionState;
|
||||||
|
readonly contextChanged: ISignal<IApplicationContextChangedEvent>;
|
||||||
|
changeContext(os: OperatingSystem): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IApplicationContextChangedEvent {
|
||||||
|
readonly newState: ICategoryCollectionState;
|
||||||
|
readonly oldState: ICategoryCollectionState;
|
||||||
|
}
|
||||||
23
src/application/Context/State/CategoryCollectionState.ts
Normal file
23
src/application/Context/State/CategoryCollectionState.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { UserFilter } from './Filter/UserFilter';
|
||||||
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
|
import { UserSelection } from './Selection/UserSelection';
|
||||||
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
import { ICategoryCollection } from '../../../domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
|
public readonly os: OperatingSystem;
|
||||||
|
public readonly code: IApplicationCode;
|
||||||
|
public readonly selection: IUserSelection;
|
||||||
|
public readonly filter: IUserFilter;
|
||||||
|
|
||||||
|
public constructor(readonly collection: ICategoryCollection) {
|
||||||
|
this.selection = new UserSelection(collection, []);
|
||||||
|
this.code = new ApplicationCode(this.selection, collection.scripting);
|
||||||
|
this.filter = new UserFilter(collection);
|
||||||
|
this.os = collection.os;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
import { IUserSelection } from '@/application/State/Selection/IUserSelection';
|
import { IUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
||||||
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { IApplicationCode } from './IApplicationCode';
|
import { IApplicationCode } from './IApplicationCode';
|
||||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
|
||||||
export class ApplicationCode implements IApplicationCode {
|
export class ApplicationCode implements IApplicationCode {
|
||||||
public readonly changed = new Signal<ICodeChangedEvent>();
|
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||||
@@ -16,10 +17,10 @@ export class ApplicationCode implements IApplicationCode {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
userSelection: IUserSelection,
|
userSelection: IUserSelection,
|
||||||
private readonly version: string,
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||||
if (!version) { throw new Error('version is null or undefined'); }
|
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||||
this.setCode(userSelection.selectedScripts);
|
this.setCode(userSelection.selectedScripts);
|
||||||
userSelection.changed.on((scripts) => {
|
userSelection.changed.on((scripts) => {
|
||||||
@@ -29,7 +30,7 @@ export class ApplicationCode implements IApplicationCode {
|
|||||||
|
|
||||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||||
const code = this.generator.buildCode(scripts, this.version);
|
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
||||||
this.current = code.code;
|
this.current = code.code;
|
||||||
this.scriptPositions = code.scriptPositions;
|
this.scriptPositions = code.scriptPositions;
|
||||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
import { SelectedScript } from '../../Selection/SelectedScript';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
const NewLine = '\n';
|
const NewLine = '\n';
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
|
|
||||||
export class CodeBuilder {
|
export abstract class CodeBuilder implements ICodeBuilder {
|
||||||
private readonly lines = new Array<string>();
|
private readonly lines = new Array<string>();
|
||||||
|
|
||||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||||
@@ -27,7 +29,7 @@ export class CodeBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public appendCommentLine(commentLine?: string): CodeBuilder {
|
public appendCommentLine(commentLine?: string): CodeBuilder {
|
||||||
this.lines.push(`:: ${commentLine}`);
|
this.lines.push(`${this.getCommentDelimiter()} ${commentLine}`);
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +37,8 @@ export class CodeBuilder {
|
|||||||
if (!name) { throw new Error('name cannot be empty or null'); }
|
if (!name) { throw new Error('name cannot be empty or null'); }
|
||||||
if (!code) { throw new Error('code cannot be empty or null'); }
|
if (!code) { throw new Error('code cannot be empty or null'); }
|
||||||
return this
|
return this
|
||||||
.appendLine()
|
|
||||||
.appendCommentLineWithHyphensAround(name)
|
.appendCommentLineWithHyphensAround(name)
|
||||||
.appendLine(`echo --- ${name}`)
|
.appendLine(this.writeStandardOut(`--- ${name}`))
|
||||||
.appendLine(code)
|
.appendLine(code)
|
||||||
.appendTrailingHyphensCommentLine();
|
.appendTrailingHyphensCommentLine();
|
||||||
}
|
}
|
||||||
@@ -54,10 +55,13 @@ export class CodeBuilder {
|
|||||||
return this
|
return this
|
||||||
.appendTrailingHyphensCommentLine()
|
.appendTrailingHyphensCommentLine()
|
||||||
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
||||||
.appendTrailingHyphensCommentLine();
|
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
|
||||||
}
|
}
|
||||||
|
|
||||||
public toString(): string {
|
public toString(): string {
|
||||||
return this.lines.join(NewLine);
|
return this.lines.join(NewLine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected abstract getCommentDelimiter(): string;
|
||||||
|
protected abstract writeStandardOut(text: string): string;
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
import { BatchBuilder } from './Languages/BatchBuilder';
|
||||||
|
import { ShellBuilder } from './Languages/ShellBuilder';
|
||||||
|
|
||||||
|
export class CodeBuilderFactory implements ICodeBuilderFactory {
|
||||||
|
public create(language: ScriptingLanguage): ICodeBuilder {
|
||||||
|
switch (language) {
|
||||||
|
case ScriptingLanguage.shellscript: return new ShellBuilder();
|
||||||
|
case ScriptingLanguage.batchfile: return new BatchBuilder();
|
||||||
|
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
export interface ICodeBuilder {
|
||||||
|
currentLine: number;
|
||||||
|
appendLine(code?: string): ICodeBuilder;
|
||||||
|
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
||||||
|
appendCommentLine(commentLine?: string): ICodeBuilder;
|
||||||
|
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
||||||
|
appendFunction(name: string, code: string): ICodeBuilder;
|
||||||
|
toString(): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
|
export interface ICodeBuilderFactory {
|
||||||
|
create(language: ScriptingLanguage): ICodeBuilder;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
|
export interface IUserScript {
|
||||||
|
code: string;
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
|
||||||
|
export interface IUserScriptGenerator {
|
||||||
|
buildCode(
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
scriptingDefinition: IScriptingDefinition): IUserScript;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
|
export class BatchBuilder extends CodeBuilder {
|
||||||
|
protected getCommentDelimiter(): string {
|
||||||
|
return '::';
|
||||||
|
}
|
||||||
|
protected writeStandardOut(text: string): string {
|
||||||
|
return `echo ${text}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { CodeBuilder } from '@/application/Context/State/Code/Generation/CodeBuilder';
|
||||||
|
|
||||||
|
export class ShellBuilder extends CodeBuilder {
|
||||||
|
protected getCommentDelimiter(): string {
|
||||||
|
return '#';
|
||||||
|
}
|
||||||
|
protected writeStandardOut(text: string): string {
|
||||||
|
return `echo '${text}'`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
||||||
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
|
import { IUserScript } from './IUserScript';
|
||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
|
||||||
|
import { CodeBuilderFactory } from './CodeBuilderFactory';
|
||||||
|
|
||||||
|
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||||
|
constructor(private readonly codeBuilderFactory: ICodeBuilderFactory = new CodeBuilderFactory()) {
|
||||||
|
|
||||||
|
}
|
||||||
|
public buildCode(
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
scriptingDefinition: IScriptingDefinition): IUserScript {
|
||||||
|
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||||
|
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||||
|
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||||
|
if (!selectedScripts.length) {
|
||||||
|
return { code: '', scriptPositions };
|
||||||
|
}
|
||||||
|
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||||
|
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||||
|
for (const selection of selectedScripts) {
|
||||||
|
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||||
|
}
|
||||||
|
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||||
|
return { code, scriptPositions };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
|
||||||
|
if (!startCode) {
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
return builder
|
||||||
|
.appendLine(startCode)
|
||||||
|
.appendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
||||||
|
if (!endCode) {
|
||||||
|
return builder.toString();
|
||||||
|
}
|
||||||
|
return builder.appendLine()
|
||||||
|
.appendLine(endCode)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendSelection(
|
||||||
|
selection: SelectedScript,
|
||||||
|
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||||
|
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||||
|
const startPosition = builder.currentLine + 1; // Because first line will be empty to separate scripts
|
||||||
|
builder = appendCode(selection, builder);
|
||||||
|
const endPosition = builder.currentLine - 1;
|
||||||
|
builder.appendLine();
|
||||||
|
const position = new CodePosition(startPosition, endPosition);
|
||||||
|
scriptPositions.set(selection, position);
|
||||||
|
return scriptPositions;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
|
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||||
|
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||||
|
return builder
|
||||||
|
.appendLine()
|
||||||
|
.appendFunction(name, scriptCode);
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ICodePosition } from './ICodePosition';
|
import { ICodePosition } from './ICodePosition';
|
||||||
export class CodePosition implements ICodePosition {
|
|
||||||
|
|
||||||
|
export class CodePosition implements ICodePosition {
|
||||||
public get totalLines(): number {
|
public get totalLines(): number {
|
||||||
return this.endLine - this.startLine;
|
return this.endLine - this.startLine;
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
|
||||||
|
|
||||||
export interface IUserFilter {
|
export interface IUserFilter {
|
||||||
|
readonly currentFilter: IFilterResult | undefined;
|
||||||
readonly filtered: ISignal<IFilterResult>;
|
readonly filtered: ISignal<IFilterResult>;
|
||||||
readonly filterRemoved: ISignal<void>;
|
readonly filterRemoved: ISignal<void>;
|
||||||
setFilter(filter: string): void;
|
setFilter(filter: string): void;
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { FilterResult } from './FilterResult';
|
import { FilterResult } from './FilterResult';
|
||||||
import { IFilterResult } from './IFilterResult';
|
import { IFilterResult } from './IFilterResult';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
|
||||||
import { IUserFilter } from './IUserFilter';
|
import { IUserFilter } from './IUserFilter';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
|
||||||
export class UserFilter implements IUserFilter {
|
export class UserFilter implements IUserFilter {
|
||||||
public readonly filtered = new Signal<IFilterResult>();
|
public readonly filtered = new Signal<IFilterResult>();
|
||||||
public readonly filterRemoved = new Signal<void>();
|
public readonly filterRemoved = new Signal<void>();
|
||||||
|
public currentFilter: IFilterResult | undefined;
|
||||||
|
|
||||||
constructor(private application: IApplication) {
|
constructor(private collection: ICategoryCollection) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,21 +19,21 @@ export class UserFilter implements IUserFilter {
|
|||||||
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
throw new Error('Filter must be defined and not empty. Use removeFilter() to remove the filter');
|
||||||
}
|
}
|
||||||
const filterLowercase = filter.toLocaleLowerCase();
|
const filterLowercase = filter.toLocaleLowerCase();
|
||||||
const filteredScripts = this.application.getAllScripts().filter(
|
const filteredScripts = this.collection.getAllScripts().filter(
|
||||||
(script) => isScriptAMatch(script, filterLowercase));
|
(script) => isScriptAMatch(script, filterLowercase));
|
||||||
const filteredCategories = this.application.getAllCategories().filter(
|
const filteredCategories = this.collection.getAllCategories().filter(
|
||||||
(category) => category.name.toLowerCase().includes(filterLowercase));
|
(category) => category.name.toLowerCase().includes(filterLowercase));
|
||||||
|
|
||||||
const matches = new FilterResult(
|
const matches = new FilterResult(
|
||||||
filteredScripts,
|
filteredScripts,
|
||||||
filteredCategories,
|
filteredCategories,
|
||||||
filter,
|
filter,
|
||||||
);
|
);
|
||||||
|
this.currentFilter = matches;
|
||||||
this.filtered.notify(matches);
|
this.filtered.notify(matches);
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFilter(): void {
|
public removeFilter(): void {
|
||||||
|
this.currentFilter = undefined;
|
||||||
this.filterRemoved.notify();
|
this.filterRemoved.notify();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -41,11 +42,11 @@ function isScriptAMatch(script: IScript, filterLowercase: string) {
|
|||||||
if (script.name.toLowerCase().includes(filterLowercase)) {
|
if (script.name.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.code.toLowerCase().includes(filterLowercase)) {
|
if (script.code.execute.toLowerCase().includes(filterLowercase)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (script.revertCode) {
|
if (script.code.revert) {
|
||||||
return script.revertCode.toLowerCase().includes(filterLowercase);
|
return script.code.revert.toLowerCase().includes(filterLowercase);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
13
src/application/Context/State/ICategoryCollectionState.ts
Normal file
13
src/application/Context/State/ICategoryCollectionState.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
|
import { IUserSelection } from './Selection/IUserSelection';
|
||||||
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
export interface ICategoryCollectionState {
|
||||||
|
readonly code: IApplicationCode;
|
||||||
|
readonly filter: IUserFilter;
|
||||||
|
readonly selection: IUserSelection;
|
||||||
|
readonly collection: ICategoryCollection;
|
||||||
|
readonly os: OperatingSystem;
|
||||||
|
}
|
||||||
@@ -1,11 +1,14 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { ISignal } from '@/infrastructure/Events/Signal';
|
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||||
|
|
||||||
export interface IUserSelection {
|
export interface IUserSelection {
|
||||||
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
readonly changed: ISignal<ReadonlyArray<SelectedScript>>;
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
||||||
readonly totalSelected: number;
|
readonly totalSelected: number;
|
||||||
|
areAllSelected(category: ICategory): boolean;
|
||||||
|
isAnySelected(category: ICategory): boolean;
|
||||||
removeAllInCategory(categoryId: number): void;
|
removeAllInCategory(categoryId: number): void;
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
addSelectedScript(scriptId: string, revert: boolean): void;
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
import { SelectedScript } from './SelectedScript';
|
import { SelectedScript } from './SelectedScript';
|
||||||
import { IApplication } from '@/domain/IApplication';
|
|
||||||
import { IUserSelection } from './IUserSelection';
|
import { IUserSelection } from './IUserSelection';
|
||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
import { Signal } from '@/infrastructure/Events/Signal';
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export class UserSelection implements IUserSelection {
|
||||||
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
public readonly changed = new Signal<ReadonlyArray<SelectedScript>>();
|
||||||
private readonly scripts: IRepository<string, SelectedScript>;
|
private readonly scripts: IRepository<string, SelectedScript>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly app: IApplication,
|
private readonly collection: ICategoryCollection,
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>) {
|
selectedScripts: ReadonlyArray<SelectedScript>) {
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
if (selectedScripts && selectedScripts.length > 0) {
|
||||||
@@ -21,8 +22,26 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public areAllSelected(category: ICategory): boolean {
|
||||||
|
if (this.selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
if (this.selectedScripts.length < scripts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return scripts.every((script) => this.selectedScripts.some((selected) => selected.id === script.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAnySelected(category: ICategory): boolean {
|
||||||
|
if (this.selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return this.selectedScripts.some((s) => category.includes(s.script));
|
||||||
|
}
|
||||||
|
|
||||||
public removeAllInCategory(categoryId: number): void {
|
public removeAllInCategory(categoryId: number): void {
|
||||||
const category = this.app.findCategory(categoryId);
|
const category = this.collection.findCategory(categoryId);
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
const scriptsToRemove = category.getAllScriptsRecursively()
|
||||||
.filter((script) => this.scripts.exists(script.id));
|
.filter((script) => this.scripts.exists(script.id));
|
||||||
if (!scriptsToRemove.length) {
|
if (!scriptsToRemove.length) {
|
||||||
@@ -35,7 +54,7 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
public addOrUpdateAllInCategory(categoryId: number, revert: boolean = false): void {
|
||||||
const category = this.app.findCategory(categoryId);
|
const category = this.collection.findCategory(categoryId);
|
||||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
||||||
.filter((script) =>
|
.filter((script) =>
|
||||||
!this.scripts.exists(script.id)
|
!this.scripts.exists(script.id)
|
||||||
@@ -52,7 +71,7 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
public addSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.app.findScript(scriptId);
|
const script = this.collection.findScript(scriptId);
|
||||||
if (!script) {
|
if (!script) {
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
||||||
}
|
}
|
||||||
@@ -62,7 +81,7 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
||||||
const script = this.app.findScript(scriptId);
|
const script = this.collection.findScript(scriptId);
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
const selectedScript = new SelectedScript(script, revert);
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
@@ -87,7 +106,7 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
for (const script of this.app.getAllScripts()) {
|
for (const script of this.collection.getAllScripts()) {
|
||||||
if (!this.scripts.exists(script.id)) {
|
if (!this.scripts.exists(script.id)) {
|
||||||
const selection = new SelectedScript(script, false);
|
const selection = new SelectedScript(script, false);
|
||||||
this.scripts.addItem(selection);
|
this.scripts.addItem(selection);
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OperatingSystem } from '../OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { DetectorBuilder } from './DetectorBuilder';
|
import { DetectorBuilder } from './DetectorBuilder';
|
||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
import { IBrowserOsDetector } from './IBrowserOsDetector';
|
||||||
import { OperatingSystem } from '../OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export class DetectorBuilder {
|
export class DetectorBuilder {
|
||||||
private readonly existingPartsInUserAgent = new Array<string>();
|
private readonly existingPartsInUserAgent = new Array<string>();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { OperatingSystem } from '../OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IBrowserOsDetector {
|
export interface IBrowserOsDetector {
|
||||||
detect(userAgent: string): OperatingSystem;
|
detect(userAgent: string): OperatingSystem;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
import { BrowserOsDetector } from './BrowserOs/BrowserOsDetector';
|
||||||
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
import { IBrowserOsDetector } from './BrowserOs/IBrowserOsDetector';
|
||||||
import { IEnvironment } from './IEnvironment';
|
import { IEnvironment } from './IEnvironment';
|
||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
interface IEnvironmentVariables {
|
interface IEnvironmentVariables {
|
||||||
readonly window: Window & typeof globalThis;
|
readonly window: Window & typeof globalThis;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { OperatingSystem } from './OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
export interface IEnvironment {
|
export interface IEnvironment {
|
||||||
isDesktop: boolean;
|
readonly isDesktop: boolean;
|
||||||
os: OperatingSystem;
|
readonly os: OperatingSystem;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,38 @@
|
|||||||
import { Category } from '@/domain/Category';
|
|
||||||
import { Application } from '@/domain/Application';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
import { IApplication } from '@/domain/IApplication';
|
||||||
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { parseCategory } from './CategoryParser';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
import WindowsData from 'js-yaml-loader!@/application/collections/windows.yaml';
|
||||||
|
import MacOsData from 'js-yaml-loader!@/application/collections/macos.yaml';
|
||||||
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
|
import { Application } from '@/domain/Application';
|
||||||
|
|
||||||
export function parseApplication(content: ApplicationYaml): IApplication {
|
export function parseApplication(
|
||||||
validate(content);
|
parser = CategoryCollectionParser,
|
||||||
const categories = new Array<Category>();
|
processEnv: NodeJS.ProcessEnv = process.env,
|
||||||
for (const action of content.actions) {
|
collectionsData = PreParsedCollections): IApplication {
|
||||||
const category = parseCategory(action);
|
validateCollectionsData(collectionsData);
|
||||||
categories.push(category);
|
const information = parseProjectInformation(processEnv);
|
||||||
}
|
const collections = collectionsData.map((collection) => parser(collection, information));
|
||||||
const app = new Application(
|
const app = new Application(information, collections);
|
||||||
content.name,
|
|
||||||
content.repositoryUrl,
|
|
||||||
process.env.VUE_APP_VERSION,
|
|
||||||
categories);
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: ApplicationYaml): void {
|
export type CategoryCollectionParserType
|
||||||
if (!content) {
|
= (file: CollectionData, info: IProjectInformation) => ICategoryCollection;
|
||||||
throw new Error('application is null or undefined');
|
|
||||||
|
const CategoryCollectionParser: CategoryCollectionParserType
|
||||||
|
= (file, info) => parseCategoryCollection(file, info);
|
||||||
|
|
||||||
|
const PreParsedCollections: readonly CollectionData []
|
||||||
|
= [ WindowsData, MacOsData ];
|
||||||
|
|
||||||
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
|
if (!collections.length) {
|
||||||
|
throw new Error('no collection provided');
|
||||||
}
|
}
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
if (collections.some((collection) => !collection)) {
|
||||||
throw new Error('application does not define any action');
|
throw new Error('undefined collection provided');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
39
src/application/Parser/CategoryCollectionParser.ts
Normal file
39
src/application/Parser/CategoryCollectionParser.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { Category } from '@/domain/Category';
|
||||||
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
|
import { parseCategory } from './CategoryParser';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
||||||
|
import { createEnumParser } from '../Common/Enum';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
|
|
||||||
|
export function parseCategoryCollection(
|
||||||
|
content: CollectionData,
|
||||||
|
info: IProjectInformation,
|
||||||
|
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
|
||||||
|
validate(content);
|
||||||
|
const scripting = parseScriptingDefinition(content.scripting, info);
|
||||||
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
|
const categories = new Array<Category>();
|
||||||
|
for (const action of content.actions) {
|
||||||
|
const category = parseCategory(action, context);
|
||||||
|
categories.push(category);
|
||||||
|
}
|
||||||
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
|
const collection = new CategoryCollection(
|
||||||
|
os,
|
||||||
|
categories,
|
||||||
|
scripting);
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validate(content: CollectionData): void {
|
||||||
|
if (!content) {
|
||||||
|
throw new Error('content is null or undefined');
|
||||||
|
}
|
||||||
|
if (!content.actions || content.actions.length <= 0) {
|
||||||
|
throw new Error('content does not define any action');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import { YamlCategory, YamlScript } from 'js-yaml-loader!./application.yaml';
|
import { CategoryData, ScriptData, CategoryOrScriptData } from 'js-yaml-loader!@/*';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
import { parseDocUrls } from './DocumentationParser';
|
||||||
import { parseScript } from './ScriptParser';
|
import { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
|
import { parseScript } from './Script/ScriptParser';
|
||||||
|
|
||||||
let categoryIdCounter: number = 0;
|
let categoryIdCounter: number = 0;
|
||||||
|
|
||||||
@@ -11,14 +12,15 @@ interface ICategoryChildren {
|
|||||||
subScripts: Script[];
|
subScripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseCategory(category: YamlCategory): Category {
|
export function parseCategory(category: CategoryData, context: ICategoryCollectionParseContext): Category {
|
||||||
|
if (!context) { throw new Error('undefined context'); }
|
||||||
ensureValid(category);
|
ensureValid(category);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
subScripts: new Array<Script>(),
|
subScripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const categoryOrScript of category.children) {
|
for (const data of category.children) {
|
||||||
parseCategoryChild(categoryOrScript, children, category);
|
parseCategoryChild(data, children, category, context);
|
||||||
}
|
}
|
||||||
return new Category(
|
return new Category(
|
||||||
/*id*/ categoryIdCounter++,
|
/*id*/ categoryIdCounter++,
|
||||||
@@ -29,12 +31,12 @@ export function parseCategory(category: YamlCategory): Category {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValid(category: YamlCategory) {
|
function ensureValid(category: CategoryData) {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
throw Error('category is null or undefined');
|
throw Error('category is null or undefined');
|
||||||
}
|
}
|
||||||
if (!category.children || category.children.length === 0) {
|
if (!category.children || category.children.length === 0) {
|
||||||
throw Error('category has no children');
|
throw Error(`category has no children: "${category.category}"`);
|
||||||
}
|
}
|
||||||
if (!category.category || category.category.length === 0) {
|
if (!category.category || category.category.length === 0) {
|
||||||
throw Error('category has no name');
|
throw Error('category has no name');
|
||||||
@@ -42,24 +44,28 @@ function ensureValid(category: YamlCategory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
categoryOrScript: any, children: ICategoryChildren, parent: YamlCategory) {
|
data: CategoryOrScriptData,
|
||||||
if (isCategory(categoryOrScript)) {
|
children: ICategoryChildren,
|
||||||
const subCategory = parseCategory(categoryOrScript as YamlCategory);
|
parent: CategoryData,
|
||||||
|
context: ICategoryCollectionParseContext) {
|
||||||
|
if (isCategory(data)) {
|
||||||
|
const subCategory = parseCategory(data as CategoryData, context);
|
||||||
children.subCategories.push(subCategory);
|
children.subCategories.push(subCategory);
|
||||||
} else if (isScript(categoryOrScript)) {
|
} else if (isScript(data)) {
|
||||||
const yamlScript = categoryOrScript as YamlScript;
|
const scriptData = data as ScriptData;
|
||||||
const script = parseScript(yamlScript);
|
const script = parseScript(scriptData, context);
|
||||||
children.subScripts.push(script);
|
children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Child element is neither a category or a script.
|
throw new Error(`Child element is neither a category or a script.
|
||||||
Parent: ${parent.category}, element: ${JSON.stringify(categoryOrScript)}`);
|
Parent: ${parent.category}, element: ${JSON.stringify(data)}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isScript(categoryOrScript: any): boolean {
|
function isScript(data: any): boolean {
|
||||||
return categoryOrScript.code && categoryOrScript.code.length > 0;
|
return (data.code && data.code.length > 0)
|
||||||
|
|| data.call;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(categoryOrScript: any): boolean {
|
function isCategory(data: any): boolean {
|
||||||
return categoryOrScript.category && categoryOrScript.category.length > 0;
|
return data.category && data.category.length > 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { YamlDocumentable, DocumentationUrls } from 'js-yaml-loader!./application.yaml';
|
import { DocumentableData, DocumentationUrlsData } from 'js-yaml-loader!@/*';
|
||||||
|
|
||||||
export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<string> {
|
export function parseDocUrls(documentable: DocumentableData): ReadonlyArray<string> {
|
||||||
if (!documentable) {
|
if (!documentable) {
|
||||||
throw new Error('documentable is null or undefined');
|
throw new Error('documentable is null or undefined');
|
||||||
}
|
}
|
||||||
@@ -13,7 +13,7 @@ export function parseDocUrls(documentable: YamlDocumentable): ReadonlyArray<stri
|
|||||||
return result.getAll();
|
return result.getAll();
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(docs: DocumentationUrls, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
function addDocs(docs: DocumentationUrlsData, urls: DocumentationUrlContainer): DocumentationUrlContainer {
|
||||||
if (docs instanceof Array) {
|
if (docs instanceof Array) {
|
||||||
urls.addUrls(docs);
|
urls.addUrls(docs);
|
||||||
} else if (typeof docs === 'string') {
|
} else if (typeof docs === 'string') {
|
||||||
@@ -32,7 +32,7 @@ class DocumentationUrlContainer {
|
|||||||
this.urls.push(url);
|
this.urls.push(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
public addUrls(urls: any[]) {
|
public addUrls(urls: readonly any[]) {
|
||||||
for (const url of urls) {
|
for (const url of urls) {
|
||||||
if (typeof url !== 'string') {
|
if (typeof url !== 'string') {
|
||||||
throw new Error('Docs field (documentation url) must be an array of strings');
|
throw new Error('Docs field (documentation url) must be an array of strings');
|
||||||
|
|||||||
12
src/application/Parser/ProjectInformationParser.ts
Normal file
12
src/application/Parser/ProjectInformationParser.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
|
|
||||||
|
export function parseProjectInformation(
|
||||||
|
environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||||
|
return new ProjectInformation(
|
||||||
|
environment.VUE_APP_NAME,
|
||||||
|
environment.VUE_APP_VERSION,
|
||||||
|
environment.VUE_APP_REPOSITORY_URL,
|
||||||
|
environment.VUE_APP_HOMEPAGE_URL,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { FunctionData } from 'js-yaml-loader!*';
|
||||||
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
import { SyntaxFactory } from './Syntax/SyntaxFactory';
|
||||||
|
import { ISyntaxFactory } from './Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
|
public readonly compiler: IScriptCompiler;
|
||||||
|
public readonly syntax: ILanguageSyntax;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
|
scripting: IScriptingDefinition,
|
||||||
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory()) {
|
||||||
|
if (!scripting) { throw new Error('undefined scripting'); }
|
||||||
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
|
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
||||||
|
}
|
||||||
|
}
|
||||||
73
src/application/Parser/Script/Compiler/ILCode.ts
Normal file
73
src/application/Parser/Script/Compiler/ILCode.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
export interface IILCode {
|
||||||
|
compile(): string;
|
||||||
|
getUniqueParameterNames(): string[];
|
||||||
|
substituteParameter(parameterName: string, parameterValue: string): IILCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateIlCode(rawText: string): IILCode {
|
||||||
|
const ilCode = generateIl(rawText);
|
||||||
|
return new ILCode(ilCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
class ILCode implements IILCode {
|
||||||
|
private readonly ilCode: string;
|
||||||
|
|
||||||
|
constructor(ilCode: string) {
|
||||||
|
this.ilCode = ilCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public substituteParameter(parameterName: string, parameterValue: string): IILCode {
|
||||||
|
const newCode = substituteParameter(this.ilCode, parameterName, parameterValue);
|
||||||
|
return new ILCode(newCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUniqueParameterNames(): string[] {
|
||||||
|
return getUniqueParameterNames(this.ilCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public compile(): string {
|
||||||
|
ensureNoExpressionLeft(this.ilCode);
|
||||||
|
return this.ilCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
||||||
|
function generateIl(rawText: string): string {
|
||||||
|
return rawText.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
||||||
|
return `\{\{exp|${match.trim()}\}\}`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// finds all "{{exp|..}} left"
|
||||||
|
function ensureNoExpressionLeft(ilCode: string) {
|
||||||
|
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
||||||
|
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
||||||
|
const uniqueExpressions = getDistinctValues(allMatches);
|
||||||
|
if (uniqueExpressions.length > 0) {
|
||||||
|
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses all distinct usages of {{exp|$parameterName}}
|
||||||
|
function getUniqueParameterNames(ilCode: string) {
|
||||||
|
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
||||||
|
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
||||||
|
const uniqueParameterNames = getDistinctValues(allParameters);
|
||||||
|
return uniqueParameterNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
// substitutes {{exp|$parameterName}} to value of the parameter
|
||||||
|
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
||||||
|
const pattern = `{{exp|$${parameterName}}}`;
|
||||||
|
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDistinctValues(values: readonly string[]): string[] {
|
||||||
|
return values.filter((value, index, self) => {
|
||||||
|
return self.indexOf(value) === index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('","')}"`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||||
|
|
||||||
|
export interface IScriptCompiler {
|
||||||
|
canCompile(script: ScriptData): boolean;
|
||||||
|
compile(script: ScriptData): IScriptCode;
|
||||||
|
}
|
||||||
171
src/application/Parser/Script/Compiler/ScriptCompiler.ts
Normal file
171
src/application/Parser/Script/Compiler/ScriptCompiler.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import { generateIlCode, IILCode } from './ILCode';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { ScriptData, FunctionData, FunctionCallData, ScriptFunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||||
|
import { IScriptCompiler } from './IScriptCompiler';
|
||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
interface ICompiledCode {
|
||||||
|
readonly code: string;
|
||||||
|
readonly revertCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ScriptCompiler implements IScriptCompiler {
|
||||||
|
constructor(
|
||||||
|
private readonly functions: readonly FunctionData[] | undefined,
|
||||||
|
private syntax: ILanguageSyntax) {
|
||||||
|
ensureValidFunctions(functions);
|
||||||
|
if (!syntax) { throw new Error('undefined syntax'); }
|
||||||
|
}
|
||||||
|
public canCompile(script: ScriptData): boolean {
|
||||||
|
if (!script.call) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
public compile(script: ScriptData): IScriptCode {
|
||||||
|
this.ensureCompilable(script.call);
|
||||||
|
const compiledCodes = new Array<ICompiledCode>();
|
||||||
|
const calls = getCallSequence(script.call);
|
||||||
|
calls.forEach((currentCall, currentCallIndex) => {
|
||||||
|
ensureValidCall(currentCall, script.name);
|
||||||
|
const commonFunction = this.getFunctionByName(currentCall.function);
|
||||||
|
let functionCode = compileCode(commonFunction, currentCall.parameters);
|
||||||
|
if (currentCallIndex !== calls.length - 1) {
|
||||||
|
functionCode = appendLine(functionCode);
|
||||||
|
}
|
||||||
|
compiledCodes.push(functionCode);
|
||||||
|
});
|
||||||
|
const scriptCode = merge(compiledCodes);
|
||||||
|
return new ScriptCode(scriptCode.code, scriptCode.revertCode, script.name, this.syntax);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getFunctionByName(name: string): FunctionData {
|
||||||
|
const func = this.functions.find((f) => f.name === name);
|
||||||
|
if (!func) {
|
||||||
|
throw new Error(`called function is not defined "${name}"`);
|
||||||
|
}
|
||||||
|
return func;
|
||||||
|
}
|
||||||
|
private ensureCompilable(call: ScriptFunctionCallData) {
|
||||||
|
if (!this.functions || this.functions.length === 0) {
|
||||||
|
throw new Error('cannot compile without shared functions');
|
||||||
|
}
|
||||||
|
if (typeof call !== 'object') {
|
||||||
|
throw new Error('called function(s) must be an object');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDuplicates(texts: readonly string[]): string[] {
|
||||||
|
return texts.filter((item, index) => texts.indexOf(item) !== index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function printList(list: readonly string[]): string {
|
||||||
|
return `"${list.join('","')}"`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
||||||
|
const duplicateFunctionNames = getDuplicates(functions
|
||||||
|
.map((func) => func.name.toLowerCase()));
|
||||||
|
if (duplicateFunctionNames.length) {
|
||||||
|
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
||||||
|
if (functions.some((func) => !func)) {
|
||||||
|
throw new Error(`some functions are undefined`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
|
||||||
|
const functionsWithParameters = functions
|
||||||
|
.filter((func) => func.parameters && func.parameters.length > 0);
|
||||||
|
for (const func of functionsWithParameters) {
|
||||||
|
const duplicateParameterNames = getDuplicates(func.parameters);
|
||||||
|
if (duplicateParameterNames.length) {
|
||||||
|
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||||
|
const duplicateCodes = getDuplicates(functions.map((func) => func.code));
|
||||||
|
if (duplicateCodes.length > 0) {
|
||||||
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
|
}
|
||||||
|
const duplicateRevertCodes = getDuplicates(functions
|
||||||
|
.filter((func) => func.revertCode)
|
||||||
|
.map((func) => func.revertCode));
|
||||||
|
if (duplicateRevertCodes.length > 0) {
|
||||||
|
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
|
if (!functions || functions.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ensureNoUndefinedItem(functions);
|
||||||
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
|
ensureNoDuplicatesInParameterNames(functions);
|
||||||
|
ensureNoDuplicateCode(functions);
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLine(code: ICompiledCode): ICompiledCode {
|
||||||
|
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
|
||||||
|
return {
|
||||||
|
code: appendLineIfNotEmpty(code.code),
|
||||||
|
revertCode: appendLineIfNotEmpty(code.revertCode),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
|
||||||
|
return {
|
||||||
|
code: codes.map((code) => code.code).join(''),
|
||||||
|
revertCode: codes.map((code) => code.revertCode).join(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileCode(func: FunctionData, parameters: FunctionCallParametersData): ICompiledCode {
|
||||||
|
return {
|
||||||
|
code: compileExpressions(func.code, parameters),
|
||||||
|
revertCode: compileExpressions(func.revertCode, parameters),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileExpressions(code: string, parameters: FunctionCallParametersData): string {
|
||||||
|
let intermediateCode = generateIlCode(code);
|
||||||
|
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||||
|
return intermediateCode.compile();
|
||||||
|
}
|
||||||
|
|
||||||
|
function substituteParameters(intermediateCode: IILCode, parameters: FunctionCallParametersData): IILCode {
|
||||||
|
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||||
|
if (parameterNames.length && !parameters) {
|
||||||
|
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
|
||||||
|
}
|
||||||
|
for (const parameterName of parameterNames) {
|
||||||
|
const parameterValue = parameters[parameterName];
|
||||||
|
if (!parameterValue) {
|
||||||
|
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||||
|
}
|
||||||
|
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||||
|
}
|
||||||
|
return intermediateCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCall(call: FunctionCallData, scriptName: string) {
|
||||||
|
if (!call) {
|
||||||
|
throw new Error(`undefined function call in script "${scriptName}"`);
|
||||||
|
}
|
||||||
|
if (!call.function) {
|
||||||
|
throw new Error(`empty function name called in script "${scriptName}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
|
||||||
|
if (call instanceof Array) {
|
||||||
|
return call as FunctionCallData[];
|
||||||
|
}
|
||||||
|
return [ call as FunctionCallData ];
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
|
||||||
|
export interface ICategoryCollectionParseContext {
|
||||||
|
readonly compiler: IScriptCompiler;
|
||||||
|
readonly syntax: ILanguageSyntax;
|
||||||
|
}
|
||||||
54
src/application/Parser/Script/ScriptParser.ts
Normal file
54
src/application/Parser/Script/ScriptParser.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Script } from '@/domain/Script';
|
||||||
|
import { ScriptData } from 'js-yaml-loader!@/*';
|
||||||
|
import { parseDocUrls } from '../DocumentationParser';
|
||||||
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
|
import { createEnumParser, IEnumParser } from '../../Common/Enum';
|
||||||
|
import { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
|
||||||
|
export function parseScript(
|
||||||
|
data: ScriptData, context: ICategoryCollectionParseContext,
|
||||||
|
levelParser = createEnumParser(RecommendationLevel)): Script {
|
||||||
|
validateScript(data);
|
||||||
|
if (!context) { throw new Error('undefined context'); }
|
||||||
|
const script = new Script(
|
||||||
|
/* name */ data.name,
|
||||||
|
/* code */ parseCode(data, context),
|
||||||
|
/* docs */ parseDocUrls(data),
|
||||||
|
/* level */ parseLevel(data.recommend, levelParser));
|
||||||
|
return script;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined {
|
||||||
|
if (!level) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return parser.parseEnum(level, 'level');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCode(script: ScriptData, context: ICategoryCollectionParseContext): IScriptCode {
|
||||||
|
if (context.compiler.canCompile(script)) {
|
||||||
|
return context.compiler.compile(script);
|
||||||
|
}
|
||||||
|
return new ScriptCode(script.code, script.revertCode, script.name, context.syntax);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNotBothCallAndCode(script: ScriptData) {
|
||||||
|
if (script.code && script.call) {
|
||||||
|
throw new Error('cannot define both "call" and "code"');
|
||||||
|
}
|
||||||
|
if (script.revertCode && script.call) {
|
||||||
|
throw new Error('cannot define "revertCode" if "call" is defined');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateScript(script: ScriptData) {
|
||||||
|
if (!script) {
|
||||||
|
throw new Error('undefined script');
|
||||||
|
}
|
||||||
|
if (!script.code && !script.call) {
|
||||||
|
throw new Error('must define either "call" or "code"');
|
||||||
|
}
|
||||||
|
ensureNotBothCallAndCode(script);
|
||||||
|
}
|
||||||
6
src/application/Parser/Script/Syntax/BatchFileSyntax.ts
Normal file
6
src/application/Parser/Script/Syntax/BatchFileSyntax.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
export class BatchFileSyntax implements ILanguageSyntax {
|
||||||
|
public readonly commentDelimiters = [ 'REM', '::' ];
|
||||||
|
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
||||||
|
}
|
||||||
6
src/application/Parser/Script/Syntax/ISyntaxFactory.ts
Normal file
6
src/application/Parser/Script/Syntax/ISyntaxFactory.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
|
||||||
|
export interface ISyntaxFactory {
|
||||||
|
create(language: ScriptingLanguage): ILanguageSyntax;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
|
||||||
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
|
public readonly commentDelimiters = [ '#' ];
|
||||||
|
public readonly commonCodeParts = [ '(', ')', 'else' ];
|
||||||
|
}
|
||||||
15
src/application/Parser/Script/Syntax/SyntaxFactory.ts
Normal file
15
src/application/Parser/Script/Syntax/SyntaxFactory.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { ILanguageSyntax } from '@/domain/ScriptCode';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { ISyntaxFactory } from './ISyntaxFactory';
|
||||||
|
import { BatchFileSyntax } from './BatchFileSyntax';
|
||||||
|
import { ShellScriptSyntax } from './ShellScriptSyntax';
|
||||||
|
|
||||||
|
export class SyntaxFactory implements ISyntaxFactory {
|
||||||
|
public create(language: ScriptingLanguage): ILanguageSyntax {
|
||||||
|
switch (language) {
|
||||||
|
case ScriptingLanguage.batchfile: return new BatchFileSyntax();
|
||||||
|
case ScriptingLanguage.shellscript: return new ShellScriptSyntax();
|
||||||
|
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import { Script } from '@/domain/Script';
|
|
||||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
|
||||||
import { parseDocUrls } from './DocumentationParser';
|
|
||||||
|
|
||||||
export function parseScript(yamlScript: YamlScript): Script {
|
|
||||||
if (!yamlScript) {
|
|
||||||
throw new Error('script is null or undefined');
|
|
||||||
}
|
|
||||||
const script = new Script(
|
|
||||||
/* name */ yamlScript.name,
|
|
||||||
/* code */ yamlScript.code,
|
|
||||||
/* revertCode */ yamlScript.revertCode,
|
|
||||||
/* docs */ parseDocUrls(yamlScript),
|
|
||||||
/* isRecommended */ yamlScript.recommend);
|
|
||||||
return script;
|
|
||||||
}
|
|
||||||
36
src/application/Parser/ScriptingDefinitionParser.ts
Normal file
36
src/application/Parser/ScriptingDefinitionParser.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { ScriptingDefinitionData } from 'js-yaml-loader!@/*';
|
||||||
|
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||||
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
|
import { createEnumParser } from '../Common/Enum';
|
||||||
|
import { generateIlCode } from './Script/Compiler/ILCode';
|
||||||
|
|
||||||
|
export function parseScriptingDefinition(
|
||||||
|
definition: ScriptingDefinitionData,
|
||||||
|
info: IProjectInformation,
|
||||||
|
date = new Date(),
|
||||||
|
languageParser = createEnumParser(ScriptingLanguage)): IScriptingDefinition {
|
||||||
|
if (!info) {
|
||||||
|
throw new Error('undefined info');
|
||||||
|
}
|
||||||
|
if (!definition) {
|
||||||
|
throw new Error('undefined definition');
|
||||||
|
}
|
||||||
|
const language = languageParser.parseEnum(definition.language, 'language');
|
||||||
|
const startCode = applySubstitutions(definition.startCode, info, date);
|
||||||
|
const endCode = applySubstitutions(definition.endCode, info, date);
|
||||||
|
return new ScriptingDefinition(
|
||||||
|
language,
|
||||||
|
startCode,
|
||||||
|
endCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySubstitutions(code: string, info: IProjectInformation, date: Date): string {
|
||||||
|
let ilCode = generateIlCode(code);
|
||||||
|
ilCode = ilCode.substituteParameter('homepage', info.homepage);
|
||||||
|
ilCode = ilCode.substituteParameter('version', info.version);
|
||||||
|
ilCode = ilCode.substituteParameter('date', date.toUTCString());
|
||||||
|
return ilCode.compile();
|
||||||
|
}
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
import { UserFilter } from './Filter/UserFilter';
|
|
||||||
import { IUserFilter } from './Filter/IUserFilter';
|
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
|
||||||
import { UserSelection } from './Selection/UserSelection';
|
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
|
||||||
import { Signal } from '@/infrastructure/Events/Signal';
|
|
||||||
import { parseApplication } from '../Parser/ApplicationParser';
|
|
||||||
import { IApplicationState } from './IApplicationState';
|
|
||||||
import { Script } from '@/domain/Script';
|
|
||||||
import { IApplication } from '@/domain/IApplication';
|
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
|
||||||
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
|
||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
|
||||||
|
|
||||||
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
|
||||||
export class ApplicationState implements IApplicationState {
|
|
||||||
/** Get singleton application state */
|
|
||||||
public static GetAsync(): Promise<IApplicationState> {
|
|
||||||
return ApplicationState.instance.getValueAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Application instance with all scripts. */
|
|
||||||
private static instance = new AsyncLazy<IApplicationState>(() => {
|
|
||||||
const application = parseApplication(applicationFile);
|
|
||||||
const selectedScripts = new Array<Script>();
|
|
||||||
const state = new ApplicationState(application, selectedScripts);
|
|
||||||
return Promise.resolve(state);
|
|
||||||
});
|
|
||||||
|
|
||||||
public readonly code: IApplicationCode;
|
|
||||||
public readonly stateChanged = new Signal<IApplicationState>();
|
|
||||||
public readonly selection: IUserSelection;
|
|
||||||
public readonly filter: IUserFilter;
|
|
||||||
|
|
||||||
private constructor(
|
|
||||||
/** Inner instance of the all scripts */
|
|
||||||
public readonly app: IApplication,
|
|
||||||
/** Initially selected scripts */
|
|
||||||
public readonly defaultScripts: Script[]) {
|
|
||||||
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
|
||||||
this.code = new ApplicationCode(this.selection, app.version);
|
|
||||||
this.filter = new UserFilter(app);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
|
||||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
|
||||||
|
|
||||||
export interface IUserScript {
|
|
||||||
code: string;
|
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
export interface IUserScriptGenerator {
|
|
||||||
buildCode(
|
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
|
||||||
version: string): IUserScript;
|
|
||||||
}
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
|
||||||
import { CodeBuilder } from './CodeBuilder';
|
|
||||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
|
||||||
import { IUserScript } from './IUserScript';
|
|
||||||
|
|
||||||
export const adminRightsScript = {
|
|
||||||
name: 'Ensure admin privileges',
|
|
||||||
code: 'fltmc >nul 2>&1 || (\n' +
|
|
||||||
' echo Administrator privileges are required.\n' +
|
|
||||||
' PowerShell Start -Verb RunAs \'%0\' 2> nul || (\n' +
|
|
||||||
' echo Right-click on the script and select "Run as administrator".\n' +
|
|
||||||
' pause & exit 1\n' +
|
|
||||||
' )\n' +
|
|
||||||
' exit 0\n' +
|
|
||||||
')',
|
|
||||||
};
|
|
||||||
|
|
||||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
|
||||||
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
|
|
||||||
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
|
||||||
if (!version) { throw new Error('version is undefined'); }
|
|
||||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
|
||||||
if (!selectedScripts.length) {
|
|
||||||
return { code: '', scriptPositions };
|
|
||||||
}
|
|
||||||
const builder = initializeCode(version);
|
|
||||||
for (const selection of selectedScripts) {
|
|
||||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
|
||||||
}
|
|
||||||
const code = finalizeCode(builder);
|
|
||||||
return { code, scriptPositions };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeCode(version: string): CodeBuilder {
|
|
||||||
return new CodeBuilder()
|
|
||||||
.appendLine('@echo off')
|
|
||||||
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
|
||||||
.appendFunction(adminRightsScript.name, adminRightsScript.code)
|
|
||||||
.appendLine();
|
|
||||||
}
|
|
||||||
|
|
||||||
function finalizeCode(builder: CodeBuilder): string {
|
|
||||||
return builder.appendLine()
|
|
||||||
.appendLine('pause')
|
|
||||||
.appendLine('exit /b 0')
|
|
||||||
.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendSelection(
|
|
||||||
selection: SelectedScript,
|
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
|
||||||
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
|
|
||||||
const startPosition = builder.currentLine + 1;
|
|
||||||
appendCode(selection, builder);
|
|
||||||
const endPosition = builder.currentLine - 1;
|
|
||||||
builder.appendLine();
|
|
||||||
scriptPositions.set(selection, new CodePosition(startPosition, endPosition));
|
|
||||||
return scriptPositions;
|
|
||||||
}
|
|
||||||
|
|
||||||
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
|
||||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
|
||||||
const scriptCode = selection.revert ? selection.script.revertCode : selection.script.code;
|
|
||||||
builder.appendFunction(name, scriptCode);
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
import { IApplication } from './../../domain/IApplication';
|
|
||||||
import { IUserFilter } from './Filter/IUserFilter';
|
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
|
||||||
export { IUserSelection, IApplicationCode, IUserFilter };
|
|
||||||
|
|
||||||
export interface IApplicationState {
|
|
||||||
/** Event that fires when the application states changes with new application state as parameter */
|
|
||||||
readonly code: IApplicationCode;
|
|
||||||
readonly filter: IUserFilter;
|
|
||||||
readonly stateChanged: ISignal<IApplicationState>;
|
|
||||||
readonly selection: IUserSelection;
|
|
||||||
readonly app: IApplication;
|
|
||||||
}
|
|
||||||
29
src/application/application.yaml.d.ts
vendored
29
src/application/application.yaml.d.ts
vendored
@@ -1,29 +0,0 @@
|
|||||||
declare module 'js-yaml-loader!*' {
|
|
||||||
export type CategoryOrScript = YamlCategory | YamlScript;
|
|
||||||
export type DocumentationUrls = ReadonlyArray<string> | string;
|
|
||||||
|
|
||||||
export interface YamlDocumentable {
|
|
||||||
docs?: DocumentationUrls;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YamlScript extends YamlDocumentable {
|
|
||||||
name: string;
|
|
||||||
code: string;
|
|
||||||
revertCode: string;
|
|
||||||
recommend: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface YamlCategory extends YamlDocumentable {
|
|
||||||
children: ReadonlyArray<CategoryOrScript>;
|
|
||||||
category: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApplicationYaml {
|
|
||||||
name: string;
|
|
||||||
repositoryUrl: string;
|
|
||||||
actions: ReadonlyArray<YamlCategory>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const content: ApplicationYaml;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
56
src/application/collections/collection.yaml.d.ts
vendored
Normal file
56
src/application/collections/collection.yaml.d.ts
vendored
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
declare module 'js-yaml-loader!*' {
|
||||||
|
export interface CollectionData {
|
||||||
|
readonly os: string;
|
||||||
|
readonly scripting: ScriptingDefinitionData;
|
||||||
|
readonly actions: ReadonlyArray<CategoryData>;
|
||||||
|
readonly functions?: ReadonlyArray<FunctionData>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategoryData extends DocumentableData {
|
||||||
|
readonly children: ReadonlyArray<CategoryOrScriptData>;
|
||||||
|
readonly category: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||||
|
export type DocumentationUrlsData = ReadonlyArray<string> | string;
|
||||||
|
|
||||||
|
export interface DocumentableData {
|
||||||
|
readonly docs?: DocumentationUrlsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionData {
|
||||||
|
name: string;
|
||||||
|
code: string;
|
||||||
|
revertCode?: string;
|
||||||
|
parameters?: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionCallParametersData {
|
||||||
|
[index: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionCallData {
|
||||||
|
function: string;
|
||||||
|
parameters?: FunctionCallParametersData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||||
|
|
||||||
|
export interface ScriptData extends DocumentableData {
|
||||||
|
name: string;
|
||||||
|
code?: string;
|
||||||
|
revertCode?: string;
|
||||||
|
call: ScriptFunctionCallData;
|
||||||
|
recommend?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptingDefinitionData {
|
||||||
|
readonly language: string;
|
||||||
|
readonly fileExtension: string;
|
||||||
|
readonly startCode: string;
|
||||||
|
readonly endCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const content: CollectionData;
|
||||||
|
export default content;
|
||||||
|
}
|
||||||
225
src/application/collections/macos.yaml
Normal file
225
src/application/collections/macos.yaml
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
# Structure documented in "docs/collections.md"
|
||||||
|
os: macos
|
||||||
|
scripting:
|
||||||
|
language: shellscript
|
||||||
|
startCode: |-
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# {{ $homepage }} — v{{ $version }} — {{ $date }}
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
script_path=$([[ "$0" = /* ]] && echo "$0" || echo "$PWD/${0#./}")
|
||||||
|
sudo "$script_path" || (
|
||||||
|
echo 'Administrator privileges are required.'
|
||||||
|
exit 1
|
||||||
|
)
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
endCode: |-
|
||||||
|
echo 'Your privacy and security is now hardened 🎉💪'
|
||||||
|
echo 'Press any key to exit.'
|
||||||
|
read -n 1 -s
|
||||||
|
actions:
|
||||||
|
-
|
||||||
|
category: Privacy cleanup
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
category: Clear terminal history
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Clear bash history
|
||||||
|
recommend: standard
|
||||||
|
code: rm -f ~/.bash_history
|
||||||
|
-
|
||||||
|
name: Clear zsh history
|
||||||
|
recommend: standard
|
||||||
|
code: rm -f ~/.zsh_history
|
||||||
|
-
|
||||||
|
name: Clear CUPS printer job cache
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo rm -rfv /var/spool/cups/c0*
|
||||||
|
sudo rm -rfv /var/spool/cups/tmp/*
|
||||||
|
sudo rm -rfv /var/spool/cups/cache/job.cache*
|
||||||
|
-
|
||||||
|
name: Clear the list of iOS devices connected
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo defaults delete /Users/$USER/Library/Preferences/com.apple.iPod.plist "conn:128:Last Connect"
|
||||||
|
sudo defaults delete /Users/$USER/Library/Preferences/com.apple.iPod.plist Devices
|
||||||
|
sudo defaults delete /Library/Preferences/com.apple.iPod.plist "conn:128:Last Connect"
|
||||||
|
sudo defaults delete /Library/Preferences/com.apple.iPod.plist Devices
|
||||||
|
sudo rm -rfv /var/db/lockdown/*
|
||||||
|
-
|
||||||
|
name: Reset privacy database (remove all permissions)
|
||||||
|
code: sudo tccutil reset All
|
||||||
|
-
|
||||||
|
category: Configure programs
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Disable Firefox telemetry
|
||||||
|
recommend: standard
|
||||||
|
docs: https://github.com/mozilla/policy-templates/blob/master/README.md
|
||||||
|
code: |-
|
||||||
|
# Enable Firefox policies so the telemetry can be configured.
|
||||||
|
sudo defaults write /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled -bool TRUE
|
||||||
|
# Disable sending usage data
|
||||||
|
sudo defaults write /Library/Preferences/org.mozilla.firefox DisableTelemetry -bool TRUE
|
||||||
|
revertCode: |-
|
||||||
|
sudo defaults delete /Library/Preferences/org.mozilla.firefox EnterprisePoliciesEnabled
|
||||||
|
sudo defaults delete /Library/Preferences/org.mozilla.firefox DisableTelemetry
|
||||||
|
-
|
||||||
|
name: Disable Microsoft Office diagnostics data sending
|
||||||
|
recommend: standard
|
||||||
|
code: defaults write com.microsoft.office DiagnosticDataTypePreference -string ZeroDiagnosticData
|
||||||
|
revertCode: defaults delete com.microsoft.office DiagnosticDataTypePreference
|
||||||
|
-
|
||||||
|
name: Uninstall Google update
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
googleUpdateFile=~/Library/Google/GoogleSoftwareUpdate/GoogleSoftwareUpdate.bundle/Contents/Resources/ksinstall
|
||||||
|
if [ -f "$googleUpdateFile" ]; then
|
||||||
|
$googleUpdateFile --nuke
|
||||||
|
echo Uninstalled google update
|
||||||
|
else
|
||||||
|
echo Google update file does not exist
|
||||||
|
fi
|
||||||
|
-
|
||||||
|
name: Disable Homebrew user behavior analytics
|
||||||
|
recommend: standard
|
||||||
|
docs: https://docs.brew.sh/Analytics
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: PersistUserEnvironmentConfiguration
|
||||||
|
parameters:
|
||||||
|
configuration: export HOMEBREW_NO_ANALYTICS=1
|
||||||
|
-
|
||||||
|
name: Disable NET Core CLI telemetry
|
||||||
|
recommend: standard
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: PersistUserEnvironmentConfiguration
|
||||||
|
parameters:
|
||||||
|
configuration: export DOTNET_CLI_TELEMETRY_OPTOUT=1
|
||||||
|
-
|
||||||
|
name: Disable PowerShell Core telemetry
|
||||||
|
recommend: standard
|
||||||
|
docs: https://github.com/PowerShell/PowerShell/tree/release/v7.1.1#telemetry
|
||||||
|
call:
|
||||||
|
-
|
||||||
|
function: PersistUserEnvironmentConfiguration
|
||||||
|
parameters:
|
||||||
|
configuration: export POWERSHELL_TELEMETRY_OPTOUT=1
|
||||||
|
-
|
||||||
|
category: Configure OS
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
category: Configure Apple Remote Desktop
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Deactivate the Remote Management Service
|
||||||
|
recommend: strict
|
||||||
|
code: sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -deactivate -stop
|
||||||
|
revertCode: sudo /System/Library/CoreServices/RemoteManagement/ARDAgent.app/Contents/Resources/kickstart -activate -restart -agent -console
|
||||||
|
-
|
||||||
|
name: Remove Apple Remote Desktop Settings
|
||||||
|
recommend: strict
|
||||||
|
code: |-
|
||||||
|
sudo rm -rf /var/db/RemoteManagement
|
||||||
|
sudo defaults delete /Library/Preferences/com.apple.RemoteDesktop.plist
|
||||||
|
defaults delete ~/Library/Preferences/com.apple.RemoteDesktop.plist
|
||||||
|
sudo rm -r /Library/Application\ Support/Apple/Remote\ Desktop/
|
||||||
|
rm -r ~/Library/Application\ Support/Remote\ Desktop/
|
||||||
|
rm -r ~/Library/Containers/com.apple.RemoteDesktop
|
||||||
|
-
|
||||||
|
name: Disable Internet based spell correction
|
||||||
|
code: defaults write NSGlobalDomain WebAutomaticSpellingCorrectionEnabled -bool false
|
||||||
|
revertCode: defaults delete NSGlobalDomain WebAutomaticSpellingCorrectionEnabled
|
||||||
|
-
|
||||||
|
name: Disable Remote Apple Events
|
||||||
|
recommend: strict
|
||||||
|
code: sudo systemsetup -setremoteappleevents off
|
||||||
|
revertCode: sudo systemsetup -setremoteappleevents on
|
||||||
|
-
|
||||||
|
name: Do not store documents to iCloud Drive by default
|
||||||
|
docs: https://macos-defaults.com/finder/nsdocumentsavenewdocumentstocloud.html
|
||||||
|
recommend: standard
|
||||||
|
code: defaults write NSGlobalDomain NSDocumentSaveNewDocumentsToCloud -bool false
|
||||||
|
revertCode: defaults delete NSGlobalDomain NSDocumentSaveNewDocumentsToCloud
|
||||||
|
-
|
||||||
|
category: Security improvements
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
category: Configure macOS Application Firewall
|
||||||
|
children:
|
||||||
|
-
|
||||||
|
name: Enable firewall
|
||||||
|
recommend: standard
|
||||||
|
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81681
|
||||||
|
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate on
|
||||||
|
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setglobalstate off
|
||||||
|
-
|
||||||
|
name: Turn on firewall logging
|
||||||
|
recommend: standard
|
||||||
|
docs: https://www.stigviewer.com/stig/apple_os_x_10.13/2018-10-01/finding/V-81671
|
||||||
|
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode on
|
||||||
|
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setloggingmode off
|
||||||
|
-
|
||||||
|
name: Turn on stealth mode
|
||||||
|
recommend: standard
|
||||||
|
docs: https://www.stigviewer.com/stig/apple_os_x_10.8_mountain_lion_workstation/2015-02-10/finding/V-51327
|
||||||
|
code: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode on
|
||||||
|
revertCode: /usr/libexec/ApplicationFirewall/socketfilterfw --setstealthmode off
|
||||||
|
-
|
||||||
|
name: Disable Spotlight indexing
|
||||||
|
code: sudo mdutil -i off -d /
|
||||||
|
revertCode: sudo mdutil -i on /
|
||||||
|
-
|
||||||
|
name: Disable Captive portal
|
||||||
|
docs:
|
||||||
|
- https://web.archive.org/web/20171008071031if_/http://blog.erratasec.com/2010/09/apples-secret-wispr-request.html#.WdnPa5OyL6Y
|
||||||
|
- https://web.archive.org/web/20130407200745/http://www.divertednetworks.net/apple-captiveportal.html
|
||||||
|
- https://web.archive.org/web/20170622064304/https://grpugh.wordpress.com/2014/10/29/an-undocumented-change-to-captive-network-assistant-settings-in-os-x-10-10-yosemite/
|
||||||
|
code: sudo defaults write /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active -bool false
|
||||||
|
revertCode: sudo defaults delete /Library/Preferences/SystemConfiguration/com.apple.captive.control.plist Active
|
||||||
|
-
|
||||||
|
name: Require a password to wake the computer from sleep or screen saver
|
||||||
|
code: defaults write /Library/Preferences/com.apple.screensaver askForPassword -bool true
|
||||||
|
revertCode: sudo defaults delete /Library/Preferences/com.apple.screensaver askForPassword
|
||||||
|
-
|
||||||
|
name: Do not show recent items on dock
|
||||||
|
docs: https://developer.apple.com/documentation/devicemanagement/dock
|
||||||
|
code: defaults write com.apple.dock show-recents -bool false
|
||||||
|
revertCode: defaults delete com.apple.dock show-recents
|
||||||
|
-
|
||||||
|
name: Disable AirDrop file sharing
|
||||||
|
recommend: strict
|
||||||
|
code: defaults write com.apple.NetworkBrowser DisableAirDrop -bool true
|
||||||
|
revertCode: defaults write com.apple.NetworkBrowser DisableAirDrop -bool false
|
||||||
|
functions:
|
||||||
|
-
|
||||||
|
name: PersistUserEnvironmentConfiguration
|
||||||
|
parameters: [ configuration ]
|
||||||
|
code: |-
|
||||||
|
command='{{ $configuration }}'
|
||||||
|
declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile")
|
||||||
|
for profile_file in "${profile_files[@]}"
|
||||||
|
do
|
||||||
|
touch "$profile_file"
|
||||||
|
if ! grep -q "$command" "${profile_file}"; then
|
||||||
|
echo "$command" >> "$profile_file"
|
||||||
|
echo "[$profile_file] Configured"
|
||||||
|
else
|
||||||
|
echo "[$profile_file] No need for any action, already configured"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
revertCode: |-
|
||||||
|
command='{{ $configuration }}'
|
||||||
|
declare -a profile_files=("$HOME/.bash_profile" "$HOME/.zprofile")
|
||||||
|
for profile_file in "${profile_files[@]}"
|
||||||
|
do
|
||||||
|
if grep -q "$command" "${profile_file}" 2>/dev/null; then
|
||||||
|
sed -i '' "/$command/d" "$profile_file"
|
||||||
|
echo "[$profile_file] Reverted configuration"
|
||||||
|
else
|
||||||
|
echo "[$profile_file] No need for any action, configuration does not exist"
|
||||||
|
fi
|
||||||
|
done
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,116 +1,47 @@
|
|||||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
|
||||||
import { ICategory } from './ICategory';
|
|
||||||
import { IScript } from './IScript';
|
|
||||||
import { IApplication } from './IApplication';
|
import { IApplication } from './IApplication';
|
||||||
|
import { ICategoryCollection } from './ICategoryCollection';
|
||||||
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
export class Application implements IApplication {
|
export class Application implements IApplication {
|
||||||
public get totalScripts(): number { return this.flattened.allScripts.length; }
|
constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) {
|
||||||
public get totalCategories(): number { return this.flattened.allCategories.length; }
|
validateInformation(info);
|
||||||
|
validateCollections(collections);
|
||||||
private readonly flattened: IFlattenedApplication;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public readonly name: string,
|
|
||||||
public readonly repositoryUrl: string,
|
|
||||||
public readonly version: string,
|
|
||||||
public readonly actions: ReadonlyArray<ICategory>) {
|
|
||||||
if (!name) { throw Error('Application has no name'); }
|
|
||||||
if (!repositoryUrl) { throw Error('Application has no repository url'); }
|
|
||||||
if (!version) { throw Error('Version cannot be empty'); }
|
|
||||||
this.flattened = flatten(actions);
|
|
||||||
ensureValid(this.flattened);
|
|
||||||
ensureNoDuplicates(this.flattened.allCategories);
|
|
||||||
ensureNoDuplicates(this.flattened.allScripts);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findCategory(categoryId: number): ICategory | undefined {
|
public getSupportedOsList(): OperatingSystem[] {
|
||||||
return this.flattened.allCategories.find((category) => category.id === categoryId);
|
return this.collections.map((collection) => collection.os);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getRecommendedScripts(): readonly IScript[] {
|
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
|
||||||
return this.flattened.allScripts.filter((script) => script.isRecommended);
|
return this.collections.find((collection) => collection.os === operatingSystem);
|
||||||
}
|
|
||||||
|
|
||||||
public findScript(scriptId: string): IScript | undefined {
|
|
||||||
return this.flattened.allScripts.find((script) => script.id === scriptId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllScripts(): IScript[] {
|
|
||||||
return this.flattened.allScripts;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllCategories(): ICategory[] {
|
|
||||||
return this.flattened.allCategories;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
function validateInformation(info: IProjectInformation) {
|
||||||
const totalOccurencesById = new Map<TKey, number>();
|
if (!info) {
|
||||||
for (const entity of entities) {
|
throw new Error('undefined project information');
|
||||||
totalOccurencesById.set(entity.id, (totalOccurencesById.get(entity.id) || 0) + 1);
|
|
||||||
}
|
|
||||||
const duplicatedIds = new Array<TKey>();
|
|
||||||
totalOccurencesById.forEach((index, id) => {
|
|
||||||
if (index > 1) {
|
|
||||||
duplicatedIds.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (duplicatedIds.length > 0) {
|
|
||||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
|
||||||
throw new Error(
|
|
||||||
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IFlattenedApplication {
|
function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||||
allCategories: ICategory[];
|
if (!collections) {
|
||||||
allScripts: IScript[];
|
throw new Error('undefined collections');
|
||||||
}
|
|
||||||
|
|
||||||
function ensureValid(application: IFlattenedApplication) {
|
|
||||||
if (!application.allCategories || application.allCategories.length === 0) {
|
|
||||||
throw new Error('Application must consist of at least one category');
|
|
||||||
}
|
}
|
||||||
if (!application.allScripts || application.allScripts.length === 0) {
|
if (collections.length === 0) {
|
||||||
throw new Error('Application must consist of at least one script');
|
throw new Error('no collection in the list');
|
||||||
}
|
}
|
||||||
if (application.allScripts.filter((script) => script.isRecommended).length === 0) {
|
if (collections.filter((c) => !c).length > 0) {
|
||||||
throw new Error('Application must consist of at least one recommended script');
|
throw new Error('undefined collection in the list');
|
||||||
|
}
|
||||||
|
const osList = collections.map((c) => c.os);
|
||||||
|
const duplicates = getDuplicates(osList);
|
||||||
|
if (duplicates.length > 0) {
|
||||||
|
throw new Error('multiple collections with same os: ' +
|
||||||
|
duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenCategories(
|
function getDuplicates(list: readonly OperatingSystem[]): OperatingSystem[] {
|
||||||
categories: ReadonlyArray<ICategory>,
|
return list.filter((os, index) => list.indexOf(os) !== index);
|
||||||
flattened: IFlattenedApplication): IFlattenedApplication {
|
|
||||||
if (!categories || categories.length === 0) {
|
|
||||||
return flattened;
|
|
||||||
}
|
|
||||||
for (const category of categories) {
|
|
||||||
flattened.allCategories.push(category);
|
|
||||||
flattened = flattenScripts(category.scripts, flattened);
|
|
||||||
flattened = flattenCategories(category.subCategories, flattened);
|
|
||||||
}
|
|
||||||
return flattened;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenScripts(
|
|
||||||
scripts: ReadonlyArray<IScript>,
|
|
||||||
flattened: IFlattenedApplication): IFlattenedApplication {
|
|
||||||
if (!scripts) {
|
|
||||||
return flattened;
|
|
||||||
}
|
|
||||||
for (const script of scripts) {
|
|
||||||
flattened.allScripts.push(script);
|
|
||||||
}
|
|
||||||
return flattened;
|
|
||||||
}
|
|
||||||
|
|
||||||
function flatten(
|
|
||||||
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
|
|
||||||
let flattened: IFlattenedApplication = {
|
|
||||||
allCategories: new Array<ICategory>(),
|
|
||||||
allScripts: new Array<IScript>(),
|
|
||||||
};
|
|
||||||
flattened = flattenCategories(categories, flattened);
|
|
||||||
return flattened;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
validateCategory(this);
|
validateCategory(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public includes(script: IScript): boolean {
|
||||||
|
return this.getAllScriptsRecursively().some((childScript) => childScript.id === script.id);
|
||||||
|
}
|
||||||
|
|
||||||
public getAllScriptsRecursively(): readonly IScript[] {
|
public getAllScriptsRecursively(): readonly IScript[] {
|
||||||
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
return this.allSubScripts || (this.allSubScripts = parseScriptsRecursively(this));
|
||||||
}
|
}
|
||||||
|
|||||||
168
src/domain/CategoryCollection.ts
Normal file
168
src/domain/CategoryCollection.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { getEnumNames, getEnumValues } from '@/application/Common/Enum';
|
||||||
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
|
import { ICategory } from './ICategory';
|
||||||
|
import { IScript } from './IScript';
|
||||||
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
import { IScriptingDefinition } from './IScriptingDefinition';
|
||||||
|
import { ICategoryCollection } from './ICategoryCollection';
|
||||||
|
|
||||||
|
export class CategoryCollection implements ICategoryCollection {
|
||||||
|
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||||
|
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||||
|
|
||||||
|
private readonly queryable: IQueryableCollection;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly os: OperatingSystem,
|
||||||
|
public readonly actions: ReadonlyArray<ICategory>,
|
||||||
|
public readonly scripting: IScriptingDefinition) {
|
||||||
|
if (!scripting) {
|
||||||
|
throw new Error('undefined scripting definition');
|
||||||
|
}
|
||||||
|
this.queryable = makeQueryable(actions);
|
||||||
|
ensureValidOs(os);
|
||||||
|
ensureValid(this.queryable);
|
||||||
|
ensureNoDuplicates(this.queryable.allCategories);
|
||||||
|
ensureNoDuplicates(this.queryable.allScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public findCategory(categoryId: number): ICategory | undefined {
|
||||||
|
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||||
|
if (isNaN(level)) {
|
||||||
|
throw new Error('undefined level');
|
||||||
|
}
|
||||||
|
if (!(level in RecommendationLevel)) {
|
||||||
|
throw new Error(`invalid level: ${level}`);
|
||||||
|
}
|
||||||
|
return this.queryable.scriptsByLevel.get(level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public findScript(scriptId: string): IScript | undefined {
|
||||||
|
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllScripts(): IScript[] {
|
||||||
|
return this.queryable.allScripts;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllCategories(): ICategory[] {
|
||||||
|
return this.queryable.allCategories;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidOs(os: OperatingSystem): void {
|
||||||
|
if (os === undefined) {
|
||||||
|
throw new Error('undefined os');
|
||||||
|
}
|
||||||
|
if (os === OperatingSystem.Unknown) {
|
||||||
|
throw new Error('unknown os');
|
||||||
|
}
|
||||||
|
if (!(os in OperatingSystem)) {
|
||||||
|
throw new Error(`os "${os}" is out of range`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||||
|
const totalOccurrencesById = new Map<TKey, number>();
|
||||||
|
for (const entity of entities) {
|
||||||
|
totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1);
|
||||||
|
}
|
||||||
|
const duplicatedIds = new Array<TKey>();
|
||||||
|
totalOccurrencesById.forEach((index, id) => {
|
||||||
|
if (index > 1) {
|
||||||
|
duplicatedIds.push(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (duplicatedIds.length > 0) {
|
||||||
|
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||||
|
throw new Error(
|
||||||
|
`Duplicate entities are detected with following id(s): ${duplicatedIdsText}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IQueryableCollection {
|
||||||
|
allCategories: ICategory[];
|
||||||
|
allScripts: IScript[];
|
||||||
|
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValid(application: IQueryableCollection) {
|
||||||
|
ensureValidCategories(application.allCategories);
|
||||||
|
ensureValidScripts(application.allScripts);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||||
|
if (!allCategories || allCategories.length === 0) {
|
||||||
|
throw new Error('must consist of at least one category');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||||
|
if (!allScripts || allScripts.length === 0) {
|
||||||
|
throw new Error('must consist of at least one script');
|
||||||
|
}
|
||||||
|
for (const level of getEnumValues(RecommendationLevel)) {
|
||||||
|
if (allScripts.every((script) => script.level !== level)) {
|
||||||
|
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
|
||||||
|
const allCategories = new Array<ICategory>();
|
||||||
|
const allScripts = new Array<IScript>();
|
||||||
|
flattenCategories(categories, allCategories, allScripts);
|
||||||
|
return [
|
||||||
|
allCategories,
|
||||||
|
allScripts,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenCategories(
|
||||||
|
categories: ReadonlyArray<ICategory>,
|
||||||
|
allCategories: ICategory[],
|
||||||
|
allScripts: IScript[]): IQueryableCollection {
|
||||||
|
if (!categories || categories.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const category of categories) {
|
||||||
|
allCategories.push(category);
|
||||||
|
flattenScripts(category.scripts, allScripts);
|
||||||
|
flattenCategories(category.subCategories, allCategories, allScripts);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenScripts(
|
||||||
|
scripts: ReadonlyArray<IScript>,
|
||||||
|
allScripts: IScript[]): IScript[] {
|
||||||
|
if (!scripts) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const script of scripts) {
|
||||||
|
allScripts.push(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeQueryable(
|
||||||
|
actions: ReadonlyArray<ICategory>): IQueryableCollection {
|
||||||
|
const flattened = flattenApplication(actions);
|
||||||
|
return {
|
||||||
|
allCategories: flattened[0],
|
||||||
|
allScripts: flattened[1],
|
||||||
|
scriptsByLevel: groupByLevel(flattened[1]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
|
||||||
|
const map = new Map<RecommendationLevel, readonly IScript[]>();
|
||||||
|
for (const levelName of getEnumNames(RecommendationLevel)) {
|
||||||
|
const level = RecommendationLevel[levelName];
|
||||||
|
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
|
||||||
|
map.set(level, scripts);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
@@ -1,20 +1,11 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { ICategoryCollection } from './ICategoryCollection';
|
||||||
import { ICategory } from '@/domain/ICategory';
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
export interface IApplication {
|
export interface IApplication {
|
||||||
readonly name: string;
|
readonly info: IProjectInformation;
|
||||||
readonly repositoryUrl: string;
|
readonly collections: readonly ICategoryCollection[];
|
||||||
readonly version: string;
|
|
||||||
readonly totalScripts: number;
|
|
||||||
readonly totalCategories: number;
|
|
||||||
readonly actions: ReadonlyArray<ICategory>;
|
|
||||||
|
|
||||||
getRecommendedScripts(): ReadonlyArray<IScript>;
|
getSupportedOsList(): OperatingSystem[];
|
||||||
findCategory(categoryId: number): ICategory | undefined;
|
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
|
||||||
findScript(scriptId: string): IScript | undefined;
|
|
||||||
getAllScripts(): ReadonlyArray<IScript>;
|
|
||||||
getAllCategories(): ReadonlyArray<ICategory>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export { IScript } from '@/domain/IScript';
|
|
||||||
export { ICategory } from '@/domain/ICategory';
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface ICategory extends IEntity<number>, IDocumentable {
|
|||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts?: ReadonlyArray<IScript>;
|
||||||
|
includes(script: IScript): boolean;
|
||||||
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
19
src/domain/ICategoryCollection.ts
Normal file
19
src/domain/ICategoryCollection.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
|
||||||
|
export interface ICategoryCollection {
|
||||||
|
readonly scripting: IScriptingDefinition;
|
||||||
|
readonly os: OperatingSystem;
|
||||||
|
readonly totalScripts: number;
|
||||||
|
readonly totalCategories: number;
|
||||||
|
readonly actions: ReadonlyArray<ICategory>;
|
||||||
|
|
||||||
|
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||||
|
findCategory(categoryId: number): ICategory | undefined;
|
||||||
|
findScript(scriptId: string): IScript | undefined;
|
||||||
|
getAllScripts(): ReadonlyArray<IScript>;
|
||||||
|
getAllCategories(): ReadonlyArray<ICategory>;
|
||||||
|
}
|
||||||
11
src/domain/IProjectInformation.ts
Normal file
11
src/domain/IProjectInformation.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
export interface IProjectInformation {
|
||||||
|
readonly name: string;
|
||||||
|
readonly version: string;
|
||||||
|
readonly repositoryUrl: string;
|
||||||
|
readonly homepage: string;
|
||||||
|
readonly feedbackUrl: string;
|
||||||
|
readonly releaseUrl: string;
|
||||||
|
readonly repositoryWebUrl: string;
|
||||||
|
getDownloadUrl(os: OperatingSystem): string;
|
||||||
|
}
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { IDocumentable } from './IDocumentable';
|
import { IDocumentable } from './IDocumentable';
|
||||||
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
|
import { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
export interface IScript extends IEntity<string>, IDocumentable {
|
export interface IScript extends IEntity<string>, IDocumentable {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly isRecommended: boolean;
|
readonly level?: RecommendationLevel;
|
||||||
readonly documentationUrls: ReadonlyArray<string>;
|
readonly documentationUrls: ReadonlyArray<string>;
|
||||||
readonly code: string;
|
readonly code: IScriptCode;
|
||||||
readonly revertCode: string;
|
|
||||||
canRevert(): boolean;
|
canRevert(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
4
src/domain/IScriptCode.ts
Normal file
4
src/domain/IScriptCode.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IScriptCode {
|
||||||
|
readonly execute: string;
|
||||||
|
readonly revert: string;
|
||||||
|
}
|
||||||
8
src/domain/IScriptingDefinition.ts
Normal file
8
src/domain/IScriptingDefinition.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { ScriptingLanguage } from './ScriptingLanguage';
|
||||||
|
|
||||||
|
export interface IScriptingDefinition {
|
||||||
|
readonly fileExtension: string;
|
||||||
|
readonly language: ScriptingLanguage;
|
||||||
|
readonly startCode: string;
|
||||||
|
readonly endCode: string;
|
||||||
|
}
|
||||||
55
src/domain/ProjectInformation.ts
Normal file
55
src/domain/ProjectInformation.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { IProjectInformation } from './IProjectInformation';
|
||||||
|
import { OperatingSystem } from './OperatingSystem';
|
||||||
|
|
||||||
|
export class ProjectInformation implements IProjectInformation {
|
||||||
|
public readonly repositoryWebUrl: string;
|
||||||
|
constructor(
|
||||||
|
public readonly name: string,
|
||||||
|
public readonly version: string,
|
||||||
|
public readonly repositoryUrl: string,
|
||||||
|
public readonly homepage: string,
|
||||||
|
) {
|
||||||
|
if (!name) {
|
||||||
|
throw new Error('name is undefined');
|
||||||
|
}
|
||||||
|
if (!version || +version <= 0) {
|
||||||
|
throw new Error('version should be higher than zero');
|
||||||
|
}
|
||||||
|
if (!repositoryUrl) {
|
||||||
|
throw new Error('repositoryUrl is undefined');
|
||||||
|
}
|
||||||
|
if (!homepage) {
|
||||||
|
throw new Error('homepage is undefined');
|
||||||
|
}
|
||||||
|
this.repositoryWebUrl = getWebUrl(this.repositoryUrl);
|
||||||
|
}
|
||||||
|
public getDownloadUrl(os: OperatingSystem): string {
|
||||||
|
return `${this.repositoryWebUrl}/releases/download/${this.version}/${getFileName(os, this.version)}`;
|
||||||
|
}
|
||||||
|
public get feedbackUrl(): string {
|
||||||
|
return `${this.repositoryWebUrl}/issues`;
|
||||||
|
}
|
||||||
|
public get releaseUrl(): string {
|
||||||
|
return `${this.repositoryWebUrl}/releases/tag/${this.version}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWebUrl(gitUrl: string) {
|
||||||
|
if (gitUrl.endsWith('.git')) {
|
||||||
|
return gitUrl.substring(0, gitUrl.length - 4);
|
||||||
|
}
|
||||||
|
return gitUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFileName(os: OperatingSystem, version: string): string {
|
||||||
|
switch (os) {
|
||||||
|
case OperatingSystem.Linux:
|
||||||
|
return `privacy.sexy-${version}.AppImage`;
|
||||||
|
case OperatingSystem.macOS:
|
||||||
|
return `privacy.sexy-${version}.dmg`;
|
||||||
|
case OperatingSystem.Windows:
|
||||||
|
return `privacy.sexy-Setup-${version}.exe`;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/domain/RecommendationLevel.ts
Normal file
4
src/domain/RecommendationLevel.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum RecommendationLevel {
|
||||||
|
Standard = 0,
|
||||||
|
Strict = 1,
|
||||||
|
}
|
||||||
@@ -1,60 +1,27 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
|
import { RecommendationLevel } from './RecommendationLevel';
|
||||||
|
import { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
export class Script extends BaseEntity<string> implements IScript {
|
export class Script extends BaseEntity<string> implements IScript {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly code: string,
|
public readonly code: IScriptCode,
|
||||||
public readonly revertCode: string,
|
|
||||||
public readonly documentationUrls: ReadonlyArray<string>,
|
public readonly documentationUrls: ReadonlyArray<string>,
|
||||||
public readonly isRecommended: boolean) {
|
public readonly level?: RecommendationLevel) {
|
||||||
super(name);
|
super(name);
|
||||||
validateCode(name, code);
|
if (!code) {
|
||||||
if (revertCode) {
|
throw new Error(`undefined code (script: ${name})`);
|
||||||
validateCode(name, revertCode);
|
|
||||||
if (code === revertCode) {
|
|
||||||
throw new Error(`${name}: Code itself and its reverting code cannot be the same`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
public canRevert(): boolean {
|
public canRevert(): boolean {
|
||||||
return Boolean(this.revertCode);
|
return Boolean(this.code.revert);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(name: string, code: string): void {
|
function validateLevel(level?: RecommendationLevel) {
|
||||||
if (!code || code.length === 0) {
|
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||||
throw new Error(`Code of ${name} is empty or null`);
|
throw new Error(`invalid level: ${level}`);
|
||||||
}
|
|
||||||
ensureCodeHasUniqueLines(name, code);
|
|
||||||
ensureNoEmptyLines(name, code);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoEmptyLines(name: string, code: string): void {
|
|
||||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
|
||||||
throw Error(`Script has empty lines "${name}"`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function mayBeUniqueLine(codeLine: string): boolean {
|
|
||||||
const trimmed = codeLine.trim();
|
|
||||||
if (trimmed === ')' || trimmed === '(') { // "(" and ")" are used often in batch code
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (codeLine.startsWith(':: ') || codeLine.startsWith('REM ')) { // Is comment?
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureCodeHasUniqueLines(name: string, code: string): void {
|
|
||||||
const lines = code.split('\n')
|
|
||||||
.filter((line) => mayBeUniqueLine(line));
|
|
||||||
if (lines.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
|
||||||
if (duplicateLines.length !== 0) {
|
|
||||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
61
src/domain/ScriptCode.ts
Normal file
61
src/domain/ScriptCode.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
import { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
|
export class ScriptCode implements IScriptCode {
|
||||||
|
constructor(
|
||||||
|
public readonly execute: string,
|
||||||
|
public readonly revert: string,
|
||||||
|
scriptName: string,
|
||||||
|
syntax: ILanguageSyntax) {
|
||||||
|
if (!scriptName) { throw new Error('script name is undefined'); }
|
||||||
|
if (!syntax) { throw new Error('syntax is undefined'); }
|
||||||
|
validateCode(scriptName, execute, syntax);
|
||||||
|
if (revert) {
|
||||||
|
scriptName = `${scriptName} (revert)`;
|
||||||
|
validateCode(scriptName, revert, syntax);
|
||||||
|
if (execute === revert) {
|
||||||
|
throw new Error(`${scriptName}: Code itself and its reverting code cannot be the same`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILanguageSyntax {
|
||||||
|
readonly commentDelimiters: string[];
|
||||||
|
readonly commonCodeParts: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCode(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||||
|
if (!code || code.length === 0) {
|
||||||
|
throw new Error(`code of ${name} is empty or undefined`);
|
||||||
|
}
|
||||||
|
ensureNoEmptyLines(name, code);
|
||||||
|
ensureCodeHasUniqueLines(name, code, syntax);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureNoEmptyLines(name: string, code: string): void {
|
||||||
|
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||||
|
throw Error(`script has empty lines "${name}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCodeHasUniqueLines(name: string, code: string, syntax: ILanguageSyntax): void {
|
||||||
|
const lines = code.split('\n')
|
||||||
|
.filter((line) => !shouldIgnoreLine(line, syntax));
|
||||||
|
if (lines.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const duplicateLines = lines.filter((e, i, a) => a.indexOf(e) !== i);
|
||||||
|
if (duplicateLines.length !== 0) {
|
||||||
|
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldIgnoreLine(codeLine: string, syntax: ILanguageSyntax): boolean {
|
||||||
|
codeLine = codeLine.toLowerCase();
|
||||||
|
const isCommentLine = () => syntax.commentDelimiters.some((delimiter) => codeLine.startsWith(delimiter));
|
||||||
|
const consistsOfFrequentCommands = () => {
|
||||||
|
const trimmed = codeLine.trim().split(' ');
|
||||||
|
return trimmed.every((part) => syntax.commonCodeParts.includes(part));
|
||||||
|
};
|
||||||
|
return isCommentLine() || consistsOfFrequentCommands();
|
||||||
|
}
|
||||||
32
src/domain/ScriptingDefinition.ts
Normal file
32
src/domain/ScriptingDefinition.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { ScriptingLanguage } from './ScriptingLanguage';
|
||||||
|
import { IScriptingDefinition } from './IScriptingDefinition';
|
||||||
|
|
||||||
|
export class ScriptingDefinition implements IScriptingDefinition {
|
||||||
|
public readonly fileExtension: string;
|
||||||
|
constructor(
|
||||||
|
public readonly language: ScriptingLanguage,
|
||||||
|
public readonly startCode: string,
|
||||||
|
public readonly endCode: string,
|
||||||
|
) {
|
||||||
|
this.fileExtension = findExtension(language);
|
||||||
|
validateCode(startCode, 'start code');
|
||||||
|
validateCode(endCode, 'end code');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findExtension(language: ScriptingLanguage): string {
|
||||||
|
switch (language) {
|
||||||
|
case ScriptingLanguage.shellscript:
|
||||||
|
return 'sh';
|
||||||
|
case ScriptingLanguage.batchfile:
|
||||||
|
return 'bat';
|
||||||
|
default:
|
||||||
|
throw new Error(`unsupported language: ${language}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateCode(code: string, name: string) {
|
||||||
|
if (!code) {
|
||||||
|
throw new Error(`undefined ${name}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
4
src/domain/ScriptingLanguage.ts
Normal file
4
src/domain/ScriptingLanguage.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum ScriptingLanguage {
|
||||||
|
batchfile = 0,
|
||||||
|
shellscript = 1,
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
|
import { IEventSubscription } from './ISubscription';
|
||||||
export interface ISignal<T> {
|
export interface ISignal<T> {
|
||||||
on(handler: (data: T) => void): void;
|
on(handler: EventHandler<T>): IEventSubscription;
|
||||||
off(handler: (data: T) => void): void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventHandler<T> = (data: T) => void;
|
||||||
|
|||||||
3
src/infrastructure/Events/ISubscription.ts
Normal file
3
src/infrastructure/Events/ISubscription.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export interface IEventSubscription {
|
||||||
|
unsubscribe(): void;
|
||||||
|
}
|
||||||
@@ -1,18 +1,28 @@
|
|||||||
import { ISignal } from './ISignal';
|
import { EventHandler, ISignal } from './ISignal';
|
||||||
export { ISignal };
|
import { IEventSubscription } from './ISubscription';
|
||||||
|
|
||||||
export class Signal<T> implements ISignal<T> {
|
export class Signal<T> implements ISignal<T> {
|
||||||
private handlers: Array<(data: T) => void> = [];
|
private handlers = new Map<number, EventHandler<T>>();
|
||||||
|
|
||||||
public on(handler: (data: T) => void): void {
|
public on(handler: EventHandler<T>): IEventSubscription {
|
||||||
this.handlers.push(handler);
|
const id = this.getUniqueEventHandlerId();
|
||||||
}
|
this.handlers.set(id, handler);
|
||||||
|
return {
|
||||||
public off(handler: (data: T) => void): void {
|
unsubscribe: () => this.handlers.delete(id),
|
||||||
this.handlers = this.handlers.filter((h) => h !== handler);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public notify(data: T) {
|
public notify(data: T) {
|
||||||
this.handlers.slice(0).forEach((h) => h(data));
|
for (const handler of Array.from(this.handlers.values())) {
|
||||||
|
handler(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private getUniqueEventHandlerId(): number {
|
||||||
|
const id = Math.random();
|
||||||
|
if (this.handlers.has(id)) {
|
||||||
|
return this.getUniqueEventHandlerId();
|
||||||
|
}
|
||||||
|
return id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import fileSaver from 'file-saver';
|
|||||||
|
|
||||||
export enum FileType {
|
export enum FileType {
|
||||||
BatchFile,
|
BatchFile,
|
||||||
|
ShellScript,
|
||||||
}
|
}
|
||||||
export class SaveFileDialog {
|
export class SaveFileDialog {
|
||||||
public static saveFile(text: string, fileName: string, type: FileType): void {
|
public static saveFile(text: string, fileName: string, type: FileType): void {
|
||||||
@@ -11,7 +12,8 @@ export class SaveFileDialog {
|
|||||||
private static readonly mimeTypes = new Map<FileType, string>([
|
private static readonly mimeTypes = new Map<FileType, string>([
|
||||||
// Some browsers (including firefox + IE) require right mime type
|
// Some browsers (including firefox + IE) require right mime type
|
||||||
// otherwise they ignore extension and save the file as text.
|
// otherwise they ignore extension and save the file as text.
|
||||||
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
|
[ FileType.BatchFile, 'application/bat' ], // https://en.wikipedia.org/wiki/Batch_file
|
||||||
|
[ FileType.ShellScript, 'text/x-shellscript' ], // https://de.wikipedia.org/wiki/Shellskript#MIME-Typ
|
||||||
]);
|
]);
|
||||||
|
|
||||||
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
private static saveBlob(file: BlobPart, fileType: string, fileName: string): void {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import App from './App.vue';
|
import App from './App.vue';
|
||||||
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
|
import { ApplicationBootstrapper } from './presentation/Bootstrapping/ApplicationBootstrapper';
|
||||||
|
import 'core-js/fn/array/flat-map'; // Here until Vue 3 & CLI v4 https://github.com/vuejs/vue-cli/issues/3834
|
||||||
|
|
||||||
new ApplicationBootstrapper()
|
new ApplicationBootstrapper()
|
||||||
.bootstrap(Vue);
|
.bootstrap(Vue);
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
|
|||||||
/** REGULAR ICONS (PREFIX: far) */
|
/** REGULAR ICONS (PREFIX: far) */
|
||||||
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
import { faFolderOpen, faFolder, faSmile } from '@fortawesome/free-regular-svg-icons';
|
||||||
/** SOLID ICONS (PREFIX: fas (default)) */
|
/** SOLID ICONS (PREFIX: fas (default)) */
|
||||||
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop, faTag, faGlobe } from '@fortawesome/free-solid-svg-icons';
|
import { faTimes, faFileDownload, faCopy, faSearch, faInfoCircle, faUserSecret, faDesktop,
|
||||||
|
faTag, faGlobe, faSave, faBatteryFull, faBatteryHalf } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
|
||||||
|
|
||||||
export class IconBootstrapper implements IVueBootstrapper {
|
export class IconBootstrapper implements IVueBootstrapper {
|
||||||
@@ -21,9 +22,10 @@ export class IconBootstrapper implements IVueBootstrapper {
|
|||||||
faFolderOpen,
|
faFolderOpen,
|
||||||
faFolder,
|
faFolder,
|
||||||
faTimes,
|
faTimes,
|
||||||
faFileDownload,
|
faFileDownload, faSave,
|
||||||
faCopy,
|
faCopy,
|
||||||
faSearch,
|
faSearch,
|
||||||
|
faBatteryFull, faBatteryHalf,
|
||||||
faInfoCircle);
|
faInfoCircle);
|
||||||
vue.component('font-awesome-icon', FontAwesomeIcon);
|
vue.component('font-awesome-icon', FontAwesomeIcon);
|
||||||
}
|
}
|
||||||
|
|||||||
55
src/presentation/CodeButtons/Code.vue
Normal file
55
src/presentation/CodeButtons/Code.vue
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<template>
|
||||||
|
<span class="code-wrapper">
|
||||||
|
<span class="dollar">$</span>
|
||||||
|
<code><slot></slot></code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="copy-button"
|
||||||
|
:icon="['fas', 'copy']"
|
||||||
|
@click="copyCode"
|
||||||
|
v-tooltip.top-center="'Copy'"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Vue } from 'vue-property-decorator';
|
||||||
|
import { Clipboard } from '@/infrastructure/Clipboard';
|
||||||
|
|
||||||
|
@Component
|
||||||
|
export default class Code extends Vue {
|
||||||
|
public copyCode(): void {
|
||||||
|
const code = this.$slots.default[0].text;
|
||||||
|
Clipboard.copyText(code);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
|
||||||
|
.code-wrapper {
|
||||||
|
white-space: nowrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: $normal-font;
|
||||||
|
background-color: $slate;
|
||||||
|
color: $light-gray;
|
||||||
|
padding-left: 0.3rem;
|
||||||
|
padding-right: 0.3rem;
|
||||||
|
.dollar {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
.copy-button {
|
||||||
|
margin-left: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
code {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -8,11 +8,10 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Component, Prop, Emit } from 'vue-property-decorator';
|
import { Component, Prop, Emit, Vue } from 'vue-property-decorator';
|
||||||
import { StatefulVue } from './StatefulVue';
|
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
export default class IconButton extends StatefulVue {
|
export default class IconButton extends Vue {
|
||||||
@Prop() public text!: number;
|
@Prop() public text!: number;
|
||||||
@Prop() public iconPrefix!: string;
|
@Prop() public iconPrefix!: string;
|
||||||
@Prop() public iconName!: string;
|
@Prop() public iconName!: string;
|
||||||
@@ -21,7 +20,6 @@ export default class IconButton extends StatefulVue {
|
|||||||
public onClicked() {
|
public onClicked() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
119
src/presentation/CodeButtons/MacOsInstructions.vue
Normal file
119
src/presentation/CodeButtons/MacOsInstructions.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<template>
|
||||||
|
<div class="instructions">
|
||||||
|
<!-- <p>
|
||||||
|
Since you're using online version of {{ this.appName }}, you will need to do additional steps after downloading the file to execute your script on macOS:
|
||||||
|
</p> -->
|
||||||
|
<p>
|
||||||
|
<ol>
|
||||||
|
<li>
|
||||||
|
<span>Download the file</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="'You should be prompted to save the script file now, otherwise try to download it again'"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Open terminal</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="'Type Terminal into Spotlight or open from the Applications -> Utilities folder'"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Navigate to the folder where you downloaded the file e.g.:</span>
|
||||||
|
<div>
|
||||||
|
<Code>cd ~/Downloads</Code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="
|
||||||
|
'Press on Enter/Return key after running the command.<br/>' +
|
||||||
|
'If the file is not downloaded on Downloads folder, change `Downloads` to path where the file is downloaded.<br/>' +
|
||||||
|
'• `cd` will change the current folder.<br/>' +
|
||||||
|
'• `~` is the user home directory.'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Give the file execute permissions:</span>
|
||||||
|
<div>
|
||||||
|
<Code>chmod +x {{ this.fileName }}</Code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="
|
||||||
|
'Press on Enter/Return key after running the command.<br/>' +
|
||||||
|
'It will make the file executable.'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>Execute the file:</span>
|
||||||
|
<div>
|
||||||
|
<Code>./{{ this.fileName }}</Code>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="'Alternatively you can double click on the file'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<span>If asked, enter your administrator password</span>
|
||||||
|
<font-awesome-icon
|
||||||
|
class="explanation"
|
||||||
|
:icon="['fas', 'info-circle']"
|
||||||
|
v-tooltip.top-center="
|
||||||
|
'Press on Enter/Return key after typing your password<br/>' +
|
||||||
|
'Your password will not be shown by default.<br/>' +
|
||||||
|
'Administor privileges are required to configure OS.'"
|
||||||
|
/>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</p>
|
||||||
|
<!-- <p>
|
||||||
|
Or download the <a :href="this.macOsDownloadUrl">offline version</a> to run your scripts directly to skip these steps.
|
||||||
|
</p> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { Component, Prop } from 'vue-property-decorator';
|
||||||
|
import { StatefulVue } from '@/presentation/StatefulVue';
|
||||||
|
import Code from './Code.vue';
|
||||||
|
import { IApplication } from '@/domain/IApplication';
|
||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
components: {
|
||||||
|
Code,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
export default class MacOsInstructions extends StatefulVue {
|
||||||
|
@Prop() public fileName: string;
|
||||||
|
public appName = '';
|
||||||
|
public macOsDownloadUrl = '';
|
||||||
|
|
||||||
|
protected initialize(app: IApplication): void {
|
||||||
|
this.appName = app.info.name;
|
||||||
|
this.macOsDownloadUrl = app.info.getDownloadUrl(OperatingSystem.macOS);
|
||||||
|
}
|
||||||
|
protected handleCollectionState(): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import "@/presentation/styles/colors.scss";
|
||||||
|
@import "@/presentation/styles/fonts.scss";
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
.explanation {
|
||||||
|
margin-left: 0.5em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user