Compare commits

...

93 Commits

Author SHA1 Message Date
undergroundwires
50ba00b0af win: fix, constrain and document WNS #227 #314
This change addresses issues #227 and #314 by preventing unintended side
effects on newer Windows versions while still offering WNS control on
supported systems.

Changes:

- Constrain `WpnUserService` disabling to Windows 10 v1909 and earlier.
- Update documentation for WNS and related services.
- Remove redundant warnings (in generated code and script title).
- Improve DisablePerUserService function:
  - Add documentation and generated comments
  - Implement Windows version constraint capability
2024-08-13 11:19:46 +02:00
undergroundwires
29e1069bf2 win, mac: fix minor typos, formatting, dead URLs
- Update dead URLs to archived versions
- Correct Windows version references (22H3 to 23H2)
- Correct reference order
- Fix incorrect usage of double quotes
2024-08-12 09:28:55 +02:00
undergroundwires
c7e57b8913 win: improve disabling NCSI #189, #216, #279
- Group NCSI disabling under single category for better organization.
- Remove NCSI from 'Strict' recommendations due to side effects
  (addressing #189, #216).
- Improve documentation with cautions about breaking internet status and
  captive portals (addressing #189, #216).
- Add removal of new `NcsiUwpApp` system app #279.
- Add more ways to disable the feature.
- Add ability to constrain Windows version in `DisableService`.
2024-08-10 12:16:33 +02:00
undergroundwires
4cea6b26ec win: unify registry data setting, fix #380
This commit unifies and centralizes registry data operations. This
improves reliability and robustness by avoiding bugs caused by incorrect
syntax when making modifications, as operations are now centralized.
This change resolves issue #380.

It also enhances reversibility by adding all missing revert codes and
correcting existing revert codes, tested against default values on fresh
OS and software installations.

Key changes:

- Add ability to revert to OS default data in `SetRegistryValue`
- Refactor manual registry operations to use shared functions
- Improve documentation for some affected scripts
- Fix incorrect revert codes (adding value instead of deleting) for some scripts
- Add missing revert logic for affected scripts

Other supporting changes:

- Fix revert code generation in `SetRegistryValueAsTrustedInstaller` to
  avoid generating empty revert code when no actions are needed.
- Add ability to delete keys on revert in `CreateRegistryKey`.
- Change `HKCR` key modifications to `HKLM|HKCU\Software\Classes` keys
  to always generate correct revert logic.
2024-08-09 17:13:00 +02:00
undergroundwires
c2f4b68786 win: improve Microsoft Edge associations removal
This commit refactors the removal of Edge associations to use shared
registry functions, streamlining all registry operations. It also
enhances the logic for removing associations with several improvements.

Key changes:

- Create separate shared functions for each association modification.
- Prefer modifying real keys over `HKCR` keys for reliability
- Add more documentation for affected scripts and new shared functions
- Replace loops with explicit calls for clarity and maintainability
- Extend handling of registry keys to both HKCU/HKLM hives
- Add missing association removals
- Add OS checks to apply only on correct Windows versions
- Split scripts for more granularity and maintainability
- Handle permission errors for user choice keys on recent Windows

Other supporting changes:

- Add `REG_DWORD` revert support for `DeleteRegistryKey`
- Add `grantPermissions` support for `DeleteRegistryValue`
- Add delete data only if undesired for `DeleteRegistryValue`
- Shorten generated code in `DeleteRegistryValue` to prevent reaching
  `cmd.exe` limits
- Add more Windows versions for script constraints
2024-08-08 17:57:19 +02:00
undergroundwires
e8add5ec08 win: improve folder hiding in "This PC" #16
This commit improves how folders are hidden under "This PC" on Windows.
It introduces shared functions to improve maintainability. This increases
the robustness and simplifies future updates and maintenance.

Key changes:

- Fix revert codes to match the default operating system state.
- Implement shared functions for higher maintainability.
- Add more documentation.
- Introduce more methods to hide folders, adding the suggestion from
  #16.

Other supporting changes:

- Add ability to revert `DeleteRegistryKey`.
- Add ability to contrain Windows version in `DeleteRegistryKey`.
- Add generated comment by default in `DeleteRegistryKey`.
2024-08-07 11:51:28 +02:00
undergroundwires
55c23e9d4c win: improve registry value deletion #381
This commit enhances the deletion of registry values with improved
robustness and better error handling. One-line `reg.exe` calls where
errors were suppressed are replaced with PowerShell commands that
provide proper error handling. This fixes #381 where wrong `reg delete`
syntax was used.

Key changes:

- Introduce `DeleteRegistryValue` and change registry value deletion
  logic to use it.
- Fix Windows version comparison to ignore patch numbers.
  This ensures versions like `10.0.19045.0` are treated the same as
  `10.0.19045`, resolving issues where scripts were incorrectly skipped
  due to patch number differences in Windows versions.

Other supporting changes:

- Add missing revert codes.
- Include more comments in the generated code.
- Use `-LiteralPath` in all registry deletion commands to prevent
  unintended wildcard expansion when '*' is used in registry paths.
- Remove unused `revertCodeComment` parameter from `DeleteRegistryKey`.

Changed scripts:

- 'Remove "Scan with Microsoft Defender" from context menu':
  - Use `DeleteRegistryKey` in script.
  - Remove problematic `HKCR\*\shellex\ContextMenuHandlers` key deletion.
    This caused errors on both Windows 10 (22H2) and Windows 11 (23H2).
    The wildcard usage made this operation potentially risky, so it's
    replaced with more specific registry cleanup.
  - Remove modifications to `HKCR` values. `HKCR` is a virtual hive,
    and changes to `HKLM` are automatically reflected in `HKCR`.
- Update 'Disable automatic OneDrive installation' to target only
  Windows 1909, improve documentation, and recommend in 'Standard'.
- Simplify 'Disable Diagnostics Hub log collection' by removing VS
  version check, enhance documentation, recommend in 'Standard'.
2024-08-06 07:15:26 +02:00
undergroundwires
d77c3cbbe2 Fix PowerShell code block inlining in compiler
This commit enhances the compiler's ability to inline PowerShell code
blocks. Previously, the compiler attempted to inline all lines ending
with brackets (`}` and `{`) using semicolons, which leads to syntax
errors. This improvement allows for more flexible PowerShell code
writing with reliable outcomes.

Key Changes:

- Update InlinePowerShell pipe to handle code blocks specifically
- Extend unit tests for the InlinePowerShell pipe

Other supporting changes:

- Refactor InlinePowerShell tests for improved scalability
- Enhance pipe unit test running with regex support
- Expand test coverage for various PowerShell syntax used in
  privacy.sexy
- Update related interfaces to align with new code conventions, dropping
  `I` prefix
- Optimize line merging to skip lines already ending with semicolons
- Increase timeout in E2E tests to accommodate for slower application
  load caused by more processing introduced in this commit.
2024-08-05 19:44:30 +02:00
undergroundwires
f89c2322b0 win: fix, improve and unify Windows version logic
This commit centralizes Windows version constraints through a new
function for improved clarity, maintainability and reusability.

Changes:

- Add `RunPowerShellWithWindowsVersionConstraints` function
- Support specifying minimum and maximum Windows versions
- Introduce user-friendly tags like `Windows11-FirstRelease`
- Fix version logic by correcting incorrect block syntax in various
  functions.
2024-08-04 15:29:29 +02:00
undergroundwires
ded55a66d6 Refactor executable IDs to use strings #262
This commit unifies executable ID structure across categories and
scripts, paving the way for more complex ID solutions for #262.
It also refactors related code to adapt to the changes.

Key changes:

- Change numeric IDs to string IDs for categories
- Use named types for string IDs to improve code clarity
- Add unit tests to verify ID uniqueness

Other supporting changes:

- Separate concerns in entities for data access and executables by using
  separate abstractions (`Identifiable` and `RepositoryEntity`)
- Simplify usage and construction of entities.
- Remove `BaseEntity` for simplicity.
- Move creation of categories/scripts to domain layer
- Refactor CategoryCollection for better validation logic isolation
- Rename some categories to keep the names (used as pseudo-IDs) unique
  on Windows.
2024-08-03 16:54:14 +02:00
undergroundwires
6fbc81675f Relax linting to allow null recommendation
This commit updates the YAML schema to permit explicitly setting
`recommend: null` or `recommend: ~` in scripts within collection files.

Previously, the schema only allowed string values for the recommendation
field, restricting it to 'standard' or 'strict'. By introducing the
`RecommendationLevel` definition, the schema now supports both string
and null values, providing more flexibility in specifying
recommendations in collection YAML files.
2024-08-02 16:44:15 +02:00
undergroundwires
48d97afdf6 win: improve registry/recent cleaning
This commit introduces a new shared function to centralize all usages of
`reg delete .. /va`. The new function generates comments in code and can
recurse through subkeys. This enhances maintainability and reliability
by avoiding potential misuse or syntax errors.

Key changes:

- Add `ClearRegistryValues` function
- Update scripts to use the new function
- Add ability to recurse subkeys for registry value deletion, addressing
  issues where desired data was not deleted.

Other supporting changes:

- Improve documentation of the changed scripts.
- Add missing registry paths in scripts.
- Change value removal to value/subkey removal for correct behavior.
- Remove removal of undocumented keys.
- Rename related scripts for clarity.
- Adjust script recommendations.
2024-08-01 23:02:01 +02:00
undergroundwires
109fc01c9a win: fix and document VStudio license removal
Before this commit, license deletion used `reg delete .. /va /f`, which
deletes all valued under a key. However, the license data did not exist
under the specified subkeys, making the logic ineffective. This commit
changes it to delete the license registry key completely to correctly
remove the license data.

Changes:

- Change `reg delete /va` to delete the correct registry data.
- Create shared function for deleting license data for better
  maintainability.
- Add comment in generated script code for license removal.
- Add documentation for the scripts.
- Include a missing script to clear Visual Studio 2013 telemetry.
- Remove redundant lines of code in `CreateRegistryKey` and
  `DeleteRegistryKey` that initialize an unused variable.
2024-07-31 13:35:39 +02:00
undergroundwires
b185255a0a win: centralize, improve Defender data collection
This commit reorganizes scripts related to disabling Defender's data
collection and telemetry into a dedicated category. This improves
usability for users focused on enhancing privacy without needing to
understand technical details of each option.

Changes:

- Create "Disable Defender data collection" category
- Move related scripts under new category
- Improve script documentation and naming
- Add alternate configurations to some scripts
- Fix extended cloud check feature being enabled instead of disabled
- Update script recommendations to 'Strict'
2024-07-28 23:50:38 +02:00
undergroundwires
c2d3cddc47 win: improve, fix, restructure CEIP disabling
- Restructure and expand rename CEIP-related scripts for clarity and
  granularity.
- Add missing tasks and registry keys for comprehensive CEIP disabling.
- Improve documentation with detailed explanations and references.
- Rename scripts for better user understanding and consistency
- Fix incorrect revert behavior in some scripts
2024-07-26 15:45:33 +02:00
undergroundwires
8526d2510b win: unify registry setting as TrustedInstaller
- Introduce SetRegistryValueAsTrustedInstaller function to unify setting
  registry values as TrustedInstaller.
- Introduce RunPowerShellWithMinimumWindowsVersion function to unify
  Windows version specific registry modifications.
- Add more documentation for scripts using TrustedInstaller.
- Correct revert code for affected scripts to match default OS behavior
  (setting registry value back) instead of just deleting keys.
2024-07-25 14:23:31 +02:00
undergroundwires
11e566d0e5 win: improve disabling SmartScreen #385
- Add comprehensive documentation with security cautions
- Expand SmartScreen disabling for Internet Explorer
- Fix registry data for Internet Explorer SmartScreen disabling
- Add disabling of `smartscreen.exe` process, resolving #385
- Implement additional SmartScreen disabling methods
- Correct registry key for Store apps
- Simplify script names for clarity
2024-07-24 16:23:28 +02:00
undergroundwires
ae0165f1fe Ensure tests do not log warning or errors
This commit increases strictnes of tests by failing on tests (even
though they pass) if `console.warn` or `console.error` is used. This is
used to fix warning outputs from Vue, cleaning up test output and
preventing potential issues with tests.

This commit fixes all of the failing tests, including refactoring in
code to make them more testable through injecting Vue lifecycle
hook function stubs. This removes `shallowMount`ing done on places,
improving the speed of executing unit tests. It also reduces complexity
and increases maintainability by removing `@vue/test-utils` dependency
for these tests.

Changes:

- Register global hook for all tests to fail if console.error or
  console.warn is being used.
- Fix all issues with failing tests.
- Create test helper function for running code in a wrapper component to
  run code in reliable/unified way to surpress Vue warnings about code
  not running inside `setup`.
2024-07-23 16:08:04 +02:00
undergroundwires
a6505587bf Fix intermittent ModalDialog unit test failures
Refactor `ModalDialog` unit tests to use `shallowMount` consistently.
Previously, tests sometimes failed due to the `UseSvgLoader` hook
attempting icon loads during component teardown, which occasionally led
led to errors when the `window` object became unavailable. By
switching to `shallowMount`, tests no longer deeply render child
components, mitigating the risk of such errors and aligning with
unit testing best practices.

Additionally, this commit sets a default value for the `modelValue`
prop in test setups to address Vue warnings about missing required
props, further stabilizing the test environment.
2024-07-22 15:10:12 +02:00
undergroundwires
b16e13678c Improve compiler error display for latest Chromium
This commit addresses the issue of Chromium v126 and later not displaying
error messages correctly when the error object's `message` property uses
a getter. It refactors the code to utilize an immutable Error object with
recursive context, improves error message formatting and leverages the
`cause` property.

Changes:

- Refactor error wrapping internals to use an immutable error object,
  eliminating `message` getters.
- Utilize the `cause` property in contextual errors for enhanced error
  display in the console.
- Enhance message formatting with better indentation and listing.
- Improve clarity by renaming values thrown during validations.
2024-07-21 10:18:27 +02:00
undergroundwires
abe03cef3f Refactor styles to match new CSS nesting behavior
This commit refactors SCSS to resolve deprecation warnings related to
mixed declaration after nested rules.

Sass is changing how it processes declarations that appear after nested
rules to align with CSS standards. Previously, Sass would hoist
declarations to avoid duplicating selectors, However, this behavior will
soon change to make declarations apply in the order they appear, as per
CSS standards.
2024-07-20 11:56:31 +02:00
undergroundwires
dd7239b8c1 Bump dependencies to latest 2024-07-19 10:29:38 +02:00
undergroundwires
851917e049 Refactor text utilities and expand their usage
This commit refactors existing text utility functions into the
application layer for broad reuse and integrates them across
the codebase. Initially, these utilities were confined to test
code, which limited their application.

Changes:

- Move text utilities to the application layer.
- Centralize text utilities into dedicated files for better
  maintainability.
- Improve robustness of utility functions with added type checks.
- Replace duplicated logic with centralized utility functions
  throughout the codebase.
- Expand unit tests to cover refactored code parts.
2024-07-18 20:49:21 +02:00
undergroundwires
8d7a7eb434 win: support Microsoft Store Firefox installations
This commit updates the Windows scripts to handle Firefox installations
acquired through the Microsoft Store. It adds support by modifying
script functions to clear and delete profile directories specific to
this version of Firefox.
2024-07-10 08:37:59 +02:00
undergroundwires
0239b52385 win: refactor version-specific actions
Optimize PowerShell script invocation to differentiate actions based on
Windows version. This revision introduces a more efficient way to handle
version-specific scripting within Windows collection by abstraction
complexity into dedicated shared functions.
2024-07-09 19:09:06 +02:00
undergroundwires
19ea8dbc5b Fix close button overlap by scrollbar
This commit positions the close button on modaals to remain visible on
small screens.

This fix addresses an issue where the dialog's close button was being
obscured by the vertical scrollbar on narrow screens. This problem was
particularly evident in Chromium browsers, which display a larger
scrollbar.

This change includes a new mixin to document the purpose. The mixin sets
the position of close button to `absolute` and aligns it to the top
right. This way, the button stays visible and accessible regaRdless of
The scrollbar's presence, thereby improving the user inTerfaCE usability
on deviceS with smaller screens.
2024-07-08 12:16:51 +02:00
undergroundwires
70959ccada Fix documentation button spacing on small screens
The previous layout lacked a specified gap between the node header and
the documentation toggle button in the tree view. This resulted in a
crowded appearance, making the interface look cluttered and reducing
readability, especially on smaller screens.

This commit introduces a relative gap, adjusting the spacing based on
the text size. This change enhances the visual separation and improves
user interaction by ensuring the documentation button and text do not
overlap, regardless of screen size.
2024-07-07 11:59:27 +02:00
undergroundwires
5d365f65fa win: improve service disabling as TrustedInstaller
This commit changes the mechanism to disable services using
TrustedInstaller privileges, improving consistency and flexibility.

Key changes:

- Introduce `DisableServiceInRegistryAsTrustedInstaller` as a shared
  function to standardize the disabling process. This function aligns
  with existing ones to facilitate easier testing and method switching.
- Update the revert logic to avoid unnecessary service restarts when
  they are manually started.
- Enhance readability with added comments in generated code sections.
- Improve documentation for `DisableService` and
  `DisableServiceInRegistry` to reflect new functionalities.
- Support multiline code in `RunInlineCodeAsTrustedInstaller` for
  complex scenarios.

Other supporting changes:

- Remove redundant TrustedInstaller privileges in the `Sense` service
  disabling.
- Document default service statuses to inform about service behaviors
  across different Windows versions.
2024-07-06 12:28:42 +02:00
undergroundwires-bot
cca397c8c7 ⬆️ bump everywhere to 0.13.5 2024-06-27 08:13:03 +00:00
undergroundwires
1430d5215a win: add more Edge scripts including AI & ads
This commit improves the scripts configuring Edge. It improves their
categorization, naming and adds scripts to disable Bing ads and Search
bar along with others to disable ads/data collection.

Changes:

- Add new scripts to configure Edge, such as blocking ads and AI
  features that collects data.
- Improve categorization and documentation consistency.
2024-06-26 16:48:49 +02:00
undergroundwires
c09c5ffa47 win, linux, mac: fix typos #373
This commit fixes typos, corrects markdown syntax, and archived URLs.

Co-authored-by: RainRat <rainrat78@yahoo.ca>
2024-06-26 08:13:13 +02:00
undergroundwires
ed7e69c07e win: add disabling Edge/WebView2 auto-updates #309
This commit adds scripts to block automatic updates for Microsoft Edge
and WebView2 on Windows, aimed at enhancing user privacy as per the
issue #309.

Changes:

- Create a new category for scripts targeting Edge and WebView2 updates.
- Add scripts for disabling automatic update services, scheduled tasks,
  and executable blocking, along with registry configurations.

Other supporting changes:

- Remove comments in code that indicates reusing of small text parts.
  This approach does not encourage creating unique content.
2024-06-25 12:23:55 +02:00
undergroundwires
f286f92b1f win: categorize, rename, doc Chrome & Edge scripts
This commit improves the script organization, documentation and code for
Edge and Chrome browser by simplifying naming, categorizing
configurations, and unifying documentation and generated code.

Changes:

- Rename "Edge (Chromium)" to "Edge" for clarity, with "Edge (Legacy)"
  detailed explicitly in the script titles.
- Flatten Edge settings under a unified "Configure" category.
- Enhance script documentation to improve clarity.
- Move "Your browser is managed" warning from script titles to script
  documentation.
- Introduce shared functions for configuring Edge and Chrome, leading to
  better consistency in generated code.
- Update scripts to include restart suggestions in generated code.
- Improve documentation of affected scripts.
- Split some scripts for increased granularity, easier maintenance and
  focused documentation.
- Fix some Windows UI scripts being incorrectly categorized as Edge
  configurations.
2024-06-24 20:27:52 +02:00
undergroundwires
e7031a3ae4 win: fix latest Edge removal on Windows 10 #309
This commit introduces a placeholder ifle creation step necessary for
the uninstallation process of Microsoft Edge on Windows 10, as discussed
in #309. The file simulates the presence of Microsoft Edge (Legacy),
which newer uninstallers check for before proceeding with the
uninstallation.

This change resolves the observed issue where the uninstaller fails to
recognize the absence of Legacy Edge, hindering the uninstallation
process.

Changes:

- Add placeholder file creation/removal for legacy Edge.
- Update and improve the documentation.
2024-06-23 12:51:15 +02:00
undergroundwires
2f828735a8 win: fix errors due to missing Edge uninstaller
If Edge is uninstalled using an existing installer, it may delete other
installers. When the script attempts to use these deleted installers,
it results in an error: `The system cannot find the file specified.`.

This commit addresses the issue by checking for the existence of the
uninstaller during the iteration and handling cases where it is missing.
2024-06-22 14:01:06 +02:00
undergroundwires
78c62cfc95 Trim compiler error output for better readability
Previously, compiler outputted whole executable in error context. This
caused long and hard to read error messages, especially when the
executable is a long category with many children. This commit improves
readability by trimming the error output.

Changes:

- Trim the error output (max characters: 1000).
- Improve indenting and newlines.
2024-06-21 12:46:30 +02:00
undergroundwires
ed93614ca3 Bump Electron to latest
- Bump Electron to latest.
- Adjust types to new Electron types.
2024-06-20 10:49:24 +02:00
undergroundwires
fac26a6ca0 Add type validation for parameters and fix types
This commit introduces type validation for parameter values within the
parser/compiler, aligning with the YAML schema. It aims to eliminate
dependencies on side effects in the collection files.

This update changes the treatment of data types in the Windows
collection, moving away from unintended type casting by the compiler.
Previously, numeric and boolean values were used even though only
string types were supported. This behavior was unstable and untested,
and has now been adjusted to use strings exclusively.

Changes ensure that parameter values are correctly validated
as strings, enhancing stability and maintainability.
2024-06-19 17:01:27 +02:00
undergroundwires
48761f62a2 win: fix incomplete VSCEIP, location scripts
This commit improves the validation logic in parser, corrects Windows
collection files to adhere to expected structure. This validation helps
catch errors that previously led to incomplete generated code in scripts
for disabling VSCEIP and location settings.

Changes:

- Add type validation for function call structures in the
  parser/compiler. This helps prevent runtime errors by ensuring that
  only correctly structured data is processed.
- Fix scripts in the Windows collection that previoulsy had incomplete
  `code` or `revertCode` values. These corrections ensure that the
  scripts function as intended.
- Refactor related logic within the compiler/parser to improve
  testability and maintainability.
2024-06-18 17:59:32 +02:00
undergroundwires
dc03bff324 Add schema validation for collection files #369
This commit improves collection file editing and error detection
directly in the IDE. It adds YAML schema, IDE configuration and
automatic tests to validate it.

- Introduce a YAML schema for collection file.
- Use `yaml-language-server` for enhanced YAML support in VSCode.
- Add telemetry disabling in `configure_vscode.py` to respect user
  privacy.
- Add automated checks to validate YAML file structure against the
  schema.
- Remove unused properties and do not allow them in compiler.
2024-06-17 14:01:07 +02:00
undergroundwires
e9a52859f6 mac: document, improve, encourage clearing logs
Previously, scripts under the 'Clear operating system logs' category for
macOS were misaligned due to a lack of individual script
recommendations, as the category itself wrongly used the `recommend:
strict` property. This misconfiguration caused none of these scripts to
appear recommended.

This commit assigns accurate `recommend:` values to each script
within the category.

Key changes:

- Introduce individual recommendations for each script.
- Document scripts to justify recommendations.
- Standardize deletion operations through shared functions.
- Improve script and category naming for clarity.
- Simplify code by unifying redundant path references.
- Add comments in generated user script code.
- Fix specific issue where clearing daily os logs inadvertently affected
  configuration files.
2024-06-16 11:27:48 +02:00
undergroundwires
1a10cf2e5f win: fix text and handwriting script omission #369
This commit corrects a syntax error that prevented the 'Disable text
and handwriting data collection' script from being included since
version 0.13.4. The error was identified in a previous syntax validation
update (commit 6ecfa9b954).

Changes:

- Add a missing dash before 'Disable location access' category. This
  fixes the script omission as reported in the issue #369.
- Remove the dash from `revertCode` of `HarvestContacts` registry
  modification code, fixing the revert code for 'Disable text and
  handwriting data collection' script.

This fix addresses the build errors introduced by stricter syntax checks
and ensures that the script is now properly recognized and executed.
This fix is part of ongoing efforts to improve data handling robustness
and management in script processing.
2024-06-15 08:32:58 +02:00
undergroundwires
1c2d82dc9b win: fix missing app access recommendations #369
This script fixes the recommendation property syntax in Windows script
collection. This syntax error prevented the application from
recommending these scripts, even though they were intended to be
recommended.

Affected scripts:

- Disable app access to physical movement
- Disable app access to eye tracking
- Disable app access to human presence
- Disable app access to screen capture

Previously, these scripts used the unsupported 'recommended: standard'
property, which was identified as incorrect after implementing stricter
property validation.
Related commit: 6ecfa9b954
Related issue: #369

This change update these properties to the correct 'recommend:
standard', resolving issues where scripts were not being recommended as
expected.
2024-06-14 12:36:15 +02:00
undergroundwires
6ecfa9b954 Add object property validation in parser #369
This commit introduces stricter type validation across the application
to reject objects with unexpected properties, enhancing the robustness
and predictability of data handling.

Changes include:

- Implement a common utility to validate object types.
- Refactor across various parsers and data handlers to utilize the new
  validations.
- Update error messages for better clarity and troubleshooting.
2024-06-13 22:26:57 +02:00
undergroundwires
c138f74460 Refactor to unify scripts/categories as Executable
This commit consolidates scripts and categories under a unified
'Executable' concept. This simplifies the architecture and improves code
readability.

- Introduce subfolders within `src/domain` to segregate domain elements.
- Update class and interface names by removing the 'I' prefix in
  alignment with new coding standards.
- Replace 'Node' with 'Executable' to clarify usage; reserve 'Node'
  exclusively for the UI's tree component.
2024-06-12 12:36:40 +02:00
undergroundwires
8becc7dbc4 win: fix revert scripts for removing shortcuts
Revert scripts for removing shortcuts previously used hardcoded paths.
These paths are now replaced with system environment variables to allow
for broader configuration compatibility.
2024-06-11 12:06:46 +02:00
undergroundwires
b29cd7b5f7 mac: discourage and document captive portal script
This commit adjusts the recommendation level for disabling captive
portal detection from 'Standard' to 'Strict'. This aligns macOS settings
with equivalent recommendations for Linux and Windows.

It improves documentation to provide additional context on implications,
facilitating a better understanding of the change.
2024-06-10 13:22:32 +02:00
undergroundwires
f21ef9250a win: improve executable blocking, Chrome reporting
This commit improves blocking of execution of executables, providing a
more reliable way to stop execution of unwanted executables.

Introduce a new function to block shell execution of an executable. This
logic is extracted from disabling Chrome Software Reporter tool with
improved logic which does no longer or reset if there is other rules.
This resolves potential issues if there was a blocking rule using same
number which privacy.sexy before overwrote or restored on revert.

Other scripts which terminated executables on launch does now block
their shell execution too for more reliability. A common function is
introduced which streamlines blocking execution of an executable all
known ways which is now reused by these scripts.

This commit additionally improves the Google Software Reporter disabling
script. It removes the code that adds denies permissions on its
installation directory as the new way of preventing executable from
running should be enough. It also adds missing documentation to the
related scripts.
2024-06-09 13:59:06 +02:00
undergroundwires
fa2a92bf89 Add image to README.md to thank supporters 2024-06-08 11:49:04 +02:00
undergroundwires
8341411be4 win: document and improve Firefox telemetry #259
This commit improves the existing Firefox privacy scripts and improves
the categorization and documentation to be simpler and more clear.

Changes:

- Rename and reorganize scripts for disabling browser telemetry and
  default browser agent reporting to simplify the structure.
- Improve documentation across scripts to provide clearer guidance on
  how the changes improve user privacy.
- Fix revert scripts by removing unnecessary registry key configuration.
2024-05-29 08:17:50 +02:00
undergroundwires
22d6c7991e ci/cd: centralize and bump artifact uploads
- Upgrade `actions/upload-artifact` to `v4` to address deprecation
  warnings related to Node.js 16, improving compatibility with GitHub
  runners. This resolves the following warning from the runners:
  > Node.js 16 actions are deprecated. Please update the following actions
  > to use Node.js 20: actions/upload-artifact@v3.
- Centralize the use of the `upload-artifact` action through a new
  custom action, improving maintainability and consistency across
  workflows.
2024-05-28 12:53:45 +02:00
undergroundwires-bot
795b7f0321 ⬆️ bump everywhere to 0.13.4 2024-05-27 13:45:04 +00:00
undergroundwires
9e34e64449 win, mac, linux: fix typos and dead URLs #367
- Fix multiple spelling errors in various scripts.
- Fix dead URLs with archived versions.
- Fix incorrect registry keys previously introduced in commit
  cec0b4b4f6.

Co-authored-by: RainRat <rainrat78@yahoo.ca>
2024-05-27 14:54:17 +02:00
undergroundwires
ce4cfdd169 win: add script to disable Recall feature 2024-05-27 10:44:11 +02:00
undergroundwires
12b1f183f7 win: document disabling firewall #115 #152 #364
This commit updates documentation to clarify the impacts of disabling
firewall services, specifically how they affect Windows Sandbox, Docker
and WSL.

This update responds to user feedback from issues #115, #152, #364. The
documentation now guides users more clearly on the consequences of their
actions, potentially preventing unintended service disruptions.

Changes include:

- Expand the caution notes to explicitly mention the impact on
  virtualization and isolation features like Windows Sandbox, Docker and
  WSL.
- Expand script titles to briefly mention affects on these features.
- Expand documentation to suggest system restart.
- Add an informative message to restart the computer in terminal outputs
  after service changes to ensure the settings are applied.
2024-05-26 13:42:25 +02:00
undergroundwires
4212c7b9e0 Improve context for errors thrown by compiler
This commit introduces a custom error object to provide additional
context for errors throwing during parsing and compiling operations,
improving troubleshooting.

By integrating error context handling, the error messages become more
informative and user-friendly, providing sequence of trace with context
to aid in troubleshooting.

Changes include:

- Introduce custom error object that extends errors with contextual
  information. This replaces previous usages of `AggregateError` which
  is not displayed well by browsers when logged.
- Improve parsing functions to encapsulate error context with more
  details.
- Increase unit test coverage and refactor the related code to be more
  testable.
2024-05-25 13:55:30 +02:00
undergroundwires
7794846185 win: discourage blocking app access #121 #339 #350
This commit adjusts the recommendation level for scripts that disable
UWP app access to accommodate user issues #121, #339, #350. It also
extends their documentation to reflect the new changes and with
cautions.

Changes:

- Add caution text for all scripts about potential impacts.
- Move disabling app access to notifications from 'Standard' to
  'Strict'. This addresses #121 and #339, where users report lack of
  notification as unintended side-effects.
- Move disabling app access to phone calls from 'Standard' to 'Strict'.
  This addresses #350 where its effect on the Phone Link app was
  reported as an unintended side-effect.
2024-05-24 10:45:23 +02:00
undergroundwires
150e067039 win: improve printing removal /w Print Queue #279
- Consolidate removal of printing UIs under the same category.
- Improve documentation for printing app removal scripts.
- Add removal of previously unlisted 'Print Queue' app, #279.
- Combine removal of `Microsoft.Print3D` and `Windows.Print3D`
  into a single script.
- Highlight the importance of removing 'Print 3D' app due to
  security risks and recommend it on 'Standard'.
2024-05-23 09:27:14 +02:00
undergroundwires
f347fde0c8 win: document and discourage RSA key script #363
This commit improves the documentation of RSA key handling script and
changes its recommendation level to address potential issues with
Hyper-V (as reported in #363).

Changes:

- Add documentation to describe potential disruptions caused by stronger
  RSA key requirements.
- Move RSA key script from 'Standard' to 'Strict' due to its impact on
  Hyper-V VMs.
- Use bullet points for easier expansion in cautions of secret key
  hardening scripts.
2024-05-22 08:10:37 +02:00
undergroundwires
ff3d5c4841 win: improve app access disabling and docs #138
This commit improves disabling app access by correcting minor issues,
adding missing access control configurations and adding more
documentation to increase maintainability and user understanding of
privacy settings, resolving #138.

- Introduce shared functions to streamline modifications of app access,
  improving maintainability and simplifying the codebase.
- Move disabling app access to first position in the category.
- Improve code comments for better clarity on generated outputs.
- Resolve error display in revert codes due to incorrect use of `reg
  delete` commands.
- Fix disabling app access to trusted devices disables access to account
  information, name and picture on older versions of Windows.
- Add missing privacy settings and configurations.
- Add more documentation to scripts
- Rename script names for consistency and clarity.
- Move disabling access to SMS/MMS to phone access disablement category.
- Set empty `REG_MULTI_SZ` values to null for GPO access settings to
  maintain registry integrity.
2024-05-21 13:02:48 +02:00
undergroundwires
292362135d Centralize and optimize ResizeObserver usage
This commit addresses failures in end-to-end tests that occurred due to
`ResizeObserver` loop limit exceptions.

These errors were triggered by Vue dependency upgrades in the commit
aae5434451.
The errors had the following message:
> `ResizeObserver loop completed with undelivered notifications`

This error happens when there are too many observations and the observer
is not able to deliver all observations within a single animation frame.
See: WICG/resize-observer#38

his commit resolves the issue by controlling how many observations are
delivered per animation frame and limiting it to only one.

It improves performance by reducing layout trashing, improving frame
rates, and managing resources more effectively.

Changes:

- Introduce an animation frame control to manage observations more
  efficiently.
- Centralized `ResizeObserver` management within the `UseResizeObserver`
  hook to improve consistency and reuse across the application.
2024-05-20 10:36:49 +02:00
undergroundwires
aae5434451 Bump Vue to latest and fix universal selector CSS
This commit updates the Vue package from v3.4.21 to v3.4.27.

This version change addressed styling issues introduced by changes in
CSS universal selector handling in Vue 3.4.22.
The change that has caused this:
- vuejs/core#10551
- vuejs/core#10548
- vuejs/core@54a6afa75a

This commit fixes two main issues that this has led to:

1. Universal CSS selector causing 'Revert' buttons to stretch and
   truncate incorrectly.
   This is fixed by modifying selectors to apply styles more
   specifically, maintaining correct display of toggle buttons.
2. Universal `*` selector that's used to understand parent HTML
   structure causing information tooltip icons to be misaligned.
   This is fixed by replacing `*` with a new `InfoTooltipWrapper`
   component, which manages layout concerns more explicitly and
   maintainably.
2024-05-19 15:02:38 +02:00
undergroundwires
2390530d92 ci/cd: fix quality checks not running on all OSes
Previously, quality checks were mistakenly configured to run only on
Ubuntu.

This commit modifies the CI/CD workflow to use the matrix strategy,
allowing the quality checks to be executed on macOS, Ubuntu and Windows.

Additionally, this update resolves the `MD034/no-bare-urls Bare URL
used` linting error that surfaced when testing on Windows.
2024-05-18 13:14:05 +02:00
undergroundwires
9ab3ff75b0 Migrate to GitHub issue forms
This commit transitions from HTML-based issue templates to GitHub issue
forms, enhancing user experience by preventing accidental submissions
with comment-like metadata. This change makes submitting issues more
intuitive and reduces the chances of user errors (such as #355).

Key change include:

- Use a friendlier tone in the templates.
- Detail examples and descriptions to guide users more effectively.
- Rename templates for improved clarity and easy navigation.
- Add "a note from the maintainer".
- Include a direct link for donations to support the project.
2024-05-17 17:52:14 +02:00
undergroundwires
d25c4e8c81 Add support for macOS universal binary #348, #362
This commit introduces a universal binary format in the distributed MDG
files for macOS, improving support for both Apple Silicon (ARM) and
Intel (x64) architectures.

It uses `electron-builder` to package both architectures into a single
executable, ensuring the application can natively on any macOS hardware
without depending on the GitHub runners' architecture. It fixes the
issue related to prior releases that supported only the architecture of
the build environment itself, which is subject to change.

Changes:

- Update DMG distribution to include both ARM64 and x64 architectures.
- Enhance system requirements documentation to reflect support for both
  architectures.
- Modify CI/CD workflows to check desktop runtime errors for both ARM64
  and x64 versions on macOS.

Resolves:

- Issue #348: Initial request for Apple Silicon support.
- Issue #362: Correction of distribution limited to ARM64 in release
  0.13.3.

`electron-builder` support:
- electron-userland/electron-builder#5475
- electron-userland/electron-builder#5689
- electron-userland/electron-builder#5426
2024-05-16 10:53:55 +02:00
undergroundwires
4a7efa27c8 Fix e2e test failing on Windows
The recent addition of revert logic in first visible card on Windows
(Privacy Cleanup) in cec0b4b, introduced an issue where end-to-end (e2e)
started failing due to the handling of hidden elements.

This commit improves the test to correctly handle the hidden card
scenario, explicitly filtering visible elements to ensure that only
visible elements are handled.
2024-05-15 09:03:18 +02:00
undergroundwires
cec0b4b4f6 win: standardize registry edit + delete on revert
This commit standardizes the management of registry keys and their
corresponding revert on delete action across all scripts using
`SetRegistryValue` function.

It improves script reliability, addresses previous errors, and corrects
the revert actions to match the default OS state when not explicitly set
by the OS.

Key changes:

- Use SetRegistryValue for uniformity.
- Remove error messages for non-existent registry keys, recognizing them
  as expected states rather than errors.
- Add missing revert actions to scripts where they were absent.
- Correct the revert logic in existing scripts to match the default OS
  configurations, particularly when the OS does not set a default value.
- Update documentation about default OS state for the related scripts.

This change improves maintainability by centralizing and standardizing
registry interactions, reducing the risk of errors and inconsistencies
in script behaviors.
2024-05-14 12:41:20 +02:00
undergroundwires
a1922c50c1 ci/cd: fix recent Docker build failures on macOS
The GitHub workflow for testing Docker builds on macOS was consistently
failing. This commit downgrades the macOS version used for Docker tests
to `macos-13`, which is the latest Intel-based macOS runner, instead of
the ARM-based `macos-14` which `macos-latest` points to.

This change is necessary because the hypervisor framework required for
Docker is not supported on the ARM-based macOS runners provided by
GitHub. This issue was causing failures when attempting to run Colima
with QEMU using `-accel hvf`, which is unsupported on these runners.
Switching to an Intel-based runner resolves this issue.

Related issues:
- actions/runner-images#9460
- actions/runner-images#9741
- abiosoft/colima#1023
2024-05-13 11:09:58 +02:00
undergroundwires
870120bc13 Add specific empty function name compiler error
This commit adds checks to rjeect functions with empty or whitespace
names. The compiler throws a specific errror when it encounters a
function data object lacking a proper name.

This provides early detection and clear feedback on invalid function
definitions, helping in faster debugging and ensuring script integrity
in the compilation process.

The enhancement aims to provide early detection and clear feedback
on invalid function definitions, aiding in faster debugging and
ensuring script integrity in the compilation process.

when it encounters a function data object lacking a proper name.
It covers scenarios where the function name might be an empty string,
undefined, or solely consist of whitespace.
2024-05-12 12:32:42 +02:00
undergroundwires-bot
f38cf73485 ⬆️ bump everywhere to 0.13.3 2024-05-11 09:58:21 +00:00
undergroundwires
9fd193e676 win: categorize and rename network security #131
This commit restructures the categorization of network security-related
scripts to improve clarity and align with user expectations. It involves
renaming and reorganizing categories to more accurately reflect their
functions beyond just IIS configurations.

This reorganization helps users find and utilize network security
scripts more efficiently and ensures that the categorization accurately
reflects the broader application of the scripts beyond server
configurations.

Changes:

- Merge network security enhancements under a single category.
- Rename categories for simplicity and increased technical accuracy.
- Flatten nested categories to streamline navigation and enhance
  clarity.
- Update documentation to match the new category structures.
- Revise script recommendations to encourage broader use.
- Fix revert codes of some related scripts to reflect default OS
  settings.
2024-05-11 11:39:28 +02:00
undergroundwires
52a4730073 ci/cd: remove check-latest from setup-node
This commit addresses the issue where Windows GitHub runners experience
failures due to unstable Node.js releases, particularly version 20.13.0,
as detailed in nodejs/node#52884 and nodejs/node#52682.

The 'check-latest' input in the 'setup-node' GitHub Action forces
every job to verify and potentially install the latest Node.js version.
This input was originally introduced to reduce maintenance efforts to
keep CI/CD setup up-to-date with the latest Node version.

However, the necessity to always run the latest Node.js version is not
critical for the CI/CD setup. Additionally, it causes increased network
requests and may inadvertently introduce unstable Node.js versions.

This commit removes the 'check-latest' option to prevent the immediate
adoption of new, potentially unstable Node.js releases, thus simplifying
the CI/CD pipeline. This keeps  CI/CD process is robust and predictable,
reducing the chances of unexpected disruptions in service deployment.
2024-05-10 12:18:16 +02:00
undergroundwires
bc4879cfe9 Fix Chromium scrollbar-induced layout shifts
This commit addresses an issue in Chromium on Linux and Windows where
the appearance of a vertical scrollbar causes unexpected horizontal
layout shifts. This behavior typically occurs when the window is
resized, a card is opened or a script is selected, resulting in content
being pushed to the left.

The solution implemented involves using `scrollbar-gutter: stable` to
ensure space is always allocated for the scrollbar, thus preventing any
shift in the page layout. This fix primarily affects Chromium-based
browsers on Linux and Windows. It has no impact on Firefox on any
platform, or any browser on macOS (including Chromium). Because these
render the scrollbar as an overlay, and do not suffer from this issue.

Steps to reproduce the issue using Chromium browser on Linux/Windows:

1. Open the app with a height large enough where a vertical scrollbar is
   not visible.
2. Resize the window to a height that triggers a vertical scrollbar.
3. Notice the layout shift as the body content moves to the right.

Changes:

- Add a CSS mixin to handle scrollbar gutter allocation with a fallback.
- Add support for modal dialog background lock to handle
  `scrollbar-gutter: stable;` in calculations to avoid layout shift when
  a modal is open.
- Add E2E test to avoid regression.
- Update DevToolkit to accommodate new scrollbar spacing.
2024-05-09 18:35:02 +02:00
undergroundwires
dd71536316 Fix misaligned tooltip positions in modal dialogs
This commit fixes a bug that causes tooltips to be slightly misaligned.

Tooltip positioning was incorrect during modal transitions due to their
initial movement, causing tooltips to align incorrectly at the start of
the animation rather than the end.

One way to solve this would be using `autoUpdate` from `floating-ui`
with `animationFrame: true`. However, this recalculates positions tens
of times per second, impacting performance. This is a monkey solution.

This commit adopts a more efficient approach by updating tooltip
positions only at the end of the transitions, which reduces calculations
and conserves resources.

Key changes:

- Addd transition end event listener for updating tooltip positions.
- Use throttling to eliminate excessive position recalculations.

Other supporting changes:

- Improve throttle function to support efficient recalculations of
  positions:
  - Add ability to optionally exclude the first execution (leading
    call).
  - Refactor to simplify it make it easier to follow and read.
  - Fix a bug where initial calls were incorrectly throttled if
    `dateNow()` returned `0`.
- Introduce and use a global hook for efficient DOM event management.
  This greatily introduce safety, reuse and testability of event
  listening.
2024-05-08 15:24:12 +02:00
undergroundwires
a3343205b1 Fix win execution with whitespace in username #351
This commit addresses the issue where scripts fail to execute on Windows
environments with usernames containing spaces. The problem stemmed from
PowerShell and cmd shell's handling of spaces in quoted arguments.

The solution involves encoding PowerShell commands before execution,
which mitigates the quoting issues previously causing script failures.
This approach is now integrated into the execution flow, ensuring that
commands are correctly handled irrespective of user names or other
variables that may include spaces.

Changes:

- Implement encoding for PowerShell commands to handle spaces in usernames
  and other similar scenarios.
- Update script documentation URLs to reflect changes in directory
  structure.

Fixes #351
2024-05-07 13:57:19 +02:00
undergroundwires
1d7cafc831 Fix VSCode script issues with added CI/CD tests
- Correct incorrect attribute in `configure_vscode.py`.
- Introduce CI tests for early error detection in the script.
- Replace emojis with ASCII in CI logs to avoid Windows encoding issues.
2024-05-06 17:18:19 +02:00
undergroundwires
c75df1c8c1 win: improve enabling secure connections #175
This commit refines the configuration of TLS and DTLS protocols on
Windows to enhance compatibility and stability across different
Windows versions.

Changes:

- Enable TLS 1.3 exclusively on Windows 11 and newer, addressing
  stability concerns with previous Windows versions, and resolving
  issue #175.
- Enable DTLS 1.2, replacing DTLS 1.3 due to lack of support in
  Windows. DTLS is enabled only on Windows 10 version 16007 and later
  for compatibility.
- Reorganize script categories for better clarity and manageability.
- Update revert codes for registry deletions to prevent false negative
  error outputs.
- Adjust recommendation levels to encourage more scripts due to system
  stability and documentation improvements introduced in this commit.
- Remove incorrect registry keys previously set for .NET apps.
- Add missing 64-bit registry keys for .NET apps.
- Rename scripts for improved simplicity and consistency.
- Improve documentation for affected scripts, correcting the
  misleading information about DTLS 1.2 vulnerability.
- Convert hexadecimal values to decimal in scripts to improve
  clarity.
- Introduce shared functions to reduce redundancy and improve
  script maintainability.
- Add more comments in generated code and simplify existing comments.
2024-05-05 10:57:45 +02:00
undergroundwires
ab25e0a066 Improve desktop icon quality and generation
This commit refactors the icon and logo generation process by replacing
multiple dependencies with ImageMagick. This simplifies the build
process and enhances maintainability.

Key changes:

- Remove unnecessary icon files for macOS (.icns) and Linux
  (size-specific PNGs). Electron-builder can now auto-generate these
  from a single `logo.png` starting from version 19.54.0, see:
  - electron-userland/electron-builder#1682
  - electron-userland/electron-builder#2533
- Retain `ico` generation with multiple sizes to fix pixelated/bad
  looking icons on Windows, see:
  - electron-userland/electron-builder#7328
  - electron-userland/electron-builder#3867
- Replaced `svgexport`, `icon-gen`, and `electron-icon-builder`
  dependencies with ImageMagick, addressing issues with outdated
  dependencies and unreliable CI/CD builds.
- Move electron-builder build resources to
  `src/presentation/electron/build` for better project structure.
- Improve `electron-builder` configuration file by making it
  importable/reusable without prebuilding the Electron application.
2024-05-04 12:09:42 +02:00
undergroundwires
813d820b85 Fix blank window on load on desktop version #348
This commit updates the application startup behavior to prevent showing
a blank window until it's fully loaded on all platforms. This enhancement
improves the user experience by ensuring the UI only becomes visible
when it is ready to interact with.

This fix contributes to a smoother user experience by aligning the
window display timing with content readiness, thus avoiding the brief
display of an empty screen.

Changes:

- Set window to initially hide until fully loaded using the
  `ready-to-show` event.
- Show the window, focus on it and bring it front once it is loaded.
  Windows requires additional logic to put Window to front, see
  electron/electron#2867.
- Parametrize the behavior of opening developer tools for easier
  configuration during testing.
2024-05-03 12:03:36 +02:00
undergroundwires
66a56888a4 win: fix Copilot by excluding r.bing.com #329
This commit modifies the blocking behavior of `r.bing.com` due to its
extensive use across multiple Windows features, including Copilot and
Maps. Previously, included in the 'Cortana and Live Tiles' block list,
this host was causing issues for Copilot functionalites, as noted in
issue #329. By excluding `r.bing.com` from block list, this update aims
to prevent unintended disruptions without compromising the privacy gains
of other scripts.

Changes include:

- Exclude `r.bing.com` from the "Cortana and Live Tiles" block list.
- Improve documentation to clarify the role and exclusions.
- Improve documentation with consistent header for blocked hosts.
2024-05-02 13:52:09 +02:00
undergroundwires
4ef16cea56 win: improve disabling protocols
This commit groups scripts related to disabling protocols under same
category, streamlining the process for disabling protocols like NetBios,
SMBv1, and various TLS/SSL versions. It improves the documentation and
scripts of the related scripts.

Key changes:

- Introduce new category for disabling insecure protocols and move
  related scripts under it.
- Remove .NET configuration from TLS 1.0 disabling to prevent unwanted
  side effects on .NET applications, maintaining system integrity.
- Remove the script disabling DTLS 1.1 as this protocol does not exist.
- Recommend previously not recommended scripts:
  - SSL 2.0 in 'Standard' because it's already removed from Windows.
  - SSL 3.0 in 'Standard' because it's already disabled by default.
  - TLS 1.0 in 'Strict' as it's deprecated on Windows.
  - TLS 1.1 in 'Strict' as it's deprecated on Windows.
- Rename and reorder scripts for consistency and enhanced readability.
- Fix revert codes to accurately reflect successful operations, by
  adding `2>nul` on `reg delete` commands.
- Expand documentation to include detailed precautions and references,
  aiding users in understanding the implications of their actions
  (addressing user feedback from #57, #131, #183, #185).

Other supporting changes:

- Convert hexadecimal values to decimal to enhance script readability.
- Refactor scripts to utilize shared functions, improving maintainability.
- Add detailed comments within the scripts to aid in comprehension.
- Minor updates to other crypto scripts for consistency.
- Reorganize protocol listing by age for a logical script flow.
- Standardize comments across various TLS configuration scripts for
  clarity.
- Fix enabling DTLS 1.3 being categorized as disabling insecure
  connection.
2024-05-01 12:18:55 +02:00
undergroundwires
8c17396285 Fix script cancellation with new dialog on Linux
This commit improves the management of script execution process by
enhancing the way terminal commands are handled, paving the way for
easier future modifications and providing clearer feedback to users when
scripts are cancelled.

Previously, the UI displayed a generic error message which could lead to
confusion if the user intentionally cancelled the script execution. Now,
a specific error dialog will appear, improving the user experience by
accurately reflecting the action taken by the user.

This change affects code execution on Linux where closing GNOME terminal
returns exit code `137` which is then treated by script cancellation by
privacy.sexy to show the accurate error dialog. It does not affect macOS
and Windows as curret commands result in success (`0`) exit code on
cancellation.

Additionally, this update encapsulates OS-specific logic into dedicated
classes, promoting better separation of concerns and increasing the
modularity of the codebase. This makes it simpler to maintain and extend
the application.

Key changes:

- Display a specific error message for script cancellations.
- Refactor command execution into dedicated classes.
- Improve file permission setting flexibility and avoid setting file
  permissions on Windows as it's not required to execute files.
- Introduce more granular error types for script execution.
- Increase logging for shell commands to aid in debugging.
- Expand test coverage to ensure reliability.
- Fix error dialogs not showing the error messages due to incorrect
  propagation of errors.

Other supported changes:

- Update `SECURITY.md` with details on script readback and verification.
- Fix a typo in `IpcRegistration.spec.ts`.
- Document antivirus scans in `desktop-vs-web-features.md`.
2024-04-30 15:04:59 +02:00
undergroundwires
694bf1a74d win, linux, mac: fix various typos #349
This commit fixes various typos in documentation and code.

Co-authored-by: RainRat <rainrat78@yahoo.ca>
2024-04-29 13:04:33 +02:00
undergroundwires
0fc2ffc1ea Add system requirements documentation #134
- Create system requirements documentation for desktop versions,
  addressing issue #134.
- Reorganize related documents into `docs/desktop` for improved
  structure and accessibility.
- Update references to address ARM chip emulation issues noted in user
  feedback, issue #348.
2024-04-28 16:57:55 +02:00
undergroundwires
d19dde603d win: improve disabling insecure hashes #131
This commit addresses reports in issue #131 about third-party cloud
services like MEGA and Dropbox being affected by hash disabling. It
updates the documentation to guide users on the potential impact,
adjusts the recommendation levels along with other minor improvements.

- Recommend hash disabling scripts in 'Strict'.
- Expand and refine documentation, adding warnings to inform user
  decisions (addressing issues #57, #131, #175, #183).
- Add a new shared function to standardize hash disabling, increasing
  code maintainability.
- Change from hexadecimal to decimal in scripts for clarity.
- Improve code comments for better understanding.
- Add comments in generated to code to make it easier to follow.
- Fix revert codes showing errors by using `2>nul` in `reg delete`
  commands.
- Rename scripts for consistent naming conventions.

Supporting changes in other SSL/TLS handshake scripts:

- Update documentation for consistency.
- Rename shared functions for consistency and clarity.
- Improve generated code comments for clarity.
2024-04-27 11:27:26 +02:00
undergroundwires
23bac0fc76 ci/cd: lint Python scripts using pylint
This commit integrates `pylint` into the CI/CD pipeline to improve the
quality of Python scripts within the project. By enforcing stricter
linting standards, the aim is to identify and correct potential issues
more efficiently, ultimately contributing to more reliable and
maintainable code.

Changes:

- Introduce `npm run lint:pylint` command to facilitate unified way to
  run linting on different environments.
- Include `npm run lint:pylint` in the CI/CD workflow to ensure all
  commits adhere to established Python coding standards.
- Fix an issue identified by `pylint` in `configure_vscode.py`.
- Rename the workflow to match the latest naming convention.
2024-04-26 17:03:38 +02:00
undergroundwires
e18907ca91 win: improve 'Snipping Tool' removal #343
Due to changes in how Windows handles the Snipping Tool, this commit
reclassifies the tool's disablement into its own distinct category.
This update introduces alternative methods to disable the tool,
enhances documentation, and improves script functionality.

Changes include:

- Move Snipping Tool removal to a standalone category for clearer
  navigation.
- Expand documentation to better describe the tool's impact on privacy.
- Add methods to disable the tool without removing the app.
- Implement a shared function to disable specific Windows hotkeys.
- Rename Cortana shortcut disablement script for consistency.
2024-04-25 10:36:33 +02:00
undergroundwires
4e21f05031 ci/cd: add check for TODO comments
This commit introduces a new GitHub Actions job within the quality
checks workflow that scans the latest commit for TODO comments. The
intention is to prevent such comments from being merged into the main
branch, promoting cleaner and more maintainable code.

The script uses a specific pattern to avoid IDE detection and
misclassification of the script line as a TODO item itself. If any TODO
comments are found, the script exists with a non-zero status,
indicating an issue that must be addressed before proceeding.
2024-04-24 23:59:55 +02:00
undergroundwires
8b224eefe7 win: doc, improve, encourage cipher disabling
- Introduce 'Disable insecure ciphers' category to organize and group
  cipher disabling scripts.
- Expand documentation, adding cautionary notes to help users make
  informed decisions, addressing issues #57, #131, #175, and #183.
- Implement `DisableCipherAlgorithm` function to standardize the
  approach to disabling cipher algorithms, enhancing maintainability
  and promoting code reuse.
- Replace hexadecimal numbers with decimals in scripts to improve
  readability.
- Add comments to generated code for better understandability.
- Update revert codes to avoid incorrect error messages when
  operations are successful, using `2>nul` in `reg delete` commands.
- Rename scripts for consistency, incorporating 'insecure' in titles.
- Adjust recommendations to disable all insecure ciphers in 'Strict'
  mode due to security risks, and recommend disabling `NULL` in
  'Standard' mode as it removes encryption.
- Remove disabling of `DES 56`, correcting a redundancy as this cipher
  configuration does not exist.
2024-04-21 14:31:00 +02:00
undergroundwires
f261ab4cd9 win: improve disabling insecure renegotiations
This commit improves script clarity and user guidance on disabling
insecure renegotiations.

- Update script name for clarity.
- Improve documentation for better understanding.
- Recommend the script as 'Strict' to align with its security focus.
- Modify revert codes to suppress misleading error messages upon
  successful reversion by including `2>nul` in `reg delete` commands.
- Convert hexadecimal to decimal in registry commands to improve
  readability.
2024-04-20 19:18:52 +02:00
undergroundwires
f584fabb50 win: improve disabling SMBv1 protocol
- Improve documentation.
- Add disabling `mrxsmb10` service (enabled with SMB1 feature).
- Configure Windows Server service for server side.
2024-04-19 16:16:00 +02:00
undergroundwires
2eed6f4afb win: organize and document network disablement
Reorganize and document scripts for disabling network features,
enhancing their discoverability and manageability. This commit
categorizes scripts related to disabling insecure network connections,
improves documentation, and makes these scripts more accessible.

- Group scripts under `Disable insecure connections` category.
- Move SMBv1 and NetBios disablement scripts to this new category.
- Improve documentation, highlighting the security improvements
  and potential compatibility issues with older systems.

Addresses issues #57, #115, #183, #175, and #185 by simplifying the
process of troubleshooting and reversing changes if necessary.
2024-04-17 21:35:56 +02:00
undergroundwires-bot
1c9dc93246 ⬆️ bump everywhere to 0.13.2 2024-04-16 07:45:17 +00:00
599 changed files with 40942 additions and 17797 deletions

View File

@@ -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.
-->

View 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>
---

View 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>
---

View File

@@ -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.
-->

View File

@@ -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.
-->

View 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>
---

View File

@@ -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.
-->

View 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>
---

View File

@@ -1 +1,7 @@
blank_issues_enabled: true # This file must be named `config.yml`. GitHub does not recognize the file if it is named `config.yaml`.
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.

View File

@@ -6,4 +6,4 @@ runs:
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: 20.x node-version: 20.x
check-latest: true # check-latest: true # Newest versions can potentially have undiscovered bugs or regressions

View File

@@ -0,0 +1,15 @@
inputs:
name:
required: true
path:
required: true
runs:
using: composite
steps:
-
name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.name }}
path: ${{ inputs.path }}

View File

@@ -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

View File

@@ -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
@@ -66,7 +70,7 @@ jobs:
- -
name: Upload screenshot name: Upload screenshot
if: always() # Run even if previous step fails if: always() # Run even if previous step fails
uses: actions/upload-artifact@v3 uses: ./.github/actions/upload-artifact
with: with:
name: screenshot-${{ matrix.os }} name: screenshot-${{ matrix.os }}
path: screenshot.png path: screenshot.png

View File

@@ -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,74 @@ 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
validate-collection-files:
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: python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
-
name: Validate
run: python3 ./scripts/validate-collections-yaml

View File

@@ -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

View File

@@ -51,14 +51,14 @@ jobs:
- -
name: Upload screenshots name: Upload screenshots
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
uses: actions/upload-artifact@v3 uses: ./.github/actions/upload-artifact
with: with:
name: e2e-screenshots-${{ matrix.os }} name: e2e-screenshots-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }} path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
- -
name: Upload videos name: Upload videos
if: always() # Run even if previous steps fail because test run video is always captured if: always() # Run even if previous steps fail because test run video is always captured
uses: actions/upload-artifact@v3 uses: ./.github/actions/upload-artifact
with: with:
name: e2e-videos-${{ matrix.os }} name: e2e-videos-${{ matrix.os }}
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }} path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}

