Compare commits
71 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e34e64449 | ||
|
|
ce4cfdd169 | ||
|
|
12b1f183f7 | ||
|
|
4212c7b9e0 | ||
|
|
7794846185 | ||
|
|
150e067039 | ||
|
|
f347fde0c8 | ||
|
|
ff3d5c4841 | ||
|
|
292362135d | ||
|
|
aae5434451 | ||
|
|
2390530d92 | ||
|
|
9ab3ff75b0 | ||
|
|
d25c4e8c81 | ||
|
|
4a7efa27c8 | ||
|
|
cec0b4b4f6 | ||
|
|
a1922c50c1 | ||
|
|
870120bc13 | ||
|
|
f38cf73485 | ||
|
|
9fd193e676 | ||
|
|
52a4730073 | ||
|
|
bc4879cfe9 | ||
|
|
dd71536316 | ||
|
|
a3343205b1 | ||
|
|
1d7cafc831 | ||
|
|
c75df1c8c1 | ||
|
|
ab25e0a066 | ||
|
|
813d820b85 | ||
|
|
66a56888a4 | ||
|
|
4ef16cea56 | ||
|
|
8c17396285 | ||
|
|
694bf1a74d | ||
|
|
0fc2ffc1ea | ||
|
|
d19dde603d | ||
|
|
23bac0fc76 | ||
|
|
e18907ca91 | ||
|
|
4e21f05031 | ||
|
|
8b224eefe7 | ||
|
|
f261ab4cd9 | ||
|
|
f584fabb50 | ||
|
|
2eed6f4afb | ||
|
|
1c9dc93246 | ||
|
|
cb144ae472 | ||
|
|
f3571abeaf | ||
|
|
b87b7aac7d | ||
|
|
ae172000a6 | ||
|
|
ffd647d152 | ||
|
|
4142d084f6 | ||
|
|
b7a20d9d41 | ||
|
|
b68711ef88 | ||
|
|
7b546c567c | ||
|
|
49f22f048f | ||
|
|
4472c2852e | ||
|
|
5d940b57ef | ||
|
|
bc7e1faa1c | ||
|
|
557cea3f48 | ||
|
|
4fb6302c67 | ||
|
|
59decd17e2 | ||
|
|
52fadcd617 | ||
|
|
8a5592f92b | ||
|
|
79183d6417 | ||
|
|
89243371fa | ||
|
|
4a9b430702 | ||
|
|
ac176935f5 | ||
|
|
abec9def07 | ||
|
|
b71ad797a3 | ||
|
|
ec34ac1124 | ||
|
|
840adf9429 | ||
|
|
5eff3a0488 | ||
|
|
5abf8ff216 | ||
|
|
e7218850ba | ||
|
|
adc2089887 |
57
.github/ISSUE_TEMPLATE/1-bug-report-scripts.md
vendored
@@ -1,57 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report (script bug or unexpected script behavior)
|
|
||||||
about: Create a bug report for generated scripts to help privacy.sexy improve
|
|
||||||
labels: bug
|
|
||||||
title: '[BUG]: '
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for reporting an issue with generated script(s).
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### OS
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Which OS are you using? What version of OS you were using?
|
|
||||||
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
|
||||||
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
|
||||||
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Reproduction steps
|
|
||||||
|
|
||||||
<!--
|
|
||||||
How can the bug be recreated?
|
|
||||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
|
||||||
E.g.
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Scripts
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, please attach the generated privacy.sexy file instead of copy pasting which becomes too long.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional information
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context about the problem here.
|
|
||||||
-->
|
|
||||||
114
.github/ISSUE_TEMPLATE/1-bug-report-scripts.yaml
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
name: "Bug Report: Script Issues"
|
||||||
|
description: 🐛 Report issues with generated scripts to enhance privacy.sexy
|
||||||
|
labels: [ 'bug' ]
|
||||||
|
title: '[Bug]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: A clear and concise description of what the bug is.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "After running the cleanup script, music playback stopped functioning."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: How can the bug be recreated?
|
||||||
|
description: |-
|
||||||
|
This is the most important information in the bug report.
|
||||||
|
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||||
|
placeholder: |-
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: |-
|
||||||
|
Please specify your operating system and its version.
|
||||||
|
|
||||||
|
- On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||||
|
- On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||||
|
- On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Windows 11 Pro 22H3"
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Script file
|
||||||
|
description: |-
|
||||||
|
If applicable, share the generated privacy.sexy file.
|
||||||
|
|
||||||
|
GitHub may restrict script file attachments.
|
||||||
|
Upload your script file to a service like [GitHub Gist](https://gist.github.com/) and share the link here.
|
||||||
|
|
||||||
|
If you used the desktop version to run the script, it is already stored on your system.
|
||||||
|
See the [documentation to locate it](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#secure-script-executionstorage).
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach script files by dragging them into this area.
|
||||||
|
placeholder: |-
|
||||||
|
Attach the script, or post GitHub Gist link.
|
||||||
|
For example: https://gist.github.com/privacysexy-forks/6d85ad8ca27acc8c6a5417d4af28c9b6.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: |-
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||||
|
placeholder: Attach screenshots here or link to image hosting.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
|
||||||
|
Helpful information includes:
|
||||||
|
|
||||||
|
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||||
|
- Terminal output
|
||||||
|
- Proposed solutions
|
||||||
|
- Other related context such as related issues, software behavior, etc.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
104
.github/ISSUE_TEMPLATE/2-bug-report-general.yaml
vendored
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
name: "Bug Report: General"
|
||||||
|
description: 🐛 Report general issues to enhance privacy.sexy
|
||||||
|
labels: [ 'bug' ]
|
||||||
|
title: '[Bug]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Description
|
||||||
|
description: Provide a clear and concise description of the issue.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "I cannot select any scripts."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Reproduction steps
|
||||||
|
description: |-
|
||||||
|
This is the most important information in the bug report.
|
||||||
|
Bugs that cannot be reproduced cannot be fixed or verified.
|
||||||
|
placeholder: |-
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Expected behavior
|
||||||
|
description: Describe what you expected to happen when the error occurred.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "I expected the settings menu to open smoothly without crashing.".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Screenshots
|
||||||
|
description: |-
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach screenshots by clicking this area to highlight it and then pasting them or dragging files in.
|
||||||
|
placeholder: >-
|
||||||
|
Attach screenshots here or link to image hosting.
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: privacy.sexy environment details
|
||||||
|
description: |-
|
||||||
|
If applicable, mention how you were using privacy.sexy when the bug occurred:
|
||||||
|
|
||||||
|
- Web (on which operating system and browser?)
|
||||||
|
- Or desktop (Windows, macOS, or Linux?)
|
||||||
|
placeholder: >-
|
||||||
|
For example: "The web version on Edge browser on Windows 11 23H2."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional context
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context about the problem here.
|
||||||
|
|
||||||
|
Helpful information includes:
|
||||||
|
|
||||||
|
- Application logs (desktop version only), see: [how to find application logs](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md#logging).
|
||||||
|
- Terminal output
|
||||||
|
- Proposed solutions
|
||||||
|
- Other related context such as related issues, software behavior, etc.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach log files by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Here are the logs I get from the privacy.sexy 0.13.2 desktop application: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
55
.github/ISSUE_TEMPLATE/2-bug-report-generic.md
vendored
@@ -1,55 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report (unrelated to generated scripts)
|
|
||||||
about: Create a bug report that's not related to generated scripts to help privacy.sexy improve
|
|
||||||
labels: bug
|
|
||||||
title: '[BUG]: '
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for reporting an issue.
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
As a small open source project with small community, it can sometimes take a long time for issues to be addressed so please be patient.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Reproduction steps
|
|
||||||
|
|
||||||
<!--
|
|
||||||
It's the most important information in the bug report. Bugs that cannot be reproduced cannot be fixed and verified.
|
|
||||||
Steps to reproduce the behavior:
|
|
||||||
1. Go to '...'
|
|
||||||
2. Click on '....'
|
|
||||||
3. Scroll down to '....'
|
|
||||||
4. See error
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Expected behavior
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of what you expected to happen.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add screenshots to help explain your problem.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Distribution
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, mention how you were using privacy.sexy when the bug was encountered:
|
|
||||||
- Web (on Desktop or mobile?)
|
|
||||||
- Or desktop (Windows, macOS or Linux?)
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional context
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context about the problem here.
|
|
||||||
-->
|
|
||||||
36
.github/ISSUE_TEMPLATE/3-feature-request.md
vendored
@@ -1,36 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for suggesting an idea to improve privacy better 🤗.
|
|
||||||
Please fill in as much of the template below as you're able.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Problem description
|
|
||||||
|
|
||||||
<!--
|
|
||||||
What are we trying to solve?
|
|
||||||
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
|
||||||
E.g. I'm always frustrated when [...]
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Proposed solution
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Describe the solution you'd like in a clear and concise manner.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Alternatives considered
|
|
||||||
|
|
||||||
<!--
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Additional information
|
|
||||||
|
|
||||||
<!--
|
|
||||||
If applicable, add any other context or screenshots about the feature request here.
|
|
||||||
-->
|
|
||||||
73
.github/ISSUE_TEMPLATE/3-suggestion-feature.yaml
vendored
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
name: "Suggestion: Feature"
|
||||||
|
description: 💡 Suggest new ideas to enhance privacy.sexy
|
||||||
|
labels: [ 'enhancement' ]
|
||||||
|
title: '[Feature]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Problem statement
|
||||||
|
description: |-
|
||||||
|
What are we trying to solve?
|
||||||
|
|
||||||
|
Please add a clear and concise description of the problem you are seeking to solve with this feature request.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Every time I use the app, I struggle with..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Proposed solution
|
||||||
|
description: |-
|
||||||
|
Describe the solution you'd like in a clear and concise manner.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "It would be great if the app could..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Alternatives considered
|
||||||
|
description: |-
|
||||||
|
Have you considered any alternative solutions or features?
|
||||||
|
Different perspectives can inspire new ideas.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "We could also solve it by...".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context or screenshots about the feature request here.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach files or screenshots by dragging them into this area.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Challenges can be ..., but I'm unsure about ..., here is some documentation about it: ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
---
|
|
||||||
name: New script suggestion
|
|
||||||
about: Suggest a new script for privacy.sexy
|
|
||||||
labels: enhancement
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Thank you for contributing to privacy.sexy! 🌟
|
|
||||||
For guidance, see our script guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md.
|
|
||||||
Consider submitting a PR for faster implementation: https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Operating system
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Specify the OS: Windows, macOS, or Linux.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Name
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a name for the script.
|
|
||||||
Naming conventions: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Code
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide or explain the code to execute when the script runs.
|
|
||||||
Code guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Revert code
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Include code to revert changes to the default state.
|
|
||||||
Leave blank for non-reversible scripts.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Category
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a category for the script.
|
|
||||||
If unsure, leave blank for maintainers to decide.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Recommendation level
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Suggest a recommendation level: STANDARD (non-breaking), STRICT (limits functionality), or NONE (for advanced users).
|
|
||||||
If unsure, leave blank for maintainers to decide.
|
|
||||||
-->
|
|
||||||
|
|
||||||
### Documentation/References
|
|
||||||
|
|
||||||
<!--
|
|
||||||
Provide any relevant documentation or references.
|
|
||||||
Prefer high-quality sources such as vendor documentation.
|
|
||||||
Documentation guidelines: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation.
|
|
||||||
-->
|
|
||||||
133
.github/ISSUE_TEMPLATE/4-suggestion-new-script.yaml
vendored
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
name: "Suggestion: New Script"
|
||||||
|
description: 💡 Suggest new scripts to enhance privacy.sexy
|
||||||
|
labels: [ 'enhancement' ]
|
||||||
|
title: '[New script]: '
|
||||||
|
body:
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
Thank you for contributing to privacy.sexy and guiding our direction! 🌟
|
||||||
|
Please complete as much of the form below as possible.
|
||||||
|
Your feedback is valuable, even if you can't provide all details.
|
||||||
|
|
||||||
|
For guidance, see our [script guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md).
|
||||||
|
Consider submitting a PR to get your script added more quickly: (see [CONTRIBUTING.md](https://github.com/undergroundwires/privacy.sexy/blob/master/CONTRIBUTING.md#extend-scripts))
|
||||||
|
-
|
||||||
|
type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Operating system
|
||||||
|
description: Which operating system will the new script configure?
|
||||||
|
options:
|
||||||
|
- macOS
|
||||||
|
- Windows
|
||||||
|
- Linux
|
||||||
|
- All of them
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Name of the script
|
||||||
|
description: |-
|
||||||
|
Suggest a name for the script that clearly describes its function.
|
||||||
|
|
||||||
|
See [script naming conventions](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#name) for best practices.
|
||||||
|
placeholder: E.g, "Disable error data submission"
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Documentation/References
|
||||||
|
description: |-
|
||||||
|
Provide any relevant documentation or references.
|
||||||
|
Prefer high-quality sources such as vendor documentation.
|
||||||
|
|
||||||
|
See [documentation guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#documentation) for best practices.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "This script will disable the error data submission, see https://microsoft.com/...".
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Code
|
||||||
|
description: |-
|
||||||
|
If possible, provide or explain the code that the script should execute.
|
||||||
|
|
||||||
|
See [script code guidelines](https://github.com/undergroundwires/privacy.sexy/blob/master/docs/script-guidelines.md#code).
|
||||||
|
placeholder: |-
|
||||||
|
For example: "Set registry key like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "1"`".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Revert code
|
||||||
|
description: |-
|
||||||
|
If applicable, provide revert code to restore the changes made by the script.
|
||||||
|
|
||||||
|
The revert code restores changes to their default state before script execution.
|
||||||
|
|
||||||
|
Leave blank for non-reversible scripts.
|
||||||
|
placeholder: |-
|
||||||
|
For example: "Revert to operating system default like this `reg add "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\DataCollection" /v "AllowTelemetry" /t "REG_DWORD" /d "0"`".
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Suggested category
|
||||||
|
description: |-
|
||||||
|
Suggest a category for the script.
|
||||||
|
|
||||||
|
If unsure, leave blank for maintainers to decide.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Privacy Cleanup > Clear system logs"
|
||||||
|
-
|
||||||
|
type: dropdown
|
||||||
|
attributes:
|
||||||
|
label: Recommendation level
|
||||||
|
description: |-
|
||||||
|
Suggest a recommendation level for the script:
|
||||||
|
|
||||||
|
- **Standard**: Recommended for most users without side-effects.
|
||||||
|
- **Strict**: Provides improved privacy at the cost of some functionality.
|
||||||
|
- **None**: For advanced users or specific needs.
|
||||||
|
|
||||||
|
If unsure, leave blank for maintainers to decide.
|
||||||
|
options:
|
||||||
|
- Standard
|
||||||
|
- Strict
|
||||||
|
- None (do not recommend)
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: textarea
|
||||||
|
attributes:
|
||||||
|
label: Additional information
|
||||||
|
description: |-
|
||||||
|
If applicable, add any other context or screenshots about the script request here.
|
||||||
|
|
||||||
|
> **💡 Tip:** You can attach additional documents or screenshots by dragging them into this area or pasting directly.
|
||||||
|
placeholder: >-
|
||||||
|
For example: "Challenges can be ..., but I am unsure about ..."
|
||||||
|
validations:
|
||||||
|
required: false
|
||||||
|
-
|
||||||
|
type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |-
|
||||||
|
---
|
||||||
|
|
||||||
|
**✉️ A friendly note from the maintainer:**
|
||||||
|
|
||||||
|
> [!NOTE]
|
||||||
|
> We are a small open-source project with a small community.
|
||||||
|
> It can sometimes take a long time for issues to be addressed, so please be patient.
|
||||||
|
> Consider [donating](https://undergroundwires.dev/donate) to keep privacy.sexy alive and improve support ❤️.
|
||||||
|
> But your issue will eventually get attention regardless.
|
||||||
|
> <p align="right">@undergroundwires</p>
|
||||||
|
|
||||||
|
---
|
||||||
6
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1 +1,7 @@
|
|||||||
|
# This file must be named `config.yml`. GitHub does not recognize the file if it is named `config.yaml`.
|
||||||
blank_issues_enabled: true
|
blank_issues_enabled: true
|
||||||
|
contact_links:
|
||||||
|
- name: Donate
|
||||||
|
url: https://undergroundwires.dev/donate/
|
||||||
|
about: ❤️ Donate to support the free software you love to keep it alive.
|
||||||
|
# A separate link for reporting vulnerabilities is not included here because GitHub generates it automatically.
|
||||||
|
|||||||
32
.github/actions/force-ipv4/README.md
vendored
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# force-ipv4
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This GitHub action enforces IPv4 for all outgoing network requests. It addresses connectivity issues encountered in GitHub runners, where IPv6 requests may lead to timeouts due to the lack of IPv6 support [1] [2].
|
||||||
|
|
||||||
|
## Background
|
||||||
|
|
||||||
|
Some applications attempt network connections over IPv6.
|
||||||
|
Such as requests made by Node's `fetch` API causes `UND_ERR_CONNECT_TIMEOUT` [3] [4] and similar issues [5].
|
||||||
|
This happens when the software cannot handle this such as by using Happy Eyeballs [6] [7].
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
To use this action in your GitHub workflow, add the following step before any job that requires network access:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Enforce IPv4 Connectivity
|
||||||
|
uses: ./.github/actions/force-ipv4
|
||||||
|
```
|
||||||
|
|
||||||
|
## Note
|
||||||
|
|
||||||
|
This action is a workaround addressing specific IPv6-related connectivity issues on GitHub runners and may not be necessary if GitHub's infrastructure evolves to fully support IPv6 in the future.
|
||||||
|
|
||||||
|
[1]: https://archive.ph/2024.03.28-185829/https://github.com/actions/runner/issues/3138 "Actions Runner fails on IPv6 only host · Issue #3138 · actions/runner · GitHub | github.com"
|
||||||
|
[2]: https://archive.ph/2024.03.28-185838/https://github.com/actions/runner-images/issues/668 "IPv6 on GitHub-hosted runners · Issue #668 · actions/runner-images · GitHub | github.com"
|
||||||
|
[3]: https://archive.ph/2024.03.28-185847/https://github.com/actions/runner/issues/3213 "GitHub runner cannot send `fetch` with `node`, failing with IPv6 DNS error `UND_ERR_CONNECT_TIMEOUT` · Issue #3213 · actions/runner · GitHub | github.com"
|
||||||
|
[4]: https://archive.ph/2024.03.28-185853/https://github.com/actions/runner-images/issues/9540 "Cannot send outbound requests using node fetch, failing with IPv6 DNS error UND_ERR_CONNECT_TIMEOUT · Issue #9540 · actions/runner-images · GitHub | github.com"
|
||||||
|
[5]: https://archive.today/2024.03.30-113315/https://github.com/nodejs/node/issues/40537 "\"localhost\" favours IPv6 in node v17, used to favour IPv4 · Issue #40537 · nodejs/node · GitHub"
|
||||||
|
[6]: https://archive.ph/2024.03.28-185900/https://github.com/nodejs/node/issues/41625 "Happy Eyeballs support (address IPv6 issues in Node 17) · Issue #41625 · nodejs/node · GitHub | github.com"
|
||||||
|
[7]: https://archive.ph/2024.03.28-185910/https://github.com/nodejs/undici/issues/1531 "fetch times out in under 5 seconds · Issue #1531 · nodejs/undici · GitHub | github.com"
|
||||||
12
.github/actions/force-ipv4/action.yml
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
inputs:
|
||||||
|
project-root:
|
||||||
|
required: false
|
||||||
|
default: '.'
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Run prefer IPv4 script
|
||||||
|
shell: bash
|
||||||
|
run: ./.github/actions/force-ipv4/force-ipv4.sh
|
||||||
|
working-directory: ${{ inputs.project-root }}
|
||||||
80
.github/actions/force-ipv4/force-ipv4.sh
vendored
Executable file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
main() {
|
||||||
|
if is_linux; then
|
||||||
|
echo 'Configuring Linux...'
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_linux # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
|
||||||
|
prefer_ipv4_on_linux # [DOES NOT WORK] It does not resolve the issue when run independently on GitHub runners without IPv6 support.
|
||||||
|
|
||||||
|
# Considered alternatives:
|
||||||
|
# - `sysctl` commands, and direct changes to `/proc/sys/net/` and `/etc/sysctl.conf` led to silent
|
||||||
|
# Node 18 exits (code: 13) when using `fetch`.
|
||||||
|
elif is_macos; then
|
||||||
|
echo 'Configuring macOS...'
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_macos # [WORKS] Resolves the issue when run independently on GitHub runners lacking IPv6 support.
|
||||||
|
disable_ipv6_on_macos # [WORKS INCONSISTENTLY] Resolves the issue inconsistently when run independently on GitHub runners without IPv6 support.
|
||||||
|
fi
|
||||||
|
echo "IPv4: $(curl --ipv4 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
|
||||||
|
echo "IPv6: $(curl --ipv6 --silent --max-time 15 --retry 3 --user-agent Mozilla https://api.ip.sb/geoip)"
|
||||||
|
}
|
||||||
|
|
||||||
|
is_linux() {
|
||||||
|
[[ "$(uname -s)" == "Linux" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
is_macos() {
|
||||||
|
[[ "$(uname -s)" == "Darwin" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_linux() {
|
||||||
|
install_warp_on_debian
|
||||||
|
configure_warp_doh_and_exclude_ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_warp_with_doh_and_ipv6_exclusion_on_macos() {
|
||||||
|
brew install cloudflare-warp
|
||||||
|
configure_warp_doh_and_exclude_ipv6
|
||||||
|
}
|
||||||
|
|
||||||
|
configure_warp_doh_and_exclude_ipv6() {
|
||||||
|
echo 'Beginning configuration of the Cloudflare WARP client with DNS-over-HTTPS and IPv6 exclusion...'
|
||||||
|
echo 'Initiating client registration with Cloudflare...'
|
||||||
|
warp-cli --accept-tos registration new
|
||||||
|
echo 'Configuring WARP to operate in DNS-over-HTTPS mode (warp+doh)...'
|
||||||
|
warp-cli --accept-tos mode warp+doh
|
||||||
|
echo 'Excluding IPv6 traffic from WARP by configuring it as a split tunnel...'
|
||||||
|
warp-cli --accept-tos add-excluded-route '::/0' # Exclude IPv6, forcing IPv4 resolution
|
||||||
|
# `tunnel ip add` does not work with IP ranges, see https://community.cloudflare.com/t/cant-cidr-for-split-tunnling/630834
|
||||||
|
echo 'Establishing WARP connection...'
|
||||||
|
warp-cli --accept-tos connect
|
||||||
|
}
|
||||||
|
|
||||||
|
install_warp_on_debian() {
|
||||||
|
curl -fsSL https://pkg.cloudflareclient.com/pubkey.gpg | sudo gpg --yes --dearmor --output /usr/share/keyrings/cloudflare-warp-archive-keyring.gpg
|
||||||
|
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/cloudflare-warp-archive-keyring.gpg] https://pkg.cloudflareclient.com/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflare-client.list
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y cloudflare-warp
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_ipv6_on_macos() {
|
||||||
|
networksetup -listallnetworkservices \
|
||||||
|
| tail -n +2 \
|
||||||
|
| while IFS= read -r interface; do
|
||||||
|
echo "Disabling IPv6 on: $interface..."
|
||||||
|
networksetup -setv6off "$interface"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
prefer_ipv4_on_linux() {
|
||||||
|
local -r gai_config_file_path='/etc/gai.conf'
|
||||||
|
if [ ! -f "$gai_config_file_path" ]; then
|
||||||
|
echo "Creating $gai_config_file_path since it doesn't exist..."
|
||||||
|
touch "$gai_config_file_path"
|
||||||
|
fi
|
||||||
|
echo "precedence ::ffff:0:0/96 100" | sudo tee -a "$gai_config_file_path" > /dev/null
|
||||||
|
echo "Configuration complete."
|
||||||
|
}
|
||||||
|
|
||||||
|
main
|
||||||
3
.github/actions/setup-node/action.yml
vendored
@@ -5,4 +5,5 @@ runs:
|
|||||||
name: Setup node
|
name: Setup node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 20.x
|
||||||
|
# check-latest: true # Newest versions can potentially have undiscovered bugs or regressions
|
||||||
|
|||||||
17
.github/workflows/checks.build.yaml
vendored
@@ -72,16 +72,19 @@ jobs:
|
|||||||
build-docker:
|
build-docker:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu ] # Windows runners do not support Linux containers
|
os:
|
||||||
|
- macos-13 # Downgraded due to lack of nested virtualization support in ARM-based runners (See: actions/runner-images#9460, actions/runner-images#9741, abiosoft/colima#1023)
|
||||||
|
- ubuntu-latest
|
||||||
|
# - windows-latest # Windows runners do not support Linux containers
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
-
|
-
|
||||||
name: Install Docker on macOS
|
name: Install Docker on macOS
|
||||||
if: matrix.os == 'macos' # macOS runner is missing Docker
|
if: contains(matrix.os, 'macos') # macOS runner is missing Docker
|
||||||
run: |-
|
run: |-
|
||||||
# Install Docker
|
# Install Docker
|
||||||
brew install docker
|
brew install docker
|
||||||
@@ -95,6 +98,12 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run Docker image on port 8080
|
name: Run Docker image on port 8080
|
||||||
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
|
run: docker run -d -p 8080:80 --rm --name privacy.sexy undergroundwires/privacy.sexy:latest
|
||||||
|
-
|
||||||
|
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
|
||||||
|
uses: ./.github/actions/force-ipv4
|
||||||
-
|
-
|
||||||
name: Check server is up and returns HTTP 200
|
name: Check server is up and returns HTTP 200
|
||||||
run: node ./scripts/verify-web-server-status.js --url http://localhost:8080
|
run: >-
|
||||||
|
node ./scripts/verify-web-server-status.js \
|
||||||
|
--url http://localhost:8080 \
|
||||||
|
--max-retries ${{ matrix.os == 'macos' && '90' || '30' }}
|
||||||
|
|||||||
@@ -9,9 +9,13 @@ jobs:
|
|||||||
run-check:
|
run-check:
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ macos, ubuntu, windows ]
|
os:
|
||||||
|
- macos-latest # Apple silicon (ARM64)
|
||||||
|
- macos-13 # Intel-based (x86-64)
|
||||||
|
- ubuntu-latest
|
||||||
|
- windows-latest
|
||||||
fail-fast: false # Allows to see results from other combinations
|
fail-fast: false # Allows to see results from other combinations
|
||||||
runs-on: ${{ matrix.os }}-latest
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
@@ -24,7 +28,7 @@ jobs:
|
|||||||
uses: ./.github/actions/npm-install-dependencies
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
-
|
-
|
||||||
name: Configure Ubuntu
|
name: Configure Ubuntu
|
||||||
if: matrix.os == 'ubuntu'
|
if: contains(matrix.os, 'ubuntu') # macOS runner is missing Docker
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |-
|
run: |-
|
||||||
sudo apt update
|
sudo apt update
|
||||||
|
|||||||
8
.github/workflows/checks.external-urls.yaml
vendored
@@ -1,6 +1,7 @@
|
|||||||
name: checks.external-urls
|
name: checks.external-urls
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
push:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
- cron: '0 0 * * 0' # at 00:00 on every Sunday
|
||||||
|
|
||||||
@@ -17,6 +18,13 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Install dependencies
|
name: Install dependencies
|
||||||
uses: ./.github/actions/npm-install-dependencies
|
uses: ./.github/actions/npm-install-dependencies
|
||||||
|
-
|
||||||
|
name: Enforce IPv4 Connectivity # Used due to GitHub runners' lack of IPv6 support, preventing request timeouts.
|
||||||
|
uses: ./.github/actions/force-ipv4
|
||||||
-
|
-
|
||||||
name: Test
|
name: Test
|
||||||
run: npm run check:external-urls
|
run: npm run check:external-urls
|
||||||
|
env:
|
||||||
|
RANDOMIZED_URL_CHECK_LIMIT: "${{ github.event_name == 'push' && '100' || '3000' }}"
|
||||||
|
# - Scheduled checks has high limit for thorough testing.
|
||||||
|
# - For push events, triggered by code changes, the amount of URLs are limited to provide quick feedback.
|
||||||
|
|||||||
50
.github/workflows/checks.quality.yaml
vendored
@@ -1,10 +1,10 @@
|
|||||||
name: quality-checks
|
name: checks.quality
|
||||||
|
|
||||||
on: [ push, pull_request ]
|
on: [ push, pull_request ]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
lint-command:
|
lint-command:
|
||||||
@@ -28,3 +28,49 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Lint
|
name: Lint
|
||||||
run: ${{ matrix.lint-command }}
|
run: ${{ matrix.lint-command }}
|
||||||
|
|
||||||
|
todo-check:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Scan latest commit for TODO comments
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
readonly todo_comment_search_pattern='TODO'':' # Define search pattern in parts to prevent IDE from flagging this script line as a TODO item
|
||||||
|
if git grep "$todo_comment_search_pattern" HEAD; then
|
||||||
|
echo 'TODO comments found in the latest commit.'
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo 'No TODO comments found in the latest commit.'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
pylint:
|
||||||
|
runs-on: ${{ matrix.os }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos, ubuntu, windows ]
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Setup node
|
||||||
|
uses: ./.github/actions/setup-node
|
||||||
|
-
|
||||||
|
name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
-
|
||||||
|
name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install pylint
|
||||||
|
-
|
||||||
|
name: Analyzing the code with pylint
|
||||||
|
run: npm run lint:pylint
|
||||||
|
|||||||
32
.github/workflows/checks.scripts.yaml
vendored
@@ -15,6 +15,10 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Checkout
|
name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Install ImageMagick on macOS
|
||||||
|
if: matrix.os == 'macos'
|
||||||
|
run: brew install imagemagick
|
||||||
-
|
-
|
||||||
name: Setup node
|
name: Setup node
|
||||||
uses: ./.github/actions/setup-node
|
uses: ./.github/actions/setup-node
|
||||||
@@ -53,3 +57,31 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run install-deps
|
name: Run install-deps
|
||||||
run: ${{ matrix.install-command }}
|
run: ${{ matrix.install-command }}
|
||||||
|
|
||||||
|
configure-vscode:
|
||||||
|
runs-on: ${{ matrix.os.name }}-latest
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os:
|
||||||
|
- name: macos
|
||||||
|
install-vscode-command: brew install --cask visual-studio-code
|
||||||
|
- name: ubuntu
|
||||||
|
install-vscode-command: sudo snap install code --classic
|
||||||
|
- name: windows
|
||||||
|
install-vscode-command: choco install vscode
|
||||||
|
fail-fast: false # Still interested to see results from other combinations
|
||||||
|
steps:
|
||||||
|
-
|
||||||
|
name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
-
|
||||||
|
name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.x'
|
||||||
|
-
|
||||||
|
name: Install VSCode
|
||||||
|
run: ${{ matrix.os.install-vscode-command }}
|
||||||
|
-
|
||||||
|
name: Configure VSCode
|
||||||
|
run: python3 ./scripts/configure_vscode.py
|
||||||
|
|||||||
71
CHANGELOG.md
@@ -1,5 +1,76 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.13.3 (2024-05-11)
|
||||||
|
|
||||||
|
* win: organize and document network disablement | [2eed6f4](https://github.com/undergroundwires/privacy.sexy/commit/2eed6f4afb6cf85fdc1d6acb808f82405a35cafd)
|
||||||
|
* win: improve disabling SMBv1 protocol | [f584fab](https://github.com/undergroundwires/privacy.sexy/commit/f584fabb50c7de70ba43751d721af94d8fa2fa8a)
|
||||||
|
* win: improve disabling insecure renegotiations | [f261ab4](https://github.com/undergroundwires/privacy.sexy/commit/f261ab4cd9a53e31325e5c6da9129542971fe84b)
|
||||||
|
* win: doc, improve, encourage cipher disabling | [8b224ee](https://github.com/undergroundwires/privacy.sexy/commit/8b224eefe71be6a556a1085d8fe20dbd4b889430)
|
||||||
|
* ci/cd: add check for TODO comments | [4e21f05](https://github.com/undergroundwires/privacy.sexy/commit/4e21f05031d6cc90cda684bd598bec4735f8103b)
|
||||||
|
* win: improve 'Snipping Tool' removal #343 | [e18907c](https://github.com/undergroundwires/privacy.sexy/commit/e18907ca91e483255b44d14d7d923d7eef92afbd)
|
||||||
|
* ci/cd: lint Python scripts using `pylint` | [23bac0f](https://github.com/undergroundwires/privacy.sexy/commit/23bac0fc76ad697abb34f3fb327df5cdeb40286a)
|
||||||
|
* win: improve disabling insecure hashes #131 | [d19dde6](https://github.com/undergroundwires/privacy.sexy/commit/d19dde603ddac47022ee2e0ea865d53857560c26)
|
||||||
|
* Add system requirements documentation #134 | [0fc2ffc](https://github.com/undergroundwires/privacy.sexy/commit/0fc2ffc1ea36a9248c6a92da85a29f7b04b33796)
|
||||||
|
* win, linux, mac: fix various typos #349 | [694bf1a](https://github.com/undergroundwires/privacy.sexy/commit/694bf1a74d935531d7cd46891823af1fa58c3c8c)
|
||||||
|
* Fix script cancellation with new dialog on Linux | [8c17396](https://github.com/undergroundwires/privacy.sexy/commit/8c173962857a39dc0c9e5886cb2af4937e6618e7)
|
||||||
|
* win: improve disabling protocols | [4ef16ce](https://github.com/undergroundwires/privacy.sexy/commit/4ef16cea56789120cd041412d86b5577cccf0725)
|
||||||
|
* win: fix Copilot by excluding `r.bing.com` #329 | [66a5688](https://github.com/undergroundwires/privacy.sexy/commit/66a56888a4b3ead1a6bfef0feffa0218535701fe)
|
||||||
|
* Fix blank window on load on desktop version #348 | [813d820](https://github.com/undergroundwires/privacy.sexy/commit/813d820b85e1b623c50f8e0325ad372bf2f344f9)
|
||||||
|
* Improve desktop icon quality and generation | [ab25e0a](https://github.com/undergroundwires/privacy.sexy/commit/ab25e0a066be14ea979dafd0f80e1091bd5d33f8)
|
||||||
|
* win: improve enabling secure connections #175 | [c75df1c](https://github.com/undergroundwires/privacy.sexy/commit/c75df1c8c1151b64cbf014383dea0b748a8c78b3)
|
||||||
|
* Fix VSCode script issues with added CI/CD tests | [1d7cafc](https://github.com/undergroundwires/privacy.sexy/commit/1d7cafc831dcc339a10646794410dad7096bfe60)
|
||||||
|
* Fix win execution with whitespace in username #351 | [a334320](https://github.com/undergroundwires/privacy.sexy/commit/a3343205b1196d5a81fd3cee2ae661ce871a7bef)
|
||||||
|
* Fix misaligned tooltip positions in modal dialogs | [dd71536](https://github.com/undergroundwires/privacy.sexy/commit/dd71536316ec819caeb418b8635d544ac80e58ad)
|
||||||
|
* Fix Chromium scrollbar-induced layout shifts | [bc4879c](https://github.com/undergroundwires/privacy.sexy/commit/bc4879cfe97becac3c54f6b40780a89464d3b772)
|
||||||
|
* ci/cd: remove `check-latest` from `setup-node` | [52a4730](https://github.com/undergroundwires/privacy.sexy/commit/52a4730073b8ebfb2ce9d530b44e4a179f5849fe)
|
||||||
|
* win: categorize and rename network security #131 | [9fd193e](https://github.com/undergroundwires/privacy.sexy/commit/9fd193e676f1f0646898f5130fbfaaf25050b2e3)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.2...0.13.3)
|
||||||
|
|
||||||
|
## 0.13.2 (2024-04-15)
|
||||||
|
|
||||||
|
* Update documentation for `logo-update.js` script | [4a9b430](https://github.com/undergroundwires/privacy.sexy/commit/4a9b430702bc6082426b50ecc3a06362b5720796)
|
||||||
|
* win: improve and document removing Phone apps #279 | [8924337](https://github.com/undergroundwires/privacy.sexy/commit/89243371faa5d6aef5fce52b0d54a442143cdd39)
|
||||||
|
* Fix bottom gap in card expansion panel | [79183d6](https://github.com/undergroundwires/privacy.sexy/commit/79183d64173e588d88bf074d5b50a52a71c2d885)
|
||||||
|
* ci/cd: Fix macOS Docker build reliability issues | [8a5592f](https://github.com/undergroundwires/privacy.sexy/commit/8a5592f92be4366a806afc9eee9135696a1dd993)
|
||||||
|
* ci/cd: fix IPv6 timeouts with `force-ipv4` action | [52fadcd](https://github.com/undergroundwires/privacy.sexy/commit/52fadcd6177ed06216be9c67dad57192ae02a4f9)
|
||||||
|
* ci/cd: bump Node.js environment to 20.x | [59decd1](https://github.com/undergroundwires/privacy.sexy/commit/59decd17e273bada1493eaa855c43cbabf90308f)
|
||||||
|
* ci/cd: trigger URL checks more, and limit amount | [4fb6302](https://github.com/undergroundwires/privacy.sexy/commit/4fb6302c67f2a3fedff419e8c22872593cf800ef)
|
||||||
|
* Fix overflow in tree node content on small screens | [557cea3](https://github.com/undergroundwires/privacy.sexy/commit/557cea3f4866dc33236874f5fe4d2d69ee963dae)
|
||||||
|
* Fix horizontal layout shift after script selection | [bc7e1fa](https://github.com/undergroundwires/privacy.sexy/commit/bc7e1faa1c3f2b61bf2046fdd6d6a4141b484662)
|
||||||
|
* Fix card header expansion glitch on card collapse | [5d940b5](https://github.com/undergroundwires/privacy.sexy/commit/5d940b57ef2a4c219932cd15201401f8550cfb41)
|
||||||
|
* Ignore `ResizeObserver` errors in Cypress tests | [4472c28](https://github.com/undergroundwires/privacy.sexy/commit/4472c2852e4b87083bda7979471ab9f377d17a01)
|
||||||
|
* win: improve and document secret key scripts | [49f22f0](https://github.com/undergroundwires/privacy.sexy/commit/49f22f048f39e7388633c488b5fe59101b831984)
|
||||||
|
* Fix card arrow not being animated in sync | [7b546c5](https://github.com/undergroundwires/privacy.sexy/commit/7b546c567c4683a37fe94595362f4c2bf92ffd59)
|
||||||
|
* win: improve Windows feature disablement scripts | [b68711e](https://github.com/undergroundwires/privacy.sexy/commit/b68711ef88982c0ee2b1d41b4452e899821adc64)
|
||||||
|
* Fix top script menu overflow on small screens | [b7a20d9](https://github.com/undergroundwires/privacy.sexy/commit/b7a20d9d41ea8bcefdd553b87641f3c22b4cde97)
|
||||||
|
* win: fix Visual Studio remote analysis script #327 | [4142d08](https://github.com/undergroundwires/privacy.sexy/commit/4142d084f64a3b540487ff68b28032977d12006d)
|
||||||
|
* win: improve firewall docs /w `winget` impact #142 | [ffd647d](https://github.com/undergroundwires/privacy.sexy/commit/ffd647d1529375474b81900cc7bee4c32fbf861f)
|
||||||
|
* Centralize and use global spacing variables | [ae17200](https://github.com/undergroundwires/privacy.sexy/commit/ae172000a64416e5a3e2b2e32b7846f039f445f0)
|
||||||
|
* win: improve service revert and docs | [b87b7aa](https://github.com/undergroundwires/privacy.sexy/commit/b87b7aac7d118a23a0d1bfb881e385347de4adb7)
|
||||||
|
* Bump dependencies to latest, hold ESLint | [f3571ab](https://github.com/undergroundwires/privacy.sexy/commit/f3571abeafdbe1e6d152958fab26de91a9c08bc3)
|
||||||
|
* Fix inability to tap outside modal on mobile | [cb144ae](https://github.com/undergroundwires/privacy.sexy/commit/cb144ae47273deeb7058d4b1380e480ebccdaf81)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.1...0.13.2)
|
||||||
|
|
||||||
|
## 0.13.1 (2024-03-22)
|
||||||
|
|
||||||
|
* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010)
|
||||||
|
* Fix tooltip falling behind elements on fade out | [1964524](https://github.com/undergroundwires/privacy.sexy/commit/19645248ab7bc78dc872fa176c1a3650d7d6d644)
|
||||||
|
* Improve VSCode detection in `configure_vscode.py` | [98845e6](https://github.com/undergroundwires/privacy.sexy/commit/98845e6caee168db131aaf0736533e450827a52c)
|
||||||
|
* Bump TypeScript to 5.3 with `verbatimModuleSyntax` | [a721e82](https://github.com/undergroundwires/privacy.sexy/commit/a721e82a4fb603c0732ccfdffc87396c2a01363e)
|
||||||
|
* Migrate to Vite 5 and adjust configurations | [4ac1425](https://github.com/undergroundwires/privacy.sexy/commit/4ac1425f76079352268c488f3ff607d1fdc1beb2)
|
||||||
|
* win: improve and unify service start/stop logic | [adc2089](https://github.com/undergroundwires/privacy.sexy/commit/adc20898873d50a8873ffc74c48257e69a45d367)
|
||||||
|
* Upgrade vitest to v1 and fix test definitions | [e721885](https://github.com/undergroundwires/privacy.sexy/commit/e7218850ba62a7bebaf4768b13e46cba0dedd906)
|
||||||
|
* Improve URL checks to reduce false-negatives | [5abf8ff](https://github.com/undergroundwires/privacy.sexy/commit/5abf8ff216a1da737fd489864eeee880f78d6601)
|
||||||
|
* win: improve OneDrive data deletion safety | [5eff3a0](https://github.com/undergroundwires/privacy.sexy/commit/5eff3a04886d0d23a6e4c13a0178bb247105c5cb)
|
||||||
|
* Bump Electron to latest and use native ESM | [840adf9](https://github.com/undergroundwires/privacy.sexy/commit/840adf9429ed47f9e88c05e90f1d3ab930c2dfc4)
|
||||||
|
* Fix tooltip styling inconsistency | [ec34ac1](https://github.com/undergroundwires/privacy.sexy/commit/ec34ac1124e8b8ae53bf31a4dbdc88bb078b3d4e)
|
||||||
|
* win: fix VSCode manual update switch script #312 | [b71ad79](https://github.com/undergroundwires/privacy.sexy/commit/b71ad797a3af0db45143249903cb5e178692de7c)
|
||||||
|
* mac, linux, win: fix dead URLs and improve docs | [abec9de](https://github.com/undergroundwires/privacy.sexy/commit/abec9def075d82fdaee9663ef8fe1a488911f45b)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.0...0.13.1)
|
||||||
|
|
||||||
## 0.13.0 (2024-02-11)
|
## 0.13.0 (2024-02-11)
|
||||||
|
|
||||||
* win: add disabling clipboard features #251, #247 | [c6ebba8](https://github.com/undergroundwires/privacy.sexy/commit/c6ebba85fb1b362be0d81d3078f19db71e0528b2)
|
* win: add disabling clipboard features #251, #247 | [c6ebba8](https://github.com/undergroundwires/privacy.sexy/commit/c6ebba85fb1b362be0d81d3078f19db71e0528b2)
|
||||||
|
|||||||
11
README.md
@@ -60,8 +60,8 @@
|
|||||||
<br />
|
<br />
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.quality.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
<img
|
<img
|
||||||
alt="Quality checks status"
|
alt="Status of quality checks"
|
||||||
src="https://github.com/undergroundwires/privacy.sexy/workflows/quality-checks/badge.svg"
|
src="https://github.com/undergroundwires/privacy.sexy/workflows/checks.quality/badge.svg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
<a href="https://github.com/undergroundwires/privacy.sexy/actions/workflows/checks.build.yaml" target="_blank" rel="noopener noreferrer">
|
||||||
@@ -122,9 +122,12 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-Setup-0.13.0.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-0.13.0.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.0/privacy.sexy-0.13.0.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-Setup-0.13.3.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.3/privacy.sexy-0.13.3.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
For a detailed comparison of features between the desktop and web versions of privacy.sexy, see [Desktop vs. Web Features](./docs/desktop-vs-web-features.md).
|
See also:
|
||||||
|
|
||||||
|
- [Desktop vs. Web Features](./docs/desktop/desktop-vs-web-features.md): Differences and unique aspects of desktop and web versions.
|
||||||
|
- [System Requirements](./docs/desktop/system-requirements.md): Hardware and software requirements for the desktop version.
|
||||||
|
|
||||||
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
💡 Regularly applying your configuration with privacy.sexy is recommended, especially after each new release and major operating system updates. Each version updates scripts to enhance stability, privacy, and security.
|
||||||
|
|
||||||
|
|||||||
13
SECURITY.md
@@ -43,10 +43,17 @@ privacy.sexy adopts a defense in depth strategy to protect users on multiple lay
|
|||||||
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
elevation of privileges for system modifications with explicit user consent and logs every action taken with high privileges. This
|
||||||
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
approach actively minimizes potential security risks by limiting privileged operations and aligning with the principle of least privilege.
|
||||||
- **Secure Script Execution/Storage:**
|
- **Secure Script Execution/Storage:**
|
||||||
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans. This safeguards against
|
- **Antivirus scans:**
|
||||||
any unwanted modifications. Furthermore, the application incorporates integrity checks for tamper protection. If the script file differs from
|
Before executing any script, the desktop application stores a copy to allow antivirus software to perform scans.
|
||||||
the user's selected script, the application will not execute or save the script, ensuring the processing of authentic scripts.
|
This step allows confirming that the scripts are secure and safe to use.
|
||||||
|
- **Tamper protection:**
|
||||||
|
The application incorporates integrity checks for tamper protection.
|
||||||
|
If the script file differs from the user's selected script, the application will not execute or save the script, ensuring the processing
|
||||||
|
of authentic scripts.
|
||||||
|
This safeguards against any unwanted modifications.
|
||||||
|
- **Clean-up:**
|
||||||
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts.
|
||||||
|
This allows users to maintain their privacy by removing traces of their usage patterns or script preferences.
|
||||||
|
|
||||||
### Update Security and Integrity
|
### Update Security and Integrity
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# build
|
|
||||||
|
|
||||||
This folder contains files that are used by Electron to serve the desktop version.
|
|
||||||
|
|
||||||
Icons are created from the main logo file and should not be changed manually, see [related documentation](./../img/README.md).
|
|
||||||
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 553 B |
|
Before Width: | Height: | Size: 963 B |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 2.9 KiB |
@@ -1,6 +1,6 @@
|
|||||||
# Desktop vs. Web Features
|
# Desktop vs. Web Features
|
||||||
|
|
||||||
This table highlights differences between the desktop and web versions of `privacy.sexy`.
|
This table outlines the differences between the desktop and web versions of `privacy.sexy`.
|
||||||
|
|
||||||
| Feature | Desktop | Web |
|
| Feature | Desktop | Web |
|
||||||
| ------- | ------- | --- |
|
| ------- | ------- | --- |
|
||||||
@@ -8,10 +8,8 @@ This table highlights differences between the desktop and web versions of `priva
|
|||||||
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
| [Offline usage](#offline-usage) | 🟢 Available | 🟡 Partially available |
|
||||||
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
| [Auto-updates](#auto-updates) | 🟢 Available | 🟢 Available |
|
||||||
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
| [Logging](#logging) | 🟢 Available | 🔴 Not available |
|
||||||
| [Script execution](#script-execution) | 🟢 Available | 🔴 Not available |
|
|
||||||
| [Error handling](#error-handling) | 🟢 Advanced | 🟡 Limited |
|
|
||||||
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
|
||||||
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
| [Secure script execution/storage](#secure-script-executionstorage) | 🟢 Available | 🔴 Not available |
|
||||||
|
| [Native dialogs](#native-dialogs) | 🟢 Available | 🔴 Not available |
|
||||||
|
|
||||||
## Feature descriptions
|
## Feature descriptions
|
||||||
|
|
||||||
@@ -30,11 +28,11 @@ Desktop version inherently allows offline usage.
|
|||||||
|
|
||||||
### Auto-updates
|
### Auto-updates
|
||||||
|
|
||||||
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./ci-cd.md).
|
Both the desktop and web versions of privacy.sexy provide timely access to the latest features and security improvements. The updates are automatically deployed from source code, reflecting the latest changes for enhanced security and reliability. For more details, see [CI/CD documentation](./../ci-cd.md).
|
||||||
|
|
||||||
The desktop version ensures secure delivery through cryptographic signatures and version checks.
|
The desktop version ensures secure delivery through cryptographic signatures and version checks.
|
||||||
|
|
||||||
[Security is a top priority](./../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
[Security is a top priority](./../../SECURITY.md#update-security-and-integrity) at privacy.sexy.
|
||||||
|
|
||||||
> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
> **Note for macOS users:** On macOS, the desktop version's auto-update process involves manual steps due to Apple's code signing costs.
|
||||||
> Users get notified about updates but might need to complete the installation manually.
|
> Users get notified about updates but might need to complete the installation manually.
|
||||||
@@ -53,7 +51,7 @@ Log file locations vary by operating system:
|
|||||||
|
|
||||||
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
> 💡 privacy.sexy provides scripts to securely erase these logs.
|
||||||
|
|
||||||
### Script execution
|
### Secure script execution/storage
|
||||||
|
|
||||||
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
The desktop version of privacy.sexy enables direct script execution, providing a seamless and integrated experience.
|
||||||
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
This direct execution capability isn't available in the web version due to inherent browser restrictions.
|
||||||
@@ -69,31 +67,27 @@ These locations vary based on the operating system:
|
|||||||
|
|
||||||
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
> 💡 privacy.sexy provides scripts to securely erase your script execution history.
|
||||||
|
|
||||||
### Error handling
|
**Script antivirus scans:**
|
||||||
|
|
||||||
The desktop version of privacy.sexy features advanced error handling capabilities.
|
To enhance system protection, the desktop version of privacy.sexy automatically verifies the security of script
|
||||||
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
execution files by reading them back.
|
||||||
In contrast, the web version has more basic error handling due to browser limitations and the nature of web applications.
|
This process triggers antivirus scans to verify that scripts are safe before the execution.
|
||||||
|
|
||||||
### Native dialogs
|
**Script integrity checks:**
|
||||||
|
|
||||||
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
|
||||||
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
|
||||||
|
|
||||||
### Secure script execution/storage
|
|
||||||
|
|
||||||
**Integrity checks:**
|
|
||||||
|
|
||||||
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
The desktop version of privacy.sexy implements robust integrity checks for both script execution and storage.
|
||||||
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
Featuring tamper protection, the application actively verifies the integrity of script files before executing or saving them.
|
||||||
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
If the actual contents of a script file do not align with the expected contents, the application refuses to execute or save the script.
|
||||||
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
This proactive approach ensures only unaltered and verified scripts undergo processing, thereby enhancing both security and reliability.
|
||||||
Due to browser constraints, this feature is absent in the web version.
|
|
||||||
|
|
||||||
**Error handling:**
|
**Error handling:**
|
||||||
|
|
||||||
|
The desktop version of privacy.sexy features advanced error handling capabilities.
|
||||||
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
In scenarios where script execution or storage encounters failure, the desktop application initiates automated troubleshooting and self-healing processes.
|
||||||
It also guides users through potential issues with filesystem or third-party software, such as antivirus interventions.
|
It employs robust and reliable execution strategies, including self-healing mechanisms, and provides guidance and troubleshooting information to resolve issues effectively.
|
||||||
Specifically, the application is capable of identifying when antivirus software blocks or removes a script, providing users with tailored error messages
|
This proactive error handling and user guidance enhances the application's security and reliability.
|
||||||
and detailed resolution steps. This level of proactive error handling and user guidance enhances the application's security and reliability,
|
|
||||||
offering a feature not achievable in the web version due to browser limitations.
|
### Native dialogs
|
||||||
|
|
||||||
|
The desktop version uses native dialogs, offering more features and reliability compared to the browser's file system dialogs.
|
||||||
|
These native dialogs provide a more integrated and user-friendly experience, aligning with the operating system's standard interface and functionalities.
|
||||||
36
docs/desktop/system-requirements.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# System Requirements for the Desktop Version
|
||||||
|
|
||||||
|
The following system requirements are the official ones for the desktop version.
|
||||||
|
While we have tested and confirmed these requirements, the application might also work on other
|
||||||
|
systems or configurations that haven't undergone official testing.
|
||||||
|
|
||||||
|
## Windows
|
||||||
|
|
||||||
|
- **Version:** Windows 10 and later.
|
||||||
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
|
- **Architecture:** 64-bit (x86-64), ARM (ARM64).
|
||||||
|
|
||||||
|
> **⚠️ Compatibility Note:**
|
||||||
|
> ARM version is only compatible with Windows 11 and later.
|
||||||
|
> It runs non-natively, leading to slower performance due to emulation [1].
|
||||||
|
|
||||||
|
## macOS
|
||||||
|
|
||||||
|
- **Version:** macOS Catalina (10.15) and later.
|
||||||
|
- **Architecture:** Intel-based (x86-64), Apple silicon (ARM64).
|
||||||
|
|
||||||
|
## Linux
|
||||||
|
|
||||||
|
- **Version:** Ubuntu 18.04 and later, Fedora 32 and later, and Debian 10 and later.
|
||||||
|
- **Processor:** Intel Pentium 4 or later.
|
||||||
|
- **Architecture:** 64-bit (x86-64).
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
System requirements reflect Electron's platform capabilities [2] and Chromium's recommended configurations [3].
|
||||||
|
|
||||||
|
For details on the build process, see [electron-builder configuration file](./../../electron-builder.cjs).
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240428082726/https://learn.microsoft.com/en-us/windows/arm/add-arm-support#emulation-on-arm-based-devices-for-x86-or-x64-windows-apps "Add support Arm devices to your Windows app | Microsoft Learn | learn.microsoft.com"
|
||||||
|
[2]: https://archive.ph/2024.04.28-082958/https://github.com/electron/electron/blob/main/README.md#platform-support "Platform Support | electron/README.md at main · electron/electron · GitHub | github.com"
|
||||||
|
[3]: https://web.archive.org/web/20240428082945/https://support.google.com/chrome/a/answer/7100626?hl=en "Chrome browser system requirements - Chrome Enterprise and Education Help | support.google.com"
|
||||||
@@ -14,18 +14,19 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
|
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
|
||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components, helpers and styles coupled to Vue components.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
- [**`Hooks`**](../src/presentation/components/Shared/Hooks): Hooks used by components through [dependency injection](#dependency-injections).
|
||||||
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
- [**`/public/`**](../src/presentation/public/): Contains static assets.
|
||||||
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
- [**`assets/`**](./../src/presentation/assets/styles/): Contains assets processed by Vite.
|
||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
- [`/main/` **`index.ts`**](./../src/presentation/electron/main/index.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
- [`/preload/` **`index.ts`**](./../src/presentation/electron/preload/index.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
|
- [**`/shared/`**](./../src/presentation/electron/shared/): Shared logic between different Electron processes.
|
||||||
|
- [**`/build/`**](./../src/presentation/electron/build/): `electron-builder` build resources directory, [README.md](./../src/presentation/electron/build/README.md).
|
||||||
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
- [**`/vite.config.ts`**](./../vite.config.ts): Contains Vite configurations for building web application.
|
||||||
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
- [**`/electron.vite.config.ts`**](./../electron.vite.config.ts): Contains Vite configurations for building desktop applications.
|
||||||
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
- [**`/postcss.config.cjs`**](./../postcss.config.cjs): Contains PostCSS configurations for Vite.
|
||||||
@@ -38,6 +39,13 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
They should also have different visual state when hovering/touching on them that indicates that they are being clicked, which helps with accessibility.
|
||||||
- **Borders**:
|
- **Borders**:
|
||||||
privacy.sexy prefers sharper edges in its design language.
|
privacy.sexy prefers sharper edges in its design language.
|
||||||
|
- **Fonts**:
|
||||||
|
- Use the primary font for regular text and monospace font for code or specific data.
|
||||||
|
- Use cursive and logo fonts solely for branding.
|
||||||
|
- Refer to [standardized font size variables](../src/presentation/assets/styles/_typography.scss) for font sizing, avoiding arbitrary `px`, `em`, `rem`, or percentage values.
|
||||||
|
- **Spacing**:
|
||||||
|
Use [global spacing variables](../src/presentation/assets/styles/_spacing.scss) for consistent margin, padding, and gap definitions.
|
||||||
|
This provides uniform spatial distribution and alignment of elements, enhancing visual harmony and making the UI more scalable and maintainable.
|
||||||
|
|
||||||
## Application data
|
## Application data
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ Key attributes of a good script:
|
|||||||
- `Minimize` over `Limit`, `Reduce`
|
- `Minimize` over `Limit`, `Reduce`
|
||||||
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
- `Maximize` over `Extend`, `Delay`, `Postpone`, `Prolong`
|
||||||
- `Remove` over `Uninstall`
|
- `Remove` over `Uninstall`
|
||||||
|
- `Improve` over `Increase`
|
||||||
- Structure your phrases for clarity, examples:
|
- Structure your phrases for clarity, examples:
|
||||||
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
- Prefer `Disable XX telemetry` over `Disable telemetry in XX`
|
||||||
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
- Prefer `Clear XX data` over `Clear data from XX`, or `Clear data of XX`.
|
||||||
@@ -35,8 +36,8 @@ Key attributes of a good script:
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- Use credible and reputable sources for references.
|
- Use credible and reputable sources for references.
|
||||||
- Use archived links by using [archive.org](https://archive.org) or [archive.today](https://archive.today).
|
- Use archived links by using [archive.org](https://archive.org) or [archive.ph](https://archive.ph).
|
||||||
- Format archive.today links fully, for example: `https://archive.today/YYYYMMDDhhmmss/https://privacy.sexy`.
|
- Format archive.today links fully, for example: `https://archive.ph/YYYYMMDDhhmmss/https://privacy.sexy`.
|
||||||
- Explain the default behavior if the script is not executed.
|
- Explain the default behavior if the script is not executed.
|
||||||
|
|
||||||
## Shared functions
|
## Shared functions
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
/* eslint-disable no-template-curly-in-string */
|
/* eslint-disable no-template-curly-in-string */
|
||||||
|
|
||||||
const { join } = require('node:path');
|
const { join, resolve } = require('node:path');
|
||||||
|
const { readdirSync, existsSync } = require('node:fs');
|
||||||
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {import('electron-builder').Configuration}
|
||||||
|
* @see https://www.electron.build/configuration/configuration
|
||||||
|
*/
|
||||||
module.exports = {
|
module.exports = {
|
||||||
// Common options
|
// Common options
|
||||||
publish: {
|
publish: {
|
||||||
@@ -12,9 +17,12 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
directories: {
|
directories: {
|
||||||
output: electronBundled,
|
output: electronBundled,
|
||||||
|
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
|
||||||
},
|
},
|
||||||
extraMetadata: {
|
extraMetadata: {
|
||||||
main: join(electronUnbundled, 'main/index.cjs'), // do not `path.resolve`, it expects a relative path
|
main: findMainEntryFile(
|
||||||
|
join(electronUnbundled, 'main'), // do not `path.resolve`, it expects a relative path
|
||||||
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
// Windows
|
// Windows
|
||||||
@@ -35,9 +43,32 @@ module.exports = {
|
|||||||
|
|
||||||
// macOS
|
// macOS
|
||||||
mac: {
|
mac: {
|
||||||
|
target: {
|
||||||
target: 'dmg',
|
target: 'dmg',
|
||||||
|
arch: 'universal',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
dmg: {
|
dmg: {
|
||||||
artifactName: '${name}-${version}.${ext}',
|
artifactName: '${name}-${version}.${ext}',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds by accommodating different JS file extensions and module formats.
|
||||||
|
*/
|
||||||
|
function findMainEntryFile(parentDirectory) {
|
||||||
|
const absoluteParentDirectory = resolvePathFromProjectRoot(parentDirectory);
|
||||||
|
if (!existsSync(absoluteParentDirectory)) {
|
||||||
|
return null; // Avoid disrupting other processes such `npm install`.
|
||||||
|
}
|
||||||
|
const files = readdirSync(absoluteParentDirectory);
|
||||||
|
const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
|
||||||
|
if (!entryFile) {
|
||||||
|
throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
|
||||||
|
}
|
||||||
|
return join(parentDirectory, entryFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolvePathFromProjectRoot(pathSegment) {
|
||||||
|
return resolve(__dirname, pathSegment);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const ELECTRON_DIST_SUBDIRECTORIES = {
|
|||||||
renderer: resolveElectronDistSubdirectory('renderer'),
|
renderer: resolveElectronDistSubdirectory('renderer'),
|
||||||
};
|
};
|
||||||
|
|
||||||
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.cjs');
|
process.env.ELECTRON_ENTRY = resolve(ELECTRON_DIST_SUBDIRECTORIES.main, 'index.mjs');
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
main: getSharedElectronConfig({
|
main: getSharedElectronConfig({
|
||||||
@@ -54,13 +54,23 @@ function getSharedElectronConfig(options: {
|
|||||||
},
|
},
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
// Mark: electron-esm-support
|
format: 'es',
|
||||||
// This is needed so `type="module"` works
|
|
||||||
entryFileNames: '[name].cjs',
|
// Ensure all generated files use '.mjs' for module consistency.
|
||||||
|
// Otherwise, preloader process get `.mjs` extension but main process get `.js` extension, see https://github.com/alex8088/electron-vite/issues/397.
|
||||||
|
entryFileNames: '[name].mjs',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [externalizeDepsPlugin()],
|
plugins: [externalizeDepsPlugin({
|
||||||
|
exclude: [
|
||||||
|
// Keep 'electron-log' in bundling process.
|
||||||
|
// This is a workaround for inability of Electron's ESM loader to resolve subpath imports.
|
||||||
|
// Do not externalize `electron-log` so subpath imports such as `electron-log/main` works.
|
||||||
|
// See https://github.com/electron/electron/issues/41241, https://github.com/alex8088/electron-vite/issues/401
|
||||||
|
'electron-log',
|
||||||
|
],
|
||||||
|
})],
|
||||||
define: {
|
define: {
|
||||||
...getClientEnvironmentVariables(),
|
...getClientEnvironmentVariables(),
|
||||||
},
|
},
|
||||||
|
|||||||
12404
package-lock.json
generated
82
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.0",
|
"version": "0.13.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Privacy is sexy",
|
"slogan": "Privacy is sexy",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy.",
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
"test:integration": "vitest run --dir tests/integration",
|
"test:integration": "vitest run --dir tests/integration",
|
||||||
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
"test:cy:run": "start-server-and-test \"vite build && vite preview --port 7070\" http://localhost:7070 \"cypress run --config baseUrl=http://localhost:7070\"",
|
||||||
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
"test:cy:open": "start-server-and-test \"vite --port 7070 --mode production\" http://localhost:7070 \"cypress open --config baseUrl=http://localhost:7070\"",
|
||||||
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml",
|
"lint": "npm run lint:md && npm run lint:md:consistency && npm run lint:md:relative-urls && npm run lint:eslint && npm run lint:yaml && npm run lint:pylint",
|
||||||
"install-deps": "node scripts/npm-install.js",
|
"install-deps": "node scripts/npm-install.js",
|
||||||
"icons:build": "node scripts/logo-update.js",
|
"icons:build": "node scripts/logo-update.js",
|
||||||
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
"check:desktop": "vitest run --dir tests/checks/desktop-runtime-errors --environment node",
|
||||||
@@ -29,66 +29,68 @@
|
|||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
"lint:yaml": "yamllint **/*.yaml --ignore=node_modules/**/*.yaml",
|
||||||
|
"lint:pylint": "pylint **/*.py",
|
||||||
"postinstall": "electron-builder install-app-deps",
|
"postinstall": "electron-builder install-app-deps",
|
||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.6",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"@types/markdown-it": "^13.0.7",
|
"ace-builds": "^1.33.0",
|
||||||
"ace-builds": "^1.30.0",
|
"electron-log": "^5.1.2",
|
||||||
"electron-log": "^5.0.1",
|
"electron-progressbar": "^2.2.1",
|
||||||
"electron-progressbar": "^2.1.0",
|
"electron-updater": "^6.1.9",
|
||||||
"electron-updater": "^6.1.4",
|
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^13.0.2",
|
"markdown-it": "^14.1.0",
|
||||||
"vue": "^3.3.7"
|
"vue": "^3.4.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
"@rushstack/eslint-patch": "^1.6.1",
|
"@rushstack/eslint-patch": "^1.10.2",
|
||||||
"@types/ace": "^0.0.49",
|
"@types/ace": "^0.0.52",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.17.0",
|
"@types/markdown-it": "^14.0.1",
|
||||||
"@typescript-eslint/parser": "^6.17.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"@vitejs/plugin-legacy": "^5.3.2",
|
"@vitejs/plugin-legacy": "^5.3.2",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "12.0.0",
|
||||||
"@vue/test-utils": "^2.4.1",
|
"@vue/test-utils": "^2.4.5",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.19",
|
||||||
"cypress": "^13.3.1",
|
"cypress": "^13.7.3",
|
||||||
"electron": "^27.0.0",
|
"electron": "^29.3.0",
|
||||||
"electron-builder": "^24.6.4",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-icon-builder": "^2.0.1",
|
|
||||||
"electron-vite": "^2.1.0",
|
"electron-vite": "^2.1.0",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-plugin-cypress": "^2.15.1",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-vue": "^9.19.2",
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^2.2.0",
|
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||||
"icon-gen": "^4.0.0",
|
"jsdom": "^24.0.0",
|
||||||
"jsdom": "^22.1.0",
|
"markdownlint-cli": "^0.39.0",
|
||||||
"markdownlint-cli": "^0.37.0",
|
"postcss": "^8.4.38",
|
||||||
"postcss": "^8.4.31",
|
|
||||||
"remark-cli": "^12.0.0",
|
"remark-cli": "^12.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^5.1.2",
|
"remark-preset-lint-consistent": "^6.0.0",
|
||||||
"remark-validate-links": "^13.0.0",
|
"remark-validate-links": "^13.0.1",
|
||||||
"sass": "^1.69.3",
|
"sass": "^1.75.0",
|
||||||
"start-server-and-test": "^2.0.1",
|
"start-server-and-test": "^2.0.3",
|
||||||
"svgexport": "^0.4.2",
|
"terser": "^5.30.3",
|
||||||
"terser": "^5.21.0",
|
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.1.6",
|
"vite": "^5.2.8",
|
||||||
"vitest": "^0.34.6",
|
"vitest": "^1.5.0",
|
||||||
"vue-tsc": "^1.8.19",
|
"vue-tsc": "^2.0.13",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
"terser": "Used by `@vitejs/plugin-legacy` for minification",
|
"terser": "Used by `@vitejs/plugin-legacy` for minification",
|
||||||
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`"
|
"@rushstack/eslint-patch": "Needed by `@vue/eslint-config-typescript` and `@vue/eslint-config-airbnb-with-typescript`",
|
||||||
|
"@typescript-eslint/eslint-plugin": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||||
|
"@typescript-eslint/parser": "Cannot migrate to v7 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||||
|
"@vue/eslint-config-typescript": "Cannot migrate to v13 because of `@vue/eslint-config-airbnb-with-typescript`, see https://github.com/vuejs/eslint-config-airbnb/issues/63",
|
||||||
|
"eslint": "Cannot migrate to v9 `@typescript-eslint/eslint-plugin` (≤ v7), `@typescript-eslint/parser` (≤ v7), `@vue/eslint-config-airbnb-with-typescript@` (≤ v8) requires `eslint` ≤ v8, see https://github.com/vuejs/eslint-config-airbnb/issues/65, https://github.com/typescript-eslint/typescript-eslint/issues/8211"
|
||||||
},
|
},
|
||||||
"homepage": "https://privacy.sexy",
|
"homepage": "https://privacy.sexy",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
"""
|
"""
|
||||||
|
Description:
|
||||||
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
This script configures project-level VSCode settings in '.vscode/settings.json' for
|
||||||
development and installs recommended extensions from '.vscode/extensions.json'.
|
development and installs recommended extensions from '.vscode/extensions.json'.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python3 ./scripts/configure_vscode.py
|
||||||
"""
|
"""
|
||||||
# pylint: disable=missing-function-docstring
|
# pylint: disable=missing-function-docstring
|
||||||
|
|
||||||
@@ -40,7 +44,7 @@ def ensure_setting_file_exists() -> None:
|
|||||||
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||||
except IOError as error:
|
except IOError as error:
|
||||||
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
print_error(f"Error creating file {VSCODE_SETTINGS_JSON_FILE}: {error}")
|
||||||
print(f"📄 Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
print_success(f"Created empty {VSCODE_SETTINGS_JSON_FILE}")
|
||||||
|
|
||||||
def add_or_update_settings() -> None:
|
def add_or_update_settings() -> None:
|
||||||
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
configure_setting_key('eslint.validate', ['vue', 'javascript', 'typescript'])
|
||||||
@@ -98,7 +102,8 @@ def locate_vscode_cli() -> Optional[str]:
|
|||||||
if vscode_alias:
|
if vscode_alias:
|
||||||
return vscode_alias
|
return vscode_alias
|
||||||
potential_vscode_cli_paths = [
|
potential_vscode_cli_paths = [
|
||||||
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code' # macOS VS Code may not register 'code' command in PATH
|
# VS Code on macOS may not register 'code' command in PATH
|
||||||
|
'/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code'
|
||||||
]
|
]
|
||||||
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
for vscode_cli_candidate_path in potential_vscode_cli_paths:
|
||||||
if Path(vscode_cli_candidate_path).is_file():
|
if Path(vscode_cli_candidate_path).is_file():
|
||||||
@@ -109,7 +114,7 @@ def remove_json_comments(json_like: str) -> str:
|
|||||||
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
pattern: str = r'(?:"(?:\\.|[^"\\])*"|/\*[\s\S]*?\*/|//.*)|([^:]//.*$)'
|
||||||
return re.sub(
|
return re.sub(
|
||||||
pattern,
|
pattern,
|
||||||
lambda m: '' if m.group(1) else m.agroup(0), json_like, flags=re.MULTILINE,
|
lambda m: '' if m.group(1) else m.group(0), json_like, flags=re.MULTILINE,
|
||||||
)
|
)
|
||||||
|
|
||||||
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
def install_vscode_extensions(vscode_cli_path: str, extensions: list[str]) -> None:
|
||||||
@@ -166,16 +171,16 @@ def print_installation_results(successful_installations: int, total_extensions:
|
|||||||
print_error("Failed to install any of the recommended extensions.")
|
print_error("Failed to install any of the recommended extensions.")
|
||||||
|
|
||||||
def print_error(message: str) -> None:
|
def print_error(message: str) -> None:
|
||||||
print(f"💀 Error: {message}", file=sys.stderr)
|
print(f"[ERROR] {message}", file=sys.stderr)
|
||||||
|
|
||||||
def print_success(message: str) -> None:
|
def print_success(message: str) -> None:
|
||||||
print(f"✅ Success: {message}")
|
print(f"[SUCCESS] {message}")
|
||||||
|
|
||||||
def print_skip(message: str) -> None:
|
def print_skip(message: str) -> None:
|
||||||
print(f"⏩ Skipped: {message}")
|
print(f"[SKIPPED] {message}")
|
||||||
|
|
||||||
def print_warning(message: str) -> None:
|
def print_warning(message: str) -> None:
|
||||||
print(f"⚠️ Warning: {message}", file=sys.stderr)
|
print(f"[WARNING] {message}", file=sys.stderr)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -1,84 +1,120 @@
|
|||||||
#!/usr/bin/env bash
|
/**
|
||||||
import { resolve, join } from 'node:path';
|
* Description:
|
||||||
import { rm, mkdtemp, stat } from 'node:fs/promises';
|
* This script updates the logo images across the project based on the primary
|
||||||
|
* logo file ('img/logo.svg' file).
|
||||||
|
*
|
||||||
|
* It handles the creation and update of various icon sizes for different purposes,
|
||||||
|
* including desktop launcher icons, tray icons, and web favicons from a single source
|
||||||
|
* SVG logo file.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* node ./scripts/logo-update.js
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* ImageMagick must be installed and accessible in the system's PATH
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { resolve, join, dirname } from 'node:path';
|
||||||
|
import { stat } from 'node:fs/promises';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { URL, fileURLToPath } from 'node:url';
|
import { URL, fileURLToPath } from 'node:url';
|
||||||
|
import electronBuilderConfig from '../electron-builder.cjs';
|
||||||
|
|
||||||
class Paths {
|
class ImageAssetPaths {
|
||||||
constructor(selfDirectory) {
|
constructor(currentScriptDirectory) {
|
||||||
const projectRoot = resolve(selfDirectory, '../');
|
const projectRoot = resolve(currentScriptDirectory, '../');
|
||||||
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
this.sourceImage = join(projectRoot, 'img/logo.svg');
|
||||||
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
this.publicDirectory = join(projectRoot, 'src/presentation/public');
|
||||||
this.electronBuildDirectory = join(projectRoot, 'build');
|
this.electronBuildResourcesDirectory = electronBuilderConfig.directories.buildResources;
|
||||||
|
}
|
||||||
|
|
||||||
|
get electronTrayIconFile() {
|
||||||
|
return join(this.publicDirectory, 'icon.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
get webFaviconFile() {
|
||||||
|
return join(this.publicDirectory, 'favicon.ico');
|
||||||
}
|
}
|
||||||
|
|
||||||
toString() {
|
toString() {
|
||||||
return `Source image: ${this.sourceImage}\n`
|
return `Source image: ${this.sourceImage}`
|
||||||
+ `Public directory: ${this.publicDirectory}\n`
|
+ `\nPublic directory: ${this.publicDirectory}`
|
||||||
+ `Electron build directory: ${this.electronBuildDirectory}`;
|
+ `\n\t Electron tray icon file: ${this.electronTrayIconFile}`
|
||||||
|
+ `\n\t Web favicon file: ${this.webFaviconFile}`
|
||||||
|
+ `\nElectron build directory: ${this.electronBuildResourcesDirectory}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const paths = new Paths(getCurrentScriptDirectory());
|
const paths = new ImageAssetPaths(getCurrentScriptDirectory());
|
||||||
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
console.log(`Paths:\n\t${paths.toString().replaceAll('\n', '\n\t')}`);
|
||||||
await updateDesktopLauncherAndTrayIcon(paths.sourceImage, paths.publicDirectory);
|
const convertCommand = await findAvailableImageMagickCommand();
|
||||||
await updateWebFavicon(paths.sourceImage, paths.publicDirectory);
|
await generateDesktopAndTrayIcons(
|
||||||
await updateDesktopIcons(paths.sourceImage, paths.electronBuildDirectory);
|
paths.sourceImage,
|
||||||
|
paths.electronTrayIconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateWebFavicon(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.webFaviconFile,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
|
await generateDesktopIcons(
|
||||||
|
paths.sourceImage,
|
||||||
|
paths.electronBuildResourcesDirectory,
|
||||||
|
convertCommand,
|
||||||
|
);
|
||||||
console.log('🎉 (Re)created icons successfully.');
|
console.log('🎉 (Re)created icons successfully.');
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopLauncherAndTrayIcon(sourceImage, publicFolder) {
|
async function generateDesktopAndTrayIcons(sourceImage, targetFile, convertCommand) {
|
||||||
|
// Reference: https://web.archive.org/web/20240502124306/https://www.electronjs.org/docs/latest/api/tray
|
||||||
|
console.log(`Updating desktop launcher and tray icon at ${targetFile}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(publicFolder);
|
await ensureParentFolderExists(targetFile);
|
||||||
const electronTrayIconFile = join(publicFolder, 'icon.png');
|
await convertFromSvgToPng(
|
||||||
console.log(`Updating desktop launcher and tray icon at ${electronTrayIconFile}.`);
|
convertCommand,
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
electronTrayIconFile,
|
targetFile,
|
||||||
|
'512x512',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateWebFavicon(sourceImage, faviconFolder) {
|
async function generateWebFavicon(sourceImage, faviconFilePath, convertCommand) {
|
||||||
console.log('Updating favicon');
|
console.log(`Updating favicon at ${faviconFilePath}.`);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(faviconFolder);
|
await ensureParentFolderExists(faviconFilePath);
|
||||||
await runCommand(
|
await convertFromSvgToIco(
|
||||||
'npx',
|
convertCommand,
|
||||||
'icon-gen',
|
sourceImage,
|
||||||
`--input ${sourceImage}`,
|
faviconFilePath,
|
||||||
`--output ${faviconFolder}`,
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
'--ico',
|
|
||||||
'--ico-name \'favicon\'',
|
|
||||||
'--report',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateDesktopIcons(sourceImage, electronIconsDir) {
|
async function generateDesktopIcons(sourceImage, electronBuildResourcesDirectory, convertCommand) {
|
||||||
|
console.log(`Creating Electron icon files to ${electronBuildResourcesDirectory}.`);
|
||||||
|
// Reference: https://web.archive.org/web/20240501103645/https://www.electron.build/icons.html
|
||||||
|
await ensureFolderExists(electronBuildResourcesDirectory);
|
||||||
await ensureFileExists(sourceImage);
|
await ensureFileExists(sourceImage);
|
||||||
await ensureFolderExists(electronIconsDir);
|
const electronMainIconFile = join(electronBuildResourcesDirectory, 'icon.png');
|
||||||
const temporaryDir = await mkdtemp('icon-');
|
await convertFromSvgToPng(
|
||||||
const temporaryPngFile = join(temporaryDir, 'icon.png');
|
convertCommand,
|
||||||
console.log(`Converting from SVG (${sourceImage}) to PNG: ${temporaryPngFile}`); // required by `icon-builder`
|
|
||||||
await runCommand(
|
|
||||||
'npx',
|
|
||||||
'svgexport',
|
|
||||||
sourceImage,
|
sourceImage,
|
||||||
temporaryPngFile,
|
electronMainIconFile,
|
||||||
'1024:1024',
|
'1024x1024', // Should be at least 512x512
|
||||||
);
|
);
|
||||||
console.log(`Creating electron icons to ${electronIconsDir}.`);
|
// Relying on `electron-builder`s conversion from png to ico results in pixelated look on Windows
|
||||||
await runCommand(
|
// 10 and 11 according to tests, see:
|
||||||
'npx',
|
// - https://web.archive.org/web/20240502114650/https://github.com/electron-userland/electron-builder/issues/7328
|
||||||
'electron-icon-builder',
|
// - https://web.archive.org/web/20240502115448/https://github.com/electron-userland/electron-builder/issues/3867
|
||||||
`--input="${temporaryPngFile}"`,
|
const electronWindowsIconFile = join(electronBuildResourcesDirectory, 'icon.ico');
|
||||||
`--output="${electronIconsDir}"`,
|
await convertFromSvgToIco(
|
||||||
'--flatten',
|
convertCommand,
|
||||||
|
sourceImage,
|
||||||
|
electronWindowsIconFile,
|
||||||
|
[16, 24, 32, 48, 64, 128, 256],
|
||||||
);
|
);
|
||||||
console.log('Cleaning up temporary directory.');
|
|
||||||
await rm(temporaryDir, { recursive: true, force: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFileExists(filePath) {
|
async function ensureFileExists(filePath) {
|
||||||
@@ -89,12 +125,60 @@ async function ensureFileExists(filePath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ensureFolderExists(folderPath) {
|
async function ensureFolderExists(folderPath) {
|
||||||
|
if (!folderPath) {
|
||||||
|
throw new Error('Path is missing');
|
||||||
|
}
|
||||||
const path = await stat(folderPath);
|
const path = await stat(folderPath);
|
||||||
if (!path.isDirectory()) {
|
if (!path.isDirectory()) {
|
||||||
throw new Error(`Not a directory: ${folderPath}`);
|
throw new Error(`Not a directory: ${folderPath}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureParentFolderExists(filePath) {
|
||||||
|
return ensureFolderExists(dirname(filePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
const BaseImageMagickConvertArguments = Object.freeze([
|
||||||
|
'-background none', // Transparent, so they do not get filled with white.
|
||||||
|
'-strip', // Strip metadata.
|
||||||
|
'-gravity Center', // Center the image when there's empty space
|
||||||
|
]);
|
||||||
|
|
||||||
|
async function convertFromSvgToIco(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
sizes,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
`-density ${Math.max(...sizes).toString()}`, // High enough for sharpness
|
||||||
|
`-define icon:auto-resize=${sizes.map((s) => s.toString()).join(',')}`, // Automatically store multiple sizes in an ico image
|
||||||
|
'-compress None',
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function convertFromSvgToPng(
|
||||||
|
convertCommand,
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
size = undefined,
|
||||||
|
) {
|
||||||
|
await runCommand(
|
||||||
|
convertCommand,
|
||||||
|
...BaseImageMagickConvertArguments,
|
||||||
|
...(size === undefined ? [] : [
|
||||||
|
`-resize ${size}`,
|
||||||
|
`-density ${size}`, // High enough for sharpness
|
||||||
|
]),
|
||||||
|
inputFile,
|
||||||
|
outputFile,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function runCommand(...args) {
|
async function runCommand(...args) {
|
||||||
const command = args.join(' ');
|
const command = args.join(' ');
|
||||||
console.log(`Running command: ${command}`);
|
console.log(`Running command: ${command}`);
|
||||||
@@ -124,4 +208,27 @@ function getCurrentScriptDirectory() {
|
|||||||
return fileURLToPath(new URL('.', import.meta.url));
|
return fileURLToPath(new URL('.', import.meta.url));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function findAvailableImageMagickCommand() {
|
||||||
|
// Reference: https://web.archive.org/web/20240502120041/https://imagemagick.org/script/convert.php
|
||||||
|
const potentialBaseCommands = [
|
||||||
|
'convert', // Legacy command, usually available on Linux/macOS installations
|
||||||
|
'magick convert', // Newer command, available on Windows installations
|
||||||
|
];
|
||||||
|
for (const baseCommand of potentialBaseCommands) {
|
||||||
|
const testCommand = `${baseCommand} -version`;
|
||||||
|
try {
|
||||||
|
await runCommand(testCommand); // eslint-disable-line no-await-in-loop
|
||||||
|
console.log(`Confirmed: ImageMagick command '${baseCommand}' is available and operational.`);
|
||||||
|
return baseCommand;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`Error: The command '${baseCommand}' is not found or failed to execute. Detailed error: ${err.message}"`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error([
|
||||||
|
'Unable to locate any operational ImageMagick command.',
|
||||||
|
`Attempted commands were: ${potentialBaseCommands.join(', ')}.`,
|
||||||
|
'Please ensure ImageMagick is correctly installed and accessible.',
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
await main();
|
await main();
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ function getBuildVerificationConfigs() {
|
|||||||
'--electron-unbundled': {
|
'--electron-unbundled': {
|
||||||
printDistDirScriptArgument: '--electron-unbundled',
|
printDistDirScriptArgument: '--electron-unbundled',
|
||||||
filePatterns: [
|
filePatterns: [
|
||||||
/main[/\\]index\.cjs/,
|
/main[/\\]index\.(cjs|mjs|js)/,
|
||||||
/preload[/\\]index\.cjs/,
|
/preload[/\\]index\.(cjs|mjs|js)/,
|
||||||
/renderer[/\\]index\.htm(l)?/,
|
/renderer[/\\]index\.htm(l)?/,
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Description:
|
* Description:
|
||||||
* This script checks if a server, provided as a CLI argument, is up
|
* This script checks if a server, provided as a CLI argument, is up
|
||||||
@@ -6,57 +8,80 @@
|
|||||||
* and will retry a specified number of times.
|
* and will retry a specified number of times.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* node ./scripts/verify-web-server-status.js --url [URL]
|
* node ./scripts/verify-web-server-status.js --url [URL] [--max-retries NUMBER]
|
||||||
*
|
*
|
||||||
* Options:
|
* Options:
|
||||||
* --url URL of the server to check
|
* --url URL of the server to check
|
||||||
|
* --max-retries Maximum number of retry attempts (default: 30)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { get } from 'http';
|
const DEFAULT_MAX_RETRIES = 30;
|
||||||
|
|
||||||
const MAX_RETRIES = 30;
|
|
||||||
const RETRY_DELAY_IN_SECONDS = 3;
|
const RETRY_DELAY_IN_SECONDS = 3;
|
||||||
const URL_PARAMETER_NAME = '--url';
|
const PARAMETER_NAME_URL = '--url';
|
||||||
|
const PARAMETER_NAME_MAX_RETRIES = '--max-retries';
|
||||||
|
|
||||||
function checkServer(currentRetryCount = 1) {
|
async function checkServer(currentRetryCount = 1) {
|
||||||
const serverUrl = getServerUrl();
|
const serverUrl = readRequiredParameterValue(PARAMETER_NAME_URL);
|
||||||
console.log(`Requesting ${serverUrl}...`);
|
const maxRetries = parseNumber(
|
||||||
get(serverUrl, (res) => {
|
readOptionalParameterValue(PARAMETER_NAME_MAX_RETRIES, DEFAULT_MAX_RETRIES),
|
||||||
if (res.statusCode === 200) {
|
);
|
||||||
|
console.log(`🌐 Requesting ${serverUrl}...`);
|
||||||
|
try {
|
||||||
|
const response = await fetch(serverUrl);
|
||||||
|
if (response.status === 200) {
|
||||||
console.log('🎊 Success: The server is up and returned HTTP 200.');
|
console.log('🎊 Success: The server is up and returned HTTP 200.');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Server returned HTTP status code ${res.statusCode}.`);
|
exitWithError(`Server returned unexpected HTTP status code ${response.statusCode}.`);
|
||||||
retry(currentRetryCount);
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error making the request:', error);
|
||||||
|
scheduleNextRetry(maxRetries, currentRetryCount);
|
||||||
}
|
}
|
||||||
}).on('error', (err) => {
|
|
||||||
console.error('Error making the request:', err);
|
|
||||||
retry(currentRetryCount);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function retry(currentRetryCount) {
|
function scheduleNextRetry(maxRetries, currentRetryCount) {
|
||||||
console.log(`Attempt ${currentRetryCount}/${MAX_RETRIES}:`);
|
console.log(`Attempt ${currentRetryCount}/${maxRetries}:`);
|
||||||
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
|
console.log(`Retrying in ${RETRY_DELAY_IN_SECONDS} seconds.`);
|
||||||
|
|
||||||
const remainingTime = (MAX_RETRIES - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
const remainingTime = (maxRetries - currentRetryCount) * RETRY_DELAY_IN_SECONDS;
|
||||||
console.log(`Time remaining before timeout: ${remainingTime}s`);
|
console.log(`Time remaining before timeout: ${remainingTime}s`);
|
||||||
|
|
||||||
if (currentRetryCount < MAX_RETRIES) {
|
if (currentRetryCount < maxRetries) {
|
||||||
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
|
setTimeout(() => checkServer(currentRetryCount + 1), RETRY_DELAY_IN_SECONDS * 1000);
|
||||||
} else {
|
} else {
|
||||||
console.log('Failure: The server at did not return HTTP 200 within the allocated time. Exiting.');
|
exitWithError('The server at did not return HTTP 200 within the allocated time.');
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServerUrl() {
|
function readRequiredParameterValue(parameterName) {
|
||||||
const urlIndex = process.argv.indexOf(URL_PARAMETER_NAME);
|
const parameterValue = readOptionalParameterValue(parameterName);
|
||||||
if (urlIndex === -1 || urlIndex === process.argv.length - 1) {
|
if (parameterValue === undefined) {
|
||||||
console.error(`Parameter "${URL_PARAMETER_NAME}" is not provided.`);
|
exitWithError(`Parameter "${parameterName}" is required but not provided.`);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
return process.argv[urlIndex + 1];
|
return parameterValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
checkServer();
|
function readOptionalParameterValue(parameterName, defaultValue) {
|
||||||
|
const index = process.argv.indexOf(parameterName);
|
||||||
|
if (index === -1 || index === process.argv.length - 1) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
return process.argv[index + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNumber(numberLike) {
|
||||||
|
const number = parseInt(numberLike, 10);
|
||||||
|
if (Number.isNaN(number)) {
|
||||||
|
exitWithError(`Invalid number: ${numberLike}`);
|
||||||
|
}
|
||||||
|
return number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitWithError(message) {
|
||||||
|
console.error(`Failure: ${message}`);
|
||||||
|
console.log('Exiting');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkServer();
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ export type CodeRunErrorType =
|
|||||||
| 'FileWriteError'
|
| 'FileWriteError'
|
||||||
| 'FileReadbackVerificationError'
|
| 'FileReadbackVerificationError'
|
||||||
| 'FilePathGenerationError'
|
| 'FilePathGenerationError'
|
||||||
| 'UnsupportedOperatingSystem'
|
| 'UnsupportedPlatform'
|
||||||
| 'FileExecutionError'
|
|
||||||
| 'DirectoryCreationError'
|
| 'DirectoryCreationError'
|
||||||
| 'UnexpectedError';
|
| 'FilePermissionChangeError'
|
||||||
|
| 'FileExecutionError'
|
||||||
|
| 'ExternalProcessTermination';
|
||||||
|
|
||||||
interface CodeRunStatus {
|
interface CodeRunStatus {
|
||||||
readonly success: boolean;
|
readonly success: boolean;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isFunction } from '@/TypeHelpers';
|
import { isFunction, type ConstructorArguments } from '@/TypeHelpers';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Provides a unified and resilient way to extend errors across platforms.
|
Provides a unified and resilient way to extend errors across platforms.
|
||||||
@@ -12,8 +12,8 @@ import { isFunction } from '@/TypeHelpers';
|
|||||||
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
> https://web.archive.org/web/20230810014143/https://github.com/Microsoft/TypeScript-wiki/blob/main/Breaking-Changes.md#extending-built-ins-like-error-array-and-map-may-no-longer-work
|
||||||
*/
|
*/
|
||||||
export abstract class CustomError extends Error {
|
export abstract class CustomError extends Error {
|
||||||
constructor(message?: string, options?: ErrorOptions) {
|
constructor(...args: ConstructorArguments<typeof Error>) {
|
||||||
super(message, options);
|
super(...args);
|
||||||
|
|
||||||
fixPrototype(this, new.target.prototype);
|
fixPrototype(this, new.target.prototype);
|
||||||
ensureStackTrace(this);
|
ensureStackTrace(this);
|
||||||
|
|||||||
12
src/application/Common/Shuffle.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
/*
|
||||||
|
Shuffle an array of strings, returning a new array with elements in random order.
|
||||||
|
Uses the Fisher-Yates (or Durstenfeld) algorithm.
|
||||||
|
*/
|
||||||
|
export function shuffle<T>(array: readonly T[]): T[] {
|
||||||
|
const shuffledArray = [...array];
|
||||||
|
for (let i = array.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[shuffledArray[i], shuffledArray[j]] = [shuffledArray[j], shuffledArray[i]];
|
||||||
|
}
|
||||||
|
return shuffledArray;
|
||||||
|
}
|
||||||
@@ -1,44 +1,164 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { PlatformTimer } from './PlatformTimer';
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
import type { Timer, TimeoutType } from './Timer';
|
import type { Timer, TimeoutType } from './Timer';
|
||||||
|
|
||||||
export type CallbackType = (..._: readonly unknown[]) => void;
|
export type CallbackType = (..._: readonly unknown[]) => void;
|
||||||
|
|
||||||
export function throttle(
|
export interface ThrottleOptions {
|
||||||
callback: CallbackType,
|
/** Skip the immediate execution of the callback on the first invoke */
|
||||||
waitInMs: number,
|
readonly excludeLeadingCall: boolean;
|
||||||
timer: Timer = PlatformTimer,
|
readonly timer: Timer;
|
||||||
): CallbackType {
|
|
||||||
const throttler = new Throttler(timer, waitInMs, callback);
|
|
||||||
return (...args: unknown[]) => throttler.invoke(...args);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class Throttler {
|
const DefaultOptions: ThrottleOptions = {
|
||||||
private queuedExecutionId: TimeoutType | undefined;
|
excludeLeadingCall: false,
|
||||||
|
timer: PlatformTimer,
|
||||||
|
};
|
||||||
|
|
||||||
private previouslyRun: number;
|
export interface ThrottleFunction {
|
||||||
|
(
|
||||||
|
callback: CallbackType,
|
||||||
|
waitInMs: number,
|
||||||
|
options?: Partial<ThrottleOptions>,
|
||||||
|
): CallbackType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const throttle: ThrottleFunction = (
|
||||||
|
callback: CallbackType,
|
||||||
|
waitInMs: number,
|
||||||
|
options: Partial<ThrottleOptions> = DefaultOptions,
|
||||||
|
): CallbackType => {
|
||||||
|
const defaultedOptions: ThrottleOptions = {
|
||||||
|
...DefaultOptions,
|
||||||
|
...options,
|
||||||
|
};
|
||||||
|
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
|
||||||
|
return (...args: unknown[]) => throttler.invoke(...args);
|
||||||
|
};
|
||||||
|
|
||||||
|
class Throttler {
|
||||||
|
private lastExecutionTime: number | null = null;
|
||||||
|
|
||||||
|
private executionScheduler: DelayedCallbackScheduler;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timer: Timer,
|
|
||||||
private readonly waitInMs: number,
|
private readonly waitInMs: number,
|
||||||
private readonly callback: CallbackType,
|
private readonly callback: CallbackType,
|
||||||
|
private readonly options: ThrottleOptions,
|
||||||
) {
|
) {
|
||||||
if (!waitInMs) { throw new Error('missing delay'); }
|
if (!waitInMs) { throw new Error('missing delay'); }
|
||||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||||
|
this.executionScheduler = new DelayedCallbackScheduler(options.timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public invoke(...args: unknown[]): void {
|
public invoke(...args: unknown[]): void {
|
||||||
const now = this.timer.dateNow();
|
switch (true) {
|
||||||
if (this.queuedExecutionId !== undefined) {
|
case this.isLeadingCallWithinThrottlePeriod(): {
|
||||||
this.timer.clearTimeout(this.queuedExecutionId);
|
if (this.options.excludeLeadingCall) {
|
||||||
this.queuedExecutionId = undefined;
|
this.scheduleNext(args);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!this.previouslyRun || (now - this.previouslyRun >= this.waitInMs)) {
|
this.executeNow(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case this.isAlreadyScheduled(): {
|
||||||
|
this.updateNextScheduled(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case !this.isThrottlePeriodPassed(): {
|
||||||
|
this.scheduleNext(args);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error('Throttle logical error: no conditions for execution or scheduling were met.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private isLeadingCallWithinThrottlePeriod(): boolean {
|
||||||
|
return this.isThrottlePeriodPassed()
|
||||||
|
&& !this.isAlreadyScheduled();
|
||||||
|
}
|
||||||
|
|
||||||
|
private isThrottlePeriodPassed(): boolean {
|
||||||
|
if (this.lastExecutionTime === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const timeSinceLastExecution = this.options.timer.dateNow() - this.lastExecutionTime;
|
||||||
|
const isThrottleTimePassed = timeSinceLastExecution >= this.waitInMs;
|
||||||
|
return isThrottleTimePassed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private isAlreadyScheduled(): boolean {
|
||||||
|
return this.executionScheduler.getNext() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleNext(args: unknown[]): void {
|
||||||
|
if (this.executionScheduler.getNext()) {
|
||||||
|
throw new Error('An execution is already scheduled.');
|
||||||
|
}
|
||||||
|
this.executionScheduler.resetNext(
|
||||||
|
() => this.executeNow(args),
|
||||||
|
this.waitInMs,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateNextScheduled(args: unknown[]): void {
|
||||||
|
const nextScheduled = this.executionScheduler.getNext();
|
||||||
|
if (!nextScheduled) {
|
||||||
|
throw new Error('A non-existent scheduled execution cannot be updated.');
|
||||||
|
}
|
||||||
|
const nextDelay = nextScheduled.scheduledTime - this.dateNow();
|
||||||
|
this.executionScheduler.resetNext(
|
||||||
|
() => this.executeNow(args),
|
||||||
|
nextDelay,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeNow(args: unknown[]): void {
|
||||||
this.callback(...args);
|
this.callback(...args);
|
||||||
this.previouslyRun = now;
|
this.lastExecutionTime = this.dateNow();
|
||||||
} else {
|
}
|
||||||
const nextCall = () => this.invoke(...args);
|
|
||||||
const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun);
|
private dateNow(): number {
|
||||||
this.queuedExecutionId = this.timer.setTimeout(nextCall, nextCallDelayInMs);
|
return this.options.timer.dateNow();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScheduledCallback {
|
||||||
|
readonly scheduleTimeoutId: TimeoutType;
|
||||||
|
readonly scheduledTime: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DelayedCallbackScheduler {
|
||||||
|
private scheduledCallback: ScheduledCallback | null = null;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly timer: Timer,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public getNext(): ScheduledCallback | null {
|
||||||
|
return this.scheduledCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetNext(
|
||||||
|
callback: () => void,
|
||||||
|
delayInMs: number,
|
||||||
|
) {
|
||||||
|
this.clear();
|
||||||
|
this.scheduledCallback = {
|
||||||
|
scheduledTime: this.timer.dateNow() + delayInMs,
|
||||||
|
scheduleTimeoutId: this.timer.setTimeout(() => {
|
||||||
|
this.clear();
|
||||||
|
callback();
|
||||||
|
}, delayInMs),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private clear() {
|
||||||
|
if (this.scheduledCallback === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.timer.clearTimeout(this.scheduledCallback.scheduleTimeoutId);
|
||||||
|
this.scheduledCallback = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import type {
|
|||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import { parseDocs } from './DocumentationParser';
|
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||||
import { parseScript } from './Script/ScriptParser';
|
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||||
|
import { createNodeDataValidator, type NodeDataValidator, type NodeDataValidatorFactory } from './NodeValidation/NodeDataValidator';
|
||||||
|
import { NodeDataType } from './NodeValidation/NodeDataType';
|
||||||
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
|
|
||||||
let categoryIdCounter = 0;
|
let categoryIdCounter = 0;
|
||||||
@@ -14,96 +16,108 @@ let categoryIdCounter = 0;
|
|||||||
export function parseCategory(
|
export function parseCategory(
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
factory: CategoryFactoryType = CategoryFactory,
|
utilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||||
): Category {
|
): Category {
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
context,
|
context,
|
||||||
factory,
|
utilities,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategoryParseContext {
|
interface CategoryParseContext {
|
||||||
readonly categoryData: CategoryData,
|
readonly categoryData: CategoryData;
|
||||||
readonly context: ICategoryCollectionParseContext,
|
readonly context: ICategoryCollectionParseContext;
|
||||||
readonly factory: CategoryFactoryType,
|
readonly parentCategory?: CategoryData;
|
||||||
readonly parentCategory?: CategoryData,
|
readonly utilities: CategoryParserUtilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
function parseCategoryRecursively(
|
||||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
context: CategoryParseContext,
|
||||||
const children: ICategoryChildren = {
|
): Category | never {
|
||||||
subCategories: new Array<Category>(),
|
const validator = ensureValidCategory(context);
|
||||||
subScripts: new Array<Script>(),
|
const children: CategoryChildren = {
|
||||||
|
subcategories: new Array<Category>(),
|
||||||
|
subscripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const data of context.categoryData.children) {
|
for (const data of context.categoryData.children) {
|
||||||
parseNode({
|
parseNode({
|
||||||
nodeData: data,
|
nodeData: data,
|
||||||
children,
|
children,
|
||||||
parent: context.categoryData,
|
parent: context.categoryData,
|
||||||
factory: context.factory,
|
utilities: context.utilities,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
return context.factory(
|
return context.utilities.createCategory({
|
||||||
/* id: */ categoryIdCounter++,
|
id: categoryIdCounter++,
|
||||||
/* name: */ context.categoryData.category,
|
name: context.categoryData.category,
|
||||||
/* docs: */ parseDocs(context.categoryData),
|
docs: context.utilities.parseDocs(context.categoryData),
|
||||||
/* categories: */ children.subCategories,
|
subcategories: children.subcategories,
|
||||||
/* scripts: */ children.subScripts,
|
scripts: children.subscripts,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw context.utilities.wrapError(
|
||||||
|
error,
|
||||||
|
validator.createContextualErrorMessage('Failed to parse category.'),
|
||||||
);
|
);
|
||||||
} catch (err) {
|
}
|
||||||
return new NodeValidator({
|
}
|
||||||
type: NodeType.Category,
|
|
||||||
|
function ensureValidCategory(
|
||||||
|
context: CategoryParseContext,
|
||||||
|
): NodeDataValidator {
|
||||||
|
const category = context.categoryData;
|
||||||
|
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||||
|
type: NodeDataType.Category,
|
||||||
selfNode: context.categoryData,
|
selfNode: context.categoryData,
|
||||||
parentNode: context.parentCategory,
|
parentNode: context.parentCategory,
|
||||||
}).throw(err.message);
|
});
|
||||||
}
|
validator.assertDefined(category);
|
||||||
}
|
validator.assertValidName(category.category);
|
||||||
|
validator.assert(
|
||||||
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
|
() => Boolean(category.children) && category.children.length > 0,
|
||||||
new NodeValidator({
|
|
||||||
type: NodeType.Category,
|
|
||||||
selfNode: category,
|
|
||||||
parentNode: parentCategory,
|
|
||||||
})
|
|
||||||
.assertDefined(category)
|
|
||||||
.assertValidName(category.category)
|
|
||||||
.assert(
|
|
||||||
() => category.children.length > 0,
|
|
||||||
`"${category.category}" has no children.`,
|
`"${category.category}" has no children.`,
|
||||||
);
|
);
|
||||||
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ICategoryChildren {
|
interface CategoryChildren {
|
||||||
subCategories: Category[];
|
readonly subcategories: Category[];
|
||||||
subScripts: Script[];
|
readonly subscripts: Script[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface INodeParseContext {
|
interface NodeParseContext {
|
||||||
readonly nodeData: CategoryOrScriptData;
|
readonly nodeData: CategoryOrScriptData;
|
||||||
readonly children: ICategoryChildren;
|
readonly children: CategoryChildren;
|
||||||
readonly parent: CategoryData;
|
readonly parent: CategoryData;
|
||||||
readonly factory: CategoryFactoryType;
|
|
||||||
readonly context: ICategoryCollectionParseContext;
|
readonly context: ICategoryCollectionParseContext;
|
||||||
|
|
||||||
|
readonly utilities: CategoryParserUtilities;
|
||||||
}
|
}
|
||||||
function parseNode(context: INodeParseContext) {
|
|
||||||
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
|
function parseNode(context: NodeParseContext) {
|
||||||
|
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||||
|
selfNode: context.nodeData,
|
||||||
|
parentNode: context.parent,
|
||||||
|
});
|
||||||
validator.assertDefined(context.nodeData);
|
validator.assertDefined(context.nodeData);
|
||||||
|
validator.assert(
|
||||||
|
() => isCategory(context.nodeData) || isScript(context.nodeData),
|
||||||
|
'Node is neither a category or a script.',
|
||||||
|
);
|
||||||
if (isCategory(context.nodeData)) {
|
if (isCategory(context.nodeData)) {
|
||||||
const subCategory = parseCategoryRecursively({
|
const subCategory = parseCategoryRecursively({
|
||||||
categoryData: context.nodeData,
|
categoryData: context.nodeData,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
factory: context.factory,
|
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
|
utilities: context.utilities,
|
||||||
});
|
});
|
||||||
context.children.subCategories.push(subCategory);
|
context.children.subcategories.push(subCategory);
|
||||||
} else if (isScript(context.nodeData)) {
|
} else { // A script
|
||||||
const script = parseScript(context.nodeData, context.context);
|
const script = context.utilities.parseScript(context.nodeData, context.context);
|
||||||
context.children.subScripts.push(script);
|
context.children.subscripts.push(script);
|
||||||
} else {
|
|
||||||
validator.throw('Node is neither a category or a script.');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,11 +137,35 @@ function hasCall(data: unknown) {
|
|||||||
return hasProperty(data, 'call');
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasProperty(object: unknown, propertyName: string) {
|
function hasProperty(
|
||||||
|
object: unknown,
|
||||||
|
propertyName: string,
|
||||||
|
): object is NonNullable<object> {
|
||||||
|
if (typeof object !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (object === null) { // `typeof object` is `null`
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryFactoryType = (
|
export type CategoryFactory = (
|
||||||
...parameters: ConstructorParameters<typeof Category>) => Category;
|
...parameters: ConstructorParameters<typeof Category>
|
||||||
|
) => ICategory;
|
||||||
|
|
||||||
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);
|
interface CategoryParserUtilities {
|
||||||
|
readonly createCategory: CategoryFactory;
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createValidator: NodeDataValidatorFactory;
|
||||||
|
readonly parseScript: ScriptParser;
|
||||||
|
readonly parseDocs: DocsParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||||
|
createCategory: (...parameters) => new Category(...parameters),
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createValidator: createNodeDataValidator,
|
||||||
|
parseScript,
|
||||||
|
parseDocs,
|
||||||
|
};
|
||||||
|
|||||||
42
src/application/Parser/ContextualError.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
|
|
||||||
|
export interface ErrorWithContextWrapper {
|
||||||
|
(
|
||||||
|
error: Error,
|
||||||
|
additionalContext: string,
|
||||||
|
): Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||||
|
error: Error,
|
||||||
|
additionalContext: string,
|
||||||
|
) => {
|
||||||
|
return (error instanceof ContextualError ? error : new ContextualError(error))
|
||||||
|
.withAdditionalContext(additionalContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
||||||
|
class ContextualError extends CustomError {
|
||||||
|
private readonly additionalContext = new Array<string>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly innerError: Error,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public withAdditionalContext(additionalContext: string): this {
|
||||||
|
this.additionalContext.push(additionalContext);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get message(): string { // toString() is not used when Chromium logs it on console
|
||||||
|
return [
|
||||||
|
'\n',
|
||||||
|
this.innerError.message,
|
||||||
|
'\n',
|
||||||
|
'Additional context:',
|
||||||
|
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
import { isString, isArray } from '@/TypeHelpers';
|
import { isString, isArray } from '@/TypeHelpers';
|
||||||
|
|
||||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
export const parseDocs: DocsParser = (documentable) => {
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
@@ -9,6 +9,12 @@ export function parseDocs(documentable: DocumentableData): readonly string[] {
|
|||||||
let result = new DocumentationContainer();
|
let result = new DocumentationContainer();
|
||||||
result = addDocs(docs, result);
|
result = addDocs(docs, result);
|
||||||
return result.getAll();
|
return result.getAll();
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface DocsParser {
|
||||||
|
(
|
||||||
|
documentable: DocumentableData,
|
||||||
|
): readonly string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addDocs(
|
function addDocs(
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
import { CustomError } from '@/application/Common/CustomError';
|
|
||||||
import { NodeType } from './NodeType';
|
|
||||||
import type { NodeData } from './NodeData';
|
|
||||||
|
|
||||||
export class NodeDataError extends CustomError {
|
|
||||||
constructor(message: string, public readonly context: INodeDataErrorContext) {
|
|
||||||
super(createMessage(message, context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface INodeDataErrorContext {
|
|
||||||
readonly type?: NodeType;
|
|
||||||
readonly selfNode: NodeData;
|
|
||||||
readonly parentNode?: NodeData;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createMessage(errorMessage: string, context: INodeDataErrorContext) {
|
|
||||||
let message = '';
|
|
||||||
if (context.type !== undefined) {
|
|
||||||
message += `${NodeType[context.type]}: `;
|
|
||||||
}
|
|
||||||
message += errorMessage;
|
|
||||||
message += `\n${dump(context)}`;
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dump(context: INodeDataErrorContext): string {
|
|
||||||
const printJson = (obj: unknown) => JSON.stringify(obj, undefined, 2);
|
|
||||||
let output = `Self: ${printJson(context.selfNode)}`;
|
|
||||||
if (context.parentNode) {
|
|
||||||
output += `\nParent: ${printJson(context.parentNode)}`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { CategoryData, ScriptData } from '@/application/collections/';
|
||||||
|
import { NodeDataType } from './NodeDataType';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export type NodeDataErrorContext = {
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
} & (CategoryNodeErrorContext | ScriptNodeErrorContext | UnknownNodeErrorContext);
|
||||||
|
|
||||||
|
export type CategoryNodeErrorContext = {
|
||||||
|
readonly type: NodeDataType.Category;
|
||||||
|
readonly selfNode: CategoryData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptNodeErrorContext = {
|
||||||
|
readonly type: NodeDataType.Script;
|
||||||
|
readonly selfNode: ScriptData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnknownNodeErrorContext = {
|
||||||
|
readonly type?: undefined;
|
||||||
|
readonly selfNode: NodeData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { NodeDataType } from './NodeDataType';
|
||||||
|
import type { NodeDataErrorContext } from './NodeDataErrorContext';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export interface NodeContextErrorMessageCreator {
|
||||||
|
(
|
||||||
|
errorMessage: string,
|
||||||
|
context: NodeDataErrorContext,
|
||||||
|
): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNodeContextErrorMessage: NodeContextErrorMessageCreator = (
|
||||||
|
errorMessage,
|
||||||
|
context,
|
||||||
|
) => {
|
||||||
|
let message = '';
|
||||||
|
if (context.type !== undefined) {
|
||||||
|
message += `${NodeDataType[context.type]}: `;
|
||||||
|
}
|
||||||
|
message += errorMessage;
|
||||||
|
message += `\n${getErrorContextDetails(context)}`;
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getErrorContextDetails(context: NodeDataErrorContext): string {
|
||||||
|
let output = `Self: ${printNodeDataAsJson(context.selfNode)}`;
|
||||||
|
if (context.parentNode) {
|
||||||
|
output += `\nParent: ${printNodeDataAsJson(context.parentNode)}`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printNodeDataAsJson(node: NodeData): string {
|
||||||
|
return JSON.stringify(node, undefined, 2);
|
||||||
|
}
|
||||||
4
src/application/Parser/NodeValidation/NodeDataType.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum NodeDataType {
|
||||||
|
Script,
|
||||||
|
Category,
|
||||||
|
}
|
||||||
69
src/application/Parser/NodeValidation/NodeDataValidator.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { isString } from '@/TypeHelpers';
|
||||||
|
import { type NodeDataErrorContext } from './NodeDataErrorContext';
|
||||||
|
import { createNodeContextErrorMessage, type NodeContextErrorMessageCreator } from './NodeDataErrorContextMessage';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export interface NodeDataValidatorFactory {
|
||||||
|
(context: NodeDataErrorContext): NodeDataValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeDataValidator {
|
||||||
|
assertValidName(nameValue: string): void;
|
||||||
|
assertDefined(
|
||||||
|
node: NodeData | undefined,
|
||||||
|
): asserts node is NonNullable<NodeData> & void;
|
||||||
|
assert(
|
||||||
|
validationPredicate: () => boolean,
|
||||||
|
errorMessage: string,
|
||||||
|
): asserts validationPredicate is (() => true);
|
||||||
|
createContextualErrorMessage(errorMessage: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNodeDataValidator
|
||||||
|
: NodeDataValidatorFactory = (context) => new ContextualNodeDataValidator(context);
|
||||||
|
|
||||||
|
export class ContextualNodeDataValidator implements NodeDataValidator {
|
||||||
|
constructor(
|
||||||
|
private readonly context: NodeDataErrorContext,
|
||||||
|
private readonly createErrorMessage
|
||||||
|
: NodeContextErrorMessageCreator = createNodeContextErrorMessage,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertValidName(nameValue: string): void {
|
||||||
|
this.assert(() => Boolean(nameValue), 'missing name');
|
||||||
|
this.assert(
|
||||||
|
() => isString(nameValue),
|
||||||
|
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertDefined(
|
||||||
|
node: NodeData,
|
||||||
|
): asserts node is NonNullable<NodeData> {
|
||||||
|
this.assert(
|
||||||
|
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||||
|
'missing node data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assert(
|
||||||
|
validationPredicate: () => boolean,
|
||||||
|
errorMessage: string,
|
||||||
|
): asserts validationPredicate is (() => true) {
|
||||||
|
if (!validationPredicate()) {
|
||||||
|
this.throw(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public createContextualErrorMessage(errorMessage: string): string {
|
||||||
|
return this.createErrorMessage(errorMessage, this.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private throw(errorMessage: string): never {
|
||||||
|
throw new Error(
|
||||||
|
this.createContextualErrorMessage(errorMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export enum NodeType {
|
|
||||||
Script,
|
|
||||||
Category,
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
import { type INodeDataErrorContext, NodeDataError } from './NodeDataError';
|
|
||||||
import type { NodeData } from './NodeData';
|
|
||||||
|
|
||||||
export class NodeValidator {
|
|
||||||
constructor(private readonly context: INodeDataErrorContext) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public assertValidName(nameValue: string) {
|
|
||||||
return this
|
|
||||||
.assert(
|
|
||||||
() => Boolean(nameValue),
|
|
||||||
'missing name',
|
|
||||||
)
|
|
||||||
.assert(
|
|
||||||
() => isString(nameValue),
|
|
||||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public assertDefined(node: NodeData) {
|
|
||||||
return this.assert(
|
|
||||||
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
|
||||||
'missing node data',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public assert(validationPredicate: () => boolean, errorMessage: string) {
|
|
||||||
if (!validationPredicate()) {
|
|
||||||
this.throw(errorMessage);
|
|
||||||
}
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public throw(errorMessage: string): never {
|
|
||||||
throw new NodeDataError(errorMessage, this.context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,15 +7,18 @@ import type { IReadOnlyFunctionParameterCollection } from '../../Function/Parame
|
|||||||
import type { IExpression } from './IExpression';
|
import type { IExpression } from './IExpression';
|
||||||
|
|
||||||
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
export type ExpressionEvaluator = (context: IExpressionEvaluationContext) => string;
|
||||||
|
|
||||||
export class Expression implements IExpression {
|
export class Expression implements IExpression {
|
||||||
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
public readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
|
|
||||||
constructor(
|
public readonly position: ExpressionPosition;
|
||||||
public readonly position: ExpressionPosition,
|
|
||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator;
|
||||||
parameters?: IReadOnlyFunctionParameterCollection,
|
|
||||||
) {
|
constructor(parameters: ExpressionInitParameters) {
|
||||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
this.parameters = parameters.parameters ?? new FunctionParameterCollection();
|
||||||
|
this.evaluator = parameters.evaluator;
|
||||||
|
this.position = parameters.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
@@ -26,6 +29,12 @@ export class Expression implements IExpression {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpressionInitParameters {
|
||||||
|
readonly position: ExpressionPosition,
|
||||||
|
readonly evaluator: ExpressionEvaluator,
|
||||||
|
readonly parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
|
}
|
||||||
|
|
||||||
function validateThatAllRequiredParametersAreSatisfied(
|
function validateThatAllRequiredParametersAreSatisfied(
|
||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
|
||||||
export function createPositionFromRegexFullMatch(
|
export interface ExpressionPositionFactory {
|
||||||
|
(
|
||||||
match: RegExpMatchArray,
|
match: RegExpMatchArray,
|
||||||
): ExpressionPosition {
|
): ExpressionPosition
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createPositionFromRegexFullMatch
|
||||||
|
: ExpressionPositionFactory = (match) => {
|
||||||
const startPos = match.index;
|
const startPos = match.index;
|
||||||
if (startPos === undefined) {
|
if (startPos === undefined) {
|
||||||
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||||
@@ -13,4 +18,4 @@ export function createPositionFromRegexFullMatch(
|
|||||||
}
|
}
|
||||||
const endPos = startPos + fullMatch.length;
|
const endPos = startPos + fullMatch.length;
|
||||||
return new ExpressionPosition(startPos, endPos);
|
return new ExpressionPosition(startPos, endPos);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import { WithParser } from '../SyntaxParsers/WithParser';
|
|||||||
import type { IExpression } from '../Expression/IExpression';
|
import type { IExpression } from '../Expression/IExpression';
|
||||||
import type { IExpressionParser } from './IExpressionParser';
|
import type { IExpressionParser } from './IExpressionParser';
|
||||||
|
|
||||||
const Parsers = [
|
const Parsers: readonly IExpressionParser[] = [
|
||||||
new ParameterSubstitutionParser(),
|
new ParameterSubstitutionParser(),
|
||||||
new WithParser(),
|
new WithParser(),
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
|
|||||||
@@ -1,53 +1,127 @@
|
|||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||||
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
import type { IExpressionParser } from '../IExpressionParser';
|
import type { IExpressionParser } from '../IExpressionParser';
|
||||||
import type { IExpression } from '../../Expression/IExpression';
|
import type { IExpression } from '../../Expression/IExpression';
|
||||||
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
|
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
|
export interface RegexParserUtilities {
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createPosition: ExpressionPositionFactory;
|
||||||
|
readonly createExpression: ExpressionFactory;
|
||||||
|
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
|
|
||||||
|
public constructor(
|
||||||
|
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
return Array.from(this.findRegexExpressions(code));
|
return Array.from(this.findRegexExpressions(code));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract buildExpression(match: RegExpMatchArray): IPrimitiveExpression;
|
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
|
||||||
|
|
||||||
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
private* findRegexExpressions(code: string): Iterable<IExpression> {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('missing code');
|
throw new Error(
|
||||||
|
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const matches = code.matchAll(this.regex);
|
const createErrorContext = (message: string): ErrorContext => ({ code, errorMessage: message });
|
||||||
|
const matches = this.doOrRethrow(
|
||||||
|
() => code.matchAll(this.regex),
|
||||||
|
createErrorContext('Failed to match regex.'),
|
||||||
|
);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.doOrRethrow(
|
||||||
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
() => this.buildExpression(match),
|
||||||
const parameters = createParameters(primitiveExpression);
|
createErrorContext('Failed to build expression.'),
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
);
|
||||||
|
const position = this.doOrRethrow(
|
||||||
|
() => this.utilities.createPosition(match),
|
||||||
|
createErrorContext('Failed to create position.'),
|
||||||
|
);
|
||||||
|
const parameters = this.doOrRethrow(
|
||||||
|
() => createParameters(
|
||||||
|
primitiveExpression,
|
||||||
|
this.utilities.createParameterCollection(),
|
||||||
|
),
|
||||||
|
createErrorContext('Failed to create parameters.'),
|
||||||
|
);
|
||||||
|
const expression = this.doOrRethrow(
|
||||||
|
() => this.utilities.createExpression({
|
||||||
|
position,
|
||||||
|
evaluator: primitiveExpression.evaluator,
|
||||||
|
parameters,
|
||||||
|
}),
|
||||||
|
createErrorContext('Failed to create expression.'),
|
||||||
|
);
|
||||||
yield expression;
|
yield expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
|
private doOrRethrow<T>(
|
||||||
|
action: () => T,
|
||||||
|
context: ErrorContext,
|
||||||
|
): T {
|
||||||
try {
|
try {
|
||||||
return action();
|
return action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
|
throw this.utilities.wrapError(
|
||||||
}
|
error,
|
||||||
|
this.buildErrorMessageWithContext(context),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildErrorMessageWithContext(context: ErrorContext): string {
|
||||||
|
return [
|
||||||
|
context.errorMessage,
|
||||||
|
`Class name: ${this.constructor.name}`,
|
||||||
|
`Regex pattern used: ${this.regex}`,
|
||||||
|
`Code: ${context.code}`,
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ErrorContext {
|
||||||
|
readonly errorMessage: string,
|
||||||
|
readonly code: string,
|
||||||
|
}
|
||||||
|
|
||||||
function createParameters(
|
function createParameters(
|
||||||
expression: IPrimitiveExpression,
|
expression: PrimitiveExpression,
|
||||||
): FunctionParameterCollection {
|
parameterCollection: IFunctionParameterCollection,
|
||||||
|
): IReadOnlyFunctionParameterCollection {
|
||||||
return (expression.parameters || [])
|
return (expression.parameters || [])
|
||||||
.reduce((parameters, parameter) => {
|
.reduce((parameters, parameter) => {
|
||||||
parameters.addParameter(parameter);
|
parameters.addParameter(parameter);
|
||||||
return parameters;
|
return parameters;
|
||||||
}, new FunctionParameterCollection());
|
}, parameterCollection);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPrimitiveExpression {
|
export interface PrimitiveExpression {
|
||||||
evaluator: ExpressionEvaluator;
|
readonly evaluator: ExpressionEvaluator;
|
||||||
parameters?: readonly IFunctionParameter[];
|
readonly parameters?: readonly IFunctionParameter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ExpressionFactory {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof Expression>
|
||||||
|
): IExpression;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultRegexParserUtilities: RegexParserUtilities = {
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createPosition: createPositionFromRegexFullMatch,
|
||||||
|
createExpression: (...args) => new Expression(...args),
|
||||||
|
createParameterCollection: createFunctionParameterCollection,
|
||||||
|
};
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ function getLines(code: string): string[] {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules#here-strings
|
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
||||||
*/
|
*/
|
||||||
function mergeHereStrings(code: string) {
|
function mergeHereStrings(code: string) {
|
||||||
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
const regex = /@(['"])\s*(?:\r\n|\r|\n)((.|\n|\r)+?)(\r\n|\r|\n)\1@/g;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { RegexParser, type IPrimitiveExpression } from '../Parser/Regex/RegexParser';
|
import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
export class ParameterSubstitutionParser extends RegexParser {
|
export class ParameterSubstitutionParser extends RegexParser {
|
||||||
@@ -12,7 +12,7 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
.expectExpressionEnd()
|
.expectExpressionEnd()
|
||||||
.buildRegExp();
|
.buildRegExp();
|
||||||
|
|
||||||
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
|
protected buildExpression(match: RegExpMatchArray): PrimitiveExpression {
|
||||||
const parameterName = match[1];
|
const parameterName = match[1];
|
||||||
const pipeline = match[2];
|
const pipeline = match[2];
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export class FunctionCallArgument implements IFunctionCallArgument {
|
|||||||
) {
|
) {
|
||||||
ensureValidParameterName(parameterName);
|
ensureValidParameterName(parameterName);
|
||||||
if (!argumentValue) {
|
if (!argumentValue) {
|
||||||
throw new Error(`missing argument value for "${parameterName}"`);
|
throw new Error(`Missing argument value for the parameter "${parameterName}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ function throwIfUnexpectedParametersExist(
|
|||||||
// eslint-disable-next-line prefer-template
|
// eslint-disable-next-line prefer-template
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: `
|
`Function "${functionName}" has unexpected parameter(s) provided: `
|
||||||
+ `"${unexpectedParameters.join('", "')}"`
|
+ `"${unexpectedParameters.join('", "')}"`
|
||||||
+ '. Expected parameter(s): '
|
+ '.\nExpected parameter(s): '
|
||||||
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'),
|
+ (expectedParameters.length ? `"${expectedParameters.join('", "')}".` : 'none'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,11 +6,14 @@ import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/
|
|||||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import type { ArgumentCompiler } from './ArgumentCompiler';
|
import type { ArgumentCompiler } from './ArgumentCompiler';
|
||||||
|
|
||||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||||
|
private readonly wrapError: ErrorWithContextWrapper
|
||||||
|
= wrapErrorWithAdditionalContext,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
public createCompiledNestedCall(
|
public createCompiledNestedCall(
|
||||||
@@ -22,18 +25,26 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
|||||||
nestedFunction,
|
nestedFunction,
|
||||||
parentFunction.args,
|
parentFunction.args,
|
||||||
context,
|
context,
|
||||||
this.expressionsCompiler,
|
{
|
||||||
|
expressionsCompiler: this.expressionsCompiler,
|
||||||
|
wrapError: this.wrapError,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
||||||
return compiledCall;
|
return compiledCall;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ArgumentCompilationUtilities {
|
||||||
|
readonly expressionsCompiler: IExpressionsCompiler,
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
}
|
||||||
|
|
||||||
function compileNestedFunctionArguments(
|
function compileNestedFunctionArguments(
|
||||||
nestedFunction: FunctionCall,
|
nestedFunction: FunctionCall,
|
||||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
context: FunctionCallCompilationContext,
|
context: FunctionCallCompilationContext,
|
||||||
expressionsCompiler: IExpressionsCompiler,
|
utilities: ArgumentCompilationUtilities,
|
||||||
): IReadOnlyFunctionCallArgumentCollection {
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
const requiredParameterNames = context
|
const requiredParameterNames = context
|
||||||
.allFunctions
|
.allFunctions
|
||||||
@@ -47,7 +58,7 @@ function compileNestedFunctionArguments(
|
|||||||
paramName,
|
paramName,
|
||||||
nestedFunction,
|
nestedFunction,
|
||||||
parentFunctionArgs,
|
parentFunctionArgs,
|
||||||
expressionsCompiler,
|
utilities,
|
||||||
),
|
),
|
||||||
}))
|
}))
|
||||||
// Filter out arguments with absent values
|
// Filter out arguments with absent values
|
||||||
@@ -89,13 +100,13 @@ function compileArgument(
|
|||||||
parameterName: string,
|
parameterName: string,
|
||||||
nestedFunction: FunctionCall,
|
nestedFunction: FunctionCall,
|
||||||
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
parentFunctionArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
expressionsCompiler: IExpressionsCompiler,
|
utilities: ArgumentCompilationUtilities,
|
||||||
): string {
|
): string {
|
||||||
try {
|
try {
|
||||||
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
const { argumentValue: codeInArgument } = nestedFunction.args.getArgument(parameterName);
|
||||||
return expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
return utilities.expressionsCompiler.compileExpressions(codeInArgument, parentFunctionArgs);
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
throw new AggregateError([err], `Error when compiling argument for "${parameterName}"`);
|
throw utilities.wrapError(error, `Error when compiling argument for "${parameterName}"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,21 @@
|
|||||||
import { type CallFunctionBody, FunctionBodyType, type ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import {
|
||||||
|
type CallFunctionBody, FunctionBodyType,
|
||||||
|
type ISharedFunction,
|
||||||
|
} from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
||||||
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
||||||
|
|
||||||
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly argumentCompiler: ArgumentCompiler = new NestedFunctionArgumentCompiler(),
|
private readonly argumentCompiler: ArgumentCompiler
|
||||||
|
= new NestedFunctionArgumentCompiler(),
|
||||||
|
private readonly wrapError: ErrorWithContextWrapper
|
||||||
|
= wrapErrorWithAdditionalContext,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,8 +36,11 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
const compiledNestedCall = context.singleCallCompiler
|
const compiledNestedCall = context.singleCallCompiler
|
||||||
.compileSingleCall(compiledParentCall, context);
|
.compileSingleCall(compiledParentCall, context);
|
||||||
return compiledNestedCall;
|
return compiledNestedCall;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
throw new AggregateError([err], `Error with call to "${nestedCall.functionName}" function from "${callToFunction.functionName}" function`);
|
throw this.wrapError(
|
||||||
|
error,
|
||||||
|
`Failed to call '${nestedCall.functionName}' (callee function) from '${callToFunction.functionName}' (caller function).`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}).flat();
|
}).flat();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FunctionParameterCollection } from './FunctionParameterCollection';
|
||||||
|
import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
|
||||||
|
|
||||||
|
export interface FunctionParameterCollectionFactory {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof FunctionParameterCollection>
|
||||||
|
): IFunctionParameterCollection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createFunctionParameterCollection: FunctionParameterCollectionFactory = (...args) => {
|
||||||
|
return new FunctionParameterCollection(...args);
|
||||||
|
};
|
||||||
@@ -15,7 +15,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
|||||||
if (!name) { throw Error('missing function name'); }
|
if (!name) { throw Error('missing function name'); }
|
||||||
const func = this.functionsByName.get(name);
|
const func = this.functionsByName.get(name);
|
||||||
if (!func) {
|
if (!func) {
|
||||||
throw new Error(`called function is not defined "${name}"`);
|
throw new Error(`Called function is not defined: "${name}"`);
|
||||||
}
|
}
|
||||||
return func;
|
return func;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
|
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
|
||||||
|
CallInstruction, ParameterDefinitionData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
@@ -7,20 +8,30 @@ import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmp
|
|||||||
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { FunctionParameter } from './Parameter/FunctionParameter';
|
import { FunctionParameter } from './Parameter/FunctionParameter';
|
||||||
import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection';
|
|
||||||
import { parseFunctionCalls } from './Call/FunctionCallParser';
|
import { parseFunctionCalls } from './Call/FunctionCallParser';
|
||||||
|
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory';
|
||||||
import type { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
import type { ISharedFunctionCollection } from './ISharedFunctionCollection';
|
||||||
import type { ISharedFunctionsParser } from './ISharedFunctionsParser';
|
import type { ISharedFunctionsParser } from './ISharedFunctionsParser';
|
||||||
import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
import type { ISharedFunction } from './ISharedFunction';
|
import type { ISharedFunction } from './ISharedFunction';
|
||||||
|
|
||||||
|
const DefaultSharedFunctionsParsingUtilities: SharedFunctionsParsingUtilities = {
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createParameter: (...args) => new FunctionParameter(...args),
|
||||||
|
codeValidator: CodeValidator.instance,
|
||||||
|
createParameterCollection: createFunctionParameterCollection,
|
||||||
|
};
|
||||||
|
|
||||||
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
export class SharedFunctionsParser implements ISharedFunctionsParser {
|
||||||
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
|
||||||
|
|
||||||
constructor(private readonly codeValidator: ICodeValidator = CodeValidator.instance) { }
|
constructor(
|
||||||
|
private readonly utilities = DefaultSharedFunctionsParsingUtilities,
|
||||||
|
) { }
|
||||||
|
|
||||||
public parseFunctions(
|
public parseFunctions(
|
||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
@@ -32,7 +43,7 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
return functions
|
return functions
|
||||||
.map((func) => parseFunction(func, syntax, this.codeValidator))
|
.map((func) => parseFunction(func, syntax, this.utilities))
|
||||||
.reduce((acc, func) => {
|
.reduce((acc, func) => {
|
||||||
acc.addFunction(func);
|
acc.addFunction(func);
|
||||||
return acc;
|
return acc;
|
||||||
@@ -40,15 +51,26 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SharedFunctionsParsingUtilities {
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createParameter: FunctionParameterFactory;
|
||||||
|
readonly codeValidator: ICodeValidator;
|
||||||
|
readonly createParameterCollection: FunctionParameterCollectionFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FunctionParameterFactory = (
|
||||||
|
...args: ConstructorParameters<typeof FunctionParameter>
|
||||||
|
) => FunctionParameter;
|
||||||
|
|
||||||
function parseFunction(
|
function parseFunction(
|
||||||
data: FunctionData,
|
data: FunctionData,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
validator: ICodeValidator,
|
utilities: SharedFunctionsParsingUtilities,
|
||||||
): ISharedFunction {
|
): ISharedFunction {
|
||||||
const { name } = data;
|
const { name } = data;
|
||||||
const parameters = parseParameters(data);
|
const parameters = parseParameters(data, utilities);
|
||||||
if (hasCode(data)) {
|
if (hasCode(data)) {
|
||||||
validateCode(data, syntax, validator);
|
validateCode(data, syntax, utilities.codeValidator);
|
||||||
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
|
||||||
}
|
}
|
||||||
// Has call
|
// Has call
|
||||||
@@ -71,22 +93,38 @@ function validateCode(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(
|
||||||
|
data: FunctionData,
|
||||||
|
utilities: SharedFunctionsParsingUtilities,
|
||||||
|
): IReadOnlyFunctionParameterCollection {
|
||||||
return (data.parameters || [])
|
return (data.parameters || [])
|
||||||
.map((parameter) => {
|
.map((parameter) => createFunctionParameter(
|
||||||
try {
|
data.name,
|
||||||
return new FunctionParameter(
|
parameter,
|
||||||
parameter.name,
|
utilities,
|
||||||
parameter.optional || false,
|
))
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
throw new Error(`"${data.name}": ${err.message}`);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.reduce((parameters, parameter) => {
|
.reduce((parameters, parameter) => {
|
||||||
parameters.addParameter(parameter);
|
parameters.addParameter(parameter);
|
||||||
return parameters;
|
return parameters;
|
||||||
}, new FunctionParameterCollection());
|
}, utilities.createParameterCollection());
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFunctionParameter(
|
||||||
|
functionName: string,
|
||||||
|
parameterData: ParameterDefinitionData,
|
||||||
|
utilities: SharedFunctionsParsingUtilities,
|
||||||
|
): FunctionParameter {
|
||||||
|
try {
|
||||||
|
return utilities.createParameter(
|
||||||
|
parameterData.name,
|
||||||
|
parameterData.optional || false,
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
throw utilities.wrapError(
|
||||||
|
err,
|
||||||
|
`Failed to create parameter: ${parameterData.name} for function "${functionName}"`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: FunctionData): data is CodeFunctionData {
|
function hasCode(data: FunctionData): data is CodeFunctionData {
|
||||||
@@ -98,6 +136,7 @@ function hasCall(data: FunctionData): data is CallFunctionData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
|
ensureNoUnnamedFunctions(functions);
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
ensureEitherCallOrCodeIsDefined(functions);
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
ensureNoDuplicateCode(functions);
|
ensureNoDuplicateCode(functions);
|
||||||
@@ -108,6 +147,16 @@ function printList(list: readonly string[]): string {
|
|||||||
return `"${list.join('","')}"`;
|
return `"${list.join('","')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ensureNoUnnamedFunctions(functions: readonly FunctionData[]) {
|
||||||
|
const functionsWithoutNames = functions.filter(
|
||||||
|
(func) => !func.name || func.name.trim().length === 0,
|
||||||
|
);
|
||||||
|
if (functionsWithoutNames.length) {
|
||||||
|
const invalidFunctions = functionsWithoutNames.map((f) => JSON.stringify(f));
|
||||||
|
throw new Error(`Some function(s) have no names:\n${invalidFunctions.join('\n')}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||||
// Ensure functions do not define both call and code
|
// Ensure functions do not define both call and code
|
||||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import { createScriptCode, type ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||||
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
import { parseFunctionCalls } from './Function/Call/FunctionCallParser';
|
||||||
@@ -23,6 +24,8 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
|
private readonly wrapError: ErrorWithContextWrapper = wrapErrorWithAdditionalContext,
|
||||||
|
private readonly scriptCodeFactory: ScriptCodeFactory = createScriptCode,
|
||||||
) {
|
) {
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
@@ -39,12 +42,12 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||||
validateCompiledCode(compiledCode, this.codeValidator);
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
return new ScriptCode(
|
return this.scriptCodeFactory(
|
||||||
compiledCode.code,
|
compiledCode.code,
|
||||||
compiledCode.revertCode,
|
compiledCode.revertCode,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw Error(`Script "${script.name}" ${error.message}`);
|
throw this.wrapError(error, `Failed to compile script: ${script.name}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,37 +4,52 @@ import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syn
|
|||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||||
import type { IScriptCode } from '@/domain/IScriptCode';
|
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
|
||||||
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator';
|
||||||
import { parseDocs } from '../DocumentationParser';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import type { ScriptCodeFactory } from '@/domain/ScriptCodeFactory';
|
||||||
|
import { createScriptCode } from '@/domain/ScriptCodeFactory';
|
||||||
|
import type { IScript } from '@/domain/IScript';
|
||||||
|
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||||
import { createEnumParser, type IEnumParser } from '../../Common/Enum';
|
import { createEnumParser, type IEnumParser } from '../../Common/Enum';
|
||||||
import { NodeType } from '../NodeValidation/NodeType';
|
import { NodeDataType } from '../NodeValidation/NodeDataType';
|
||||||
import { NodeValidator } from '../NodeValidation/NodeValidator';
|
import { createNodeDataValidator, type NodeDataValidator, type NodeDataValidatorFactory } from '../NodeValidation/NodeDataValidator';
|
||||||
import { CodeValidator } from './Validation/CodeValidator';
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
|
||||||
export function parseScript(
|
export interface ScriptParser {
|
||||||
|
(
|
||||||
data: ScriptData,
|
data: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
levelParser = createEnumParser(RecommendationLevel),
|
utilities?: ScriptParserUtilities,
|
||||||
scriptFactory: ScriptFactoryType = ScriptFactory,
|
): IScript;
|
||||||
codeValidator: ICodeValidator = CodeValidator.instance,
|
}
|
||||||
): Script {
|
|
||||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
export const parseScript: ScriptParser = (
|
||||||
|
data,
|
||||||
|
context,
|
||||||
|
utilities = DefaultScriptParserUtilities,
|
||||||
|
) => {
|
||||||
|
const validator = utilities.createValidator({
|
||||||
|
type: NodeDataType.Script,
|
||||||
|
selfNode: data,
|
||||||
|
});
|
||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
try {
|
try {
|
||||||
const script = scriptFactory(
|
const script = utilities.createScript({
|
||||||
/* name: */ data.name,
|
name: data.name,
|
||||||
/* code: */ parseCode(data, context, codeValidator),
|
code: parseCode(data, context, utilities.codeValidator, utilities.createCode),
|
||||||
/* docs: */ parseDocs(data),
|
docs: utilities.parseDocs(data),
|
||||||
/* level: */ parseLevel(data.recommend, levelParser),
|
level: parseLevel(data.recommend, utilities.levelParser),
|
||||||
);
|
});
|
||||||
return script;
|
return script;
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
return validator.throw(err.message);
|
throw utilities.wrapError(
|
||||||
}
|
error,
|
||||||
|
validator.createContextualErrorMessage('Failed to parse script.'),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
level: string | undefined,
|
level: string | undefined,
|
||||||
@@ -50,18 +65,19 @@ function parseCode(
|
|||||||
script: ScriptData,
|
script: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
codeValidator: ICodeValidator,
|
codeValidator: ICodeValidator,
|
||||||
|
createCode: ScriptCodeFactory,
|
||||||
): IScriptCode {
|
): IScriptCode {
|
||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||||
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
|
const code = createCode(codeScript.code, codeScript.revertCode);
|
||||||
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHardcodedCodeWithoutCalls(
|
function validateHardcodedCodeWithoutCalls(
|
||||||
scriptCode: ScriptCode,
|
scriptCode: IScriptCode,
|
||||||
validator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
) {
|
) {
|
||||||
@@ -77,25 +93,48 @@ function validateHardcodedCodeWithoutCalls(
|
|||||||
|
|
||||||
function validateScript(
|
function validateScript(
|
||||||
script: ScriptData,
|
script: ScriptData,
|
||||||
validator: NodeValidator,
|
validator: NodeDataValidator,
|
||||||
): asserts script is NonNullable<ScriptData> {
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator
|
validator.assertDefined(script);
|
||||||
.assertDefined(script)
|
validator.assertValidName(script.name);
|
||||||
.assertValidName(script.name)
|
validator.assert(
|
||||||
.assert(
|
|
||||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||||
'Neither "call" or "code" is defined.',
|
'Neither "call" or "code" is defined.',
|
||||||
)
|
);
|
||||||
.assert(
|
validator.assert(
|
||||||
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||||
'Both "call" and "code" are defined.',
|
'Both "call" and "code" are defined.',
|
||||||
)
|
);
|
||||||
.assert(
|
validator.assert(
|
||||||
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||||
'Both "call" and "revertCode" are defined.',
|
'Both "call" and "revertCode" are defined.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScriptFactoryType = (...parameters: ConstructorParameters<typeof Script>) => Script;
|
interface ScriptParserUtilities {
|
||||||
|
readonly levelParser: IEnumParser<RecommendationLevel>;
|
||||||
|
readonly createScript: ScriptFactory;
|
||||||
|
readonly codeValidator: ICodeValidator;
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createValidator: NodeDataValidatorFactory;
|
||||||
|
readonly createCode: ScriptCodeFactory;
|
||||||
|
readonly parseDocs: DocsParser;
|
||||||
|
}
|
||||||
|
|
||||||
const ScriptFactory: ScriptFactoryType = (...parameters) => new Script(...parameters);
|
export type ScriptFactory = (
|
||||||
|
...parameters: ConstructorParameters<typeof Script>
|
||||||
|
) => IScript;
|
||||||
|
|
||||||
|
const createScript: ScriptFactory = (...parameters) => {
|
||||||
|
return new Script(...parameters);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DefaultScriptParserUtilities: ScriptParserUtilities = {
|
||||||
|
levelParser: createEnumParser(RecommendationLevel),
|
||||||
|
createScript,
|
||||||
|
codeValidator: CodeValidator.instance,
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createValidator: createNodeDataValidator,
|
||||||
|
createCode: createScriptCode,
|
||||||
|
parseDocs,
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expres
|
|||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
|
import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||||
import type { ICodeSubstituter } from './ICodeSubstituter';
|
import type { ICodeSubstituter } from './ICodeSubstituter';
|
||||||
|
|
||||||
export class CodeSubstituter implements ICodeSubstituter {
|
export class CodeSubstituter implements ICodeSubstituter {
|
||||||
@@ -29,7 +30,9 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createSubstituteCompiler(): IExpressionsCompiler {
|
function createSubstituteCompiler(): IExpressionsCompiler {
|
||||||
const parsers = [new ParameterSubstitutionParser()];
|
const parsers: readonly IExpressionParser[] = [
|
||||||
|
new ParameterSubstitutionParser(),
|
||||||
|
] as const;
|
||||||
const parser = new CompositeExpressionParser(parsers);
|
const parser = new CompositeExpressionParser(parsers);
|
||||||
const expressionCompiler = new ExpressionsCompiler(parser);
|
const expressionCompiler = new ExpressionsCompiler(parser);
|
||||||
return expressionCompiler;
|
return expressionCompiler;
|
||||||
|
|||||||
@@ -74,7 +74,7 @@ actions:
|
|||||||
- [tcsh source code](https://web.archive.org/web/20221029212024/https://github.com/tcsh-org/tcsh).
|
- [tcsh source code](https://web.archive.org/web/20221029212024/https://github.com/tcsh-org/tcsh).
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029134950/https://linux.die.net/man/1/tcsh "tcsh(1) - Linux man page | linux.die.net"
|
[1]: https://web.archive.org/web/20221029134950/https://linux.die.net/man/1/tcsh "tcsh(1) - Linux man page | linux.die.net"
|
||||||
[2]: https://web.archive.org/web/20221029135041/https://books.google.com/books?id=LyDP5b2xzaMC&pg=PA56 "Sams Teach Yourself FreeBSD in 24 Hours - Michael Urban, Brian Tiemann - Google Books | books.google.com"
|
[2]: https://web.archive.org/web/20221029135007/https://books.google.com/books?id=LyDP5b2xzaMC&pg=PA56#v=onepage&q&f=false "Sams Teach Yourself FreeBSD in 24 Hours - Michael Urban, Brian Tiemann - Google Books | books.google.com"
|
||||||
call:
|
call:
|
||||||
function: DeleteFileFromUserAndRootHome
|
function: DeleteFileFromUserAndRootHome
|
||||||
parameters:
|
parameters:
|
||||||
@@ -184,7 +184,7 @@ actions:
|
|||||||
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
||||||
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
@@ -202,7 +202,7 @@ actions:
|
|||||||
> - This action is irreversible. Deleted script files cannot be retrieved.
|
> - This action is irreversible. Deleted script files cannot be retrieved.
|
||||||
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -223,7 +223,7 @@ actions:
|
|||||||
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
||||||
> - Logs can contain valuable information for technical support should you need assistance.
|
> - Logs can contain valuable information for technical support should you need assistance.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -1733,7 +1733,7 @@ actions:
|
|||||||
See also:
|
See also:
|
||||||
- [Source code for the Ubuntu Report tool | github.com](https://web.archive.org/web/20221029221854/https://github.com/ubuntu/ubuntu-report/)
|
- [Source code for the Ubuntu Report tool | github.com](https://web.archive.org/web/20221029221854/https://github.com/ubuntu/ubuntu-report/)
|
||||||
- [Statistics gathered and visualized | ubuntu.com/desktop/statistics](https://web.archive.org/web/20221029221910/https://ubuntu.com/desktop/statistics)
|
- [Statistics gathered and visualized | ubuntu.com/desktop/statistics](https://web.archive.org/web/20221029221910/https://ubuntu.com/desktop/statistics)
|
||||||
- [ubuntu-devel mailing list thread where ubuntu-report was first proposed, | lists.ubuntu.com ](https://web.archive.org/web/20221029221924/https://lists.ubuntu.com/archives/ubuntu-devel/2018-February/040139.html)
|
- [ubuntu-devel mailing list thread where ubuntu-report was first proposed | lists.ubuntu.com](https://web.archive.org/web/20221029162523/https://lists.ubuntu.com/archives/ubuntu-devel/2018-February/040139.html)
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029162505/https://github.com/ubuntu/ubuntu-report/blob/30e902ebc17e4e10d83392d7cd3dc05fc9e35cc4/README.md "ubuntu-report/README.md at master · ubuntu/ubuntu-report | github.com"
|
[1]: https://web.archive.org/web/20221029162505/https://github.com/ubuntu/ubuntu-report/blob/30e902ebc17e4e10d83392d7cd3dc05fc9e35cc4/README.md "ubuntu-report/README.md at master · ubuntu/ubuntu-report | github.com"
|
||||||
[2]: https://web.archive.org/web/20221029162538/https://github.com/ubuntu/ubuntu-report/blob/8e6030ff9bbeacacf41a9b58ea638a5c9a6f864d/README.md "More diagnostics data from desktop | lists.ubuntu.com"
|
[2]: https://web.archive.org/web/20221029162538/https://github.com/ubuntu/ubuntu-report/blob/8e6030ff9bbeacacf41a9b58ea638a5c9a6f864d/README.md "More diagnostics data from desktop | lists.ubuntu.com"
|
||||||
@@ -1974,10 +1974,10 @@ actions:
|
|||||||
|
|
||||||
Read more about Zeitgeist:
|
Read more about Zeitgeist:
|
||||||
|
|
||||||
- [Official website | zeitgeist.freedesktop.org](https://web.archive.org/web/20221029222739/https://zeitgeist.freedesktop.org/)
|
- [Official website | zeitgeist.freedesktop.org](https://web.archive.org/web/20221029150843/https://zeitgeist.freedesktop.org/)
|
||||||
- [Wikipedia article | en.wikipedia.org](https://web.archive.org/web/20221029222921/https://en.wikipedia.org/wiki/Zeitgeist_%28free_software%29)
|
- [Wikipedia article | en.wikipedia.org](https://web.archive.org/web/20221029222921/https://en.wikipedia.org/wiki/Zeitgeist_%28free_software%29)
|
||||||
- [Launchpad project page | launchpad.net](https://web.archive.org/web/20221029223026/https://launchpad.net/zeitgeist/)
|
- [Launchpad project page | launchpad.net](https://web.archive.org/web/20221029223026/https://launchpad.net/zeitgeist/)
|
||||||
- [ArchWiki article | wiki.archlinux.org](https://web.archive.org/web/20221029223033/https://wiki.archlinux.org/title/Zeitgeist)
|
- [ArchWiki article | wiki.archlinux.org](https://web.archive.org/web/20221029164539/https://wiki.archlinux.org/title/Zeitgeist)
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029163704/https://packages.debian.org/en/sid/libdevel/libzeitgeist-2.0-dev "libzeitgeist-2.0-dev | Debian Packages | packages.debian.org"
|
[1]: https://web.archive.org/web/20221029163704/https://packages.debian.org/en/sid/libdevel/libzeitgeist-2.0-dev "libzeitgeist-2.0-dev | Debian Packages | packages.debian.org"
|
||||||
[2]: https://web.archive.org/web/20221029163817/https://gitlab.gnome.org/crvi/gnome-activity-journal "crvi / GNOME Activity Journal · GitLab | gitlab.gnome.org"
|
[2]: https://web.archive.org/web/20221029163817/https://gitlab.gnome.org/crvi/gnome-activity-journal "crvi / GNOME Activity Journal · GitLab | gitlab.gnome.org"
|
||||||
@@ -2072,8 +2072,8 @@ actions:
|
|||||||
[1]: https://web.archive.org/web/20221029165307/https://packages.fedoraproject.org/pkgs/zeitgeist/zeitgeist/index.html "zeitgeist - Fedora Packages | packages.fedoraproject.org"
|
[1]: https://web.archive.org/web/20221029165307/https://packages.fedoraproject.org/pkgs/zeitgeist/zeitgeist/index.html "zeitgeist - Fedora Packages | packages.fedoraproject.org"
|
||||||
[2]: https://web.archive.org/web/20221029165603/https://archlinux.org/packages/extra/x86_64/zeitgeist/ "Arch Linux - zeitgeist 1.0.4-1 (x86_64) | archlinux.org"
|
[2]: https://web.archive.org/web/20221029165603/https://archlinux.org/packages/extra/x86_64/zeitgeist/ "Arch Linux - zeitgeist 1.0.4-1 (x86_64) | archlinux.org"
|
||||||
[3]: https://web.archive.org/web/20221029165609/https://packages.debian.org/search?keywords=zeitgeist-core "Debian -- Package Search Results -- zeitgeist-core | packages.debian.org"
|
[3]: https://web.archive.org/web/20221029165609/https://packages.debian.org/search?keywords=zeitgeist-core "Debian -- Package Search Results -- zeitgeist-core | packages.debian.org"
|
||||||
[4]: https://web.archive.org/web/20221029165714/https://releases.ubuntu.com/xenial/ubuntu-16.04.6-desktop-i386.manifest "List of sofware packags shipped with Ubuntu 16.04.6 | releases.ubuntu.com"
|
[4]: https://web.archive.org/web/20221029165714/https://releases.ubuntu.com/xenial/ubuntu-16.04.6-desktop-i386.manifest "List of software packages shipped with Ubuntu 16.04.6 | releases.ubuntu.com"
|
||||||
[5]: https://web.archive.org/web/20221029165726/https://releases.ubuntu.com/18.04/ubuntu-18.04.6-desktop-amd64.manifest "List of sofware packags shipped with Ubuntu 18.04.6 | releases.ubuntu.com"
|
[5]: https://web.archive.org/web/20221029165726/https://releases.ubuntu.com/18.04/ubuntu-18.04.6-desktop-amd64.manifest "List of software packages shipped with Ubuntu 18.04.6 | releases.ubuntu.com"
|
||||||
[6]: https://web.archive.org/web/20221029165821/https://bugs.archlinux.org/task/52326 "FS#52326 : [midori-gtk2] Please remove the zeitgeist dependency! | archlinux.org"
|
[6]: https://web.archive.org/web/20221029165821/https://bugs.archlinux.org/task/52326 "FS#52326 : [midori-gtk2] Please remove the zeitgeist dependency! | archlinux.org"
|
||||||
[7]: https://web.archive.org/web/20221029165914/https://forum.artixlinux.org/index.php/topic,1432.0.html "Remove Unmaintained Zeitgeist (Spyware/Telemetry) from Default MATE installation | artixlinux.org"
|
[7]: https://web.archive.org/web/20221029165914/https://forum.artixlinux.org/index.php/topic,1432.0.html "Remove Unmaintained Zeitgeist (Spyware/Telemetry) from Default MATE installation | artixlinux.org"
|
||||||
[8]: https://web.archive.org/web/20221029165902/https://askubuntu.com/questions/45548/disabling-zeitgeist/57487 "Disabling Zeitgeist - Ask Ubuntu | askubuntu.com"
|
[8]: https://web.archive.org/web/20221029165902/https://askubuntu.com/questions/45548/disabling-zeitgeist/57487 "Disabling Zeitgeist - Ask Ubuntu | askubuntu.com"
|
||||||
@@ -2116,7 +2116,7 @@ actions:
|
|||||||
[3]: https://web.archive.org/web/20221029170026/https://packages.ubuntu.com/bionic/all/network-manager-config-connectivity-ubuntu/filelist "Ubuntu - File list of package network-manager-config-connectivity-ubuntu/bionic/all | packages.ubuntu.com"
|
[3]: https://web.archive.org/web/20221029170026/https://packages.ubuntu.com/bionic/all/network-manager-config-connectivity-ubuntu/filelist "Ubuntu - File list of package network-manager-config-connectivity-ubuntu/bionic/all | packages.ubuntu.com"
|
||||||
[4]: https://web.archive.org/web/20221029170108/https://github.com/pop-os/connectivity/blob/master/debian/20-connectivity-pop.conf "connectivity/20-connectivity-pop.conf at master · pop-os/connectivity | github.com"
|
[4]: https://web.archive.org/web/20221029170108/https://github.com/pop-os/connectivity/blob/master/debian/20-connectivity-pop.conf "connectivity/20-connectivity-pop.conf at master · pop-os/connectivity | github.com"
|
||||||
[5]: https://web.archive.org/web/20221029170202/https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/contrib/fedora/rpm/20-connectivity-fedora.conf "20-connectivity-fedora.conf\rpm\fedora\contrib - NetworkManager/NetworkManager - Network connection manager and user applications | reedesktop.org"
|
[5]: https://web.archive.org/web/20221029170202/https://cgit.freedesktop.org/NetworkManager/NetworkManager/tree/contrib/fedora/rpm/20-connectivity-fedora.conf "20-connectivity-fedora.conf\rpm\fedora\contrib - NetworkManager/NetworkManager - Network connection manager and user applications | reedesktop.org"
|
||||||
[6]: https://web.archive.org/web/20221029170207/https://fedora.pkgs.org/35/fedora-updates-testing-x86_64/NetworkManager-config-connectivity-fedora-1.32.12-1.fc35.noarch.rpm.html "NetworkManager-config-connectivity-fedora | fedora.pkgs.org"
|
[6]: https://archive.ph/2023.12.06-185917/https://pkgs.org/download/NetworkManager-config-connectivity-fedora "Networkmanager-config-connectivity-fedora Download (RPM) | pkgs.org"
|
||||||
call:
|
call:
|
||||||
function: RunIfCommandExists
|
function: RunIfCommandExists
|
||||||
parameters:
|
parameters:
|
||||||
@@ -2202,7 +2202,7 @@ actions:
|
|||||||
- Diagnostic information about your system and usage is sent to Microsoft servers [3].
|
- Diagnostic information about your system and usage is sent to Microsoft servers [3].
|
||||||
- Your usage data and data about feature performance [3].
|
- Your usage data and data about feature performance [3].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221029170818/https://en.wikipedia.org/wiki/Visual_Studio_Code "Visual Studio Code - Wikipedia | en.wikipedia.org"
|
[1]: https://web.archive.org/web/20221029142001/https://en.wikipedia.org/wiki/Visual_Studio_Code "Visual Studio Code - Wikipedia | en.wikipedia.org"
|
||||||
[2]: https://web.archive.org/web/20221029170840/https://code.visualstudio.com/updates/v1_26#_offline-mode "Visual Studio Code July 2018 | code.visualstudio.com"
|
[2]: https://web.archive.org/web/20221029170840/https://code.visualstudio.com/updates/v1_26#_offline-mode "Visual Studio Code July 2018 | code.visualstudio.com"
|
||||||
[3]: https://web.archive.org/web/20221029171138/https://code.visualstudio.com/docs/getstarted/telemetry "Visual Studio Code Telemetry | code.visualstudio.com"
|
[3]: https://web.archive.org/web/20221029171138/https://code.visualstudio.com/docs/getstarted/telemetry "Visual Studio Code Telemetry | code.visualstudio.com"
|
||||||
children:
|
children:
|
||||||
@@ -2697,7 +2697,7 @@ actions:
|
|||||||
[2]: https://web.archive.org/web/20231003094154/https://bugzilla.mozilla.org/show_bug.cgi?id=1746646 "1746646 - (tcp-mochitests) [meta] Make mochitests work with TCP enabled (cookieBehavior = 5) | bugzilla.mozilla.org"
|
[2]: https://web.archive.org/web/20231003094154/https://bugzilla.mozilla.org/show_bug.cgi?id=1746646 "1746646 - (tcp-mochitests) [meta] Make mochitests work with TCP enabled (cookieBehavior = 5) | bugzilla.mozilla.org"
|
||||||
[3]: https://web.archive.org/web/20230918172155/https://developer.mozilla.org/en-US/docs/Web/Privacy/State_Partitioning#disable_dynamic_state_partitioning "State Partitioning - Privacy on the web | MDN"
|
[3]: https://web.archive.org/web/20230918172155/https://developer.mozilla.org/en-US/docs/Web/Privacy/State_Partitioning#disable_dynamic_state_partitioning "State Partitioning - Privacy on the web | MDN"
|
||||||
[4]: https://web.archive.org/web/20231003094207/https://bugzilla.mozilla.org/show_bug.cgi?id=1649876#c5 "1649876 - Migrate FPI users to dFPI | bugzilla.mozilla.org"
|
[4]: https://web.archive.org/web/20231003094207/https://bugzilla.mozilla.org/show_bug.cgi?id=1649876#c5 "1649876 - Migrate FPI users to dFPI | bugzilla.mozilla.org"
|
||||||
[5]: https://blog.mozilla.org/en/products/firefox/firefox-rolls-out-total-cookie-protection-by-default-to-all-users-worldwide/ "Firefox Rolls Out Total Cookie Protection By Default"
|
[5]: https://web.archive.org/web/20231207105610/https://blog.mozilla.org/en/products/firefox/firefox-rolls-out-total-cookie-protection-by-default-to-all-users-worldwide/ "Firefox Rolls Out Total Cookie Protection By Default"
|
||||||
[6]: https://web.archive.org/web/20231003094350/https://bugzilla.mozilla.org/show_bug.cgi?id=1631676#c25 "1631676 - Disable dfpi when privacy.firstparty.isolate=true | bugzilla.mozilla.org"
|
[6]: https://web.archive.org/web/20231003094350/https://bugzilla.mozilla.org/show_bug.cgi?id=1631676#c25 "1631676 - Disable dfpi when privacy.firstparty.isolate=true | bugzilla.mozilla.org"
|
||||||
call:
|
call:
|
||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
@@ -2761,7 +2761,7 @@ actions:
|
|||||||
docs: |-
|
docs: |-
|
||||||
Firefox provides an option for Enhanced Tracking Protection [1], which blocks trackers that
|
Firefox provides an option for Enhanced Tracking Protection [1], which blocks trackers that
|
||||||
gather information about your browsing behavior without disrupting site functionality [1].
|
gather information about your browsing behavior without disrupting site functionality [1].
|
||||||
This feature also includes protections against harmful scripts such as malware that drains
|
This feature also includes protections against harmful scripts such as malware that drain
|
||||||
your battery [1].
|
your battery [1].
|
||||||
|
|
||||||
This script enables the `privacy.resistFingerprinting` preference,
|
This script enables the `privacy.resistFingerprinting` preference,
|
||||||
@@ -2791,7 +2791,7 @@ actions:
|
|||||||
This script enables the `privacy.resistFingerprinting` preference, activating
|
This script enables the `privacy.resistFingerprinting` preference, activating
|
||||||
anti-fingerprinting [1][2].
|
anti-fingerprinting [1][2].
|
||||||
|
|
||||||
As an experimental feature, it might cause some website breakage [2], such as impacting web
|
As an experimental feature, it might cause some website breakages [2], such as impacting web
|
||||||
speech functionality [3] and favicons [4].
|
speech functionality [3] and favicons [4].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221025201025/https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting "Firefox's protection against fingerprinting | Firefox Help | support.mozilla.org"
|
[1]: https://web.archive.org/web/20221025201025/https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting "Firefox's protection against fingerprinting | Firefox Help | support.mozilla.org"
|
||||||
@@ -2876,7 +2876,7 @@ actions:
|
|||||||
|
|
||||||
It's configured to be enabled in nightly, aurora, beta, or default (developer) builds.
|
It's configured to be enabled in nightly, aurora, beta, or default (developer) builds.
|
||||||
In release builds, however, it's set to false [1]. This setting is hard-coded into the C++
|
In release builds, however, it's set to false [1]. This setting is hard-coded into the C++
|
||||||
code to prevent easy disabling [2]. Developers have been approached about this issue but
|
code to prevent easy disabling [2]. Developers have been approached about this issue, but
|
||||||
have rejected proposals to unlock it [3].
|
have rejected proposals to unlock it [3].
|
||||||
|
|
||||||
Mozilla's plan is to deprecate this setting eventually, followed by removal [1].
|
Mozilla's plan is to deprecate this setting eventually, followed by removal [1].
|
||||||
@@ -2887,7 +2887,7 @@ actions:
|
|||||||
setting [4].
|
setting [4].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org"
|
[1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org"
|
||||||
[2]: https://web.archive.org/web/20221015102305/https://searchfox.org/mozilla-central/source/modules/libpref/Preferences.cpp#3213
|
[2]: https://web.archive.org/web/20221015102338/https://searchfox.org/mozilla-central/source/modules/libpref/Preferences.cpp#3213
|
||||||
[3]: https://web.archive.org/web/20221015102419/https://bugzilla.mozilla.org/show_bug.cgi?id=1422689#c1
|
[3]: https://web.archive.org/web/20221015102419/https://bugzilla.mozilla.org/show_bug.cgi?id=1422689#c1
|
||||||
[4]: https://web.archive.org/web/20221015102604/https://stigviewer.com/stig/mozilla_firefox/2020-12-10/finding/V-223170
|
[4]: https://web.archive.org/web/20221015102604/https://stigviewer.com/stig/mozilla_firefox/2020-12-10/finding/V-223170
|
||||||
call:
|
call:
|
||||||
@@ -3012,7 +3012,7 @@ actions:
|
|||||||
recommend: standard
|
recommend: standard
|
||||||
docs: |-
|
docs: |-
|
||||||
This script sets `toolkit.telemetry.server` to be empty.
|
This script sets `toolkit.telemetry.server` to be empty.
|
||||||
This preference defines the server to which Telemetry pings are sent [1].
|
This preference defines the server to which telemetry pings are sent [1].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org"
|
[1]: https://web.archive.org/web/20221015102124/https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/internals/preferences.html "Preferences and Defines — Firefox Source Docs documentation | firefox-source-docs.mozilla.org"
|
||||||
call:
|
call:
|
||||||
@@ -3133,7 +3133,7 @@ actions:
|
|||||||
name: Disable Firefox Pioneer study monitoring
|
name: Disable Firefox Pioneer study monitoring
|
||||||
recommend: standard
|
recommend: standard
|
||||||
docs: |-
|
docs: |-
|
||||||
This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out.
|
This script configures `toolkit.telemetry.pioneer-new-studies-available` to be disabled to opt out
|
||||||
Firefox Pioneer program.
|
Firefox Pioneer program.
|
||||||
|
|
||||||
This setting disables availability check for Firefox Pioneer studies [1].
|
This setting disables availability check for Firefox Pioneer studies [1].
|
||||||
@@ -3173,7 +3173,7 @@ actions:
|
|||||||
portal is in place and blocking traffic, this feature prevents all other connection attempts,
|
portal is in place and blocking traffic, this feature prevents all other connection attempts,
|
||||||
possibly revealing your usage habits.
|
possibly revealing your usage habits.
|
||||||
|
|
||||||
See also: [Captive portal | Wikipedia](https://web.archive.org/web/20221029223534/https://en.wikipedia.org/wiki/Captive_portal).
|
See also: [Captive portal | Wikipedia](https://web.archive.org/web/20221029163002/https://en.wikipedia.org/wiki/Captive_portal).
|
||||||
|
|
||||||
This script sets `network.captive-portal-service.enabled` to 'false', thereby disabling automatic
|
This script sets `network.captive-portal-service.enabled` to 'false', thereby disabling automatic
|
||||||
connections [1].
|
connections [1].
|
||||||
@@ -3207,7 +3207,7 @@ actions:
|
|||||||
There have been concerns about the potential for Google Safe Browsing to be used for censorship
|
There have been concerns about the potential for Google Safe Browsing to be used for censorship
|
||||||
in the future, although this has not occurred as of yet [3].
|
in the future, although this has not occurred as of yet [3].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221025192643/https://wiki.mozilla.org/Security/Safe_Browsing "Security/Safe Browsing - MozillaWiki | wiki.mozilla.org"
|
[1]: https://web.archive.org/web/20221026164502/https://wiki.mozilla.org/Security/Safe_Browsing "Security/Safe Browsing - MozillaWiki | wiki.mozilla.org"
|
||||||
[2]: https://web.archive.org/web/20221025193000/https://support.mozilla.org/en-US/kb/how-does-phishing-and-malware-protection-work#w_what-information-is-sent-to-mozilla-or-its-partners-when-phishing-and-malware-protection-is-enabled
|
[2]: https://web.archive.org/web/20221025193000/https://support.mozilla.org/en-US/kb/how-does-phishing-and-malware-protection-work#w_what-information-is-sent-to-mozilla-or-its-partners-when-phishing-and-malware-protection-is-enabled
|
||||||
[3]: https://web.archive.org/web/20221025192516/https://www.usnews.com/opinion/articles/2016-06-22/google-is-the-worlds-biggest-censor-and-its-power-must-be-regulated "Google Is the World's Biggest Censor and Its Power Must Be Regulated | usnews.com"
|
[3]: https://web.archive.org/web/20221025192516/https://www.usnews.com/opinion/articles/2016-06-22/google-is-the-worlds-biggest-censor-and-its-power-must-be-regulated "Google Is the World's Biggest Censor and Its Power Must Be Regulated | usnews.com"
|
||||||
children:
|
children:
|
||||||
@@ -3226,7 +3226,7 @@ actions:
|
|||||||
|
|
||||||
If this blocking is removed, the user should be knowledgeable about the potential risks and will take precautions.
|
If this blocking is removed, the user should be knowledgeable about the potential risks and will take precautions.
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221025192643/https://wiki.mozilla.org/Security/Safe_Browsing#Prefs "Security/Safe Browsing - MozillaWiki | wiki.mozilla.org"
|
[1]: https://web.archive.org/web/20221026164502/https://wiki.mozilla.org/Security/Safe_Browsing#Prefs "Security/Safe Browsing - MozillaWiki | wiki.mozilla.org"
|
||||||
[2]: https://web.archive.org/web/20230811024650/https://blog.mozilla.org/addons/2020/08/24/introducing-a-scalable-add-ons-blocklist/ "Introducing a scalable add-ons blocklist | Mozilla Add-ons Community Blog"
|
[2]: https://web.archive.org/web/20230811024650/https://blog.mozilla.org/addons/2020/08/24/introducing-a-scalable-add-ons-blocklist/ "Introducing a scalable add-ons blocklist | Mozilla Add-ons Community Blog"
|
||||||
call:
|
call:
|
||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
@@ -3286,7 +3286,7 @@ actions:
|
|||||||
|
|
||||||
It is active by default [2].
|
It is active by default [2].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20221025192643/https://wiki.mozilla.org/Security/Safe_Browsing#Prefs "Security/Safe Browsing - MozillaWiki | wiki.mozilla.org"
|
[1]: https://web.archive.org/web/20221026164502/https://wiki.mozilla.org/Security/Safe_Browsing#Prefs "Security/Safe Browsing - MozillaWiki | wiki.mozilla.org"
|
||||||
[2]: https://web.archive.org/web/20221029173442/https://github.com/mozilla/policy-templates/blob/master/README.md#preferences "policy-templates/README.md at master · mozilla/policy-templates · GitHub | github.com"
|
[2]: https://web.archive.org/web/20221029173442/https://github.com/mozilla/policy-templates/blob/master/README.md#preferences "policy-templates/README.md at master · mozilla/policy-templates · GitHub | github.com"
|
||||||
call:
|
call:
|
||||||
function: AddFirefoxPrefs
|
function: AddFirefoxPrefs
|
||||||
@@ -3711,7 +3711,7 @@ functions:
|
|||||||
# User-specific:
|
# User-specific:
|
||||||
# [~/.profile]
|
# [~/.profile]
|
||||||
# User-specific shell initialization scripts.
|
# User-specific shell initialization scripts.
|
||||||
# ✅ Recomended by Debian to edit for user-specific environment variables.
|
# ✅ Recommended by Debian to edit for user-specific environment variables.
|
||||||
# [~/.bashrc]
|
# [~/.bashrc]
|
||||||
# User-based configuration file to set environment variables for Bash shell.
|
# User-based configuration file to set environment variables for Bash shell.
|
||||||
# ❌ Bash-specific.
|
# ❌ Bash-specific.
|
||||||
@@ -3783,7 +3783,7 @@ functions:
|
|||||||
if [[ -f "$cronjob_path" ]]; then
|
if [[ -f "$cronjob_path" ]]; then
|
||||||
if [[ -x "$cronjob_path" ]]; then
|
if [[ -x "$cronjob_path" ]]; then
|
||||||
sudo chmod -x "$cronjob_path"
|
sudo chmod -x "$cronjob_path"
|
||||||
echo "Succesfully disabled cronjob \"$job_name\"."
|
echo "Successfully disabled cronjob \"$job_name\"."
|
||||||
else
|
else
|
||||||
echo "Skipping, cronjob \"$job_name\" is already disabled."
|
echo "Skipping, cronjob \"$job_name\" is already disabled."
|
||||||
fi
|
fi
|
||||||
@@ -3797,7 +3797,7 @@ functions:
|
|||||||
echo "Skipping, cronjob \"$job_name\" is already enabled."
|
echo "Skipping, cronjob \"$job_name\" is already enabled."
|
||||||
else
|
else
|
||||||
sudo chmod +x "$cronjob_path"
|
sudo chmod +x "$cronjob_path"
|
||||||
echo "Succesfully enabled cronjob \"$job_name\"."
|
echo "Successfully enabled cronjob \"$job_name\"."
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
>&2 echo "Failed to enable cronjob \"$job_name\" because it's missing."
|
>&2 echo "Failed to enable cronjob \"$job_name\" because it's missing."
|
||||||
@@ -3939,7 +3939,7 @@ functions:
|
|||||||
echo "Backup file exists: $file."
|
echo "Backup file exists: $file."
|
||||||
sudo mv "$backup_file" "$file"
|
sudo mv "$backup_file" "$file"
|
||||||
echo "Moved to: $file."
|
echo "Moved to: $file."
|
||||||
echo "Succesfully restored."
|
echo "Successfully restored."
|
||||||
else
|
else
|
||||||
>&2 echo "Failed to restore, backup file could not be found at $backup_file."
|
>&2 echo "Failed to restore, backup file could not be found at $backup_file."
|
||||||
>&2 echo "Was the change initially applied by privacy.sexy?"
|
>&2 echo "Was the change initially applied by privacy.sexy?"
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ actions:
|
|||||||
name: Clear user activity audit logs (login, logout, authentication, etc.)
|
name: Clear user activity audit logs (login, logout, authentication, etc.)
|
||||||
docs:
|
docs:
|
||||||
- https://papers.put.as/papers/macosx/2012/Mac_Log_Analysis_Sarah_Edwards_DFIRSummit2012.pdf
|
- https://papers.put.as/papers/macosx/2012/Mac_Log_Analysis_Sarah_Edwards_DFIRSummit2012.pdf
|
||||||
- http://macadmins.psu.edu/wp-content/uploads/sites/24696/2016/06/psumac2016-19-osxlogs_macadmins_2016.pdf
|
- https://web.archive.org/web/20240314054514/https://bpb-us-e1.wpmucdn.com/sites.psu.edu/dist/4/24696/files/2016/06/psumac2016-19-osxlogs_macadmins_2016.pdf
|
||||||
code: |-
|
code: |-
|
||||||
sudo rm -rfv /var/audit/*
|
sudo rm -rfv /var/audit/*
|
||||||
sudo rm -rfv /private/var/audit/*
|
sudo rm -rfv /private/var/audit/*
|
||||||
@@ -171,7 +171,7 @@ actions:
|
|||||||
-
|
-
|
||||||
name: Clear Safari last session (open tabs) history
|
name: Clear Safari last session (open tabs) history
|
||||||
docs:
|
docs:
|
||||||
- https://apple.stackexchange.com/a/374116
|
- https://web.archive.org/web/20240314061752/https://apple.stackexchange.com/questions/374099/where-does-safari-store-the-open-tabs/374116#374116
|
||||||
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-7127
|
- https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2013-7127
|
||||||
code: rm -f ~/Library/Safari/LastSession.plist
|
code: rm -f ~/Library/Safari/LastSession.plist
|
||||||
-
|
-
|
||||||
@@ -191,7 +191,7 @@ actions:
|
|||||||
name: Clear Safari webpage previews (thumbnails)
|
name: Clear Safari webpage previews (thumbnails)
|
||||||
docs:
|
docs:
|
||||||
- https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
|
- https://davidkoepi.wordpress.com/2013/04/20/safariforensic/
|
||||||
- https://www.reddit.com/r/apple/comments/18lp92/your_apple_computer_keeps_a_screen_shot_of_nearly/
|
- https://archive.ph/2024.03.14-100910/https://www.reddit.com/r/apple/comments/18lp92/your_apple_computer_keeps_a_screen_shot_of_nearly/?rdt=59921
|
||||||
code: rm -rfv ~/Library/Caches/com.apple.Safari/Webpage\ Previews
|
code: rm -rfv ~/Library/Caches/com.apple.Safari/Webpage\ Previews
|
||||||
-
|
-
|
||||||
name: Clear Safari history copy
|
name: Clear Safari history copy
|
||||||
@@ -204,8 +204,8 @@ actions:
|
|||||||
-
|
-
|
||||||
name: Clear Safari cookies
|
name: Clear Safari cookies
|
||||||
docs:
|
docs:
|
||||||
- https://www.toolbox.com/tech/operating-systems/blogs/understanding-the-safari-cookiesbinarycookies-file-format-010712/
|
- https://web.archive.org/web/20240314132018/https://community.spiceworks.com/t/understanding-the-safari-cookies-binarycookies-file-format/928827
|
||||||
- https://link.springer.com/content/pdf/10.1007/0-387-36891-4_13.pdf
|
- https://web.archive.org/web/20240314060318/https://link.springer.com/content/pdf/10.1007/0-387-36891-4_13.pdf
|
||||||
code: |-
|
code: |-
|
||||||
rm -f ~/Library/Cookies/Cookies.binarycookies
|
rm -f ~/Library/Cookies/Cookies.binarycookies
|
||||||
# Used before Safari 5.1
|
# Used before Safari 5.1
|
||||||
@@ -300,7 +300,7 @@ actions:
|
|||||||
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
> - Logs are valuable for diagnosing issues and understanding past actions [1].
|
||||||
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
> - Script files can help review changes made to the system and aid in reverting those changes if needed.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
@@ -318,7 +318,7 @@ actions:
|
|||||||
> - This action is irreversible. Deleted script files cannot be retrieved.
|
> - This action is irreversible. Deleted script files cannot be retrieved.
|
||||||
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
> - These files might be necessary for troubleshooting if you experience issues after using privacy.sexy scripts.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -339,7 +339,7 @@ actions:
|
|||||||
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
> - Removing logs will prevent you from reviewing the application's activities, which could be helpful in diagnosing issues.
|
||||||
> - Logs can contain valuable information for technical support should you need assistance.
|
> - Logs can contain valuable information for technical support should you need assistance.
|
||||||
|
|
||||||
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
[1]: https://github.com/undergroundwires/privacy.sexy/blob/master/docs/desktop/desktop-vs-web-features.md "Desktop vs. Web Features | privacy.sexy | github.com"
|
||||||
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
[2]: https://github.com/undergroundwires/privacy.sexy/blob/master/SECURITY.md "SECURITY.md | privacy.sexy | github.com"
|
||||||
call:
|
call:
|
||||||
function: ClearDirectoryContents
|
function: ClearDirectoryContents
|
||||||
@@ -520,7 +520,7 @@ actions:
|
|||||||
you'll be prompted to grant or deny permission. It's a proactive step to ensure that your sensitive information
|
you'll be prompted to grant or deny permission. It's a proactive step to ensure that your sensitive information
|
||||||
or system services are accessed only with your current and informed consent.
|
or system services are accessed only with your current and informed consent.
|
||||||
children:
|
children:
|
||||||
# Main documentation: https://archive.ph/26Hlq (https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services)
|
# Main documentation: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services
|
||||||
-
|
-
|
||||||
name: Clear **"All"** permissions
|
name: Clear **"All"** permissions
|
||||||
docs: |-
|
docs: |-
|
||||||
@@ -536,7 +536,7 @@ actions:
|
|||||||
This script resets permissions for camera access [1].
|
This script resets permissions for camera access [1].
|
||||||
It ensures no application can access the system camera without explicit user permission, protecting against unauthorized surveillance and data breaches.
|
It ensures no application can access the system camera without explicit user permission, protecting against unauthorized surveillance and data breaches.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -547,7 +547,7 @@ actions:
|
|||||||
This script resets permissions for microphone access [1].
|
This script resets permissions for microphone access [1].
|
||||||
It revokes all granted access to the microphone, protecting against eavesdropping and unauthorized audio recording by applications.
|
It revokes all granted access to the microphone, protecting against eavesdropping and unauthorized audio recording by applications.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -558,7 +558,7 @@ actions:
|
|||||||
This script resets permissions for accessibility features [1].
|
This script resets permissions for accessibility features [1].
|
||||||
It revokes application access to accessibility services, preventing misuse and ensuring these features are used only with user consent.
|
It revokes application access to accessibility services, preventing misuse and ensuring these features are used only with user consent.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -569,7 +569,7 @@ actions:
|
|||||||
This script resets permissions for screen capture [1].
|
This script resets permissions for screen capture [1].
|
||||||
It ensures applications cannot capture screen content without user authorization, protecting sensitive information displayed on the screen.
|
It ensures applications cannot capture screen content without user authorization, protecting sensitive information displayed on the screen.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -580,7 +580,7 @@ actions:
|
|||||||
This script resets permissions for accessing reminders information managed by the Reminders app [1].
|
This script resets permissions for accessing reminders information managed by the Reminders app [1].
|
||||||
It ensures applications cannot access or modify reminders data without explicit user permission, maintaining the privacy of personal reminders.
|
It ensures applications cannot access or modify reminders data without explicit user permission, maintaining the privacy of personal reminders.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -591,7 +591,7 @@ actions:
|
|||||||
This script resets permissions for accessing the pictures managed by the Photos app [1].
|
This script resets permissions for accessing the pictures managed by the Photos app [1].
|
||||||
It revokes all permissions granted to applications, safeguarding personal photos and media from unauthorized access.
|
It revokes all permissions granted to applications, safeguarding personal photos and media from unauthorized access.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -602,7 +602,7 @@ actions:
|
|||||||
This script resets permissions for accessing the calendar information managed by the Calendar app [1].
|
This script resets permissions for accessing the calendar information managed by the Calendar app [1].
|
||||||
It ensures that applications cannot access calendar data without user consent, protecting personal and sensitive calendar information.
|
It ensures that applications cannot access calendar data without user consent, protecting personal and sensitive calendar information.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -614,7 +614,7 @@ actions:
|
|||||||
Full disk access allows the application access to all protected files, including system administration files [1].
|
Full disk access allows the application access to all protected files, including system administration files [1].
|
||||||
It revokes broad file access from applications, significantly reducing the risk of data exposure and enhancing overall system security.
|
It revokes broad file access from applications, significantly reducing the risk of data exposure and enhancing overall system security.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -626,7 +626,7 @@ actions:
|
|||||||
The contact information managed by the Contacts app [1].
|
The contact information managed by the Contacts app [1].
|
||||||
It ensures that applications cannot access the user's contact list without explicit permission, maintaining the confidentiality of personal contacts.
|
It ensures that applications cannot access the user's contact list without explicit permission, maintaining the confidentiality of personal contacts.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -637,7 +637,7 @@ actions:
|
|||||||
This script resets permissions for accessing the Desktop folder [1].
|
This script resets permissions for accessing the Desktop folder [1].
|
||||||
It revokes application access to files on the desktop, protecting personal and work-related documents from unauthorized access.
|
It revokes application access to files on the desktop, protecting personal and work-related documents from unauthorized access.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -648,7 +648,7 @@ actions:
|
|||||||
This script resets permissions for accessing the Documents folder [1].
|
This script resets permissions for accessing the Documents folder [1].
|
||||||
It prevents applications from accessing files in this folder without user consent, safeguarding important and private documents.
|
It prevents applications from accessing files in this folder without user consent, safeguarding important and private documents.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -659,7 +659,7 @@ actions:
|
|||||||
This script resets permissions for accessing the Downloads folder [1].
|
This script resets permissions for accessing the Downloads folder [1].
|
||||||
It ensures that applications cannot access downloaded files without user authorization, protecting downloaded content from misuse.
|
It ensures that applications cannot access downloaded files without user authorization, protecting downloaded content from misuse.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -670,7 +670,7 @@ actions:
|
|||||||
This script resets permissions for Apple Events [1].
|
This script resets permissions for Apple Events [1].
|
||||||
It revokes permissions for applications to send restricted Apple Events to other processes [1], enhancing privacy and security.
|
It revokes permissions for applications to send restricted Apple Events to other processes [1], enhancing privacy and security.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -681,7 +681,7 @@ actions:
|
|||||||
This script resets permissions for File Provider Presence [1].
|
This script resets permissions for File Provider Presence [1].
|
||||||
It revokes the ability of File Provider applications to know when the user is accessing their managed files [1], enhancing user privacy.
|
It revokes the ability of File Provider applications to know when the user is accessing their managed files [1], enhancing user privacy.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -692,7 +692,7 @@ actions:
|
|||||||
This script resets "ListenEvent" permissions [1].
|
This script resets "ListenEvent" permissions [1].
|
||||||
It revokes application access to listen to system events [1], preventing unauthorized monitoring of user interactions with the system.
|
It revokes application access to listen to system events [1], preventing unauthorized monitoring of user interactions with the system.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -703,7 +703,7 @@ actions:
|
|||||||
This script resets permissions for accessing the Media Library [1].
|
This script resets permissions for accessing the Media Library [1].
|
||||||
It ensures that applications cannot access Apple Music, music and video activity, and the media library [1] without user consent.
|
It ensures that applications cannot access Apple Music, music and video activity, and the media library [1] without user consent.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -714,7 +714,7 @@ actions:
|
|||||||
This script resets permissions for sending "PostEvent" [1].
|
This script resets permissions for sending "PostEvent" [1].
|
||||||
It prevents applications from using CoreGraphics APIs to send system events [1], safeguarding against potential misuse.
|
It prevents applications from using CoreGraphics APIs to send system events [1], safeguarding against potential misuse.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -726,7 +726,7 @@ actions:
|
|||||||
This script resets permissions for using Speech Recognition [1].
|
This script resets permissions for using Speech Recognition [1].
|
||||||
It revokes application access to the speech recognition facility and sending speech data to Apple [1], protecting user privacy.
|
It revokes application access to the speech recognition facility and sending speech data to Apple [1], protecting user privacy.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -737,7 +737,7 @@ actions:
|
|||||||
This script resets permissions for modifying other apps [1].
|
This script resets permissions for modifying other apps [1].
|
||||||
It prevents applications from updating or deleting other apps [1], maintaining system integrity and user control.
|
It prevents applications from updating or deleting other apps [1], maintaining system integrity and user control.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -748,7 +748,7 @@ actions:
|
|||||||
This script resets permissions for accessing application data [1].
|
This script resets permissions for accessing application data [1].
|
||||||
It revokes application access to specific application data, enhancing privacy and data security.
|
It revokes application access to specific application data, enhancing privacy and data security.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -759,7 +759,7 @@ actions:
|
|||||||
This script resets permissions for accessing files on network volumes [1].
|
This script resets permissions for accessing files on network volumes [1].
|
||||||
It ensures applications cannot access network files without user authorization.
|
It ensures applications cannot access network files without user authorization.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -770,7 +770,7 @@ actions:
|
|||||||
This script resets permissions for accessing files on removable volumes [1].
|
This script resets permissions for accessing files on removable volumes [1].
|
||||||
It protects data on external drives from unauthorized application access.
|
It protects data on external drives from unauthorized application access.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -781,7 +781,7 @@ actions:
|
|||||||
This script resets permissions for accessing system administration files [1].
|
This script resets permissions for accessing system administration files [1].
|
||||||
It enhances system security by restricting application access to critical system files.
|
It enhances system security by restricting application access to critical system files.
|
||||||
|
|
||||||
[1]: https://archive.ph/26Hlq "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
[1]: https://archive.ph/2023.11.24-170934/https://developer.apple.com/documentation/devicemanagement/privacypreferencespolicycontrol/services "PrivacyPreferencesPolicyControl.Services | Apple Developer Documentation | apple.com"
|
||||||
call:
|
call:
|
||||||
function: ResetServicePermissions
|
function: ResetServicePermissions
|
||||||
parameters:
|
parameters:
|
||||||
@@ -877,7 +877,7 @@ actions:
|
|||||||
There is also `WelcomeScreenPromo.PromoOff` setting that's pre-configured to `1` (`no` as
|
There is also `WelcomeScreenPromo.PromoOff` setting that's pre-configured to `1` (`no` as
|
||||||
default). It's undocumented but still kept disabled by this script.
|
default). It's undocumented but still kept disabled by this script.
|
||||||
|
|
||||||
[1]: https://web.archive.org/save/https://forum.parallels.com/threads/unable-to-process-the-upgrade-request.345603/ "Unable to process the upgrade request | Parallels Forums | forum.parallels.com"
|
[1]: https://web.archive.org/web/20240314062932/https://forum.parallels.com/threads/unable-to-process-the-upgrade-request.345603/ "Unable to process the upgrade request | Parallels Forums | forum.parallels.com"
|
||||||
[2]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
|
[2]: https://web.archive.org/web/20221012151800/https://kb.parallels.com/114422 "How do I turn off notifications in Parallels Desktop and Parallels Access? | Knowledge Base | parallels.com"
|
||||||
code: |-
|
code: |-
|
||||||
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes
|
defaults write 'com.parallels.Parallels Desktop' 'ProductPromo.ForcePromoOff' -bool yes
|
||||||
@@ -988,16 +988,16 @@ actions:
|
|||||||
recommend: strict
|
recommend: strict
|
||||||
docs:
|
docs:
|
||||||
- https://github.com/privacysexy-forks/starter/blob/master/system/siri.sh
|
- https://github.com/privacysexy-forks/starter/blob/master/system/siri.sh
|
||||||
- https://machippie.github.io/system/
|
- https://web.archive.org/web/20201002133713/https://machippie.github.io/system/
|
||||||
code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3
|
code: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 3
|
||||||
revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2
|
revertCode: defaults write com.apple.assistant.backedup 'Use device speaker for TTS' -int 2
|
||||||
-
|
-
|
||||||
name: Disable Siri services (Siri and assistantd)
|
name: Disable Siri services (Siri and assistantd)
|
||||||
recommend: strict
|
recommend: strict
|
||||||
docs:
|
docs:
|
||||||
- https://apple.stackexchange.com/questions/57514/what-is-assistantd
|
- https://web.archive.org/web/20240314060540/https://apple.stackexchange.com/questions/57514/what-is-assistantd
|
||||||
- https://www.jamf.com/jamf-nation/discussions/22757/kill-siri#responseChild137563
|
- https://archive.ph/2024.03.14-055010/https://community.jamf.com/t5/jamf-pro/kill-siri/td-p/171543
|
||||||
- https://apple.stackexchange.com/a/370426
|
- https://web.archive.org/web/20240314060501/https://apple.stackexchange.com/questions/258816/how-to-completely-disable-siri-on-sierra/370426#370426
|
||||||
# To see status: • `launchctl print-disabled system` • `launchctl print-disabled user/$UID` • `launchctl print-disabled gui/$UID`
|
# To see status: • `launchctl print-disabled system` • `launchctl print-disabled user/$UID` • `launchctl print-disabled gui/$UID`
|
||||||
code: |-
|
code: |-
|
||||||
launchctl disable "user/$UID/com.apple.assistantd"
|
launchctl disable "user/$UID/com.apple.assistantd"
|
||||||
@@ -1021,10 +1021,20 @@ actions:
|
|||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: Disable "Do you want to enable Siri?" pop-up
|
name: Disable "Do you want to enable Siri?" pop-up
|
||||||
docs:
|
docs: |-
|
||||||
- https://discussions.apple.com/thread/7694127?answerId=30752577022#30752577022
|
This script stops the "Enable Siri" pop-up [1] from appearing the first time a user logs into macOS [2].
|
||||||
- https://windowsreport.com/mac/siri-keeps-popping-up/
|
|
||||||
- https://www.jamf.com/jamf-nation/discussions/21783/disable-siri-setup-assistant-in-macos-sierra#responseChild131588
|
Introduced in macOS version 10.12 [2], this pop-up asks, "Do you want to enable Siri?" [1]
|
||||||
|
which could lead to Siri being enabled unintentionally.
|
||||||
|
|
||||||
|
This script configures the `com.apple.SetupAssistant!DidSeeSiriSetup` setting to suppress this pop-up [1] [2] [3] [4].
|
||||||
|
This command tells the system that the Siri setup is complete, preventing the pop-up in future sessions and
|
||||||
|
enhancing privacy by avoiding unintended Siri activation.
|
||||||
|
|
||||||
|
[1]: https://archive.ph/2024.03.14-053325/https://discussions.apple.com/thread/7694127?answerId=30752577022&sortBy=best%2330752577022 "macOS keeps nagging me about enabling Siri - Apple Community | discussions.apple.com"
|
||||||
|
[2]: https://web.archive.org/web/20240314052600/https://derflounder.wordpress.com/2016/09/20/supressing-siri-pop-up-windows-on-macos-sierra/ "Suppressing Siri pop-up windows on macOS Sierra | Der Flounder"
|
||||||
|
[3]: https://web.archive.org/web/20240314052901/https://windowsreport.com/mac/siri-keeps-popping-up/ "Siri keeps popping up on Mac? Here's how to easily fix that • MacTips | windowsreport.com"
|
||||||
|
[4]: https://web.archive.org/web/20240314052247/https://community.jamf.com/t5/jamf-pro/disable-siri-setup-assistant-in-macos-sierra/m-p/205836/highlight/true#M194536 "Solved: Re: Disable Siri setup assistant in macOS Sierra - Jamf Nation Community - 205834 | community.jamf.com"
|
||||||
code: defaults write com.apple.SetupAssistant 'DidSeeSiriSetup' -bool True
|
code: defaults write com.apple.SetupAssistant 'DidSeeSiriSetup' -bool True
|
||||||
revertCode: defaults delete com.apple.SetupAssistant 'DidSeeSiriSetup'
|
revertCode: defaults delete com.apple.SetupAssistant 'DidSeeSiriSetup'
|
||||||
-
|
-
|
||||||
@@ -1084,7 +1094,7 @@ actions:
|
|||||||
by default.
|
by default.
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20230731152633/https://www.apple.com/legal/privacy/data/en/apple-advertising/ "Legal - Apple Advertising & Privacy - Apple"
|
[1]: https://web.archive.org/web/20230731152633/https://www.apple.com/legal/privacy/data/en/apple-advertising/ "Legal - Apple Advertising & Privacy - Apple"
|
||||||
[2]: https://web.archive.org/web/20220805052411/https://support.apple.com/en-sg/guide/mac-help/mh32356/mac: "Change Privacy preferences on Mac - Apple Support (SG)"
|
[2]: https://web.archive.org/web/20220805052411/https://support.apple.com/en-sg/guide/mac-help/mh32356/mac "Change Privacy preferences on Mac - Apple Support (SG)"
|
||||||
[3]: https://web.archive.org/web/20230731155827/https://developer.apple.com/documentation/devicemanagement/restrictions "Restrictions | Apple Developer Documentation"
|
[3]: https://web.archive.org/web/20230731155827/https://developer.apple.com/documentation/devicemanagement/restrictions "Restrictions | Apple Developer Documentation"
|
||||||
[4]: https://web.archive.org/web/20230731155653/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_11_0_Big_Sur_Benchmark_v2_0_0.pdf "CIS Apple macOS 11.0 Big Sur Benchmark"
|
[4]: https://web.archive.org/web/20230731155653/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_11_0_Big_Sur_Benchmark_v2_0_0.pdf "CIS Apple macOS 11.0 Big Sur Benchmark"
|
||||||
[5]: https://web.archive.org/web/20230731155131/https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier "advertisingIdentifier | Apple Developer Documentation"
|
[5]: https://web.archive.org/web/20230731155131/https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier "advertisingIdentifier | Apple Developer Documentation"
|
||||||
@@ -1280,7 +1290,7 @@ actions:
|
|||||||
# OS tracks downloaded files with help of quarantine-aware applications
|
# OS tracks downloaded files with help of quarantine-aware applications
|
||||||
# (such as Safari, Chrome) adding quarantine extended attributes to files.
|
# (such as Safari, Chrome) adding quarantine extended attributes to files.
|
||||||
# then OS warns and asks if you really want to open it
|
# then OS warns and asks if you really want to open it
|
||||||
docs: https://support.apple.com/en-gb/HT202491
|
docs: https://web.archive.org/web/20210319081714/https://support.apple.com/en-gb/HT202491
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
category: Clean File Quarantine from downloaded files
|
category: Clean File Quarantine from downloaded files
|
||||||
@@ -1391,14 +1401,14 @@ actions:
|
|||||||
name: Disable Gatekeeper's automatic reactivation
|
name: Disable Gatekeeper's automatic reactivation
|
||||||
docs:
|
docs:
|
||||||
- https://osxdaily.com/2015/11/05/stop-gatekeeper-auto-rearm-mac-os-x/
|
- https://osxdaily.com/2015/11/05/stop-gatekeeper-auto-rearm-mac-os-x/
|
||||||
- https://www.cnet.com/tech/computing/how-to-disable-gatekeeper-permanently-on-os-x/
|
- https://web.archive.org/web/20230327050142/https://www.cnet.com/tech/computing/how-to-disable-gatekeeper-permanently-on-os-x/
|
||||||
code: sudo defaults write /Library/Preferences/com.apple.security GKAutoRearm -bool true
|
code: sudo defaults write /Library/Preferences/com.apple.security GKAutoRearm -bool true
|
||||||
revertCode: sudo defaults write /Library/Preferences/com.apple.security GKAutoRearm -bool false
|
revertCode: sudo defaults write /Library/Preferences/com.apple.security GKAutoRearm -bool false
|
||||||
-
|
-
|
||||||
name: Disable Gatekeeper
|
name: Disable Gatekeeper
|
||||||
docs:
|
docs:
|
||||||
# References for spctl --master-disable
|
# References for spctl --master-disable
|
||||||
- https://www.manpagez.com/man/8/spctl/
|
- https://web.archive.org/web/20240523173608/https://www.manpagez.com/man/8/spctl/
|
||||||
# References for /var/db/SystemPolicy-prefs.plist
|
# References for /var/db/SystemPolicy-prefs.plist
|
||||||
- https://krypted.com/mac-security/manage-gatekeeper-from-the-command-line-in-mountain-lion/
|
- https://krypted.com/mac-security/manage-gatekeeper-from-the-command-line-in-mountain-lion/
|
||||||
- https://community.jamf.com/t5/jamf-pro/users-can-t-change-password-greyed-out/m-p/54228
|
- https://community.jamf.com/t5/jamf-pro/users-can-t-change-password-greyed-out/m-p/54228
|
||||||
@@ -1450,13 +1460,19 @@ actions:
|
|||||||
revertCode: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool false
|
revertCode: sudo defaults write /Library/Preferences/com.apple.security.libraryvalidation.plist 'DisableLibraryValidation' -bool false
|
||||||
-
|
-
|
||||||
category: Disable automatic updates
|
category: Disable automatic updates
|
||||||
docs:
|
docs: |-
|
||||||
- https://developer.apple.com/documentation/devicemanagement/deviceinformationresponse/queryresponses/osupdatesettings
|
This category contains scripts to disable automatic operating system updates.
|
||||||
- https://macadminsdoc.readthedocs.io/en/master/Profiles-and-Settings/OS-X-Updates.html
|
|
||||||
|
Disabling automatic updates gives users full control over when and which updates are applied to their system.
|
||||||
|
It improves privacy by preventing unwanted data collection, new vulnerabilities and unapproved changes to system settings.
|
||||||
|
|
||||||
|
> **Caution**:
|
||||||
|
> Disabling automatic updates can leave your system vulnerable to unpatched exploits.
|
||||||
|
> Manually check and apply updates to stay protected.
|
||||||
children:
|
children:
|
||||||
-
|
-
|
||||||
name: Disable automatic checks for updates
|
name: Disable automatic checks for updates
|
||||||
docs: https://developer.apple.com/documentation/devicemanagement/softwareupdate
|
docs: https://archive.ph/2024.03.21-180353/https://developer.apple.com/documentation/devicemanagement/softwareupdate
|
||||||
code: |-
|
code: |-
|
||||||
# For OS X Yosemite and newer (>= 10.10)
|
# For OS X Yosemite and newer (>= 10.10)
|
||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticCheckEnabled' -bool false
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticCheckEnabled' -bool false
|
||||||
@@ -1465,7 +1481,7 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticCheckEnabled' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticCheckEnabled' -bool true
|
||||||
-
|
-
|
||||||
name: Disable automatic downloads for updates
|
name: Disable automatic downloads for updates
|
||||||
docs: https://developer.apple.com/documentation/devicemanagement/softwareupdate
|
docs: https://archive.ph/2024.03.21-180353/https://developer.apple.com/documentation/devicemanagement/softwareupdate
|
||||||
code: |-
|
code: |-
|
||||||
# For OS X Yosemite and newer (>= 10.10)
|
# For OS X Yosemite and newer (>= 10.10)
|
||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticDownload' -bool false
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticDownload' -bool false
|
||||||
@@ -1474,12 +1490,41 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticDownload' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticDownload' -bool true
|
||||||
-
|
-
|
||||||
name: Disable automatic installation of macOS updates
|
name: Disable automatic installation of macOS updates
|
||||||
docs:
|
docs: |-
|
||||||
# References for AutoUpdateRestartRequired
|
This script stops macOS from automatically installing updates.
|
||||||
- https://kb.vmware.com/s/article/2960635
|
|
||||||
- https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/
|
This script improves privacy by reducing unwanted data collection and ensuring updates don't change
|
||||||
# References for AutomaticallyInstallMacOSUpdates
|
settings or data without your approval.
|
||||||
- https://developer.apple.com/documentation/devicemanagement/softwareupdate
|
|
||||||
|
The Center for Internet Security (CIS) advises against automatic updates in scenarios where changes require
|
||||||
|
thorough testing and approval processes to avoid operational disruptions [1] [2] [3] [4].
|
||||||
|
|
||||||
|
This script configures following to stop macOS from installing updates automatically:
|
||||||
|
|
||||||
|
1. `/Library/Preferences/com.apple.commerce!AutoUpdateRestartRequired`:
|
||||||
|
This preference stops the system from automatically installing macOS updates [1] [2] [3] [4] [5] [6] [7] [8].
|
||||||
|
By doing this, updates will only be installed when you decide, giving you a chance to check them first [1] [2] [3] [4] [5] [6] [7] [8].
|
||||||
|
This setting applies to OS X Yosemite through macOS High Sierra [7] [9].
|
||||||
|
|
||||||
|
2. `/Library/Preferences/com.apple.commerce!AutomaticallyInstallMacOSUpdates`:
|
||||||
|
Changing this setting stops macOS from installing updates automatically [3] [5] [9] [10], giving you control over when to update.
|
||||||
|
If restricts the *Install macOS Updates* option and prevents the user from changing the option [10].
|
||||||
|
While this setting enhances privacy, it's generally not advised by NIST due to potential security risks [9].
|
||||||
|
This setting applies to macOS Mojave and newer versions [9].
|
||||||
|
|
||||||
|
> **Caution**: Disabling automatic updates requires you to manually check and apply updates to stay protected against security threats [1] [2] [3] [4].
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240321165149/https://www.tenable.com/audits/items/CIS_Apple_macOS_10.12_v1.1.0_Level_1.audit:e02dfdd6bec9556a3ce537f60b91b549 "CIS Apple macOS 10.12 L1 v1.1.0 | 1.5 Enable OS X update installs | Tenable®"
|
||||||
|
[2]: https://web.archive.org/web/20240321165851/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_10_13_Benchmark_v1_1_0---PDF.pdf "CIS Apple macOS 10.13 Benchmark v1.1.0 | paper.bobylive.com"
|
||||||
|
[3]: https://web.archive.org/web/20240321170400/https://www.tenable.com/audits/items/CIS_Apple_macOS_13.0_Ventura_v1.0.0_L1.audit:fe03c59a39c7c949507ff20d07f89993 "1.4 Ensure Install of macOS Updates Is Enabled | Tenable® | www.tenable.com"
|
||||||
|
[4]: https://web.archive.org/web/20240321170036/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_10_14_Benchmark_v1_4_0_PDF.pdf "CIS Apple macOS 10.14 Benchmark v1.4.0 | paper.bobylive.com"
|
||||||
|
[5]: https://web.archive.org/web/20240321164917/https://www.ncsc.gov.uk/files/macos_provisioning_script.sh_.txt "macOS provisioning script | UK National Cyber Security Centre | www.ncsc.gov.uk"
|
||||||
|
[6]: https://web.archive.org/web/20240321165118/https://macadminsdoc.readthedocs.io/en/master/Profiles-and-Settings/OS-X-Updates.html "macOS Updates — MacAdmins Community Documentation documentation | macadminsdoc.readthedocs.io"
|
||||||
|
[7]: https://web.archive.org/web/20240321165304/https://derflounder.wordpress.com/2014/12/29/managing-automatic-app-store-and-os-x-update-installation-on-yosemite/ "Managing automatic App Store and OS X update installation on Yosemite | Der Flounder | derflounder.wordpress.com"
|
||||||
|
[8]: https://web.archive.org/web/20240321170034/https://krypted.com/mac-os-x/app-store-preferences-set-server-5-4-macos-high-sierra/ "App Store Preferences To Set In On Server 5.4 for macOS High Sierra – krypted | krypted.com"
|
||||||
|
[9]: https://web.archive.org/web/20240321170251/https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/ "Enabling automatic macOS software updates for OS X Yosemite through macOS Mojave | Der Flounder | derflounder.wordpress.com"
|
||||||
|
[10]: https://archive.ph/2024.03.21-180353/https://developer.apple.com/documentation/devicemanagement/softwareupdate "SoftwareUpdate | Apple Developer Documentation | developer.apple.com"
|
||||||
|
[11]: https://web.archive.org/web/20240321165931/https://csrc.nist.gov/CSRC/media/Projects/national-vulnerability-database/documents/CCE/CCE-macos_monterey.xls "CCE-91129-7 | CCE-macos_monterey.xls | Sheet 1 - NIST Computer Security Resource Center | csrc.nist.gov"
|
||||||
code: |-
|
code: |-
|
||||||
# For OS X Yosemite through macOS High Sierra (>= 10.10 && < 10.14)
|
# For OS X Yosemite through macOS High Sierra (>= 10.10 && < 10.14)
|
||||||
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdateRestartRequired' -bool false
|
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdateRestartRequired' -bool false
|
||||||
@@ -1492,9 +1537,44 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallMacOSUpdates' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallMacOSUpdates' -bool true
|
||||||
-
|
-
|
||||||
name: Disable automatic app updates from the App Store
|
name: Disable automatic app updates from the App Store
|
||||||
docs:
|
docs: |-
|
||||||
- https://kb.vmware.com/s/article/2960635
|
This script disables automatic app updates [1] [2] [3] [4] from the App Store [5] [6] [7] [8] [9] [10] [11] [12] [13].
|
||||||
- https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/
|
It prevents automatic installation of application updates as soon as they become available from Apple [2] [3] [6] [9] [11] [12] [13].
|
||||||
|
Thus, applications are updated only when you choose to do so [5].
|
||||||
|
|
||||||
|
Disabling automatic updates prevents unexpected app behavior or settings changes.
|
||||||
|
It helps you to maintain your current app configurations and privacy settings.
|
||||||
|
It also protects against potential zero-day vulnerabilities in your apps.
|
||||||
|
This gives you the ability to choose which updates to install and when, enabling you to review the details of updates before deciding to proceed.
|
||||||
|
|
||||||
|
The script modifies the following settings:
|
||||||
|
|
||||||
|
1. `/Library/Preferences/com.apple.commerce!AutoUpdate`:
|
||||||
|
Disables automated app updates [1] [2] [3] [6] [9] [10] [13] from the App Store [7] [8].
|
||||||
|
This setting applies to OS X Yosemite and newer versions [1].
|
||||||
|
2. `/Library/Preferences/com.apple.SoftwareUpdate!AutomaticallyInstallAppUpdates`:
|
||||||
|
Stops the automatic installation of app updates [1] [4] from App Store [9] [10] [11] [12] [13].
|
||||||
|
It deselects the *Install app updates from the App Store* option and prevents the user from changing the option [10].
|
||||||
|
While this setting enhances privacy, it's generally not advised by NIST due to potential security risks [4].
|
||||||
|
This setting applies to macOS Mojave and newer versions [1].
|
||||||
|
|
||||||
|
> **Caution**:
|
||||||
|
> Disabling app updates means you should manually check for and install important security patches for every application
|
||||||
|
> to protect against vulnerabilities [2] [3] [5] [6] [9] [11] [12] [13].
|
||||||
|
|
||||||
|
[1]: https://web.archive.org/web/20240321170251/https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/ "Enabling automatic macOS software updates for OS X Yosemite through macOS Mojave | Der Flounder | derflounder.wordpress.com"
|
||||||
|
[2]: https://web.archive.org/web/20240321190032/https://www.irs.gov/pub/irs-utl/safeguards-scsem-macosx-v6-1-093021.xlsx "SCSEM OSX 10.14 | Internal Revenue Service Office of Safeguards | www.irs.gov"
|
||||||
|
[3]: https://web.archive.org/web/20240321170036/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_10_14_Benchmark_v1_4_0_PDF.pdf "CIS Apple macOS 10.14 Benchmark v1.4.0 | paper.bobylive.com"
|
||||||
|
[5]: https://web.archive.org/web/20240321190244/https://github-wiki-see.page/m/edamametechnologies/threatmodels/wiki/threatmodel-macOS-EN "threatmodel macOS EN - edamametechnologies/threatmodels GitHub Wiki | github-wiki-see.page"
|
||||||
|
[6]: https://web.archive.org/web/20240321190315/https://www.tenable.com/audits/items/CIS_Apple_macOS_14.0_Sonoma_v1.0.0_L1.audit:66d3b86318384ba7947a3409e0c6e902 "1.5 Ensure Install Application Updates from the App Store Is E... | Tenable® | www.tenable.com"
|
||||||
|
[7]: https://web.archive.org/web/20240321165304/https://derflounder.wordpress.com/2014/12/29/managing-automatic-app-store-and-os-x-update-installation-on-yosemite/ "Managing automatic App Store and OS X update installation on Yosemite | Der Flounder | derflounder.wordpress.com"
|
||||||
|
[8]: https://web.archive.org/web/20240321190410/https://krypted.com/mac-security/app-store-preferences-set-server-5-2-macos-sierra/ "App Store Preferences To Set In On Server 5.2 for macOS Sierra – krypted | krypted.com"
|
||||||
|
[4]: https://web.archive.org/web/20240321165931/https://csrc.nist.gov/CSRC/media/Projects/national-vulnerability-database/documents/CCE/CCE-macos_monterey.xls "CCE-91129-7 | CCE-macos_monterey.xls | Sheet 1 - NIST Computer Security Resource Center | csrc.nist.gov"
|
||||||
|
[9]: https://web.archive.org/web/20240321190114/https://www.irs.gov/pub/irs-utl/safeguards-scsem-macosx.xlsx "SCSEM OSX 13.0 | Internal Revenue Service Office of Safeguards | www.irs.gov"
|
||||||
|
[10]: https://archive.ph/2024.03.21-180353/https://developer.apple.com/documentation/devicemanagement/softwareupdate "SoftwareUpdate | Apple Developer Documentation | developer.apple.com"
|
||||||
|
[11]: https://web.archive.org/web/20240321190122/https://paper.bobylive.com/Security/CIS/CIS_Apple_macOS_12_0_Monterey_Benchmark_v1_0_0.pdf "CIS Apple macOS 12.0 Monterey | CIS Benchmarks | paper.bobylive.com"
|
||||||
|
[12]: https://web.archive.org/web/20240321190537/https://www.tenable.com/audits/items/CIS_Apple_macOS_11_v2.0.0_L1.audit:55e8759872dce781b8dbc5a3f42e23b9 "1.4 Ensure Installation of App Update Is Enabled | Tenable® | www.tenable.com"
|
||||||
|
[13]: https://web.archive.org/web/20240321164917/https://www.ncsc.gov.uk/files/macos_provisioning_script.sh_.txt "macOS provisioning script | UK National Cyber Security Centre | www.ncsc.gov.uk"
|
||||||
code: |-
|
code: |-
|
||||||
# For OS X Yosemite and newer (>= 10.10)
|
# For OS X Yosemite and newer (>= 10.10)
|
||||||
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdate' -bool false
|
sudo defaults write /Library/Preferences/com.apple.commerce 'AutoUpdate' -bool false
|
||||||
@@ -1507,7 +1587,7 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallAppUpdates' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AutomaticallyInstallAppUpdates' -bool true
|
||||||
-
|
-
|
||||||
name: Disable macOS beta release installation
|
name: Disable macOS beta release installation
|
||||||
docs: https://support.apple.com/en-gb/HT203018
|
docs: https://web.archive.org/web/20170106103856/https://support.apple.com/en-gb/HT203018
|
||||||
code: |-
|
code: |-
|
||||||
# For OS X Yosemite and newer (>= 10.10)
|
# For OS X Yosemite and newer (>= 10.10)
|
||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AllowPreReleaseInstallation' -bool false
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AllowPreReleaseInstallation' -bool false
|
||||||
@@ -1516,7 +1596,7 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AllowPreReleaseInstallation' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'AllowPreReleaseInstallation' -bool true
|
||||||
-
|
-
|
||||||
name: Disable automatic installation for configuration data (e.g. XProtect, Gatekeeper, MRT)
|
name: Disable automatic installation for configuration data (e.g. XProtect, Gatekeeper, MRT)
|
||||||
docs: https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/
|
docs: https://web.archive.org/web/20240321170251/https://derflounder.wordpress.com/2018/12/28/enabling-automatic-macos-software-updates-for-os-x-yosemite-through-macos-mojave/
|
||||||
code: |-
|
code: |-
|
||||||
# For OS X Yosemite and newer (>= 10.10)
|
# For OS X Yosemite and newer (>= 10.10)
|
||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'ConfigDataInstall' -bool false
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'ConfigDataInstall' -bool false
|
||||||
@@ -1525,12 +1605,47 @@ actions:
|
|||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'ConfigDataInstall' -bool true
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'ConfigDataInstall' -bool true
|
||||||
-
|
-
|
||||||
name: Disable automatic installation for system data files and security updates
|
name: Disable automatic installation for system data files and security updates
|
||||||
docs:
|
docs: |-
|
||||||
# References for CriticalUpdateInstall
|
This script stops automatic installations of critical updates [1],
|
||||||
- https://derflounder.wordpress.com/2014/12/24/managing-os-xs-automatic-security-updates/
|
including security [1] [2] [3] [4] [5] [6] [7] and system data file [1] [8] updates.
|
||||||
- https://developer.apple.com/documentation/devicemanagement/softwareupdate
|
|
||||||
# References for softwareupdate --background-critical
|
It improves privacy by providing:
|
||||||
- https://managingosx.wordpress.com/2013/04/30/undocumented-options/
|
|
||||||
|
- **Control Over Update Timing**:
|
||||||
|
Users can review updates before installation to ensure they meet privacy standards and do not introduce
|
||||||
|
unwanted telemetry or changes.
|
||||||
|
- **Reduced External Communications**:
|
||||||
|
Reduces how often it connects to update servers, potentially protection user information.
|
||||||
|
|
||||||
|
The script configures the `/Library/Preferences/com.apple.SoftwareUpdate!CriticalUpdateInstall` setting [1] [4] [5] [7] [8].
|
||||||
|
This action prevents automatic downloads and installations of updates [1].
|
||||||
|
It also prevents users from changing the Install system data files and security updates option manually [1].
|
||||||
|
This script is compatible with OS X Yosemite and later versions [6] [8].
|
||||||
|
|
||||||
|
The revert script triggers `softwareupdate --background-critical` to install any pending critical updates directly [2] [9].
|
||||||
|
|
||||||
|
> **Caution:**
|
||||||
|
> Only disable automatic updates if you're committed to manually installing them quickly to maintain your computer's security [4] [5] [8].
|
||||||
|
> It's important to install updates soon to protect your computer. [4] [5] [8].
|
||||||
|
>
|
||||||
|
> This script disables:
|
||||||
|
>
|
||||||
|
> - Definition updates for **XProtect** and **Gatekeeper** that keep your computer safe from new threats [5].
|
||||||
|
> - **Rapid Security Response** [10] [11].
|
||||||
|
> **Rapid Security Responses** are software releases providing important security improvements between standard updates [12].
|
||||||
|
|
||||||
|
[1]: https://archive.ph/2024.03.21-180353/https://developer.apple.com/documentation/devicemanagement/softwareupdate "SoftwareUpdate | Apple Developer Documentation | developer.apple.com"
|
||||||
|
[2]: https://web.archive.org/web/20240321201417/https://derflounder.wordpress.com/2014/12/24/managing-os-xs-automatic-security-updates/ "Managing OS X’s automatic security updates | Der Flounder | derflounder.wordpress.com"
|
||||||
|
[3]: https://web.archive.org/web/20240321165118/https://macadminsdoc.readthedocs.io/en/master/Profiles-and-Settings/OS-X-Updates.html "macOS Updates — MacAdmins Community Documentation documentation | macadminsdoc.readthedocs.io"
|
||||||
|
[4]: https://web.archive.org/web/20240321165931/https://csrc.nist.gov/CSRC/media/Projects/national-vulnerability-database/documents/CCE/CCE-macos_monterey.xls "CCE-91129-7 | CCE-macos_monterey.xls | Sheet 1 - NIST Computer Security Resource Center | csrc.nist.gov"
|
||||||
|
[5]: https://web.archive.org/web/20240321201450/https://paper.bobylive.com/Security/CIS/CIS_Apple_OSX_10_9_Benchmark_v1_3_0.pdf "CIS Apple OSX 10.9 Benchmark | paper.bobylive.com"
|
||||||
|
[6]: https://web.archive.org/web/20240321201643/https://derflounder.wordpress.com/2014/12/27/managing-automatic-installation-of-configdata-and-security-software-updates-on-yosemite/ "Managing automatic installation of ConfigData and security software updates on Yosemite | Der Flounder | derflounder.wordpress.com"
|
||||||
|
[7]: https://web.archive.org/web/20240321201652/https://ss64.com/mac/syntax-defaults.html "System preference settings for macOS - macOS - SS64.com | ss64.com"
|
||||||
|
[8]: https://web.archive.org/web/20240321201436/https://www.tenable.com/audits/items/CIS_OSX_10.10_v1.2.0_L1.audit:97f36c2eaa06045e85a1beff1a76a088 "1.4 Enable system data files and security update installs - 'C... | Tenable® | www.tenable.com"
|
||||||
|
[9]: https://web.archive.org/web/20240321201406/https://managingosx.wordpress.com/2013/04/30/undocumented-options/ "Undocumented options – Managing OS X | managingosx.wordpress.com"
|
||||||
|
[10]: https://web.archive.org/web/20240321201558/https://www.intuneirl.com/rapid-security-response/ "Managing Rapid Security Response on Apple Devices | www.intuneirl.com"
|
||||||
|
[11]: https://web.archive.org/web/20240321201614/https://onsitegroup.co.za/rapid-security-response/ "Rapid security response - Onsite | onsitegroup.co.za"
|
||||||
|
[12]: https://web.archive.org/web/20240321201623/https://support.apple.com/en-us/102657 "About Rapid Security Responses for iOS, iPadOS, and macOS - Apple Support | support.apple.com"
|
||||||
code: |-
|
code: |-
|
||||||
# For OS X Yosemite and newer (>= 10.10)
|
# For OS X Yosemite and newer (>= 10.10)
|
||||||
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool false
|
sudo defaults write /Library/Preferences/com.apple.SoftwareUpdate 'CriticalUpdateInstall' -bool false
|
||||||
|
|||||||
@@ -5,15 +5,21 @@ import type { IScript } from './IScript';
|
|||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
||||||
|
|
||||||
constructor(
|
public readonly name: string;
|
||||||
id: number,
|
|
||||||
public readonly name: string,
|
public readonly docs: ReadonlyArray<string>;
|
||||||
public readonly docs: ReadonlyArray<string>,
|
|
||||||
public readonly subCategories: ReadonlyArray<ICategory>,
|
public readonly subCategories: ReadonlyArray<ICategory>;
|
||||||
public readonly scripts: ReadonlyArray<IScript>,
|
|
||||||
) {
|
public readonly scripts: ReadonlyArray<IScript>;
|
||||||
super(id);
|
|
||||||
validateCategory(this);
|
constructor(parameters: CategoryInitParameters) {
|
||||||
|
super(parameters.id);
|
||||||
|
validateParameters(parameters);
|
||||||
|
this.name = parameters.name;
|
||||||
|
this.docs = parameters.docs;
|
||||||
|
this.subCategories = parameters.subcategories;
|
||||||
|
this.scripts = parameters.scripts;
|
||||||
}
|
}
|
||||||
|
|
||||||
public includes(script: IScript): boolean {
|
public includes(script: IScript): boolean {
|
||||||
@@ -28,6 +34,14 @@ export class Category extends BaseEntity<number> implements ICategory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CategoryInitParameters {
|
||||||
|
readonly id: number;
|
||||||
|
readonly name: string;
|
||||||
|
readonly docs: ReadonlyArray<string>;
|
||||||
|
readonly subcategories: ReadonlyArray<ICategory>;
|
||||||
|
readonly scripts: ReadonlyArray<IScript>;
|
||||||
|
}
|
||||||
|
|
||||||
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||||
return [
|
return [
|
||||||
...category.scripts,
|
...category.scripts,
|
||||||
@@ -35,11 +49,11 @@ function parseScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCategory(category: ICategory) {
|
function validateParameters(parameters: CategoryInitParameters) {
|
||||||
if (!category.name) {
|
if (!parameters.name) {
|
||||||
throw new Error('missing name');
|
throw new Error('missing name');
|
||||||
}
|
}
|
||||||
if (category.subCategories.length === 0 && category.scripts.length === 0) {
|
if (parameters.subcategories.length === 0 && parameters.scripts.length === 0) {
|
||||||
throw new Error('A category must have at least one sub-category or script');
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,21 @@ import type { IScript } from './IScript';
|
|||||||
import type { IScriptCode } from './IScriptCode';
|
import type { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
export class Script extends BaseEntity<string> implements IScript {
|
export class Script extends BaseEntity<string> implements IScript {
|
||||||
constructor(
|
public readonly name: string;
|
||||||
public readonly name: string,
|
|
||||||
public readonly code: IScriptCode,
|
public readonly code: IScriptCode;
|
||||||
public readonly docs: ReadonlyArray<string>,
|
|
||||||
public readonly level?: RecommendationLevel,
|
public readonly docs: ReadonlyArray<string>;
|
||||||
) {
|
|
||||||
super(name);
|
public readonly level?: RecommendationLevel;
|
||||||
validateLevel(level);
|
|
||||||
|
constructor(parameters: ScriptInitParameters) {
|
||||||
|
super(parameters.name);
|
||||||
|
this.name = parameters.name;
|
||||||
|
this.code = parameters.code;
|
||||||
|
this.docs = parameters.docs;
|
||||||
|
this.level = parameters.level;
|
||||||
|
validateLevel(parameters.level);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canRevert(): boolean {
|
public canRevert(): boolean {
|
||||||
@@ -19,6 +26,13 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ScriptInitParameters {
|
||||||
|
readonly name: string;
|
||||||
|
readonly code: IScriptCode;
|
||||||
|
readonly docs: ReadonlyArray<string>;
|
||||||
|
readonly level?: RecommendationLevel;
|
||||||
|
}
|
||||||
|
|
||||||
function validateLevel(level?: RecommendationLevel) {
|
function validateLevel(level?: RecommendationLevel) {
|
||||||
if (level !== undefined && !(level in RecommendationLevel)) {
|
if (level !== undefined && !(level in RecommendationLevel)) {
|
||||||
throw new Error(`invalid level: ${level}`);
|
throw new Error(`invalid level: ${level}`);
|
||||||
|
|||||||
10
src/domain/ScriptCodeFactory.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { ScriptCode } from './ScriptCode';
|
||||||
|
import type { IScriptCode } from './IScriptCode';
|
||||||
|
|
||||||
|
export interface ScriptCodeFactory {
|
||||||
|
(
|
||||||
|
...args: ConstructorParameters<typeof ScriptCode>
|
||||||
|
): IScriptCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createScriptCode: ScriptCodeFactory = (...args) => new ScriptCode(...args);
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
export interface CommandDefinition {
|
||||||
|
buildShellCommand(filePath: string): string;
|
||||||
|
isExecutionTerminatedExternally(exitCode: number): boolean;
|
||||||
|
isExecutablePermissionsRequiredOnFile(): boolean;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { PosixShellArgumentEscaper } from './ShellArgument/PosixShellArgumentEscaper';
|
||||||
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||||
|
|
||||||
|
export const LinuxTerminalEmulator = 'x-terminal-emulator';
|
||||||
|
|
||||||
|
export class LinuxVisibleTerminalCommand implements CommandDefinition {
|
||||||
|
constructor(
|
||||||
|
private readonly escaper: ShellArgumentEscaper = new PosixShellArgumentEscaper(),
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public buildShellCommand(filePath: string): string {
|
||||||
|
return `${LinuxTerminalEmulator} -e ${this.escaper.escapePathArgument(filePath)}`;
|
||||||
|
/*
|
||||||
|
🤔 Potential improvements:
|
||||||
|
Use user-friendly GUI sudo prompt (not terminal-based).
|
||||||
|
If `pkexec` exists, we could do `x-terminal-emulator -e pkexec 'path'`, which always
|
||||||
|
prompts with user-friendly GUI sudo prompt.
|
||||||
|
📝 Options:
|
||||||
|
`x-terminal-emulator -e 'path'`:
|
||||||
|
✅ Visible terminal window
|
||||||
|
❌ Terminal-based (not GUI) sudo prompt.
|
||||||
|
`x-terminal-emulator -e pkexec 'path'
|
||||||
|
✅ Visible terminal window
|
||||||
|
✅ Always prompts with user-friendly GUI sudo prompt.
|
||||||
|
🤔 Not using `pkexec` as it is not in all Linux distributions. It should have smarter
|
||||||
|
logic to handle if it does not exist.
|
||||||
|
`electron.shell.openPath`:
|
||||||
|
❌ Opens the script in the default text editor, verified on
|
||||||
|
Debian/Ubuntu-based distributions.
|
||||||
|
`child_process.execFile()`:
|
||||||
|
❌ Script execution in the background without a visible terminal.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExecutionTerminatedExternally(exitCode: number): boolean {
|
||||||
|
return exitCode === 137;
|
||||||
|
/*
|
||||||
|
`x-terminal-emulator` may return exit code `137` under specific circumstances like when the
|
||||||
|
user closes the terminal (observed with `gnome-terminal` on Pop!_OS). This exit code (128 +
|
||||||
|
Unix signal 9) indicates the process was terminated by a SIGKILL signal, which can occur due
|
||||||
|
to user action (cancelling the progress) or the system (e.g., due to memory shortages).
|
||||||
|
|
||||||
|
Additional exit codes noted for future consideration (currently not handled as they have not
|
||||||
|
been reproduced):
|
||||||
|
|
||||||
|
- 130 (130 = 128 + Unix signal 2): Indicates the script was terminated by the user
|
||||||
|
(Control-C), corresponding to a SIGINT signal.
|
||||||
|
- 143 (128 + Unix signal 15): Indicates termination by a SIGTERM signal, suggesting a request
|
||||||
|
to gracefully terminate the process.
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExecutablePermissionsRequiredOnFile(): boolean {
|
||||||
|
/*
|
||||||
|
On Linux, a script file without executable permissions cannot be run directly by its path
|
||||||
|
without specifying a shell explicitly.
|
||||||
|
*/
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import { PosixShellArgumentEscaper } from './ShellArgument/PosixShellArgumentEscaper';
|
||||||
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||||
|
|
||||||
|
export class MacOsVisibleTerminalCommand implements CommandDefinition {
|
||||||
|
constructor(
|
||||||
|
private readonly escaper: ShellArgumentEscaper = new PosixShellArgumentEscaper(),
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public buildShellCommand(filePath: string): string {
|
||||||
|
return `open -a Terminal.app ${this.escaper.escapePathArgument(filePath)}`;
|
||||||
|
/*
|
||||||
|
📝 Options:
|
||||||
|
`child_process.execFile()`
|
||||||
|
"path", `cmd.exe /c "path"`
|
||||||
|
❌ Script execution in the background without a visible terminal.
|
||||||
|
This occurs only when the user runs the application as administrator, as seen
|
||||||
|
in Windows Pro VMs on Azure.
|
||||||
|
`PowerShell Start -Verb RunAs "path"`
|
||||||
|
✅ Visible terminal window
|
||||||
|
✅ GUI sudo prompt (through `RunAs` option)
|
||||||
|
`PowerShell Start "path"`
|
||||||
|
`explorer.exe "path"`
|
||||||
|
`electron.shell.openPath`
|
||||||
|
`start cmd.exe /c "$path"`
|
||||||
|
✅ Visible terminal window
|
||||||
|
✅ GUI sudo prompt (through `RunAs` option)
|
||||||
|
👍 Among all options `start` command is the most explicit one, being the most resilient
|
||||||
|
against the potential changes in Windows or Electron framework (e.g. https://github.com/electron/electron/issues/36765).
|
||||||
|
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||||
|
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||||
|
*/
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExecutionTerminatedExternally(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExecutablePermissionsRequiredOnFile(): boolean {
|
||||||
|
/*
|
||||||
|
On macOS, a script file without executable permissions cannot be run directly by its path
|
||||||
|
without specifying a shell explicitly.
|
||||||
|
*/
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvokeShellCommandCreator';
|
||||||
|
|
||||||
|
/**
|
||||||
|
Encoding PowerShell commands resolve issues with quote handling.
|
||||||
|
|
||||||
|
There are known problems with PowerShell's handling of double quotes in command line arguments:
|
||||||
|
- Quote stripping in PowerShell command line arguments: https://web.archive.org/web/20240507102706/https://stackoverflow.com/questions/6714165/powershell-stripping-double-quotes-from-command-line-arguments
|
||||||
|
- privacy.sexy double quotes issue when calling PowerShell from command line: https://web.archive.org/web/20240507102841/https://github.com/undergroundwires/privacy.sexy/issues/351
|
||||||
|
- Challenges with single quotes in PowerShell command line: https://web.archive.org/web/20240507102047/https://stackoverflow.com/questions/20958388/command-line-escaping-single-quote-for-powershell
|
||||||
|
|
||||||
|
Using the `EncodedCommand` parameter is recommended by Microsoft for handling
|
||||||
|
complex quoting scenarios. This approach helps avoid issues by encoding the entire
|
||||||
|
command as a Base64 string:
|
||||||
|
- Microsoft's documentation on using the `EncodedCommand` parameter: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||||
|
*/
|
||||||
|
export class EncodedPowerShellInvokeCmdCommandCreator
|
||||||
|
implements PowerShellInvokeShellCommandCreator {
|
||||||
|
public createCommandToInvokePowerShell(powerShellScript: string): string {
|
||||||
|
return generateEncodedPowershellCommand(powerShellScript);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateEncodedPowershellCommand(powerShellScript: string): string {
|
||||||
|
const encodedCommand = encodeForPowershellExecution(powerShellScript);
|
||||||
|
return `PowerShell -EncodedCommand ${encodedCommand}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function encodeForPowershellExecution(script: string): string {
|
||||||
|
// The string must be formatted using UTF-16LE character encoding, see: https://web.archive.org/web/20240507102733/https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_powershell_exe?view=powershell-5.1#-encodedcommand-base64encodedcommand
|
||||||
|
const buffer = Buffer.from(script, 'utf16le');
|
||||||
|
return buffer.toString('base64');
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface PowerShellInvokeShellCommandCreator {
|
||||||
|
createCommandToInvokePowerShell(powerShellCommand: string): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
||||||
|
|
||||||
|
export class PosixShellArgumentEscaper implements ShellArgumentEscaper {
|
||||||
|
public escapePathArgument(pathArgument: string): string {
|
||||||
|
return posixShellPathArgumentEscape(pathArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function posixShellPathArgumentEscape(pathArgument: string): string {
|
||||||
|
/*
|
||||||
|
- Wraps the path in single quotes, which is a standard practice in POSIX shells
|
||||||
|
(like bash and zsh) found on macOS/Linux to ensure that characters like spaces, '*', and
|
||||||
|
'?' are treated as literals, not as special characters.
|
||||||
|
- Escapes any single quotes within the path itself. This allows paths containing single
|
||||||
|
quotes to be correctly interpreted in POSIX-compliant systems, such as Linux and macOS.
|
||||||
|
*/
|
||||||
|
return `'${pathArgument.replaceAll('\'', '\'\\\'\'')}'`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import type { ShellArgumentEscaper } from './ShellArgumentEscaper';
|
||||||
|
|
||||||
|
export class PowerShellArgumentEscaper implements ShellArgumentEscaper {
|
||||||
|
public escapePathArgument(pathArgument: string): string {
|
||||||
|
return powerShellPathArgumentEscape(pathArgument);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function powerShellPathArgumentEscape(pathArgument: string): string {
|
||||||
|
// - Encloses the path in single quotes to handle spaces and most special characters.
|
||||||
|
// - Single quotes are used in PowerShell to ensure the string is treated as a literal string.
|
||||||
|
// - Paths in Windows can include single quotes ('), so any internal single quotes are escaped
|
||||||
|
// using double quotes.
|
||||||
|
return `'${pathArgument.replace(/'/g, "''")}'`;
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export interface ShellArgumentEscaper {
|
||||||
|
escapePathArgument(pathArgument: string): string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
|
import { PowerShellArgumentEscaper } from './ShellArgument/PowerShellArgumentEscaper';
|
||||||
|
import { EncodedPowerShellInvokeCmdCommandCreator } from './PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator';
|
||||||
|
import type { ShellArgumentEscaper } from './ShellArgument/ShellArgumentEscaper';
|
||||||
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
import type { PowerShellInvokeShellCommandCreator } from './PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||||
|
|
||||||
|
export class WindowsVisibleTerminalCommand implements CommandDefinition {
|
||||||
|
constructor(
|
||||||
|
private readonly escaper: ShellArgumentEscaper = new PowerShellArgumentEscaper(),
|
||||||
|
private readonly powershellCommandCreator: PowerShellInvokeShellCommandCreator
|
||||||
|
= new EncodedPowerShellInvokeCmdCommandCreator(),
|
||||||
|
private readonly logger: Logger = ElectronLogger,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public buildShellCommand(filePath: string): string {
|
||||||
|
const powershellCommand = [
|
||||||
|
'Start-Process',
|
||||||
|
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
||||||
|
`-FilePath ${this.escaper.escapePathArgument(filePath)}`,
|
||||||
|
].join(' ');
|
||||||
|
/*
|
||||||
|
Running PowerShell command is preferred due to its flexibility and the way it provides
|
||||||
|
GUI sudo prompt through `RunAs` argument.
|
||||||
|
Other options considered:
|
||||||
|
`child_process.execFile()`
|
||||||
|
"path", `cmd.exe /c "path"`
|
||||||
|
❌ Script execution in the background without a visible terminal.
|
||||||
|
This occurs only when the user runs the application as administrator, as seen
|
||||||
|
in Windows Pro VMs on Azure.
|
||||||
|
`PowerShell Start -Verb RunAs "path"`
|
||||||
|
✅ Visible terminal window
|
||||||
|
✅ GUI sudo prompt (through `RunAs` option)
|
||||||
|
`PowerShell Start "path"`
|
||||||
|
`explorer.exe "path"`
|
||||||
|
`electron.shell.openPath`
|
||||||
|
`start cmd.exe /c "$path"`
|
||||||
|
✅ Visible terminal window
|
||||||
|
✅ GUI sudo prompt (through `RunAs` option)
|
||||||
|
👍 Among all options `start` command is the most explicit one, being the most resilient
|
||||||
|
against the potential changes in Windows or Electron framework (e.g. https://github.com/electron/electron/issues/36765).
|
||||||
|
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
||||||
|
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
||||||
|
*/
|
||||||
|
this.logger.info(`Building command for PowerShell execution:\n\tCommand: ${powershellCommand}`);
|
||||||
|
return this.powershellCommandCreator.createCommandToInvokePowerShell(powershellCommand);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExecutionTerminatedExternally(): boolean {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExecutablePermissionsRequiredOnFile(): boolean {
|
||||||
|
/*
|
||||||
|
In Windows, whether a file can be executed is determined by its file extension
|
||||||
|
(.exe, .bat, .cmd, etc.) rather than executable permissions set on the file.
|
||||||
|
*/
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
|
||||||
|
export interface CommandDefinitionFactory {
|
||||||
|
provideCommandDefinition(): CommandDefinition;
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
||||||
|
import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
||||||
|
import { WindowsVisibleTerminalCommand } from '../Commands/WindowsVisibleTerminalCommand';
|
||||||
|
import { LinuxVisibleTerminalCommand } from '../Commands/LinuxVisibleTerminalCommand';
|
||||||
|
import { MacOsVisibleTerminalCommand } from '../Commands/MacOsVisibleTerminalCommand';
|
||||||
|
import type { CommandDefinitionFactory } from './CommandDefinitionFactory';
|
||||||
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
|
||||||
|
export class OsSpecificTerminalLaunchCommandFactory implements CommandDefinitionFactory {
|
||||||
|
constructor(
|
||||||
|
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public provideCommandDefinition(): CommandDefinition {
|
||||||
|
const { os } = this.environment;
|
||||||
|
if (os === undefined) {
|
||||||
|
throw new Error('Operating system could not be identified from environment.');
|
||||||
|
}
|
||||||
|
return getOperatingSystemCommandDefinition(os);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getOperatingSystemCommandDefinition(
|
||||||
|
operatingSystem: OperatingSystem,
|
||||||
|
): CommandDefinition {
|
||||||
|
const definition = SupportedDesktopCommandDefinitions[operatingSystem];
|
||||||
|
if (!definition) {
|
||||||
|
throw new Error(`Unsupported operating system: ${OperatingSystem[operatingSystem]}`);
|
||||||
|
}
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SupportedDesktopCommandDefinitions: Readonly<Partial<Record<
|
||||||
|
OperatingSystem,
|
||||||
|
CommandDefinition>>> = {
|
||||||
|
[OperatingSystem.Windows]: new WindowsVisibleTerminalCommand(),
|
||||||
|
[OperatingSystem.Linux]: new LinuxVisibleTerminalCommand(),
|
||||||
|
[OperatingSystem.macOS]: new MacOsVisibleTerminalCommand(),
|
||||||
|
} as const;
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ScriptFileExecutionOutcome } from '../../ScriptFileExecutor';
|
||||||
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
|
||||||
|
export interface CommandDefinitionRunner {
|
||||||
|
runCommandDefinition(
|
||||||
|
commandDefinition: CommandDefinition,
|
||||||
|
filePath: string,
|
||||||
|
): Promise<ScriptFileExecutionOutcome>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
import type { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
||||||
|
import { FileSystemExecutablePermissionSetter } from './PermissionSetter/FileSystemExecutablePermissionSetter';
|
||||||
|
import { LoggingNodeShellCommandRunner } from './ShellRunner/LoggingNodeShellCommandRunner';
|
||||||
|
import type { FailedScriptFileExecution, ScriptFileExecutionOutcome } from '../../ScriptFileExecutor';
|
||||||
|
import type { CommandDefinition } from '../CommandDefinition';
|
||||||
|
import type { CommandDefinitionRunner } from './CommandDefinitionRunner';
|
||||||
|
import type { ExecutablePermissionSetter } from './PermissionSetter/ExecutablePermissionSetter';
|
||||||
|
import type { ShellCommandOutcome, ShellCommandRunner } from './ShellRunner/ShellCommandRunner';
|
||||||
|
|
||||||
|
export class ExecutableFileShellCommandDefinitionRunner implements CommandDefinitionRunner {
|
||||||
|
constructor(
|
||||||
|
private readonly executablePermissionSetter: ExecutablePermissionSetter
|
||||||
|
= new FileSystemExecutablePermissionSetter(),
|
||||||
|
private readonly shellCommandRunner: ShellCommandRunner
|
||||||
|
= new LoggingNodeShellCommandRunner(),
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async runCommandDefinition(
|
||||||
|
commandDefinition: CommandDefinition,
|
||||||
|
filePath: string,
|
||||||
|
): Promise<ScriptFileExecutionOutcome> {
|
||||||
|
if (commandDefinition.isExecutablePermissionsRequiredOnFile()) {
|
||||||
|
const filePermissionsResult = await this.executablePermissionSetter
|
||||||
|
.makeFileExecutable(filePath);
|
||||||
|
if (!filePermissionsResult.success) {
|
||||||
|
return filePermissionsResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const command = commandDefinition.buildShellCommand(filePath);
|
||||||
|
const shellOutcome = await this.shellCommandRunner.runShellCommand(command);
|
||||||
|
return interpretShellOutcome(shellOutcome, commandDefinition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function interpretShellOutcome(
|
||||||
|
outcome: ShellCommandOutcome,
|
||||||
|
commandDefinition: CommandDefinition,
|
||||||
|
): ScriptFileExecutionOutcome {
|
||||||
|
switch (outcome.type) {
|
||||||
|
case 'RegularProcessExit':
|
||||||
|
if (outcome.exitCode === 0) {
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
if (commandDefinition.isExecutionTerminatedExternally(outcome.exitCode)) {
|
||||||
|
return createFailureOutcome(
|
||||||
|
'ExternalProcessTermination',
|
||||||
|
`Process terminated externally: Exit code ${outcome.exitCode}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return createFailureOutcome(
|
||||||
|
'FileExecutionError',
|
||||||
|
`Unexpected exit code: ${outcome.exitCode}.`,
|
||||||
|
);
|
||||||
|
case 'ExternallyTerminated':
|
||||||
|
return createFailureOutcome(
|
||||||
|
'ExternalProcessTermination',
|
||||||
|
`Process terminated by signal ${outcome.terminationSignal}.`,
|
||||||
|
);
|
||||||
|
case 'ExecutionError':
|
||||||
|
return createFailureOutcome(
|
||||||
|
'FileExecutionError',
|
||||||
|
`Execution error: ${outcome.error.message}.`,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown outcome type: ${outcome}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createFailureOutcome(
|
||||||
|
type: CodeRunErrorType,
|
||||||
|
errorMessage: string,
|
||||||
|
): FailedScriptFileExecution {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type,
|
||||||
|
message: `Error during command execution: ${errorMessage}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { ScriptFileExecutionOutcome } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
|
||||||
|
|
||||||
|
export interface ExecutablePermissionSetter {
|
||||||
|
makeFileExecutable(filePath: string): Promise<ScriptFileExecutionOutcome>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
||||||
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
|
import type { SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||||
|
import type { ScriptFileExecutionOutcome } from '@/infrastructure/CodeRunner/Execution/ScriptFileExecutor';
|
||||||
|
import type { ExecutablePermissionSetter } from './ExecutablePermissionSetter';
|
||||||
|
|
||||||
|
export class FileSystemExecutablePermissionSetter implements ExecutablePermissionSetter {
|
||||||
|
constructor(
|
||||||
|
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
||||||
|
private readonly logger: Logger = ElectronLogger,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async makeFileExecutable(filePath: string): Promise<ScriptFileExecutionOutcome> {
|
||||||
|
/*
|
||||||
|
This is required on macOS and Linux otherwise the terminal emulators will refuse to
|
||||||
|
execute the script. It's not needed on Windows.
|
||||||
|
*/
|
||||||
|
try {
|
||||||
|
this.logger.info(`Setting execution permissions for file at ${filePath}`);
|
||||||
|
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
||||||
|
this.logger.info(`Execution permissions set successfully for ${filePath}`);
|
||||||
|
return { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: 'FilePermissionChangeError',
|
||||||
|
message: `Error setting script file permission: ${error.message}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
|
import type { SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
||||||
|
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
||||||
|
import type { ShellCommandOutcome, ShellCommandRunner } from './ShellCommandRunner';
|
||||||
|
|
||||||
|
export class LoggingNodeShellCommandRunner implements ShellCommandRunner {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: Logger = ElectronLogger,
|
||||||
|
private readonly systemOps: SystemOperations = new NodeElectronSystemOperations(),
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public runShellCommand(command: string): Promise<ShellCommandOutcome> {
|
||||||
|
this.logger.info(`Executing command: ${command}`);
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
this.systemOps.command.exec(command)
|
||||||
|
// https://archive.today/2024.01.19-004011/https://nodejs.org/api/child_process.html#child_process_event_exit
|
||||||
|
.on('exit', (
|
||||||
|
code, // The exit code if the child exited on its own.
|
||||||
|
signal, // The signal by which the child process was terminated.
|
||||||
|
) => {
|
||||||
|
// One of `code` or `signal` will always be non-null.
|
||||||
|
// If the process exited, code is the final exit code of the process, otherwise null.
|
||||||
|
if (code !== null) {
|
||||||
|
this.logger.info(`Command completed with exit code ${code}.`);
|
||||||
|
resolve({ type: 'RegularProcessExit', exitCode: code });
|
||||||
|
return; // Prevent further execution to avoid multiple promise resolutions and logs.
|
||||||
|
}
|
||||||
|
// If the process terminated due to receipt of a signal, signal is the string name of
|
||||||
|
// the signal, otherwise null.
|
||||||
|
resolve({ type: 'ExternallyTerminated', terminationSignal: signal as NodeJS.Signals });
|
||||||
|
this.logger.warn(`Command terminated by signal: ${signal}`);
|
||||||
|
})
|
||||||
|
.on('error', (error) => {
|
||||||
|
// https://archive.ph/20200912193803/https://nodejs.org/api/child_process.html#child_process_event_error
|
||||||
|
// The 'error' event is emitted whenever:
|
||||||
|
// - The process could not be spawned, or
|
||||||
|
// - The process could not be killed, or
|
||||||
|
// - Sending a message to the child process failed.
|
||||||
|
// The 'exit' event may or may not fire after an error has occurred.
|
||||||
|
this.logger.error('Command execution failed:', error);
|
||||||
|
resolve({ type: 'ExecutionError', error });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
export interface ShellCommandRunner {
|
||||||
|
runShellCommand(command: string): Promise<ShellCommandOutcome>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ShellCommandOutcome = ProcessStatus & ({
|
||||||
|
readonly type: 'RegularProcessExit',
|
||||||
|
readonly exitCode: number;
|
||||||
|
} | {
|
||||||
|
readonly type: 'ExternallyTerminated';
|
||||||
|
readonly terminationSignal: NodeJS.Signals;
|
||||||
|
} | {
|
||||||
|
readonly type: 'ExecutionError';
|
||||||
|
readonly error: Error;
|
||||||
|
});
|
||||||
|
|
||||||
|
type ProcessOutcomeType = 'RegularProcessExit' | 'ExternallyTerminated' | 'ExecutionError';
|
||||||
|
|
||||||
|
interface ProcessStatus {
|
||||||
|
readonly type: ProcessOutcomeType;
|
||||||
|
readonly error?: Error;
|
||||||
|
readonly terminationSignal?: NodeJS.Signals;
|
||||||
|
readonly exitCode?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
import type { Logger } from '@/application/Common/Log/Logger';
|
||||||
|
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
||||||
|
import { OsSpecificTerminalLaunchCommandFactory } from './CommandDefinition/Factory/OsSpecificTerminalLaunchCommandFactory';
|
||||||
|
import { ExecutableFileShellCommandDefinitionRunner } from './CommandDefinition/Runner/ExecutableFileShellCommandDefinitionRunner';
|
||||||
|
import type { ScriptFileExecutionOutcome, ScriptFileExecutor } from './ScriptFileExecutor';
|
||||||
|
import type { CommandDefinitionFactory } from './CommandDefinition/Factory/CommandDefinitionFactory';
|
||||||
|
import type { CommandDefinitionRunner } from './CommandDefinition/Runner/CommandDefinitionRunner';
|
||||||
|
import type { CommandDefinition } from './CommandDefinition/CommandDefinition';
|
||||||
|
|
||||||
|
export class VisibleTerminalFileRunner implements ScriptFileExecutor {
|
||||||
|
constructor(
|
||||||
|
private readonly logger: Logger = ElectronLogger,
|
||||||
|
private readonly commandFactory: CommandDefinitionFactory
|
||||||
|
= new OsSpecificTerminalLaunchCommandFactory(),
|
||||||
|
private readonly commandRunner: CommandDefinitionRunner
|
||||||
|
= new ExecutableFileShellCommandDefinitionRunner(),
|
||||||
|
) { }
|
||||||
|
|
||||||
|
public async executeScriptFile(
|
||||||
|
filePath: string,
|
||||||
|
): Promise<ScriptFileExecutionOutcome> {
|
||||||
|
this.logger.info(`Executing script file: ${filePath}.`);
|
||||||
|
const outcome = await this.findAndExecuteCommand(filePath);
|
||||||
|
this.logOutcome(outcome);
|
||||||
|
return outcome;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async findAndExecuteCommand(
|
||||||
|
filePath: string,
|
||||||
|
): Promise<ScriptFileExecutionOutcome> {
|
||||||
|
try {
|
||||||
|
let commandDefinition: CommandDefinition;
|
||||||
|
try {
|
||||||
|
commandDefinition = this.commandFactory.provideCommandDefinition();
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: 'UnsupportedPlatform',
|
||||||
|
message: `Error finding command: ${error.message}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const runOutcome = await this.commandRunner.runCommandDefinition(
|
||||||
|
commandDefinition,
|
||||||
|
filePath,
|
||||||
|
);
|
||||||
|
return runOutcome;
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: {
|
||||||
|
type: 'FileExecutionError',
|
||||||
|
message: `Unexpected error: ${error.message}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private logOutcome(outcome: ScriptFileExecutionOutcome) {
|
||||||
|
if (outcome.success) {
|
||||||
|
this.logger.info('Executed script file in terminal successfully.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.logger.error(
|
||||||
|
'Failed to execute the script file in terminal.',
|
||||||
|
outcome.error.type,
|
||||||
|
outcome.error.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
|
||||||
import type { CommandOps, SystemOperations } from '@/infrastructure/CodeRunner/System/SystemOperations';
|
|
||||||
import type { Logger } from '@/application/Common/Log/Logger';
|
|
||||||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
|
|
||||||
import type { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment';
|
|
||||||
import { NodeElectronSystemOperations } from '@/infrastructure/CodeRunner/System/NodeElectronSystemOperations';
|
|
||||||
import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory';
|
|
||||||
import type { CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
|
|
||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
import type { FailedScriptFileExecution, ScriptFileExecutionOutcome, ScriptFileExecutor } from './ScriptFileExecutor';
|
|
||||||
|
|
||||||
export class VisibleTerminalScriptExecutor implements ScriptFileExecutor {
|
|
||||||
constructor(
|
|
||||||
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
|
|
||||||
private readonly logger: Logger = ElectronLogger,
|
|
||||||
private readonly environment: RuntimeEnvironment = CurrentEnvironment,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public async executeScriptFile(filePath: string): Promise<ScriptFileExecutionOutcome> {
|
|
||||||
const { os } = this.environment;
|
|
||||||
if (os === undefined) {
|
|
||||||
return this.handleError('UnsupportedOperatingSystem', 'Operating system could not be identified from environment.');
|
|
||||||
}
|
|
||||||
const filePermissionsResult = await this.setFileExecutablePermissions(filePath);
|
|
||||||
if (!filePermissionsResult.success) {
|
|
||||||
return filePermissionsResult;
|
|
||||||
}
|
|
||||||
const scriptExecutionResult = await this.runFileWithRunner(filePath, os);
|
|
||||||
if (!scriptExecutionResult.success) {
|
|
||||||
return scriptExecutionResult;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private async setFileExecutablePermissions(
|
|
||||||
filePath: string,
|
|
||||||
): Promise<ScriptFileExecutionOutcome> {
|
|
||||||
/*
|
|
||||||
This is required on macOS and Linux otherwise the terminal emulators will refuse to
|
|
||||||
execute the script. It's not needed on Windows.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
this.logger.info(`Setting execution permissions for file at ${filePath}`);
|
|
||||||
await this.system.fileSystem.setFilePermissions(filePath, '755');
|
|
||||||
this.logger.info(`Execution permissions set successfully for ${filePath}`);
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return this.handleError('FileExecutionError', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async runFileWithRunner(
|
|
||||||
filePath: string,
|
|
||||||
os: OperatingSystem,
|
|
||||||
): Promise<ScriptFileExecutionOutcome> {
|
|
||||||
this.logger.info(`Executing script file: ${filePath} on ${OperatingSystem[os]}.`);
|
|
||||||
const runner = TerminalRunners[os];
|
|
||||||
if (!runner) {
|
|
||||||
return this.handleError('UnsupportedOperatingSystem', `Unsupported operating system: ${OperatingSystem[os]}`);
|
|
||||||
}
|
|
||||||
const context: TerminalExecutionContext = {
|
|
||||||
scriptFilePath: filePath,
|
|
||||||
commandOps: this.system.command,
|
|
||||||
logger: this.logger,
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
await runner(context);
|
|
||||||
this.logger.info('Command script file successfully.');
|
|
||||||
return { success: true };
|
|
||||||
} catch (error) {
|
|
||||||
return this.handleError('FileExecutionError', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleError(
|
|
||||||
type: CodeRunErrorType,
|
|
||||||
error: Error | string,
|
|
||||||
): FailedScriptFileExecution {
|
|
||||||
const errorMessage = 'Error during script file execution';
|
|
||||||
this.logger.error([type, errorMessage, ...(error ? [error] : [])]);
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
error: {
|
|
||||||
type,
|
|
||||||
message: `${errorMessage}: ${isString(error) ? error : errorMessage}`,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TerminalExecutionContext {
|
|
||||||
readonly scriptFilePath: string;
|
|
||||||
readonly commandOps: CommandOps;
|
|
||||||
readonly logger: Logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
type TerminalRunner = (context: TerminalExecutionContext) => Promise<void>;
|
|
||||||
|
|
||||||
export const LinuxTerminalEmulator = 'x-terminal-emulator';
|
|
||||||
|
|
||||||
const TerminalRunners: Partial<Record<OperatingSystem, TerminalRunner>> = {
|
|
||||||
[OperatingSystem.Windows]: async (context) => {
|
|
||||||
const command = [
|
|
||||||
'PowerShell',
|
|
||||||
'Start-Process',
|
|
||||||
'-Verb RunAs', // Run as administrator with GUI sudo prompt
|
|
||||||
`-FilePath ${cmdShellPathArgumentEscape(context.scriptFilePath)}`,
|
|
||||||
].join(' ');
|
|
||||||
/*
|
|
||||||
📝 Options:
|
|
||||||
`child_process.execFile()`
|
|
||||||
"path", `cmd.exe /c "path"`
|
|
||||||
❌ Script execution in the background without a visible terminal.
|
|
||||||
This occurs only when the user runs the application as administrator, as seen
|
|
||||||
in Windows Pro VMs on Azure.
|
|
||||||
`PowerShell Start -Verb RunAs "path"`
|
|
||||||
✅ Visible terminal window
|
|
||||||
✅ GUI sudo prompt (through `RunAs` option)
|
|
||||||
`PowerShell Start "path"`
|
|
||||||
`explorer.exe "path"`
|
|
||||||
`electron.shell.openPath`
|
|
||||||
`start cmd.exe /c "$path"`
|
|
||||||
✅ Visible terminal window
|
|
||||||
✅ GUI sudo prompt (through `RunAs` option)
|
|
||||||
👍 Among all options `start` command is the most explicit one, being the most resilient
|
|
||||||
against the potential changes in Windows or Electron framework (e.g. https://github.com/electron/electron/issues/36765).
|
|
||||||
`%COMSPEC%` environment variable should be checked before defaulting to `cmd.exe.
|
|
||||||
Related docs: https://web.archive.org/web/20240106002357/https://nodejs.org/api/child_process.html#spawning-bat-and-cmd-files-on-windows
|
|
||||||
*/
|
|
||||||
await runCommand(command, context);
|
|
||||||
},
|
|
||||||
[OperatingSystem.Linux]: async (context) => {
|
|
||||||
const command = `${LinuxTerminalEmulator} -e ${posixShellPathArgumentEscape(context.scriptFilePath)}`;
|
|
||||||
/*
|
|
||||||
🤔 Potential improvements:
|
|
||||||
Use user-friendly GUI sudo prompt (not terminal-based).
|
|
||||||
If `pkexec` exists, we could do `x-terminal-emulator -e pkexec 'path'`, which always
|
|
||||||
prompts with user-friendly GUI sudo prompt.
|
|
||||||
📝 Options:
|
|
||||||
`x-terminal-emulator -e 'path'`:
|
|
||||||
✅ Visible terminal window
|
|
||||||
❌ Terminal-based (not GUI) sudo prompt.
|
|
||||||
`x-terminal-emulator -e pkexec 'path'
|
|
||||||
✅ Visible terminal window
|
|
||||||
✅ Always prompts with user-friendly GUI sudo prompt.
|
|
||||||
🤔 Not using `pkexec` as it is not in all Linux distributions. It should have smarter
|
|
||||||
logic to handle if it does not exist.
|
|
||||||
`electron.shell.openPath`:
|
|
||||||
❌ Opens the script in the default text editor, verified on
|
|
||||||
Debian/Ubuntu-based distributions.
|
|
||||||
`child_process.execFile()`:
|
|
||||||
❌ Script execution in the background without a visible terminal.
|
|
||||||
*/
|
|
||||||
await runCommand(command, context);
|
|
||||||
},
|
|
||||||
[OperatingSystem.macOS]: async (context) => {
|
|
||||||
const command = `open -a Terminal.app ${posixShellPathArgumentEscape(context.scriptFilePath)}`;
|
|
||||||
// -a Specifies the application to use for opening the file
|
|
||||||
/* eslint-disable vue/max-len */
|
|
||||||
/*
|
|
||||||
🤔 Potential improvements:
|
|
||||||
Use user-friendly GUI sudo prompt for running the script.
|
|
||||||
📝 Options:
|
|
||||||
`open -a Terminal.app 'path'`
|
|
||||||
✅ Visible terminal window
|
|
||||||
❌ Terminal-based (not GUI) sudo prompt.
|
|
||||||
❌ Terminal app requires many privileges to execute the script, this prompts user
|
|
||||||
to grant privileges to the Terminal app.
|
|
||||||
`osascript -e 'do shell script "'/tmp/test.sh'" with administrator privileges'`
|
|
||||||
✅ Script as root
|
|
||||||
✅ GUI sudo prompt.
|
|
||||||
❌ Script execution in the background without a visible terminal.
|
|
||||||
`osascript -e 'do shell script "open -a 'Terminal.app' '/tmp/test.sh'" with administrator privileges'`
|
|
||||||
❌ Script as user, not root
|
|
||||||
✅ GUI sudo prompt.
|
|
||||||
✅ Visible terminal window
|
|
||||||
`osascript -e 'do shell script "/System/Applications/Utilities/Terminal.app/Contents/MacOS/Terminal '/tmp/test.sh'" with administrator privileges'`
|
|
||||||
✅ Script as root
|
|
||||||
✅ GUI sudo prompt.
|
|
||||||
✅ Visible terminal window
|
|
||||||
Useful resources about `do shell script .. with administrator privileges`:
|
|
||||||
- Change "osascript wants to make changes" prompt: https://web.archive.org/web/20240109191128/https://apple.stackexchange.com/questions/283353/how-to-rename-osascript-in-the-administrator-privileges-dialog
|
|
||||||
- More about `do shell script`: https://web.archive.org/web/20100906222226/http://developer.apple.com/mac/library/technotes/tn2002/tn2065.html
|
|
||||||
*/
|
|
||||||
/* eslint-enable vue/max-len */
|
|
||||||
await runCommand(command, context);
|
|
||||||
},
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
async function runCommand(command: string, context: TerminalExecutionContext): Promise<void> {
|
|
||||||
context.logger.info(`Executing command:\n${command}`);
|
|
||||||
await context.commandOps.exec(command);
|
|
||||||
context.logger.info('Executed command successfully.');
|
|
||||||
}
|
|
||||||
|
|
||||||
function posixShellPathArgumentEscape(pathArgument: string): string {
|
|
||||||
/*
|
|
||||||
- Wraps the path in single quotes, which is a standard practice in POSIX shells
|
|
||||||
(like bash and zsh) found on macOS/Linux to ensure that characters like spaces, '*', and
|
|
||||||
'?' are treated as literals, not as special characters.
|
|
||||||
- Escapes any single quotes within the path itself. This allows paths containing single
|
|
||||||
quotes to be correctly interpreted in POSIX-compliant systems, such as Linux and macOS.
|
|
||||||
*/
|
|
||||||
return `'${pathArgument.replaceAll('\'', '\'\\\'\'')}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function cmdShellPathArgumentEscape(pathArgument: string): string {
|
|
||||||
// - Encloses the path in double quotes, which is necessary for Windows command line (cmd.exe)
|
|
||||||
// to correctly handle paths containing spaces.
|
|
||||||
// - Paths in Windows cannot include double quotes `"` themselves, so these are not escaped.
|
|
||||||
return `"${pathArgument}"`;
|
|
||||||
}
|
|
||||||
@@ -4,15 +4,15 @@ import type {
|
|||||||
CodeRunError, CodeRunOutcome, CodeRunner, FailedCodeRun,
|
CodeRunError, CodeRunOutcome, CodeRunner, FailedCodeRun,
|
||||||
} from '@/application/CodeRunner/CodeRunner';
|
} from '@/application/CodeRunner/CodeRunner';
|
||||||
import { ElectronLogger } from '../Log/ElectronLogger';
|
import { ElectronLogger } from '../Log/ElectronLogger';
|
||||||
import { VisibleTerminalScriptExecutor } from './Execution/VisibleTerminalScriptFileExecutor';
|
|
||||||
import { ScriptFileCreationOrchestrator } from './Creation/ScriptFileCreationOrchestrator';
|
import { ScriptFileCreationOrchestrator } from './Creation/ScriptFileCreationOrchestrator';
|
||||||
|
import { VisibleTerminalFileRunner } from './Execution/VisibleTerminalFileRunner';
|
||||||
import type { ScriptFileExecutor } from './Execution/ScriptFileExecutor';
|
import type { ScriptFileExecutor } from './Execution/ScriptFileExecutor';
|
||||||
import type { ScriptFileCreator } from './Creation/ScriptFileCreator';
|
import type { ScriptFileCreator } from './Creation/ScriptFileCreator';
|
||||||
|
|
||||||
export class ScriptFileCodeRunner implements CodeRunner {
|
export class ScriptFileCodeRunner implements CodeRunner {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly scriptFileExecutor
|
private readonly scriptFileExecutor
|
||||||
: ScriptFileExecutor = new VisibleTerminalScriptExecutor(),
|
: ScriptFileExecutor = new VisibleTerminalFileRunner(),
|
||||||
private readonly scriptFileCreator: ScriptFileCreator = new ScriptFileCreationOrchestrator(),
|
private readonly scriptFileCreator: ScriptFileCreator = new ScriptFileCreationOrchestrator(),
|
||||||
private readonly logger: Logger = ElectronLogger,
|
private readonly logger: Logger = ElectronLogger,
|
||||||
) { }
|
) { }
|
||||||
|
|||||||