From 93329a732888fad7214b3620742684535eda9a62 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 1 Apr 2024 10:46:53 +0200 Subject: [PATCH 01/14] merge from local --- .prettierignore | 6 - .prettierrc.yaml | 4 - .vscode/settings.json | 13 +- TODO.md | 9 - package.json | 78 +- packages/cli/bin | 2 + packages/cli/package.json | 24 + packages/cli/src/index.js | 166 +++ packages/core/package.json | 38 + packages/core/src/classes/ManifestConfig.js | 34 + packages/core/src/classes/PatchManager.js | 117 +++ packages/core/src/db.js | 113 +++ packages/core/src/generic_steps/git_clone.js | 46 + packages/core/src/generic_steps/git_pull.js | 30 + packages/core/src/generic_steps/git_reset.js | 77 ++ packages/core/src/generic_steps/http.js | 62 ++ packages/core/src/generic_steps/index.js | 42 + packages/core/src/handlers/apply.js | 82 ++ packages/core/src/handlers/execute.js | 64 ++ packages/core/src/handlers/install.js | 163 +++ packages/core/src/handlers/list.js | 5 + packages/core/src/handlers/uninstall.js | 63 ++ packages/core/src/handlers/update.js | 114 +++ packages/core/src/helpers/downloadHttpFile.js | 73 ++ packages/core/src/helpers/sendToRender.js | 43 + packages/core/src/helpers/setup.js | 125 +++ packages/core/src/index.js | 52 + .../core/src/libraries}/execa/index.d.ts | 0 .../core/src/libraries}/execa/index.js | 0 .../core/src/libraries}/execa/lib/command.js | 0 .../core/src/libraries}/execa/lib/error.js | 0 .../core/src/libraries}/execa/lib/kill.js | 0 .../core/src/libraries}/execa/lib/pipe.js | 0 .../core/src/libraries}/execa/lib/promise.js | 0 .../core/src/libraries}/execa/lib/stdio.js | 0 .../core/src/libraries}/execa/lib/stream.js | 0 .../core/src/libraries}/execa/lib/verbose.js | 0 .../src/libraries}/get-stream/array-buffer.js | 0 .../core/src/libraries}/get-stream/array.js | 0 .../core/src/libraries}/get-stream/buffer.js | 0 .../src/libraries}/get-stream/contents.js | 0 .../core/src/libraries}/get-stream/index.d.ts | 0 .../core/src/libraries}/get-stream/index.js | 0 .../src/libraries}/get-stream/index.test-d.ts | 0 .../core/src/libraries}/get-stream/string.js | 0 .../core/src/libraries}/get-stream/utils.js | 0 .../core/src/libraries}/human-signals/core.js | 0 .../src/libraries}/human-signals/index.js | 0 .../src/libraries}/human-signals/realtime.js | 0 .../src/libraries}/human-signals/signals.js | 0 .../core/src/libraries}/is-stream/index.d.ts | 0 .../core/src/libraries}/is-stream/index.js | 0 .../src/libraries/lowdb/adapters/Memory.js | 24 + .../libraries/lowdb/adapters/node/DataFile.js | 51 + .../libraries/lowdb/adapters/node/JSONFile.js | 19 + .../libraries/lowdb/adapters/node/TextFile.js | 65 ++ packages/core/src/libraries/lowdb/core/Low.js | 48 + .../core/src/libraries/lowdb/presets/node.js | 23 + .../core/src/libraries/lowdb/steno/index.js | 98 ++ .../src/libraries}/mimic-function/index.js | 0 .../src/libraries}/npm-run-path/index.d.ts | 0 .../core/src/libraries}/npm-run-path/index.js | 0 .../core/src/libraries}/onetime/index.d.ts | 0 .../core/src/libraries}/onetime/index.js | 0 .../libraries}/strip-final-newline/index.d.ts | 0 .../libraries}/strip-final-newline/index.js | 0 packages/core/src/logger.js | 40 + packages/core/src/manifest/libraries.js | 23 + packages/core/src/manifest/libs/auth/index.js | 61 ++ packages/core/src/manifest/libs/fs/index.js | 39 + packages/core/src/manifest/libs/index.js | 15 + .../src/manifest/libs}/mcl/authenticator.js | 0 .../core/src/manifest/libs}/mcl/handler.js | 0 packages/core/src/manifest/libs/mcl/index.js | 47 + .../core/src/manifest/libs}/mcl/launcher.js | 0 packages/core/src/manifest/libs/open/index.js | 13 + packages/core/src/manifest/libs/path/index.js | 3 + packages/core/src/manifest/reader.js | 44 + packages/core/src/manifest/vm.js | 71 ++ packages/core/src/prerequisites.js | 69 ++ packages/core/src/utils/chmodRecursive.js | 16 + packages/core/src/utils/extractFile.js | 46 + packages/core/src/utils/parseStringVars.js | 21 + .../core/src}/utils/readDirRecurse.js | 0 packages/core/src/utils/resolveOs.js | 17 + .../core/src/utils/resolveRemoteBinPath.js | 15 + packages/core/src/vars.js | 35 + packages/gui/.gitignore | 41 + .../gui/dev-app-update.yml | 0 .../gui/electron-builder.yml | 0 .../gui/electron.vite.config.js | 0 packages/gui/package.json | 75 ++ .../gui/resources}/icon.ico | Bin .../gui/resources}/icon.png | Bin .../gui/resources}/icon.svg | 0 {src => packages/gui/src}/main/auth.js | 0 .../gui/src}/main/commands/apply.js | 0 .../gui/src}/main/commands/execute.js | 0 .../gui/src}/main/commands/install.js | 0 .../gui/src}/main/commands/uninstall.js | 0 .../gui/src}/main/commands/update.js | 0 .../gui/src}/main/defaults/local_db.js | 0 .../gui/src}/main/defaults/pkg_manifest.js | 0 .../gui/src}/main/generic_steps/drive.js | 0 .../gui/src}/main/generic_steps/git_clone.js | 0 .../gui/src}/main/generic_steps/git_pull.js | 0 .../gui/src}/main/generic_steps/git_reset.js | 0 .../gui/src}/main/generic_steps/http.js | 0 .../gui/src}/main/generic_steps/index.js | 0 {src => packages/gui/src}/main/index.js | 21 +- .../gui/src}/main/lib/auth/index.js | 0 packages/gui/src/main/lib/execa/index.d.ts | 955 ++++++++++++++++++ packages/gui/src/main/lib/execa/index.js | 309 ++++++ .../gui/src/main/lib/execa/lib/command.js | 119 +++ packages/gui/src/main/lib/execa/lib/error.js | 87 ++ packages/gui/src/main/lib/execa/lib/kill.js | 102 ++ packages/gui/src/main/lib/execa/lib/pipe.js | 42 + .../gui/src/main/lib/execa/lib/promise.js | 36 + packages/gui/src/main/lib/execa/lib/stdio.js | 49 + packages/gui/src/main/lib/execa/lib/stream.js | 133 +++ .../gui/src/main/lib/execa/lib/verbose.js | 19 + .../gui/src}/main/lib/execa/public_lib.js | 0 .../src/main/lib/get-stream/array-buffer.js | 84 ++ packages/gui/src/main/lib/get-stream/array.js | 32 + .../gui/src/main/lib/get-stream/buffer.js | 20 + .../gui/src/main/lib/get-stream/contents.js | 101 ++ .../gui/src/main/lib/get-stream/index.d.ts | 119 +++ packages/gui/src/main/lib/get-stream/index.js | 5 + .../src/main/lib/get-stream/index.test-d.ts | 98 ++ .../gui/src/main/lib/get-stream/string.js | 36 + packages/gui/src/main/lib/get-stream/utils.js | 11 + .../gui/src}/main/lib/google_drive/index.js | 0 .../gui/src/main/lib/human-signals/core.js | 275 +++++ .../gui/src/main/lib/human-signals/index.js | 70 ++ .../gui/src}/main/lib/human-signals/index.ts | 0 .../src/main/lib/human-signals/realtime.js | 16 + .../gui/src/main/lib/human-signals/signals.js | 34 + .../gui/src/main/lib/is-stream/index.d.ts | 81 ++ packages/gui/src/main/lib/is-stream/index.js | 29 + .../src}/main/lib/lowdb/adapters/Memory.ts | 0 .../main/lib/lowdb/adapters/node/DataFile.ts | 0 .../main/lib/lowdb/adapters/node/JSONFile.ts | 0 .../main/lib/lowdb/adapters/node/TextFile.ts | 0 .../gui/src}/main/lib/lowdb/core/Low.ts | 0 .../gui/src}/main/lib/lowdb/index.ts | 0 .../gui/src}/main/lib/lowdb/presets/node.ts | 0 .../gui/src/main/lib/mcl/authenticator.js | 167 +++ packages/gui/src/main/lib/mcl/handler.js | 783 ++++++++++++++ .../gui/src}/main/lib/mcl/index.js | 0 packages/gui/src/main/lib/mcl/launcher.js | 224 ++++ .../gui/src/main/lib/mimic-function/index.js | 71 ++ .../gui/src/main/lib/npm-run-path/index.d.ts | 84 ++ .../gui/src/main/lib/npm-run-path/index.js | 51 + packages/gui/src/main/lib/onetime/index.d.ts | 59 ++ packages/gui/src/main/lib/onetime/index.js | 41 + .../gui/src}/main/lib/public_bind.js | 0 .../gui/src}/main/lib/renderer_ipc/index.js | 0 .../gui/src}/main/lib/rfs/index.js | 0 .../gui/src}/main/lib/steno/index.ts | 0 .../main/lib/strip-final-newline/index.d.ts | 18 + .../src/main/lib/strip-final-newline/index.js | 26 + {src => packages/gui/src}/main/local_db.js | 0 {src => packages/gui/src}/main/manager.js | 0 packages/gui/src/main/prerequisites.js | 35 + {src => packages/gui/src}/main/setup.js | 1 - .../gui/src}/main/utils/extractFile.js | 0 .../gui/src}/main/utils/initManifest.js | 0 .../gui/src}/main/utils/parseStringVars.js | 0 packages/gui/src/main/utils/readDirRecurse.js | 25 + .../gui/src}/main/utils/readManifest.js | 0 .../gui/src}/main/utils/resolveJavaPath.js | 0 .../gui/src}/main/utils/sendToRender.js | 0 {src => packages/gui/src}/main/vars.js | 0 {src => packages/gui/src}/preload/index.js | 0 .../gui/src}/renderer/assets/icon.jsx | 0 .../src}/renderer/config/paths_decorators.js | 0 {src => packages/gui/src}/renderer/index.html | 0 .../gui/src}/renderer/src/App.jsx | 0 .../gui/src}/renderer/src/GlobalApp.jsx | 0 .../renderer/src/components/Icons/index.jsx | 0 .../src/components/InstallConfigAsk/index.jsx | 0 .../components/InstallConfigAsk/index.less | 0 .../src/components/ManifestInfo/index.jsx | 0 .../src/components/ManifestInfo/index.less | 0 .../src/components/NewInstallation/index.jsx | 0 .../src/components/NewInstallation/index.less | 0 .../components/PackageConfigItem/index.jsx | 0 .../src/components/PackageItem/index.jsx | 0 .../src/components/PackageItem/index.less | 0 .../PackageUpdateAvailable/index.jsx | 0 .../PackageUpdateAvailable/index.less | 0 .../gui/src}/renderer/src/contexts/global.js | 0 .../renderer/src/contexts/installations.jsx | 0 .../src/layout/components/Drawer/index.jsx | 0 .../src/layout/components/Header/index.jsx | 0 .../src/layout/components/Header/index.less | 0 .../layout/components/ModalDialog/index.jsx | 0 .../gui/src}/renderer/src/layout/index.jsx | 0 .../gui/src}/renderer/src/main.jsx | 0 .../gui/src}/renderer/src/pages/index.jsx | 0 .../gui/src}/renderer/src/pages/index.less | 0 .../src}/renderer/src/pages/pkg/[pkg_id].jsx | 0 .../src}/renderer/src/pages/pkg/index.less | 0 .../renderer/src/pages/settings/index.jsx | 0 .../renderer/src/pages/settings/index.less | 0 .../gui/src}/renderer/src/router.jsx | 0 .../gui/src}/renderer/src/settings_list.jsx | 0 .../gui/src}/renderer/src/style/fix.less | 0 .../gui/src}/renderer/src/style/index.less | 0 .../gui/src}/renderer/src/style/reset.css | 0 .../gui/src}/renderer/src/style/vars.less | 0 .../renderer/src/utils/getRootCssVar/index.js | 0 .../renderer/src/utils/getVersions/index.js | 0 scripts/postinstall.js | 35 + 214 files changed, 7061 insertions(+), 106 deletions(-) delete mode 100644 .prettierignore delete mode 100644 .prettierrc.yaml delete mode 100644 TODO.md create mode 100755 packages/cli/bin create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/index.js create mode 100644 packages/core/package.json create mode 100644 packages/core/src/classes/ManifestConfig.js create mode 100644 packages/core/src/classes/PatchManager.js create mode 100644 packages/core/src/db.js create mode 100644 packages/core/src/generic_steps/git_clone.js create mode 100644 packages/core/src/generic_steps/git_pull.js create mode 100644 packages/core/src/generic_steps/git_reset.js create mode 100644 packages/core/src/generic_steps/http.js create mode 100644 packages/core/src/generic_steps/index.js create mode 100644 packages/core/src/handlers/apply.js create mode 100644 packages/core/src/handlers/execute.js create mode 100644 packages/core/src/handlers/install.js create mode 100644 packages/core/src/handlers/list.js create mode 100644 packages/core/src/handlers/uninstall.js create mode 100644 packages/core/src/handlers/update.js create mode 100644 packages/core/src/helpers/downloadHttpFile.js create mode 100644 packages/core/src/helpers/sendToRender.js create mode 100644 packages/core/src/helpers/setup.js create mode 100644 packages/core/src/index.js rename {src/main/lib => packages/core/src/libraries}/execa/index.d.ts (100%) rename {src/main/lib => packages/core/src/libraries}/execa/index.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/command.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/error.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/kill.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/pipe.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/promise.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/stdio.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/stream.js (100%) rename {src/main/lib => packages/core/src/libraries}/execa/lib/verbose.js (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/array-buffer.js (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/array.js (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/buffer.js (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/contents.js (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/index.d.ts (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/index.js (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/index.test-d.ts (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/string.js (100%) rename {src/main/lib => packages/core/src/libraries}/get-stream/utils.js (100%) rename {src/main/lib => packages/core/src/libraries}/human-signals/core.js (100%) rename {src/main/lib => packages/core/src/libraries}/human-signals/index.js (100%) rename {src/main/lib => packages/core/src/libraries}/human-signals/realtime.js (100%) rename {src/main/lib => packages/core/src/libraries}/human-signals/signals.js (100%) rename {src/main/lib => packages/core/src/libraries}/is-stream/index.d.ts (100%) rename {src/main/lib => packages/core/src/libraries}/is-stream/index.js (100%) create mode 100644 packages/core/src/libraries/lowdb/adapters/Memory.js create mode 100644 packages/core/src/libraries/lowdb/adapters/node/DataFile.js create mode 100644 packages/core/src/libraries/lowdb/adapters/node/JSONFile.js create mode 100644 packages/core/src/libraries/lowdb/adapters/node/TextFile.js create mode 100644 packages/core/src/libraries/lowdb/core/Low.js create mode 100644 packages/core/src/libraries/lowdb/presets/node.js create mode 100644 packages/core/src/libraries/lowdb/steno/index.js rename {src/main/lib => packages/core/src/libraries}/mimic-function/index.js (100%) rename {src/main/lib => packages/core/src/libraries}/npm-run-path/index.d.ts (100%) rename {src/main/lib => packages/core/src/libraries}/npm-run-path/index.js (100%) rename {src/main/lib => packages/core/src/libraries}/onetime/index.d.ts (100%) rename {src/main/lib => packages/core/src/libraries}/onetime/index.js (100%) rename {src/main/lib => packages/core/src/libraries}/strip-final-newline/index.d.ts (100%) rename {src/main/lib => packages/core/src/libraries}/strip-final-newline/index.js (100%) create mode 100644 packages/core/src/logger.js create mode 100644 packages/core/src/manifest/libraries.js create mode 100644 packages/core/src/manifest/libs/auth/index.js create mode 100644 packages/core/src/manifest/libs/fs/index.js create mode 100644 packages/core/src/manifest/libs/index.js rename {src/main/lib => packages/core/src/manifest/libs}/mcl/authenticator.js (100%) rename {src/main/lib => packages/core/src/manifest/libs}/mcl/handler.js (100%) create mode 100644 packages/core/src/manifest/libs/mcl/index.js rename {src/main/lib => packages/core/src/manifest/libs}/mcl/launcher.js (100%) create mode 100644 packages/core/src/manifest/libs/open/index.js create mode 100644 packages/core/src/manifest/libs/path/index.js create mode 100644 packages/core/src/manifest/reader.js create mode 100644 packages/core/src/manifest/vm.js create mode 100644 packages/core/src/prerequisites.js create mode 100644 packages/core/src/utils/chmodRecursive.js create mode 100644 packages/core/src/utils/extractFile.js create mode 100644 packages/core/src/utils/parseStringVars.js rename {src/main => packages/core/src}/utils/readDirRecurse.js (100%) create mode 100644 packages/core/src/utils/resolveOs.js create mode 100644 packages/core/src/utils/resolveRemoteBinPath.js create mode 100644 packages/core/src/vars.js create mode 100644 packages/gui/.gitignore rename dev-app-update.yml => packages/gui/dev-app-update.yml (100%) rename electron-builder.yml => packages/gui/electron-builder.yml (100%) rename electron.vite.config.js => packages/gui/electron.vite.config.js (100%) create mode 100644 packages/gui/package.json rename {resources => packages/gui/resources}/icon.ico (100%) rename {resources => packages/gui/resources}/icon.png (100%) rename {resources => packages/gui/resources}/icon.svg (100%) rename {src => packages/gui/src}/main/auth.js (100%) rename {src => packages/gui/src}/main/commands/apply.js (100%) rename {src => packages/gui/src}/main/commands/execute.js (100%) rename {src => packages/gui/src}/main/commands/install.js (100%) rename {src => packages/gui/src}/main/commands/uninstall.js (100%) rename {src => packages/gui/src}/main/commands/update.js (100%) rename {src => packages/gui/src}/main/defaults/local_db.js (100%) rename {src => packages/gui/src}/main/defaults/pkg_manifest.js (100%) rename {src => packages/gui/src}/main/generic_steps/drive.js (100%) rename {src => packages/gui/src}/main/generic_steps/git_clone.js (100%) rename {src => packages/gui/src}/main/generic_steps/git_pull.js (100%) rename {src => packages/gui/src}/main/generic_steps/git_reset.js (100%) rename {src => packages/gui/src}/main/generic_steps/http.js (100%) rename {src => packages/gui/src}/main/generic_steps/index.js (100%) rename {src => packages/gui/src}/main/index.js (95%) rename {src => packages/gui/src}/main/lib/auth/index.js (100%) create mode 100755 packages/gui/src/main/lib/execa/index.d.ts create mode 100755 packages/gui/src/main/lib/execa/index.js create mode 100755 packages/gui/src/main/lib/execa/lib/command.js create mode 100755 packages/gui/src/main/lib/execa/lib/error.js create mode 100755 packages/gui/src/main/lib/execa/lib/kill.js create mode 100755 packages/gui/src/main/lib/execa/lib/pipe.js create mode 100755 packages/gui/src/main/lib/execa/lib/promise.js create mode 100755 packages/gui/src/main/lib/execa/lib/stdio.js create mode 100755 packages/gui/src/main/lib/execa/lib/stream.js create mode 100755 packages/gui/src/main/lib/execa/lib/verbose.js rename {src => packages/gui/src}/main/lib/execa/public_lib.js (100%) create mode 100644 packages/gui/src/main/lib/get-stream/array-buffer.js create mode 100644 packages/gui/src/main/lib/get-stream/array.js create mode 100644 packages/gui/src/main/lib/get-stream/buffer.js create mode 100644 packages/gui/src/main/lib/get-stream/contents.js create mode 100644 packages/gui/src/main/lib/get-stream/index.d.ts create mode 100644 packages/gui/src/main/lib/get-stream/index.js create mode 100644 packages/gui/src/main/lib/get-stream/index.test-d.ts create mode 100644 packages/gui/src/main/lib/get-stream/string.js create mode 100644 packages/gui/src/main/lib/get-stream/utils.js rename {src => packages/gui/src}/main/lib/google_drive/index.js (100%) create mode 100644 packages/gui/src/main/lib/human-signals/core.js create mode 100644 packages/gui/src/main/lib/human-signals/index.js rename {src => packages/gui/src}/main/lib/human-signals/index.ts (100%) create mode 100644 packages/gui/src/main/lib/human-signals/realtime.js create mode 100644 packages/gui/src/main/lib/human-signals/signals.js create mode 100644 packages/gui/src/main/lib/is-stream/index.d.ts create mode 100644 packages/gui/src/main/lib/is-stream/index.js rename {src => packages/gui/src}/main/lib/lowdb/adapters/Memory.ts (100%) rename {src => packages/gui/src}/main/lib/lowdb/adapters/node/DataFile.ts (100%) rename {src => packages/gui/src}/main/lib/lowdb/adapters/node/JSONFile.ts (100%) rename {src => packages/gui/src}/main/lib/lowdb/adapters/node/TextFile.ts (100%) rename {src => packages/gui/src}/main/lib/lowdb/core/Low.ts (100%) rename {src => packages/gui/src}/main/lib/lowdb/index.ts (100%) rename {src => packages/gui/src}/main/lib/lowdb/presets/node.ts (100%) create mode 100644 packages/gui/src/main/lib/mcl/authenticator.js create mode 100644 packages/gui/src/main/lib/mcl/handler.js rename {src => packages/gui/src}/main/lib/mcl/index.js (100%) create mode 100644 packages/gui/src/main/lib/mcl/launcher.js create mode 100644 packages/gui/src/main/lib/mimic-function/index.js create mode 100644 packages/gui/src/main/lib/npm-run-path/index.d.ts create mode 100644 packages/gui/src/main/lib/npm-run-path/index.js create mode 100644 packages/gui/src/main/lib/onetime/index.d.ts create mode 100644 packages/gui/src/main/lib/onetime/index.js rename {src => packages/gui/src}/main/lib/public_bind.js (100%) rename {src => packages/gui/src}/main/lib/renderer_ipc/index.js (100%) rename {src => packages/gui/src}/main/lib/rfs/index.js (100%) rename {src => packages/gui/src}/main/lib/steno/index.ts (100%) create mode 100644 packages/gui/src/main/lib/strip-final-newline/index.d.ts create mode 100644 packages/gui/src/main/lib/strip-final-newline/index.js rename {src => packages/gui/src}/main/local_db.js (100%) rename {src => packages/gui/src}/main/manager.js (100%) create mode 100644 packages/gui/src/main/prerequisites.js rename {src => packages/gui/src}/main/setup.js (99%) rename {src => packages/gui/src}/main/utils/extractFile.js (100%) rename {src => packages/gui/src}/main/utils/initManifest.js (100%) rename {src => packages/gui/src}/main/utils/parseStringVars.js (100%) create mode 100644 packages/gui/src/main/utils/readDirRecurse.js rename {src => packages/gui/src}/main/utils/readManifest.js (100%) rename {src => packages/gui/src}/main/utils/resolveJavaPath.js (100%) rename {src => packages/gui/src}/main/utils/sendToRender.js (100%) rename {src => packages/gui/src}/main/vars.js (100%) rename {src => packages/gui/src}/preload/index.js (100%) rename {src => packages/gui/src}/renderer/assets/icon.jsx (100%) rename {src => packages/gui/src}/renderer/config/paths_decorators.js (100%) rename {src => packages/gui/src}/renderer/index.html (100%) rename {src => packages/gui/src}/renderer/src/App.jsx (100%) rename {src => packages/gui/src}/renderer/src/GlobalApp.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/Icons/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/InstallConfigAsk/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/InstallConfigAsk/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/ManifestInfo/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/ManifestInfo/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/NewInstallation/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/NewInstallation/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/PackageConfigItem/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/PackageItem/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/PackageItem/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/PackageUpdateAvailable/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/PackageUpdateAvailable/index.less (100%) rename {src => packages/gui/src}/renderer/src/contexts/global.js (100%) rename {src => packages/gui/src}/renderer/src/contexts/installations.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/components/Drawer/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/components/Header/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/components/Header/index.less (100%) rename {src => packages/gui/src}/renderer/src/layout/components/ModalDialog/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/main.jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/index.less (100%) rename {src => packages/gui/src}/renderer/src/pages/pkg/[pkg_id].jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/pkg/index.less (100%) rename {src => packages/gui/src}/renderer/src/pages/settings/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/settings/index.less (100%) rename {src => packages/gui/src}/renderer/src/router.jsx (100%) rename {src => packages/gui/src}/renderer/src/settings_list.jsx (100%) rename {src => packages/gui/src}/renderer/src/style/fix.less (100%) rename {src => packages/gui/src}/renderer/src/style/index.less (100%) rename {src => packages/gui/src}/renderer/src/style/reset.css (100%) rename {src => packages/gui/src}/renderer/src/style/vars.less (100%) rename {src => packages/gui/src}/renderer/src/utils/getRootCssVar/index.js (100%) rename {src => packages/gui/src}/renderer/src/utils/getVersions/index.js (100%) create mode 100644 scripts/postinstall.js diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 9c6b791..0000000 --- a/.prettierignore +++ /dev/null @@ -1,6 +0,0 @@ -out -dist -pnpm-lock.yaml -LICENSE.md -tsconfig.json -tsconfig.*.json diff --git a/.prettierrc.yaml b/.prettierrc.yaml deleted file mode 100644 index f99263a..0000000 --- a/.prettierrc.yaml +++ /dev/null @@ -1,4 +0,0 @@ -singleQuote: false -semi: false -printWidth: 100 -trailingComma: none diff --git a/.vscode/settings.json b/.vscode/settings.json index e879dfd..2d9dc4b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,16 @@ }, "[json]": { "editor.defaultFormatter": "esbenp.prettier-vscode" - } + }, + "cSpell.words": [ + "admzip", + "APPDATA", + "catched", + "execa", + "rclone", + "sevenzip", + "unzipper", + "upath", + "userdata" + ] } diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 2bf4f29..0000000 --- a/TODO.md +++ /dev/null @@ -1,9 +0,0 @@ -[] auto install java on setup -[x] support install ask configs -[] DEVLOGS -[] improve package last task view (statusText) -[] show git clone status - -[] fix app update modal -[] fix update removes "options.txt" -[] improve child process on management \ No newline at end of file diff --git a/package.json b/package.json index b32085a..674dfc8 100644 --- a/package.json +++ b/package.json @@ -1,75 +1,13 @@ { - "name": "rs-bundler", - "version": "0.15.0", - "description": "RageStudio Bundler Utility GUI", - "main": "./out/main/index.js", - "author": "RageStudio", + "name": "@ragestudio/relic", + "private": true, + "workspaces": [ + "packages/*" + ], + "repository": "https://github.com/srgooglo/rs_bundler", + "author": "SrGooglo ", "license": "MIT", "scripts": { - "format": "prettier --write .", - "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", - "start": "electron-vite preview", - "dev": "electron-vite dev", - "build": "electron-vite build", - "postinstall": "electron-builder install-app-deps", - "pack:win": "electron-builder --win --config", - "pack:mac": "electron-builder --mac --config", - "pack:linux": "electron-builder --linux --config", - "build:win": "npm run build && npm run pack:win", - "build:mac": "npm run build && npm run pack:mac", - "build:linux": "npm run build && npm run pack:linux" - }, - "dependencies": { - "@electron-toolkit/preload": "^2.0.0", - "@electron-toolkit/utils": "^2.0.0", - "@getstation/electron-google-oauth2": "^14.0.0", - "@imjs/electron-differential-updater": "^5.1.7", - "@loadable/component": "^5.16.3", - "@ragestudio/hermes": "^0.1.1", - "adm-zip": "^0.5.10", - "antd": "^5.13.2", - "checksum": "^1.0.0", - "classnames": "^2.3.2", - "electron-differential-updater": "^4.3.2", - "electron-is-dev": "^2.0.0", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.1", - "googleapis": "^105.0.0", - "got": "11.8.3", - "human-format": "^1.2.0", - "less": "^4.2.0", - "lodash": "^4.17.21", - "merge-stream": "^2.0.0", - "node-7z": "^3.0.0", - "open": "8.4.2", - "progress-stream": "^2.0.0", - "protocol-registry": "^1.4.1", - "react-icons": "^4.11.0", - "react-router-dom": "6.6.2", - "react-spinners": "^0.13.8", - "react-spring": "^9.7.3", - "react-motion": "0.5.2", - "request": "^2.88.2", - "rimraf": "^5.0.5", - "signal-exit": "^4.1.0", - "unzipper": "^0.10.14", - "upath": "^2.0.1", - "uuid": "^9.0.1", - "which": "^4.0.0", - "winreg": "^1.2.5" - }, - "devDependencies": { - "@electron-toolkit/eslint-config": "^1.0.1", - "@electron-toolkit/eslint-config-prettier": "^1.0.1", - "@vitejs/plugin-react": "^4.0.4", - "electron": "^25.6.0", - "electron-builder": "^24.6.3", - "electron-vite": "^1.0.27", - "eslint": "^8.47.0", - "eslint-plugin-react": "^7.33.2", - "prettier": "^3.0.2", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "vite": "^4.4.9" + "postinstall": "node scripts/postinstall.js" } } diff --git a/packages/cli/bin b/packages/cli/bin new file mode 100755 index 0000000..e1b3d5a --- /dev/null +++ b/packages/cli/bin @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require("./dist/index.js") \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..5b6c5fc --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,24 @@ +{ + "name": "@ragestudio/relic-cli", + "version": "0.16.0", + "license": "MIT", + "author": "RageStudio", + "description": "RageStudio Relic, yet another package manager.", + "main": "./dist/index.js", + "bin": { + "relic": "./bin.js" + }, + "scripts": { + "dev": "hermes-node ./src/index.js", + "build": "hermes build" + }, + "dependencies": { + "commander": "^12.0.0", + "glob": "^10.3.12", + "lowdb": "^7.0.1", + "yargs": "^17.7.2" + }, + "devDependencies": { + "@ragestudio/hermes": "^0.1.1" + } +} diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js new file mode 100644 index 0000000..2a05234 --- /dev/null +++ b/packages/cli/src/index.js @@ -0,0 +1,166 @@ +import RelicCore from "@ragestudio/relic-core" +import { program, Command, Argument } from "commander" + +import pkg from "../package.json" + +const commands = [ + { + cmd: "install", + description: "Install a package manifest from a path or URL", + arguments: [ + { + name: "package_manifest", + description: "Path or URL to a package manifest", + } + ], + fn: async (package_manifest, options) => { + await core.initialize() + + return await core.package.install(package_manifest, options) + } + }, + { + cmd: "run", + description: "Execute a package", + arguments: [ + { + name: "id", + description: "The id of the package to execute", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.execute(pkg_id, options) + } + }, + { + cmd: "update", + description: "Update a package", + arguments: [ + { + name: "id", + description: "The id of the package to update", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.update(pkg_id, options) + } + }, + { + cmd: "uninstall", + description: "Uninstall a package", + arguments: [ + { + name: "id", + description: "The id of the package to uninstall", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.uninstall(pkg_id, options) + } + }, + { + cmd: "apply", + description: "Apply changes to a installed package", + arguments: [ + { + name: "id", + description: "The id of the package to apply changes to", + }, + ], + options: [ + { + name: "add_patches", + description: "Add patches to the package", + }, + { + name: "remove_patches", + description: "Remove patches from the package", + }, + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.apply(pkg_id, options) + } + }, + { + cmd: "list", + description: "List installed package manifests", + fn: async () => { + await core.initialize() + + return console.log(await core.package.list()) + } + }, + { + cmd: "open-path", + description: "Open the base path or a package path", + options: [ + { + name: "pkg_id", + description: "Path to open", + } + ], + fn: async (options) => { + await core.initialize() + + await core.openPath(options.pkg_id) + } + } +] + +async function main() { + global.core = new RelicCore() + + program + .name(pkg.name) + .description(pkg.description) + .version(pkg.version) + + for await (const command of commands) { + const cmd = new Command(command.cmd).action(command.fn) + + if (command.description) { + cmd.description(command.description) + } + + if (Array.isArray(command.arguments)) { + for await (const argument of command.arguments) { + if (typeof argument === "string") { + cmd.addArgument(new Argument(argument)) + } else { + const arg = new Argument(argument.name, argument.description) + + if (argument.default) { + arg.default(argument.default) + } + + cmd.addArgument(arg) + } + } + } + + if (Array.isArray(command.options)) { + for await (const option of command.options) { + if (typeof option === "string") { + cmd.option(option) + } else { + cmd.option(option.name, option.description, option.default) + } + } + } + + program.addCommand(cmd) + } + + program.parse() +} + + +main() \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..73d8369 --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,38 @@ +{ + "name": "@ragestudio/relic-core", + "version": "0.16.0", + "license": "MIT", + "author": "RageStudio", + "description": "RageStudio Relic, yet another package manager.", + "main": "./dist/index.js", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "hermes build" + }, + "dependencies": { + "@foxify/events": "^2.1.0", + "adm-zip": "^0.5.12", + "axios": "^1.6.8", + "checksum": "^1.0.0", + "cli-color": "^2.0.4", + "cli-progress": "^3.12.0", + "extends-classes": "^1.0.5", + "googleapis": "^134.0.0", + "human-format": "^1.2.0", + "isolated-vm": "^4.7.2", + "merge-stream": "^2.0.0", + "module-alias": "^2.2.3", + "node-7z": "^3.0.0", + "open": "8.4.2", + "progress-stream": "^2.0.0", + "request": "^2.88.2", + "rimraf": "^5.0.5", + "unzipper": "^0.10.14", + "upath": "^2.0.1", + "uuid": "^9.0.1", + "winston": "^3.13.0" + } +} diff --git a/packages/core/src/classes/ManifestConfig.js b/packages/core/src/classes/ManifestConfig.js new file mode 100644 index 0000000..7bc379b --- /dev/null +++ b/packages/core/src/classes/ManifestConfig.js @@ -0,0 +1,34 @@ +import DB from "../db" + +export default class ManifestConfigManager { + constructor(pkg_id) { + this.pkg_id = pkg_id + this.config = null + } + + async initialize() { + const pkg = await DB.getPackages(this.pkg_id) ?? {} + + this.config = pkg.config + } + + set(key, value) { + this.config[key] = value + + DB.updatePackageById(pkg_id, { config: this.config }) + + return this.config + } + + get(key) { + return this.config[key] + } + + delete(key) { + delete this.config[key] + + DB.updatePackageById(pkg_id, { config: this.config }) + + return this.config + } +} \ No newline at end of file diff --git a/packages/core/src/classes/PatchManager.js b/packages/core/src/classes/PatchManager.js new file mode 100644 index 0000000..0084eb9 --- /dev/null +++ b/packages/core/src/classes/PatchManager.js @@ -0,0 +1,117 @@ +import fs from "node:fs" + +import GenericSteps from "../generic_steps" +import parseStringVars from "../utils/parseStringVars" + +export default class PatchManager { + constructor(pkg, manifest) { + this.pkg = pkg + this.manifest = manifest + + this.log = Logger.child({ service: `PATCH-MANAGER|${pkg.id}` }) + } + + async get(patch) { + if (!this.manifest.patches) { + return [] + } + + let list = [] + + if (typeof patch === "undefined") { + list = this.manifest.patches + } else { + list = this.manifest.patches.find((p) => p.id === patch.id) + } + + return list + } + + async patch(patch) { + const list = await this.get(patch) + + for await (let patch of list) { + global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + status_text: `Applying patch [${patch.id}]...`, + }) + + this.log.info(`Applying patch [${patch.id}]...`) + + if (Array.isArray(patch.additions)) { + this.log.info(`Applying ${patch.additions.length} Additions...`) + + for await (let addition of patch.additions) { + this.log.info(`Applying addition [${addition.id}]...`) + + global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + status_text: `Applying addition [${additions.id}]...`, + }) + + // resolve patch file + addition.file = await parseStringVars(addition.file, this.pkg) + + if (fs.existsSync(addition.file)) { + this.log.info(`Addition [${addition.file}] already exists. Skipping...`) + continue + } + + await GenericSteps(this.pkg, addition.steps, this.log) + } + } + + pkg.applied_patches.push(patch.id) + } + + global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + status_text: `${list.length} Patches applied`, + }) + + this.log.info(`${list.length} Patches applied`) + + return this.pkg + } + + async remove(patch) { + const list = await this.get(patch) + + for await (let patch of list) { + global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + status_text: `Removing patch [${patch.id}]...`, + }) + + Log.info(`Removing patch [${patch.id}]...`) + + if (Array.isArray(patch.additions)) { + this.log.info(`Removing ${patch.additions.length} Additions...`) + + for await (let addition of patch.additions) { + this.log.info(`Removing addition [${addition.id}]...`) + + global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + status_text: `Removing addition [${additions.id}]...`, + }) + + addition.file = await parseStringVars(addition.file, this.pkg) + + if (!fs.existsSync(addition.file)) { + continue + } + + await fs.promises.unlink(addition.file) + } + } + + pkg.applied_patches = pkg.applied_patches.filter((p) => { + return p !== patch.id + }) + } + + global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + status_text: `${list.length} Patches removed`, + }) + + this.log.info(`${list.length} Patches removed`) + + return this.pkg + } +} \ No newline at end of file diff --git a/packages/core/src/db.js b/packages/core/src/db.js new file mode 100644 index 0000000..6d736a0 --- /dev/null +++ b/packages/core/src/db.js @@ -0,0 +1,113 @@ +import { JSONFilePreset } from "./libraries/lowdb/presets/node" +import Vars from "./vars" +import pkg from "../package.json" +import fs from "node:fs" +import lodash from "lodash" + +export default class DB { + static get defaultRoot() { + return { + created_at_version: pkg.version, + packages: [], + } + } + + static defaultPackageState({ + id, + name, + version, + install_path, + description, + license, + last_status, + remote_manifest, + local_manifest, + config, + }) { + return { + id: id, + name: name, + version: version, + install_path: install_path, + description: description, + license: license ?? "unlicensed", + local_manifest: local_manifest ?? null, + remote_manifest: remote_manifest ?? null, + applied_patches: [], + config: typeof config === "object" ? config : {}, + last_status: last_status ?? "installing", + last_update: null, + installed_at: null, + } + } + + static async withDB() { + return await JSONFilePreset(Vars.db_path, DB.defaultRoot) + } + + static async initialize() { + await this.cleanOrphans() + } + + static async cleanOrphans() { + const list = await this.getPackages() + + for (const pkg of list) { + if (!fs.existsSync(pkg.install_path)) { + await this.deletePackage(pkg.id) + } + } + } + + static async getPackages(pkg_id) { + const db = await this.withDB() + + if (pkg_id) { + return db.data["packages"].find((i) => i.id === pkg_id) + } + + return db.data["packages"] + } + + static async writePackage(pkg) { + const db = await this.withDB() + + await db.update((data) => { + const prevIndex = data["packages"].findIndex((i) => i.id === pkg.id) + + if (prevIndex !== -1) { + data["packages"][prevIndex] = pkg + } else { + data["packages"].push(pkg) + } + + return data + }) + + return pkg + } + + static async updatePackageById(pkg_id, obj) { + const pkg = await this.getPackages(pkg_id) + + if (!pkg) { + throw new Error("Package not found") + } + + pkg = lodash.merge(pkg, obj) + + return await this.writePackage(pkg) + } + + static async deletePackage(pkg_id) { + const db = await this.withDB() + + await db.update((data) => { + data["packages"] = data["packages"].filter((i) => i.id !== pkg_id) + + return data + }) + + return pkg_id + } +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_clone.js b/packages/core/src/generic_steps/git_clone.js new file mode 100644 index 0000000..574e953 --- /dev/null +++ b/packages/core/src/generic_steps/git_clone.js @@ -0,0 +1,46 @@ +import path from "node:path" +import fs from "node:fs" +import upath from "upath" +import { execa } from "../libraries/execa" + +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + const final_path = upath.normalizeSafe(path.resolve(pkg.install_path, step.path)) + + if (!fs.existsSync(final_path)) { + fs.mkdirSync(final_path, { recursive: true }) + } + + Log.info(`Cloning from [${step.url}]`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Cloning from [${step.url}]...`, + }) + + const args = [ + "clone", + //`--depth ${step.depth ?? 1}`, + //"--filter=blob:none", + //"--filter=tree:0", + "--recurse-submodules", + "--remote-submodules", + step.url, + final_path, + ] + + await execa(gitCMD, args, { + cwd: final_path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_pull.js b/packages/core/src/generic_steps/git_pull.js new file mode 100644 index 0000000..e4b60cb --- /dev/null +++ b/packages/core/src/generic_steps/git_pull.js @@ -0,0 +1,30 @@ +import path from "node:path" +import fs from "node:fs" +import { execa } from "../libraries/execa" + +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + const _path = path.resolve(pkg.install_path, step.path) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Pulling...`, + }) + + Log.info(`Pulling from HEAD...`) + + await execa(gitCMD, ["pull", "--rebase"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_reset.js b/packages/core/src/generic_steps/git_reset.js new file mode 100644 index 0000000..bb78a8f --- /dev/null +++ b/packages/core/src/generic_steps/git_reset.js @@ -0,0 +1,77 @@ +import path from "node:path" +import fs from "node:fs" +import { execa } from "../libraries/execa" + +import git_pull from "./git_pull" +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + + const _path = path.resolve(pkg.install_path, step.path) + const from = step.from ?? "HEAD" + + if (!fs.existsSync(_path)) { + fs.mkdirSync(_path, { recursive: true }) + } + + Log.info(`Fetching from origin`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Fetching from origin...`, + }) + + // fetch from origin + await execa(gitCMD, ["fetch", "origin"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + Log.info(`Cleaning untracked files...`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Cleaning untracked files...`, + }) + + await execa(gitCMD, ["clean", "-df"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + Log.info(`Resetting to ${from}`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Resetting to ${from}`, + }) + + await execa(gitCMD, ["reset", "--hard", from], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + // pull the latest + await git_pull(pkg, step) + + Log.info(`Checkout to HEAD`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Checkout to HEAD`, + }) + + await execa(gitCMD, ["checkout", "HEAD"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/http.js b/packages/core/src/generic_steps/http.js new file mode 100644 index 0000000..ade7830 --- /dev/null +++ b/packages/core/src/generic_steps/http.js @@ -0,0 +1,62 @@ +import path from "node:path" +import fs from "node:fs" +import os from "node:os" + +import downloadHttpFile from "../helpers/downloadHttpFile" +import parseStringVars from "../utils/parseStringVars" +import extractFile from "../utils/extractFile" + +export default async (pkg, step, logger) => { + if (!step.path) { + step.path = `./${path.basename(step.url)}` + } + + step.path = await parseStringVars(step.path, pkg) + + let _path = path.resolve(pkg.install_path, step.path) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status: "loading", + statusText: `Downloading [${step.url}]`, + }) + + logger.info(`Downloading [${step.url} to ${_path}]`) + + if (step.tmp) { + _path = path.resolve(os.tmpdir(), String(new Date().getTime()), path.basename(step.url)) + } + + fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) + + await downloadHttpFile(step.url, _path, (progress) => { + global._relic_eventBus(`pkg:update:state:${pkg.id}`, { + statusText: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, + }) + }) + + logger.info(`Downloaded finished.`) + + if (step.extract) { + if (typeof step.extract === "string") { + step.extract = path.resolve(pkg.install_path, step.extract) + } else { + step.extract = path.resolve(pkg.install_path, ".") + } + + global._relic_eventBus(`pkg:update:state:${pkg.id}`, { + statusText: `Extracting bundle...`, + }) + + await extractFile(_path, step.extract) + + if (step.deleteAfterExtract !== false) { + logger.info(`Deleting temporal file [${_path}]...`) + + global._relic_eventBus(`pkg:update:state:${pkg.id}`, { + statusText: `Deleting temporal files...`, + }) + + await fs.promises.rm(_path, { recursive: true }) + } + } +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/index.js b/packages/core/src/generic_steps/index.js new file mode 100644 index 0000000..714ce0a --- /dev/null +++ b/packages/core/src/generic_steps/index.js @@ -0,0 +1,42 @@ +import ISM_GIT_CLONE from "./git_clone" +import ISM_GIT_PULL from "./git_pull" +import ISM_GIT_RESET from "./git_reset" +import ISM_HTTP from "./http" + +const InstallationStepsMethods = { + git_clone: ISM_GIT_CLONE, + git_pull: ISM_GIT_PULL, + git_reset: ISM_GIT_RESET, + http_file: ISM_HTTP, +} + +const StepsOrders = [ + "git_clones", + "git_pull", + "git_reset", + "http_file", +] + +export default async function processGenericSteps(pkg, steps, logger = Logger) { + logger.info(`Processing generic steps...`) + + if (steps.length === 0) { + return pkg + } + + steps = steps.sort((a, b) => { + return StepsOrders.indexOf(a.type) - StepsOrders.indexOf(b.type) + }) + + for await (let step of steps) { + step.type = step.type.toLowerCase() + + if (!InstallationStepsMethods[step.type]) { + throw new Error(`Unknown step: ${step.type}`) + } + + await InstallationStepsMethods[step.type](pkg, step, logger) + } + + return pkg +} diff --git a/packages/core/src/handlers/apply.js b/packages/core/src/handlers/apply.js new file mode 100644 index 0000000..3d41d97 --- /dev/null +++ b/packages/core/src/handlers/apply.js @@ -0,0 +1,82 @@ +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import DB from "../db" + +const BaseLog = Logger.child({ service: "APPLIER" }) + +function findPatch(manifest, changes, mustBeInstalled) { + return manifest.patches + .filter((patch) => { + const patchID = patch.id + + if (typeof changes.patches[patchID] === "undefined") { + return false + } + + if (mustBeInstalled === true && !manifest.applied_patches.includes(patch.id) && changes.patches[patchID] === true) { + return true + } + + if (mustBeInstalled === false && manifest.applied_patches.includes(patch.id) && changes.patches[patchID] === false) { + return true + } + + return false + }) +} + +export default async function apply(pkg_id, changes = {}) { + try { + let pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.error(`Package not found [${pkg_id}]`) + return null + } + + let manifest = await ManifestReader(pkg.local_manifest) + manifest = await ManifestVM(ManifestRead.code) + + const Log = Logger.child({ service: `APPLIER|${pkg.id}` }) + + Log.info(`Applying changes to package...`) + + if (changes.patches) { + if (!Array.isArray(pkg.applied_patches)) { + pkg.applied_patches = [] + } + + const patches = new PatchManager(pkg, manifest) + + await patches.remove(findPatch(manifest, changes, false)) + await patches.patch(findPatch(manifest, changes, true)) + } + + if (changes.config) { + Log.info(`Applying config to package...`) + + if (Object.keys(changes.config).length !== 0) { + Object.entries(changes.config).forEach(([key, value]) => { + pkg.config[key] = value + }) + } + } + + await DB.writePackage(pkg) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + state: "All changes applied", + }) + + Log.info(`All changes applied to package.`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + + BaseLog.error(`Failed to apply changes to package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/execute.js b/packages/core/src/handlers/execute.js new file mode 100644 index 0000000..039af00 --- /dev/null +++ b/packages/core/src/handlers/execute.js @@ -0,0 +1,64 @@ +import fs from "node:fs" + +import DB from "../db" +import SetupHelper from "../helpers/setup" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import parseStringVars from "../utils/parseStringVars" +import { execa } from "../libraries/execa" + +const BaseLog = Logger.child({ service: "EXECUTER" }) + +export default async function execute(pkg_id, { useRemote = false, force = false } = {}) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.info(`Package not found [${pkg_id}]`) + return false + } + + await SetupHelper() + + const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest + + if (!fs.existsSync(manifestPath)) { + BaseLog.error(`Manifest not found in expected path [${manifestPath}] + \nMaybe the package installation has not been completed yet or corrupted. + `) + + return false + } + + const ManifestRead = await ManifestReader(manifestPath) + + const manifest = await ManifestVM(ManifestRead.code) + + if (typeof manifest.execute === "function") { + await manifest.execute(pkg) + } + + if (typeof manifest.execute === "string") { + manifest.execute = parseStringVars(manifest.execute, pkg) + + BaseLog.info(`Executing binary > [${manifest.execute}]`) + + const args = Array.isArray(manifest.execute_args) ? manifest.execute_args : [] + + await execa(manifest.execute, args, { + cwd: pkg.install_path, + stdout: "inherit", + stderr: "inherit", + }) + } + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + + BaseLog.error(`Failed to execute package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} diff --git a/packages/core/src/handlers/install.js b/packages/core/src/handlers/install.js new file mode 100644 index 0000000..4efd4a9 --- /dev/null +++ b/packages/core/src/handlers/install.js @@ -0,0 +1,163 @@ +import fs from "node:fs" + +import DB from "../db" +import SetupHelper from "../helpers/setup" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import GenericSteps from "../generic_steps" +import Apply from "../handlers/apply" + +const BaseLog = Logger.child({ service: "INSTALLER" }) + +export default async function install(manifest) { + let id = null + + try { + await SetupHelper() + + BaseLog.info(`Invoking new installation...`) + BaseLog.info(`Fetching manifest [${manifest}]`) + + const ManifestRead = await ManifestReader(manifest) + + manifest = await ManifestVM(ManifestRead.code) + + id = manifest.constructor.id + + const Log = BaseLog.child({ service: `INSTALLER|${id}` }) + + Log.info(`Creating install path [${manifest.install_path}]`) + + if (fs.existsSync(manifest.install_path)) { + Log.info(`Package already exists, removing...`) + await fs.rmSync(manifest.install_path, { recursive: true }) + } + + await fs.mkdirSync(manifest.install_path, { recursive: true }) + + Log.info(`Initializing manifest...`) + + if (typeof manifest.initialize === "function") { + await manifest.initialize() + } + + Log.info(`Appending to db...`) + + const pkg = DB.defaultPackageState({ + id: id, + name: manifest.constructor.pkg_name, + version: manifest.constructor.version, + install_path: manifest.install_path, + description: manifest.constructor.description, + license: manifest.constructor.license, + last_status: "installing", + remote_manifest: ManifestRead.remote_manifest, + local_manifest: ManifestRead.local_manifest, + }) + + await DB.writePackage(pkg) + + global._relic_eventBus.emit("pkg:new", pkg) + + if (manifest.configuration) { + Log.info(`Applying default config to package...`) + + pkg.config = Object.entries(manifest.configuration).reduce((acc, [key, value]) => { + acc[key] = value.default + + return acc + }, {}) + } + + if (typeof manifest.beforeInstall === "function") { + Log.info(`Executing beforeInstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state:${id}`, { + status_text: `Performing beforeInstall hook...`, + }) + + await manifest.beforeInstall(pkg) + } + + if (Array.isArray(manifest.installSteps)) { + Log.info(`Executing generic install steps...`) + + global._relic_eventBus.emit(`pkg:update:state:${id}`, { + status_text: `Performing generic install steps...`, + }) + + await GenericSteps(pkg, manifest.installSteps, Log) + } + + if (typeof manifest.afterInstall === "function") { + Log.info(`Executing afterInstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state:${id}`, { + status_text: `Performing afterInstall hook...`, + }) + + await manifest.afterInstall(pkg) + } + + global._relic_eventBus.emit(`pkg:update:state:${id}`, { + status_text: `Finishing up...`, + }) + + Log.info(`Copying manifest to the final location...`) + + const finalPath = `${manifest.install_path}/.rmanifest` + + if (fs.existsSync(finalPath)) { + await fs.promises.unlink(finalPath) + } + + await fs.promises.copyFile(ManifestRead.local_manifest, finalPath) + + if (ManifestRead.is_catched) { + Log.info(`Removing cache manifest...`) + await fs.promises.unlink(ManifestRead.local_manifest) + } + + pkg.local_manifest = finalPath + pkg.last_status = "installed" + pkg.installed_at = Date.now() + + await DB.writePackage(pkg) + + if (manifest.patches) { + const defaultPatches = manifest.patches.filter((patch) => patch.default) + + if (defaultPatches.length > 0) { + Log.info(`Applying default patches...`) + + global._relic_eventBus.emit(`pkg:update:state:${id}`, { + status_text: `Applying default patches...`, + }) + + await Apply(id, { + patches: Object.fromEntries(defaultPatches.map((patch) => [patch.id, true])), + }) + } + } + + global._relic_eventBus.emit(`pkg:update:state:${id}`, { + status_text: `Installation completed successfully`, + }) + + Log.info(`Package installed successfully!`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:${id}:error`, error) + + global._relic_eventBus.emit(`pkg:update:state:${id}`, { + last_status: "failed", + status_text: `Installation failed`, + }) + + BaseLog.error(`Error during installation of package [${id}] >`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/list.js b/packages/core/src/handlers/list.js new file mode 100644 index 0000000..eb51f5a --- /dev/null +++ b/packages/core/src/handlers/list.js @@ -0,0 +1,5 @@ +import DB from "../db" + +export default async function list() { + return await DB.getPackages() +} \ No newline at end of file diff --git a/packages/core/src/handlers/uninstall.js b/packages/core/src/handlers/uninstall.js new file mode 100644 index 0000000..531245c --- /dev/null +++ b/packages/core/src/handlers/uninstall.js @@ -0,0 +1,63 @@ +import DB from "../db" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +import { rimraf } from "rimraf" + +const BaseLog = Logger.child({ service: "UNINSTALLER" }) + +export default async function uninstall(pkg_id) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.info(`Package not found [${pkg_id}]`) + return null + } + + const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` }) + + Log.info(`Uninstalling package...`) + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Uninstalling package...`, + }) + + const ManifestRead = await ManifestReader(pkg.local_manifest) + const manifest = await ManifestVM(ManifestRead.code) + + if (typeof manifest.uninstall === "function") { + Log.info(`Performing uninstall hook...`) + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Performing uninstall hook...`, + }) + await manifest.uninstall(pkg) + } + + Log.info(`Deleting package directory...`) + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Deleting package directory...`, + }) + await rimraf(pkg.install_path) + + Log.info(`Removing package from database...`) + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Removing package from database...`, + }) + await DB.deletePackage(pkg.id) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status: "deleted", + status_text: `Uninstalling package...`, + }) + Log.info(`Package uninstalled successfully!`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + + BaseLog.error(`Failed to uninstall package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/update.js b/packages/core/src/handlers/update.js new file mode 100644 index 0000000..8cd46e3 --- /dev/null +++ b/packages/core/src/handlers/update.js @@ -0,0 +1,114 @@ +import DB from "../db" + +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +import GenericSteps from "../generic_steps" +import PatchManager from "../classes/PatchManager" + +const BaseLog = Logger.child({ service: "UPDATER" }) + +const AllowedPkgChanges = [ + "id", + "name", + "version", + "description", + "author", + "license", + "icon", + "core_minimum_version", + "remote_manifest", +] + +const ManifestKeysMap = { + "name": "pkg_name", +} + +export default async function update(pkg_id) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.error(`Package not found [${pkg_id}]`) + + return null + } + + const Log = BaseLog.child({ service: `UPDATER|${pkg.id}` }) + + let ManifestRead = await ManifestReader(pkg.local_manifest) + let manifest = await ManifestVM(ManifestRead.code) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status: "updating", + status_text: `Updating package...`, + }) + + if (typeof manifest.update === "function") { + Log.info(`Performing update hook...`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Performing update hook...`, + }) + + await manifest.update(pkg) + } + + if (manifest.updateSteps) { + Log.info(`Performing update steps...`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Performing update steps...`, + }) + + await GenericSteps(pkg, manifest.updateSteps, Log) + } + + if (Array.isArray(pkg.applied_patches)) { + const patchManager = new PatchManager(pkg, manifest) + + await patchManager.patch(pkg.applied_patches) + } + + if (typeof manifest.afterUpdate === "function") { + Log.info(`Performing after update hook...`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status_text: `Performing after update hook...`, + }) + + await manifest.afterUpdate(pkg) + } + + ManifestRead = await ManifestReader(pkg.local_manifest) + manifest = await ManifestVM(ManifestRead.code) + + // override public static values + for await (const key of AllowedPkgChanges) { + if (key in manifest.constructor) { + const mapKey = ManifestKeysMap[key] || key + pkg[key] = manifest.constructor[mapKey] + } + } + + pkg.status = "installed" + pkg.last_update = Date.now() + + await DB.writePackage(pkg) + + Log.info(`Package updated successfully`) + + global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + status: "installed", + }) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + + BaseLog.error(`Failed to update package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/helpers/downloadHttpFile.js b/packages/core/src/helpers/downloadHttpFile.js new file mode 100644 index 0000000..d347b81 --- /dev/null +++ b/packages/core/src/helpers/downloadHttpFile.js @@ -0,0 +1,73 @@ +import fs from "node:fs" +import axios from "axios" +import humanFormat from "human-format" +import cliProgress from "cli-progress" + +function convertSize(size) { + return `${humanFormat(size, { + decimals: 2, + })}B` +} + +export default async (url, destination, progressCallback) => { + const progressBar = new cliProgress.SingleBar({ + format: "[{bar}] {percentage}% | {total_formatted} | {speed}/s | {eta_formatted}", + barCompleteChar: "\u2588", + barIncompleteChar: "\u2591", + hideCursor: true + }, cliProgress.Presets.shades_classic) + + const { data: remoteStream, headers } = await axios.get(url, { + responseType: "stream", + }) + + const localStream = fs.createWriteStream(destination) + + let progress = { + total: Number(headers["content-length"] ?? 0), + transferred: 0, + speed: 0, + } + + let lastTickTransferred = 0 + + progressBar.start(progress.total, 0, { + speed: "0B/s", + total_formatted: convertSize(progress.total), + }) + + remoteStream.pipe(localStream) + + remoteStream.on("data", (data) => { + progress.transferred = progress.transferred + Buffer.byteLength(data) + }) + + const progressInterval = setInterval(() => { + progress.speed = ((progress.transferred ?? 0) - lastTickTransferred) / 1 + + lastTickTransferred = progress.transferred ?? 0 + + progress.transferredString = convertSize(progress.transferred ?? 0) + progress.totalString = convertSize(progress.total) + progress.speedString = convertSize(progress.speed) + + progressBar.update(progress.transferred, { + speed: progress.speedString, + }) + + if (typeof progressCallback === "function") { + progressCallback(progress) + } + }, 1000) + + await new Promise((resolve, reject) => { + localStream.on("finish", resolve) + localStream.on("error", reject) + }) + + progressBar.stop() + + clearInterval(progressInterval) + + return destination +} \ No newline at end of file diff --git a/packages/core/src/helpers/sendToRender.js b/packages/core/src/helpers/sendToRender.js new file mode 100644 index 0000000..8a534a5 --- /dev/null +++ b/packages/core/src/helpers/sendToRender.js @@ -0,0 +1,43 @@ +import lodash from "lodash" + +const forbidden = [ + "libraries" +] + +export default (event, data) => { + if (!global.win) { + return false + } + + try { + function serializeIpc(data) { + if (!data) { + return undefined + } + + data = JSON.stringify(data) + + data = JSON.parse(data) + + const copy = lodash.cloneDeep(data) + + if (!Array.isArray(copy)) { + Object.keys(copy).forEach((key) => { + if (forbidden.includes(key)) { + delete copy[key] + } + + if (typeof copy[key] === "function") { + delete copy[key] + } + }) + } + + return copy + } + + global.win.webContents.send(event, serializeIpc(data)) + } catch (error) { + console.error(error) + } +} \ No newline at end of file diff --git a/packages/core/src/helpers/setup.js b/packages/core/src/helpers/setup.js new file mode 100644 index 0000000..737040b --- /dev/null +++ b/packages/core/src/helpers/setup.js @@ -0,0 +1,125 @@ +import Logger from "../logger" + +const Log = Logger.child({ service: "SETUP" }) + +import path from "node:path" +import fs from "node:fs" +import os from "node:os" +import admzip from "adm-zip" +import resolveOs from "../utils/resolveOs" +import chmodRecursive from "../utils/chmodRecursive" + +import downloadFile from "../helpers/downloadHttpFile" + +import Vars from "../vars" +import Prerequisites from "../prerequisites" + +export default async () => { + if (!fs.existsSync(Vars.binaries_path)) { + Log.info(`Creating binaries directory: ${Vars.binaries_path}...`) + await fs.promises.mkdir(Vars.binaries_path, { recursive: true }) + } + + for await (let prerequisite of Prerequisites) { + try { + Log.info(`Checking prerequisite: ${prerequisite.id}...`) + + if (Array.isArray(prerequisite.requireOs) && !prerequisite.requireOs.includes(os.platform())) { + Log.info(`Prerequisite: ${prerequisite.id} is not required for this os.`) + continue + } + + if (!fs.existsSync(prerequisite.finalBin)) { + Log.info(`Missing prerequisite: ${prerequisite.id}, installing...`) + + if (fs.existsSync(prerequisite.destination)) { + Log.info(`Deleting temporal file [${prerequisite.destination}]`) + await fs.promises.rm(prerequisite.destination) + } + + if (fs.existsSync(prerequisite.extract)) { + Log.info(`Deleting temporal directory [${prerequisite.extract}]`) + await fs.promises.rm(prerequisite.extract, { recursive: true }) + } + + Log.info(`Creating base directory: ${Vars.binaries_path}/${prerequisite.id}...`) + await fs.promises.mkdir(path.resolve(Vars.binaries_path, prerequisite.id), { recursive: true }) + + if (typeof prerequisite.url === "function") { + prerequisite.url = await prerequisite.url(resolveOs(), os.arch()) + Log.info(`Resolved url: ${prerequisite.url}`) + } + + Log.info(`Downloading ${prerequisite.id} from [${prerequisite.url}] to destination [${prerequisite.destination}]...`) + + try { + await downloadFile( + prerequisite.url, + prerequisite.destination + ) + } catch (error) { + await fs.promises.rm(prerequisite.destination) + + throw error + } + + if (typeof prerequisite.extract === "string") { + Log.info(`Extracting ${prerequisite.id} to destination [${prerequisite.extract}]...`) + + const zip = new admzip(prerequisite.destination) + + await zip.extractAllTo(prerequisite.extract, true) + + Log.info(`Extraction ok...`) + } + + if (prerequisite.extractTargetFromName === true) { + let name = path.basename(prerequisite.url) + const ext = path.extname(name) + + name = name.replace(ext, "") + + if (fs.existsSync(path.resolve(prerequisite.extract, name))) { + await fs.promises.rename(path.resolve(prerequisite.extract, name), `${prerequisite.extract}_old`) + await fs.promises.rm(prerequisite.extract, { recursive: true }) + await fs.promises.rename(`${prerequisite.extract}_old`, prerequisite.extract) + } + } + + if (prerequisite.deleteBeforeExtract === true) { + Log.info(`Deleting temporal file [${prerequisite.destination}]`) + await fs.promises.unlink(prerequisite.destination) + } + + if (typeof prerequisite.rewriteExecutionPermission !== "undefined") { + const to = typeof prerequisite.rewriteExecutionPermission === "string" ? + prerequisite.rewriteExecutionPermission : + prerequisite.finalBin + + Log.info(`Rewriting permissions to ${to}...`) + await chmodRecursive(to, 0o755) + } + + if (Array.isArray(prerequisite.moveDirs)) { + for (const dir of prerequisite.moveDirs) { + Log.info(`Moving ${dir.from} to ${dir.to}...`) + + await fs.promises.rename(dir.from, dir.to) + + if (dir.deleteParentBefore === true) { + await fs.promises.rm(path.dirname(dir.from), { recursive: true }) + } + } + } + } + + Log.info(`Prerequisite: ${prerequisite.id} is ready!`) + } catch (error) { + Log.error(error) + Log.error("Aborting setup due to an error...") + return false + } + + Log.info(`All prerequisites are ready!`) + } +} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js new file mode 100644 index 0000000..9fe38dd --- /dev/null +++ b/packages/core/src/index.js @@ -0,0 +1,52 @@ +import fs from "node:fs" +import { EventEmitter } from "@foxify/events" +import { onExit } from "signal-exit" +import open from "open" + +import SetupHelper from "./helpers/setup" +import Logger from "./logger" + +import Vars from "./vars" +import DB from "./db" + +export default class RelicCore { + constructor(params) { + this.params = params + } + + eventBus = global._relic_eventBus = new EventEmitter() + logger = global.Logger = Logger + + async initialize() { + await DB.initialize() + + onExit(this.onExit) + } + + onExit = () => { + if (fs.existsSync(Vars.cache_path)) { + fs.rmSync(Vars.cache_path, { recursive: true, force: true }) + } + } + + async setup() { + return await SetupHelper() + } + + package = { + install: require("./handlers/install").default, + execute: require("./handlers/execute").default, + uninstall: require("./handlers/uninstall").default, + update: require("./handlers/update").default, + apply: require("./handlers/apply").default, + list: require("./handlers/list").default, + } + + openPath(pkg_id) { + if (!pkg_id) { + return open(Vars.runtime_path) + } + + return open(Vars.packages_path + "/" + pkg_id) + } +} \ No newline at end of file diff --git a/src/main/lib/execa/index.d.ts b/packages/core/src/libraries/execa/index.d.ts similarity index 100% rename from src/main/lib/execa/index.d.ts rename to packages/core/src/libraries/execa/index.d.ts diff --git a/src/main/lib/execa/index.js b/packages/core/src/libraries/execa/index.js similarity index 100% rename from src/main/lib/execa/index.js rename to packages/core/src/libraries/execa/index.js diff --git a/src/main/lib/execa/lib/command.js b/packages/core/src/libraries/execa/lib/command.js similarity index 100% rename from src/main/lib/execa/lib/command.js rename to packages/core/src/libraries/execa/lib/command.js diff --git a/src/main/lib/execa/lib/error.js b/packages/core/src/libraries/execa/lib/error.js similarity index 100% rename from src/main/lib/execa/lib/error.js rename to packages/core/src/libraries/execa/lib/error.js diff --git a/src/main/lib/execa/lib/kill.js b/packages/core/src/libraries/execa/lib/kill.js similarity index 100% rename from src/main/lib/execa/lib/kill.js rename to packages/core/src/libraries/execa/lib/kill.js diff --git a/src/main/lib/execa/lib/pipe.js b/packages/core/src/libraries/execa/lib/pipe.js similarity index 100% rename from src/main/lib/execa/lib/pipe.js rename to packages/core/src/libraries/execa/lib/pipe.js diff --git a/src/main/lib/execa/lib/promise.js b/packages/core/src/libraries/execa/lib/promise.js similarity index 100% rename from src/main/lib/execa/lib/promise.js rename to packages/core/src/libraries/execa/lib/promise.js diff --git a/src/main/lib/execa/lib/stdio.js b/packages/core/src/libraries/execa/lib/stdio.js similarity index 100% rename from src/main/lib/execa/lib/stdio.js rename to packages/core/src/libraries/execa/lib/stdio.js diff --git a/src/main/lib/execa/lib/stream.js b/packages/core/src/libraries/execa/lib/stream.js similarity index 100% rename from src/main/lib/execa/lib/stream.js rename to packages/core/src/libraries/execa/lib/stream.js diff --git a/src/main/lib/execa/lib/verbose.js b/packages/core/src/libraries/execa/lib/verbose.js similarity index 100% rename from src/main/lib/execa/lib/verbose.js rename to packages/core/src/libraries/execa/lib/verbose.js diff --git a/src/main/lib/get-stream/array-buffer.js b/packages/core/src/libraries/get-stream/array-buffer.js similarity index 100% rename from src/main/lib/get-stream/array-buffer.js rename to packages/core/src/libraries/get-stream/array-buffer.js diff --git a/src/main/lib/get-stream/array.js b/packages/core/src/libraries/get-stream/array.js similarity index 100% rename from src/main/lib/get-stream/array.js rename to packages/core/src/libraries/get-stream/array.js diff --git a/src/main/lib/get-stream/buffer.js b/packages/core/src/libraries/get-stream/buffer.js similarity index 100% rename from src/main/lib/get-stream/buffer.js rename to packages/core/src/libraries/get-stream/buffer.js diff --git a/src/main/lib/get-stream/contents.js b/packages/core/src/libraries/get-stream/contents.js similarity index 100% rename from src/main/lib/get-stream/contents.js rename to packages/core/src/libraries/get-stream/contents.js diff --git a/src/main/lib/get-stream/index.d.ts b/packages/core/src/libraries/get-stream/index.d.ts similarity index 100% rename from src/main/lib/get-stream/index.d.ts rename to packages/core/src/libraries/get-stream/index.d.ts diff --git a/src/main/lib/get-stream/index.js b/packages/core/src/libraries/get-stream/index.js similarity index 100% rename from src/main/lib/get-stream/index.js rename to packages/core/src/libraries/get-stream/index.js diff --git a/src/main/lib/get-stream/index.test-d.ts b/packages/core/src/libraries/get-stream/index.test-d.ts similarity index 100% rename from src/main/lib/get-stream/index.test-d.ts rename to packages/core/src/libraries/get-stream/index.test-d.ts diff --git a/src/main/lib/get-stream/string.js b/packages/core/src/libraries/get-stream/string.js similarity index 100% rename from src/main/lib/get-stream/string.js rename to packages/core/src/libraries/get-stream/string.js diff --git a/src/main/lib/get-stream/utils.js b/packages/core/src/libraries/get-stream/utils.js similarity index 100% rename from src/main/lib/get-stream/utils.js rename to packages/core/src/libraries/get-stream/utils.js diff --git a/src/main/lib/human-signals/core.js b/packages/core/src/libraries/human-signals/core.js similarity index 100% rename from src/main/lib/human-signals/core.js rename to packages/core/src/libraries/human-signals/core.js diff --git a/src/main/lib/human-signals/index.js b/packages/core/src/libraries/human-signals/index.js similarity index 100% rename from src/main/lib/human-signals/index.js rename to packages/core/src/libraries/human-signals/index.js diff --git a/src/main/lib/human-signals/realtime.js b/packages/core/src/libraries/human-signals/realtime.js similarity index 100% rename from src/main/lib/human-signals/realtime.js rename to packages/core/src/libraries/human-signals/realtime.js diff --git a/src/main/lib/human-signals/signals.js b/packages/core/src/libraries/human-signals/signals.js similarity index 100% rename from src/main/lib/human-signals/signals.js rename to packages/core/src/libraries/human-signals/signals.js diff --git a/src/main/lib/is-stream/index.d.ts b/packages/core/src/libraries/is-stream/index.d.ts similarity index 100% rename from src/main/lib/is-stream/index.d.ts rename to packages/core/src/libraries/is-stream/index.d.ts diff --git a/src/main/lib/is-stream/index.js b/packages/core/src/libraries/is-stream/index.js similarity index 100% rename from src/main/lib/is-stream/index.js rename to packages/core/src/libraries/is-stream/index.js diff --git a/packages/core/src/libraries/lowdb/adapters/Memory.js b/packages/core/src/libraries/lowdb/adapters/Memory.js new file mode 100644 index 0000000..798cd36 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/Memory.js @@ -0,0 +1,24 @@ +export class Memory { + #data = null + + read() { + return Promise.resolve(this.#data) + } + + write(obj) { + this.#data = obj + return Promise.resolve() + } +} + +export class MemorySync { + #data = null + + read() { + return this.#data || null + } + + write(obj) { + this.#data = obj + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/DataFile.js b/packages/core/src/libraries/lowdb/adapters/node/DataFile.js new file mode 100644 index 0000000..0506e0c --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/DataFile.js @@ -0,0 +1,51 @@ +import { TextFile, TextFileSync } from "./TextFile.js" + +export class DataFile { + #adapter + #parse + #stringify + + constructor(filename, { parse, stringify }) { + this.#adapter = new TextFile(filename) + this.#parse = parse + this.#stringify = stringify + } + + async read() { + const data = await this.#adapter.read() + if (data === null) { + return null + } else { + return this.#parse(data) + } + } + + write(obj) { + return this.#adapter.write(this.#stringify(obj)) + } +} + +export class DataFileSync { + #adapter + #parse + #stringify + + constructor(filename, { parse, stringify }) { + this.#adapter = new TextFileSync(filename) + this.#parse = parse + this.#stringify = stringify + } + + read() { + const data = this.#adapter.read() + if (data === null) { + return null + } else { + return this.#parse(data) + } + } + + write(obj) { + this.#adapter.write(this.#stringify(obj)) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js b/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js new file mode 100644 index 0000000..8811a87 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js @@ -0,0 +1,19 @@ +import { DataFile, DataFileSync } from "./DataFile.js"; + +export class JSONFile extends DataFile { + constructor(filename) { + super(filename, { + parse: JSON.parse, + stringify: (data) => JSON.stringify(data, null, 2), + }); + } +} + +export class JSONFileSync extends DataFileSync { + constructor(filename) { + super(filename, { + parse: JSON.parse, + stringify: (data) => JSON.stringify(data, null, 2), + }); + } +} diff --git a/packages/core/src/libraries/lowdb/adapters/node/TextFile.js b/packages/core/src/libraries/lowdb/adapters/node/TextFile.js new file mode 100644 index 0000000..b1f3321 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/TextFile.js @@ -0,0 +1,65 @@ +import { readFileSync, renameSync, writeFileSync } from "node:fs" +import { readFile } from "node:fs/promises" +import path from "node:path" + +import { Writer } from "../../steno" + +export class TextFile { + #filename + #writer + + constructor(filename) { + this.#filename = filename + this.#writer = new Writer(filename) + } + + async read() { + let data + + try { + data = await readFile(this.#filename, "utf-8") + } catch (e) { + if (e.code === "ENOENT") { + return null + } + throw e + } + + return data + } + + write(str) { + return this.#writer.write(str) + } +} + +export class TextFileSync { + #tempFilename + #filename + + constructor(filename) { + this.#filename = filename + const f = filename.toString() + this.#tempFilename = path.join(path.dirname(f), `.${path.basename(f)}.tmp`) + } + + read() { + let data + + try { + data = readFileSync(this.#filename, "utf-8") + } catch (e) { + if (e.code === "ENOENT") { + return null + } + throw e + } + + return data + } + + write(str) { + writeFileSync(this.#tempFilename, str) + renameSync(this.#tempFilename, this.#filename) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/core/Low.js b/packages/core/src/libraries/lowdb/core/Low.js new file mode 100644 index 0000000..f0b76e5 --- /dev/null +++ b/packages/core/src/libraries/lowdb/core/Low.js @@ -0,0 +1,48 @@ +function checkArgs(adapter, defaultData) { + if (adapter === undefined) throw new Error("lowdb: missing adapter") + if (defaultData === undefined) throw new Error("lowdb: missing default data") +} + +export class Low { + constructor(adapter, defaultData) { + checkArgs(adapter, defaultData) + this.adapter = adapter + this.data = defaultData + } + + async read() { + const data = await this.adapter.read() + if (data) this.data = data + } + + async write() { + if (this.data) await this.adapter.write(this.data) + } + + async update(fn) { + fn(this.data) + await this.write() + } +} + +export class LowSync { + constructor(adapter, defaultData) { + checkArgs(adapter, defaultData) + this.adapter = adapter + this.data = defaultData + } + + read() { + const data = this.adapter.read() + if (data) this.data = data + } + + write() { + if (this.data) this.adapter.write(this.data) + } + + update(fn) { + fn(this.data) + this.write() + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/presets/node.js b/packages/core/src/libraries/lowdb/presets/node.js new file mode 100644 index 0000000..e8526fb --- /dev/null +++ b/packages/core/src/libraries/lowdb/presets/node.js @@ -0,0 +1,23 @@ +import { Memory, MemorySync } from "../adapters/Memory.js" +import { JSONFile, JSONFileSync } from "../adapters/node/JSONFile.js" +import { Low, LowSync } from "../core/Low.js" + +export async function JSONFilePreset(filename, defaultData) { + const adapter = process.env.NODE_ENV === "test" ? new Memory() : new JSONFile(filename) + + const db = new Low(adapter, defaultData) + + await db.read() + + return db +} + +export function JSONFileSyncPreset(filename, defaultData) { + const adapter = process.env.NODE_ENV === "test" ? new MemorySync() : new JSONFileSync(filename) + + const db = new LowSync(adapter, defaultData) + + db.read() + + return db +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/steno/index.js b/packages/core/src/libraries/lowdb/steno/index.js new file mode 100644 index 0000000..d0c5558 --- /dev/null +++ b/packages/core/src/libraries/lowdb/steno/index.js @@ -0,0 +1,98 @@ +import { rename, writeFile } from "node:fs/promises" +import { basename, dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +// Returns a temporary file +// Example: for /some/file will return /some/.file.tmp +function getTempFilename(file) { + const f = file instanceof URL ? fileURLToPath(file) : file.toString() + return join(dirname(f), `.${basename(f)}.tmp`) +} + +// Retries an asynchronous operation with a delay between retries and a maximum retry count +async function retryAsyncOperation(fn, maxRetries, delayMs) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fn() + } catch (error) { + if (i < maxRetries - 1) { + await new Promise(resolve => setTimeout(resolve, delayMs)) + } else { + throw error // Rethrow the error if max retries reached + } + } + } +} + +export class Writer { + #filename + #tempFilename + #locked = false + #prev = null + #next = null + #nextPromise = null + #nextData = null + + // File is locked, add data for later + #add(data) { + // Only keep most recent data + this.#nextData = data + + // Create a singleton promise to resolve all next promises once next data is written + this.#nextPromise ||= new Promise((resolve, reject) => { + this.#next = [resolve, reject] + }) + + // Return a promise that will resolve at the same time as next promise + return new Promise((resolve, reject) => { + this.#nextPromise?.then(resolve).catch(reject) + }) + } + + // File isn't locked, write data + async #write(data) { + // Lock file + this.#locked = true + try { + // Atomic write + await writeFile(this.#tempFilename, data, "utf-8") + await retryAsyncOperation( + async () => { + await rename(this.#tempFilename, this.#filename) + }, + 10, + 100 + ) + + // Call resolve + this.#prev?.[0]() + } catch (err) { + // Call reject + if (err instanceof Error) { + this.#prev?.[1](err) + } + throw err + } finally { + // Unlock file + this.#locked = false + + this.#prev = this.#next + this.#next = this.#nextPromise = null + + if (this.#nextData !== null) { + const nextData = this.#nextData + this.#nextData = null + await this.write(nextData) + } + } + } + + constructor(filename) { + this.#filename = filename + this.#tempFilename = getTempFilename(filename) + } + + async write(data) { + return this.#locked ? this.#add(data) : this.#write(data) + } +} \ No newline at end of file diff --git a/src/main/lib/mimic-function/index.js b/packages/core/src/libraries/mimic-function/index.js similarity index 100% rename from src/main/lib/mimic-function/index.js rename to packages/core/src/libraries/mimic-function/index.js diff --git a/src/main/lib/npm-run-path/index.d.ts b/packages/core/src/libraries/npm-run-path/index.d.ts similarity index 100% rename from src/main/lib/npm-run-path/index.d.ts rename to packages/core/src/libraries/npm-run-path/index.d.ts diff --git a/src/main/lib/npm-run-path/index.js b/packages/core/src/libraries/npm-run-path/index.js similarity index 100% rename from src/main/lib/npm-run-path/index.js rename to packages/core/src/libraries/npm-run-path/index.js diff --git a/src/main/lib/onetime/index.d.ts b/packages/core/src/libraries/onetime/index.d.ts similarity index 100% rename from src/main/lib/onetime/index.d.ts rename to packages/core/src/libraries/onetime/index.d.ts diff --git a/src/main/lib/onetime/index.js b/packages/core/src/libraries/onetime/index.js similarity index 100% rename from src/main/lib/onetime/index.js rename to packages/core/src/libraries/onetime/index.js diff --git a/src/main/lib/strip-final-newline/index.d.ts b/packages/core/src/libraries/strip-final-newline/index.d.ts similarity index 100% rename from src/main/lib/strip-final-newline/index.d.ts rename to packages/core/src/libraries/strip-final-newline/index.d.ts diff --git a/src/main/lib/strip-final-newline/index.js b/packages/core/src/libraries/strip-final-newline/index.js similarity index 100% rename from src/main/lib/strip-final-newline/index.js rename to packages/core/src/libraries/strip-final-newline/index.js diff --git a/packages/core/src/logger.js b/packages/core/src/logger.js new file mode 100644 index 0000000..1f3d6d1 --- /dev/null +++ b/packages/core/src/logger.js @@ -0,0 +1,40 @@ +import winston from "winston" +import colors from "cli-color" + +const servicesToColor = { + "CORE": { + color: "whiteBright", + background: "bgBlackBright", + }, + "INSTALL": { + color: "whiteBright", + background: "bgBlueBright", + }, +} + +const paintText = (level, service, ...args) => { + let { color, background } = servicesToColor[service ?? "CORE"] ?? servicesToColor["CORE"] + + if (level === "error") { + color = "whiteBright" + background = "bgRedBright" + } + + return colors[background][color](...args) +} + +const format = winston.format.printf(({ timestamp, service = "CORE", level, message, }) => { + return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}` +}) + +export default winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp(), + format + ), + transports: [ + new winston.transports.Console(), + //new winston.transports.File({ filename: "error.log", level: "error" }), + //new winston.transports.File({ filename: "combined.log" }), + ], +}) \ No newline at end of file diff --git a/packages/core/src/manifest/libraries.js b/packages/core/src/manifest/libraries.js new file mode 100644 index 0000000..edab737 --- /dev/null +++ b/packages/core/src/manifest/libraries.js @@ -0,0 +1,23 @@ +import PublicInternalLibraries from "./libs" + +const isAClass = (x) => x && typeof x === "function" && x.prototype && typeof x.prototype.constructor === "function" + +export default async (dependencies, bindCtx) => { + const libraries = {} + + for await (const lib of dependencies) { + if (PublicInternalLibraries[lib]) { + if (typeof PublicInternalLibraries[lib] === "function" && isAClass(PublicInternalLibraries[lib])) { + libraries[lib] = new PublicInternalLibraries[lib](bindCtx) + + if (libraries[lib].initialize) { + await libraries[lib].initialize() + } + } else { + libraries[lib] = PublicInternalLibraries[lib] + } + } + } + + return libraries +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/auth/index.js b/packages/core/src/manifest/libs/auth/index.js new file mode 100644 index 0000000..f095692 --- /dev/null +++ b/packages/core/src/manifest/libs/auth/index.js @@ -0,0 +1,61 @@ +import open from "open" +import axios from "axios" + +export default class Auth { + constructor(ctx) { + this.manifest = ctx.manifest + } + + async get() { + return { + assigned_username: "test", + } + + const authData = global.authService.getAuth(this.manifest.id) + + console.log(authData) + + if (authData && this.manifest.auth && this.manifest.auth.getter) { + const result = await axios({ + method: "POST", + url: this.manifest.auth.getter, + headers: { + "Content-Type": "application/json", + }, + data: { + auth_data: authData, + } + }).catch((err) => { + sendToRender(`new:notification`, { + type: "error", + message: "Failed to authorize", + description: err.response.data.message ?? err.response.data.error ?? err.message, + duration: 10 + }) + + return err + }) + + if (result instanceof Error) { + throw result + } + + console.log(result.data) + + return result.data + } + + return authData + } + + request() { + return true + if (!this.manifest.auth) { + return false + } + + const authURL = this.manifest.auth.fetcher + + open(authURL) + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/fs/index.js b/packages/core/src/manifest/libs/fs/index.js new file mode 100644 index 0000000..a025bc1 --- /dev/null +++ b/packages/core/src/manifest/libs/fs/index.js @@ -0,0 +1,39 @@ +import fs from "node:fs" +import path from "node:path" + +// Protect from reading or write operations outside of the package directory +export default class SecureFileSystem { + constructor(ctx) { + this.jailPath = ctx.manifest.install_path + } + + checkOutsideJail(target) { + // if (!path.resolve(target).startsWith(this.jailPath)) { + // throw new Error("Cannot access resource outside of package directory") + // } + } + + readFileSync(destination, options) { + this.checkOutsideJail(destination) + + return fs.readFileSync(finalPath, options) + } + + copyFileSync(from, to) { + this.checkOutsideJail(from) + this.checkOutsideJail(to) + + return fs.copyFileSync(from, to) + } + + writeFileSync(destination, data, options) { + this.checkOutsideJail(destination) + + return fs.writeFileSync(finalPath, data, options) + } + + // don't need to check finalPath + existsSync(...args) { + return fs.existsSync(...args) + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/index.js b/packages/core/src/manifest/libs/index.js new file mode 100644 index 0000000..3a33e39 --- /dev/null +++ b/packages/core/src/manifest/libs/index.js @@ -0,0 +1,15 @@ +import Open from "./open" +import Path from "./path" +import Fs from "./fs" +import Auth from "./auth" + +// Third party libraries +import Mcl from "./mcl" + +export default { + fs: Fs, + path: Path, + open: Open, + auth: Auth, + mcl: Mcl +} \ No newline at end of file diff --git a/src/main/lib/mcl/authenticator.js b/packages/core/src/manifest/libs/mcl/authenticator.js similarity index 100% rename from src/main/lib/mcl/authenticator.js rename to packages/core/src/manifest/libs/mcl/authenticator.js diff --git a/src/main/lib/mcl/handler.js b/packages/core/src/manifest/libs/mcl/handler.js similarity index 100% rename from src/main/lib/mcl/handler.js rename to packages/core/src/manifest/libs/mcl/handler.js diff --git a/packages/core/src/manifest/libs/mcl/index.js b/packages/core/src/manifest/libs/mcl/index.js new file mode 100644 index 0000000..11b5a2c --- /dev/null +++ b/packages/core/src/manifest/libs/mcl/index.js @@ -0,0 +1,47 @@ +import Client from "./launcher" +import Authenticator from "./authenticator" + +const Log = Logger.child({ service: "MCL" }) + +export default class MCL { + /** + * Asynchronously authenticate the user using the provided username and password. + * + * @param {string} username - the username of the user + * @param {string} password - the password of the user + * @return {Promise} the authentication information + */ + async auth(username, password) { + return await Authenticator.getAuth(username, password) + } + + /** + * Launches a new client with the given options. + * + * @param {Object} opts - The options to be passed for launching the client. + * @return {Promise} A promise that resolves with the launched client. + */ + async launch(opts, callbacks) { + const launcher = new Client() + + launcher.on("debug", (e) => console.log(e)) + launcher.on("data", (e) => console.log(e)) + launcher.on("close", (e) => console.log(e)) + launcher.on("error", (e) => console.log(e)) + + if (typeof callbacks === "undefined") { + callbacks = { + install: () => { + Log.info("Downloading Minecraft assets...") + }, + init_assets: () => { + Log.info("Initializing Minecraft assets...") + } + } + } + + await launcher.launch(opts, callbacks) + + return launcher + } +} \ No newline at end of file diff --git a/src/main/lib/mcl/launcher.js b/packages/core/src/manifest/libs/mcl/launcher.js similarity index 100% rename from src/main/lib/mcl/launcher.js rename to packages/core/src/manifest/libs/mcl/launcher.js diff --git a/packages/core/src/manifest/libs/open/index.js b/packages/core/src/manifest/libs/open/index.js new file mode 100644 index 0000000..d957150 --- /dev/null +++ b/packages/core/src/manifest/libs/open/index.js @@ -0,0 +1,13 @@ +import open, { apps } from "open" + +const Log = Logger.child({ service: "OPEN-LIB" }) + +export default { + spawn: async (...args) => { + Log.info("Open spawned with args >") + console.log(...args) + + return await open(...args) + }, + apps: apps, +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/path/index.js b/packages/core/src/manifest/libs/path/index.js new file mode 100644 index 0000000..1b4bd73 --- /dev/null +++ b/packages/core/src/manifest/libs/path/index.js @@ -0,0 +1,3 @@ +import path from "node:path" + +export default path \ No newline at end of file diff --git a/packages/core/src/manifest/reader.js b/packages/core/src/manifest/reader.js new file mode 100644 index 0000000..3a6017e --- /dev/null +++ b/packages/core/src/manifest/reader.js @@ -0,0 +1,44 @@ +import fs from "node:fs" +import path from "node:path" +import downloadHttpFile from "../helpers/downloadHttpFile" + +import Vars from "../vars" + +export async function readManifest(manifest) { + // check if manifest is a directory or a url + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi + + const target = manifest?.remote_url ?? manifest + + if (urlRegex.test(target)) { + if (!fs.existsSync(Vars.cache_path)) { + fs.mkdirSync(Vars.cache_path, { recursive: true }) + } + + const cachedManifest = await downloadHttpFile(manifest, path.resolve(Vars.cache_path, `${Date.now()}.rmanifest`)) + + return { + remote_manifest: manifest, + local_manifest: cachedManifest, + is_catched: true, + code: fs.readFileSync(cachedManifest, "utf8"), + } + } else { + if (!fs.existsSync(target)) { + throw new Error(`Manifest not found: ${target}`) + } + + if (!fs.statSync(target).isFile()) { + throw new Error(`Manifest is not a file: ${target}`) + } + + return { + remote_manifest: undefined, + local_manifest: target, + is_catched: false, + code: fs.readFileSync(target, "utf8"), + } + } +} + +export default readManifest \ No newline at end of file diff --git a/packages/core/src/manifest/vm.js b/packages/core/src/manifest/vm.js new file mode 100644 index 0000000..80341e7 --- /dev/null +++ b/packages/core/src/manifest/vm.js @@ -0,0 +1,71 @@ +import os from "node:os" +import vm from "node:vm" +import path from "node:path" +import ManifestConfigManager from "../classes/ManifestConfig" + +import resolveOs from "../utils/resolveOs" +import FetchLibraries from "./libraries" + +import Vars from "../vars" + +async function BuildManifest(baseClass, context, soft = false) { + const configManager = new ManifestConfigManager(baseClass.id) + + await configManager.initialize() + + let dependencies = [] + + if (Array.isArray(baseClass.useLib)) { + dependencies = [ + ...dependencies, + ...baseClass.useLib + ] + } + + // inject install_path + context.install_path = path.resolve(Vars.packages_path, baseClass.id) + baseClass.install_path = context.install_path + + // modify context + context.Log = Logger.child({ service: `VM|${baseClass.id}` }) + context.Lib = await FetchLibraries(dependencies, { + manifest: baseClass, + install_path: context.install_path, + }) + context.Config = configManager + + // Construct the instance + const instance = new baseClass() + + instance.install_path = context.install_path + + return instance +} + +function injectUseManifest(code) { + return code + "\n\nuse(Manifest);" +} + +export default async (code) => { + return await new Promise(async (resolve, reject) => { + try { + code = injectUseManifest(code) + + const context = { + Vars: Vars, + Log: Logger.child({ service: "MANIFEST_VM" }), + use: (baseClass) => { + BuildManifest(baseClass, context).then(resolve) + }, + os_string: resolveOs(), + arch: os.arch(), + } + + vm.createContext(context) + + await vm.runInContext(code, context) + } catch (error) { + reject(error) + } + }) +} \ No newline at end of file diff --git a/packages/core/src/prerequisites.js b/packages/core/src/prerequisites.js new file mode 100644 index 0000000..b2f357d --- /dev/null +++ b/packages/core/src/prerequisites.js @@ -0,0 +1,69 @@ +import resolveRemoteBinPath from "./utils/resolveRemoteBinPath" +import Vars from "./vars" +import path from "node:path" +import axios from "axios" + +const baseURL = "https://storage.ragestudio.net/rstudio/binaries" + +export default [ + { + id: "7z-bin", + finalBin: Vars.sevenzip_bin, + url: resolveRemoteBinPath(`${baseURL}/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za"), + destination: Vars.sevenzip_bin, + rewriteExecutionPermission: true, + }, + { + id: "git-bin", + finalBin: Vars.git_bin, + url: resolveRemoteBinPath(`${baseURL}/git`, "git-bundle-2.4.0.zip"), + destination: path.resolve(Vars.binaries_path, "git-bundle.zip"), + extract: path.resolve(Vars.binaries_path, "git-bin"), + requireOs: ["win32"], + rewriteExecutionPermission: true, + deleteBeforeExtract: true, + }, + { + id: "rclone-bin", + finalBin: Vars.rclone_bin, + url: resolveRemoteBinPath(`${baseURL}/rclone-bin`, "rclone-bin.zip"), + destination: path.resolve(Vars.binaries_path, "rclone-bin.zip"), + extract: path.resolve(Vars.binaries_path, "rclone-bin"), + requireOs: ["win32"], + rewriteExecutionPermission: true, + deleteBeforeExtract: true, + }, + { + id: "java_jre_bin", + finalBin: Vars.java_jre_bin, + url: async (os, arch) => { + const { data } = await axios({ + method: "GET", + url: "https://api.azul.com/metadata/v1/zulu/packages", + params: { + arch: arch, + java_version: "JAVA_22", + os: os, + archive_type: "zip", + javafx_bundled: "false", + java_package_type: "jre", + page_size: "1", + } + }) + + return data[0].download_url + }, + destination: path.resolve(Vars.binaries_path, "java-jre.zip"), + extract: path.resolve(Vars.binaries_path, "java_jre_bin"), + extractTargetFromName: true, + moveDirs: [ + { + from: path.resolve(Vars.binaries_path, "java_jre_bin", "zulu-22.jre", "Contents"), + to: path.resolve(Vars.binaries_path, "java_jre_bin", "Contents"), + deleteParentBefore: true + } + ], + rewriteExecutionPermission: path.resolve(Vars.binaries_path, "java_jre_bin"), + deleteBeforeExtract: true, + }, +] \ No newline at end of file diff --git a/packages/core/src/utils/chmodRecursive.js b/packages/core/src/utils/chmodRecursive.js new file mode 100644 index 0000000..8a1d7a1 --- /dev/null +++ b/packages/core/src/utils/chmodRecursive.js @@ -0,0 +1,16 @@ +import fs from "node:fs" +import path from "node:path" + +async function chmodRecursive(target, mode) { + if (fs.lstatSync(target).isDirectory()) { + const files = await fs.promises.readdir(target, { withFileTypes: true }) + + for (const file of files) { + await chmodRecursive(path.join(target, file.name), mode) + } + } else { + await fs.promises.chmod(target, mode) + } +} + +export default chmodRecursive diff --git a/packages/core/src/utils/extractFile.js b/packages/core/src/utils/extractFile.js new file mode 100644 index 0000000..cc22a54 --- /dev/null +++ b/packages/core/src/utils/extractFile.js @@ -0,0 +1,46 @@ +import fs from "node:fs" +import path from "node:path" +import { pipeline as streamPipeline } from "node:stream/promises" + +import { extractFull } from "node-7z" +import unzipper from "unzipper" + +import Vars from "../vars" + +const Log = Logger.child({ service: "EXTRACTOR" }) + +export async function extractFile(file, dest) { + const ext = path.extname(file) + + Log.info(`Extracting ${file} to ${dest}`) + + switch (ext) { + case ".zip": { + await streamPipeline( + fs.createReadStream(file), + unzipper.Extract({ + path: dest, + }) + ) + break + } + case ".7z": { + await extractFull(file, dest, { + $bin: Vars.sevenzip_bin, + }) + break + } + case ".gz": { + await extractFull(file, dest, { + $bin: Vars.sevenzip_bin + }) + break + } + default: + throw new Error(`Unsupported file extension: ${ext}`) + } + + return dest +} + +export default extractFile \ No newline at end of file diff --git a/packages/core/src/utils/parseStringVars.js b/packages/core/src/utils/parseStringVars.js new file mode 100644 index 0000000..9042d92 --- /dev/null +++ b/packages/core/src/utils/parseStringVars.js @@ -0,0 +1,21 @@ +export default function parseStringVars(str, pkg) { + if (!pkg) { + return str + } + + const vars = { + id: pkg.id, + name: pkg.name, + version: pkg.version, + install_path: pkg.install_path, + remote: pkg.remote, + } + + const regex = /%([^%]+)%/g + + str = str.replace(regex, (match, varName) => { + return vars[varName] + }) + + return str +} \ No newline at end of file diff --git a/src/main/utils/readDirRecurse.js b/packages/core/src/utils/readDirRecurse.js similarity index 100% rename from src/main/utils/readDirRecurse.js rename to packages/core/src/utils/readDirRecurse.js diff --git a/packages/core/src/utils/resolveOs.js b/packages/core/src/utils/resolveOs.js new file mode 100644 index 0000000..1bf58de --- /dev/null +++ b/packages/core/src/utils/resolveOs.js @@ -0,0 +1,17 @@ +import os from "node:os" + +export default () => { + if (os.platform() === "win32") { + return "windows" + } + + if (os.platform() === "darwin") { + return "macos" + } + + if (os.platform() === "linux") { + return "linux" + } + + return os.platform() +} \ No newline at end of file diff --git a/packages/core/src/utils/resolveRemoteBinPath.js b/packages/core/src/utils/resolveRemoteBinPath.js new file mode 100644 index 0000000..acc8926 --- /dev/null +++ b/packages/core/src/utils/resolveRemoteBinPath.js @@ -0,0 +1,15 @@ +export default (pre, post) => { + let url = null + + if (process.platform === "darwin") { + url = `${pre}/mac/${process.arch}/${post}` + } + else if (process.platform === "win32") { + url = `${pre}/win/${process.arch}/${post}` + } + else { + url = `${pre}/linux/${process.arch}/${post}` + } + + return url +} \ No newline at end of file diff --git a/packages/core/src/vars.js b/packages/core/src/vars.js new file mode 100644 index 0000000..3fc23fc --- /dev/null +++ b/packages/core/src/vars.js @@ -0,0 +1,35 @@ +import path from "node:path" +import upath from "upath" + +const isWin = process.platform.includes("win") +const isMac = process.platform.includes("darwin") + +const runtimeName = "rs-relic" + +const userdata_path = upath.normalizeSafe(path.resolve( + process.env.APPDATA || + (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"), +)) +const runtime_path = upath.normalizeSafe(path.join(userdata_path, runtimeName)) +const cache_path = upath.normalizeSafe(path.join(runtime_path, "cache")) +const packages_path = upath.normalizeSafe(path.join(runtime_path, "packages")) +const binaries_path = upath.normalizeSafe(path.resolve(runtime_path, "binaries")) +const db_path = upath.normalizeSafe(path.resolve(runtime_path, "db.json")) + +const binaries = { + sevenzip_bin: upath.normalizeSafe(path.resolve(binaries_path, "7z-bin", isWin ? "7za.exe" : "7za")), + git_bin: upath.normalizeSafe(path.resolve(binaries_path, "git-bin", "bin", isWin ? "git.exe" : "git")), + rclone_bin: upath.normalizeSafe(path.resolve(binaries_path, "rclone-bin", isWin ? "rclone.exe" : "rclone")), + java_jre_bin: upath.normalizeSafe(path.resolve(binaries_path, "java_jre_bin", (isMac ? "Contents/Home/bin/java" : (isWin ? "bin/java.exe" : "bin/java")))), +} + +export default { + runtimeName, + db_path, + userdata_path, + runtime_path, + cache_path, + packages_path, + binaries_path, + ...binaries, +} \ No newline at end of file diff --git a/packages/gui/.gitignore b/packages/gui/.gitignore new file mode 100644 index 0000000..672ad48 --- /dev/null +++ b/packages/gui/.gitignore @@ -0,0 +1,41 @@ +# Secrets +/**/**/.env +/**/**/origin.server +/**/**/server.manifest +/**/**/server.registry +/**/**/*.secret.* + +/**/**/_shared + +# Trash +/**/**/*.log +/**/**/dumps.log +/**/**/.crash.log +/**/**/.tmp +/**/**/.cache +/**/**/cache +/**/**/out +/**/**/.out +/**/**/dist +/**/**/node_modules +/**/**/corenode_modules +/**/**/.DS_Store +/**/**/package-lock.json +/**/**/yarn.lock +/**/**/.evite +/**/**/build +/**/**/uploads +/**/**/d_data +/**/**/*.tar +/**/**/*.7z +/**/**/*.zip +/**/**/*.env + +# Logs +/**/**/npm-debug.log* +/**/**/yarn-error.log +/**/**/dumps.log +/**/**/corenode.log + +# Temporal configurations +/**/**/.aliaser diff --git a/dev-app-update.yml b/packages/gui/dev-app-update.yml similarity index 100% rename from dev-app-update.yml rename to packages/gui/dev-app-update.yml diff --git a/electron-builder.yml b/packages/gui/electron-builder.yml similarity index 100% rename from electron-builder.yml rename to packages/gui/electron-builder.yml diff --git a/electron.vite.config.js b/packages/gui/electron.vite.config.js similarity index 100% rename from electron.vite.config.js rename to packages/gui/electron.vite.config.js diff --git a/packages/gui/package.json b/packages/gui/package.json new file mode 100644 index 0000000..fe0b25c --- /dev/null +++ b/packages/gui/package.json @@ -0,0 +1,75 @@ +{ + "name": "@ragestudio/relic-gui", + "version": "0.15.0", + "description": "RageStudio Relic, yet another package manager.", + "main": "./out/main/index.js", + "author": "RageStudio", + "license": "MIT", + "scripts": { + "format": "prettier --write .", + "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "electron-vite build", + "postinstall": "electron-builder install-app-deps", + "pack:win": "electron-builder --win --config", + "pack:mac": "electron-builder --mac --config", + "pack:linux": "electron-builder --linux --config", + "build:win": "npm run build && npm run pack:win", + "build:mac": "npm run build && npm run pack:mac", + "build:linux": "npm run build && npm run pack:linux" + }, + "dependencies": { + "@electron-toolkit/preload": "^2.0.0", + "@electron-toolkit/utils": "^2.0.0", + "@getstation/electron-google-oauth2": "^14.0.0", + "@imjs/electron-differential-updater": "^5.1.7", + "@loadable/component": "^5.16.3", + "@ragestudio/hermes": "^0.1.1", + "adm-zip": "^0.5.10", + "antd": "^5.13.2", + "checksum": "^1.0.0", + "classnames": "^2.3.2", + "electron-differential-updater": "^4.3.2", + "electron-is-dev": "^2.0.0", + "electron-store": "^8.1.0", + "electron-updater": "^6.1.1", + "googleapis": "^105.0.0", + "got": "11.8.3", + "human-format": "^1.2.0", + "less": "^4.2.0", + "lodash": "^4.17.21", + "merge-stream": "^2.0.0", + "node-7z": "^3.0.0", + "open": "8.4.2", + "progress-stream": "^2.0.0", + "protocol-registry": "^1.4.1", + "react-icons": "^4.11.0", + "react-router-dom": "6.6.2", + "react-spinners": "^0.13.8", + "react-spring": "^9.7.3", + "react-motion": "0.5.2", + "request": "^2.88.2", + "rimraf": "^5.0.5", + "signal-exit": "^4.1.0", + "unzipper": "^0.10.14", + "upath": "^2.0.1", + "uuid": "^9.0.1", + "which": "^4.0.0", + "winreg": "^1.2.5" + }, + "devDependencies": { + "@electron-toolkit/eslint-config": "^1.0.1", + "@electron-toolkit/eslint-config-prettier": "^1.0.1", + "@vitejs/plugin-react": "^4.0.4", + "electron": "^25.6.0", + "electron-builder": "^24.6.3", + "electron-vite": "^1.0.27", + "eslint": "^8.47.0", + "eslint-plugin-react": "^7.33.2", + "prettier": "^3.0.2", + "react": "^17.0.2", + "react-dom": "^17.0.2", + "vite": "^4.4.9" + } +} diff --git a/resources/icon.ico b/packages/gui/resources/icon.ico similarity index 100% rename from resources/icon.ico rename to packages/gui/resources/icon.ico diff --git a/resources/icon.png b/packages/gui/resources/icon.png similarity index 100% rename from resources/icon.png rename to packages/gui/resources/icon.png diff --git a/resources/icon.svg b/packages/gui/resources/icon.svg similarity index 100% rename from resources/icon.svg rename to packages/gui/resources/icon.svg diff --git a/src/main/auth.js b/packages/gui/src/main/auth.js similarity index 100% rename from src/main/auth.js rename to packages/gui/src/main/auth.js diff --git a/src/main/commands/apply.js b/packages/gui/src/main/commands/apply.js similarity index 100% rename from src/main/commands/apply.js rename to packages/gui/src/main/commands/apply.js diff --git a/src/main/commands/execute.js b/packages/gui/src/main/commands/execute.js similarity index 100% rename from src/main/commands/execute.js rename to packages/gui/src/main/commands/execute.js diff --git a/src/main/commands/install.js b/packages/gui/src/main/commands/install.js similarity index 100% rename from src/main/commands/install.js rename to packages/gui/src/main/commands/install.js diff --git a/src/main/commands/uninstall.js b/packages/gui/src/main/commands/uninstall.js similarity index 100% rename from src/main/commands/uninstall.js rename to packages/gui/src/main/commands/uninstall.js diff --git a/src/main/commands/update.js b/packages/gui/src/main/commands/update.js similarity index 100% rename from src/main/commands/update.js rename to packages/gui/src/main/commands/update.js diff --git a/src/main/defaults/local_db.js b/packages/gui/src/main/defaults/local_db.js similarity index 100% rename from src/main/defaults/local_db.js rename to packages/gui/src/main/defaults/local_db.js diff --git a/src/main/defaults/pkg_manifest.js b/packages/gui/src/main/defaults/pkg_manifest.js similarity index 100% rename from src/main/defaults/pkg_manifest.js rename to packages/gui/src/main/defaults/pkg_manifest.js diff --git a/src/main/generic_steps/drive.js b/packages/gui/src/main/generic_steps/drive.js similarity index 100% rename from src/main/generic_steps/drive.js rename to packages/gui/src/main/generic_steps/drive.js diff --git a/src/main/generic_steps/git_clone.js b/packages/gui/src/main/generic_steps/git_clone.js similarity index 100% rename from src/main/generic_steps/git_clone.js rename to packages/gui/src/main/generic_steps/git_clone.js diff --git a/src/main/generic_steps/git_pull.js b/packages/gui/src/main/generic_steps/git_pull.js similarity index 100% rename from src/main/generic_steps/git_pull.js rename to packages/gui/src/main/generic_steps/git_pull.js diff --git a/src/main/generic_steps/git_reset.js b/packages/gui/src/main/generic_steps/git_reset.js similarity index 100% rename from src/main/generic_steps/git_reset.js rename to packages/gui/src/main/generic_steps/git_reset.js diff --git a/src/main/generic_steps/http.js b/packages/gui/src/main/generic_steps/http.js similarity index 100% rename from src/main/generic_steps/http.js rename to packages/gui/src/main/generic_steps/http.js diff --git a/src/main/generic_steps/index.js b/packages/gui/src/main/generic_steps/index.js similarity index 100% rename from src/main/generic_steps/index.js rename to packages/gui/src/main/generic_steps/index.js diff --git a/src/main/index.js b/packages/gui/src/main/index.js similarity index 95% rename from src/main/index.js rename to packages/gui/src/main/index.js index 2a55508..7732d37 100644 --- a/src/main/index.js +++ b/packages/gui/src/main/index.js @@ -1,5 +1,6 @@ -import sendToRender from "./utils/sendToRender" +import RelicCore from "../../../core/src/index" +import sendToRender from "./utils/sendToRender" global.SettingsStore = new Store({ name: "settings", watch: true, @@ -14,13 +15,8 @@ import Store from "electron-store" import pkg from "../../package.json" -import setup from "./setup" - import PkgManager from "./manager" import { readManifest } from "./utils/readManifest" - -import GoogleDriveAPI from "./lib/google_drive" - import AuthService from "./auth" const { autoUpdater } = require("electron-differential-updater") @@ -34,6 +30,8 @@ class ElectronApp { this.win = null } + core = new RelicCore() + authService = global.authService = new AuthService() handlers = { @@ -105,7 +103,7 @@ class ElectronApp { }, "app:init": async (event, data) => { try { - await setup() + await this.core.setup() } catch (err) { console.error(err) @@ -115,14 +113,9 @@ class ElectronApp { }) } - // check if can decode google drive token - const googleDrive_enabled = !!(await GoogleDriveAPI.readCredentials()) - return { pkg: pkg, - authorizedServices: { - drive: googleDrive_enabled - } + authorizedServices: {} } } } @@ -288,8 +281,6 @@ class ElectronApp { } } - await GoogleDriveAPI.init() - await this.createWindow() if (!isDev) { diff --git a/src/main/lib/auth/index.js b/packages/gui/src/main/lib/auth/index.js similarity index 100% rename from src/main/lib/auth/index.js rename to packages/gui/src/main/lib/auth/index.js diff --git a/packages/gui/src/main/lib/execa/index.d.ts b/packages/gui/src/main/lib/execa/index.d.ts new file mode 100755 index 0000000..7cef754 --- /dev/null +++ b/packages/gui/src/main/lib/execa/index.d.ts @@ -0,0 +1,955 @@ +import {type Buffer} from 'node:buffer'; +import {type ChildProcess} from 'node:child_process'; +import {type Stream, type Readable as ReadableStream, type Writable as WritableStream} from 'node:stream'; + +export type StdioOption = + | 'pipe' + | 'overlapped' + | 'ipc' + | 'ignore' + | 'inherit' + | Stream + | number + | undefined; + +type EncodingOption = + | 'utf8' + // eslint-disable-next-line unicorn/text-encoding-identifier-case + | 'utf-8' + | 'utf16le' + | 'utf-16le' + | 'ucs2' + | 'ucs-2' + | 'latin1' + | 'binary' + | 'ascii' + | 'hex' + | 'base64' + | 'base64url' + | 'buffer' + | null + | undefined; +type DefaultEncodingOption = 'utf8'; +type BufferEncodingOption = 'buffer' | null; + +export type CommonOptions = { + /** + Kill the spawned process when the parent process exits unless either: + - the spawned process is [`detached`](https://nodejs.org/api/child_process.html#child_process_options_detached) + - the parent process is terminated abruptly, for example, with `SIGKILL` as opposed to `SIGTERM` or a normal exit + + @default true + */ + readonly cleanup?: boolean; + + /** + Prefer locally installed binaries when looking for a binary to execute. + + If you `$ npm install foo`, you can then `execa('foo')`. + + @default `true` with `$`, `false` otherwise + */ + readonly preferLocal?: boolean; + + /** + Preferred path to find locally installed binaries in (use with `preferLocal`). + + @default process.cwd() + */ + readonly localDir?: string | URL; + + /** + Path to the Node.js executable to use in child processes. + + This can be either an absolute path or a path relative to the `cwd` option. + + Requires `preferLocal` to be `true`. + + For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process. + + @default process.execPath + */ + readonly execPath?: string; + + /** + Buffer the output from the spawned process. When set to `false`, you must read the output of `stdout` and `stderr` (or `all` if the `all` option is `true`). Otherwise the returned promise will not be resolved/rejected. + + If the spawned process fails, `error.stdout`, `error.stderr`, and `error.all` will contain the buffered data. + + @default true + */ + readonly buffer?: boolean; + + /** + Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). + + @default `inherit` with `$`, `pipe` otherwise + */ + readonly stdin?: StdioOption; + + /** + Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). + + @default 'pipe' + */ + readonly stdout?: StdioOption; + + /** + Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). + + @default 'pipe' + */ + readonly stderr?: StdioOption; + + /** + Setting this to `false` resolves the promise with the error instead of rejecting it. + + @default true + */ + readonly reject?: boolean; + + /** + Add an `.all` property on the promise and the resolved value. The property contains the output of the process with `stdout` and `stderr` interleaved. + + @default false + */ + readonly all?: boolean; + + /** + Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from the output. + + @default true + */ + readonly stripFinalNewline?: boolean; + + /** + Set to `false` if you don't want to extend the environment variables when providing the `env` property. + + @default true + */ + readonly extendEnv?: boolean; + + /** + Current working directory of the child process. + + @default process.cwd() + */ + readonly cwd?: string | URL; + + /** + Environment key-value pairs. Extends automatically from `process.env`. Set `extendEnv` to `false` if you don't want this. + + @default process.env + */ + readonly env?: NodeJS.ProcessEnv; + + /** + Explicitly set the value of `argv[0]` sent to the child process. This will be set to `command` or `file` if not specified. + */ + readonly argv0?: string; + + /** + Child's [stdio](https://nodejs.org/api/child_process.html#child_process_options_stdio) configuration. + + @default 'pipe' + */ + readonly stdio?: 'pipe' | 'overlapped' | 'ignore' | 'inherit' | readonly StdioOption[]; + + /** + Specify the kind of serialization used for sending messages between processes when using the `stdio: 'ipc'` option or `execaNode()`: + - `json`: Uses `JSON.stringify()` and `JSON.parse()`. + - `advanced`: Uses [`v8.serialize()`](https://nodejs.org/api/v8.html#v8_v8_serialize_value) + + [More info.](https://nodejs.org/api/child_process.html#child_process_advanced_serialization) + + @default 'json' + */ + readonly serialization?: 'json' | 'advanced'; + + /** + Prepare child to run independently of its parent process. Specific behavior [depends on the platform](https://nodejs.org/api/child_process.html#child_process_options_detached). + + @default false + */ + readonly detached?: boolean; + + /** + Sets the user identity of the process. + */ + readonly uid?: number; + + /** + Sets the group identity of the process. + */ + readonly gid?: number; + + /** + If `true`, runs `command` inside of a shell. Uses `/bin/sh` on UNIX and `cmd.exe` on Windows. A different shell can be specified as a string. The shell should understand the `-c` switch on UNIX or `/d /s /c` on Windows. + + We recommend against using this option since it is: + - not cross-platform, encouraging shell-specific syntax. + - slower, because of the additional shell interpretation. + - unsafe, potentially allowing command injection. + + @default false + */ + readonly shell?: boolean | string; + + /** + Specify the character encoding used to decode the `stdout` and `stderr` output. If set to `'buffer'` or `null`, then `stdout` and `stderr` will be a `Buffer` instead of a string. + + @default 'utf8' + */ + readonly encoding?: EncodingType; + + /** + If `timeout` is greater than `0`, the parent will send the signal identified by the `killSignal` property (the default is `SIGTERM`) if the child runs longer than `timeout` milliseconds. + + @default 0 + */ + readonly timeout?: number; + + /** + Largest amount of data in bytes allowed on `stdout` or `stderr`. Default: 100 MB. + + @default 100_000_000 + */ + readonly maxBuffer?: number; + + /** + Signal value to be used when the spawned process will be killed. + + @default 'SIGTERM' + */ + readonly killSignal?: string | number; + + /** + You can abort the spawned process using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). + + When `AbortController.abort()` is called, [`.isCanceled`](https://github.com/sindresorhus/execa#iscanceled) becomes `true`. + + @example + ``` + import {execa} from 'execa'; + + const abortController = new AbortController(); + const subprocess = execa('node', [], {signal: abortController.signal}); + + setTimeout(() => { + abortController.abort(); + }, 1000); + + try { + await subprocess; + } catch (error) { + console.log(subprocess.killed); // true + console.log(error.isCanceled); // true + } + ``` + */ + readonly signal?: AbortSignal; + + /** + If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`. + + @default false + */ + readonly windowsVerbatimArguments?: boolean; + + /** + On Windows, do not create a new console window. Please note this also prevents `CTRL-C` [from working](https://github.com/nodejs/node/issues/29837) on Windows. + + @default true + */ + readonly windowsHide?: boolean; + + /** + Print each command on `stderr` before executing it. + + This can also be enabled by setting the `NODE_DEBUG=execa` environment variable in the current process. + + @default false + */ + readonly verbose?: boolean; +}; + +export type Options = { + /** + Write some input to the `stdin` of your binary. + + If the input is a file, use the `inputFile` option instead. + */ + readonly input?: string | Buffer | ReadableStream; + + /** + Use a file as input to the the `stdin` of your binary. + + If the input is not a file, use the `input` option instead. + */ + readonly inputFile?: string; +} & CommonOptions; + +export type SyncOptions = { + /** + Write some input to the `stdin` of your binary. + + If the input is a file, use the `inputFile` option instead. + */ + readonly input?: string | Buffer; + + /** + Use a file as input to the the `stdin` of your binary. + + If the input is not a file, use the `input` option instead. + */ + readonly inputFile?: string; +} & CommonOptions; + +export type NodeOptions = { + /** + The Node.js executable to use. + + @default process.execPath + */ + readonly nodePath?: string; + + /** + List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable. + + @default process.execArgv + */ + readonly nodeOptions?: string[]; +} & Options; + +type StdoutStderrAll = string | Buffer | undefined; + +export type ExecaReturnBase = { + /** + The file and arguments that were run, for logging purposes. + + This is not escaped and should not be executed directly as a process, including using `execa()` or `execaCommand()`. + */ + command: string; + + /** + Same as `command` but escaped. + + This is meant to be copy and pasted into a shell, for debugging purposes. + Since the escaping is fairly basic, this should not be executed directly as a process, including using `execa()` or `execaCommand()`. + */ + escapedCommand: string; + + /** + The numeric exit code of the process that was run. + */ + exitCode: number; + + /** + The output of the process on stdout. + */ + stdout: StdoutStderrType; + + /** + The output of the process on stderr. + */ + stderr: StdoutStderrType; + + /** + Whether the process failed to run. + */ + failed: boolean; + + /** + Whether the process timed out. + */ + timedOut: boolean; + + /** + Whether the process was killed. + */ + killed: boolean; + + /** + The name of the signal that was used to terminate the process. For example, `SIGFPE`. + + If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. + */ + signal?: string; + + /** + A human-friendly description of the signal that was used to terminate the process. For example, `Floating point arithmetic error`. + + If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen. + */ + signalDescription?: string; + + /** + The `cwd` of the command if provided in the command options. Otherwise it is `process.cwd()`. + */ + cwd: string; +}; + +export type ExecaSyncReturnValue = { +} & ExecaReturnBase; + +/** +Result of a child process execution. On success this is a plain object. On failure this is also an `Error` instance. + +The child process fails when: +- its exit code is not `0` +- it was killed with a signal +- timing out +- being canceled +- there's not enough memory or there are already too many child processes +*/ +export type ExecaReturnValue = { + /** + The output of the process with `stdout` and `stderr` interleaved. + + This is `undefined` if either: + - the `all` option is `false` (default value) + - `execaSync()` was used + */ + all?: StdoutStderrType; + + /** + Whether the process was canceled. + + You can cancel the spawned process using the [`signal`](https://github.com/sindresorhus/execa#signal-1) option. + */ + isCanceled: boolean; +} & ExecaSyncReturnValue; + +export type ExecaSyncError = { + /** + Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored. + + The child process stderr then stdout are appended to the end, separated with newlines and not interleaved. + */ + message: string; + + /** + This is the same as the `message` property except it does not include the child process stdout/stderr. + */ + shortMessage: string; + + /** + Original error message. This is the same as the `message` property except it includes neither the child process stdout/stderr nor some additional information added by Execa. + + This is `undefined` unless the child process exited due to an `error` event or a timeout. + */ + originalMessage?: string; +} & Error & ExecaReturnBase; + +export type ExecaError = { + /** + The output of the process with `stdout` and `stderr` interleaved. + + This is `undefined` if either: + - the `all` option is `false` (default value) + - `execaSync()` was used + */ + all?: StdoutStderrType; + + /** + Whether the process was canceled. + */ + isCanceled: boolean; +} & ExecaSyncError; + +export type KillOptions = { + /** + Milliseconds to wait for the child process to terminate before sending `SIGKILL`. + + Can be disabled with `false`. + + @default 5000 + */ + forceKillAfterTimeout?: number | false; +}; + +export type ExecaChildPromise = { + /** + Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr). + + This is `undefined` if either: + - the `all` option is `false` (the default value) + - both `stdout` and `stderr` options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio) + */ + all?: ReadableStream; + + catch( + onRejected?: (reason: ExecaError) => ResultType | PromiseLike + ): Promise | ResultType>; + + /** + Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal), except if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. Note that this graceful termination does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events) (`SIGKILL` and `SIGTERM` has the same effect of force-killing the process immediately.) If you want to achieve graceful termination on Windows, you have to use other means, such as [`taskkill`](https://github.com/sindresorhus/taskkill). + */ + kill(signal?: string, options?: KillOptions): void; + + /** + Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This used to be preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. But now this is deprecated and you should either use `.kill()` or the `signal` option when creating the child process. + */ + cancel(): void; + + /** + [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be: + - Another `execa()` return value + - A writable stream + - A file path string + + If the `target` is another `execa()` return value, it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the final result. + + The `stdout` option] must be kept as `pipe`, its default value. + */ + pipeStdout?>(target: Target): Target; + pipeStdout?(target: WritableStream | string): ExecaChildProcess; + + /** + Like `pipeStdout()` but piping the child process's `stderr` instead. + + The `stderr` option must be kept as `pipe`, its default value. + */ + pipeStderr?>(target: Target): Target; + pipeStderr?(target: WritableStream | string): ExecaChildProcess; + + /** + Combines both `pipeStdout()` and `pipeStderr()`. + + Either the `stdout` option or the `stderr` option must be kept as `pipe`, their default value. Also, the `all` option must be set to `true`. + */ + pipeAll?>(target: Target): Target; + pipeAll?(target: WritableStream | string): ExecaChildProcess; +}; + +export type ExecaChildProcess = ChildProcess & +ExecaChildPromise & +Promise>; + +/** +Executes a command using `file ...arguments`. `arguments` are specified as an array of strings. Returns a `childProcess`. + +Arguments are automatically escaped. They can contain any character, including spaces. + +This is the preferred method when executing single commands. + +@param file - The program/script to execute. +@param arguments - Arguments to pass to `file` on execution. +@returns An `ExecaChildProcess` that is both: + - a `Promise` resolving or rejecting with a `childProcessResult`. + - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. +@throws A `childProcessResult` error + +@example Promise interface +``` +import {execa} from 'execa'; + +const {stdout} = await execa('echo', ['unicorns']); +console.log(stdout); +//=> 'unicorns' +``` + +@example Redirect output to a file +``` +import {execa} from 'execa'; + +// Similar to `echo unicorns > stdout.txt` in Bash +await execa('echo', ['unicorns']).pipeStdout('stdout.txt'); + +// Similar to `echo unicorns 2> stdout.txt` in Bash +await execa('echo', ['unicorns']).pipeStderr('stderr.txt'); + +// Similar to `echo unicorns &> stdout.txt` in Bash +await execa('echo', ['unicorns'], {all: true}).pipeAll('all.txt'); +``` + +@example Redirect input from a file +``` +import {execa} from 'execa'; + +// Similar to `cat < stdin.txt` in Bash +const {stdout} = await execa('cat', {inputFile: 'stdin.txt'}); +console.log(stdout); +//=> 'unicorns' +``` + +@example Save and pipe output from a child process +``` +import {execa} from 'execa'; + +const {stdout} = await execa('echo', ['unicorns']).pipeStdout(process.stdout); +// Prints `unicorns` +console.log(stdout); +// Also returns 'unicorns' +``` + +@example Pipe multiple processes +``` +import {execa} from 'execa'; + +// Similar to `echo unicorns | cat` in Bash +const {stdout} = await execa('echo', ['unicorns']).pipeStdout(execa('cat')); +console.log(stdout); +//=> 'unicorns' +``` + +@example Handling errors +``` +import {execa} from 'execa'; + +// Catching an error +try { + await execa('unknown', ['command']); +} catch (error) { + console.log(error); + /* + { + message: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', + errno: -2, + code: 'ENOENT', + syscall: 'spawn unknown', + path: 'unknown', + spawnargs: ['command'], + originalMessage: 'spawn unknown ENOENT', + shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', + command: 'unknown command', + escapedCommand: 'unknown command', + stdout: '', + stderr: '', + failed: true, + timedOut: false, + isCanceled: false, + killed: false, + cwd: '/path/to/cwd' + } + \*\/ +} +``` + +@example Graceful termination +``` +const subprocess = execa('node'); + +setTimeout(() => { + subprocess.kill('SIGTERM', { + forceKillAfterTimeout: 2000 + }); +}, 1000); +``` +*/ +export function execa( + file: string, + arguments?: readonly string[], + options?: Options +): ExecaChildProcess; +export function execa( + file: string, + arguments?: readonly string[], + options?: Options +): ExecaChildProcess; +export function execa(file: string, options?: Options): ExecaChildProcess; +export function execa(file: string, options?: Options): ExecaChildProcess; + +/** +Same as `execa()` but synchronous. + +@param file - The program/script to execute. +@param arguments - Arguments to pass to `file` on execution. +@returns A `childProcessResult` object +@throws A `childProcessResult` error + +@example Promise interface +``` +import {execa} from 'execa'; + +const {stdout} = execaSync('echo', ['unicorns']); +console.log(stdout); +//=> 'unicorns' +``` + +@example Redirect input from a file +``` +import {execa} from 'execa'; + +// Similar to `cat < stdin.txt` in Bash +const {stdout} = execaSync('cat', {inputFile: 'stdin.txt'}); +console.log(stdout); +//=> 'unicorns' +``` + +@example Handling errors +``` +import {execa} from 'execa'; + +// Catching an error +try { + execaSync('unknown', ['command']); +} catch (error) { + console.log(error); + /* + { + message: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT', + errno: -2, + code: 'ENOENT', + syscall: 'spawnSync unknown', + path: 'unknown', + spawnargs: ['command'], + originalMessage: 'spawnSync unknown ENOENT', + shortMessage: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT', + command: 'unknown command', + escapedCommand: 'unknown command', + stdout: '', + stderr: '', + failed: true, + timedOut: false, + isCanceled: false, + killed: false, + cwd: '/path/to/cwd' + } + \*\/ +} +``` +*/ +export function execaSync( + file: string, + arguments?: readonly string[], + options?: SyncOptions +): ExecaSyncReturnValue; +export function execaSync( + file: string, + arguments?: readonly string[], + options?: SyncOptions +): ExecaSyncReturnValue; +export function execaSync(file: string, options?: SyncOptions): ExecaSyncReturnValue; +export function execaSync( + file: string, + options?: SyncOptions +): ExecaSyncReturnValue; + +/** +Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a `childProcess`. + +Arguments are automatically escaped. They can contain any character, but spaces must be escaped with a backslash like `execaCommand('echo has\\ space')`. + +This is the preferred method when executing a user-supplied `command` string, such as in a REPL. + +@param command - The program/script to execute and its arguments. +@returns An `ExecaChildProcess` that is both: + - a `Promise` resolving or rejecting with a `childProcessResult`. + - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. +@throws A `childProcessResult` error + +@example +``` +import {execaCommand} from 'execa'; + +const {stdout} = await execaCommand('echo unicorns'); +console.log(stdout); +//=> 'unicorns' +``` +*/ +export function execaCommand(command: string, options?: Options): ExecaChildProcess; +export function execaCommand(command: string, options?: Options): ExecaChildProcess; + +/** +Same as `execaCommand()` but synchronous. + +@param command - The program/script to execute and its arguments. +@returns A `childProcessResult` object +@throws A `childProcessResult` error + +@example +``` +import {execaCommandSync} from 'execa'; + +const {stdout} = execaCommandSync('echo unicorns'); +console.log(stdout); +//=> 'unicorns' +``` +*/ +export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; +export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; + +type TemplateExpression = + | string + | number + | ExecaReturnValue + | ExecaSyncReturnValue + | Array | ExecaSyncReturnValue>; + +type Execa$ = { + /** + Returns a new instance of `$` but with different default `options`. Consecutive calls are merged to previous ones. + + This can be used to either: + - Set options for a specific command: `` $(options)`command` `` + - Share options for multiple commands: `` const $$ = $(options); $$`command`; $$`otherCommand` `` + + @param options - Options to set + @returns A new instance of `$` with those `options` set + + @example + ``` + import {$} from 'execa'; + + const $$ = $({stdio: 'inherit'}); + + await $$`echo unicorns`; + //=> 'unicorns' + + await $$`echo rainbows`; + //=> 'rainbows' + ``` + */ + (options: Options): Execa$; + (options: Options): Execa$; + (options: Options): Execa$; + ( + templates: TemplateStringsArray, + ...expressions: TemplateExpression[] + ): ExecaChildProcess; + + /** + Same as $\`command\` but synchronous. + + @returns A `childProcessResult` object + @throws A `childProcessResult` error + + @example Basic + ``` + import {$} from 'execa'; + + const branch = $.sync`git branch --show-current`; + $.sync`dep deploy --branch=${branch}`; + ``` + + @example Multiple arguments + ``` + import {$} from 'execa'; + + const args = ['unicorns', '&', 'rainbows!']; + const {stdout} = $.sync`echo ${args}`; + console.log(stdout); + //=> 'unicorns & rainbows!' + ``` + + @example With options + ``` + import {$} from 'execa'; + + $.sync({stdio: 'inherit'})`echo unicorns`; + //=> 'unicorns' + ``` + + @example Shared options + ``` + import {$} from 'execa'; + + const $$ = $({stdio: 'inherit'}); + + $$.sync`echo unicorns`; + //=> 'unicorns' + + $$.sync`echo rainbows`; + //=> 'rainbows' + ``` + */ + sync( + templates: TemplateStringsArray, + ...expressions: TemplateExpression[] + ): ExecaSyncReturnValue; +}; + +/** +Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a `childProcess`. + +Arguments are automatically escaped. They can contain any character, but spaces must use `${}` like `` $`echo ${'has space'}` ``. + +This is the preferred method when executing multiple commands in a script file. + +The `command` string can inject any `${value}` with the following types: string, number, `childProcess` or an array of those types. For example: `` $`echo one ${'two'} ${3} ${['four', 'five']}` ``. For `${childProcess}`, the process's `stdout` is used. + +@returns An `ExecaChildProcess` that is both: + - a `Promise` resolving or rejecting with a `childProcessResult`. + - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. +@throws A `childProcessResult` error + +@example Basic +``` +import {$} from 'execa'; + +const branch = await $`git branch --show-current`; +await $`dep deploy --branch=${branch}`; +``` + +@example Multiple arguments +``` +import {$} from 'execa'; + +const args = ['unicorns', '&', 'rainbows!']; +const {stdout} = await $`echo ${args}`; +console.log(stdout); +//=> 'unicorns & rainbows!' +``` + +@example With options +``` +import {$} from 'execa'; + +await $({stdio: 'inherit'})`echo unicorns`; +//=> 'unicorns' +``` + +@example Shared options +``` +import {$} from 'execa'; + +const $$ = $({stdio: 'inherit'}); + +await $$`echo unicorns`; +//=> 'unicorns' + +await $$`echo rainbows`; +//=> 'rainbows' +``` +*/ +export const $: Execa$; + +/** +Execute a Node.js script as a child process. + +Arguments are automatically escaped. They can contain any character, including spaces. + +This is the preferred method when executing Node.js files. + +Like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options): + - the current Node version and options are used. This can be overridden using the `nodePath` and `nodeOptions` options. + - the `shell` option cannot be used + - an extra channel [`ipc`](https://nodejs.org/api/child_process.html#child_process_options_stdio) is passed to `stdio` + +@param scriptPath - Node.js script to execute. +@param arguments - Arguments to pass to `scriptPath` on execution. +@returns An `ExecaChildProcess` that is both: + - a `Promise` resolving or rejecting with a `childProcessResult`. + - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. +@throws A `childProcessResult` error + +@example +``` +import {execa} from 'execa'; + +await execaNode('scriptPath', ['argument']); +``` +*/ +export function execaNode( + scriptPath: string, + arguments?: readonly string[], + options?: NodeOptions +): ExecaChildProcess; +export function execaNode( + scriptPath: string, + arguments?: readonly string[], + options?: NodeOptions +): ExecaChildProcess; +export function execaNode(scriptPath: string, options?: NodeOptions): ExecaChildProcess; +export function execaNode(scriptPath: string, options?: NodeOptions): ExecaChildProcess; diff --git a/packages/gui/src/main/lib/execa/index.js b/packages/gui/src/main/lib/execa/index.js new file mode 100755 index 0000000..fca5389 --- /dev/null +++ b/packages/gui/src/main/lib/execa/index.js @@ -0,0 +1,309 @@ +import {Buffer} from 'node:buffer'; +import path from 'node:path'; +import childProcess from 'node:child_process'; +import process from 'node:process'; +import crossSpawn from 'cross-spawn'; +import stripFinalNewline from '../strip-final-newline'; +import {npmRunPathEnv} from '../npm-run-path'; +import onetime from '../onetime'; +import {makeError} from './lib/error.js'; +import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; +import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; +import {addPipeMethods} from './lib/pipe.js'; +import {handleInput, getSpawnedResult, makeAllStream, handleInputSync} from './lib/stream.js'; +import {mergePromise, getSpawnedPromise} from './lib/promise.js'; +import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js'; +import {logCommand, verboseDefault} from './lib/verbose.js'; + +const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; + +const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { + const env = extendEnv ? {...process.env, ...envOption} : envOption; + + if (preferLocal) { + return npmRunPathEnv({env, cwd: localDir, execPath}); + } + + return env; +}; + +const handleArguments = (file, args, options = {}) => { + const parsed = crossSpawn._parse(file, args, options); + file = parsed.command; + args = parsed.args; + options = parsed.options; + + options = { + maxBuffer: DEFAULT_MAX_BUFFER, + buffer: true, + stripFinalNewline: true, + extendEnv: true, + preferLocal: false, + localDir: options.cwd || process.cwd(), + execPath: process.execPath, + encoding: 'utf8', + reject: true, + cleanup: true, + all: false, + windowsHide: true, + verbose: verboseDefault, + ...options, + }; + + options.env = getEnv(options); + + options.stdio = normalizeStdio(options); + + if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { + // #116 + args.unshift('/q'); + } + + return {file, args, options, parsed}; +}; + +const handleOutput = (options, value, error) => { + if (typeof value !== 'string' && !Buffer.isBuffer(value)) { + // When `execaSync()` errors, we normalize it to '' to mimic `execa()` + return error === undefined ? undefined : ''; + } + + if (options.stripFinalNewline) { + return stripFinalNewline(value); + } + + return value; +}; + +export function execa(file, args, options) { + const parsed = handleArguments(file, args, options); + const command = joinCommand(file, args); + const escapedCommand = getEscapedCommand(file, args); + logCommand(escapedCommand, parsed.options); + + validateTimeout(parsed.options); + + let spawned; + try { + spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); + } catch (error) { + // Ensure the returned error is always both a promise and a child process + const dummySpawned = new childProcess.ChildProcess(); + const errorPromise = Promise.reject(makeError({ + error, + stdout: '', + stderr: '', + all: '', + command, + escapedCommand, + parsed, + timedOut: false, + isCanceled: false, + killed: false, + })); + mergePromise(dummySpawned, errorPromise); + return dummySpawned; + } + + const spawnedPromise = getSpawnedPromise(spawned); + const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); + const processDone = setExitHandler(spawned, parsed.options, timedPromise); + + const context = {isCanceled: false}; + + spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); + spawned.cancel = spawnedCancel.bind(null, spawned, context); + + const handlePromise = async () => { + const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); + const stdout = handleOutput(parsed.options, stdoutResult); + const stderr = handleOutput(parsed.options, stderrResult); + const all = handleOutput(parsed.options, allResult); + + if (error || exitCode !== 0 || signal !== null) { + const returnedError = makeError({ + error, + exitCode, + signal, + stdout, + stderr, + all, + command, + escapedCommand, + parsed, + timedOut, + isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false), + killed: spawned.killed, + }); + + if (!parsed.options.reject) { + return returnedError; + } + + throw returnedError; + } + + return { + command, + escapedCommand, + exitCode: 0, + stdout, + stderr, + all, + failed: false, + timedOut: false, + isCanceled: false, + killed: false, + }; + }; + + const handlePromiseOnce = onetime(handlePromise); + + handleInput(spawned, parsed.options); + + spawned.all = makeAllStream(spawned, parsed.options); + + addPipeMethods(spawned); + mergePromise(spawned, handlePromiseOnce); + return spawned; +} + +export function execaSync(file, args, options) { + const parsed = handleArguments(file, args, options); + const command = joinCommand(file, args); + const escapedCommand = getEscapedCommand(file, args); + logCommand(escapedCommand, parsed.options); + + const input = handleInputSync(parsed.options); + + let result; + try { + result = childProcess.spawnSync(parsed.file, parsed.args, {...parsed.options, input}); + } catch (error) { + throw makeError({ + error, + stdout: '', + stderr: '', + all: '', + command, + escapedCommand, + parsed, + timedOut: false, + isCanceled: false, + killed: false, + }); + } + + const stdout = handleOutput(parsed.options, result.stdout, result.error); + const stderr = handleOutput(parsed.options, result.stderr, result.error); + + if (result.error || result.status !== 0 || result.signal !== null) { + const error = makeError({ + stdout, + stderr, + error: result.error, + signal: result.signal, + exitCode: result.status, + command, + escapedCommand, + parsed, + timedOut: result.error && result.error.code === 'ETIMEDOUT', + isCanceled: false, + killed: result.signal !== null, + }); + + if (!parsed.options.reject) { + return error; + } + + throw error; + } + + return { + command, + escapedCommand, + exitCode: 0, + stdout, + stderr, + failed: false, + timedOut: false, + isCanceled: false, + killed: false, + }; +} + +const normalizeScriptStdin = ({input, inputFile, stdio}) => input === undefined && inputFile === undefined && stdio === undefined + ? {stdin: 'inherit'} + : {}; + +const normalizeScriptOptions = (options = {}) => ({ + preferLocal: true, + ...normalizeScriptStdin(options), + ...options, +}); + +function create$(options) { + function $(templatesOrOptions, ...expressions) { + if (!Array.isArray(templatesOrOptions)) { + return create$({...options, ...templatesOrOptions}); + } + + const [file, ...args] = parseTemplates(templatesOrOptions, expressions); + return execa(file, args, normalizeScriptOptions(options)); + } + + $.sync = (templates, ...expressions) => { + if (!Array.isArray(templates)) { + throw new TypeError('Please use $(options).sync`command` instead of $.sync(options)`command`.'); + } + + const [file, ...args] = parseTemplates(templates, expressions); + return execaSync(file, args, normalizeScriptOptions(options)); + }; + + return $; +} + +export const $ = create$(); + +export function execaCommand(command, options) { + const [file, ...args] = parseCommand(command); + return execa(file, args, options); +} + +export function execaCommandSync(command, options) { + const [file, ...args] = parseCommand(command); + return execaSync(file, args, options); +} + +export function execaNode(scriptPath, args, options = {}) { + if (args && !Array.isArray(args) && typeof args === 'object') { + options = args; + args = []; + } + + const stdio = normalizeStdioNode(options); + const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); + + const { + nodePath = process.execPath, + nodeOptions = defaultExecArgv, + } = options; + + return execa( + nodePath, + [ + ...nodeOptions, + scriptPath, + ...(Array.isArray(args) ? args : []), + ], + { + ...options, + stdin: undefined, + stdout: undefined, + stderr: undefined, + stdio, + shell: false, + }, + ); +} diff --git a/packages/gui/src/main/lib/execa/lib/command.js b/packages/gui/src/main/lib/execa/lib/command.js new file mode 100755 index 0000000..727ce5f --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/command.js @@ -0,0 +1,119 @@ +import {Buffer} from 'node:buffer'; +import {ChildProcess} from 'node:child_process'; + +const normalizeArgs = (file, args = []) => { + if (!Array.isArray(args)) { + return [file]; + } + + return [file, ...args]; +}; + +const NO_ESCAPE_REGEXP = /^[\w.-]+$/; + +const escapeArg = arg => { + if (typeof arg !== 'string' || NO_ESCAPE_REGEXP.test(arg)) { + return arg; + } + + return `"${arg.replaceAll('"', '\\"')}"`; +}; + +export const joinCommand = (file, args) => normalizeArgs(file, args).join(' '); + +export const getEscapedCommand = (file, args) => normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' '); + +const SPACES_REGEXP = / +/g; + +// Handle `execaCommand()` +export const parseCommand = command => { + const tokens = []; + for (const token of command.trim().split(SPACES_REGEXP)) { + // Allow spaces to be escaped by a backslash if not meant as a delimiter + const previousToken = tokens.at(-1); + if (previousToken && previousToken.endsWith('\\')) { + // Merge previous token with current one + tokens[tokens.length - 1] = `${previousToken.slice(0, -1)} ${token}`; + } else { + tokens.push(token); + } + } + + return tokens; +}; + +const parseExpression = expression => { + const typeOfExpression = typeof expression; + + if (typeOfExpression === 'string') { + return expression; + } + + if (typeOfExpression === 'number') { + return String(expression); + } + + if ( + typeOfExpression === 'object' + && expression !== null + && !(expression instanceof ChildProcess) + && 'stdout' in expression + ) { + const typeOfStdout = typeof expression.stdout; + + if (typeOfStdout === 'string') { + return expression.stdout; + } + + if (Buffer.isBuffer(expression.stdout)) { + return expression.stdout.toString(); + } + + throw new TypeError(`Unexpected "${typeOfStdout}" stdout in template expression`); + } + + throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`); +}; + +const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0 + ? [...tokens, ...nextTokens] + : [ + ...tokens.slice(0, -1), + `${tokens.at(-1)}${nextTokens[0]}`, + ...nextTokens.slice(1), + ]; + +const parseTemplate = ({templates, expressions, tokens, index, template}) => { + const templateString = template ?? templates.raw[index]; + const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean); + const newTokens = concatTokens( + tokens, + templateTokens, + templateString.startsWith(' '), + ); + + if (index === expressions.length) { + return newTokens; + } + + const expression = expressions[index]; + const expressionTokens = Array.isArray(expression) + ? expression.map(expression => parseExpression(expression)) + : [parseExpression(expression)]; + return concatTokens( + newTokens, + expressionTokens, + templateString.endsWith(' '), + ); +}; + +export const parseTemplates = (templates, expressions) => { + let tokens = []; + + for (const [index, template] of templates.entries()) { + tokens = parseTemplate({templates, expressions, tokens, index, template}); + } + + return tokens; +}; + diff --git a/packages/gui/src/main/lib/execa/lib/error.js b/packages/gui/src/main/lib/execa/lib/error.js new file mode 100755 index 0000000..761032b --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/error.js @@ -0,0 +1,87 @@ +import process from 'node:process'; +import {signalsByName} from '../../human-signals'; + +const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { + if (timedOut) { + return `timed out after ${timeout} milliseconds`; + } + + if (isCanceled) { + return 'was canceled'; + } + + if (errorCode !== undefined) { + return `failed with ${errorCode}`; + } + + if (signal !== undefined) { + return `was killed with ${signal} (${signalDescription})`; + } + + if (exitCode !== undefined) { + return `failed with exit code ${exitCode}`; + } + + return 'failed'; +}; + +export const makeError = ({ + stdout, + stderr, + all, + error, + signal, + exitCode, + command, + escapedCommand, + timedOut, + isCanceled, + killed, + parsed: {options: {timeout, cwd = process.cwd()}}, +}) => { + // `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`. + // We normalize them to `undefined` + exitCode = exitCode === null ? undefined : exitCode; + signal = signal === null ? undefined : signal; + const signalDescription = signal === undefined ? undefined : signalsByName[signal].description; + + const errorCode = error && error.code; + + const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); + const execaMessage = `Command ${prefix}: ${command}`; + const isError = Object.prototype.toString.call(error) === '[object Error]'; + const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage; + const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); + + if (isError) { + error.originalMessage = error.message; + error.message = message; + } else { + error = new Error(message); + } + + error.shortMessage = shortMessage; + error.command = command; + error.escapedCommand = escapedCommand; + error.exitCode = exitCode; + error.signal = signal; + error.signalDescription = signalDescription; + error.stdout = stdout; + error.stderr = stderr; + error.cwd = cwd; + + if (all !== undefined) { + error.all = all; + } + + if ('bufferedData' in error) { + delete error.bufferedData; + } + + error.failed = true; + error.timedOut = Boolean(timedOut); + error.isCanceled = isCanceled; + error.killed = killed && !timedOut; + + return error; +}; diff --git a/packages/gui/src/main/lib/execa/lib/kill.js b/packages/gui/src/main/lib/execa/lib/kill.js new file mode 100755 index 0000000..12ce0a1 --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/kill.js @@ -0,0 +1,102 @@ +import os from 'node:os'; +import {onExit} from 'signal-exit'; + +const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; + +// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior +export const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { + const killResult = kill(signal); + setKillTimeout(kill, signal, options, killResult); + return killResult; +}; + +const setKillTimeout = (kill, signal, options, killResult) => { + if (!shouldForceKill(signal, options, killResult)) { + return; + } + + const timeout = getForceKillAfterTimeout(options); + const t = setTimeout(() => { + kill('SIGKILL'); + }, timeout); + + // Guarded because there's no `.unref()` when `execa` is used in the renderer + // process in Electron. This cannot be tested since we don't run tests in + // Electron. + // istanbul ignore else + if (t.unref) { + t.unref(); + } +}; + +const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult; + +const isSigterm = signal => signal === os.constants.signals.SIGTERM + || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); + +const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { + if (forceKillAfterTimeout === true) { + return DEFAULT_FORCE_KILL_TIMEOUT; + } + + if (!Number.isFinite(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { + throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); + } + + return forceKillAfterTimeout; +}; + +// `childProcess.cancel()` +export const spawnedCancel = (spawned, context) => { + const killResult = spawned.kill(); + + if (killResult) { + context.isCanceled = true; + } +}; + +const timeoutKill = (spawned, signal, reject) => { + spawned.kill(signal); + reject(Object.assign(new Error('Timed out'), {timedOut: true, signal})); +}; + +// `timeout` option handling +export const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { + if (timeout === 0 || timeout === undefined) { + return spawnedPromise; + } + + let timeoutId; + const timeoutPromise = new Promise((resolve, reject) => { + timeoutId = setTimeout(() => { + timeoutKill(spawned, killSignal, reject); + }, timeout); + }); + + const safeSpawnedPromise = spawnedPromise.finally(() => { + clearTimeout(timeoutId); + }); + + return Promise.race([timeoutPromise, safeSpawnedPromise]); +}; + +export const validateTimeout = ({timeout}) => { + if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) { + throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); + } +}; + +// `cleanup` option handling +export const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { + if (!cleanup || detached) { + return timedPromise; + } + + const removeExitHandler = onExit(() => { + spawned.kill(); + }); + + return timedPromise.finally(() => { + removeExitHandler(); + }); +}; diff --git a/packages/gui/src/main/lib/execa/lib/pipe.js b/packages/gui/src/main/lib/execa/lib/pipe.js new file mode 100755 index 0000000..f26715d --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/pipe.js @@ -0,0 +1,42 @@ +import {createWriteStream} from 'node:fs'; +import {ChildProcess} from 'node:child_process'; +import {isWritableStream} from '../../is-stream'; + +const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function'; + +const pipeToTarget = (spawned, streamName, target) => { + if (typeof target === 'string') { + spawned[streamName].pipe(createWriteStream(target)); + return spawned; + } + + if (isWritableStream(target)) { + spawned[streamName].pipe(target); + return spawned; + } + + if (!isExecaChildProcess(target)) { + throw new TypeError('The second argument must be a string, a stream or an Execa child process.'); + } + + if (!isWritableStream(target.stdin)) { + throw new TypeError('The target child process\'s stdin must be available.'); + } + + spawned[streamName].pipe(target.stdin); + return target; +}; + +export const addPipeMethods = spawned => { + if (spawned.stdout !== null) { + spawned.pipeStdout = pipeToTarget.bind(undefined, spawned, 'stdout'); + } + + if (spawned.stderr !== null) { + spawned.pipeStderr = pipeToTarget.bind(undefined, spawned, 'stderr'); + } + + if (spawned.all !== undefined) { + spawned.pipeAll = pipeToTarget.bind(undefined, spawned, 'all'); + } +}; diff --git a/packages/gui/src/main/lib/execa/lib/promise.js b/packages/gui/src/main/lib/execa/lib/promise.js new file mode 100755 index 0000000..a4773f3 --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/promise.js @@ -0,0 +1,36 @@ +// eslint-disable-next-line unicorn/prefer-top-level-await +const nativePromisePrototype = (async () => {})().constructor.prototype; + +const descriptors = ['then', 'catch', 'finally'].map(property => [ + property, + Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property), +]); + +// The return value is a mixin of `childProcess` and `Promise` +export const mergePromise = (spawned, promise) => { + for (const [property, descriptor] of descriptors) { + // Starting the main `promise` is deferred to avoid consuming streams + const value = typeof promise === 'function' + ? (...args) => Reflect.apply(descriptor.value, promise(), args) + : descriptor.value.bind(promise); + + Reflect.defineProperty(spawned, property, {...descriptor, value}); + } +}; + +// Use promises instead of `child_process` events +export const getSpawnedPromise = spawned => new Promise((resolve, reject) => { + spawned.on('exit', (exitCode, signal) => { + resolve({exitCode, signal}); + }); + + spawned.on('error', error => { + reject(error); + }); + + if (spawned.stdin) { + spawned.stdin.on('error', error => { + reject(error); + }); + } +}); diff --git a/packages/gui/src/main/lib/execa/lib/stdio.js b/packages/gui/src/main/lib/execa/lib/stdio.js new file mode 100755 index 0000000..e8c1132 --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/stdio.js @@ -0,0 +1,49 @@ +const aliases = ['stdin', 'stdout', 'stderr']; + +const hasAlias = options => aliases.some(alias => options[alias] !== undefined); + +export const normalizeStdio = options => { + if (!options) { + return; + } + + const {stdio} = options; + + if (stdio === undefined) { + return aliases.map(alias => options[alias]); + } + + if (hasAlias(options)) { + throw new Error(`It's not possible to provide \`stdio\` in combination with one of ${aliases.map(alias => `\`${alias}\``).join(', ')}`); + } + + if (typeof stdio === 'string') { + return stdio; + } + + if (!Array.isArray(stdio)) { + throw new TypeError(`Expected \`stdio\` to be of type \`string\` or \`Array\`, got \`${typeof stdio}\``); + } + + const length = Math.max(stdio.length, aliases.length); + return Array.from({length}, (value, index) => stdio[index]); +}; + +// `ipc` is pushed unless it is already present +export const normalizeStdioNode = options => { + const stdio = normalizeStdio(options); + + if (stdio === 'ipc') { + return 'ipc'; + } + + if (stdio === undefined || typeof stdio === 'string') { + return [stdio, stdio, stdio, 'ipc']; + } + + if (stdio.includes('ipc')) { + return stdio; + } + + return [...stdio, 'ipc']; +}; diff --git a/packages/gui/src/main/lib/execa/lib/stream.js b/packages/gui/src/main/lib/execa/lib/stream.js new file mode 100755 index 0000000..6912270 --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/stream.js @@ -0,0 +1,133 @@ +import {createReadStream, readFileSync} from 'node:fs'; +import {setTimeout} from 'node:timers/promises'; +import {isStream} from '../../is-stream'; +import getStream, {getStreamAsBuffer} from '../../get-stream'; +import mergeStream from 'merge-stream'; + +const validateInputOptions = input => { + if (input !== undefined) { + throw new TypeError('The `input` and `inputFile` options cannot be both set.'); + } +}; + +const getInputSync = ({input, inputFile}) => { + if (typeof inputFile !== 'string') { + return input; + } + + validateInputOptions(input); + return readFileSync(inputFile); +}; + +// `input` and `inputFile` option in sync mode +export const handleInputSync = options => { + const input = getInputSync(options); + + if (isStream(input)) { + throw new TypeError('The `input` option cannot be a stream in sync mode'); + } + + return input; +}; + +const getInput = ({input, inputFile}) => { + if (typeof inputFile !== 'string') { + return input; + } + + validateInputOptions(input); + return createReadStream(inputFile); +}; + +// `input` and `inputFile` option in async mode +export const handleInput = (spawned, options) => { + const input = getInput(options); + + if (input === undefined) { + return; + } + + if (isStream(input)) { + input.pipe(spawned.stdin); + } else { + spawned.stdin.end(input); + } +}; + +// `all` interleaves `stdout` and `stderr` +export const makeAllStream = (spawned, {all}) => { + if (!all || (!spawned.stdout && !spawned.stderr)) { + return; + } + + const mixed = mergeStream(); + + if (spawned.stdout) { + mixed.add(spawned.stdout); + } + + if (spawned.stderr) { + mixed.add(spawned.stderr); + } + + return mixed; +}; + +// On failure, `result.stdout|stderr|all` should contain the currently buffered stream +const getBufferedData = async (stream, streamPromise) => { + // When `buffer` is `false`, `streamPromise` is `undefined` and there is no buffered data to retrieve + if (!stream || streamPromise === undefined) { + return; + } + + // Wait for the `all` stream to receive the last chunk before destroying the stream + await setTimeout(0); + + stream.destroy(); + + try { + return await streamPromise; + } catch (error) { + return error.bufferedData; + } +}; + +const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => { + if (!stream || !buffer) { + return; + } + + // eslint-disable-next-line unicorn/text-encoding-identifier-case + if (encoding === 'utf8' || encoding === 'utf-8') { + return getStream(stream, {maxBuffer}); + } + + if (encoding === null || encoding === 'buffer') { + return getStreamAsBuffer(stream, {maxBuffer}); + } + + return applyEncoding(stream, maxBuffer, encoding); +}; + +const applyEncoding = async (stream, maxBuffer, encoding) => { + const buffer = await getStreamAsBuffer(stream, {maxBuffer}); + return buffer.toString(encoding); +}; + +// Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) +export const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { + const stdoutPromise = getStreamPromise(stdout, {encoding, buffer, maxBuffer}); + const stderrPromise = getStreamPromise(stderr, {encoding, buffer, maxBuffer}); + const allPromise = getStreamPromise(all, {encoding, buffer, maxBuffer: maxBuffer * 2}); + + try { + return await Promise.all([processDone, stdoutPromise, stderrPromise, allPromise]); + } catch (error) { + return Promise.all([ + {error, signal: error.signal, timedOut: error.timedOut}, + getBufferedData(stdout, stdoutPromise), + getBufferedData(stderr, stderrPromise), + getBufferedData(all, allPromise), + ]); + } +}; diff --git a/packages/gui/src/main/lib/execa/lib/verbose.js b/packages/gui/src/main/lib/execa/lib/verbose.js new file mode 100755 index 0000000..5f5490e --- /dev/null +++ b/packages/gui/src/main/lib/execa/lib/verbose.js @@ -0,0 +1,19 @@ +import {debuglog} from 'node:util'; +import process from 'node:process'; + +export const verboseDefault = debuglog('execa').enabled; + +const padField = (field, padding) => String(field).padStart(padding, '0'); + +const getTimestamp = () => { + const date = new Date(); + return `${padField(date.getHours(), 2)}:${padField(date.getMinutes(), 2)}:${padField(date.getSeconds(), 2)}.${padField(date.getMilliseconds(), 3)}`; +}; + +export const logCommand = (escapedCommand, {verbose}) => { + if (!verbose) { + return; + } + + process.stderr.write(`[${getTimestamp()}] ${escapedCommand}\n`); +}; diff --git a/src/main/lib/execa/public_lib.js b/packages/gui/src/main/lib/execa/public_lib.js similarity index 100% rename from src/main/lib/execa/public_lib.js rename to packages/gui/src/main/lib/execa/public_lib.js diff --git a/packages/gui/src/main/lib/get-stream/array-buffer.js b/packages/gui/src/main/lib/get-stream/array-buffer.js new file mode 100644 index 0000000..a547405 --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/array-buffer.js @@ -0,0 +1,84 @@ +import {getStreamContents} from './contents.js'; +import {noop, throwObjectStream, getLengthProp} from './utils.js'; + +export async function getStreamAsArrayBuffer(stream, options) { + return getStreamContents(stream, arrayBufferMethods, options); +} + +const initArrayBuffer = () => ({contents: new ArrayBuffer(0)}); + +const useTextEncoder = chunk => textEncoder.encode(chunk); +const textEncoder = new TextEncoder(); + +const useUint8Array = chunk => new Uint8Array(chunk); + +const useUint8ArrayWithOffset = chunk => new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); + +const truncateArrayBufferChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); + +// `contents` is an increasingly growing `Uint8Array`. +const addArrayBufferChunk = (convertedChunk, {contents, length: previousLength}, length) => { + const newContents = hasArrayBufferResize() ? resizeArrayBuffer(contents, length) : resizeArrayBufferSlow(contents, length); + new Uint8Array(newContents).set(convertedChunk, previousLength); + return newContents; +}; + +// Without `ArrayBuffer.resize()`, `contents` size is always a power of 2. +// This means its last bytes are zeroes (not stream data), which need to be +// trimmed at the end with `ArrayBuffer.slice()`. +const resizeArrayBufferSlow = (contents, length) => { + if (length <= contents.byteLength) { + return contents; + } + + const arrayBuffer = new ArrayBuffer(getNewContentsLength(length)); + new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); + return arrayBuffer; +}; + +// With `ArrayBuffer.resize()`, `contents` size matches exactly the size of +// the stream data. It does not include extraneous zeroes to trim at the end. +// The underlying `ArrayBuffer` does allocate a number of bytes that is a power +// of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`. +const resizeArrayBuffer = (contents, length) => { + if (length <= contents.maxByteLength) { + contents.resize(length); + return contents; + } + + const arrayBuffer = new ArrayBuffer(length, {maxByteLength: getNewContentsLength(length)}); + new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); + return arrayBuffer; +}; + +// Retrieve the closest `length` that is both >= and a power of 2 +const getNewContentsLength = length => SCALE_FACTOR ** Math.ceil(Math.log(length) / Math.log(SCALE_FACTOR)); + +const SCALE_FACTOR = 2; + +const finalizeArrayBuffer = ({contents, length}) => hasArrayBufferResize() ? contents : contents.slice(0, length); + +// `ArrayBuffer.slice()` is slow. When `ArrayBuffer.resize()` is available +// (Node >=20.0.0, Safari >=16.4 and Chrome), we can use it instead. +// eslint-disable-next-line no-warning-comments +// TODO: remove after dropping support for Node 20. +// eslint-disable-next-line no-warning-comments +// TODO: use `ArrayBuffer.transferToFixedLength()` instead once it is available +const hasArrayBufferResize = () => 'resize' in ArrayBuffer.prototype; + +const arrayBufferMethods = { + init: initArrayBuffer, + convertChunk: { + string: useTextEncoder, + buffer: useUint8Array, + arrayBuffer: useUint8Array, + dataView: useUint8ArrayWithOffset, + typedArray: useUint8ArrayWithOffset, + others: throwObjectStream, + }, + getSize: getLengthProp, + truncateChunk: truncateArrayBufferChunk, + addChunk: addArrayBufferChunk, + getFinalChunk: noop, + finalize: finalizeArrayBuffer, +}; diff --git a/packages/gui/src/main/lib/get-stream/array.js b/packages/gui/src/main/lib/get-stream/array.js new file mode 100644 index 0000000..468bad1 --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/array.js @@ -0,0 +1,32 @@ +import {getStreamContents} from './contents.js'; +import {identity, noop, getContentsProp} from './utils.js'; + +export async function getStreamAsArray(stream, options) { + return getStreamContents(stream, arrayMethods, options); +} + +const initArray = () => ({contents: []}); + +const increment = () => 1; + +const addArrayChunk = (convertedChunk, {contents}) => { + contents.push(convertedChunk); + return contents; +}; + +const arrayMethods = { + init: initArray, + convertChunk: { + string: identity, + buffer: identity, + arrayBuffer: identity, + dataView: identity, + typedArray: identity, + others: identity, + }, + getSize: increment, + truncateChunk: noop, + addChunk: addArrayChunk, + getFinalChunk: noop, + finalize: getContentsProp, +}; diff --git a/packages/gui/src/main/lib/get-stream/buffer.js b/packages/gui/src/main/lib/get-stream/buffer.js new file mode 100644 index 0000000..7d22d78 --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/buffer.js @@ -0,0 +1,20 @@ +import {getStreamAsArrayBuffer} from './array-buffer.js'; + +export async function getStreamAsBuffer(stream, options) { + if (!('Buffer' in globalThis)) { + throw new Error('getStreamAsBuffer() is only supported in Node.js'); + } + + try { + return arrayBufferToNodeBuffer(await getStreamAsArrayBuffer(stream, options)); + } catch (error) { + if (error.bufferedData !== undefined) { + error.bufferedData = arrayBufferToNodeBuffer(error.bufferedData); + } + + throw error; + } +} + +// eslint-disable-next-line n/prefer-global/buffer +const arrayBufferToNodeBuffer = arrayBuffer => globalThis.Buffer.from(arrayBuffer); diff --git a/packages/gui/src/main/lib/get-stream/contents.js b/packages/gui/src/main/lib/get-stream/contents.js new file mode 100644 index 0000000..2ca36f2 --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/contents.js @@ -0,0 +1,101 @@ +export const getStreamContents = async (stream, {init, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, finalize}, {maxBuffer = Number.POSITIVE_INFINITY} = {}) => { + if (!isAsyncIterable(stream)) { + throw new Error('The first argument must be a Readable, a ReadableStream, or an async iterable.'); + } + + const state = init(); + state.length = 0; + + try { + for await (const chunk of stream) { + const chunkType = getChunkType(chunk); + const convertedChunk = convertChunk[chunkType](chunk, state); + appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); + } + + appendFinalChunk({state, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}); + return finalize(state); + } catch (error) { + error.bufferedData = finalize(state); + throw error; + } +}; + +const appendFinalChunk = ({state, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}) => { + const convertedChunk = getFinalChunk(state); + if (convertedChunk !== undefined) { + appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); + } +}; + +const appendChunk = ({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}) => { + const chunkSize = getSize(convertedChunk); + const newLength = state.length + chunkSize; + + if (newLength <= maxBuffer) { + addNewChunk(convertedChunk, state, addChunk, newLength); + return; + } + + const truncatedChunk = truncateChunk(convertedChunk, maxBuffer - state.length); + + if (truncatedChunk !== undefined) { + addNewChunk(truncatedChunk, state, addChunk, maxBuffer); + } + + throw new MaxBufferError(); +}; + +const addNewChunk = (convertedChunk, state, addChunk, newLength) => { + state.contents = addChunk(convertedChunk, state, newLength); + state.length = newLength; +}; + +const isAsyncIterable = stream => typeof stream === 'object' && stream !== null && typeof stream[Symbol.asyncIterator] === 'function'; + +const getChunkType = chunk => { + const typeOfChunk = typeof chunk; + + if (typeOfChunk === 'string') { + return 'string'; + } + + if (typeOfChunk !== 'object' || chunk === null) { + return 'others'; + } + + // eslint-disable-next-line n/prefer-global/buffer + if (globalThis.Buffer?.isBuffer(chunk)) { + return 'buffer'; + } + + const prototypeName = objectToString.call(chunk); + + if (prototypeName === '[object ArrayBuffer]') { + return 'arrayBuffer'; + } + + if (prototypeName === '[object DataView]') { + return 'dataView'; + } + + if ( + Number.isInteger(chunk.byteLength) + && Number.isInteger(chunk.byteOffset) + && objectToString.call(chunk.buffer) === '[object ArrayBuffer]' + ) { + return 'typedArray'; + } + + return 'others'; +}; + +const {toString: objectToString} = Object.prototype; + +export class MaxBufferError extends Error { + name = 'MaxBufferError'; + + constructor() { + super('maxBuffer exceeded'); + } +} diff --git a/packages/gui/src/main/lib/get-stream/index.d.ts b/packages/gui/src/main/lib/get-stream/index.d.ts new file mode 100644 index 0000000..0a456ca --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/index.d.ts @@ -0,0 +1,119 @@ +import {type Readable} from 'node:stream'; +import {type Buffer} from 'node:buffer'; + +export class MaxBufferError extends Error { + readonly name: 'MaxBufferError'; + constructor(); +} + +type TextStreamItem = string | Buffer | ArrayBuffer | ArrayBufferView; +export type AnyStream = Readable | ReadableStream | AsyncIterable; + +export type Options = { + /** + Maximum length of the stream. If exceeded, the promise will be rejected with a `MaxBufferError`. + + Depending on the [method](#api), the length is measured with [`string.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length), [`buffer.length`](https://nodejs.org/api/buffer.html#buflength), [`arrayBuffer.byteLength`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/byteLength) or [`array.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length). + + @default Infinity + */ + readonly maxBuffer?: number; +}; + +/** +Get the given `stream` as a string. + +@returns The stream's contents as a promise. + +@example +``` +import fs from 'node:fs'; +import getStream from 'get-stream'; + +const stream = fs.createReadStream('unicorn.txt'); + +console.log(await getStream(stream)); +// ,,))))))));, +// __)))))))))))))), +// \|/ -\(((((''''((((((((. +// -*-==//////(('' . `)))))), +// /|\ ))| o ;-. '((((( ,(, +// ( `| / ) ;))))' ,_))^;(~ +// | | | ,))((((_ _____------~~~-. %,;(;(>';'~ +// o_); ; )))(((` ~---~ `:: \ %%~~)(v;(`('~ +// ; ''''```` `: `:::|\,__,%% );`'; ~ +// | _ ) / `:|`----' `-' +// ______/\/~ | / / +// /~;;.____/;;' / ___--,-( `;;;/ +// / // _;______;'------~~~~~ /;;/\ / +// // | | / ; \;;,\ +// (<_ | ; /',/-----' _> +// \_| ||_ //~;~~~~~~~~~ +// `\_| (,~~ +// \~\ +// ~~ +``` + +@example +``` +import getStream from 'get-stream'; + +const {body: readableStream} = await fetch('https://example.com'); +console.log(await getStream(readableStream)); +``` + +@example +``` +import {opendir} from 'node:fs/promises'; +import {getStreamAsArray} from 'get-stream'; + +const asyncIterable = await opendir(directory); +console.log(await getStreamAsArray(asyncIterable)); +``` +*/ +export default function getStream(stream: AnyStream, options?: Options): Promise; + +/** +Get the given `stream` as a Node.js [`Buffer`](https://nodejs.org/api/buffer.html#class-buffer). + +@returns The stream's contents as a promise. + +@example +``` +import {getStreamAsBuffer} from 'get-stream'; + +const stream = fs.createReadStream('unicorn.png'); +console.log(await getStreamAsBuffer(stream)); +``` +*/ +export function getStreamAsBuffer(stream: AnyStream, options?: Options): Promise; + +/** +Get the given `stream` as an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). + +@returns The stream's contents as a promise. + +@example +``` +import {getStreamAsArrayBuffer} from 'get-stream'; + +const {body: readableStream} = await fetch('https://example.com'); +console.log(await getStreamAsArrayBuffer(readableStream)); +``` +*/ +export function getStreamAsArrayBuffer(stream: AnyStream, options?: Options): Promise; + +/** +Get the given `stream` as an array. Unlike [other methods](#api), this supports [streams of objects](https://nodejs.org/api/stream.html#object-mode). + +@returns The stream's contents as a promise. + +@example +``` +import {getStreamAsArray} from 'get-stream'; + +const {body: readableStream} = await fetch('https://example.com'); +console.log(await getStreamAsArray(readableStream)); +``` +*/ +export function getStreamAsArray(stream: AnyStream, options?: Options): Promise; diff --git a/packages/gui/src/main/lib/get-stream/index.js b/packages/gui/src/main/lib/get-stream/index.js new file mode 100644 index 0000000..43c2dd4 --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/index.js @@ -0,0 +1,5 @@ +export {getStreamAsArray} from './array.js'; +export {getStreamAsArrayBuffer} from './array-buffer.js'; +export {getStreamAsBuffer} from './buffer.js'; +export {getStreamAsString as default} from './string.js'; +export {MaxBufferError} from './contents.js'; diff --git a/packages/gui/src/main/lib/get-stream/index.test-d.ts b/packages/gui/src/main/lib/get-stream/index.test-d.ts new file mode 100644 index 0000000..c90068f --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/index.test-d.ts @@ -0,0 +1,98 @@ +import {Buffer} from 'node:buffer'; +import {open} from 'node:fs/promises'; +import {type Readable} from 'node:stream'; +import fs from 'node:fs'; +import {expectType, expectError, expectAssignable, expectNotAssignable} from 'tsd'; +import getStream, {getStreamAsBuffer, getStreamAsArrayBuffer, getStreamAsArray, MaxBufferError, type Options, type AnyStream} from './index.js'; + +const nodeStream = fs.createReadStream('foo') as Readable; + +const fileHandle = await open('test'); +const readableStream = fileHandle.readableWebStream(); + +const asyncIterable = (value: T): AsyncGenerator => (async function * () { + yield value; +})(); +const stringAsyncIterable = asyncIterable(''); +const bufferAsyncIterable = asyncIterable(Buffer.from('')); +const arrayBufferAsyncIterable = asyncIterable(new ArrayBuffer(0)); +const dataViewAsyncIterable = asyncIterable(new DataView(new ArrayBuffer(0))); +const typedArrayAsyncIterable = asyncIterable(new Uint8Array([])); +const objectItem = {test: true}; +const objectAsyncIterable = asyncIterable(objectItem); + +expectType(await getStream(nodeStream)); +expectType(await getStream(nodeStream, {maxBuffer: 10})); +expectType(await getStream(readableStream)); +expectType(await getStream(stringAsyncIterable)); +expectType(await getStream(bufferAsyncIterable)); +expectType(await getStream(arrayBufferAsyncIterable)); +expectType(await getStream(dataViewAsyncIterable)); +expectType(await getStream(typedArrayAsyncIterable)); +expectError(await getStream(objectAsyncIterable)); +expectError(await getStream({})); +expectError(await getStream(nodeStream, {maxBuffer: '10'})); +expectError(await getStream(nodeStream, {unknownOption: 10})); +expectError(await getStream(nodeStream, {maxBuffer: 10}, {})); + +expectType(await getStreamAsBuffer(nodeStream)); +expectType(await getStreamAsBuffer(nodeStream, {maxBuffer: 10})); +expectType(await getStreamAsBuffer(readableStream)); +expectType(await getStreamAsBuffer(stringAsyncIterable)); +expectType(await getStreamAsBuffer(bufferAsyncIterable)); +expectType(await getStreamAsBuffer(arrayBufferAsyncIterable)); +expectType(await getStreamAsBuffer(dataViewAsyncIterable)); +expectType(await getStreamAsBuffer(typedArrayAsyncIterable)); +expectError(await getStreamAsBuffer(objectAsyncIterable)); +expectError(await getStreamAsBuffer({})); +expectError(await getStreamAsBuffer(nodeStream, {maxBuffer: '10'})); +expectError(await getStreamAsBuffer(nodeStream, {unknownOption: 10})); +expectError(await getStreamAsBuffer(nodeStream, {maxBuffer: 10}, {})); + +expectType(await getStreamAsArrayBuffer(nodeStream)); +expectType(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: 10})); +expectType(await getStreamAsArrayBuffer(readableStream)); +expectType(await getStreamAsArrayBuffer(stringAsyncIterable)); +expectType(await getStreamAsArrayBuffer(bufferAsyncIterable)); +expectType(await getStreamAsArrayBuffer(arrayBufferAsyncIterable)); +expectType(await getStreamAsArrayBuffer(dataViewAsyncIterable)); +expectType(await getStreamAsArrayBuffer(typedArrayAsyncIterable)); +expectError(await getStreamAsArrayBuffer(objectAsyncIterable)); +expectError(await getStreamAsArrayBuffer({})); +expectError(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: '10'})); +expectError(await getStreamAsArrayBuffer(nodeStream, {unknownOption: 10})); +expectError(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: 10}, {})); + +expectType(await getStreamAsArray(nodeStream)); +expectType(await getStreamAsArray(nodeStream, {maxBuffer: 10})); +expectType(await getStreamAsArray(readableStream)); +expectType(await getStreamAsArray(readableStream as ReadableStream)); +expectType(await getStreamAsArray(stringAsyncIterable)); +expectType(await getStreamAsArray(bufferAsyncIterable)); +expectType(await getStreamAsArray(arrayBufferAsyncIterable)); +expectType(await getStreamAsArray(dataViewAsyncIterable)); +expectType(await getStreamAsArray(typedArrayAsyncIterable)); +expectType>(await getStreamAsArray(objectAsyncIterable)); +expectError(await getStreamAsArray({})); +expectError(await getStreamAsArray(nodeStream, {maxBuffer: '10'})); +expectError(await getStreamAsArray(nodeStream, {unknownOption: 10})); +expectError(await getStreamAsArray(nodeStream, {maxBuffer: 10}, {})); + +expectAssignable(nodeStream); +expectAssignable(readableStream); +expectAssignable(stringAsyncIterable); +expectAssignable(bufferAsyncIterable); +expectAssignable(arrayBufferAsyncIterable); +expectAssignable(dataViewAsyncIterable); +expectAssignable(typedArrayAsyncIterable); +expectAssignable>(objectAsyncIterable); +expectNotAssignable(objectAsyncIterable); +expectAssignable>(stringAsyncIterable); +expectNotAssignable>(bufferAsyncIterable); +expectNotAssignable({}); + +expectAssignable({maxBuffer: 10}); +expectNotAssignable({maxBuffer: '10'}); +expectNotAssignable({unknownOption: 10}); + +expectType(new MaxBufferError()); diff --git a/packages/gui/src/main/lib/get-stream/string.js b/packages/gui/src/main/lib/get-stream/string.js new file mode 100644 index 0000000..90f94b9 --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/string.js @@ -0,0 +1,36 @@ +import {getStreamContents} from './contents.js'; +import {identity, getContentsProp, throwObjectStream, getLengthProp} from './utils.js'; + +export async function getStreamAsString(stream, options) { + return getStreamContents(stream, stringMethods, options); +} + +const initString = () => ({contents: '', textDecoder: new TextDecoder()}); + +const useTextDecoder = (chunk, {textDecoder}) => textDecoder.decode(chunk, {stream: true}); + +const addStringChunk = (convertedChunk, {contents}) => contents + convertedChunk; + +const truncateStringChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); + +const getFinalStringChunk = ({textDecoder}) => { + const finalChunk = textDecoder.decode(); + return finalChunk === '' ? undefined : finalChunk; +}; + +const stringMethods = { + init: initString, + convertChunk: { + string: identity, + buffer: useTextDecoder, + arrayBuffer: useTextDecoder, + dataView: useTextDecoder, + typedArray: useTextDecoder, + others: throwObjectStream, + }, + getSize: getLengthProp, + truncateChunk: truncateStringChunk, + addChunk: addStringChunk, + getFinalChunk: getFinalStringChunk, + finalize: getContentsProp, +}; diff --git a/packages/gui/src/main/lib/get-stream/utils.js b/packages/gui/src/main/lib/get-stream/utils.js new file mode 100644 index 0000000..af8d5e2 --- /dev/null +++ b/packages/gui/src/main/lib/get-stream/utils.js @@ -0,0 +1,11 @@ +export const identity = value => value; + +export const noop = () => undefined; + +export const getContentsProp = ({contents}) => contents; + +export const throwObjectStream = chunk => { + throw new Error(`Streams in object mode are not supported: ${String(chunk)}`); +}; + +export const getLengthProp = convertedChunk => convertedChunk.length; diff --git a/src/main/lib/google_drive/index.js b/packages/gui/src/main/lib/google_drive/index.js similarity index 100% rename from src/main/lib/google_drive/index.js rename to packages/gui/src/main/lib/google_drive/index.js diff --git a/packages/gui/src/main/lib/human-signals/core.js b/packages/gui/src/main/lib/human-signals/core.js new file mode 100644 index 0000000..e083d8f --- /dev/null +++ b/packages/gui/src/main/lib/human-signals/core.js @@ -0,0 +1,275 @@ +/* eslint-disable max-lines */ +// List of known process signals with information about them +export const SIGNALS = [ + { + name: 'SIGHUP', + number: 1, + action: 'terminate', + description: 'Terminal closed', + standard: 'posix', + }, + { + name: 'SIGINT', + number: 2, + action: 'terminate', + description: 'User interruption with CTRL-C', + standard: 'ansi', + }, + { + name: 'SIGQUIT', + number: 3, + action: 'core', + description: 'User interruption with CTRL-\\', + standard: 'posix', + }, + { + name: 'SIGILL', + number: 4, + action: 'core', + description: 'Invalid machine instruction', + standard: 'ansi', + }, + { + name: 'SIGTRAP', + number: 5, + action: 'core', + description: 'Debugger breakpoint', + standard: 'posix', + }, + { + name: 'SIGABRT', + number: 6, + action: 'core', + description: 'Aborted', + standard: 'ansi', + }, + { + name: 'SIGIOT', + number: 6, + action: 'core', + description: 'Aborted', + standard: 'bsd', + }, + { + name: 'SIGBUS', + number: 7, + action: 'core', + description: + 'Bus error due to misaligned, non-existing address or paging error', + standard: 'bsd', + }, + { + name: 'SIGEMT', + number: 7, + action: 'terminate', + description: 'Command should be emulated but is not implemented', + standard: 'other', + }, + { + name: 'SIGFPE', + number: 8, + action: 'core', + description: 'Floating point arithmetic error', + standard: 'ansi', + }, + { + name: 'SIGKILL', + number: 9, + action: 'terminate', + description: 'Forced termination', + standard: 'posix', + forced: true, + }, + { + name: 'SIGUSR1', + number: 10, + action: 'terminate', + description: 'Application-specific signal', + standard: 'posix', + }, + { + name: 'SIGSEGV', + number: 11, + action: 'core', + description: 'Segmentation fault', + standard: 'ansi', + }, + { + name: 'SIGUSR2', + number: 12, + action: 'terminate', + description: 'Application-specific signal', + standard: 'posix', + }, + { + name: 'SIGPIPE', + number: 13, + action: 'terminate', + description: 'Broken pipe or socket', + standard: 'posix', + }, + { + name: 'SIGALRM', + number: 14, + action: 'terminate', + description: 'Timeout or timer', + standard: 'posix', + }, + { + name: 'SIGTERM', + number: 15, + action: 'terminate', + description: 'Termination', + standard: 'ansi', + }, + { + name: 'SIGSTKFLT', + number: 16, + action: 'terminate', + description: 'Stack is empty or overflowed', + standard: 'other', + }, + { + name: 'SIGCHLD', + number: 17, + action: 'ignore', + description: 'Child process terminated, paused or unpaused', + standard: 'posix', + }, + { + name: 'SIGCLD', + number: 17, + action: 'ignore', + description: 'Child process terminated, paused or unpaused', + standard: 'other', + }, + { + name: 'SIGCONT', + number: 18, + action: 'unpause', + description: 'Unpaused', + standard: 'posix', + forced: true, + }, + { + name: 'SIGSTOP', + number: 19, + action: 'pause', + description: 'Paused', + standard: 'posix', + forced: true, + }, + { + name: 'SIGTSTP', + number: 20, + action: 'pause', + description: 'Paused using CTRL-Z or "suspend"', + standard: 'posix', + }, + { + name: 'SIGTTIN', + number: 21, + action: 'pause', + description: 'Background process cannot read terminal input', + standard: 'posix', + }, + { + name: 'SIGBREAK', + number: 21, + action: 'terminate', + description: 'User interruption with CTRL-BREAK', + standard: 'other', + }, + { + name: 'SIGTTOU', + number: 22, + action: 'pause', + description: 'Background process cannot write to terminal output', + standard: 'posix', + }, + { + name: 'SIGURG', + number: 23, + action: 'ignore', + description: 'Socket received out-of-band data', + standard: 'bsd', + }, + { + name: 'SIGXCPU', + number: 24, + action: 'core', + description: 'Process timed out', + standard: 'bsd', + }, + { + name: 'SIGXFSZ', + number: 25, + action: 'core', + description: 'File too big', + standard: 'bsd', + }, + { + name: 'SIGVTALRM', + number: 26, + action: 'terminate', + description: 'Timeout or timer', + standard: 'bsd', + }, + { + name: 'SIGPROF', + number: 27, + action: 'terminate', + description: 'Timeout or timer', + standard: 'bsd', + }, + { + name: 'SIGWINCH', + number: 28, + action: 'ignore', + description: 'Terminal window size changed', + standard: 'bsd', + }, + { + name: 'SIGIO', + number: 29, + action: 'terminate', + description: 'I/O is available', + standard: 'other', + }, + { + name: 'SIGPOLL', + number: 29, + action: 'terminate', + description: 'Watched event', + standard: 'other', + }, + { + name: 'SIGINFO', + number: 29, + action: 'ignore', + description: 'Request for process information', + standard: 'other', + }, + { + name: 'SIGPWR', + number: 30, + action: 'terminate', + description: 'Device running out of power', + standard: 'systemv', + }, + { + name: 'SIGSYS', + number: 31, + action: 'core', + description: 'Invalid system call', + standard: 'other', + }, + { + name: 'SIGUNUSED', + number: 31, + action: 'terminate', + description: 'Invalid system call', + standard: 'other', + }, +] +/* eslint-enable max-lines */ diff --git a/packages/gui/src/main/lib/human-signals/index.js b/packages/gui/src/main/lib/human-signals/index.js new file mode 100644 index 0000000..fb6e64b --- /dev/null +++ b/packages/gui/src/main/lib/human-signals/index.js @@ -0,0 +1,70 @@ +import { constants } from 'node:os' + +import { SIGRTMAX } from './realtime.js' +import { getSignals } from './signals.js' + +// Retrieve `signalsByName`, an object mapping signal name to signal properties. +// We make sure the object is sorted by `number`. +const getSignalsByName = () => { + const signals = getSignals() + return Object.fromEntries(signals.map(getSignalByName)) +} + +const getSignalByName = ({ + name, + number, + description, + supported, + action, + forced, + standard, +}) => [name, { name, number, description, supported, action, forced, standard }] + +export const signalsByName = getSignalsByName() + +// Retrieve `signalsByNumber`, an object mapping signal number to signal +// properties. +// We make sure the object is sorted by `number`. +const getSignalsByNumber = () => { + const signals = getSignals() + const length = SIGRTMAX + 1 + const signalsA = Array.from({ length }, (value, number) => + getSignalByNumber(number, signals), + ) + return Object.assign({}, ...signalsA) +} + +const getSignalByNumber = (number, signals) => { + const signal = findSignalByNumber(number, signals) + + if (signal === undefined) { + return {} + } + + const { name, description, supported, action, forced, standard } = signal + return { + [number]: { + name, + number, + description, + supported, + action, + forced, + standard, + }, + } +} + +// Several signals might end up sharing the same number because of OS-specific +// numbers, in which case those prevail. +const findSignalByNumber = (number, signals) => { + const signal = signals.find(({ name }) => constants.signals[name] === number) + + if (signal !== undefined) { + return signal + } + + return signals.find((signalA) => signalA.number === number) +} + +export const signalsByNumber = getSignalsByNumber() diff --git a/src/main/lib/human-signals/index.ts b/packages/gui/src/main/lib/human-signals/index.ts similarity index 100% rename from src/main/lib/human-signals/index.ts rename to packages/gui/src/main/lib/human-signals/index.ts diff --git a/packages/gui/src/main/lib/human-signals/realtime.js b/packages/gui/src/main/lib/human-signals/realtime.js new file mode 100644 index 0000000..1825d08 --- /dev/null +++ b/packages/gui/src/main/lib/human-signals/realtime.js @@ -0,0 +1,16 @@ +// List of realtime signals with information about them +export const getRealtimeSignals = () => { + const length = SIGRTMAX - SIGRTMIN + 1 + return Array.from({ length }, getRealtimeSignal) +} + +const getRealtimeSignal = (value, index) => ({ + name: `SIGRT${index + 1}`, + number: SIGRTMIN + index, + action: 'terminate', + description: 'Application-specific signal (realtime)', + standard: 'posix', +}) + +const SIGRTMIN = 34 +export const SIGRTMAX = 64 diff --git a/packages/gui/src/main/lib/human-signals/signals.js b/packages/gui/src/main/lib/human-signals/signals.js new file mode 100644 index 0000000..d76382b --- /dev/null +++ b/packages/gui/src/main/lib/human-signals/signals.js @@ -0,0 +1,34 @@ +import { constants } from 'node:os' + +import { SIGNALS } from './core.js' +import { getRealtimeSignals } from './realtime.js' + +// Retrieve list of know signals (including realtime) with information about +// them +export const getSignals = () => { + const realtimeSignals = getRealtimeSignals() + const signals = [...SIGNALS, ...realtimeSignals].map(normalizeSignal) + return signals +} + +// Normalize signal: +// - `number`: signal numbers are OS-specific. This is taken into account by +// `os.constants.signals`. However we provide a default `number` since some +// signals are not defined for some OS. +// - `forced`: set default to `false` +// - `supported`: set value +const normalizeSignal = ({ + name, + number: defaultNumber, + description, + action, + forced = false, + standard, +}) => { + const { + signals: { [name]: constantSignal }, + } = constants + const supported = constantSignal !== undefined + const number = supported ? constantSignal : defaultNumber + return { name, number, description, supported, action, forced, standard } +} diff --git a/packages/gui/src/main/lib/is-stream/index.d.ts b/packages/gui/src/main/lib/is-stream/index.d.ts new file mode 100644 index 0000000..df994e0 --- /dev/null +++ b/packages/gui/src/main/lib/is-stream/index.d.ts @@ -0,0 +1,81 @@ +import { + Stream, + Writable as WritableStream, + Readable as ReadableStream, + Duplex as DuplexStream, + Transform as TransformStream, +} from 'node:stream'; + +/** +@returns Whether `stream` is a [`Stream`](https://nodejs.org/api/stream.html#stream_stream). + +@example +``` +import fs from 'node:fs'; +import {isStream} from 'is-stream'; + +isStream(fs.createReadStream('unicorn.png')); +//=> true + +isStream({}); +//=> false +``` +*/ +export function isStream(stream: unknown): stream is Stream; + +/** +@returns Whether `stream` is a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable). + +@example +``` +import fs from 'node:fs'; +import {isWritableStream} from 'is-stream'; + +isWritableStream(fs.createWriteStrem('unicorn.txt')); +//=> true +``` +*/ +export function isWritableStream(stream: unknown): stream is WritableStream; + +/** +@returns Whether `stream` is a [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable). + +@example +``` +import fs from 'node:fs'; +import {isReadableStream} from 'is-stream'; + +isReadableStream(fs.createReadStream('unicorn.png')); +//=> true +``` +*/ +export function isReadableStream(stream: unknown): stream is ReadableStream; + +/** +@returns Whether `stream` is a [`stream.Duplex`](https://nodejs.org/api/stream.html#stream_class_stream_duplex). + +@example +``` +import {Duplex as DuplexStream} from 'node:stream'; +import {isDuplexStream} from 'is-stream'; + +isDuplexStream(new DuplexStream()); +//=> true +``` +*/ +export function isDuplexStream(stream: unknown): stream is DuplexStream; + +/** +@returns Whether `stream` is a [`stream.Transform`](https://nodejs.org/api/stream.html#stream_class_stream_transform). + +@example +``` +import fs from 'node:fs'; +import StringifyStream from 'streaming-json-stringify'; +import {isTransformStream} from 'is-stream'; + +isTransformStream(StringifyStream()); +//=> true +``` +*/ +export function isTransformStream(stream: unknown): stream is TransformStream; diff --git a/packages/gui/src/main/lib/is-stream/index.js b/packages/gui/src/main/lib/is-stream/index.js new file mode 100644 index 0000000..887e601 --- /dev/null +++ b/packages/gui/src/main/lib/is-stream/index.js @@ -0,0 +1,29 @@ +export function isStream(stream) { + return stream !== null + && typeof stream === 'object' + && typeof stream.pipe === 'function'; +} + +export function isWritableStream(stream) { + return isStream(stream) + && stream.writable !== false + && typeof stream._write === 'function' + && typeof stream._writableState === 'object'; +} + +export function isReadableStream(stream) { + return isStream(stream) + && stream.readable !== false + && typeof stream._read === 'function' + && typeof stream._readableState === 'object'; +} + +export function isDuplexStream(stream) { + return isWritableStream(stream) + && isReadableStream(stream); +} + +export function isTransformStream(stream) { + return isDuplexStream(stream) + && typeof stream._transform === 'function'; +} diff --git a/src/main/lib/lowdb/adapters/Memory.ts b/packages/gui/src/main/lib/lowdb/adapters/Memory.ts similarity index 100% rename from src/main/lib/lowdb/adapters/Memory.ts rename to packages/gui/src/main/lib/lowdb/adapters/Memory.ts diff --git a/src/main/lib/lowdb/adapters/node/DataFile.ts b/packages/gui/src/main/lib/lowdb/adapters/node/DataFile.ts similarity index 100% rename from src/main/lib/lowdb/adapters/node/DataFile.ts rename to packages/gui/src/main/lib/lowdb/adapters/node/DataFile.ts diff --git a/src/main/lib/lowdb/adapters/node/JSONFile.ts b/packages/gui/src/main/lib/lowdb/adapters/node/JSONFile.ts similarity index 100% rename from src/main/lib/lowdb/adapters/node/JSONFile.ts rename to packages/gui/src/main/lib/lowdb/adapters/node/JSONFile.ts diff --git a/src/main/lib/lowdb/adapters/node/TextFile.ts b/packages/gui/src/main/lib/lowdb/adapters/node/TextFile.ts similarity index 100% rename from src/main/lib/lowdb/adapters/node/TextFile.ts rename to packages/gui/src/main/lib/lowdb/adapters/node/TextFile.ts diff --git a/src/main/lib/lowdb/core/Low.ts b/packages/gui/src/main/lib/lowdb/core/Low.ts similarity index 100% rename from src/main/lib/lowdb/core/Low.ts rename to packages/gui/src/main/lib/lowdb/core/Low.ts diff --git a/src/main/lib/lowdb/index.ts b/packages/gui/src/main/lib/lowdb/index.ts similarity index 100% rename from src/main/lib/lowdb/index.ts rename to packages/gui/src/main/lib/lowdb/index.ts diff --git a/src/main/lib/lowdb/presets/node.ts b/packages/gui/src/main/lib/lowdb/presets/node.ts similarity index 100% rename from src/main/lib/lowdb/presets/node.ts rename to packages/gui/src/main/lib/lowdb/presets/node.ts diff --git a/packages/gui/src/main/lib/mcl/authenticator.js b/packages/gui/src/main/lib/mcl/authenticator.js new file mode 100644 index 0000000..c5973d8 --- /dev/null +++ b/packages/gui/src/main/lib/mcl/authenticator.js @@ -0,0 +1,167 @@ +const request = require('request') +const { v3 } = require('uuid') + +let uuid +let api_url = 'https://authserver.mojang.com' + +function parsePropts(array) { + if (array) { + const newObj = {} + for (const entry of array) { + if (newObj[entry.name]) { + newObj[entry.name].push(entry.value) + } else { + newObj[entry.name] = [entry.value] + } + } + return JSON.stringify(newObj) + } else { + return '{}' + } +} + +function getUUID(value) { + if (!uuid) { + uuid = v3(value, v3.DNS) + } + return uuid +} + +const Authenticator = { + getAuth: (username, password, client_token = null) => { + return new Promise((resolve, reject) => { + getUUID(username) + if (!password) { + const user = { + access_token: uuid, + client_token: client_token || uuid, + uuid, + name: username, + user_properties: '{}' + } + + return resolve(user) + } + + const requestObject = { + url: api_url + '/authenticate', + json: { + agent: { + name: 'Minecraft', + version: 1 + }, + username, + password, + clientToken: uuid, + requestUser: true + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + if (!body || !body.selectedProfile) { + return reject(new Error('Validation error: ' + response.statusMessage)) + } + + const userProfile = { + access_token: body.accessToken, + client_token: body.clientToken, + uuid: body.selectedProfile.id, + name: body.selectedProfile.name, + selected_profile: body.selectedProfile, + user_properties: parsePropts(body.user.properties) + } + + resolve(userProfile) + }) + }) + }, + validate: (accessToken, clientToken) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/validate', + json: { + accessToken, + clientToken + } + } + + request.post(requestObject, async function (error, response, body) { + if (error) return reject(error) + + if (!body) resolve(true) + else reject(body) + }) + }) + }, + refreshAuth: (accessToken, clientToken) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/refresh', + json: { + accessToken, + clientToken, + requestUser: true + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + if (!body || !body.selectedProfile) { + return reject(new Error('Validation error: ' + response.statusMessage)) + } + + const userProfile = { + access_token: body.accessToken, + client_token: getUUID(body.selectedProfile.name), + uuid: body.selectedProfile.id, + name: body.selectedProfile.name, + user_properties: parsePropts(body.user.properties) + } + + return resolve(userProfile) + }) + }) + }, + invalidate: (accessToken, clientToken) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/invalidate', + json: { + accessToken, + clientToken + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + + if (!body) return resolve(true) + else return reject(body) + }) + }) + }, + signOut: (username, password) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/signout', + json: { + username, + password + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + + if (!body) return resolve(true) + else return reject(body) + }) + }) + }, + changeApiUrl: (url) => { + api_url = url + } +} + +export default Authenticator \ No newline at end of file diff --git a/packages/gui/src/main/lib/mcl/handler.js b/packages/gui/src/main/lib/mcl/handler.js new file mode 100644 index 0000000..ca0a477 --- /dev/null +++ b/packages/gui/src/main/lib/mcl/handler.js @@ -0,0 +1,783 @@ +const fs = require('fs') +const path = require('path') +const request = require('request') +const checksum = require('checksum') +const Zip = require('adm-zip') +const child = require('child_process') +let counter = 0 + +export default class Handler { + constructor (client) { + this.client = client + this.options = client.options + this.baseRequest = request.defaults({ + pool: { maxSockets: this.options.overrides.maxSockets || 2 }, + timeout: this.options.timeout || 10000 + }) + } + + checkJava (java) { + return new Promise(resolve => { + child.exec(`"${java}" -version`, (error, stdout, stderr) => { + if (error) { + resolve({ + run: false, + message: error + }) + } else { + this.client.emit('debug', `[MCLC]: Using Java version ${stderr.match(/"(.*?)"/).pop()} ${stderr.includes('64-Bit') ? '64-bit' : '32-Bit'}`) + resolve({ + run: true + }) + } + }) + }) + } + + downloadAsync (url, directory, name, retry, type) { + return new Promise(resolve => { + fs.mkdirSync(directory, { recursive: true }) + + const _request = this.baseRequest(url) + + let receivedBytes = 0 + let totalBytes = 0 + + _request.on('response', (data) => { + if (data.statusCode === 404) { + this.client.emit('debug', `[MCLC]: Failed to download ${url} due to: File not found...`) + return resolve(false) + } + + totalBytes = parseInt(data.headers['content-length']) + }) + + _request.on('error', async (error) => { + this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${error}.` + + ` Retrying... ${retry}`) + if (retry) await this.downloadAsync(url, directory, name, false, type) + resolve() + }) + + _request.on('data', (data) => { + receivedBytes += data.length + this.client.emit('download-status', { + name: name, + type: type, + current: receivedBytes, + total: totalBytes + }) + }) + + const file = fs.createWriteStream(path.join(directory, name)) + _request.pipe(file) + + file.once('finish', () => { + this.client.emit('download', name) + resolve({ + failed: false, + asset: null + }) + }) + + file.on('error', async (e) => { + this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${e}.` + + ` Retrying... ${retry}`) + if (fs.existsSync(path.join(directory, name))) fs.unlinkSync(path.join(directory, name)) + if (retry) await this.downloadAsync(url, directory, name, false, type) + resolve() + }) + }) + } + + checkSum (hash, file) { + return new Promise((resolve, reject) => { + checksum.file(file, (err, sum) => { + if (err) { + this.client.emit('debug', `[MCLC]: Failed to check file hash due to ${err}`) + resolve(false) + } else { + resolve(hash === sum) + } + }) + }) + } + + getVersion () { + return new Promise(resolve => { + const versionJsonPath = this.options.overrides.versionJson || path.join(this.options.directory, `${this.options.version.number}.json`) + + if (fs.existsSync(versionJsonPath)) { + this.version = JSON.parse(fs.readFileSync(versionJsonPath)) + + return resolve(this.version) + } + + const manifest = `${this.options.overrides.url.meta}/mc/game/version_manifest.json` + + const cache = this.options.cache ? `${this.options.cache}/json` : `${this.options.root}/cache/json` + + request.get(manifest, (error, response, body) => { + if (error && error.code !== 'ENOTFOUND') { + return resolve(error) + } + + if (!error) { + if (!fs.existsSync(cache)) { + fs.mkdirSync(cache, { recursive: true }) + + this.client.emit('debug', '[MCLC]: Cache directory created.') + } + + fs.writeFile(path.join(`${cache}/version_manifest.json`), body, (err) => { + if (err) { + return resolve(err) + } + + this.client.emit('debug', '[MCLC]: Cached version_manifest.json (from request)') + }) + } + + let parsed = null + + if (error && (error.code === 'ENOTFOUND')) { + parsed = JSON.parse(fs.readFileSync(`${cache}/version_manifest.json`)) + } else { + parsed = JSON.parse(body) + } + + const versionManifest = parsed.versions.find((version) => { + return version.id === this.options.version.number + }) + + if (!versionManifest) { + return resolve(new Error(`Version not found`)) + } + + request.get(versionManifest.url, (error, response, body) => { + if (error && error.code !== 'ENOTFOUND') { + return resolve(error) + } + + if (!error) { + fs.writeFile(path.join(`${cache}/${this.options.version.number}.json`), body, (err) => { + if (err) { + return resolve(err) + } + + this.client.emit('debug', `[MCLC]: Cached ${this.options.version.number}.json`) + }) + } + + this.client.emit('debug', '[MCLC]: Parsed version from version manifest') + + if (error && (error.code === 'ENOTFOUND')) { + this.version = JSON.parse(fs.readFileSync(`${cache}/${this.options.version.number}.json`)) + } else { + this.version = JSON.parse(body) + } + + this.client.emit('debug', this.version) + + return resolve(this.version) + }) + }) + }) + } + + async getJar () { + await this.downloadAsync(this.version.downloads.client.url, this.options.directory, `${this.options.version.custom ? this.options.version.custom : this.options.version.number}.jar`, true, 'version-jar') + fs.writeFileSync(path.join(this.options.directory, `${this.options.version.number}.json`), JSON.stringify(this.version, null, 4)) + return this.client.emit('debug', '[MCLC]: Downloaded version jar and wrote version json') + } + + async getAssets () { + const assetDirectory = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) + const assetId = this.options.version.custom || this.options.version.number + if (!fs.existsSync(path.join(assetDirectory, 'indexes', `${assetId}.json`))) { + await this.downloadAsync(this.version.assetIndex.url, path.join(assetDirectory, 'indexes'), + `${assetId}.json`, true, 'asset-json') + } + + const index = JSON.parse(fs.readFileSync(path.join(assetDirectory, 'indexes', `${assetId}.json`), { encoding: 'utf8' })) + + this.client.emit('progress', { + type: 'assets', + task: 0, + total: Object.keys(index.objects).length + }) + + await Promise.all(Object.keys(index.objects).map(async asset => { + const hash = index.objects[asset].hash + const subhash = hash.substring(0, 2) + const subAsset = path.join(assetDirectory, 'objects', subhash) + + if (!fs.existsSync(path.join(subAsset, hash)) || !await this.checkSum(hash, path.join(subAsset, hash))) { + await this.downloadAsync(`${this.options.overrides.url.resource}/${subhash}/${hash}`, subAsset, hash, + true, 'assets') + } + counter++ + this.client.emit('progress', { + type: 'assets', + task: counter, + total: Object.keys(index.objects).length + }) + })) + counter = 0 + + // Copy assets to legacy if it's an older Minecraft version. + if (this.isLegacy()) { + if (fs.existsSync(path.join(assetDirectory, 'legacy'))) { + this.client.emit('debug', '[MCLC]: The \'legacy\' directory is no longer used as Minecraft looks ' + + 'for the resouces folder regardless of what is passed in the assetDirecotry launch option. I\'d ' + + `recommend removing the directory (${path.join(assetDirectory, 'legacy')})`) + } + + const legacyDirectory = path.join(this.options.root, 'resources') + this.client.emit('debug', `[MCLC]: Copying assets over to ${legacyDirectory}`) + + this.client.emit('progress', { + type: 'assets-copy', + task: 0, + total: Object.keys(index.objects).length + }) + + await Promise.all(Object.keys(index.objects).map(async asset => { + const hash = index.objects[asset].hash + const subhash = hash.substring(0, 2) + const subAsset = path.join(assetDirectory, 'objects', subhash) + + const legacyAsset = asset.split('/') + legacyAsset.pop() + + if (!fs.existsSync(path.join(legacyDirectory, legacyAsset.join('/')))) { + fs.mkdirSync(path.join(legacyDirectory, legacyAsset.join('/')), { recursive: true }) + } + + if (!fs.existsSync(path.join(legacyDirectory, asset))) { + fs.copyFileSync(path.join(subAsset, hash), path.join(legacyDirectory, asset)) + } + counter++ + this.client.emit('progress', { + type: 'assets-copy', + task: counter, + total: Object.keys(index.objects).length + }) + })) + } + counter = 0 + + this.client.emit('debug', '[MCLC]: Downloaded assets') + } + + parseRule (lib) { + if (lib.rules) { + if (lib.rules.length > 1) { + if (lib.rules[0].action === 'allow' && + lib.rules[1].action === 'disallow' && + lib.rules[1].os.name === 'osx') { + return this.getOS() === 'osx' + } else { + return true + } + } else { + if (lib.rules[0].action === 'allow' && lib.rules[0].os) return lib.rules[0].os.name !== this.getOS() + } + } else { + return false + } + } + + async getNatives () { + const nativeDirectory = path.resolve(this.options.overrides.natives || path.join(this.options.root, 'natives', this.version.id)) + + if (parseInt(this.version.id.split('.')[1]) >= 19) return this.options.overrides.cwd || this.options.root + + if (!fs.existsSync(nativeDirectory) || !fs.readdirSync(nativeDirectory).length) { + fs.mkdirSync(nativeDirectory, { recursive: true }) + + const natives = async () => { + const natives = [] + await Promise.all(this.version.libraries.map(async (lib) => { + if (!lib.downloads || !lib.downloads.classifiers) return + if (this.parseRule(lib)) return + + const native = this.getOS() === 'osx' + ? lib.downloads.classifiers['natives-osx'] || lib.downloads.classifiers['natives-macos'] + : lib.downloads.classifiers[`natives-${this.getOS()}`] + + natives.push(native) + })) + return natives + } + const stat = await natives() + + this.client.emit('progress', { + type: 'natives', + task: 0, + total: stat.length + }) + + await Promise.all(stat.map(async (native) => { + if (!native) return + const name = native.path.split('/').pop() + await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') + if (!await this.checkSum(native.sha1, path.join(nativeDirectory, name))) { + await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') + } + try { + new Zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true) + } catch (e) { + // Only doing a console.warn since a stupid error happens. You can basically ignore this. + // if it says Invalid file name, just means two files were downloaded and both were deleted. + // All is well. + console.warn(e) + } + fs.unlinkSync(path.join(nativeDirectory, name)) + counter++ + this.client.emit('progress', { + type: 'natives', + task: counter, + total: stat.length + }) + })) + this.client.emit('debug', '[MCLC]: Downloaded and extracted natives') + } + + counter = 0 + this.client.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`) + + return nativeDirectory + } + + fwAddArgs () { + const forgeWrapperAgrs = [ + `-Dforgewrapper.librariesDir=${path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries'))}`, + `-Dforgewrapper.installer=${this.options.forge}`, + `-Dforgewrapper.minecraft=${this.options.mcPath}` + ] + this.options.customArgs + ? this.options.customArgs = this.options.customArgs.concat(forgeWrapperAgrs) + : this.options.customArgs = forgeWrapperAgrs + } + + isModernForge (json) { + return json.inheritsFrom && json.inheritsFrom.split('.')[1] >= 12 && !(json.inheritsFrom === '1.12.2' && (json.id.split('.')[json.id.split('.').length - 1]) === '2847') + } + + async getForgedWrapped () { + let json = null + let installerJson = null + const versionPath = path.join(this.options.root, 'forge', `${this.version.id}`, 'version.json') + // Since we're building a proper "custom" JSON that will work nativly with MCLC, the version JSON will not + // be re-generated on the next run. + if (fs.existsSync(versionPath)) { + try { + json = JSON.parse(fs.readFileSync(versionPath)) + if (!json.forgeWrapperVersion || !(json.forgeWrapperVersion === this.options.overrides.fw.version)) { + this.client.emit('debug', '[MCLC]: Old ForgeWrapper has generated this version JSON, re-generating') + } else { + // If forge is modern, add ForgeWrappers launch arguments and set forge to null so MCLC treats it as a custom json. + if (this.isModernForge(json)) { + this.fwAddArgs() + this.options.forge = null + } + return json + } + } catch (e) { + console.warn(e) + this.client.emit('debug', '[MCLC]: Failed to parse Forge version JSON, re-generating') + } + } + + this.client.emit('debug', '[MCLC]: Generating a proper version json, this might take a bit') + const zipFile = new Zip(this.options.forge) + json = zipFile.readAsText('version.json') + if (zipFile.getEntry('install_profile.json')) installerJson = zipFile.readAsText('install_profile.json') + + try { + json = JSON.parse(json) + if (installerJson) installerJson = JSON.parse(installerJson) + } catch (e) { + this.client.emit('debug', '[MCLC]: Failed to load json files for ForgeWrapper, using Vanilla instead') + return null + } + // Adding the installer libraries as mavenFiles so MCLC downloads them but doesn't add them to the class paths. + if (installerJson) { + json.mavenFiles + ? json.mavenFiles = json.mavenFiles.concat(installerJson.libraries) + : json.mavenFiles = installerJson.libraries + } + + // Holder for the specifc jar ending which depends on the specifc forge version. + let jarEnding = 'universal' + // We need to handle modern forge differently than legacy. + if (this.isModernForge(json)) { + // If forge is modern and above 1.12.2, we add ForgeWrapper to the libraries so MCLC includes it in the classpaths. + if (json.inheritsFrom !== '1.12.2') { + this.fwAddArgs() + const fwName = `ForgeWrapper-${this.options.overrides.fw.version}.jar` + const fwPathArr = ['io', 'github', 'zekerzhayard', 'ForgeWrapper', this.options.overrides.fw.version] + json.libraries.push({ + name: fwPathArr.join(':'), + downloads: { + artifact: { + path: [...fwPathArr, fwName].join('/'), + url: `${this.options.overrides.fw.baseUrl}${this.options.overrides.fw.version}/${fwName}`, + sha1: this.options.overrides.fw.sh1, + size: this.options.overrides.fw.size + } + } + }) + json.mainClass = 'io.github.zekerzhayard.forgewrapper.installer.Main' + jarEnding = 'launcher' + + // Providing a download URL to the universal jar mavenFile so it can be downloaded properly. + for (const library of json.mavenFiles) { + const lib = library.name.split(':') + if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { + library.downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + library.downloads.artifact.path + break + } + } + } else { + // Remove the forge dependent since we're going to overwrite the first entry anyways. + for (const library in json.mavenFiles) { + const lib = json.mavenFiles[library].name.split(':') + if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { + delete json.mavenFiles[library] + break + } + } + } + } else { + // Modifying legacy library format to play nice with MCLC's downloadToDirectory function. + await Promise.all(json.libraries.map(async library => { + const lib = library.name.split(':') + if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) return + + let url = this.options.overrides.url.mavenForge + const name = `${lib[1]}-${lib[2]}.jar` + + if (!library.url) { + if (library.serverreq || library.clientreq) { + url = this.options.overrides.url.defaultRepoForge + } else { + return + } + } + library.url = url + const downloadLink = `${url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` + // Checking if the file still exists on Forge's server, if not, replace it with the fallback. + // Not checking for sucess, only if it 404s. + this.baseRequest(downloadLink, (error, response, body) => { + if (error) { + this.client.emit('debug', `[MCLC]: Failed checking request for ${downloadLink}`) + } else { + if (response.statusCode === 404) library.url = this.options.overrides.url.fallbackMaven + } + }) + })) + } + // If a downloads property exists, we modify the inital forge entry to include ${jarEnding} so ForgeWrapper can work properly. + // If it doesn't, we simply remove it since we're already providing the universal jar. + if (json.libraries[0].downloads) { + if (json.libraries[0].name.includes('minecraftforge')) { + json.libraries[0].name = json.libraries[0].name + `:${jarEnding}` + json.libraries[0].downloads.artifact.path = json.libraries[0].downloads.artifact.path.replace('.jar', `-${jarEnding}.jar`) + json.libraries[0].downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + json.libraries[0].downloads.artifact.path + } + } else { + delete json.libraries[0] + } + + // Removing duplicates and null types + json.libraries = this.cleanUp(json.libraries) + if (json.mavenFiles) json.mavenFiles = this.cleanUp(json.mavenFiles) + + json.forgeWrapperVersion = this.options.overrides.fw.version + + // Saving file for next run! + if (!fs.existsSync(path.join(this.options.root, 'forge', this.version.id))) { + fs.mkdirSync(path.join(this.options.root, 'forge', this.version.id), { recursive: true }) + } + fs.writeFileSync(versionPath, JSON.stringify(json, null, 4)) + + // Make MCLC treat modern forge as a custom version json rather then legacy forge. + if (this.isModernForge(json)) this.options.forge = null + + return json + } + + runInstaller (path) { + return new Promise(resolve => { + const installer = child.exec(path) + installer.on('close', (code) => resolve(code)) + }) + } + + async downloadToDirectory (directory, libraries, eventName) { + const libs = [] + + await Promise.all(libraries.map(async library => { + if (!library) return + if (this.parseRule(library)) return + const lib = library.name.split(':') + + let jarPath + let name + if (library.downloads && library.downloads.artifact && library.downloads.artifact.path) { + name = library.downloads.artifact.path.split('/')[library.downloads.artifact.path.split('/').length - 1] + jarPath = path.join(directory, this.popString(library.downloads.artifact.path)) + } else { + name = `${lib[1]}-${lib[2]}${lib[3] ? '-' + lib[3] : ''}.jar` + jarPath = path.join(directory, `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`) + } + + const downloadLibrary = async library => { + if (library.url) { + const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` + await this.downloadAsync(url, jarPath, name, true, eventName) + } else if (library.downloads && library.downloads.artifact) { + await this.downloadAsync(library.downloads.artifact.url, jarPath, name, true, eventName) + } + } + + if (!fs.existsSync(path.join(jarPath, name))) downloadLibrary(library) + else if (library.downloads && library.downloads.artifact) { + if (!this.checkSum(library.downloads.artifact.sha1, path.join(jarPath, name))) downloadLibrary(library) + } + + counter++ + this.client.emit('progress', { + type: eventName, + task: counter, + total: libraries.length + }) + libs.push(`${jarPath}${path.sep}${name}`) + })) + counter = 0 + + return libs + } + + async getClasses (classJson) { + let libs = [] + + const libraryDirectory = path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries')) + + if (classJson) { + if (classJson.mavenFiles) { + await this.downloadToDirectory(libraryDirectory, classJson.mavenFiles, 'classes-maven-custom') + } + libs = (await this.downloadToDirectory(libraryDirectory, classJson.libraries, 'classes-custom')) + } + + const parsed = this.version.libraries.map(lib => { + if (lib.downloads && lib.downloads.artifact && !this.parseRule(lib)) return lib + }) + + libs = libs.concat((await this.downloadToDirectory(libraryDirectory, parsed, 'classes'))) + counter = 0 + + // Temp Quilt support + if (classJson) libs.sort() + + this.client.emit('debug', '[MCLC]: Collected class paths') + return libs + } + + popString (path) { + const tempArray = path.split('/') + tempArray.pop() + return tempArray.join('/') + } + + cleanUp (array) { + const newArray = [] + for (const classPath in array) { + if (newArray.includes(array[classPath]) || array[classPath] === null) continue + newArray.push(array[classPath]) + } + return newArray + } + + formatQuickPlay () { + const types = { + singleplayer: '--quickPlaySingleplayer', + multiplayer: '--quickPlayMultiplayer', + realms: '--quickPlayRealms', + legacy: null + } + const { type, identifier, path } = this.options.quickPlay + const keys = Object.keys(types) + if (!keys.includes(type)) { + this.client.emit('debug', `[MCLC]: quickPlay type is not valid. Valid types are: ${keys.join(', ')}`) + return null + } + const returnArgs = type === 'legacy' + ? ['--server', identifier.split(':')[0], '--port', identifier.split(':')[1] || '25565'] + : [types[type], identifier] + if (path) returnArgs.push('--quickPlayPath', path) + return returnArgs + } + + async getLaunchOptions (modification) { + const type = Object.assign({}, this.version, modification) + + let args = type.minecraftArguments + ? type.minecraftArguments.split(' ') + : type.arguments.game + const assetRoot = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) + const assetPath = this.isLegacy() + ? path.join(this.options.root, 'resources') + : path.join(assetRoot) + + const minArgs = this.options.overrides.minArgs || this.isLegacy() ? 5 : 11 + if (args.length < minArgs) args = args.concat(this.version.minecraftArguments ? this.version.minecraftArguments.split(' ') : this.version.arguments.game) + if (this.options.customLaunchArgs) args = args.concat(this.options.customLaunchArgs) + + this.options.authorization = await Promise.resolve(this.options.authorization) + this.options.authorization.meta = this.options.authorization.meta ? this.options.authorization.meta : { type: 'mojang' } + const fields = { + '${auth_access_token}': this.options.authorization.access_token, + '${auth_session}': this.options.authorization.access_token, + '${auth_player_name}': this.options.authorization.name, + '${auth_uuid}': this.options.authorization.uuid, + '${auth_xuid}': this.options.authorization.meta.xuid || this.options.authorization.access_token, + '${user_properties}': this.options.authorization.user_properties, + '${user_type}': this.options.authorization.meta.type, + '${version_name}': this.options.version.number, + '${assets_index_name}': this.options.overrides.assetIndex || this.options.version.custom || this.options.version.number, + '${game_directory}': this.options.overrides.gameDirectory || this.options.root, + '${assets_root}': assetPath, + '${game_assets}': assetPath, + '${version_type}': this.options.version.type, + '${clientid}': this.options.authorization.meta.clientId || (this.options.authorization.client_token || this.options.authorization.access_token), + '${resolution_width}': this.options.window ? this.options.window.width : 856, + '${resolution_height}': this.options.window ? this.options.window.height : 482 + } + + if (this.options.authorization.meta.demo && (this.options.features ? !this.options.features.includes('is_demo_user') : true)) { + args.push('--demo') + } + + const replaceArg = (obj, index) => { + if (Array.isArray(obj.value)) { + for (const arg of obj.value) { + args.push(arg) + } + } else { + args.push(obj.value) + } + delete args[index] + } + + for (let index = 0; index < args.length; index++) { + if (typeof args[index] === 'object') { + if (args[index].rules) { + if (!this.options.features) continue + const featureFlags = [] + for (const rule of args[index].rules) { + featureFlags.push(...Object.keys(rule.features)) + } + let hasAllRules = true + for (const feature of this.options.features) { + if (!featureFlags.includes(feature)) { + hasAllRules = false + } + } + if (hasAllRules) replaceArg(args[index], index) + } else { + replaceArg(args[index], index) + } + } else { + if (Object.keys(fields).includes(args[index])) { + args[index] = fields[args[index]] + } + } + } + if (this.options.window) { + // eslint-disable-next-line no-unused-expressions + this.options.window.fullscreen + ? args.push('--fullscreen') + : () => { + if (this.options.features ? !this.options.features.includes('has_custom_resolution') : true) { + args.push('--width', this.options.window.width, '--height', this.options.window.height) + } + } + } + if (this.options.server) this.client.emit('debug', '[MCLC]: server and port are deprecated launch flags. Use the quickPlay field.') + if (this.options.quickPlay) args = args.concat(this.formatQuickPlay()) + if (this.options.proxy) { + args.push( + '--proxyHost', + this.options.proxy.host, + '--proxyPort', + this.options.proxy.port || '8080', + '--proxyUser', + this.options.proxy.username, + '--proxyPass', + this.options.proxy.password + ) + } + args = args.filter(value => typeof value === 'string' || typeof value === 'number') + this.client.emit('debug', '[MCLC]: Set launch options') + return args + } + + async getJVM () { + const opts = { + windows: '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump', + osx: '-XstartOnFirstThread', + linux: '-Xss1M' + } + return opts[this.getOS()] + } + + isLegacy () { + return this.version.assets === 'legacy' || this.version.assets === 'pre-1.6' + } + + getOS () { + if (this.options.os) { + return this.options.os + } else { + switch (process.platform) { + case 'win32': return 'windows' + case 'darwin': return 'osx' + default: return 'linux' + } + } + } + + // To prevent launchers from breaking when they update. Will be reworked with rewrite. + getMemory () { + if (!this.options.memory) { + this.client.emit('debug', '[MCLC]: Memory not set! Setting 1GB as MAX!') + this.options.memory = { + min: 512, + max: 1023 + } + } + if (!isNaN(this.options.memory.max) && !isNaN(this.options.memory.min)) { + if (this.options.memory.max < this.options.memory.min) { + this.client.emit('debug', '[MCLC]: MIN memory is higher then MAX! Resetting!') + this.options.memory.max = 1023 + this.options.memory.min = 512 + } + return [`${this.options.memory.max}M`, `${this.options.memory.min}M`] + } else { return [`${this.options.memory.max}`, `${this.options.memory.min}`] } + } + + async extractPackage (options = this.options) { + if (options.clientPackage.startsWith('http')) { + await this.downloadAsync(options.clientPackage, options.root, 'clientPackage.zip', true, 'client-package') + options.clientPackage = path.join(options.root, 'clientPackage.zip') + } + new Zip(options.clientPackage).extractAllTo(options.root, true) + if (options.removePackage) fs.unlinkSync(options.clientPackage) + + return this.client.emit('package-extract', true) + } +} \ No newline at end of file diff --git a/src/main/lib/mcl/index.js b/packages/gui/src/main/lib/mcl/index.js similarity index 100% rename from src/main/lib/mcl/index.js rename to packages/gui/src/main/lib/mcl/index.js diff --git a/packages/gui/src/main/lib/mcl/launcher.js b/packages/gui/src/main/lib/mcl/launcher.js new file mode 100644 index 0000000..5a5aa76 --- /dev/null +++ b/packages/gui/src/main/lib/mcl/launcher.js @@ -0,0 +1,224 @@ +import fs from "node:fs" +import path from "node:path" +import { EventEmitter } from "events" +import child from "child_process" + +import Handler from "./handler" + +export default class MCLCore extends EventEmitter { + async launch(options, callbacks = {}) { + try { + this.options = { ...options } + + this.options.root = path.resolve(this.options.root) + + this.options.overrides = { + detached: true, + ...this.options.overrides, + url: { + meta: 'https://launchermeta.mojang.com', + resource: 'https://resources.download.minecraft.net', + mavenForge: 'http://files.minecraftforge.net/maven/', + defaultRepoForge: 'https://libraries.minecraft.net/', + fallbackMaven: 'https://search.maven.org/remotecontent?filepath=', + ...this.options.overrides + ? this.options.overrides.url + : undefined + }, + fw: { + baseUrl: 'https://github.com/ZekerZhayard/ForgeWrapper/releases/download/', + version: '1.5.6', + sh1: 'b38d28e8b7fde13b1bc0db946a2da6760fecf98d', + size: 34715, + ...this.options.overrides + ? this.options.overrides.fw + : undefined + } + } + + this.handler = new Handler(this) + + this.printVersion() + + const java = await this.handler.checkJava(this.options.javaPath || 'java') + + if (!java.run) { + this.emit('debug', `[MCLC]: Couldn't start Minecraft due to: ${java.message}`) + this.emit('close', 1) + return null + } + + this.createRootDirectory() + this.createGameDirectory() + + await this.extractPackage() + + if (this.options.installer) { + // So installers that create a profile in launcher_profiles.json can run without breaking. + const profilePath = path.join(this.options.root, 'launcher_profiles.json') + if (!fs.existsSync(profilePath) || !JSON.parse(fs.readFileSync(profilePath)).profiles) { + fs.writeFileSync(profilePath, JSON.stringify({ profiles: {} }, null, 4)) + } + const code = await this.handler.runInstaller(this.options.installer) + if (!this.options.version.custom && code === 0) { + this.emit('debug', '[MCLC]: Installer successfully ran, but no custom version was provided') + } + this.emit('debug', `[MCLC]: Installer closed with code ${code}`) + } + + const directory = this.options.overrides.directory || path.join(this.options.root, 'versions', this.options.version.custom ? this.options.version.custom : this.options.version.number) + this.options.directory = directory + + const versionFile = await this.handler.getVersion() + + const mcPath = this.options.overrides.minecraftJar || (this.options.version.custom + ? path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.jar`) + : path.join(directory, `${this.options.version.number}.jar`)) + + this.options.mcPath = mcPath + + const nativePath = await this.handler.getNatives() + + if (!fs.existsSync(mcPath)) { + this.emit('debug', '[MCLC]: Attempting to download Minecraft version jar') + + if (typeof callbacks.install === "function") { + callbacks.install() + } + + await this.handler.getJar() + } + + const modifyJson = await this.getModifyJson() + + const args = [] + + let jvm = [ + '-XX:-UseAdaptiveSizePolicy', + '-XX:-OmitStackTraceInFastThrow', + '-Dfml.ignorePatchDiscrepancies=true', + '-Dfml.ignoreInvalidMinecraftCertificates=true', + `-Djava.library.path=${nativePath}`, + `-Xmx${this.handler.getMemory()[0]}`, + `-Xms${this.handler.getMemory()[1]}` + ] + if (this.handler.getOS() === 'osx') { + if (parseInt(versionFile.id.split('.')[1]) > 12) jvm.push(await this.handler.getJVM()) + } else jvm.push(await this.handler.getJVM()) + + if (this.options.customArgs) jvm = jvm.concat(this.options.customArgs) + if (this.options.overrides.logj4ConfigurationFile) { + jvm.push(`-Dlog4j.configurationFile=${path.resolve(this.options.overrides.logj4ConfigurationFile)}`) + } + // https://help.minecraft.net/hc/en-us/articles/4416199399693-Security-Vulnerability-in-Minecraft-Java-Edition + if (parseInt(versionFile.id.split('.')[1]) === 18 && !parseInt(versionFile.id.split('.')[2])) jvm.push('-Dlog4j2.formatMsgNoLookups=true') + if (parseInt(versionFile.id.split('.')[1]) === 17) jvm.push('-Dlog4j2.formatMsgNoLookups=true') + if (parseInt(versionFile.id.split('.')[1]) < 17) { + if (!jvm.find(arg => arg.includes('Dlog4j.configurationFile'))) { + const configPath = path.resolve(this.options.overrides.cwd || this.options.root) + const intVersion = parseInt(versionFile.id.split('.')[1]) + if (intVersion >= 12) { + await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml', + configPath, 'log4j2_112-116.xml', true, 'log4j') + jvm.push('-Dlog4j.configurationFile=log4j2_112-116.xml') + } else if (intVersion >= 7) { + await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/dd2b723346a8dcd48e7f4d245f6bf09e98db9696/log4j2_17-111.xml', + configPath, 'log4j2_17-111.xml', true, 'log4j') + jvm.push('-Dlog4j.configurationFile=log4j2_17-111.xml') + } + } + } + + const classes = this.options.overrides.classes || this.handler.cleanUp(await this.handler.getClasses(modifyJson)) + const classPaths = ['-cp'] + const separator = this.handler.getOS() === 'windows' ? ';' : ':' + + this.emit('debug', `[MCLC]: Using ${separator} to separate class paths`) + + // Handling launch arguments. + const file = modifyJson || versionFile + + // So mods like fabric work. + const jar = fs.existsSync(mcPath) + ? `${separator}${mcPath}` + : `${separator}${path.join(directory, `${this.options.version.number}.jar`)}` + classPaths.push(`${this.options.forge ? this.options.forge + separator : ''}${classes.join(separator)}${jar}`) + classPaths.push(file.mainClass) + + this.emit('debug', '[MCLC]: Attempting to download assets') + + if (typeof callbacks.init_assets === "function") { + callbacks.init_assets() + } + + await this.handler.getAssets() + + // Forge -> Custom -> Vanilla + const launchOptions = await this.handler.getLaunchOptions(modifyJson) + + const launchArguments = args.concat(jvm, classPaths, launchOptions) + this.emit('arguments', launchArguments) + this.emit('debug', `[MCLC]: Launching with arguments ${launchArguments.join(' ')}`) + + return this.startMinecraft(launchArguments) + } catch (e) { + this.emit('debug', `[MCLC]: Failed to start due to ${e}, closing...`) + return null + } + } + + printVersion() { + if (fs.existsSync(path.join(__dirname, '..', 'package.json'))) { + const { version } = require('../package.json') + this.emit('debug', `[MCLC]: MCLC version ${version}`) + } else { this.emit('debug', '[MCLC]: Package JSON not found, skipping MCLC version check.') } + } + + createRootDirectory() { + if (!fs.existsSync(this.options.root)) { + this.emit('debug', '[MCLC]: Attempting to create root folder') + fs.mkdirSync(this.options.root) + } + } + + createGameDirectory() { + if (this.options.overrides.gameDirectory) { + this.options.overrides.gameDirectory = path.resolve(this.options.overrides.gameDirectory) + if (!fs.existsSync(this.options.overrides.gameDirectory)) { + fs.mkdirSync(this.options.overrides.gameDirectory, { recursive: true }) + } + } + } + + async extractPackage() { + if (this.options.clientPackage) { + this.emit('debug', `[MCLC]: Extracting client package to ${this.options.root}`) + await this.handler.extractPackage() + } + } + + async getModifyJson() { + let modifyJson = null + + if (this.options.forge) { + this.options.forge = path.resolve(this.options.forge) + this.emit('debug', '[MCLC]: Detected Forge in options, getting dependencies') + modifyJson = await this.handler.getForgedWrapped() + } else if (this.options.version.custom) { + this.emit('debug', '[MCLC]: Detected custom in options, setting custom version file') + modifyJson = modifyJson || JSON.parse(fs.readFileSync(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`), { encoding: 'utf8' })) + } + + return modifyJson + } + + startMinecraft(launchArguments) { + const minecraft = child.spawn(this.options.javaPath ? this.options.javaPath : 'java', launchArguments, + { cwd: this.options.overrides.cwd || this.options.root, detached: this.options.overrides.detached }) + + minecraft.stdout.on('data', (data) => this.emit('data', data.toString('utf-8'))) + minecraft.stderr.on('data', (data) => this.emit('data', data.toString('utf-8'))) + minecraft.on('close', (code) => this.emit('close', code)) + return minecraft + } +} \ No newline at end of file diff --git a/packages/gui/src/main/lib/mimic-function/index.js b/packages/gui/src/main/lib/mimic-function/index.js new file mode 100644 index 0000000..61e6701 --- /dev/null +++ b/packages/gui/src/main/lib/mimic-function/index.js @@ -0,0 +1,71 @@ +const copyProperty = (to, from, property, ignoreNonConfigurable) => { + // `Function#length` should reflect the parameters of `to` not `from` since we keep its body. + // `Function#prototype` is non-writable and non-configurable so can never be modified. + if (property === 'length' || property === 'prototype') { + return; + } + + // `Function#arguments` and `Function#caller` should not be copied. They were reported to be present in `Reflect.ownKeys` for some devices in React Native (#41), so we explicitly ignore them here. + if (property === 'arguments' || property === 'caller') { + return; + } + + const toDescriptor = Object.getOwnPropertyDescriptor(to, property); + const fromDescriptor = Object.getOwnPropertyDescriptor(from, property); + + if (!canCopyProperty(toDescriptor, fromDescriptor) && ignoreNonConfigurable) { + return; + } + + Object.defineProperty(to, property, fromDescriptor); +}; + +// `Object.defineProperty()` throws if the property exists, is not configurable and either: +// - one its descriptors is changed +// - it is non-writable and its value is changed +const canCopyProperty = function (toDescriptor, fromDescriptor) { + return toDescriptor === undefined || toDescriptor.configurable || ( + toDescriptor.writable === fromDescriptor.writable + && toDescriptor.enumerable === fromDescriptor.enumerable + && toDescriptor.configurable === fromDescriptor.configurable + && (toDescriptor.writable || toDescriptor.value === fromDescriptor.value) + ); +}; + +const changePrototype = (to, from) => { + const fromPrototype = Object.getPrototypeOf(from); + if (fromPrototype === Object.getPrototypeOf(to)) { + return; + } + + Object.setPrototypeOf(to, fromPrototype); +}; + +const wrappedToString = (withName, fromBody) => `/* Wrapped ${withName}*/\n${fromBody}`; + +const toStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, 'toString'); +const toStringName = Object.getOwnPropertyDescriptor(Function.prototype.toString, 'name'); + +// We call `from.toString()` early (not lazily) to ensure `from` can be garbage collected. +// We use `bind()` instead of a closure for the same reason. +// Calling `from.toString()` early also allows caching it in case `to.toString()` is called several times. +const changeToString = (to, from, name) => { + const withName = name === '' ? '' : `with ${name.trim()}() `; + const newToString = wrappedToString.bind(null, withName, from.toString()); + // Ensure `to.toString.toString` is non-enumerable and has the same `same` + Object.defineProperty(newToString, 'name', toStringName); + Object.defineProperty(to, 'toString', { ...toStringDescriptor, value: newToString }); +}; + +export default function mimicFunction(to, from, { ignoreNonConfigurable = false } = {}) { + const { name } = to; + + for (const property of Reflect.ownKeys(from)) { + copyProperty(to, from, property, ignoreNonConfigurable); + } + + changePrototype(to, from); + changeToString(to, from, name); + + return to; +} \ No newline at end of file diff --git a/packages/gui/src/main/lib/npm-run-path/index.d.ts b/packages/gui/src/main/lib/npm-run-path/index.d.ts new file mode 100644 index 0000000..0c1b160 --- /dev/null +++ b/packages/gui/src/main/lib/npm-run-path/index.d.ts @@ -0,0 +1,84 @@ +export interface RunPathOptions { + /** + Working directory. + + @default process.cwd() + */ + readonly cwd?: string | URL; + + /** + PATH to be appended. Default: [`PATH`](https://github.com/sindresorhus/path-key). + + Set it to an empty string to exclude the default PATH. + */ + readonly path?: string; + + /** + Path to the Node.js executable to use in child processes if that is different from the current one. Its directory is pushed to the front of PATH. + + This can be either an absolute path or a path relative to the `cwd` option. + + @default process.execPath + */ + readonly execPath?: string | URL; +} + +export type ProcessEnv = Record; + +export interface EnvOptions { + /** + The working directory. + + @default process.cwd() + */ + readonly cwd?: string | URL; + + /** + Accepts an object of environment variables, like `process.env`, and modifies the PATH using the correct [PATH key](https://github.com/sindresorhus/path-key). Use this if you're modifying the PATH for use in the `child_process` options. + */ + readonly env?: ProcessEnv; + + /** + The path to the current Node.js executable. Its directory is pushed to the front of PATH. + + This can be either an absolute path or a path relative to the `cwd` option. + + @default process.execPath + */ + readonly execPath?: string | URL; +} + +/** +Get your [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) prepended with locally installed binaries. + +@returns The augmented path string. + +@example +``` +import childProcess from 'node:child_process'; +import {npmRunPath} from 'npm-run-path'; + +console.log(process.env.PATH); +//=> '/usr/local/bin' + +console.log(npmRunPath()); +//=> '/Users/sindresorhus/dev/foo/node_modules/.bin:/Users/sindresorhus/dev/node_modules/.bin:/Users/sindresorhus/node_modules/.bin:/Users/node_modules/.bin:/node_modules/.bin:/usr/local/bin' +``` +*/ +export function npmRunPath(options?: RunPathOptions): string; + +/** +@returns The augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object. + +@example +``` +import childProcess from 'node:child_process'; +import {npmRunPathEnv} from 'npm-run-path'; + +// `foo` is a locally installed binary +childProcess.execFileSync('foo', { + env: npmRunPathEnv() +}); +``` +*/ +export function npmRunPathEnv(options?: EnvOptions): ProcessEnv; diff --git a/packages/gui/src/main/lib/npm-run-path/index.js b/packages/gui/src/main/lib/npm-run-path/index.js new file mode 100644 index 0000000..782a96a --- /dev/null +++ b/packages/gui/src/main/lib/npm-run-path/index.js @@ -0,0 +1,51 @@ +import process from 'node:process'; +import path from 'node:path'; +import url from 'node:url'; + +function pathKey(options = {}) { + const { + env = process.env, + platform = process.platform + } = options; + + if (platform !== 'win32') { + return 'PATH'; + } + + return Object.keys(env).reverse().find(key => key.toUpperCase() === 'PATH') || 'Path'; +} + +export function npmRunPath(options = {}) { + const { + cwd = process.cwd(), + path: path_ = process.env[pathKey()], + execPath = process.execPath, + } = options; + + let previous; + const execPathString = execPath instanceof URL ? url.fileURLToPath(execPath) : execPath; + const cwdString = cwd instanceof URL ? url.fileURLToPath(cwd) : cwd; + let cwdPath = path.resolve(cwdString); + const result = []; + + while (previous !== cwdPath) { + result.push(path.join(cwdPath, 'node_modules/.bin')); + previous = cwdPath; + cwdPath = path.resolve(cwdPath, '..'); + } + + // Ensure the running `node` binary is used. + result.push(path.resolve(cwdString, execPathString, '..')); + + return [...result, path_].join(path.delimiter); +} + +export function npmRunPathEnv({ env = process.env, ...options } = {}) { + env = { ...env }; + + const path = pathKey({ env }); + options.path = env[path]; + env[path] = npmRunPath(options); + + return env; +} diff --git a/packages/gui/src/main/lib/onetime/index.d.ts b/packages/gui/src/main/lib/onetime/index.d.ts new file mode 100644 index 0000000..fa9fc20 --- /dev/null +++ b/packages/gui/src/main/lib/onetime/index.d.ts @@ -0,0 +1,59 @@ +export type Options = { + /** + Throw an error when called more than once. + + @default false + */ + readonly throw?: boolean; +}; + +declare const onetime: { + /** + Ensure a function is only called once. When called multiple times it will return the return value from the first call. + + @param fn - The function that should only be called once. + @returns A function that only calls `fn` once. + + @example + ``` + import onetime from 'onetime'; + + let index = 0; + + const foo = onetime(() => ++index); + + foo(); //=> 1 + foo(); //=> 1 + foo(); //=> 1 + + onetime.callCount(foo); //=> 3 + ``` + */ + ( + fn: (...arguments_: ArgumentsType) => ReturnType, + options?: Options + ): (...arguments_: ArgumentsType) => ReturnType; + + /** + Get the number of times `fn` has been called. + + @param fn - The function to get call count from. + @returns A number representing how many times `fn` has been called. + + @example + ``` + import onetime from 'onetime'; + + const foo = onetime(() => {}); + foo(); + foo(); + foo(); + + console.log(onetime.callCount(foo)); + //=> 3 + ``` + */ + callCount(fn: (...arguments_: any[]) => unknown): number; +}; + +export default onetime; diff --git a/packages/gui/src/main/lib/onetime/index.js b/packages/gui/src/main/lib/onetime/index.js new file mode 100644 index 0000000..880e94d --- /dev/null +++ b/packages/gui/src/main/lib/onetime/index.js @@ -0,0 +1,41 @@ +import mimicFunction from '../mimic-function'; + +const calledFunctions = new WeakMap(); + +const onetime = (function_, options = {}) => { + if (typeof function_ !== 'function') { + throw new TypeError('Expected a function'); + } + + let returnValue; + let callCount = 0; + const functionName = function_.displayName || function_.name || ''; + + const onetime = function (...arguments_) { + calledFunctions.set(onetime, ++callCount); + + if (callCount === 1) { + returnValue = function_.apply(this, arguments_); + function_ = undefined; + } else if (options.throw === true) { + throw new Error(`Function \`${functionName}\` can only be called once`); + } + + return returnValue; + }; + + mimicFunction(onetime, function_); + calledFunctions.set(onetime, callCount); + + return onetime; +}; + +onetime.callCount = function_ => { + if (!calledFunctions.has(function_)) { + throw new Error(`The given function \`${function_.name}\` is not wrapped by the \`onetime\` package`); + } + + return calledFunctions.get(function_); +}; + +export default onetime; diff --git a/src/main/lib/public_bind.js b/packages/gui/src/main/lib/public_bind.js similarity index 100% rename from src/main/lib/public_bind.js rename to packages/gui/src/main/lib/public_bind.js diff --git a/src/main/lib/renderer_ipc/index.js b/packages/gui/src/main/lib/renderer_ipc/index.js similarity index 100% rename from src/main/lib/renderer_ipc/index.js rename to packages/gui/src/main/lib/renderer_ipc/index.js diff --git a/src/main/lib/rfs/index.js b/packages/gui/src/main/lib/rfs/index.js similarity index 100% rename from src/main/lib/rfs/index.js rename to packages/gui/src/main/lib/rfs/index.js diff --git a/src/main/lib/steno/index.ts b/packages/gui/src/main/lib/steno/index.ts similarity index 100% rename from src/main/lib/steno/index.ts rename to packages/gui/src/main/lib/steno/index.ts diff --git a/packages/gui/src/main/lib/strip-final-newline/index.d.ts b/packages/gui/src/main/lib/strip-final-newline/index.d.ts new file mode 100644 index 0000000..e8fa1d3 --- /dev/null +++ b/packages/gui/src/main/lib/strip-final-newline/index.d.ts @@ -0,0 +1,18 @@ +/** +Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from a string or Uint8Array. + +@returns The input without any final newline. + +@example +``` +import stripFinalNewline from 'strip-final-newline'; + +stripFinalNewline('foo\nbar\n\n'); +//=> 'foo\nbar\n' + +const uint8Array = new TextEncoder().encode('foo\nbar\n\n') +new TextDecoder().decode(stripFinalNewline(uint8Array)); +//=> 'foo\nbar\n' +``` +*/ +export default function stripFinalNewline(input: T): T; diff --git a/packages/gui/src/main/lib/strip-final-newline/index.js b/packages/gui/src/main/lib/strip-final-newline/index.js new file mode 100644 index 0000000..a63ed26 --- /dev/null +++ b/packages/gui/src/main/lib/strip-final-newline/index.js @@ -0,0 +1,26 @@ +export default function stripFinalNewline(input) { + if (typeof input === 'string') { + return stripFinalNewlineString(input); + } + + if (!(ArrayBuffer.isView(input) && input.BYTES_PER_ELEMENT === 1)) { + throw new Error('Input must be a string or a Uint8Array'); + } + + return stripFinalNewlineBinary(input); +} + +const stripFinalNewlineString = input => + input.at(-1) === LF + ? input.slice(0, input.at(-2) === CR ? -2 : -1) + : input; + +const stripFinalNewlineBinary = input => + input.at(-1) === LF_BINARY + ? input.subarray(0, input.at(-2) === CR_BINARY ? -2 : -1) + : input; + +const LF = '\n'; +const LF_BINARY = LF.codePointAt(0); +const CR = '\r'; +const CR_BINARY = CR.codePointAt(0); diff --git a/src/main/local_db.js b/packages/gui/src/main/local_db.js similarity index 100% rename from src/main/local_db.js rename to packages/gui/src/main/local_db.js diff --git a/src/main/manager.js b/packages/gui/src/main/manager.js similarity index 100% rename from src/main/manager.js rename to packages/gui/src/main/manager.js diff --git a/packages/gui/src/main/prerequisites.js b/packages/gui/src/main/prerequisites.js new file mode 100644 index 0000000..4134e1c --- /dev/null +++ b/packages/gui/src/main/prerequisites.js @@ -0,0 +1,35 @@ +import resolveDestBin from "@utils/resolveDestBin" +import Vars from "@vars" + +const baseURL = "https://storage.ragestudio.net/rstudio/binaries" + +export default [ + { + id: "7zip-bin", + url: resolveDestBin(`${baseURL}/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za"), + destination: Vars.sevenzip_path, + rewritePermissions: true, + extract: false, + }, + { + id: "git-bin", + url: resolveDestBin(`${baseURL}/git`, "git-bundle-2.4.0.zip"), + destination: Vars.git_path, + rewritePermissions: true, + extract: true, + }, + { + id: "rclone-bin", + url: resolveDestBin(`${baseURL}/rclone-bin`, "rclone-bin.zip"), + destination: Vars.rclone_path, + rewritePermissions: true, + extract: true, + }, + { + id: "java-jdk", + url: resolveDestBin(`${baseURL}/java-jdk`, "java-jdk.zip"), + destination: Vars.java_path, + rewritePermissions: true, + extract: true, + }, +] \ No newline at end of file diff --git a/src/main/setup.js b/packages/gui/src/main/setup.js similarity index 99% rename from src/main/setup.js rename to packages/gui/src/main/setup.js index 63d6f87..e9902ea 100644 --- a/src/main/setup.js +++ b/packages/gui/src/main/setup.js @@ -35,7 +35,6 @@ async function main() { let sevenzip_exec = Vars.sevenzip_path let git_exec = Vars.git_path let rclone_exec = Vars.rclone_path - let java_exec = Vars.java_path if (!fs.existsSync(sevenzip_exec)) { global.win.webContents.send("setup_step", "Downloading 7z binaries...") diff --git a/src/main/utils/extractFile.js b/packages/gui/src/main/utils/extractFile.js similarity index 100% rename from src/main/utils/extractFile.js rename to packages/gui/src/main/utils/extractFile.js diff --git a/src/main/utils/initManifest.js b/packages/gui/src/main/utils/initManifest.js similarity index 100% rename from src/main/utils/initManifest.js rename to packages/gui/src/main/utils/initManifest.js diff --git a/src/main/utils/parseStringVars.js b/packages/gui/src/main/utils/parseStringVars.js similarity index 100% rename from src/main/utils/parseStringVars.js rename to packages/gui/src/main/utils/parseStringVars.js diff --git a/packages/gui/src/main/utils/readDirRecurse.js b/packages/gui/src/main/utils/readDirRecurse.js new file mode 100644 index 0000000..342dda0 --- /dev/null +++ b/packages/gui/src/main/utils/readDirRecurse.js @@ -0,0 +1,25 @@ +import fs from "node:fs" +import path from "node:path" + +async function readDirRecurse(dir, maxDepth = 3, current = 0) { + if (current > maxDepth) { + return [] + } + + const files = await fs.promises.readdir(dir) + + const promises = files.map(async (file) => { + const filePath = path.join(dir, file) + const stat = await fs.promises.stat(filePath) + + if (stat.isDirectory()) { + return readDirRecurse(filePath, maxDepth, current + 1) + } + + return filePath + }) + + return (await Promise.all(promises)).flat() +} + +export default readDirRecurse \ No newline at end of file diff --git a/src/main/utils/readManifest.js b/packages/gui/src/main/utils/readManifest.js similarity index 100% rename from src/main/utils/readManifest.js rename to packages/gui/src/main/utils/readManifest.js diff --git a/src/main/utils/resolveJavaPath.js b/packages/gui/src/main/utils/resolveJavaPath.js similarity index 100% rename from src/main/utils/resolveJavaPath.js rename to packages/gui/src/main/utils/resolveJavaPath.js diff --git a/src/main/utils/sendToRender.js b/packages/gui/src/main/utils/sendToRender.js similarity index 100% rename from src/main/utils/sendToRender.js rename to packages/gui/src/main/utils/sendToRender.js diff --git a/src/main/vars.js b/packages/gui/src/main/vars.js similarity index 100% rename from src/main/vars.js rename to packages/gui/src/main/vars.js diff --git a/src/preload/index.js b/packages/gui/src/preload/index.js similarity index 100% rename from src/preload/index.js rename to packages/gui/src/preload/index.js diff --git a/src/renderer/assets/icon.jsx b/packages/gui/src/renderer/assets/icon.jsx similarity index 100% rename from src/renderer/assets/icon.jsx rename to packages/gui/src/renderer/assets/icon.jsx diff --git a/src/renderer/config/paths_decorators.js b/packages/gui/src/renderer/config/paths_decorators.js similarity index 100% rename from src/renderer/config/paths_decorators.js rename to packages/gui/src/renderer/config/paths_decorators.js diff --git a/src/renderer/index.html b/packages/gui/src/renderer/index.html similarity index 100% rename from src/renderer/index.html rename to packages/gui/src/renderer/index.html diff --git a/src/renderer/src/App.jsx b/packages/gui/src/renderer/src/App.jsx similarity index 100% rename from src/renderer/src/App.jsx rename to packages/gui/src/renderer/src/App.jsx diff --git a/src/renderer/src/GlobalApp.jsx b/packages/gui/src/renderer/src/GlobalApp.jsx similarity index 100% rename from src/renderer/src/GlobalApp.jsx rename to packages/gui/src/renderer/src/GlobalApp.jsx diff --git a/src/renderer/src/components/Icons/index.jsx b/packages/gui/src/renderer/src/components/Icons/index.jsx similarity index 100% rename from src/renderer/src/components/Icons/index.jsx rename to packages/gui/src/renderer/src/components/Icons/index.jsx diff --git a/src/renderer/src/components/InstallConfigAsk/index.jsx b/packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx similarity index 100% rename from src/renderer/src/components/InstallConfigAsk/index.jsx rename to packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx diff --git a/src/renderer/src/components/InstallConfigAsk/index.less b/packages/gui/src/renderer/src/components/InstallConfigAsk/index.less similarity index 100% rename from src/renderer/src/components/InstallConfigAsk/index.less rename to packages/gui/src/renderer/src/components/InstallConfigAsk/index.less diff --git a/src/renderer/src/components/ManifestInfo/index.jsx b/packages/gui/src/renderer/src/components/ManifestInfo/index.jsx similarity index 100% rename from src/renderer/src/components/ManifestInfo/index.jsx rename to packages/gui/src/renderer/src/components/ManifestInfo/index.jsx diff --git a/src/renderer/src/components/ManifestInfo/index.less b/packages/gui/src/renderer/src/components/ManifestInfo/index.less similarity index 100% rename from src/renderer/src/components/ManifestInfo/index.less rename to packages/gui/src/renderer/src/components/ManifestInfo/index.less diff --git a/src/renderer/src/components/NewInstallation/index.jsx b/packages/gui/src/renderer/src/components/NewInstallation/index.jsx similarity index 100% rename from src/renderer/src/components/NewInstallation/index.jsx rename to packages/gui/src/renderer/src/components/NewInstallation/index.jsx diff --git a/src/renderer/src/components/NewInstallation/index.less b/packages/gui/src/renderer/src/components/NewInstallation/index.less similarity index 100% rename from src/renderer/src/components/NewInstallation/index.less rename to packages/gui/src/renderer/src/components/NewInstallation/index.less diff --git a/src/renderer/src/components/PackageConfigItem/index.jsx b/packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx similarity index 100% rename from src/renderer/src/components/PackageConfigItem/index.jsx rename to packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx diff --git a/src/renderer/src/components/PackageItem/index.jsx b/packages/gui/src/renderer/src/components/PackageItem/index.jsx similarity index 100% rename from src/renderer/src/components/PackageItem/index.jsx rename to packages/gui/src/renderer/src/components/PackageItem/index.jsx diff --git a/src/renderer/src/components/PackageItem/index.less b/packages/gui/src/renderer/src/components/PackageItem/index.less similarity index 100% rename from src/renderer/src/components/PackageItem/index.less rename to packages/gui/src/renderer/src/components/PackageItem/index.less diff --git a/src/renderer/src/components/PackageUpdateAvailable/index.jsx b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx similarity index 100% rename from src/renderer/src/components/PackageUpdateAvailable/index.jsx rename to packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx diff --git a/src/renderer/src/components/PackageUpdateAvailable/index.less b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less similarity index 100% rename from src/renderer/src/components/PackageUpdateAvailable/index.less rename to packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less diff --git a/src/renderer/src/contexts/global.js b/packages/gui/src/renderer/src/contexts/global.js similarity index 100% rename from src/renderer/src/contexts/global.js rename to packages/gui/src/renderer/src/contexts/global.js diff --git a/src/renderer/src/contexts/installations.jsx b/packages/gui/src/renderer/src/contexts/installations.jsx similarity index 100% rename from src/renderer/src/contexts/installations.jsx rename to packages/gui/src/renderer/src/contexts/installations.jsx diff --git a/src/renderer/src/layout/components/Drawer/index.jsx b/packages/gui/src/renderer/src/layout/components/Drawer/index.jsx similarity index 100% rename from src/renderer/src/layout/components/Drawer/index.jsx rename to packages/gui/src/renderer/src/layout/components/Drawer/index.jsx diff --git a/src/renderer/src/layout/components/Header/index.jsx b/packages/gui/src/renderer/src/layout/components/Header/index.jsx similarity index 100% rename from src/renderer/src/layout/components/Header/index.jsx rename to packages/gui/src/renderer/src/layout/components/Header/index.jsx diff --git a/src/renderer/src/layout/components/Header/index.less b/packages/gui/src/renderer/src/layout/components/Header/index.less similarity index 100% rename from src/renderer/src/layout/components/Header/index.less rename to packages/gui/src/renderer/src/layout/components/Header/index.less diff --git a/src/renderer/src/layout/components/ModalDialog/index.jsx b/packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx similarity index 100% rename from src/renderer/src/layout/components/ModalDialog/index.jsx rename to packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx diff --git a/src/renderer/src/layout/index.jsx b/packages/gui/src/renderer/src/layout/index.jsx similarity index 100% rename from src/renderer/src/layout/index.jsx rename to packages/gui/src/renderer/src/layout/index.jsx diff --git a/src/renderer/src/main.jsx b/packages/gui/src/renderer/src/main.jsx similarity index 100% rename from src/renderer/src/main.jsx rename to packages/gui/src/renderer/src/main.jsx diff --git a/src/renderer/src/pages/index.jsx b/packages/gui/src/renderer/src/pages/index.jsx similarity index 100% rename from src/renderer/src/pages/index.jsx rename to packages/gui/src/renderer/src/pages/index.jsx diff --git a/src/renderer/src/pages/index.less b/packages/gui/src/renderer/src/pages/index.less similarity index 100% rename from src/renderer/src/pages/index.less rename to packages/gui/src/renderer/src/pages/index.less diff --git a/src/renderer/src/pages/pkg/[pkg_id].jsx b/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx similarity index 100% rename from src/renderer/src/pages/pkg/[pkg_id].jsx rename to packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx diff --git a/src/renderer/src/pages/pkg/index.less b/packages/gui/src/renderer/src/pages/pkg/index.less similarity index 100% rename from src/renderer/src/pages/pkg/index.less rename to packages/gui/src/renderer/src/pages/pkg/index.less diff --git a/src/renderer/src/pages/settings/index.jsx b/packages/gui/src/renderer/src/pages/settings/index.jsx similarity index 100% rename from src/renderer/src/pages/settings/index.jsx rename to packages/gui/src/renderer/src/pages/settings/index.jsx diff --git a/src/renderer/src/pages/settings/index.less b/packages/gui/src/renderer/src/pages/settings/index.less similarity index 100% rename from src/renderer/src/pages/settings/index.less rename to packages/gui/src/renderer/src/pages/settings/index.less diff --git a/src/renderer/src/router.jsx b/packages/gui/src/renderer/src/router.jsx similarity index 100% rename from src/renderer/src/router.jsx rename to packages/gui/src/renderer/src/router.jsx diff --git a/src/renderer/src/settings_list.jsx b/packages/gui/src/renderer/src/settings_list.jsx similarity index 100% rename from src/renderer/src/settings_list.jsx rename to packages/gui/src/renderer/src/settings_list.jsx diff --git a/src/renderer/src/style/fix.less b/packages/gui/src/renderer/src/style/fix.less similarity index 100% rename from src/renderer/src/style/fix.less rename to packages/gui/src/renderer/src/style/fix.less diff --git a/src/renderer/src/style/index.less b/packages/gui/src/renderer/src/style/index.less similarity index 100% rename from src/renderer/src/style/index.less rename to packages/gui/src/renderer/src/style/index.less diff --git a/src/renderer/src/style/reset.css b/packages/gui/src/renderer/src/style/reset.css similarity index 100% rename from src/renderer/src/style/reset.css rename to packages/gui/src/renderer/src/style/reset.css diff --git a/src/renderer/src/style/vars.less b/packages/gui/src/renderer/src/style/vars.less similarity index 100% rename from src/renderer/src/style/vars.less rename to packages/gui/src/renderer/src/style/vars.less diff --git a/src/renderer/src/utils/getRootCssVar/index.js b/packages/gui/src/renderer/src/utils/getRootCssVar/index.js similarity index 100% rename from src/renderer/src/utils/getRootCssVar/index.js rename to packages/gui/src/renderer/src/utils/getRootCssVar/index.js diff --git a/src/renderer/src/utils/getVersions/index.js b/packages/gui/src/renderer/src/utils/getVersions/index.js similarity index 100% rename from src/renderer/src/utils/getVersions/index.js rename to packages/gui/src/renderer/src/utils/getVersions/index.js diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..a1302bc --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,35 @@ +const path = require("path") +const child_process = require("child_process") + +const packagesPath = path.resolve(__dirname, "..", "packages") + +const linkRoot = path.resolve(packagesPath, "core") + +const linkPackages = [ + path.resolve(packagesPath, "cli"), + //path.resolve(packagesPath, "gui"), +] + +async function main() { + console.log(`Linking @core to other packages...`) + + const rootPkg = require(path.resolve(linkRoot, "package.json")) + + await child_process.execSync("yarn link", { + cwd: linkRoot, + stdio: "inherit", + stdout: "inherit", + }) + + for (const linkPackage of linkPackages) { + await child_process.execSync(`yarn link "${rootPkg.name}"`, { + cwd: linkPackage, + stdio: "inherit", + stdout: "inherit", + }) + } + + console.log(`Done!`) +} + +main() \ No newline at end of file From 4342339aae79f0b93cc1f0260dc4946d818b4ec6 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 1 Apr 2024 10:48:25 +0200 Subject: [PATCH 02/14] link to gui --- scripts/postinstall.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/postinstall.js b/scripts/postinstall.js index a1302bc..f29056b 100644 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -7,7 +7,7 @@ const linkRoot = path.resolve(packagesPath, "core") const linkPackages = [ path.resolve(packagesPath, "cli"), - //path.resolve(packagesPath, "gui"), + path.resolve(packagesPath, "gui"), ] async function main() { From bcc889c6fa044b882304283cf41be3ce118533b3 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Mon, 1 Apr 2024 17:14:39 +0200 Subject: [PATCH 03/14] merge from local --- packages/cli/src/index.js | 3 + packages/core/package.json | 1 + packages/core/src/classes/PatchManager.js | 86 +- packages/core/src/db.js | 30 +- packages/core/src/generic_steps/git_clone.js | 7 +- packages/core/src/generic_steps/git_pull.js | 5 +- packages/core/src/generic_steps/git_reset.js | 14 +- packages/core/src/generic_steps/http.js | 22 +- packages/core/src/generic_steps/index.js | 6 + packages/core/src/handlers/apply.js | 55 +- packages/core/src/handlers/execute.js | 22 +- packages/core/src/handlers/install.js | 43 +- packages/core/src/handlers/read.js | 9 + packages/core/src/handlers/uninstall.js | 25 +- packages/core/src/handlers/update.js | 34 +- packages/core/src/index.js | 26 +- packages/core/src/manifest/libs/mcl/index.js | 2 + packages/core/src/manifest/libs/open/index.js | 2 + packages/core/src/manifest/reader.js | 13 +- packages/core/src/manifest/vm.js | 26 +- packages/core/src/utils/extractFile.js | 2 + packages/gui/dev-app-update.yml | 3 - packages/gui/electron-builder.yml | 8 +- packages/gui/electron.vite.config.js | 14 - packages/gui/package.json | 27 +- packages/gui/src/main/auth.js | 34 - packages/gui/src/main/classes/CoreAdapter.js | 45 + packages/gui/src/main/commands/apply.js | 169 ---- packages/gui/src/main/commands/execute.js | 116 --- packages/gui/src/main/commands/install.js | 128 --- packages/gui/src/main/commands/uninstall.js | 53 - packages/gui/src/main/commands/update.js | 137 --- packages/gui/src/main/defaults/local_db.js | 6 - .../gui/src/main/defaults/pkg_manifest.js | 19 - packages/gui/src/main/generic_steps/drive.js | 79 -- .../gui/src/main/generic_steps/git_clone.js | 44 - .../gui/src/main/generic_steps/git_pull.js | 29 - .../gui/src/main/generic_steps/git_reset.js | 77 -- packages/gui/src/main/generic_steps/http.js | 108 -- packages/gui/src/main/generic_steps/index.js | 79 -- packages/gui/src/main/index.js | 91 +- packages/gui/src/main/lib/auth/index.js | 59 -- packages/gui/src/main/lib/execa/index.d.ts | 955 ------------------ packages/gui/src/main/lib/execa/index.js | 309 ------ .../gui/src/main/lib/execa/lib/command.js | 119 --- packages/gui/src/main/lib/execa/lib/error.js | 87 -- packages/gui/src/main/lib/execa/lib/kill.js | 102 -- packages/gui/src/main/lib/execa/lib/pipe.js | 42 - .../gui/src/main/lib/execa/lib/promise.js | 36 - packages/gui/src/main/lib/execa/lib/stdio.js | 49 - packages/gui/src/main/lib/execa/lib/stream.js | 133 --- .../gui/src/main/lib/execa/lib/verbose.js | 19 - packages/gui/src/main/lib/execa/public_lib.js | 17 - .../src/main/lib/get-stream/array-buffer.js | 84 -- packages/gui/src/main/lib/get-stream/array.js | 32 - .../gui/src/main/lib/get-stream/buffer.js | 20 - .../gui/src/main/lib/get-stream/contents.js | 101 -- .../gui/src/main/lib/get-stream/index.d.ts | 119 --- packages/gui/src/main/lib/get-stream/index.js | 5 - .../src/main/lib/get-stream/index.test-d.ts | 98 -- .../gui/src/main/lib/get-stream/string.js | 36 - packages/gui/src/main/lib/get-stream/utils.js | 11 - .../gui/src/main/lib/google_drive/index.js | 194 ---- .../gui/src/main/lib/human-signals/core.js | 275 ----- .../gui/src/main/lib/human-signals/index.js | 70 -- .../gui/src/main/lib/human-signals/index.ts | 73 -- .../src/main/lib/human-signals/realtime.js | 16 - .../gui/src/main/lib/human-signals/signals.js | 34 - .../gui/src/main/lib/is-stream/index.d.ts | 81 -- packages/gui/src/main/lib/is-stream/index.js | 29 - .../gui/src/main/lib/lowdb/adapters/Memory.ts | 26 - .../main/lib/lowdb/adapters/node/DataFile.ts | 72 -- .../main/lib/lowdb/adapters/node/JSONFile.ts | 21 - .../main/lib/lowdb/adapters/node/TextFile.ts | 67 -- packages/gui/src/main/lib/lowdb/core/Low.ts | 64 -- packages/gui/src/main/lib/lowdb/index.ts | 3 - .../gui/src/main/lib/lowdb/presets/node.ts | 31 - .../gui/src/main/lib/mcl/authenticator.js | 167 --- packages/gui/src/main/lib/mcl/handler.js | 783 -------------- packages/gui/src/main/lib/mcl/index.js | 34 - packages/gui/src/main/lib/mcl/launcher.js | 224 ---- .../gui/src/main/lib/mimic-function/index.js | 71 -- .../gui/src/main/lib/npm-run-path/index.d.ts | 84 -- .../gui/src/main/lib/npm-run-path/index.js | 51 - packages/gui/src/main/lib/onetime/index.d.ts | 59 -- packages/gui/src/main/lib/onetime/index.js | 41 - packages/gui/src/main/lib/public_bind.js | 13 - .../gui/src/main/lib/renderer_ipc/index.js | 7 - packages/gui/src/main/lib/rfs/index.js | 47 - packages/gui/src/main/lib/steno/index.ts | 107 -- .../main/lib/strip-final-newline/index.d.ts | 18 - .../src/main/lib/strip-final-newline/index.js | 26 - packages/gui/src/main/local_db.js | 50 - packages/gui/src/main/manager.js | 108 -- packages/gui/src/main/prerequisites.js | 35 - packages/gui/src/main/setup.js | 144 --- packages/gui/src/main/utils/extractFile.js | 44 - packages/gui/src/main/utils/initManifest.js | 56 - .../gui/src/main/utils/parseStringVars.js | 21 - packages/gui/src/main/utils/readDirRecurse.js | 25 - packages/gui/src/main/utils/readManifest.js | 58 -- .../gui/src/main/utils/resolveJavaPath.js | 178 ---- packages/gui/src/main/vars.js | 31 - packages/gui/src/renderer/index.html | 2 +- .../src/components/ManifestInfo/index.jsx | 13 +- .../src/components/PackageItem/index.jsx | 44 +- .../{installations.jsx => packages.jsx} | 7 +- packages/gui/src/renderer/src/pages/index.jsx | 2 +- .../src/renderer/src/pages/pkg/[pkg_id].jsx | 50 +- .../src/renderer/src/pages/settings/index.jsx | 2 +- 110 files changed, 476 insertions(+), 7314 deletions(-) create mode 100644 packages/core/src/handlers/read.js delete mode 100644 packages/gui/dev-app-update.yml delete mode 100644 packages/gui/src/main/auth.js create mode 100644 packages/gui/src/main/classes/CoreAdapter.js delete mode 100644 packages/gui/src/main/commands/apply.js delete mode 100644 packages/gui/src/main/commands/execute.js delete mode 100644 packages/gui/src/main/commands/install.js delete mode 100644 packages/gui/src/main/commands/uninstall.js delete mode 100644 packages/gui/src/main/commands/update.js delete mode 100644 packages/gui/src/main/defaults/local_db.js delete mode 100644 packages/gui/src/main/defaults/pkg_manifest.js delete mode 100644 packages/gui/src/main/generic_steps/drive.js delete mode 100644 packages/gui/src/main/generic_steps/git_clone.js delete mode 100644 packages/gui/src/main/generic_steps/git_pull.js delete mode 100644 packages/gui/src/main/generic_steps/git_reset.js delete mode 100644 packages/gui/src/main/generic_steps/http.js delete mode 100644 packages/gui/src/main/generic_steps/index.js delete mode 100644 packages/gui/src/main/lib/auth/index.js delete mode 100755 packages/gui/src/main/lib/execa/index.d.ts delete mode 100755 packages/gui/src/main/lib/execa/index.js delete mode 100755 packages/gui/src/main/lib/execa/lib/command.js delete mode 100755 packages/gui/src/main/lib/execa/lib/error.js delete mode 100755 packages/gui/src/main/lib/execa/lib/kill.js delete mode 100755 packages/gui/src/main/lib/execa/lib/pipe.js delete mode 100755 packages/gui/src/main/lib/execa/lib/promise.js delete mode 100755 packages/gui/src/main/lib/execa/lib/stdio.js delete mode 100755 packages/gui/src/main/lib/execa/lib/stream.js delete mode 100755 packages/gui/src/main/lib/execa/lib/verbose.js delete mode 100644 packages/gui/src/main/lib/execa/public_lib.js delete mode 100644 packages/gui/src/main/lib/get-stream/array-buffer.js delete mode 100644 packages/gui/src/main/lib/get-stream/array.js delete mode 100644 packages/gui/src/main/lib/get-stream/buffer.js delete mode 100644 packages/gui/src/main/lib/get-stream/contents.js delete mode 100644 packages/gui/src/main/lib/get-stream/index.d.ts delete mode 100644 packages/gui/src/main/lib/get-stream/index.js delete mode 100644 packages/gui/src/main/lib/get-stream/index.test-d.ts delete mode 100644 packages/gui/src/main/lib/get-stream/string.js delete mode 100644 packages/gui/src/main/lib/get-stream/utils.js delete mode 100644 packages/gui/src/main/lib/google_drive/index.js delete mode 100644 packages/gui/src/main/lib/human-signals/core.js delete mode 100644 packages/gui/src/main/lib/human-signals/index.js delete mode 100644 packages/gui/src/main/lib/human-signals/index.ts delete mode 100644 packages/gui/src/main/lib/human-signals/realtime.js delete mode 100644 packages/gui/src/main/lib/human-signals/signals.js delete mode 100644 packages/gui/src/main/lib/is-stream/index.d.ts delete mode 100644 packages/gui/src/main/lib/is-stream/index.js delete mode 100644 packages/gui/src/main/lib/lowdb/adapters/Memory.ts delete mode 100644 packages/gui/src/main/lib/lowdb/adapters/node/DataFile.ts delete mode 100644 packages/gui/src/main/lib/lowdb/adapters/node/JSONFile.ts delete mode 100644 packages/gui/src/main/lib/lowdb/adapters/node/TextFile.ts delete mode 100644 packages/gui/src/main/lib/lowdb/core/Low.ts delete mode 100644 packages/gui/src/main/lib/lowdb/index.ts delete mode 100644 packages/gui/src/main/lib/lowdb/presets/node.ts delete mode 100644 packages/gui/src/main/lib/mcl/authenticator.js delete mode 100644 packages/gui/src/main/lib/mcl/handler.js delete mode 100644 packages/gui/src/main/lib/mcl/index.js delete mode 100644 packages/gui/src/main/lib/mcl/launcher.js delete mode 100644 packages/gui/src/main/lib/mimic-function/index.js delete mode 100644 packages/gui/src/main/lib/npm-run-path/index.d.ts delete mode 100644 packages/gui/src/main/lib/npm-run-path/index.js delete mode 100644 packages/gui/src/main/lib/onetime/index.d.ts delete mode 100644 packages/gui/src/main/lib/onetime/index.js delete mode 100644 packages/gui/src/main/lib/public_bind.js delete mode 100644 packages/gui/src/main/lib/renderer_ipc/index.js delete mode 100644 packages/gui/src/main/lib/rfs/index.js delete mode 100644 packages/gui/src/main/lib/steno/index.ts delete mode 100644 packages/gui/src/main/lib/strip-final-newline/index.d.ts delete mode 100644 packages/gui/src/main/lib/strip-final-newline/index.js delete mode 100644 packages/gui/src/main/local_db.js delete mode 100644 packages/gui/src/main/manager.js delete mode 100644 packages/gui/src/main/prerequisites.js delete mode 100644 packages/gui/src/main/setup.js delete mode 100644 packages/gui/src/main/utils/extractFile.js delete mode 100644 packages/gui/src/main/utils/initManifest.js delete mode 100644 packages/gui/src/main/utils/parseStringVars.js delete mode 100644 packages/gui/src/main/utils/readDirRecurse.js delete mode 100644 packages/gui/src/main/utils/readManifest.js delete mode 100644 packages/gui/src/main/utils/resolveJavaPath.js delete mode 100644 packages/gui/src/main/vars.js rename packages/gui/src/renderer/src/contexts/{installations.jsx => packages.jsx} (90%) diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index 2a05234..b507daf 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -15,6 +15,7 @@ const commands = [ ], fn: async (package_manifest, options) => { await core.initialize() + await core.setup() return await core.package.install(package_manifest, options) } @@ -30,6 +31,7 @@ const commands = [ ], fn: async (pkg_id, options) => { await core.initialize() + await core.setup() return await core.package.execute(pkg_id, options) } @@ -45,6 +47,7 @@ const commands = [ ], fn: async (pkg_id, options) => { await core.initialize() + await core.setup() return await core.package.update(pkg_id, options) } diff --git a/packages/core/package.json b/packages/core/package.json index 73d8369..da5bd99 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -19,6 +19,7 @@ "checksum": "^1.0.0", "cli-color": "^2.0.4", "cli-progress": "^3.12.0", + "deep-object-diff": "^1.1.9", "extends-classes": "^1.0.5", "googleapis": "^134.0.0", "human-format": "^1.2.0", diff --git a/packages/core/src/classes/PatchManager.js b/packages/core/src/classes/PatchManager.js index 0084eb9..870d224 100644 --- a/packages/core/src/classes/PatchManager.js +++ b/packages/core/src/classes/PatchManager.js @@ -1,3 +1,6 @@ +import Logger from "../logger" + +import DB from "../db" import fs from "node:fs" import GenericSteps from "../generic_steps" @@ -11,27 +14,44 @@ export default class PatchManager { this.log = Logger.child({ service: `PATCH-MANAGER|${pkg.id}` }) } - async get(patch) { + async get(select) { if (!this.manifest.patches) { return [] } let list = [] - if (typeof patch === "undefined") { + if (typeof select === "undefined") { list = this.manifest.patches - } else { - list = this.manifest.patches.find((p) => p.id === patch.id) + } + + if (Array.isArray(select)) { + for await (let id of select) { + const patch = this.manifest.patches.find((patch) => patch.id === id) + + if (patch) { + list.push(patch) + } + } } return list } - async patch(patch) { - const list = await this.get(patch) + async reapply() { + if (Array.isArray(this.pkg.applied_patches)) { + return await this.patch(this.pkg.applied_patches) + } + + return true + } + + async patch(select) { + const list = await this.get(select) for await (let patch of list) { - global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, status_text: `Applying patch [${patch.id}]...`, }) @@ -41,12 +61,6 @@ export default class PatchManager { this.log.info(`Applying ${patch.additions.length} Additions...`) for await (let addition of patch.additions) { - this.log.info(`Applying addition [${addition.id}]...`) - - global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { - status_text: `Applying addition [${additions.id}]...`, - }) - // resolve patch file addition.file = await parseStringVars(addition.file, this.pkg) @@ -55,14 +69,26 @@ export default class PatchManager { continue } + this.log.info(`Applying addition [${addition.file}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Applying addition [${addition.file}]`, + }) + await GenericSteps(this.pkg, addition.steps, this.log) } } - pkg.applied_patches.push(patch.id) + if (!this.pkg.applied_patches.includes(patch.id)) { + this.pkg.applied_patches.push(patch.id) + } } - global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, status_text: `${list.length} Patches applied`, }) @@ -71,42 +97,48 @@ export default class PatchManager { return this.pkg } - async remove(patch) { - const list = await this.get(patch) + async remove(select) { + const list = await this.get(select) for await (let patch of list) { - global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, status_text: `Removing patch [${patch.id}]...`, }) - Log.info(`Removing patch [${patch.id}]...`) + this.log.info(`Removing patch [${patch.id}]...`) if (Array.isArray(patch.additions)) { this.log.info(`Removing ${patch.additions.length} Additions...`) for await (let addition of patch.additions) { - this.log.info(`Removing addition [${addition.id}]...`) - - global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { - status_text: `Removing addition [${additions.id}]...`, - }) - addition.file = await parseStringVars(addition.file, this.pkg) if (!fs.existsSync(addition.file)) { + this.log.info(`Addition [${addition.file}] does not exist. Skipping...`) continue } + this.log.info(`Removing addition [${addition.file}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Removing addition [${addition.file}]`, + }) + await fs.promises.unlink(addition.file) } } - pkg.applied_patches = pkg.applied_patches.filter((p) => { + this.pkg.applied_patches = this.pkg.applied_patches.filter((p) => { return p !== patch.id }) } - global._relic_eventBus.emit(`pkg:update:state:${this.pkg.id}`, { + await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, status_text: `${list.length} Patches removed`, }) diff --git a/packages/core/src/db.js b/packages/core/src/db.js index 6d736a0..4d99499 100644 --- a/packages/core/src/db.js +++ b/packages/core/src/db.js @@ -15,7 +15,9 @@ export default class DB { static defaultPackageState({ id, name, + icon, version, + author, install_path, description, license, @@ -23,13 +25,16 @@ export default class DB { remote_manifest, local_manifest, config, + executable, }) { return { id: id, name: name, version: version, + icon: icon, install_path: install_path, description: description, + author: author, license: license ?? "unlicensed", local_manifest: local_manifest ?? null, remote_manifest: remote_manifest ?? null, @@ -38,6 +43,7 @@ export default class DB { last_status: last_status ?? "installing", last_update: null, installed_at: null, + executable: executable ?? false, } } @@ -72,31 +78,27 @@ export default class DB { static async writePackage(pkg) { const db = await this.withDB() - await db.update((data) => { - const prevIndex = data["packages"].findIndex((i) => i.id === pkg.id) + const prevIndex = db.data["packages"].findIndex((i) => i.id === pkg.id) - if (prevIndex !== -1) { - data["packages"][prevIndex] = pkg - } else { - data["packages"].push(pkg) - } + if (prevIndex !== -1) { + db.data["packages"][prevIndex] = pkg + } else { + db.data["packages"].push(pkg) + } - return data - }) + await db.write() - return pkg + return db.data } static async updatePackageById(pkg_id, obj) { - const pkg = await this.getPackages(pkg_id) + let pkg = await this.getPackages(pkg_id) if (!pkg) { throw new Error("Package not found") } - pkg = lodash.merge(pkg, obj) - - return await this.writePackage(pkg) + return await this.writePackage(lodash.merge({ ...pkg }, obj)) } static async deletePackage(pkg_id) { diff --git a/packages/core/src/generic_steps/git_clone.js b/packages/core/src/generic_steps/git_clone.js index 574e953..da0a3c2 100644 --- a/packages/core/src/generic_steps/git_clone.js +++ b/packages/core/src/generic_steps/git_clone.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import path from "node:path" import fs from "node:fs" import upath from "upath" @@ -21,8 +23,9 @@ export default async (pkg, step) => { Log.info(`Cloning from [${step.url}]`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { - status_text: `Cloning from [${step.url}]...`, + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Cloning from [${step.url}]`, }) const args = [ diff --git a/packages/core/src/generic_steps/git_pull.js b/packages/core/src/generic_steps/git_pull.js index e4b60cb..f60db44 100644 --- a/packages/core/src/generic_steps/git_pull.js +++ b/packages/core/src/generic_steps/git_pull.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import path from "node:path" import fs from "node:fs" import { execa } from "../libraries/execa" @@ -14,7 +16,8 @@ export default async (pkg, step) => { const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" const _path = path.resolve(pkg.install_path, step.path) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Pulling...`, }) diff --git a/packages/core/src/generic_steps/git_reset.js b/packages/core/src/generic_steps/git_reset.js index bb78a8f..60c31f4 100644 --- a/packages/core/src/generic_steps/git_reset.js +++ b/packages/core/src/generic_steps/git_reset.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import path from "node:path" import fs from "node:fs" import { execa } from "../libraries/execa" @@ -23,7 +25,8 @@ export default async (pkg, step) => { Log.info(`Fetching from origin`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Fetching from origin...`, }) @@ -36,7 +39,8 @@ export default async (pkg, step) => { Log.info(`Cleaning untracked files...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Cleaning untracked files...`, }) @@ -48,7 +52,8 @@ export default async (pkg, step) => { Log.info(`Resetting to ${from}`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Resetting to ${from}`, }) @@ -63,7 +68,8 @@ export default async (pkg, step) => { Log.info(`Checkout to HEAD`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Checkout to HEAD`, }) diff --git a/packages/core/src/generic_steps/http.js b/packages/core/src/generic_steps/http.js index ade7830..57f614c 100644 --- a/packages/core/src/generic_steps/http.js +++ b/packages/core/src/generic_steps/http.js @@ -15,9 +15,9 @@ export default async (pkg, step, logger) => { let _path = path.resolve(pkg.install_path, step.path) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { - status: "loading", - statusText: `Downloading [${step.url}]`, + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Downloading [${step.url}]`, }) logger.info(`Downloading [${step.url} to ${_path}]`) @@ -29,8 +29,10 @@ export default async (pkg, step, logger) => { fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) await downloadHttpFile(step.url, _path, (progress) => { - global._relic_eventBus(`pkg:update:state:${pkg.id}`, { - statusText: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + use_id_only: true, + status_text: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, }) }) @@ -43,8 +45,9 @@ export default async (pkg, step, logger) => { step.extract = path.resolve(pkg.install_path, ".") } - global._relic_eventBus(`pkg:update:state:${pkg.id}`, { - statusText: `Extracting bundle...`, + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Extracting bundle...`, }) await extractFile(_path, step.extract) @@ -52,8 +55,9 @@ export default async (pkg, step, logger) => { if (step.deleteAfterExtract !== false) { logger.info(`Deleting temporal file [${_path}]...`) - global._relic_eventBus(`pkg:update:state:${pkg.id}`, { - statusText: `Deleting temporal files...`, + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Deleting temporal files...`, }) await fs.promises.rm(_path, { recursive: true }) diff --git a/packages/core/src/generic_steps/index.js b/packages/core/src/generic_steps/index.js index 714ce0a..96e2017 100644 --- a/packages/core/src/generic_steps/index.js +++ b/packages/core/src/generic_steps/index.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import ISM_GIT_CLONE from "./git_clone" import ISM_GIT_PULL from "./git_pull" import ISM_GIT_RESET from "./git_reset" @@ -20,6 +22,10 @@ const StepsOrders = [ export default async function processGenericSteps(pkg, steps, logger = Logger) { logger.info(`Processing generic steps...`) + if (!Array.isArray(steps)) { + throw new Error(`Steps must be an array`) + } + if (steps.length === 0) { return pkg } diff --git a/packages/core/src/handlers/apply.js b/packages/core/src/handlers/apply.js index 3d41d97..7b9f47e 100644 --- a/packages/core/src/handlers/apply.js +++ b/packages/core/src/handlers/apply.js @@ -1,28 +1,30 @@ +import Logger from "../logger" + +import PatchManager from "../classes/PatchManager" import ManifestReader from "../manifest/reader" import ManifestVM from "../manifest/vm" import DB from "../db" const BaseLog = Logger.child({ service: "APPLIER" }) -function findPatch(manifest, changes, mustBeInstalled) { - return manifest.patches - .filter((patch) => { - const patchID = patch.id +function findPatch(patches, applied_patches, changes, mustBeInstalled) { + return patches.filter((patch) => { + const patchID = patch.id - if (typeof changes.patches[patchID] === "undefined") { - return false - } + if (typeof changes.patches[patchID] === "undefined") { + return false + } - if (mustBeInstalled === true && !manifest.applied_patches.includes(patch.id) && changes.patches[patchID] === true) { - return true - } + if (mustBeInstalled === true && !applied_patches.includes(patch.id) && changes.patches[patchID] === true) { + return true + } - if (mustBeInstalled === false && manifest.applied_patches.includes(patch.id) && changes.patches[patchID] === false) { - return true - } + if (mustBeInstalled === false && applied_patches.includes(patch.id) && changes.patches[patchID] === false) { + return true + } - return false - }) + return false + }).map((patch) => patch.id) } export default async function apply(pkg_id, changes = {}) { @@ -35,11 +37,18 @@ export default async function apply(pkg_id, changes = {}) { } let manifest = await ManifestReader(pkg.local_manifest) - manifest = await ManifestVM(ManifestRead.code) + manifest = await ManifestVM(manifest.code) const Log = Logger.child({ service: `APPLIER|${pkg.id}` }) Log.info(`Applying changes to package...`) + Log.info(`Changes: ${JSON.stringify(changes)}`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Applying changes to package...`, + last_status: "loading", + }) if (changes.patches) { if (!Array.isArray(pkg.applied_patches)) { @@ -48,8 +57,8 @@ export default async function apply(pkg_id, changes = {}) { const patches = new PatchManager(pkg, manifest) - await patches.remove(findPatch(manifest, changes, false)) - await patches.patch(findPatch(manifest, changes, true)) + await patches.remove(findPatch(manifest.patches, pkg.applied_patches, changes, false)) + await patches.patch(findPatch(manifest.patches, pkg.applied_patches, changes, true)) } if (changes.config) { @@ -64,15 +73,19 @@ export default async function apply(pkg_id, changes = {}) { await DB.writePackage(pkg) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { - state: "All changes applied", + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: "All changes applied", }) Log.info(`All changes applied to package.`) return pkg } catch (error) { - global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + error + }) BaseLog.error(`Failed to apply changes to package [${pkg_id}]`, error) BaseLog.error(error.stack) diff --git a/packages/core/src/handlers/execute.js b/packages/core/src/handlers/execute.js index 039af00..6f31d7b 100644 --- a/packages/core/src/handlers/execute.js +++ b/packages/core/src/handlers/execute.js @@ -1,7 +1,8 @@ +import Logger from "../logger" + import fs from "node:fs" import DB from "../db" -import SetupHelper from "../helpers/setup" import ManifestReader from "../manifest/reader" import ManifestVM from "../manifest/vm" import parseStringVars from "../utils/parseStringVars" @@ -18,8 +19,6 @@ export default async function execute(pkg_id, { useRemote = false, force = false return false } - await SetupHelper() - const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest if (!fs.existsSync(manifestPath)) { @@ -30,6 +29,12 @@ export default async function execute(pkg_id, { useRemote = false, force = false return false } + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "loading", + status_text: null, + }) + const ManifestRead = await ManifestReader(manifestPath) const manifest = await ManifestVM(ManifestRead.code) @@ -52,9 +57,18 @@ export default async function execute(pkg_id, { useRemote = false, force = false }) } + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "installed", + status_text: null, + }) + return pkg } catch (error) { - global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + error + }) BaseLog.error(`Failed to execute package [${pkg_id}]`, error) BaseLog.error(error.stack) diff --git a/packages/core/src/handlers/install.js b/packages/core/src/handlers/install.js index 4efd4a9..45e80cf 100644 --- a/packages/core/src/handlers/install.js +++ b/packages/core/src/handlers/install.js @@ -1,7 +1,8 @@ +import Logger from "../logger" + import fs from "node:fs" import DB from "../db" -import SetupHelper from "../helpers/setup" import ManifestReader from "../manifest/reader" import ManifestVM from "../manifest/vm" import GenericSteps from "../generic_steps" @@ -13,8 +14,6 @@ export default async function install(manifest) { let id = null try { - await SetupHelper() - BaseLog.info(`Invoking new installation...`) BaseLog.info(`Fetching manifest [${manifest}]`) @@ -44,6 +43,7 @@ export default async function install(manifest) { Log.info(`Appending to db...`) const pkg = DB.defaultPackageState({ + ...manifest.constructor, id: id, name: manifest.constructor.pkg_name, version: manifest.constructor.version, @@ -53,6 +53,7 @@ export default async function install(manifest) { last_status: "installing", remote_manifest: ManifestRead.remote_manifest, local_manifest: ManifestRead.local_manifest, + executable: !!manifest.execute }) await DB.writePackage(pkg) @@ -72,7 +73,8 @@ export default async function install(manifest) { if (typeof manifest.beforeInstall === "function") { Log.info(`Executing beforeInstall hook...`) - global._relic_eventBus.emit(`pkg:update:state:${id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Performing beforeInstall hook...`, }) @@ -82,7 +84,8 @@ export default async function install(manifest) { if (Array.isArray(manifest.installSteps)) { Log.info(`Executing generic install steps...`) - global._relic_eventBus.emit(`pkg:update:state:${id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Performing generic install steps...`, }) @@ -92,14 +95,16 @@ export default async function install(manifest) { if (typeof manifest.afterInstall === "function") { Log.info(`Executing afterInstall hook...`) - global._relic_eventBus.emit(`pkg:update:state:${id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Performing afterInstall hook...`, }) await manifest.afterInstall(pkg) } - global._relic_eventBus.emit(`pkg:update:state:${id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Finishing up...`, }) @@ -119,7 +124,7 @@ export default async function install(manifest) { } pkg.local_manifest = finalPath - pkg.last_status = "installed" + pkg.last_status = "loading" pkg.installed_at = Date.now() await DB.writePackage(pkg) @@ -130,7 +135,8 @@ export default async function install(manifest) { if (defaultPatches.length > 0) { Log.info(`Applying default patches...`) - global._relic_eventBus.emit(`pkg:update:state:${id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Applying default patches...`, }) @@ -140,17 +146,30 @@ export default async function install(manifest) { } } - global._relic_eventBus.emit(`pkg:update:state:${id}`, { + pkg.last_status = "installed" + + await DB.writePackage(pkg) + + global._relic_eventBus.emit(`pkg:update:state`, { + ...pkg, + id: pkg.id, + last_status: "installed", status_text: `Installation completed successfully`, }) + global._relic_eventBus.emit(`pkg:new:done`, pkg) + Log.info(`Package installed successfully!`) return pkg } catch (error) { - global._relic_eventBus.emit(`pkg:${id}:error`, error) + global._relic_eventBus.emit(`pkg:error`, { + id: pkg.id, + error + }) - global._relic_eventBus.emit(`pkg:update:state:${id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, last_status: "failed", status_text: `Installation failed`, }) diff --git a/packages/core/src/handlers/read.js b/packages/core/src/handlers/read.js new file mode 100644 index 0000000..225842f --- /dev/null +++ b/packages/core/src/handlers/read.js @@ -0,0 +1,9 @@ +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +export default async function softRead(manifest, options = {}) { + const Reader = await ManifestReader(manifest) + const VM = await ManifestVM(Reader.code, options) + + return VM +} \ No newline at end of file diff --git a/packages/core/src/handlers/uninstall.js b/packages/core/src/handlers/uninstall.js index 531245c..b0ea282 100644 --- a/packages/core/src/handlers/uninstall.js +++ b/packages/core/src/handlers/uninstall.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import DB from "../db" import ManifestReader from "../manifest/reader" import ManifestVM from "../manifest/vm" @@ -18,7 +20,8 @@ export default async function uninstall(pkg_id) { const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` }) Log.info(`Uninstalling package...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Uninstalling package...`, }) @@ -27,33 +30,41 @@ export default async function uninstall(pkg_id) { if (typeof manifest.uninstall === "function") { Log.info(`Performing uninstall hook...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Performing uninstall hook...`, }) await manifest.uninstall(pkg) } Log.info(`Deleting package directory...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Deleting package directory...`, }) await rimraf(pkg.install_path) Log.info(`Removing package from database...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Removing package from database...`, }) await DB.deletePackage(pkg.id) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { - status: "deleted", + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "deleted", status_text: `Uninstalling package...`, }) + global._relic_eventBus.emit(`pkg:remove`, pkg) Log.info(`Package uninstalled successfully!`) return pkg } catch (error) { - global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + error + }) BaseLog.error(`Failed to uninstall package [${pkg_id}]`, error) BaseLog.error(error.stack) diff --git a/packages/core/src/handlers/update.js b/packages/core/src/handlers/update.js index 8cd46e3..87d4ce0 100644 --- a/packages/core/src/handlers/update.js +++ b/packages/core/src/handlers/update.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import DB from "../db" import ManifestReader from "../manifest/reader" @@ -39,15 +41,21 @@ export default async function update(pkg_id) { let ManifestRead = await ManifestReader(pkg.local_manifest) let manifest = await ManifestVM(ManifestRead.code) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { - status: "updating", + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "updating", status_text: `Updating package...`, }) + pkg.last_status = "updating" + + await DB.writePackage(pkg) + if (typeof manifest.update === "function") { Log.info(`Performing update hook...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Performing update hook...`, }) @@ -57,7 +65,8 @@ export default async function update(pkg_id) { if (manifest.updateSteps) { Log.info(`Performing update steps...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Performing update steps...`, }) @@ -67,13 +76,14 @@ export default async function update(pkg_id) { if (Array.isArray(pkg.applied_patches)) { const patchManager = new PatchManager(pkg, manifest) - await patchManager.patch(pkg.applied_patches) + await patchManager.reapply() } if (typeof manifest.afterUpdate === "function") { Log.info(`Performing after update hook...`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, status_text: `Performing after update hook...`, }) @@ -91,20 +101,24 @@ export default async function update(pkg_id) { } } - pkg.status = "installed" + pkg.last_status = "installed" pkg.last_update = Date.now() await DB.writePackage(pkg) Log.info(`Package updated successfully`) - global._relic_eventBus.emit(`pkg:update:state:${pkg.id}`, { - status: "installed", + global._relic_eventBus.emit(`pkg:update:state`, { + ...pkg, + id: pkg.id, }) return pkg } catch (error) { - global._relic_eventBus.emit(`pkg:${pkg_id}:error`, error) + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + error + }) BaseLog.error(`Failed to update package [${pkg_id}]`, error) BaseLog.error(error.stack) diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 9fe38dd..0b24d70 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -9,13 +9,24 @@ import Logger from "./logger" import Vars from "./vars" import DB from "./db" +import PackageInstall from "./handlers/install" +import PackageExecute from "./handlers/execute" +import PackageUninstall from "./handlers/uninstall" +import PackageUpdate from "./handlers/update" +import PackageApply from "./handlers/apply" +import PackageList from "./handlers/list" +import PackageRead from "./handlers/read" + export default class RelicCore { constructor(params) { this.params = params } eventBus = global._relic_eventBus = new EventEmitter() - logger = global.Logger = Logger + + logger = Logger + + db = DB async initialize() { await DB.initialize() @@ -34,12 +45,13 @@ export default class RelicCore { } package = { - install: require("./handlers/install").default, - execute: require("./handlers/execute").default, - uninstall: require("./handlers/uninstall").default, - update: require("./handlers/update").default, - apply: require("./handlers/apply").default, - list: require("./handlers/list").default, + install: PackageInstall, + execute: PackageExecute, + uninstall: PackageUninstall, + update: PackageUpdate, + apply: PackageApply, + list: PackageList, + read: PackageRead, } openPath(pkg_id) { diff --git a/packages/core/src/manifest/libs/mcl/index.js b/packages/core/src/manifest/libs/mcl/index.js index 11b5a2c..1386624 100644 --- a/packages/core/src/manifest/libs/mcl/index.js +++ b/packages/core/src/manifest/libs/mcl/index.js @@ -1,3 +1,5 @@ +import Logger from "../../../logger" + import Client from "./launcher" import Authenticator from "./authenticator" diff --git a/packages/core/src/manifest/libs/open/index.js b/packages/core/src/manifest/libs/open/index.js index d957150..d696826 100644 --- a/packages/core/src/manifest/libs/open/index.js +++ b/packages/core/src/manifest/libs/open/index.js @@ -1,3 +1,5 @@ +import Logger from "../../../logger" + import open, { apps } from "open" const Log = Logger.child({ service: "OPEN-LIB" }) diff --git a/packages/core/src/manifest/reader.js b/packages/core/src/manifest/reader.js index 3a6017e..830278f 100644 --- a/packages/core/src/manifest/reader.js +++ b/packages/core/src/manifest/reader.js @@ -1,6 +1,7 @@ import fs from "node:fs" import path from "node:path" -import downloadHttpFile from "../helpers/downloadHttpFile" +import axios from "axios" +import checksum from "checksum" import Vars from "../vars" @@ -15,13 +16,19 @@ export async function readManifest(manifest) { fs.mkdirSync(Vars.cache_path, { recursive: true }) } - const cachedManifest = await downloadHttpFile(manifest, path.resolve(Vars.cache_path, `${Date.now()}.rmanifest`)) + const { data: code } = await axios.get(target) + + const manifestChecksum = checksum(code, { algorithm: "md5" }) + + const cachedManifest = path.join(Vars.cache_path, `${manifestChecksum}.rmanifest`) + + await fs.promises.writeFile(cachedManifest, code) return { remote_manifest: manifest, local_manifest: cachedManifest, is_catched: true, - code: fs.readFileSync(cachedManifest, "utf8"), + code: code, } } else { if (!fs.existsSync(target)) { diff --git a/packages/core/src/manifest/vm.js b/packages/core/src/manifest/vm.js index 80341e7..65bcbe3 100644 --- a/packages/core/src/manifest/vm.js +++ b/packages/core/src/manifest/vm.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import os from "node:os" import vm from "node:vm" import path from "node:path" @@ -8,7 +10,15 @@ import FetchLibraries from "./libraries" import Vars from "../vars" -async function BuildManifest(baseClass, context, soft = false) { +async function BuildManifest(baseClass, context, { soft = false } = {}) { + // inject install_path + context.install_path = path.resolve(Vars.packages_path, baseClass.id) + baseClass.install_path = context.install_path + + if (soft === true) { + return baseClass + } + const configManager = new ManifestConfigManager(baseClass.id) await configManager.initialize() @@ -22,10 +32,6 @@ async function BuildManifest(baseClass, context, soft = false) { ] } - // inject install_path - context.install_path = path.resolve(Vars.packages_path, baseClass.id) - baseClass.install_path = context.install_path - // modify context context.Log = Logger.child({ service: `VM|${baseClass.id}` }) context.Lib = await FetchLibraries(dependencies, { @@ -46,7 +52,7 @@ function injectUseManifest(code) { return code + "\n\nuse(Manifest);" } -export default async (code) => { +export default async (code, { soft = false } = {}) => { return await new Promise(async (resolve, reject) => { try { code = injectUseManifest(code) @@ -55,7 +61,13 @@ export default async (code) => { Vars: Vars, Log: Logger.child({ service: "MANIFEST_VM" }), use: (baseClass) => { - BuildManifest(baseClass, context).then(resolve) + return BuildManifest( + baseClass, + context, + { + soft: soft, + } + ).then(resolve) }, os_string: resolveOs(), arch: os.arch(), diff --git a/packages/core/src/utils/extractFile.js b/packages/core/src/utils/extractFile.js index cc22a54..19040a7 100644 --- a/packages/core/src/utils/extractFile.js +++ b/packages/core/src/utils/extractFile.js @@ -1,3 +1,5 @@ +import Logger from "../logger" + import fs from "node:fs" import path from "node:path" import { pipeline as streamPipeline } from "node:stream/promises" diff --git a/packages/gui/dev-app-update.yml b/packages/gui/dev-app-update.yml deleted file mode 100644 index c8c0c0f..0000000 --- a/packages/gui/dev-app-update.yml +++ /dev/null @@ -1,3 +0,0 @@ -provider: generic -url: https://example.com/auto-updates -updaterCacheDirName: rs-bundler-updater diff --git a/packages/gui/electron-builder.yml b/packages/gui/electron-builder.yml index 6677137..1293417 100644 --- a/packages/gui/electron-builder.yml +++ b/packages/gui/electron-builder.yml @@ -1,5 +1,5 @@ -appId: com.ragestudio.bundler -productName: rs-bundler +appId: com.ragestudio.relic +productName: Relic directories: buildResources: build files: @@ -11,7 +11,7 @@ files: asarUnpack: - resources/** win: - executableName: rs-bundler + executableName: relic icon: resources/icon.ico nsis: artifactName: ${name}-${version}-setup.${ext} @@ -40,4 +40,4 @@ appImage: npmRebuild: false publish: provider: generic - url: https://storage.ragestudio.net/rs-bundler/release + url: https://storage.ragestudio.net/relic/release diff --git a/packages/gui/electron.vite.config.js b/packages/gui/electron.vite.config.js index 9fad50e..f1c9f06 100644 --- a/packages/gui/electron.vite.config.js +++ b/packages/gui/electron.vite.config.js @@ -5,23 +5,9 @@ import react from "@vitejs/plugin-react" export default defineConfig({ main: { plugins: [externalizeDepsPlugin()], - // build: { - // rollupOptions: { - // output: { - // format: "es" - // } - // } - // }, }, preload: { plugins: [externalizeDepsPlugin()], - // build: { - // rollupOptions: { - // output: { - // format: "es" - // } - // } - // }, }, renderer: { server: { diff --git a/packages/gui/package.json b/packages/gui/package.json index fe0b25c..a57a5e4 100644 --- a/packages/gui/package.json +++ b/packages/gui/package.json @@ -6,8 +6,6 @@ "author": "RageStudio", "license": "MIT", "scripts": { - "format": "prettier --write .", - "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "start": "electron-vite preview", "dev": "electron-vite dev", "build": "electron-vite build", @@ -26,48 +24,33 @@ "@imjs/electron-differential-updater": "^5.1.7", "@loadable/component": "^5.16.3", "@ragestudio/hermes": "^0.1.1", - "adm-zip": "^0.5.10", "antd": "^5.13.2", "checksum": "^1.0.0", "classnames": "^2.3.2", + "electron-build": "^0.0.3", "electron-differential-updater": "^4.3.2", "electron-is-dev": "^2.0.0", "electron-store": "^8.1.0", "electron-updater": "^6.1.1", - "googleapis": "^105.0.0", "got": "11.8.3", "human-format": "^1.2.0", "less": "^4.2.0", "lodash": "^4.17.21", - "merge-stream": "^2.0.0", - "node-7z": "^3.0.0", - "open": "8.4.2", - "progress-stream": "^2.0.0", "protocol-registry": "^1.4.1", "react-icons": "^4.11.0", + "react-motion": "0.5.2", "react-router-dom": "6.6.2", "react-spinners": "^0.13.8", "react-spring": "^9.7.3", - "react-motion": "0.5.2", - "request": "^2.88.2", - "rimraf": "^5.0.5", "signal-exit": "^4.1.0", - "unzipper": "^0.10.14", - "upath": "^2.0.1", - "uuid": "^9.0.1", "which": "^4.0.0", "winreg": "^1.2.5" }, "devDependencies": { - "@electron-toolkit/eslint-config": "^1.0.1", - "@electron-toolkit/eslint-config-prettier": "^1.0.1", "@vitejs/plugin-react": "^4.0.4", - "electron": "^25.6.0", - "electron-builder": "^24.6.3", - "electron-vite": "^1.0.27", - "eslint": "^8.47.0", - "eslint-plugin-react": "^7.33.2", - "prettier": "^3.0.2", + "electron": "29.1.6", + "electron-builder": "24.6.3", + "electron-vite": "^2.1.0", "react": "^17.0.2", "react-dom": "^17.0.2", "vite": "^4.4.9" diff --git a/packages/gui/src/main/auth.js b/packages/gui/src/main/auth.js deleted file mode 100644 index 7107cde..0000000 --- a/packages/gui/src/main/auth.js +++ /dev/null @@ -1,34 +0,0 @@ -import { safeStorage } from "electron" -import sendToRender from "./utils/sendToRender" - -export default class AuthService { - authorize(pkg_id, token) { - console.log("Authorizing", pkg_id, token) - global.SettingsStore.set(`auth:${pkg_id}`, safeStorage.encryptString(token)) - - sendToRender(`new:notification`, { - message: "Authorized", - description: "Now you can start this package", - }) - - return true - } - - unauthorize(pkg_id) { - global.SettingsStore.delete(`auth:${pkg_id}`) - - return true - } - - getAuth(pkg_id) { - const value = global.SettingsStore.get(`auth:${pkg_id}`) - - if (!value) { - return null - } - - console.log("getAuth", value) - - return safeStorage.decryptString(Buffer.from(value.data)) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/classes/CoreAdapter.js b/packages/gui/src/main/classes/CoreAdapter.js new file mode 100644 index 0000000..8f6ed83 --- /dev/null +++ b/packages/gui/src/main/classes/CoreAdapter.js @@ -0,0 +1,45 @@ +import sendToRender from "../utils/sendToRender" + +export default class CoreAdapter { + constructor(electronApp, RelicCore) { + this.app = electronApp + this.core = RelicCore + + this.initialize() + } + + events = { + "pkg:new": (pkg) => { + sendToRender("pkg:new", pkg) + }, + "pkg:remove": (pkg) => { + sendToRender("pkg:remove", pkg) + }, + "pkg:update:state": (data = {}) => { + if (!data.id) { + return false + } + + if (data.use_id_only === true) { + return sendToRender(`pkg:update:state:${data.id}`, data) + } + + return sendToRender("pkg:update:state", data) + }, + "pkg:new:done": (pkg) => { + sendToRender("pkg:new:done", pkg) + } + } + + initialize = () => { + for (const [key, handler] of Object.entries(this.events)) { + global._relic_eventBus.on(key, handler) + } + } + + deinitialize = () => { + for (const [key, handler] of Object.entries(this.events)) { + global._relic_eventBus.off(key, handler) + } + } +} \ No newline at end of file diff --git a/packages/gui/src/main/commands/apply.js b/packages/gui/src/main/commands/apply.js deleted file mode 100644 index 92a4f8e..0000000 --- a/packages/gui/src/main/commands/apply.js +++ /dev/null @@ -1,169 +0,0 @@ -import fs from "node:fs" - -import sendToRender from "../utils/sendToRender" -import initManifest from "../utils/initManifest" -import parseStringVars from "../utils/parseStringVars" -import processGenericSteps from "../generic_steps" - -import { - updateInstalledPackage, - getInstalledPackages, -} from "../local_db" - -function findPatch(pkg, changes, mustBeInstalled) { - return pkg.patches - .filter((patch) => { - const patchID = patch.id - - if (typeof changes.patches[patchID] === "undefined") { - return false - } - - if (mustBeInstalled === true && !pkg.applied_patches.includes(patch.id) && changes.patches[patchID] === true) { - return true - } - - if (mustBeInstalled === false && pkg.applied_patches.includes(patch.id) && changes.patches[patchID] === false) { - return true - } - - return false - }) -} - -export default async function apply(pkg_id, changes) { - try { - let pkg = await getInstalledPackages(pkg_id) - - if (!pkg) { - sendToRender("runtime:error", "Package not found") - return false - } - - pkg = await initManifest(pkg) - - console.log(`[${pkg_id}] apply() | Applying changes... >`, changes) - - if (typeof changes.patches !== "undefined") { - if (!Array.isArray(pkg.applied_patches)) { - pkg.applied_patches = [] - } - - const disablePatches = findPatch(pkg, changes, false) - - const installPatches = findPatch(pkg, changes, true) - - for await (let patch of disablePatches) { - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "loading", - statusText: `Removing patch [${patch.id}]...`, - }) - - console.log(`[${pkg_id}] apply() | Removing patch [${patch.id}]...`) - - // remove patch additions - for await (let addition of patch.additions) { - // resolve patch file - addition.file = await parseStringVars(addition.file, pkg) - - console.log(`[${pkg_id}] apply() | Removing addition [${addition.file}]...`) - - if (!fs.existsSync(addition.file)) { - continue - } - - // remove addition - await fs.promises.unlink(addition.file, { force: true, recursive: true }) - } - - // TODO: remove file patch overrides with original file - // remove from applied patches - pkg.applied_patches = pkg.applied_patches.filter((p) => { - return p !== patch.id - }) - - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "done", - statusText: `Patch [${patch.id}] removed!`, - }) - } - - for await (let patch of installPatches) { - if (pkg.applied_patches.includes(patch.id)) { - console.log(`[${pkg_id}] apply() | Patch [${patch.id}] already applied. Skipping...`) - continue - } - - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "loading", - statusText: `Applying patch [${patch.id}]...`, - }) - - console.log(`[${pkg_id}] apply() | Applying patch [${patch.id}]...`) - - for await (let addition of patch.additions) { - console.log(`Processing addition [${addition.file}]`, addition) - - // resolve patch file - addition.file = await parseStringVars(addition.file, pkg) - - if (fs.existsSync(addition.file)) { - continue - } - - await processGenericSteps(pkg, addition.steps) - } - - // add to applied patches - pkg.applied_patches.push(patch.id) - - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "done", - statusText: `Patch [${patch.id}] applied!`, - }) - } - } - - if (changes.configs) { - if (!pkg.storaged_configs) { - pkg.storaged_configs = Object.entries(pkg.configs).reduce((acc, [key, value]) => { - acc[key] = value.default - - return acc - }, {}) - } - - if (Object.keys(changes.configs).length !== 0) { - Object.entries(changes.configs).forEach(([key, value]) => { - pkg.storaged_configs[key] = value - }) - } - } - - await updateInstalledPackage(pkg) - - sendToRender(`new:message`, { - type: "info", - message: "Changes applied", - }) - - sendToRender(`pkg:update:status`, { - ...pkg, - }) - - console.log(`[${pkg_id}] apply() | Changes applied`) - - return true - } catch (error) { - console.log(error) - - sendToRender(`new:notification`, { - type: "error", - message: "Failed to apply changes", - }) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/commands/execute.js b/packages/gui/src/main/commands/execute.js deleted file mode 100644 index 5c8eb9a..0000000 --- a/packages/gui/src/main/commands/execute.js +++ /dev/null @@ -1,116 +0,0 @@ -import { - getInstalledPackages, -} from "../local_db" - -import readManifest from "../utils/readManifest" -import initManifest from "../utils/initManifest" -import parseStringVars from "../utils/parseStringVars" -import sendToRender from "../utils/sendToRender" - -import UpdateCMD from "./update" - -export default async function execute(pkg_id, { force = false } = {}) { - let pkg = await getInstalledPackages(pkg_id) - - if (!pkg) { - sendToRender("runtime:error", "Package not found") - return false - } - - sendToRender("pkg:update:status", { - id: pkg_id, - status: "loading", - statusText: `Executing...`, - }) - - console.log(`[${pkg_id}] execute() | Executing...`) - - if (pkg.remote_url) { - pkg = { - ...pkg, - ...await readManifest(pkg, { just_read: true }), - } - } - - pkg = await initManifest(pkg) - - if (pkg.check_updates_after_execute === true) { - if (pkg._original_manifest) { - if ((pkg._original_manifest.version !== pkg.version) && !force) { - console.log(`[${pkg_id}] execute() | Update available (${pkg._original_manifest.version} -> ${pkg.version}). Aborting...`,) - - if (global.SettingsStore.get("pkg_auto_update_on_execute") === true) { - await UpdateCMD(pkg_id) - } else { - sendToRender("pkg:update_available", { - manifest: pkg._original_manifest, - current_version: pkg._original_manifest.version, - new_version: pkg.version, - }) - - sendToRender("pkg:update:status", { - id: pkg_id, - status: "installed", - }) - - return false - } - } - } - } - - if (typeof pkg.after_execute === "function") { - await pkg.after_execute(pkg) - } - - if (typeof pkg.execute === "string") { - pkg.execute = parseStringVars(pkg.execute, pkg) - - console.log(`[${pkg_id}] execute() | Executing binary from path >`, pkg.execute) - - await new Promise((resolve, reject) => { - const process = child_process.execFile(pkg.execute, [], { - shell: true, - cwd: pkg.install_path, - }) - - process.on("exit", resolve) - process.on("error", reject) - }) - } else { - try { - if (typeof pkg.execute !== "function") { - sendToRender("installation:status", { - id: pkg_id, - status: "error", - statusText: "No execute function found", - }) - - return false - } - - await pkg.execute(pkg) - } catch (error) { - sendToRender("new:notification", { - type: "error", - message: "Failed to launch", - description: error.toString(), - }) - - return sendToRender("pkg:update:status", { - id: pkg_id, - status: "installed", - statusText: `Failed to launch`, - }) - } - } - - sendToRender("pkg:update:status", { - id: pkg_id, - status: "installed", - }) - - console.log(`[${pkg_id}] execute() | Successfully executed`) - - return true -} \ No newline at end of file diff --git a/packages/gui/src/main/commands/install.js b/packages/gui/src/main/commands/install.js deleted file mode 100644 index 68012c3..0000000 --- a/packages/gui/src/main/commands/install.js +++ /dev/null @@ -1,128 +0,0 @@ -import fs from "node:fs" - -import readManifest from "../utils/readManifest" -import initManifest from "../utils/initManifest" -import sendToRender from "../utils/sendToRender" - -import defaultManifest from "../defaults/pkg_manifest" -import processGenericSteps from "../generic_steps" - -import applyChanges from "./apply" - -import { - updateInstalledPackage, -} from "../local_db" - -export default async function install(manifest) { - manifest = await readManifest(manifest).catch((error) => { - sendToRender("runtime:error", "Cannot fetch this manifest") - - return false - }) - - if (!manifest) { - return false - } - - let pkg = { - ...defaultManifest, - ...manifest, - status: "installing", - } - - const pkg_id = pkg.id - - sendToRender("pkg:new", pkg) - - console.log(`[${pkg_id}] install() | Starting to install...`) - - try { - pkg = await initManifest(pkg) - - if (fs.existsSync(pkg.install_path)) { - await fs.rmSync(pkg.install_path, { recursive: true }) - } - - console.log(`[${pkg_id}] install() | creating install path [${pkg.install_path}]...`) - - await fs.mkdirSync(pkg.install_path, { recursive: true }) - - // append to db - await updateInstalledPackage(pkg) - - if (typeof pkg.before_install === "function") { - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "installing", - statusText: `Performing before_install hook...`, - }) - - console.log(`[${pkg_id}] install() | Performing before_install hook...`) - - // execute before_install - await pkg.before_install(pkg) - } - - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "installing", - statusText: `Performing install steps...`, - }) - - // Execute generic install steps - await processGenericSteps(pkg, pkg.install_steps) - - if (typeof pkg.after_install === "function") { - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "installing", - statusText: `Performing after_install hook...`, - }) - - console.log(`[${pkg_id}] install() | Performing after_install hook...`) - - // execute after_install - await pkg.after_install(pkg) - } - - pkg.status = "installed" - pkg.installed_at = new Date() - - // update to db - await updateInstalledPackage(pkg) - - if (pkg.patches) { - // process default patches - const defaultPatches = pkg.patches.filter((patch) => patch.default) - - await applyChanges(pkg.id, { - patches: Object.fromEntries(defaultPatches.map((patch) => [patch.id, true])), - }) - } - - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "installed", - }) - - sendToRender(`new:message`, { - message: `Successfully installed ${pkg.name}!`, - }) - - if (Array.isArray(pkg.install_ask_configs)) { - sendToRender("pkg:install:ask", pkg) - } - - console.log(`[${pkg_id}] install() | Successfully installed ${pkg.name}!`) - } catch (error) { - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "error", - statusText: error.toString(), - }) - - console.error(error) - - fs.rmdirSync(pkg.install_path, { recursive: true }) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/commands/uninstall.js b/packages/gui/src/main/commands/uninstall.js deleted file mode 100644 index 813f952..0000000 --- a/packages/gui/src/main/commands/uninstall.js +++ /dev/null @@ -1,53 +0,0 @@ -import { - getInstalledPackages, - deleteInstalledPackage, -} from "../local_db" - -import sendToRender from "../utils/sendToRender" -import readManifest from "../utils/readManifest" -import initManifest from "../utils/initManifest" - -import { rimraf } from "rimraf" - -export default async function uninstall(pkg_id) { - let pkg = await getInstalledPackages(pkg_id) - - if (!pkg) { - sendToRender("runtime:error", "Package not found") - return false - } - - sendToRender("pkg:update:status", { - id: pkg_id, - status: "uninstalling", - statusText: `Uninstalling...`, - }) - - console.log(`[${pkg_id}] uninstall() | Uninstalling...`) - - if (pkg.remote_url) { - pkg = await readManifest(pkg.remote_url, { just_read: true }) - - if (typeof pkg.uninstall === "function") { - console.log(`Performing uninstall hook...`) - - await pkg.uninstall(pkg) - } - } - - pkg = await initManifest(pkg) - - await deleteInstalledPackage(pkg_id) - - await rimraf(pkg.install_path) - - sendToRender("pkg:update:status", { - id: pkg_id, - status: "uninstalling", - statusText: null, - }) - - sendToRender("pkg:remove", { - id: pkg_id - }) -} \ No newline at end of file diff --git a/packages/gui/src/main/commands/update.js b/packages/gui/src/main/commands/update.js deleted file mode 100644 index befc348..0000000 --- a/packages/gui/src/main/commands/update.js +++ /dev/null @@ -1,137 +0,0 @@ -import fs from "node:fs" - -import { - updateInstalledPackage, - getInstalledPackages, -} from "../local_db" - -import readManifest from "../utils/readManifest" -import initManifest from "../utils/initManifest" -import sendToRender from "../utils/sendToRender" -import parseStringVars from "../utils/parseStringVars" - -import processGenericSteps from "../generic_steps" - -export default async function update(pkg_id) { - // find package manifest - let pkg = await getInstalledPackages(pkg_id) - - if (!pkg) { - sendToRender("runtime:error", "Package not found") - return false - } - - try { - // output to logs - console.log(`[${pkg_id}] update() | Updating to latest version...`) - - // update render - sendToRender("pkg:update:status", { - id: pkg_id, - status: "loading", - statusText: `Updating to latest version...`, - }) - - // fulfill if remote available - if (pkg.remote_url) { - pkg = { - ...pkg, - ...await readManifest(pkg.remote_url, { just_read: true }), - } - } - - // initialize package manifest - pkg = await initManifest(pkg) - - // check if package manifest has a update function - if (typeof pkg.update === "function") { - // update render - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "loading", - statusText: `Performing update hook...`, - }) - - // output to logs - console.log(`[${pkg_id}] update() | Performing update hook`) - - // execute update function - await pkg.update(pkg) - } - - // Process generic steps - await processGenericSteps(pkg, pkg.update_steps) - - // reapply patches - if (Array.isArray(pkg.applied_patches)) { - for await (const patchKey of pkg.applied_patches) { - const patch = pkg.patches.find((patch) => patch.id === patchKey) - - if (!patch || !Array.isArray(patch.additions)) { - continue - } - - console.log(`Processing patch [${patch.id}]`, patch) - - for await (let addition of patch.additions) { - console.log(`Processing addition [${addition.file}]`, addition) - - // resolve patch file - addition.file = await parseStringVars(addition.file, pkg) - - if (fs.existsSync(addition.file)) { - continue - } - - await processGenericSteps(pkg, addition.steps) - } - } - } - - // check if package manifest has an after_update function - if (typeof pkg.after_update === "function") { - // update render - sendToRender(`pkg:update:status`, { - id: pkg_id, - status: "loading", - statusText: `Performing after_update hook...`, - }) - - // output to logs - console.log(`[${pkg_id}] update() | Performing after_update hook`) - - // execute after_update function - await pkg.after_update(pkg) - } - - // update package vars - pkg.status = "installed" - pkg.last_update = new Date() - - // update package manifest on db - await updateInstalledPackage(pkg) - - // update render - sendToRender(`pkg:update:status`, { - ...pkg, - status: "installed", - }) - - sendToRender(`new:notification`, { - message: `(${pkg.name}) successfully updated!`, - }) - - // output to logs - console.log(`[${pkg_id}] update() | Successfully updated!`) - } catch (error) { - // update render - sendToRender(`pkg:update:status`, { - ...pkg, - status: "error", - statusText: error.toString(), - }) - - // output to logs - console.error(error) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/defaults/local_db.js b/packages/gui/src/main/defaults/local_db.js deleted file mode 100644 index e029a57..0000000 --- a/packages/gui/src/main/defaults/local_db.js +++ /dev/null @@ -1,6 +0,0 @@ -import pkg from "../../../package.json" - -export default { - created_at_version: pkg.version, - packages: [], -} \ No newline at end of file diff --git a/packages/gui/src/main/defaults/pkg_manifest.js b/packages/gui/src/main/defaults/pkg_manifest.js deleted file mode 100644 index 40d1760..0000000 --- a/packages/gui/src/main/defaults/pkg_manifest.js +++ /dev/null @@ -1,19 +0,0 @@ -export default { - id: null, - name: null, - description: null, - icon: null, - version: null, - install_path: null, - remote_url: null, - last_update: null, - - status: "pending", - statusText: "Pending...", - - patches: [], - applied_patches: [], - - configs: {}, - storaged_configs: {} -} \ No newline at end of file diff --git a/packages/gui/src/main/generic_steps/drive.js b/packages/gui/src/main/generic_steps/drive.js deleted file mode 100644 index 9c355cf..0000000 --- a/packages/gui/src/main/generic_steps/drive.js +++ /dev/null @@ -1,79 +0,0 @@ -import path from "node:path" -import fs from "node:fs" - -import humanFormat from "human-format" - -import sendToRender from "../utils/sendToRender" -import extractFile from "../utils/extractFile" - -import GoogleDriveAPI from "../lib/google_drive" - -function convertSize(size) { - return `${humanFormat(size, { - decimals: 2, - })}B` -} - -export default async (manifest, step) => { - let _path = path.resolve(manifest.install_path, step.path ?? ".") - - console.log(`[${manifest.id}] steps.drive() | Downloading ${step.id} to ${_path}...`) - - sendToRender(`pkg:update:status:${manifest.id}`, { - status: "loading", - statusText: `Downloading file id ${step.id}`, - }) - - if (step.tmp) { - _path = path.resolve(TMP_PATH, String(new Date().getTime())) - } - - fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) - - sendToRender(`pkg:update:status:${manifest.id}`, { - statusText: `Starting download...`, - }) - - // Download file from drive - await new Promise((resolve, reject) => { - GoogleDriveAPI.operations.downloadFile( - step.id, - _path, - (err) => { - if (err) { - return reject(err) - } - - return resolve() - }, - (progress) => { - sendToRender(`pkg:update:status:${manifest.id}`, { - progress: progress, - statusText: `Downloaded ${convertSize(progress.transferred ?? 0)} / ${convertSize(progress.length)} | ${convertSize(progress.speed)}/s`, - }) - } - ) - }) - - if (step.extract) { - if (typeof step.extract === "string") { - step.extract = path.resolve(manifest.install_path, step.extract) - } else { - step.extract = path.resolve(manifest.install_path, ".") - } - - sendToRender(`pkg:update:status:${manifest.id}`, { - statusText: `Extracting bundle...`, - }) - - await extractFile(_path, step.extract) - - if (step.delete_after_extract) { - sendToRender(`pkg:update:status:${manifest.id}`, { - statusText: `Deleting temporal files...`, - }) - - await fs.promises.rm(_path, { recursive: true }) - } - } -} \ No newline at end of file diff --git a/packages/gui/src/main/generic_steps/git_clone.js b/packages/gui/src/main/generic_steps/git_clone.js deleted file mode 100644 index e93d3f7..0000000 --- a/packages/gui/src/main/generic_steps/git_clone.js +++ /dev/null @@ -1,44 +0,0 @@ -import path from "node:path" -import fs from "node:fs" -import upath from "upath" -import { execa } from "../lib/execa" - -import sendToRender from "../utils/sendToRender" -import Vars from "../vars" - -export default async (manifest, step) => { - const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" - const final_path = upath.normalizeSafe(path.resolve(manifest.install_path, step.path)) - - if (!fs.existsSync(final_path)) { - fs.mkdirSync(final_path, { recursive: true }) - } - - sendToRender(`pkg:update:status`, { - id: manifest.id, - statusText: `Cloning ${step.url}`, - }) - - console.log(`USING GIT BIN >`, gitCMD) - - console.log(`[${manifest.id}] steps.git_clone() | Cloning ${step.url}...`) - - const args = [ - "clone", - //`--depth ${step.depth ?? 1}`, - //"--filter=blob:none", - //"--filter=tree:0", - "--recurse-submodules", - "--remote-submodules", - step.url, - final_path, - ] - - await execa(gitCMD, args, { - cwd: final_path, - stdout: "inherit", - stderr: "inherit", - }) - - return manifest -} \ No newline at end of file diff --git a/packages/gui/src/main/generic_steps/git_pull.js b/packages/gui/src/main/generic_steps/git_pull.js deleted file mode 100644 index 9a1475e..0000000 --- a/packages/gui/src/main/generic_steps/git_pull.js +++ /dev/null @@ -1,29 +0,0 @@ -import path from "node:path" -import fs from "node:fs" -import { execa } from "../lib/execa" - -import sendToRender from "../utils/sendToRender" - -import Vars from "../vars" - -export default async (manifest, step) => { - const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" - const _path = path.resolve(manifest.install_path, step.path) - - sendToRender(`pkg:update:status`, { - id: manifest.id, - statusText: `Pulling...`, - }) - - console.log(`[${manifest.id}] steps.git_pull() | Pulling...`) - - fs.mkdirSync(_path, { recursive: true }) - - await execa(gitCMD, ["pull", "--rebase"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - return manifest -} \ No newline at end of file diff --git a/packages/gui/src/main/generic_steps/git_reset.js b/packages/gui/src/main/generic_steps/git_reset.js deleted file mode 100644 index 33edb66..0000000 --- a/packages/gui/src/main/generic_steps/git_reset.js +++ /dev/null @@ -1,77 +0,0 @@ -import path from "node:path" -import fs from "node:fs" -import { execa } from "../lib/execa" - -import sendToRender from "../utils/sendToRender" - -import git_pull from "./git_pull" -import Vars from "../vars" - -export default async (manifest, step) => { - const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" - - const _path = path.resolve(manifest.install_path, step.path) - const from = step.from ?? "HEAD" - - if (!fs.existsSync(_path)) { - fs.mkdirSync(_path, { recursive: true }) - } - - sendToRender(`pkg:update:status`, { - id: manifest.id, - statusText: `Fetching from origin...`, - }) - - console.log(`[${manifest.id}] steps.git_reset() | Fetching from origin`) - - // fetch from origin - await execa(gitCMD, ["fetch", "origin"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - sendToRender(`pkg:update:status`, { - id: manifest.id, - statusText: `Cleaning untracked files...`, - }) - - console.log(`[${manifest.id}] steps.git_reset() | Cleaning`) - - await execa(gitCMD, ["clean", "-df"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - sendToRender(`pkg:update:status`, { - id: manifest.id, - statusText: `Reset from ${from}`, - }) - - console.log(`[${manifest.id}] steps.git_reset() | Resetting to ${from}`) - - await execa(gitCMD, ["reset", "--hard", from], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - // pull the latest - await git_pull(manifest, step) - - sendToRender(`pkg:update:status`, { - id: manifest.id, - statusText: `Checkout to HEAD`, - }) - - console.log(`[${manifest.id}] steps.git_reset() | Checkout to head`) - - await execa(gitCMD, ["checkout", "HEAD"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - return manifest -} \ No newline at end of file diff --git a/packages/gui/src/main/generic_steps/http.js b/packages/gui/src/main/generic_steps/http.js deleted file mode 100644 index 471dc45..0000000 --- a/packages/gui/src/main/generic_steps/http.js +++ /dev/null @@ -1,108 +0,0 @@ -import path from "node:path" -import fs from "node:fs" -import os from "node:os" - -import { pipeline as streamPipeline } from "node:stream/promises" - -import humanFormat from "human-format" - -import got from "got" - -import parseStringVars from "../utils/parseStringVars" -import sendToRender from "../utils/sendToRender" -import extractFile from "../utils/extractFile" - -function convertSize(size) { - return `${humanFormat(size, { - decimals: 2, - })}B` -} - -export default async (manifest, step) => { - step.path = await parseStringVars(step.path, manifest) - - let _path = path.resolve(manifest.install_path, step.path ?? ".") - - sendToRender(`pkg:update:status:${manifest.id}`, { - status: "loading", - statusText: `Downloading ${step.url}`, - }) - - console.log(`[${manifest.id}] steps.http() | Downloading ${step.url} to ${_path}`) - - if (step.tmp) { - _path = path.resolve(os.tmpdir(), String(new Date().getTime()), path.basename(step.url)) - } - - fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) - - if (step.simple) { - await streamPipeline( - got.stream(step.url), - fs.createWriteStream(_path) - ) - } else { - const remoteStream = got.stream(step.url) - const localStream = fs.createWriteStream(_path) - - let progress = { - transferred: 0, - total: 0, - speed: 0, - } - - let lastTransferred = 0 - - sendToRender(`pkg:update:status:${manifest.id}`, { - statusText: `Starting download...`, - }) - - remoteStream.pipe(localStream) - - remoteStream.on("downloadProgress", (_progress) => { - progress = _progress - }) - - const progressInterval = setInterval(() => { - progress.speed = ((progress.transferred ?? 0) - lastTransferred) / 1 - - lastTransferred = progress.transferred ?? 0 - - sendToRender(`pkg:update:status:${manifest.id}`, { - progress: progress, - statusText: `Downloaded ${convertSize(progress.transferred ?? 0)} / ${convertSize(progress.total)} | ${convertSize(progress.speed)}/s`, - }) - }, 1000) - - await new Promise((resolve, reject) => { - localStream.on("finish", resolve) - localStream.on("error", reject) - }) - - clearInterval(progressInterval) - } - - if (step.extract) { - if (typeof step.extract === "string") { - step.extract = path.resolve(manifest.install_path, step.extract) - } else { - step.extract = path.resolve(manifest.install_path, ".") - } - - sendToRender(`pkg:update:status:${manifest.id}`, { - statusText: `Extracting bundle...`, - }) - - await extractFile(_path, step.extract) - - if (step.delete_after_extract) { - console.log(`[${manifest.id}] steps.http() | Deleting temporal file [${_path}]...`) - - sendToRender(`pkg:update:status:${manifest.id}`, { - statusText: `Deleting temporal files...`, - }) - - await fs.promises.rm(_path, { recursive: true }) - } - } -} \ No newline at end of file diff --git a/packages/gui/src/main/generic_steps/index.js b/packages/gui/src/main/generic_steps/index.js deleted file mode 100644 index cde65cf..0000000 --- a/packages/gui/src/main/generic_steps/index.js +++ /dev/null @@ -1,79 +0,0 @@ -import ISM_DRIVE_DL from "./drive" -import ISM_HTTP from "./http" -import ISM_GIT_CLONE from "./git_clone" -import ISM_GIT_PULL from "./git_pull" -import ISM_GIT_RESET from "./git_reset" - -const InstallationStepsMethods = { - drive_dl: ISM_DRIVE_DL, - http: ISM_HTTP, - git_clone: ISM_GIT_CLONE, - git_pull: ISM_GIT_PULL, - git_reset: ISM_GIT_RESET, -} - -const StepsOrders = [ - "git_clones", - "git_clones_steps", - "git_pulls", - "git_update", - "git_pulls_steps", - "git_reset", - "drive_downloads", - "http_downloads", -] - -export default async function processGenericSteps(pkg, steps) { - console.log(`[${pkg.id}] steps() | Processing steps...`, steps) - - let stepsEntries = Object.entries(steps) - - stepsEntries = stepsEntries.sort((a, b) => StepsOrders.indexOf(a[0]) - StepsOrders.indexOf(b[0])) - - if (stepsEntries.length === 0) { - return pkg - } - - for await (const [stepKey, stepValue] of stepsEntries) { - switch (stepKey) { - case "drive_downloads": { - for await (const dl_step of stepValue) { - await InstallationStepsMethods.drive_dl(pkg, dl_step) - } - break; - } - case "http_downloads": { - for await (const dl_step of stepValue) { - await InstallationStepsMethods.http(pkg, dl_step) - } - break; - } - case "git_clones": - case "git_clones_steps": { - for await (const clone_step of stepValue) { - await InstallationStepsMethods.git_clone(pkg, clone_step) - } - break; - } - case "git_pulls": - case "git_update": - case "git_pulls_steps": { - for await (const pull_step of stepValue) { - await InstallationStepsMethods.git_pull(pkg, pull_step) - } - break; - } - case "git_reset": { - for await (const reset_step of stepValue) { - await InstallationStepsMethods.git_reset(pkg, reset_step) - } - break; - } - default: { - throw new Error(`Unknown step: ${stepKey}`) - } - } - } - - return pkg -} diff --git a/packages/gui/src/main/index.js b/packages/gui/src/main/index.js index 7732d37..a54b0b0 100644 --- a/packages/gui/src/main/index.js +++ b/packages/gui/src/main/index.js @@ -1,11 +1,13 @@ -import RelicCore from "../../../core/src/index" - -import sendToRender from "./utils/sendToRender" global.SettingsStore = new Store({ name: "settings", watch: true, }) +import RelicCore from "@ragestudio/relic-core/src" +import CoreAdapter from "./classes/CoreAdapter" + +import sendToRender from "./utils/sendToRender" + import path from "node:path" import { app, shell, BrowserWindow, ipcMain } from "electron" @@ -15,71 +17,57 @@ import Store from "electron-store" import pkg from "../../package.json" -import PkgManager from "./manager" -import { readManifest } from "./utils/readManifest" -import AuthService from "./auth" - const { autoUpdater } = require("electron-differential-updater") const ProtocolRegistry = require("protocol-registry") -const protocolRegistryNamespace = "rsbundle" +const protocolRegistryNamespace = "relic" class ElectronApp { constructor() { - this.pkgManager = new PkgManager() this.win = null + this.core = new RelicCore() + this.adapter = new CoreAdapter(this, this.core) } - core = new RelicCore() - - authService = global.authService = new AuthService() - handlers = { "pkg:list": async () => { - return await this.pkgManager.getInstalledPackages() - }, - "pkg:get": async (event, manifest_id) => { - return await this.pkgManager.getInstalledPackages(manifest_id) + return await this.core.package.list() }, - "pkg:read": async (event, manifest_url) => { - return JSON.stringify(await readManifest(manifest_url)) + "pkg:get": async (event, pkg_id) => { + return await this.core.db.getPackages(pkg_id) }, - "pkg:install": async (event, manifest) => { - this.pkgManager.install(manifest) - }, - "pkg:update": async (event, manifest_id, { execOnFinish = false } = {}) => { - await this.pkgManager.update(manifest_id) + "pkg:read": async (event, manifest_path, options = {}) => { + const manifest = await this.core.package.read(manifest_path, options) - if (execOnFinish) { - await this.pkgManager.execute(manifest_id) - } + return JSON.stringify({ + ...this.core.db.defaultPackageState({ ...manifest }), + ...manifest, + name: manifest.pkg_name, + }) }, - "pkg:apply": async (event, manifest_id, changes) => { - return await this.pkgManager.applyChanges(manifest_id, changes) + "pkg:install": async (event, manifest_path) => { + return await this.core.package.install(manifest_path) }, - "pkg:retry_install": async (event, manifest_id) => { - const pkg = await this.pkgManager.getInstalledPackages(manifest_id) + "pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => { + await this.core.package.update(pkg_id) - if (!pkg) { - return false + if (execOnFinish) { + await this.core.package.execute(pkg_id) } - await this.pkgManager.install(pkg) - }, - "pkg:cancel_install": async (event, manifest_id) => { - return await this.pkgManager.uninstall(manifest_id) + return true }, - "pkg:delete_auth": async (event, manifest_id) => { - return this.authService.unauthorize(manifest_id) + "pkg:apply": async (event, pkg_id, changes) => { + return await this.core.package.apply(pkg_id, changes) }, - "pkg:uninstall": async (event, ...args) => { - return await this.pkgManager.uninstall(...args) + "pkg:uninstall": async (event, pkg_id) => { + return await this.core.package.uninstall(pkg_id) }, - "pkg:execute": async (event, ...args) => { - return await this.pkgManager.execute(...args) + "pkg:execute": async (event, pkg_id) => { + return await this.core.package.execute(pkg_id) }, - "pkg:open": async (event, manifest_id) => { - return await this.pkgManager.open(manifest_id) + "pkg:open": async (event, pkg_id) => { + return await this.core.openPath(pkg_id) }, "updater:check": () => { autoUpdater.checkForUpdates() @@ -103,6 +91,7 @@ class ElectronApp { }, "app:init": async (event, data) => { try { + await this.core.initialize() await this.core.setup() } catch (err) { console.error(err) @@ -122,7 +111,7 @@ class ElectronApp { events = { "open-runtime-path": () => { - return this.pkgManager.openRuntimePath() + return this.core.openPath() }, "open-dev-logs": () => { return sendToRender("new:message", { @@ -165,8 +154,6 @@ class ElectronApp { handleURLProtocol(url) { const urlStarter = `${protocolRegistryNamespace}://` - console.log(url) - if (url.startsWith(urlStarter)) { const urlValue = url.split(urlStarter)[1] @@ -177,16 +164,14 @@ class ElectronApp { explicitAction[0] = explicitAction[0].slice(0, -1) } - console.log(explicitAction) - if (explicitAction.length > 0) { switch (explicitAction[0]) { case "authorize": { if (!explicitAction[2]) { - const [pkgid, token] = explicitAction[1].split("%23") - return this.authService.authorize(pkgid, token) + const [pkg_id, token] = explicitAction[1].split("%23") + return this.core.auth.authorize(pkg_id, token) } else { - return this.authService.authorize(explicitAction[1], explicitAction[2]) + return this.core.auth.authorize(explicitAction[1], explicitAction[2]) } } default: { diff --git a/packages/gui/src/main/lib/auth/index.js b/packages/gui/src/main/lib/auth/index.js deleted file mode 100644 index 5601955..0000000 --- a/packages/gui/src/main/lib/auth/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import open from "open" -import axios from "axios" -import sendToRender from "../../utils/sendToRender" - -export default class Auth { - constructor(manifest) { - this.manifest = manifest - - console.log(this.manifest) - } - - async get() { - const authData = global.authService.getAuth(this.manifest.id) - - console.log(authData) - - if (authData && this.manifest.auth && this.manifest.auth.getter) { - const result = await axios({ - method: "POST", - url: this.manifest.auth.getter, - headers: { - "Content-Type": "application/json", - }, - data: { - auth_data: authData, - } - }).catch((err) => { - sendToRender(`new:notification`, { - type: "error", - message: "Failed to authorize", - description: err.response.data.message ?? err.response.data.error ?? err.message, - duration: 10 - }) - - return err - }) - - if (result instanceof Error) { - throw result - } - - console.log(result.data) - - return result.data - } - - return authData - } - - request() { - if (!this.manifest.auth) { - return false - } - - const authURL = this.manifest.auth.fetcher - - open(authURL) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/execa/index.d.ts b/packages/gui/src/main/lib/execa/index.d.ts deleted file mode 100755 index 7cef754..0000000 --- a/packages/gui/src/main/lib/execa/index.d.ts +++ /dev/null @@ -1,955 +0,0 @@ -import {type Buffer} from 'node:buffer'; -import {type ChildProcess} from 'node:child_process'; -import {type Stream, type Readable as ReadableStream, type Writable as WritableStream} from 'node:stream'; - -export type StdioOption = - | 'pipe' - | 'overlapped' - | 'ipc' - | 'ignore' - | 'inherit' - | Stream - | number - | undefined; - -type EncodingOption = - | 'utf8' - // eslint-disable-next-line unicorn/text-encoding-identifier-case - | 'utf-8' - | 'utf16le' - | 'utf-16le' - | 'ucs2' - | 'ucs-2' - | 'latin1' - | 'binary' - | 'ascii' - | 'hex' - | 'base64' - | 'base64url' - | 'buffer' - | null - | undefined; -type DefaultEncodingOption = 'utf8'; -type BufferEncodingOption = 'buffer' | null; - -export type CommonOptions = { - /** - Kill the spawned process when the parent process exits unless either: - - the spawned process is [`detached`](https://nodejs.org/api/child_process.html#child_process_options_detached) - - the parent process is terminated abruptly, for example, with `SIGKILL` as opposed to `SIGTERM` or a normal exit - - @default true - */ - readonly cleanup?: boolean; - - /** - Prefer locally installed binaries when looking for a binary to execute. - - If you `$ npm install foo`, you can then `execa('foo')`. - - @default `true` with `$`, `false` otherwise - */ - readonly preferLocal?: boolean; - - /** - Preferred path to find locally installed binaries in (use with `preferLocal`). - - @default process.cwd() - */ - readonly localDir?: string | URL; - - /** - Path to the Node.js executable to use in child processes. - - This can be either an absolute path or a path relative to the `cwd` option. - - Requires `preferLocal` to be `true`. - - For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process. - - @default process.execPath - */ - readonly execPath?: string; - - /** - Buffer the output from the spawned process. When set to `false`, you must read the output of `stdout` and `stderr` (or `all` if the `all` option is `true`). Otherwise the returned promise will not be resolved/rejected. - - If the spawned process fails, `error.stdout`, `error.stderr`, and `error.all` will contain the buffered data. - - @default true - */ - readonly buffer?: boolean; - - /** - Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). - - @default `inherit` with `$`, `pipe` otherwise - */ - readonly stdin?: StdioOption; - - /** - Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). - - @default 'pipe' - */ - readonly stdout?: StdioOption; - - /** - Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). - - @default 'pipe' - */ - readonly stderr?: StdioOption; - - /** - Setting this to `false` resolves the promise with the error instead of rejecting it. - - @default true - */ - readonly reject?: boolean; - - /** - Add an `.all` property on the promise and the resolved value. The property contains the output of the process with `stdout` and `stderr` interleaved. - - @default false - */ - readonly all?: boolean; - - /** - Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from the output. - - @default true - */ - readonly stripFinalNewline?: boolean; - - /** - Set to `false` if you don't want to extend the environment variables when providing the `env` property. - - @default true - */ - readonly extendEnv?: boolean; - - /** - Current working directory of the child process. - - @default process.cwd() - */ - readonly cwd?: string | URL; - - /** - Environment key-value pairs. Extends automatically from `process.env`. Set `extendEnv` to `false` if you don't want this. - - @default process.env - */ - readonly env?: NodeJS.ProcessEnv; - - /** - Explicitly set the value of `argv[0]` sent to the child process. This will be set to `command` or `file` if not specified. - */ - readonly argv0?: string; - - /** - Child's [stdio](https://nodejs.org/api/child_process.html#child_process_options_stdio) configuration. - - @default 'pipe' - */ - readonly stdio?: 'pipe' | 'overlapped' | 'ignore' | 'inherit' | readonly StdioOption[]; - - /** - Specify the kind of serialization used for sending messages between processes when using the `stdio: 'ipc'` option or `execaNode()`: - - `json`: Uses `JSON.stringify()` and `JSON.parse()`. - - `advanced`: Uses [`v8.serialize()`](https://nodejs.org/api/v8.html#v8_v8_serialize_value) - - [More info.](https://nodejs.org/api/child_process.html#child_process_advanced_serialization) - - @default 'json' - */ - readonly serialization?: 'json' | 'advanced'; - - /** - Prepare child to run independently of its parent process. Specific behavior [depends on the platform](https://nodejs.org/api/child_process.html#child_process_options_detached). - - @default false - */ - readonly detached?: boolean; - - /** - Sets the user identity of the process. - */ - readonly uid?: number; - - /** - Sets the group identity of the process. - */ - readonly gid?: number; - - /** - If `true`, runs `command` inside of a shell. Uses `/bin/sh` on UNIX and `cmd.exe` on Windows. A different shell can be specified as a string. The shell should understand the `-c` switch on UNIX or `/d /s /c` on Windows. - - We recommend against using this option since it is: - - not cross-platform, encouraging shell-specific syntax. - - slower, because of the additional shell interpretation. - - unsafe, potentially allowing command injection. - - @default false - */ - readonly shell?: boolean | string; - - /** - Specify the character encoding used to decode the `stdout` and `stderr` output. If set to `'buffer'` or `null`, then `stdout` and `stderr` will be a `Buffer` instead of a string. - - @default 'utf8' - */ - readonly encoding?: EncodingType; - - /** - If `timeout` is greater than `0`, the parent will send the signal identified by the `killSignal` property (the default is `SIGTERM`) if the child runs longer than `timeout` milliseconds. - - @default 0 - */ - readonly timeout?: number; - - /** - Largest amount of data in bytes allowed on `stdout` or `stderr`. Default: 100 MB. - - @default 100_000_000 - */ - readonly maxBuffer?: number; - - /** - Signal value to be used when the spawned process will be killed. - - @default 'SIGTERM' - */ - readonly killSignal?: string | number; - - /** - You can abort the spawned process using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). - - When `AbortController.abort()` is called, [`.isCanceled`](https://github.com/sindresorhus/execa#iscanceled) becomes `true`. - - @example - ``` - import {execa} from 'execa'; - - const abortController = new AbortController(); - const subprocess = execa('node', [], {signal: abortController.signal}); - - setTimeout(() => { - abortController.abort(); - }, 1000); - - try { - await subprocess; - } catch (error) { - console.log(subprocess.killed); // true - console.log(error.isCanceled); // true - } - ``` - */ - readonly signal?: AbortSignal; - - /** - If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`. - - @default false - */ - readonly windowsVerbatimArguments?: boolean; - - /** - On Windows, do not create a new console window. Please note this also prevents `CTRL-C` [from working](https://github.com/nodejs/node/issues/29837) on Windows. - - @default true - */ - readonly windowsHide?: boolean; - - /** - Print each command on `stderr` before executing it. - - This can also be enabled by setting the `NODE_DEBUG=execa` environment variable in the current process. - - @default false - */ - readonly verbose?: boolean; -}; - -export type Options = { - /** - Write some input to the `stdin` of your binary. - - If the input is a file, use the `inputFile` option instead. - */ - readonly input?: string | Buffer | ReadableStream; - - /** - Use a file as input to the the `stdin` of your binary. - - If the input is not a file, use the `input` option instead. - */ - readonly inputFile?: string; -} & CommonOptions; - -export type SyncOptions = { - /** - Write some input to the `stdin` of your binary. - - If the input is a file, use the `inputFile` option instead. - */ - readonly input?: string | Buffer; - - /** - Use a file as input to the the `stdin` of your binary. - - If the input is not a file, use the `input` option instead. - */ - readonly inputFile?: string; -} & CommonOptions; - -export type NodeOptions = { - /** - The Node.js executable to use. - - @default process.execPath - */ - readonly nodePath?: string; - - /** - List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable. - - @default process.execArgv - */ - readonly nodeOptions?: string[]; -} & Options; - -type StdoutStderrAll = string | Buffer | undefined; - -export type ExecaReturnBase = { - /** - The file and arguments that were run, for logging purposes. - - This is not escaped and should not be executed directly as a process, including using `execa()` or `execaCommand()`. - */ - command: string; - - /** - Same as `command` but escaped. - - This is meant to be copy and pasted into a shell, for debugging purposes. - Since the escaping is fairly basic, this should not be executed directly as a process, including using `execa()` or `execaCommand()`. - */ - escapedCommand: string; - - /** - The numeric exit code of the process that was run. - */ - exitCode: number; - - /** - The output of the process on stdout. - */ - stdout: StdoutStderrType; - - /** - The output of the process on stderr. - */ - stderr: StdoutStderrType; - - /** - Whether the process failed to run. - */ - failed: boolean; - - /** - Whether the process timed out. - */ - timedOut: boolean; - - /** - Whether the process was killed. - */ - killed: boolean; - - /** - The name of the signal that was used to terminate the process. For example, `SIGFPE`. - - If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. - */ - signal?: string; - - /** - A human-friendly description of the signal that was used to terminate the process. For example, `Floating point arithmetic error`. - - If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen. - */ - signalDescription?: string; - - /** - The `cwd` of the command if provided in the command options. Otherwise it is `process.cwd()`. - */ - cwd: string; -}; - -export type ExecaSyncReturnValue = { -} & ExecaReturnBase; - -/** -Result of a child process execution. On success this is a plain object. On failure this is also an `Error` instance. - -The child process fails when: -- its exit code is not `0` -- it was killed with a signal -- timing out -- being canceled -- there's not enough memory or there are already too many child processes -*/ -export type ExecaReturnValue = { - /** - The output of the process with `stdout` and `stderr` interleaved. - - This is `undefined` if either: - - the `all` option is `false` (default value) - - `execaSync()` was used - */ - all?: StdoutStderrType; - - /** - Whether the process was canceled. - - You can cancel the spawned process using the [`signal`](https://github.com/sindresorhus/execa#signal-1) option. - */ - isCanceled: boolean; -} & ExecaSyncReturnValue; - -export type ExecaSyncError = { - /** - Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored. - - The child process stderr then stdout are appended to the end, separated with newlines and not interleaved. - */ - message: string; - - /** - This is the same as the `message` property except it does not include the child process stdout/stderr. - */ - shortMessage: string; - - /** - Original error message. This is the same as the `message` property except it includes neither the child process stdout/stderr nor some additional information added by Execa. - - This is `undefined` unless the child process exited due to an `error` event or a timeout. - */ - originalMessage?: string; -} & Error & ExecaReturnBase; - -export type ExecaError = { - /** - The output of the process with `stdout` and `stderr` interleaved. - - This is `undefined` if either: - - the `all` option is `false` (default value) - - `execaSync()` was used - */ - all?: StdoutStderrType; - - /** - Whether the process was canceled. - */ - isCanceled: boolean; -} & ExecaSyncError; - -export type KillOptions = { - /** - Milliseconds to wait for the child process to terminate before sending `SIGKILL`. - - Can be disabled with `false`. - - @default 5000 - */ - forceKillAfterTimeout?: number | false; -}; - -export type ExecaChildPromise = { - /** - Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr). - - This is `undefined` if either: - - the `all` option is `false` (the default value) - - both `stdout` and `stderr` options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio) - */ - all?: ReadableStream; - - catch( - onRejected?: (reason: ExecaError) => ResultType | PromiseLike - ): Promise | ResultType>; - - /** - Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal), except if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. Note that this graceful termination does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events) (`SIGKILL` and `SIGTERM` has the same effect of force-killing the process immediately.) If you want to achieve graceful termination on Windows, you have to use other means, such as [`taskkill`](https://github.com/sindresorhus/taskkill). - */ - kill(signal?: string, options?: KillOptions): void; - - /** - Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This used to be preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. But now this is deprecated and you should either use `.kill()` or the `signal` option when creating the child process. - */ - cancel(): void; - - /** - [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be: - - Another `execa()` return value - - A writable stream - - A file path string - - If the `target` is another `execa()` return value, it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the final result. - - The `stdout` option] must be kept as `pipe`, its default value. - */ - pipeStdout?>(target: Target): Target; - pipeStdout?(target: WritableStream | string): ExecaChildProcess; - - /** - Like `pipeStdout()` but piping the child process's `stderr` instead. - - The `stderr` option must be kept as `pipe`, its default value. - */ - pipeStderr?>(target: Target): Target; - pipeStderr?(target: WritableStream | string): ExecaChildProcess; - - /** - Combines both `pipeStdout()` and `pipeStderr()`. - - Either the `stdout` option or the `stderr` option must be kept as `pipe`, their default value. Also, the `all` option must be set to `true`. - */ - pipeAll?>(target: Target): Target; - pipeAll?(target: WritableStream | string): ExecaChildProcess; -}; - -export type ExecaChildProcess = ChildProcess & -ExecaChildPromise & -Promise>; - -/** -Executes a command using `file ...arguments`. `arguments` are specified as an array of strings. Returns a `childProcess`. - -Arguments are automatically escaped. They can contain any character, including spaces. - -This is the preferred method when executing single commands. - -@param file - The program/script to execute. -@param arguments - Arguments to pass to `file` on execution. -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example Promise interface -``` -import {execa} from 'execa'; - -const {stdout} = await execa('echo', ['unicorns']); -console.log(stdout); -//=> 'unicorns' -``` - -@example Redirect output to a file -``` -import {execa} from 'execa'; - -// Similar to `echo unicorns > stdout.txt` in Bash -await execa('echo', ['unicorns']).pipeStdout('stdout.txt'); - -// Similar to `echo unicorns 2> stdout.txt` in Bash -await execa('echo', ['unicorns']).pipeStderr('stderr.txt'); - -// Similar to `echo unicorns &> stdout.txt` in Bash -await execa('echo', ['unicorns'], {all: true}).pipeAll('all.txt'); -``` - -@example Redirect input from a file -``` -import {execa} from 'execa'; - -// Similar to `cat < stdin.txt` in Bash -const {stdout} = await execa('cat', {inputFile: 'stdin.txt'}); -console.log(stdout); -//=> 'unicorns' -``` - -@example Save and pipe output from a child process -``` -import {execa} from 'execa'; - -const {stdout} = await execa('echo', ['unicorns']).pipeStdout(process.stdout); -// Prints `unicorns` -console.log(stdout); -// Also returns 'unicorns' -``` - -@example Pipe multiple processes -``` -import {execa} from 'execa'; - -// Similar to `echo unicorns | cat` in Bash -const {stdout} = await execa('echo', ['unicorns']).pipeStdout(execa('cat')); -console.log(stdout); -//=> 'unicorns' -``` - -@example Handling errors -``` -import {execa} from 'execa'; - -// Catching an error -try { - await execa('unknown', ['command']); -} catch (error) { - console.log(error); - /* - { - message: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', - errno: -2, - code: 'ENOENT', - syscall: 'spawn unknown', - path: 'unknown', - spawnargs: ['command'], - originalMessage: 'spawn unknown ENOENT', - shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', - command: 'unknown command', - escapedCommand: 'unknown command', - stdout: '', - stderr: '', - failed: true, - timedOut: false, - isCanceled: false, - killed: false, - cwd: '/path/to/cwd' - } - \*\/ -} -``` - -@example Graceful termination -``` -const subprocess = execa('node'); - -setTimeout(() => { - subprocess.kill('SIGTERM', { - forceKillAfterTimeout: 2000 - }); -}, 1000); -``` -*/ -export function execa( - file: string, - arguments?: readonly string[], - options?: Options -): ExecaChildProcess; -export function execa( - file: string, - arguments?: readonly string[], - options?: Options -): ExecaChildProcess; -export function execa(file: string, options?: Options): ExecaChildProcess; -export function execa(file: string, options?: Options): ExecaChildProcess; - -/** -Same as `execa()` but synchronous. - -@param file - The program/script to execute. -@param arguments - Arguments to pass to `file` on execution. -@returns A `childProcessResult` object -@throws A `childProcessResult` error - -@example Promise interface -``` -import {execa} from 'execa'; - -const {stdout} = execaSync('echo', ['unicorns']); -console.log(stdout); -//=> 'unicorns' -``` - -@example Redirect input from a file -``` -import {execa} from 'execa'; - -// Similar to `cat < stdin.txt` in Bash -const {stdout} = execaSync('cat', {inputFile: 'stdin.txt'}); -console.log(stdout); -//=> 'unicorns' -``` - -@example Handling errors -``` -import {execa} from 'execa'; - -// Catching an error -try { - execaSync('unknown', ['command']); -} catch (error) { - console.log(error); - /* - { - message: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT', - errno: -2, - code: 'ENOENT', - syscall: 'spawnSync unknown', - path: 'unknown', - spawnargs: ['command'], - originalMessage: 'spawnSync unknown ENOENT', - shortMessage: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT', - command: 'unknown command', - escapedCommand: 'unknown command', - stdout: '', - stderr: '', - failed: true, - timedOut: false, - isCanceled: false, - killed: false, - cwd: '/path/to/cwd' - } - \*\/ -} -``` -*/ -export function execaSync( - file: string, - arguments?: readonly string[], - options?: SyncOptions -): ExecaSyncReturnValue; -export function execaSync( - file: string, - arguments?: readonly string[], - options?: SyncOptions -): ExecaSyncReturnValue; -export function execaSync(file: string, options?: SyncOptions): ExecaSyncReturnValue; -export function execaSync( - file: string, - options?: SyncOptions -): ExecaSyncReturnValue; - -/** -Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a `childProcess`. - -Arguments are automatically escaped. They can contain any character, but spaces must be escaped with a backslash like `execaCommand('echo has\\ space')`. - -This is the preferred method when executing a user-supplied `command` string, such as in a REPL. - -@param command - The program/script to execute and its arguments. -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example -``` -import {execaCommand} from 'execa'; - -const {stdout} = await execaCommand('echo unicorns'); -console.log(stdout); -//=> 'unicorns' -``` -*/ -export function execaCommand(command: string, options?: Options): ExecaChildProcess; -export function execaCommand(command: string, options?: Options): ExecaChildProcess; - -/** -Same as `execaCommand()` but synchronous. - -@param command - The program/script to execute and its arguments. -@returns A `childProcessResult` object -@throws A `childProcessResult` error - -@example -``` -import {execaCommandSync} from 'execa'; - -const {stdout} = execaCommandSync('echo unicorns'); -console.log(stdout); -//=> 'unicorns' -``` -*/ -export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; -export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; - -type TemplateExpression = - | string - | number - | ExecaReturnValue - | ExecaSyncReturnValue - | Array | ExecaSyncReturnValue>; - -type Execa$ = { - /** - Returns a new instance of `$` but with different default `options`. Consecutive calls are merged to previous ones. - - This can be used to either: - - Set options for a specific command: `` $(options)`command` `` - - Share options for multiple commands: `` const $$ = $(options); $$`command`; $$`otherCommand` `` - - @param options - Options to set - @returns A new instance of `$` with those `options` set - - @example - ``` - import {$} from 'execa'; - - const $$ = $({stdio: 'inherit'}); - - await $$`echo unicorns`; - //=> 'unicorns' - - await $$`echo rainbows`; - //=> 'rainbows' - ``` - */ - (options: Options): Execa$; - (options: Options): Execa$; - (options: Options): Execa$; - ( - templates: TemplateStringsArray, - ...expressions: TemplateExpression[] - ): ExecaChildProcess; - - /** - Same as $\`command\` but synchronous. - - @returns A `childProcessResult` object - @throws A `childProcessResult` error - - @example Basic - ``` - import {$} from 'execa'; - - const branch = $.sync`git branch --show-current`; - $.sync`dep deploy --branch=${branch}`; - ``` - - @example Multiple arguments - ``` - import {$} from 'execa'; - - const args = ['unicorns', '&', 'rainbows!']; - const {stdout} = $.sync`echo ${args}`; - console.log(stdout); - //=> 'unicorns & rainbows!' - ``` - - @example With options - ``` - import {$} from 'execa'; - - $.sync({stdio: 'inherit'})`echo unicorns`; - //=> 'unicorns' - ``` - - @example Shared options - ``` - import {$} from 'execa'; - - const $$ = $({stdio: 'inherit'}); - - $$.sync`echo unicorns`; - //=> 'unicorns' - - $$.sync`echo rainbows`; - //=> 'rainbows' - ``` - */ - sync( - templates: TemplateStringsArray, - ...expressions: TemplateExpression[] - ): ExecaSyncReturnValue; -}; - -/** -Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a `childProcess`. - -Arguments are automatically escaped. They can contain any character, but spaces must use `${}` like `` $`echo ${'has space'}` ``. - -This is the preferred method when executing multiple commands in a script file. - -The `command` string can inject any `${value}` with the following types: string, number, `childProcess` or an array of those types. For example: `` $`echo one ${'two'} ${3} ${['four', 'five']}` ``. For `${childProcess}`, the process's `stdout` is used. - -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example Basic -``` -import {$} from 'execa'; - -const branch = await $`git branch --show-current`; -await $`dep deploy --branch=${branch}`; -``` - -@example Multiple arguments -``` -import {$} from 'execa'; - -const args = ['unicorns', '&', 'rainbows!']; -const {stdout} = await $`echo ${args}`; -console.log(stdout); -//=> 'unicorns & rainbows!' -``` - -@example With options -``` -import {$} from 'execa'; - -await $({stdio: 'inherit'})`echo unicorns`; -//=> 'unicorns' -``` - -@example Shared options -``` -import {$} from 'execa'; - -const $$ = $({stdio: 'inherit'}); - -await $$`echo unicorns`; -//=> 'unicorns' - -await $$`echo rainbows`; -//=> 'rainbows' -``` -*/ -export const $: Execa$; - -/** -Execute a Node.js script as a child process. - -Arguments are automatically escaped. They can contain any character, including spaces. - -This is the preferred method when executing Node.js files. - -Like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options): - - the current Node version and options are used. This can be overridden using the `nodePath` and `nodeOptions` options. - - the `shell` option cannot be used - - an extra channel [`ipc`](https://nodejs.org/api/child_process.html#child_process_options_stdio) is passed to `stdio` - -@param scriptPath - Node.js script to execute. -@param arguments - Arguments to pass to `scriptPath` on execution. -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example -``` -import {execa} from 'execa'; - -await execaNode('scriptPath', ['argument']); -``` -*/ -export function execaNode( - scriptPath: string, - arguments?: readonly string[], - options?: NodeOptions -): ExecaChildProcess; -export function execaNode( - scriptPath: string, - arguments?: readonly string[], - options?: NodeOptions -): ExecaChildProcess; -export function execaNode(scriptPath: string, options?: NodeOptions): ExecaChildProcess; -export function execaNode(scriptPath: string, options?: NodeOptions): ExecaChildProcess; diff --git a/packages/gui/src/main/lib/execa/index.js b/packages/gui/src/main/lib/execa/index.js deleted file mode 100755 index fca5389..0000000 --- a/packages/gui/src/main/lib/execa/index.js +++ /dev/null @@ -1,309 +0,0 @@ -import {Buffer} from 'node:buffer'; -import path from 'node:path'; -import childProcess from 'node:child_process'; -import process from 'node:process'; -import crossSpawn from 'cross-spawn'; -import stripFinalNewline from '../strip-final-newline'; -import {npmRunPathEnv} from '../npm-run-path'; -import onetime from '../onetime'; -import {makeError} from './lib/error.js'; -import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; -import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; -import {addPipeMethods} from './lib/pipe.js'; -import {handleInput, getSpawnedResult, makeAllStream, handleInputSync} from './lib/stream.js'; -import {mergePromise, getSpawnedPromise} from './lib/promise.js'; -import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js'; -import {logCommand, verboseDefault} from './lib/verbose.js'; - -const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; - -const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { - const env = extendEnv ? {...process.env, ...envOption} : envOption; - - if (preferLocal) { - return npmRunPathEnv({env, cwd: localDir, execPath}); - } - - return env; -}; - -const handleArguments = (file, args, options = {}) => { - const parsed = crossSpawn._parse(file, args, options); - file = parsed.command; - args = parsed.args; - options = parsed.options; - - options = { - maxBuffer: DEFAULT_MAX_BUFFER, - buffer: true, - stripFinalNewline: true, - extendEnv: true, - preferLocal: false, - localDir: options.cwd || process.cwd(), - execPath: process.execPath, - encoding: 'utf8', - reject: true, - cleanup: true, - all: false, - windowsHide: true, - verbose: verboseDefault, - ...options, - }; - - options.env = getEnv(options); - - options.stdio = normalizeStdio(options); - - if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { - // #116 - args.unshift('/q'); - } - - return {file, args, options, parsed}; -}; - -const handleOutput = (options, value, error) => { - if (typeof value !== 'string' && !Buffer.isBuffer(value)) { - // When `execaSync()` errors, we normalize it to '' to mimic `execa()` - return error === undefined ? undefined : ''; - } - - if (options.stripFinalNewline) { - return stripFinalNewline(value); - } - - return value; -}; - -export function execa(file, args, options) { - const parsed = handleArguments(file, args, options); - const command = joinCommand(file, args); - const escapedCommand = getEscapedCommand(file, args); - logCommand(escapedCommand, parsed.options); - - validateTimeout(parsed.options); - - let spawned; - try { - spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); - } catch (error) { - // Ensure the returned error is always both a promise and a child process - const dummySpawned = new childProcess.ChildProcess(); - const errorPromise = Promise.reject(makeError({ - error, - stdout: '', - stderr: '', - all: '', - command, - escapedCommand, - parsed, - timedOut: false, - isCanceled: false, - killed: false, - })); - mergePromise(dummySpawned, errorPromise); - return dummySpawned; - } - - const spawnedPromise = getSpawnedPromise(spawned); - const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); - const processDone = setExitHandler(spawned, parsed.options, timedPromise); - - const context = {isCanceled: false}; - - spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); - spawned.cancel = spawnedCancel.bind(null, spawned, context); - - const handlePromise = async () => { - const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); - const stdout = handleOutput(parsed.options, stdoutResult); - const stderr = handleOutput(parsed.options, stderrResult); - const all = handleOutput(parsed.options, allResult); - - if (error || exitCode !== 0 || signal !== null) { - const returnedError = makeError({ - error, - exitCode, - signal, - stdout, - stderr, - all, - command, - escapedCommand, - parsed, - timedOut, - isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false), - killed: spawned.killed, - }); - - if (!parsed.options.reject) { - return returnedError; - } - - throw returnedError; - } - - return { - command, - escapedCommand, - exitCode: 0, - stdout, - stderr, - all, - failed: false, - timedOut: false, - isCanceled: false, - killed: false, - }; - }; - - const handlePromiseOnce = onetime(handlePromise); - - handleInput(spawned, parsed.options); - - spawned.all = makeAllStream(spawned, parsed.options); - - addPipeMethods(spawned); - mergePromise(spawned, handlePromiseOnce); - return spawned; -} - -export function execaSync(file, args, options) { - const parsed = handleArguments(file, args, options); - const command = joinCommand(file, args); - const escapedCommand = getEscapedCommand(file, args); - logCommand(escapedCommand, parsed.options); - - const input = handleInputSync(parsed.options); - - let result; - try { - result = childProcess.spawnSync(parsed.file, parsed.args, {...parsed.options, input}); - } catch (error) { - throw makeError({ - error, - stdout: '', - stderr: '', - all: '', - command, - escapedCommand, - parsed, - timedOut: false, - isCanceled: false, - killed: false, - }); - } - - const stdout = handleOutput(parsed.options, result.stdout, result.error); - const stderr = handleOutput(parsed.options, result.stderr, result.error); - - if (result.error || result.status !== 0 || result.signal !== null) { - const error = makeError({ - stdout, - stderr, - error: result.error, - signal: result.signal, - exitCode: result.status, - command, - escapedCommand, - parsed, - timedOut: result.error && result.error.code === 'ETIMEDOUT', - isCanceled: false, - killed: result.signal !== null, - }); - - if (!parsed.options.reject) { - return error; - } - - throw error; - } - - return { - command, - escapedCommand, - exitCode: 0, - stdout, - stderr, - failed: false, - timedOut: false, - isCanceled: false, - killed: false, - }; -} - -const normalizeScriptStdin = ({input, inputFile, stdio}) => input === undefined && inputFile === undefined && stdio === undefined - ? {stdin: 'inherit'} - : {}; - -const normalizeScriptOptions = (options = {}) => ({ - preferLocal: true, - ...normalizeScriptStdin(options), - ...options, -}); - -function create$(options) { - function $(templatesOrOptions, ...expressions) { - if (!Array.isArray(templatesOrOptions)) { - return create$({...options, ...templatesOrOptions}); - } - - const [file, ...args] = parseTemplates(templatesOrOptions, expressions); - return execa(file, args, normalizeScriptOptions(options)); - } - - $.sync = (templates, ...expressions) => { - if (!Array.isArray(templates)) { - throw new TypeError('Please use $(options).sync`command` instead of $.sync(options)`command`.'); - } - - const [file, ...args] = parseTemplates(templates, expressions); - return execaSync(file, args, normalizeScriptOptions(options)); - }; - - return $; -} - -export const $ = create$(); - -export function execaCommand(command, options) { - const [file, ...args] = parseCommand(command); - return execa(file, args, options); -} - -export function execaCommandSync(command, options) { - const [file, ...args] = parseCommand(command); - return execaSync(file, args, options); -} - -export function execaNode(scriptPath, args, options = {}) { - if (args && !Array.isArray(args) && typeof args === 'object') { - options = args; - args = []; - } - - const stdio = normalizeStdioNode(options); - const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); - - const { - nodePath = process.execPath, - nodeOptions = defaultExecArgv, - } = options; - - return execa( - nodePath, - [ - ...nodeOptions, - scriptPath, - ...(Array.isArray(args) ? args : []), - ], - { - ...options, - stdin: undefined, - stdout: undefined, - stderr: undefined, - stdio, - shell: false, - }, - ); -} diff --git a/packages/gui/src/main/lib/execa/lib/command.js b/packages/gui/src/main/lib/execa/lib/command.js deleted file mode 100755 index 727ce5f..0000000 --- a/packages/gui/src/main/lib/execa/lib/command.js +++ /dev/null @@ -1,119 +0,0 @@ -import {Buffer} from 'node:buffer'; -import {ChildProcess} from 'node:child_process'; - -const normalizeArgs = (file, args = []) => { - if (!Array.isArray(args)) { - return [file]; - } - - return [file, ...args]; -}; - -const NO_ESCAPE_REGEXP = /^[\w.-]+$/; - -const escapeArg = arg => { - if (typeof arg !== 'string' || NO_ESCAPE_REGEXP.test(arg)) { - return arg; - } - - return `"${arg.replaceAll('"', '\\"')}"`; -}; - -export const joinCommand = (file, args) => normalizeArgs(file, args).join(' '); - -export const getEscapedCommand = (file, args) => normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' '); - -const SPACES_REGEXP = / +/g; - -// Handle `execaCommand()` -export const parseCommand = command => { - const tokens = []; - for (const token of command.trim().split(SPACES_REGEXP)) { - // Allow spaces to be escaped by a backslash if not meant as a delimiter - const previousToken = tokens.at(-1); - if (previousToken && previousToken.endsWith('\\')) { - // Merge previous token with current one - tokens[tokens.length - 1] = `${previousToken.slice(0, -1)} ${token}`; - } else { - tokens.push(token); - } - } - - return tokens; -}; - -const parseExpression = expression => { - const typeOfExpression = typeof expression; - - if (typeOfExpression === 'string') { - return expression; - } - - if (typeOfExpression === 'number') { - return String(expression); - } - - if ( - typeOfExpression === 'object' - && expression !== null - && !(expression instanceof ChildProcess) - && 'stdout' in expression - ) { - const typeOfStdout = typeof expression.stdout; - - if (typeOfStdout === 'string') { - return expression.stdout; - } - - if (Buffer.isBuffer(expression.stdout)) { - return expression.stdout.toString(); - } - - throw new TypeError(`Unexpected "${typeOfStdout}" stdout in template expression`); - } - - throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`); -}; - -const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0 - ? [...tokens, ...nextTokens] - : [ - ...tokens.slice(0, -1), - `${tokens.at(-1)}${nextTokens[0]}`, - ...nextTokens.slice(1), - ]; - -const parseTemplate = ({templates, expressions, tokens, index, template}) => { - const templateString = template ?? templates.raw[index]; - const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean); - const newTokens = concatTokens( - tokens, - templateTokens, - templateString.startsWith(' '), - ); - - if (index === expressions.length) { - return newTokens; - } - - const expression = expressions[index]; - const expressionTokens = Array.isArray(expression) - ? expression.map(expression => parseExpression(expression)) - : [parseExpression(expression)]; - return concatTokens( - newTokens, - expressionTokens, - templateString.endsWith(' '), - ); -}; - -export const parseTemplates = (templates, expressions) => { - let tokens = []; - - for (const [index, template] of templates.entries()) { - tokens = parseTemplate({templates, expressions, tokens, index, template}); - } - - return tokens; -}; - diff --git a/packages/gui/src/main/lib/execa/lib/error.js b/packages/gui/src/main/lib/execa/lib/error.js deleted file mode 100755 index 761032b..0000000 --- a/packages/gui/src/main/lib/execa/lib/error.js +++ /dev/null @@ -1,87 +0,0 @@ -import process from 'node:process'; -import {signalsByName} from '../../human-signals'; - -const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { - if (timedOut) { - return `timed out after ${timeout} milliseconds`; - } - - if (isCanceled) { - return 'was canceled'; - } - - if (errorCode !== undefined) { - return `failed with ${errorCode}`; - } - - if (signal !== undefined) { - return `was killed with ${signal} (${signalDescription})`; - } - - if (exitCode !== undefined) { - return `failed with exit code ${exitCode}`; - } - - return 'failed'; -}; - -export const makeError = ({ - stdout, - stderr, - all, - error, - signal, - exitCode, - command, - escapedCommand, - timedOut, - isCanceled, - killed, - parsed: {options: {timeout, cwd = process.cwd()}}, -}) => { - // `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`. - // We normalize them to `undefined` - exitCode = exitCode === null ? undefined : exitCode; - signal = signal === null ? undefined : signal; - const signalDescription = signal === undefined ? undefined : signalsByName[signal].description; - - const errorCode = error && error.code; - - const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); - const execaMessage = `Command ${prefix}: ${command}`; - const isError = Object.prototype.toString.call(error) === '[object Error]'; - const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage; - const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); - - if (isError) { - error.originalMessage = error.message; - error.message = message; - } else { - error = new Error(message); - } - - error.shortMessage = shortMessage; - error.command = command; - error.escapedCommand = escapedCommand; - error.exitCode = exitCode; - error.signal = signal; - error.signalDescription = signalDescription; - error.stdout = stdout; - error.stderr = stderr; - error.cwd = cwd; - - if (all !== undefined) { - error.all = all; - } - - if ('bufferedData' in error) { - delete error.bufferedData; - } - - error.failed = true; - error.timedOut = Boolean(timedOut); - error.isCanceled = isCanceled; - error.killed = killed && !timedOut; - - return error; -}; diff --git a/packages/gui/src/main/lib/execa/lib/kill.js b/packages/gui/src/main/lib/execa/lib/kill.js deleted file mode 100755 index 12ce0a1..0000000 --- a/packages/gui/src/main/lib/execa/lib/kill.js +++ /dev/null @@ -1,102 +0,0 @@ -import os from 'node:os'; -import {onExit} from 'signal-exit'; - -const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; - -// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior -export const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { - const killResult = kill(signal); - setKillTimeout(kill, signal, options, killResult); - return killResult; -}; - -const setKillTimeout = (kill, signal, options, killResult) => { - if (!shouldForceKill(signal, options, killResult)) { - return; - } - - const timeout = getForceKillAfterTimeout(options); - const t = setTimeout(() => { - kill('SIGKILL'); - }, timeout); - - // Guarded because there's no `.unref()` when `execa` is used in the renderer - // process in Electron. This cannot be tested since we don't run tests in - // Electron. - // istanbul ignore else - if (t.unref) { - t.unref(); - } -}; - -const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult; - -const isSigterm = signal => signal === os.constants.signals.SIGTERM - || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); - -const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { - if (forceKillAfterTimeout === true) { - return DEFAULT_FORCE_KILL_TIMEOUT; - } - - if (!Number.isFinite(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { - throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); - } - - return forceKillAfterTimeout; -}; - -// `childProcess.cancel()` -export const spawnedCancel = (spawned, context) => { - const killResult = spawned.kill(); - - if (killResult) { - context.isCanceled = true; - } -}; - -const timeoutKill = (spawned, signal, reject) => { - spawned.kill(signal); - reject(Object.assign(new Error('Timed out'), {timedOut: true, signal})); -}; - -// `timeout` option handling -export const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { - if (timeout === 0 || timeout === undefined) { - return spawnedPromise; - } - - let timeoutId; - const timeoutPromise = new Promise((resolve, reject) => { - timeoutId = setTimeout(() => { - timeoutKill(spawned, killSignal, reject); - }, timeout); - }); - - const safeSpawnedPromise = spawnedPromise.finally(() => { - clearTimeout(timeoutId); - }); - - return Promise.race([timeoutPromise, safeSpawnedPromise]); -}; - -export const validateTimeout = ({timeout}) => { - if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) { - throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); - } -}; - -// `cleanup` option handling -export const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { - if (!cleanup || detached) { - return timedPromise; - } - - const removeExitHandler = onExit(() => { - spawned.kill(); - }); - - return timedPromise.finally(() => { - removeExitHandler(); - }); -}; diff --git a/packages/gui/src/main/lib/execa/lib/pipe.js b/packages/gui/src/main/lib/execa/lib/pipe.js deleted file mode 100755 index f26715d..0000000 --- a/packages/gui/src/main/lib/execa/lib/pipe.js +++ /dev/null @@ -1,42 +0,0 @@ -import {createWriteStream} from 'node:fs'; -import {ChildProcess} from 'node:child_process'; -import {isWritableStream} from '../../is-stream'; - -const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function'; - -const pipeToTarget = (spawned, streamName, target) => { - if (typeof target === 'string') { - spawned[streamName].pipe(createWriteStream(target)); - return spawned; - } - - if (isWritableStream(target)) { - spawned[streamName].pipe(target); - return spawned; - } - - if (!isExecaChildProcess(target)) { - throw new TypeError('The second argument must be a string, a stream or an Execa child process.'); - } - - if (!isWritableStream(target.stdin)) { - throw new TypeError('The target child process\'s stdin must be available.'); - } - - spawned[streamName].pipe(target.stdin); - return target; -}; - -export const addPipeMethods = spawned => { - if (spawned.stdout !== null) { - spawned.pipeStdout = pipeToTarget.bind(undefined, spawned, 'stdout'); - } - - if (spawned.stderr !== null) { - spawned.pipeStderr = pipeToTarget.bind(undefined, spawned, 'stderr'); - } - - if (spawned.all !== undefined) { - spawned.pipeAll = pipeToTarget.bind(undefined, spawned, 'all'); - } -}; diff --git a/packages/gui/src/main/lib/execa/lib/promise.js b/packages/gui/src/main/lib/execa/lib/promise.js deleted file mode 100755 index a4773f3..0000000 --- a/packages/gui/src/main/lib/execa/lib/promise.js +++ /dev/null @@ -1,36 +0,0 @@ -// eslint-disable-next-line unicorn/prefer-top-level-await -const nativePromisePrototype = (async () => {})().constructor.prototype; - -const descriptors = ['then', 'catch', 'finally'].map(property => [ - property, - Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property), -]); - -// The return value is a mixin of `childProcess` and `Promise` -export const mergePromise = (spawned, promise) => { - for (const [property, descriptor] of descriptors) { - // Starting the main `promise` is deferred to avoid consuming streams - const value = typeof promise === 'function' - ? (...args) => Reflect.apply(descriptor.value, promise(), args) - : descriptor.value.bind(promise); - - Reflect.defineProperty(spawned, property, {...descriptor, value}); - } -}; - -// Use promises instead of `child_process` events -export const getSpawnedPromise = spawned => new Promise((resolve, reject) => { - spawned.on('exit', (exitCode, signal) => { - resolve({exitCode, signal}); - }); - - spawned.on('error', error => { - reject(error); - }); - - if (spawned.stdin) { - spawned.stdin.on('error', error => { - reject(error); - }); - } -}); diff --git a/packages/gui/src/main/lib/execa/lib/stdio.js b/packages/gui/src/main/lib/execa/lib/stdio.js deleted file mode 100755 index e8c1132..0000000 --- a/packages/gui/src/main/lib/execa/lib/stdio.js +++ /dev/null @@ -1,49 +0,0 @@ -const aliases = ['stdin', 'stdout', 'stderr']; - -const hasAlias = options => aliases.some(alias => options[alias] !== undefined); - -export const normalizeStdio = options => { - if (!options) { - return; - } - - const {stdio} = options; - - if (stdio === undefined) { - return aliases.map(alias => options[alias]); - } - - if (hasAlias(options)) { - throw new Error(`It's not possible to provide \`stdio\` in combination with one of ${aliases.map(alias => `\`${alias}\``).join(', ')}`); - } - - if (typeof stdio === 'string') { - return stdio; - } - - if (!Array.isArray(stdio)) { - throw new TypeError(`Expected \`stdio\` to be of type \`string\` or \`Array\`, got \`${typeof stdio}\``); - } - - const length = Math.max(stdio.length, aliases.length); - return Array.from({length}, (value, index) => stdio[index]); -}; - -// `ipc` is pushed unless it is already present -export const normalizeStdioNode = options => { - const stdio = normalizeStdio(options); - - if (stdio === 'ipc') { - return 'ipc'; - } - - if (stdio === undefined || typeof stdio === 'string') { - return [stdio, stdio, stdio, 'ipc']; - } - - if (stdio.includes('ipc')) { - return stdio; - } - - return [...stdio, 'ipc']; -}; diff --git a/packages/gui/src/main/lib/execa/lib/stream.js b/packages/gui/src/main/lib/execa/lib/stream.js deleted file mode 100755 index 6912270..0000000 --- a/packages/gui/src/main/lib/execa/lib/stream.js +++ /dev/null @@ -1,133 +0,0 @@ -import {createReadStream, readFileSync} from 'node:fs'; -import {setTimeout} from 'node:timers/promises'; -import {isStream} from '../../is-stream'; -import getStream, {getStreamAsBuffer} from '../../get-stream'; -import mergeStream from 'merge-stream'; - -const validateInputOptions = input => { - if (input !== undefined) { - throw new TypeError('The `input` and `inputFile` options cannot be both set.'); - } -}; - -const getInputSync = ({input, inputFile}) => { - if (typeof inputFile !== 'string') { - return input; - } - - validateInputOptions(input); - return readFileSync(inputFile); -}; - -// `input` and `inputFile` option in sync mode -export const handleInputSync = options => { - const input = getInputSync(options); - - if (isStream(input)) { - throw new TypeError('The `input` option cannot be a stream in sync mode'); - } - - return input; -}; - -const getInput = ({input, inputFile}) => { - if (typeof inputFile !== 'string') { - return input; - } - - validateInputOptions(input); - return createReadStream(inputFile); -}; - -// `input` and `inputFile` option in async mode -export const handleInput = (spawned, options) => { - const input = getInput(options); - - if (input === undefined) { - return; - } - - if (isStream(input)) { - input.pipe(spawned.stdin); - } else { - spawned.stdin.end(input); - } -}; - -// `all` interleaves `stdout` and `stderr` -export const makeAllStream = (spawned, {all}) => { - if (!all || (!spawned.stdout && !spawned.stderr)) { - return; - } - - const mixed = mergeStream(); - - if (spawned.stdout) { - mixed.add(spawned.stdout); - } - - if (spawned.stderr) { - mixed.add(spawned.stderr); - } - - return mixed; -}; - -// On failure, `result.stdout|stderr|all` should contain the currently buffered stream -const getBufferedData = async (stream, streamPromise) => { - // When `buffer` is `false`, `streamPromise` is `undefined` and there is no buffered data to retrieve - if (!stream || streamPromise === undefined) { - return; - } - - // Wait for the `all` stream to receive the last chunk before destroying the stream - await setTimeout(0); - - stream.destroy(); - - try { - return await streamPromise; - } catch (error) { - return error.bufferedData; - } -}; - -const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => { - if (!stream || !buffer) { - return; - } - - // eslint-disable-next-line unicorn/text-encoding-identifier-case - if (encoding === 'utf8' || encoding === 'utf-8') { - return getStream(stream, {maxBuffer}); - } - - if (encoding === null || encoding === 'buffer') { - return getStreamAsBuffer(stream, {maxBuffer}); - } - - return applyEncoding(stream, maxBuffer, encoding); -}; - -const applyEncoding = async (stream, maxBuffer, encoding) => { - const buffer = await getStreamAsBuffer(stream, {maxBuffer}); - return buffer.toString(encoding); -}; - -// Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) -export const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { - const stdoutPromise = getStreamPromise(stdout, {encoding, buffer, maxBuffer}); - const stderrPromise = getStreamPromise(stderr, {encoding, buffer, maxBuffer}); - const allPromise = getStreamPromise(all, {encoding, buffer, maxBuffer: maxBuffer * 2}); - - try { - return await Promise.all([processDone, stdoutPromise, stderrPromise, allPromise]); - } catch (error) { - return Promise.all([ - {error, signal: error.signal, timedOut: error.timedOut}, - getBufferedData(stdout, stdoutPromise), - getBufferedData(stderr, stderrPromise), - getBufferedData(all, allPromise), - ]); - } -}; diff --git a/packages/gui/src/main/lib/execa/lib/verbose.js b/packages/gui/src/main/lib/execa/lib/verbose.js deleted file mode 100755 index 5f5490e..0000000 --- a/packages/gui/src/main/lib/execa/lib/verbose.js +++ /dev/null @@ -1,19 +0,0 @@ -import {debuglog} from 'node:util'; -import process from 'node:process'; - -export const verboseDefault = debuglog('execa').enabled; - -const padField = (field, padding) => String(field).padStart(padding, '0'); - -const getTimestamp = () => { - const date = new Date(); - return `${padField(date.getHours(), 2)}:${padField(date.getMinutes(), 2)}:${padField(date.getSeconds(), 2)}.${padField(date.getMilliseconds(), 3)}`; -}; - -export const logCommand = (escapedCommand, {verbose}) => { - if (!verbose) { - return; - } - - process.stderr.write(`[${getTimestamp()}] ${escapedCommand}\n`); -}; diff --git a/packages/gui/src/main/lib/execa/public_lib.js b/packages/gui/src/main/lib/execa/public_lib.js deleted file mode 100644 index 329057d..0000000 --- a/packages/gui/src/main/lib/execa/public_lib.js +++ /dev/null @@ -1,17 +0,0 @@ -import { execa } from "." -import path from "node:path" - -export default class ExecLib { - constructor(manifest) { - this.manifest = manifest - } - - async file(file, args, options) { - file = path.resolve(this.manifest.install_path, file) - - return await execa(file, [...args], { - ...options, - cwd: this.manifest.install_path, - }) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/get-stream/array-buffer.js b/packages/gui/src/main/lib/get-stream/array-buffer.js deleted file mode 100644 index a547405..0000000 --- a/packages/gui/src/main/lib/get-stream/array-buffer.js +++ /dev/null @@ -1,84 +0,0 @@ -import {getStreamContents} from './contents.js'; -import {noop, throwObjectStream, getLengthProp} from './utils.js'; - -export async function getStreamAsArrayBuffer(stream, options) { - return getStreamContents(stream, arrayBufferMethods, options); -} - -const initArrayBuffer = () => ({contents: new ArrayBuffer(0)}); - -const useTextEncoder = chunk => textEncoder.encode(chunk); -const textEncoder = new TextEncoder(); - -const useUint8Array = chunk => new Uint8Array(chunk); - -const useUint8ArrayWithOffset = chunk => new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); - -const truncateArrayBufferChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); - -// `contents` is an increasingly growing `Uint8Array`. -const addArrayBufferChunk = (convertedChunk, {contents, length: previousLength}, length) => { - const newContents = hasArrayBufferResize() ? resizeArrayBuffer(contents, length) : resizeArrayBufferSlow(contents, length); - new Uint8Array(newContents).set(convertedChunk, previousLength); - return newContents; -}; - -// Without `ArrayBuffer.resize()`, `contents` size is always a power of 2. -// This means its last bytes are zeroes (not stream data), which need to be -// trimmed at the end with `ArrayBuffer.slice()`. -const resizeArrayBufferSlow = (contents, length) => { - if (length <= contents.byteLength) { - return contents; - } - - const arrayBuffer = new ArrayBuffer(getNewContentsLength(length)); - new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); - return arrayBuffer; -}; - -// With `ArrayBuffer.resize()`, `contents` size matches exactly the size of -// the stream data. It does not include extraneous zeroes to trim at the end. -// The underlying `ArrayBuffer` does allocate a number of bytes that is a power -// of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`. -const resizeArrayBuffer = (contents, length) => { - if (length <= contents.maxByteLength) { - contents.resize(length); - return contents; - } - - const arrayBuffer = new ArrayBuffer(length, {maxByteLength: getNewContentsLength(length)}); - new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); - return arrayBuffer; -}; - -// Retrieve the closest `length` that is both >= and a power of 2 -const getNewContentsLength = length => SCALE_FACTOR ** Math.ceil(Math.log(length) / Math.log(SCALE_FACTOR)); - -const SCALE_FACTOR = 2; - -const finalizeArrayBuffer = ({contents, length}) => hasArrayBufferResize() ? contents : contents.slice(0, length); - -// `ArrayBuffer.slice()` is slow. When `ArrayBuffer.resize()` is available -// (Node >=20.0.0, Safari >=16.4 and Chrome), we can use it instead. -// eslint-disable-next-line no-warning-comments -// TODO: remove after dropping support for Node 20. -// eslint-disable-next-line no-warning-comments -// TODO: use `ArrayBuffer.transferToFixedLength()` instead once it is available -const hasArrayBufferResize = () => 'resize' in ArrayBuffer.prototype; - -const arrayBufferMethods = { - init: initArrayBuffer, - convertChunk: { - string: useTextEncoder, - buffer: useUint8Array, - arrayBuffer: useUint8Array, - dataView: useUint8ArrayWithOffset, - typedArray: useUint8ArrayWithOffset, - others: throwObjectStream, - }, - getSize: getLengthProp, - truncateChunk: truncateArrayBufferChunk, - addChunk: addArrayBufferChunk, - getFinalChunk: noop, - finalize: finalizeArrayBuffer, -}; diff --git a/packages/gui/src/main/lib/get-stream/array.js b/packages/gui/src/main/lib/get-stream/array.js deleted file mode 100644 index 468bad1..0000000 --- a/packages/gui/src/main/lib/get-stream/array.js +++ /dev/null @@ -1,32 +0,0 @@ -import {getStreamContents} from './contents.js'; -import {identity, noop, getContentsProp} from './utils.js'; - -export async function getStreamAsArray(stream, options) { - return getStreamContents(stream, arrayMethods, options); -} - -const initArray = () => ({contents: []}); - -const increment = () => 1; - -const addArrayChunk = (convertedChunk, {contents}) => { - contents.push(convertedChunk); - return contents; -}; - -const arrayMethods = { - init: initArray, - convertChunk: { - string: identity, - buffer: identity, - arrayBuffer: identity, - dataView: identity, - typedArray: identity, - others: identity, - }, - getSize: increment, - truncateChunk: noop, - addChunk: addArrayChunk, - getFinalChunk: noop, - finalize: getContentsProp, -}; diff --git a/packages/gui/src/main/lib/get-stream/buffer.js b/packages/gui/src/main/lib/get-stream/buffer.js deleted file mode 100644 index 7d22d78..0000000 --- a/packages/gui/src/main/lib/get-stream/buffer.js +++ /dev/null @@ -1,20 +0,0 @@ -import {getStreamAsArrayBuffer} from './array-buffer.js'; - -export async function getStreamAsBuffer(stream, options) { - if (!('Buffer' in globalThis)) { - throw new Error('getStreamAsBuffer() is only supported in Node.js'); - } - - try { - return arrayBufferToNodeBuffer(await getStreamAsArrayBuffer(stream, options)); - } catch (error) { - if (error.bufferedData !== undefined) { - error.bufferedData = arrayBufferToNodeBuffer(error.bufferedData); - } - - throw error; - } -} - -// eslint-disable-next-line n/prefer-global/buffer -const arrayBufferToNodeBuffer = arrayBuffer => globalThis.Buffer.from(arrayBuffer); diff --git a/packages/gui/src/main/lib/get-stream/contents.js b/packages/gui/src/main/lib/get-stream/contents.js deleted file mode 100644 index 2ca36f2..0000000 --- a/packages/gui/src/main/lib/get-stream/contents.js +++ /dev/null @@ -1,101 +0,0 @@ -export const getStreamContents = async (stream, {init, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, finalize}, {maxBuffer = Number.POSITIVE_INFINITY} = {}) => { - if (!isAsyncIterable(stream)) { - throw new Error('The first argument must be a Readable, a ReadableStream, or an async iterable.'); - } - - const state = init(); - state.length = 0; - - try { - for await (const chunk of stream) { - const chunkType = getChunkType(chunk); - const convertedChunk = convertChunk[chunkType](chunk, state); - appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); - } - - appendFinalChunk({state, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}); - return finalize(state); - } catch (error) { - error.bufferedData = finalize(state); - throw error; - } -}; - -const appendFinalChunk = ({state, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}) => { - const convertedChunk = getFinalChunk(state); - if (convertedChunk !== undefined) { - appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); - } -}; - -const appendChunk = ({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}) => { - const chunkSize = getSize(convertedChunk); - const newLength = state.length + chunkSize; - - if (newLength <= maxBuffer) { - addNewChunk(convertedChunk, state, addChunk, newLength); - return; - } - - const truncatedChunk = truncateChunk(convertedChunk, maxBuffer - state.length); - - if (truncatedChunk !== undefined) { - addNewChunk(truncatedChunk, state, addChunk, maxBuffer); - } - - throw new MaxBufferError(); -}; - -const addNewChunk = (convertedChunk, state, addChunk, newLength) => { - state.contents = addChunk(convertedChunk, state, newLength); - state.length = newLength; -}; - -const isAsyncIterable = stream => typeof stream === 'object' && stream !== null && typeof stream[Symbol.asyncIterator] === 'function'; - -const getChunkType = chunk => { - const typeOfChunk = typeof chunk; - - if (typeOfChunk === 'string') { - return 'string'; - } - - if (typeOfChunk !== 'object' || chunk === null) { - return 'others'; - } - - // eslint-disable-next-line n/prefer-global/buffer - if (globalThis.Buffer?.isBuffer(chunk)) { - return 'buffer'; - } - - const prototypeName = objectToString.call(chunk); - - if (prototypeName === '[object ArrayBuffer]') { - return 'arrayBuffer'; - } - - if (prototypeName === '[object DataView]') { - return 'dataView'; - } - - if ( - Number.isInteger(chunk.byteLength) - && Number.isInteger(chunk.byteOffset) - && objectToString.call(chunk.buffer) === '[object ArrayBuffer]' - ) { - return 'typedArray'; - } - - return 'others'; -}; - -const {toString: objectToString} = Object.prototype; - -export class MaxBufferError extends Error { - name = 'MaxBufferError'; - - constructor() { - super('maxBuffer exceeded'); - } -} diff --git a/packages/gui/src/main/lib/get-stream/index.d.ts b/packages/gui/src/main/lib/get-stream/index.d.ts deleted file mode 100644 index 0a456ca..0000000 --- a/packages/gui/src/main/lib/get-stream/index.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -import {type Readable} from 'node:stream'; -import {type Buffer} from 'node:buffer'; - -export class MaxBufferError extends Error { - readonly name: 'MaxBufferError'; - constructor(); -} - -type TextStreamItem = string | Buffer | ArrayBuffer | ArrayBufferView; -export type AnyStream = Readable | ReadableStream | AsyncIterable; - -export type Options = { - /** - Maximum length of the stream. If exceeded, the promise will be rejected with a `MaxBufferError`. - - Depending on the [method](#api), the length is measured with [`string.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length), [`buffer.length`](https://nodejs.org/api/buffer.html#buflength), [`arrayBuffer.byteLength`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/byteLength) or [`array.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length). - - @default Infinity - */ - readonly maxBuffer?: number; -}; - -/** -Get the given `stream` as a string. - -@returns The stream's contents as a promise. - -@example -``` -import fs from 'node:fs'; -import getStream from 'get-stream'; - -const stream = fs.createReadStream('unicorn.txt'); - -console.log(await getStream(stream)); -// ,,))))))));, -// __)))))))))))))), -// \|/ -\(((((''''((((((((. -// -*-==//////(('' . `)))))), -// /|\ ))| o ;-. '((((( ,(, -// ( `| / ) ;))))' ,_))^;(~ -// | | | ,))((((_ _____------~~~-. %,;(;(>';'~ -// o_); ; )))(((` ~---~ `:: \ %%~~)(v;(`('~ -// ; ''''```` `: `:::|\,__,%% );`'; ~ -// | _ ) / `:|`----' `-' -// ______/\/~ | / / -// /~;;.____/;;' / ___--,-( `;;;/ -// / // _;______;'------~~~~~ /;;/\ / -// // | | / ; \;;,\ -// (<_ | ; /',/-----' _> -// \_| ||_ //~;~~~~~~~~~ -// `\_| (,~~ -// \~\ -// ~~ -``` - -@example -``` -import getStream from 'get-stream'; - -const {body: readableStream} = await fetch('https://example.com'); -console.log(await getStream(readableStream)); -``` - -@example -``` -import {opendir} from 'node:fs/promises'; -import {getStreamAsArray} from 'get-stream'; - -const asyncIterable = await opendir(directory); -console.log(await getStreamAsArray(asyncIterable)); -``` -*/ -export default function getStream(stream: AnyStream, options?: Options): Promise; - -/** -Get the given `stream` as a Node.js [`Buffer`](https://nodejs.org/api/buffer.html#class-buffer). - -@returns The stream's contents as a promise. - -@example -``` -import {getStreamAsBuffer} from 'get-stream'; - -const stream = fs.createReadStream('unicorn.png'); -console.log(await getStreamAsBuffer(stream)); -``` -*/ -export function getStreamAsBuffer(stream: AnyStream, options?: Options): Promise; - -/** -Get the given `stream` as an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). - -@returns The stream's contents as a promise. - -@example -``` -import {getStreamAsArrayBuffer} from 'get-stream'; - -const {body: readableStream} = await fetch('https://example.com'); -console.log(await getStreamAsArrayBuffer(readableStream)); -``` -*/ -export function getStreamAsArrayBuffer(stream: AnyStream, options?: Options): Promise; - -/** -Get the given `stream` as an array. Unlike [other methods](#api), this supports [streams of objects](https://nodejs.org/api/stream.html#object-mode). - -@returns The stream's contents as a promise. - -@example -``` -import {getStreamAsArray} from 'get-stream'; - -const {body: readableStream} = await fetch('https://example.com'); -console.log(await getStreamAsArray(readableStream)); -``` -*/ -export function getStreamAsArray(stream: AnyStream, options?: Options): Promise; diff --git a/packages/gui/src/main/lib/get-stream/index.js b/packages/gui/src/main/lib/get-stream/index.js deleted file mode 100644 index 43c2dd4..0000000 --- a/packages/gui/src/main/lib/get-stream/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export {getStreamAsArray} from './array.js'; -export {getStreamAsArrayBuffer} from './array-buffer.js'; -export {getStreamAsBuffer} from './buffer.js'; -export {getStreamAsString as default} from './string.js'; -export {MaxBufferError} from './contents.js'; diff --git a/packages/gui/src/main/lib/get-stream/index.test-d.ts b/packages/gui/src/main/lib/get-stream/index.test-d.ts deleted file mode 100644 index c90068f..0000000 --- a/packages/gui/src/main/lib/get-stream/index.test-d.ts +++ /dev/null @@ -1,98 +0,0 @@ -import {Buffer} from 'node:buffer'; -import {open} from 'node:fs/promises'; -import {type Readable} from 'node:stream'; -import fs from 'node:fs'; -import {expectType, expectError, expectAssignable, expectNotAssignable} from 'tsd'; -import getStream, {getStreamAsBuffer, getStreamAsArrayBuffer, getStreamAsArray, MaxBufferError, type Options, type AnyStream} from './index.js'; - -const nodeStream = fs.createReadStream('foo') as Readable; - -const fileHandle = await open('test'); -const readableStream = fileHandle.readableWebStream(); - -const asyncIterable = (value: T): AsyncGenerator => (async function * () { - yield value; -})(); -const stringAsyncIterable = asyncIterable(''); -const bufferAsyncIterable = asyncIterable(Buffer.from('')); -const arrayBufferAsyncIterable = asyncIterable(new ArrayBuffer(0)); -const dataViewAsyncIterable = asyncIterable(new DataView(new ArrayBuffer(0))); -const typedArrayAsyncIterable = asyncIterable(new Uint8Array([])); -const objectItem = {test: true}; -const objectAsyncIterable = asyncIterable(objectItem); - -expectType(await getStream(nodeStream)); -expectType(await getStream(nodeStream, {maxBuffer: 10})); -expectType(await getStream(readableStream)); -expectType(await getStream(stringAsyncIterable)); -expectType(await getStream(bufferAsyncIterable)); -expectType(await getStream(arrayBufferAsyncIterable)); -expectType(await getStream(dataViewAsyncIterable)); -expectType(await getStream(typedArrayAsyncIterable)); -expectError(await getStream(objectAsyncIterable)); -expectError(await getStream({})); -expectError(await getStream(nodeStream, {maxBuffer: '10'})); -expectError(await getStream(nodeStream, {unknownOption: 10})); -expectError(await getStream(nodeStream, {maxBuffer: 10}, {})); - -expectType(await getStreamAsBuffer(nodeStream)); -expectType(await getStreamAsBuffer(nodeStream, {maxBuffer: 10})); -expectType(await getStreamAsBuffer(readableStream)); -expectType(await getStreamAsBuffer(stringAsyncIterable)); -expectType(await getStreamAsBuffer(bufferAsyncIterable)); -expectType(await getStreamAsBuffer(arrayBufferAsyncIterable)); -expectType(await getStreamAsBuffer(dataViewAsyncIterable)); -expectType(await getStreamAsBuffer(typedArrayAsyncIterable)); -expectError(await getStreamAsBuffer(objectAsyncIterable)); -expectError(await getStreamAsBuffer({})); -expectError(await getStreamAsBuffer(nodeStream, {maxBuffer: '10'})); -expectError(await getStreamAsBuffer(nodeStream, {unknownOption: 10})); -expectError(await getStreamAsBuffer(nodeStream, {maxBuffer: 10}, {})); - -expectType(await getStreamAsArrayBuffer(nodeStream)); -expectType(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: 10})); -expectType(await getStreamAsArrayBuffer(readableStream)); -expectType(await getStreamAsArrayBuffer(stringAsyncIterable)); -expectType(await getStreamAsArrayBuffer(bufferAsyncIterable)); -expectType(await getStreamAsArrayBuffer(arrayBufferAsyncIterable)); -expectType(await getStreamAsArrayBuffer(dataViewAsyncIterable)); -expectType(await getStreamAsArrayBuffer(typedArrayAsyncIterable)); -expectError(await getStreamAsArrayBuffer(objectAsyncIterable)); -expectError(await getStreamAsArrayBuffer({})); -expectError(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: '10'})); -expectError(await getStreamAsArrayBuffer(nodeStream, {unknownOption: 10})); -expectError(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: 10}, {})); - -expectType(await getStreamAsArray(nodeStream)); -expectType(await getStreamAsArray(nodeStream, {maxBuffer: 10})); -expectType(await getStreamAsArray(readableStream)); -expectType(await getStreamAsArray(readableStream as ReadableStream)); -expectType(await getStreamAsArray(stringAsyncIterable)); -expectType(await getStreamAsArray(bufferAsyncIterable)); -expectType(await getStreamAsArray(arrayBufferAsyncIterable)); -expectType(await getStreamAsArray(dataViewAsyncIterable)); -expectType(await getStreamAsArray(typedArrayAsyncIterable)); -expectType>(await getStreamAsArray(objectAsyncIterable)); -expectError(await getStreamAsArray({})); -expectError(await getStreamAsArray(nodeStream, {maxBuffer: '10'})); -expectError(await getStreamAsArray(nodeStream, {unknownOption: 10})); -expectError(await getStreamAsArray(nodeStream, {maxBuffer: 10}, {})); - -expectAssignable(nodeStream); -expectAssignable(readableStream); -expectAssignable(stringAsyncIterable); -expectAssignable(bufferAsyncIterable); -expectAssignable(arrayBufferAsyncIterable); -expectAssignable(dataViewAsyncIterable); -expectAssignable(typedArrayAsyncIterable); -expectAssignable>(objectAsyncIterable); -expectNotAssignable(objectAsyncIterable); -expectAssignable>(stringAsyncIterable); -expectNotAssignable>(bufferAsyncIterable); -expectNotAssignable({}); - -expectAssignable({maxBuffer: 10}); -expectNotAssignable({maxBuffer: '10'}); -expectNotAssignable({unknownOption: 10}); - -expectType(new MaxBufferError()); diff --git a/packages/gui/src/main/lib/get-stream/string.js b/packages/gui/src/main/lib/get-stream/string.js deleted file mode 100644 index 90f94b9..0000000 --- a/packages/gui/src/main/lib/get-stream/string.js +++ /dev/null @@ -1,36 +0,0 @@ -import {getStreamContents} from './contents.js'; -import {identity, getContentsProp, throwObjectStream, getLengthProp} from './utils.js'; - -export async function getStreamAsString(stream, options) { - return getStreamContents(stream, stringMethods, options); -} - -const initString = () => ({contents: '', textDecoder: new TextDecoder()}); - -const useTextDecoder = (chunk, {textDecoder}) => textDecoder.decode(chunk, {stream: true}); - -const addStringChunk = (convertedChunk, {contents}) => contents + convertedChunk; - -const truncateStringChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); - -const getFinalStringChunk = ({textDecoder}) => { - const finalChunk = textDecoder.decode(); - return finalChunk === '' ? undefined : finalChunk; -}; - -const stringMethods = { - init: initString, - convertChunk: { - string: identity, - buffer: useTextDecoder, - arrayBuffer: useTextDecoder, - dataView: useTextDecoder, - typedArray: useTextDecoder, - others: throwObjectStream, - }, - getSize: getLengthProp, - truncateChunk: truncateStringChunk, - addChunk: addStringChunk, - getFinalChunk: getFinalStringChunk, - finalize: getContentsProp, -}; diff --git a/packages/gui/src/main/lib/get-stream/utils.js b/packages/gui/src/main/lib/get-stream/utils.js deleted file mode 100644 index af8d5e2..0000000 --- a/packages/gui/src/main/lib/get-stream/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -export const identity = value => value; - -export const noop = () => undefined; - -export const getContentsProp = ({contents}) => contents; - -export const throwObjectStream = chunk => { - throw new Error(`Streams in object mode are not supported: ${String(chunk)}`); -}; - -export const getLengthProp = convertedChunk => convertedChunk.length; diff --git a/packages/gui/src/main/lib/google_drive/index.js b/packages/gui/src/main/lib/google_drive/index.js deleted file mode 100644 index 2225c59..0000000 --- a/packages/gui/src/main/lib/google_drive/index.js +++ /dev/null @@ -1,194 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -const ElectronGoogleOAuth2 = require("@getstation/electron-google-oauth2").default - -import { ipcMain } from "electron" -import progressHandler from "progress-stream" - -import { google } from "googleapis" - -import { safeStorage } from "electron" - -import sendToRender from "../../utils/sendToRender" - -export default class GoogleDriveAPI { - static async createClientAuthFromCredentials(credentials) { - return await google.auth.fromJSON(credentials) - } - - static async getDriveInstance() { - const credentials = await GoogleDriveAPI.readCredentials() - - if (!credentials) { - throw new Error("No credentials or auth found") - } - - const client = await GoogleDriveAPI.createClientAuthFromCredentials(credentials) - - return google.drive({ - version: "v3", - auth: client, - }) - } - - static async readCredentials() { - const encryptedValue = global.SettingsStore.get("drive_auth") - - if (!encryptedValue) { - return null - } - - const decryptedValue = safeStorage.decryptString(Buffer.from(encryptedValue, "latin1")) - - if (!decryptedValue) { - return null - } - - return JSON.parse(decryptedValue) - } - - static async saveCredentials(credentials) { - const payload = { - type: "authorized_user", - client_id: credentials.client_id, - client_secret: credentials.client_secret, - access_token: credentials.access_token, - refresh_token: credentials.refresh_token, - } - - const encryptedBuffer = safeStorage.encryptString(JSON.stringify(payload)) - - global.SettingsStore.set("drive_auth", encryptedBuffer.toString("latin1")) - - console.log("Saved Drive credentials...",) - } - - static async authorize() { - console.log("Authorizing Google Drive...") - - const auth = await global._drive_oauth.openAuthWindowAndGetTokens() - - await GoogleDriveAPI.saveCredentials({ - ...auth, - client_id: import.meta.env.MAIN_VITE_DRIVE_ID, - client_secret: import.meta.env.MAIN_VITE_DRIVE_SECRET, - }) - - await sendToRender("drive:authorized") - - return auth - } - - static async unauthorize() { - console.log("unauthorize Google Drive...") - - global.SettingsStore.delete("drive_auth") - - await sendToRender("drive:unauthorized") - } - - static async init() { - console.log("Initializing Google Drive...") - - global._drive_oauth = new ElectronGoogleOAuth2( - import.meta.env.MAIN_VITE_DRIVE_ID, - import.meta.env.MAIN_VITE_DRIVE_SECRET, - ["https://www.googleapis.com/auth/drive.readonly"], - ) - - // register ipc events - for (const [key, fn] of Object.entries(GoogleDriveAPI.ipcHandlers)) { - ipcMain.handle(key, fn) - } - } - - static operations = { - listFiles: async () => { - const drive = await GoogleDriveAPI.getDriveInstance() - - const res = await drive.files.list({ - pageSize: 10, - fields: "nextPageToken, files(id, name)", - }) - - const files = res.data.files.map((file) => { - return { - id: file.id, - name: file.name, - } - }) - - return files - }, - downloadFile: (file_id, dest_path, callback, progressCallback) => { - return new Promise(async (resolve, reject) => { - if (!file_id) { - throw new Error("No file_id provided") - } - - if (!dest_path) { - throw new Error("No destination path provided") - } - - const drive = await GoogleDriveAPI.getDriveInstance() - - const { data: metadata } = await drive.files.get({ - fileId: file_id, - }) - - if (!metadata) { - throw new Error("Cannot retrieve file metadata") - } - - let progress = progressHandler({ - length: metadata.size, - time: 500, - }) - - const dest_stream = fs.createWriteStream(dest_path) - - drive.files.get({ - fileId: file_id, - alt: "media", - }, { - responseType: "stream", - }, (err, { data }) => { - if (err) { - return reject(err) - } - - data - .on("error", (err) => { - if (typeof callback === "function") { - callback(err) - } - - reject(err) - }) - .pipe(progress).pipe(dest_stream) - }) - - progress.on("progress", (progress) => { - if (typeof progressCallback === "function") { - progressCallback(progress) - } - }) - - dest_stream.on("finish", () => { - if (typeof callback === "function") { - callback() - } - - resolve() - }) - }) - } - } - - static ipcHandlers = { - "drive:listFiles": GoogleDriveAPI.operations.listFiles, - "drive:authorize": GoogleDriveAPI.authorize, - "drive:unauthorize": GoogleDriveAPI.unauthorize, - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/human-signals/core.js b/packages/gui/src/main/lib/human-signals/core.js deleted file mode 100644 index e083d8f..0000000 --- a/packages/gui/src/main/lib/human-signals/core.js +++ /dev/null @@ -1,275 +0,0 @@ -/* eslint-disable max-lines */ -// List of known process signals with information about them -export const SIGNALS = [ - { - name: 'SIGHUP', - number: 1, - action: 'terminate', - description: 'Terminal closed', - standard: 'posix', - }, - { - name: 'SIGINT', - number: 2, - action: 'terminate', - description: 'User interruption with CTRL-C', - standard: 'ansi', - }, - { - name: 'SIGQUIT', - number: 3, - action: 'core', - description: 'User interruption with CTRL-\\', - standard: 'posix', - }, - { - name: 'SIGILL', - number: 4, - action: 'core', - description: 'Invalid machine instruction', - standard: 'ansi', - }, - { - name: 'SIGTRAP', - number: 5, - action: 'core', - description: 'Debugger breakpoint', - standard: 'posix', - }, - { - name: 'SIGABRT', - number: 6, - action: 'core', - description: 'Aborted', - standard: 'ansi', - }, - { - name: 'SIGIOT', - number: 6, - action: 'core', - description: 'Aborted', - standard: 'bsd', - }, - { - name: 'SIGBUS', - number: 7, - action: 'core', - description: - 'Bus error due to misaligned, non-existing address or paging error', - standard: 'bsd', - }, - { - name: 'SIGEMT', - number: 7, - action: 'terminate', - description: 'Command should be emulated but is not implemented', - standard: 'other', - }, - { - name: 'SIGFPE', - number: 8, - action: 'core', - description: 'Floating point arithmetic error', - standard: 'ansi', - }, - { - name: 'SIGKILL', - number: 9, - action: 'terminate', - description: 'Forced termination', - standard: 'posix', - forced: true, - }, - { - name: 'SIGUSR1', - number: 10, - action: 'terminate', - description: 'Application-specific signal', - standard: 'posix', - }, - { - name: 'SIGSEGV', - number: 11, - action: 'core', - description: 'Segmentation fault', - standard: 'ansi', - }, - { - name: 'SIGUSR2', - number: 12, - action: 'terminate', - description: 'Application-specific signal', - standard: 'posix', - }, - { - name: 'SIGPIPE', - number: 13, - action: 'terminate', - description: 'Broken pipe or socket', - standard: 'posix', - }, - { - name: 'SIGALRM', - number: 14, - action: 'terminate', - description: 'Timeout or timer', - standard: 'posix', - }, - { - name: 'SIGTERM', - number: 15, - action: 'terminate', - description: 'Termination', - standard: 'ansi', - }, - { - name: 'SIGSTKFLT', - number: 16, - action: 'terminate', - description: 'Stack is empty or overflowed', - standard: 'other', - }, - { - name: 'SIGCHLD', - number: 17, - action: 'ignore', - description: 'Child process terminated, paused or unpaused', - standard: 'posix', - }, - { - name: 'SIGCLD', - number: 17, - action: 'ignore', - description: 'Child process terminated, paused or unpaused', - standard: 'other', - }, - { - name: 'SIGCONT', - number: 18, - action: 'unpause', - description: 'Unpaused', - standard: 'posix', - forced: true, - }, - { - name: 'SIGSTOP', - number: 19, - action: 'pause', - description: 'Paused', - standard: 'posix', - forced: true, - }, - { - name: 'SIGTSTP', - number: 20, - action: 'pause', - description: 'Paused using CTRL-Z or "suspend"', - standard: 'posix', - }, - { - name: 'SIGTTIN', - number: 21, - action: 'pause', - description: 'Background process cannot read terminal input', - standard: 'posix', - }, - { - name: 'SIGBREAK', - number: 21, - action: 'terminate', - description: 'User interruption with CTRL-BREAK', - standard: 'other', - }, - { - name: 'SIGTTOU', - number: 22, - action: 'pause', - description: 'Background process cannot write to terminal output', - standard: 'posix', - }, - { - name: 'SIGURG', - number: 23, - action: 'ignore', - description: 'Socket received out-of-band data', - standard: 'bsd', - }, - { - name: 'SIGXCPU', - number: 24, - action: 'core', - description: 'Process timed out', - standard: 'bsd', - }, - { - name: 'SIGXFSZ', - number: 25, - action: 'core', - description: 'File too big', - standard: 'bsd', - }, - { - name: 'SIGVTALRM', - number: 26, - action: 'terminate', - description: 'Timeout or timer', - standard: 'bsd', - }, - { - name: 'SIGPROF', - number: 27, - action: 'terminate', - description: 'Timeout or timer', - standard: 'bsd', - }, - { - name: 'SIGWINCH', - number: 28, - action: 'ignore', - description: 'Terminal window size changed', - standard: 'bsd', - }, - { - name: 'SIGIO', - number: 29, - action: 'terminate', - description: 'I/O is available', - standard: 'other', - }, - { - name: 'SIGPOLL', - number: 29, - action: 'terminate', - description: 'Watched event', - standard: 'other', - }, - { - name: 'SIGINFO', - number: 29, - action: 'ignore', - description: 'Request for process information', - standard: 'other', - }, - { - name: 'SIGPWR', - number: 30, - action: 'terminate', - description: 'Device running out of power', - standard: 'systemv', - }, - { - name: 'SIGSYS', - number: 31, - action: 'core', - description: 'Invalid system call', - standard: 'other', - }, - { - name: 'SIGUNUSED', - number: 31, - action: 'terminate', - description: 'Invalid system call', - standard: 'other', - }, -] -/* eslint-enable max-lines */ diff --git a/packages/gui/src/main/lib/human-signals/index.js b/packages/gui/src/main/lib/human-signals/index.js deleted file mode 100644 index fb6e64b..0000000 --- a/packages/gui/src/main/lib/human-signals/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import { constants } from 'node:os' - -import { SIGRTMAX } from './realtime.js' -import { getSignals } from './signals.js' - -// Retrieve `signalsByName`, an object mapping signal name to signal properties. -// We make sure the object is sorted by `number`. -const getSignalsByName = () => { - const signals = getSignals() - return Object.fromEntries(signals.map(getSignalByName)) -} - -const getSignalByName = ({ - name, - number, - description, - supported, - action, - forced, - standard, -}) => [name, { name, number, description, supported, action, forced, standard }] - -export const signalsByName = getSignalsByName() - -// Retrieve `signalsByNumber`, an object mapping signal number to signal -// properties. -// We make sure the object is sorted by `number`. -const getSignalsByNumber = () => { - const signals = getSignals() - const length = SIGRTMAX + 1 - const signalsA = Array.from({ length }, (value, number) => - getSignalByNumber(number, signals), - ) - return Object.assign({}, ...signalsA) -} - -const getSignalByNumber = (number, signals) => { - const signal = findSignalByNumber(number, signals) - - if (signal === undefined) { - return {} - } - - const { name, description, supported, action, forced, standard } = signal - return { - [number]: { - name, - number, - description, - supported, - action, - forced, - standard, - }, - } -} - -// Several signals might end up sharing the same number because of OS-specific -// numbers, in which case those prevail. -const findSignalByNumber = (number, signals) => { - const signal = signals.find(({ name }) => constants.signals[name] === number) - - if (signal !== undefined) { - return signal - } - - return signals.find((signalA) => signalA.number === number) -} - -export const signalsByNumber = getSignalsByNumber() diff --git a/packages/gui/src/main/lib/human-signals/index.ts b/packages/gui/src/main/lib/human-signals/index.ts deleted file mode 100644 index 864d501..0000000 --- a/packages/gui/src/main/lib/human-signals/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -/** - * What is the default action for this signal when it is not handled. - */ -export type SignalAction = 'terminate' | 'core' | 'ignore' | 'pause' | 'unpause' - -/** - * Which standard defined that signal. - */ -export type SignalStandard = 'ansi' | 'posix' | 'bsd' | 'systemv' | 'other' - -/** - * Standard name of the signal, for example 'SIGINT'. - */ -export type SignalName = `SIG${string}` - -/** - * Code number of the signal, for example 2. - * While most number are cross-platform, some are different between different - * OS. - */ -export type SignalNumber = number - -export interface Signal { - /** - * Standard name of the signal, for example 'SIGINT'. - */ - name: SignalName - - /** - * Code number of the signal, for example 2. - * While most number are cross-platform, some are different between different - * OS. - */ - number: SignalNumber - - /** - * Human-friendly description for the signal, for example - * 'User interruption with CTRL-C'. - */ - description: string - - /** - * Whether the current OS can handle this signal in Node.js using - * `process.on(name, handler)`. The list of supported signals is OS-specific. - */ - supported: boolean - - /** - * What is the default action for this signal when it is not handled. - */ - action: SignalAction - - /** - * Whether the signal's default action cannot be prevented. - * This is true for SIGTERM, SIGKILL and SIGSTOP. - */ - forced: boolean - - /** - * Which standard defined that signal. - */ - standard: SignalStandard -} - -/** - * Object whose keys are signal names and values are signal objects. - */ -export declare const signalsByName: { [signalName: SignalName]: Signal } - -/** - * Object whose keys are signal numbers and values are signal objects. - */ -export declare const signalsByNumber: { [signalNumber: SignalNumber]: Signal } diff --git a/packages/gui/src/main/lib/human-signals/realtime.js b/packages/gui/src/main/lib/human-signals/realtime.js deleted file mode 100644 index 1825d08..0000000 --- a/packages/gui/src/main/lib/human-signals/realtime.js +++ /dev/null @@ -1,16 +0,0 @@ -// List of realtime signals with information about them -export const getRealtimeSignals = () => { - const length = SIGRTMAX - SIGRTMIN + 1 - return Array.from({ length }, getRealtimeSignal) -} - -const getRealtimeSignal = (value, index) => ({ - name: `SIGRT${index + 1}`, - number: SIGRTMIN + index, - action: 'terminate', - description: 'Application-specific signal (realtime)', - standard: 'posix', -}) - -const SIGRTMIN = 34 -export const SIGRTMAX = 64 diff --git a/packages/gui/src/main/lib/human-signals/signals.js b/packages/gui/src/main/lib/human-signals/signals.js deleted file mode 100644 index d76382b..0000000 --- a/packages/gui/src/main/lib/human-signals/signals.js +++ /dev/null @@ -1,34 +0,0 @@ -import { constants } from 'node:os' - -import { SIGNALS } from './core.js' -import { getRealtimeSignals } from './realtime.js' - -// Retrieve list of know signals (including realtime) with information about -// them -export const getSignals = () => { - const realtimeSignals = getRealtimeSignals() - const signals = [...SIGNALS, ...realtimeSignals].map(normalizeSignal) - return signals -} - -// Normalize signal: -// - `number`: signal numbers are OS-specific. This is taken into account by -// `os.constants.signals`. However we provide a default `number` since some -// signals are not defined for some OS. -// - `forced`: set default to `false` -// - `supported`: set value -const normalizeSignal = ({ - name, - number: defaultNumber, - description, - action, - forced = false, - standard, -}) => { - const { - signals: { [name]: constantSignal }, - } = constants - const supported = constantSignal !== undefined - const number = supported ? constantSignal : defaultNumber - return { name, number, description, supported, action, forced, standard } -} diff --git a/packages/gui/src/main/lib/is-stream/index.d.ts b/packages/gui/src/main/lib/is-stream/index.d.ts deleted file mode 100644 index df994e0..0000000 --- a/packages/gui/src/main/lib/is-stream/index.d.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - Stream, - Writable as WritableStream, - Readable as ReadableStream, - Duplex as DuplexStream, - Transform as TransformStream, -} from 'node:stream'; - -/** -@returns Whether `stream` is a [`Stream`](https://nodejs.org/api/stream.html#stream_stream). - -@example -``` -import fs from 'node:fs'; -import {isStream} from 'is-stream'; - -isStream(fs.createReadStream('unicorn.png')); -//=> true - -isStream({}); -//=> false -``` -*/ -export function isStream(stream: unknown): stream is Stream; - -/** -@returns Whether `stream` is a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable). - -@example -``` -import fs from 'node:fs'; -import {isWritableStream} from 'is-stream'; - -isWritableStream(fs.createWriteStrem('unicorn.txt')); -//=> true -``` -*/ -export function isWritableStream(stream: unknown): stream is WritableStream; - -/** -@returns Whether `stream` is a [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable). - -@example -``` -import fs from 'node:fs'; -import {isReadableStream} from 'is-stream'; - -isReadableStream(fs.createReadStream('unicorn.png')); -//=> true -``` -*/ -export function isReadableStream(stream: unknown): stream is ReadableStream; - -/** -@returns Whether `stream` is a [`stream.Duplex`](https://nodejs.org/api/stream.html#stream_class_stream_duplex). - -@example -``` -import {Duplex as DuplexStream} from 'node:stream'; -import {isDuplexStream} from 'is-stream'; - -isDuplexStream(new DuplexStream()); -//=> true -``` -*/ -export function isDuplexStream(stream: unknown): stream is DuplexStream; - -/** -@returns Whether `stream` is a [`stream.Transform`](https://nodejs.org/api/stream.html#stream_class_stream_transform). - -@example -``` -import fs from 'node:fs'; -import StringifyStream from 'streaming-json-stringify'; -import {isTransformStream} from 'is-stream'; - -isTransformStream(StringifyStream()); -//=> true -``` -*/ -export function isTransformStream(stream: unknown): stream is TransformStream; diff --git a/packages/gui/src/main/lib/is-stream/index.js b/packages/gui/src/main/lib/is-stream/index.js deleted file mode 100644 index 887e601..0000000 --- a/packages/gui/src/main/lib/is-stream/index.js +++ /dev/null @@ -1,29 +0,0 @@ -export function isStream(stream) { - return stream !== null - && typeof stream === 'object' - && typeof stream.pipe === 'function'; -} - -export function isWritableStream(stream) { - return isStream(stream) - && stream.writable !== false - && typeof stream._write === 'function' - && typeof stream._writableState === 'object'; -} - -export function isReadableStream(stream) { - return isStream(stream) - && stream.readable !== false - && typeof stream._read === 'function' - && typeof stream._readableState === 'object'; -} - -export function isDuplexStream(stream) { - return isWritableStream(stream) - && isReadableStream(stream); -} - -export function isTransformStream(stream) { - return isDuplexStream(stream) - && typeof stream._transform === 'function'; -} diff --git a/packages/gui/src/main/lib/lowdb/adapters/Memory.ts b/packages/gui/src/main/lib/lowdb/adapters/Memory.ts deleted file mode 100644 index c9de116..0000000 --- a/packages/gui/src/main/lib/lowdb/adapters/Memory.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Adapter, SyncAdapter } from '../core/Low.js' - -export class Memory implements Adapter { - #data: T | null = null - - read(): Promise { - return Promise.resolve(this.#data) - } - - write(obj: T): Promise { - this.#data = obj - return Promise.resolve() - } -} - -export class MemorySync implements SyncAdapter { - #data: T | null = null - - read(): T | null { - return this.#data || null - } - - write(obj: T): void { - this.#data = obj - } -} diff --git a/packages/gui/src/main/lib/lowdb/adapters/node/DataFile.ts b/packages/gui/src/main/lib/lowdb/adapters/node/DataFile.ts deleted file mode 100644 index 8f1bfe9..0000000 --- a/packages/gui/src/main/lib/lowdb/adapters/node/DataFile.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { PathLike } from 'fs' - -import { Adapter, SyncAdapter } from '../../core/Low.js' -import { TextFile, TextFileSync } from './TextFile.js' - -export class DataFile implements Adapter { - #adapter: TextFile - #parse: (str: string) => T - #stringify: (data: T) => string - - constructor( - filename: PathLike, - { - parse, - stringify, - }: { - parse: (str: string) => T - stringify: (data: T) => string - }, - ) { - this.#adapter = new TextFile(filename) - this.#parse = parse - this.#stringify = stringify - } - - async read(): Promise { - const data = await this.#adapter.read() - if (data === null) { - return null - } else { - return this.#parse(data) - } - } - - write(obj: T): Promise { - return this.#adapter.write(this.#stringify(obj)) - } -} - -export class DataFileSync implements SyncAdapter { - #adapter: TextFileSync - #parse: (str: string) => T - #stringify: (data: T) => string - - constructor( - filename: PathLike, - { - parse, - stringify, - }: { - parse: (str: string) => T - stringify: (data: T) => string - }, - ) { - this.#adapter = new TextFileSync(filename) - this.#parse = parse - this.#stringify = stringify - } - - read(): T | null { - const data = this.#adapter.read() - if (data === null) { - return null - } else { - return this.#parse(data) - } - } - - write(obj: T): void { - this.#adapter.write(this.#stringify(obj)) - } -} diff --git a/packages/gui/src/main/lib/lowdb/adapters/node/JSONFile.ts b/packages/gui/src/main/lib/lowdb/adapters/node/JSONFile.ts deleted file mode 100644 index 203427f..0000000 --- a/packages/gui/src/main/lib/lowdb/adapters/node/JSONFile.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { PathLike } from 'fs' - -import { DataFile, DataFileSync } from './DataFile.js' - -export class JSONFile extends DataFile { - constructor(filename: PathLike) { - super(filename, { - parse: JSON.parse, - stringify: (data: T) => JSON.stringify(data, null, 2), - }) - } -} - -export class JSONFileSync extends DataFileSync { - constructor(filename: PathLike) { - super(filename, { - parse: JSON.parse, - stringify: (data: T) => JSON.stringify(data, null, 2), - }) - } -} diff --git a/packages/gui/src/main/lib/lowdb/adapters/node/TextFile.ts b/packages/gui/src/main/lib/lowdb/adapters/node/TextFile.ts deleted file mode 100644 index 1b882e4..0000000 --- a/packages/gui/src/main/lib/lowdb/adapters/node/TextFile.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { PathLike, readFileSync, renameSync, writeFileSync } from 'node:fs' -import { readFile } from 'node:fs/promises' -import path from 'node:path' - -import { Writer } from '../../../steno' - -import { Adapter, SyncAdapter } from '../../core/Low.js' - -export class TextFile implements Adapter { - #filename: PathLike - #writer: Writer - - constructor(filename: PathLike) { - this.#filename = filename - this.#writer = new Writer(filename) - } - - async read(): Promise { - let data - - try { - data = await readFile(this.#filename, 'utf-8') - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') { - return null - } - throw e - } - - return data - } - - write(str: string): Promise { - return this.#writer.write(str) - } -} - -export class TextFileSync implements SyncAdapter { - #tempFilename: PathLike - #filename: PathLike - - constructor(filename: PathLike) { - this.#filename = filename - const f = filename.toString() - this.#tempFilename = path.join(path.dirname(f), `.${path.basename(f)}.tmp`) - } - - read(): string | null { - let data - - try { - data = readFileSync(this.#filename, 'utf-8') - } catch (e) { - if ((e as NodeJS.ErrnoException).code === 'ENOENT') { - return null - } - throw e - } - - return data - } - - write(str: string): void { - writeFileSync(this.#tempFilename, str) - renameSync(this.#tempFilename, this.#filename) - } -} diff --git a/packages/gui/src/main/lib/lowdb/core/Low.ts b/packages/gui/src/main/lib/lowdb/core/Low.ts deleted file mode 100644 index e58a5ec..0000000 --- a/packages/gui/src/main/lib/lowdb/core/Low.ts +++ /dev/null @@ -1,64 +0,0 @@ -export interface Adapter { - read: () => Promise - write: (data: T) => Promise -} - -export interface SyncAdapter { - read: () => T | null - write: (data: T) => void -} - -function checkArgs(adapter: unknown, defaultData: unknown) { - if (adapter === undefined) throw new Error('lowdb: missing adapter') - if (defaultData === undefined) throw new Error('lowdb: missing default data') -} - -export class Low { - adapter: Adapter - data: T - - constructor(adapter: Adapter, defaultData: T) { - checkArgs(adapter, defaultData) - this.adapter = adapter - this.data = defaultData - } - - async read(): Promise { - const data = await this.adapter.read() - if (data) this.data = data - } - - async write(): Promise { - if (this.data) await this.adapter.write(this.data) - } - - async update(fn: (data: T) => unknown): Promise { - fn(this.data) - await this.write() - } -} - -export class LowSync { - adapter: SyncAdapter - data: T - - constructor(adapter: SyncAdapter, defaultData: T) { - checkArgs(adapter, defaultData) - this.adapter = adapter - this.data = defaultData - } - - read(): void { - const data = this.adapter.read() - if (data) this.data = data - } - - write(): void { - if (this.data) this.adapter.write(this.data) - } - - update(fn: (data: T) => unknown): void { - fn(this.data) - this.write() - } -} diff --git a/packages/gui/src/main/lib/lowdb/index.ts b/packages/gui/src/main/lib/lowdb/index.ts deleted file mode 100644 index 544f279..0000000 --- a/packages/gui/src/main/lib/lowdb/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './adapters/node/JSONFile.js' -export * from './adapters/node/TextFile.js' -export * from './presets/node.js' diff --git a/packages/gui/src/main/lib/lowdb/presets/node.ts b/packages/gui/src/main/lib/lowdb/presets/node.ts deleted file mode 100644 index f48982c..0000000 --- a/packages/gui/src/main/lib/lowdb/presets/node.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { PathLike } from 'node:fs' - -import { Memory, MemorySync } from '../adapters/Memory.js' -import { JSONFile, JSONFileSync } from '../adapters/node/JSONFile.js' -import { Low, LowSync } from '../core/Low.js' - -export async function JSONFilePreset( - filename: PathLike, - defaultData: Data, -): Promise> { - const adapter = - process.env.NODE_ENV === 'test' - ? new Memory() - : new JSONFile(filename) - const db = new Low(adapter, defaultData) - await db.read() - return db -} - -export function JSONFileSyncPreset( - filename: PathLike, - defaultData: Data, -): LowSync { - const adapter = - process.env.NODE_ENV === 'test' - ? new MemorySync() - : new JSONFileSync(filename) - const db = new LowSync(adapter, defaultData) - db.read() - return db -} diff --git a/packages/gui/src/main/lib/mcl/authenticator.js b/packages/gui/src/main/lib/mcl/authenticator.js deleted file mode 100644 index c5973d8..0000000 --- a/packages/gui/src/main/lib/mcl/authenticator.js +++ /dev/null @@ -1,167 +0,0 @@ -const request = require('request') -const { v3 } = require('uuid') - -let uuid -let api_url = 'https://authserver.mojang.com' - -function parsePropts(array) { - if (array) { - const newObj = {} - for (const entry of array) { - if (newObj[entry.name]) { - newObj[entry.name].push(entry.value) - } else { - newObj[entry.name] = [entry.value] - } - } - return JSON.stringify(newObj) - } else { - return '{}' - } -} - -function getUUID(value) { - if (!uuid) { - uuid = v3(value, v3.DNS) - } - return uuid -} - -const Authenticator = { - getAuth: (username, password, client_token = null) => { - return new Promise((resolve, reject) => { - getUUID(username) - if (!password) { - const user = { - access_token: uuid, - client_token: client_token || uuid, - uuid, - name: username, - user_properties: '{}' - } - - return resolve(user) - } - - const requestObject = { - url: api_url + '/authenticate', - json: { - agent: { - name: 'Minecraft', - version: 1 - }, - username, - password, - clientToken: uuid, - requestUser: true - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - if (!body || !body.selectedProfile) { - return reject(new Error('Validation error: ' + response.statusMessage)) - } - - const userProfile = { - access_token: body.accessToken, - client_token: body.clientToken, - uuid: body.selectedProfile.id, - name: body.selectedProfile.name, - selected_profile: body.selectedProfile, - user_properties: parsePropts(body.user.properties) - } - - resolve(userProfile) - }) - }) - }, - validate: (accessToken, clientToken) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/validate', - json: { - accessToken, - clientToken - } - } - - request.post(requestObject, async function (error, response, body) { - if (error) return reject(error) - - if (!body) resolve(true) - else reject(body) - }) - }) - }, - refreshAuth: (accessToken, clientToken) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/refresh', - json: { - accessToken, - clientToken, - requestUser: true - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - if (!body || !body.selectedProfile) { - return reject(new Error('Validation error: ' + response.statusMessage)) - } - - const userProfile = { - access_token: body.accessToken, - client_token: getUUID(body.selectedProfile.name), - uuid: body.selectedProfile.id, - name: body.selectedProfile.name, - user_properties: parsePropts(body.user.properties) - } - - return resolve(userProfile) - }) - }) - }, - invalidate: (accessToken, clientToken) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/invalidate', - json: { - accessToken, - clientToken - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - - if (!body) return resolve(true) - else return reject(body) - }) - }) - }, - signOut: (username, password) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/signout', - json: { - username, - password - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - - if (!body) return resolve(true) - else return reject(body) - }) - }) - }, - changeApiUrl: (url) => { - api_url = url - } -} - -export default Authenticator \ No newline at end of file diff --git a/packages/gui/src/main/lib/mcl/handler.js b/packages/gui/src/main/lib/mcl/handler.js deleted file mode 100644 index ca0a477..0000000 --- a/packages/gui/src/main/lib/mcl/handler.js +++ /dev/null @@ -1,783 +0,0 @@ -const fs = require('fs') -const path = require('path') -const request = require('request') -const checksum = require('checksum') -const Zip = require('adm-zip') -const child = require('child_process') -let counter = 0 - -export default class Handler { - constructor (client) { - this.client = client - this.options = client.options - this.baseRequest = request.defaults({ - pool: { maxSockets: this.options.overrides.maxSockets || 2 }, - timeout: this.options.timeout || 10000 - }) - } - - checkJava (java) { - return new Promise(resolve => { - child.exec(`"${java}" -version`, (error, stdout, stderr) => { - if (error) { - resolve({ - run: false, - message: error - }) - } else { - this.client.emit('debug', `[MCLC]: Using Java version ${stderr.match(/"(.*?)"/).pop()} ${stderr.includes('64-Bit') ? '64-bit' : '32-Bit'}`) - resolve({ - run: true - }) - } - }) - }) - } - - downloadAsync (url, directory, name, retry, type) { - return new Promise(resolve => { - fs.mkdirSync(directory, { recursive: true }) - - const _request = this.baseRequest(url) - - let receivedBytes = 0 - let totalBytes = 0 - - _request.on('response', (data) => { - if (data.statusCode === 404) { - this.client.emit('debug', `[MCLC]: Failed to download ${url} due to: File not found...`) - return resolve(false) - } - - totalBytes = parseInt(data.headers['content-length']) - }) - - _request.on('error', async (error) => { - this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${error}.` + - ` Retrying... ${retry}`) - if (retry) await this.downloadAsync(url, directory, name, false, type) - resolve() - }) - - _request.on('data', (data) => { - receivedBytes += data.length - this.client.emit('download-status', { - name: name, - type: type, - current: receivedBytes, - total: totalBytes - }) - }) - - const file = fs.createWriteStream(path.join(directory, name)) - _request.pipe(file) - - file.once('finish', () => { - this.client.emit('download', name) - resolve({ - failed: false, - asset: null - }) - }) - - file.on('error', async (e) => { - this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${e}.` + - ` Retrying... ${retry}`) - if (fs.existsSync(path.join(directory, name))) fs.unlinkSync(path.join(directory, name)) - if (retry) await this.downloadAsync(url, directory, name, false, type) - resolve() - }) - }) - } - - checkSum (hash, file) { - return new Promise((resolve, reject) => { - checksum.file(file, (err, sum) => { - if (err) { - this.client.emit('debug', `[MCLC]: Failed to check file hash due to ${err}`) - resolve(false) - } else { - resolve(hash === sum) - } - }) - }) - } - - getVersion () { - return new Promise(resolve => { - const versionJsonPath = this.options.overrides.versionJson || path.join(this.options.directory, `${this.options.version.number}.json`) - - if (fs.existsSync(versionJsonPath)) { - this.version = JSON.parse(fs.readFileSync(versionJsonPath)) - - return resolve(this.version) - } - - const manifest = `${this.options.overrides.url.meta}/mc/game/version_manifest.json` - - const cache = this.options.cache ? `${this.options.cache}/json` : `${this.options.root}/cache/json` - - request.get(manifest, (error, response, body) => { - if (error && error.code !== 'ENOTFOUND') { - return resolve(error) - } - - if (!error) { - if (!fs.existsSync(cache)) { - fs.mkdirSync(cache, { recursive: true }) - - this.client.emit('debug', '[MCLC]: Cache directory created.') - } - - fs.writeFile(path.join(`${cache}/version_manifest.json`), body, (err) => { - if (err) { - return resolve(err) - } - - this.client.emit('debug', '[MCLC]: Cached version_manifest.json (from request)') - }) - } - - let parsed = null - - if (error && (error.code === 'ENOTFOUND')) { - parsed = JSON.parse(fs.readFileSync(`${cache}/version_manifest.json`)) - } else { - parsed = JSON.parse(body) - } - - const versionManifest = parsed.versions.find((version) => { - return version.id === this.options.version.number - }) - - if (!versionManifest) { - return resolve(new Error(`Version not found`)) - } - - request.get(versionManifest.url, (error, response, body) => { - if (error && error.code !== 'ENOTFOUND') { - return resolve(error) - } - - if (!error) { - fs.writeFile(path.join(`${cache}/${this.options.version.number}.json`), body, (err) => { - if (err) { - return resolve(err) - } - - this.client.emit('debug', `[MCLC]: Cached ${this.options.version.number}.json`) - }) - } - - this.client.emit('debug', '[MCLC]: Parsed version from version manifest') - - if (error && (error.code === 'ENOTFOUND')) { - this.version = JSON.parse(fs.readFileSync(`${cache}/${this.options.version.number}.json`)) - } else { - this.version = JSON.parse(body) - } - - this.client.emit('debug', this.version) - - return resolve(this.version) - }) - }) - }) - } - - async getJar () { - await this.downloadAsync(this.version.downloads.client.url, this.options.directory, `${this.options.version.custom ? this.options.version.custom : this.options.version.number}.jar`, true, 'version-jar') - fs.writeFileSync(path.join(this.options.directory, `${this.options.version.number}.json`), JSON.stringify(this.version, null, 4)) - return this.client.emit('debug', '[MCLC]: Downloaded version jar and wrote version json') - } - - async getAssets () { - const assetDirectory = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) - const assetId = this.options.version.custom || this.options.version.number - if (!fs.existsSync(path.join(assetDirectory, 'indexes', `${assetId}.json`))) { - await this.downloadAsync(this.version.assetIndex.url, path.join(assetDirectory, 'indexes'), - `${assetId}.json`, true, 'asset-json') - } - - const index = JSON.parse(fs.readFileSync(path.join(assetDirectory, 'indexes', `${assetId}.json`), { encoding: 'utf8' })) - - this.client.emit('progress', { - type: 'assets', - task: 0, - total: Object.keys(index.objects).length - }) - - await Promise.all(Object.keys(index.objects).map(async asset => { - const hash = index.objects[asset].hash - const subhash = hash.substring(0, 2) - const subAsset = path.join(assetDirectory, 'objects', subhash) - - if (!fs.existsSync(path.join(subAsset, hash)) || !await this.checkSum(hash, path.join(subAsset, hash))) { - await this.downloadAsync(`${this.options.overrides.url.resource}/${subhash}/${hash}`, subAsset, hash, - true, 'assets') - } - counter++ - this.client.emit('progress', { - type: 'assets', - task: counter, - total: Object.keys(index.objects).length - }) - })) - counter = 0 - - // Copy assets to legacy if it's an older Minecraft version. - if (this.isLegacy()) { - if (fs.existsSync(path.join(assetDirectory, 'legacy'))) { - this.client.emit('debug', '[MCLC]: The \'legacy\' directory is no longer used as Minecraft looks ' + - 'for the resouces folder regardless of what is passed in the assetDirecotry launch option. I\'d ' + - `recommend removing the directory (${path.join(assetDirectory, 'legacy')})`) - } - - const legacyDirectory = path.join(this.options.root, 'resources') - this.client.emit('debug', `[MCLC]: Copying assets over to ${legacyDirectory}`) - - this.client.emit('progress', { - type: 'assets-copy', - task: 0, - total: Object.keys(index.objects).length - }) - - await Promise.all(Object.keys(index.objects).map(async asset => { - const hash = index.objects[asset].hash - const subhash = hash.substring(0, 2) - const subAsset = path.join(assetDirectory, 'objects', subhash) - - const legacyAsset = asset.split('/') - legacyAsset.pop() - - if (!fs.existsSync(path.join(legacyDirectory, legacyAsset.join('/')))) { - fs.mkdirSync(path.join(legacyDirectory, legacyAsset.join('/')), { recursive: true }) - } - - if (!fs.existsSync(path.join(legacyDirectory, asset))) { - fs.copyFileSync(path.join(subAsset, hash), path.join(legacyDirectory, asset)) - } - counter++ - this.client.emit('progress', { - type: 'assets-copy', - task: counter, - total: Object.keys(index.objects).length - }) - })) - } - counter = 0 - - this.client.emit('debug', '[MCLC]: Downloaded assets') - } - - parseRule (lib) { - if (lib.rules) { - if (lib.rules.length > 1) { - if (lib.rules[0].action === 'allow' && - lib.rules[1].action === 'disallow' && - lib.rules[1].os.name === 'osx') { - return this.getOS() === 'osx' - } else { - return true - } - } else { - if (lib.rules[0].action === 'allow' && lib.rules[0].os) return lib.rules[0].os.name !== this.getOS() - } - } else { - return false - } - } - - async getNatives () { - const nativeDirectory = path.resolve(this.options.overrides.natives || path.join(this.options.root, 'natives', this.version.id)) - - if (parseInt(this.version.id.split('.')[1]) >= 19) return this.options.overrides.cwd || this.options.root - - if (!fs.existsSync(nativeDirectory) || !fs.readdirSync(nativeDirectory).length) { - fs.mkdirSync(nativeDirectory, { recursive: true }) - - const natives = async () => { - const natives = [] - await Promise.all(this.version.libraries.map(async (lib) => { - if (!lib.downloads || !lib.downloads.classifiers) return - if (this.parseRule(lib)) return - - const native = this.getOS() === 'osx' - ? lib.downloads.classifiers['natives-osx'] || lib.downloads.classifiers['natives-macos'] - : lib.downloads.classifiers[`natives-${this.getOS()}`] - - natives.push(native) - })) - return natives - } - const stat = await natives() - - this.client.emit('progress', { - type: 'natives', - task: 0, - total: stat.length - }) - - await Promise.all(stat.map(async (native) => { - if (!native) return - const name = native.path.split('/').pop() - await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') - if (!await this.checkSum(native.sha1, path.join(nativeDirectory, name))) { - await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') - } - try { - new Zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true) - } catch (e) { - // Only doing a console.warn since a stupid error happens. You can basically ignore this. - // if it says Invalid file name, just means two files were downloaded and both were deleted. - // All is well. - console.warn(e) - } - fs.unlinkSync(path.join(nativeDirectory, name)) - counter++ - this.client.emit('progress', { - type: 'natives', - task: counter, - total: stat.length - }) - })) - this.client.emit('debug', '[MCLC]: Downloaded and extracted natives') - } - - counter = 0 - this.client.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`) - - return nativeDirectory - } - - fwAddArgs () { - const forgeWrapperAgrs = [ - `-Dforgewrapper.librariesDir=${path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries'))}`, - `-Dforgewrapper.installer=${this.options.forge}`, - `-Dforgewrapper.minecraft=${this.options.mcPath}` - ] - this.options.customArgs - ? this.options.customArgs = this.options.customArgs.concat(forgeWrapperAgrs) - : this.options.customArgs = forgeWrapperAgrs - } - - isModernForge (json) { - return json.inheritsFrom && json.inheritsFrom.split('.')[1] >= 12 && !(json.inheritsFrom === '1.12.2' && (json.id.split('.')[json.id.split('.').length - 1]) === '2847') - } - - async getForgedWrapped () { - let json = null - let installerJson = null - const versionPath = path.join(this.options.root, 'forge', `${this.version.id}`, 'version.json') - // Since we're building a proper "custom" JSON that will work nativly with MCLC, the version JSON will not - // be re-generated on the next run. - if (fs.existsSync(versionPath)) { - try { - json = JSON.parse(fs.readFileSync(versionPath)) - if (!json.forgeWrapperVersion || !(json.forgeWrapperVersion === this.options.overrides.fw.version)) { - this.client.emit('debug', '[MCLC]: Old ForgeWrapper has generated this version JSON, re-generating') - } else { - // If forge is modern, add ForgeWrappers launch arguments and set forge to null so MCLC treats it as a custom json. - if (this.isModernForge(json)) { - this.fwAddArgs() - this.options.forge = null - } - return json - } - } catch (e) { - console.warn(e) - this.client.emit('debug', '[MCLC]: Failed to parse Forge version JSON, re-generating') - } - } - - this.client.emit('debug', '[MCLC]: Generating a proper version json, this might take a bit') - const zipFile = new Zip(this.options.forge) - json = zipFile.readAsText('version.json') - if (zipFile.getEntry('install_profile.json')) installerJson = zipFile.readAsText('install_profile.json') - - try { - json = JSON.parse(json) - if (installerJson) installerJson = JSON.parse(installerJson) - } catch (e) { - this.client.emit('debug', '[MCLC]: Failed to load json files for ForgeWrapper, using Vanilla instead') - return null - } - // Adding the installer libraries as mavenFiles so MCLC downloads them but doesn't add them to the class paths. - if (installerJson) { - json.mavenFiles - ? json.mavenFiles = json.mavenFiles.concat(installerJson.libraries) - : json.mavenFiles = installerJson.libraries - } - - // Holder for the specifc jar ending which depends on the specifc forge version. - let jarEnding = 'universal' - // We need to handle modern forge differently than legacy. - if (this.isModernForge(json)) { - // If forge is modern and above 1.12.2, we add ForgeWrapper to the libraries so MCLC includes it in the classpaths. - if (json.inheritsFrom !== '1.12.2') { - this.fwAddArgs() - const fwName = `ForgeWrapper-${this.options.overrides.fw.version}.jar` - const fwPathArr = ['io', 'github', 'zekerzhayard', 'ForgeWrapper', this.options.overrides.fw.version] - json.libraries.push({ - name: fwPathArr.join(':'), - downloads: { - artifact: { - path: [...fwPathArr, fwName].join('/'), - url: `${this.options.overrides.fw.baseUrl}${this.options.overrides.fw.version}/${fwName}`, - sha1: this.options.overrides.fw.sh1, - size: this.options.overrides.fw.size - } - } - }) - json.mainClass = 'io.github.zekerzhayard.forgewrapper.installer.Main' - jarEnding = 'launcher' - - // Providing a download URL to the universal jar mavenFile so it can be downloaded properly. - for (const library of json.mavenFiles) { - const lib = library.name.split(':') - if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { - library.downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + library.downloads.artifact.path - break - } - } - } else { - // Remove the forge dependent since we're going to overwrite the first entry anyways. - for (const library in json.mavenFiles) { - const lib = json.mavenFiles[library].name.split(':') - if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { - delete json.mavenFiles[library] - break - } - } - } - } else { - // Modifying legacy library format to play nice with MCLC's downloadToDirectory function. - await Promise.all(json.libraries.map(async library => { - const lib = library.name.split(':') - if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) return - - let url = this.options.overrides.url.mavenForge - const name = `${lib[1]}-${lib[2]}.jar` - - if (!library.url) { - if (library.serverreq || library.clientreq) { - url = this.options.overrides.url.defaultRepoForge - } else { - return - } - } - library.url = url - const downloadLink = `${url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` - // Checking if the file still exists on Forge's server, if not, replace it with the fallback. - // Not checking for sucess, only if it 404s. - this.baseRequest(downloadLink, (error, response, body) => { - if (error) { - this.client.emit('debug', `[MCLC]: Failed checking request for ${downloadLink}`) - } else { - if (response.statusCode === 404) library.url = this.options.overrides.url.fallbackMaven - } - }) - })) - } - // If a downloads property exists, we modify the inital forge entry to include ${jarEnding} so ForgeWrapper can work properly. - // If it doesn't, we simply remove it since we're already providing the universal jar. - if (json.libraries[0].downloads) { - if (json.libraries[0].name.includes('minecraftforge')) { - json.libraries[0].name = json.libraries[0].name + `:${jarEnding}` - json.libraries[0].downloads.artifact.path = json.libraries[0].downloads.artifact.path.replace('.jar', `-${jarEnding}.jar`) - json.libraries[0].downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + json.libraries[0].downloads.artifact.path - } - } else { - delete json.libraries[0] - } - - // Removing duplicates and null types - json.libraries = this.cleanUp(json.libraries) - if (json.mavenFiles) json.mavenFiles = this.cleanUp(json.mavenFiles) - - json.forgeWrapperVersion = this.options.overrides.fw.version - - // Saving file for next run! - if (!fs.existsSync(path.join(this.options.root, 'forge', this.version.id))) { - fs.mkdirSync(path.join(this.options.root, 'forge', this.version.id), { recursive: true }) - } - fs.writeFileSync(versionPath, JSON.stringify(json, null, 4)) - - // Make MCLC treat modern forge as a custom version json rather then legacy forge. - if (this.isModernForge(json)) this.options.forge = null - - return json - } - - runInstaller (path) { - return new Promise(resolve => { - const installer = child.exec(path) - installer.on('close', (code) => resolve(code)) - }) - } - - async downloadToDirectory (directory, libraries, eventName) { - const libs = [] - - await Promise.all(libraries.map(async library => { - if (!library) return - if (this.parseRule(library)) return - const lib = library.name.split(':') - - let jarPath - let name - if (library.downloads && library.downloads.artifact && library.downloads.artifact.path) { - name = library.downloads.artifact.path.split('/')[library.downloads.artifact.path.split('/').length - 1] - jarPath = path.join(directory, this.popString(library.downloads.artifact.path)) - } else { - name = `${lib[1]}-${lib[2]}${lib[3] ? '-' + lib[3] : ''}.jar` - jarPath = path.join(directory, `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`) - } - - const downloadLibrary = async library => { - if (library.url) { - const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` - await this.downloadAsync(url, jarPath, name, true, eventName) - } else if (library.downloads && library.downloads.artifact) { - await this.downloadAsync(library.downloads.artifact.url, jarPath, name, true, eventName) - } - } - - if (!fs.existsSync(path.join(jarPath, name))) downloadLibrary(library) - else if (library.downloads && library.downloads.artifact) { - if (!this.checkSum(library.downloads.artifact.sha1, path.join(jarPath, name))) downloadLibrary(library) - } - - counter++ - this.client.emit('progress', { - type: eventName, - task: counter, - total: libraries.length - }) - libs.push(`${jarPath}${path.sep}${name}`) - })) - counter = 0 - - return libs - } - - async getClasses (classJson) { - let libs = [] - - const libraryDirectory = path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries')) - - if (classJson) { - if (classJson.mavenFiles) { - await this.downloadToDirectory(libraryDirectory, classJson.mavenFiles, 'classes-maven-custom') - } - libs = (await this.downloadToDirectory(libraryDirectory, classJson.libraries, 'classes-custom')) - } - - const parsed = this.version.libraries.map(lib => { - if (lib.downloads && lib.downloads.artifact && !this.parseRule(lib)) return lib - }) - - libs = libs.concat((await this.downloadToDirectory(libraryDirectory, parsed, 'classes'))) - counter = 0 - - // Temp Quilt support - if (classJson) libs.sort() - - this.client.emit('debug', '[MCLC]: Collected class paths') - return libs - } - - popString (path) { - const tempArray = path.split('/') - tempArray.pop() - return tempArray.join('/') - } - - cleanUp (array) { - const newArray = [] - for (const classPath in array) { - if (newArray.includes(array[classPath]) || array[classPath] === null) continue - newArray.push(array[classPath]) - } - return newArray - } - - formatQuickPlay () { - const types = { - singleplayer: '--quickPlaySingleplayer', - multiplayer: '--quickPlayMultiplayer', - realms: '--quickPlayRealms', - legacy: null - } - const { type, identifier, path } = this.options.quickPlay - const keys = Object.keys(types) - if (!keys.includes(type)) { - this.client.emit('debug', `[MCLC]: quickPlay type is not valid. Valid types are: ${keys.join(', ')}`) - return null - } - const returnArgs = type === 'legacy' - ? ['--server', identifier.split(':')[0], '--port', identifier.split(':')[1] || '25565'] - : [types[type], identifier] - if (path) returnArgs.push('--quickPlayPath', path) - return returnArgs - } - - async getLaunchOptions (modification) { - const type = Object.assign({}, this.version, modification) - - let args = type.minecraftArguments - ? type.minecraftArguments.split(' ') - : type.arguments.game - const assetRoot = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) - const assetPath = this.isLegacy() - ? path.join(this.options.root, 'resources') - : path.join(assetRoot) - - const minArgs = this.options.overrides.minArgs || this.isLegacy() ? 5 : 11 - if (args.length < minArgs) args = args.concat(this.version.minecraftArguments ? this.version.minecraftArguments.split(' ') : this.version.arguments.game) - if (this.options.customLaunchArgs) args = args.concat(this.options.customLaunchArgs) - - this.options.authorization = await Promise.resolve(this.options.authorization) - this.options.authorization.meta = this.options.authorization.meta ? this.options.authorization.meta : { type: 'mojang' } - const fields = { - '${auth_access_token}': this.options.authorization.access_token, - '${auth_session}': this.options.authorization.access_token, - '${auth_player_name}': this.options.authorization.name, - '${auth_uuid}': this.options.authorization.uuid, - '${auth_xuid}': this.options.authorization.meta.xuid || this.options.authorization.access_token, - '${user_properties}': this.options.authorization.user_properties, - '${user_type}': this.options.authorization.meta.type, - '${version_name}': this.options.version.number, - '${assets_index_name}': this.options.overrides.assetIndex || this.options.version.custom || this.options.version.number, - '${game_directory}': this.options.overrides.gameDirectory || this.options.root, - '${assets_root}': assetPath, - '${game_assets}': assetPath, - '${version_type}': this.options.version.type, - '${clientid}': this.options.authorization.meta.clientId || (this.options.authorization.client_token || this.options.authorization.access_token), - '${resolution_width}': this.options.window ? this.options.window.width : 856, - '${resolution_height}': this.options.window ? this.options.window.height : 482 - } - - if (this.options.authorization.meta.demo && (this.options.features ? !this.options.features.includes('is_demo_user') : true)) { - args.push('--demo') - } - - const replaceArg = (obj, index) => { - if (Array.isArray(obj.value)) { - for (const arg of obj.value) { - args.push(arg) - } - } else { - args.push(obj.value) - } - delete args[index] - } - - for (let index = 0; index < args.length; index++) { - if (typeof args[index] === 'object') { - if (args[index].rules) { - if (!this.options.features) continue - const featureFlags = [] - for (const rule of args[index].rules) { - featureFlags.push(...Object.keys(rule.features)) - } - let hasAllRules = true - for (const feature of this.options.features) { - if (!featureFlags.includes(feature)) { - hasAllRules = false - } - } - if (hasAllRules) replaceArg(args[index], index) - } else { - replaceArg(args[index], index) - } - } else { - if (Object.keys(fields).includes(args[index])) { - args[index] = fields[args[index]] - } - } - } - if (this.options.window) { - // eslint-disable-next-line no-unused-expressions - this.options.window.fullscreen - ? args.push('--fullscreen') - : () => { - if (this.options.features ? !this.options.features.includes('has_custom_resolution') : true) { - args.push('--width', this.options.window.width, '--height', this.options.window.height) - } - } - } - if (this.options.server) this.client.emit('debug', '[MCLC]: server and port are deprecated launch flags. Use the quickPlay field.') - if (this.options.quickPlay) args = args.concat(this.formatQuickPlay()) - if (this.options.proxy) { - args.push( - '--proxyHost', - this.options.proxy.host, - '--proxyPort', - this.options.proxy.port || '8080', - '--proxyUser', - this.options.proxy.username, - '--proxyPass', - this.options.proxy.password - ) - } - args = args.filter(value => typeof value === 'string' || typeof value === 'number') - this.client.emit('debug', '[MCLC]: Set launch options') - return args - } - - async getJVM () { - const opts = { - windows: '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump', - osx: '-XstartOnFirstThread', - linux: '-Xss1M' - } - return opts[this.getOS()] - } - - isLegacy () { - return this.version.assets === 'legacy' || this.version.assets === 'pre-1.6' - } - - getOS () { - if (this.options.os) { - return this.options.os - } else { - switch (process.platform) { - case 'win32': return 'windows' - case 'darwin': return 'osx' - default: return 'linux' - } - } - } - - // To prevent launchers from breaking when they update. Will be reworked with rewrite. - getMemory () { - if (!this.options.memory) { - this.client.emit('debug', '[MCLC]: Memory not set! Setting 1GB as MAX!') - this.options.memory = { - min: 512, - max: 1023 - } - } - if (!isNaN(this.options.memory.max) && !isNaN(this.options.memory.min)) { - if (this.options.memory.max < this.options.memory.min) { - this.client.emit('debug', '[MCLC]: MIN memory is higher then MAX! Resetting!') - this.options.memory.max = 1023 - this.options.memory.min = 512 - } - return [`${this.options.memory.max}M`, `${this.options.memory.min}M`] - } else { return [`${this.options.memory.max}`, `${this.options.memory.min}`] } - } - - async extractPackage (options = this.options) { - if (options.clientPackage.startsWith('http')) { - await this.downloadAsync(options.clientPackage, options.root, 'clientPackage.zip', true, 'client-package') - options.clientPackage = path.join(options.root, 'clientPackage.zip') - } - new Zip(options.clientPackage).extractAllTo(options.root, true) - if (options.removePackage) fs.unlinkSync(options.clientPackage) - - return this.client.emit('package-extract', true) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/mcl/index.js b/packages/gui/src/main/lib/mcl/index.js deleted file mode 100644 index a280b5a..0000000 --- a/packages/gui/src/main/lib/mcl/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import Client from "./launcher" -import Authenticator from "./authenticator" - -export default class MCL { - /** - * Asynchronously authenticate the user using the provided username and password. - * - * @param {string} username - the username of the user - * @param {string} password - the password of the user - * @return {Promise} the authentication information - */ - async auth(username, password) { - return await Authenticator.getAuth(username, password) - } - - /** - * Launches a new client with the given options. - * - * @param {Object} opts - The options to be passed for launching the client. - * @return {Promise} A promise that resolves with the launched client. - */ - async launch(opts, callbacks) { - const launcher = new Client() - - launcher.on("debug", (e) => console.log(e)) - launcher.on("data", (e) => console.log(e)) - launcher.on("close", (e) => console.log(e)) - launcher.on("error", (e) => console.log(e)) - - await launcher.launch(opts, callbacks) - - return launcher - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/mcl/launcher.js b/packages/gui/src/main/lib/mcl/launcher.js deleted file mode 100644 index 5a5aa76..0000000 --- a/packages/gui/src/main/lib/mcl/launcher.js +++ /dev/null @@ -1,224 +0,0 @@ -import fs from "node:fs" -import path from "node:path" -import { EventEmitter } from "events" -import child from "child_process" - -import Handler from "./handler" - -export default class MCLCore extends EventEmitter { - async launch(options, callbacks = {}) { - try { - this.options = { ...options } - - this.options.root = path.resolve(this.options.root) - - this.options.overrides = { - detached: true, - ...this.options.overrides, - url: { - meta: 'https://launchermeta.mojang.com', - resource: 'https://resources.download.minecraft.net', - mavenForge: 'http://files.minecraftforge.net/maven/', - defaultRepoForge: 'https://libraries.minecraft.net/', - fallbackMaven: 'https://search.maven.org/remotecontent?filepath=', - ...this.options.overrides - ? this.options.overrides.url - : undefined - }, - fw: { - baseUrl: 'https://github.com/ZekerZhayard/ForgeWrapper/releases/download/', - version: '1.5.6', - sh1: 'b38d28e8b7fde13b1bc0db946a2da6760fecf98d', - size: 34715, - ...this.options.overrides - ? this.options.overrides.fw - : undefined - } - } - - this.handler = new Handler(this) - - this.printVersion() - - const java = await this.handler.checkJava(this.options.javaPath || 'java') - - if (!java.run) { - this.emit('debug', `[MCLC]: Couldn't start Minecraft due to: ${java.message}`) - this.emit('close', 1) - return null - } - - this.createRootDirectory() - this.createGameDirectory() - - await this.extractPackage() - - if (this.options.installer) { - // So installers that create a profile in launcher_profiles.json can run without breaking. - const profilePath = path.join(this.options.root, 'launcher_profiles.json') - if (!fs.existsSync(profilePath) || !JSON.parse(fs.readFileSync(profilePath)).profiles) { - fs.writeFileSync(profilePath, JSON.stringify({ profiles: {} }, null, 4)) - } - const code = await this.handler.runInstaller(this.options.installer) - if (!this.options.version.custom && code === 0) { - this.emit('debug', '[MCLC]: Installer successfully ran, but no custom version was provided') - } - this.emit('debug', `[MCLC]: Installer closed with code ${code}`) - } - - const directory = this.options.overrides.directory || path.join(this.options.root, 'versions', this.options.version.custom ? this.options.version.custom : this.options.version.number) - this.options.directory = directory - - const versionFile = await this.handler.getVersion() - - const mcPath = this.options.overrides.minecraftJar || (this.options.version.custom - ? path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.jar`) - : path.join(directory, `${this.options.version.number}.jar`)) - - this.options.mcPath = mcPath - - const nativePath = await this.handler.getNatives() - - if (!fs.existsSync(mcPath)) { - this.emit('debug', '[MCLC]: Attempting to download Minecraft version jar') - - if (typeof callbacks.install === "function") { - callbacks.install() - } - - await this.handler.getJar() - } - - const modifyJson = await this.getModifyJson() - - const args = [] - - let jvm = [ - '-XX:-UseAdaptiveSizePolicy', - '-XX:-OmitStackTraceInFastThrow', - '-Dfml.ignorePatchDiscrepancies=true', - '-Dfml.ignoreInvalidMinecraftCertificates=true', - `-Djava.library.path=${nativePath}`, - `-Xmx${this.handler.getMemory()[0]}`, - `-Xms${this.handler.getMemory()[1]}` - ] - if (this.handler.getOS() === 'osx') { - if (parseInt(versionFile.id.split('.')[1]) > 12) jvm.push(await this.handler.getJVM()) - } else jvm.push(await this.handler.getJVM()) - - if (this.options.customArgs) jvm = jvm.concat(this.options.customArgs) - if (this.options.overrides.logj4ConfigurationFile) { - jvm.push(`-Dlog4j.configurationFile=${path.resolve(this.options.overrides.logj4ConfigurationFile)}`) - } - // https://help.minecraft.net/hc/en-us/articles/4416199399693-Security-Vulnerability-in-Minecraft-Java-Edition - if (parseInt(versionFile.id.split('.')[1]) === 18 && !parseInt(versionFile.id.split('.')[2])) jvm.push('-Dlog4j2.formatMsgNoLookups=true') - if (parseInt(versionFile.id.split('.')[1]) === 17) jvm.push('-Dlog4j2.formatMsgNoLookups=true') - if (parseInt(versionFile.id.split('.')[1]) < 17) { - if (!jvm.find(arg => arg.includes('Dlog4j.configurationFile'))) { - const configPath = path.resolve(this.options.overrides.cwd || this.options.root) - const intVersion = parseInt(versionFile.id.split('.')[1]) - if (intVersion >= 12) { - await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml', - configPath, 'log4j2_112-116.xml', true, 'log4j') - jvm.push('-Dlog4j.configurationFile=log4j2_112-116.xml') - } else if (intVersion >= 7) { - await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/dd2b723346a8dcd48e7f4d245f6bf09e98db9696/log4j2_17-111.xml', - configPath, 'log4j2_17-111.xml', true, 'log4j') - jvm.push('-Dlog4j.configurationFile=log4j2_17-111.xml') - } - } - } - - const classes = this.options.overrides.classes || this.handler.cleanUp(await this.handler.getClasses(modifyJson)) - const classPaths = ['-cp'] - const separator = this.handler.getOS() === 'windows' ? ';' : ':' - - this.emit('debug', `[MCLC]: Using ${separator} to separate class paths`) - - // Handling launch arguments. - const file = modifyJson || versionFile - - // So mods like fabric work. - const jar = fs.existsSync(mcPath) - ? `${separator}${mcPath}` - : `${separator}${path.join(directory, `${this.options.version.number}.jar`)}` - classPaths.push(`${this.options.forge ? this.options.forge + separator : ''}${classes.join(separator)}${jar}`) - classPaths.push(file.mainClass) - - this.emit('debug', '[MCLC]: Attempting to download assets') - - if (typeof callbacks.init_assets === "function") { - callbacks.init_assets() - } - - await this.handler.getAssets() - - // Forge -> Custom -> Vanilla - const launchOptions = await this.handler.getLaunchOptions(modifyJson) - - const launchArguments = args.concat(jvm, classPaths, launchOptions) - this.emit('arguments', launchArguments) - this.emit('debug', `[MCLC]: Launching with arguments ${launchArguments.join(' ')}`) - - return this.startMinecraft(launchArguments) - } catch (e) { - this.emit('debug', `[MCLC]: Failed to start due to ${e}, closing...`) - return null - } - } - - printVersion() { - if (fs.existsSync(path.join(__dirname, '..', 'package.json'))) { - const { version } = require('../package.json') - this.emit('debug', `[MCLC]: MCLC version ${version}`) - } else { this.emit('debug', '[MCLC]: Package JSON not found, skipping MCLC version check.') } - } - - createRootDirectory() { - if (!fs.existsSync(this.options.root)) { - this.emit('debug', '[MCLC]: Attempting to create root folder') - fs.mkdirSync(this.options.root) - } - } - - createGameDirectory() { - if (this.options.overrides.gameDirectory) { - this.options.overrides.gameDirectory = path.resolve(this.options.overrides.gameDirectory) - if (!fs.existsSync(this.options.overrides.gameDirectory)) { - fs.mkdirSync(this.options.overrides.gameDirectory, { recursive: true }) - } - } - } - - async extractPackage() { - if (this.options.clientPackage) { - this.emit('debug', `[MCLC]: Extracting client package to ${this.options.root}`) - await this.handler.extractPackage() - } - } - - async getModifyJson() { - let modifyJson = null - - if (this.options.forge) { - this.options.forge = path.resolve(this.options.forge) - this.emit('debug', '[MCLC]: Detected Forge in options, getting dependencies') - modifyJson = await this.handler.getForgedWrapped() - } else if (this.options.version.custom) { - this.emit('debug', '[MCLC]: Detected custom in options, setting custom version file') - modifyJson = modifyJson || JSON.parse(fs.readFileSync(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`), { encoding: 'utf8' })) - } - - return modifyJson - } - - startMinecraft(launchArguments) { - const minecraft = child.spawn(this.options.javaPath ? this.options.javaPath : 'java', launchArguments, - { cwd: this.options.overrides.cwd || this.options.root, detached: this.options.overrides.detached }) - - minecraft.stdout.on('data', (data) => this.emit('data', data.toString('utf-8'))) - minecraft.stderr.on('data', (data) => this.emit('data', data.toString('utf-8'))) - minecraft.on('close', (code) => this.emit('close', code)) - return minecraft - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/mimic-function/index.js b/packages/gui/src/main/lib/mimic-function/index.js deleted file mode 100644 index 61e6701..0000000 --- a/packages/gui/src/main/lib/mimic-function/index.js +++ /dev/null @@ -1,71 +0,0 @@ -const copyProperty = (to, from, property, ignoreNonConfigurable) => { - // `Function#length` should reflect the parameters of `to` not `from` since we keep its body. - // `Function#prototype` is non-writable and non-configurable so can never be modified. - if (property === 'length' || property === 'prototype') { - return; - } - - // `Function#arguments` and `Function#caller` should not be copied. They were reported to be present in `Reflect.ownKeys` for some devices in React Native (#41), so we explicitly ignore them here. - if (property === 'arguments' || property === 'caller') { - return; - } - - const toDescriptor = Object.getOwnPropertyDescriptor(to, property); - const fromDescriptor = Object.getOwnPropertyDescriptor(from, property); - - if (!canCopyProperty(toDescriptor, fromDescriptor) && ignoreNonConfigurable) { - return; - } - - Object.defineProperty(to, property, fromDescriptor); -}; - -// `Object.defineProperty()` throws if the property exists, is not configurable and either: -// - one its descriptors is changed -// - it is non-writable and its value is changed -const canCopyProperty = function (toDescriptor, fromDescriptor) { - return toDescriptor === undefined || toDescriptor.configurable || ( - toDescriptor.writable === fromDescriptor.writable - && toDescriptor.enumerable === fromDescriptor.enumerable - && toDescriptor.configurable === fromDescriptor.configurable - && (toDescriptor.writable || toDescriptor.value === fromDescriptor.value) - ); -}; - -const changePrototype = (to, from) => { - const fromPrototype = Object.getPrototypeOf(from); - if (fromPrototype === Object.getPrototypeOf(to)) { - return; - } - - Object.setPrototypeOf(to, fromPrototype); -}; - -const wrappedToString = (withName, fromBody) => `/* Wrapped ${withName}*/\n${fromBody}`; - -const toStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, 'toString'); -const toStringName = Object.getOwnPropertyDescriptor(Function.prototype.toString, 'name'); - -// We call `from.toString()` early (not lazily) to ensure `from` can be garbage collected. -// We use `bind()` instead of a closure for the same reason. -// Calling `from.toString()` early also allows caching it in case `to.toString()` is called several times. -const changeToString = (to, from, name) => { - const withName = name === '' ? '' : `with ${name.trim()}() `; - const newToString = wrappedToString.bind(null, withName, from.toString()); - // Ensure `to.toString.toString` is non-enumerable and has the same `same` - Object.defineProperty(newToString, 'name', toStringName); - Object.defineProperty(to, 'toString', { ...toStringDescriptor, value: newToString }); -}; - -export default function mimicFunction(to, from, { ignoreNonConfigurable = false } = {}) { - const { name } = to; - - for (const property of Reflect.ownKeys(from)) { - copyProperty(to, from, property, ignoreNonConfigurable); - } - - changePrototype(to, from); - changeToString(to, from, name); - - return to; -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/npm-run-path/index.d.ts b/packages/gui/src/main/lib/npm-run-path/index.d.ts deleted file mode 100644 index 0c1b160..0000000 --- a/packages/gui/src/main/lib/npm-run-path/index.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -export interface RunPathOptions { - /** - Working directory. - - @default process.cwd() - */ - readonly cwd?: string | URL; - - /** - PATH to be appended. Default: [`PATH`](https://github.com/sindresorhus/path-key). - - Set it to an empty string to exclude the default PATH. - */ - readonly path?: string; - - /** - Path to the Node.js executable to use in child processes if that is different from the current one. Its directory is pushed to the front of PATH. - - This can be either an absolute path or a path relative to the `cwd` option. - - @default process.execPath - */ - readonly execPath?: string | URL; -} - -export type ProcessEnv = Record; - -export interface EnvOptions { - /** - The working directory. - - @default process.cwd() - */ - readonly cwd?: string | URL; - - /** - Accepts an object of environment variables, like `process.env`, and modifies the PATH using the correct [PATH key](https://github.com/sindresorhus/path-key). Use this if you're modifying the PATH for use in the `child_process` options. - */ - readonly env?: ProcessEnv; - - /** - The path to the current Node.js executable. Its directory is pushed to the front of PATH. - - This can be either an absolute path or a path relative to the `cwd` option. - - @default process.execPath - */ - readonly execPath?: string | URL; -} - -/** -Get your [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) prepended with locally installed binaries. - -@returns The augmented path string. - -@example -``` -import childProcess from 'node:child_process'; -import {npmRunPath} from 'npm-run-path'; - -console.log(process.env.PATH); -//=> '/usr/local/bin' - -console.log(npmRunPath()); -//=> '/Users/sindresorhus/dev/foo/node_modules/.bin:/Users/sindresorhus/dev/node_modules/.bin:/Users/sindresorhus/node_modules/.bin:/Users/node_modules/.bin:/node_modules/.bin:/usr/local/bin' -``` -*/ -export function npmRunPath(options?: RunPathOptions): string; - -/** -@returns The augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object. - -@example -``` -import childProcess from 'node:child_process'; -import {npmRunPathEnv} from 'npm-run-path'; - -// `foo` is a locally installed binary -childProcess.execFileSync('foo', { - env: npmRunPathEnv() -}); -``` -*/ -export function npmRunPathEnv(options?: EnvOptions): ProcessEnv; diff --git a/packages/gui/src/main/lib/npm-run-path/index.js b/packages/gui/src/main/lib/npm-run-path/index.js deleted file mode 100644 index 782a96a..0000000 --- a/packages/gui/src/main/lib/npm-run-path/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import process from 'node:process'; -import path from 'node:path'; -import url from 'node:url'; - -function pathKey(options = {}) { - const { - env = process.env, - platform = process.platform - } = options; - - if (platform !== 'win32') { - return 'PATH'; - } - - return Object.keys(env).reverse().find(key => key.toUpperCase() === 'PATH') || 'Path'; -} - -export function npmRunPath(options = {}) { - const { - cwd = process.cwd(), - path: path_ = process.env[pathKey()], - execPath = process.execPath, - } = options; - - let previous; - const execPathString = execPath instanceof URL ? url.fileURLToPath(execPath) : execPath; - const cwdString = cwd instanceof URL ? url.fileURLToPath(cwd) : cwd; - let cwdPath = path.resolve(cwdString); - const result = []; - - while (previous !== cwdPath) { - result.push(path.join(cwdPath, 'node_modules/.bin')); - previous = cwdPath; - cwdPath = path.resolve(cwdPath, '..'); - } - - // Ensure the running `node` binary is used. - result.push(path.resolve(cwdString, execPathString, '..')); - - return [...result, path_].join(path.delimiter); -} - -export function npmRunPathEnv({ env = process.env, ...options } = {}) { - env = { ...env }; - - const path = pathKey({ env }); - options.path = env[path]; - env[path] = npmRunPath(options); - - return env; -} diff --git a/packages/gui/src/main/lib/onetime/index.d.ts b/packages/gui/src/main/lib/onetime/index.d.ts deleted file mode 100644 index fa9fc20..0000000 --- a/packages/gui/src/main/lib/onetime/index.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -export type Options = { - /** - Throw an error when called more than once. - - @default false - */ - readonly throw?: boolean; -}; - -declare const onetime: { - /** - Ensure a function is only called once. When called multiple times it will return the return value from the first call. - - @param fn - The function that should only be called once. - @returns A function that only calls `fn` once. - - @example - ``` - import onetime from 'onetime'; - - let index = 0; - - const foo = onetime(() => ++index); - - foo(); //=> 1 - foo(); //=> 1 - foo(); //=> 1 - - onetime.callCount(foo); //=> 3 - ``` - */ - ( - fn: (...arguments_: ArgumentsType) => ReturnType, - options?: Options - ): (...arguments_: ArgumentsType) => ReturnType; - - /** - Get the number of times `fn` has been called. - - @param fn - The function to get call count from. - @returns A number representing how many times `fn` has been called. - - @example - ``` - import onetime from 'onetime'; - - const foo = onetime(() => {}); - foo(); - foo(); - foo(); - - console.log(onetime.callCount(foo)); - //=> 3 - ``` - */ - callCount(fn: (...arguments_: any[]) => unknown): number; -}; - -export default onetime; diff --git a/packages/gui/src/main/lib/onetime/index.js b/packages/gui/src/main/lib/onetime/index.js deleted file mode 100644 index 880e94d..0000000 --- a/packages/gui/src/main/lib/onetime/index.js +++ /dev/null @@ -1,41 +0,0 @@ -import mimicFunction from '../mimic-function'; - -const calledFunctions = new WeakMap(); - -const onetime = (function_, options = {}) => { - if (typeof function_ !== 'function') { - throw new TypeError('Expected a function'); - } - - let returnValue; - let callCount = 0; - const functionName = function_.displayName || function_.name || ''; - - const onetime = function (...arguments_) { - calledFunctions.set(onetime, ++callCount); - - if (callCount === 1) { - returnValue = function_.apply(this, arguments_); - function_ = undefined; - } else if (options.throw === true) { - throw new Error(`Function \`${functionName}\` can only be called once`); - } - - return returnValue; - }; - - mimicFunction(onetime, function_); - calledFunctions.set(onetime, callCount); - - return onetime; -}; - -onetime.callCount = function_ => { - if (!calledFunctions.has(function_)) { - throw new Error(`The given function \`${function_.name}\` is not wrapped by the \`onetime\` package`); - } - - return calledFunctions.get(function_); -}; - -export default onetime; diff --git a/packages/gui/src/main/lib/public_bind.js b/packages/gui/src/main/lib/public_bind.js deleted file mode 100644 index 4030c4f..0000000 --- a/packages/gui/src/main/lib/public_bind.js +++ /dev/null @@ -1,13 +0,0 @@ -import mcl from "./mcl" -import ipc from "./renderer_ipc" -import rfs from "./rfs" -import exec from "./execa/public_lib" -import auth from "./auth" - -export default { - mcl: mcl, - ipc: ipc, - rfs: rfs, - exec: exec, - auth: auth, -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/renderer_ipc/index.js b/packages/gui/src/main/lib/renderer_ipc/index.js deleted file mode 100644 index 6a1cc1f..0000000 --- a/packages/gui/src/main/lib/renderer_ipc/index.js +++ /dev/null @@ -1,7 +0,0 @@ -import sendToRender from "../../utils/sendToRender" - -export default class RendererIPC { - async send(...args) { - return await sendToRender(...args) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/rfs/index.js b/packages/gui/src/main/lib/rfs/index.js deleted file mode 100644 index de15366..0000000 --- a/packages/gui/src/main/lib/rfs/index.js +++ /dev/null @@ -1,47 +0,0 @@ -import path from "node:path" -import fs from "node:fs" -import { execa } from "../../lib/execa" - -import Vars from "../../vars" - -export default class RFS { - constructor(manifest) { - this.manifest = manifest - } - - async mount(remote_dir, to, cb) { - let mountPoint = path.resolve(this.manifest.install_path) - - if (typeof to === "string") { - mountPoint = path.join(mountPoint, to) - } else { - mountPoint = path.join(mountPoint, "rfs_mount") - } - - // check if already mounted - if (fs.existsSync(mountPoint)) { - return true - } - - const process = execa( - Vars.rclone_path, - [ - "mount", - "--vfs-cache-mode", - "full", - "--http-url", - remote_dir, - ":http:", - mountPoint, - ], { - stdout: "inherit", - stderr: "inherit", - }) - - if (typeof cb === "function") { - cb(process) - } - - return process - } -} \ No newline at end of file diff --git a/packages/gui/src/main/lib/steno/index.ts b/packages/gui/src/main/lib/steno/index.ts deleted file mode 100644 index ef8bfa0..0000000 --- a/packages/gui/src/main/lib/steno/index.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { PathLike } from 'node:fs' -import { rename, writeFile } from 'node:fs/promises' -import { basename, dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' - -// Returns a temporary file -// Example: for /some/file will return /some/.file.tmp -function getTempFilename(file: PathLike): string { - const f = file instanceof URL ? fileURLToPath(file) : file.toString() - return join(dirname(f), `.${basename(f)}.tmp`) -} - -// Retries an asynchronous operation with a delay between retries and a maximum retry count -async function retryAsyncOperation( - fn: () => Promise, - maxRetries: number, - delayMs: number, -): Promise { - for (let i = 0; i < maxRetries; i++) { - try { - return await fn() - } catch (error) { - if (i < maxRetries - 1) { - await new Promise((resolve) => setTimeout(resolve, delayMs)) - } else { - throw error // Rethrow the error if max retries reached - } - } - } -} - -type Resolve = () => void -type Reject = (error: Error) => void -type Data = Parameters[1] - -export class Writer { - #filename: PathLike - #tempFilename: PathLike - #locked = false - #prev: [Resolve, Reject] | null = null - #next: [Resolve, Reject] | null = null - #nextPromise: Promise | null = null - #nextData: Data | null = null - - // File is locked, add data for later - #add(data: Data): Promise { - // Only keep most recent data - this.#nextData = data - - // Create a singleton promise to resolve all next promises once next data is written - this.#nextPromise ||= new Promise((resolve, reject) => { - this.#next = [resolve, reject] - }) - - // Return a promise that will resolve at the same time as next promise - return new Promise((resolve, reject) => { - this.#nextPromise?.then(resolve).catch(reject) - }) - } - - // File isn't locked, write data - async #write(data: Data): Promise { - // Lock file - this.#locked = true - try { - // Atomic write - await writeFile(this.#tempFilename, data, 'utf-8') - await retryAsyncOperation( - async () => { - await rename(this.#tempFilename, this.#filename) - }, - 10, - 100, - ) - - // Call resolve - this.#prev?.[0]() - } catch (err) { - // Call reject - if (err instanceof Error) { - this.#prev?.[1](err) - } - throw err - } finally { - // Unlock file - this.#locked = false - - this.#prev = this.#next - this.#next = this.#nextPromise = null - - if (this.#nextData !== null) { - const nextData = this.#nextData - this.#nextData = null - await this.write(nextData) - } - } - } - - constructor(filename: PathLike) { - this.#filename = filename - this.#tempFilename = getTempFilename(filename) - } - - async write(data: Data): Promise { - return this.#locked ? this.#add(data) : this.#write(data) - } -} diff --git a/packages/gui/src/main/lib/strip-final-newline/index.d.ts b/packages/gui/src/main/lib/strip-final-newline/index.d.ts deleted file mode 100644 index e8fa1d3..0000000 --- a/packages/gui/src/main/lib/strip-final-newline/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** -Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from a string or Uint8Array. - -@returns The input without any final newline. - -@example -``` -import stripFinalNewline from 'strip-final-newline'; - -stripFinalNewline('foo\nbar\n\n'); -//=> 'foo\nbar\n' - -const uint8Array = new TextEncoder().encode('foo\nbar\n\n') -new TextDecoder().decode(stripFinalNewline(uint8Array)); -//=> 'foo\nbar\n' -``` -*/ -export default function stripFinalNewline(input: T): T; diff --git a/packages/gui/src/main/lib/strip-final-newline/index.js b/packages/gui/src/main/lib/strip-final-newline/index.js deleted file mode 100644 index a63ed26..0000000 --- a/packages/gui/src/main/lib/strip-final-newline/index.js +++ /dev/null @@ -1,26 +0,0 @@ -export default function stripFinalNewline(input) { - if (typeof input === 'string') { - return stripFinalNewlineString(input); - } - - if (!(ArrayBuffer.isView(input) && input.BYTES_PER_ELEMENT === 1)) { - throw new Error('Input must be a string or a Uint8Array'); - } - - return stripFinalNewlineBinary(input); -} - -const stripFinalNewlineString = input => - input.at(-1) === LF - ? input.slice(0, input.at(-2) === CR ? -2 : -1) - : input; - -const stripFinalNewlineBinary = input => - input.at(-1) === LF_BINARY - ? input.subarray(0, input.at(-2) === CR_BINARY ? -2 : -1) - : input; - -const LF = '\n'; -const LF_BINARY = LF.codePointAt(0); -const CR = '\r'; -const CR_BINARY = CR.codePointAt(0); diff --git a/packages/gui/src/main/local_db.js b/packages/gui/src/main/local_db.js deleted file mode 100644 index 8e09c18..0000000 --- a/packages/gui/src/main/local_db.js +++ /dev/null @@ -1,50 +0,0 @@ -import { JSONFilePreset } from "./lib/lowdb" - -import DefaultDB from "./defaults/local_db" -import Vars from "./vars" - -export async function withDB() { - return await JSONFilePreset(Vars.local_db, DefaultDB) -} - -export async function updateInstalledPackage(pkg) { - const db = await withDB() - - await db.update((data) => { - const prevIndex = data["packages"].findIndex((i) => i.id === pkg.id) - - if (prevIndex !== -1) { - data["packages"][prevIndex] = pkg - } else { - data["packages"].push(pkg) - } - - return data - }) - - return pkg -} - -export async function getInstalledPackages(pkg_id) { - const db = await withDB() - - if (pkg_id) { - return db.data["packages"].find((i) => i.id === pkg_id) - } - - return db.data["packages"] -} - -export async function deleteInstalledPackage(pkg_id) { - const db = await withDB() - - await db.update((data) => { - data["packages"] = data["packages"].filter((i) => i.id !== pkg_id) - - return data - }) - - return pkg_id -} - -export default withDB \ No newline at end of file diff --git a/packages/gui/src/main/manager.js b/packages/gui/src/main/manager.js deleted file mode 100644 index 1c0e979..0000000 --- a/packages/gui/src/main/manager.js +++ /dev/null @@ -1,108 +0,0 @@ -import fs from "node:fs" -import open from "open" - -import Vars from "./vars" -import * as local_db from "./local_db" - -import InstallCMD from "./commands/install" -import UpdateCMD from "./commands/update" -import ApplyCMD from "./commands/apply" -import UninstallCMD from "./commands/uninstall" -import ExecuteCMD from "./commands/execute" - -export default class PkgManager { - constructor() { - this.initialize() - } - - async initialize() { - if (!fs.existsSync(Vars.runtime_path)) { - fs.mkdirSync(Vars.runtime_path, { recursive: true }) - } - - if (!fs.existsSync(Vars.packages_path)) { - fs.mkdirSync(Vars.packages_path, { recursive: true }) - } - } - - /** - * Opens the runtime path folder. - */ - openRuntimePath() { - open(Vars.runtime_path) - } - - /** - * Asynchronously retrieves the installed packages using the provided arguments. - * - * @param {...type} args - The arguments to be passed to the underlying local database function - * @return {type} The result of the local database function call - */ - async getInstalledPackages(...args) { - return await local_db.getInstalledPackages(...args) - } - - /** - * Asynchronously opens a package folder. - * - * @param {type} pkg_id - the ID of the package to open - * @return {type} undefined - */ - async open(pkg_id) { - const pkg = await local_db.getInstalledPackages(pkg_id) - - if (pkg) { - open(pkg.install_path) - } - } - - /** - * Asynchronously installs using the given arguments. - * - * @param {...*} args - variable number of arguments - * @return {Promise} a promise representing the result of the installation - */ - async install(...args) { - return await InstallCMD(...args) - } - - /** - * Asynchronously updates something using the arguments provided. - * - * @param {...*} args - The arguments to be passed to the update function - * @return {Promise} The result of the update operation - */ - async update(...args) { - return await UpdateCMD(...args) - } - - /** - * Asynchronously applies changes using the given arguments. - * - * @param {...*} args - The arguments to be passed to ApplyCMD - * @return {Promise} The result of the ApplyCMD function - */ - async applyChanges(...args) { - return await ApplyCMD(...args) - } - - /** - * Asynchronously uninstalls using the given arguments. - * - * @param {...args} args - arguments to be passed to UninstallCMD - * @return {Promise} the result of UninstallCMD - */ - async uninstall(...args) { - return await UninstallCMD(...args) - } - - /** - * Executes the command with the given arguments asynchronously. - * - * @param {...args} args - the arguments to be passed to the command - * @return {Promise} a promise that resolves to the result of the command execution - */ - async execute(...args) { - return await ExecuteCMD(...args) - } -} \ No newline at end of file diff --git a/packages/gui/src/main/prerequisites.js b/packages/gui/src/main/prerequisites.js deleted file mode 100644 index 4134e1c..0000000 --- a/packages/gui/src/main/prerequisites.js +++ /dev/null @@ -1,35 +0,0 @@ -import resolveDestBin from "@utils/resolveDestBin" -import Vars from "@vars" - -const baseURL = "https://storage.ragestudio.net/rstudio/binaries" - -export default [ - { - id: "7zip-bin", - url: resolveDestBin(`${baseURL}/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za"), - destination: Vars.sevenzip_path, - rewritePermissions: true, - extract: false, - }, - { - id: "git-bin", - url: resolveDestBin(`${baseURL}/git`, "git-bundle-2.4.0.zip"), - destination: Vars.git_path, - rewritePermissions: true, - extract: true, - }, - { - id: "rclone-bin", - url: resolveDestBin(`${baseURL}/rclone-bin`, "rclone-bin.zip"), - destination: Vars.rclone_path, - rewritePermissions: true, - extract: true, - }, - { - id: "java-jdk", - url: resolveDestBin(`${baseURL}/java-jdk`, "java-jdk.zip"), - destination: Vars.java_path, - rewritePermissions: true, - extract: true, - }, -] \ No newline at end of file diff --git a/packages/gui/src/main/setup.js b/packages/gui/src/main/setup.js deleted file mode 100644 index e9902ea..0000000 --- a/packages/gui/src/main/setup.js +++ /dev/null @@ -1,144 +0,0 @@ -import path from "node:path" -import fs from "node:fs" -import os from "node:os" -import ChildProcess from "node:child_process" -import { pipeline as streamPipeline } from "node:stream/promises" - -import unzipper from "unzipper" -import got from "got" - -import Vars from "./vars" - -function resolveDestBin(pre, post) { - let url = null - - if (process.platform === "darwin") { - url = `${pre}/mac/${process.arch}/${post}` - } - else if (process.platform === "win32") { - url = `${pre}/win/${process.arch}/${post}` - } - else { - url = `${pre}/linux/${process.arch}/${post}` - } - - return url -} - -async function main() { - const binariesPath = Vars.binaries_path - - if (!fs.existsSync(binariesPath)) { - fs.mkdirSync(binariesPath, { recursive: true }) - } - - let sevenzip_exec = Vars.sevenzip_path - let git_exec = Vars.git_path - let rclone_exec = Vars.rclone_path - - if (!fs.existsSync(sevenzip_exec)) { - global.win.webContents.send("setup_step", "Downloading 7z binaries...") - console.log(`Downloading 7z binaries...`) - - fs.mkdirSync(path.resolve(binariesPath, "7z-bin"), { recursive: true }) - - let url = resolveDestBin(`https://storage.ragestudio.net/rstudio/binaries/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za") - - await streamPipeline( - got.stream(url), - fs.createWriteStream(sevenzip_exec) - ) - - if (os.platform() !== "win32") { - ChildProcess.execSync("chmod +x " + sevenzip_exec) - } - } - - if (!fs.existsSync(git_exec) && process.platform === "win32") { - const tempPath = path.resolve(binariesPath, "git-bundle.zip") - const binPath = path.resolve(binariesPath, "git-bin") - - if (!fs.existsSync(tempPath)) { - global.win.webContents.send("setup_step", "Downloading GIT binaries...") - console.log(`Downloading git binaries...`) - - let url = resolveDestBin(`https://storage.ragestudio.net/rstudio/binaries/git`, "git-bundle-2.4.0.zip") - - await streamPipeline( - got.stream(url), - fs.createWriteStream(tempPath) - ) - } - - global.win.webContents.send("setup_step", "Extracting GIT binaries...") - console.log(`Extracting GIT...`) - - await new Promise((resolve, reject) => { - fs.createReadStream(tempPath).pipe(unzipper.Extract({ path: binPath })).on("close", resolve).on("error", reject) - }) - - fs.unlinkSync(tempPath) - } - - if (!fs.existsSync(Vars.rclone_path) && process.platform === "win32") { - console.log(`Downloading rclone binaries...`) - global.win.webContents.send("setup_step", "Downloading rclone binaries...") - - const tempPath = path.resolve(binariesPath, "rclone-bin.zip") - - let url = resolveDestBin(`https://storage.ragestudio.net/rstudio/binaries/rclone`, "rclone-bin.zip") - - await streamPipeline( - got.stream(url), - fs.createWriteStream(tempPath) - ) - - global.win.webContents.send("setup_step", "Extracting rclone binaries...") - - await new Promise((resolve, reject) => { - fs.createReadStream(tempPath).pipe(unzipper.Extract({ path: path.resolve(binariesPath, "rclone-bin") })).on("close", resolve).on("error", reject) - }) - - if (os.platform() !== "win32") { - ChildProcess.execSync("chmod +x " + Vars.rclone_path) - } - - fs.unlinkSync(tempPath) - } - - if (!fs.existsSync(Vars.java_path)) { - console.log(`Downloading java binaries...`) - global.win.webContents.send("setup_step", "Downloading Java JDK...") - - const tempPath = path.resolve(binariesPath, "java-jdk.zip") - - let url = resolveDestBin(`https://storage.ragestudio.net/rstudio/binaries/java`, "java-jdk.zip") - - await streamPipeline( - got.stream(url), - fs.createWriteStream(tempPath) - ) - - global.win.webContents.send("setup_step", "Extracting JAVA...") - - await new Promise((resolve, reject) => { - fs.createReadStream(tempPath).pipe(unzipper.Extract({ path: path.resolve(binariesPath, "java-jdk") })).on("close", resolve).on("error", reject) - }) - - if (os.platform() !== "win32") { - ChildProcess.execSync("chmod +x " + path.resolve(binariesPath, "java-jdk")) - } - - fs.unlinkSync(tempPath) - } - - console.log(`7z binaries: ${sevenzip_exec}`) - console.log(`GIT binaries: ${git_exec}`) - console.log(`rclone binaries: ${rclone_exec}`) - console.log(`JAVA jdk: ${Vars.java_path}`) - - global.win.webContents.send("setup_step", undefined) - global.win.webContents.send("setup:done") -} - -export default main \ No newline at end of file diff --git a/packages/gui/src/main/utils/extractFile.js b/packages/gui/src/main/utils/extractFile.js deleted file mode 100644 index 8cead9d..0000000 --- a/packages/gui/src/main/utils/extractFile.js +++ /dev/null @@ -1,44 +0,0 @@ -import fs from "node:fs" -import path from "node:path" -import { pipeline as streamPipeline } from "node:stream/promises" - -import { extractFull } from "node-7z" -import unzipper from "unzipper" - -import Vars from "../vars" - -export async function extractFile(file, dest) { - const ext = path.extname(file) - - console.log(`extractFile() | Extracting ${file} to ${dest}`) - - switch (ext) { - case ".zip": { - await streamPipeline( - fs.createReadStream(file), - unzipper.Extract({ - path: dest, - }) - ) - break - } - case ".7z": { - await extractFull(file, dest, { - $bin: Vars.sevenzip_path, - }) - break - } - case ".gz": { - await extractFull(file, dest, { - $bin: Vars.sevenzip_path - }) - break - } - default: - throw new Error(`Unsupported file extension: ${ext}`) - } - - return dest -} - -export default extractFile \ No newline at end of file diff --git a/packages/gui/src/main/utils/initManifest.js b/packages/gui/src/main/utils/initManifest.js deleted file mode 100644 index 3139d4b..0000000 --- a/packages/gui/src/main/utils/initManifest.js +++ /dev/null @@ -1,56 +0,0 @@ -import path from "node:path" -import os from "node:os" -import lodash from "lodash" - -import Vars from "../vars" -import PublicLibs from "../lib/public_bind" - -async function importLib(libs, bindCtx) { - const libraries = {} - - for await (const lib of libs) { - if (PublicLibs[lib]) { - if (typeof PublicLibs[lib] === "function") { - libraries[lib] = new PublicLibs[lib](bindCtx) - } else { - libraries[lib] = PublicLibs[lib] - } - } - } - - return libraries -} - -export default async (manifest = {}) => { - const install_path = path.resolve(Vars.packages_path, manifest.id) - const os_string = `${os.platform()}-${os.arch()}` - - manifest.install_path = install_path - - if (typeof manifest.init === "function") { - const init_result = await manifest.init({ - manifest: manifest, - install_path: install_path, - os_string: os_string, - }) - - manifest = lodash.merge(manifest, init_result) - - delete manifest.init - } - - if (Array.isArray(manifest.import_libs)) { - manifest.libraries = await importLib(manifest.import_libs, { - id: manifest.id, - version: manifest.version, - install_path: install_path, - auth: manifest.auth, - configs: manifest.configs, - os_string: os_string, - }) - - console.log(`[${manifest.id}] initManifest() | Using libraries: ${manifest.import_libs.join(", ")}`) - } - - return manifest -} \ No newline at end of file diff --git a/packages/gui/src/main/utils/parseStringVars.js b/packages/gui/src/main/utils/parseStringVars.js deleted file mode 100644 index 6ae8687..0000000 --- a/packages/gui/src/main/utils/parseStringVars.js +++ /dev/null @@ -1,21 +0,0 @@ -export default function parseStringVars(str, pkg) { - if (!pkg) { - return str - } - - const vars = { - id: pkg.id, - name: pkg.name, - version: pkg.version, - install_path: pkg.install_path, - remote_url: pkg.remote_url, - } - - const regex = /%([^%]+)%/g - - str = str.replace(regex, (match, varName) => { - return vars[varName] - }) - - return str -} \ No newline at end of file diff --git a/packages/gui/src/main/utils/readDirRecurse.js b/packages/gui/src/main/utils/readDirRecurse.js deleted file mode 100644 index 342dda0..0000000 --- a/packages/gui/src/main/utils/readDirRecurse.js +++ /dev/null @@ -1,25 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -async function readDirRecurse(dir, maxDepth = 3, current = 0) { - if (current > maxDepth) { - return [] - } - - const files = await fs.promises.readdir(dir) - - const promises = files.map(async (file) => { - const filePath = path.join(dir, file) - const stat = await fs.promises.stat(filePath) - - if (stat.isDirectory()) { - return readDirRecurse(filePath, maxDepth, current + 1) - } - - return filePath - }) - - return (await Promise.all(promises)).flat() -} - -export default readDirRecurse \ No newline at end of file diff --git a/packages/gui/src/main/utils/readManifest.js b/packages/gui/src/main/utils/readManifest.js deleted file mode 100644 index 9b37f41..0000000 --- a/packages/gui/src/main/utils/readManifest.js +++ /dev/null @@ -1,58 +0,0 @@ -import fs from "node:fs" -import got from "got" - -export async function fetchAndCreateModule(manifest) { - console.log(`[${manifest.id}] fetchAndCreateModule() | Fetching ${manifest}...`) - - try { - const response = await got.get(manifest) - const moduleCode = response.body - - const newModule = new module.constructor() - newModule._compile(moduleCode, manifest) - - return newModule - } catch (error) { - console.error(error) - } -} - -export async function readManifest(manifest) { - // check if manifest is a directory or a url - const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi - - const target = manifest?.remote_url ?? manifest - - if (urlRegex.test(target)) { - const _module = await fetchAndCreateModule(target) - - if (typeof manifest === "object") { - manifest._original_manifest = { - ...manifest, - } - - manifest = { - ...manifest, - ..._module.exports, - } - } else { - manifest = _module.exports - } - - manifest.remote_url = target - } else { - if (!fs.existsSync(target)) { - throw new Error(`Manifest not found: ${target}`) - } - - if (!fs.statSync(target).isFile()) { - throw new Error(`Manifest is not a file: ${target}`) - } - - manifest = require(target) - } - - return manifest -} - -export default readManifest \ No newline at end of file diff --git a/packages/gui/src/main/utils/resolveJavaPath.js b/packages/gui/src/main/utils/resolveJavaPath.js deleted file mode 100644 index 65d90af..0000000 --- a/packages/gui/src/main/utils/resolveJavaPath.js +++ /dev/null @@ -1,178 +0,0 @@ -/* Copyright 2013 Joseph Spencer. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as fs from "fs" -import * as path from "path" -import * as cp from "child_process" - -import {execSync} from "child_process" -import which from "which" - -import WinReg from "winreg" - -const isWindows = process.platform.indexOf('win') === 0 - -const jdkRegistryKeyPaths = [ - "\\SOFTWARE\\JavaSoft\\JDK", - "\\SOFTWARE\\JavaSoft\\Java Development Kit" -] - -const jreRegistryKeyPaths = [ - "\\SOFTWARE\\JavaSoft\\Java Runtime Environment" -] - -async function findJavaHome(options) { - const allowJre = !!(options && options.allowJre) - - const JAVA_FILENAME = (allowJre ? 'java' : 'javac') + (isWindows ? '.exe' : '') - - // Search both "x64" and "x86" registries for Java runtimes if not specified - const regs = (options && options.registry) ? [options.registry] : ["x64", "x86"] - - // From env - if (process.env.JAVA_HOME && dirIsJavaHome(process.env.JAVA_HOME, JAVA_FILENAME)) { - return process.env.JAVA_HOME - } - - // From registry (windows only) - if (isWindows) { - const possibleKeyPaths = allowJre ? jdkRegistryKeyPaths.concat(jreRegistryKeyPaths) : jdkRegistryKeyPaths - const javaHome = await findInRegistry(possibleKeyPaths, regs) - - if (javaHome) { - return javaHome - } - } - - // From PATH - return await findInPath(JAVA_FILENAME) -} - -function findInPath(JAVA_FILENAME) { - return new Promise((resolve) => { - which(JAVA_FILENAME, (err, proposed) => { - if (err || !proposed) { - return resolve(null) - } - - if (/\.jenv\/shims/.test(proposed)) { - try { - proposed = execSync(`jenv which ${JAVA_FILENAME}`).toString().trim() - } catch (ex) { - console.error(ex) - } - } - - //resolve symlinks - proposed = fs.realpathSync(proposed) - - //get the /bin directory - proposed = path.dirname(proposed) - - //on mac, java install has a utility script called java_home that does the - //dirty work for us - const macUtility = path.resolve(proposed, 'java_home') - - if (fs.existsSync(macUtility)) { - let buffer - - try { - buffer = cp.execSync(macUtility, { cwd: proposed }) - - const javaHome = '' + buffer.toString().replace(/\n$/, '') - - return resolve(javaHome) - } catch (error) { - return resolve(null) - } - } - - //up one from /bin - resolve(path.dirname(proposed)) - }) - }) -} - -async function findInRegistry(keyPaths, regArchs) { - if (!keyPaths.length) return null - - const promises = [] - - for (const arch of regArchs) { - for (const keyPath of keyPaths) { - promises.push(promisifyFindPossibleRegKey(keyPath, arch)) - } - } - - const keysFoundSegments = await Promise.all(promises) - - const keysFound = Array.prototype.concat.apply([], keysFoundSegments) - - if (!keysFound.length) { - return null - } - - const sortedKeysFound = keysFound.sort(function (a, b) { - const aVer = parseFloat(a.key) - const bVer = parseFloat(b.key) - - return bVer - aVer - }) - - for (const key of sortedKeysFound) { - const res = await promisifyFindJavaHomeInRegKey(key) - - if (res) { - return res - } - } - - return null -} - -function promisifyFindPossibleRegKey(keyPath, regArch) { - return new Promise((resolve) => { - const winreg = new WinReg({ - hive: WinReg.HKLM, - key: keyPath, - arch: regArch - }) - - winreg.keys((err, result) => { - if (err) { - return resolve([]) - } - resolve(result) - }) - }) -} - -function promisifyFindJavaHomeInRegKey(reg) { - return new Promise((resolve) => { - reg.get('JavaHome', function (err, home) { - if (err || !home) { - return resolve(null) - } - - return resolve(home.value) - }) - }) -} - -function dirIsJavaHome(dir, javaFilename) { - return fs.existsSync('' + dir) && fs.statSync(dir).isDirectory() && fs.existsSync(path.resolve(dir, 'bin', javaFilename)) -} - -export default findJavaHome \ No newline at end of file diff --git a/packages/gui/src/main/vars.js b/packages/gui/src/main/vars.js deleted file mode 100644 index a766b87..0000000 --- a/packages/gui/src/main/vars.js +++ /dev/null @@ -1,31 +0,0 @@ -import path from "node:path" -import upath from "upath" - -global.OS_USERDATA_PATH = upath.normalizeSafe(path.resolve( - process.env.APPDATA || - (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"), -)) -global.RUNTIME_PATH = upath.normalizeSafe(path.join(global.OS_USERDATA_PATH, "rs-bundler")) - -global.PACKAGES_PATH = upath.normalizeSafe(path.join(global.RUNTIME_PATH, "packages")) -global.BINARIES_PATH = upath.normalizeSafe(path.resolve(global.RUNTIME_PATH, "bin_lib")) - -global.LOCAL_DB = upath.normalizeSafe(path.join(global.RUNTIME_PATH, "local_db.json")) - -global.SEVENZIP_PATH = upath.normalizeSafe(path.resolve(global.BINARIES_PATH, "7z-bin", process.platform === "win32" ? "7za.exe" : "7za")) -global.GIT_PATH = upath.normalizeSafe(path.resolve(global.BINARIES_PATH, "git-bin", "bin", process.platform === "win32" ? "git.exe" : "git")) -global.RCLONE_PATH = upath.normalizeSafe(path.resolve(global.BINARIES_PATH, "rclone-bin", process.platform === "win32" ? "rclone.exe" : "rclone")) -global.JAVA_PATH = upath.normalizeSafe(path.resolve(global.BINARIES_PATH, "java-jdk", "bin", process.platform === "win32" ? "java.exe" : "java")) - -export default { - binaries_path: global.BINARIES_PATH, - - sevenzip_path: global.SEVENZIP_PATH, - git_path: global.GIT_PATH, - rclone_path: global.RCLONE_PATH, - java_path: global.JAVA_PATH, - - runtime_path: global.RUNTIME_PATH, - packages_path: global.PACKAGES_PATH, - local_db: global.LOCAL_DB, -} \ No newline at end of file diff --git a/packages/gui/src/renderer/index.html b/packages/gui/src/renderer/index.html index 59218b3..c994f68 100644 --- a/packages/gui/src/renderer/index.html +++ b/packages/gui/src/renderer/index.html @@ -2,7 +2,7 @@ - RageStudio Bundler + Relic - - -
- - - + + + Relic + + + + +
+ + + + \ No newline at end of file diff --git a/packages/gui/src/renderer/src/App.jsx b/packages/gui/src/renderer/src/App.jsx index dda0d7a..995491d 100644 --- a/packages/gui/src/renderer/src/App.jsx +++ b/packages/gui/src/renderer/src/App.jsx @@ -17,26 +17,24 @@ window.app = GlobalApp class App extends React.Component { state = { - loading: true, + initializing: true, pkg: null, - initializing: false, - setup_step: null, - updateAvailable: false, - updateText: null, + appSetup: { + error: false, + installed: false, + message: null, + }, - authorizedServices: { - drive: false, + appUpdate: { + changelog: null, + available: false, }, + + authorizedServices: [], } ipcEvents = { - "runtime:error": (event, data) => { - antd.message.error(data) - }, - "runtime:info": (event, data) => { - antd.message.info(data) - }, "new:notification": (event, data) => { app.notification[data.type || "info"]({ message: data.message, @@ -50,8 +48,13 @@ class App extends React.Component { "new:message": (event, data) => { antd.message[data.type || "info"](data.message) }, + "app:setup": (event, data) => { + this.setState({ + appSetup: data, + }) + }, "app:update_available": (event, data) => { - if (this.state.loading) { + if (this.state.initializing) { return false } @@ -62,73 +65,49 @@ class App extends React.Component { app.appUpdateAvailable(data) }, "pkg:install:ask": (event, data) => { - if (this.state.loading) { + if (this.state.initializing) { return false } app.pkgInstallWizard(data) }, "pkg:update_available": (event, data) => { - if (this.state.loading) { + if (this.state.initializing) { return false } app.pkgUpdateAvailable(data) }, - "installation:invoked": (event, manifest) => { - if (this.state.loading) { - return false - } - - app.invokeInstall(manifest) - }, - "drive:authorized": (event, data) => { - this.setState({ - authorizedServices: { - drive: true, - }, - }) + "pkg:installation:invoked": (event, data) => { + if (this.state.initializing) { + return false + } - message.success("Google Drive API authorized") - }, - "drive:unauthorized": (event, data) => { - this.setState({ - authorizedServices: { - drive: false, - }, - }) - - message.success("Google Drive API unauthorized") - }, - "setup_step": (event, data) => { - console.log(`setup:step`, data) - - this.setState({ - setup_step: data, - }) - }, + app.invokeInstall(data) + } } componentDidMount = async () => { - const initResult = await ipc.exec("app:init") - - console.log(`Using React version > ${versions["react"]}`) - console.log(`Using DOMRouter version > ${versions["react-router-dom"]}`) - console.log(`[APP] app:init() | Result >`, initResult) + window.app.style.appendClassname("initializing") for (const event in this.ipcEvents) { ipc.exclusiveListen(event, this.ipcEvents[event]) } - app.location.push("/") + const mainInitialization = await ipc.exec("app:init") + + console.log(`React version > ${versions["react"]}`) + console.log(`DOMRouter version > ${versions["react-router-dom"]}`) + console.log(`app:init() | Result >`, mainInitialization) - this.setState({ - loading: false, - pkg: initResult.pkg, - authorizedServices: { - drive: initResult.authorizedServices?.drive ?? false - }, + await this.setState({ + initializing: false, + pkg: mainInitialization.pkg, }) + + app.location.push("/") + + window.app.style.removeClassname("initializing") } render() { diff --git a/packages/gui/src/renderer/src/GlobalApp.jsx b/packages/gui/src/renderer/src/GlobalApp.jsx index 054bd05..432d170 100644 --- a/packages/gui/src/renderer/src/GlobalApp.jsx +++ b/packages/gui/src/renderer/src/GlobalApp.jsx @@ -10,7 +10,25 @@ globalThis.getRootCssVar = getRootCssVar globalThis.notification = notification globalThis.message = message +class GlobalStyleController { + static root = document.getElementById("root") + + static appendClassname = (classname) => { + console.log(`appending classname >`, classname) + GlobalStyleController.root.classList.add(classname) + } + + static removeClassname = (classname) => { + console.log(`removing classname >`, classname) + GlobalStyleController.root.classList.remove(classname) + } + + static getRootCssVar = getRootCssVar +} + export default class GlobalCTXApp { + static style = GlobalStyleController + static applyUpdate = () => { message.loading("Updating, please wait...") diff --git a/packages/gui/src/renderer/src/components/Splash/index.jsx b/packages/gui/src/renderer/src/components/Splash/index.jsx new file mode 100644 index 0000000..d0fa2cc --- /dev/null +++ b/packages/gui/src/renderer/src/components/Splash/index.jsx @@ -0,0 +1,47 @@ +import React from "react" +import * as antd from "antd" +import { BarLoader } from "react-spinners" +import GlobalStateContext from "contexts/global" + +import "./index.less" + +const Splash = (props) => { + const globalState = React.useContext(GlobalStateContext) + + return
+ { + !!globalState.appSetup.message &&
+

+ Setting up... +

+

+ Please wait while the application is being set up. +

+
+ } + + { + globalState.appSetup.message && <> +
+ + {globalState.appSetup.message} + +
+ + + + } + + { + !globalState.appSetup.message && + } +
+} + +export default Splash \ No newline at end of file diff --git a/packages/gui/src/renderer/src/components/Splash/index.less b/packages/gui/src/renderer/src/components/Splash/index.less new file mode 100644 index 0000000..711cf82 --- /dev/null +++ b/packages/gui/src/renderer/src/components/Splash/index.less @@ -0,0 +1,32 @@ +.splash { + display: flex; + flex-direction: column; + + justify-content: center; + + height: 100%; + + gap: 20px; + + .app-setup_header { + display: flex; + flex-direction: column; + + gap: 10px; + + h1 { + font-size: 1.7rem; + } + } + + .app-setup_message { + padding: 15px 10px; + border-radius: 12px; + + transition: all 150ms ease-in-out; + + background-color: var(--background-color-secondary); + + font-family: "DM Mono", monospace; + } +} \ No newline at end of file diff --git a/packages/gui/src/renderer/src/contexts/packages.jsx b/packages/gui/src/renderer/src/contexts/packages.jsx index 8ef709a..a4bf844 100644 --- a/packages/gui/src/renderer/src/contexts/packages.jsx +++ b/packages/gui/src/renderer/src/contexts/packages.jsx @@ -5,8 +5,8 @@ export const Context = React.createContext([]) export class WithContext extends React.Component { state = { + loading: true, packages: [], - pendingInstallation: false, } ipcEvents = { @@ -62,24 +62,27 @@ export class WithContext extends React.Component { packages: newData }) } - - console.log(`[ipc] pkg:update:state >`, data) } } - componentDidMount = async () => { + loadPackages = async () => { + await this.setState({ + loading: true, + }) + const packages = await ipc.exec("pkg:list") + await this.setState({ + packages: packages, + }) + } + + componentDidMount = async () => { for (const event in this.ipcEvents) { ipc.exclusiveListen(event, this.ipcEvents[event]) } - this.setState({ - packages: [ - ...this.state.packages, - ...packages, - ] - }) + await this.loadPackages() } render() { diff --git a/packages/gui/src/renderer/src/layout/components/Header/index.less b/packages/gui/src/renderer/src/layout/components/Header/index.less index 78ac904..61f04f3 100644 --- a/packages/gui/src/renderer/src/layout/components/Header/index.less +++ b/packages/gui/src/renderer/src/layout/components/Header/index.less @@ -12,18 +12,21 @@ view-transition-name: main-header; + height: var(--app_header_height); + display: inline-flex; flex-direction: row; align-items: center; - background-color: darken(@var-background-color-primary, 10%); - gap: 30px; + padding: 0 20px; - height: var(--app_header_height); + overflow: hidden; - padding: 0 20px; + transition: 150ms ease-in-out; + + background-color: darken(@var-background-color-primary, 10%); .branding { display: inline-flex; @@ -69,9 +72,12 @@ justify-content: center; border-radius: 50%; + padding: 3px; background-color: var(--primary-color); - font-size: 1.4rem; + font-size: 1.6rem; + + color: #3b3b3b; } .app_header_nav_title { diff --git a/packages/gui/src/renderer/src/pages/index.jsx b/packages/gui/src/renderer/src/pages/index.jsx index 055fe62..992ae26 100644 --- a/packages/gui/src/renderer/src/pages/index.jsx +++ b/packages/gui/src/renderer/src/pages/index.jsx @@ -10,25 +10,26 @@ import NewInstallation from "components/NewInstallation" import "./index.less" -class InstallationsManager extends React.Component { +class Packages extends React.Component { static contextType = InstallationsContext render() { - const { packages } = this.context + const { packages, loading } = this.context const empty = packages.length == 0 - return
-
+ return
+
} onClick={() => app.drawer.open(NewInstallation, { - title: "Add new installation", + title: "Install new package", height: "200px", })} + className="add-btn" > - Add new installation + Add new
-
+
{ - empty && + loading && } { - packages.map((manifest) => { + !loading && empty && + } + + { + !loading && packages.map((manifest) => { return }) } @@ -54,10 +59,10 @@ class InstallationsManager extends React.Component { } } -const InstallationsManagerPage = (props) => { +const PackagesPage = (props) => { return - + } -export default InstallationsManagerPage \ No newline at end of file +export default PackagesPage \ No newline at end of file diff --git a/packages/gui/src/renderer/src/pages/index.less b/packages/gui/src/renderer/src/pages/index.less index 7149fb5..5249677 100644 --- a/packages/gui/src/renderer/src/pages/index.less +++ b/packages/gui/src/renderer/src/pages/index.less @@ -1,4 +1,4 @@ -.installations_manager { +.packages { display: flex; flex-direction: column; @@ -6,17 +6,52 @@ gap: 10px; - .installations_manager-header { + .packages-header { display: flex; - flex-direction: column; + flex-direction: row; gap: 10px; .ant-btn-default { background-color: var(--background-color-secondary); } + + .add-btn { + display: flex; + flex-direction: row; + + padding: 5px; + border-radius: 12px; + gap: 0px; + + span:not(.ant-btn-icon) { + opacity: 0; + + transition: all 150ms ease; + + max-width: 0px; + } + + &:hover { + padding: 5px 10px; + border-radius: 8px; + + gap: 7px; + + span:not(.ant-btn-icon) { + opacity: 1; + max-width: 200px; + } + } + + .ant-btn-icon { + margin: 0; + + font-size: 1.2rem; + } + } } - .installations_list { + .packages-list { display: flex; flex-direction: column; diff --git a/packages/gui/src/renderer/src/router.jsx b/packages/gui/src/renderer/src/router.jsx index 68f1867..599c16e 100644 --- a/packages/gui/src/renderer/src/router.jsx +++ b/packages/gui/src/renderer/src/router.jsx @@ -1,5 +1,4 @@ import React from "react" -import BarLoader from "react-spinners/BarLoader" import { Skeleton } from "antd" import { HashRouter, Route, Routes, useNavigate, useParams } from "react-router-dom" @@ -7,12 +6,14 @@ import loadable from "@loadable/component" import GlobalStateContext from "contexts/global" +import SplashScreen from "components/Splash" + const DefaultNotFoundRender = () => { return
Not found
} const DefaultLoadingRender = () => { - return + return } const BuildPageController = (route, element, bindProps) => { @@ -155,19 +156,8 @@ export const PageRender = (props) => { const globalState = React.useContext(GlobalStateContext) - if (globalState.setup_step || globalState.loading) { - return
- - -

Setting up...

- - -
{globalState.setup_step}
-
-
+ if (globalState.initializing) { + return } return diff --git a/packages/gui/src/renderer/src/style/index.less b/packages/gui/src/renderer/src/style/index.less index 5a729c1..bf6bd32 100644 --- a/packages/gui/src/renderer/src/style/index.less +++ b/packages/gui/src/renderer/src/style/index.less @@ -41,6 +41,12 @@ body { height: 100%; overflow: hidden; + + &.initializing { + .app_header { + height: 0px; + } + } } .app_layout { From 01d6031473c8a26780142dcabdca10e25daeff8f Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 2 Apr 2024 17:34:15 +0200 Subject: [PATCH 06/14] merge from local --- packages/core/src/handlers/checkUpdate.js | 43 ++++++++++++++ packages/core/src/helpers/setup.js | 15 ++++- packages/core/src/index.js | 4 ++ packages/core/src/prerequisites.js | 3 +- packages/gui/src/main/index.js | 35 +++++++---- packages/gui/src/renderer/assets/bruh_fox.jpg | Bin 0 -> 337649 bytes packages/gui/src/renderer/src/App.jsx | 26 +++++--- packages/gui/src/renderer/src/GlobalApp.jsx | 12 ++-- .../renderer/src/components/Crash/index.jsx | 27 +++++++++ .../renderer/src/components/Crash/index.less | 56 ++++++++++++++++++ .../PackageUpdateAvailable/index.jsx | 6 +- .../renderer/src/components/Splash/index.jsx | 2 + .../renderer/src/components/Splash/index.less | 26 ++++++-- .../src/layout/components/Header/index.jsx | 4 +- packages/gui/src/renderer/src/router.jsx | 19 ++++-- .../gui/src/renderer/src/style/index.less | 13 ++++ 16 files changed, 246 insertions(+), 45 deletions(-) create mode 100644 packages/core/src/handlers/checkUpdate.js create mode 100644 packages/gui/src/renderer/assets/bruh_fox.jpg create mode 100644 packages/gui/src/renderer/src/components/Crash/index.jsx create mode 100644 packages/gui/src/renderer/src/components/Crash/index.less diff --git a/packages/core/src/handlers/checkUpdate.js b/packages/core/src/handlers/checkUpdate.js new file mode 100644 index 0000000..48fe56b --- /dev/null +++ b/packages/core/src/handlers/checkUpdate.js @@ -0,0 +1,43 @@ +import Logger from "../logger" +import DB from "../db" + +import softRead from "./read" + +const Log = Logger.child({ service: "CHECK_UPDATE" }) + +export default async function checkUpdate(pkg_id) { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + Log.error("Package not found") + return false + } + + Log.info(`Checking update for [${pkg_id}]`) + + const remoteSoftManifest = await softRead(pkg.remote_manifest, { + soft: true + }) + + if (!remoteSoftManifest) { + Log.error("Cannot read remote manifest") + return false + } + + if (pkg.version === remoteSoftManifest.version) { + Log.info("No update available") + return false + } + + Log.info("Update available") + Log.info("Local:", pkg.version) + Log.info("Remote:", remoteSoftManifest.version) + Log.info("Changelog:", remoteSoftManifest.changelog_url) + + return { + id: pkg.id, + local: pkg.version, + remote: remoteSoftManifest.version, + changelog: remoteSoftManifest.changelog_url, + } +} \ No newline at end of file diff --git a/packages/core/src/helpers/setup.js b/packages/core/src/helpers/setup.js index 0058dcc..826c4c6 100644 --- a/packages/core/src/helpers/setup.js +++ b/packages/core/src/helpers/setup.js @@ -92,7 +92,9 @@ export default async () => { } ) } catch (error) { - await fs.promises.rm(prerequisite.destination) + if (fs.existsSync(prerequisite.destination)) { + await fs.promises.rm(prerequisite.destination) + } throw error } @@ -153,6 +155,12 @@ export default async () => { if (Array.isArray(prerequisite.moveDirs)) { for (const dir of prerequisite.moveDirs) { + if (Array.isArray(dir.requireOs)) { + if (!dir.requireOs.includes(resolveOs())) { + continue + } + } + Log.info(`Moving ${dir.from} to ${dir.to}...`) global._relic_eventBus.emit("app:setup", { @@ -182,9 +190,10 @@ export default async () => { message: error.message, }) - Log.error(error) Log.error("Aborting setup due to an error...") - return false + Log.error(error) + + throw error } Log.info(`All prerequisites are ready!`) diff --git a/packages/core/src/index.js b/packages/core/src/index.js index 841c1f7..de39263 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -17,6 +17,7 @@ import PackageApply from "./handlers/apply" import PackageList from "./handlers/list" import PackageRead from "./handlers/read" import PackageAuthorize from "./handlers/authorize" +import PackageCheckUpdate from "./handlers/checkUpdate" export default class RelicCore { constructor(params) { @@ -27,6 +28,8 @@ export default class RelicCore { logger = Logger + db = DB + async initialize() { await DB.initialize() @@ -52,6 +55,7 @@ export default class RelicCore { list: PackageList, read: PackageRead, authorize: PackageAuthorize, + checkUpdate: PackageCheckUpdate } openPath(pkg_id) { diff --git a/packages/core/src/prerequisites.js b/packages/core/src/prerequisites.js index b2f357d..9fb7df7 100644 --- a/packages/core/src/prerequisites.js +++ b/packages/core/src/prerequisites.js @@ -26,7 +26,7 @@ export default [ { id: "rclone-bin", finalBin: Vars.rclone_bin, - url: resolveRemoteBinPath(`${baseURL}/rclone-bin`, "rclone-bin.zip"), + url: resolveRemoteBinPath(`${baseURL}/rclone`, "rclone-bin.zip"), destination: path.resolve(Vars.binaries_path, "rclone-bin.zip"), extract: path.resolve(Vars.binaries_path, "rclone-bin"), requireOs: ["win32"], @@ -58,6 +58,7 @@ export default [ extractTargetFromName: true, moveDirs: [ { + requireOs: ["macos"], from: path.resolve(Vars.binaries_path, "java_jre_bin", "zulu-22.jre", "Contents"), to: path.resolve(Vars.binaries_path, "java_jre_bin", "Contents"), deleteParentBefore: true diff --git a/packages/gui/src/main/index.js b/packages/gui/src/main/index.js index c21f305..5184c4a 100644 --- a/packages/gui/src/main/index.js +++ b/packages/gui/src/main/index.js @@ -3,7 +3,7 @@ global.SettingsStore = new Store({ watch: true, }) -import RelicCore from "@ragestudio/relic-core/src" +import RelicCore from "../../../core/src" import CoreAdapter from "./classes/CoreAdapter" import sendToRender from "./utils/sendToRender" @@ -63,7 +63,16 @@ class ElectronApp { "pkg:uninstall": async (event, pkg_id) => { return await this.core.package.uninstall(pkg_id) }, - "pkg:execute": async (event, pkg_id) => { + "pkg:execute": async (event, pkg_id, { force = false } = {}) => { + // check for updates first + if (!force) { + const update = await this.core.package.checkUpdate(pkg_id) + + if (update) { + return sendToRender("pkg:update_available", update) + } + } + return await this.core.package.execute(pkg_id) }, "pkg:open": async (event, pkg_id) => { @@ -93,18 +102,22 @@ class ElectronApp { try { await this.core.initialize() await this.core.setup() - } catch (err) { - console.error(err) - sendToRender("new:notification", { - message: "Setup failed", - description: err.message + return { + pkg: pkg, + authorizedServices: {} + } + } catch (error) { + console.error(error) + + sendToRender("app:init:failed", { + message: "Initalization failed", + error: error, }) - } - return { - pkg: pkg, - authorizedServices: {} + return { + error + } } } } diff --git a/packages/gui/src/renderer/assets/bruh_fox.jpg b/packages/gui/src/renderer/assets/bruh_fox.jpg new file mode 100644 index 0000000000000000000000000000000000000000..bc3399e700c9fc695452e99a177ae071a130789d GIT binary patch literal 337649 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�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

rsa)NZdeW(J# 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

)g~?~Nuv3FBIG~y}C1AV? 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

rKSscDr6K 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 MIdyoIj9KXa14#J3VgLXD literal 0 HcmV?d00001 diff --git a/packages/gui/src/renderer/src/App.jsx b/packages/gui/src/renderer/src/App.jsx index 995491d..3323cb8 100644 --- a/packages/gui/src/renderer/src/App.jsx +++ b/packages/gui/src/renderer/src/App.jsx @@ -59,7 +59,9 @@ class App extends React.Component { } this.setState({ - updateAvailable: true, + appUpdate: { + available: true, + }, }) app.appUpdateAvailable(data) @@ -79,15 +81,23 @@ class App extends React.Component { app.pkgUpdateAvailable(data) }, "pkg:installation:invoked": (event, data) => { - if (this.state.initializing) { - return false - } + if (this.state.initializing) { + return false + } - app.invokeInstall(data) + app.invokeInstall(data) + }, + "app:init:failed": (event, data) => { + this.setState({ + crash: data, + }) } } componentDidMount = async () => { + console.log(`React version > ${versions["react"]}`) + console.log(`DOMRouter version > ${versions["react-router-dom"]}`) + window.app.style.appendClassname("initializing") for (const event in this.ipcEvents) { @@ -96,10 +106,12 @@ class App extends React.Component { const mainInitialization = await ipc.exec("app:init") - console.log(`React version > ${versions["react"]}`) - console.log(`DOMRouter version > ${versions["react-router-dom"]}`) console.log(`app:init() | Result >`, mainInitialization) + if (mainInitialization.error) { + return false + } + await this.setState({ initializing: false, pkg: mainInitialization.pkg, diff --git a/packages/gui/src/renderer/src/GlobalApp.jsx b/packages/gui/src/renderer/src/GlobalApp.jsx index 432d170..4a50a20 100644 --- a/packages/gui/src/renderer/src/GlobalApp.jsx +++ b/packages/gui/src/renderer/src/GlobalApp.jsx @@ -29,12 +29,6 @@ class GlobalStyleController { export default class GlobalCTXApp { static style = GlobalStyleController - static applyUpdate = () => { - message.loading("Updating, please wait...") - - ipc.exec("updater:apply") - } - static invokeInstall = (manifest) => { console.log(`installation invoked >`, manifest) @@ -80,6 +74,12 @@ export default class GlobalCTXApp { }) } + static applyUpdate = () => { + message.loading("Updating, please wait...") + + ipc.exec("updater:apply") + } + static checkUpdates = () => { ipc.exec("updater:check") } diff --git a/packages/gui/src/renderer/src/components/Crash/index.jsx b/packages/gui/src/renderer/src/components/Crash/index.jsx new file mode 100644 index 0000000..dfad2bc --- /dev/null +++ b/packages/gui/src/renderer/src/components/Crash/index.jsx @@ -0,0 +1,27 @@ +import React from "react" +import "./index.less" + +const Crash = (props) => { + const { crash } = props + + return
+
+ +
+ +

Crash

+

The application has encontered a critical error that cannot handle it, so must be terminated.

+ +
+

Detailed error:

+ + + {JSON.stringify(crash, null, 2)} + +
+
+} + +export default Crash \ No newline at end of file diff --git a/packages/gui/src/renderer/src/components/Crash/index.less b/packages/gui/src/renderer/src/components/Crash/index.less new file mode 100644 index 0000000..54aa325 --- /dev/null +++ b/packages/gui/src/renderer/src/components/Crash/index.less @@ -0,0 +1,56 @@ +.app-crash { + display: flex; + flex-direction: column; + + height: 100%; + + gap: 20px; + + h1 { + font-size: 1.5rem; + font-weight: bold; + } + + .crash-icon { + display: flex; + flex-direction: row; + + justify-content: center; + align-items: center; + + width: 100%; + + img { + width: 200px; + height: 200px; + + object-fit: contain; + + border-radius: 12px; + } + } + + .crash-details { + display: flex; + flex-direction: column; + + width: 100%; + + gap: 7px; + + code { + background-color: var(--background-color-secondary); + + padding: 10px; + + border-radius: 12px; + + font-family: "DM Mono", monospace; + + font-size: 0.8rem; + + word-break: break-all; + white-space: pre-wrap; + } + } +} \ No newline at end of file diff --git a/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx index 03c9e15..99a6eb9 100644 --- a/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx +++ b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx @@ -16,7 +16,7 @@ const PackageUpdateAvailable = ({ update, close }) => { } function handleUpdate() { - ipc.exec("pkg:update", update.manifest.id, { + ipc.exec("pkg:update", update.id, { execOnFinish: true }) @@ -24,7 +24,7 @@ const PackageUpdateAvailable = ({ update, close }) => { } function handleContinue() { - ipc.exec("pkg:execute", update.manifest.id, { + ipc.exec("pkg:execute", update.id, { force: true }) @@ -38,7 +38,7 @@ const PackageUpdateAvailable = ({ update, close }) => {

- {update.current_version} {`->`} {update.new_version} + {update.local} {`->`} {update.remote}

diff --git a/packages/gui/src/renderer/src/components/Splash/index.jsx b/packages/gui/src/renderer/src/components/Splash/index.jsx index d0fa2cc..5ab9e95 100644 --- a/packages/gui/src/renderer/src/components/Splash/index.jsx +++ b/packages/gui/src/renderer/src/components/Splash/index.jsx @@ -22,11 +22,13 @@ const Splash = (props) => { { globalState.appSetup.message && <> +
{globalState.appSetup.message}
+
{ } { - ctx.updateAvailable && } onClick={app.applyUpdate} type="primary" > - Update now + Update app } diff --git a/packages/gui/src/renderer/src/router.jsx b/packages/gui/src/renderer/src/router.jsx index 599c16e..33c696b 100644 --- a/packages/gui/src/renderer/src/router.jsx +++ b/packages/gui/src/renderer/src/router.jsx @@ -7,6 +7,7 @@ import loadable from "@loadable/component" import GlobalStateContext from "contexts/global" import SplashScreen from "components/Splash" +import CrashError from "components/Crash" const DefaultNotFoundRender = () => { return
Not found
@@ -130,6 +131,18 @@ export const InternalRouter = (props) => { } export const PageRender = (props) => { + const globalState = React.useContext(GlobalStateContext) + + if (globalState.crash) { + return + } + + if (globalState.initializing) { + return + } + const routes = React.useMemo(() => { let paths = { ...import.meta.glob("/src/pages/**/[a-z[]*.jsx"), @@ -154,12 +167,6 @@ export const PageRender = (props) => { return paths }, []) - const globalState = React.useContext(GlobalStateContext) - - if (globalState.initializing) { - return - } - return { routes.map((route, index) => { diff --git a/packages/gui/src/renderer/src/style/index.less b/packages/gui/src/renderer/src/style/index.less index bf6bd32..b53479d 100644 --- a/packages/gui/src/renderer/src/style/index.less +++ b/packages/gui/src/renderer/src/style/index.less @@ -15,6 +15,19 @@ --app_global_padding: @var-app_global_padding; } +::-webkit-scrollbar { + width: 6px; +} + +::-webkit-scrollbar-track { + opacity: 0; +} + +::-webkit-scrollbar-thumb { + background-color: rgba(255, 255, 255, 0.1); + border-radius: 10px; +} + html, body { padding: 0; From 74bb53ada4cb2e8cc7afedf02199874e04bd63ae Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 2 Apr 2024 18:47:20 +0200 Subject: [PATCH 07/14] move core to another repo --- .gitmodules | 3 +++ relic-core | 1 + 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 relic-core diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..1095958 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "relic-core"] + path = relic-core + url = https://github.com/ragestudio/relic-core diff --git a/relic-core b/relic-core new file mode 160000 index 0000000..5da2cb0 --- /dev/null +++ b/relic-core @@ -0,0 +1 @@ +Subproject commit 5da2cb0765d75f51222414b3e7e690e0d74d408d From 5abcd2640c73f1ef172593e90e3837e6c4f7fc6f Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 2 Apr 2024 18:47:41 +0200 Subject: [PATCH 08/14] merge from local --- .editorconfig | 9 - .vscode/extensions.json | 3 - .vscode/launch.json | 39 - .vscode/settings.json | 27 - ...ectron-builder.yml => electron-builder.yml | 0 ....vite.config.js => electron.vite.config.js | 0 package.json | 57 +- packages/cli/bin | 2 - packages/cli/package.json | 21 - packages/cli/src/index.js | 169 ---- packages/core/package.json | 38 - packages/core/src/classes/ManifestAuthDB.js | 36 - packages/core/src/classes/ManifestConfig.js | 34 - packages/core/src/classes/PatchManager.js | 149 --- packages/core/src/db.js | 115 --- packages/core/src/generic_steps/git_clone.js | 49 - packages/core/src/generic_steps/git_pull.js | 33 - packages/core/src/generic_steps/git_reset.js | 83 -- packages/core/src/generic_steps/http.js | 66 -- packages/core/src/generic_steps/index.js | 48 - packages/core/src/handlers/apply.js | 95 -- packages/core/src/handlers/authorize.js | 33 - packages/core/src/handlers/checkUpdate.js | 43 - packages/core/src/handlers/execute.js | 80 -- packages/core/src/handlers/install.js | 182 ---- packages/core/src/handlers/list.js | 5 - packages/core/src/handlers/read.js | 9 - packages/core/src/handlers/uninstall.js | 74 -- packages/core/src/handlers/update.js | 128 --- packages/core/src/helpers/downloadHttpFile.js | 73 -- packages/core/src/helpers/sendToRender.js | 43 - packages/core/src/helpers/setup.js | 201 ---- packages/core/src/index.js | 68 -- packages/core/src/libraries/execa/index.d.ts | 955 ------------------ packages/core/src/libraries/execa/index.js | 309 ------ .../core/src/libraries/execa/lib/command.js | 119 --- .../core/src/libraries/execa/lib/error.js | 87 -- packages/core/src/libraries/execa/lib/kill.js | 102 -- packages/core/src/libraries/execa/lib/pipe.js | 42 - .../core/src/libraries/execa/lib/promise.js | 36 - .../core/src/libraries/execa/lib/stdio.js | 49 - .../core/src/libraries/execa/lib/stream.js | 133 --- .../core/src/libraries/execa/lib/verbose.js | 19 - .../src/libraries/get-stream/array-buffer.js | 84 -- .../core/src/libraries/get-stream/array.js | 32 - .../core/src/libraries/get-stream/buffer.js | 20 - .../core/src/libraries/get-stream/contents.js | 101 -- .../core/src/libraries/get-stream/index.d.ts | 119 --- .../core/src/libraries/get-stream/index.js | 5 - .../src/libraries/get-stream/index.test-d.ts | 98 -- .../core/src/libraries/get-stream/string.js | 36 - .../core/src/libraries/get-stream/utils.js | 11 - .../core/src/libraries/human-signals/core.js | 275 ----- .../core/src/libraries/human-signals/index.js | 70 -- .../src/libraries/human-signals/realtime.js | 16 - .../src/libraries/human-signals/signals.js | 34 - .../core/src/libraries/is-stream/index.d.ts | 81 -- .../core/src/libraries/is-stream/index.js | 29 - .../src/libraries/lowdb/adapters/Memory.js | 24 - .../libraries/lowdb/adapters/node/DataFile.js | 51 - .../libraries/lowdb/adapters/node/JSONFile.js | 19 - .../libraries/lowdb/adapters/node/TextFile.js | 65 -- packages/core/src/libraries/lowdb/core/Low.js | 48 - .../core/src/libraries/lowdb/presets/node.js | 23 - .../core/src/libraries/lowdb/steno/index.js | 98 -- .../src/libraries/mimic-function/index.js | 71 -- .../src/libraries/npm-run-path/index.d.ts | 84 -- .../core/src/libraries/npm-run-path/index.js | 51 - .../core/src/libraries/onetime/index.d.ts | 59 -- packages/core/src/libraries/onetime/index.js | 41 - .../libraries/strip-final-newline/index.d.ts | 18 - .../libraries/strip-final-newline/index.js | 26 - packages/core/src/logger.js | 40 - packages/core/src/manifest/libraries.js | 23 - packages/core/src/manifest/libs/auth/index.js | 54 - packages/core/src/manifest/libs/fs/index.js | 39 - packages/core/src/manifest/libs/index.js | 15 - .../src/manifest/libs/mcl/authenticator.js | 167 --- .../core/src/manifest/libs/mcl/handler.js | 783 -------------- packages/core/src/manifest/libs/mcl/index.js | 49 - .../core/src/manifest/libs/mcl/launcher.js | 224 ---- packages/core/src/manifest/libs/open/index.js | 15 - packages/core/src/manifest/libs/path/index.js | 3 - packages/core/src/manifest/reader.js | 51 - packages/core/src/manifest/vm.js | 83 -- packages/core/src/prerequisites.js | 70 -- packages/core/src/utils/chmodRecursive.js | 16 - packages/core/src/utils/extractFile.js | 48 - packages/core/src/utils/parseStringVars.js | 21 - packages/core/src/utils/readDirRecurse.js | 25 - packages/core/src/utils/resolveOs.js | 17 - .../core/src/utils/resolveRemoteBinPath.js | 15 - packages/core/src/vars.js | 35 - packages/gui/.gitignore | 41 - packages/gui/package.json | 54 - .../gui/resources => resources}/icon.ico | Bin .../gui/resources => resources}/icon.png | Bin .../gui/resources => resources}/icon.svg | 0 scripts/postinstall.js | 35 - .../src => src}/main/classes/CoreAdapter.js | 0 {packages/gui/src => src}/main/index.js | 2 +- .../src => src}/main/utils/sendToRender.js | 0 {packages/gui/src => src}/preload/index.js | 0 .../src => src}/renderer/assets/bruh_fox.jpg | Bin .../gui/src => src}/renderer/assets/icon.jsx | 0 .../renderer/config/paths_decorators.js | 0 {packages/gui/src => src}/renderer/index.html | 0 .../gui/src => src}/renderer/src/App.jsx | 0 .../src => src}/renderer/src/GlobalApp.jsx | 0 .../renderer/src/components/Crash/index.jsx | 0 .../renderer/src/components/Crash/index.less | 0 .../renderer/src/components/Icons/index.jsx | 0 .../src/components/InstallConfigAsk/index.jsx | 0 .../components/InstallConfigAsk/index.less | 0 .../src/components/ManifestInfo/index.jsx | 0 .../src/components/ManifestInfo/index.less | 0 .../src/components/NewInstallation/index.jsx | 0 .../src/components/NewInstallation/index.less | 0 .../components/PackageConfigItem/index.jsx | 0 .../src/components/PackageItem/index.jsx | 0 .../src/components/PackageItem/index.less | 0 .../PackageUpdateAvailable/index.jsx | 0 .../PackageUpdateAvailable/index.less | 0 .../renderer/src/components/Splash/index.jsx | 0 .../renderer/src/components/Splash/index.less | 0 .../renderer/src/contexts/global.js | 0 .../renderer/src/contexts/packages.jsx | 0 .../src/layout/components/Drawer/index.jsx | 0 .../src/layout/components/Header/index.jsx | 0 .../src/layout/components/Header/index.less | 0 .../layout/components/ModalDialog/index.jsx | 0 .../src => src}/renderer/src/layout/index.jsx | 0 .../gui/src => src}/renderer/src/main.jsx | 0 .../src => src}/renderer/src/pages/index.jsx | 0 .../src => src}/renderer/src/pages/index.less | 0 .../renderer/src/pages/pkg/[pkg_id].jsx | 0 .../renderer/src/pages/pkg/index.less | 0 .../renderer/src/pages/settings/index.jsx | 0 .../renderer/src/pages/settings/index.less | 0 .../gui/src => src}/renderer/src/router.jsx | 0 .../renderer/src/settings_list.jsx | 0 .../src => src}/renderer/src/style/fix.less | 0 .../src => src}/renderer/src/style/index.less | 0 .../src => src}/renderer/src/style/reset.css | 0 .../src => src}/renderer/src/style/vars.less | 0 .../renderer/src/utils/getRootCssVar/index.js | 0 .../renderer/src/utils/getVersions/index.js | 0 147 files changed, 50 insertions(+), 7427 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .vscode/extensions.json delete mode 100644 .vscode/launch.json delete mode 100644 .vscode/settings.json rename packages/gui/electron-builder.yml => electron-builder.yml (100%) rename packages/gui/electron.vite.config.js => electron.vite.config.js (100%) delete mode 100755 packages/cli/bin delete mode 100644 packages/cli/package.json delete mode 100644 packages/cli/src/index.js delete mode 100644 packages/core/package.json delete mode 100644 packages/core/src/classes/ManifestAuthDB.js delete mode 100644 packages/core/src/classes/ManifestConfig.js delete mode 100644 packages/core/src/classes/PatchManager.js delete mode 100644 packages/core/src/db.js delete mode 100644 packages/core/src/generic_steps/git_clone.js delete mode 100644 packages/core/src/generic_steps/git_pull.js delete mode 100644 packages/core/src/generic_steps/git_reset.js delete mode 100644 packages/core/src/generic_steps/http.js delete mode 100644 packages/core/src/generic_steps/index.js delete mode 100644 packages/core/src/handlers/apply.js delete mode 100644 packages/core/src/handlers/authorize.js delete mode 100644 packages/core/src/handlers/checkUpdate.js delete mode 100644 packages/core/src/handlers/execute.js delete mode 100644 packages/core/src/handlers/install.js delete mode 100644 packages/core/src/handlers/list.js delete mode 100644 packages/core/src/handlers/read.js delete mode 100644 packages/core/src/handlers/uninstall.js delete mode 100644 packages/core/src/handlers/update.js delete mode 100644 packages/core/src/helpers/downloadHttpFile.js delete mode 100644 packages/core/src/helpers/sendToRender.js delete mode 100644 packages/core/src/helpers/setup.js delete mode 100644 packages/core/src/index.js delete mode 100755 packages/core/src/libraries/execa/index.d.ts delete mode 100755 packages/core/src/libraries/execa/index.js delete mode 100755 packages/core/src/libraries/execa/lib/command.js delete mode 100755 packages/core/src/libraries/execa/lib/error.js delete mode 100755 packages/core/src/libraries/execa/lib/kill.js delete mode 100755 packages/core/src/libraries/execa/lib/pipe.js delete mode 100755 packages/core/src/libraries/execa/lib/promise.js delete mode 100755 packages/core/src/libraries/execa/lib/stdio.js delete mode 100755 packages/core/src/libraries/execa/lib/stream.js delete mode 100755 packages/core/src/libraries/execa/lib/verbose.js delete mode 100644 packages/core/src/libraries/get-stream/array-buffer.js delete mode 100644 packages/core/src/libraries/get-stream/array.js delete mode 100644 packages/core/src/libraries/get-stream/buffer.js delete mode 100644 packages/core/src/libraries/get-stream/contents.js delete mode 100644 packages/core/src/libraries/get-stream/index.d.ts delete mode 100644 packages/core/src/libraries/get-stream/index.js delete mode 100644 packages/core/src/libraries/get-stream/index.test-d.ts delete mode 100644 packages/core/src/libraries/get-stream/string.js delete mode 100644 packages/core/src/libraries/get-stream/utils.js delete mode 100644 packages/core/src/libraries/human-signals/core.js delete mode 100644 packages/core/src/libraries/human-signals/index.js delete mode 100644 packages/core/src/libraries/human-signals/realtime.js delete mode 100644 packages/core/src/libraries/human-signals/signals.js delete mode 100644 packages/core/src/libraries/is-stream/index.d.ts delete mode 100644 packages/core/src/libraries/is-stream/index.js delete mode 100644 packages/core/src/libraries/lowdb/adapters/Memory.js delete mode 100644 packages/core/src/libraries/lowdb/adapters/node/DataFile.js delete mode 100644 packages/core/src/libraries/lowdb/adapters/node/JSONFile.js delete mode 100644 packages/core/src/libraries/lowdb/adapters/node/TextFile.js delete mode 100644 packages/core/src/libraries/lowdb/core/Low.js delete mode 100644 packages/core/src/libraries/lowdb/presets/node.js delete mode 100644 packages/core/src/libraries/lowdb/steno/index.js delete mode 100644 packages/core/src/libraries/mimic-function/index.js delete mode 100644 packages/core/src/libraries/npm-run-path/index.d.ts delete mode 100644 packages/core/src/libraries/npm-run-path/index.js delete mode 100644 packages/core/src/libraries/onetime/index.d.ts delete mode 100644 packages/core/src/libraries/onetime/index.js delete mode 100644 packages/core/src/libraries/strip-final-newline/index.d.ts delete mode 100644 packages/core/src/libraries/strip-final-newline/index.js delete mode 100644 packages/core/src/logger.js delete mode 100644 packages/core/src/manifest/libraries.js delete mode 100644 packages/core/src/manifest/libs/auth/index.js delete mode 100644 packages/core/src/manifest/libs/fs/index.js delete mode 100644 packages/core/src/manifest/libs/index.js delete mode 100644 packages/core/src/manifest/libs/mcl/authenticator.js delete mode 100644 packages/core/src/manifest/libs/mcl/handler.js delete mode 100644 packages/core/src/manifest/libs/mcl/index.js delete mode 100644 packages/core/src/manifest/libs/mcl/launcher.js delete mode 100644 packages/core/src/manifest/libs/open/index.js delete mode 100644 packages/core/src/manifest/libs/path/index.js delete mode 100644 packages/core/src/manifest/reader.js delete mode 100644 packages/core/src/manifest/vm.js delete mode 100644 packages/core/src/prerequisites.js delete mode 100644 packages/core/src/utils/chmodRecursive.js delete mode 100644 packages/core/src/utils/extractFile.js delete mode 100644 packages/core/src/utils/parseStringVars.js delete mode 100644 packages/core/src/utils/readDirRecurse.js delete mode 100644 packages/core/src/utils/resolveOs.js delete mode 100644 packages/core/src/utils/resolveRemoteBinPath.js delete mode 100644 packages/core/src/vars.js delete mode 100644 packages/gui/.gitignore delete mode 100644 packages/gui/package.json rename {packages/gui/resources => resources}/icon.ico (100%) rename {packages/gui/resources => resources}/icon.png (100%) rename {packages/gui/resources => resources}/icon.svg (100%) delete mode 100644 scripts/postinstall.js rename {packages/gui/src => src}/main/classes/CoreAdapter.js (100%) rename {packages/gui/src => src}/main/index.js (99%) rename {packages/gui/src => src}/main/utils/sendToRender.js (100%) rename {packages/gui/src => src}/preload/index.js (100%) rename {packages/gui/src => src}/renderer/assets/bruh_fox.jpg (100%) rename {packages/gui/src => src}/renderer/assets/icon.jsx (100%) rename {packages/gui/src => src}/renderer/config/paths_decorators.js (100%) rename {packages/gui/src => src}/renderer/index.html (100%) rename {packages/gui/src => src}/renderer/src/App.jsx (100%) rename {packages/gui/src => src}/renderer/src/GlobalApp.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/Crash/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/Crash/index.less (100%) rename {packages/gui/src => src}/renderer/src/components/Icons/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/InstallConfigAsk/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/InstallConfigAsk/index.less (100%) rename {packages/gui/src => src}/renderer/src/components/ManifestInfo/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/ManifestInfo/index.less (100%) rename {packages/gui/src => src}/renderer/src/components/NewInstallation/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/NewInstallation/index.less (100%) rename {packages/gui/src => src}/renderer/src/components/PackageConfigItem/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/PackageItem/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/PackageItem/index.less (100%) rename {packages/gui/src => src}/renderer/src/components/PackageUpdateAvailable/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/PackageUpdateAvailable/index.less (100%) rename {packages/gui/src => src}/renderer/src/components/Splash/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/components/Splash/index.less (100%) rename {packages/gui/src => src}/renderer/src/contexts/global.js (100%) rename {packages/gui/src => src}/renderer/src/contexts/packages.jsx (100%) rename {packages/gui/src => src}/renderer/src/layout/components/Drawer/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/layout/components/Header/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/layout/components/Header/index.less (100%) rename {packages/gui/src => src}/renderer/src/layout/components/ModalDialog/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/layout/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/main.jsx (100%) rename {packages/gui/src => src}/renderer/src/pages/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/pages/index.less (100%) rename {packages/gui/src => src}/renderer/src/pages/pkg/[pkg_id].jsx (100%) rename {packages/gui/src => src}/renderer/src/pages/pkg/index.less (100%) rename {packages/gui/src => src}/renderer/src/pages/settings/index.jsx (100%) rename {packages/gui/src => src}/renderer/src/pages/settings/index.less (100%) rename {packages/gui/src => src}/renderer/src/router.jsx (100%) rename {packages/gui/src => src}/renderer/src/settings_list.jsx (100%) rename {packages/gui/src => src}/renderer/src/style/fix.less (100%) rename {packages/gui/src => src}/renderer/src/style/index.less (100%) rename {packages/gui/src => src}/renderer/src/style/reset.css (100%) rename {packages/gui/src => src}/renderer/src/style/vars.less (100%) rename {packages/gui/src => src}/renderer/src/utils/getRootCssVar/index.js (100%) rename {packages/gui/src => src}/renderer/src/utils/getVersions/index.js (100%) diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index cf640d5..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index 940260d..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "recommendations": ["dbaeumer.vscode-eslint"] -} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 0b6b9a6..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "name": "Debug Main Process", - "type": "node", - "request": "launch", - "cwd": "${workspaceRoot}", - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", - "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" - }, - "runtimeArgs": ["--sourcemap"], - "env": { - "REMOTE_DEBUGGING_PORT": "9222" - } - }, - { - "name": "Debug Renderer Process", - "port": 9222, - "request": "attach", - "type": "chrome", - "webRoot": "${workspaceFolder}/src/renderer", - "timeout": 60000, - "presentation": { - "hidden": true - } - } - ], - "compounds": [ - { - "name": "Debug All", - "configurations": ["Debug Main Process", "Debug Renderer Process"], - "presentation": { - "order": 1 - } - } - ] -} diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 71d5ef0..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "[typescript]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "[javascript]": { - "editor.defaultFormatter": "vscode.typescript-language-features" - }, - "[json]": { - "editor.defaultFormatter": "esbenp.prettier-vscode" - }, - "cSpell.words": [ - "admzip", - "antd", - "APPDATA", - "catched", - "Classname", - "execa", - "getstation", - "imjs", - "ragestudio", - "rclone", - "sevenzip", - "unzipper", - "upath", - "userdata" - ] -} diff --git a/packages/gui/electron-builder.yml b/electron-builder.yml similarity index 100% rename from packages/gui/electron-builder.yml rename to electron-builder.yml diff --git a/packages/gui/electron.vite.config.js b/electron.vite.config.js similarity index 100% rename from packages/gui/electron.vite.config.js rename to electron.vite.config.js diff --git a/package.json b/package.json index 674dfc8..55f9050 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,54 @@ { - "name": "@ragestudio/relic", - "private": true, - "workspaces": [ - "packages/*" - ], - "repository": "https://github.com/srgooglo/rs_bundler", - "author": "SrGooglo ", + "name": "@ragestudio/relic-gui", + "version": "0.17.0", + "description": "RageStudio Relic, yet another package manager.", + "main": "./out/main/index.js", + "author": "RageStudio", "license": "MIT", "scripts": { - "postinstall": "node scripts/postinstall.js" + "start": "electron-vite preview", + "dev": "electron-vite dev", + "build": "electron-vite build", + "postinstall": "electron-builder install-app-deps", + "pack:win": "electron-builder --win --config", + "pack:mac": "electron-builder --mac --config", + "pack:linux": "electron-builder --linux --config", + "build:win": "npm run build && npm run pack:win", + "build:mac": "npm run build && npm run pack:mac", + "build:linux": "npm run build && npm run pack:linux" + }, + "dependencies": { + "@electron-toolkit/preload": "^2.0.0", + "@electron-toolkit/utils": "^2.0.0", + "@getstation/electron-google-oauth2": "^14.0.0", + "@imjs/electron-differential-updater": "^5.1.7", + "@loadable/component": "^5.16.3", + "@ragestudio/relic-core": "^0.17.0", + "antd": "^5.13.2", + "classnames": "^2.3.2", + "electron-differential-updater": "^4.3.2", + "electron-is-dev": "^2.0.0", + "electron-store": "^8.1.0", + "electron-updater": "^6.1.1", + "got": "11.8.3", + "human-format": "^1.2.0", + "protocol-registry": "^1.4.1", + "less": "^4.2.0", + "lodash": "^4.17.21", + "react-icons": "^4.11.0", + "react-motion": "0.5.2", + "react-router-dom": "6.6.2", + "react-spinners": "^0.13.8", + "react-spring": "^9.7.3" + }, + "devDependencies": { + "@ragestudio/hermes": "^0.1.1", + "@vitejs/plugin-react": "^4.0.4", + "electron": "25.6.0", + "electron-builder": "24.6.3", + "electron-vite": "^2.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite": "^4.4.9" } } diff --git a/packages/cli/bin b/packages/cli/bin deleted file mode 100755 index e1b3d5a..0000000 --- a/packages/cli/bin +++ /dev/null @@ -1,2 +0,0 @@ -#!/usr/bin/env node -require("./dist/index.js") \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json deleted file mode 100644 index 0103959..0000000 --- a/packages/cli/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "@ragestudio/relic-cli", - "version": "0.17.0", - "license": "MIT", - "author": "RageStudio", - "description": "RageStudio Relic, yet another package manager.", - "main": "./dist/index.js", - "bin": { - "relic": "./bin.js" - }, - "scripts": { - "dev": "hermes-node ./src/index.js", - "build": "hermes build" - }, - "dependencies": { - "commander": "^12.0.0" - }, - "devDependencies": { - "@ragestudio/hermes": "^0.1.1" - } -} diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js deleted file mode 100644 index b507daf..0000000 --- a/packages/cli/src/index.js +++ /dev/null @@ -1,169 +0,0 @@ -import RelicCore from "@ragestudio/relic-core" -import { program, Command, Argument } from "commander" - -import pkg from "../package.json" - -const commands = [ - { - cmd: "install", - description: "Install a package manifest from a path or URL", - arguments: [ - { - name: "package_manifest", - description: "Path or URL to a package manifest", - } - ], - fn: async (package_manifest, options) => { - await core.initialize() - await core.setup() - - return await core.package.install(package_manifest, options) - } - }, - { - cmd: "run", - description: "Execute a package", - arguments: [ - { - name: "id", - description: "The id of the package to execute", - } - ], - fn: async (pkg_id, options) => { - await core.initialize() - await core.setup() - - return await core.package.execute(pkg_id, options) - } - }, - { - cmd: "update", - description: "Update a package", - arguments: [ - { - name: "id", - description: "The id of the package to update", - } - ], - fn: async (pkg_id, options) => { - await core.initialize() - await core.setup() - - return await core.package.update(pkg_id, options) - } - }, - { - cmd: "uninstall", - description: "Uninstall a package", - arguments: [ - { - name: "id", - description: "The id of the package to uninstall", - } - ], - fn: async (pkg_id, options) => { - await core.initialize() - - return await core.package.uninstall(pkg_id, options) - } - }, - { - cmd: "apply", - description: "Apply changes to a installed package", - arguments: [ - { - name: "id", - description: "The id of the package to apply changes to", - }, - ], - options: [ - { - name: "add_patches", - description: "Add patches to the package", - }, - { - name: "remove_patches", - description: "Remove patches from the package", - }, - ], - fn: async (pkg_id, options) => { - await core.initialize() - - return await core.package.apply(pkg_id, options) - } - }, - { - cmd: "list", - description: "List installed package manifests", - fn: async () => { - await core.initialize() - - return console.log(await core.package.list()) - } - }, - { - cmd: "open-path", - description: "Open the base path or a package path", - options: [ - { - name: "pkg_id", - description: "Path to open", - } - ], - fn: async (options) => { - await core.initialize() - - await core.openPath(options.pkg_id) - } - } -] - -async function main() { - global.core = new RelicCore() - - program - .name(pkg.name) - .description(pkg.description) - .version(pkg.version) - - for await (const command of commands) { - const cmd = new Command(command.cmd).action(command.fn) - - if (command.description) { - cmd.description(command.description) - } - - if (Array.isArray(command.arguments)) { - for await (const argument of command.arguments) { - if (typeof argument === "string") { - cmd.addArgument(new Argument(argument)) - } else { - const arg = new Argument(argument.name, argument.description) - - if (argument.default) { - arg.default(argument.default) - } - - cmd.addArgument(arg) - } - } - } - - if (Array.isArray(command.options)) { - for await (const option of command.options) { - if (typeof option === "string") { - cmd.option(option) - } else { - cmd.option(option.name, option.description, option.default) - } - } - } - - program.addCommand(cmd) - } - - program.parse() -} - - -main() \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json deleted file mode 100644 index ea3e7fc..0000000 --- a/packages/core/package.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "name": "@ragestudio/relic-core", - "version": "0.17.0", - "license": "MIT", - "author": "RageStudio", - "description": "RageStudio Relic, yet another package manager.", - "main": "./dist/index.js", - "files": [ - "dist", - "src" - ], - "scripts": { - "build": "hermes build" - }, - "dependencies": { - "@foxify/events": "^2.1.0", - "adm-zip": "^0.5.12", - "axios": "^1.6.8", - "checksum": "^1.0.0", - "cli-color": "^2.0.4", - "cli-progress": "^3.12.0", - "deep-object-diff": "^1.1.9", - "extends-classes": "^1.0.5", - "googleapis": "^134.0.0", - "human-format": "^1.2.0", - "merge-stream": "^2.0.0", - "module-alias": "^2.2.3", - "node-7z": "^3.0.0", - "open": "8.4.2", - "request": "^2.88.2", - "rimraf": "^5.0.5", - "signal-exit": "^4.1.0", - "unzipper": "^0.10.14", - "upath": "^2.0.1", - "uuid": "^9.0.1", - "winston": "^3.13.0" - } -} diff --git a/packages/core/src/classes/ManifestAuthDB.js b/packages/core/src/classes/ManifestAuthDB.js deleted file mode 100644 index fe32684..0000000 --- a/packages/core/src/classes/ManifestAuthDB.js +++ /dev/null @@ -1,36 +0,0 @@ -import path from "path" -import { JSONFilePreset } from "../libraries/lowdb/presets/node" - -import Vars from "../vars" - -//! WARNING: Please DO NOT storage any password or sensitive data here, -// cause its not use any encryption method, and it will be stored in plain text. -// This is intended to store session tokens among other vars. - -export default class ManifestAuthService { - static vaultPath = path.resolve(Vars.runtime_path, "auth.json") - - static async withDB() { - return await JSONFilePreset(ManifestAuthService.vaultPath, {}) - } - - static has = async (pkg_id) => { - const db = await this.withDB() - - return !!db.data[pkg_id] - } - - static set = async (pkg_id, value) => { - const db = await this.withDB() - - return await db.update((data) => { - data[pkg_id] = value - }) - } - - static get = async (pkg_id) => { - const db = await this.withDB() - - return await db.data[pkg_id] - } -} \ No newline at end of file diff --git a/packages/core/src/classes/ManifestConfig.js b/packages/core/src/classes/ManifestConfig.js deleted file mode 100644 index 7bc379b..0000000 --- a/packages/core/src/classes/ManifestConfig.js +++ /dev/null @@ -1,34 +0,0 @@ -import DB from "../db" - -export default class ManifestConfigManager { - constructor(pkg_id) { - this.pkg_id = pkg_id - this.config = null - } - - async initialize() { - const pkg = await DB.getPackages(this.pkg_id) ?? {} - - this.config = pkg.config - } - - set(key, value) { - this.config[key] = value - - DB.updatePackageById(pkg_id, { config: this.config }) - - return this.config - } - - get(key) { - return this.config[key] - } - - delete(key) { - delete this.config[key] - - DB.updatePackageById(pkg_id, { config: this.config }) - - return this.config - } -} \ No newline at end of file diff --git a/packages/core/src/classes/PatchManager.js b/packages/core/src/classes/PatchManager.js deleted file mode 100644 index 870d224..0000000 --- a/packages/core/src/classes/PatchManager.js +++ /dev/null @@ -1,149 +0,0 @@ -import Logger from "../logger" - -import DB from "../db" -import fs from "node:fs" - -import GenericSteps from "../generic_steps" -import parseStringVars from "../utils/parseStringVars" - -export default class PatchManager { - constructor(pkg, manifest) { - this.pkg = pkg - this.manifest = manifest - - this.log = Logger.child({ service: `PATCH-MANAGER|${pkg.id}` }) - } - - async get(select) { - if (!this.manifest.patches) { - return [] - } - - let list = [] - - if (typeof select === "undefined") { - list = this.manifest.patches - } - - if (Array.isArray(select)) { - for await (let id of select) { - const patch = this.manifest.patches.find((patch) => patch.id === id) - - if (patch) { - list.push(patch) - } - } - } - - return list - } - - async reapply() { - if (Array.isArray(this.pkg.applied_patches)) { - return await this.patch(this.pkg.applied_patches) - } - - return true - } - - async patch(select) { - const list = await this.get(select) - - for await (let patch of list) { - global._relic_eventBus.emit(`pkg:update:state`, { - id: this.pkg.id, - status_text: `Applying patch [${patch.id}]...`, - }) - - this.log.info(`Applying patch [${patch.id}]...`) - - if (Array.isArray(patch.additions)) { - this.log.info(`Applying ${patch.additions.length} Additions...`) - - for await (let addition of patch.additions) { - // resolve patch file - addition.file = await parseStringVars(addition.file, this.pkg) - - if (fs.existsSync(addition.file)) { - this.log.info(`Addition [${addition.file}] already exists. Skipping...`) - continue - } - - this.log.info(`Applying addition [${addition.file}]`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: this.pkg.id, - status_text: `Applying addition [${addition.file}]`, - }) - - await GenericSteps(this.pkg, addition.steps, this.log) - } - } - - if (!this.pkg.applied_patches.includes(patch.id)) { - this.pkg.applied_patches.push(patch.id) - } - } - - await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: this.pkg.id, - status_text: `${list.length} Patches applied`, - }) - - this.log.info(`${list.length} Patches applied`) - - return this.pkg - } - - async remove(select) { - const list = await this.get(select) - - for await (let patch of list) { - global._relic_eventBus.emit(`pkg:update:state`, { - id: this.pkg.id, - status_text: `Removing patch [${patch.id}]...`, - }) - - this.log.info(`Removing patch [${patch.id}]...`) - - if (Array.isArray(patch.additions)) { - this.log.info(`Removing ${patch.additions.length} Additions...`) - - for await (let addition of patch.additions) { - addition.file = await parseStringVars(addition.file, this.pkg) - - if (!fs.existsSync(addition.file)) { - this.log.info(`Addition [${addition.file}] does not exist. Skipping...`) - continue - } - - this.log.info(`Removing addition [${addition.file}]`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: this.pkg.id, - status_text: `Removing addition [${addition.file}]`, - }) - - await fs.promises.unlink(addition.file) - } - } - - this.pkg.applied_patches = this.pkg.applied_patches.filter((p) => { - return p !== patch.id - }) - } - - await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: this.pkg.id, - status_text: `${list.length} Patches removed`, - }) - - this.log.info(`${list.length} Patches removed`) - - return this.pkg - } -} \ No newline at end of file diff --git a/packages/core/src/db.js b/packages/core/src/db.js deleted file mode 100644 index 4d99499..0000000 --- a/packages/core/src/db.js +++ /dev/null @@ -1,115 +0,0 @@ -import { JSONFilePreset } from "./libraries/lowdb/presets/node" -import Vars from "./vars" -import pkg from "../package.json" -import fs from "node:fs" -import lodash from "lodash" - -export default class DB { - static get defaultRoot() { - return { - created_at_version: pkg.version, - packages: [], - } - } - - static defaultPackageState({ - id, - name, - icon, - version, - author, - install_path, - description, - license, - last_status, - remote_manifest, - local_manifest, - config, - executable, - }) { - return { - id: id, - name: name, - version: version, - icon: icon, - install_path: install_path, - description: description, - author: author, - license: license ?? "unlicensed", - local_manifest: local_manifest ?? null, - remote_manifest: remote_manifest ?? null, - applied_patches: [], - config: typeof config === "object" ? config : {}, - last_status: last_status ?? "installing", - last_update: null, - installed_at: null, - executable: executable ?? false, - } - } - - static async withDB() { - return await JSONFilePreset(Vars.db_path, DB.defaultRoot) - } - - static async initialize() { - await this.cleanOrphans() - } - - static async cleanOrphans() { - const list = await this.getPackages() - - for (const pkg of list) { - if (!fs.existsSync(pkg.install_path)) { - await this.deletePackage(pkg.id) - } - } - } - - static async getPackages(pkg_id) { - const db = await this.withDB() - - if (pkg_id) { - return db.data["packages"].find((i) => i.id === pkg_id) - } - - return db.data["packages"] - } - - static async writePackage(pkg) { - const db = await this.withDB() - - const prevIndex = db.data["packages"].findIndex((i) => i.id === pkg.id) - - if (prevIndex !== -1) { - db.data["packages"][prevIndex] = pkg - } else { - db.data["packages"].push(pkg) - } - - await db.write() - - return db.data - } - - static async updatePackageById(pkg_id, obj) { - let pkg = await this.getPackages(pkg_id) - - if (!pkg) { - throw new Error("Package not found") - } - - return await this.writePackage(lodash.merge({ ...pkg }, obj)) - } - - static async deletePackage(pkg_id) { - const db = await this.withDB() - - await db.update((data) => { - data["packages"] = data["packages"].filter((i) => i.id !== pkg_id) - - return data - }) - - return pkg_id - } -} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_clone.js b/packages/core/src/generic_steps/git_clone.js deleted file mode 100644 index da0a3c2..0000000 --- a/packages/core/src/generic_steps/git_clone.js +++ /dev/null @@ -1,49 +0,0 @@ -import Logger from "../logger" - -import path from "node:path" -import fs from "node:fs" -import upath from "upath" -import { execa } from "../libraries/execa" - -import Vars from "../vars" - -export default async (pkg, step) => { - if (!step.path) { - step.path = `.` - } - - const Log = Logger.child({ service: `GIT|${pkg.id}` }) - - const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" - const final_path = upath.normalizeSafe(path.resolve(pkg.install_path, step.path)) - - if (!fs.existsSync(final_path)) { - fs.mkdirSync(final_path, { recursive: true }) - } - - Log.info(`Cloning from [${step.url}]`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Cloning from [${step.url}]`, - }) - - const args = [ - "clone", - //`--depth ${step.depth ?? 1}`, - //"--filter=blob:none", - //"--filter=tree:0", - "--recurse-submodules", - "--remote-submodules", - step.url, - final_path, - ] - - await execa(gitCMD, args, { - cwd: final_path, - stdout: "inherit", - stderr: "inherit", - }) - - return pkg -} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_pull.js b/packages/core/src/generic_steps/git_pull.js deleted file mode 100644 index f60db44..0000000 --- a/packages/core/src/generic_steps/git_pull.js +++ /dev/null @@ -1,33 +0,0 @@ -import Logger from "../logger" - -import path from "node:path" -import fs from "node:fs" -import { execa } from "../libraries/execa" - -import Vars from "../vars" - -export default async (pkg, step) => { - if (!step.path) { - step.path = `.` - } - - const Log = Logger.child({ service: `GIT|${pkg.id}` }) - - const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" - const _path = path.resolve(pkg.install_path, step.path) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Pulling...`, - }) - - Log.info(`Pulling from HEAD...`) - - await execa(gitCMD, ["pull", "--rebase"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - return pkg -} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_reset.js b/packages/core/src/generic_steps/git_reset.js deleted file mode 100644 index 60c31f4..0000000 --- a/packages/core/src/generic_steps/git_reset.js +++ /dev/null @@ -1,83 +0,0 @@ -import Logger from "../logger" - -import path from "node:path" -import fs from "node:fs" -import { execa } from "../libraries/execa" - -import git_pull from "./git_pull" -import Vars from "../vars" - -export default async (pkg, step) => { - if (!step.path) { - step.path = `.` - } - - const Log = Logger.child({ service: `GIT|${pkg.id}` }) - - const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" - - const _path = path.resolve(pkg.install_path, step.path) - const from = step.from ?? "HEAD" - - if (!fs.existsSync(_path)) { - fs.mkdirSync(_path, { recursive: true }) - } - - Log.info(`Fetching from origin`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Fetching from origin...`, - }) - - // fetch from origin - await execa(gitCMD, ["fetch", "origin"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - Log.info(`Cleaning untracked files...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Cleaning untracked files...`, - }) - - await execa(gitCMD, ["clean", "-df"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - Log.info(`Resetting to ${from}`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Resetting to ${from}`, - }) - - await execa(gitCMD, ["reset", "--hard", from], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - // pull the latest - await git_pull(pkg, step) - - Log.info(`Checkout to HEAD`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Checkout to HEAD`, - }) - - await execa(gitCMD, ["checkout", "HEAD"], { - cwd: _path, - stdout: "inherit", - stderr: "inherit", - }) - - return pkg -} \ No newline at end of file diff --git a/packages/core/src/generic_steps/http.js b/packages/core/src/generic_steps/http.js deleted file mode 100644 index 57f614c..0000000 --- a/packages/core/src/generic_steps/http.js +++ /dev/null @@ -1,66 +0,0 @@ -import path from "node:path" -import fs from "node:fs" -import os from "node:os" - -import downloadHttpFile from "../helpers/downloadHttpFile" -import parseStringVars from "../utils/parseStringVars" -import extractFile from "../utils/extractFile" - -export default async (pkg, step, logger) => { - if (!step.path) { - step.path = `./${path.basename(step.url)}` - } - - step.path = await parseStringVars(step.path, pkg) - - let _path = path.resolve(pkg.install_path, step.path) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Downloading [${step.url}]`, - }) - - logger.info(`Downloading [${step.url} to ${_path}]`) - - if (step.tmp) { - _path = path.resolve(os.tmpdir(), String(new Date().getTime()), path.basename(step.url)) - } - - fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) - - await downloadHttpFile(step.url, _path, (progress) => { - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - use_id_only: true, - status_text: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, - }) - }) - - logger.info(`Downloaded finished.`) - - if (step.extract) { - if (typeof step.extract === "string") { - step.extract = path.resolve(pkg.install_path, step.extract) - } else { - step.extract = path.resolve(pkg.install_path, ".") - } - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Extracting bundle...`, - }) - - await extractFile(_path, step.extract) - - if (step.deleteAfterExtract !== false) { - logger.info(`Deleting temporal file [${_path}]...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Deleting temporal files...`, - }) - - await fs.promises.rm(_path, { recursive: true }) - } - } -} \ No newline at end of file diff --git a/packages/core/src/generic_steps/index.js b/packages/core/src/generic_steps/index.js deleted file mode 100644 index 96e2017..0000000 --- a/packages/core/src/generic_steps/index.js +++ /dev/null @@ -1,48 +0,0 @@ -import Logger from "../logger" - -import ISM_GIT_CLONE from "./git_clone" -import ISM_GIT_PULL from "./git_pull" -import ISM_GIT_RESET from "./git_reset" -import ISM_HTTP from "./http" - -const InstallationStepsMethods = { - git_clone: ISM_GIT_CLONE, - git_pull: ISM_GIT_PULL, - git_reset: ISM_GIT_RESET, - http_file: ISM_HTTP, -} - -const StepsOrders = [ - "git_clones", - "git_pull", - "git_reset", - "http_file", -] - -export default async function processGenericSteps(pkg, steps, logger = Logger) { - logger.info(`Processing generic steps...`) - - if (!Array.isArray(steps)) { - throw new Error(`Steps must be an array`) - } - - if (steps.length === 0) { - return pkg - } - - steps = steps.sort((a, b) => { - return StepsOrders.indexOf(a.type) - StepsOrders.indexOf(b.type) - }) - - for await (let step of steps) { - step.type = step.type.toLowerCase() - - if (!InstallationStepsMethods[step.type]) { - throw new Error(`Unknown step: ${step.type}`) - } - - await InstallationStepsMethods[step.type](pkg, step, logger) - } - - return pkg -} diff --git a/packages/core/src/handlers/apply.js b/packages/core/src/handlers/apply.js deleted file mode 100644 index 7b9f47e..0000000 --- a/packages/core/src/handlers/apply.js +++ /dev/null @@ -1,95 +0,0 @@ -import Logger from "../logger" - -import PatchManager from "../classes/PatchManager" -import ManifestReader from "../manifest/reader" -import ManifestVM from "../manifest/vm" -import DB from "../db" - -const BaseLog = Logger.child({ service: "APPLIER" }) - -function findPatch(patches, applied_patches, changes, mustBeInstalled) { - return patches.filter((patch) => { - const patchID = patch.id - - if (typeof changes.patches[patchID] === "undefined") { - return false - } - - if (mustBeInstalled === true && !applied_patches.includes(patch.id) && changes.patches[patchID] === true) { - return true - } - - if (mustBeInstalled === false && applied_patches.includes(patch.id) && changes.patches[patchID] === false) { - return true - } - - return false - }).map((patch) => patch.id) -} - -export default async function apply(pkg_id, changes = {}) { - try { - let pkg = await DB.getPackages(pkg_id) - - if (!pkg) { - BaseLog.error(`Package not found [${pkg_id}]`) - return null - } - - let manifest = await ManifestReader(pkg.local_manifest) - manifest = await ManifestVM(manifest.code) - - const Log = Logger.child({ service: `APPLIER|${pkg.id}` }) - - Log.info(`Applying changes to package...`) - Log.info(`Changes: ${JSON.stringify(changes)}`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Applying changes to package...`, - last_status: "loading", - }) - - if (changes.patches) { - if (!Array.isArray(pkg.applied_patches)) { - pkg.applied_patches = [] - } - - const patches = new PatchManager(pkg, manifest) - - await patches.remove(findPatch(manifest.patches, pkg.applied_patches, changes, false)) - await patches.patch(findPatch(manifest.patches, pkg.applied_patches, changes, true)) - } - - if (changes.config) { - Log.info(`Applying config to package...`) - - if (Object.keys(changes.config).length !== 0) { - Object.entries(changes.config).forEach(([key, value]) => { - pkg.config[key] = value - }) - } - } - - await DB.writePackage(pkg) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: "All changes applied", - }) - - Log.info(`All changes applied to package.`) - - return pkg - } catch (error) { - global._relic_eventBus.emit(`pkg:error`, { - id: pkg_id, - error - }) - - BaseLog.error(`Failed to apply changes to package [${pkg_id}]`, error) - BaseLog.error(error.stack) - - return null - } -} \ No newline at end of file diff --git a/packages/core/src/handlers/authorize.js b/packages/core/src/handlers/authorize.js deleted file mode 100644 index e1d19ab..0000000 --- a/packages/core/src/handlers/authorize.js +++ /dev/null @@ -1,33 +0,0 @@ -import ManifestAuthDB from "../classes/ManifestAuthDB" -import DB from "../db" - -import Logger from "../logger" - -const Log = Logger.child({ service: "AUTH" }) - -export default async (pkg_id, value) => { - if (!pkg_id) { - Log.error("pkg_id is required") - return false - } - - if (!value) { - Log.error("value is required") - return false - } - - const pkg = await DB.getPackages(pkg_id) - - if (!pkg) { - Log.error("Package not found") - return false - } - - Log.info(`Setting auth for [${pkg_id}]`) - - await ManifestAuthDB.set(pkg_id, value) - - global._relic_eventBus.emit("pkg:authorized", pkg) - - return true -} \ No newline at end of file diff --git a/packages/core/src/handlers/checkUpdate.js b/packages/core/src/handlers/checkUpdate.js deleted file mode 100644 index 48fe56b..0000000 --- a/packages/core/src/handlers/checkUpdate.js +++ /dev/null @@ -1,43 +0,0 @@ -import Logger from "../logger" -import DB from "../db" - -import softRead from "./read" - -const Log = Logger.child({ service: "CHECK_UPDATE" }) - -export default async function checkUpdate(pkg_id) { - const pkg = await DB.getPackages(pkg_id) - - if (!pkg) { - Log.error("Package not found") - return false - } - - Log.info(`Checking update for [${pkg_id}]`) - - const remoteSoftManifest = await softRead(pkg.remote_manifest, { - soft: true - }) - - if (!remoteSoftManifest) { - Log.error("Cannot read remote manifest") - return false - } - - if (pkg.version === remoteSoftManifest.version) { - Log.info("No update available") - return false - } - - Log.info("Update available") - Log.info("Local:", pkg.version) - Log.info("Remote:", remoteSoftManifest.version) - Log.info("Changelog:", remoteSoftManifest.changelog_url) - - return { - id: pkg.id, - local: pkg.version, - remote: remoteSoftManifest.version, - changelog: remoteSoftManifest.changelog_url, - } -} \ No newline at end of file diff --git a/packages/core/src/handlers/execute.js b/packages/core/src/handlers/execute.js deleted file mode 100644 index 4351d24..0000000 --- a/packages/core/src/handlers/execute.js +++ /dev/null @@ -1,80 +0,0 @@ -import Logger from "../logger" - -import fs from "node:fs" - -import DB from "../db" -import ManifestReader from "../manifest/reader" -import ManifestVM from "../manifest/vm" -import parseStringVars from "../utils/parseStringVars" -import { execa } from "../libraries/execa" - -const BaseLog = Logger.child({ service: "EXECUTER" }) - -export default async function execute(pkg_id, { useRemote = false, force = false } = {}) { - try { - const pkg = await DB.getPackages(pkg_id) - - if (!pkg) { - BaseLog.info(`Package not found [${pkg_id}]`) - return false - } - - const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest - - if (!fs.existsSync(manifestPath)) { - BaseLog.error(`Manifest not found in expected path [${manifestPath}] - \nMaybe the package installation has not been completed yet or corrupted. - `) - - return false - } - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - last_status: "loading", - status_text: null, - }) - - const ManifestRead = await ManifestReader(manifestPath) - - const manifest = await ManifestVM(ManifestRead.code) - - if (typeof manifest.execute === "function") { - await manifest.execute(pkg) - } - - if (typeof manifest.execute === "string") { - manifest.execute = parseStringVars(manifest.execute, pkg) - - BaseLog.info(`Executing binary > [${manifest.execute}]`) - - const args = Array.isArray(manifest.execute_args) ? manifest.execute_args : [] - - await execa(manifest.execute, args, { - cwd: pkg.install_path, - stdout: "inherit", - stderr: "inherit", - }) - } - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - last_status: "installed", - status_text: null, - }) - - return pkg - } catch (error) { - global._relic_eventBus.emit(`pkg:error`, { - id: pkg_id, - event: "execute", - last_status: "installed", - error, - }) - - BaseLog.error(`Failed to execute package [${pkg_id}]`, error) - BaseLog.error(error.stack) - - return null - } -} diff --git a/packages/core/src/handlers/install.js b/packages/core/src/handlers/install.js deleted file mode 100644 index 45e80cf..0000000 --- a/packages/core/src/handlers/install.js +++ /dev/null @@ -1,182 +0,0 @@ -import Logger from "../logger" - -import fs from "node:fs" - -import DB from "../db" -import ManifestReader from "../manifest/reader" -import ManifestVM from "../manifest/vm" -import GenericSteps from "../generic_steps" -import Apply from "../handlers/apply" - -const BaseLog = Logger.child({ service: "INSTALLER" }) - -export default async function install(manifest) { - let id = null - - try { - BaseLog.info(`Invoking new installation...`) - BaseLog.info(`Fetching manifest [${manifest}]`) - - const ManifestRead = await ManifestReader(manifest) - - manifest = await ManifestVM(ManifestRead.code) - - id = manifest.constructor.id - - const Log = BaseLog.child({ service: `INSTALLER|${id}` }) - - Log.info(`Creating install path [${manifest.install_path}]`) - - if (fs.existsSync(manifest.install_path)) { - Log.info(`Package already exists, removing...`) - await fs.rmSync(manifest.install_path, { recursive: true }) - } - - await fs.mkdirSync(manifest.install_path, { recursive: true }) - - Log.info(`Initializing manifest...`) - - if (typeof manifest.initialize === "function") { - await manifest.initialize() - } - - Log.info(`Appending to db...`) - - const pkg = DB.defaultPackageState({ - ...manifest.constructor, - id: id, - name: manifest.constructor.pkg_name, - version: manifest.constructor.version, - install_path: manifest.install_path, - description: manifest.constructor.description, - license: manifest.constructor.license, - last_status: "installing", - remote_manifest: ManifestRead.remote_manifest, - local_manifest: ManifestRead.local_manifest, - executable: !!manifest.execute - }) - - await DB.writePackage(pkg) - - global._relic_eventBus.emit("pkg:new", pkg) - - if (manifest.configuration) { - Log.info(`Applying default config to package...`) - - pkg.config = Object.entries(manifest.configuration).reduce((acc, [key, value]) => { - acc[key] = value.default - - return acc - }, {}) - } - - if (typeof manifest.beforeInstall === "function") { - Log.info(`Executing beforeInstall hook...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Performing beforeInstall hook...`, - }) - - await manifest.beforeInstall(pkg) - } - - if (Array.isArray(manifest.installSteps)) { - Log.info(`Executing generic install steps...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Performing generic install steps...`, - }) - - await GenericSteps(pkg, manifest.installSteps, Log) - } - - if (typeof manifest.afterInstall === "function") { - Log.info(`Executing afterInstall hook...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Performing afterInstall hook...`, - }) - - await manifest.afterInstall(pkg) - } - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Finishing up...`, - }) - - Log.info(`Copying manifest to the final location...`) - - const finalPath = `${manifest.install_path}/.rmanifest` - - if (fs.existsSync(finalPath)) { - await fs.promises.unlink(finalPath) - } - - await fs.promises.copyFile(ManifestRead.local_manifest, finalPath) - - if (ManifestRead.is_catched) { - Log.info(`Removing cache manifest...`) - await fs.promises.unlink(ManifestRead.local_manifest) - } - - pkg.local_manifest = finalPath - pkg.last_status = "loading" - pkg.installed_at = Date.now() - - await DB.writePackage(pkg) - - if (manifest.patches) { - const defaultPatches = manifest.patches.filter((patch) => patch.default) - - if (defaultPatches.length > 0) { - Log.info(`Applying default patches...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Applying default patches...`, - }) - - await Apply(id, { - patches: Object.fromEntries(defaultPatches.map((patch) => [patch.id, true])), - }) - } - } - - pkg.last_status = "installed" - - await DB.writePackage(pkg) - - global._relic_eventBus.emit(`pkg:update:state`, { - ...pkg, - id: pkg.id, - last_status: "installed", - status_text: `Installation completed successfully`, - }) - - global._relic_eventBus.emit(`pkg:new:done`, pkg) - - Log.info(`Package installed successfully!`) - - return pkg - } catch (error) { - global._relic_eventBus.emit(`pkg:error`, { - id: pkg.id, - error - }) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - last_status: "failed", - status_text: `Installation failed`, - }) - - BaseLog.error(`Error during installation of package [${id}] >`, error) - BaseLog.error(error.stack) - - return null - } -} \ No newline at end of file diff --git a/packages/core/src/handlers/list.js b/packages/core/src/handlers/list.js deleted file mode 100644 index eb51f5a..0000000 --- a/packages/core/src/handlers/list.js +++ /dev/null @@ -1,5 +0,0 @@ -import DB from "../db" - -export default async function list() { - return await DB.getPackages() -} \ No newline at end of file diff --git a/packages/core/src/handlers/read.js b/packages/core/src/handlers/read.js deleted file mode 100644 index 225842f..0000000 --- a/packages/core/src/handlers/read.js +++ /dev/null @@ -1,9 +0,0 @@ -import ManifestReader from "../manifest/reader" -import ManifestVM from "../manifest/vm" - -export default async function softRead(manifest, options = {}) { - const Reader = await ManifestReader(manifest) - const VM = await ManifestVM(Reader.code, options) - - return VM -} \ No newline at end of file diff --git a/packages/core/src/handlers/uninstall.js b/packages/core/src/handlers/uninstall.js deleted file mode 100644 index b0ea282..0000000 --- a/packages/core/src/handlers/uninstall.js +++ /dev/null @@ -1,74 +0,0 @@ -import Logger from "../logger" - -import DB from "../db" -import ManifestReader from "../manifest/reader" -import ManifestVM from "../manifest/vm" - -import { rimraf } from "rimraf" - -const BaseLog = Logger.child({ service: "UNINSTALLER" }) - -export default async function uninstall(pkg_id) { - try { - const pkg = await DB.getPackages(pkg_id) - - if (!pkg) { - BaseLog.info(`Package not found [${pkg_id}]`) - return null - } - - const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` }) - - Log.info(`Uninstalling package...`) - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Uninstalling package...`, - }) - - const ManifestRead = await ManifestReader(pkg.local_manifest) - const manifest = await ManifestVM(ManifestRead.code) - - if (typeof manifest.uninstall === "function") { - Log.info(`Performing uninstall hook...`) - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Performing uninstall hook...`, - }) - await manifest.uninstall(pkg) - } - - Log.info(`Deleting package directory...`) - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Deleting package directory...`, - }) - await rimraf(pkg.install_path) - - Log.info(`Removing package from database...`) - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Removing package from database...`, - }) - await DB.deletePackage(pkg.id) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - last_status: "deleted", - status_text: `Uninstalling package...`, - }) - global._relic_eventBus.emit(`pkg:remove`, pkg) - Log.info(`Package uninstalled successfully!`) - - return pkg - } catch (error) { - global._relic_eventBus.emit(`pkg:error`, { - id: pkg_id, - error - }) - - BaseLog.error(`Failed to uninstall package [${pkg_id}]`, error) - BaseLog.error(error.stack) - - return null - } -} \ No newline at end of file diff --git a/packages/core/src/handlers/update.js b/packages/core/src/handlers/update.js deleted file mode 100644 index 87d4ce0..0000000 --- a/packages/core/src/handlers/update.js +++ /dev/null @@ -1,128 +0,0 @@ -import Logger from "../logger" - -import DB from "../db" - -import ManifestReader from "../manifest/reader" -import ManifestVM from "../manifest/vm" - -import GenericSteps from "../generic_steps" -import PatchManager from "../classes/PatchManager" - -const BaseLog = Logger.child({ service: "UPDATER" }) - -const AllowedPkgChanges = [ - "id", - "name", - "version", - "description", - "author", - "license", - "icon", - "core_minimum_version", - "remote_manifest", -] - -const ManifestKeysMap = { - "name": "pkg_name", -} - -export default async function update(pkg_id) { - try { - const pkg = await DB.getPackages(pkg_id) - - if (!pkg) { - BaseLog.error(`Package not found [${pkg_id}]`) - - return null - } - - const Log = BaseLog.child({ service: `UPDATER|${pkg.id}` }) - - let ManifestRead = await ManifestReader(pkg.local_manifest) - let manifest = await ManifestVM(ManifestRead.code) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - last_status: "updating", - status_text: `Updating package...`, - }) - - pkg.last_status = "updating" - - await DB.writePackage(pkg) - - if (typeof manifest.update === "function") { - Log.info(`Performing update hook...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Performing update hook...`, - }) - - await manifest.update(pkg) - } - - if (manifest.updateSteps) { - Log.info(`Performing update steps...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Performing update steps...`, - }) - - await GenericSteps(pkg, manifest.updateSteps, Log) - } - - if (Array.isArray(pkg.applied_patches)) { - const patchManager = new PatchManager(pkg, manifest) - - await patchManager.reapply() - } - - if (typeof manifest.afterUpdate === "function") { - Log.info(`Performing after update hook...`) - - global._relic_eventBus.emit(`pkg:update:state`, { - id: pkg.id, - status_text: `Performing after update hook...`, - }) - - await manifest.afterUpdate(pkg) - } - - ManifestRead = await ManifestReader(pkg.local_manifest) - manifest = await ManifestVM(ManifestRead.code) - - // override public static values - for await (const key of AllowedPkgChanges) { - if (key in manifest.constructor) { - const mapKey = ManifestKeysMap[key] || key - pkg[key] = manifest.constructor[mapKey] - } - } - - pkg.last_status = "installed" - pkg.last_update = Date.now() - - await DB.writePackage(pkg) - - Log.info(`Package updated successfully`) - - global._relic_eventBus.emit(`pkg:update:state`, { - ...pkg, - id: pkg.id, - }) - - return pkg - } catch (error) { - global._relic_eventBus.emit(`pkg:error`, { - id: pkg_id, - error - }) - - BaseLog.error(`Failed to update package [${pkg_id}]`, error) - BaseLog.error(error.stack) - - return null - } -} \ No newline at end of file diff --git a/packages/core/src/helpers/downloadHttpFile.js b/packages/core/src/helpers/downloadHttpFile.js deleted file mode 100644 index d347b81..0000000 --- a/packages/core/src/helpers/downloadHttpFile.js +++ /dev/null @@ -1,73 +0,0 @@ -import fs from "node:fs" -import axios from "axios" -import humanFormat from "human-format" -import cliProgress from "cli-progress" - -function convertSize(size) { - return `${humanFormat(size, { - decimals: 2, - })}B` -} - -export default async (url, destination, progressCallback) => { - const progressBar = new cliProgress.SingleBar({ - format: "[{bar}] {percentage}% | {total_formatted} | {speed}/s | {eta_formatted}", - barCompleteChar: "\u2588", - barIncompleteChar: "\u2591", - hideCursor: true - }, cliProgress.Presets.shades_classic) - - const { data: remoteStream, headers } = await axios.get(url, { - responseType: "stream", - }) - - const localStream = fs.createWriteStream(destination) - - let progress = { - total: Number(headers["content-length"] ?? 0), - transferred: 0, - speed: 0, - } - - let lastTickTransferred = 0 - - progressBar.start(progress.total, 0, { - speed: "0B/s", - total_formatted: convertSize(progress.total), - }) - - remoteStream.pipe(localStream) - - remoteStream.on("data", (data) => { - progress.transferred = progress.transferred + Buffer.byteLength(data) - }) - - const progressInterval = setInterval(() => { - progress.speed = ((progress.transferred ?? 0) - lastTickTransferred) / 1 - - lastTickTransferred = progress.transferred ?? 0 - - progress.transferredString = convertSize(progress.transferred ?? 0) - progress.totalString = convertSize(progress.total) - progress.speedString = convertSize(progress.speed) - - progressBar.update(progress.transferred, { - speed: progress.speedString, - }) - - if (typeof progressCallback === "function") { - progressCallback(progress) - } - }, 1000) - - await new Promise((resolve, reject) => { - localStream.on("finish", resolve) - localStream.on("error", reject) - }) - - progressBar.stop() - - clearInterval(progressInterval) - - return destination -} \ No newline at end of file diff --git a/packages/core/src/helpers/sendToRender.js b/packages/core/src/helpers/sendToRender.js deleted file mode 100644 index 8a534a5..0000000 --- a/packages/core/src/helpers/sendToRender.js +++ /dev/null @@ -1,43 +0,0 @@ -import lodash from "lodash" - -const forbidden = [ - "libraries" -] - -export default (event, data) => { - if (!global.win) { - return false - } - - try { - function serializeIpc(data) { - if (!data) { - return undefined - } - - data = JSON.stringify(data) - - data = JSON.parse(data) - - const copy = lodash.cloneDeep(data) - - if (!Array.isArray(copy)) { - Object.keys(copy).forEach((key) => { - if (forbidden.includes(key)) { - delete copy[key] - } - - if (typeof copy[key] === "function") { - delete copy[key] - } - }) - } - - return copy - } - - global.win.webContents.send(event, serializeIpc(data)) - } catch (error) { - console.error(error) - } -} \ No newline at end of file diff --git a/packages/core/src/helpers/setup.js b/packages/core/src/helpers/setup.js deleted file mode 100644 index 826c4c6..0000000 --- a/packages/core/src/helpers/setup.js +++ /dev/null @@ -1,201 +0,0 @@ -import Logger from "../logger" - -const Log = Logger.child({ service: "SETUP" }) - -import path from "node:path" -import fs from "node:fs" -import os from "node:os" -import admzip from "adm-zip" -import resolveOs from "../utils/resolveOs" -import chmodRecursive from "../utils/chmodRecursive" - -import downloadFile from "../helpers/downloadHttpFile" - -import Vars from "../vars" -import Prerequisites from "../prerequisites" - -export default async () => { - if (!fs.existsSync(Vars.binaries_path)) { - Log.info(`Creating binaries directory: ${Vars.binaries_path}...`) - await fs.promises.mkdir(Vars.binaries_path, { recursive: true }) - } - - for await (let prerequisite of Prerequisites) { - try { - Log.info(`Checking prerequisite: ${prerequisite.id}...`) - - if (Array.isArray(prerequisite.requireOs) && !prerequisite.requireOs.includes(os.platform())) { - Log.info(`Prerequisite: ${prerequisite.id} is not required for this os.`) - continue - } - - if (!fs.existsSync(prerequisite.finalBin)) { - Log.info(`Missing prerequisite: ${prerequisite.id}, installing...`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Installing ${prerequisite.id}`, - }) - - if (fs.existsSync(prerequisite.destination)) { - Log.info(`Deleting temporal file [${prerequisite.destination}]`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Deleting temporal file [${prerequisite.destination}]`, - }) - - await fs.promises.rm(prerequisite.destination) - } - - if (fs.existsSync(prerequisite.extract)) { - Log.info(`Deleting temporal directory [${prerequisite.extract}]`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Deleting temporal directory [${prerequisite.extract}]`, - }) - - await fs.promises.rm(prerequisite.extract, { recursive: true }) - } - - Log.info(`Creating base directory: ${Vars.binaries_path}/${prerequisite.id}...`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Creating base directory: ${Vars.binaries_path}/${prerequisite.id}`, - }) - - await fs.promises.mkdir(path.resolve(Vars.binaries_path, prerequisite.id), { recursive: true }) - - if (typeof prerequisite.url === "function") { - prerequisite.url = await prerequisite.url(resolveOs(), os.arch()) - Log.info(`Resolved url: ${prerequisite.url}`) - } - - Log.info(`Downloading ${prerequisite.id} from [${prerequisite.url}] to destination [${prerequisite.destination}]...`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Starting download ${prerequisite.id} from [${prerequisite.url}] to destination [${prerequisite.destination}]`, - }) - - try { - await downloadFile( - prerequisite.url, - prerequisite.destination, - (progress) => { - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, - }) - } - ) - } catch (error) { - if (fs.existsSync(prerequisite.destination)) { - await fs.promises.rm(prerequisite.destination) - } - - throw error - } - - if (typeof prerequisite.extract === "string") { - Log.info(`Extracting ${prerequisite.id} to destination [${prerequisite.extract}]...`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Extracting ${prerequisite.id} to destination [${prerequisite.extract}]`, - }) - - const zip = new admzip(prerequisite.destination) - - await zip.extractAllTo(prerequisite.extract, true) - - Log.info(`Extraction ok...`) - } - - if (prerequisite.extractTargetFromName === true) { - let name = path.basename(prerequisite.url) - const ext = path.extname(name) - - name = name.replace(ext, "") - - if (fs.existsSync(path.resolve(prerequisite.extract, name))) { - await fs.promises.rename(path.resolve(prerequisite.extract, name), `${prerequisite.extract}_old`) - await fs.promises.rm(prerequisite.extract, { recursive: true }) - await fs.promises.rename(`${prerequisite.extract}_old`, prerequisite.extract) - } - } - - if (prerequisite.deleteBeforeExtract === true) { - Log.info(`Deleting temporal file [${prerequisite.destination}]`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Deleting temporal file [${prerequisite.destination}]`, - }) - - await fs.promises.unlink(prerequisite.destination) - } - - if (typeof prerequisite.rewriteExecutionPermission !== "undefined") { - const to = typeof prerequisite.rewriteExecutionPermission === "string" ? - prerequisite.rewriteExecutionPermission : - prerequisite.finalBin - - Log.info(`Rewriting permissions to ${to}...`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Rewriting permissions to ${to}`, - }) - - await chmodRecursive(to, 0o755) - } - - if (Array.isArray(prerequisite.moveDirs)) { - for (const dir of prerequisite.moveDirs) { - if (Array.isArray(dir.requireOs)) { - if (!dir.requireOs.includes(resolveOs())) { - continue - } - } - - Log.info(`Moving ${dir.from} to ${dir.to}...`) - - global._relic_eventBus.emit("app:setup", { - installed: false, - message: `Moving ${dir.from} to ${dir.to}`, - }) - - await fs.promises.rename(dir.from, dir.to) - - if (dir.deleteParentBefore === true) { - await fs.promises.rm(path.dirname(dir.from), { recursive: true }) - } - } - } - } - - global._relic_eventBus.emit("app:setup", { - installed: true, - message: null, - }) - - Log.info(`Prerequisite: ${prerequisite.id} is ready!`) - } catch (error) { - global._relic_eventBus.emit("app:setup", { - installed: false, - error: error, - message: error.message, - }) - - Log.error("Aborting setup due to an error...") - Log.error(error) - - throw error - } - - Log.info(`All prerequisites are ready!`) - } -} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js deleted file mode 100644 index de39263..0000000 --- a/packages/core/src/index.js +++ /dev/null @@ -1,68 +0,0 @@ -import fs from "node:fs" -import { EventEmitter } from "@foxify/events" -import { onExit } from "signal-exit" -import open from "open" - -import SetupHelper from "./helpers/setup" -import Logger from "./logger" - -import Vars from "./vars" -import DB from "./db" - -import PackageInstall from "./handlers/install" -import PackageExecute from "./handlers/execute" -import PackageUninstall from "./handlers/uninstall" -import PackageUpdate from "./handlers/update" -import PackageApply from "./handlers/apply" -import PackageList from "./handlers/list" -import PackageRead from "./handlers/read" -import PackageAuthorize from "./handlers/authorize" -import PackageCheckUpdate from "./handlers/checkUpdate" - -export default class RelicCore { - constructor(params) { - this.params = params - } - - eventBus = global._relic_eventBus = new EventEmitter() - - logger = Logger - - db = DB - - async initialize() { - await DB.initialize() - - onExit(this.onExit) - } - - onExit = () => { - if (fs.existsSync(Vars.cache_path)) { - fs.rmSync(Vars.cache_path, { recursive: true, force: true }) - } - } - - async setup() { - return await SetupHelper() - } - - package = { - install: PackageInstall, - execute: PackageExecute, - uninstall: PackageUninstall, - update: PackageUpdate, - apply: PackageApply, - list: PackageList, - read: PackageRead, - authorize: PackageAuthorize, - checkUpdate: PackageCheckUpdate - } - - openPath(pkg_id) { - if (!pkg_id) { - return open(Vars.runtime_path) - } - - return open(Vars.packages_path + "/" + pkg_id) - } -} \ No newline at end of file diff --git a/packages/core/src/libraries/execa/index.d.ts b/packages/core/src/libraries/execa/index.d.ts deleted file mode 100755 index 7cef754..0000000 --- a/packages/core/src/libraries/execa/index.d.ts +++ /dev/null @@ -1,955 +0,0 @@ -import {type Buffer} from 'node:buffer'; -import {type ChildProcess} from 'node:child_process'; -import {type Stream, type Readable as ReadableStream, type Writable as WritableStream} from 'node:stream'; - -export type StdioOption = - | 'pipe' - | 'overlapped' - | 'ipc' - | 'ignore' - | 'inherit' - | Stream - | number - | undefined; - -type EncodingOption = - | 'utf8' - // eslint-disable-next-line unicorn/text-encoding-identifier-case - | 'utf-8' - | 'utf16le' - | 'utf-16le' - | 'ucs2' - | 'ucs-2' - | 'latin1' - | 'binary' - | 'ascii' - | 'hex' - | 'base64' - | 'base64url' - | 'buffer' - | null - | undefined; -type DefaultEncodingOption = 'utf8'; -type BufferEncodingOption = 'buffer' | null; - -export type CommonOptions = { - /** - Kill the spawned process when the parent process exits unless either: - - the spawned process is [`detached`](https://nodejs.org/api/child_process.html#child_process_options_detached) - - the parent process is terminated abruptly, for example, with `SIGKILL` as opposed to `SIGTERM` or a normal exit - - @default true - */ - readonly cleanup?: boolean; - - /** - Prefer locally installed binaries when looking for a binary to execute. - - If you `$ npm install foo`, you can then `execa('foo')`. - - @default `true` with `$`, `false` otherwise - */ - readonly preferLocal?: boolean; - - /** - Preferred path to find locally installed binaries in (use with `preferLocal`). - - @default process.cwd() - */ - readonly localDir?: string | URL; - - /** - Path to the Node.js executable to use in child processes. - - This can be either an absolute path or a path relative to the `cwd` option. - - Requires `preferLocal` to be `true`. - - For example, this can be used together with [`get-node`](https://github.com/ehmicky/get-node) to run a specific Node.js version in a child process. - - @default process.execPath - */ - readonly execPath?: string; - - /** - Buffer the output from the spawned process. When set to `false`, you must read the output of `stdout` and `stderr` (or `all` if the `all` option is `true`). Otherwise the returned promise will not be resolved/rejected. - - If the spawned process fails, `error.stdout`, `error.stderr`, and `error.all` will contain the buffered data. - - @default true - */ - readonly buffer?: boolean; - - /** - Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). - - @default `inherit` with `$`, `pipe` otherwise - */ - readonly stdin?: StdioOption; - - /** - Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). - - @default 'pipe' - */ - readonly stdout?: StdioOption; - - /** - Same options as [`stdio`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio). - - @default 'pipe' - */ - readonly stderr?: StdioOption; - - /** - Setting this to `false` resolves the promise with the error instead of rejecting it. - - @default true - */ - readonly reject?: boolean; - - /** - Add an `.all` property on the promise and the resolved value. The property contains the output of the process with `stdout` and `stderr` interleaved. - - @default false - */ - readonly all?: boolean; - - /** - Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from the output. - - @default true - */ - readonly stripFinalNewline?: boolean; - - /** - Set to `false` if you don't want to extend the environment variables when providing the `env` property. - - @default true - */ - readonly extendEnv?: boolean; - - /** - Current working directory of the child process. - - @default process.cwd() - */ - readonly cwd?: string | URL; - - /** - Environment key-value pairs. Extends automatically from `process.env`. Set `extendEnv` to `false` if you don't want this. - - @default process.env - */ - readonly env?: NodeJS.ProcessEnv; - - /** - Explicitly set the value of `argv[0]` sent to the child process. This will be set to `command` or `file` if not specified. - */ - readonly argv0?: string; - - /** - Child's [stdio](https://nodejs.org/api/child_process.html#child_process_options_stdio) configuration. - - @default 'pipe' - */ - readonly stdio?: 'pipe' | 'overlapped' | 'ignore' | 'inherit' | readonly StdioOption[]; - - /** - Specify the kind of serialization used for sending messages between processes when using the `stdio: 'ipc'` option or `execaNode()`: - - `json`: Uses `JSON.stringify()` and `JSON.parse()`. - - `advanced`: Uses [`v8.serialize()`](https://nodejs.org/api/v8.html#v8_v8_serialize_value) - - [More info.](https://nodejs.org/api/child_process.html#child_process_advanced_serialization) - - @default 'json' - */ - readonly serialization?: 'json' | 'advanced'; - - /** - Prepare child to run independently of its parent process. Specific behavior [depends on the platform](https://nodejs.org/api/child_process.html#child_process_options_detached). - - @default false - */ - readonly detached?: boolean; - - /** - Sets the user identity of the process. - */ - readonly uid?: number; - - /** - Sets the group identity of the process. - */ - readonly gid?: number; - - /** - If `true`, runs `command` inside of a shell. Uses `/bin/sh` on UNIX and `cmd.exe` on Windows. A different shell can be specified as a string. The shell should understand the `-c` switch on UNIX or `/d /s /c` on Windows. - - We recommend against using this option since it is: - - not cross-platform, encouraging shell-specific syntax. - - slower, because of the additional shell interpretation. - - unsafe, potentially allowing command injection. - - @default false - */ - readonly shell?: boolean | string; - - /** - Specify the character encoding used to decode the `stdout` and `stderr` output. If set to `'buffer'` or `null`, then `stdout` and `stderr` will be a `Buffer` instead of a string. - - @default 'utf8' - */ - readonly encoding?: EncodingType; - - /** - If `timeout` is greater than `0`, the parent will send the signal identified by the `killSignal` property (the default is `SIGTERM`) if the child runs longer than `timeout` milliseconds. - - @default 0 - */ - readonly timeout?: number; - - /** - Largest amount of data in bytes allowed on `stdout` or `stderr`. Default: 100 MB. - - @default 100_000_000 - */ - readonly maxBuffer?: number; - - /** - Signal value to be used when the spawned process will be killed. - - @default 'SIGTERM' - */ - readonly killSignal?: string | number; - - /** - You can abort the spawned process using [`AbortController`](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). - - When `AbortController.abort()` is called, [`.isCanceled`](https://github.com/sindresorhus/execa#iscanceled) becomes `true`. - - @example - ``` - import {execa} from 'execa'; - - const abortController = new AbortController(); - const subprocess = execa('node', [], {signal: abortController.signal}); - - setTimeout(() => { - abortController.abort(); - }, 1000); - - try { - await subprocess; - } catch (error) { - console.log(subprocess.killed); // true - console.log(error.isCanceled); // true - } - ``` - */ - readonly signal?: AbortSignal; - - /** - If `true`, no quoting or escaping of arguments is done on Windows. Ignored on other platforms. This is set to `true` automatically when the `shell` option is `true`. - - @default false - */ - readonly windowsVerbatimArguments?: boolean; - - /** - On Windows, do not create a new console window. Please note this also prevents `CTRL-C` [from working](https://github.com/nodejs/node/issues/29837) on Windows. - - @default true - */ - readonly windowsHide?: boolean; - - /** - Print each command on `stderr` before executing it. - - This can also be enabled by setting the `NODE_DEBUG=execa` environment variable in the current process. - - @default false - */ - readonly verbose?: boolean; -}; - -export type Options = { - /** - Write some input to the `stdin` of your binary. - - If the input is a file, use the `inputFile` option instead. - */ - readonly input?: string | Buffer | ReadableStream; - - /** - Use a file as input to the the `stdin` of your binary. - - If the input is not a file, use the `input` option instead. - */ - readonly inputFile?: string; -} & CommonOptions; - -export type SyncOptions = { - /** - Write some input to the `stdin` of your binary. - - If the input is a file, use the `inputFile` option instead. - */ - readonly input?: string | Buffer; - - /** - Use a file as input to the the `stdin` of your binary. - - If the input is not a file, use the `input` option instead. - */ - readonly inputFile?: string; -} & CommonOptions; - -export type NodeOptions = { - /** - The Node.js executable to use. - - @default process.execPath - */ - readonly nodePath?: string; - - /** - List of [CLI options](https://nodejs.org/api/cli.html#cli_options) passed to the Node.js executable. - - @default process.execArgv - */ - readonly nodeOptions?: string[]; -} & Options; - -type StdoutStderrAll = string | Buffer | undefined; - -export type ExecaReturnBase = { - /** - The file and arguments that were run, for logging purposes. - - This is not escaped and should not be executed directly as a process, including using `execa()` or `execaCommand()`. - */ - command: string; - - /** - Same as `command` but escaped. - - This is meant to be copy and pasted into a shell, for debugging purposes. - Since the escaping is fairly basic, this should not be executed directly as a process, including using `execa()` or `execaCommand()`. - */ - escapedCommand: string; - - /** - The numeric exit code of the process that was run. - */ - exitCode: number; - - /** - The output of the process on stdout. - */ - stdout: StdoutStderrType; - - /** - The output of the process on stderr. - */ - stderr: StdoutStderrType; - - /** - Whether the process failed to run. - */ - failed: boolean; - - /** - Whether the process timed out. - */ - timedOut: boolean; - - /** - Whether the process was killed. - */ - killed: boolean; - - /** - The name of the signal that was used to terminate the process. For example, `SIGFPE`. - - If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. - */ - signal?: string; - - /** - A human-friendly description of the signal that was used to terminate the process. For example, `Floating point arithmetic error`. - - If a signal terminated the process, this property is defined and included in the error message. Otherwise it is `undefined`. It is also `undefined` when the signal is very uncommon which should seldomly happen. - */ - signalDescription?: string; - - /** - The `cwd` of the command if provided in the command options. Otherwise it is `process.cwd()`. - */ - cwd: string; -}; - -export type ExecaSyncReturnValue = { -} & ExecaReturnBase; - -/** -Result of a child process execution. On success this is a plain object. On failure this is also an `Error` instance. - -The child process fails when: -- its exit code is not `0` -- it was killed with a signal -- timing out -- being canceled -- there's not enough memory or there are already too many child processes -*/ -export type ExecaReturnValue = { - /** - The output of the process with `stdout` and `stderr` interleaved. - - This is `undefined` if either: - - the `all` option is `false` (default value) - - `execaSync()` was used - */ - all?: StdoutStderrType; - - /** - Whether the process was canceled. - - You can cancel the spawned process using the [`signal`](https://github.com/sindresorhus/execa#signal-1) option. - */ - isCanceled: boolean; -} & ExecaSyncReturnValue; - -export type ExecaSyncError = { - /** - Error message when the child process failed to run. In addition to the underlying error message, it also contains some information related to why the child process errored. - - The child process stderr then stdout are appended to the end, separated with newlines and not interleaved. - */ - message: string; - - /** - This is the same as the `message` property except it does not include the child process stdout/stderr. - */ - shortMessage: string; - - /** - Original error message. This is the same as the `message` property except it includes neither the child process stdout/stderr nor some additional information added by Execa. - - This is `undefined` unless the child process exited due to an `error` event or a timeout. - */ - originalMessage?: string; -} & Error & ExecaReturnBase; - -export type ExecaError = { - /** - The output of the process with `stdout` and `stderr` interleaved. - - This is `undefined` if either: - - the `all` option is `false` (default value) - - `execaSync()` was used - */ - all?: StdoutStderrType; - - /** - Whether the process was canceled. - */ - isCanceled: boolean; -} & ExecaSyncError; - -export type KillOptions = { - /** - Milliseconds to wait for the child process to terminate before sending `SIGKILL`. - - Can be disabled with `false`. - - @default 5000 - */ - forceKillAfterTimeout?: number | false; -}; - -export type ExecaChildPromise = { - /** - Stream combining/interleaving [`stdout`](https://nodejs.org/api/child_process.html#child_process_subprocess_stdout) and [`stderr`](https://nodejs.org/api/child_process.html#child_process_subprocess_stderr). - - This is `undefined` if either: - - the `all` option is `false` (the default value) - - both `stdout` and `stderr` options are set to [`'inherit'`, `'ipc'`, `Stream` or `integer`](https://nodejs.org/dist/latest-v6.x/docs/api/child_process.html#child_process_options_stdio) - */ - all?: ReadableStream; - - catch( - onRejected?: (reason: ExecaError) => ResultType | PromiseLike - ): Promise | ResultType>; - - /** - Same as the original [`child_process#kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal), except if `signal` is `SIGTERM` (the default value) and the child process is not terminated after 5 seconds, force it by sending `SIGKILL`. Note that this graceful termination does not work on Windows, because Windows [doesn't support signals](https://nodejs.org/api/process.html#process_signal_events) (`SIGKILL` and `SIGTERM` has the same effect of force-killing the process immediately.) If you want to achieve graceful termination on Windows, you have to use other means, such as [`taskkill`](https://github.com/sindresorhus/taskkill). - */ - kill(signal?: string, options?: KillOptions): void; - - /** - Similar to [`childProcess.kill()`](https://nodejs.org/api/child_process.html#child_process_subprocess_kill_signal). This used to be preferred when cancelling the child process execution as the error is more descriptive and [`childProcessResult.isCanceled`](#iscanceled) is set to `true`. But now this is deprecated and you should either use `.kill()` or the `signal` option when creating the child process. - */ - cancel(): void; - - /** - [Pipe](https://nodejs.org/api/stream.html#readablepipedestination-options) the child process's `stdout` to `target`, which can be: - - Another `execa()` return value - - A writable stream - - A file path string - - If the `target` is another `execa()` return value, it is returned. Otherwise, the original `execa()` return value is returned. This allows chaining `pipeStdout()` then `await`ing the final result. - - The `stdout` option] must be kept as `pipe`, its default value. - */ - pipeStdout?>(target: Target): Target; - pipeStdout?(target: WritableStream | string): ExecaChildProcess; - - /** - Like `pipeStdout()` but piping the child process's `stderr` instead. - - The `stderr` option must be kept as `pipe`, its default value. - */ - pipeStderr?>(target: Target): Target; - pipeStderr?(target: WritableStream | string): ExecaChildProcess; - - /** - Combines both `pipeStdout()` and `pipeStderr()`. - - Either the `stdout` option or the `stderr` option must be kept as `pipe`, their default value. Also, the `all` option must be set to `true`. - */ - pipeAll?>(target: Target): Target; - pipeAll?(target: WritableStream | string): ExecaChildProcess; -}; - -export type ExecaChildProcess = ChildProcess & -ExecaChildPromise & -Promise>; - -/** -Executes a command using `file ...arguments`. `arguments` are specified as an array of strings. Returns a `childProcess`. - -Arguments are automatically escaped. They can contain any character, including spaces. - -This is the preferred method when executing single commands. - -@param file - The program/script to execute. -@param arguments - Arguments to pass to `file` on execution. -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example Promise interface -``` -import {execa} from 'execa'; - -const {stdout} = await execa('echo', ['unicorns']); -console.log(stdout); -//=> 'unicorns' -``` - -@example Redirect output to a file -``` -import {execa} from 'execa'; - -// Similar to `echo unicorns > stdout.txt` in Bash -await execa('echo', ['unicorns']).pipeStdout('stdout.txt'); - -// Similar to `echo unicorns 2> stdout.txt` in Bash -await execa('echo', ['unicorns']).pipeStderr('stderr.txt'); - -// Similar to `echo unicorns &> stdout.txt` in Bash -await execa('echo', ['unicorns'], {all: true}).pipeAll('all.txt'); -``` - -@example Redirect input from a file -``` -import {execa} from 'execa'; - -// Similar to `cat < stdin.txt` in Bash -const {stdout} = await execa('cat', {inputFile: 'stdin.txt'}); -console.log(stdout); -//=> 'unicorns' -``` - -@example Save and pipe output from a child process -``` -import {execa} from 'execa'; - -const {stdout} = await execa('echo', ['unicorns']).pipeStdout(process.stdout); -// Prints `unicorns` -console.log(stdout); -// Also returns 'unicorns' -``` - -@example Pipe multiple processes -``` -import {execa} from 'execa'; - -// Similar to `echo unicorns | cat` in Bash -const {stdout} = await execa('echo', ['unicorns']).pipeStdout(execa('cat')); -console.log(stdout); -//=> 'unicorns' -``` - -@example Handling errors -``` -import {execa} from 'execa'; - -// Catching an error -try { - await execa('unknown', ['command']); -} catch (error) { - console.log(error); - /* - { - message: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', - errno: -2, - code: 'ENOENT', - syscall: 'spawn unknown', - path: 'unknown', - spawnargs: ['command'], - originalMessage: 'spawn unknown ENOENT', - shortMessage: 'Command failed with ENOENT: unknown command spawn unknown ENOENT', - command: 'unknown command', - escapedCommand: 'unknown command', - stdout: '', - stderr: '', - failed: true, - timedOut: false, - isCanceled: false, - killed: false, - cwd: '/path/to/cwd' - } - \*\/ -} -``` - -@example Graceful termination -``` -const subprocess = execa('node'); - -setTimeout(() => { - subprocess.kill('SIGTERM', { - forceKillAfterTimeout: 2000 - }); -}, 1000); -``` -*/ -export function execa( - file: string, - arguments?: readonly string[], - options?: Options -): ExecaChildProcess; -export function execa( - file: string, - arguments?: readonly string[], - options?: Options -): ExecaChildProcess; -export function execa(file: string, options?: Options): ExecaChildProcess; -export function execa(file: string, options?: Options): ExecaChildProcess; - -/** -Same as `execa()` but synchronous. - -@param file - The program/script to execute. -@param arguments - Arguments to pass to `file` on execution. -@returns A `childProcessResult` object -@throws A `childProcessResult` error - -@example Promise interface -``` -import {execa} from 'execa'; - -const {stdout} = execaSync('echo', ['unicorns']); -console.log(stdout); -//=> 'unicorns' -``` - -@example Redirect input from a file -``` -import {execa} from 'execa'; - -// Similar to `cat < stdin.txt` in Bash -const {stdout} = execaSync('cat', {inputFile: 'stdin.txt'}); -console.log(stdout); -//=> 'unicorns' -``` - -@example Handling errors -``` -import {execa} from 'execa'; - -// Catching an error -try { - execaSync('unknown', ['command']); -} catch (error) { - console.log(error); - /* - { - message: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT', - errno: -2, - code: 'ENOENT', - syscall: 'spawnSync unknown', - path: 'unknown', - spawnargs: ['command'], - originalMessage: 'spawnSync unknown ENOENT', - shortMessage: 'Command failed with ENOENT: unknown command spawnSync unknown ENOENT', - command: 'unknown command', - escapedCommand: 'unknown command', - stdout: '', - stderr: '', - failed: true, - timedOut: false, - isCanceled: false, - killed: false, - cwd: '/path/to/cwd' - } - \*\/ -} -``` -*/ -export function execaSync( - file: string, - arguments?: readonly string[], - options?: SyncOptions -): ExecaSyncReturnValue; -export function execaSync( - file: string, - arguments?: readonly string[], - options?: SyncOptions -): ExecaSyncReturnValue; -export function execaSync(file: string, options?: SyncOptions): ExecaSyncReturnValue; -export function execaSync( - file: string, - options?: SyncOptions -): ExecaSyncReturnValue; - -/** -Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a `childProcess`. - -Arguments are automatically escaped. They can contain any character, but spaces must be escaped with a backslash like `execaCommand('echo has\\ space')`. - -This is the preferred method when executing a user-supplied `command` string, such as in a REPL. - -@param command - The program/script to execute and its arguments. -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example -``` -import {execaCommand} from 'execa'; - -const {stdout} = await execaCommand('echo unicorns'); -console.log(stdout); -//=> 'unicorns' -``` -*/ -export function execaCommand(command: string, options?: Options): ExecaChildProcess; -export function execaCommand(command: string, options?: Options): ExecaChildProcess; - -/** -Same as `execaCommand()` but synchronous. - -@param command - The program/script to execute and its arguments. -@returns A `childProcessResult` object -@throws A `childProcessResult` error - -@example -``` -import {execaCommandSync} from 'execa'; - -const {stdout} = execaCommandSync('echo unicorns'); -console.log(stdout); -//=> 'unicorns' -``` -*/ -export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; -export function execaCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue; - -type TemplateExpression = - | string - | number - | ExecaReturnValue - | ExecaSyncReturnValue - | Array | ExecaSyncReturnValue>; - -type Execa$ = { - /** - Returns a new instance of `$` but with different default `options`. Consecutive calls are merged to previous ones. - - This can be used to either: - - Set options for a specific command: `` $(options)`command` `` - - Share options for multiple commands: `` const $$ = $(options); $$`command`; $$`otherCommand` `` - - @param options - Options to set - @returns A new instance of `$` with those `options` set - - @example - ``` - import {$} from 'execa'; - - const $$ = $({stdio: 'inherit'}); - - await $$`echo unicorns`; - //=> 'unicorns' - - await $$`echo rainbows`; - //=> 'rainbows' - ``` - */ - (options: Options): Execa$; - (options: Options): Execa$; - (options: Options): Execa$; - ( - templates: TemplateStringsArray, - ...expressions: TemplateExpression[] - ): ExecaChildProcess; - - /** - Same as $\`command\` but synchronous. - - @returns A `childProcessResult` object - @throws A `childProcessResult` error - - @example Basic - ``` - import {$} from 'execa'; - - const branch = $.sync`git branch --show-current`; - $.sync`dep deploy --branch=${branch}`; - ``` - - @example Multiple arguments - ``` - import {$} from 'execa'; - - const args = ['unicorns', '&', 'rainbows!']; - const {stdout} = $.sync`echo ${args}`; - console.log(stdout); - //=> 'unicorns & rainbows!' - ``` - - @example With options - ``` - import {$} from 'execa'; - - $.sync({stdio: 'inherit'})`echo unicorns`; - //=> 'unicorns' - ``` - - @example Shared options - ``` - import {$} from 'execa'; - - const $$ = $({stdio: 'inherit'}); - - $$.sync`echo unicorns`; - //=> 'unicorns' - - $$.sync`echo rainbows`; - //=> 'rainbows' - ``` - */ - sync( - templates: TemplateStringsArray, - ...expressions: TemplateExpression[] - ): ExecaSyncReturnValue; -}; - -/** -Executes a command. The `command` string includes both the `file` and its `arguments`. Returns a `childProcess`. - -Arguments are automatically escaped. They can contain any character, but spaces must use `${}` like `` $`echo ${'has space'}` ``. - -This is the preferred method when executing multiple commands in a script file. - -The `command` string can inject any `${value}` with the following types: string, number, `childProcess` or an array of those types. For example: `` $`echo one ${'two'} ${3} ${['four', 'five']}` ``. For `${childProcess}`, the process's `stdout` is used. - -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example Basic -``` -import {$} from 'execa'; - -const branch = await $`git branch --show-current`; -await $`dep deploy --branch=${branch}`; -``` - -@example Multiple arguments -``` -import {$} from 'execa'; - -const args = ['unicorns', '&', 'rainbows!']; -const {stdout} = await $`echo ${args}`; -console.log(stdout); -//=> 'unicorns & rainbows!' -``` - -@example With options -``` -import {$} from 'execa'; - -await $({stdio: 'inherit'})`echo unicorns`; -//=> 'unicorns' -``` - -@example Shared options -``` -import {$} from 'execa'; - -const $$ = $({stdio: 'inherit'}); - -await $$`echo unicorns`; -//=> 'unicorns' - -await $$`echo rainbows`; -//=> 'rainbows' -``` -*/ -export const $: Execa$; - -/** -Execute a Node.js script as a child process. - -Arguments are automatically escaped. They can contain any character, including spaces. - -This is the preferred method when executing Node.js files. - -Like [`child_process#fork()`](https://nodejs.org/api/child_process.html#child_process_child_process_fork_modulepath_args_options): - - the current Node version and options are used. This can be overridden using the `nodePath` and `nodeOptions` options. - - the `shell` option cannot be used - - an extra channel [`ipc`](https://nodejs.org/api/child_process.html#child_process_options_stdio) is passed to `stdio` - -@param scriptPath - Node.js script to execute. -@param arguments - Arguments to pass to `scriptPath` on execution. -@returns An `ExecaChildProcess` that is both: - - a `Promise` resolving or rejecting with a `childProcessResult`. - - a [`child_process` instance](https://nodejs.org/api/child_process.html#child_process_class_childprocess) with some additional methods and properties. -@throws A `childProcessResult` error - -@example -``` -import {execa} from 'execa'; - -await execaNode('scriptPath', ['argument']); -``` -*/ -export function execaNode( - scriptPath: string, - arguments?: readonly string[], - options?: NodeOptions -): ExecaChildProcess; -export function execaNode( - scriptPath: string, - arguments?: readonly string[], - options?: NodeOptions -): ExecaChildProcess; -export function execaNode(scriptPath: string, options?: NodeOptions): ExecaChildProcess; -export function execaNode(scriptPath: string, options?: NodeOptions): ExecaChildProcess; diff --git a/packages/core/src/libraries/execa/index.js b/packages/core/src/libraries/execa/index.js deleted file mode 100755 index fca5389..0000000 --- a/packages/core/src/libraries/execa/index.js +++ /dev/null @@ -1,309 +0,0 @@ -import {Buffer} from 'node:buffer'; -import path from 'node:path'; -import childProcess from 'node:child_process'; -import process from 'node:process'; -import crossSpawn from 'cross-spawn'; -import stripFinalNewline from '../strip-final-newline'; -import {npmRunPathEnv} from '../npm-run-path'; -import onetime from '../onetime'; -import {makeError} from './lib/error.js'; -import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; -import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; -import {addPipeMethods} from './lib/pipe.js'; -import {handleInput, getSpawnedResult, makeAllStream, handleInputSync} from './lib/stream.js'; -import {mergePromise, getSpawnedPromise} from './lib/promise.js'; -import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js'; -import {logCommand, verboseDefault} from './lib/verbose.js'; - -const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; - -const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { - const env = extendEnv ? {...process.env, ...envOption} : envOption; - - if (preferLocal) { - return npmRunPathEnv({env, cwd: localDir, execPath}); - } - - return env; -}; - -const handleArguments = (file, args, options = {}) => { - const parsed = crossSpawn._parse(file, args, options); - file = parsed.command; - args = parsed.args; - options = parsed.options; - - options = { - maxBuffer: DEFAULT_MAX_BUFFER, - buffer: true, - stripFinalNewline: true, - extendEnv: true, - preferLocal: false, - localDir: options.cwd || process.cwd(), - execPath: process.execPath, - encoding: 'utf8', - reject: true, - cleanup: true, - all: false, - windowsHide: true, - verbose: verboseDefault, - ...options, - }; - - options.env = getEnv(options); - - options.stdio = normalizeStdio(options); - - if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { - // #116 - args.unshift('/q'); - } - - return {file, args, options, parsed}; -}; - -const handleOutput = (options, value, error) => { - if (typeof value !== 'string' && !Buffer.isBuffer(value)) { - // When `execaSync()` errors, we normalize it to '' to mimic `execa()` - return error === undefined ? undefined : ''; - } - - if (options.stripFinalNewline) { - return stripFinalNewline(value); - } - - return value; -}; - -export function execa(file, args, options) { - const parsed = handleArguments(file, args, options); - const command = joinCommand(file, args); - const escapedCommand = getEscapedCommand(file, args); - logCommand(escapedCommand, parsed.options); - - validateTimeout(parsed.options); - - let spawned; - try { - spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); - } catch (error) { - // Ensure the returned error is always both a promise and a child process - const dummySpawned = new childProcess.ChildProcess(); - const errorPromise = Promise.reject(makeError({ - error, - stdout: '', - stderr: '', - all: '', - command, - escapedCommand, - parsed, - timedOut: false, - isCanceled: false, - killed: false, - })); - mergePromise(dummySpawned, errorPromise); - return dummySpawned; - } - - const spawnedPromise = getSpawnedPromise(spawned); - const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); - const processDone = setExitHandler(spawned, parsed.options, timedPromise); - - const context = {isCanceled: false}; - - spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); - spawned.cancel = spawnedCancel.bind(null, spawned, context); - - const handlePromise = async () => { - const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); - const stdout = handleOutput(parsed.options, stdoutResult); - const stderr = handleOutput(parsed.options, stderrResult); - const all = handleOutput(parsed.options, allResult); - - if (error || exitCode !== 0 || signal !== null) { - const returnedError = makeError({ - error, - exitCode, - signal, - stdout, - stderr, - all, - command, - escapedCommand, - parsed, - timedOut, - isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false), - killed: spawned.killed, - }); - - if (!parsed.options.reject) { - return returnedError; - } - - throw returnedError; - } - - return { - command, - escapedCommand, - exitCode: 0, - stdout, - stderr, - all, - failed: false, - timedOut: false, - isCanceled: false, - killed: false, - }; - }; - - const handlePromiseOnce = onetime(handlePromise); - - handleInput(spawned, parsed.options); - - spawned.all = makeAllStream(spawned, parsed.options); - - addPipeMethods(spawned); - mergePromise(spawned, handlePromiseOnce); - return spawned; -} - -export function execaSync(file, args, options) { - const parsed = handleArguments(file, args, options); - const command = joinCommand(file, args); - const escapedCommand = getEscapedCommand(file, args); - logCommand(escapedCommand, parsed.options); - - const input = handleInputSync(parsed.options); - - let result; - try { - result = childProcess.spawnSync(parsed.file, parsed.args, {...parsed.options, input}); - } catch (error) { - throw makeError({ - error, - stdout: '', - stderr: '', - all: '', - command, - escapedCommand, - parsed, - timedOut: false, - isCanceled: false, - killed: false, - }); - } - - const stdout = handleOutput(parsed.options, result.stdout, result.error); - const stderr = handleOutput(parsed.options, result.stderr, result.error); - - if (result.error || result.status !== 0 || result.signal !== null) { - const error = makeError({ - stdout, - stderr, - error: result.error, - signal: result.signal, - exitCode: result.status, - command, - escapedCommand, - parsed, - timedOut: result.error && result.error.code === 'ETIMEDOUT', - isCanceled: false, - killed: result.signal !== null, - }); - - if (!parsed.options.reject) { - return error; - } - - throw error; - } - - return { - command, - escapedCommand, - exitCode: 0, - stdout, - stderr, - failed: false, - timedOut: false, - isCanceled: false, - killed: false, - }; -} - -const normalizeScriptStdin = ({input, inputFile, stdio}) => input === undefined && inputFile === undefined && stdio === undefined - ? {stdin: 'inherit'} - : {}; - -const normalizeScriptOptions = (options = {}) => ({ - preferLocal: true, - ...normalizeScriptStdin(options), - ...options, -}); - -function create$(options) { - function $(templatesOrOptions, ...expressions) { - if (!Array.isArray(templatesOrOptions)) { - return create$({...options, ...templatesOrOptions}); - } - - const [file, ...args] = parseTemplates(templatesOrOptions, expressions); - return execa(file, args, normalizeScriptOptions(options)); - } - - $.sync = (templates, ...expressions) => { - if (!Array.isArray(templates)) { - throw new TypeError('Please use $(options).sync`command` instead of $.sync(options)`command`.'); - } - - const [file, ...args] = parseTemplates(templates, expressions); - return execaSync(file, args, normalizeScriptOptions(options)); - }; - - return $; -} - -export const $ = create$(); - -export function execaCommand(command, options) { - const [file, ...args] = parseCommand(command); - return execa(file, args, options); -} - -export function execaCommandSync(command, options) { - const [file, ...args] = parseCommand(command); - return execaSync(file, args, options); -} - -export function execaNode(scriptPath, args, options = {}) { - if (args && !Array.isArray(args) && typeof args === 'object') { - options = args; - args = []; - } - - const stdio = normalizeStdioNode(options); - const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); - - const { - nodePath = process.execPath, - nodeOptions = defaultExecArgv, - } = options; - - return execa( - nodePath, - [ - ...nodeOptions, - scriptPath, - ...(Array.isArray(args) ? args : []), - ], - { - ...options, - stdin: undefined, - stdout: undefined, - stderr: undefined, - stdio, - shell: false, - }, - ); -} diff --git a/packages/core/src/libraries/execa/lib/command.js b/packages/core/src/libraries/execa/lib/command.js deleted file mode 100755 index 727ce5f..0000000 --- a/packages/core/src/libraries/execa/lib/command.js +++ /dev/null @@ -1,119 +0,0 @@ -import {Buffer} from 'node:buffer'; -import {ChildProcess} from 'node:child_process'; - -const normalizeArgs = (file, args = []) => { - if (!Array.isArray(args)) { - return [file]; - } - - return [file, ...args]; -}; - -const NO_ESCAPE_REGEXP = /^[\w.-]+$/; - -const escapeArg = arg => { - if (typeof arg !== 'string' || NO_ESCAPE_REGEXP.test(arg)) { - return arg; - } - - return `"${arg.replaceAll('"', '\\"')}"`; -}; - -export const joinCommand = (file, args) => normalizeArgs(file, args).join(' '); - -export const getEscapedCommand = (file, args) => normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' '); - -const SPACES_REGEXP = / +/g; - -// Handle `execaCommand()` -export const parseCommand = command => { - const tokens = []; - for (const token of command.trim().split(SPACES_REGEXP)) { - // Allow spaces to be escaped by a backslash if not meant as a delimiter - const previousToken = tokens.at(-1); - if (previousToken && previousToken.endsWith('\\')) { - // Merge previous token with current one - tokens[tokens.length - 1] = `${previousToken.slice(0, -1)} ${token}`; - } else { - tokens.push(token); - } - } - - return tokens; -}; - -const parseExpression = expression => { - const typeOfExpression = typeof expression; - - if (typeOfExpression === 'string') { - return expression; - } - - if (typeOfExpression === 'number') { - return String(expression); - } - - if ( - typeOfExpression === 'object' - && expression !== null - && !(expression instanceof ChildProcess) - && 'stdout' in expression - ) { - const typeOfStdout = typeof expression.stdout; - - if (typeOfStdout === 'string') { - return expression.stdout; - } - - if (Buffer.isBuffer(expression.stdout)) { - return expression.stdout.toString(); - } - - throw new TypeError(`Unexpected "${typeOfStdout}" stdout in template expression`); - } - - throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`); -}; - -const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0 - ? [...tokens, ...nextTokens] - : [ - ...tokens.slice(0, -1), - `${tokens.at(-1)}${nextTokens[0]}`, - ...nextTokens.slice(1), - ]; - -const parseTemplate = ({templates, expressions, tokens, index, template}) => { - const templateString = template ?? templates.raw[index]; - const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean); - const newTokens = concatTokens( - tokens, - templateTokens, - templateString.startsWith(' '), - ); - - if (index === expressions.length) { - return newTokens; - } - - const expression = expressions[index]; - const expressionTokens = Array.isArray(expression) - ? expression.map(expression => parseExpression(expression)) - : [parseExpression(expression)]; - return concatTokens( - newTokens, - expressionTokens, - templateString.endsWith(' '), - ); -}; - -export const parseTemplates = (templates, expressions) => { - let tokens = []; - - for (const [index, template] of templates.entries()) { - tokens = parseTemplate({templates, expressions, tokens, index, template}); - } - - return tokens; -}; - diff --git a/packages/core/src/libraries/execa/lib/error.js b/packages/core/src/libraries/execa/lib/error.js deleted file mode 100755 index 761032b..0000000 --- a/packages/core/src/libraries/execa/lib/error.js +++ /dev/null @@ -1,87 +0,0 @@ -import process from 'node:process'; -import {signalsByName} from '../../human-signals'; - -const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { - if (timedOut) { - return `timed out after ${timeout} milliseconds`; - } - - if (isCanceled) { - return 'was canceled'; - } - - if (errorCode !== undefined) { - return `failed with ${errorCode}`; - } - - if (signal !== undefined) { - return `was killed with ${signal} (${signalDescription})`; - } - - if (exitCode !== undefined) { - return `failed with exit code ${exitCode}`; - } - - return 'failed'; -}; - -export const makeError = ({ - stdout, - stderr, - all, - error, - signal, - exitCode, - command, - escapedCommand, - timedOut, - isCanceled, - killed, - parsed: {options: {timeout, cwd = process.cwd()}}, -}) => { - // `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`. - // We normalize them to `undefined` - exitCode = exitCode === null ? undefined : exitCode; - signal = signal === null ? undefined : signal; - const signalDescription = signal === undefined ? undefined : signalsByName[signal].description; - - const errorCode = error && error.code; - - const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); - const execaMessage = `Command ${prefix}: ${command}`; - const isError = Object.prototype.toString.call(error) === '[object Error]'; - const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage; - const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); - - if (isError) { - error.originalMessage = error.message; - error.message = message; - } else { - error = new Error(message); - } - - error.shortMessage = shortMessage; - error.command = command; - error.escapedCommand = escapedCommand; - error.exitCode = exitCode; - error.signal = signal; - error.signalDescription = signalDescription; - error.stdout = stdout; - error.stderr = stderr; - error.cwd = cwd; - - if (all !== undefined) { - error.all = all; - } - - if ('bufferedData' in error) { - delete error.bufferedData; - } - - error.failed = true; - error.timedOut = Boolean(timedOut); - error.isCanceled = isCanceled; - error.killed = killed && !timedOut; - - return error; -}; diff --git a/packages/core/src/libraries/execa/lib/kill.js b/packages/core/src/libraries/execa/lib/kill.js deleted file mode 100755 index 12ce0a1..0000000 --- a/packages/core/src/libraries/execa/lib/kill.js +++ /dev/null @@ -1,102 +0,0 @@ -import os from 'node:os'; -import {onExit} from 'signal-exit'; - -const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; - -// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior -export const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { - const killResult = kill(signal); - setKillTimeout(kill, signal, options, killResult); - return killResult; -}; - -const setKillTimeout = (kill, signal, options, killResult) => { - if (!shouldForceKill(signal, options, killResult)) { - return; - } - - const timeout = getForceKillAfterTimeout(options); - const t = setTimeout(() => { - kill('SIGKILL'); - }, timeout); - - // Guarded because there's no `.unref()` when `execa` is used in the renderer - // process in Electron. This cannot be tested since we don't run tests in - // Electron. - // istanbul ignore else - if (t.unref) { - t.unref(); - } -}; - -const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult; - -const isSigterm = signal => signal === os.constants.signals.SIGTERM - || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); - -const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { - if (forceKillAfterTimeout === true) { - return DEFAULT_FORCE_KILL_TIMEOUT; - } - - if (!Number.isFinite(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { - throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); - } - - return forceKillAfterTimeout; -}; - -// `childProcess.cancel()` -export const spawnedCancel = (spawned, context) => { - const killResult = spawned.kill(); - - if (killResult) { - context.isCanceled = true; - } -}; - -const timeoutKill = (spawned, signal, reject) => { - spawned.kill(signal); - reject(Object.assign(new Error('Timed out'), {timedOut: true, signal})); -}; - -// `timeout` option handling -export const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { - if (timeout === 0 || timeout === undefined) { - return spawnedPromise; - } - - let timeoutId; - const timeoutPromise = new Promise((resolve, reject) => { - timeoutId = setTimeout(() => { - timeoutKill(spawned, killSignal, reject); - }, timeout); - }); - - const safeSpawnedPromise = spawnedPromise.finally(() => { - clearTimeout(timeoutId); - }); - - return Promise.race([timeoutPromise, safeSpawnedPromise]); -}; - -export const validateTimeout = ({timeout}) => { - if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) { - throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); - } -}; - -// `cleanup` option handling -export const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { - if (!cleanup || detached) { - return timedPromise; - } - - const removeExitHandler = onExit(() => { - spawned.kill(); - }); - - return timedPromise.finally(() => { - removeExitHandler(); - }); -}; diff --git a/packages/core/src/libraries/execa/lib/pipe.js b/packages/core/src/libraries/execa/lib/pipe.js deleted file mode 100755 index f26715d..0000000 --- a/packages/core/src/libraries/execa/lib/pipe.js +++ /dev/null @@ -1,42 +0,0 @@ -import {createWriteStream} from 'node:fs'; -import {ChildProcess} from 'node:child_process'; -import {isWritableStream} from '../../is-stream'; - -const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function'; - -const pipeToTarget = (spawned, streamName, target) => { - if (typeof target === 'string') { - spawned[streamName].pipe(createWriteStream(target)); - return spawned; - } - - if (isWritableStream(target)) { - spawned[streamName].pipe(target); - return spawned; - } - - if (!isExecaChildProcess(target)) { - throw new TypeError('The second argument must be a string, a stream or an Execa child process.'); - } - - if (!isWritableStream(target.stdin)) { - throw new TypeError('The target child process\'s stdin must be available.'); - } - - spawned[streamName].pipe(target.stdin); - return target; -}; - -export const addPipeMethods = spawned => { - if (spawned.stdout !== null) { - spawned.pipeStdout = pipeToTarget.bind(undefined, spawned, 'stdout'); - } - - if (spawned.stderr !== null) { - spawned.pipeStderr = pipeToTarget.bind(undefined, spawned, 'stderr'); - } - - if (spawned.all !== undefined) { - spawned.pipeAll = pipeToTarget.bind(undefined, spawned, 'all'); - } -}; diff --git a/packages/core/src/libraries/execa/lib/promise.js b/packages/core/src/libraries/execa/lib/promise.js deleted file mode 100755 index a4773f3..0000000 --- a/packages/core/src/libraries/execa/lib/promise.js +++ /dev/null @@ -1,36 +0,0 @@ -// eslint-disable-next-line unicorn/prefer-top-level-await -const nativePromisePrototype = (async () => {})().constructor.prototype; - -const descriptors = ['then', 'catch', 'finally'].map(property => [ - property, - Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property), -]); - -// The return value is a mixin of `childProcess` and `Promise` -export const mergePromise = (spawned, promise) => { - for (const [property, descriptor] of descriptors) { - // Starting the main `promise` is deferred to avoid consuming streams - const value = typeof promise === 'function' - ? (...args) => Reflect.apply(descriptor.value, promise(), args) - : descriptor.value.bind(promise); - - Reflect.defineProperty(spawned, property, {...descriptor, value}); - } -}; - -// Use promises instead of `child_process` events -export const getSpawnedPromise = spawned => new Promise((resolve, reject) => { - spawned.on('exit', (exitCode, signal) => { - resolve({exitCode, signal}); - }); - - spawned.on('error', error => { - reject(error); - }); - - if (spawned.stdin) { - spawned.stdin.on('error', error => { - reject(error); - }); - } -}); diff --git a/packages/core/src/libraries/execa/lib/stdio.js b/packages/core/src/libraries/execa/lib/stdio.js deleted file mode 100755 index e8c1132..0000000 --- a/packages/core/src/libraries/execa/lib/stdio.js +++ /dev/null @@ -1,49 +0,0 @@ -const aliases = ['stdin', 'stdout', 'stderr']; - -const hasAlias = options => aliases.some(alias => options[alias] !== undefined); - -export const normalizeStdio = options => { - if (!options) { - return; - } - - const {stdio} = options; - - if (stdio === undefined) { - return aliases.map(alias => options[alias]); - } - - if (hasAlias(options)) { - throw new Error(`It's not possible to provide \`stdio\` in combination with one of ${aliases.map(alias => `\`${alias}\``).join(', ')}`); - } - - if (typeof stdio === 'string') { - return stdio; - } - - if (!Array.isArray(stdio)) { - throw new TypeError(`Expected \`stdio\` to be of type \`string\` or \`Array\`, got \`${typeof stdio}\``); - } - - const length = Math.max(stdio.length, aliases.length); - return Array.from({length}, (value, index) => stdio[index]); -}; - -// `ipc` is pushed unless it is already present -export const normalizeStdioNode = options => { - const stdio = normalizeStdio(options); - - if (stdio === 'ipc') { - return 'ipc'; - } - - if (stdio === undefined || typeof stdio === 'string') { - return [stdio, stdio, stdio, 'ipc']; - } - - if (stdio.includes('ipc')) { - return stdio; - } - - return [...stdio, 'ipc']; -}; diff --git a/packages/core/src/libraries/execa/lib/stream.js b/packages/core/src/libraries/execa/lib/stream.js deleted file mode 100755 index 6912270..0000000 --- a/packages/core/src/libraries/execa/lib/stream.js +++ /dev/null @@ -1,133 +0,0 @@ -import {createReadStream, readFileSync} from 'node:fs'; -import {setTimeout} from 'node:timers/promises'; -import {isStream} from '../../is-stream'; -import getStream, {getStreamAsBuffer} from '../../get-stream'; -import mergeStream from 'merge-stream'; - -const validateInputOptions = input => { - if (input !== undefined) { - throw new TypeError('The `input` and `inputFile` options cannot be both set.'); - } -}; - -const getInputSync = ({input, inputFile}) => { - if (typeof inputFile !== 'string') { - return input; - } - - validateInputOptions(input); - return readFileSync(inputFile); -}; - -// `input` and `inputFile` option in sync mode -export const handleInputSync = options => { - const input = getInputSync(options); - - if (isStream(input)) { - throw new TypeError('The `input` option cannot be a stream in sync mode'); - } - - return input; -}; - -const getInput = ({input, inputFile}) => { - if (typeof inputFile !== 'string') { - return input; - } - - validateInputOptions(input); - return createReadStream(inputFile); -}; - -// `input` and `inputFile` option in async mode -export const handleInput = (spawned, options) => { - const input = getInput(options); - - if (input === undefined) { - return; - } - - if (isStream(input)) { - input.pipe(spawned.stdin); - } else { - spawned.stdin.end(input); - } -}; - -// `all` interleaves `stdout` and `stderr` -export const makeAllStream = (spawned, {all}) => { - if (!all || (!spawned.stdout && !spawned.stderr)) { - return; - } - - const mixed = mergeStream(); - - if (spawned.stdout) { - mixed.add(spawned.stdout); - } - - if (spawned.stderr) { - mixed.add(spawned.stderr); - } - - return mixed; -}; - -// On failure, `result.stdout|stderr|all` should contain the currently buffered stream -const getBufferedData = async (stream, streamPromise) => { - // When `buffer` is `false`, `streamPromise` is `undefined` and there is no buffered data to retrieve - if (!stream || streamPromise === undefined) { - return; - } - - // Wait for the `all` stream to receive the last chunk before destroying the stream - await setTimeout(0); - - stream.destroy(); - - try { - return await streamPromise; - } catch (error) { - return error.bufferedData; - } -}; - -const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => { - if (!stream || !buffer) { - return; - } - - // eslint-disable-next-line unicorn/text-encoding-identifier-case - if (encoding === 'utf8' || encoding === 'utf-8') { - return getStream(stream, {maxBuffer}); - } - - if (encoding === null || encoding === 'buffer') { - return getStreamAsBuffer(stream, {maxBuffer}); - } - - return applyEncoding(stream, maxBuffer, encoding); -}; - -const applyEncoding = async (stream, maxBuffer, encoding) => { - const buffer = await getStreamAsBuffer(stream, {maxBuffer}); - return buffer.toString(encoding); -}; - -// Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) -export const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { - const stdoutPromise = getStreamPromise(stdout, {encoding, buffer, maxBuffer}); - const stderrPromise = getStreamPromise(stderr, {encoding, buffer, maxBuffer}); - const allPromise = getStreamPromise(all, {encoding, buffer, maxBuffer: maxBuffer * 2}); - - try { - return await Promise.all([processDone, stdoutPromise, stderrPromise, allPromise]); - } catch (error) { - return Promise.all([ - {error, signal: error.signal, timedOut: error.timedOut}, - getBufferedData(stdout, stdoutPromise), - getBufferedData(stderr, stderrPromise), - getBufferedData(all, allPromise), - ]); - } -}; diff --git a/packages/core/src/libraries/execa/lib/verbose.js b/packages/core/src/libraries/execa/lib/verbose.js deleted file mode 100755 index 5f5490e..0000000 --- a/packages/core/src/libraries/execa/lib/verbose.js +++ /dev/null @@ -1,19 +0,0 @@ -import {debuglog} from 'node:util'; -import process from 'node:process'; - -export const verboseDefault = debuglog('execa').enabled; - -const padField = (field, padding) => String(field).padStart(padding, '0'); - -const getTimestamp = () => { - const date = new Date(); - return `${padField(date.getHours(), 2)}:${padField(date.getMinutes(), 2)}:${padField(date.getSeconds(), 2)}.${padField(date.getMilliseconds(), 3)}`; -}; - -export const logCommand = (escapedCommand, {verbose}) => { - if (!verbose) { - return; - } - - process.stderr.write(`[${getTimestamp()}] ${escapedCommand}\n`); -}; diff --git a/packages/core/src/libraries/get-stream/array-buffer.js b/packages/core/src/libraries/get-stream/array-buffer.js deleted file mode 100644 index a547405..0000000 --- a/packages/core/src/libraries/get-stream/array-buffer.js +++ /dev/null @@ -1,84 +0,0 @@ -import {getStreamContents} from './contents.js'; -import {noop, throwObjectStream, getLengthProp} from './utils.js'; - -export async function getStreamAsArrayBuffer(stream, options) { - return getStreamContents(stream, arrayBufferMethods, options); -} - -const initArrayBuffer = () => ({contents: new ArrayBuffer(0)}); - -const useTextEncoder = chunk => textEncoder.encode(chunk); -const textEncoder = new TextEncoder(); - -const useUint8Array = chunk => new Uint8Array(chunk); - -const useUint8ArrayWithOffset = chunk => new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); - -const truncateArrayBufferChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); - -// `contents` is an increasingly growing `Uint8Array`. -const addArrayBufferChunk = (convertedChunk, {contents, length: previousLength}, length) => { - const newContents = hasArrayBufferResize() ? resizeArrayBuffer(contents, length) : resizeArrayBufferSlow(contents, length); - new Uint8Array(newContents).set(convertedChunk, previousLength); - return newContents; -}; - -// Without `ArrayBuffer.resize()`, `contents` size is always a power of 2. -// This means its last bytes are zeroes (not stream data), which need to be -// trimmed at the end with `ArrayBuffer.slice()`. -const resizeArrayBufferSlow = (contents, length) => { - if (length <= contents.byteLength) { - return contents; - } - - const arrayBuffer = new ArrayBuffer(getNewContentsLength(length)); - new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); - return arrayBuffer; -}; - -// With `ArrayBuffer.resize()`, `contents` size matches exactly the size of -// the stream data. It does not include extraneous zeroes to trim at the end. -// The underlying `ArrayBuffer` does allocate a number of bytes that is a power -// of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`. -const resizeArrayBuffer = (contents, length) => { - if (length <= contents.maxByteLength) { - contents.resize(length); - return contents; - } - - const arrayBuffer = new ArrayBuffer(length, {maxByteLength: getNewContentsLength(length)}); - new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); - return arrayBuffer; -}; - -// Retrieve the closest `length` that is both >= and a power of 2 -const getNewContentsLength = length => SCALE_FACTOR ** Math.ceil(Math.log(length) / Math.log(SCALE_FACTOR)); - -const SCALE_FACTOR = 2; - -const finalizeArrayBuffer = ({contents, length}) => hasArrayBufferResize() ? contents : contents.slice(0, length); - -// `ArrayBuffer.slice()` is slow. When `ArrayBuffer.resize()` is available -// (Node >=20.0.0, Safari >=16.4 and Chrome), we can use it instead. -// eslint-disable-next-line no-warning-comments -// TODO: remove after dropping support for Node 20. -// eslint-disable-next-line no-warning-comments -// TODO: use `ArrayBuffer.transferToFixedLength()` instead once it is available -const hasArrayBufferResize = () => 'resize' in ArrayBuffer.prototype; - -const arrayBufferMethods = { - init: initArrayBuffer, - convertChunk: { - string: useTextEncoder, - buffer: useUint8Array, - arrayBuffer: useUint8Array, - dataView: useUint8ArrayWithOffset, - typedArray: useUint8ArrayWithOffset, - others: throwObjectStream, - }, - getSize: getLengthProp, - truncateChunk: truncateArrayBufferChunk, - addChunk: addArrayBufferChunk, - getFinalChunk: noop, - finalize: finalizeArrayBuffer, -}; diff --git a/packages/core/src/libraries/get-stream/array.js b/packages/core/src/libraries/get-stream/array.js deleted file mode 100644 index 468bad1..0000000 --- a/packages/core/src/libraries/get-stream/array.js +++ /dev/null @@ -1,32 +0,0 @@ -import {getStreamContents} from './contents.js'; -import {identity, noop, getContentsProp} from './utils.js'; - -export async function getStreamAsArray(stream, options) { - return getStreamContents(stream, arrayMethods, options); -} - -const initArray = () => ({contents: []}); - -const increment = () => 1; - -const addArrayChunk = (convertedChunk, {contents}) => { - contents.push(convertedChunk); - return contents; -}; - -const arrayMethods = { - init: initArray, - convertChunk: { - string: identity, - buffer: identity, - arrayBuffer: identity, - dataView: identity, - typedArray: identity, - others: identity, - }, - getSize: increment, - truncateChunk: noop, - addChunk: addArrayChunk, - getFinalChunk: noop, - finalize: getContentsProp, -}; diff --git a/packages/core/src/libraries/get-stream/buffer.js b/packages/core/src/libraries/get-stream/buffer.js deleted file mode 100644 index 7d22d78..0000000 --- a/packages/core/src/libraries/get-stream/buffer.js +++ /dev/null @@ -1,20 +0,0 @@ -import {getStreamAsArrayBuffer} from './array-buffer.js'; - -export async function getStreamAsBuffer(stream, options) { - if (!('Buffer' in globalThis)) { - throw new Error('getStreamAsBuffer() is only supported in Node.js'); - } - - try { - return arrayBufferToNodeBuffer(await getStreamAsArrayBuffer(stream, options)); - } catch (error) { - if (error.bufferedData !== undefined) { - error.bufferedData = arrayBufferToNodeBuffer(error.bufferedData); - } - - throw error; - } -} - -// eslint-disable-next-line n/prefer-global/buffer -const arrayBufferToNodeBuffer = arrayBuffer => globalThis.Buffer.from(arrayBuffer); diff --git a/packages/core/src/libraries/get-stream/contents.js b/packages/core/src/libraries/get-stream/contents.js deleted file mode 100644 index 2ca36f2..0000000 --- a/packages/core/src/libraries/get-stream/contents.js +++ /dev/null @@ -1,101 +0,0 @@ -export const getStreamContents = async (stream, {init, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, finalize}, {maxBuffer = Number.POSITIVE_INFINITY} = {}) => { - if (!isAsyncIterable(stream)) { - throw new Error('The first argument must be a Readable, a ReadableStream, or an async iterable.'); - } - - const state = init(); - state.length = 0; - - try { - for await (const chunk of stream) { - const chunkType = getChunkType(chunk); - const convertedChunk = convertChunk[chunkType](chunk, state); - appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); - } - - appendFinalChunk({state, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}); - return finalize(state); - } catch (error) { - error.bufferedData = finalize(state); - throw error; - } -}; - -const appendFinalChunk = ({state, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}) => { - const convertedChunk = getFinalChunk(state); - if (convertedChunk !== undefined) { - appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); - } -}; - -const appendChunk = ({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}) => { - const chunkSize = getSize(convertedChunk); - const newLength = state.length + chunkSize; - - if (newLength <= maxBuffer) { - addNewChunk(convertedChunk, state, addChunk, newLength); - return; - } - - const truncatedChunk = truncateChunk(convertedChunk, maxBuffer - state.length); - - if (truncatedChunk !== undefined) { - addNewChunk(truncatedChunk, state, addChunk, maxBuffer); - } - - throw new MaxBufferError(); -}; - -const addNewChunk = (convertedChunk, state, addChunk, newLength) => { - state.contents = addChunk(convertedChunk, state, newLength); - state.length = newLength; -}; - -const isAsyncIterable = stream => typeof stream === 'object' && stream !== null && typeof stream[Symbol.asyncIterator] === 'function'; - -const getChunkType = chunk => { - const typeOfChunk = typeof chunk; - - if (typeOfChunk === 'string') { - return 'string'; - } - - if (typeOfChunk !== 'object' || chunk === null) { - return 'others'; - } - - // eslint-disable-next-line n/prefer-global/buffer - if (globalThis.Buffer?.isBuffer(chunk)) { - return 'buffer'; - } - - const prototypeName = objectToString.call(chunk); - - if (prototypeName === '[object ArrayBuffer]') { - return 'arrayBuffer'; - } - - if (prototypeName === '[object DataView]') { - return 'dataView'; - } - - if ( - Number.isInteger(chunk.byteLength) - && Number.isInteger(chunk.byteOffset) - && objectToString.call(chunk.buffer) === '[object ArrayBuffer]' - ) { - return 'typedArray'; - } - - return 'others'; -}; - -const {toString: objectToString} = Object.prototype; - -export class MaxBufferError extends Error { - name = 'MaxBufferError'; - - constructor() { - super('maxBuffer exceeded'); - } -} diff --git a/packages/core/src/libraries/get-stream/index.d.ts b/packages/core/src/libraries/get-stream/index.d.ts deleted file mode 100644 index 0a456ca..0000000 --- a/packages/core/src/libraries/get-stream/index.d.ts +++ /dev/null @@ -1,119 +0,0 @@ -import {type Readable} from 'node:stream'; -import {type Buffer} from 'node:buffer'; - -export class MaxBufferError extends Error { - readonly name: 'MaxBufferError'; - constructor(); -} - -type TextStreamItem = string | Buffer | ArrayBuffer | ArrayBufferView; -export type AnyStream = Readable | ReadableStream | AsyncIterable; - -export type Options = { - /** - Maximum length of the stream. If exceeded, the promise will be rejected with a `MaxBufferError`. - - Depending on the [method](#api), the length is measured with [`string.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/length), [`buffer.length`](https://nodejs.org/api/buffer.html#buflength), [`arrayBuffer.byteLength`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer/byteLength) or [`array.length`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length). - - @default Infinity - */ - readonly maxBuffer?: number; -}; - -/** -Get the given `stream` as a string. - -@returns The stream's contents as a promise. - -@example -``` -import fs from 'node:fs'; -import getStream from 'get-stream'; - -const stream = fs.createReadStream('unicorn.txt'); - -console.log(await getStream(stream)); -// ,,))))))));, -// __)))))))))))))), -// \|/ -\(((((''''((((((((. -// -*-==//////(('' . `)))))), -// /|\ ))| o ;-. '((((( ,(, -// ( `| / ) ;))))' ,_))^;(~ -// | | | ,))((((_ _____------~~~-. %,;(;(>';'~ -// o_); ; )))(((` ~---~ `:: \ %%~~)(v;(`('~ -// ; ''''```` `: `:::|\,__,%% );`'; ~ -// | _ ) / `:|`----' `-' -// ______/\/~ | / / -// /~;;.____/;;' / ___--,-( `;;;/ -// / // _;______;'------~~~~~ /;;/\ / -// // | | / ; \;;,\ -// (<_ | ; /',/-----' _> -// \_| ||_ //~;~~~~~~~~~ -// `\_| (,~~ -// \~\ -// ~~ -``` - -@example -``` -import getStream from 'get-stream'; - -const {body: readableStream} = await fetch('https://example.com'); -console.log(await getStream(readableStream)); -``` - -@example -``` -import {opendir} from 'node:fs/promises'; -import {getStreamAsArray} from 'get-stream'; - -const asyncIterable = await opendir(directory); -console.log(await getStreamAsArray(asyncIterable)); -``` -*/ -export default function getStream(stream: AnyStream, options?: Options): Promise; - -/** -Get the given `stream` as a Node.js [`Buffer`](https://nodejs.org/api/buffer.html#class-buffer). - -@returns The stream's contents as a promise. - -@example -``` -import {getStreamAsBuffer} from 'get-stream'; - -const stream = fs.createReadStream('unicorn.png'); -console.log(await getStreamAsBuffer(stream)); -``` -*/ -export function getStreamAsBuffer(stream: AnyStream, options?: Options): Promise; - -/** -Get the given `stream` as an [`ArrayBuffer`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ArrayBuffer). - -@returns The stream's contents as a promise. - -@example -``` -import {getStreamAsArrayBuffer} from 'get-stream'; - -const {body: readableStream} = await fetch('https://example.com'); -console.log(await getStreamAsArrayBuffer(readableStream)); -``` -*/ -export function getStreamAsArrayBuffer(stream: AnyStream, options?: Options): Promise; - -/** -Get the given `stream` as an array. Unlike [other methods](#api), this supports [streams of objects](https://nodejs.org/api/stream.html#object-mode). - -@returns The stream's contents as a promise. - -@example -``` -import {getStreamAsArray} from 'get-stream'; - -const {body: readableStream} = await fetch('https://example.com'); -console.log(await getStreamAsArray(readableStream)); -``` -*/ -export function getStreamAsArray(stream: AnyStream, options?: Options): Promise; diff --git a/packages/core/src/libraries/get-stream/index.js b/packages/core/src/libraries/get-stream/index.js deleted file mode 100644 index 43c2dd4..0000000 --- a/packages/core/src/libraries/get-stream/index.js +++ /dev/null @@ -1,5 +0,0 @@ -export {getStreamAsArray} from './array.js'; -export {getStreamAsArrayBuffer} from './array-buffer.js'; -export {getStreamAsBuffer} from './buffer.js'; -export {getStreamAsString as default} from './string.js'; -export {MaxBufferError} from './contents.js'; diff --git a/packages/core/src/libraries/get-stream/index.test-d.ts b/packages/core/src/libraries/get-stream/index.test-d.ts deleted file mode 100644 index c90068f..0000000 --- a/packages/core/src/libraries/get-stream/index.test-d.ts +++ /dev/null @@ -1,98 +0,0 @@ -import {Buffer} from 'node:buffer'; -import {open} from 'node:fs/promises'; -import {type Readable} from 'node:stream'; -import fs from 'node:fs'; -import {expectType, expectError, expectAssignable, expectNotAssignable} from 'tsd'; -import getStream, {getStreamAsBuffer, getStreamAsArrayBuffer, getStreamAsArray, MaxBufferError, type Options, type AnyStream} from './index.js'; - -const nodeStream = fs.createReadStream('foo') as Readable; - -const fileHandle = await open('test'); -const readableStream = fileHandle.readableWebStream(); - -const asyncIterable = (value: T): AsyncGenerator => (async function * () { - yield value; -})(); -const stringAsyncIterable = asyncIterable(''); -const bufferAsyncIterable = asyncIterable(Buffer.from('')); -const arrayBufferAsyncIterable = asyncIterable(new ArrayBuffer(0)); -const dataViewAsyncIterable = asyncIterable(new DataView(new ArrayBuffer(0))); -const typedArrayAsyncIterable = asyncIterable(new Uint8Array([])); -const objectItem = {test: true}; -const objectAsyncIterable = asyncIterable(objectItem); - -expectType(await getStream(nodeStream)); -expectType(await getStream(nodeStream, {maxBuffer: 10})); -expectType(await getStream(readableStream)); -expectType(await getStream(stringAsyncIterable)); -expectType(await getStream(bufferAsyncIterable)); -expectType(await getStream(arrayBufferAsyncIterable)); -expectType(await getStream(dataViewAsyncIterable)); -expectType(await getStream(typedArrayAsyncIterable)); -expectError(await getStream(objectAsyncIterable)); -expectError(await getStream({})); -expectError(await getStream(nodeStream, {maxBuffer: '10'})); -expectError(await getStream(nodeStream, {unknownOption: 10})); -expectError(await getStream(nodeStream, {maxBuffer: 10}, {})); - -expectType(await getStreamAsBuffer(nodeStream)); -expectType(await getStreamAsBuffer(nodeStream, {maxBuffer: 10})); -expectType(await getStreamAsBuffer(readableStream)); -expectType(await getStreamAsBuffer(stringAsyncIterable)); -expectType(await getStreamAsBuffer(bufferAsyncIterable)); -expectType(await getStreamAsBuffer(arrayBufferAsyncIterable)); -expectType(await getStreamAsBuffer(dataViewAsyncIterable)); -expectType(await getStreamAsBuffer(typedArrayAsyncIterable)); -expectError(await getStreamAsBuffer(objectAsyncIterable)); -expectError(await getStreamAsBuffer({})); -expectError(await getStreamAsBuffer(nodeStream, {maxBuffer: '10'})); -expectError(await getStreamAsBuffer(nodeStream, {unknownOption: 10})); -expectError(await getStreamAsBuffer(nodeStream, {maxBuffer: 10}, {})); - -expectType(await getStreamAsArrayBuffer(nodeStream)); -expectType(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: 10})); -expectType(await getStreamAsArrayBuffer(readableStream)); -expectType(await getStreamAsArrayBuffer(stringAsyncIterable)); -expectType(await getStreamAsArrayBuffer(bufferAsyncIterable)); -expectType(await getStreamAsArrayBuffer(arrayBufferAsyncIterable)); -expectType(await getStreamAsArrayBuffer(dataViewAsyncIterable)); -expectType(await getStreamAsArrayBuffer(typedArrayAsyncIterable)); -expectError(await getStreamAsArrayBuffer(objectAsyncIterable)); -expectError(await getStreamAsArrayBuffer({})); -expectError(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: '10'})); -expectError(await getStreamAsArrayBuffer(nodeStream, {unknownOption: 10})); -expectError(await getStreamAsArrayBuffer(nodeStream, {maxBuffer: 10}, {})); - -expectType(await getStreamAsArray(nodeStream)); -expectType(await getStreamAsArray(nodeStream, {maxBuffer: 10})); -expectType(await getStreamAsArray(readableStream)); -expectType(await getStreamAsArray(readableStream as ReadableStream)); -expectType(await getStreamAsArray(stringAsyncIterable)); -expectType(await getStreamAsArray(bufferAsyncIterable)); -expectType(await getStreamAsArray(arrayBufferAsyncIterable)); -expectType(await getStreamAsArray(dataViewAsyncIterable)); -expectType(await getStreamAsArray(typedArrayAsyncIterable)); -expectType>(await getStreamAsArray(objectAsyncIterable)); -expectError(await getStreamAsArray({})); -expectError(await getStreamAsArray(nodeStream, {maxBuffer: '10'})); -expectError(await getStreamAsArray(nodeStream, {unknownOption: 10})); -expectError(await getStreamAsArray(nodeStream, {maxBuffer: 10}, {})); - -expectAssignable(nodeStream); -expectAssignable(readableStream); -expectAssignable(stringAsyncIterable); -expectAssignable(bufferAsyncIterable); -expectAssignable(arrayBufferAsyncIterable); -expectAssignable(dataViewAsyncIterable); -expectAssignable(typedArrayAsyncIterable); -expectAssignable>(objectAsyncIterable); -expectNotAssignable(objectAsyncIterable); -expectAssignable>(stringAsyncIterable); -expectNotAssignable>(bufferAsyncIterable); -expectNotAssignable({}); - -expectAssignable({maxBuffer: 10}); -expectNotAssignable({maxBuffer: '10'}); -expectNotAssignable({unknownOption: 10}); - -expectType(new MaxBufferError()); diff --git a/packages/core/src/libraries/get-stream/string.js b/packages/core/src/libraries/get-stream/string.js deleted file mode 100644 index 90f94b9..0000000 --- a/packages/core/src/libraries/get-stream/string.js +++ /dev/null @@ -1,36 +0,0 @@ -import {getStreamContents} from './contents.js'; -import {identity, getContentsProp, throwObjectStream, getLengthProp} from './utils.js'; - -export async function getStreamAsString(stream, options) { - return getStreamContents(stream, stringMethods, options); -} - -const initString = () => ({contents: '', textDecoder: new TextDecoder()}); - -const useTextDecoder = (chunk, {textDecoder}) => textDecoder.decode(chunk, {stream: true}); - -const addStringChunk = (convertedChunk, {contents}) => contents + convertedChunk; - -const truncateStringChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); - -const getFinalStringChunk = ({textDecoder}) => { - const finalChunk = textDecoder.decode(); - return finalChunk === '' ? undefined : finalChunk; -}; - -const stringMethods = { - init: initString, - convertChunk: { - string: identity, - buffer: useTextDecoder, - arrayBuffer: useTextDecoder, - dataView: useTextDecoder, - typedArray: useTextDecoder, - others: throwObjectStream, - }, - getSize: getLengthProp, - truncateChunk: truncateStringChunk, - addChunk: addStringChunk, - getFinalChunk: getFinalStringChunk, - finalize: getContentsProp, -}; diff --git a/packages/core/src/libraries/get-stream/utils.js b/packages/core/src/libraries/get-stream/utils.js deleted file mode 100644 index af8d5e2..0000000 --- a/packages/core/src/libraries/get-stream/utils.js +++ /dev/null @@ -1,11 +0,0 @@ -export const identity = value => value; - -export const noop = () => undefined; - -export const getContentsProp = ({contents}) => contents; - -export const throwObjectStream = chunk => { - throw new Error(`Streams in object mode are not supported: ${String(chunk)}`); -}; - -export const getLengthProp = convertedChunk => convertedChunk.length; diff --git a/packages/core/src/libraries/human-signals/core.js b/packages/core/src/libraries/human-signals/core.js deleted file mode 100644 index e083d8f..0000000 --- a/packages/core/src/libraries/human-signals/core.js +++ /dev/null @@ -1,275 +0,0 @@ -/* eslint-disable max-lines */ -// List of known process signals with information about them -export const SIGNALS = [ - { - name: 'SIGHUP', - number: 1, - action: 'terminate', - description: 'Terminal closed', - standard: 'posix', - }, - { - name: 'SIGINT', - number: 2, - action: 'terminate', - description: 'User interruption with CTRL-C', - standard: 'ansi', - }, - { - name: 'SIGQUIT', - number: 3, - action: 'core', - description: 'User interruption with CTRL-\\', - standard: 'posix', - }, - { - name: 'SIGILL', - number: 4, - action: 'core', - description: 'Invalid machine instruction', - standard: 'ansi', - }, - { - name: 'SIGTRAP', - number: 5, - action: 'core', - description: 'Debugger breakpoint', - standard: 'posix', - }, - { - name: 'SIGABRT', - number: 6, - action: 'core', - description: 'Aborted', - standard: 'ansi', - }, - { - name: 'SIGIOT', - number: 6, - action: 'core', - description: 'Aborted', - standard: 'bsd', - }, - { - name: 'SIGBUS', - number: 7, - action: 'core', - description: - 'Bus error due to misaligned, non-existing address or paging error', - standard: 'bsd', - }, - { - name: 'SIGEMT', - number: 7, - action: 'terminate', - description: 'Command should be emulated but is not implemented', - standard: 'other', - }, - { - name: 'SIGFPE', - number: 8, - action: 'core', - description: 'Floating point arithmetic error', - standard: 'ansi', - }, - { - name: 'SIGKILL', - number: 9, - action: 'terminate', - description: 'Forced termination', - standard: 'posix', - forced: true, - }, - { - name: 'SIGUSR1', - number: 10, - action: 'terminate', - description: 'Application-specific signal', - standard: 'posix', - }, - { - name: 'SIGSEGV', - number: 11, - action: 'core', - description: 'Segmentation fault', - standard: 'ansi', - }, - { - name: 'SIGUSR2', - number: 12, - action: 'terminate', - description: 'Application-specific signal', - standard: 'posix', - }, - { - name: 'SIGPIPE', - number: 13, - action: 'terminate', - description: 'Broken pipe or socket', - standard: 'posix', - }, - { - name: 'SIGALRM', - number: 14, - action: 'terminate', - description: 'Timeout or timer', - standard: 'posix', - }, - { - name: 'SIGTERM', - number: 15, - action: 'terminate', - description: 'Termination', - standard: 'ansi', - }, - { - name: 'SIGSTKFLT', - number: 16, - action: 'terminate', - description: 'Stack is empty or overflowed', - standard: 'other', - }, - { - name: 'SIGCHLD', - number: 17, - action: 'ignore', - description: 'Child process terminated, paused or unpaused', - standard: 'posix', - }, - { - name: 'SIGCLD', - number: 17, - action: 'ignore', - description: 'Child process terminated, paused or unpaused', - standard: 'other', - }, - { - name: 'SIGCONT', - number: 18, - action: 'unpause', - description: 'Unpaused', - standard: 'posix', - forced: true, - }, - { - name: 'SIGSTOP', - number: 19, - action: 'pause', - description: 'Paused', - standard: 'posix', - forced: true, - }, - { - name: 'SIGTSTP', - number: 20, - action: 'pause', - description: 'Paused using CTRL-Z or "suspend"', - standard: 'posix', - }, - { - name: 'SIGTTIN', - number: 21, - action: 'pause', - description: 'Background process cannot read terminal input', - standard: 'posix', - }, - { - name: 'SIGBREAK', - number: 21, - action: 'terminate', - description: 'User interruption with CTRL-BREAK', - standard: 'other', - }, - { - name: 'SIGTTOU', - number: 22, - action: 'pause', - description: 'Background process cannot write to terminal output', - standard: 'posix', - }, - { - name: 'SIGURG', - number: 23, - action: 'ignore', - description: 'Socket received out-of-band data', - standard: 'bsd', - }, - { - name: 'SIGXCPU', - number: 24, - action: 'core', - description: 'Process timed out', - standard: 'bsd', - }, - { - name: 'SIGXFSZ', - number: 25, - action: 'core', - description: 'File too big', - standard: 'bsd', - }, - { - name: 'SIGVTALRM', - number: 26, - action: 'terminate', - description: 'Timeout or timer', - standard: 'bsd', - }, - { - name: 'SIGPROF', - number: 27, - action: 'terminate', - description: 'Timeout or timer', - standard: 'bsd', - }, - { - name: 'SIGWINCH', - number: 28, - action: 'ignore', - description: 'Terminal window size changed', - standard: 'bsd', - }, - { - name: 'SIGIO', - number: 29, - action: 'terminate', - description: 'I/O is available', - standard: 'other', - }, - { - name: 'SIGPOLL', - number: 29, - action: 'terminate', - description: 'Watched event', - standard: 'other', - }, - { - name: 'SIGINFO', - number: 29, - action: 'ignore', - description: 'Request for process information', - standard: 'other', - }, - { - name: 'SIGPWR', - number: 30, - action: 'terminate', - description: 'Device running out of power', - standard: 'systemv', - }, - { - name: 'SIGSYS', - number: 31, - action: 'core', - description: 'Invalid system call', - standard: 'other', - }, - { - name: 'SIGUNUSED', - number: 31, - action: 'terminate', - description: 'Invalid system call', - standard: 'other', - }, -] -/* eslint-enable max-lines */ diff --git a/packages/core/src/libraries/human-signals/index.js b/packages/core/src/libraries/human-signals/index.js deleted file mode 100644 index fb6e64b..0000000 --- a/packages/core/src/libraries/human-signals/index.js +++ /dev/null @@ -1,70 +0,0 @@ -import { constants } from 'node:os' - -import { SIGRTMAX } from './realtime.js' -import { getSignals } from './signals.js' - -// Retrieve `signalsByName`, an object mapping signal name to signal properties. -// We make sure the object is sorted by `number`. -const getSignalsByName = () => { - const signals = getSignals() - return Object.fromEntries(signals.map(getSignalByName)) -} - -const getSignalByName = ({ - name, - number, - description, - supported, - action, - forced, - standard, -}) => [name, { name, number, description, supported, action, forced, standard }] - -export const signalsByName = getSignalsByName() - -// Retrieve `signalsByNumber`, an object mapping signal number to signal -// properties. -// We make sure the object is sorted by `number`. -const getSignalsByNumber = () => { - const signals = getSignals() - const length = SIGRTMAX + 1 - const signalsA = Array.from({ length }, (value, number) => - getSignalByNumber(number, signals), - ) - return Object.assign({}, ...signalsA) -} - -const getSignalByNumber = (number, signals) => { - const signal = findSignalByNumber(number, signals) - - if (signal === undefined) { - return {} - } - - const { name, description, supported, action, forced, standard } = signal - return { - [number]: { - name, - number, - description, - supported, - action, - forced, - standard, - }, - } -} - -// Several signals might end up sharing the same number because of OS-specific -// numbers, in which case those prevail. -const findSignalByNumber = (number, signals) => { - const signal = signals.find(({ name }) => constants.signals[name] === number) - - if (signal !== undefined) { - return signal - } - - return signals.find((signalA) => signalA.number === number) -} - -export const signalsByNumber = getSignalsByNumber() diff --git a/packages/core/src/libraries/human-signals/realtime.js b/packages/core/src/libraries/human-signals/realtime.js deleted file mode 100644 index 1825d08..0000000 --- a/packages/core/src/libraries/human-signals/realtime.js +++ /dev/null @@ -1,16 +0,0 @@ -// List of realtime signals with information about them -export const getRealtimeSignals = () => { - const length = SIGRTMAX - SIGRTMIN + 1 - return Array.from({ length }, getRealtimeSignal) -} - -const getRealtimeSignal = (value, index) => ({ - name: `SIGRT${index + 1}`, - number: SIGRTMIN + index, - action: 'terminate', - description: 'Application-specific signal (realtime)', - standard: 'posix', -}) - -const SIGRTMIN = 34 -export const SIGRTMAX = 64 diff --git a/packages/core/src/libraries/human-signals/signals.js b/packages/core/src/libraries/human-signals/signals.js deleted file mode 100644 index d76382b..0000000 --- a/packages/core/src/libraries/human-signals/signals.js +++ /dev/null @@ -1,34 +0,0 @@ -import { constants } from 'node:os' - -import { SIGNALS } from './core.js' -import { getRealtimeSignals } from './realtime.js' - -// Retrieve list of know signals (including realtime) with information about -// them -export const getSignals = () => { - const realtimeSignals = getRealtimeSignals() - const signals = [...SIGNALS, ...realtimeSignals].map(normalizeSignal) - return signals -} - -// Normalize signal: -// - `number`: signal numbers are OS-specific. This is taken into account by -// `os.constants.signals`. However we provide a default `number` since some -// signals are not defined for some OS. -// - `forced`: set default to `false` -// - `supported`: set value -const normalizeSignal = ({ - name, - number: defaultNumber, - description, - action, - forced = false, - standard, -}) => { - const { - signals: { [name]: constantSignal }, - } = constants - const supported = constantSignal !== undefined - const number = supported ? constantSignal : defaultNumber - return { name, number, description, supported, action, forced, standard } -} diff --git a/packages/core/src/libraries/is-stream/index.d.ts b/packages/core/src/libraries/is-stream/index.d.ts deleted file mode 100644 index df994e0..0000000 --- a/packages/core/src/libraries/is-stream/index.d.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { - Stream, - Writable as WritableStream, - Readable as ReadableStream, - Duplex as DuplexStream, - Transform as TransformStream, -} from 'node:stream'; - -/** -@returns Whether `stream` is a [`Stream`](https://nodejs.org/api/stream.html#stream_stream). - -@example -``` -import fs from 'node:fs'; -import {isStream} from 'is-stream'; - -isStream(fs.createReadStream('unicorn.png')); -//=> true - -isStream({}); -//=> false -``` -*/ -export function isStream(stream: unknown): stream is Stream; - -/** -@returns Whether `stream` is a [`stream.Writable`](https://nodejs.org/api/stream.html#stream_class_stream_writable). - -@example -``` -import fs from 'node:fs'; -import {isWritableStream} from 'is-stream'; - -isWritableStream(fs.createWriteStrem('unicorn.txt')); -//=> true -``` -*/ -export function isWritableStream(stream: unknown): stream is WritableStream; - -/** -@returns Whether `stream` is a [`stream.Readable`](https://nodejs.org/api/stream.html#stream_class_stream_readable). - -@example -``` -import fs from 'node:fs'; -import {isReadableStream} from 'is-stream'; - -isReadableStream(fs.createReadStream('unicorn.png')); -//=> true -``` -*/ -export function isReadableStream(stream: unknown): stream is ReadableStream; - -/** -@returns Whether `stream` is a [`stream.Duplex`](https://nodejs.org/api/stream.html#stream_class_stream_duplex). - -@example -``` -import {Duplex as DuplexStream} from 'node:stream'; -import {isDuplexStream} from 'is-stream'; - -isDuplexStream(new DuplexStream()); -//=> true -``` -*/ -export function isDuplexStream(stream: unknown): stream is DuplexStream; - -/** -@returns Whether `stream` is a [`stream.Transform`](https://nodejs.org/api/stream.html#stream_class_stream_transform). - -@example -``` -import fs from 'node:fs'; -import StringifyStream from 'streaming-json-stringify'; -import {isTransformStream} from 'is-stream'; - -isTransformStream(StringifyStream()); -//=> true -``` -*/ -export function isTransformStream(stream: unknown): stream is TransformStream; diff --git a/packages/core/src/libraries/is-stream/index.js b/packages/core/src/libraries/is-stream/index.js deleted file mode 100644 index 887e601..0000000 --- a/packages/core/src/libraries/is-stream/index.js +++ /dev/null @@ -1,29 +0,0 @@ -export function isStream(stream) { - return stream !== null - && typeof stream === 'object' - && typeof stream.pipe === 'function'; -} - -export function isWritableStream(stream) { - return isStream(stream) - && stream.writable !== false - && typeof stream._write === 'function' - && typeof stream._writableState === 'object'; -} - -export function isReadableStream(stream) { - return isStream(stream) - && stream.readable !== false - && typeof stream._read === 'function' - && typeof stream._readableState === 'object'; -} - -export function isDuplexStream(stream) { - return isWritableStream(stream) - && isReadableStream(stream); -} - -export function isTransformStream(stream) { - return isDuplexStream(stream) - && typeof stream._transform === 'function'; -} diff --git a/packages/core/src/libraries/lowdb/adapters/Memory.js b/packages/core/src/libraries/lowdb/adapters/Memory.js deleted file mode 100644 index 798cd36..0000000 --- a/packages/core/src/libraries/lowdb/adapters/Memory.js +++ /dev/null @@ -1,24 +0,0 @@ -export class Memory { - #data = null - - read() { - return Promise.resolve(this.#data) - } - - write(obj) { - this.#data = obj - return Promise.resolve() - } -} - -export class MemorySync { - #data = null - - read() { - return this.#data || null - } - - write(obj) { - this.#data = obj - } -} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/DataFile.js b/packages/core/src/libraries/lowdb/adapters/node/DataFile.js deleted file mode 100644 index 0506e0c..0000000 --- a/packages/core/src/libraries/lowdb/adapters/node/DataFile.js +++ /dev/null @@ -1,51 +0,0 @@ -import { TextFile, TextFileSync } from "./TextFile.js" - -export class DataFile { - #adapter - #parse - #stringify - - constructor(filename, { parse, stringify }) { - this.#adapter = new TextFile(filename) - this.#parse = parse - this.#stringify = stringify - } - - async read() { - const data = await this.#adapter.read() - if (data === null) { - return null - } else { - return this.#parse(data) - } - } - - write(obj) { - return this.#adapter.write(this.#stringify(obj)) - } -} - -export class DataFileSync { - #adapter - #parse - #stringify - - constructor(filename, { parse, stringify }) { - this.#adapter = new TextFileSync(filename) - this.#parse = parse - this.#stringify = stringify - } - - read() { - const data = this.#adapter.read() - if (data === null) { - return null - } else { - return this.#parse(data) - } - } - - write(obj) { - this.#adapter.write(this.#stringify(obj)) - } -} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js b/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js deleted file mode 100644 index 8811a87..0000000 --- a/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js +++ /dev/null @@ -1,19 +0,0 @@ -import { DataFile, DataFileSync } from "./DataFile.js"; - -export class JSONFile extends DataFile { - constructor(filename) { - super(filename, { - parse: JSON.parse, - stringify: (data) => JSON.stringify(data, null, 2), - }); - } -} - -export class JSONFileSync extends DataFileSync { - constructor(filename) { - super(filename, { - parse: JSON.parse, - stringify: (data) => JSON.stringify(data, null, 2), - }); - } -} diff --git a/packages/core/src/libraries/lowdb/adapters/node/TextFile.js b/packages/core/src/libraries/lowdb/adapters/node/TextFile.js deleted file mode 100644 index b1f3321..0000000 --- a/packages/core/src/libraries/lowdb/adapters/node/TextFile.js +++ /dev/null @@ -1,65 +0,0 @@ -import { readFileSync, renameSync, writeFileSync } from "node:fs" -import { readFile } from "node:fs/promises" -import path from "node:path" - -import { Writer } from "../../steno" - -export class TextFile { - #filename - #writer - - constructor(filename) { - this.#filename = filename - this.#writer = new Writer(filename) - } - - async read() { - let data - - try { - data = await readFile(this.#filename, "utf-8") - } catch (e) { - if (e.code === "ENOENT") { - return null - } - throw e - } - - return data - } - - write(str) { - return this.#writer.write(str) - } -} - -export class TextFileSync { - #tempFilename - #filename - - constructor(filename) { - this.#filename = filename - const f = filename.toString() - this.#tempFilename = path.join(path.dirname(f), `.${path.basename(f)}.tmp`) - } - - read() { - let data - - try { - data = readFileSync(this.#filename, "utf-8") - } catch (e) { - if (e.code === "ENOENT") { - return null - } - throw e - } - - return data - } - - write(str) { - writeFileSync(this.#tempFilename, str) - renameSync(this.#tempFilename, this.#filename) - } -} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/core/Low.js b/packages/core/src/libraries/lowdb/core/Low.js deleted file mode 100644 index f0b76e5..0000000 --- a/packages/core/src/libraries/lowdb/core/Low.js +++ /dev/null @@ -1,48 +0,0 @@ -function checkArgs(adapter, defaultData) { - if (adapter === undefined) throw new Error("lowdb: missing adapter") - if (defaultData === undefined) throw new Error("lowdb: missing default data") -} - -export class Low { - constructor(adapter, defaultData) { - checkArgs(adapter, defaultData) - this.adapter = adapter - this.data = defaultData - } - - async read() { - const data = await this.adapter.read() - if (data) this.data = data - } - - async write() { - if (this.data) await this.adapter.write(this.data) - } - - async update(fn) { - fn(this.data) - await this.write() - } -} - -export class LowSync { - constructor(adapter, defaultData) { - checkArgs(adapter, defaultData) - this.adapter = adapter - this.data = defaultData - } - - read() { - const data = this.adapter.read() - if (data) this.data = data - } - - write() { - if (this.data) this.adapter.write(this.data) - } - - update(fn) { - fn(this.data) - this.write() - } -} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/presets/node.js b/packages/core/src/libraries/lowdb/presets/node.js deleted file mode 100644 index e8526fb..0000000 --- a/packages/core/src/libraries/lowdb/presets/node.js +++ /dev/null @@ -1,23 +0,0 @@ -import { Memory, MemorySync } from "../adapters/Memory.js" -import { JSONFile, JSONFileSync } from "../adapters/node/JSONFile.js" -import { Low, LowSync } from "../core/Low.js" - -export async function JSONFilePreset(filename, defaultData) { - const adapter = process.env.NODE_ENV === "test" ? new Memory() : new JSONFile(filename) - - const db = new Low(adapter, defaultData) - - await db.read() - - return db -} - -export function JSONFileSyncPreset(filename, defaultData) { - const adapter = process.env.NODE_ENV === "test" ? new MemorySync() : new JSONFileSync(filename) - - const db = new LowSync(adapter, defaultData) - - db.read() - - return db -} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/steno/index.js b/packages/core/src/libraries/lowdb/steno/index.js deleted file mode 100644 index d0c5558..0000000 --- a/packages/core/src/libraries/lowdb/steno/index.js +++ /dev/null @@ -1,98 +0,0 @@ -import { rename, writeFile } from "node:fs/promises" -import { basename, dirname, join } from "node:path" -import { fileURLToPath } from "node:url" - -// Returns a temporary file -// Example: for /some/file will return /some/.file.tmp -function getTempFilename(file) { - const f = file instanceof URL ? fileURLToPath(file) : file.toString() - return join(dirname(f), `.${basename(f)}.tmp`) -} - -// Retries an asynchronous operation with a delay between retries and a maximum retry count -async function retryAsyncOperation(fn, maxRetries, delayMs) { - for (let i = 0; i < maxRetries; i++) { - try { - return await fn() - } catch (error) { - if (i < maxRetries - 1) { - await new Promise(resolve => setTimeout(resolve, delayMs)) - } else { - throw error // Rethrow the error if max retries reached - } - } - } -} - -export class Writer { - #filename - #tempFilename - #locked = false - #prev = null - #next = null - #nextPromise = null - #nextData = null - - // File is locked, add data for later - #add(data) { - // Only keep most recent data - this.#nextData = data - - // Create a singleton promise to resolve all next promises once next data is written - this.#nextPromise ||= new Promise((resolve, reject) => { - this.#next = [resolve, reject] - }) - - // Return a promise that will resolve at the same time as next promise - return new Promise((resolve, reject) => { - this.#nextPromise?.then(resolve).catch(reject) - }) - } - - // File isn't locked, write data - async #write(data) { - // Lock file - this.#locked = true - try { - // Atomic write - await writeFile(this.#tempFilename, data, "utf-8") - await retryAsyncOperation( - async () => { - await rename(this.#tempFilename, this.#filename) - }, - 10, - 100 - ) - - // Call resolve - this.#prev?.[0]() - } catch (err) { - // Call reject - if (err instanceof Error) { - this.#prev?.[1](err) - } - throw err - } finally { - // Unlock file - this.#locked = false - - this.#prev = this.#next - this.#next = this.#nextPromise = null - - if (this.#nextData !== null) { - const nextData = this.#nextData - this.#nextData = null - await this.write(nextData) - } - } - } - - constructor(filename) { - this.#filename = filename - this.#tempFilename = getTempFilename(filename) - } - - async write(data) { - return this.#locked ? this.#add(data) : this.#write(data) - } -} \ No newline at end of file diff --git a/packages/core/src/libraries/mimic-function/index.js b/packages/core/src/libraries/mimic-function/index.js deleted file mode 100644 index 61e6701..0000000 --- a/packages/core/src/libraries/mimic-function/index.js +++ /dev/null @@ -1,71 +0,0 @@ -const copyProperty = (to, from, property, ignoreNonConfigurable) => { - // `Function#length` should reflect the parameters of `to` not `from` since we keep its body. - // `Function#prototype` is non-writable and non-configurable so can never be modified. - if (property === 'length' || property === 'prototype') { - return; - } - - // `Function#arguments` and `Function#caller` should not be copied. They were reported to be present in `Reflect.ownKeys` for some devices in React Native (#41), so we explicitly ignore them here. - if (property === 'arguments' || property === 'caller') { - return; - } - - const toDescriptor = Object.getOwnPropertyDescriptor(to, property); - const fromDescriptor = Object.getOwnPropertyDescriptor(from, property); - - if (!canCopyProperty(toDescriptor, fromDescriptor) && ignoreNonConfigurable) { - return; - } - - Object.defineProperty(to, property, fromDescriptor); -}; - -// `Object.defineProperty()` throws if the property exists, is not configurable and either: -// - one its descriptors is changed -// - it is non-writable and its value is changed -const canCopyProperty = function (toDescriptor, fromDescriptor) { - return toDescriptor === undefined || toDescriptor.configurable || ( - toDescriptor.writable === fromDescriptor.writable - && toDescriptor.enumerable === fromDescriptor.enumerable - && toDescriptor.configurable === fromDescriptor.configurable - && (toDescriptor.writable || toDescriptor.value === fromDescriptor.value) - ); -}; - -const changePrototype = (to, from) => { - const fromPrototype = Object.getPrototypeOf(from); - if (fromPrototype === Object.getPrototypeOf(to)) { - return; - } - - Object.setPrototypeOf(to, fromPrototype); -}; - -const wrappedToString = (withName, fromBody) => `/* Wrapped ${withName}*/\n${fromBody}`; - -const toStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, 'toString'); -const toStringName = Object.getOwnPropertyDescriptor(Function.prototype.toString, 'name'); - -// We call `from.toString()` early (not lazily) to ensure `from` can be garbage collected. -// We use `bind()` instead of a closure for the same reason. -// Calling `from.toString()` early also allows caching it in case `to.toString()` is called several times. -const changeToString = (to, from, name) => { - const withName = name === '' ? '' : `with ${name.trim()}() `; - const newToString = wrappedToString.bind(null, withName, from.toString()); - // Ensure `to.toString.toString` is non-enumerable and has the same `same` - Object.defineProperty(newToString, 'name', toStringName); - Object.defineProperty(to, 'toString', { ...toStringDescriptor, value: newToString }); -}; - -export default function mimicFunction(to, from, { ignoreNonConfigurable = false } = {}) { - const { name } = to; - - for (const property of Reflect.ownKeys(from)) { - copyProperty(to, from, property, ignoreNonConfigurable); - } - - changePrototype(to, from); - changeToString(to, from, name); - - return to; -} \ No newline at end of file diff --git a/packages/core/src/libraries/npm-run-path/index.d.ts b/packages/core/src/libraries/npm-run-path/index.d.ts deleted file mode 100644 index 0c1b160..0000000 --- a/packages/core/src/libraries/npm-run-path/index.d.ts +++ /dev/null @@ -1,84 +0,0 @@ -export interface RunPathOptions { - /** - Working directory. - - @default process.cwd() - */ - readonly cwd?: string | URL; - - /** - PATH to be appended. Default: [`PATH`](https://github.com/sindresorhus/path-key). - - Set it to an empty string to exclude the default PATH. - */ - readonly path?: string; - - /** - Path to the Node.js executable to use in child processes if that is different from the current one. Its directory is pushed to the front of PATH. - - This can be either an absolute path or a path relative to the `cwd` option. - - @default process.execPath - */ - readonly execPath?: string | URL; -} - -export type ProcessEnv = Record; - -export interface EnvOptions { - /** - The working directory. - - @default process.cwd() - */ - readonly cwd?: string | URL; - - /** - Accepts an object of environment variables, like `process.env`, and modifies the PATH using the correct [PATH key](https://github.com/sindresorhus/path-key). Use this if you're modifying the PATH for use in the `child_process` options. - */ - readonly env?: ProcessEnv; - - /** - The path to the current Node.js executable. Its directory is pushed to the front of PATH. - - This can be either an absolute path or a path relative to the `cwd` option. - - @default process.execPath - */ - readonly execPath?: string | URL; -} - -/** -Get your [PATH](https://en.wikipedia.org/wiki/PATH_(variable)) prepended with locally installed binaries. - -@returns The augmented path string. - -@example -``` -import childProcess from 'node:child_process'; -import {npmRunPath} from 'npm-run-path'; - -console.log(process.env.PATH); -//=> '/usr/local/bin' - -console.log(npmRunPath()); -//=> '/Users/sindresorhus/dev/foo/node_modules/.bin:/Users/sindresorhus/dev/node_modules/.bin:/Users/sindresorhus/node_modules/.bin:/Users/node_modules/.bin:/node_modules/.bin:/usr/local/bin' -``` -*/ -export function npmRunPath(options?: RunPathOptions): string; - -/** -@returns The augmented [`process.env`](https://nodejs.org/api/process.html#process_process_env) object. - -@example -``` -import childProcess from 'node:child_process'; -import {npmRunPathEnv} from 'npm-run-path'; - -// `foo` is a locally installed binary -childProcess.execFileSync('foo', { - env: npmRunPathEnv() -}); -``` -*/ -export function npmRunPathEnv(options?: EnvOptions): ProcessEnv; diff --git a/packages/core/src/libraries/npm-run-path/index.js b/packages/core/src/libraries/npm-run-path/index.js deleted file mode 100644 index 782a96a..0000000 --- a/packages/core/src/libraries/npm-run-path/index.js +++ /dev/null @@ -1,51 +0,0 @@ -import process from 'node:process'; -import path from 'node:path'; -import url from 'node:url'; - -function pathKey(options = {}) { - const { - env = process.env, - platform = process.platform - } = options; - - if (platform !== 'win32') { - return 'PATH'; - } - - return Object.keys(env).reverse().find(key => key.toUpperCase() === 'PATH') || 'Path'; -} - -export function npmRunPath(options = {}) { - const { - cwd = process.cwd(), - path: path_ = process.env[pathKey()], - execPath = process.execPath, - } = options; - - let previous; - const execPathString = execPath instanceof URL ? url.fileURLToPath(execPath) : execPath; - const cwdString = cwd instanceof URL ? url.fileURLToPath(cwd) : cwd; - let cwdPath = path.resolve(cwdString); - const result = []; - - while (previous !== cwdPath) { - result.push(path.join(cwdPath, 'node_modules/.bin')); - previous = cwdPath; - cwdPath = path.resolve(cwdPath, '..'); - } - - // Ensure the running `node` binary is used. - result.push(path.resolve(cwdString, execPathString, '..')); - - return [...result, path_].join(path.delimiter); -} - -export function npmRunPathEnv({ env = process.env, ...options } = {}) { - env = { ...env }; - - const path = pathKey({ env }); - options.path = env[path]; - env[path] = npmRunPath(options); - - return env; -} diff --git a/packages/core/src/libraries/onetime/index.d.ts b/packages/core/src/libraries/onetime/index.d.ts deleted file mode 100644 index fa9fc20..0000000 --- a/packages/core/src/libraries/onetime/index.d.ts +++ /dev/null @@ -1,59 +0,0 @@ -export type Options = { - /** - Throw an error when called more than once. - - @default false - */ - readonly throw?: boolean; -}; - -declare const onetime: { - /** - Ensure a function is only called once. When called multiple times it will return the return value from the first call. - - @param fn - The function that should only be called once. - @returns A function that only calls `fn` once. - - @example - ``` - import onetime from 'onetime'; - - let index = 0; - - const foo = onetime(() => ++index); - - foo(); //=> 1 - foo(); //=> 1 - foo(); //=> 1 - - onetime.callCount(foo); //=> 3 - ``` - */ - ( - fn: (...arguments_: ArgumentsType) => ReturnType, - options?: Options - ): (...arguments_: ArgumentsType) => ReturnType; - - /** - Get the number of times `fn` has been called. - - @param fn - The function to get call count from. - @returns A number representing how many times `fn` has been called. - - @example - ``` - import onetime from 'onetime'; - - const foo = onetime(() => {}); - foo(); - foo(); - foo(); - - console.log(onetime.callCount(foo)); - //=> 3 - ``` - */ - callCount(fn: (...arguments_: any[]) => unknown): number; -}; - -export default onetime; diff --git a/packages/core/src/libraries/onetime/index.js b/packages/core/src/libraries/onetime/index.js deleted file mode 100644 index 880e94d..0000000 --- a/packages/core/src/libraries/onetime/index.js +++ /dev/null @@ -1,41 +0,0 @@ -import mimicFunction from '../mimic-function'; - -const calledFunctions = new WeakMap(); - -const onetime = (function_, options = {}) => { - if (typeof function_ !== 'function') { - throw new TypeError('Expected a function'); - } - - let returnValue; - let callCount = 0; - const functionName = function_.displayName || function_.name || ''; - - const onetime = function (...arguments_) { - calledFunctions.set(onetime, ++callCount); - - if (callCount === 1) { - returnValue = function_.apply(this, arguments_); - function_ = undefined; - } else if (options.throw === true) { - throw new Error(`Function \`${functionName}\` can only be called once`); - } - - return returnValue; - }; - - mimicFunction(onetime, function_); - calledFunctions.set(onetime, callCount); - - return onetime; -}; - -onetime.callCount = function_ => { - if (!calledFunctions.has(function_)) { - throw new Error(`The given function \`${function_.name}\` is not wrapped by the \`onetime\` package`); - } - - return calledFunctions.get(function_); -}; - -export default onetime; diff --git a/packages/core/src/libraries/strip-final-newline/index.d.ts b/packages/core/src/libraries/strip-final-newline/index.d.ts deleted file mode 100644 index e8fa1d3..0000000 --- a/packages/core/src/libraries/strip-final-newline/index.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** -Strip the final [newline character](https://en.wikipedia.org/wiki/Newline) from a string or Uint8Array. - -@returns The input without any final newline. - -@example -``` -import stripFinalNewline from 'strip-final-newline'; - -stripFinalNewline('foo\nbar\n\n'); -//=> 'foo\nbar\n' - -const uint8Array = new TextEncoder().encode('foo\nbar\n\n') -new TextDecoder().decode(stripFinalNewline(uint8Array)); -//=> 'foo\nbar\n' -``` -*/ -export default function stripFinalNewline(input: T): T; diff --git a/packages/core/src/libraries/strip-final-newline/index.js b/packages/core/src/libraries/strip-final-newline/index.js deleted file mode 100644 index a63ed26..0000000 --- a/packages/core/src/libraries/strip-final-newline/index.js +++ /dev/null @@ -1,26 +0,0 @@ -export default function stripFinalNewline(input) { - if (typeof input === 'string') { - return stripFinalNewlineString(input); - } - - if (!(ArrayBuffer.isView(input) && input.BYTES_PER_ELEMENT === 1)) { - throw new Error('Input must be a string or a Uint8Array'); - } - - return stripFinalNewlineBinary(input); -} - -const stripFinalNewlineString = input => - input.at(-1) === LF - ? input.slice(0, input.at(-2) === CR ? -2 : -1) - : input; - -const stripFinalNewlineBinary = input => - input.at(-1) === LF_BINARY - ? input.subarray(0, input.at(-2) === CR_BINARY ? -2 : -1) - : input; - -const LF = '\n'; -const LF_BINARY = LF.codePointAt(0); -const CR = '\r'; -const CR_BINARY = CR.codePointAt(0); diff --git a/packages/core/src/logger.js b/packages/core/src/logger.js deleted file mode 100644 index 1f3d6d1..0000000 --- a/packages/core/src/logger.js +++ /dev/null @@ -1,40 +0,0 @@ -import winston from "winston" -import colors from "cli-color" - -const servicesToColor = { - "CORE": { - color: "whiteBright", - background: "bgBlackBright", - }, - "INSTALL": { - color: "whiteBright", - background: "bgBlueBright", - }, -} - -const paintText = (level, service, ...args) => { - let { color, background } = servicesToColor[service ?? "CORE"] ?? servicesToColor["CORE"] - - if (level === "error") { - color = "whiteBright" - background = "bgRedBright" - } - - return colors[background][color](...args) -} - -const format = winston.format.printf(({ timestamp, service = "CORE", level, message, }) => { - return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}` -}) - -export default winston.createLogger({ - format: winston.format.combine( - winston.format.timestamp(), - format - ), - transports: [ - new winston.transports.Console(), - //new winston.transports.File({ filename: "error.log", level: "error" }), - //new winston.transports.File({ filename: "combined.log" }), - ], -}) \ No newline at end of file diff --git a/packages/core/src/manifest/libraries.js b/packages/core/src/manifest/libraries.js deleted file mode 100644 index edab737..0000000 --- a/packages/core/src/manifest/libraries.js +++ /dev/null @@ -1,23 +0,0 @@ -import PublicInternalLibraries from "./libs" - -const isAClass = (x) => x && typeof x === "function" && x.prototype && typeof x.prototype.constructor === "function" - -export default async (dependencies, bindCtx) => { - const libraries = {} - - for await (const lib of dependencies) { - if (PublicInternalLibraries[lib]) { - if (typeof PublicInternalLibraries[lib] === "function" && isAClass(PublicInternalLibraries[lib])) { - libraries[lib] = new PublicInternalLibraries[lib](bindCtx) - - if (libraries[lib].initialize) { - await libraries[lib].initialize() - } - } else { - libraries[lib] = PublicInternalLibraries[lib] - } - } - } - - return libraries -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/auth/index.js b/packages/core/src/manifest/libs/auth/index.js deleted file mode 100644 index 2f1371a..0000000 --- a/packages/core/src/manifest/libs/auth/index.js +++ /dev/null @@ -1,54 +0,0 @@ -import open from "open" -import axios from "axios" -import ManifestAuthDB from "../../../classes/ManifestAuthDB" - -export default class Auth { - constructor(ctx) { - this.manifest = ctx.manifest - } - - async get() { - const storagedData = await ManifestAuthDB.get(this.manifest.id) - - if (storagedData && this.manifest.authService) { - if (!this.manifest.authService.getter) { - return storagedData - } - - const result = await axios({ - method: "POST", - url: this.manifest.authService.getter, - headers: { - "Content-Type": "application/json", - }, - data: { - auth_data: storagedData, - } - }).catch((err) => { - global._relic_eventBus.emit("auth:getter:error", err) - - return err - }) - - if (result instanceof Error) { - throw result - } - - console.log(result.data) - - return result.data - } - - return storagedData - } - - request() { - if (!this.manifest.authService || !this.manifest.authService.fetcher) { - return false - } - - const authURL = this.manifest.authService.fetcher - - open(authURL) - } -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/fs/index.js b/packages/core/src/manifest/libs/fs/index.js deleted file mode 100644 index a025bc1..0000000 --- a/packages/core/src/manifest/libs/fs/index.js +++ /dev/null @@ -1,39 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -// Protect from reading or write operations outside of the package directory -export default class SecureFileSystem { - constructor(ctx) { - this.jailPath = ctx.manifest.install_path - } - - checkOutsideJail(target) { - // if (!path.resolve(target).startsWith(this.jailPath)) { - // throw new Error("Cannot access resource outside of package directory") - // } - } - - readFileSync(destination, options) { - this.checkOutsideJail(destination) - - return fs.readFileSync(finalPath, options) - } - - copyFileSync(from, to) { - this.checkOutsideJail(from) - this.checkOutsideJail(to) - - return fs.copyFileSync(from, to) - } - - writeFileSync(destination, data, options) { - this.checkOutsideJail(destination) - - return fs.writeFileSync(finalPath, data, options) - } - - // don't need to check finalPath - existsSync(...args) { - return fs.existsSync(...args) - } -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/index.js b/packages/core/src/manifest/libs/index.js deleted file mode 100644 index 3a33e39..0000000 --- a/packages/core/src/manifest/libs/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import Open from "./open" -import Path from "./path" -import Fs from "./fs" -import Auth from "./auth" - -// Third party libraries -import Mcl from "./mcl" - -export default { - fs: Fs, - path: Path, - open: Open, - auth: Auth, - mcl: Mcl -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/authenticator.js b/packages/core/src/manifest/libs/mcl/authenticator.js deleted file mode 100644 index c5973d8..0000000 --- a/packages/core/src/manifest/libs/mcl/authenticator.js +++ /dev/null @@ -1,167 +0,0 @@ -const request = require('request') -const { v3 } = require('uuid') - -let uuid -let api_url = 'https://authserver.mojang.com' - -function parsePropts(array) { - if (array) { - const newObj = {} - for (const entry of array) { - if (newObj[entry.name]) { - newObj[entry.name].push(entry.value) - } else { - newObj[entry.name] = [entry.value] - } - } - return JSON.stringify(newObj) - } else { - return '{}' - } -} - -function getUUID(value) { - if (!uuid) { - uuid = v3(value, v3.DNS) - } - return uuid -} - -const Authenticator = { - getAuth: (username, password, client_token = null) => { - return new Promise((resolve, reject) => { - getUUID(username) - if (!password) { - const user = { - access_token: uuid, - client_token: client_token || uuid, - uuid, - name: username, - user_properties: '{}' - } - - return resolve(user) - } - - const requestObject = { - url: api_url + '/authenticate', - json: { - agent: { - name: 'Minecraft', - version: 1 - }, - username, - password, - clientToken: uuid, - requestUser: true - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - if (!body || !body.selectedProfile) { - return reject(new Error('Validation error: ' + response.statusMessage)) - } - - const userProfile = { - access_token: body.accessToken, - client_token: body.clientToken, - uuid: body.selectedProfile.id, - name: body.selectedProfile.name, - selected_profile: body.selectedProfile, - user_properties: parsePropts(body.user.properties) - } - - resolve(userProfile) - }) - }) - }, - validate: (accessToken, clientToken) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/validate', - json: { - accessToken, - clientToken - } - } - - request.post(requestObject, async function (error, response, body) { - if (error) return reject(error) - - if (!body) resolve(true) - else reject(body) - }) - }) - }, - refreshAuth: (accessToken, clientToken) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/refresh', - json: { - accessToken, - clientToken, - requestUser: true - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - if (!body || !body.selectedProfile) { - return reject(new Error('Validation error: ' + response.statusMessage)) - } - - const userProfile = { - access_token: body.accessToken, - client_token: getUUID(body.selectedProfile.name), - uuid: body.selectedProfile.id, - name: body.selectedProfile.name, - user_properties: parsePropts(body.user.properties) - } - - return resolve(userProfile) - }) - }) - }, - invalidate: (accessToken, clientToken) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/invalidate', - json: { - accessToken, - clientToken - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - - if (!body) return resolve(true) - else return reject(body) - }) - }) - }, - signOut: (username, password) => { - return new Promise((resolve, reject) => { - const requestObject = { - url: api_url + '/signout', - json: { - username, - password - } - } - - request.post(requestObject, function (error, response, body) { - if (error) return reject(error) - - if (!body) return resolve(true) - else return reject(body) - }) - }) - }, - changeApiUrl: (url) => { - api_url = url - } -} - -export default Authenticator \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/handler.js b/packages/core/src/manifest/libs/mcl/handler.js deleted file mode 100644 index ca0a477..0000000 --- a/packages/core/src/manifest/libs/mcl/handler.js +++ /dev/null @@ -1,783 +0,0 @@ -const fs = require('fs') -const path = require('path') -const request = require('request') -const checksum = require('checksum') -const Zip = require('adm-zip') -const child = require('child_process') -let counter = 0 - -export default class Handler { - constructor (client) { - this.client = client - this.options = client.options - this.baseRequest = request.defaults({ - pool: { maxSockets: this.options.overrides.maxSockets || 2 }, - timeout: this.options.timeout || 10000 - }) - } - - checkJava (java) { - return new Promise(resolve => { - child.exec(`"${java}" -version`, (error, stdout, stderr) => { - if (error) { - resolve({ - run: false, - message: error - }) - } else { - this.client.emit('debug', `[MCLC]: Using Java version ${stderr.match(/"(.*?)"/).pop()} ${stderr.includes('64-Bit') ? '64-bit' : '32-Bit'}`) - resolve({ - run: true - }) - } - }) - }) - } - - downloadAsync (url, directory, name, retry, type) { - return new Promise(resolve => { - fs.mkdirSync(directory, { recursive: true }) - - const _request = this.baseRequest(url) - - let receivedBytes = 0 - let totalBytes = 0 - - _request.on('response', (data) => { - if (data.statusCode === 404) { - this.client.emit('debug', `[MCLC]: Failed to download ${url} due to: File not found...`) - return resolve(false) - } - - totalBytes = parseInt(data.headers['content-length']) - }) - - _request.on('error', async (error) => { - this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${error}.` + - ` Retrying... ${retry}`) - if (retry) await this.downloadAsync(url, directory, name, false, type) - resolve() - }) - - _request.on('data', (data) => { - receivedBytes += data.length - this.client.emit('download-status', { - name: name, - type: type, - current: receivedBytes, - total: totalBytes - }) - }) - - const file = fs.createWriteStream(path.join(directory, name)) - _request.pipe(file) - - file.once('finish', () => { - this.client.emit('download', name) - resolve({ - failed: false, - asset: null - }) - }) - - file.on('error', async (e) => { - this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${e}.` + - ` Retrying... ${retry}`) - if (fs.existsSync(path.join(directory, name))) fs.unlinkSync(path.join(directory, name)) - if (retry) await this.downloadAsync(url, directory, name, false, type) - resolve() - }) - }) - } - - checkSum (hash, file) { - return new Promise((resolve, reject) => { - checksum.file(file, (err, sum) => { - if (err) { - this.client.emit('debug', `[MCLC]: Failed to check file hash due to ${err}`) - resolve(false) - } else { - resolve(hash === sum) - } - }) - }) - } - - getVersion () { - return new Promise(resolve => { - const versionJsonPath = this.options.overrides.versionJson || path.join(this.options.directory, `${this.options.version.number}.json`) - - if (fs.existsSync(versionJsonPath)) { - this.version = JSON.parse(fs.readFileSync(versionJsonPath)) - - return resolve(this.version) - } - - const manifest = `${this.options.overrides.url.meta}/mc/game/version_manifest.json` - - const cache = this.options.cache ? `${this.options.cache}/json` : `${this.options.root}/cache/json` - - request.get(manifest, (error, response, body) => { - if (error && error.code !== 'ENOTFOUND') { - return resolve(error) - } - - if (!error) { - if (!fs.existsSync(cache)) { - fs.mkdirSync(cache, { recursive: true }) - - this.client.emit('debug', '[MCLC]: Cache directory created.') - } - - fs.writeFile(path.join(`${cache}/version_manifest.json`), body, (err) => { - if (err) { - return resolve(err) - } - - this.client.emit('debug', '[MCLC]: Cached version_manifest.json (from request)') - }) - } - - let parsed = null - - if (error && (error.code === 'ENOTFOUND')) { - parsed = JSON.parse(fs.readFileSync(`${cache}/version_manifest.json`)) - } else { - parsed = JSON.parse(body) - } - - const versionManifest = parsed.versions.find((version) => { - return version.id === this.options.version.number - }) - - if (!versionManifest) { - return resolve(new Error(`Version not found`)) - } - - request.get(versionManifest.url, (error, response, body) => { - if (error && error.code !== 'ENOTFOUND') { - return resolve(error) - } - - if (!error) { - fs.writeFile(path.join(`${cache}/${this.options.version.number}.json`), body, (err) => { - if (err) { - return resolve(err) - } - - this.client.emit('debug', `[MCLC]: Cached ${this.options.version.number}.json`) - }) - } - - this.client.emit('debug', '[MCLC]: Parsed version from version manifest') - - if (error && (error.code === 'ENOTFOUND')) { - this.version = JSON.parse(fs.readFileSync(`${cache}/${this.options.version.number}.json`)) - } else { - this.version = JSON.parse(body) - } - - this.client.emit('debug', this.version) - - return resolve(this.version) - }) - }) - }) - } - - async getJar () { - await this.downloadAsync(this.version.downloads.client.url, this.options.directory, `${this.options.version.custom ? this.options.version.custom : this.options.version.number}.jar`, true, 'version-jar') - fs.writeFileSync(path.join(this.options.directory, `${this.options.version.number}.json`), JSON.stringify(this.version, null, 4)) - return this.client.emit('debug', '[MCLC]: Downloaded version jar and wrote version json') - } - - async getAssets () { - const assetDirectory = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) - const assetId = this.options.version.custom || this.options.version.number - if (!fs.existsSync(path.join(assetDirectory, 'indexes', `${assetId}.json`))) { - await this.downloadAsync(this.version.assetIndex.url, path.join(assetDirectory, 'indexes'), - `${assetId}.json`, true, 'asset-json') - } - - const index = JSON.parse(fs.readFileSync(path.join(assetDirectory, 'indexes', `${assetId}.json`), { encoding: 'utf8' })) - - this.client.emit('progress', { - type: 'assets', - task: 0, - total: Object.keys(index.objects).length - }) - - await Promise.all(Object.keys(index.objects).map(async asset => { - const hash = index.objects[asset].hash - const subhash = hash.substring(0, 2) - const subAsset = path.join(assetDirectory, 'objects', subhash) - - if (!fs.existsSync(path.join(subAsset, hash)) || !await this.checkSum(hash, path.join(subAsset, hash))) { - await this.downloadAsync(`${this.options.overrides.url.resource}/${subhash}/${hash}`, subAsset, hash, - true, 'assets') - } - counter++ - this.client.emit('progress', { - type: 'assets', - task: counter, - total: Object.keys(index.objects).length - }) - })) - counter = 0 - - // Copy assets to legacy if it's an older Minecraft version. - if (this.isLegacy()) { - if (fs.existsSync(path.join(assetDirectory, 'legacy'))) { - this.client.emit('debug', '[MCLC]: The \'legacy\' directory is no longer used as Minecraft looks ' + - 'for the resouces folder regardless of what is passed in the assetDirecotry launch option. I\'d ' + - `recommend removing the directory (${path.join(assetDirectory, 'legacy')})`) - } - - const legacyDirectory = path.join(this.options.root, 'resources') - this.client.emit('debug', `[MCLC]: Copying assets over to ${legacyDirectory}`) - - this.client.emit('progress', { - type: 'assets-copy', - task: 0, - total: Object.keys(index.objects).length - }) - - await Promise.all(Object.keys(index.objects).map(async asset => { - const hash = index.objects[asset].hash - const subhash = hash.substring(0, 2) - const subAsset = path.join(assetDirectory, 'objects', subhash) - - const legacyAsset = asset.split('/') - legacyAsset.pop() - - if (!fs.existsSync(path.join(legacyDirectory, legacyAsset.join('/')))) { - fs.mkdirSync(path.join(legacyDirectory, legacyAsset.join('/')), { recursive: true }) - } - - if (!fs.existsSync(path.join(legacyDirectory, asset))) { - fs.copyFileSync(path.join(subAsset, hash), path.join(legacyDirectory, asset)) - } - counter++ - this.client.emit('progress', { - type: 'assets-copy', - task: counter, - total: Object.keys(index.objects).length - }) - })) - } - counter = 0 - - this.client.emit('debug', '[MCLC]: Downloaded assets') - } - - parseRule (lib) { - if (lib.rules) { - if (lib.rules.length > 1) { - if (lib.rules[0].action === 'allow' && - lib.rules[1].action === 'disallow' && - lib.rules[1].os.name === 'osx') { - return this.getOS() === 'osx' - } else { - return true - } - } else { - if (lib.rules[0].action === 'allow' && lib.rules[0].os) return lib.rules[0].os.name !== this.getOS() - } - } else { - return false - } - } - - async getNatives () { - const nativeDirectory = path.resolve(this.options.overrides.natives || path.join(this.options.root, 'natives', this.version.id)) - - if (parseInt(this.version.id.split('.')[1]) >= 19) return this.options.overrides.cwd || this.options.root - - if (!fs.existsSync(nativeDirectory) || !fs.readdirSync(nativeDirectory).length) { - fs.mkdirSync(nativeDirectory, { recursive: true }) - - const natives = async () => { - const natives = [] - await Promise.all(this.version.libraries.map(async (lib) => { - if (!lib.downloads || !lib.downloads.classifiers) return - if (this.parseRule(lib)) return - - const native = this.getOS() === 'osx' - ? lib.downloads.classifiers['natives-osx'] || lib.downloads.classifiers['natives-macos'] - : lib.downloads.classifiers[`natives-${this.getOS()}`] - - natives.push(native) - })) - return natives - } - const stat = await natives() - - this.client.emit('progress', { - type: 'natives', - task: 0, - total: stat.length - }) - - await Promise.all(stat.map(async (native) => { - if (!native) return - const name = native.path.split('/').pop() - await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') - if (!await this.checkSum(native.sha1, path.join(nativeDirectory, name))) { - await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') - } - try { - new Zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true) - } catch (e) { - // Only doing a console.warn since a stupid error happens. You can basically ignore this. - // if it says Invalid file name, just means two files were downloaded and both were deleted. - // All is well. - console.warn(e) - } - fs.unlinkSync(path.join(nativeDirectory, name)) - counter++ - this.client.emit('progress', { - type: 'natives', - task: counter, - total: stat.length - }) - })) - this.client.emit('debug', '[MCLC]: Downloaded and extracted natives') - } - - counter = 0 - this.client.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`) - - return nativeDirectory - } - - fwAddArgs () { - const forgeWrapperAgrs = [ - `-Dforgewrapper.librariesDir=${path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries'))}`, - `-Dforgewrapper.installer=${this.options.forge}`, - `-Dforgewrapper.minecraft=${this.options.mcPath}` - ] - this.options.customArgs - ? this.options.customArgs = this.options.customArgs.concat(forgeWrapperAgrs) - : this.options.customArgs = forgeWrapperAgrs - } - - isModernForge (json) { - return json.inheritsFrom && json.inheritsFrom.split('.')[1] >= 12 && !(json.inheritsFrom === '1.12.2' && (json.id.split('.')[json.id.split('.').length - 1]) === '2847') - } - - async getForgedWrapped () { - let json = null - let installerJson = null - const versionPath = path.join(this.options.root, 'forge', `${this.version.id}`, 'version.json') - // Since we're building a proper "custom" JSON that will work nativly with MCLC, the version JSON will not - // be re-generated on the next run. - if (fs.existsSync(versionPath)) { - try { - json = JSON.parse(fs.readFileSync(versionPath)) - if (!json.forgeWrapperVersion || !(json.forgeWrapperVersion === this.options.overrides.fw.version)) { - this.client.emit('debug', '[MCLC]: Old ForgeWrapper has generated this version JSON, re-generating') - } else { - // If forge is modern, add ForgeWrappers launch arguments and set forge to null so MCLC treats it as a custom json. - if (this.isModernForge(json)) { - this.fwAddArgs() - this.options.forge = null - } - return json - } - } catch (e) { - console.warn(e) - this.client.emit('debug', '[MCLC]: Failed to parse Forge version JSON, re-generating') - } - } - - this.client.emit('debug', '[MCLC]: Generating a proper version json, this might take a bit') - const zipFile = new Zip(this.options.forge) - json = zipFile.readAsText('version.json') - if (zipFile.getEntry('install_profile.json')) installerJson = zipFile.readAsText('install_profile.json') - - try { - json = JSON.parse(json) - if (installerJson) installerJson = JSON.parse(installerJson) - } catch (e) { - this.client.emit('debug', '[MCLC]: Failed to load json files for ForgeWrapper, using Vanilla instead') - return null - } - // Adding the installer libraries as mavenFiles so MCLC downloads them but doesn't add them to the class paths. - if (installerJson) { - json.mavenFiles - ? json.mavenFiles = json.mavenFiles.concat(installerJson.libraries) - : json.mavenFiles = installerJson.libraries - } - - // Holder for the specifc jar ending which depends on the specifc forge version. - let jarEnding = 'universal' - // We need to handle modern forge differently than legacy. - if (this.isModernForge(json)) { - // If forge is modern and above 1.12.2, we add ForgeWrapper to the libraries so MCLC includes it in the classpaths. - if (json.inheritsFrom !== '1.12.2') { - this.fwAddArgs() - const fwName = `ForgeWrapper-${this.options.overrides.fw.version}.jar` - const fwPathArr = ['io', 'github', 'zekerzhayard', 'ForgeWrapper', this.options.overrides.fw.version] - json.libraries.push({ - name: fwPathArr.join(':'), - downloads: { - artifact: { - path: [...fwPathArr, fwName].join('/'), - url: `${this.options.overrides.fw.baseUrl}${this.options.overrides.fw.version}/${fwName}`, - sha1: this.options.overrides.fw.sh1, - size: this.options.overrides.fw.size - } - } - }) - json.mainClass = 'io.github.zekerzhayard.forgewrapper.installer.Main' - jarEnding = 'launcher' - - // Providing a download URL to the universal jar mavenFile so it can be downloaded properly. - for (const library of json.mavenFiles) { - const lib = library.name.split(':') - if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { - library.downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + library.downloads.artifact.path - break - } - } - } else { - // Remove the forge dependent since we're going to overwrite the first entry anyways. - for (const library in json.mavenFiles) { - const lib = json.mavenFiles[library].name.split(':') - if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { - delete json.mavenFiles[library] - break - } - } - } - } else { - // Modifying legacy library format to play nice with MCLC's downloadToDirectory function. - await Promise.all(json.libraries.map(async library => { - const lib = library.name.split(':') - if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) return - - let url = this.options.overrides.url.mavenForge - const name = `${lib[1]}-${lib[2]}.jar` - - if (!library.url) { - if (library.serverreq || library.clientreq) { - url = this.options.overrides.url.defaultRepoForge - } else { - return - } - } - library.url = url - const downloadLink = `${url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` - // Checking if the file still exists on Forge's server, if not, replace it with the fallback. - // Not checking for sucess, only if it 404s. - this.baseRequest(downloadLink, (error, response, body) => { - if (error) { - this.client.emit('debug', `[MCLC]: Failed checking request for ${downloadLink}`) - } else { - if (response.statusCode === 404) library.url = this.options.overrides.url.fallbackMaven - } - }) - })) - } - // If a downloads property exists, we modify the inital forge entry to include ${jarEnding} so ForgeWrapper can work properly. - // If it doesn't, we simply remove it since we're already providing the universal jar. - if (json.libraries[0].downloads) { - if (json.libraries[0].name.includes('minecraftforge')) { - json.libraries[0].name = json.libraries[0].name + `:${jarEnding}` - json.libraries[0].downloads.artifact.path = json.libraries[0].downloads.artifact.path.replace('.jar', `-${jarEnding}.jar`) - json.libraries[0].downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + json.libraries[0].downloads.artifact.path - } - } else { - delete json.libraries[0] - } - - // Removing duplicates and null types - json.libraries = this.cleanUp(json.libraries) - if (json.mavenFiles) json.mavenFiles = this.cleanUp(json.mavenFiles) - - json.forgeWrapperVersion = this.options.overrides.fw.version - - // Saving file for next run! - if (!fs.existsSync(path.join(this.options.root, 'forge', this.version.id))) { - fs.mkdirSync(path.join(this.options.root, 'forge', this.version.id), { recursive: true }) - } - fs.writeFileSync(versionPath, JSON.stringify(json, null, 4)) - - // Make MCLC treat modern forge as a custom version json rather then legacy forge. - if (this.isModernForge(json)) this.options.forge = null - - return json - } - - runInstaller (path) { - return new Promise(resolve => { - const installer = child.exec(path) - installer.on('close', (code) => resolve(code)) - }) - } - - async downloadToDirectory (directory, libraries, eventName) { - const libs = [] - - await Promise.all(libraries.map(async library => { - if (!library) return - if (this.parseRule(library)) return - const lib = library.name.split(':') - - let jarPath - let name - if (library.downloads && library.downloads.artifact && library.downloads.artifact.path) { - name = library.downloads.artifact.path.split('/')[library.downloads.artifact.path.split('/').length - 1] - jarPath = path.join(directory, this.popString(library.downloads.artifact.path)) - } else { - name = `${lib[1]}-${lib[2]}${lib[3] ? '-' + lib[3] : ''}.jar` - jarPath = path.join(directory, `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`) - } - - const downloadLibrary = async library => { - if (library.url) { - const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` - await this.downloadAsync(url, jarPath, name, true, eventName) - } else if (library.downloads && library.downloads.artifact) { - await this.downloadAsync(library.downloads.artifact.url, jarPath, name, true, eventName) - } - } - - if (!fs.existsSync(path.join(jarPath, name))) downloadLibrary(library) - else if (library.downloads && library.downloads.artifact) { - if (!this.checkSum(library.downloads.artifact.sha1, path.join(jarPath, name))) downloadLibrary(library) - } - - counter++ - this.client.emit('progress', { - type: eventName, - task: counter, - total: libraries.length - }) - libs.push(`${jarPath}${path.sep}${name}`) - })) - counter = 0 - - return libs - } - - async getClasses (classJson) { - let libs = [] - - const libraryDirectory = path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries')) - - if (classJson) { - if (classJson.mavenFiles) { - await this.downloadToDirectory(libraryDirectory, classJson.mavenFiles, 'classes-maven-custom') - } - libs = (await this.downloadToDirectory(libraryDirectory, classJson.libraries, 'classes-custom')) - } - - const parsed = this.version.libraries.map(lib => { - if (lib.downloads && lib.downloads.artifact && !this.parseRule(lib)) return lib - }) - - libs = libs.concat((await this.downloadToDirectory(libraryDirectory, parsed, 'classes'))) - counter = 0 - - // Temp Quilt support - if (classJson) libs.sort() - - this.client.emit('debug', '[MCLC]: Collected class paths') - return libs - } - - popString (path) { - const tempArray = path.split('/') - tempArray.pop() - return tempArray.join('/') - } - - cleanUp (array) { - const newArray = [] - for (const classPath in array) { - if (newArray.includes(array[classPath]) || array[classPath] === null) continue - newArray.push(array[classPath]) - } - return newArray - } - - formatQuickPlay () { - const types = { - singleplayer: '--quickPlaySingleplayer', - multiplayer: '--quickPlayMultiplayer', - realms: '--quickPlayRealms', - legacy: null - } - const { type, identifier, path } = this.options.quickPlay - const keys = Object.keys(types) - if (!keys.includes(type)) { - this.client.emit('debug', `[MCLC]: quickPlay type is not valid. Valid types are: ${keys.join(', ')}`) - return null - } - const returnArgs = type === 'legacy' - ? ['--server', identifier.split(':')[0], '--port', identifier.split(':')[1] || '25565'] - : [types[type], identifier] - if (path) returnArgs.push('--quickPlayPath', path) - return returnArgs - } - - async getLaunchOptions (modification) { - const type = Object.assign({}, this.version, modification) - - let args = type.minecraftArguments - ? type.minecraftArguments.split(' ') - : type.arguments.game - const assetRoot = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) - const assetPath = this.isLegacy() - ? path.join(this.options.root, 'resources') - : path.join(assetRoot) - - const minArgs = this.options.overrides.minArgs || this.isLegacy() ? 5 : 11 - if (args.length < minArgs) args = args.concat(this.version.minecraftArguments ? this.version.minecraftArguments.split(' ') : this.version.arguments.game) - if (this.options.customLaunchArgs) args = args.concat(this.options.customLaunchArgs) - - this.options.authorization = await Promise.resolve(this.options.authorization) - this.options.authorization.meta = this.options.authorization.meta ? this.options.authorization.meta : { type: 'mojang' } - const fields = { - '${auth_access_token}': this.options.authorization.access_token, - '${auth_session}': this.options.authorization.access_token, - '${auth_player_name}': this.options.authorization.name, - '${auth_uuid}': this.options.authorization.uuid, - '${auth_xuid}': this.options.authorization.meta.xuid || this.options.authorization.access_token, - '${user_properties}': this.options.authorization.user_properties, - '${user_type}': this.options.authorization.meta.type, - '${version_name}': this.options.version.number, - '${assets_index_name}': this.options.overrides.assetIndex || this.options.version.custom || this.options.version.number, - '${game_directory}': this.options.overrides.gameDirectory || this.options.root, - '${assets_root}': assetPath, - '${game_assets}': assetPath, - '${version_type}': this.options.version.type, - '${clientid}': this.options.authorization.meta.clientId || (this.options.authorization.client_token || this.options.authorization.access_token), - '${resolution_width}': this.options.window ? this.options.window.width : 856, - '${resolution_height}': this.options.window ? this.options.window.height : 482 - } - - if (this.options.authorization.meta.demo && (this.options.features ? !this.options.features.includes('is_demo_user') : true)) { - args.push('--demo') - } - - const replaceArg = (obj, index) => { - if (Array.isArray(obj.value)) { - for (const arg of obj.value) { - args.push(arg) - } - } else { - args.push(obj.value) - } - delete args[index] - } - - for (let index = 0; index < args.length; index++) { - if (typeof args[index] === 'object') { - if (args[index].rules) { - if (!this.options.features) continue - const featureFlags = [] - for (const rule of args[index].rules) { - featureFlags.push(...Object.keys(rule.features)) - } - let hasAllRules = true - for (const feature of this.options.features) { - if (!featureFlags.includes(feature)) { - hasAllRules = false - } - } - if (hasAllRules) replaceArg(args[index], index) - } else { - replaceArg(args[index], index) - } - } else { - if (Object.keys(fields).includes(args[index])) { - args[index] = fields[args[index]] - } - } - } - if (this.options.window) { - // eslint-disable-next-line no-unused-expressions - this.options.window.fullscreen - ? args.push('--fullscreen') - : () => { - if (this.options.features ? !this.options.features.includes('has_custom_resolution') : true) { - args.push('--width', this.options.window.width, '--height', this.options.window.height) - } - } - } - if (this.options.server) this.client.emit('debug', '[MCLC]: server and port are deprecated launch flags. Use the quickPlay field.') - if (this.options.quickPlay) args = args.concat(this.formatQuickPlay()) - if (this.options.proxy) { - args.push( - '--proxyHost', - this.options.proxy.host, - '--proxyPort', - this.options.proxy.port || '8080', - '--proxyUser', - this.options.proxy.username, - '--proxyPass', - this.options.proxy.password - ) - } - args = args.filter(value => typeof value === 'string' || typeof value === 'number') - this.client.emit('debug', '[MCLC]: Set launch options') - return args - } - - async getJVM () { - const opts = { - windows: '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump', - osx: '-XstartOnFirstThread', - linux: '-Xss1M' - } - return opts[this.getOS()] - } - - isLegacy () { - return this.version.assets === 'legacy' || this.version.assets === 'pre-1.6' - } - - getOS () { - if (this.options.os) { - return this.options.os - } else { - switch (process.platform) { - case 'win32': return 'windows' - case 'darwin': return 'osx' - default: return 'linux' - } - } - } - - // To prevent launchers from breaking when they update. Will be reworked with rewrite. - getMemory () { - if (!this.options.memory) { - this.client.emit('debug', '[MCLC]: Memory not set! Setting 1GB as MAX!') - this.options.memory = { - min: 512, - max: 1023 - } - } - if (!isNaN(this.options.memory.max) && !isNaN(this.options.memory.min)) { - if (this.options.memory.max < this.options.memory.min) { - this.client.emit('debug', '[MCLC]: MIN memory is higher then MAX! Resetting!') - this.options.memory.max = 1023 - this.options.memory.min = 512 - } - return [`${this.options.memory.max}M`, `${this.options.memory.min}M`] - } else { return [`${this.options.memory.max}`, `${this.options.memory.min}`] } - } - - async extractPackage (options = this.options) { - if (options.clientPackage.startsWith('http')) { - await this.downloadAsync(options.clientPackage, options.root, 'clientPackage.zip', true, 'client-package') - options.clientPackage = path.join(options.root, 'clientPackage.zip') - } - new Zip(options.clientPackage).extractAllTo(options.root, true) - if (options.removePackage) fs.unlinkSync(options.clientPackage) - - return this.client.emit('package-extract', true) - } -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/index.js b/packages/core/src/manifest/libs/mcl/index.js deleted file mode 100644 index 1386624..0000000 --- a/packages/core/src/manifest/libs/mcl/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import Logger from "../../../logger" - -import Client from "./launcher" -import Authenticator from "./authenticator" - -const Log = Logger.child({ service: "MCL" }) - -export default class MCL { - /** - * Asynchronously authenticate the user using the provided username and password. - * - * @param {string} username - the username of the user - * @param {string} password - the password of the user - * @return {Promise} the authentication information - */ - async auth(username, password) { - return await Authenticator.getAuth(username, password) - } - - /** - * Launches a new client with the given options. - * - * @param {Object} opts - The options to be passed for launching the client. - * @return {Promise} A promise that resolves with the launched client. - */ - async launch(opts, callbacks) { - const launcher = new Client() - - launcher.on("debug", (e) => console.log(e)) - launcher.on("data", (e) => console.log(e)) - launcher.on("close", (e) => console.log(e)) - launcher.on("error", (e) => console.log(e)) - - if (typeof callbacks === "undefined") { - callbacks = { - install: () => { - Log.info("Downloading Minecraft assets...") - }, - init_assets: () => { - Log.info("Initializing Minecraft assets...") - } - } - } - - await launcher.launch(opts, callbacks) - - return launcher - } -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/launcher.js b/packages/core/src/manifest/libs/mcl/launcher.js deleted file mode 100644 index 5a5aa76..0000000 --- a/packages/core/src/manifest/libs/mcl/launcher.js +++ /dev/null @@ -1,224 +0,0 @@ -import fs from "node:fs" -import path from "node:path" -import { EventEmitter } from "events" -import child from "child_process" - -import Handler from "./handler" - -export default class MCLCore extends EventEmitter { - async launch(options, callbacks = {}) { - try { - this.options = { ...options } - - this.options.root = path.resolve(this.options.root) - - this.options.overrides = { - detached: true, - ...this.options.overrides, - url: { - meta: 'https://launchermeta.mojang.com', - resource: 'https://resources.download.minecraft.net', - mavenForge: 'http://files.minecraftforge.net/maven/', - defaultRepoForge: 'https://libraries.minecraft.net/', - fallbackMaven: 'https://search.maven.org/remotecontent?filepath=', - ...this.options.overrides - ? this.options.overrides.url - : undefined - }, - fw: { - baseUrl: 'https://github.com/ZekerZhayard/ForgeWrapper/releases/download/', - version: '1.5.6', - sh1: 'b38d28e8b7fde13b1bc0db946a2da6760fecf98d', - size: 34715, - ...this.options.overrides - ? this.options.overrides.fw - : undefined - } - } - - this.handler = new Handler(this) - - this.printVersion() - - const java = await this.handler.checkJava(this.options.javaPath || 'java') - - if (!java.run) { - this.emit('debug', `[MCLC]: Couldn't start Minecraft due to: ${java.message}`) - this.emit('close', 1) - return null - } - - this.createRootDirectory() - this.createGameDirectory() - - await this.extractPackage() - - if (this.options.installer) { - // So installers that create a profile in launcher_profiles.json can run without breaking. - const profilePath = path.join(this.options.root, 'launcher_profiles.json') - if (!fs.existsSync(profilePath) || !JSON.parse(fs.readFileSync(profilePath)).profiles) { - fs.writeFileSync(profilePath, JSON.stringify({ profiles: {} }, null, 4)) - } - const code = await this.handler.runInstaller(this.options.installer) - if (!this.options.version.custom && code === 0) { - this.emit('debug', '[MCLC]: Installer successfully ran, but no custom version was provided') - } - this.emit('debug', `[MCLC]: Installer closed with code ${code}`) - } - - const directory = this.options.overrides.directory || path.join(this.options.root, 'versions', this.options.version.custom ? this.options.version.custom : this.options.version.number) - this.options.directory = directory - - const versionFile = await this.handler.getVersion() - - const mcPath = this.options.overrides.minecraftJar || (this.options.version.custom - ? path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.jar`) - : path.join(directory, `${this.options.version.number}.jar`)) - - this.options.mcPath = mcPath - - const nativePath = await this.handler.getNatives() - - if (!fs.existsSync(mcPath)) { - this.emit('debug', '[MCLC]: Attempting to download Minecraft version jar') - - if (typeof callbacks.install === "function") { - callbacks.install() - } - - await this.handler.getJar() - } - - const modifyJson = await this.getModifyJson() - - const args = [] - - let jvm = [ - '-XX:-UseAdaptiveSizePolicy', - '-XX:-OmitStackTraceInFastThrow', - '-Dfml.ignorePatchDiscrepancies=true', - '-Dfml.ignoreInvalidMinecraftCertificates=true', - `-Djava.library.path=${nativePath}`, - `-Xmx${this.handler.getMemory()[0]}`, - `-Xms${this.handler.getMemory()[1]}` - ] - if (this.handler.getOS() === 'osx') { - if (parseInt(versionFile.id.split('.')[1]) > 12) jvm.push(await this.handler.getJVM()) - } else jvm.push(await this.handler.getJVM()) - - if (this.options.customArgs) jvm = jvm.concat(this.options.customArgs) - if (this.options.overrides.logj4ConfigurationFile) { - jvm.push(`-Dlog4j.configurationFile=${path.resolve(this.options.overrides.logj4ConfigurationFile)}`) - } - // https://help.minecraft.net/hc/en-us/articles/4416199399693-Security-Vulnerability-in-Minecraft-Java-Edition - if (parseInt(versionFile.id.split('.')[1]) === 18 && !parseInt(versionFile.id.split('.')[2])) jvm.push('-Dlog4j2.formatMsgNoLookups=true') - if (parseInt(versionFile.id.split('.')[1]) === 17) jvm.push('-Dlog4j2.formatMsgNoLookups=true') - if (parseInt(versionFile.id.split('.')[1]) < 17) { - if (!jvm.find(arg => arg.includes('Dlog4j.configurationFile'))) { - const configPath = path.resolve(this.options.overrides.cwd || this.options.root) - const intVersion = parseInt(versionFile.id.split('.')[1]) - if (intVersion >= 12) { - await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml', - configPath, 'log4j2_112-116.xml', true, 'log4j') - jvm.push('-Dlog4j.configurationFile=log4j2_112-116.xml') - } else if (intVersion >= 7) { - await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/dd2b723346a8dcd48e7f4d245f6bf09e98db9696/log4j2_17-111.xml', - configPath, 'log4j2_17-111.xml', true, 'log4j') - jvm.push('-Dlog4j.configurationFile=log4j2_17-111.xml') - } - } - } - - const classes = this.options.overrides.classes || this.handler.cleanUp(await this.handler.getClasses(modifyJson)) - const classPaths = ['-cp'] - const separator = this.handler.getOS() === 'windows' ? ';' : ':' - - this.emit('debug', `[MCLC]: Using ${separator} to separate class paths`) - - // Handling launch arguments. - const file = modifyJson || versionFile - - // So mods like fabric work. - const jar = fs.existsSync(mcPath) - ? `${separator}${mcPath}` - : `${separator}${path.join(directory, `${this.options.version.number}.jar`)}` - classPaths.push(`${this.options.forge ? this.options.forge + separator : ''}${classes.join(separator)}${jar}`) - classPaths.push(file.mainClass) - - this.emit('debug', '[MCLC]: Attempting to download assets') - - if (typeof callbacks.init_assets === "function") { - callbacks.init_assets() - } - - await this.handler.getAssets() - - // Forge -> Custom -> Vanilla - const launchOptions = await this.handler.getLaunchOptions(modifyJson) - - const launchArguments = args.concat(jvm, classPaths, launchOptions) - this.emit('arguments', launchArguments) - this.emit('debug', `[MCLC]: Launching with arguments ${launchArguments.join(' ')}`) - - return this.startMinecraft(launchArguments) - } catch (e) { - this.emit('debug', `[MCLC]: Failed to start due to ${e}, closing...`) - return null - } - } - - printVersion() { - if (fs.existsSync(path.join(__dirname, '..', 'package.json'))) { - const { version } = require('../package.json') - this.emit('debug', `[MCLC]: MCLC version ${version}`) - } else { this.emit('debug', '[MCLC]: Package JSON not found, skipping MCLC version check.') } - } - - createRootDirectory() { - if (!fs.existsSync(this.options.root)) { - this.emit('debug', '[MCLC]: Attempting to create root folder') - fs.mkdirSync(this.options.root) - } - } - - createGameDirectory() { - if (this.options.overrides.gameDirectory) { - this.options.overrides.gameDirectory = path.resolve(this.options.overrides.gameDirectory) - if (!fs.existsSync(this.options.overrides.gameDirectory)) { - fs.mkdirSync(this.options.overrides.gameDirectory, { recursive: true }) - } - } - } - - async extractPackage() { - if (this.options.clientPackage) { - this.emit('debug', `[MCLC]: Extracting client package to ${this.options.root}`) - await this.handler.extractPackage() - } - } - - async getModifyJson() { - let modifyJson = null - - if (this.options.forge) { - this.options.forge = path.resolve(this.options.forge) - this.emit('debug', '[MCLC]: Detected Forge in options, getting dependencies') - modifyJson = await this.handler.getForgedWrapped() - } else if (this.options.version.custom) { - this.emit('debug', '[MCLC]: Detected custom in options, setting custom version file') - modifyJson = modifyJson || JSON.parse(fs.readFileSync(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`), { encoding: 'utf8' })) - } - - return modifyJson - } - - startMinecraft(launchArguments) { - const minecraft = child.spawn(this.options.javaPath ? this.options.javaPath : 'java', launchArguments, - { cwd: this.options.overrides.cwd || this.options.root, detached: this.options.overrides.detached }) - - minecraft.stdout.on('data', (data) => this.emit('data', data.toString('utf-8'))) - minecraft.stderr.on('data', (data) => this.emit('data', data.toString('utf-8'))) - minecraft.on('close', (code) => this.emit('close', code)) - return minecraft - } -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/open/index.js b/packages/core/src/manifest/libs/open/index.js deleted file mode 100644 index d696826..0000000 --- a/packages/core/src/manifest/libs/open/index.js +++ /dev/null @@ -1,15 +0,0 @@ -import Logger from "../../../logger" - -import open, { apps } from "open" - -const Log = Logger.child({ service: "OPEN-LIB" }) - -export default { - spawn: async (...args) => { - Log.info("Open spawned with args >") - console.log(...args) - - return await open(...args) - }, - apps: apps, -} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/path/index.js b/packages/core/src/manifest/libs/path/index.js deleted file mode 100644 index 1b4bd73..0000000 --- a/packages/core/src/manifest/libs/path/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import path from "node:path" - -export default path \ No newline at end of file diff --git a/packages/core/src/manifest/reader.js b/packages/core/src/manifest/reader.js deleted file mode 100644 index 830278f..0000000 --- a/packages/core/src/manifest/reader.js +++ /dev/null @@ -1,51 +0,0 @@ -import fs from "node:fs" -import path from "node:path" -import axios from "axios" -import checksum from "checksum" - -import Vars from "../vars" - -export async function readManifest(manifest) { - // check if manifest is a directory or a url - const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi - - const target = manifest?.remote_url ?? manifest - - if (urlRegex.test(target)) { - if (!fs.existsSync(Vars.cache_path)) { - fs.mkdirSync(Vars.cache_path, { recursive: true }) - } - - const { data: code } = await axios.get(target) - - const manifestChecksum = checksum(code, { algorithm: "md5" }) - - const cachedManifest = path.join(Vars.cache_path, `${manifestChecksum}.rmanifest`) - - await fs.promises.writeFile(cachedManifest, code) - - return { - remote_manifest: manifest, - local_manifest: cachedManifest, - is_catched: true, - code: code, - } - } else { - if (!fs.existsSync(target)) { - throw new Error(`Manifest not found: ${target}`) - } - - if (!fs.statSync(target).isFile()) { - throw new Error(`Manifest is not a file: ${target}`) - } - - return { - remote_manifest: undefined, - local_manifest: target, - is_catched: false, - code: fs.readFileSync(target, "utf8"), - } - } -} - -export default readManifest \ No newline at end of file diff --git a/packages/core/src/manifest/vm.js b/packages/core/src/manifest/vm.js deleted file mode 100644 index 65bcbe3..0000000 --- a/packages/core/src/manifest/vm.js +++ /dev/null @@ -1,83 +0,0 @@ -import Logger from "../logger" - -import os from "node:os" -import vm from "node:vm" -import path from "node:path" -import ManifestConfigManager from "../classes/ManifestConfig" - -import resolveOs from "../utils/resolveOs" -import FetchLibraries from "./libraries" - -import Vars from "../vars" - -async function BuildManifest(baseClass, context, { soft = false } = {}) { - // inject install_path - context.install_path = path.resolve(Vars.packages_path, baseClass.id) - baseClass.install_path = context.install_path - - if (soft === true) { - return baseClass - } - - const configManager = new ManifestConfigManager(baseClass.id) - - await configManager.initialize() - - let dependencies = [] - - if (Array.isArray(baseClass.useLib)) { - dependencies = [ - ...dependencies, - ...baseClass.useLib - ] - } - - // modify context - context.Log = Logger.child({ service: `VM|${baseClass.id}` }) - context.Lib = await FetchLibraries(dependencies, { - manifest: baseClass, - install_path: context.install_path, - }) - context.Config = configManager - - // Construct the instance - const instance = new baseClass() - - instance.install_path = context.install_path - - return instance -} - -function injectUseManifest(code) { - return code + "\n\nuse(Manifest);" -} - -export default async (code, { soft = false } = {}) => { - return await new Promise(async (resolve, reject) => { - try { - code = injectUseManifest(code) - - const context = { - Vars: Vars, - Log: Logger.child({ service: "MANIFEST_VM" }), - use: (baseClass) => { - return BuildManifest( - baseClass, - context, - { - soft: soft, - } - ).then(resolve) - }, - os_string: resolveOs(), - arch: os.arch(), - } - - vm.createContext(context) - - await vm.runInContext(code, context) - } catch (error) { - reject(error) - } - }) -} \ No newline at end of file diff --git a/packages/core/src/prerequisites.js b/packages/core/src/prerequisites.js deleted file mode 100644 index 9fb7df7..0000000 --- a/packages/core/src/prerequisites.js +++ /dev/null @@ -1,70 +0,0 @@ -import resolveRemoteBinPath from "./utils/resolveRemoteBinPath" -import Vars from "./vars" -import path from "node:path" -import axios from "axios" - -const baseURL = "https://storage.ragestudio.net/rstudio/binaries" - -export default [ - { - id: "7z-bin", - finalBin: Vars.sevenzip_bin, - url: resolveRemoteBinPath(`${baseURL}/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za"), - destination: Vars.sevenzip_bin, - rewriteExecutionPermission: true, - }, - { - id: "git-bin", - finalBin: Vars.git_bin, - url: resolveRemoteBinPath(`${baseURL}/git`, "git-bundle-2.4.0.zip"), - destination: path.resolve(Vars.binaries_path, "git-bundle.zip"), - extract: path.resolve(Vars.binaries_path, "git-bin"), - requireOs: ["win32"], - rewriteExecutionPermission: true, - deleteBeforeExtract: true, - }, - { - id: "rclone-bin", - finalBin: Vars.rclone_bin, - url: resolveRemoteBinPath(`${baseURL}/rclone`, "rclone-bin.zip"), - destination: path.resolve(Vars.binaries_path, "rclone-bin.zip"), - extract: path.resolve(Vars.binaries_path, "rclone-bin"), - requireOs: ["win32"], - rewriteExecutionPermission: true, - deleteBeforeExtract: true, - }, - { - id: "java_jre_bin", - finalBin: Vars.java_jre_bin, - url: async (os, arch) => { - const { data } = await axios({ - method: "GET", - url: "https://api.azul.com/metadata/v1/zulu/packages", - params: { - arch: arch, - java_version: "JAVA_22", - os: os, - archive_type: "zip", - javafx_bundled: "false", - java_package_type: "jre", - page_size: "1", - } - }) - - return data[0].download_url - }, - destination: path.resolve(Vars.binaries_path, "java-jre.zip"), - extract: path.resolve(Vars.binaries_path, "java_jre_bin"), - extractTargetFromName: true, - moveDirs: [ - { - requireOs: ["macos"], - from: path.resolve(Vars.binaries_path, "java_jre_bin", "zulu-22.jre", "Contents"), - to: path.resolve(Vars.binaries_path, "java_jre_bin", "Contents"), - deleteParentBefore: true - } - ], - rewriteExecutionPermission: path.resolve(Vars.binaries_path, "java_jre_bin"), - deleteBeforeExtract: true, - }, -] \ No newline at end of file diff --git a/packages/core/src/utils/chmodRecursive.js b/packages/core/src/utils/chmodRecursive.js deleted file mode 100644 index 8a1d7a1..0000000 --- a/packages/core/src/utils/chmodRecursive.js +++ /dev/null @@ -1,16 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -async function chmodRecursive(target, mode) { - if (fs.lstatSync(target).isDirectory()) { - const files = await fs.promises.readdir(target, { withFileTypes: true }) - - for (const file of files) { - await chmodRecursive(path.join(target, file.name), mode) - } - } else { - await fs.promises.chmod(target, mode) - } -} - -export default chmodRecursive diff --git a/packages/core/src/utils/extractFile.js b/packages/core/src/utils/extractFile.js deleted file mode 100644 index 19040a7..0000000 --- a/packages/core/src/utils/extractFile.js +++ /dev/null @@ -1,48 +0,0 @@ -import Logger from "../logger" - -import fs from "node:fs" -import path from "node:path" -import { pipeline as streamPipeline } from "node:stream/promises" - -import { extractFull } from "node-7z" -import unzipper from "unzipper" - -import Vars from "../vars" - -const Log = Logger.child({ service: "EXTRACTOR" }) - -export async function extractFile(file, dest) { - const ext = path.extname(file) - - Log.info(`Extracting ${file} to ${dest}`) - - switch (ext) { - case ".zip": { - await streamPipeline( - fs.createReadStream(file), - unzipper.Extract({ - path: dest, - }) - ) - break - } - case ".7z": { - await extractFull(file, dest, { - $bin: Vars.sevenzip_bin, - }) - break - } - case ".gz": { - await extractFull(file, dest, { - $bin: Vars.sevenzip_bin - }) - break - } - default: - throw new Error(`Unsupported file extension: ${ext}`) - } - - return dest -} - -export default extractFile \ No newline at end of file diff --git a/packages/core/src/utils/parseStringVars.js b/packages/core/src/utils/parseStringVars.js deleted file mode 100644 index 9042d92..0000000 --- a/packages/core/src/utils/parseStringVars.js +++ /dev/null @@ -1,21 +0,0 @@ -export default function parseStringVars(str, pkg) { - if (!pkg) { - return str - } - - const vars = { - id: pkg.id, - name: pkg.name, - version: pkg.version, - install_path: pkg.install_path, - remote: pkg.remote, - } - - const regex = /%([^%]+)%/g - - str = str.replace(regex, (match, varName) => { - return vars[varName] - }) - - return str -} \ No newline at end of file diff --git a/packages/core/src/utils/readDirRecurse.js b/packages/core/src/utils/readDirRecurse.js deleted file mode 100644 index 342dda0..0000000 --- a/packages/core/src/utils/readDirRecurse.js +++ /dev/null @@ -1,25 +0,0 @@ -import fs from "node:fs" -import path from "node:path" - -async function readDirRecurse(dir, maxDepth = 3, current = 0) { - if (current > maxDepth) { - return [] - } - - const files = await fs.promises.readdir(dir) - - const promises = files.map(async (file) => { - const filePath = path.join(dir, file) - const stat = await fs.promises.stat(filePath) - - if (stat.isDirectory()) { - return readDirRecurse(filePath, maxDepth, current + 1) - } - - return filePath - }) - - return (await Promise.all(promises)).flat() -} - -export default readDirRecurse \ No newline at end of file diff --git a/packages/core/src/utils/resolveOs.js b/packages/core/src/utils/resolveOs.js deleted file mode 100644 index 1bf58de..0000000 --- a/packages/core/src/utils/resolveOs.js +++ /dev/null @@ -1,17 +0,0 @@ -import os from "node:os" - -export default () => { - if (os.platform() === "win32") { - return "windows" - } - - if (os.platform() === "darwin") { - return "macos" - } - - if (os.platform() === "linux") { - return "linux" - } - - return os.platform() -} \ No newline at end of file diff --git a/packages/core/src/utils/resolveRemoteBinPath.js b/packages/core/src/utils/resolveRemoteBinPath.js deleted file mode 100644 index acc8926..0000000 --- a/packages/core/src/utils/resolveRemoteBinPath.js +++ /dev/null @@ -1,15 +0,0 @@ -export default (pre, post) => { - let url = null - - if (process.platform === "darwin") { - url = `${pre}/mac/${process.arch}/${post}` - } - else if (process.platform === "win32") { - url = `${pre}/win/${process.arch}/${post}` - } - else { - url = `${pre}/linux/${process.arch}/${post}` - } - - return url -} \ No newline at end of file diff --git a/packages/core/src/vars.js b/packages/core/src/vars.js deleted file mode 100644 index 3fc23fc..0000000 --- a/packages/core/src/vars.js +++ /dev/null @@ -1,35 +0,0 @@ -import path from "node:path" -import upath from "upath" - -const isWin = process.platform.includes("win") -const isMac = process.platform.includes("darwin") - -const runtimeName = "rs-relic" - -const userdata_path = upath.normalizeSafe(path.resolve( - process.env.APPDATA || - (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"), -)) -const runtime_path = upath.normalizeSafe(path.join(userdata_path, runtimeName)) -const cache_path = upath.normalizeSafe(path.join(runtime_path, "cache")) -const packages_path = upath.normalizeSafe(path.join(runtime_path, "packages")) -const binaries_path = upath.normalizeSafe(path.resolve(runtime_path, "binaries")) -const db_path = upath.normalizeSafe(path.resolve(runtime_path, "db.json")) - -const binaries = { - sevenzip_bin: upath.normalizeSafe(path.resolve(binaries_path, "7z-bin", isWin ? "7za.exe" : "7za")), - git_bin: upath.normalizeSafe(path.resolve(binaries_path, "git-bin", "bin", isWin ? "git.exe" : "git")), - rclone_bin: upath.normalizeSafe(path.resolve(binaries_path, "rclone-bin", isWin ? "rclone.exe" : "rclone")), - java_jre_bin: upath.normalizeSafe(path.resolve(binaries_path, "java_jre_bin", (isMac ? "Contents/Home/bin/java" : (isWin ? "bin/java.exe" : "bin/java")))), -} - -export default { - runtimeName, - db_path, - userdata_path, - runtime_path, - cache_path, - packages_path, - binaries_path, - ...binaries, -} \ No newline at end of file diff --git a/packages/gui/.gitignore b/packages/gui/.gitignore deleted file mode 100644 index 672ad48..0000000 --- a/packages/gui/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -# Secrets -/**/**/.env -/**/**/origin.server -/**/**/server.manifest -/**/**/server.registry -/**/**/*.secret.* - -/**/**/_shared - -# Trash -/**/**/*.log -/**/**/dumps.log -/**/**/.crash.log -/**/**/.tmp -/**/**/.cache -/**/**/cache -/**/**/out -/**/**/.out -/**/**/dist -/**/**/node_modules -/**/**/corenode_modules -/**/**/.DS_Store -/**/**/package-lock.json -/**/**/yarn.lock -/**/**/.evite -/**/**/build -/**/**/uploads -/**/**/d_data -/**/**/*.tar -/**/**/*.7z -/**/**/*.zip -/**/**/*.env - -# Logs -/**/**/npm-debug.log* -/**/**/yarn-error.log -/**/**/dumps.log -/**/**/corenode.log - -# Temporal configurations -/**/**/.aliaser diff --git a/packages/gui/package.json b/packages/gui/package.json deleted file mode 100644 index 30cef0a..0000000 --- a/packages/gui/package.json +++ /dev/null @@ -1,54 +0,0 @@ -{ - "name": "@ragestudio/relic-gui", - "version": "0.17.0", - "description": "RageStudio Relic, yet another package manager.", - "main": "./out/main/index.js", - "author": "RageStudio", - "license": "MIT", - "scripts": { - "start": "electron-vite preview", - "dev": "electron-vite dev", - "build": "electron-vite build", - "postinstall": "electron-builder install-app-deps", - "pack:win": "electron-builder --win --config", - "pack:mac": "electron-builder --mac --config", - "pack:linux": "electron-builder --linux --config", - "build:win": "npm run build && npm run pack:win", - "build:mac": "npm run build && npm run pack:mac", - "build:linux": "npm run build && npm run pack:linux" - }, - "dependencies": { - "@electron-toolkit/preload": "^2.0.0", - "@electron-toolkit/utils": "^2.0.0", - "@getstation/electron-google-oauth2": "^14.0.0", - "@imjs/electron-differential-updater": "^5.1.7", - "@loadable/component": "^5.16.3", - "antd": "^5.13.2", - "classnames": "^2.3.2", - "electron-build": "^0.0.3", - "electron-differential-updater": "^4.3.2", - "electron-is-dev": "^2.0.0", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.1", - "got": "11.8.3", - "human-format": "^1.2.0", - "less": "^4.2.0", - "lodash": "^4.17.21", - "react-icons": "^4.11.0", - "react-motion": "0.5.2", - "react-router-dom": "6.6.2", - "react-spinners": "^0.13.8", - "react-spring": "^9.7.3" - }, - "devDependencies": { - "@ragestudio/hermes": "^0.1.1", - "protocol-registry": "^1.4.1", - "@vitejs/plugin-react": "^4.0.4", - "electron": "29.1.6", - "electron-builder": "24.6.3", - "electron-vite": "^2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "vite": "^4.4.9" - } -} diff --git a/packages/gui/resources/icon.ico b/resources/icon.ico similarity index 100% rename from packages/gui/resources/icon.ico rename to resources/icon.ico diff --git a/packages/gui/resources/icon.png b/resources/icon.png similarity index 100% rename from packages/gui/resources/icon.png rename to resources/icon.png diff --git a/packages/gui/resources/icon.svg b/resources/icon.svg similarity index 100% rename from packages/gui/resources/icon.svg rename to resources/icon.svg diff --git a/scripts/postinstall.js b/scripts/postinstall.js deleted file mode 100644 index f29056b..0000000 --- a/scripts/postinstall.js +++ /dev/null @@ -1,35 +0,0 @@ -const path = require("path") -const child_process = require("child_process") - -const packagesPath = path.resolve(__dirname, "..", "packages") - -const linkRoot = path.resolve(packagesPath, "core") - -const linkPackages = [ - path.resolve(packagesPath, "cli"), - path.resolve(packagesPath, "gui"), -] - -async function main() { - console.log(`Linking @core to other packages...`) - - const rootPkg = require(path.resolve(linkRoot, "package.json")) - - await child_process.execSync("yarn link", { - cwd: linkRoot, - stdio: "inherit", - stdout: "inherit", - }) - - for (const linkPackage of linkPackages) { - await child_process.execSync(`yarn link "${rootPkg.name}"`, { - cwd: linkPackage, - stdio: "inherit", - stdout: "inherit", - }) - } - - console.log(`Done!`) -} - -main() \ No newline at end of file diff --git a/packages/gui/src/main/classes/CoreAdapter.js b/src/main/classes/CoreAdapter.js similarity index 100% rename from packages/gui/src/main/classes/CoreAdapter.js rename to src/main/classes/CoreAdapter.js diff --git a/packages/gui/src/main/index.js b/src/main/index.js similarity index 99% rename from packages/gui/src/main/index.js rename to src/main/index.js index 5184c4a..7a5a017 100644 --- a/packages/gui/src/main/index.js +++ b/src/main/index.js @@ -3,7 +3,7 @@ global.SettingsStore = new Store({ watch: true, }) -import RelicCore from "../../../core/src" +const RelicCore = require("@ragestudio/relic-core").default import CoreAdapter from "./classes/CoreAdapter" import sendToRender from "./utils/sendToRender" diff --git a/packages/gui/src/main/utils/sendToRender.js b/src/main/utils/sendToRender.js similarity index 100% rename from packages/gui/src/main/utils/sendToRender.js rename to src/main/utils/sendToRender.js diff --git a/packages/gui/src/preload/index.js b/src/preload/index.js similarity index 100% rename from packages/gui/src/preload/index.js rename to src/preload/index.js diff --git a/packages/gui/src/renderer/assets/bruh_fox.jpg b/src/renderer/assets/bruh_fox.jpg similarity index 100% rename from packages/gui/src/renderer/assets/bruh_fox.jpg rename to src/renderer/assets/bruh_fox.jpg diff --git a/packages/gui/src/renderer/assets/icon.jsx b/src/renderer/assets/icon.jsx similarity index 100% rename from packages/gui/src/renderer/assets/icon.jsx rename to src/renderer/assets/icon.jsx diff --git a/packages/gui/src/renderer/config/paths_decorators.js b/src/renderer/config/paths_decorators.js similarity index 100% rename from packages/gui/src/renderer/config/paths_decorators.js rename to src/renderer/config/paths_decorators.js diff --git a/packages/gui/src/renderer/index.html b/src/renderer/index.html similarity index 100% rename from packages/gui/src/renderer/index.html rename to src/renderer/index.html diff --git a/packages/gui/src/renderer/src/App.jsx b/src/renderer/src/App.jsx similarity index 100% rename from packages/gui/src/renderer/src/App.jsx rename to src/renderer/src/App.jsx diff --git a/packages/gui/src/renderer/src/GlobalApp.jsx b/src/renderer/src/GlobalApp.jsx similarity index 100% rename from packages/gui/src/renderer/src/GlobalApp.jsx rename to src/renderer/src/GlobalApp.jsx diff --git a/packages/gui/src/renderer/src/components/Crash/index.jsx b/src/renderer/src/components/Crash/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/Crash/index.jsx rename to src/renderer/src/components/Crash/index.jsx diff --git a/packages/gui/src/renderer/src/components/Crash/index.less b/src/renderer/src/components/Crash/index.less similarity index 100% rename from packages/gui/src/renderer/src/components/Crash/index.less rename to src/renderer/src/components/Crash/index.less diff --git a/packages/gui/src/renderer/src/components/Icons/index.jsx b/src/renderer/src/components/Icons/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/Icons/index.jsx rename to src/renderer/src/components/Icons/index.jsx diff --git a/packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx b/src/renderer/src/components/InstallConfigAsk/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx rename to src/renderer/src/components/InstallConfigAsk/index.jsx diff --git a/packages/gui/src/renderer/src/components/InstallConfigAsk/index.less b/src/renderer/src/components/InstallConfigAsk/index.less similarity index 100% rename from packages/gui/src/renderer/src/components/InstallConfigAsk/index.less rename to src/renderer/src/components/InstallConfigAsk/index.less diff --git a/packages/gui/src/renderer/src/components/ManifestInfo/index.jsx b/src/renderer/src/components/ManifestInfo/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/ManifestInfo/index.jsx rename to src/renderer/src/components/ManifestInfo/index.jsx diff --git a/packages/gui/src/renderer/src/components/ManifestInfo/index.less b/src/renderer/src/components/ManifestInfo/index.less similarity index 100% rename from packages/gui/src/renderer/src/components/ManifestInfo/index.less rename to src/renderer/src/components/ManifestInfo/index.less diff --git a/packages/gui/src/renderer/src/components/NewInstallation/index.jsx b/src/renderer/src/components/NewInstallation/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/NewInstallation/index.jsx rename to src/renderer/src/components/NewInstallation/index.jsx diff --git a/packages/gui/src/renderer/src/components/NewInstallation/index.less b/src/renderer/src/components/NewInstallation/index.less similarity index 100% rename from packages/gui/src/renderer/src/components/NewInstallation/index.less rename to src/renderer/src/components/NewInstallation/index.less diff --git a/packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx b/src/renderer/src/components/PackageConfigItem/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx rename to src/renderer/src/components/PackageConfigItem/index.jsx diff --git a/packages/gui/src/renderer/src/components/PackageItem/index.jsx b/src/renderer/src/components/PackageItem/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/PackageItem/index.jsx rename to src/renderer/src/components/PackageItem/index.jsx diff --git a/packages/gui/src/renderer/src/components/PackageItem/index.less b/src/renderer/src/components/PackageItem/index.less similarity index 100% rename from packages/gui/src/renderer/src/components/PackageItem/index.less rename to src/renderer/src/components/PackageItem/index.less diff --git a/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx b/src/renderer/src/components/PackageUpdateAvailable/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx rename to src/renderer/src/components/PackageUpdateAvailable/index.jsx diff --git a/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less b/src/renderer/src/components/PackageUpdateAvailable/index.less similarity index 100% rename from packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less rename to src/renderer/src/components/PackageUpdateAvailable/index.less diff --git a/packages/gui/src/renderer/src/components/Splash/index.jsx b/src/renderer/src/components/Splash/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/components/Splash/index.jsx rename to src/renderer/src/components/Splash/index.jsx diff --git a/packages/gui/src/renderer/src/components/Splash/index.less b/src/renderer/src/components/Splash/index.less similarity index 100% rename from packages/gui/src/renderer/src/components/Splash/index.less rename to src/renderer/src/components/Splash/index.less diff --git a/packages/gui/src/renderer/src/contexts/global.js b/src/renderer/src/contexts/global.js similarity index 100% rename from packages/gui/src/renderer/src/contexts/global.js rename to src/renderer/src/contexts/global.js diff --git a/packages/gui/src/renderer/src/contexts/packages.jsx b/src/renderer/src/contexts/packages.jsx similarity index 100% rename from packages/gui/src/renderer/src/contexts/packages.jsx rename to src/renderer/src/contexts/packages.jsx diff --git a/packages/gui/src/renderer/src/layout/components/Drawer/index.jsx b/src/renderer/src/layout/components/Drawer/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/layout/components/Drawer/index.jsx rename to src/renderer/src/layout/components/Drawer/index.jsx diff --git a/packages/gui/src/renderer/src/layout/components/Header/index.jsx b/src/renderer/src/layout/components/Header/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/layout/components/Header/index.jsx rename to src/renderer/src/layout/components/Header/index.jsx diff --git a/packages/gui/src/renderer/src/layout/components/Header/index.less b/src/renderer/src/layout/components/Header/index.less similarity index 100% rename from packages/gui/src/renderer/src/layout/components/Header/index.less rename to src/renderer/src/layout/components/Header/index.less diff --git a/packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx b/src/renderer/src/layout/components/ModalDialog/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx rename to src/renderer/src/layout/components/ModalDialog/index.jsx diff --git a/packages/gui/src/renderer/src/layout/index.jsx b/src/renderer/src/layout/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/layout/index.jsx rename to src/renderer/src/layout/index.jsx diff --git a/packages/gui/src/renderer/src/main.jsx b/src/renderer/src/main.jsx similarity index 100% rename from packages/gui/src/renderer/src/main.jsx rename to src/renderer/src/main.jsx diff --git a/packages/gui/src/renderer/src/pages/index.jsx b/src/renderer/src/pages/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/pages/index.jsx rename to src/renderer/src/pages/index.jsx diff --git a/packages/gui/src/renderer/src/pages/index.less b/src/renderer/src/pages/index.less similarity index 100% rename from packages/gui/src/renderer/src/pages/index.less rename to src/renderer/src/pages/index.less diff --git a/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx b/src/renderer/src/pages/pkg/[pkg_id].jsx similarity index 100% rename from packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx rename to src/renderer/src/pages/pkg/[pkg_id].jsx diff --git a/packages/gui/src/renderer/src/pages/pkg/index.less b/src/renderer/src/pages/pkg/index.less similarity index 100% rename from packages/gui/src/renderer/src/pages/pkg/index.less rename to src/renderer/src/pages/pkg/index.less diff --git a/packages/gui/src/renderer/src/pages/settings/index.jsx b/src/renderer/src/pages/settings/index.jsx similarity index 100% rename from packages/gui/src/renderer/src/pages/settings/index.jsx rename to src/renderer/src/pages/settings/index.jsx diff --git a/packages/gui/src/renderer/src/pages/settings/index.less b/src/renderer/src/pages/settings/index.less similarity index 100% rename from packages/gui/src/renderer/src/pages/settings/index.less rename to src/renderer/src/pages/settings/index.less diff --git a/packages/gui/src/renderer/src/router.jsx b/src/renderer/src/router.jsx similarity index 100% rename from packages/gui/src/renderer/src/router.jsx rename to src/renderer/src/router.jsx diff --git a/packages/gui/src/renderer/src/settings_list.jsx b/src/renderer/src/settings_list.jsx similarity index 100% rename from packages/gui/src/renderer/src/settings_list.jsx rename to src/renderer/src/settings_list.jsx diff --git a/packages/gui/src/renderer/src/style/fix.less b/src/renderer/src/style/fix.less similarity index 100% rename from packages/gui/src/renderer/src/style/fix.less rename to src/renderer/src/style/fix.less diff --git a/packages/gui/src/renderer/src/style/index.less b/src/renderer/src/style/index.less similarity index 100% rename from packages/gui/src/renderer/src/style/index.less rename to src/renderer/src/style/index.less diff --git a/packages/gui/src/renderer/src/style/reset.css b/src/renderer/src/style/reset.css similarity index 100% rename from packages/gui/src/renderer/src/style/reset.css rename to src/renderer/src/style/reset.css diff --git a/packages/gui/src/renderer/src/style/vars.less b/src/renderer/src/style/vars.less similarity index 100% rename from packages/gui/src/renderer/src/style/vars.less rename to src/renderer/src/style/vars.less diff --git a/packages/gui/src/renderer/src/utils/getRootCssVar/index.js b/src/renderer/src/utils/getRootCssVar/index.js similarity index 100% rename from packages/gui/src/renderer/src/utils/getRootCssVar/index.js rename to src/renderer/src/utils/getRootCssVar/index.js diff --git a/packages/gui/src/renderer/src/utils/getVersions/index.js b/src/renderer/src/utils/getVersions/index.js similarity index 100% rename from packages/gui/src/renderer/src/utils/getVersions/index.js rename to src/renderer/src/utils/getVersions/index.js From 3e4a6e0ca995fb194e83d7103320f816518be229 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 2 Apr 2024 19:01:41 +0200 Subject: [PATCH 09/14] added release workflow --- .github/workflows/release.yml | 56 +++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1df6bec --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,56 @@ +name: Build/release Electron app + +on: + push: + tags: + - v*.*.* + +jobs: + release: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - name: Check out Git repository + uses: actions/checkout@v3 + + - name: Install Node.js + uses: actions/setup-node@v3 + with: + node-version: 16 + + - name: Install Dependencies + run: npm install + + - name: build-linux + if: matrix.os == 'ubuntu-latest' + run: npm run build:linux + + - name: build-mac + if: matrix.os == 'macos-latest' + run: npm run build:mac + + - name: build-win + if: matrix.os == 'windows-latest' + run: npm run build:win + + - name: release + uses: softprops/action-gh-release@v1 + with: + draft: true + files: | + dist/*.exe + dist/*.zip + dist/*.dmg + dist/*.AppImage + dist/*.snap + dist/*.deb + dist/*.rpm + dist/*.tar.gz + dist/*.yml + dist/*.blockmap + env: + GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} \ No newline at end of file From eef02a7bec5c8d0556bccba9cfbf0a4475c47460 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 2 Apr 2024 19:30:14 +0200 Subject: [PATCH 10/14] fix build --- electron-builder.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/electron-builder.yml b/electron-builder.yml index 5ec3133..3a18d50 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -11,10 +11,10 @@ files: asarUnpack: - resources/** win: - executableName: relic + executableName: Relic icon: resources/icon.ico nsis: - artifactName: ${name}-${version}-setup.${ext} + artifactName: ${productName}-${version}-setup.${ext} shortcutName: ${productName} uninstallDisplayName: ${productName} createDesktopShortcut: always @@ -22,7 +22,7 @@ mac: icon: resources/icon.icns notarize: false dmg: - artifactName: ${name}-${version}.${ext} + artifactName: ${productName}-${version}.${ext} linux: target: - AppImage @@ -31,7 +31,7 @@ linux: maintainer: electronjs.org category: Utility appImage: - artifactName: ${name}-${version}.${ext} + artifactName: ${productName}-${version}.${ext} npmRebuild: false publish: provider: generic From 86a6effeb1cae6291deecaecd94ede6b8dd174cf Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 2 Apr 2024 20:21:58 +0200 Subject: [PATCH 11/14] move to monorepo --- .gitmodules | 3 - package.json | 57 +- packages/cli/bin | 2 + packages/cli/package.json | 21 + packages/cli/src/index.js | 169 ++++ packages/core/.swcrc | 11 + packages/core/package.json | 43 + packages/core/src/classes/ManifestAuthDB.js | 36 + packages/core/src/classes/ManifestConfig.js | 34 + packages/core/src/classes/PatchManager.js | 149 ++++ packages/core/src/db.js | 115 +++ packages/core/src/generic_steps/git_clone.js | 49 ++ packages/core/src/generic_steps/git_pull.js | 33 + packages/core/src/generic_steps/git_reset.js | 83 ++ packages/core/src/generic_steps/http.js | 66 ++ packages/core/src/generic_steps/index.js | 48 ++ packages/core/src/handlers/apply.js | 95 +++ packages/core/src/handlers/authorize.js | 33 + packages/core/src/handlers/checkUpdate.js | 43 + packages/core/src/handlers/execute.js | 80 ++ packages/core/src/handlers/install.js | 182 ++++ packages/core/src/handlers/list.js | 5 + packages/core/src/handlers/read.js | 9 + packages/core/src/handlers/uninstall.js | 74 ++ packages/core/src/handlers/update.js | 128 +++ packages/core/src/helpers/downloadHttpFile.js | 73 ++ packages/core/src/helpers/sendToRender.js | 43 + packages/core/src/helpers/setup.js | 201 +++++ packages/core/src/index.js | 68 ++ packages/core/src/libraries/execa/index.js | 309 +++++++ .../core/src/libraries/execa/lib/command.js | 119 +++ .../core/src/libraries/execa/lib/error.js | 87 ++ packages/core/src/libraries/execa/lib/kill.js | 102 +++ packages/core/src/libraries/execa/lib/pipe.js | 42 + .../core/src/libraries/execa/lib/promise.js | 36 + .../core/src/libraries/execa/lib/stdio.js | 49 ++ .../core/src/libraries/execa/lib/stream.js | 133 +++ .../core/src/libraries/execa/lib/verbose.js | 19 + .../src/libraries/get-stream/array-buffer.js | 84 ++ .../core/src/libraries/get-stream/array.js | 32 + .../core/src/libraries/get-stream/buffer.js | 20 + .../core/src/libraries/get-stream/contents.js | 101 +++ .../core/src/libraries/get-stream/index.js | 5 + .../core/src/libraries/get-stream/string.js | 36 + .../core/src/libraries/get-stream/utils.js | 11 + .../core/src/libraries/human-signals/core.js | 275 ++++++ .../core/src/libraries/human-signals/index.js | 70 ++ .../src/libraries/human-signals/realtime.js | 16 + .../src/libraries/human-signals/signals.js | 34 + .../core/src/libraries/is-stream/index.js | 29 + .../src/libraries/lowdb/adapters/Memory.js | 24 + .../libraries/lowdb/adapters/node/DataFile.js | 51 ++ .../libraries/lowdb/adapters/node/JSONFile.js | 19 + .../libraries/lowdb/adapters/node/TextFile.js | 65 ++ packages/core/src/libraries/lowdb/core/Low.js | 48 ++ .../core/src/libraries/lowdb/presets/node.js | 23 + .../core/src/libraries/lowdb/steno/index.js | 98 +++ .../src/libraries/mimic-function/index.js | 71 ++ .../core/src/libraries/npm-run-path/index.js | 51 ++ packages/core/src/libraries/onetime/index.js | 41 + .../libraries/strip-final-newline/index.js | 26 + packages/core/src/logger.js | 40 + packages/core/src/manifest/libraries.js | 23 + packages/core/src/manifest/libs/auth/index.js | 54 ++ packages/core/src/manifest/libs/fs/index.js | 39 + packages/core/src/manifest/libs/index.js | 15 + .../src/manifest/libs/mcl/authenticator.js | 167 ++++ .../core/src/manifest/libs/mcl/handler.js | 783 ++++++++++++++++++ packages/core/src/manifest/libs/mcl/index.js | 49 ++ .../core/src/manifest/libs/mcl/launcher.js | 224 +++++ packages/core/src/manifest/libs/open/index.js | 15 + packages/core/src/manifest/libs/path/index.js | 3 + packages/core/src/manifest/reader.js | 51 ++ packages/core/src/manifest/vm.js | 83 ++ packages/core/src/prerequisites.js | 70 ++ packages/core/src/utils/chmodRecursive.js | 16 + packages/core/src/utils/extractFile.js | 48 ++ packages/core/src/utils/parseStringVars.js | 21 + packages/core/src/utils/readDirRecurse.js | 25 + packages/core/src/utils/resolveOs.js | 17 + .../core/src/utils/resolveRemoteBinPath.js | 15 + packages/core/src/vars.js | 35 + .../gui/electron-builder.yml | 0 .../gui/electron.vite.config.js | 0 packages/gui/package.json | 55 ++ .../gui/resources}/icon.ico | Bin .../gui/resources}/icon.png | Bin .../gui/resources}/icon.svg | 0 .../gui/src}/main/classes/CoreAdapter.js | 0 {src => packages/gui/src}/main/index.js | 16 +- .../gui/src}/main/utils/sendToRender.js | 0 {src => packages/gui/src}/preload/index.js | 0 .../gui/src}/renderer/assets/bruh_fox.jpg | Bin .../gui/src}/renderer/assets/icon.jsx | 0 .../src}/renderer/config/paths_decorators.js | 0 {src => packages/gui/src}/renderer/index.html | 0 .../gui/src}/renderer/src/App.jsx | 0 .../gui/src}/renderer/src/GlobalApp.jsx | 0 .../renderer/src/components/Crash/index.jsx | 0 .../renderer/src/components/Crash/index.less | 0 .../renderer/src/components/Icons/index.jsx | 0 .../src/components/InstallConfigAsk/index.jsx | 0 .../components/InstallConfigAsk/index.less | 0 .../src/components/ManifestInfo/index.jsx | 0 .../src/components/ManifestInfo/index.less | 0 .../src/components/NewInstallation/index.jsx | 0 .../src/components/NewInstallation/index.less | 0 .../components/PackageConfigItem/index.jsx | 0 .../src/components/PackageItem/index.jsx | 0 .../src/components/PackageItem/index.less | 0 .../PackageUpdateAvailable/index.jsx | 0 .../PackageUpdateAvailable/index.less | 0 .../renderer/src/components/Splash/index.jsx | 0 .../renderer/src/components/Splash/index.less | 0 .../gui/src}/renderer/src/contexts/global.js | 0 .../src}/renderer/src/contexts/packages.jsx | 0 .../src/layout/components/Drawer/index.jsx | 0 .../src/layout/components/Header/index.jsx | 0 .../src/layout/components/Header/index.less | 0 .../layout/components/ModalDialog/index.jsx | 0 .../gui/src}/renderer/src/layout/index.jsx | 0 .../gui/src}/renderer/src/main.jsx | 0 .../gui/src}/renderer/src/pages/index.jsx | 0 .../gui/src}/renderer/src/pages/index.less | 0 .../src}/renderer/src/pages/pkg/[pkg_id].jsx | 0 .../src}/renderer/src/pages/pkg/index.less | 0 .../renderer/src/pages/settings/index.jsx | 0 .../renderer/src/pages/settings/index.less | 0 .../gui/src}/renderer/src/router.jsx | 0 .../gui/src}/renderer/src/settings_list.jsx | 0 .../gui/src}/renderer/src/style/fix.less | 0 .../gui/src}/renderer/src/style/index.less | 0 .../gui/src}/renderer/src/style/reset.css | 0 .../gui/src}/renderer/src/style/vars.less | 0 .../renderer/src/utils/getRootCssVar/index.js | 0 .../renderer/src/utils/getVersions/index.js | 0 relic-core | 1 - scripts/postinstall.js | 35 + 138 files changed, 5920 insertions(+), 59 deletions(-) delete mode 100644 .gitmodules create mode 100644 packages/cli/bin create mode 100644 packages/cli/package.json create mode 100644 packages/cli/src/index.js create mode 100644 packages/core/.swcrc create mode 100644 packages/core/package.json create mode 100644 packages/core/src/classes/ManifestAuthDB.js create mode 100644 packages/core/src/classes/ManifestConfig.js create mode 100644 packages/core/src/classes/PatchManager.js create mode 100644 packages/core/src/db.js create mode 100644 packages/core/src/generic_steps/git_clone.js create mode 100644 packages/core/src/generic_steps/git_pull.js create mode 100644 packages/core/src/generic_steps/git_reset.js create mode 100644 packages/core/src/generic_steps/http.js create mode 100644 packages/core/src/generic_steps/index.js create mode 100644 packages/core/src/handlers/apply.js create mode 100644 packages/core/src/handlers/authorize.js create mode 100644 packages/core/src/handlers/checkUpdate.js create mode 100644 packages/core/src/handlers/execute.js create mode 100644 packages/core/src/handlers/install.js create mode 100644 packages/core/src/handlers/list.js create mode 100644 packages/core/src/handlers/read.js create mode 100644 packages/core/src/handlers/uninstall.js create mode 100644 packages/core/src/handlers/update.js create mode 100644 packages/core/src/helpers/downloadHttpFile.js create mode 100644 packages/core/src/helpers/sendToRender.js create mode 100644 packages/core/src/helpers/setup.js create mode 100644 packages/core/src/index.js create mode 100644 packages/core/src/libraries/execa/index.js create mode 100644 packages/core/src/libraries/execa/lib/command.js create mode 100644 packages/core/src/libraries/execa/lib/error.js create mode 100644 packages/core/src/libraries/execa/lib/kill.js create mode 100644 packages/core/src/libraries/execa/lib/pipe.js create mode 100644 packages/core/src/libraries/execa/lib/promise.js create mode 100644 packages/core/src/libraries/execa/lib/stdio.js create mode 100644 packages/core/src/libraries/execa/lib/stream.js create mode 100644 packages/core/src/libraries/execa/lib/verbose.js create mode 100644 packages/core/src/libraries/get-stream/array-buffer.js create mode 100644 packages/core/src/libraries/get-stream/array.js create mode 100644 packages/core/src/libraries/get-stream/buffer.js create mode 100644 packages/core/src/libraries/get-stream/contents.js create mode 100644 packages/core/src/libraries/get-stream/index.js create mode 100644 packages/core/src/libraries/get-stream/string.js create mode 100644 packages/core/src/libraries/get-stream/utils.js create mode 100644 packages/core/src/libraries/human-signals/core.js create mode 100644 packages/core/src/libraries/human-signals/index.js create mode 100644 packages/core/src/libraries/human-signals/realtime.js create mode 100644 packages/core/src/libraries/human-signals/signals.js create mode 100644 packages/core/src/libraries/is-stream/index.js create mode 100644 packages/core/src/libraries/lowdb/adapters/Memory.js create mode 100644 packages/core/src/libraries/lowdb/adapters/node/DataFile.js create mode 100644 packages/core/src/libraries/lowdb/adapters/node/JSONFile.js create mode 100644 packages/core/src/libraries/lowdb/adapters/node/TextFile.js create mode 100644 packages/core/src/libraries/lowdb/core/Low.js create mode 100644 packages/core/src/libraries/lowdb/presets/node.js create mode 100644 packages/core/src/libraries/lowdb/steno/index.js create mode 100644 packages/core/src/libraries/mimic-function/index.js create mode 100644 packages/core/src/libraries/npm-run-path/index.js create mode 100644 packages/core/src/libraries/onetime/index.js create mode 100644 packages/core/src/libraries/strip-final-newline/index.js create mode 100644 packages/core/src/logger.js create mode 100644 packages/core/src/manifest/libraries.js create mode 100644 packages/core/src/manifest/libs/auth/index.js create mode 100644 packages/core/src/manifest/libs/fs/index.js create mode 100644 packages/core/src/manifest/libs/index.js create mode 100644 packages/core/src/manifest/libs/mcl/authenticator.js create mode 100644 packages/core/src/manifest/libs/mcl/handler.js create mode 100644 packages/core/src/manifest/libs/mcl/index.js create mode 100644 packages/core/src/manifest/libs/mcl/launcher.js create mode 100644 packages/core/src/manifest/libs/open/index.js create mode 100644 packages/core/src/manifest/libs/path/index.js create mode 100644 packages/core/src/manifest/reader.js create mode 100644 packages/core/src/manifest/vm.js create mode 100644 packages/core/src/prerequisites.js create mode 100644 packages/core/src/utils/chmodRecursive.js create mode 100644 packages/core/src/utils/extractFile.js create mode 100644 packages/core/src/utils/parseStringVars.js create mode 100644 packages/core/src/utils/readDirRecurse.js create mode 100644 packages/core/src/utils/resolveOs.js create mode 100644 packages/core/src/utils/resolveRemoteBinPath.js create mode 100644 packages/core/src/vars.js rename electron-builder.yml => packages/gui/electron-builder.yml (100%) rename electron.vite.config.js => packages/gui/electron.vite.config.js (100%) create mode 100644 packages/gui/package.json rename {resources => packages/gui/resources}/icon.ico (100%) rename {resources => packages/gui/resources}/icon.png (100%) rename {resources => packages/gui/resources}/icon.svg (100%) rename {src => packages/gui/src}/main/classes/CoreAdapter.js (100%) rename {src => packages/gui/src}/main/index.js (98%) rename {src => packages/gui/src}/main/utils/sendToRender.js (100%) rename {src => packages/gui/src}/preload/index.js (100%) rename {src => packages/gui/src}/renderer/assets/bruh_fox.jpg (100%) rename {src => packages/gui/src}/renderer/assets/icon.jsx (100%) rename {src => packages/gui/src}/renderer/config/paths_decorators.js (100%) rename {src => packages/gui/src}/renderer/index.html (100%) rename {src => packages/gui/src}/renderer/src/App.jsx (100%) rename {src => packages/gui/src}/renderer/src/GlobalApp.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/Crash/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/Crash/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/Icons/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/InstallConfigAsk/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/InstallConfigAsk/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/ManifestInfo/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/ManifestInfo/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/NewInstallation/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/NewInstallation/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/PackageConfigItem/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/PackageItem/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/PackageItem/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/PackageUpdateAvailable/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/PackageUpdateAvailable/index.less (100%) rename {src => packages/gui/src}/renderer/src/components/Splash/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/components/Splash/index.less (100%) rename {src => packages/gui/src}/renderer/src/contexts/global.js (100%) rename {src => packages/gui/src}/renderer/src/contexts/packages.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/components/Drawer/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/components/Header/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/components/Header/index.less (100%) rename {src => packages/gui/src}/renderer/src/layout/components/ModalDialog/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/layout/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/main.jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/index.less (100%) rename {src => packages/gui/src}/renderer/src/pages/pkg/[pkg_id].jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/pkg/index.less (100%) rename {src => packages/gui/src}/renderer/src/pages/settings/index.jsx (100%) rename {src => packages/gui/src}/renderer/src/pages/settings/index.less (100%) rename {src => packages/gui/src}/renderer/src/router.jsx (100%) rename {src => packages/gui/src}/renderer/src/settings_list.jsx (100%) rename {src => packages/gui/src}/renderer/src/style/fix.less (100%) rename {src => packages/gui/src}/renderer/src/style/index.less (100%) rename {src => packages/gui/src}/renderer/src/style/reset.css (100%) rename {src => packages/gui/src}/renderer/src/style/vars.less (100%) rename {src => packages/gui/src}/renderer/src/utils/getRootCssVar/index.js (100%) rename {src => packages/gui/src}/renderer/src/utils/getVersions/index.js (100%) delete mode 160000 relic-core create mode 100644 scripts/postinstall.js diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 1095958..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "relic-core"] - path = relic-core - url = https://github.com/ragestudio/relic-core diff --git a/package.json b/package.json index 55f9050..4d468ff 100644 --- a/package.json +++ b/package.json @@ -1,54 +1,13 @@ { - "name": "@ragestudio/relic-gui", - "version": "0.17.0", - "description": "RageStudio Relic, yet another package manager.", - "main": "./out/main/index.js", - "author": "RageStudio", + "name": "@ragestudio/relic-core", + "private": true, + "workspaces": [ + "packages/*" + ], + "repository": "https://github.com/srgooglo/rs_bundler", + "author": "SrGooglo ", "license": "MIT", "scripts": { - "start": "electron-vite preview", - "dev": "electron-vite dev", - "build": "electron-vite build", - "postinstall": "electron-builder install-app-deps", - "pack:win": "electron-builder --win --config", - "pack:mac": "electron-builder --mac --config", - "pack:linux": "electron-builder --linux --config", - "build:win": "npm run build && npm run pack:win", - "build:mac": "npm run build && npm run pack:mac", - "build:linux": "npm run build && npm run pack:linux" - }, - "dependencies": { - "@electron-toolkit/preload": "^2.0.0", - "@electron-toolkit/utils": "^2.0.0", - "@getstation/electron-google-oauth2": "^14.0.0", - "@imjs/electron-differential-updater": "^5.1.7", - "@loadable/component": "^5.16.3", - "@ragestudio/relic-core": "^0.17.0", - "antd": "^5.13.2", - "classnames": "^2.3.2", - "electron-differential-updater": "^4.3.2", - "electron-is-dev": "^2.0.0", - "electron-store": "^8.1.0", - "electron-updater": "^6.1.1", - "got": "11.8.3", - "human-format": "^1.2.0", - "protocol-registry": "^1.4.1", - "less": "^4.2.0", - "lodash": "^4.17.21", - "react-icons": "^4.11.0", - "react-motion": "0.5.2", - "react-router-dom": "6.6.2", - "react-spinners": "^0.13.8", - "react-spring": "^9.7.3" - }, - "devDependencies": { - "@ragestudio/hermes": "^0.1.1", - "@vitejs/plugin-react": "^4.0.4", - "electron": "25.6.0", - "electron-builder": "24.6.3", - "electron-vite": "^2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "vite": "^4.4.9" + "postinstall": "node scripts/postinstall.js" } } diff --git a/packages/cli/bin b/packages/cli/bin new file mode 100644 index 0000000..e1b3d5a --- /dev/null +++ b/packages/cli/bin @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require("./dist/index.js") \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..0103959 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,21 @@ +{ + "name": "@ragestudio/relic-cli", + "version": "0.17.0", + "license": "MIT", + "author": "RageStudio", + "description": "RageStudio Relic, yet another package manager.", + "main": "./dist/index.js", + "bin": { + "relic": "./bin.js" + }, + "scripts": { + "dev": "hermes-node ./src/index.js", + "build": "hermes build" + }, + "dependencies": { + "commander": "^12.0.0" + }, + "devDependencies": { + "@ragestudio/hermes": "^0.1.1" + } +} diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js new file mode 100644 index 0000000..b507daf --- /dev/null +++ b/packages/cli/src/index.js @@ -0,0 +1,169 @@ +import RelicCore from "@ragestudio/relic-core" +import { program, Command, Argument } from "commander" + +import pkg from "../package.json" + +const commands = [ + { + cmd: "install", + description: "Install a package manifest from a path or URL", + arguments: [ + { + name: "package_manifest", + description: "Path or URL to a package manifest", + } + ], + fn: async (package_manifest, options) => { + await core.initialize() + await core.setup() + + return await core.package.install(package_manifest, options) + } + }, + { + cmd: "run", + description: "Execute a package", + arguments: [ + { + name: "id", + description: "The id of the package to execute", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + await core.setup() + + return await core.package.execute(pkg_id, options) + } + }, + { + cmd: "update", + description: "Update a package", + arguments: [ + { + name: "id", + description: "The id of the package to update", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + await core.setup() + + return await core.package.update(pkg_id, options) + } + }, + { + cmd: "uninstall", + description: "Uninstall a package", + arguments: [ + { + name: "id", + description: "The id of the package to uninstall", + } + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.uninstall(pkg_id, options) + } + }, + { + cmd: "apply", + description: "Apply changes to a installed package", + arguments: [ + { + name: "id", + description: "The id of the package to apply changes to", + }, + ], + options: [ + { + name: "add_patches", + description: "Add patches to the package", + }, + { + name: "remove_patches", + description: "Remove patches from the package", + }, + ], + fn: async (pkg_id, options) => { + await core.initialize() + + return await core.package.apply(pkg_id, options) + } + }, + { + cmd: "list", + description: "List installed package manifests", + fn: async () => { + await core.initialize() + + return console.log(await core.package.list()) + } + }, + { + cmd: "open-path", + description: "Open the base path or a package path", + options: [ + { + name: "pkg_id", + description: "Path to open", + } + ], + fn: async (options) => { + await core.initialize() + + await core.openPath(options.pkg_id) + } + } +] + +async function main() { + global.core = new RelicCore() + + program + .name(pkg.name) + .description(pkg.description) + .version(pkg.version) + + for await (const command of commands) { + const cmd = new Command(command.cmd).action(command.fn) + + if (command.description) { + cmd.description(command.description) + } + + if (Array.isArray(command.arguments)) { + for await (const argument of command.arguments) { + if (typeof argument === "string") { + cmd.addArgument(new Argument(argument)) + } else { + const arg = new Argument(argument.name, argument.description) + + if (argument.default) { + arg.default(argument.default) + } + + cmd.addArgument(arg) + } + } + } + + if (Array.isArray(command.options)) { + for await (const option of command.options) { + if (typeof option === "string") { + cmd.option(option) + } else { + cmd.option(option.name, option.description, option.default) + } + } + } + + program.addCommand(cmd) + } + + program.parse() +} + + +main() \ No newline at end of file diff --git a/packages/core/.swcrc b/packages/core/.swcrc new file mode 100644 index 0000000..04c57a0 --- /dev/null +++ b/packages/core/.swcrc @@ -0,0 +1,11 @@ +{ + "$schema": "http://json.schemastore.org/swcrc", + "module": { + "type": "commonjs", + // These are defaults. + "strict": false, + "strictMode": true, + "lazy": false, + "noInterop": false + } +} \ No newline at end of file diff --git a/packages/core/package.json b/packages/core/package.json new file mode 100644 index 0000000..e3258af --- /dev/null +++ b/packages/core/package.json @@ -0,0 +1,43 @@ +{ + "name": "@ragestudio/relic-core", + "version": "0.17.0", + "license": "MIT", + "author": "RageStudio", + "description": "RageStudio Relic, yet another package manager.", + "main": "./dist/index.js", + "files": [ + "dist", + "src" + ], + "scripts": { + "build": "hermes build", + "build:swc": "npx swc ./src --out-dir ./dist --strip-leading-paths" + }, + "dependencies": { + "@foxify/events": "^2.1.0", + "adm-zip": "^0.5.12", + "axios": "^1.6.8", + "checksum": "^1.0.0", + "cli-color": "^2.0.4", + "cli-progress": "^3.12.0", + "deep-object-diff": "^1.1.9", + "extends-classes": "^1.0.5", + "googleapis": "^134.0.0", + "human-format": "^1.2.0", + "merge-stream": "^2.0.0", + "module-alias": "^2.2.3", + "node-7z": "^3.0.0", + "open": "8.4.2", + "request": "^2.88.2", + "rimraf": "^5.0.5", + "signal-exit": "^4.1.0", + "unzipper": "^0.10.14", + "upath": "^2.0.1", + "uuid": "^9.0.1", + "winston": "^3.13.0" + }, + "devDependencies": { + "@swc/cli": "^0.3.12", + "@swc/core": "^1.4.11" + } +} diff --git a/packages/core/src/classes/ManifestAuthDB.js b/packages/core/src/classes/ManifestAuthDB.js new file mode 100644 index 0000000..fe32684 --- /dev/null +++ b/packages/core/src/classes/ManifestAuthDB.js @@ -0,0 +1,36 @@ +import path from "path" +import { JSONFilePreset } from "../libraries/lowdb/presets/node" + +import Vars from "../vars" + +//! WARNING: Please DO NOT storage any password or sensitive data here, +// cause its not use any encryption method, and it will be stored in plain text. +// This is intended to store session tokens among other vars. + +export default class ManifestAuthService { + static vaultPath = path.resolve(Vars.runtime_path, "auth.json") + + static async withDB() { + return await JSONFilePreset(ManifestAuthService.vaultPath, {}) + } + + static has = async (pkg_id) => { + const db = await this.withDB() + + return !!db.data[pkg_id] + } + + static set = async (pkg_id, value) => { + const db = await this.withDB() + + return await db.update((data) => { + data[pkg_id] = value + }) + } + + static get = async (pkg_id) => { + const db = await this.withDB() + + return await db.data[pkg_id] + } +} \ No newline at end of file diff --git a/packages/core/src/classes/ManifestConfig.js b/packages/core/src/classes/ManifestConfig.js new file mode 100644 index 0000000..7bc379b --- /dev/null +++ b/packages/core/src/classes/ManifestConfig.js @@ -0,0 +1,34 @@ +import DB from "../db" + +export default class ManifestConfigManager { + constructor(pkg_id) { + this.pkg_id = pkg_id + this.config = null + } + + async initialize() { + const pkg = await DB.getPackages(this.pkg_id) ?? {} + + this.config = pkg.config + } + + set(key, value) { + this.config[key] = value + + DB.updatePackageById(pkg_id, { config: this.config }) + + return this.config + } + + get(key) { + return this.config[key] + } + + delete(key) { + delete this.config[key] + + DB.updatePackageById(pkg_id, { config: this.config }) + + return this.config + } +} \ No newline at end of file diff --git a/packages/core/src/classes/PatchManager.js b/packages/core/src/classes/PatchManager.js new file mode 100644 index 0000000..870d224 --- /dev/null +++ b/packages/core/src/classes/PatchManager.js @@ -0,0 +1,149 @@ +import Logger from "../logger" + +import DB from "../db" +import fs from "node:fs" + +import GenericSteps from "../generic_steps" +import parseStringVars from "../utils/parseStringVars" + +export default class PatchManager { + constructor(pkg, manifest) { + this.pkg = pkg + this.manifest = manifest + + this.log = Logger.child({ service: `PATCH-MANAGER|${pkg.id}` }) + } + + async get(select) { + if (!this.manifest.patches) { + return [] + } + + let list = [] + + if (typeof select === "undefined") { + list = this.manifest.patches + } + + if (Array.isArray(select)) { + for await (let id of select) { + const patch = this.manifest.patches.find((patch) => patch.id === id) + + if (patch) { + list.push(patch) + } + } + } + + return list + } + + async reapply() { + if (Array.isArray(this.pkg.applied_patches)) { + return await this.patch(this.pkg.applied_patches) + } + + return true + } + + async patch(select) { + const list = await this.get(select) + + for await (let patch of list) { + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Applying patch [${patch.id}]...`, + }) + + this.log.info(`Applying patch [${patch.id}]...`) + + if (Array.isArray(patch.additions)) { + this.log.info(`Applying ${patch.additions.length} Additions...`) + + for await (let addition of patch.additions) { + // resolve patch file + addition.file = await parseStringVars(addition.file, this.pkg) + + if (fs.existsSync(addition.file)) { + this.log.info(`Addition [${addition.file}] already exists. Skipping...`) + continue + } + + this.log.info(`Applying addition [${addition.file}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Applying addition [${addition.file}]`, + }) + + await GenericSteps(this.pkg, addition.steps, this.log) + } + } + + if (!this.pkg.applied_patches.includes(patch.id)) { + this.pkg.applied_patches.push(patch.id) + } + } + + await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `${list.length} Patches applied`, + }) + + this.log.info(`${list.length} Patches applied`) + + return this.pkg + } + + async remove(select) { + const list = await this.get(select) + + for await (let patch of list) { + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Removing patch [${patch.id}]...`, + }) + + this.log.info(`Removing patch [${patch.id}]...`) + + if (Array.isArray(patch.additions)) { + this.log.info(`Removing ${patch.additions.length} Additions...`) + + for await (let addition of patch.additions) { + addition.file = await parseStringVars(addition.file, this.pkg) + + if (!fs.existsSync(addition.file)) { + this.log.info(`Addition [${addition.file}] does not exist. Skipping...`) + continue + } + + this.log.info(`Removing addition [${addition.file}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `Removing addition [${addition.file}]`, + }) + + await fs.promises.unlink(addition.file) + } + } + + this.pkg.applied_patches = this.pkg.applied_patches.filter((p) => { + return p !== patch.id + }) + } + + await DB.updatePackageById(this.pkg.id, { applied_patches: this.pkg.applied_patches }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: this.pkg.id, + status_text: `${list.length} Patches removed`, + }) + + this.log.info(`${list.length} Patches removed`) + + return this.pkg + } +} \ No newline at end of file diff --git a/packages/core/src/db.js b/packages/core/src/db.js new file mode 100644 index 0000000..4d99499 --- /dev/null +++ b/packages/core/src/db.js @@ -0,0 +1,115 @@ +import { JSONFilePreset } from "./libraries/lowdb/presets/node" +import Vars from "./vars" +import pkg from "../package.json" +import fs from "node:fs" +import lodash from "lodash" + +export default class DB { + static get defaultRoot() { + return { + created_at_version: pkg.version, + packages: [], + } + } + + static defaultPackageState({ + id, + name, + icon, + version, + author, + install_path, + description, + license, + last_status, + remote_manifest, + local_manifest, + config, + executable, + }) { + return { + id: id, + name: name, + version: version, + icon: icon, + install_path: install_path, + description: description, + author: author, + license: license ?? "unlicensed", + local_manifest: local_manifest ?? null, + remote_manifest: remote_manifest ?? null, + applied_patches: [], + config: typeof config === "object" ? config : {}, + last_status: last_status ?? "installing", + last_update: null, + installed_at: null, + executable: executable ?? false, + } + } + + static async withDB() { + return await JSONFilePreset(Vars.db_path, DB.defaultRoot) + } + + static async initialize() { + await this.cleanOrphans() + } + + static async cleanOrphans() { + const list = await this.getPackages() + + for (const pkg of list) { + if (!fs.existsSync(pkg.install_path)) { + await this.deletePackage(pkg.id) + } + } + } + + static async getPackages(pkg_id) { + const db = await this.withDB() + + if (pkg_id) { + return db.data["packages"].find((i) => i.id === pkg_id) + } + + return db.data["packages"] + } + + static async writePackage(pkg) { + const db = await this.withDB() + + const prevIndex = db.data["packages"].findIndex((i) => i.id === pkg.id) + + if (prevIndex !== -1) { + db.data["packages"][prevIndex] = pkg + } else { + db.data["packages"].push(pkg) + } + + await db.write() + + return db.data + } + + static async updatePackageById(pkg_id, obj) { + let pkg = await this.getPackages(pkg_id) + + if (!pkg) { + throw new Error("Package not found") + } + + return await this.writePackage(lodash.merge({ ...pkg }, obj)) + } + + static async deletePackage(pkg_id) { + const db = await this.withDB() + + await db.update((data) => { + data["packages"] = data["packages"].filter((i) => i.id !== pkg_id) + + return data + }) + + return pkg_id + } +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_clone.js b/packages/core/src/generic_steps/git_clone.js new file mode 100644 index 0000000..da0a3c2 --- /dev/null +++ b/packages/core/src/generic_steps/git_clone.js @@ -0,0 +1,49 @@ +import Logger from "../logger" + +import path from "node:path" +import fs from "node:fs" +import upath from "upath" +import { execa } from "../libraries/execa" + +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + const final_path = upath.normalizeSafe(path.resolve(pkg.install_path, step.path)) + + if (!fs.existsSync(final_path)) { + fs.mkdirSync(final_path, { recursive: true }) + } + + Log.info(`Cloning from [${step.url}]`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Cloning from [${step.url}]`, + }) + + const args = [ + "clone", + //`--depth ${step.depth ?? 1}`, + //"--filter=blob:none", + //"--filter=tree:0", + "--recurse-submodules", + "--remote-submodules", + step.url, + final_path, + ] + + await execa(gitCMD, args, { + cwd: final_path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_pull.js b/packages/core/src/generic_steps/git_pull.js new file mode 100644 index 0000000..f60db44 --- /dev/null +++ b/packages/core/src/generic_steps/git_pull.js @@ -0,0 +1,33 @@ +import Logger from "../logger" + +import path from "node:path" +import fs from "node:fs" +import { execa } from "../libraries/execa" + +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + const _path = path.resolve(pkg.install_path, step.path) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Pulling...`, + }) + + Log.info(`Pulling from HEAD...`) + + await execa(gitCMD, ["pull", "--rebase"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/git_reset.js b/packages/core/src/generic_steps/git_reset.js new file mode 100644 index 0000000..60c31f4 --- /dev/null +++ b/packages/core/src/generic_steps/git_reset.js @@ -0,0 +1,83 @@ +import Logger from "../logger" + +import path from "node:path" +import fs from "node:fs" +import { execa } from "../libraries/execa" + +import git_pull from "./git_pull" +import Vars from "../vars" + +export default async (pkg, step) => { + if (!step.path) { + step.path = `.` + } + + const Log = Logger.child({ service: `GIT|${pkg.id}` }) + + const gitCMD = fs.existsSync(Vars.git_path) ? `${Vars.git_path}` : "git" + + const _path = path.resolve(pkg.install_path, step.path) + const from = step.from ?? "HEAD" + + if (!fs.existsSync(_path)) { + fs.mkdirSync(_path, { recursive: true }) + } + + Log.info(`Fetching from origin`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Fetching from origin...`, + }) + + // fetch from origin + await execa(gitCMD, ["fetch", "origin"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + Log.info(`Cleaning untracked files...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Cleaning untracked files...`, + }) + + await execa(gitCMD, ["clean", "-df"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + Log.info(`Resetting to ${from}`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Resetting to ${from}`, + }) + + await execa(gitCMD, ["reset", "--hard", from], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + // pull the latest + await git_pull(pkg, step) + + Log.info(`Checkout to HEAD`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Checkout to HEAD`, + }) + + await execa(gitCMD, ["checkout", "HEAD"], { + cwd: _path, + stdout: "inherit", + stderr: "inherit", + }) + + return pkg +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/http.js b/packages/core/src/generic_steps/http.js new file mode 100644 index 0000000..57f614c --- /dev/null +++ b/packages/core/src/generic_steps/http.js @@ -0,0 +1,66 @@ +import path from "node:path" +import fs from "node:fs" +import os from "node:os" + +import downloadHttpFile from "../helpers/downloadHttpFile" +import parseStringVars from "../utils/parseStringVars" +import extractFile from "../utils/extractFile" + +export default async (pkg, step, logger) => { + if (!step.path) { + step.path = `./${path.basename(step.url)}` + } + + step.path = await parseStringVars(step.path, pkg) + + let _path = path.resolve(pkg.install_path, step.path) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Downloading [${step.url}]`, + }) + + logger.info(`Downloading [${step.url} to ${_path}]`) + + if (step.tmp) { + _path = path.resolve(os.tmpdir(), String(new Date().getTime()), path.basename(step.url)) + } + + fs.mkdirSync(path.resolve(_path, ".."), { recursive: true }) + + await downloadHttpFile(step.url, _path, (progress) => { + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + use_id_only: true, + status_text: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, + }) + }) + + logger.info(`Downloaded finished.`) + + if (step.extract) { + if (typeof step.extract === "string") { + step.extract = path.resolve(pkg.install_path, step.extract) + } else { + step.extract = path.resolve(pkg.install_path, ".") + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Extracting bundle...`, + }) + + await extractFile(_path, step.extract) + + if (step.deleteAfterExtract !== false) { + logger.info(`Deleting temporal file [${_path}]...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Deleting temporal files...`, + }) + + await fs.promises.rm(_path, { recursive: true }) + } + } +} \ No newline at end of file diff --git a/packages/core/src/generic_steps/index.js b/packages/core/src/generic_steps/index.js new file mode 100644 index 0000000..96e2017 --- /dev/null +++ b/packages/core/src/generic_steps/index.js @@ -0,0 +1,48 @@ +import Logger from "../logger" + +import ISM_GIT_CLONE from "./git_clone" +import ISM_GIT_PULL from "./git_pull" +import ISM_GIT_RESET from "./git_reset" +import ISM_HTTP from "./http" + +const InstallationStepsMethods = { + git_clone: ISM_GIT_CLONE, + git_pull: ISM_GIT_PULL, + git_reset: ISM_GIT_RESET, + http_file: ISM_HTTP, +} + +const StepsOrders = [ + "git_clones", + "git_pull", + "git_reset", + "http_file", +] + +export default async function processGenericSteps(pkg, steps, logger = Logger) { + logger.info(`Processing generic steps...`) + + if (!Array.isArray(steps)) { + throw new Error(`Steps must be an array`) + } + + if (steps.length === 0) { + return pkg + } + + steps = steps.sort((a, b) => { + return StepsOrders.indexOf(a.type) - StepsOrders.indexOf(b.type) + }) + + for await (let step of steps) { + step.type = step.type.toLowerCase() + + if (!InstallationStepsMethods[step.type]) { + throw new Error(`Unknown step: ${step.type}`) + } + + await InstallationStepsMethods[step.type](pkg, step, logger) + } + + return pkg +} diff --git a/packages/core/src/handlers/apply.js b/packages/core/src/handlers/apply.js new file mode 100644 index 0000000..7b9f47e --- /dev/null +++ b/packages/core/src/handlers/apply.js @@ -0,0 +1,95 @@ +import Logger from "../logger" + +import PatchManager from "../classes/PatchManager" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import DB from "../db" + +const BaseLog = Logger.child({ service: "APPLIER" }) + +function findPatch(patches, applied_patches, changes, mustBeInstalled) { + return patches.filter((patch) => { + const patchID = patch.id + + if (typeof changes.patches[patchID] === "undefined") { + return false + } + + if (mustBeInstalled === true && !applied_patches.includes(patch.id) && changes.patches[patchID] === true) { + return true + } + + if (mustBeInstalled === false && applied_patches.includes(patch.id) && changes.patches[patchID] === false) { + return true + } + + return false + }).map((patch) => patch.id) +} + +export default async function apply(pkg_id, changes = {}) { + try { + let pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.error(`Package not found [${pkg_id}]`) + return null + } + + let manifest = await ManifestReader(pkg.local_manifest) + manifest = await ManifestVM(manifest.code) + + const Log = Logger.child({ service: `APPLIER|${pkg.id}` }) + + Log.info(`Applying changes to package...`) + Log.info(`Changes: ${JSON.stringify(changes)}`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Applying changes to package...`, + last_status: "loading", + }) + + if (changes.patches) { + if (!Array.isArray(pkg.applied_patches)) { + pkg.applied_patches = [] + } + + const patches = new PatchManager(pkg, manifest) + + await patches.remove(findPatch(manifest.patches, pkg.applied_patches, changes, false)) + await patches.patch(findPatch(manifest.patches, pkg.applied_patches, changes, true)) + } + + if (changes.config) { + Log.info(`Applying config to package...`) + + if (Object.keys(changes.config).length !== 0) { + Object.entries(changes.config).forEach(([key, value]) => { + pkg.config[key] = value + }) + } + } + + await DB.writePackage(pkg) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: "All changes applied", + }) + + Log.info(`All changes applied to package.`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + error + }) + + BaseLog.error(`Failed to apply changes to package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/authorize.js b/packages/core/src/handlers/authorize.js new file mode 100644 index 0000000..e1d19ab --- /dev/null +++ b/packages/core/src/handlers/authorize.js @@ -0,0 +1,33 @@ +import ManifestAuthDB from "../classes/ManifestAuthDB" +import DB from "../db" + +import Logger from "../logger" + +const Log = Logger.child({ service: "AUTH" }) + +export default async (pkg_id, value) => { + if (!pkg_id) { + Log.error("pkg_id is required") + return false + } + + if (!value) { + Log.error("value is required") + return false + } + + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + Log.error("Package not found") + return false + } + + Log.info(`Setting auth for [${pkg_id}]`) + + await ManifestAuthDB.set(pkg_id, value) + + global._relic_eventBus.emit("pkg:authorized", pkg) + + return true +} \ No newline at end of file diff --git a/packages/core/src/handlers/checkUpdate.js b/packages/core/src/handlers/checkUpdate.js new file mode 100644 index 0000000..48fe56b --- /dev/null +++ b/packages/core/src/handlers/checkUpdate.js @@ -0,0 +1,43 @@ +import Logger from "../logger" +import DB from "../db" + +import softRead from "./read" + +const Log = Logger.child({ service: "CHECK_UPDATE" }) + +export default async function checkUpdate(pkg_id) { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + Log.error("Package not found") + return false + } + + Log.info(`Checking update for [${pkg_id}]`) + + const remoteSoftManifest = await softRead(pkg.remote_manifest, { + soft: true + }) + + if (!remoteSoftManifest) { + Log.error("Cannot read remote manifest") + return false + } + + if (pkg.version === remoteSoftManifest.version) { + Log.info("No update available") + return false + } + + Log.info("Update available") + Log.info("Local:", pkg.version) + Log.info("Remote:", remoteSoftManifest.version) + Log.info("Changelog:", remoteSoftManifest.changelog_url) + + return { + id: pkg.id, + local: pkg.version, + remote: remoteSoftManifest.version, + changelog: remoteSoftManifest.changelog_url, + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/execute.js b/packages/core/src/handlers/execute.js new file mode 100644 index 0000000..4351d24 --- /dev/null +++ b/packages/core/src/handlers/execute.js @@ -0,0 +1,80 @@ +import Logger from "../logger" + +import fs from "node:fs" + +import DB from "../db" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import parseStringVars from "../utils/parseStringVars" +import { execa } from "../libraries/execa" + +const BaseLog = Logger.child({ service: "EXECUTER" }) + +export default async function execute(pkg_id, { useRemote = false, force = false } = {}) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.info(`Package not found [${pkg_id}]`) + return false + } + + const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest + + if (!fs.existsSync(manifestPath)) { + BaseLog.error(`Manifest not found in expected path [${manifestPath}] + \nMaybe the package installation has not been completed yet or corrupted. + `) + + return false + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "loading", + status_text: null, + }) + + const ManifestRead = await ManifestReader(manifestPath) + + const manifest = await ManifestVM(ManifestRead.code) + + if (typeof manifest.execute === "function") { + await manifest.execute(pkg) + } + + if (typeof manifest.execute === "string") { + manifest.execute = parseStringVars(manifest.execute, pkg) + + BaseLog.info(`Executing binary > [${manifest.execute}]`) + + const args = Array.isArray(manifest.execute_args) ? manifest.execute_args : [] + + await execa(manifest.execute, args, { + cwd: pkg.install_path, + stdout: "inherit", + stderr: "inherit", + }) + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "installed", + status_text: null, + }) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + event: "execute", + last_status: "installed", + error, + }) + + BaseLog.error(`Failed to execute package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} diff --git a/packages/core/src/handlers/install.js b/packages/core/src/handlers/install.js new file mode 100644 index 0000000..faf0919 --- /dev/null +++ b/packages/core/src/handlers/install.js @@ -0,0 +1,182 @@ +import Logger from "../logger" + +import fs from "node:fs" + +import DB from "../db" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" +import GenericSteps from "../generic_steps" +import Apply from "../handlers/apply" + +const BaseLog = Logger.child({ service: "INSTALLER" }) + +export default async function install(manifest) { + let id = null + + try { + BaseLog.info(`Invoking new installation...`) + BaseLog.info(`Fetching manifest [${manifest}]`) + + const ManifestRead = await ManifestReader(manifest) + + manifest = await ManifestVM(ManifestRead.code) + + id = manifest.constructor.id + + const Log = BaseLog.child({ service: `INSTALLER|${id}` }) + + Log.info(`Creating install path [${manifest.install_path}]`) + + if (fs.existsSync(manifest.install_path)) { + Log.info(`Package already exists, removing...`) + await fs.rmSync(manifest.install_path, { recursive: true }) + } + + await fs.mkdirSync(manifest.install_path, { recursive: true }) + + Log.info(`Initializing manifest...`) + + if (typeof manifest.initialize === "function") { + await manifest.initialize() + } + + Log.info(`Appending to db...`) + + const pkg = DB.defaultPackageState({ + ...manifest.constructor, + id: id, + name: manifest.constructor.pkg_name, + version: manifest.constructor.version, + install_path: manifest.install_path, + description: manifest.constructor.description, + license: manifest.constructor.license, + last_status: "installing", + remote_manifest: ManifestRead.remote_manifest, + local_manifest: ManifestRead.local_manifest, + executable: !!manifest.execute + }) + + await DB.writePackage(pkg) + + global._relic_eventBus.emit("pkg:new", pkg) + + if (manifest.configuration) { + Log.info(`Applying default config to package...`) + + pkg.config = Object.entries(manifest.configuration).reduce((acc, [key, value]) => { + acc[key] = value.default + + return acc + }, {}) + } + + if (typeof manifest.beforeInstall === "function") { + Log.info(`Executing beforeInstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing beforeInstall hook...`, + }) + + await manifest.beforeInstall(pkg) + } + + if (Array.isArray(manifest.installSteps)) { + Log.info(`Executing generic install steps...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing generic install steps...`, + }) + + await GenericSteps(pkg, manifest.installSteps, Log) + } + + if (typeof manifest.afterInstall === "function") { + Log.info(`Executing afterInstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing afterInstall hook...`, + }) + + await manifest.afterInstall(pkg) + } + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Finishing up...`, + }) + + Log.info(`Copying manifest to the final location...`) + + const finalPath = `${manifest.install_path}/.rmanifest` + + if (fs.existsSync(finalPath)) { + await fs.promises.unlink(finalPath) + } + + await fs.promises.copyFile(ManifestRead.local_manifest, finalPath) + + if (ManifestRead.is_catched) { + Log.info(`Removing cache manifest...`) + await fs.promises.unlink(ManifestRead.local_manifest) + } + + pkg.local_manifest = finalPath + pkg.last_status = "loading" + pkg.installed_at = Date.now() + + await DB.writePackage(pkg) + + if (manifest.patches) { + const defaultPatches = manifest.patches.filter((patch) => patch.default) + + if (defaultPatches.length > 0) { + Log.info(`Applying default patches...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Applying default patches...`, + }) + + await Apply(id, { + patches: Object.fromEntries(defaultPatches.map((patch) => [patch.id, true])), + }) + } + } + + pkg.last_status = "installed" + + await DB.writePackage(pkg) + + global._relic_eventBus.emit(`pkg:update:state`, { + ...pkg, + id: pkg.id, + last_status: "installed", + status_text: `Installation completed successfully`, + }) + + global._relic_eventBus.emit(`pkg:new:done`, pkg) + + Log.info(`Package installed successfully!`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + id: id, + error + }) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: id, + last_status: "failed", + status_text: `Installation failed`, + }) + + BaseLog.error(`Error during installation of package [${id}] >`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/list.js b/packages/core/src/handlers/list.js new file mode 100644 index 0000000..eb51f5a --- /dev/null +++ b/packages/core/src/handlers/list.js @@ -0,0 +1,5 @@ +import DB from "../db" + +export default async function list() { + return await DB.getPackages() +} \ No newline at end of file diff --git a/packages/core/src/handlers/read.js b/packages/core/src/handlers/read.js new file mode 100644 index 0000000..225842f --- /dev/null +++ b/packages/core/src/handlers/read.js @@ -0,0 +1,9 @@ +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +export default async function softRead(manifest, options = {}) { + const Reader = await ManifestReader(manifest) + const VM = await ManifestVM(Reader.code, options) + + return VM +} \ No newline at end of file diff --git a/packages/core/src/handlers/uninstall.js b/packages/core/src/handlers/uninstall.js new file mode 100644 index 0000000..b0ea282 --- /dev/null +++ b/packages/core/src/handlers/uninstall.js @@ -0,0 +1,74 @@ +import Logger from "../logger" + +import DB from "../db" +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +import { rimraf } from "rimraf" + +const BaseLog = Logger.child({ service: "UNINSTALLER" }) + +export default async function uninstall(pkg_id) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.info(`Package not found [${pkg_id}]`) + return null + } + + const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` }) + + Log.info(`Uninstalling package...`) + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Uninstalling package...`, + }) + + const ManifestRead = await ManifestReader(pkg.local_manifest) + const manifest = await ManifestVM(ManifestRead.code) + + if (typeof manifest.uninstall === "function") { + Log.info(`Performing uninstall hook...`) + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing uninstall hook...`, + }) + await manifest.uninstall(pkg) + } + + Log.info(`Deleting package directory...`) + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Deleting package directory...`, + }) + await rimraf(pkg.install_path) + + Log.info(`Removing package from database...`) + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Removing package from database...`, + }) + await DB.deletePackage(pkg.id) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "deleted", + status_text: `Uninstalling package...`, + }) + global._relic_eventBus.emit(`pkg:remove`, pkg) + Log.info(`Package uninstalled successfully!`) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + error + }) + + BaseLog.error(`Failed to uninstall package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/update.js b/packages/core/src/handlers/update.js new file mode 100644 index 0000000..87d4ce0 --- /dev/null +++ b/packages/core/src/handlers/update.js @@ -0,0 +1,128 @@ +import Logger from "../logger" + +import DB from "../db" + +import ManifestReader from "../manifest/reader" +import ManifestVM from "../manifest/vm" + +import GenericSteps from "../generic_steps" +import PatchManager from "../classes/PatchManager" + +const BaseLog = Logger.child({ service: "UPDATER" }) + +const AllowedPkgChanges = [ + "id", + "name", + "version", + "description", + "author", + "license", + "icon", + "core_minimum_version", + "remote_manifest", +] + +const ManifestKeysMap = { + "name": "pkg_name", +} + +export default async function update(pkg_id) { + try { + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + BaseLog.error(`Package not found [${pkg_id}]`) + + return null + } + + const Log = BaseLog.child({ service: `UPDATER|${pkg.id}` }) + + let ManifestRead = await ManifestReader(pkg.local_manifest) + let manifest = await ManifestVM(ManifestRead.code) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + last_status: "updating", + status_text: `Updating package...`, + }) + + pkg.last_status = "updating" + + await DB.writePackage(pkg) + + if (typeof manifest.update === "function") { + Log.info(`Performing update hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing update hook...`, + }) + + await manifest.update(pkg) + } + + if (manifest.updateSteps) { + Log.info(`Performing update steps...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing update steps...`, + }) + + await GenericSteps(pkg, manifest.updateSteps, Log) + } + + if (Array.isArray(pkg.applied_patches)) { + const patchManager = new PatchManager(pkg, manifest) + + await patchManager.reapply() + } + + if (typeof manifest.afterUpdate === "function") { + Log.info(`Performing after update hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing after update hook...`, + }) + + await manifest.afterUpdate(pkg) + } + + ManifestRead = await ManifestReader(pkg.local_manifest) + manifest = await ManifestVM(ManifestRead.code) + + // override public static values + for await (const key of AllowedPkgChanges) { + if (key in manifest.constructor) { + const mapKey = ManifestKeysMap[key] || key + pkg[key] = manifest.constructor[mapKey] + } + } + + pkg.last_status = "installed" + pkg.last_update = Date.now() + + await DB.writePackage(pkg) + + Log.info(`Package updated successfully`) + + global._relic_eventBus.emit(`pkg:update:state`, { + ...pkg, + id: pkg.id, + }) + + return pkg + } catch (error) { + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + error + }) + + BaseLog.error(`Failed to update package [${pkg_id}]`, error) + BaseLog.error(error.stack) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/helpers/downloadHttpFile.js b/packages/core/src/helpers/downloadHttpFile.js new file mode 100644 index 0000000..d347b81 --- /dev/null +++ b/packages/core/src/helpers/downloadHttpFile.js @@ -0,0 +1,73 @@ +import fs from "node:fs" +import axios from "axios" +import humanFormat from "human-format" +import cliProgress from "cli-progress" + +function convertSize(size) { + return `${humanFormat(size, { + decimals: 2, + })}B` +} + +export default async (url, destination, progressCallback) => { + const progressBar = new cliProgress.SingleBar({ + format: "[{bar}] {percentage}% | {total_formatted} | {speed}/s | {eta_formatted}", + barCompleteChar: "\u2588", + barIncompleteChar: "\u2591", + hideCursor: true + }, cliProgress.Presets.shades_classic) + + const { data: remoteStream, headers } = await axios.get(url, { + responseType: "stream", + }) + + const localStream = fs.createWriteStream(destination) + + let progress = { + total: Number(headers["content-length"] ?? 0), + transferred: 0, + speed: 0, + } + + let lastTickTransferred = 0 + + progressBar.start(progress.total, 0, { + speed: "0B/s", + total_formatted: convertSize(progress.total), + }) + + remoteStream.pipe(localStream) + + remoteStream.on("data", (data) => { + progress.transferred = progress.transferred + Buffer.byteLength(data) + }) + + const progressInterval = setInterval(() => { + progress.speed = ((progress.transferred ?? 0) - lastTickTransferred) / 1 + + lastTickTransferred = progress.transferred ?? 0 + + progress.transferredString = convertSize(progress.transferred ?? 0) + progress.totalString = convertSize(progress.total) + progress.speedString = convertSize(progress.speed) + + progressBar.update(progress.transferred, { + speed: progress.speedString, + }) + + if (typeof progressCallback === "function") { + progressCallback(progress) + } + }, 1000) + + await new Promise((resolve, reject) => { + localStream.on("finish", resolve) + localStream.on("error", reject) + }) + + progressBar.stop() + + clearInterval(progressInterval) + + return destination +} \ No newline at end of file diff --git a/packages/core/src/helpers/sendToRender.js b/packages/core/src/helpers/sendToRender.js new file mode 100644 index 0000000..8a534a5 --- /dev/null +++ b/packages/core/src/helpers/sendToRender.js @@ -0,0 +1,43 @@ +import lodash from "lodash" + +const forbidden = [ + "libraries" +] + +export default (event, data) => { + if (!global.win) { + return false + } + + try { + function serializeIpc(data) { + if (!data) { + return undefined + } + + data = JSON.stringify(data) + + data = JSON.parse(data) + + const copy = lodash.cloneDeep(data) + + if (!Array.isArray(copy)) { + Object.keys(copy).forEach((key) => { + if (forbidden.includes(key)) { + delete copy[key] + } + + if (typeof copy[key] === "function") { + delete copy[key] + } + }) + } + + return copy + } + + global.win.webContents.send(event, serializeIpc(data)) + } catch (error) { + console.error(error) + } +} \ No newline at end of file diff --git a/packages/core/src/helpers/setup.js b/packages/core/src/helpers/setup.js new file mode 100644 index 0000000..826c4c6 --- /dev/null +++ b/packages/core/src/helpers/setup.js @@ -0,0 +1,201 @@ +import Logger from "../logger" + +const Log = Logger.child({ service: "SETUP" }) + +import path from "node:path" +import fs from "node:fs" +import os from "node:os" +import admzip from "adm-zip" +import resolveOs from "../utils/resolveOs" +import chmodRecursive from "../utils/chmodRecursive" + +import downloadFile from "../helpers/downloadHttpFile" + +import Vars from "../vars" +import Prerequisites from "../prerequisites" + +export default async () => { + if (!fs.existsSync(Vars.binaries_path)) { + Log.info(`Creating binaries directory: ${Vars.binaries_path}...`) + await fs.promises.mkdir(Vars.binaries_path, { recursive: true }) + } + + for await (let prerequisite of Prerequisites) { + try { + Log.info(`Checking prerequisite: ${prerequisite.id}...`) + + if (Array.isArray(prerequisite.requireOs) && !prerequisite.requireOs.includes(os.platform())) { + Log.info(`Prerequisite: ${prerequisite.id} is not required for this os.`) + continue + } + + if (!fs.existsSync(prerequisite.finalBin)) { + Log.info(`Missing prerequisite: ${prerequisite.id}, installing...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Installing ${prerequisite.id}`, + }) + + if (fs.existsSync(prerequisite.destination)) { + Log.info(`Deleting temporal file [${prerequisite.destination}]`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Deleting temporal file [${prerequisite.destination}]`, + }) + + await fs.promises.rm(prerequisite.destination) + } + + if (fs.existsSync(prerequisite.extract)) { + Log.info(`Deleting temporal directory [${prerequisite.extract}]`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Deleting temporal directory [${prerequisite.extract}]`, + }) + + await fs.promises.rm(prerequisite.extract, { recursive: true }) + } + + Log.info(`Creating base directory: ${Vars.binaries_path}/${prerequisite.id}...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Creating base directory: ${Vars.binaries_path}/${prerequisite.id}`, + }) + + await fs.promises.mkdir(path.resolve(Vars.binaries_path, prerequisite.id), { recursive: true }) + + if (typeof prerequisite.url === "function") { + prerequisite.url = await prerequisite.url(resolveOs(), os.arch()) + Log.info(`Resolved url: ${prerequisite.url}`) + } + + Log.info(`Downloading ${prerequisite.id} from [${prerequisite.url}] to destination [${prerequisite.destination}]...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Starting download ${prerequisite.id} from [${prerequisite.url}] to destination [${prerequisite.destination}]`, + }) + + try { + await downloadFile( + prerequisite.url, + prerequisite.destination, + (progress) => { + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Downloaded ${progress.transferredString} / ${progress.totalString} | ${progress.speedString}/s`, + }) + } + ) + } catch (error) { + if (fs.existsSync(prerequisite.destination)) { + await fs.promises.rm(prerequisite.destination) + } + + throw error + } + + if (typeof prerequisite.extract === "string") { + Log.info(`Extracting ${prerequisite.id} to destination [${prerequisite.extract}]...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Extracting ${prerequisite.id} to destination [${prerequisite.extract}]`, + }) + + const zip = new admzip(prerequisite.destination) + + await zip.extractAllTo(prerequisite.extract, true) + + Log.info(`Extraction ok...`) + } + + if (prerequisite.extractTargetFromName === true) { + let name = path.basename(prerequisite.url) + const ext = path.extname(name) + + name = name.replace(ext, "") + + if (fs.existsSync(path.resolve(prerequisite.extract, name))) { + await fs.promises.rename(path.resolve(prerequisite.extract, name), `${prerequisite.extract}_old`) + await fs.promises.rm(prerequisite.extract, { recursive: true }) + await fs.promises.rename(`${prerequisite.extract}_old`, prerequisite.extract) + } + } + + if (prerequisite.deleteBeforeExtract === true) { + Log.info(`Deleting temporal file [${prerequisite.destination}]`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Deleting temporal file [${prerequisite.destination}]`, + }) + + await fs.promises.unlink(prerequisite.destination) + } + + if (typeof prerequisite.rewriteExecutionPermission !== "undefined") { + const to = typeof prerequisite.rewriteExecutionPermission === "string" ? + prerequisite.rewriteExecutionPermission : + prerequisite.finalBin + + Log.info(`Rewriting permissions to ${to}...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Rewriting permissions to ${to}`, + }) + + await chmodRecursive(to, 0o755) + } + + if (Array.isArray(prerequisite.moveDirs)) { + for (const dir of prerequisite.moveDirs) { + if (Array.isArray(dir.requireOs)) { + if (!dir.requireOs.includes(resolveOs())) { + continue + } + } + + Log.info(`Moving ${dir.from} to ${dir.to}...`) + + global._relic_eventBus.emit("app:setup", { + installed: false, + message: `Moving ${dir.from} to ${dir.to}`, + }) + + await fs.promises.rename(dir.from, dir.to) + + if (dir.deleteParentBefore === true) { + await fs.promises.rm(path.dirname(dir.from), { recursive: true }) + } + } + } + } + + global._relic_eventBus.emit("app:setup", { + installed: true, + message: null, + }) + + Log.info(`Prerequisite: ${prerequisite.id} is ready!`) + } catch (error) { + global._relic_eventBus.emit("app:setup", { + installed: false, + error: error, + message: error.message, + }) + + Log.error("Aborting setup due to an error...") + Log.error(error) + + throw error + } + + Log.info(`All prerequisites are ready!`) + } +} \ No newline at end of file diff --git a/packages/core/src/index.js b/packages/core/src/index.js new file mode 100644 index 0000000..de39263 --- /dev/null +++ b/packages/core/src/index.js @@ -0,0 +1,68 @@ +import fs from "node:fs" +import { EventEmitter } from "@foxify/events" +import { onExit } from "signal-exit" +import open from "open" + +import SetupHelper from "./helpers/setup" +import Logger from "./logger" + +import Vars from "./vars" +import DB from "./db" + +import PackageInstall from "./handlers/install" +import PackageExecute from "./handlers/execute" +import PackageUninstall from "./handlers/uninstall" +import PackageUpdate from "./handlers/update" +import PackageApply from "./handlers/apply" +import PackageList from "./handlers/list" +import PackageRead from "./handlers/read" +import PackageAuthorize from "./handlers/authorize" +import PackageCheckUpdate from "./handlers/checkUpdate" + +export default class RelicCore { + constructor(params) { + this.params = params + } + + eventBus = global._relic_eventBus = new EventEmitter() + + logger = Logger + + db = DB + + async initialize() { + await DB.initialize() + + onExit(this.onExit) + } + + onExit = () => { + if (fs.existsSync(Vars.cache_path)) { + fs.rmSync(Vars.cache_path, { recursive: true, force: true }) + } + } + + async setup() { + return await SetupHelper() + } + + package = { + install: PackageInstall, + execute: PackageExecute, + uninstall: PackageUninstall, + update: PackageUpdate, + apply: PackageApply, + list: PackageList, + read: PackageRead, + authorize: PackageAuthorize, + checkUpdate: PackageCheckUpdate + } + + openPath(pkg_id) { + if (!pkg_id) { + return open(Vars.runtime_path) + } + + return open(Vars.packages_path + "/" + pkg_id) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/execa/index.js b/packages/core/src/libraries/execa/index.js new file mode 100644 index 0000000..fca5389 --- /dev/null +++ b/packages/core/src/libraries/execa/index.js @@ -0,0 +1,309 @@ +import {Buffer} from 'node:buffer'; +import path from 'node:path'; +import childProcess from 'node:child_process'; +import process from 'node:process'; +import crossSpawn from 'cross-spawn'; +import stripFinalNewline from '../strip-final-newline'; +import {npmRunPathEnv} from '../npm-run-path'; +import onetime from '../onetime'; +import {makeError} from './lib/error.js'; +import {normalizeStdio, normalizeStdioNode} from './lib/stdio.js'; +import {spawnedKill, spawnedCancel, setupTimeout, validateTimeout, setExitHandler} from './lib/kill.js'; +import {addPipeMethods} from './lib/pipe.js'; +import {handleInput, getSpawnedResult, makeAllStream, handleInputSync} from './lib/stream.js'; +import {mergePromise, getSpawnedPromise} from './lib/promise.js'; +import {joinCommand, parseCommand, parseTemplates, getEscapedCommand} from './lib/command.js'; +import {logCommand, verboseDefault} from './lib/verbose.js'; + +const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; + +const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { + const env = extendEnv ? {...process.env, ...envOption} : envOption; + + if (preferLocal) { + return npmRunPathEnv({env, cwd: localDir, execPath}); + } + + return env; +}; + +const handleArguments = (file, args, options = {}) => { + const parsed = crossSpawn._parse(file, args, options); + file = parsed.command; + args = parsed.args; + options = parsed.options; + + options = { + maxBuffer: DEFAULT_MAX_BUFFER, + buffer: true, + stripFinalNewline: true, + extendEnv: true, + preferLocal: false, + localDir: options.cwd || process.cwd(), + execPath: process.execPath, + encoding: 'utf8', + reject: true, + cleanup: true, + all: false, + windowsHide: true, + verbose: verboseDefault, + ...options, + }; + + options.env = getEnv(options); + + options.stdio = normalizeStdio(options); + + if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { + // #116 + args.unshift('/q'); + } + + return {file, args, options, parsed}; +}; + +const handleOutput = (options, value, error) => { + if (typeof value !== 'string' && !Buffer.isBuffer(value)) { + // When `execaSync()` errors, we normalize it to '' to mimic `execa()` + return error === undefined ? undefined : ''; + } + + if (options.stripFinalNewline) { + return stripFinalNewline(value); + } + + return value; +}; + +export function execa(file, args, options) { + const parsed = handleArguments(file, args, options); + const command = joinCommand(file, args); + const escapedCommand = getEscapedCommand(file, args); + logCommand(escapedCommand, parsed.options); + + validateTimeout(parsed.options); + + let spawned; + try { + spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); + } catch (error) { + // Ensure the returned error is always both a promise and a child process + const dummySpawned = new childProcess.ChildProcess(); + const errorPromise = Promise.reject(makeError({ + error, + stdout: '', + stderr: '', + all: '', + command, + escapedCommand, + parsed, + timedOut: false, + isCanceled: false, + killed: false, + })); + mergePromise(dummySpawned, errorPromise); + return dummySpawned; + } + + const spawnedPromise = getSpawnedPromise(spawned); + const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); + const processDone = setExitHandler(spawned, parsed.options, timedPromise); + + const context = {isCanceled: false}; + + spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); + spawned.cancel = spawnedCancel.bind(null, spawned, context); + + const handlePromise = async () => { + const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); + const stdout = handleOutput(parsed.options, stdoutResult); + const stderr = handleOutput(parsed.options, stderrResult); + const all = handleOutput(parsed.options, allResult); + + if (error || exitCode !== 0 || signal !== null) { + const returnedError = makeError({ + error, + exitCode, + signal, + stdout, + stderr, + all, + command, + escapedCommand, + parsed, + timedOut, + isCanceled: context.isCanceled || (parsed.options.signal ? parsed.options.signal.aborted : false), + killed: spawned.killed, + }); + + if (!parsed.options.reject) { + return returnedError; + } + + throw returnedError; + } + + return { + command, + escapedCommand, + exitCode: 0, + stdout, + stderr, + all, + failed: false, + timedOut: false, + isCanceled: false, + killed: false, + }; + }; + + const handlePromiseOnce = onetime(handlePromise); + + handleInput(spawned, parsed.options); + + spawned.all = makeAllStream(spawned, parsed.options); + + addPipeMethods(spawned); + mergePromise(spawned, handlePromiseOnce); + return spawned; +} + +export function execaSync(file, args, options) { + const parsed = handleArguments(file, args, options); + const command = joinCommand(file, args); + const escapedCommand = getEscapedCommand(file, args); + logCommand(escapedCommand, parsed.options); + + const input = handleInputSync(parsed.options); + + let result; + try { + result = childProcess.spawnSync(parsed.file, parsed.args, {...parsed.options, input}); + } catch (error) { + throw makeError({ + error, + stdout: '', + stderr: '', + all: '', + command, + escapedCommand, + parsed, + timedOut: false, + isCanceled: false, + killed: false, + }); + } + + const stdout = handleOutput(parsed.options, result.stdout, result.error); + const stderr = handleOutput(parsed.options, result.stderr, result.error); + + if (result.error || result.status !== 0 || result.signal !== null) { + const error = makeError({ + stdout, + stderr, + error: result.error, + signal: result.signal, + exitCode: result.status, + command, + escapedCommand, + parsed, + timedOut: result.error && result.error.code === 'ETIMEDOUT', + isCanceled: false, + killed: result.signal !== null, + }); + + if (!parsed.options.reject) { + return error; + } + + throw error; + } + + return { + command, + escapedCommand, + exitCode: 0, + stdout, + stderr, + failed: false, + timedOut: false, + isCanceled: false, + killed: false, + }; +} + +const normalizeScriptStdin = ({input, inputFile, stdio}) => input === undefined && inputFile === undefined && stdio === undefined + ? {stdin: 'inherit'} + : {}; + +const normalizeScriptOptions = (options = {}) => ({ + preferLocal: true, + ...normalizeScriptStdin(options), + ...options, +}); + +function create$(options) { + function $(templatesOrOptions, ...expressions) { + if (!Array.isArray(templatesOrOptions)) { + return create$({...options, ...templatesOrOptions}); + } + + const [file, ...args] = parseTemplates(templatesOrOptions, expressions); + return execa(file, args, normalizeScriptOptions(options)); + } + + $.sync = (templates, ...expressions) => { + if (!Array.isArray(templates)) { + throw new TypeError('Please use $(options).sync`command` instead of $.sync(options)`command`.'); + } + + const [file, ...args] = parseTemplates(templates, expressions); + return execaSync(file, args, normalizeScriptOptions(options)); + }; + + return $; +} + +export const $ = create$(); + +export function execaCommand(command, options) { + const [file, ...args] = parseCommand(command); + return execa(file, args, options); +} + +export function execaCommandSync(command, options) { + const [file, ...args] = parseCommand(command); + return execaSync(file, args, options); +} + +export function execaNode(scriptPath, args, options = {}) { + if (args && !Array.isArray(args) && typeof args === 'object') { + options = args; + args = []; + } + + const stdio = normalizeStdioNode(options); + const defaultExecArgv = process.execArgv.filter(arg => !arg.startsWith('--inspect')); + + const { + nodePath = process.execPath, + nodeOptions = defaultExecArgv, + } = options; + + return execa( + nodePath, + [ + ...nodeOptions, + scriptPath, + ...(Array.isArray(args) ? args : []), + ], + { + ...options, + stdin: undefined, + stdout: undefined, + stderr: undefined, + stdio, + shell: false, + }, + ); +} diff --git a/packages/core/src/libraries/execa/lib/command.js b/packages/core/src/libraries/execa/lib/command.js new file mode 100644 index 0000000..727ce5f --- /dev/null +++ b/packages/core/src/libraries/execa/lib/command.js @@ -0,0 +1,119 @@ +import {Buffer} from 'node:buffer'; +import {ChildProcess} from 'node:child_process'; + +const normalizeArgs = (file, args = []) => { + if (!Array.isArray(args)) { + return [file]; + } + + return [file, ...args]; +}; + +const NO_ESCAPE_REGEXP = /^[\w.-]+$/; + +const escapeArg = arg => { + if (typeof arg !== 'string' || NO_ESCAPE_REGEXP.test(arg)) { + return arg; + } + + return `"${arg.replaceAll('"', '\\"')}"`; +}; + +export const joinCommand = (file, args) => normalizeArgs(file, args).join(' '); + +export const getEscapedCommand = (file, args) => normalizeArgs(file, args).map(arg => escapeArg(arg)).join(' '); + +const SPACES_REGEXP = / +/g; + +// Handle `execaCommand()` +export const parseCommand = command => { + const tokens = []; + for (const token of command.trim().split(SPACES_REGEXP)) { + // Allow spaces to be escaped by a backslash if not meant as a delimiter + const previousToken = tokens.at(-1); + if (previousToken && previousToken.endsWith('\\')) { + // Merge previous token with current one + tokens[tokens.length - 1] = `${previousToken.slice(0, -1)} ${token}`; + } else { + tokens.push(token); + } + } + + return tokens; +}; + +const parseExpression = expression => { + const typeOfExpression = typeof expression; + + if (typeOfExpression === 'string') { + return expression; + } + + if (typeOfExpression === 'number') { + return String(expression); + } + + if ( + typeOfExpression === 'object' + && expression !== null + && !(expression instanceof ChildProcess) + && 'stdout' in expression + ) { + const typeOfStdout = typeof expression.stdout; + + if (typeOfStdout === 'string') { + return expression.stdout; + } + + if (Buffer.isBuffer(expression.stdout)) { + return expression.stdout.toString(); + } + + throw new TypeError(`Unexpected "${typeOfStdout}" stdout in template expression`); + } + + throw new TypeError(`Unexpected "${typeOfExpression}" in template expression`); +}; + +const concatTokens = (tokens, nextTokens, isNew) => isNew || tokens.length === 0 || nextTokens.length === 0 + ? [...tokens, ...nextTokens] + : [ + ...tokens.slice(0, -1), + `${tokens.at(-1)}${nextTokens[0]}`, + ...nextTokens.slice(1), + ]; + +const parseTemplate = ({templates, expressions, tokens, index, template}) => { + const templateString = template ?? templates.raw[index]; + const templateTokens = templateString.split(SPACES_REGEXP).filter(Boolean); + const newTokens = concatTokens( + tokens, + templateTokens, + templateString.startsWith(' '), + ); + + if (index === expressions.length) { + return newTokens; + } + + const expression = expressions[index]; + const expressionTokens = Array.isArray(expression) + ? expression.map(expression => parseExpression(expression)) + : [parseExpression(expression)]; + return concatTokens( + newTokens, + expressionTokens, + templateString.endsWith(' '), + ); +}; + +export const parseTemplates = (templates, expressions) => { + let tokens = []; + + for (const [index, template] of templates.entries()) { + tokens = parseTemplate({templates, expressions, tokens, index, template}); + } + + return tokens; +}; + diff --git a/packages/core/src/libraries/execa/lib/error.js b/packages/core/src/libraries/execa/lib/error.js new file mode 100644 index 0000000..761032b --- /dev/null +++ b/packages/core/src/libraries/execa/lib/error.js @@ -0,0 +1,87 @@ +import process from 'node:process'; +import {signalsByName} from '../../human-signals'; + +const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { + if (timedOut) { + return `timed out after ${timeout} milliseconds`; + } + + if (isCanceled) { + return 'was canceled'; + } + + if (errorCode !== undefined) { + return `failed with ${errorCode}`; + } + + if (signal !== undefined) { + return `was killed with ${signal} (${signalDescription})`; + } + + if (exitCode !== undefined) { + return `failed with exit code ${exitCode}`; + } + + return 'failed'; +}; + +export const makeError = ({ + stdout, + stderr, + all, + error, + signal, + exitCode, + command, + escapedCommand, + timedOut, + isCanceled, + killed, + parsed: {options: {timeout, cwd = process.cwd()}}, +}) => { + // `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`. + // We normalize them to `undefined` + exitCode = exitCode === null ? undefined : exitCode; + signal = signal === null ? undefined : signal; + const signalDescription = signal === undefined ? undefined : signalsByName[signal].description; + + const errorCode = error && error.code; + + const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); + const execaMessage = `Command ${prefix}: ${command}`; + const isError = Object.prototype.toString.call(error) === '[object Error]'; + const shortMessage = isError ? `${execaMessage}\n${error.message}` : execaMessage; + const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); + + if (isError) { + error.originalMessage = error.message; + error.message = message; + } else { + error = new Error(message); + } + + error.shortMessage = shortMessage; + error.command = command; + error.escapedCommand = escapedCommand; + error.exitCode = exitCode; + error.signal = signal; + error.signalDescription = signalDescription; + error.stdout = stdout; + error.stderr = stderr; + error.cwd = cwd; + + if (all !== undefined) { + error.all = all; + } + + if ('bufferedData' in error) { + delete error.bufferedData; + } + + error.failed = true; + error.timedOut = Boolean(timedOut); + error.isCanceled = isCanceled; + error.killed = killed && !timedOut; + + return error; +}; diff --git a/packages/core/src/libraries/execa/lib/kill.js b/packages/core/src/libraries/execa/lib/kill.js new file mode 100644 index 0000000..12ce0a1 --- /dev/null +++ b/packages/core/src/libraries/execa/lib/kill.js @@ -0,0 +1,102 @@ +import os from 'node:os'; +import {onExit} from 'signal-exit'; + +const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; + +// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior +export const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { + const killResult = kill(signal); + setKillTimeout(kill, signal, options, killResult); + return killResult; +}; + +const setKillTimeout = (kill, signal, options, killResult) => { + if (!shouldForceKill(signal, options, killResult)) { + return; + } + + const timeout = getForceKillAfterTimeout(options); + const t = setTimeout(() => { + kill('SIGKILL'); + }, timeout); + + // Guarded because there's no `.unref()` when `execa` is used in the renderer + // process in Electron. This cannot be tested since we don't run tests in + // Electron. + // istanbul ignore else + if (t.unref) { + t.unref(); + } +}; + +const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => isSigterm(signal) && forceKillAfterTimeout !== false && killResult; + +const isSigterm = signal => signal === os.constants.signals.SIGTERM + || (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); + +const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { + if (forceKillAfterTimeout === true) { + return DEFAULT_FORCE_KILL_TIMEOUT; + } + + if (!Number.isFinite(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { + throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); + } + + return forceKillAfterTimeout; +}; + +// `childProcess.cancel()` +export const spawnedCancel = (spawned, context) => { + const killResult = spawned.kill(); + + if (killResult) { + context.isCanceled = true; + } +}; + +const timeoutKill = (spawned, signal, reject) => { + spawned.kill(signal); + reject(Object.assign(new Error('Timed out'), {timedOut: true, signal})); +}; + +// `timeout` option handling +export const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { + if (timeout === 0 || timeout === undefined) { + return spawnedPromise; + } + + let timeoutId; + const timeoutPromise = new Promise((resolve, reject) => { + timeoutId = setTimeout(() => { + timeoutKill(spawned, killSignal, reject); + }, timeout); + }); + + const safeSpawnedPromise = spawnedPromise.finally(() => { + clearTimeout(timeoutId); + }); + + return Promise.race([timeoutPromise, safeSpawnedPromise]); +}; + +export const validateTimeout = ({timeout}) => { + if (timeout !== undefined && (!Number.isFinite(timeout) || timeout < 0)) { + throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); + } +}; + +// `cleanup` option handling +export const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { + if (!cleanup || detached) { + return timedPromise; + } + + const removeExitHandler = onExit(() => { + spawned.kill(); + }); + + return timedPromise.finally(() => { + removeExitHandler(); + }); +}; diff --git a/packages/core/src/libraries/execa/lib/pipe.js b/packages/core/src/libraries/execa/lib/pipe.js new file mode 100644 index 0000000..f26715d --- /dev/null +++ b/packages/core/src/libraries/execa/lib/pipe.js @@ -0,0 +1,42 @@ +import {createWriteStream} from 'node:fs'; +import {ChildProcess} from 'node:child_process'; +import {isWritableStream} from '../../is-stream'; + +const isExecaChildProcess = target => target instanceof ChildProcess && typeof target.then === 'function'; + +const pipeToTarget = (spawned, streamName, target) => { + if (typeof target === 'string') { + spawned[streamName].pipe(createWriteStream(target)); + return spawned; + } + + if (isWritableStream(target)) { + spawned[streamName].pipe(target); + return spawned; + } + + if (!isExecaChildProcess(target)) { + throw new TypeError('The second argument must be a string, a stream or an Execa child process.'); + } + + if (!isWritableStream(target.stdin)) { + throw new TypeError('The target child process\'s stdin must be available.'); + } + + spawned[streamName].pipe(target.stdin); + return target; +}; + +export const addPipeMethods = spawned => { + if (spawned.stdout !== null) { + spawned.pipeStdout = pipeToTarget.bind(undefined, spawned, 'stdout'); + } + + if (spawned.stderr !== null) { + spawned.pipeStderr = pipeToTarget.bind(undefined, spawned, 'stderr'); + } + + if (spawned.all !== undefined) { + spawned.pipeAll = pipeToTarget.bind(undefined, spawned, 'all'); + } +}; diff --git a/packages/core/src/libraries/execa/lib/promise.js b/packages/core/src/libraries/execa/lib/promise.js new file mode 100644 index 0000000..a4773f3 --- /dev/null +++ b/packages/core/src/libraries/execa/lib/promise.js @@ -0,0 +1,36 @@ +// eslint-disable-next-line unicorn/prefer-top-level-await +const nativePromisePrototype = (async () => {})().constructor.prototype; + +const descriptors = ['then', 'catch', 'finally'].map(property => [ + property, + Reflect.getOwnPropertyDescriptor(nativePromisePrototype, property), +]); + +// The return value is a mixin of `childProcess` and `Promise` +export const mergePromise = (spawned, promise) => { + for (const [property, descriptor] of descriptors) { + // Starting the main `promise` is deferred to avoid consuming streams + const value = typeof promise === 'function' + ? (...args) => Reflect.apply(descriptor.value, promise(), args) + : descriptor.value.bind(promise); + + Reflect.defineProperty(spawned, property, {...descriptor, value}); + } +}; + +// Use promises instead of `child_process` events +export const getSpawnedPromise = spawned => new Promise((resolve, reject) => { + spawned.on('exit', (exitCode, signal) => { + resolve({exitCode, signal}); + }); + + spawned.on('error', error => { + reject(error); + }); + + if (spawned.stdin) { + spawned.stdin.on('error', error => { + reject(error); + }); + } +}); diff --git a/packages/core/src/libraries/execa/lib/stdio.js b/packages/core/src/libraries/execa/lib/stdio.js new file mode 100644 index 0000000..e8c1132 --- /dev/null +++ b/packages/core/src/libraries/execa/lib/stdio.js @@ -0,0 +1,49 @@ +const aliases = ['stdin', 'stdout', 'stderr']; + +const hasAlias = options => aliases.some(alias => options[alias] !== undefined); + +export const normalizeStdio = options => { + if (!options) { + return; + } + + const {stdio} = options; + + if (stdio === undefined) { + return aliases.map(alias => options[alias]); + } + + if (hasAlias(options)) { + throw new Error(`It's not possible to provide \`stdio\` in combination with one of ${aliases.map(alias => `\`${alias}\``).join(', ')}`); + } + + if (typeof stdio === 'string') { + return stdio; + } + + if (!Array.isArray(stdio)) { + throw new TypeError(`Expected \`stdio\` to be of type \`string\` or \`Array\`, got \`${typeof stdio}\``); + } + + const length = Math.max(stdio.length, aliases.length); + return Array.from({length}, (value, index) => stdio[index]); +}; + +// `ipc` is pushed unless it is already present +export const normalizeStdioNode = options => { + const stdio = normalizeStdio(options); + + if (stdio === 'ipc') { + return 'ipc'; + } + + if (stdio === undefined || typeof stdio === 'string') { + return [stdio, stdio, stdio, 'ipc']; + } + + if (stdio.includes('ipc')) { + return stdio; + } + + return [...stdio, 'ipc']; +}; diff --git a/packages/core/src/libraries/execa/lib/stream.js b/packages/core/src/libraries/execa/lib/stream.js new file mode 100644 index 0000000..6912270 --- /dev/null +++ b/packages/core/src/libraries/execa/lib/stream.js @@ -0,0 +1,133 @@ +import {createReadStream, readFileSync} from 'node:fs'; +import {setTimeout} from 'node:timers/promises'; +import {isStream} from '../../is-stream'; +import getStream, {getStreamAsBuffer} from '../../get-stream'; +import mergeStream from 'merge-stream'; + +const validateInputOptions = input => { + if (input !== undefined) { + throw new TypeError('The `input` and `inputFile` options cannot be both set.'); + } +}; + +const getInputSync = ({input, inputFile}) => { + if (typeof inputFile !== 'string') { + return input; + } + + validateInputOptions(input); + return readFileSync(inputFile); +}; + +// `input` and `inputFile` option in sync mode +export const handleInputSync = options => { + const input = getInputSync(options); + + if (isStream(input)) { + throw new TypeError('The `input` option cannot be a stream in sync mode'); + } + + return input; +}; + +const getInput = ({input, inputFile}) => { + if (typeof inputFile !== 'string') { + return input; + } + + validateInputOptions(input); + return createReadStream(inputFile); +}; + +// `input` and `inputFile` option in async mode +export const handleInput = (spawned, options) => { + const input = getInput(options); + + if (input === undefined) { + return; + } + + if (isStream(input)) { + input.pipe(spawned.stdin); + } else { + spawned.stdin.end(input); + } +}; + +// `all` interleaves `stdout` and `stderr` +export const makeAllStream = (spawned, {all}) => { + if (!all || (!spawned.stdout && !spawned.stderr)) { + return; + } + + const mixed = mergeStream(); + + if (spawned.stdout) { + mixed.add(spawned.stdout); + } + + if (spawned.stderr) { + mixed.add(spawned.stderr); + } + + return mixed; +}; + +// On failure, `result.stdout|stderr|all` should contain the currently buffered stream +const getBufferedData = async (stream, streamPromise) => { + // When `buffer` is `false`, `streamPromise` is `undefined` and there is no buffered data to retrieve + if (!stream || streamPromise === undefined) { + return; + } + + // Wait for the `all` stream to receive the last chunk before destroying the stream + await setTimeout(0); + + stream.destroy(); + + try { + return await streamPromise; + } catch (error) { + return error.bufferedData; + } +}; + +const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => { + if (!stream || !buffer) { + return; + } + + // eslint-disable-next-line unicorn/text-encoding-identifier-case + if (encoding === 'utf8' || encoding === 'utf-8') { + return getStream(stream, {maxBuffer}); + } + + if (encoding === null || encoding === 'buffer') { + return getStreamAsBuffer(stream, {maxBuffer}); + } + + return applyEncoding(stream, maxBuffer, encoding); +}; + +const applyEncoding = async (stream, maxBuffer, encoding) => { + const buffer = await getStreamAsBuffer(stream, {maxBuffer}); + return buffer.toString(encoding); +}; + +// Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) +export const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { + const stdoutPromise = getStreamPromise(stdout, {encoding, buffer, maxBuffer}); + const stderrPromise = getStreamPromise(stderr, {encoding, buffer, maxBuffer}); + const allPromise = getStreamPromise(all, {encoding, buffer, maxBuffer: maxBuffer * 2}); + + try { + return await Promise.all([processDone, stdoutPromise, stderrPromise, allPromise]); + } catch (error) { + return Promise.all([ + {error, signal: error.signal, timedOut: error.timedOut}, + getBufferedData(stdout, stdoutPromise), + getBufferedData(stderr, stderrPromise), + getBufferedData(all, allPromise), + ]); + } +}; diff --git a/packages/core/src/libraries/execa/lib/verbose.js b/packages/core/src/libraries/execa/lib/verbose.js new file mode 100644 index 0000000..5f5490e --- /dev/null +++ b/packages/core/src/libraries/execa/lib/verbose.js @@ -0,0 +1,19 @@ +import {debuglog} from 'node:util'; +import process from 'node:process'; + +export const verboseDefault = debuglog('execa').enabled; + +const padField = (field, padding) => String(field).padStart(padding, '0'); + +const getTimestamp = () => { + const date = new Date(); + return `${padField(date.getHours(), 2)}:${padField(date.getMinutes(), 2)}:${padField(date.getSeconds(), 2)}.${padField(date.getMilliseconds(), 3)}`; +}; + +export const logCommand = (escapedCommand, {verbose}) => { + if (!verbose) { + return; + } + + process.stderr.write(`[${getTimestamp()}] ${escapedCommand}\n`); +}; diff --git a/packages/core/src/libraries/get-stream/array-buffer.js b/packages/core/src/libraries/get-stream/array-buffer.js new file mode 100644 index 0000000..a547405 --- /dev/null +++ b/packages/core/src/libraries/get-stream/array-buffer.js @@ -0,0 +1,84 @@ +import {getStreamContents} from './contents.js'; +import {noop, throwObjectStream, getLengthProp} from './utils.js'; + +export async function getStreamAsArrayBuffer(stream, options) { + return getStreamContents(stream, arrayBufferMethods, options); +} + +const initArrayBuffer = () => ({contents: new ArrayBuffer(0)}); + +const useTextEncoder = chunk => textEncoder.encode(chunk); +const textEncoder = new TextEncoder(); + +const useUint8Array = chunk => new Uint8Array(chunk); + +const useUint8ArrayWithOffset = chunk => new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength); + +const truncateArrayBufferChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); + +// `contents` is an increasingly growing `Uint8Array`. +const addArrayBufferChunk = (convertedChunk, {contents, length: previousLength}, length) => { + const newContents = hasArrayBufferResize() ? resizeArrayBuffer(contents, length) : resizeArrayBufferSlow(contents, length); + new Uint8Array(newContents).set(convertedChunk, previousLength); + return newContents; +}; + +// Without `ArrayBuffer.resize()`, `contents` size is always a power of 2. +// This means its last bytes are zeroes (not stream data), which need to be +// trimmed at the end with `ArrayBuffer.slice()`. +const resizeArrayBufferSlow = (contents, length) => { + if (length <= contents.byteLength) { + return contents; + } + + const arrayBuffer = new ArrayBuffer(getNewContentsLength(length)); + new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); + return arrayBuffer; +}; + +// With `ArrayBuffer.resize()`, `contents` size matches exactly the size of +// the stream data. It does not include extraneous zeroes to trim at the end. +// The underlying `ArrayBuffer` does allocate a number of bytes that is a power +// of 2, but those bytes are only visible after calling `ArrayBuffer.resize()`. +const resizeArrayBuffer = (contents, length) => { + if (length <= contents.maxByteLength) { + contents.resize(length); + return contents; + } + + const arrayBuffer = new ArrayBuffer(length, {maxByteLength: getNewContentsLength(length)}); + new Uint8Array(arrayBuffer).set(new Uint8Array(contents), 0); + return arrayBuffer; +}; + +// Retrieve the closest `length` that is both >= and a power of 2 +const getNewContentsLength = length => SCALE_FACTOR ** Math.ceil(Math.log(length) / Math.log(SCALE_FACTOR)); + +const SCALE_FACTOR = 2; + +const finalizeArrayBuffer = ({contents, length}) => hasArrayBufferResize() ? contents : contents.slice(0, length); + +// `ArrayBuffer.slice()` is slow. When `ArrayBuffer.resize()` is available +// (Node >=20.0.0, Safari >=16.4 and Chrome), we can use it instead. +// eslint-disable-next-line no-warning-comments +// TODO: remove after dropping support for Node 20. +// eslint-disable-next-line no-warning-comments +// TODO: use `ArrayBuffer.transferToFixedLength()` instead once it is available +const hasArrayBufferResize = () => 'resize' in ArrayBuffer.prototype; + +const arrayBufferMethods = { + init: initArrayBuffer, + convertChunk: { + string: useTextEncoder, + buffer: useUint8Array, + arrayBuffer: useUint8Array, + dataView: useUint8ArrayWithOffset, + typedArray: useUint8ArrayWithOffset, + others: throwObjectStream, + }, + getSize: getLengthProp, + truncateChunk: truncateArrayBufferChunk, + addChunk: addArrayBufferChunk, + getFinalChunk: noop, + finalize: finalizeArrayBuffer, +}; diff --git a/packages/core/src/libraries/get-stream/array.js b/packages/core/src/libraries/get-stream/array.js new file mode 100644 index 0000000..468bad1 --- /dev/null +++ b/packages/core/src/libraries/get-stream/array.js @@ -0,0 +1,32 @@ +import {getStreamContents} from './contents.js'; +import {identity, noop, getContentsProp} from './utils.js'; + +export async function getStreamAsArray(stream, options) { + return getStreamContents(stream, arrayMethods, options); +} + +const initArray = () => ({contents: []}); + +const increment = () => 1; + +const addArrayChunk = (convertedChunk, {contents}) => { + contents.push(convertedChunk); + return contents; +}; + +const arrayMethods = { + init: initArray, + convertChunk: { + string: identity, + buffer: identity, + arrayBuffer: identity, + dataView: identity, + typedArray: identity, + others: identity, + }, + getSize: increment, + truncateChunk: noop, + addChunk: addArrayChunk, + getFinalChunk: noop, + finalize: getContentsProp, +}; diff --git a/packages/core/src/libraries/get-stream/buffer.js b/packages/core/src/libraries/get-stream/buffer.js new file mode 100644 index 0000000..7d22d78 --- /dev/null +++ b/packages/core/src/libraries/get-stream/buffer.js @@ -0,0 +1,20 @@ +import {getStreamAsArrayBuffer} from './array-buffer.js'; + +export async function getStreamAsBuffer(stream, options) { + if (!('Buffer' in globalThis)) { + throw new Error('getStreamAsBuffer() is only supported in Node.js'); + } + + try { + return arrayBufferToNodeBuffer(await getStreamAsArrayBuffer(stream, options)); + } catch (error) { + if (error.bufferedData !== undefined) { + error.bufferedData = arrayBufferToNodeBuffer(error.bufferedData); + } + + throw error; + } +} + +// eslint-disable-next-line n/prefer-global/buffer +const arrayBufferToNodeBuffer = arrayBuffer => globalThis.Buffer.from(arrayBuffer); diff --git a/packages/core/src/libraries/get-stream/contents.js b/packages/core/src/libraries/get-stream/contents.js new file mode 100644 index 0000000..2ca36f2 --- /dev/null +++ b/packages/core/src/libraries/get-stream/contents.js @@ -0,0 +1,101 @@ +export const getStreamContents = async (stream, {init, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, finalize}, {maxBuffer = Number.POSITIVE_INFINITY} = {}) => { + if (!isAsyncIterable(stream)) { + throw new Error('The first argument must be a Readable, a ReadableStream, or an async iterable.'); + } + + const state = init(); + state.length = 0; + + try { + for await (const chunk of stream) { + const chunkType = getChunkType(chunk); + const convertedChunk = convertChunk[chunkType](chunk, state); + appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); + } + + appendFinalChunk({state, convertChunk, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}); + return finalize(state); + } catch (error) { + error.bufferedData = finalize(state); + throw error; + } +}; + +const appendFinalChunk = ({state, getSize, truncateChunk, addChunk, getFinalChunk, maxBuffer}) => { + const convertedChunk = getFinalChunk(state); + if (convertedChunk !== undefined) { + appendChunk({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}); + } +}; + +const appendChunk = ({convertedChunk, state, getSize, truncateChunk, addChunk, maxBuffer}) => { + const chunkSize = getSize(convertedChunk); + const newLength = state.length + chunkSize; + + if (newLength <= maxBuffer) { + addNewChunk(convertedChunk, state, addChunk, newLength); + return; + } + + const truncatedChunk = truncateChunk(convertedChunk, maxBuffer - state.length); + + if (truncatedChunk !== undefined) { + addNewChunk(truncatedChunk, state, addChunk, maxBuffer); + } + + throw new MaxBufferError(); +}; + +const addNewChunk = (convertedChunk, state, addChunk, newLength) => { + state.contents = addChunk(convertedChunk, state, newLength); + state.length = newLength; +}; + +const isAsyncIterable = stream => typeof stream === 'object' && stream !== null && typeof stream[Symbol.asyncIterator] === 'function'; + +const getChunkType = chunk => { + const typeOfChunk = typeof chunk; + + if (typeOfChunk === 'string') { + return 'string'; + } + + if (typeOfChunk !== 'object' || chunk === null) { + return 'others'; + } + + // eslint-disable-next-line n/prefer-global/buffer + if (globalThis.Buffer?.isBuffer(chunk)) { + return 'buffer'; + } + + const prototypeName = objectToString.call(chunk); + + if (prototypeName === '[object ArrayBuffer]') { + return 'arrayBuffer'; + } + + if (prototypeName === '[object DataView]') { + return 'dataView'; + } + + if ( + Number.isInteger(chunk.byteLength) + && Number.isInteger(chunk.byteOffset) + && objectToString.call(chunk.buffer) === '[object ArrayBuffer]' + ) { + return 'typedArray'; + } + + return 'others'; +}; + +const {toString: objectToString} = Object.prototype; + +export class MaxBufferError extends Error { + name = 'MaxBufferError'; + + constructor() { + super('maxBuffer exceeded'); + } +} diff --git a/packages/core/src/libraries/get-stream/index.js b/packages/core/src/libraries/get-stream/index.js new file mode 100644 index 0000000..43c2dd4 --- /dev/null +++ b/packages/core/src/libraries/get-stream/index.js @@ -0,0 +1,5 @@ +export {getStreamAsArray} from './array.js'; +export {getStreamAsArrayBuffer} from './array-buffer.js'; +export {getStreamAsBuffer} from './buffer.js'; +export {getStreamAsString as default} from './string.js'; +export {MaxBufferError} from './contents.js'; diff --git a/packages/core/src/libraries/get-stream/string.js b/packages/core/src/libraries/get-stream/string.js new file mode 100644 index 0000000..90f94b9 --- /dev/null +++ b/packages/core/src/libraries/get-stream/string.js @@ -0,0 +1,36 @@ +import {getStreamContents} from './contents.js'; +import {identity, getContentsProp, throwObjectStream, getLengthProp} from './utils.js'; + +export async function getStreamAsString(stream, options) { + return getStreamContents(stream, stringMethods, options); +} + +const initString = () => ({contents: '', textDecoder: new TextDecoder()}); + +const useTextDecoder = (chunk, {textDecoder}) => textDecoder.decode(chunk, {stream: true}); + +const addStringChunk = (convertedChunk, {contents}) => contents + convertedChunk; + +const truncateStringChunk = (convertedChunk, chunkSize) => convertedChunk.slice(0, chunkSize); + +const getFinalStringChunk = ({textDecoder}) => { + const finalChunk = textDecoder.decode(); + return finalChunk === '' ? undefined : finalChunk; +}; + +const stringMethods = { + init: initString, + convertChunk: { + string: identity, + buffer: useTextDecoder, + arrayBuffer: useTextDecoder, + dataView: useTextDecoder, + typedArray: useTextDecoder, + others: throwObjectStream, + }, + getSize: getLengthProp, + truncateChunk: truncateStringChunk, + addChunk: addStringChunk, + getFinalChunk: getFinalStringChunk, + finalize: getContentsProp, +}; diff --git a/packages/core/src/libraries/get-stream/utils.js b/packages/core/src/libraries/get-stream/utils.js new file mode 100644 index 0000000..af8d5e2 --- /dev/null +++ b/packages/core/src/libraries/get-stream/utils.js @@ -0,0 +1,11 @@ +export const identity = value => value; + +export const noop = () => undefined; + +export const getContentsProp = ({contents}) => contents; + +export const throwObjectStream = chunk => { + throw new Error(`Streams in object mode are not supported: ${String(chunk)}`); +}; + +export const getLengthProp = convertedChunk => convertedChunk.length; diff --git a/packages/core/src/libraries/human-signals/core.js b/packages/core/src/libraries/human-signals/core.js new file mode 100644 index 0000000..e083d8f --- /dev/null +++ b/packages/core/src/libraries/human-signals/core.js @@ -0,0 +1,275 @@ +/* eslint-disable max-lines */ +// List of known process signals with information about them +export const SIGNALS = [ + { + name: 'SIGHUP', + number: 1, + action: 'terminate', + description: 'Terminal closed', + standard: 'posix', + }, + { + name: 'SIGINT', + number: 2, + action: 'terminate', + description: 'User interruption with CTRL-C', + standard: 'ansi', + }, + { + name: 'SIGQUIT', + number: 3, + action: 'core', + description: 'User interruption with CTRL-\\', + standard: 'posix', + }, + { + name: 'SIGILL', + number: 4, + action: 'core', + description: 'Invalid machine instruction', + standard: 'ansi', + }, + { + name: 'SIGTRAP', + number: 5, + action: 'core', + description: 'Debugger breakpoint', + standard: 'posix', + }, + { + name: 'SIGABRT', + number: 6, + action: 'core', + description: 'Aborted', + standard: 'ansi', + }, + { + name: 'SIGIOT', + number: 6, + action: 'core', + description: 'Aborted', + standard: 'bsd', + }, + { + name: 'SIGBUS', + number: 7, + action: 'core', + description: + 'Bus error due to misaligned, non-existing address or paging error', + standard: 'bsd', + }, + { + name: 'SIGEMT', + number: 7, + action: 'terminate', + description: 'Command should be emulated but is not implemented', + standard: 'other', + }, + { + name: 'SIGFPE', + number: 8, + action: 'core', + description: 'Floating point arithmetic error', + standard: 'ansi', + }, + { + name: 'SIGKILL', + number: 9, + action: 'terminate', + description: 'Forced termination', + standard: 'posix', + forced: true, + }, + { + name: 'SIGUSR1', + number: 10, + action: 'terminate', + description: 'Application-specific signal', + standard: 'posix', + }, + { + name: 'SIGSEGV', + number: 11, + action: 'core', + description: 'Segmentation fault', + standard: 'ansi', + }, + { + name: 'SIGUSR2', + number: 12, + action: 'terminate', + description: 'Application-specific signal', + standard: 'posix', + }, + { + name: 'SIGPIPE', + number: 13, + action: 'terminate', + description: 'Broken pipe or socket', + standard: 'posix', + }, + { + name: 'SIGALRM', + number: 14, + action: 'terminate', + description: 'Timeout or timer', + standard: 'posix', + }, + { + name: 'SIGTERM', + number: 15, + action: 'terminate', + description: 'Termination', + standard: 'ansi', + }, + { + name: 'SIGSTKFLT', + number: 16, + action: 'terminate', + description: 'Stack is empty or overflowed', + standard: 'other', + }, + { + name: 'SIGCHLD', + number: 17, + action: 'ignore', + description: 'Child process terminated, paused or unpaused', + standard: 'posix', + }, + { + name: 'SIGCLD', + number: 17, + action: 'ignore', + description: 'Child process terminated, paused or unpaused', + standard: 'other', + }, + { + name: 'SIGCONT', + number: 18, + action: 'unpause', + description: 'Unpaused', + standard: 'posix', + forced: true, + }, + { + name: 'SIGSTOP', + number: 19, + action: 'pause', + description: 'Paused', + standard: 'posix', + forced: true, + }, + { + name: 'SIGTSTP', + number: 20, + action: 'pause', + description: 'Paused using CTRL-Z or "suspend"', + standard: 'posix', + }, + { + name: 'SIGTTIN', + number: 21, + action: 'pause', + description: 'Background process cannot read terminal input', + standard: 'posix', + }, + { + name: 'SIGBREAK', + number: 21, + action: 'terminate', + description: 'User interruption with CTRL-BREAK', + standard: 'other', + }, + { + name: 'SIGTTOU', + number: 22, + action: 'pause', + description: 'Background process cannot write to terminal output', + standard: 'posix', + }, + { + name: 'SIGURG', + number: 23, + action: 'ignore', + description: 'Socket received out-of-band data', + standard: 'bsd', + }, + { + name: 'SIGXCPU', + number: 24, + action: 'core', + description: 'Process timed out', + standard: 'bsd', + }, + { + name: 'SIGXFSZ', + number: 25, + action: 'core', + description: 'File too big', + standard: 'bsd', + }, + { + name: 'SIGVTALRM', + number: 26, + action: 'terminate', + description: 'Timeout or timer', + standard: 'bsd', + }, + { + name: 'SIGPROF', + number: 27, + action: 'terminate', + description: 'Timeout or timer', + standard: 'bsd', + }, + { + name: 'SIGWINCH', + number: 28, + action: 'ignore', + description: 'Terminal window size changed', + standard: 'bsd', + }, + { + name: 'SIGIO', + number: 29, + action: 'terminate', + description: 'I/O is available', + standard: 'other', + }, + { + name: 'SIGPOLL', + number: 29, + action: 'terminate', + description: 'Watched event', + standard: 'other', + }, + { + name: 'SIGINFO', + number: 29, + action: 'ignore', + description: 'Request for process information', + standard: 'other', + }, + { + name: 'SIGPWR', + number: 30, + action: 'terminate', + description: 'Device running out of power', + standard: 'systemv', + }, + { + name: 'SIGSYS', + number: 31, + action: 'core', + description: 'Invalid system call', + standard: 'other', + }, + { + name: 'SIGUNUSED', + number: 31, + action: 'terminate', + description: 'Invalid system call', + standard: 'other', + }, +] +/* eslint-enable max-lines */ diff --git a/packages/core/src/libraries/human-signals/index.js b/packages/core/src/libraries/human-signals/index.js new file mode 100644 index 0000000..fb6e64b --- /dev/null +++ b/packages/core/src/libraries/human-signals/index.js @@ -0,0 +1,70 @@ +import { constants } from 'node:os' + +import { SIGRTMAX } from './realtime.js' +import { getSignals } from './signals.js' + +// Retrieve `signalsByName`, an object mapping signal name to signal properties. +// We make sure the object is sorted by `number`. +const getSignalsByName = () => { + const signals = getSignals() + return Object.fromEntries(signals.map(getSignalByName)) +} + +const getSignalByName = ({ + name, + number, + description, + supported, + action, + forced, + standard, +}) => [name, { name, number, description, supported, action, forced, standard }] + +export const signalsByName = getSignalsByName() + +// Retrieve `signalsByNumber`, an object mapping signal number to signal +// properties. +// We make sure the object is sorted by `number`. +const getSignalsByNumber = () => { + const signals = getSignals() + const length = SIGRTMAX + 1 + const signalsA = Array.from({ length }, (value, number) => + getSignalByNumber(number, signals), + ) + return Object.assign({}, ...signalsA) +} + +const getSignalByNumber = (number, signals) => { + const signal = findSignalByNumber(number, signals) + + if (signal === undefined) { + return {} + } + + const { name, description, supported, action, forced, standard } = signal + return { + [number]: { + name, + number, + description, + supported, + action, + forced, + standard, + }, + } +} + +// Several signals might end up sharing the same number because of OS-specific +// numbers, in which case those prevail. +const findSignalByNumber = (number, signals) => { + const signal = signals.find(({ name }) => constants.signals[name] === number) + + if (signal !== undefined) { + return signal + } + + return signals.find((signalA) => signalA.number === number) +} + +export const signalsByNumber = getSignalsByNumber() diff --git a/packages/core/src/libraries/human-signals/realtime.js b/packages/core/src/libraries/human-signals/realtime.js new file mode 100644 index 0000000..1825d08 --- /dev/null +++ b/packages/core/src/libraries/human-signals/realtime.js @@ -0,0 +1,16 @@ +// List of realtime signals with information about them +export const getRealtimeSignals = () => { + const length = SIGRTMAX - SIGRTMIN + 1 + return Array.from({ length }, getRealtimeSignal) +} + +const getRealtimeSignal = (value, index) => ({ + name: `SIGRT${index + 1}`, + number: SIGRTMIN + index, + action: 'terminate', + description: 'Application-specific signal (realtime)', + standard: 'posix', +}) + +const SIGRTMIN = 34 +export const SIGRTMAX = 64 diff --git a/packages/core/src/libraries/human-signals/signals.js b/packages/core/src/libraries/human-signals/signals.js new file mode 100644 index 0000000..d76382b --- /dev/null +++ b/packages/core/src/libraries/human-signals/signals.js @@ -0,0 +1,34 @@ +import { constants } from 'node:os' + +import { SIGNALS } from './core.js' +import { getRealtimeSignals } from './realtime.js' + +// Retrieve list of know signals (including realtime) with information about +// them +export const getSignals = () => { + const realtimeSignals = getRealtimeSignals() + const signals = [...SIGNALS, ...realtimeSignals].map(normalizeSignal) + return signals +} + +// Normalize signal: +// - `number`: signal numbers are OS-specific. This is taken into account by +// `os.constants.signals`. However we provide a default `number` since some +// signals are not defined for some OS. +// - `forced`: set default to `false` +// - `supported`: set value +const normalizeSignal = ({ + name, + number: defaultNumber, + description, + action, + forced = false, + standard, +}) => { + const { + signals: { [name]: constantSignal }, + } = constants + const supported = constantSignal !== undefined + const number = supported ? constantSignal : defaultNumber + return { name, number, description, supported, action, forced, standard } +} diff --git a/packages/core/src/libraries/is-stream/index.js b/packages/core/src/libraries/is-stream/index.js new file mode 100644 index 0000000..887e601 --- /dev/null +++ b/packages/core/src/libraries/is-stream/index.js @@ -0,0 +1,29 @@ +export function isStream(stream) { + return stream !== null + && typeof stream === 'object' + && typeof stream.pipe === 'function'; +} + +export function isWritableStream(stream) { + return isStream(stream) + && stream.writable !== false + && typeof stream._write === 'function' + && typeof stream._writableState === 'object'; +} + +export function isReadableStream(stream) { + return isStream(stream) + && stream.readable !== false + && typeof stream._read === 'function' + && typeof stream._readableState === 'object'; +} + +export function isDuplexStream(stream) { + return isWritableStream(stream) + && isReadableStream(stream); +} + +export function isTransformStream(stream) { + return isDuplexStream(stream) + && typeof stream._transform === 'function'; +} diff --git a/packages/core/src/libraries/lowdb/adapters/Memory.js b/packages/core/src/libraries/lowdb/adapters/Memory.js new file mode 100644 index 0000000..798cd36 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/Memory.js @@ -0,0 +1,24 @@ +export class Memory { + #data = null + + read() { + return Promise.resolve(this.#data) + } + + write(obj) { + this.#data = obj + return Promise.resolve() + } +} + +export class MemorySync { + #data = null + + read() { + return this.#data || null + } + + write(obj) { + this.#data = obj + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/DataFile.js b/packages/core/src/libraries/lowdb/adapters/node/DataFile.js new file mode 100644 index 0000000..0506e0c --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/DataFile.js @@ -0,0 +1,51 @@ +import { TextFile, TextFileSync } from "./TextFile.js" + +export class DataFile { + #adapter + #parse + #stringify + + constructor(filename, { parse, stringify }) { + this.#adapter = new TextFile(filename) + this.#parse = parse + this.#stringify = stringify + } + + async read() { + const data = await this.#adapter.read() + if (data === null) { + return null + } else { + return this.#parse(data) + } + } + + write(obj) { + return this.#adapter.write(this.#stringify(obj)) + } +} + +export class DataFileSync { + #adapter + #parse + #stringify + + constructor(filename, { parse, stringify }) { + this.#adapter = new TextFileSync(filename) + this.#parse = parse + this.#stringify = stringify + } + + read() { + const data = this.#adapter.read() + if (data === null) { + return null + } else { + return this.#parse(data) + } + } + + write(obj) { + this.#adapter.write(this.#stringify(obj)) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js b/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js new file mode 100644 index 0000000..8811a87 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/JSONFile.js @@ -0,0 +1,19 @@ +import { DataFile, DataFileSync } from "./DataFile.js"; + +export class JSONFile extends DataFile { + constructor(filename) { + super(filename, { + parse: JSON.parse, + stringify: (data) => JSON.stringify(data, null, 2), + }); + } +} + +export class JSONFileSync extends DataFileSync { + constructor(filename) { + super(filename, { + parse: JSON.parse, + stringify: (data) => JSON.stringify(data, null, 2), + }); + } +} diff --git a/packages/core/src/libraries/lowdb/adapters/node/TextFile.js b/packages/core/src/libraries/lowdb/adapters/node/TextFile.js new file mode 100644 index 0000000..b1f3321 --- /dev/null +++ b/packages/core/src/libraries/lowdb/adapters/node/TextFile.js @@ -0,0 +1,65 @@ +import { readFileSync, renameSync, writeFileSync } from "node:fs" +import { readFile } from "node:fs/promises" +import path from "node:path" + +import { Writer } from "../../steno" + +export class TextFile { + #filename + #writer + + constructor(filename) { + this.#filename = filename + this.#writer = new Writer(filename) + } + + async read() { + let data + + try { + data = await readFile(this.#filename, "utf-8") + } catch (e) { + if (e.code === "ENOENT") { + return null + } + throw e + } + + return data + } + + write(str) { + return this.#writer.write(str) + } +} + +export class TextFileSync { + #tempFilename + #filename + + constructor(filename) { + this.#filename = filename + const f = filename.toString() + this.#tempFilename = path.join(path.dirname(f), `.${path.basename(f)}.tmp`) + } + + read() { + let data + + try { + data = readFileSync(this.#filename, "utf-8") + } catch (e) { + if (e.code === "ENOENT") { + return null + } + throw e + } + + return data + } + + write(str) { + writeFileSync(this.#tempFilename, str) + renameSync(this.#tempFilename, this.#filename) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/core/Low.js b/packages/core/src/libraries/lowdb/core/Low.js new file mode 100644 index 0000000..f0b76e5 --- /dev/null +++ b/packages/core/src/libraries/lowdb/core/Low.js @@ -0,0 +1,48 @@ +function checkArgs(adapter, defaultData) { + if (adapter === undefined) throw new Error("lowdb: missing adapter") + if (defaultData === undefined) throw new Error("lowdb: missing default data") +} + +export class Low { + constructor(adapter, defaultData) { + checkArgs(adapter, defaultData) + this.adapter = adapter + this.data = defaultData + } + + async read() { + const data = await this.adapter.read() + if (data) this.data = data + } + + async write() { + if (this.data) await this.adapter.write(this.data) + } + + async update(fn) { + fn(this.data) + await this.write() + } +} + +export class LowSync { + constructor(adapter, defaultData) { + checkArgs(adapter, defaultData) + this.adapter = adapter + this.data = defaultData + } + + read() { + const data = this.adapter.read() + if (data) this.data = data + } + + write() { + if (this.data) this.adapter.write(this.data) + } + + update(fn) { + fn(this.data) + this.write() + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/presets/node.js b/packages/core/src/libraries/lowdb/presets/node.js new file mode 100644 index 0000000..e8526fb --- /dev/null +++ b/packages/core/src/libraries/lowdb/presets/node.js @@ -0,0 +1,23 @@ +import { Memory, MemorySync } from "../adapters/Memory.js" +import { JSONFile, JSONFileSync } from "../adapters/node/JSONFile.js" +import { Low, LowSync } from "../core/Low.js" + +export async function JSONFilePreset(filename, defaultData) { + const adapter = process.env.NODE_ENV === "test" ? new Memory() : new JSONFile(filename) + + const db = new Low(adapter, defaultData) + + await db.read() + + return db +} + +export function JSONFileSyncPreset(filename, defaultData) { + const adapter = process.env.NODE_ENV === "test" ? new MemorySync() : new JSONFileSync(filename) + + const db = new LowSync(adapter, defaultData) + + db.read() + + return db +} \ No newline at end of file diff --git a/packages/core/src/libraries/lowdb/steno/index.js b/packages/core/src/libraries/lowdb/steno/index.js new file mode 100644 index 0000000..d0c5558 --- /dev/null +++ b/packages/core/src/libraries/lowdb/steno/index.js @@ -0,0 +1,98 @@ +import { rename, writeFile } from "node:fs/promises" +import { basename, dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +// Returns a temporary file +// Example: for /some/file will return /some/.file.tmp +function getTempFilename(file) { + const f = file instanceof URL ? fileURLToPath(file) : file.toString() + return join(dirname(f), `.${basename(f)}.tmp`) +} + +// Retries an asynchronous operation with a delay between retries and a maximum retry count +async function retryAsyncOperation(fn, maxRetries, delayMs) { + for (let i = 0; i < maxRetries; i++) { + try { + return await fn() + } catch (error) { + if (i < maxRetries - 1) { + await new Promise(resolve => setTimeout(resolve, delayMs)) + } else { + throw error // Rethrow the error if max retries reached + } + } + } +} + +export class Writer { + #filename + #tempFilename + #locked = false + #prev = null + #next = null + #nextPromise = null + #nextData = null + + // File is locked, add data for later + #add(data) { + // Only keep most recent data + this.#nextData = data + + // Create a singleton promise to resolve all next promises once next data is written + this.#nextPromise ||= new Promise((resolve, reject) => { + this.#next = [resolve, reject] + }) + + // Return a promise that will resolve at the same time as next promise + return new Promise((resolve, reject) => { + this.#nextPromise?.then(resolve).catch(reject) + }) + } + + // File isn't locked, write data + async #write(data) { + // Lock file + this.#locked = true + try { + // Atomic write + await writeFile(this.#tempFilename, data, "utf-8") + await retryAsyncOperation( + async () => { + await rename(this.#tempFilename, this.#filename) + }, + 10, + 100 + ) + + // Call resolve + this.#prev?.[0]() + } catch (err) { + // Call reject + if (err instanceof Error) { + this.#prev?.[1](err) + } + throw err + } finally { + // Unlock file + this.#locked = false + + this.#prev = this.#next + this.#next = this.#nextPromise = null + + if (this.#nextData !== null) { + const nextData = this.#nextData + this.#nextData = null + await this.write(nextData) + } + } + } + + constructor(filename) { + this.#filename = filename + this.#tempFilename = getTempFilename(filename) + } + + async write(data) { + return this.#locked ? this.#add(data) : this.#write(data) + } +} \ No newline at end of file diff --git a/packages/core/src/libraries/mimic-function/index.js b/packages/core/src/libraries/mimic-function/index.js new file mode 100644 index 0000000..61e6701 --- /dev/null +++ b/packages/core/src/libraries/mimic-function/index.js @@ -0,0 +1,71 @@ +const copyProperty = (to, from, property, ignoreNonConfigurable) => { + // `Function#length` should reflect the parameters of `to` not `from` since we keep its body. + // `Function#prototype` is non-writable and non-configurable so can never be modified. + if (property === 'length' || property === 'prototype') { + return; + } + + // `Function#arguments` and `Function#caller` should not be copied. They were reported to be present in `Reflect.ownKeys` for some devices in React Native (#41), so we explicitly ignore them here. + if (property === 'arguments' || property === 'caller') { + return; + } + + const toDescriptor = Object.getOwnPropertyDescriptor(to, property); + const fromDescriptor = Object.getOwnPropertyDescriptor(from, property); + + if (!canCopyProperty(toDescriptor, fromDescriptor) && ignoreNonConfigurable) { + return; + } + + Object.defineProperty(to, property, fromDescriptor); +}; + +// `Object.defineProperty()` throws if the property exists, is not configurable and either: +// - one its descriptors is changed +// - it is non-writable and its value is changed +const canCopyProperty = function (toDescriptor, fromDescriptor) { + return toDescriptor === undefined || toDescriptor.configurable || ( + toDescriptor.writable === fromDescriptor.writable + && toDescriptor.enumerable === fromDescriptor.enumerable + && toDescriptor.configurable === fromDescriptor.configurable + && (toDescriptor.writable || toDescriptor.value === fromDescriptor.value) + ); +}; + +const changePrototype = (to, from) => { + const fromPrototype = Object.getPrototypeOf(from); + if (fromPrototype === Object.getPrototypeOf(to)) { + return; + } + + Object.setPrototypeOf(to, fromPrototype); +}; + +const wrappedToString = (withName, fromBody) => `/* Wrapped ${withName}*/\n${fromBody}`; + +const toStringDescriptor = Object.getOwnPropertyDescriptor(Function.prototype, 'toString'); +const toStringName = Object.getOwnPropertyDescriptor(Function.prototype.toString, 'name'); + +// We call `from.toString()` early (not lazily) to ensure `from` can be garbage collected. +// We use `bind()` instead of a closure for the same reason. +// Calling `from.toString()` early also allows caching it in case `to.toString()` is called several times. +const changeToString = (to, from, name) => { + const withName = name === '' ? '' : `with ${name.trim()}() `; + const newToString = wrappedToString.bind(null, withName, from.toString()); + // Ensure `to.toString.toString` is non-enumerable and has the same `same` + Object.defineProperty(newToString, 'name', toStringName); + Object.defineProperty(to, 'toString', { ...toStringDescriptor, value: newToString }); +}; + +export default function mimicFunction(to, from, { ignoreNonConfigurable = false } = {}) { + const { name } = to; + + for (const property of Reflect.ownKeys(from)) { + copyProperty(to, from, property, ignoreNonConfigurable); + } + + changePrototype(to, from); + changeToString(to, from, name); + + return to; +} \ No newline at end of file diff --git a/packages/core/src/libraries/npm-run-path/index.js b/packages/core/src/libraries/npm-run-path/index.js new file mode 100644 index 0000000..782a96a --- /dev/null +++ b/packages/core/src/libraries/npm-run-path/index.js @@ -0,0 +1,51 @@ +import process from 'node:process'; +import path from 'node:path'; +import url from 'node:url'; + +function pathKey(options = {}) { + const { + env = process.env, + platform = process.platform + } = options; + + if (platform !== 'win32') { + return 'PATH'; + } + + return Object.keys(env).reverse().find(key => key.toUpperCase() === 'PATH') || 'Path'; +} + +export function npmRunPath(options = {}) { + const { + cwd = process.cwd(), + path: path_ = process.env[pathKey()], + execPath = process.execPath, + } = options; + + let previous; + const execPathString = execPath instanceof URL ? url.fileURLToPath(execPath) : execPath; + const cwdString = cwd instanceof URL ? url.fileURLToPath(cwd) : cwd; + let cwdPath = path.resolve(cwdString); + const result = []; + + while (previous !== cwdPath) { + result.push(path.join(cwdPath, 'node_modules/.bin')); + previous = cwdPath; + cwdPath = path.resolve(cwdPath, '..'); + } + + // Ensure the running `node` binary is used. + result.push(path.resolve(cwdString, execPathString, '..')); + + return [...result, path_].join(path.delimiter); +} + +export function npmRunPathEnv({ env = process.env, ...options } = {}) { + env = { ...env }; + + const path = pathKey({ env }); + options.path = env[path]; + env[path] = npmRunPath(options); + + return env; +} diff --git a/packages/core/src/libraries/onetime/index.js b/packages/core/src/libraries/onetime/index.js new file mode 100644 index 0000000..880e94d --- /dev/null +++ b/packages/core/src/libraries/onetime/index.js @@ -0,0 +1,41 @@ +import mimicFunction from '../mimic-function'; + +const calledFunctions = new WeakMap(); + +const onetime = (function_, options = {}) => { + if (typeof function_ !== 'function') { + throw new TypeError('Expected a function'); + } + + let returnValue; + let callCount = 0; + const functionName = function_.displayName || function_.name || ''; + + const onetime = function (...arguments_) { + calledFunctions.set(onetime, ++callCount); + + if (callCount === 1) { + returnValue = function_.apply(this, arguments_); + function_ = undefined; + } else if (options.throw === true) { + throw new Error(`Function \`${functionName}\` can only be called once`); + } + + return returnValue; + }; + + mimicFunction(onetime, function_); + calledFunctions.set(onetime, callCount); + + return onetime; +}; + +onetime.callCount = function_ => { + if (!calledFunctions.has(function_)) { + throw new Error(`The given function \`${function_.name}\` is not wrapped by the \`onetime\` package`); + } + + return calledFunctions.get(function_); +}; + +export default onetime; diff --git a/packages/core/src/libraries/strip-final-newline/index.js b/packages/core/src/libraries/strip-final-newline/index.js new file mode 100644 index 0000000..a63ed26 --- /dev/null +++ b/packages/core/src/libraries/strip-final-newline/index.js @@ -0,0 +1,26 @@ +export default function stripFinalNewline(input) { + if (typeof input === 'string') { + return stripFinalNewlineString(input); + } + + if (!(ArrayBuffer.isView(input) && input.BYTES_PER_ELEMENT === 1)) { + throw new Error('Input must be a string or a Uint8Array'); + } + + return stripFinalNewlineBinary(input); +} + +const stripFinalNewlineString = input => + input.at(-1) === LF + ? input.slice(0, input.at(-2) === CR ? -2 : -1) + : input; + +const stripFinalNewlineBinary = input => + input.at(-1) === LF_BINARY + ? input.subarray(0, input.at(-2) === CR_BINARY ? -2 : -1) + : input; + +const LF = '\n'; +const LF_BINARY = LF.codePointAt(0); +const CR = '\r'; +const CR_BINARY = CR.codePointAt(0); diff --git a/packages/core/src/logger.js b/packages/core/src/logger.js new file mode 100644 index 0000000..1f3d6d1 --- /dev/null +++ b/packages/core/src/logger.js @@ -0,0 +1,40 @@ +import winston from "winston" +import colors from "cli-color" + +const servicesToColor = { + "CORE": { + color: "whiteBright", + background: "bgBlackBright", + }, + "INSTALL": { + color: "whiteBright", + background: "bgBlueBright", + }, +} + +const paintText = (level, service, ...args) => { + let { color, background } = servicesToColor[service ?? "CORE"] ?? servicesToColor["CORE"] + + if (level === "error") { + color = "whiteBright" + background = "bgRedBright" + } + + return colors[background][color](...args) +} + +const format = winston.format.printf(({ timestamp, service = "CORE", level, message, }) => { + return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}` +}) + +export default winston.createLogger({ + format: winston.format.combine( + winston.format.timestamp(), + format + ), + transports: [ + new winston.transports.Console(), + //new winston.transports.File({ filename: "error.log", level: "error" }), + //new winston.transports.File({ filename: "combined.log" }), + ], +}) \ No newline at end of file diff --git a/packages/core/src/manifest/libraries.js b/packages/core/src/manifest/libraries.js new file mode 100644 index 0000000..edab737 --- /dev/null +++ b/packages/core/src/manifest/libraries.js @@ -0,0 +1,23 @@ +import PublicInternalLibraries from "./libs" + +const isAClass = (x) => x && typeof x === "function" && x.prototype && typeof x.prototype.constructor === "function" + +export default async (dependencies, bindCtx) => { + const libraries = {} + + for await (const lib of dependencies) { + if (PublicInternalLibraries[lib]) { + if (typeof PublicInternalLibraries[lib] === "function" && isAClass(PublicInternalLibraries[lib])) { + libraries[lib] = new PublicInternalLibraries[lib](bindCtx) + + if (libraries[lib].initialize) { + await libraries[lib].initialize() + } + } else { + libraries[lib] = PublicInternalLibraries[lib] + } + } + } + + return libraries +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/auth/index.js b/packages/core/src/manifest/libs/auth/index.js new file mode 100644 index 0000000..2f1371a --- /dev/null +++ b/packages/core/src/manifest/libs/auth/index.js @@ -0,0 +1,54 @@ +import open from "open" +import axios from "axios" +import ManifestAuthDB from "../../../classes/ManifestAuthDB" + +export default class Auth { + constructor(ctx) { + this.manifest = ctx.manifest + } + + async get() { + const storagedData = await ManifestAuthDB.get(this.manifest.id) + + if (storagedData && this.manifest.authService) { + if (!this.manifest.authService.getter) { + return storagedData + } + + const result = await axios({ + method: "POST", + url: this.manifest.authService.getter, + headers: { + "Content-Type": "application/json", + }, + data: { + auth_data: storagedData, + } + }).catch((err) => { + global._relic_eventBus.emit("auth:getter:error", err) + + return err + }) + + if (result instanceof Error) { + throw result + } + + console.log(result.data) + + return result.data + } + + return storagedData + } + + request() { + if (!this.manifest.authService || !this.manifest.authService.fetcher) { + return false + } + + const authURL = this.manifest.authService.fetcher + + open(authURL) + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/fs/index.js b/packages/core/src/manifest/libs/fs/index.js new file mode 100644 index 0000000..a025bc1 --- /dev/null +++ b/packages/core/src/manifest/libs/fs/index.js @@ -0,0 +1,39 @@ +import fs from "node:fs" +import path from "node:path" + +// Protect from reading or write operations outside of the package directory +export default class SecureFileSystem { + constructor(ctx) { + this.jailPath = ctx.manifest.install_path + } + + checkOutsideJail(target) { + // if (!path.resolve(target).startsWith(this.jailPath)) { + // throw new Error("Cannot access resource outside of package directory") + // } + } + + readFileSync(destination, options) { + this.checkOutsideJail(destination) + + return fs.readFileSync(finalPath, options) + } + + copyFileSync(from, to) { + this.checkOutsideJail(from) + this.checkOutsideJail(to) + + return fs.copyFileSync(from, to) + } + + writeFileSync(destination, data, options) { + this.checkOutsideJail(destination) + + return fs.writeFileSync(finalPath, data, options) + } + + // don't need to check finalPath + existsSync(...args) { + return fs.existsSync(...args) + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/index.js b/packages/core/src/manifest/libs/index.js new file mode 100644 index 0000000..3a33e39 --- /dev/null +++ b/packages/core/src/manifest/libs/index.js @@ -0,0 +1,15 @@ +import Open from "./open" +import Path from "./path" +import Fs from "./fs" +import Auth from "./auth" + +// Third party libraries +import Mcl from "./mcl" + +export default { + fs: Fs, + path: Path, + open: Open, + auth: Auth, + mcl: Mcl +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/authenticator.js b/packages/core/src/manifest/libs/mcl/authenticator.js new file mode 100644 index 0000000..c5973d8 --- /dev/null +++ b/packages/core/src/manifest/libs/mcl/authenticator.js @@ -0,0 +1,167 @@ +const request = require('request') +const { v3 } = require('uuid') + +let uuid +let api_url = 'https://authserver.mojang.com' + +function parsePropts(array) { + if (array) { + const newObj = {} + for (const entry of array) { + if (newObj[entry.name]) { + newObj[entry.name].push(entry.value) + } else { + newObj[entry.name] = [entry.value] + } + } + return JSON.stringify(newObj) + } else { + return '{}' + } +} + +function getUUID(value) { + if (!uuid) { + uuid = v3(value, v3.DNS) + } + return uuid +} + +const Authenticator = { + getAuth: (username, password, client_token = null) => { + return new Promise((resolve, reject) => { + getUUID(username) + if (!password) { + const user = { + access_token: uuid, + client_token: client_token || uuid, + uuid, + name: username, + user_properties: '{}' + } + + return resolve(user) + } + + const requestObject = { + url: api_url + '/authenticate', + json: { + agent: { + name: 'Minecraft', + version: 1 + }, + username, + password, + clientToken: uuid, + requestUser: true + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + if (!body || !body.selectedProfile) { + return reject(new Error('Validation error: ' + response.statusMessage)) + } + + const userProfile = { + access_token: body.accessToken, + client_token: body.clientToken, + uuid: body.selectedProfile.id, + name: body.selectedProfile.name, + selected_profile: body.selectedProfile, + user_properties: parsePropts(body.user.properties) + } + + resolve(userProfile) + }) + }) + }, + validate: (accessToken, clientToken) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/validate', + json: { + accessToken, + clientToken + } + } + + request.post(requestObject, async function (error, response, body) { + if (error) return reject(error) + + if (!body) resolve(true) + else reject(body) + }) + }) + }, + refreshAuth: (accessToken, clientToken) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/refresh', + json: { + accessToken, + clientToken, + requestUser: true + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + if (!body || !body.selectedProfile) { + return reject(new Error('Validation error: ' + response.statusMessage)) + } + + const userProfile = { + access_token: body.accessToken, + client_token: getUUID(body.selectedProfile.name), + uuid: body.selectedProfile.id, + name: body.selectedProfile.name, + user_properties: parsePropts(body.user.properties) + } + + return resolve(userProfile) + }) + }) + }, + invalidate: (accessToken, clientToken) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/invalidate', + json: { + accessToken, + clientToken + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + + if (!body) return resolve(true) + else return reject(body) + }) + }) + }, + signOut: (username, password) => { + return new Promise((resolve, reject) => { + const requestObject = { + url: api_url + '/signout', + json: { + username, + password + } + } + + request.post(requestObject, function (error, response, body) { + if (error) return reject(error) + + if (!body) return resolve(true) + else return reject(body) + }) + }) + }, + changeApiUrl: (url) => { + api_url = url + } +} + +export default Authenticator \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/handler.js b/packages/core/src/manifest/libs/mcl/handler.js new file mode 100644 index 0000000..ca0a477 --- /dev/null +++ b/packages/core/src/manifest/libs/mcl/handler.js @@ -0,0 +1,783 @@ +const fs = require('fs') +const path = require('path') +const request = require('request') +const checksum = require('checksum') +const Zip = require('adm-zip') +const child = require('child_process') +let counter = 0 + +export default class Handler { + constructor (client) { + this.client = client + this.options = client.options + this.baseRequest = request.defaults({ + pool: { maxSockets: this.options.overrides.maxSockets || 2 }, + timeout: this.options.timeout || 10000 + }) + } + + checkJava (java) { + return new Promise(resolve => { + child.exec(`"${java}" -version`, (error, stdout, stderr) => { + if (error) { + resolve({ + run: false, + message: error + }) + } else { + this.client.emit('debug', `[MCLC]: Using Java version ${stderr.match(/"(.*?)"/).pop()} ${stderr.includes('64-Bit') ? '64-bit' : '32-Bit'}`) + resolve({ + run: true + }) + } + }) + }) + } + + downloadAsync (url, directory, name, retry, type) { + return new Promise(resolve => { + fs.mkdirSync(directory, { recursive: true }) + + const _request = this.baseRequest(url) + + let receivedBytes = 0 + let totalBytes = 0 + + _request.on('response', (data) => { + if (data.statusCode === 404) { + this.client.emit('debug', `[MCLC]: Failed to download ${url} due to: File not found...`) + return resolve(false) + } + + totalBytes = parseInt(data.headers['content-length']) + }) + + _request.on('error', async (error) => { + this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${error}.` + + ` Retrying... ${retry}`) + if (retry) await this.downloadAsync(url, directory, name, false, type) + resolve() + }) + + _request.on('data', (data) => { + receivedBytes += data.length + this.client.emit('download-status', { + name: name, + type: type, + current: receivedBytes, + total: totalBytes + }) + }) + + const file = fs.createWriteStream(path.join(directory, name)) + _request.pipe(file) + + file.once('finish', () => { + this.client.emit('download', name) + resolve({ + failed: false, + asset: null + }) + }) + + file.on('error', async (e) => { + this.client.emit('debug', `[MCLC]: Failed to download asset to ${path.join(directory, name)} due to\n${e}.` + + ` Retrying... ${retry}`) + if (fs.existsSync(path.join(directory, name))) fs.unlinkSync(path.join(directory, name)) + if (retry) await this.downloadAsync(url, directory, name, false, type) + resolve() + }) + }) + } + + checkSum (hash, file) { + return new Promise((resolve, reject) => { + checksum.file(file, (err, sum) => { + if (err) { + this.client.emit('debug', `[MCLC]: Failed to check file hash due to ${err}`) + resolve(false) + } else { + resolve(hash === sum) + } + }) + }) + } + + getVersion () { + return new Promise(resolve => { + const versionJsonPath = this.options.overrides.versionJson || path.join(this.options.directory, `${this.options.version.number}.json`) + + if (fs.existsSync(versionJsonPath)) { + this.version = JSON.parse(fs.readFileSync(versionJsonPath)) + + return resolve(this.version) + } + + const manifest = `${this.options.overrides.url.meta}/mc/game/version_manifest.json` + + const cache = this.options.cache ? `${this.options.cache}/json` : `${this.options.root}/cache/json` + + request.get(manifest, (error, response, body) => { + if (error && error.code !== 'ENOTFOUND') { + return resolve(error) + } + + if (!error) { + if (!fs.existsSync(cache)) { + fs.mkdirSync(cache, { recursive: true }) + + this.client.emit('debug', '[MCLC]: Cache directory created.') + } + + fs.writeFile(path.join(`${cache}/version_manifest.json`), body, (err) => { + if (err) { + return resolve(err) + } + + this.client.emit('debug', '[MCLC]: Cached version_manifest.json (from request)') + }) + } + + let parsed = null + + if (error && (error.code === 'ENOTFOUND')) { + parsed = JSON.parse(fs.readFileSync(`${cache}/version_manifest.json`)) + } else { + parsed = JSON.parse(body) + } + + const versionManifest = parsed.versions.find((version) => { + return version.id === this.options.version.number + }) + + if (!versionManifest) { + return resolve(new Error(`Version not found`)) + } + + request.get(versionManifest.url, (error, response, body) => { + if (error && error.code !== 'ENOTFOUND') { + return resolve(error) + } + + if (!error) { + fs.writeFile(path.join(`${cache}/${this.options.version.number}.json`), body, (err) => { + if (err) { + return resolve(err) + } + + this.client.emit('debug', `[MCLC]: Cached ${this.options.version.number}.json`) + }) + } + + this.client.emit('debug', '[MCLC]: Parsed version from version manifest') + + if (error && (error.code === 'ENOTFOUND')) { + this.version = JSON.parse(fs.readFileSync(`${cache}/${this.options.version.number}.json`)) + } else { + this.version = JSON.parse(body) + } + + this.client.emit('debug', this.version) + + return resolve(this.version) + }) + }) + }) + } + + async getJar () { + await this.downloadAsync(this.version.downloads.client.url, this.options.directory, `${this.options.version.custom ? this.options.version.custom : this.options.version.number}.jar`, true, 'version-jar') + fs.writeFileSync(path.join(this.options.directory, `${this.options.version.number}.json`), JSON.stringify(this.version, null, 4)) + return this.client.emit('debug', '[MCLC]: Downloaded version jar and wrote version json') + } + + async getAssets () { + const assetDirectory = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) + const assetId = this.options.version.custom || this.options.version.number + if (!fs.existsSync(path.join(assetDirectory, 'indexes', `${assetId}.json`))) { + await this.downloadAsync(this.version.assetIndex.url, path.join(assetDirectory, 'indexes'), + `${assetId}.json`, true, 'asset-json') + } + + const index = JSON.parse(fs.readFileSync(path.join(assetDirectory, 'indexes', `${assetId}.json`), { encoding: 'utf8' })) + + this.client.emit('progress', { + type: 'assets', + task: 0, + total: Object.keys(index.objects).length + }) + + await Promise.all(Object.keys(index.objects).map(async asset => { + const hash = index.objects[asset].hash + const subhash = hash.substring(0, 2) + const subAsset = path.join(assetDirectory, 'objects', subhash) + + if (!fs.existsSync(path.join(subAsset, hash)) || !await this.checkSum(hash, path.join(subAsset, hash))) { + await this.downloadAsync(`${this.options.overrides.url.resource}/${subhash}/${hash}`, subAsset, hash, + true, 'assets') + } + counter++ + this.client.emit('progress', { + type: 'assets', + task: counter, + total: Object.keys(index.objects).length + }) + })) + counter = 0 + + // Copy assets to legacy if it's an older Minecraft version. + if (this.isLegacy()) { + if (fs.existsSync(path.join(assetDirectory, 'legacy'))) { + this.client.emit('debug', '[MCLC]: The \'legacy\' directory is no longer used as Minecraft looks ' + + 'for the resouces folder regardless of what is passed in the assetDirecotry launch option. I\'d ' + + `recommend removing the directory (${path.join(assetDirectory, 'legacy')})`) + } + + const legacyDirectory = path.join(this.options.root, 'resources') + this.client.emit('debug', `[MCLC]: Copying assets over to ${legacyDirectory}`) + + this.client.emit('progress', { + type: 'assets-copy', + task: 0, + total: Object.keys(index.objects).length + }) + + await Promise.all(Object.keys(index.objects).map(async asset => { + const hash = index.objects[asset].hash + const subhash = hash.substring(0, 2) + const subAsset = path.join(assetDirectory, 'objects', subhash) + + const legacyAsset = asset.split('/') + legacyAsset.pop() + + if (!fs.existsSync(path.join(legacyDirectory, legacyAsset.join('/')))) { + fs.mkdirSync(path.join(legacyDirectory, legacyAsset.join('/')), { recursive: true }) + } + + if (!fs.existsSync(path.join(legacyDirectory, asset))) { + fs.copyFileSync(path.join(subAsset, hash), path.join(legacyDirectory, asset)) + } + counter++ + this.client.emit('progress', { + type: 'assets-copy', + task: counter, + total: Object.keys(index.objects).length + }) + })) + } + counter = 0 + + this.client.emit('debug', '[MCLC]: Downloaded assets') + } + + parseRule (lib) { + if (lib.rules) { + if (lib.rules.length > 1) { + if (lib.rules[0].action === 'allow' && + lib.rules[1].action === 'disallow' && + lib.rules[1].os.name === 'osx') { + return this.getOS() === 'osx' + } else { + return true + } + } else { + if (lib.rules[0].action === 'allow' && lib.rules[0].os) return lib.rules[0].os.name !== this.getOS() + } + } else { + return false + } + } + + async getNatives () { + const nativeDirectory = path.resolve(this.options.overrides.natives || path.join(this.options.root, 'natives', this.version.id)) + + if (parseInt(this.version.id.split('.')[1]) >= 19) return this.options.overrides.cwd || this.options.root + + if (!fs.existsSync(nativeDirectory) || !fs.readdirSync(nativeDirectory).length) { + fs.mkdirSync(nativeDirectory, { recursive: true }) + + const natives = async () => { + const natives = [] + await Promise.all(this.version.libraries.map(async (lib) => { + if (!lib.downloads || !lib.downloads.classifiers) return + if (this.parseRule(lib)) return + + const native = this.getOS() === 'osx' + ? lib.downloads.classifiers['natives-osx'] || lib.downloads.classifiers['natives-macos'] + : lib.downloads.classifiers[`natives-${this.getOS()}`] + + natives.push(native) + })) + return natives + } + const stat = await natives() + + this.client.emit('progress', { + type: 'natives', + task: 0, + total: stat.length + }) + + await Promise.all(stat.map(async (native) => { + if (!native) return + const name = native.path.split('/').pop() + await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') + if (!await this.checkSum(native.sha1, path.join(nativeDirectory, name))) { + await this.downloadAsync(native.url, nativeDirectory, name, true, 'natives') + } + try { + new Zip(path.join(nativeDirectory, name)).extractAllTo(nativeDirectory, true) + } catch (e) { + // Only doing a console.warn since a stupid error happens. You can basically ignore this. + // if it says Invalid file name, just means two files were downloaded and both were deleted. + // All is well. + console.warn(e) + } + fs.unlinkSync(path.join(nativeDirectory, name)) + counter++ + this.client.emit('progress', { + type: 'natives', + task: counter, + total: stat.length + }) + })) + this.client.emit('debug', '[MCLC]: Downloaded and extracted natives') + } + + counter = 0 + this.client.emit('debug', `[MCLC]: Set native path to ${nativeDirectory}`) + + return nativeDirectory + } + + fwAddArgs () { + const forgeWrapperAgrs = [ + `-Dforgewrapper.librariesDir=${path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries'))}`, + `-Dforgewrapper.installer=${this.options.forge}`, + `-Dforgewrapper.minecraft=${this.options.mcPath}` + ] + this.options.customArgs + ? this.options.customArgs = this.options.customArgs.concat(forgeWrapperAgrs) + : this.options.customArgs = forgeWrapperAgrs + } + + isModernForge (json) { + return json.inheritsFrom && json.inheritsFrom.split('.')[1] >= 12 && !(json.inheritsFrom === '1.12.2' && (json.id.split('.')[json.id.split('.').length - 1]) === '2847') + } + + async getForgedWrapped () { + let json = null + let installerJson = null + const versionPath = path.join(this.options.root, 'forge', `${this.version.id}`, 'version.json') + // Since we're building a proper "custom" JSON that will work nativly with MCLC, the version JSON will not + // be re-generated on the next run. + if (fs.existsSync(versionPath)) { + try { + json = JSON.parse(fs.readFileSync(versionPath)) + if (!json.forgeWrapperVersion || !(json.forgeWrapperVersion === this.options.overrides.fw.version)) { + this.client.emit('debug', '[MCLC]: Old ForgeWrapper has generated this version JSON, re-generating') + } else { + // If forge is modern, add ForgeWrappers launch arguments and set forge to null so MCLC treats it as a custom json. + if (this.isModernForge(json)) { + this.fwAddArgs() + this.options.forge = null + } + return json + } + } catch (e) { + console.warn(e) + this.client.emit('debug', '[MCLC]: Failed to parse Forge version JSON, re-generating') + } + } + + this.client.emit('debug', '[MCLC]: Generating a proper version json, this might take a bit') + const zipFile = new Zip(this.options.forge) + json = zipFile.readAsText('version.json') + if (zipFile.getEntry('install_profile.json')) installerJson = zipFile.readAsText('install_profile.json') + + try { + json = JSON.parse(json) + if (installerJson) installerJson = JSON.parse(installerJson) + } catch (e) { + this.client.emit('debug', '[MCLC]: Failed to load json files for ForgeWrapper, using Vanilla instead') + return null + } + // Adding the installer libraries as mavenFiles so MCLC downloads them but doesn't add them to the class paths. + if (installerJson) { + json.mavenFiles + ? json.mavenFiles = json.mavenFiles.concat(installerJson.libraries) + : json.mavenFiles = installerJson.libraries + } + + // Holder for the specifc jar ending which depends on the specifc forge version. + let jarEnding = 'universal' + // We need to handle modern forge differently than legacy. + if (this.isModernForge(json)) { + // If forge is modern and above 1.12.2, we add ForgeWrapper to the libraries so MCLC includes it in the classpaths. + if (json.inheritsFrom !== '1.12.2') { + this.fwAddArgs() + const fwName = `ForgeWrapper-${this.options.overrides.fw.version}.jar` + const fwPathArr = ['io', 'github', 'zekerzhayard', 'ForgeWrapper', this.options.overrides.fw.version] + json.libraries.push({ + name: fwPathArr.join(':'), + downloads: { + artifact: { + path: [...fwPathArr, fwName].join('/'), + url: `${this.options.overrides.fw.baseUrl}${this.options.overrides.fw.version}/${fwName}`, + sha1: this.options.overrides.fw.sh1, + size: this.options.overrides.fw.size + } + } + }) + json.mainClass = 'io.github.zekerzhayard.forgewrapper.installer.Main' + jarEnding = 'launcher' + + // Providing a download URL to the universal jar mavenFile so it can be downloaded properly. + for (const library of json.mavenFiles) { + const lib = library.name.split(':') + if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { + library.downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + library.downloads.artifact.path + break + } + } + } else { + // Remove the forge dependent since we're going to overwrite the first entry anyways. + for (const library in json.mavenFiles) { + const lib = json.mavenFiles[library].name.split(':') + if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) { + delete json.mavenFiles[library] + break + } + } + } + } else { + // Modifying legacy library format to play nice with MCLC's downloadToDirectory function. + await Promise.all(json.libraries.map(async library => { + const lib = library.name.split(':') + if (lib[0] === 'net.minecraftforge' && lib[1].includes('forge')) return + + let url = this.options.overrides.url.mavenForge + const name = `${lib[1]}-${lib[2]}.jar` + + if (!library.url) { + if (library.serverreq || library.clientreq) { + url = this.options.overrides.url.defaultRepoForge + } else { + return + } + } + library.url = url + const downloadLink = `${url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` + // Checking if the file still exists on Forge's server, if not, replace it with the fallback. + // Not checking for sucess, only if it 404s. + this.baseRequest(downloadLink, (error, response, body) => { + if (error) { + this.client.emit('debug', `[MCLC]: Failed checking request for ${downloadLink}`) + } else { + if (response.statusCode === 404) library.url = this.options.overrides.url.fallbackMaven + } + }) + })) + } + // If a downloads property exists, we modify the inital forge entry to include ${jarEnding} so ForgeWrapper can work properly. + // If it doesn't, we simply remove it since we're already providing the universal jar. + if (json.libraries[0].downloads) { + if (json.libraries[0].name.includes('minecraftforge')) { + json.libraries[0].name = json.libraries[0].name + `:${jarEnding}` + json.libraries[0].downloads.artifact.path = json.libraries[0].downloads.artifact.path.replace('.jar', `-${jarEnding}.jar`) + json.libraries[0].downloads.artifact.url = 'https://files.minecraftforge.net/maven/' + json.libraries[0].downloads.artifact.path + } + } else { + delete json.libraries[0] + } + + // Removing duplicates and null types + json.libraries = this.cleanUp(json.libraries) + if (json.mavenFiles) json.mavenFiles = this.cleanUp(json.mavenFiles) + + json.forgeWrapperVersion = this.options.overrides.fw.version + + // Saving file for next run! + if (!fs.existsSync(path.join(this.options.root, 'forge', this.version.id))) { + fs.mkdirSync(path.join(this.options.root, 'forge', this.version.id), { recursive: true }) + } + fs.writeFileSync(versionPath, JSON.stringify(json, null, 4)) + + // Make MCLC treat modern forge as a custom version json rather then legacy forge. + if (this.isModernForge(json)) this.options.forge = null + + return json + } + + runInstaller (path) { + return new Promise(resolve => { + const installer = child.exec(path) + installer.on('close', (code) => resolve(code)) + }) + } + + async downloadToDirectory (directory, libraries, eventName) { + const libs = [] + + await Promise.all(libraries.map(async library => { + if (!library) return + if (this.parseRule(library)) return + const lib = library.name.split(':') + + let jarPath + let name + if (library.downloads && library.downloads.artifact && library.downloads.artifact.path) { + name = library.downloads.artifact.path.split('/')[library.downloads.artifact.path.split('/').length - 1] + jarPath = path.join(directory, this.popString(library.downloads.artifact.path)) + } else { + name = `${lib[1]}-${lib[2]}${lib[3] ? '-' + lib[3] : ''}.jar` + jarPath = path.join(directory, `${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}`) + } + + const downloadLibrary = async library => { + if (library.url) { + const url = `${library.url}${lib[0].replace(/\./g, '/')}/${lib[1]}/${lib[2]}/${name}` + await this.downloadAsync(url, jarPath, name, true, eventName) + } else if (library.downloads && library.downloads.artifact) { + await this.downloadAsync(library.downloads.artifact.url, jarPath, name, true, eventName) + } + } + + if (!fs.existsSync(path.join(jarPath, name))) downloadLibrary(library) + else if (library.downloads && library.downloads.artifact) { + if (!this.checkSum(library.downloads.artifact.sha1, path.join(jarPath, name))) downloadLibrary(library) + } + + counter++ + this.client.emit('progress', { + type: eventName, + task: counter, + total: libraries.length + }) + libs.push(`${jarPath}${path.sep}${name}`) + })) + counter = 0 + + return libs + } + + async getClasses (classJson) { + let libs = [] + + const libraryDirectory = path.resolve(this.options.overrides.libraryRoot || path.join(this.options.root, 'libraries')) + + if (classJson) { + if (classJson.mavenFiles) { + await this.downloadToDirectory(libraryDirectory, classJson.mavenFiles, 'classes-maven-custom') + } + libs = (await this.downloadToDirectory(libraryDirectory, classJson.libraries, 'classes-custom')) + } + + const parsed = this.version.libraries.map(lib => { + if (lib.downloads && lib.downloads.artifact && !this.parseRule(lib)) return lib + }) + + libs = libs.concat((await this.downloadToDirectory(libraryDirectory, parsed, 'classes'))) + counter = 0 + + // Temp Quilt support + if (classJson) libs.sort() + + this.client.emit('debug', '[MCLC]: Collected class paths') + return libs + } + + popString (path) { + const tempArray = path.split('/') + tempArray.pop() + return tempArray.join('/') + } + + cleanUp (array) { + const newArray = [] + for (const classPath in array) { + if (newArray.includes(array[classPath]) || array[classPath] === null) continue + newArray.push(array[classPath]) + } + return newArray + } + + formatQuickPlay () { + const types = { + singleplayer: '--quickPlaySingleplayer', + multiplayer: '--quickPlayMultiplayer', + realms: '--quickPlayRealms', + legacy: null + } + const { type, identifier, path } = this.options.quickPlay + const keys = Object.keys(types) + if (!keys.includes(type)) { + this.client.emit('debug', `[MCLC]: quickPlay type is not valid. Valid types are: ${keys.join(', ')}`) + return null + } + const returnArgs = type === 'legacy' + ? ['--server', identifier.split(':')[0], '--port', identifier.split(':')[1] || '25565'] + : [types[type], identifier] + if (path) returnArgs.push('--quickPlayPath', path) + return returnArgs + } + + async getLaunchOptions (modification) { + const type = Object.assign({}, this.version, modification) + + let args = type.minecraftArguments + ? type.minecraftArguments.split(' ') + : type.arguments.game + const assetRoot = path.resolve(this.options.overrides.assetRoot || path.join(this.options.root, 'assets')) + const assetPath = this.isLegacy() + ? path.join(this.options.root, 'resources') + : path.join(assetRoot) + + const minArgs = this.options.overrides.minArgs || this.isLegacy() ? 5 : 11 + if (args.length < minArgs) args = args.concat(this.version.minecraftArguments ? this.version.minecraftArguments.split(' ') : this.version.arguments.game) + if (this.options.customLaunchArgs) args = args.concat(this.options.customLaunchArgs) + + this.options.authorization = await Promise.resolve(this.options.authorization) + this.options.authorization.meta = this.options.authorization.meta ? this.options.authorization.meta : { type: 'mojang' } + const fields = { + '${auth_access_token}': this.options.authorization.access_token, + '${auth_session}': this.options.authorization.access_token, + '${auth_player_name}': this.options.authorization.name, + '${auth_uuid}': this.options.authorization.uuid, + '${auth_xuid}': this.options.authorization.meta.xuid || this.options.authorization.access_token, + '${user_properties}': this.options.authorization.user_properties, + '${user_type}': this.options.authorization.meta.type, + '${version_name}': this.options.version.number, + '${assets_index_name}': this.options.overrides.assetIndex || this.options.version.custom || this.options.version.number, + '${game_directory}': this.options.overrides.gameDirectory || this.options.root, + '${assets_root}': assetPath, + '${game_assets}': assetPath, + '${version_type}': this.options.version.type, + '${clientid}': this.options.authorization.meta.clientId || (this.options.authorization.client_token || this.options.authorization.access_token), + '${resolution_width}': this.options.window ? this.options.window.width : 856, + '${resolution_height}': this.options.window ? this.options.window.height : 482 + } + + if (this.options.authorization.meta.demo && (this.options.features ? !this.options.features.includes('is_demo_user') : true)) { + args.push('--demo') + } + + const replaceArg = (obj, index) => { + if (Array.isArray(obj.value)) { + for (const arg of obj.value) { + args.push(arg) + } + } else { + args.push(obj.value) + } + delete args[index] + } + + for (let index = 0; index < args.length; index++) { + if (typeof args[index] === 'object') { + if (args[index].rules) { + if (!this.options.features) continue + const featureFlags = [] + for (const rule of args[index].rules) { + featureFlags.push(...Object.keys(rule.features)) + } + let hasAllRules = true + for (const feature of this.options.features) { + if (!featureFlags.includes(feature)) { + hasAllRules = false + } + } + if (hasAllRules) replaceArg(args[index], index) + } else { + replaceArg(args[index], index) + } + } else { + if (Object.keys(fields).includes(args[index])) { + args[index] = fields[args[index]] + } + } + } + if (this.options.window) { + // eslint-disable-next-line no-unused-expressions + this.options.window.fullscreen + ? args.push('--fullscreen') + : () => { + if (this.options.features ? !this.options.features.includes('has_custom_resolution') : true) { + args.push('--width', this.options.window.width, '--height', this.options.window.height) + } + } + } + if (this.options.server) this.client.emit('debug', '[MCLC]: server and port are deprecated launch flags. Use the quickPlay field.') + if (this.options.quickPlay) args = args.concat(this.formatQuickPlay()) + if (this.options.proxy) { + args.push( + '--proxyHost', + this.options.proxy.host, + '--proxyPort', + this.options.proxy.port || '8080', + '--proxyUser', + this.options.proxy.username, + '--proxyPass', + this.options.proxy.password + ) + } + args = args.filter(value => typeof value === 'string' || typeof value === 'number') + this.client.emit('debug', '[MCLC]: Set launch options') + return args + } + + async getJVM () { + const opts = { + windows: '-XX:HeapDumpPath=MojangTricksIntelDriversForPerformance_javaw.exe_minecraft.exe.heapdump', + osx: '-XstartOnFirstThread', + linux: '-Xss1M' + } + return opts[this.getOS()] + } + + isLegacy () { + return this.version.assets === 'legacy' || this.version.assets === 'pre-1.6' + } + + getOS () { + if (this.options.os) { + return this.options.os + } else { + switch (process.platform) { + case 'win32': return 'windows' + case 'darwin': return 'osx' + default: return 'linux' + } + } + } + + // To prevent launchers from breaking when they update. Will be reworked with rewrite. + getMemory () { + if (!this.options.memory) { + this.client.emit('debug', '[MCLC]: Memory not set! Setting 1GB as MAX!') + this.options.memory = { + min: 512, + max: 1023 + } + } + if (!isNaN(this.options.memory.max) && !isNaN(this.options.memory.min)) { + if (this.options.memory.max < this.options.memory.min) { + this.client.emit('debug', '[MCLC]: MIN memory is higher then MAX! Resetting!') + this.options.memory.max = 1023 + this.options.memory.min = 512 + } + return [`${this.options.memory.max}M`, `${this.options.memory.min}M`] + } else { return [`${this.options.memory.max}`, `${this.options.memory.min}`] } + } + + async extractPackage (options = this.options) { + if (options.clientPackage.startsWith('http')) { + await this.downloadAsync(options.clientPackage, options.root, 'clientPackage.zip', true, 'client-package') + options.clientPackage = path.join(options.root, 'clientPackage.zip') + } + new Zip(options.clientPackage).extractAllTo(options.root, true) + if (options.removePackage) fs.unlinkSync(options.clientPackage) + + return this.client.emit('package-extract', true) + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/index.js b/packages/core/src/manifest/libs/mcl/index.js new file mode 100644 index 0000000..1386624 --- /dev/null +++ b/packages/core/src/manifest/libs/mcl/index.js @@ -0,0 +1,49 @@ +import Logger from "../../../logger" + +import Client from "./launcher" +import Authenticator from "./authenticator" + +const Log = Logger.child({ service: "MCL" }) + +export default class MCL { + /** + * Asynchronously authenticate the user using the provided username and password. + * + * @param {string} username - the username of the user + * @param {string} password - the password of the user + * @return {Promise} the authentication information + */ + async auth(username, password) { + return await Authenticator.getAuth(username, password) + } + + /** + * Launches a new client with the given options. + * + * @param {Object} opts - The options to be passed for launching the client. + * @return {Promise} A promise that resolves with the launched client. + */ + async launch(opts, callbacks) { + const launcher = new Client() + + launcher.on("debug", (e) => console.log(e)) + launcher.on("data", (e) => console.log(e)) + launcher.on("close", (e) => console.log(e)) + launcher.on("error", (e) => console.log(e)) + + if (typeof callbacks === "undefined") { + callbacks = { + install: () => { + Log.info("Downloading Minecraft assets...") + }, + init_assets: () => { + Log.info("Initializing Minecraft assets...") + } + } + } + + await launcher.launch(opts, callbacks) + + return launcher + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/mcl/launcher.js b/packages/core/src/manifest/libs/mcl/launcher.js new file mode 100644 index 0000000..5a5aa76 --- /dev/null +++ b/packages/core/src/manifest/libs/mcl/launcher.js @@ -0,0 +1,224 @@ +import fs from "node:fs" +import path from "node:path" +import { EventEmitter } from "events" +import child from "child_process" + +import Handler from "./handler" + +export default class MCLCore extends EventEmitter { + async launch(options, callbacks = {}) { + try { + this.options = { ...options } + + this.options.root = path.resolve(this.options.root) + + this.options.overrides = { + detached: true, + ...this.options.overrides, + url: { + meta: 'https://launchermeta.mojang.com', + resource: 'https://resources.download.minecraft.net', + mavenForge: 'http://files.minecraftforge.net/maven/', + defaultRepoForge: 'https://libraries.minecraft.net/', + fallbackMaven: 'https://search.maven.org/remotecontent?filepath=', + ...this.options.overrides + ? this.options.overrides.url + : undefined + }, + fw: { + baseUrl: 'https://github.com/ZekerZhayard/ForgeWrapper/releases/download/', + version: '1.5.6', + sh1: 'b38d28e8b7fde13b1bc0db946a2da6760fecf98d', + size: 34715, + ...this.options.overrides + ? this.options.overrides.fw + : undefined + } + } + + this.handler = new Handler(this) + + this.printVersion() + + const java = await this.handler.checkJava(this.options.javaPath || 'java') + + if (!java.run) { + this.emit('debug', `[MCLC]: Couldn't start Minecraft due to: ${java.message}`) + this.emit('close', 1) + return null + } + + this.createRootDirectory() + this.createGameDirectory() + + await this.extractPackage() + + if (this.options.installer) { + // So installers that create a profile in launcher_profiles.json can run without breaking. + const profilePath = path.join(this.options.root, 'launcher_profiles.json') + if (!fs.existsSync(profilePath) || !JSON.parse(fs.readFileSync(profilePath)).profiles) { + fs.writeFileSync(profilePath, JSON.stringify({ profiles: {} }, null, 4)) + } + const code = await this.handler.runInstaller(this.options.installer) + if (!this.options.version.custom && code === 0) { + this.emit('debug', '[MCLC]: Installer successfully ran, but no custom version was provided') + } + this.emit('debug', `[MCLC]: Installer closed with code ${code}`) + } + + const directory = this.options.overrides.directory || path.join(this.options.root, 'versions', this.options.version.custom ? this.options.version.custom : this.options.version.number) + this.options.directory = directory + + const versionFile = await this.handler.getVersion() + + const mcPath = this.options.overrides.minecraftJar || (this.options.version.custom + ? path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.jar`) + : path.join(directory, `${this.options.version.number}.jar`)) + + this.options.mcPath = mcPath + + const nativePath = await this.handler.getNatives() + + if (!fs.existsSync(mcPath)) { + this.emit('debug', '[MCLC]: Attempting to download Minecraft version jar') + + if (typeof callbacks.install === "function") { + callbacks.install() + } + + await this.handler.getJar() + } + + const modifyJson = await this.getModifyJson() + + const args = [] + + let jvm = [ + '-XX:-UseAdaptiveSizePolicy', + '-XX:-OmitStackTraceInFastThrow', + '-Dfml.ignorePatchDiscrepancies=true', + '-Dfml.ignoreInvalidMinecraftCertificates=true', + `-Djava.library.path=${nativePath}`, + `-Xmx${this.handler.getMemory()[0]}`, + `-Xms${this.handler.getMemory()[1]}` + ] + if (this.handler.getOS() === 'osx') { + if (parseInt(versionFile.id.split('.')[1]) > 12) jvm.push(await this.handler.getJVM()) + } else jvm.push(await this.handler.getJVM()) + + if (this.options.customArgs) jvm = jvm.concat(this.options.customArgs) + if (this.options.overrides.logj4ConfigurationFile) { + jvm.push(`-Dlog4j.configurationFile=${path.resolve(this.options.overrides.logj4ConfigurationFile)}`) + } + // https://help.minecraft.net/hc/en-us/articles/4416199399693-Security-Vulnerability-in-Minecraft-Java-Edition + if (parseInt(versionFile.id.split('.')[1]) === 18 && !parseInt(versionFile.id.split('.')[2])) jvm.push('-Dlog4j2.formatMsgNoLookups=true') + if (parseInt(versionFile.id.split('.')[1]) === 17) jvm.push('-Dlog4j2.formatMsgNoLookups=true') + if (parseInt(versionFile.id.split('.')[1]) < 17) { + if (!jvm.find(arg => arg.includes('Dlog4j.configurationFile'))) { + const configPath = path.resolve(this.options.overrides.cwd || this.options.root) + const intVersion = parseInt(versionFile.id.split('.')[1]) + if (intVersion >= 12) { + await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/02937d122c86ce73319ef9975b58896fc1b491d1/log4j2_112-116.xml', + configPath, 'log4j2_112-116.xml', true, 'log4j') + jvm.push('-Dlog4j.configurationFile=log4j2_112-116.xml') + } else if (intVersion >= 7) { + await this.handler.downloadAsync('https://launcher.mojang.com/v1/objects/dd2b723346a8dcd48e7f4d245f6bf09e98db9696/log4j2_17-111.xml', + configPath, 'log4j2_17-111.xml', true, 'log4j') + jvm.push('-Dlog4j.configurationFile=log4j2_17-111.xml') + } + } + } + + const classes = this.options.overrides.classes || this.handler.cleanUp(await this.handler.getClasses(modifyJson)) + const classPaths = ['-cp'] + const separator = this.handler.getOS() === 'windows' ? ';' : ':' + + this.emit('debug', `[MCLC]: Using ${separator} to separate class paths`) + + // Handling launch arguments. + const file = modifyJson || versionFile + + // So mods like fabric work. + const jar = fs.existsSync(mcPath) + ? `${separator}${mcPath}` + : `${separator}${path.join(directory, `${this.options.version.number}.jar`)}` + classPaths.push(`${this.options.forge ? this.options.forge + separator : ''}${classes.join(separator)}${jar}`) + classPaths.push(file.mainClass) + + this.emit('debug', '[MCLC]: Attempting to download assets') + + if (typeof callbacks.init_assets === "function") { + callbacks.init_assets() + } + + await this.handler.getAssets() + + // Forge -> Custom -> Vanilla + const launchOptions = await this.handler.getLaunchOptions(modifyJson) + + const launchArguments = args.concat(jvm, classPaths, launchOptions) + this.emit('arguments', launchArguments) + this.emit('debug', `[MCLC]: Launching with arguments ${launchArguments.join(' ')}`) + + return this.startMinecraft(launchArguments) + } catch (e) { + this.emit('debug', `[MCLC]: Failed to start due to ${e}, closing...`) + return null + } + } + + printVersion() { + if (fs.existsSync(path.join(__dirname, '..', 'package.json'))) { + const { version } = require('../package.json') + this.emit('debug', `[MCLC]: MCLC version ${version}`) + } else { this.emit('debug', '[MCLC]: Package JSON not found, skipping MCLC version check.') } + } + + createRootDirectory() { + if (!fs.existsSync(this.options.root)) { + this.emit('debug', '[MCLC]: Attempting to create root folder') + fs.mkdirSync(this.options.root) + } + } + + createGameDirectory() { + if (this.options.overrides.gameDirectory) { + this.options.overrides.gameDirectory = path.resolve(this.options.overrides.gameDirectory) + if (!fs.existsSync(this.options.overrides.gameDirectory)) { + fs.mkdirSync(this.options.overrides.gameDirectory, { recursive: true }) + } + } + } + + async extractPackage() { + if (this.options.clientPackage) { + this.emit('debug', `[MCLC]: Extracting client package to ${this.options.root}`) + await this.handler.extractPackage() + } + } + + async getModifyJson() { + let modifyJson = null + + if (this.options.forge) { + this.options.forge = path.resolve(this.options.forge) + this.emit('debug', '[MCLC]: Detected Forge in options, getting dependencies') + modifyJson = await this.handler.getForgedWrapped() + } else if (this.options.version.custom) { + this.emit('debug', '[MCLC]: Detected custom in options, setting custom version file') + modifyJson = modifyJson || JSON.parse(fs.readFileSync(path.join(this.options.root, 'versions', this.options.version.custom, `${this.options.version.custom}.json`), { encoding: 'utf8' })) + } + + return modifyJson + } + + startMinecraft(launchArguments) { + const minecraft = child.spawn(this.options.javaPath ? this.options.javaPath : 'java', launchArguments, + { cwd: this.options.overrides.cwd || this.options.root, detached: this.options.overrides.detached }) + + minecraft.stdout.on('data', (data) => this.emit('data', data.toString('utf-8'))) + minecraft.stderr.on('data', (data) => this.emit('data', data.toString('utf-8'))) + minecraft.on('close', (code) => this.emit('close', code)) + return minecraft + } +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/open/index.js b/packages/core/src/manifest/libs/open/index.js new file mode 100644 index 0000000..d696826 --- /dev/null +++ b/packages/core/src/manifest/libs/open/index.js @@ -0,0 +1,15 @@ +import Logger from "../../../logger" + +import open, { apps } from "open" + +const Log = Logger.child({ service: "OPEN-LIB" }) + +export default { + spawn: async (...args) => { + Log.info("Open spawned with args >") + console.log(...args) + + return await open(...args) + }, + apps: apps, +} \ No newline at end of file diff --git a/packages/core/src/manifest/libs/path/index.js b/packages/core/src/manifest/libs/path/index.js new file mode 100644 index 0000000..1b4bd73 --- /dev/null +++ b/packages/core/src/manifest/libs/path/index.js @@ -0,0 +1,3 @@ +import path from "node:path" + +export default path \ No newline at end of file diff --git a/packages/core/src/manifest/reader.js b/packages/core/src/manifest/reader.js new file mode 100644 index 0000000..830278f --- /dev/null +++ b/packages/core/src/manifest/reader.js @@ -0,0 +1,51 @@ +import fs from "node:fs" +import path from "node:path" +import axios from "axios" +import checksum from "checksum" + +import Vars from "../vars" + +export async function readManifest(manifest) { + // check if manifest is a directory or a url + const urlRegex = /https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/gi + + const target = manifest?.remote_url ?? manifest + + if (urlRegex.test(target)) { + if (!fs.existsSync(Vars.cache_path)) { + fs.mkdirSync(Vars.cache_path, { recursive: true }) + } + + const { data: code } = await axios.get(target) + + const manifestChecksum = checksum(code, { algorithm: "md5" }) + + const cachedManifest = path.join(Vars.cache_path, `${manifestChecksum}.rmanifest`) + + await fs.promises.writeFile(cachedManifest, code) + + return { + remote_manifest: manifest, + local_manifest: cachedManifest, + is_catched: true, + code: code, + } + } else { + if (!fs.existsSync(target)) { + throw new Error(`Manifest not found: ${target}`) + } + + if (!fs.statSync(target).isFile()) { + throw new Error(`Manifest is not a file: ${target}`) + } + + return { + remote_manifest: undefined, + local_manifest: target, + is_catched: false, + code: fs.readFileSync(target, "utf8"), + } + } +} + +export default readManifest \ No newline at end of file diff --git a/packages/core/src/manifest/vm.js b/packages/core/src/manifest/vm.js new file mode 100644 index 0000000..65bcbe3 --- /dev/null +++ b/packages/core/src/manifest/vm.js @@ -0,0 +1,83 @@ +import Logger from "../logger" + +import os from "node:os" +import vm from "node:vm" +import path from "node:path" +import ManifestConfigManager from "../classes/ManifestConfig" + +import resolveOs from "../utils/resolveOs" +import FetchLibraries from "./libraries" + +import Vars from "../vars" + +async function BuildManifest(baseClass, context, { soft = false } = {}) { + // inject install_path + context.install_path = path.resolve(Vars.packages_path, baseClass.id) + baseClass.install_path = context.install_path + + if (soft === true) { + return baseClass + } + + const configManager = new ManifestConfigManager(baseClass.id) + + await configManager.initialize() + + let dependencies = [] + + if (Array.isArray(baseClass.useLib)) { + dependencies = [ + ...dependencies, + ...baseClass.useLib + ] + } + + // modify context + context.Log = Logger.child({ service: `VM|${baseClass.id}` }) + context.Lib = await FetchLibraries(dependencies, { + manifest: baseClass, + install_path: context.install_path, + }) + context.Config = configManager + + // Construct the instance + const instance = new baseClass() + + instance.install_path = context.install_path + + return instance +} + +function injectUseManifest(code) { + return code + "\n\nuse(Manifest);" +} + +export default async (code, { soft = false } = {}) => { + return await new Promise(async (resolve, reject) => { + try { + code = injectUseManifest(code) + + const context = { + Vars: Vars, + Log: Logger.child({ service: "MANIFEST_VM" }), + use: (baseClass) => { + return BuildManifest( + baseClass, + context, + { + soft: soft, + } + ).then(resolve) + }, + os_string: resolveOs(), + arch: os.arch(), + } + + vm.createContext(context) + + await vm.runInContext(code, context) + } catch (error) { + reject(error) + } + }) +} \ No newline at end of file diff --git a/packages/core/src/prerequisites.js b/packages/core/src/prerequisites.js new file mode 100644 index 0000000..9fb7df7 --- /dev/null +++ b/packages/core/src/prerequisites.js @@ -0,0 +1,70 @@ +import resolveRemoteBinPath from "./utils/resolveRemoteBinPath" +import Vars from "./vars" +import path from "node:path" +import axios from "axios" + +const baseURL = "https://storage.ragestudio.net/rstudio/binaries" + +export default [ + { + id: "7z-bin", + finalBin: Vars.sevenzip_bin, + url: resolveRemoteBinPath(`${baseURL}/7zip-bin`, process.platform === "win32" ? "7za.exe" : "7za"), + destination: Vars.sevenzip_bin, + rewriteExecutionPermission: true, + }, + { + id: "git-bin", + finalBin: Vars.git_bin, + url: resolveRemoteBinPath(`${baseURL}/git`, "git-bundle-2.4.0.zip"), + destination: path.resolve(Vars.binaries_path, "git-bundle.zip"), + extract: path.resolve(Vars.binaries_path, "git-bin"), + requireOs: ["win32"], + rewriteExecutionPermission: true, + deleteBeforeExtract: true, + }, + { + id: "rclone-bin", + finalBin: Vars.rclone_bin, + url: resolveRemoteBinPath(`${baseURL}/rclone`, "rclone-bin.zip"), + destination: path.resolve(Vars.binaries_path, "rclone-bin.zip"), + extract: path.resolve(Vars.binaries_path, "rclone-bin"), + requireOs: ["win32"], + rewriteExecutionPermission: true, + deleteBeforeExtract: true, + }, + { + id: "java_jre_bin", + finalBin: Vars.java_jre_bin, + url: async (os, arch) => { + const { data } = await axios({ + method: "GET", + url: "https://api.azul.com/metadata/v1/zulu/packages", + params: { + arch: arch, + java_version: "JAVA_22", + os: os, + archive_type: "zip", + javafx_bundled: "false", + java_package_type: "jre", + page_size: "1", + } + }) + + return data[0].download_url + }, + destination: path.resolve(Vars.binaries_path, "java-jre.zip"), + extract: path.resolve(Vars.binaries_path, "java_jre_bin"), + extractTargetFromName: true, + moveDirs: [ + { + requireOs: ["macos"], + from: path.resolve(Vars.binaries_path, "java_jre_bin", "zulu-22.jre", "Contents"), + to: path.resolve(Vars.binaries_path, "java_jre_bin", "Contents"), + deleteParentBefore: true + } + ], + rewriteExecutionPermission: path.resolve(Vars.binaries_path, "java_jre_bin"), + deleteBeforeExtract: true, + }, +] \ No newline at end of file diff --git a/packages/core/src/utils/chmodRecursive.js b/packages/core/src/utils/chmodRecursive.js new file mode 100644 index 0000000..8a1d7a1 --- /dev/null +++ b/packages/core/src/utils/chmodRecursive.js @@ -0,0 +1,16 @@ +import fs from "node:fs" +import path from "node:path" + +async function chmodRecursive(target, mode) { + if (fs.lstatSync(target).isDirectory()) { + const files = await fs.promises.readdir(target, { withFileTypes: true }) + + for (const file of files) { + await chmodRecursive(path.join(target, file.name), mode) + } + } else { + await fs.promises.chmod(target, mode) + } +} + +export default chmodRecursive diff --git a/packages/core/src/utils/extractFile.js b/packages/core/src/utils/extractFile.js new file mode 100644 index 0000000..19040a7 --- /dev/null +++ b/packages/core/src/utils/extractFile.js @@ -0,0 +1,48 @@ +import Logger from "../logger" + +import fs from "node:fs" +import path from "node:path" +import { pipeline as streamPipeline } from "node:stream/promises" + +import { extractFull } from "node-7z" +import unzipper from "unzipper" + +import Vars from "../vars" + +const Log = Logger.child({ service: "EXTRACTOR" }) + +export async function extractFile(file, dest) { + const ext = path.extname(file) + + Log.info(`Extracting ${file} to ${dest}`) + + switch (ext) { + case ".zip": { + await streamPipeline( + fs.createReadStream(file), + unzipper.Extract({ + path: dest, + }) + ) + break + } + case ".7z": { + await extractFull(file, dest, { + $bin: Vars.sevenzip_bin, + }) + break + } + case ".gz": { + await extractFull(file, dest, { + $bin: Vars.sevenzip_bin + }) + break + } + default: + throw new Error(`Unsupported file extension: ${ext}`) + } + + return dest +} + +export default extractFile \ No newline at end of file diff --git a/packages/core/src/utils/parseStringVars.js b/packages/core/src/utils/parseStringVars.js new file mode 100644 index 0000000..9042d92 --- /dev/null +++ b/packages/core/src/utils/parseStringVars.js @@ -0,0 +1,21 @@ +export default function parseStringVars(str, pkg) { + if (!pkg) { + return str + } + + const vars = { + id: pkg.id, + name: pkg.name, + version: pkg.version, + install_path: pkg.install_path, + remote: pkg.remote, + } + + const regex = /%([^%]+)%/g + + str = str.replace(regex, (match, varName) => { + return vars[varName] + }) + + return str +} \ No newline at end of file diff --git a/packages/core/src/utils/readDirRecurse.js b/packages/core/src/utils/readDirRecurse.js new file mode 100644 index 0000000..342dda0 --- /dev/null +++ b/packages/core/src/utils/readDirRecurse.js @@ -0,0 +1,25 @@ +import fs from "node:fs" +import path from "node:path" + +async function readDirRecurse(dir, maxDepth = 3, current = 0) { + if (current > maxDepth) { + return [] + } + + const files = await fs.promises.readdir(dir) + + const promises = files.map(async (file) => { + const filePath = path.join(dir, file) + const stat = await fs.promises.stat(filePath) + + if (stat.isDirectory()) { + return readDirRecurse(filePath, maxDepth, current + 1) + } + + return filePath + }) + + return (await Promise.all(promises)).flat() +} + +export default readDirRecurse \ No newline at end of file diff --git a/packages/core/src/utils/resolveOs.js b/packages/core/src/utils/resolveOs.js new file mode 100644 index 0000000..1bf58de --- /dev/null +++ b/packages/core/src/utils/resolveOs.js @@ -0,0 +1,17 @@ +import os from "node:os" + +export default () => { + if (os.platform() === "win32") { + return "windows" + } + + if (os.platform() === "darwin") { + return "macos" + } + + if (os.platform() === "linux") { + return "linux" + } + + return os.platform() +} \ No newline at end of file diff --git a/packages/core/src/utils/resolveRemoteBinPath.js b/packages/core/src/utils/resolveRemoteBinPath.js new file mode 100644 index 0000000..acc8926 --- /dev/null +++ b/packages/core/src/utils/resolveRemoteBinPath.js @@ -0,0 +1,15 @@ +export default (pre, post) => { + let url = null + + if (process.platform === "darwin") { + url = `${pre}/mac/${process.arch}/${post}` + } + else if (process.platform === "win32") { + url = `${pre}/win/${process.arch}/${post}` + } + else { + url = `${pre}/linux/${process.arch}/${post}` + } + + return url +} \ No newline at end of file diff --git a/packages/core/src/vars.js b/packages/core/src/vars.js new file mode 100644 index 0000000..3fc23fc --- /dev/null +++ b/packages/core/src/vars.js @@ -0,0 +1,35 @@ +import path from "node:path" +import upath from "upath" + +const isWin = process.platform.includes("win") +const isMac = process.platform.includes("darwin") + +const runtimeName = "rs-relic" + +const userdata_path = upath.normalizeSafe(path.resolve( + process.env.APPDATA || + (process.platform == "darwin" ? process.env.HOME + "/Library/Preferences" : process.env.HOME + "/.local/share"), +)) +const runtime_path = upath.normalizeSafe(path.join(userdata_path, runtimeName)) +const cache_path = upath.normalizeSafe(path.join(runtime_path, "cache")) +const packages_path = upath.normalizeSafe(path.join(runtime_path, "packages")) +const binaries_path = upath.normalizeSafe(path.resolve(runtime_path, "binaries")) +const db_path = upath.normalizeSafe(path.resolve(runtime_path, "db.json")) + +const binaries = { + sevenzip_bin: upath.normalizeSafe(path.resolve(binaries_path, "7z-bin", isWin ? "7za.exe" : "7za")), + git_bin: upath.normalizeSafe(path.resolve(binaries_path, "git-bin", "bin", isWin ? "git.exe" : "git")), + rclone_bin: upath.normalizeSafe(path.resolve(binaries_path, "rclone-bin", isWin ? "rclone.exe" : "rclone")), + java_jre_bin: upath.normalizeSafe(path.resolve(binaries_path, "java_jre_bin", (isMac ? "Contents/Home/bin/java" : (isWin ? "bin/java.exe" : "bin/java")))), +} + +export default { + runtimeName, + db_path, + userdata_path, + runtime_path, + cache_path, + packages_path, + binaries_path, + ...binaries, +} \ No newline at end of file diff --git a/electron-builder.yml b/packages/gui/electron-builder.yml similarity index 100% rename from electron-builder.yml rename to packages/gui/electron-builder.yml diff --git a/electron.vite.config.js b/packages/gui/electron.vite.config.js similarity index 100% rename from electron.vite.config.js rename to packages/gui/electron.vite.config.js diff --git a/packages/gui/package.json b/packages/gui/package.json new file mode 100644 index 0000000..1297121 --- /dev/null +++ b/packages/gui/package.json @@ -0,0 +1,55 @@ +{ + "name": "@ragestudio/relic-gui", + "version": "0.17.0", + "description": "RageStudio Relic, yet another package manager.", + "main": "./out/main/index.js", + "author": "RageStudio", + "license": "MIT", + "scripts": { + "start": "electron-vite preview", + "dev": "npm run build:core && electron-vite dev", + "build": "npm run build:core && electron-vite build", + "postinstall": "electron-builder install-app-deps", + "pack:win": "electron-builder --win --config", + "pack:mac": "electron-builder --mac --config", + "pack:linux": "electron-builder --linux --config", + "build:win": "npm run build && npm run pack:win", + "build:mac": "npm run build && npm run pack:mac", + "build:linux": "npm run build && npm run pack:linux", + "build:core": "cd ../core && npm run build:swc" + }, + "dependencies": { + "@electron-toolkit/preload": "^2.0.0", + "@electron-toolkit/utils": "^2.0.0", + "@getstation/electron-google-oauth2": "^14.0.0", + "@imjs/electron-differential-updater": "^5.1.7", + "@loadable/component": "^5.16.3", + "@ragestudio/relic-core": "^0.17.0", + "antd": "^5.13.2", + "classnames": "^2.3.2", + "electron-differential-updater": "^4.3.2", + "electron-is-dev": "^2.0.0", + "electron-store": "^8.1.0", + "electron-updater": "^6.1.1", + "got": "11.8.3", + "human-format": "^1.2.0", + "protocol-registry": "^1.4.1", + "less": "^4.2.0", + "lodash": "^4.17.21", + "react-icons": "^4.11.0", + "react-motion": "0.5.2", + "react-router-dom": "6.6.2", + "react-spinners": "^0.13.8", + "react-spring": "^9.7.3" + }, + "devDependencies": { + "@ragestudio/hermes": "^0.1.1", + "@vitejs/plugin-react": "^4.0.4", + "electron": "25.6.0", + "electron-builder": "24.6.3", + "electron-vite": "^2.1.0", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "vite": "^4.4.9" + } +} diff --git a/resources/icon.ico b/packages/gui/resources/icon.ico similarity index 100% rename from resources/icon.ico rename to packages/gui/resources/icon.ico diff --git a/resources/icon.png b/packages/gui/resources/icon.png similarity index 100% rename from resources/icon.png rename to packages/gui/resources/icon.png diff --git a/resources/icon.svg b/packages/gui/resources/icon.svg similarity index 100% rename from resources/icon.svg rename to packages/gui/resources/icon.svg diff --git a/src/main/classes/CoreAdapter.js b/packages/gui/src/main/classes/CoreAdapter.js similarity index 100% rename from src/main/classes/CoreAdapter.js rename to packages/gui/src/main/classes/CoreAdapter.js diff --git a/src/main/index.js b/packages/gui/src/main/index.js similarity index 98% rename from src/main/index.js rename to packages/gui/src/main/index.js index 7a5a017..b8fc8e8 100644 --- a/src/main/index.js +++ b/packages/gui/src/main/index.js @@ -2,12 +2,6 @@ global.SettingsStore = new Store({ name: "settings", watch: true, }) - -const RelicCore = require("@ragestudio/relic-core").default -import CoreAdapter from "./classes/CoreAdapter" - -import sendToRender from "./utils/sendToRender" - import path from "node:path" import { app, shell, BrowserWindow, ipcMain } from "electron" @@ -15,6 +9,16 @@ import { electronApp, optimizer, is } from "@electron-toolkit/utils" import isDev from "electron-is-dev" import Store from "electron-store" +let RelicCore = null + +if (isDev) { + RelicCore = require("../../../core").default +} else { + RelicCore = require("@ragestudio/relic-core").default +} + +import CoreAdapter from "./classes/CoreAdapter" +import sendToRender from "./utils/sendToRender" import pkg from "../../package.json" const { autoUpdater } = require("electron-differential-updater") diff --git a/src/main/utils/sendToRender.js b/packages/gui/src/main/utils/sendToRender.js similarity index 100% rename from src/main/utils/sendToRender.js rename to packages/gui/src/main/utils/sendToRender.js diff --git a/src/preload/index.js b/packages/gui/src/preload/index.js similarity index 100% rename from src/preload/index.js rename to packages/gui/src/preload/index.js diff --git a/src/renderer/assets/bruh_fox.jpg b/packages/gui/src/renderer/assets/bruh_fox.jpg similarity index 100% rename from src/renderer/assets/bruh_fox.jpg rename to packages/gui/src/renderer/assets/bruh_fox.jpg diff --git a/src/renderer/assets/icon.jsx b/packages/gui/src/renderer/assets/icon.jsx similarity index 100% rename from src/renderer/assets/icon.jsx rename to packages/gui/src/renderer/assets/icon.jsx diff --git a/src/renderer/config/paths_decorators.js b/packages/gui/src/renderer/config/paths_decorators.js similarity index 100% rename from src/renderer/config/paths_decorators.js rename to packages/gui/src/renderer/config/paths_decorators.js diff --git a/src/renderer/index.html b/packages/gui/src/renderer/index.html similarity index 100% rename from src/renderer/index.html rename to packages/gui/src/renderer/index.html diff --git a/src/renderer/src/App.jsx b/packages/gui/src/renderer/src/App.jsx similarity index 100% rename from src/renderer/src/App.jsx rename to packages/gui/src/renderer/src/App.jsx diff --git a/src/renderer/src/GlobalApp.jsx b/packages/gui/src/renderer/src/GlobalApp.jsx similarity index 100% rename from src/renderer/src/GlobalApp.jsx rename to packages/gui/src/renderer/src/GlobalApp.jsx diff --git a/src/renderer/src/components/Crash/index.jsx b/packages/gui/src/renderer/src/components/Crash/index.jsx similarity index 100% rename from src/renderer/src/components/Crash/index.jsx rename to packages/gui/src/renderer/src/components/Crash/index.jsx diff --git a/src/renderer/src/components/Crash/index.less b/packages/gui/src/renderer/src/components/Crash/index.less similarity index 100% rename from src/renderer/src/components/Crash/index.less rename to packages/gui/src/renderer/src/components/Crash/index.less diff --git a/src/renderer/src/components/Icons/index.jsx b/packages/gui/src/renderer/src/components/Icons/index.jsx similarity index 100% rename from src/renderer/src/components/Icons/index.jsx rename to packages/gui/src/renderer/src/components/Icons/index.jsx diff --git a/src/renderer/src/components/InstallConfigAsk/index.jsx b/packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx similarity index 100% rename from src/renderer/src/components/InstallConfigAsk/index.jsx rename to packages/gui/src/renderer/src/components/InstallConfigAsk/index.jsx diff --git a/src/renderer/src/components/InstallConfigAsk/index.less b/packages/gui/src/renderer/src/components/InstallConfigAsk/index.less similarity index 100% rename from src/renderer/src/components/InstallConfigAsk/index.less rename to packages/gui/src/renderer/src/components/InstallConfigAsk/index.less diff --git a/src/renderer/src/components/ManifestInfo/index.jsx b/packages/gui/src/renderer/src/components/ManifestInfo/index.jsx similarity index 100% rename from src/renderer/src/components/ManifestInfo/index.jsx rename to packages/gui/src/renderer/src/components/ManifestInfo/index.jsx diff --git a/src/renderer/src/components/ManifestInfo/index.less b/packages/gui/src/renderer/src/components/ManifestInfo/index.less similarity index 100% rename from src/renderer/src/components/ManifestInfo/index.less rename to packages/gui/src/renderer/src/components/ManifestInfo/index.less diff --git a/src/renderer/src/components/NewInstallation/index.jsx b/packages/gui/src/renderer/src/components/NewInstallation/index.jsx similarity index 100% rename from src/renderer/src/components/NewInstallation/index.jsx rename to packages/gui/src/renderer/src/components/NewInstallation/index.jsx diff --git a/src/renderer/src/components/NewInstallation/index.less b/packages/gui/src/renderer/src/components/NewInstallation/index.less similarity index 100% rename from src/renderer/src/components/NewInstallation/index.less rename to packages/gui/src/renderer/src/components/NewInstallation/index.less diff --git a/src/renderer/src/components/PackageConfigItem/index.jsx b/packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx similarity index 100% rename from src/renderer/src/components/PackageConfigItem/index.jsx rename to packages/gui/src/renderer/src/components/PackageConfigItem/index.jsx diff --git a/src/renderer/src/components/PackageItem/index.jsx b/packages/gui/src/renderer/src/components/PackageItem/index.jsx similarity index 100% rename from src/renderer/src/components/PackageItem/index.jsx rename to packages/gui/src/renderer/src/components/PackageItem/index.jsx diff --git a/src/renderer/src/components/PackageItem/index.less b/packages/gui/src/renderer/src/components/PackageItem/index.less similarity index 100% rename from src/renderer/src/components/PackageItem/index.less rename to packages/gui/src/renderer/src/components/PackageItem/index.less diff --git a/src/renderer/src/components/PackageUpdateAvailable/index.jsx b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx similarity index 100% rename from src/renderer/src/components/PackageUpdateAvailable/index.jsx rename to packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.jsx diff --git a/src/renderer/src/components/PackageUpdateAvailable/index.less b/packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less similarity index 100% rename from src/renderer/src/components/PackageUpdateAvailable/index.less rename to packages/gui/src/renderer/src/components/PackageUpdateAvailable/index.less diff --git a/src/renderer/src/components/Splash/index.jsx b/packages/gui/src/renderer/src/components/Splash/index.jsx similarity index 100% rename from src/renderer/src/components/Splash/index.jsx rename to packages/gui/src/renderer/src/components/Splash/index.jsx diff --git a/src/renderer/src/components/Splash/index.less b/packages/gui/src/renderer/src/components/Splash/index.less similarity index 100% rename from src/renderer/src/components/Splash/index.less rename to packages/gui/src/renderer/src/components/Splash/index.less diff --git a/src/renderer/src/contexts/global.js b/packages/gui/src/renderer/src/contexts/global.js similarity index 100% rename from src/renderer/src/contexts/global.js rename to packages/gui/src/renderer/src/contexts/global.js diff --git a/src/renderer/src/contexts/packages.jsx b/packages/gui/src/renderer/src/contexts/packages.jsx similarity index 100% rename from src/renderer/src/contexts/packages.jsx rename to packages/gui/src/renderer/src/contexts/packages.jsx diff --git a/src/renderer/src/layout/components/Drawer/index.jsx b/packages/gui/src/renderer/src/layout/components/Drawer/index.jsx similarity index 100% rename from src/renderer/src/layout/components/Drawer/index.jsx rename to packages/gui/src/renderer/src/layout/components/Drawer/index.jsx diff --git a/src/renderer/src/layout/components/Header/index.jsx b/packages/gui/src/renderer/src/layout/components/Header/index.jsx similarity index 100% rename from src/renderer/src/layout/components/Header/index.jsx rename to packages/gui/src/renderer/src/layout/components/Header/index.jsx diff --git a/src/renderer/src/layout/components/Header/index.less b/packages/gui/src/renderer/src/layout/components/Header/index.less similarity index 100% rename from src/renderer/src/layout/components/Header/index.less rename to packages/gui/src/renderer/src/layout/components/Header/index.less diff --git a/src/renderer/src/layout/components/ModalDialog/index.jsx b/packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx similarity index 100% rename from src/renderer/src/layout/components/ModalDialog/index.jsx rename to packages/gui/src/renderer/src/layout/components/ModalDialog/index.jsx diff --git a/src/renderer/src/layout/index.jsx b/packages/gui/src/renderer/src/layout/index.jsx similarity index 100% rename from src/renderer/src/layout/index.jsx rename to packages/gui/src/renderer/src/layout/index.jsx diff --git a/src/renderer/src/main.jsx b/packages/gui/src/renderer/src/main.jsx similarity index 100% rename from src/renderer/src/main.jsx rename to packages/gui/src/renderer/src/main.jsx diff --git a/src/renderer/src/pages/index.jsx b/packages/gui/src/renderer/src/pages/index.jsx similarity index 100% rename from src/renderer/src/pages/index.jsx rename to packages/gui/src/renderer/src/pages/index.jsx diff --git a/src/renderer/src/pages/index.less b/packages/gui/src/renderer/src/pages/index.less similarity index 100% rename from src/renderer/src/pages/index.less rename to packages/gui/src/renderer/src/pages/index.less diff --git a/src/renderer/src/pages/pkg/[pkg_id].jsx b/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx similarity index 100% rename from src/renderer/src/pages/pkg/[pkg_id].jsx rename to packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx diff --git a/src/renderer/src/pages/pkg/index.less b/packages/gui/src/renderer/src/pages/pkg/index.less similarity index 100% rename from src/renderer/src/pages/pkg/index.less rename to packages/gui/src/renderer/src/pages/pkg/index.less diff --git a/src/renderer/src/pages/settings/index.jsx b/packages/gui/src/renderer/src/pages/settings/index.jsx similarity index 100% rename from src/renderer/src/pages/settings/index.jsx rename to packages/gui/src/renderer/src/pages/settings/index.jsx diff --git a/src/renderer/src/pages/settings/index.less b/packages/gui/src/renderer/src/pages/settings/index.less similarity index 100% rename from src/renderer/src/pages/settings/index.less rename to packages/gui/src/renderer/src/pages/settings/index.less diff --git a/src/renderer/src/router.jsx b/packages/gui/src/renderer/src/router.jsx similarity index 100% rename from src/renderer/src/router.jsx rename to packages/gui/src/renderer/src/router.jsx diff --git a/src/renderer/src/settings_list.jsx b/packages/gui/src/renderer/src/settings_list.jsx similarity index 100% rename from src/renderer/src/settings_list.jsx rename to packages/gui/src/renderer/src/settings_list.jsx diff --git a/src/renderer/src/style/fix.less b/packages/gui/src/renderer/src/style/fix.less similarity index 100% rename from src/renderer/src/style/fix.less rename to packages/gui/src/renderer/src/style/fix.less diff --git a/src/renderer/src/style/index.less b/packages/gui/src/renderer/src/style/index.less similarity index 100% rename from src/renderer/src/style/index.less rename to packages/gui/src/renderer/src/style/index.less diff --git a/src/renderer/src/style/reset.css b/packages/gui/src/renderer/src/style/reset.css similarity index 100% rename from src/renderer/src/style/reset.css rename to packages/gui/src/renderer/src/style/reset.css diff --git a/src/renderer/src/style/vars.less b/packages/gui/src/renderer/src/style/vars.less similarity index 100% rename from src/renderer/src/style/vars.less rename to packages/gui/src/renderer/src/style/vars.less diff --git a/src/renderer/src/utils/getRootCssVar/index.js b/packages/gui/src/renderer/src/utils/getRootCssVar/index.js similarity index 100% rename from src/renderer/src/utils/getRootCssVar/index.js rename to packages/gui/src/renderer/src/utils/getRootCssVar/index.js diff --git a/src/renderer/src/utils/getVersions/index.js b/packages/gui/src/renderer/src/utils/getVersions/index.js similarity index 100% rename from src/renderer/src/utils/getVersions/index.js rename to packages/gui/src/renderer/src/utils/getVersions/index.js diff --git a/relic-core b/relic-core deleted file mode 160000 index 5da2cb0..0000000 --- a/relic-core +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 5da2cb0765d75f51222414b3e7e690e0d74d408d diff --git a/scripts/postinstall.js b/scripts/postinstall.js new file mode 100644 index 0000000..f29056b --- /dev/null +++ b/scripts/postinstall.js @@ -0,0 +1,35 @@ +const path = require("path") +const child_process = require("child_process") + +const packagesPath = path.resolve(__dirname, "..", "packages") + +const linkRoot = path.resolve(packagesPath, "core") + +const linkPackages = [ + path.resolve(packagesPath, "cli"), + path.resolve(packagesPath, "gui"), +] + +async function main() { + console.log(`Linking @core to other packages...`) + + const rootPkg = require(path.resolve(linkRoot, "package.json")) + + await child_process.execSync("yarn link", { + cwd: linkRoot, + stdio: "inherit", + stdout: "inherit", + }) + + for (const linkPackage of linkPackages) { + await child_process.execSync(`yarn link "${rootPkg.name}"`, { + cwd: linkPackage, + stdio: "inherit", + stdout: "inherit", + }) + } + + console.log(`Done!`) +} + +main() \ No newline at end of file From e187a499475aa9c15198f9a91c4bcf00bb553d29 Mon Sep 17 00:00:00 2001 From: SrGooglo Date: Tue, 2 Apr 2024 22:04:44 +0200 Subject: [PATCH 12/14] merge from local --- packages/core/src/generic_steps/git_clone.js | 1 + packages/core/src/handlers/apply.js | 1 + packages/core/src/handlers/execute.js | 14 ++ packages/core/src/handlers/install.js | 7 +- .../core/src/handlers/lastOperationRetry.js | 76 +++++++++ packages/core/src/handlers/uninstall.js | 27 +++- packages/core/src/handlers/update.js | 13 +- packages/core/src/index.js | 4 +- packages/core/src/logger.js | 13 +- packages/core/src/manifest/reader.js | 9 ++ packages/gui/src/main/classes/CoreAdapter.js | 105 ++++++++++++- packages/gui/src/main/index.js | 146 ++++++++---------- packages/gui/src/main/utils/sendToRender.js | 2 +- packages/gui/src/renderer/src/App.jsx | 53 +++++-- .../renderer/src/components/Crash/index.less | 2 + .../src/components/PackageItem/index.jsx | 35 +++-- .../gui/src/renderer/src/pages/logs/index.jsx | 67 ++++++++ .../src/renderer/src/pages/logs/index.less | 47 ++++++ .../src/renderer/src/pages/pkg/[pkg_id].jsx | 2 - packages/gui/src/renderer/src/router.jsx | 19 +-- .../gui/src/renderer/src/settings_list.jsx | 10 +- 21 files changed, 507 insertions(+), 146 deletions(-) create mode 100644 packages/core/src/handlers/lastOperationRetry.js create mode 100644 packages/gui/src/renderer/src/pages/logs/index.jsx create mode 100644 packages/gui/src/renderer/src/pages/logs/index.less diff --git a/packages/core/src/generic_steps/git_clone.js b/packages/core/src/generic_steps/git_clone.js index da0a3c2..1b857c8 100644 --- a/packages/core/src/generic_steps/git_clone.js +++ b/packages/core/src/generic_steps/git_clone.js @@ -33,6 +33,7 @@ export default async (pkg, step) => { //`--depth ${step.depth ?? 1}`, //"--filter=blob:none", //"--filter=tree:0", + "--progress", "--recurse-submodules", "--remote-submodules", step.url, diff --git a/packages/core/src/handlers/apply.js b/packages/core/src/handlers/apply.js index 7b9f47e..1270b84 100644 --- a/packages/core/src/handlers/apply.js +++ b/packages/core/src/handlers/apply.js @@ -83,6 +83,7 @@ export default async function apply(pkg_id, changes = {}) { return pkg } catch (error) { global._relic_eventBus.emit(`pkg:error`, { + event: "apply", id: pkg_id, error }) diff --git a/packages/core/src/handlers/execute.js b/packages/core/src/handlers/execute.js index 4351d24..b22684b 100644 --- a/packages/core/src/handlers/execute.js +++ b/packages/core/src/handlers/execute.js @@ -19,6 +19,20 @@ export default async function execute(pkg_id, { useRemote = false, force = false return false } + if (pkg.last_status !== "installed") { + if (!force) { + BaseLog.info(`Package not installed [${pkg_id}], aborting execution`) + + global._relic_eventBus.emit(`pkg:error`, { + id: pkg_id, + event: "execute", + error: new Error("Package not valid or not installed"), + }) + + return false + } + } + const manifestPath = useRemote ? pkg.remote_manifest : pkg.local_manifest if (!fs.existsSync(manifestPath)) { diff --git a/packages/core/src/handlers/install.js b/packages/core/src/handlers/install.js index faf0919..e94274c 100644 --- a/packages/core/src/handlers/install.js +++ b/packages/core/src/handlers/install.js @@ -164,12 +164,13 @@ export default async function install(manifest) { return pkg } catch (error) { global._relic_eventBus.emit(`pkg:error`, { - id: id, - error + id: id ?? manifest.constructor.id, + event: "install", + error, }) global._relic_eventBus.emit(`pkg:update:state`, { - id: id, + id: id ?? manifest.constructor.id, last_status: "failed", status_text: `Installation failed`, }) diff --git a/packages/core/src/handlers/lastOperationRetry.js b/packages/core/src/handlers/lastOperationRetry.js new file mode 100644 index 0000000..9da6b9b --- /dev/null +++ b/packages/core/src/handlers/lastOperationRetry.js @@ -0,0 +1,76 @@ +import fs from "node:fs" +import path from "node:path" + +import Logger from "../logger" +import DB from "../db" + +import PackageInstall from "./install" +import PackageUpdate from "./update" +import PackageUninstall from "./uninstall" + +import Vars from "../vars" + +export default async function lastOperationRetry(pkg_id) { + try { + const Log = Logger.child({ service: `OPERATION_RETRY|${pkg_id}` }) + const pkg = await DB.getPackages(pkg_id) + + if (!pkg) { + Log.error(`This package doesn't exist`) + return null + } + + Log.info(`Try performing last operation retry...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing last operation retry...`, + }) + + switch (pkg.last_status) { + case "installing": + await PackageInstall(pkg.local_manifest) + break + case "updating": + await PackageUpdate(pkg_id) + break + case "uninstalling": + await PackageUninstall(pkg_id) + break + case "failed": { + // copy pkg.local_manifest to cache after uninstall + const cachedManifest = path.join(Vars.cache_path, path.basename(pkg.local_manifest)) + + await fs.promises.copyFile(pkg.local_manifest, cachedManifest) + + await PackageUninstall(pkg_id) + await PackageInstall(cachedManifest) + break + } + default: { + Log.error(`Invalid last status: ${pkg.last_status}`) + + global._relic_eventBus.emit(`pkg:error`, { + id: pkg.id, + event: "retrying last operation", + status_text: `Performing last operation retry...`, + }) + + return null + } + } + + return pkg + } catch (error) { + Logger.error(`Failed to perform last operation retry of [${pkg_id}]`) + Logger.error(error) + + global._relic_eventBus.emit(`pkg:error`, { + event: "retrying last operation", + id: pkg_id, + error: error, + }) + + return null + } +} \ No newline at end of file diff --git a/packages/core/src/handlers/uninstall.js b/packages/core/src/handlers/uninstall.js index b0ea282..f2c0984 100644 --- a/packages/core/src/handlers/uninstall.js +++ b/packages/core/src/handlers/uninstall.js @@ -20,21 +20,33 @@ export default async function uninstall(pkg_id) { const Log = Logger.child({ service: `UNINSTALLER|${pkg.id}` }) Log.info(`Uninstalling package...`) + global._relic_eventBus.emit(`pkg:update:state`, { id: pkg.id, status_text: `Uninstalling package...`, }) - const ManifestRead = await ManifestReader(pkg.local_manifest) - const manifest = await ManifestVM(ManifestRead.code) + try { + const ManifestRead = await ManifestReader(pkg.local_manifest) + const manifest = await ManifestVM(ManifestRead.code) + + if (typeof manifest.uninstall === "function") { + Log.info(`Performing uninstall hook...`) + + global._relic_eventBus.emit(`pkg:update:state`, { + id: pkg.id, + status_text: `Performing uninstall hook...`, + }) - if (typeof manifest.uninstall === "function") { - Log.info(`Performing uninstall hook...`) - global._relic_eventBus.emit(`pkg:update:state`, { + await manifest.uninstall(pkg) + } + } catch (error) { + Log.error(`Failed to perform uninstall hook`, error) + global._relic_eventBus.emit(`pkg:error`, { + event: "uninstall", id: pkg.id, - status_text: `Performing uninstall hook...`, + error }) - await manifest.uninstall(pkg) } Log.info(`Deleting package directory...`) @@ -62,6 +74,7 @@ export default async function uninstall(pkg_id) { return pkg } catch (error) { global._relic_eventBus.emit(`pkg:error`, { + event: "uninstall", id: pkg_id, error }) diff --git a/packages/core/src/handlers/update.js b/packages/core/src/handlers/update.js index 87d4ce0..ac7ca9d 100644 --- a/packages/core/src/handlers/update.js +++ b/packages/core/src/handlers/update.js @@ -116,10 +116,21 @@ export default async function update(pkg_id) { return pkg } catch (error) { global._relic_eventBus.emit(`pkg:error`, { + event: "update", id: pkg_id, - error + error, + last_status: "failed" }) + try { + await DB.updatePackageById(pkg_id, { + last_status: "failed", + }) + } catch (error) { + BaseLog.error(`Failed to update status of pkg [${pkg_id}]`) + BaseLog.error(error.stack) + } + BaseLog.error(`Failed to update package [${pkg_id}]`, error) BaseLog.error(error.stack) diff --git a/packages/core/src/index.js b/packages/core/src/index.js index de39263..5440832 100644 --- a/packages/core/src/index.js +++ b/packages/core/src/index.js @@ -18,6 +18,7 @@ import PackageList from "./handlers/list" import PackageRead from "./handlers/read" import PackageAuthorize from "./handlers/authorize" import PackageCheckUpdate from "./handlers/checkUpdate" +import PackageLastOperationRetry from "./handlers/lastOperationRetry" export default class RelicCore { constructor(params) { @@ -55,7 +56,8 @@ export default class RelicCore { list: PackageList, read: PackageRead, authorize: PackageAuthorize, - checkUpdate: PackageCheckUpdate + checkUpdate: PackageCheckUpdate, + lastOperationRetry: PackageLastOperationRetry, } openPath(pkg_id) { diff --git a/packages/core/src/logger.js b/packages/core/src/logger.js index 1f3d6d1..12bbc1a 100644 --- a/packages/core/src/logger.js +++ b/packages/core/src/logger.js @@ -1,4 +1,5 @@ import winston from "winston" +import WinstonTransport from "winston-transport" import colors from "cli-color" const servicesToColor = { @@ -6,10 +7,6 @@ const servicesToColor = { color: "whiteBright", background: "bgBlackBright", }, - "INSTALL": { - color: "whiteBright", - background: "bgBlueBright", - }, } const paintText = (level, service, ...args) => { @@ -27,6 +24,13 @@ const format = winston.format.printf(({ timestamp, service = "CORE", level, mess return `${paintText(level, service, `(${level}) [${service}]`)} > ${message}` }) +class EventBusTransport extends WinstonTransport { + log(info, next) { + global._relic_eventBus.emit(`logger:new`, info) + next() + } +} + export default winston.createLogger({ format: winston.format.combine( winston.format.timestamp(), @@ -34,6 +38,7 @@ export default winston.createLogger({ ), transports: [ new winston.transports.Console(), + new EventBusTransport(), //new winston.transports.File({ filename: "error.log", level: "error" }), //new winston.transports.File({ filename: "combined.log" }), ], diff --git a/packages/core/src/manifest/reader.js b/packages/core/src/manifest/reader.js index 830278f..a34915f 100644 --- a/packages/core/src/manifest/reader.js +++ b/packages/core/src/manifest/reader.js @@ -39,6 +39,15 @@ export async function readManifest(manifest) { throw new Error(`Manifest is not a file: ${target}`) } + // copy to cache + const cachedManifest = path.join(Vars.cache_path, path.basename(target)) + + await fs.promises.copyFile(target, cachedManifest) + + if (!fs.existsSync(cachedManifest)) { + throw new Error(`Manifest copy failed: ${target}`) + } + return { remote_manifest: undefined, local_manifest: target, diff --git a/packages/gui/src/main/classes/CoreAdapter.js b/packages/gui/src/main/classes/CoreAdapter.js index 9fa931d..12be69a 100644 --- a/packages/gui/src/main/classes/CoreAdapter.js +++ b/packages/gui/src/main/classes/CoreAdapter.js @@ -1,14 +1,76 @@ import sendToRender from "../utils/sendToRender" +import { ipcMain } from "electron" export default class CoreAdapter { constructor(electronApp, RelicCore) { this.app = electronApp this.core = RelicCore + this.initialized = false + } + + loggerWindow = null + + ipcEvents = { + "pkg:list": async () => { + return await this.core.package.list() + }, + "pkg:get": async (event, pkg_id) => { + return await this.core.db.getPackages(pkg_id) + }, + "pkg:read": async (event, manifest_path, options = {}) => { + const manifest = await this.core.package.read(manifest_path, options) + + return JSON.stringify({ + ...this.core.db.defaultPackageState({ ...manifest }), + ...manifest, + name: manifest.pkg_name, + }) + }, + "pkg:install": async (event, manifest_path) => { + return await this.core.package.install(manifest_path) + }, + "pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => { + await this.core.package.update(pkg_id) - this.initialize() + if (execOnFinish) { + await this.core.package.execute(pkg_id) + } + + return true + }, + "pkg:apply": async (event, pkg_id, changes) => { + return await this.core.package.apply(pkg_id, changes) + }, + "pkg:uninstall": async (event, pkg_id) => { + return await this.core.package.uninstall(pkg_id) + }, + "pkg:execute": async (event, pkg_id, { force = false } = {}) => { + // check for updates first + if (!force) { + const update = await this.core.package.checkUpdate(pkg_id) + + if (update) { + return sendToRender("pkg:update_available", update) + } + } + + return await this.core.package.execute(pkg_id) + }, + "pkg:open": async (event, pkg_id) => { + return await this.core.openPath(pkg_id) + }, + "pkg:last_operation_retry": async (event, pkg_id) => { + return await this.core.package.lastOperationRetry(pkg_id) + }, + "pkg:cancel_current_operation": async (event, pkg_id) => { + return await this.core.package.cancelCurrentOperation(pkg_id) + }, + "core:open-path": async (event, pkg_id) => { + return await this.core.openPath(pkg_id) + }, } - events = { + coreEvents = { "pkg:new": (pkg) => { sendToRender("pkg:new", pkg) }, @@ -51,22 +113,53 @@ export default class CoreAdapter { sendToRender(`new:notification`, { type: "error", message: `An error occurred`, - description: `Something failed to ${data.event} package ${data.pkg_id}`, + description: `Something failed to ${data.event} package ${data.id}`, }) sendToRender(`pkg:update:state`, data) + }, + "logger:new": (data) => { + if (this.loggerWindow) { + this.loggerWindow.webContents.send("logger:new", data) + } } } - initialize = () => { - for (const [key, handler] of Object.entries(this.events)) { + attachLogger = (window) => { + this.loggerWindow = window + + window.webContents.send("logger:new", { + timestamp: new Date().getTime(), + message: "Core adapter Logger attached", + }) + } + + initialize = async () => { + if (this.initialized) { + return + } + + for (const [key, handler] of Object.entries(this.coreEvents)) { global._relic_eventBus.on(key, handler) } + + for (const [key, handler] of Object.entries(this.ipcEvents)) { + ipcMain.handle(key, handler) + } + + await this.core.initialize() + await this.core.setup() + + this.initialized = true } deinitialize = () => { - for (const [key, handler] of Object.entries(this.events)) { + for (const [key, handler] of Object.entries(this.coreEvents)) { global._relic_eventBus.off(key, handler) } + + for (const [key, handler] of Object.entries(this.ipcEvents)) { + ipcMain.removeHandler(key, handler) + } } } \ No newline at end of file diff --git a/packages/gui/src/main/index.js b/packages/gui/src/main/index.js index b8fc8e8..42bd7c2 100644 --- a/packages/gui/src/main/index.js +++ b/packages/gui/src/main/index.js @@ -26,62 +26,54 @@ const ProtocolRegistry = require("protocol-registry") const protocolRegistryNamespace = "relic" +class LogsViewer { + window = null + + async createWindow() { + this.window = new BrowserWindow({ + width: 800, + height: 600, + show: false, + resizable: true, + autoHideMenuBar: true, + icon: "../../resources/icon.png", + webPreferences: { + preload: path.join(__dirname, "../preload/index.js"), + sandbox: false, + }, + }) + + if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { + this.window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}/logs`) + } else { + this.window.loadFile(path.join(__dirname, "../renderer/index.html")) + } + + await new Promise((resolve) => this.window.once("ready-to-show", resolve)) + + this.window.show() + + return this.window + } + + closeWindow() { + if (this.window) { + this.window.close() + } + } +} + class ElectronApp { constructor() { - this.win = null this.core = new RelicCore() this.adapter = new CoreAdapter(this, this.core) } - handlers = { - "pkg:list": async () => { - return await this.core.package.list() - }, - "pkg:get": async (event, pkg_id) => { - return await this.core.db.getPackages(pkg_id) - }, - "pkg:read": async (event, manifest_path, options = {}) => { - const manifest = await this.core.package.read(manifest_path, options) + window = null - return JSON.stringify({ - ...this.core.db.defaultPackageState({ ...manifest }), - ...manifest, - name: manifest.pkg_name, - }) - }, - "pkg:install": async (event, manifest_path) => { - return await this.core.package.install(manifest_path) - }, - "pkg:update": async (event, pkg_id, { execOnFinish = false } = {}) => { - await this.core.package.update(pkg_id) - - if (execOnFinish) { - await this.core.package.execute(pkg_id) - } - - return true - }, - "pkg:apply": async (event, pkg_id, changes) => { - return await this.core.package.apply(pkg_id, changes) - }, - "pkg:uninstall": async (event, pkg_id) => { - return await this.core.package.uninstall(pkg_id) - }, - "pkg:execute": async (event, pkg_id, { force = false } = {}) => { - // check for updates first - if (!force) { - const update = await this.core.package.checkUpdate(pkg_id) - - if (update) { - return sendToRender("pkg:update_available", update) - } - } + logsViewer = new LogsViewer() - return await this.core.package.execute(pkg_id) - }, - "pkg:open": async (event, pkg_id) => { - return await this.core.openPath(pkg_id) - }, + handlers = { "updater:check": () => { autoUpdater.checkForUpdates() }, @@ -90,22 +82,31 @@ class ElectronApp { autoUpdater.quitAndInstall() }, 3000) }, - "settings:get": (e, key) => { + "settings:get": (event, key) => { return global.SettingsStore.get(key) }, - "settings:set": (e, key, value) => { + "settings:set": (event, key, value) => { return global.SettingsStore.set(key, value) }, - "settings:delete": (e, key) => { + "settings:delete": (event, key) => { return global.SettingsStore.delete(key) }, - "settings:has": (e, key) => { + "settings:has": (event, key) => { return global.SettingsStore.has(key) }, + "app:open-logs": async (event) => { + const loggerWindow = await this.logsViewer.createWindow() + + this.adapter.attachLogger(loggerWindow) + + loggerWindow.webContents.send("logger:new", { + timestamp: new Date().getTime(), + message: "Logger opened, starting watching logs", + }) + }, "app:init": async (event, data) => { try { - await this.core.initialize() - await this.core.setup() + await this.adapter.initialize() return { pkg: pkg, @@ -126,19 +127,8 @@ class ElectronApp { } } - events = { - "open-runtime-path": () => { - return this.core.openPath() - }, - "open-dev-logs": () => { - return sendToRender("new:message", { - message: "Not implemented yet", - }) - } - } - createWindow() { - this.win = global.win = new BrowserWindow({ + this.window = global.mainWindow = new BrowserWindow({ width: 450, height: 670, show: false, @@ -151,20 +141,20 @@ class ElectronApp { } }) - this.win.on("ready-to-show", () => { - this.win.show() + this.window.on("ready-to-show", () => { + this.window.show() }) - this.win.webContents.setWindowOpenHandler((details) => { + this.window.webContents.setWindowOpenHandler((details) => { shell.openExternal(details.url) return { action: "deny" } }) if (is.dev && process.env["ELECTRON_RENDERER_URL"]) { - this.win.loadURL(process.env["ELECTRON_RENDERER_URL"]) + this.window.loadURL(process.env["ELECTRON_RENDERER_URL"]) } else { - this.win.loadFile(path.join(__dirname, "../renderer/index.html")) + this.window.loadFile(path.join(__dirname, "../renderer/index.html")) } } @@ -206,12 +196,12 @@ class ElectronApp { event.preventDefault() // Someone tried to run a second instance, we should focus our window. - if (this.win) { - if (this.win.isMinimized()) { - this.win.restore() + if (this.window) { + if (this.window.isMinimized()) { + this.window.restore() } - this.win.focus() + this.window.focus() } console.log(`Second instance >`, commandLine) @@ -235,10 +225,6 @@ class ElectronApp { ipcMain.handle(key, this.handlers[key]) } - for (const key in this.events) { - ipcMain.on(key, this.events[key]) - } - app.on("second-instance", this.handleOnSecondInstance) app.on("open-url", (event, url) => { @@ -308,4 +294,4 @@ class ElectronApp { } } -new ElectronApp().initialize() +new ElectronApp().initialize() \ No newline at end of file diff --git a/packages/gui/src/main/utils/sendToRender.js b/packages/gui/src/main/utils/sendToRender.js index 6fc3784..df4266f 100644 --- a/packages/gui/src/main/utils/sendToRender.js +++ b/packages/gui/src/main/utils/sendToRender.js @@ -32,7 +32,7 @@ export default (event, data) => { return copy } - global.win.webContents.send(event, serializeIpc(data)) + global.mainWindow.webContents.send(event, serializeIpc(data)) } catch (error) { console.error(error) } diff --git a/packages/gui/src/renderer/src/App.jsx b/packages/gui/src/renderer/src/App.jsx index 3323cb8..02f26ee 100644 --- a/packages/gui/src/renderer/src/App.jsx +++ b/packages/gui/src/renderer/src/App.jsx @@ -12,14 +12,19 @@ import AppDrawer from "layout/components/Drawer" import { InternalRouter, PageRender } from "./router.jsx" +import CrashError from "components/Crash" +import LogsViewer from "./pages/logs" + // create a global app context window.app = GlobalApp class App extends React.Component { state = { - initializing: true, pkg: null, + crash: null, + initializing: true, + appSetup: { error: false, installed: false, @@ -98,6 +103,15 @@ class App extends React.Component { console.log(`React version > ${versions["react"]}`) console.log(`DOMRouter version > ${versions["react-router-dom"]}`) + //check if path is /logs + if (window.location.pathname === "/logs") { + return await this.setState({ + initializing: false, + no_layout: true, + log_viewer_mode: true, + }) + } + window.app.style.appendClassname("initializing") for (const event in this.ipcEvents) { @@ -133,17 +147,34 @@ class App extends React.Component { algorithm: antd.theme.darkAlgorithm }} > - - - - - + { + this.state.log_viewer_mode && + } - - - - - + { + !this.state.log_viewer_mode && <> + + + { + !this.state.crash && <> + + + + + + + + } + + { + this.state.crash && + } + + + + } } } diff --git a/packages/gui/src/renderer/src/components/Crash/index.less b/packages/gui/src/renderer/src/components/Crash/index.less index 54aa325..587ef18 100644 --- a/packages/gui/src/renderer/src/components/Crash/index.less +++ b/packages/gui/src/renderer/src/components/Crash/index.less @@ -6,6 +6,8 @@ gap: 20px; + padding: 20px; + h1 { font-size: 1.5rem; font-weight: bold; diff --git a/packages/gui/src/renderer/src/components/PackageItem/index.jsx b/packages/gui/src/renderer/src/components/PackageItem/index.jsx index b0f6bd9..3986322 100644 --- a/packages/gui/src/renderer/src/components/PackageItem/index.jsx +++ b/packages/gui/src/renderer/src/components/PackageItem/index.jsx @@ -14,7 +14,7 @@ const PackageItem = (props) => { const isLoading = manifest.last_status === "loading" || manifest.last_status === "installing" || manifest.last_status === "updating" const isInstalling = manifest.last_status === "installing" const isInstalled = !!manifest.installed_at - const isFailed = manifest.last_status === "error" + const isFailed = manifest.last_status === "failed" console.log(manifest, { isLoading, @@ -38,7 +38,7 @@ const PackageItem = (props) => { } const onClickFolder = () => { - ipc.exec("pkg:open", manifest.id) + ipc.exec("core:open-path", manifest.id) } const onClickDelete = () => { @@ -60,7 +60,7 @@ const PackageItem = (props) => { } const onClickRetryInstall = () => { - ipc.exec("pkg:retry_install", manifest.id) + ipc.exec("pkg:last_operation_retry", manifest.id) } function handleUpdate(event, data) { @@ -75,7 +75,7 @@ const PackageItem = (props) => { return manifest.last_status } - return `v${manifest.version}` ?? "N/A" + return `${isFailed ? "failed |" : ""} v${manifest.version}` ?? "N/A" } const MenuProps = { @@ -148,7 +148,6 @@ const PackageItem = (props) => { manifest.icon && } -

{ @@ -164,16 +163,24 @@ const PackageItem = (props) => {
{ - isFailed && - Retry - + isFailed && <> + + Retry + + + } + type="primary" + onClick={onClickDelete} + /> + } { - isInstalled && manifest.executable && { } { - isInstalled && !manifest.executable && @@ -199,7 +206,7 @@ const PackageItem = (props) => { } { - isInstalling && diff --git a/packages/gui/src/renderer/src/pages/logs/index.jsx b/packages/gui/src/renderer/src/pages/logs/index.jsx new file mode 100644 index 0000000..c494cea --- /dev/null +++ b/packages/gui/src/renderer/src/pages/logs/index.jsx @@ -0,0 +1,67 @@ +import React from "react" + +import "./index.less" + +const Timestamp = ({ timestamp }) => { + if (isNaN(timestamp)) { + return {timestamp} + } + + return + { + new Date(timestamp).toLocaleString().split(", ").join("|") + } + +} + +const LogEntry = ({ log }) => { + return
+ + {">"} + + + {log.timestamp && } + + {!log.timestamp && - no timestamp -} + +

+ {log.message ?? "No message"} +

+
+} + +const LogsViewer = () => { + const listRef = React.useRef() + const [timeline, setTimeline] = React.useState([]) + + const events = { + "logger:new": (event, log) => { + setTimeline((timeline) => [...timeline, log]) + + listRef.current.scrollTop = listRef.current.scrollHeight + } + } + + React.useEffect(() => { + for (const event in events) { + ipc.exclusiveListen(event, events[event]) + } + }, []) + + return
+ { + timeline.length === 0 &&

No logs

+ } + + { + timeline.map((log) => ) + } +
+} + +export default LogsViewer \ No newline at end of file diff --git a/packages/gui/src/renderer/src/pages/logs/index.less b/packages/gui/src/renderer/src/pages/logs/index.less new file mode 100644 index 0000000..74b6c71 --- /dev/null +++ b/packages/gui/src/renderer/src/pages/logs/index.less @@ -0,0 +1,47 @@ +.app-logs { + display: flex; + flex-direction: column; + + padding: 10px; + + font-family: "DM Mono", monospace; + + overflow-x: hidden; + overflow-y: scroll; + + height: 100vh; + + .log-entry { + display: flex; + flex-direction: row; + + align-items: flex-start; + + gap: 7px; + font-size: 0.8rem; + line-height: 0.8rem; + + border-radius: 8px; + + padding: 8px; + + color: var(--text-color); + + span { + color: var(--text-color); + + white-space: nowrap; + word-break: break-all; + } + + .timestamp { + opacity: 0.9; + + font-size: 0.7rem; + } + + &:nth-child(odd) { + background-color: var(--background-color-secondary); + } + } +} \ No newline at end of file diff --git a/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx b/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx index a8cad61..565325b 100644 --- a/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx +++ b/packages/gui/src/renderer/src/pages/pkg/[pkg_id].jsx @@ -307,8 +307,6 @@ const PackageOptionsLoader = (props) => { }) } - console.log(manifest) - if (!manifest) { return } diff --git a/packages/gui/src/renderer/src/router.jsx b/packages/gui/src/renderer/src/router.jsx index 33c696b..599c16e 100644 --- a/packages/gui/src/renderer/src/router.jsx +++ b/packages/gui/src/renderer/src/router.jsx @@ -7,7 +7,6 @@ import loadable from "@loadable/component" import GlobalStateContext from "contexts/global" import SplashScreen from "components/Splash" -import CrashError from "components/Crash" const DefaultNotFoundRender = () => { return
Not found
@@ -131,18 +130,6 @@ export const InternalRouter = (props) => { } export const PageRender = (props) => { - const globalState = React.useContext(GlobalStateContext) - - if (globalState.crash) { - return - } - - if (globalState.initializing) { - return - } - const routes = React.useMemo(() => { let paths = { ...import.meta.glob("/src/pages/**/[a-z[]*.jsx"), @@ -167,6 +154,12 @@ export const PageRender = (props) => { return paths }, []) + const globalState = React.useContext(GlobalStateContext) + + if (globalState.initializing) { + return + } + return { routes.map((route, index) => { diff --git a/packages/gui/src/renderer/src/settings_list.jsx b/packages/gui/src/renderer/src/settings_list.jsx index 1478796..809b799 100644 --- a/packages/gui/src/renderer/src/settings_list.jsx +++ b/packages/gui/src/renderer/src/settings_list.jsx @@ -20,6 +20,7 @@ export default [ render: (props) => { return (