4
.gitignore vendored
View File

@@ -14,3 +14,7 @@ node_modules
# macOS # macOS
.DS_Store .DS_Store
# Python
__pycache__
.venv

View File

@@ -5,8 +5,10 @@
"wengerk.highlight-bad-chars", // Highlights bad chars. "wengerk.highlight-bad-chars", // Highlights bad chars.
"wayou.vscode-todo-highlight", // Highlights TODO. "wayou.vscode-todo-highlight", // Highlights TODO.
"wix.vscode-import-cost", // Shows in KB how much a require include in code. "wix.vscode-import-cost", // Shows in KB how much a require include in code.
// Documentation // Markdown
"davidanson.vscode-markdownlint", // Lints markdown. "davidanson.vscode-markdownlint", // Lints markdown.
// YAML
"redhat.vscode-yaml", // Lints YAML files, validates against schema.
// TypeScript / JavaScript // TypeScript / JavaScript
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript. "dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports. "pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.

View File

@@ -1,5 +1,107 @@
# Changelog # Changelog
## 0.13.5 (2024-06-26)
* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703)
* win: document and improve Firefox telemetry #259 | [8341411](https://github.com/undergroundwires/privacy.sexy/commit/8341411be434c6d145e942b1792020ccf02f58c8)
* Add image to `README.md` to thank supporters | [fa2a92b](https://github.com/undergroundwires/privacy.sexy/commit/fa2a92bf893933bf5cd04512a712b7aa1b921277)
* win: improve executable blocking, Chrome reporting | [f21ef92](https://github.com/undergroundwires/privacy.sexy/commit/f21ef9250a2f459dbd4f789d857c78298fc202e6)
* mac: discourage and document captive portal script | [b29cd7b](https://github.com/undergroundwires/privacy.sexy/commit/b29cd7b5f74accf92c9700c3171670f82c8cb3b3)
* win: fix revert scripts for removing shortcuts | [8becc7d](https://github.com/undergroundwires/privacy.sexy/commit/8becc7dbc46af4441900e9841a716a53735bc82e)
* Refactor to unify scripts/categories as Executable | [c138f74](https://github.com/undergroundwires/privacy.sexy/commit/c138f74460bafaba3da55a65f3942bb6f95b1d99)
* Add object property validation in parser #369 | [6ecfa9b](https://github.com/undergroundwires/privacy.sexy/commit/6ecfa9b954edc10401acaf5c735eec0fc9f991cd)
* win: fix missing app access recommendations #369 | [1c2d82d](https://github.com/undergroundwires/privacy.sexy/commit/1c2d82dc9bd412ea601ab2550ba0b4f7d144f8e8)
* win: fix text and handwriting script omission #369 | [1a10cf2](https://github.com/undergroundwires/privacy.sexy/commit/1a10cf2e5f87cd8eb421ef77f6ce764b5482515e)
* mac: document, improve, encourage clearing logs | [e9a5285](https://github.com/undergroundwires/privacy.sexy/commit/e9a52859f63609c3f56def0b3e4d1ac6e5661536)
* Add schema validation for collection files #369 | [dc03bff](https://github.com/undergroundwires/privacy.sexy/commit/dc03bff324d673101002bb16f14e0429e8170fbb)
* win: fix incomplete VSCEIP, location scripts | [48761f6](https://github.com/undergroundwires/privacy.sexy/commit/48761f62a242f0910307994271cbe6730fb30f7e)
* Add type validation for parameters and fix types | [fac26a6](https://github.com/undergroundwires/privacy.sexy/commit/fac26a6ca07479c84fe62c5ea2a572dad1898ef8)
* Bump Electron to latest | [ed93614](https://github.com/undergroundwires/privacy.sexy/commit/ed93614ca34b1ab166e645cc5bedd497b0caeaac)
* Trim compiler error output for better readability | [78c62cf](https://github.com/undergroundwires/privacy.sexy/commit/78c62cfc953dbba543d8bdc42828a4ef4b13a7c7)
* win: fix errors due to missing Edge uninstaller | [2f82873](https://github.com/undergroundwires/privacy.sexy/commit/2f828735a87f98ba87b4fc826823d1482d4f2db2)
* win: fix latest Edge removal on Windows 10 #309 | [e7031a3](https://github.com/undergroundwires/privacy.sexy/commit/e7031a3ae4e57b6522c6ca67fc30e8a8718506b2)
* win: categorize, rename, doc Chrome & Edge scripts | [f286f92](https://github.com/undergroundwires/privacy.sexy/commit/f286f92b1fec49e89eea8982dffbc3d6ef1defde)
* win: add disabling Edge/WebView2 auto-updates #309 | [ed7e69c](https://github.com/undergroundwires/privacy.sexy/commit/ed7e69c07efe83fdb7f4af13aa220ff991fbbe59)
* win, linux, mac: fix typos #373 | [c09c5ff](https://github.com/undergroundwires/privacy.sexy/commit/c09c5ffa47865f7c76910644558b6783ed44f1e4)
* win: add more Edge scripts including AI & ads | [1430d52](https://github.com/undergroundwires/privacy.sexy/commit/1430d5215ab094d8201710761d631dc2bd740918)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.4...0.13.5)
## 0.13.4 (2024-05-27)
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
* ci/cd: fix recent Docker build failures on macOS | [a1922c5](https://github.com/undergroundwires/privacy.sexy/commit/a1922c50c12b3b7806e9e681ace842194a178bda)
* win: standardize registry edit + delete on revert | [cec0b4b](https://github.com/undergroundwires/privacy.sexy/commit/cec0b4b4f63c3563a0e7923ce6324a38d71a3955)
* Fix e2e test failing on Windows | [4a7efa2](https://github.com/undergroundwires/privacy.sexy/commit/4a7efa27c8df73ef9b7960afed29f216b066cba2)
* Add support for macOS universal binary #348, #362 | [d25c4e8](https://github.com/undergroundwires/privacy.sexy/commit/d25c4e8c812b8d012010ba38070a2931dcd28908)
* Migrate to GitHub issue forms | [9ab3ff7](https://github.com/undergroundwires/privacy.sexy/commit/9ab3ff75b0a69ac2ba27dd02e82db9b5bd76ea0f)
* ci/cd: fix quality checks not running on all OSes | [2390530](https://github.com/undergroundwires/privacy.sexy/commit/2390530d929fb92c266558c52376569a0ecb90c1)
* Bump Vue to latest and fix universal selector CSS | [aae5434](https://github.com/undergroundwires/privacy.sexy/commit/aae54344511ec51d17ad0420a92cb5a064e0e7bb)
* Centralize and optimize `ResizeObserver` usage | [2923621](https://github.com/undergroundwires/privacy.sexy/commit/292362135db0519ec1050bab80ed373aad115731)
* win: improve app access disabling and docs #138 | [ff3d5c4](https://github.com/undergroundwires/privacy.sexy/commit/ff3d5c48419f663379f5aba8936636c22f2c5de8)
* win: document and discourage RSA key script #363 | [f347fde](https://github.com/undergroundwires/privacy.sexy/commit/f347fde0c85f8b51b0060fdea0a2724b042aaeed)
* win: improve printing removal /w Print Queue #279 | [150e067](https://github.com/undergroundwires/privacy.sexy/commit/150e0670392bb62348c20ec644a4ed8a6bbffe74)
* win: discourage blocking app access #121 #339 #350 | [7794846](https://github.com/undergroundwires/privacy.sexy/commit/77948461856e6837ddfbcbbef72a1bf9fc706b4e)
* Improve context for errors thrown by compiler | [4212c7b](https://github.com/undergroundwires/privacy.sexy/commit/4212c7b9e0b1500378a1e4e88efc2d59f39f3d29)
* win: document disabling firewall #115 #152 #364 | [12b1f18](https://github.com/undergroundwires/privacy.sexy/commit/12b1f183f7ce966d6ce090d98aeea7ec491f8c7c)
* win: add script to disable Recall feature | [ce4cfdd](https://github.com/undergroundwires/privacy.sexy/commit/ce4cfdd169b7da0edc3da61143c988ed5f3c976e)
* win, mac, linux: fix typos and dead URLs #367 | [9e34e64](https://github.com/undergroundwires/privacy.sexy/commit/9e34e644493674ca709b64a47206763d5d4bd60c)
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.3...0.13.4)
## 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) ## 0.13.1 (2024-03-22)
* ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010) * ci/cd: Fix cross-platform git command compability | [255c51c](https://github.com/undergroundwires/privacy.sexy/commit/255c51c8a0524d3ea8a3b16ffc1b178650525010)

View File

@@ -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.1/privacy.sexy-Setup-0.13.1.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-0.13.1.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.1/privacy.sexy-0.13.1.AppImage). For more options, see [here](#additional-install-options). - 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-Setup-0.13.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.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.
@@ -183,3 +186,7 @@ Check [architecture.md](./docs/architecture.md) for an overview of design and ho
Security is a top priority at privacy.sexy. Security is a top priority at privacy.sexy.
An extensive commitment to security verification ensures this priority. An extensive commitment to security verification ensures this priority.
For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md). For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
## Supporters
[![Supporters appreciation banner showing the supporters](https://undergroundwires.dev/img/supporters.jpg)](https://undergroundwires.dev/supporters)

View File

@@ -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.
Recognizing that some users prefer not to keep these records, privacy.sexy provides specialized scripts for deletion of these scripts. - **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.
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

View File

@@ -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).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 963 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

View File

@@ -41,5 +41,5 @@ Application layer compiles templating syntax during parsing to create the end sc
The steps to extend the templating syntax: The steps to extend the templating syntax:
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more. 1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts). 2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).

View File

@@ -1,11 +1,11 @@
# Collection files # Collection files
privacy.sexy is a data-driven application that reads YAML files. privacy.sexy is a data-driven application that reads YAML files.
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. The YAML schema [`.schema.yaml`](./../src/application/collections/.schema.yaml) is provided to provide better IDE support and be used in automated validations.
Related documentation: Related documentation:
- 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types. - 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation.
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices. - 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
## Objects ## Objects
@@ -28,11 +28,22 @@ Related documentation:
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)** - `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
- Sets the scripting language for all inline code used within the collection. - Sets the scripting language for all inline code used within the collection.
### `Category` ### Executables
They represent independently executable actions with documentation and reversibility.
An Executable is a logical entity that can
- execute once compiled,
- include a `docs` property for documentation.
It's either [Category](#category) or a [Script](#script).
#### `Category`
Represents a logical group of scripts and subcategories. Represents a logical group of scripts and subcategories.
#### `Category` syntax ##### `Category` syntax
- `category:` *`string`* **(required)** - `category:` *`string`* **(required)**
- Name of the category. - Name of the category.
@@ -43,7 +54,7 @@ Represents a logical group of scripts and subcategories.
- `docs`: *`string`* | `[`*`string`*`, ... ]` - `docs`: *`string`* | `[`*`string`*`, ... ]`
- Markdown-formatted documentation related to the category. - Markdown-formatted documentation related to the category.
### `Script` #### `Script`
Represents an individual tweak. Represents an individual tweak.
@@ -58,7 +69,7 @@ Types (like [functions](#function)):
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md). 📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
#### `Script` syntax ##### `Script` syntax
- `name`: *`string`* **(required)** - `name`: *`string`* **(required)**
- Script name. - Script name.

View File

@@ -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.

View 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"

View File

@@ -80,8 +80,10 @@ See [ci-cd.md](./ci-cd.md) for more information.
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js): - [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features. - Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies. - For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py): - [**`python3 ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment. - Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
- [**`python3 ./scripts/validate-collections-yaml`**](../scripts/validate-collections-yaml/README.md):
- Validates the syntax and structure of collection YAML files.
#### Automation scripts #### Automation scripts

View File

@@ -23,8 +23,10 @@ The presentation layer uses an event-driven architecture for bidirectional react
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles. - [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
- [**`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.

View File

@@ -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`.

View File

@@ -1,7 +1,7 @@
/* 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 } = require('fs'); const { readdirSync, existsSync } = require('node:fs');
const { electronBundled, electronUnbundled } = require('./dist-dirs.json'); const { electronBundled, electronUnbundled } = require('./dist-dirs.json');
/** /**
@@ -17,6 +17,7 @@ module.exports = {
}, },
directories: { directories: {
output: electronBundled, output: electronBundled,
buildResources: resolvePathFromProjectRoot('src/presentation/electron/build'),
}, },
extraMetadata: { extraMetadata: {
main: findMainEntryFile( main: findMainEntryFile(
@@ -42,7 +43,10 @@ module.exports = {
// macOS // macOS
mac: { mac: {
target: 'dmg', target: {
target: 'dmg',
arch: 'universal',
},
}, },
dmg: { dmg: {
artifactName: '${name}-${version}.${ext}', artifactName: '${name}-${version}.${ext}',
@@ -53,10 +57,18 @@ module.exports = {
* Finds by accommodating different JS file extensions and module formats. * Finds by accommodating different JS file extensions and module formats.
*/ */
function findMainEntryFile(parentDirectory) { function findMainEntryFile(parentDirectory) {
const files = readdirSync(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)); const entryFile = files.find((file) => /^index\.(cjs|mjs|js)$/.test(file));
if (!entryFile) { if (!entryFile) {
throw new Error(`Main entry file not found in ${parentDirectory}.`); throw new Error(`Main entry file not found in ${absoluteParentDirectory}.`);
} }
return join(parentDirectory, entryFile); return join(parentDirectory, entryFile);
} }
function resolvePathFromProjectRoot(pathSegment) {
return resolve(__dirname, pathSegment);
}

9361
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.13.1", "version": "0.13.5",
"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,61 +29,59 @@
"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.6", "@floating-ui/vue": "^1.1.1",
"@juggle/resize-observer": "^3.4.0", "@juggle/resize-observer": "^3.4.0",
"ace-builds": "^1.33.0", "ace-builds": "^1.35.3",
"electron-log": "^5.1.2", "electron-log": "^5.1.6",
"electron-progressbar": "^2.2.1", "electron-progressbar": "^2.2.1",
"electron-updater": "^6.1.9", "electron-updater": "^6.2.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"vue": "^3.4.21" "vue": "^3.4.32"
}, },
"devDependencies": { "devDependencies": {
"@modyfi/vite-plugin-yaml": "^1.1.0", "@modyfi/vite-plugin-yaml": "^1.1.0",
"@rushstack/eslint-patch": "^1.10.2", "@rushstack/eslint-patch": "^1.10.3",
"@types/ace": "^0.0.52", "@types/ace": "^0.0.52",
"@types/file-saver": "^2.0.7", "@types/file-saver": "^2.0.7",
"@types/markdown-it": "^14.0.1", "@types/markdown-it": "^14.1.1",
"@typescript-eslint/eslint-plugin": "6.21.0", "@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0", "@typescript-eslint/parser": "6.21.0",
"@vitejs/plugin-legacy": "^5.3.2", "@vitejs/plugin-legacy": "^5.4.1",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.5",
"@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.5", "@vue/test-utils": "^2.4.6",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"cypress": "^13.7.3", "cypress": "^13.13.1",
"electron": "^29.3.0", "electron": "^31.2.1",
"electron-builder": "^24.13.3", "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.3.0",
"electron-vite": "^2.1.0",
"eslint": "8.57.0", "eslint": "8.57.0",
"eslint-plugin-cypress": "^2.15.1", "eslint-plugin-cypress": "^3.3.0",
"eslint-plugin-vue": "^9.25.0", "eslint-plugin-vue": "^9.27.0",
"eslint-plugin-vuejs-accessibility": "^2.2.1", "eslint-plugin-vuejs-accessibility": "^2.4.0",
"icon-gen": "^4.0.0", "jsdom": "^24.1.0",
"jsdom": "^24.0.0", "markdownlint-cli": "^0.41.0",
"markdownlint-cli": "^0.39.0", "postcss": "^8.4.39",
"postcss": "^8.4.38", "remark-cli": "^12.0.1",
"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": "^6.0.0", "remark-preset-lint-consistent": "^6.0.0",
"remark-validate-links": "^13.0.1", "remark-validate-links": "^13.0.1",
"sass": "^1.75.0", "sass": "^1.77.8",
"start-server-and-test": "^2.0.3", "start-server-and-test": "^2.0.4",
"svgexport": "^0.4.2", "terser": "^5.31.3",
"terser": "^5.30.3", "tslib": "^2.6.3",
"tslib": "^2.6.2",
"typescript": "^5.4.5", "typescript": "^5.4.5",
"vite": "^5.2.8", "vite": "^5.3.4",
"vitest": "^1.5.0", "vitest": "^2.0.3",
"vue-tsc": "^2.0.13", "vue-tsc": "^2.0.26",
"yaml-lint": "^1.7.0" "yaml-lint": "^1.7.0"
}, },
"//devDependencies": { "//devDependencies": {

View File

@@ -1,6 +1,10 @@
""" """
This script configures project-level VSCode settings in '.vscode/settings.json' for Description:
development and installs recommended extensions from '.vscode/extensions.json'. This script configures project-level VSCode settings in '.vscode/settings.json' for
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'])
@@ -54,6 +58,10 @@ def add_or_update_settings() -> None:
# Details: # pylint: disable-next=line-too-long # Details: # pylint: disable-next=line-too-long
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274 # - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
# Disable telemetry
configure_setting_key('redhat.telemetry.enabled', False)
configure_setting_key('gitlens.telemetry.enabled', False)
def configure_setting_key(configuration_key: str, desired_value: Any) -> None: def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
try: try:
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file: with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
@@ -98,7 +106,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 +118,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 +175,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()

View File

@@ -2,94 +2,119 @@
* Description: * Description:
* This script updates the logo images across the project based on the primary * This script updates the logo images across the project based on the primary
* logo file ('img/logo.svg' file). * logo file ('img/logo.svg' file).
*
* It handles the creation and update of various icon sizes for different purposes, * 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 * including desktop launcher icons, tray icons, and web favicons from a single source
* SVG logo file. * SVG logo file.
* *
* Usage: * Usage:
* node ./scripts/logo-update.js * node ./scripts/logo-update.js
*
* Notes:
* ImageMagick must be installed and accessible in the system's PATH
*/ */
import { resolve, join } from 'node:path'; import { resolve, join, dirname } from 'node:path';
import { rm, mkdtemp, stat } from 'node:fs/promises'; 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) {
@@ -100,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}`);
@@ -135,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();

View File

@@ -0,0 +1,51 @@
# validate-collections-yaml
This script validates YAML collection files against a predefined schema to ensure their integrity.
## Prerequisites
- Python 3.x installed on your system.
## Running in a Virtual Environment (Recommended)
Using a virtual environment isolates dependencies and prevents conflicts.
1. **Create a virtual environment:**
```bash
python3 -m venv ./scripts/validate-collections-yaml/.venv
```
2. **Activate the virtual environment:**
```bash
source ./scripts/validate-collections-yaml/.venv/bin/activate
```
3. **Install dependencies:**
```bash
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
```
4. **Run the script:**
```bash
python3 ./scripts/validate-collections-yaml
```
## Running Globally
Running the script globally is less recommended due to potential dependency conflicts.
1. **Install dependencies:**
```bash
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
```
2. **Run the script:**
```bash
python3 ./scripts/validate-collections-yaml
```

View File

@@ -0,0 +1,62 @@
"""
Description:
This script validates collection YAML files against the expected schema.
Usage:
python3 ./scripts/validate-collections-yaml
Notes:
This script requires the `jsonschema` and `pyyaml` packages (see requirements.txt).
"""
# pylint: disable=missing-function-docstring
from os import path
import sys
from glob import glob
from typing import List
from jsonschema import exceptions, validate # pylint: disable=import-error
import yaml # pylint: disable=import-error
SCHEMA_FILE_PATH = './src/application/collections/.schema.yaml'
COLLECTIONS_GLOB_PATTERN = './src/application/collections/*.yaml'
def main() -> None:
schema_yaml = read_file(SCHEMA_FILE_PATH)
schema_json = convert_yaml_to_json(schema_yaml)
collection_file_paths = find_collection_files(COLLECTIONS_GLOB_PATTERN)
print(f'Found {len(collection_file_paths)} YAML files to validate.')
total_invalid_files = 0
for collection_file_path in collection_file_paths:
file_name = path.basename(collection_file_path)
print(f'Validating {file_name}...')
collection_yaml = read_file(collection_file_path)
collection_json = convert_yaml_to_json(collection_yaml)
try:
validate(instance=collection_json, schema=schema_json)
print(f'Success: {file_name} is valid.')
except exceptions.ValidationError as err:
print(f'Error: Validation failed for {file_name}.', file=sys.stderr)
print(str(err), file=sys.stderr)
total_invalid_files += 1
if total_invalid_files > 0:
print(f'Validation complete with {total_invalid_files} invalid files.', file=sys.stderr)
sys.exit(1)
else:
print('Validation complete. All files are valid.')
sys.exit(0)
def read_file(file_path: str) -> str:
with open(file_path, 'r', encoding='utf-8') as file:
return file.read()
def find_collection_files(glob_pattern: str) -> List[str]:
files = glob(glob_pattern)
filtered_files = [f for f in files if not path.basename(f).startswith('.')]
return filtered_files
def convert_yaml_to_json(yaml_content: str) -> dict:
return yaml.safe_load(yaml_content)
if __name__ == '__main__':
main()

View File

@@ -0,0 +1,6 @@
attrs==23.2.0
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
PyYAML==6.0.1
referencing==0.35.1
rpds-py==0.18.1

View File

@@ -91,7 +91,7 @@ async function verifyFilesExist(directoryPath, filePatterns) {
if (!match) { if (!match) {
die( die(
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``, `No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`, `\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`,
); );
} }
} }

View File

@@ -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;

View File

@@ -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);

View File

@@ -5,13 +5,13 @@ export type EnumType = number | string;
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
= { [key in T]: TEnumValue }; = { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> { export interface EnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum; parseEnum(value: string, propertyName: string): TEnum;
} }
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>( export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
enumVariable: EnumVariable<T, TEnumValue>, enumVariable: EnumVariable<T, TEnumValue>,
): IEnumParser<TEnumValue> { ): EnumParser<TEnumValue> {
return { return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
}; };

View File

@@ -0,0 +1,25 @@
import { isArray } from '@/TypeHelpers';
export type OptionalString = string | undefined | null;
export function filterEmptyStrings(
texts: readonly OptionalString[],
isArrayType: typeof isArray = isArray,
): string[] {
if (!isArrayType(texts)) {
throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`);
}
assertArrayItemsAreStringLike(texts);
return texts
.filter((title): title is string => Boolean(title));
}
function assertArrayItemsAreStringLike(
texts: readonly unknown[],
): asserts texts is readonly OptionalString[] {
const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null));
if (invalidItems.length > 0) {
const invalidTypes = invalidItems.map((item) => typeof item).join(', ');
throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`);
}
}

View File

@@ -0,0 +1,29 @@
import { isString } from '@/TypeHelpers';
import { splitTextIntoLines } from './SplitTextIntoLines';
export function indentText(
text: string,
indentLevel = 1,
utilities: TextIndentationUtilities = DefaultUtilities,
): string {
if (!utilities.isStringType(text)) {
throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`);
}
if (indentLevel <= 0) {
throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`);
}
const indentation = '\t'.repeat(indentLevel);
return utilities.splitIntoLines(text)
.map((line) => (line ? `${indentation}${line}` : line))
.join('\n');
}
interface TextIndentationUtilities {
readonly splitIntoLines: typeof splitTextIntoLines;
readonly isStringType: typeof isString;
}
const DefaultUtilities: TextIndentationUtilities = {
splitIntoLines: splitTextIntoLines,
isStringType: isString,
};

View File

@@ -0,0 +1,11 @@
import { isString } from '@/TypeHelpers';
export function splitTextIntoLines(
text: string,
isStringType = isString,
): string[] {
if (!isStringType(text)) {
throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`);
}
return text.split(/\r\n|\r|\n/);
}

View File

@@ -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.callback(...args); this.executeNow(args);
this.previouslyRun = now; return;
} else { }
const nextCall = () => this.invoke(...args); case this.isAlreadyScheduled(): {
const nextCallDelayInMs = this.waitInMs - (now - this.previouslyRun); this.updateNextScheduled(args);
this.queuedExecutionId = this.timer.setTimeout(nextCall, nextCallDelayInMs); 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.lastExecutionTime = this.dateNow();
}
private dateNow(): number {
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;
}
} }

View File

@@ -1,6 +1,6 @@
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum'; import { assertInRange } from '@/application/Common/Enum';
import { CategoryCollectionState } from './State/CategoryCollectionState'; import { CategoryCollectionState } from './State/CategoryCollectionState';

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext'; import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
import { ApplicationCode } from './Code/ApplicationCode'; import { ApplicationCode } from './Code/ApplicationCode';

View File

@@ -1,18 +1,20 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { ICodeChangedEvent } from './ICodeChangedEvent'; import type { ICodeChangedEvent } from './ICodeChangedEvent';
export class CodeChangedEvent implements ICodeChangedEvent { export class CodeChangedEvent implements ICodeChangedEvent {
public readonly code: string; public readonly code: string;
public readonly addedScripts: ReadonlyArray<IScript>; public readonly addedScripts: ReadonlyArray<Script>;
public readonly removedScripts: ReadonlyArray<IScript>; public readonly removedScripts: ReadonlyArray<Script>;
public readonly changedScripts: ReadonlyArray<IScript>; public readonly changedScripts: ReadonlyArray<Script>;
private readonly scripts: Map<IScript, ICodePosition>; private readonly scripts: Map<Script, ICodePosition>;
constructor( constructor(
code: string, code: string,
@@ -25,7 +27,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
this.addedScripts = selectIfNotExists(newScripts, oldScripts); this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts); this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts); this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>(); this.scripts = new Map<Script, ICodePosition>();
scripts.forEach((position, selection) => { scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position); this.scripts.set(selection.script, position);
}); });
@@ -35,13 +37,13 @@ export class CodeChangedEvent implements ICodeChangedEvent {
return this.scripts.size === 0; return this.scripts.size === 0;
} }
public getScriptPositionInCode(script: IScript): ICodePosition { public getScriptPositionInCode(script: Script): ICodePosition {
return this.getPositionById(script.id); return this.getPositionById(script.executableId);
} }
private getPositionById(scriptId: string): ICodePosition { private getPositionById(scriptId: ExecutableId): ICodePosition {
const position = [...this.scripts.entries()] const position = [...this.scripts.entries()]
.filter(([s]) => s.id === scriptId) .filter(([s]) => s.executableId === scriptId)
.map(([, pos]) => pos) .map(([, pos]) => pos)
.at(0); .at(0);
if (!position) { if (!position) {
@@ -52,12 +54,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
} }
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) { function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
const totalLines = script.split(/\r\n|\r|\n/).length; const totalLines = splitTextIntoLines(script).length;
const missingPositions = positions.filter((position) => position.endLine > totalLines); const missingPositions = positions.filter((position) => position.endLine > totalLines);
if (missingPositions.length > 0) { if (missingPositions.length > 0) {
throw new Error( throw new Error(
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"` `Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
+ `(total code lines: ${totalLines}).`, + ` (total code lines: ${totalLines}).`,
); );
} }
} }
@@ -65,7 +67,7 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
function getChangedScripts( function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>, oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>, newScripts: ReadonlyArray<SelectedScript>,
): ReadonlyArray<IScript> { ): ReadonlyArray<Script> {
return newScripts return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id .filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
&& oldScript.revert !== newScript.revert)) && oldScript.revert !== newScript.revert))

View File

@@ -1,11 +1,11 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
export interface ICodeChangedEvent { export interface ICodeChangedEvent {
readonly code: string; readonly code: string;
readonly addedScripts: ReadonlyArray<IScript>; readonly addedScripts: ReadonlyArray<Script>;
readonly removedScripts: ReadonlyArray<IScript>; readonly removedScripts: ReadonlyArray<Script>;
readonly changedScripts: ReadonlyArray<IScript>; readonly changedScripts: ReadonlyArray<Script>;
isEmpty(): boolean; isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition; getScriptPositionInCode(script: Script): ICodePosition;
} }

View File

@@ -1,3 +1,4 @@
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
import type { ICodeBuilder } from './ICodeBuilder'; import type { ICodeBuilder } from './ICodeBuilder';
const TotalFunctionSeparatorChars = 58; const TotalFunctionSeparatorChars = 58;
@@ -15,7 +16,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
this.lines.push(''); this.lines.push('');
return this; return this;
} }
const lines = code.match(/[^\r\n]+/g); const lines = splitTextIntoLines(code);
if (lines) { if (lines) {
this.lines.push(...lines); this.lines.push(...lines);
} }

View File

@@ -1,5 +1,5 @@
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { FilterChange } from './Event/FilterChange'; import { FilterChange } from './Event/FilterChange';
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy'; import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
import type { FilterResult } from './Result/FilterResult'; import type { FilterResult } from './Result/FilterResult';

View File

@@ -1,11 +1,11 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { FilterResult } from './FilterResult'; import type { FilterResult } from './FilterResult';
export class AppliedFilterResult implements FilterResult { export class AppliedFilterResult implements FilterResult {
constructor( constructor(
public readonly scriptMatches: ReadonlyArray<IScript>, public readonly scriptMatches: ReadonlyArray<Script>,
public readonly categoryMatches: ReadonlyArray<ICategory>, public readonly categoryMatches: ReadonlyArray<Category>,
public readonly query: string, public readonly query: string,
) { ) {
if (!query) { throw new Error('Query is empty or undefined'); } if (!query) { throw new Error('Query is empty or undefined'); }

View File

@@ -1,8 +1,9 @@
import type { IScript, ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
export interface FilterResult { export interface FilterResult {
readonly categoryMatches: ReadonlyArray<ICategory>; readonly categoryMatches: ReadonlyArray<Category>;
readonly scriptMatches: ReadonlyArray<IScript>; readonly scriptMatches: ReadonlyArray<Script>;
readonly query: string; readonly query: string;
hasAnyMatches(): boolean; hasAnyMatches(): boolean;
} }

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { FilterResult } from '../Result/FilterResult'; import type { FilterResult } from '../Result/FilterResult';
export interface FilterStrategy { export interface FilterStrategy {

View File

@@ -1,7 +1,8 @@
import type { ICategory, IScript } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { IScriptCode } from '@/domain/IScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { IDocumentable } from '@/domain/IDocumentable'; import type { Documentable } from '@/domain/Executables/Documentable';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { Script } from '@/domain/Executables/Script/Script';
import { AppliedFilterResult } from '../Result/AppliedFilterResult'; import { AppliedFilterResult } from '../Result/AppliedFilterResult';
import type { FilterStrategy } from './FilterStrategy'; import type { FilterStrategy } from './FilterStrategy';
import type { FilterResult } from '../Result/FilterResult'; import type { FilterResult } from '../Result/FilterResult';
@@ -24,7 +25,7 @@ export class LinearFilterStrategy implements FilterStrategy {
} }
function matchesCategory( function matchesCategory(
category: ICategory, category: Category,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -34,7 +35,7 @@ function matchesCategory(
} }
function matchesScript( function matchesScript(
script: IScript, script: Script,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -58,7 +59,7 @@ function matchName(
} }
function matchCode( function matchCode(
code: IScriptCode, code: ScriptCode,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
if (code.execute.toLowerCase().includes(filterLowercase)) { if (code.execute.toLowerCase().includes(filterLowercase)) {
@@ -71,7 +72,7 @@ function matchCode(
} }
function matchDocumentation( function matchDocumentation(
documentable: IDocumentable, documentable: Documentable,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return documentable.docs.some( return documentable.docs.some(

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { IApplicationCode } from './Code/IApplicationCode'; import type { IApplicationCode } from './Code/IApplicationCode';
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext'; import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';

View File

@@ -1,9 +1,9 @@
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
export interface ReadonlyCategorySelection { export interface ReadonlyCategorySelection {
areAllScriptsSelected(category: ICategory): boolean; areAllScriptsSelected(category: Category): boolean;
isAnyScriptSelected(category: ICategory): boolean; isAnyScriptSelected(category: Category): boolean;
} }
export interface CategorySelection extends ReadonlyCategorySelection { export interface CategorySelection extends ReadonlyCategorySelection {

View File

@@ -1,3 +1,5 @@
import type { ExecutableId } from '@/domain/Executables/Identifiable';
type CategorySelectionStatus = { type CategorySelectionStatus = {
readonly isSelected: true; readonly isSelected: true;
readonly isReverted: boolean; readonly isReverted: boolean;
@@ -6,7 +8,7 @@ type CategorySelectionStatus = {
}; };
export interface CategorySelectionChange { export interface CategorySelectionChange {
readonly categoryId: number; readonly categoryId: ExecutableId;
readonly newStatus: CategorySelectionStatus; readonly newStatus: CategorySelectionStatus;
} }

View File

@@ -1,5 +1,5 @@
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
import type { CategorySelection } from './CategorySelection'; import type { CategorySelection } from './CategorySelection';
import type { ScriptSelection } from '../Script/ScriptSelection'; import type { ScriptSelection } from '../Script/ScriptSelection';
@@ -13,7 +13,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
} }
public areAllScriptsSelected(category: ICategory): boolean { public areAllScriptsSelected(category: Category): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;
@@ -23,11 +23,11 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
return false; return false;
} }
return scripts.every( return scripts.every(
(script) => selectedScripts.some((selected) => selected.id === script.id), (script) => selectedScripts.some((selected) => selected.id === script.executableId),
); );
} }
public isAnyScriptSelected(category: ICategory): boolean { public isAnyScriptSelected(category: Category): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
const scripts = category.getAllScriptsRecursively(); const scripts = category.getAllScriptsRecursively();
const scriptsChangesInCategory = scripts const scriptsChangesInCategory = scripts
.map((script): ScriptSelectionChange => ({ .map((script): ScriptSelectionChange => ({
scriptId: script.id, scriptId: script.executableId,
newStatus: { newStatus: {
...change.newStatus, ...change.newStatus,
}, },

View File

@@ -1,9 +1,10 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import { EventSource } from '@/infrastructure/Events/EventSource'; import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository'; import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce'; import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import { UserSelectedScript } from './UserSelectedScript'; import { UserSelectedScript } from './UserSelectedScript';
import type { ScriptSelection } from './ScriptSelection'; import type { ScriptSelection } from './ScriptSelection';
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange'; import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
@@ -16,7 +17,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
export class DebouncedScriptSelection implements ScriptSelection { export class DebouncedScriptSelection implements ScriptSelection {
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>(); public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
private readonly scripts: Repository<string, SelectedScript>; private readonly scripts: Repository<SelectedScript>;
public readonly processChanges: ScriptSelection['processChanges']; public readonly processChanges: ScriptSelection['processChanges'];
@@ -25,7 +26,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
selectedScripts: ReadonlyArray<SelectedScript>, selectedScripts: ReadonlyArray<SelectedScript>,
debounce: DebounceFunction = batchedDebounce, debounce: DebounceFunction = batchedDebounce,
) { ) {
this.scripts = new InMemoryRepository<string, SelectedScript>(); this.scripts = new InMemoryRepository<SelectedScript>();
for (const script of selectedScripts) { for (const script of selectedScripts) {
this.scripts.addItem(script); this.scripts.addItem(script);
} }
@@ -38,8 +39,8 @@ export class DebouncedScriptSelection implements ScriptSelection {
); );
} }
public isSelected(scriptId: string): boolean { public isSelected(scriptExecutableId: ExecutableId): boolean {
return this.scripts.exists(scriptId); return this.scripts.exists(scriptExecutableId);
} }
public get selectedScripts(): readonly SelectedScript[] { public get selectedScripts(): readonly SelectedScript[] {
@@ -49,7 +50,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
public selectAll(): void { public selectAll(): void {
const scriptsToSelect = this.collection const scriptsToSelect = this.collection
.getAllScripts() .getAllScripts()
.filter((script) => !this.scripts.exists(script.id)) .filter((script) => !this.scripts.exists(script.executableId))
.map((script) => new UserSelectedScript(script, false)); .map((script) => new UserSelectedScript(script, false));
if (scriptsToSelect.length === 0) { if (scriptsToSelect.length === 0) {
return; return;
@@ -80,7 +81,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
}); });
} }
public selectOnly(scripts: readonly IScript[]): void { public selectOnly(scripts: readonly Script[]): void {
assertNonEmptyScriptSelection(scripts); assertNonEmptyScriptSelection(scripts);
this.processChanges({ this.processChanges({
changes: [ changes: [
@@ -116,12 +117,12 @@ export class DebouncedScriptSelection implements ScriptSelection {
private applyChange(change: ScriptSelectionChange): number { private applyChange(change: ScriptSelectionChange): number {
const script = this.collection.getScript(change.scriptId); const script = this.collection.getScript(change.scriptId);
if (change.newStatus.isSelected) { if (change.newStatus.isSelected) {
return this.addOrUpdateScript(script.id, change.newStatus.isReverted); return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted);
} }
return this.removeScript(script.id); return this.removeScript(script.executableId);
} }
private addOrUpdateScript(scriptId: string, revert: boolean): number { private addOrUpdateScript(scriptId: ExecutableId, revert: boolean): number {
const script = this.collection.getScript(scriptId); const script = this.collection.getScript(scriptId);
const selectedScript = new UserSelectedScript(script, revert); const selectedScript = new UserSelectedScript(script, revert);
if (!this.scripts.exists(selectedScript.id)) { if (!this.scripts.exists(selectedScript.id)) {
@@ -136,7 +137,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
return 1; return 1;
} }
private removeScript(scriptId: string): number { private removeScript(scriptId: ExecutableId): number {
if (!this.scripts.exists(scriptId)) { if (!this.scripts.exists(scriptId)) {
return 0; return 0;
} }
@@ -145,31 +146,31 @@ export class DebouncedScriptSelection implements ScriptSelection {
} }
} }
function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) { function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
if (selectedItems.length === 0) { if (selectedItems.length === 0) {
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.'); throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
} }
} }
function getScriptIdsToBeSelected( function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<SelectedScript>,
desiredScripts: readonly IScript[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return desiredScripts return desiredScripts
.filter((script) => !existingItems.exists(script.id)) .filter((script) => !existingItems.exists(script.executableId))
.map((script) => script.id); .map((script) => script.executableId);
} }
function getScriptIdsToBeDeselected( function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<SelectedScript>,
desiredScripts: readonly IScript[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return existingItems return existingItems
.getItems() .getItems()
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id)) .filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
.map((script) => script.id); .map((script) => script.id);
} }
function equals(a: SelectedScript, b: SelectedScript): boolean { function equals(a: SelectedScript, b: SelectedScript): boolean {
return a.script.equals(b.script.id) && a.revert === b.revert; return a.script.executableId === b.script.executableId && a.revert === b.revert;
} }

View File

@@ -1,16 +1,17 @@
import type { IEventSource } from '@/infrastructure/Events/IEventSource'; import type { IEventSource } from '@/infrastructure/Events/IEventSource';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ExecutableId } from '@/domain/Executables/Identifiable';
import type { SelectedScript } from './SelectedScript'; import type { SelectedScript } from './SelectedScript';
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange'; import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
export interface ReadonlyScriptSelection { export interface ReadonlyScriptSelection {
readonly changed: IEventSource<readonly SelectedScript[]>; readonly changed: IEventSource<readonly SelectedScript[]>;
readonly selectedScripts: readonly SelectedScript[]; readonly selectedScripts: readonly SelectedScript[];
isSelected(scriptId: string): boolean; isSelected(scriptExecutableId: ExecutableId): boolean;
} }
export interface ScriptSelection extends ReadonlyScriptSelection { export interface ScriptSelection extends ReadonlyScriptSelection {
selectOnly(scripts: readonly IScript[]): void; selectOnly(scripts: readonly Script[]): void;
selectAll(): void; selectAll(): void;
deselectAll(): void; deselectAll(): void;
processChanges(action: ScriptSelectionChangeCommand): void; processChanges(action: ScriptSelectionChangeCommand): void;

View File

@@ -1,3 +1,5 @@
import type { ExecutableId } from '@/domain/Executables/Identifiable';
export type ScriptSelectionStatus = { export type ScriptSelectionStatus = {
readonly isSelected: true; readonly isSelected: true;
readonly isReverted: boolean; readonly isReverted: boolean;
@@ -7,7 +9,7 @@ export type ScriptSelectionStatus = {
}; };
export interface ScriptSelectionChange { export interface ScriptSelectionChange {
readonly scriptId: string; readonly scriptId: ExecutableId;
readonly newStatus: ScriptSelectionStatus; readonly newStatus: ScriptSelectionStatus;
} }

View File

@@ -1,9 +1,7 @@
import type { IEntity } from '@/infrastructure/Entity/IEntity'; import type { Script } from '@/domain/Executables/Script/Script';
import type { IScript } from '@/domain/IScript'; import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
type ScriptId = IScript['id']; export interface SelectedScript extends RepositoryEntity {
readonly script: Script;
export interface SelectedScript extends IEntity<ScriptId> {
readonly script: IScript;
readonly revert: boolean; readonly revert: boolean;
} }

View File

@@ -1,17 +1,16 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import type { Script } from '@/domain/Executables/Script/Script';
import type { IScript } from '@/domain/IScript'; import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
import type { SelectedScript } from './SelectedScript';
type SelectedScriptId = SelectedScript['id']; export class UserSelectedScript implements RepositoryEntity {
public readonly id: string;
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor( constructor(
public readonly script: IScript, public readonly script: Script,
public readonly revert: boolean, public readonly revert: boolean,
) { ) {
super(script.id); this.id = script.executableId;
if (revert && !script.canRevert()) { if (revert && !script.canRevert()) {
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`); throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`);
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper'; import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection'; import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
import type { CategorySelection } from './Category/CategorySelection'; import type { CategorySelection } from './Category/CategorySelection';

View File

@@ -1,40 +1,48 @@
import type { CollectionData } from '@/application/collections/'; import type { CollectionData } from '@/application/collections/';
import type { IApplication } from '@/domain/IApplication'; import type { IApplication } from '@/domain/IApplication';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import WindowsData from '@/application/collections/windows.yaml'; import WindowsData from '@/application/collections/windows.yaml';
import MacOsData from '@/application/collections/macos.yaml'; import MacOsData from '@/application/collections/macos.yaml';
import LinuxData from '@/application/collections/linux.yaml'; import LinuxData from '@/application/collections/linux.yaml';
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser'; import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata'; import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
import { parseCategoryCollection } from './CategoryCollectionParser';
export function parseApplication( export function parseApplication(
categoryParser = parseCategoryCollection, collectionsData: readonly CollectionData[] = PreParsedCollections,
projectDetailsParser = parseProjectDetails, utilities: ApplicationParserUtilities = DefaultUtilities,
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
collectionsData = PreParsedCollections,
): IApplication { ): IApplication {
validateCollectionsData(collectionsData); validateCollectionsData(collectionsData, utilities.validator);
const projectDetails = projectDetailsParser(metadata); const projectDetails = utilities.parseProjectDetails();
const collections = collectionsData.map( const collections = collectionsData.map(
(collection) => categoryParser(collection, projectDetails), (collection) => utilities.parseCategoryCollection(collection, projectDetails),
); );
const app = new Application(projectDetails, collections); const app = new Application(projectDetails, collections);
return app; return app;
} }
export type CategoryCollectionParserType const PreParsedCollections: readonly CollectionData[] = [
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
const PreParsedCollections: readonly CollectionData [] = [
WindowsData, MacOsData, LinuxData, WindowsData, MacOsData, LinuxData,
]; ];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(
if (!collections.length) { collections: readonly CollectionData[],
throw new Error('missing collections'); validator: TypeValidator,
} ) {
validator.assertNonEmptyCollection({
value: collections,
valueName: 'Collections',
});
} }
interface ApplicationParserUtilities {
readonly parseCategoryCollection: CategoryCollectionParser;
readonly validator: TypeValidator;
readonly parseProjectDetails: ProjectDetailsParser;
}
const DefaultUtilities: ApplicationParserUtilities = {
parseCategoryCollection,
parseProjectDetails,
validator: createTypeValidator(),
};

View File

@@ -1,34 +1,75 @@
import type { CollectionData } from '@/application/collections/'; import type { CollectionData } from '@/application/collections/';
import { OperatingSystem } from '@/domain/OperatingSystem'; import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import { createEnumParser } from '../Common/Enum'; import { createEnumParser, type EnumParser } from '../Common/Enum';
import { parseCategory } from './CategoryParser'; import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext'; import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
export function parseCategoryCollection( export const parseCategoryCollection: CategoryCollectionParser = (
content: CollectionData, content,
projectDetails: ProjectDetails, projectDetails,
osParser = createEnumParser(OperatingSystem), utilities: CategoryCollectionParserUtilities = DefaultUtilities,
): ICategoryCollection { ) => {
validate(content); validateCollection(content, utilities.validator);
const scripting = new ScriptingDefinitionParser() const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
.parse(content.scripting, projectDetails); const collectionUtilities = utilities.createUtilities(content.functions, scripting);
const context = new CategoryCollectionParseContext(content.functions, scripting); const categories = content.actions.map(
const categories = content.actions.map((action) => parseCategory(action, context)); (action) => utilities.parseCategory(action, collectionUtilities),
const os = osParser.parseEnum(content.os, 'os');
const collection = new CategoryCollection(
os,
categories,
scripting,
); );
const os = utilities.osParser.parseEnum(content.os, 'os');
const collection = utilities.createCategoryCollection({
os, actions: categories, scripting,
});
return collection; return collection;
};
export type CategoryCollectionFactory = (
...parameters: ConstructorParameters<typeof CategoryCollection>
) => ICategoryCollection;
export interface CategoryCollectionParser {
(
content: CollectionData,
projectDetails: ProjectDetails,
utilities?: CategoryCollectionParserUtilities,
): ICategoryCollection;
} }
function validate(content: CollectionData): void { function validateCollection(
if (!content.actions.length) { content: CollectionData,
throw new Error('content does not define any action'); validator: TypeValidator,
} ): void {
validator.assertObject({
value: content,
valueName: 'Collection',
allowedProperties: [
'os', 'scripting', 'actions', 'functions',
],
});
validator.assertNonEmptyCollection({
value: content.actions,
valueName: '\'actions\' in collection',
});
} }
interface CategoryCollectionParserUtilities {
readonly osParser: EnumParser<OperatingSystem>;
readonly validator: TypeValidator;
readonly parseScriptingDefinition: ScriptingDefinitionParser;
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
readonly parseCategory: CategoryParser;
readonly createCategoryCollection: CategoryCollectionFactory;
}
const DefaultUtilities: CategoryCollectionParserUtilities = {
osParser: createEnumParser(OperatingSystem),
validator: createTypeValidator(),
parseScriptingDefinition,
createUtilities: createCollectionUtilities,
parseCategory,
createCategoryCollection: (...args) => new CategoryCollection(...args),
};

View File

@@ -1,133 +0,0 @@
import type {
CategoryData, ScriptData, CategoryOrScriptData,
} from '@/application/collections/';
import { Script } from '@/domain/Script';
import { Category } from '@/domain/Category';
import { NodeValidator } from '@/application/Parser/NodeValidation/NodeValidator';
import { NodeType } from '@/application/Parser/NodeValidation/NodeType';
import { parseDocs } from './DocumentationParser';
import { parseScript } from './Script/ScriptParser';
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
let categoryIdCounter = 0;
export function parseCategory(
category: CategoryData,
context: ICategoryCollectionParseContext,
factory: CategoryFactoryType = CategoryFactory,
): Category {
return parseCategoryRecursively({
categoryData: category,
context,
factory,
});
}
interface ICategoryParseContext {
readonly categoryData: CategoryData,
readonly context: ICategoryCollectionParseContext,
readonly factory: CategoryFactoryType,
readonly parentCategory?: CategoryData,
}
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
ensureValidCategory(context.categoryData, context.parentCategory);
const children: ICategoryChildren = {
subCategories: new Array<Category>(),
subScripts: new Array<Script>(),
};
for (const data of context.categoryData.children) {
parseNode({
nodeData: data,
children,
parent: context.categoryData,
factory: context.factory,
context: context.context,
});
}
try {
return context.factory(
/* id: */ categoryIdCounter++,
/* name: */ context.categoryData.category,
/* docs: */ parseDocs(context.categoryData),
/* categories: */ children.subCategories,
/* scripts: */ children.subScripts,
);
} catch (err) {
return new NodeValidator({
type: NodeType.Category,
selfNode: context.categoryData,
parentNode: context.parentCategory,
}).throw(err.message);
}
}
function ensureValidCategory(category: CategoryData, parentCategory?: CategoryData) {
new NodeValidator({
type: NodeType.Category,
selfNode: category,
parentNode: parentCategory,
})
.assertDefined(category)
.assertValidName(category.category)
.assert(
() => category.children.length > 0,
`"${category.category}" has no children.`,
);
}
interface ICategoryChildren {
subCategories: Category[];
subScripts: Script[];
}
interface INodeParseContext {
readonly nodeData: CategoryOrScriptData;
readonly children: ICategoryChildren;
readonly parent: CategoryData;
readonly factory: CategoryFactoryType;
readonly context: ICategoryCollectionParseContext;
}
function parseNode(context: INodeParseContext) {
const validator = new NodeValidator({ selfNode: context.nodeData, parentNode: context.parent });
validator.assertDefined(context.nodeData);
if (isCategory(context.nodeData)) {
const subCategory = parseCategoryRecursively({
categoryData: context.nodeData,
context: context.context,
factory: context.factory,
parentCategory: context.parent,
});
context.children.subCategories.push(subCategory);
} else if (isScript(context.nodeData)) {
const script = parseScript(context.nodeData, context.context);
context.children.subScripts.push(script);
} else {
validator.throw('Node is neither a category or a script.');
}
}
function isScript(data: CategoryOrScriptData): data is ScriptData {
return hasCode(data) || hasCall(data);
}
function isCategory(data: CategoryOrScriptData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}
function hasProperty(object: unknown, propertyName: string) {
return Object.prototype.hasOwnProperty.call(object, propertyName);
}
export type CategoryFactoryType = (
...parameters: ConstructorParameters<typeof Category>) => Category;
const CategoryFactory: CategoryFactoryType = (...parameters) => new Category(...parameters);

View File

@@ -0,0 +1,116 @@
import { CustomError } from '@/application/Common/CustomError';
import { indentText } from '@/application/Common/Text/IndentText';
export interface ErrorWithContextWrapper {
(
innerError: Error,
additionalContext: string,
): Error;
}
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
innerError,
additionalContext,
) => {
if (!additionalContext) {
throw new Error('Missing additional context');
}
return new ContextualError({
innerError,
additionalContext,
});
};
/**
* Class for building a detailed error trace.
*
* Alternatives considered:
* - `AggregateError`:
* Similar but not well-serialized or displayed by browsers such as Chromium (last tested v126).
* - `cause` property:
* Not displayed by all browsers (last tested v126).
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
*
* This is immutable where the constructor sets the values because using getter functions such as
* `get cause()`, `get message()` does not work on Chromium (last tested v126), but works fine on
* Firefox (last tested v127).
*/
class ContextualError extends CustomError {
constructor(public readonly context: ErrorContext) {
super(
generateDetailedErrorMessageWithContext(context),
{
cause: context.innerError,
},
);
}
}
interface ErrorContext {
readonly innerError: Error;
readonly additionalContext: string;
}
function generateDetailedErrorMessageWithContext(
context: ErrorContext,
): string {
return [
'\n',
// Display the current error message first, then the root cause.
// This prevents repetitive main messages for errors with a `cause:` chain,
// aligning with browser error display conventions.
context.additionalContext,
'\n',
'Error Trace (starting from root cause):',
indentText(
formatErrorTrace(
// Displaying contexts from the top frame (deepest, most recent) aligns with
// common debugger/compiler standard.
extractErrorTraceAscendingFromDeepest(context),
),
),
'\n',
].join('\n');
}
function extractErrorTraceAscendingFromDeepest(
context: ErrorContext,
): string[] {
const originalError = findRootError(context.innerError);
const contextsDescendingFromMostRecent: string[] = [
context.additionalContext,
...gatherContextsFromErrorChain(context.innerError),
originalError.toString(),
];
const contextsAscendingFromDeepest = contextsDescendingFromMostRecent.reverse();
return contextsAscendingFromDeepest;
}
function findRootError(error: Error): Error {
if (error instanceof ContextualError) {
return findRootError(error.context.innerError);
}
return error;
}
function gatherContextsFromErrorChain(
error: Error,
accumulatedContexts: string[] = [],
): string[] {
if (error instanceof ContextualError) {
accumulatedContexts.push(error.context.additionalContext);
return gatherContextsFromErrorChain(error.context.innerError, accumulatedContexts);
}
return accumulatedContexts;
}
function formatErrorTrace(
errorMessages: readonly string[],
): string {
if (errorMessages.length === 1) {
return errorMessages[0];
}
return errorMessages
.map((context, index) => `${index + 1}.${indentText(context)}`)
.join('\n');
}

View File

@@ -0,0 +1,131 @@
import type { PropertyKeys } from '@/TypeHelpers';
import {
isNullOrUndefined, isArray, isPlainObject, isString,
} from '@/TypeHelpers';
export interface TypeValidator {
assertObject<T>(assertion: ObjectAssertion<T>): void;
assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void;
assertNonEmptyString(assertion: NonEmptyStringAssertion): void;
}
export interface NonEmptyCollectionAssertion {
readonly value: unknown;
readonly valueName: string;
}
export interface RegexValidationRule {
readonly expectedMatch: RegExp;
readonly errorMessage: string;
}
export interface NonEmptyStringAssertion {
readonly value: unknown;
readonly valueName: string;
readonly rule?: RegexValidationRule;
}
export interface ObjectAssertion<T> {
readonly value: T | unknown;
readonly valueName: string;
readonly allowedProperties?: readonly PropertyKeys<T>[];
}
export function createTypeValidator(): TypeValidator {
return {
assertObject: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertPlainObject(assertion.value, assertion.valueName);
assertNoEmptyProperties(assertion.value, assertion.valueName);
if (assertion.allowedProperties !== undefined) {
const allowedProperties = assertion.allowedProperties.map((p) => p as string);
assertAllowedProperties(assertion.value, assertion.valueName, allowedProperties);
}
},
assertNonEmptyCollection: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertArray(assertion.value, assertion.valueName);
assertNonEmpty(assertion.value, assertion.valueName);
},
assertNonEmptyString: (assertion) => {
assertDefined(assertion.value, assertion.valueName);
assertString(assertion.value, assertion.valueName);
if (assertion.value.length === 0) {
throw new Error(`'${assertion.valueName}' is missing.`);
}
if (assertion.rule) {
if (!assertion.value.match(assertion.rule.expectedMatch)) {
throw new Error(assertion.rule.errorMessage);
}
}
},
};
}
function assertDefined<T>(
value: T,
valueName: string,
): asserts value is NonNullable<T> {
if (isNullOrUndefined(value)) {
throw new Error(`'${valueName}' is missing.`);
}
}
function assertPlainObject(
value: unknown,
valueName: string,
): asserts value is object {
if (!isPlainObject(value)) {
throw new Error(`'${valueName}' is not an object.`);
}
}
function assertNoEmptyProperties(
value: object,
valueName: string,
): void {
if (Object.keys(value).length === 0) {
throw new Error(`'${valueName}' is an empty object without properties.`);
}
}
function assertAllowedProperties(
value: object,
valueName: string,
allowedProperties: readonly string[],
): void {
const properties = Object.keys(value).map((p) => p as string);
const disallowedProperties = properties.filter(
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
);
if (disallowedProperties.length > 0) {
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);
}
}
function assertArray(
value: unknown,
valueName: string,
): asserts value is Array<unknown> {
if (!isArray(value)) {
throw new Error(`${valueName} should be of type 'array', but is of type '${typeof value}'.`);
}
}
function assertString(
value: unknown,
valueName: string,
): asserts value is string {
if (!isString(value)) {
throw new Error(`${valueName} should be of type 'string', but is of type '${typeof value}'.`);
}
}
function assertNonEmpty(
value: Array<unknown>,
valueName: string,
): void {
if (value.length === 0) {
throw new Error(`'${valueName}' cannot be an empty array.`);
}
}

View File

@@ -0,0 +1,35 @@
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import type { FunctionData } from '@/application/collections/';
import { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
import { SyntaxFactory } from './Script/Validation/Syntax/SyntaxFactory';
import type { IScriptCompiler } from './Script/Compiler/IScriptCompiler';
import type { ILanguageSyntax } from './Script/Validation/Syntax/ILanguageSyntax';
import type { ISyntaxFactory } from './Script/Validation/Syntax/ISyntaxFactory';
export interface CategoryCollectionSpecificUtilities {
readonly compiler: IScriptCompiler;
readonly syntax: ILanguageSyntax;
}
export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFactory = (
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
) => {
const syntax = syntaxFactory.create(scripting.language);
return {
compiler: new ScriptCompiler({
functions: functionsData ?? [],
syntax,
}),
syntax,
};
};
export interface CategoryCollectionSpecificUtilitiesFactory {
(
functionsData: ReadonlyArray<FunctionData> | undefined,
scripting: IScriptingDefinition,
syntaxFactory?: ISyntaxFactory,
): CategoryCollectionSpecificUtilities;
}

View File

@@ -0,0 +1,181 @@
import type {
CategoryData, ScriptData, ExecutableData,
} from '@/application/collections/';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
import { parseDocs, type DocsParser } from './DocumentationParser';
import { parseScript, type ScriptParser } from './Script/ScriptParser';
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
import { ExecutableType } from './Validation/ExecutableType';
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
export const parseCategory: CategoryParser = (
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
) => {
return parseCategoryRecursively({
categoryData: category,
collectionUtilities,
categoryUtilities,
});
};
export interface CategoryParser {
(
category: CategoryData,
collectionUtilities: CategoryCollectionSpecificUtilities,
categoryUtilities?: CategoryParserUtilities,
): Category;
}
interface CategoryParseContext {
readonly categoryData: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly parentCategory?: CategoryData;
readonly categoryUtilities: CategoryParserUtilities;
}
function parseCategoryRecursively(
context: CategoryParseContext,
): Category | never {
const validator = ensureValidCategory(context);
const children: CategoryChildren = {
subcategories: new Array<Category>(),
subscripts: new Array<Script>(),
};
for (const data of context.categoryData.children) {
parseUnknownExecutable({
data,
children,
parent: context.categoryData,
categoryUtilities: context.categoryUtilities,
collectionUtilities: context.collectionUtilities,
});
}
try {
return context.categoryUtilities.createCategory({
executableId: context.categoryData.category, // Pseudo-ID for uniqueness until real ID support
name: context.categoryData.category,
docs: context.categoryUtilities.parseDocs(context.categoryData),
subcategories: children.subcategories,
scripts: children.subscripts,
});
} catch (error) {
throw context.categoryUtilities.wrapError(
error,
validator.createContextualErrorMessage('Failed to parse category.'),
);
}
}
function ensureValidCategory(
context: CategoryParseContext,
): ExecutableValidator {
const category = context.categoryData;
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
type: ExecutableType.Category,
self: context.categoryData,
parentCategory: context.parentCategory,
});
validator.assertType((v) => v.assertObject({
value: category,
valueName: `Category '${category.category}'` ?? 'Category',
allowedProperties: [
'docs', 'children', 'category',
],
}));
validator.assertValidName(category.category);
validator.assertType((v) => v.assertNonEmptyCollection({
value: category.children,
valueName: category.category,
}));
return validator;
}
interface CategoryChildren {
readonly subcategories: Category[];
readonly subscripts: Script[];
}
interface ExecutableParseContext {
readonly data: ExecutableData;
readonly children: CategoryChildren;
readonly parent: CategoryData;
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
readonly categoryUtilities: CategoryParserUtilities;
}
function parseUnknownExecutable(context: ExecutableParseContext) {
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
self: context.data,
parentCategory: context.parent,
});
validator.assertType((v) => v.assertObject({
value: context.data,
valueName: 'Executable',
}));
validator.assert(
() => isCategory(context.data) || isScript(context.data),
'Executable is neither a category or a script.',
);
if (isCategory(context.data)) {
const subCategory = parseCategoryRecursively({
categoryData: context.data,
collectionUtilities: context.collectionUtilities,
parentCategory: context.parent,
categoryUtilities: context.categoryUtilities,
});
context.children.subcategories.push(subCategory);
} else { // A script
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
context.children.subscripts.push(script);
}
}
function isScript(data: ExecutableData): data is ScriptData {
return hasCode(data) || hasCall(data);
}
function isCategory(data: ExecutableData): data is CategoryData {
return hasProperty(data, 'category');
}
function hasCode(data: unknown): boolean {
return hasProperty(data, 'code');
}
function hasCall(data: unknown) {
return hasProperty(data, 'call');
}
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);
}
interface CategoryParserUtilities {
readonly createCategory: CategoryFactory;
readonly wrapError: ErrorWithContextWrapper;
readonly createValidator: ExecutableValidatorFactory;
readonly parseScript: ScriptParser;
readonly parseDocs: DocsParser;
}
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
createCategory,
wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator,
parseScript,
parseDocs,
};

View File

@@ -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(
@@ -44,5 +50,5 @@ class DocumentationContainer {
} }
function throwInvalidType(): never { function throwInvalidType(): never {
throw new Error('docs field (documentation) must be an array of strings'); throw new Error('docs field (documentation) must be a single string or an array of strings.');
} }

View File

@@ -1,4 +1,4 @@
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
@@ -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,

View File

@@ -1,8 +1,13 @@
import { ExpressionPosition } from './ExpressionPosition'; import { ExpressionPosition } from './ExpressionPosition';
export function createPositionFromRegexFullMatch( export interface ExpressionPositionFactory {
match: RegExpMatchArray, (
): ExpressionPosition { match: RegExpMatchArray,
): 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);
} };

View File

@@ -1,4 +1,4 @@
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser'; import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection'; import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
import type { IExpressionsCompiler } from './IExpressionsCompiler'; import type { IExpressionsCompiler } from './IExpressionsCompiler';

View File

@@ -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) {

View File

@@ -0,0 +1,127 @@
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
import type { IExpressionParser } from '../IExpressionParser';
import type { IExpression } from '../../Expression/IExpression';
import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter';
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 {
protected abstract readonly regex: RegExp;
public constructor(
private readonly utilities: RegexParserUtilities = DefaultRegexParserUtilities,
) {
}
public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code));
}
protected abstract buildExpression(match: RegExpMatchArray): PrimitiveExpression;
private* findRegexExpressions(code: string): Iterable<IExpression> {
if (!code) {
throw new Error(
this.buildErrorMessageWithContext({ errorMessage: 'missing code', code: 'EMPTY' }),
);
}
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) {
const primitiveExpression = this.doOrRethrow(
() => this.buildExpression(match),
createErrorContext('Failed to build expression.'),
);
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;
}
}
private doOrRethrow<T>(
action: () => T,
context: ErrorContext,
): T {
try {
return action();
} catch (error) {
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(
expression: PrimitiveExpression,
parameterCollection: IFunctionParameterCollection,
): IReadOnlyFunctionParameterCollection {
return (expression.parameters || [])
.reduce((parameters, parameter) => {
parameters.addParameter(parameter);
return parameters;
}, parameterCollection);
}
export interface PrimitiveExpression {
readonly evaluator: ExpressionEvaluator;
readonly parameters?: readonly FunctionParameter[];
}
export interface ExpressionFactory {
(
...args: ConstructorParameters<typeof Expression>
): IExpression;
}
const DefaultRegexParserUtilities: RegexParserUtilities = {
wrapError: wrapErrorWithAdditionalContext,
createPosition: createPositionFromRegexFullMatch,
createExpression: (...args) => new Expression(...args),
createParameterCollection: createFunctionParameterCollection,
};

View File

@@ -1,4 +1,4 @@
export interface IPipe { export interface Pipe {
readonly name: string; readonly name: string;
apply(input: string): string; apply(input: string): string;
} }

View File

@@ -1,11 +1,11 @@
import type { IPipe } from '../IPipe'; import type { Pipe } from '../Pipe';
export class EscapeDoubleQuotes implements IPipe { export class EscapeDoubleQuotes implements Pipe {
public readonly name: string = 'escapeDoubleQuotes'; public readonly name: string = 'escapeDoubleQuotes';
public apply(raw: string): string { public apply(raw: string): string {
if (!raw) { if (!raw) {
return raw; return '';
} }
return raw.replaceAll('"', '"^""'); return raw.replaceAll('"', '"^""');
/* eslint-disable vue/max-len */ /* eslint-disable vue/max-len */

View File

@@ -1,6 +1,7 @@
import type { IPipe } from '../IPipe'; import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
import type { Pipe } from '../Pipe';
export class InlinePowerShell implements IPipe { export class InlinePowerShell implements Pipe {
public readonly name: string = 'inlinePowerShell'; public readonly name: string = 'inlinePowerShell';
public apply(code: string): string { public apply(code: string): string {
@@ -8,9 +9,11 @@ export class InlinePowerShell implements IPipe {
return code; return code;
} }
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent" const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
// Order is important
inlineComments, inlineComments,
mergeLinesWithBacktick,
mergeHereStrings, mergeHereStrings,
mergeLinesWithBacktick,
mergeLinesWithBracketCodeBlocks,
mergeNewLines, mergeNewLines,
]).reduce((a, b) => (data) => b(a(data))); ]).reduce((a, b) => (data) => b(a(data)));
const newCode = processor(code); const newCode = processor(code);
@@ -89,10 +92,6 @@ function inlineComments(code: string): string {
*/ */
} }
function getLines(code: string): string[] {
return (code?.split(/\r\n|\r|\n/) || []);
}
/* /*
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://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
@@ -102,18 +101,18 @@ function mergeHereStrings(code: string) {
return code.replaceAll(regex, (_$, quotes, scope) => { return code.replaceAll(regex, (_$, quotes, scope) => {
const newString = getHereStringHandler(quotes); const newString = getHereStringHandler(quotes);
const escaped = scope.replaceAll(quotes, newString.escapedQuotes); const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
const lines = getLines(escaped); const lines = splitTextIntoLines(escaped);
const inlined = lines.join(newString.separator); const inlined = lines.join(newString.separator);
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`; const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
return quoted; return quoted;
}); });
} }
interface IInlinedHereString { interface InlinedHereString {
readonly quotesAround: string; readonly quotesAround: string;
readonly escapedQuotes: string; readonly escapedQuotes: string;
readonly separator: string; readonly separator: string;
} }
function getHereStringHandler(quotes: string): IInlinedHereString { function getHereStringHandler(quotes: string): InlinedHereString {
/* /*
We handle @' and @" differently. We handle @' and @" differently.
Single quotes are interpreted literally and doubles are expandable. Single quotes are interpreted literally and doubles are expandable.
@@ -158,9 +157,33 @@ function mergeLinesWithBacktick(code: string) {
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' '); return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
} }
function mergeNewLines(code: string) { /**
return getLines(code) * Inlines code blocks in PowerShell scripts while preserving correct syntax.
.map((line) => line.trim()) * It removes unnecessary newlines and spaces around brackets,
.filter((line) => line.length > 0) * inlining the code where possible.
.join('; '); * This prevents syntax errors like "Unexpected token '}'" when inlining brackets.
*/
function mergeLinesWithBracketCodeBlocks(code: string): string {
return code
// Opening bracket: [whitespace] Opening bracket (newline)
.replace(/(?<=.*)\s*{[\r\n][\s\r\n]*/g, ' { ')
// Closing bracket: [whitespace] Closing bracket (newline) (continuation keyword)
.replace(/\s*}[\r\n][\s\r\n]*(?=elseif|else|catch|finally|until)/g, ' } ')
.replace(/(?<=do\s*{.*)[\r\n\s]*}[\r\n][\r\n\s]*(?=while)/g, ' } '); // Do-While
}
function mergeNewLines(code: string) {
const nonEmptyLines = splitTextIntoLines(code)
.map((line) => line.trim())
.filter((line) => line.length > 0);
return nonEmptyLines
.map((line, index) => {
const isLastLine = index === nonEmptyLines.length - 1;
if (isLastLine) {
return line;
}
return line.endsWith(';') ? line : `${line};`;
})
.join(' ');
} }

View File

@@ -1,6 +1,6 @@
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell'; import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes'; import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
import type { IPipe } from './IPipe'; import type { Pipe } from './Pipe';
const RegisteredPipes = [ const RegisteredPipes = [
new EscapeDoubleQuotes(), new EscapeDoubleQuotes(),
@@ -8,19 +8,19 @@ const RegisteredPipes = [
]; ];
export interface IPipeFactory { export interface IPipeFactory {
get(pipeName: string): IPipe; get(pipeName: string): Pipe;
} }
export class PipeFactory implements IPipeFactory { export class PipeFactory implements IPipeFactory {
private readonly pipes = new Map<string, IPipe>(); private readonly pipes = new Map<string, Pipe>();
constructor(pipes: readonly IPipe[] = RegisteredPipes) { constructor(pipes: readonly Pipe[] = RegisteredPipes) {
for (const pipe of pipes) { for (const pipe of pipes) {
this.registerPipe(pipe); this.registerPipe(pipe);
} }
} }
public get(pipeName: string): IPipe { public get(pipeName: string): Pipe {
validatePipeName(pipeName); validatePipeName(pipeName);
const pipe = this.pipes.get(pipeName); const pipe = this.pipes.get(pipeName);
if (!pipe) { if (!pipe) {
@@ -29,7 +29,7 @@ export class PipeFactory implements IPipeFactory {
return pipe; return pipe;
} }
private registerPipe(pipe: IPipe): void { private registerPipe(pipe: Pipe): void {
validatePipeName(pipe.name); validatePipeName(pipe.name);
if (this.pipes.has(pipe.name)) { if (this.pipes.has(pipe.name)) {
throw new Error(`Pipe name must be unique: "${pipe.name}"`); throw new Error(`Pipe name must be unique: "${pipe.name}"`);

Some files were not shown because too many files have changed in this diff Show More