├── .changeset └── config.json ├── .gitattributes ├── .github ├── PULL_REQUEST_TEMPLATE │ └── basic_pull_request.md └── workflows │ ├── ci.yml │ ├── docs.yml │ └── release.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── biome.json ├── docs ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── index.ts │ │ └── style.css ├── package.json └── src │ ├── components │ ├── PackageCard.vue │ ├── PackageHeader.vue │ └── package-versions.data.ts │ ├── guides │ ├── downloading-media.md │ ├── fetching-content.md │ ├── getting-started.md │ └── running-applications.md │ ├── index.md │ ├── public │ └── google39b48f38931b597a.html │ ├── recipes │ ├── custom-content-plugin.md │ ├── custom-content-source.md │ ├── custom-monitor-plugin.md │ ├── static-web-monitor.md │ └── transforming-sanity-images.md │ └── reference │ ├── cli │ ├── commands.md │ ├── config-loading.md │ ├── env.md │ └── index.md │ ├── content │ ├── content-config.md │ ├── data-store.md │ ├── index.md │ ├── plugins │ │ ├── index.md │ │ ├── md-to-html.md │ │ ├── media-downloader.md │ │ ├── sanity-image-url-transform.md │ │ ├── sanity-to-html.md │ │ ├── sanity-to-md.md │ │ ├── sanity-to-plain.md │ │ └── sharp.md │ └── sources │ │ ├── airtable-source.md │ │ ├── contentful-source.md │ │ ├── index.md │ │ ├── json-source.md │ │ ├── sanity-source.md │ │ └── strapi-source.md │ ├── monitor │ ├── index.md │ ├── monitor-config.md │ └── plugins.md │ └── scaffold │ ├── index.md │ └── scaffold-config.md ├── lefthook.yml ├── package-lock.json ├── package.json ├── packages ├── cli │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── cli.ts │ │ ├── commands │ │ │ ├── content.ts │ │ │ ├── monitor.ts │ │ │ ├── scaffold.ts │ │ │ ├── start.ts │ │ │ └── stop.ts │ │ ├── errors.ts │ │ ├── index.ts │ │ ├── launchpad-config.ts │ │ └── utils │ │ │ ├── command-utils.ts │ │ │ ├── config.ts │ │ │ └── env.ts │ └── tsconfig.json ├── content │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ ├── content-integration.test.ts │ │ │ ├── content-plugin-driver.test.ts │ │ │ └── launchpad-content.test.ts │ │ ├── content-config.ts │ │ ├── content-plugin-driver.ts │ │ ├── index.ts │ │ ├── launchpad-content.ts │ │ ├── plugins │ │ │ ├── __tests__ │ │ │ │ ├── md-to-html.test.ts │ │ │ │ ├── media-downloader.test.ts │ │ │ │ ├── plugins.test-utils.ts │ │ │ │ ├── sanity-image-url-transform.test.ts │ │ │ │ ├── sanity-to-html.test.ts │ │ │ │ ├── sanity-to-markdown.test.ts │ │ │ │ ├── sanity-to-plain.test.ts │ │ │ │ └── sharp.test.ts │ │ │ ├── contentPluginHelpers.ts │ │ │ ├── index.ts │ │ │ ├── md-to-html.ts │ │ │ ├── media-downloader.ts │ │ │ ├── sanity-image-url-transform.ts │ │ │ ├── sanity-to-html.ts │ │ │ ├── sanity-to-markdown.ts │ │ │ ├── sanity-to-plain.ts │ │ │ └── sharp.ts │ │ ├── sources │ │ │ ├── __tests__ │ │ │ │ ├── airtable-source.test.ts │ │ │ │ ├── contentful-source.test.ts │ │ │ │ ├── json-source.test.ts │ │ │ │ ├── sanity-source.test.ts │ │ │ │ └── strapi-source.test.ts │ │ │ ├── airtable-source.ts │ │ │ ├── contentful-source.ts │ │ │ ├── index.ts │ │ │ ├── json-source.ts │ │ │ ├── sanity-source.ts │ │ │ ├── source.ts │ │ │ └── strapi-source.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ ├── content-transform-utils.test.ts │ │ │ ├── data-store.test.ts │ │ │ ├── fetch-paginated.test.ts │ │ │ ├── file-utils.test.ts │ │ │ ├── markdown-it-italic-bold.test.ts │ │ │ ├── result-async-queue.test.ts │ │ │ └── safe-ky.test.ts │ │ │ ├── content-transform-utils.ts │ │ │ ├── data-store.ts │ │ │ ├── fetch-logger.ts │ │ │ ├── fetch-paginated.ts │ │ │ ├── file-utils.ts │ │ │ ├── markdown-it-italic-bold.ts │ │ │ ├── result-async-queue.ts │ │ │ └── safe-ky.ts │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ └── vitest.config.ts ├── dashboard │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── launchpad │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ └── index.ts │ └── tsconfig.json ├── monitor │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── __tests__ │ │ │ └── launchpad-monitor.test.ts │ │ ├── core │ │ │ ├── __tests__ │ │ │ │ ├── app-manager.test.ts │ │ │ │ ├── bus-manager.test.ts │ │ │ │ ├── core.test-utils.ts │ │ │ │ ├── monitor-plugin-driver.test.ts │ │ │ │ └── process-manager.test.ts │ │ │ ├── app-manager.ts │ │ │ ├── bus-manager.ts │ │ │ ├── monitor-plugin-driver.ts │ │ │ └── process-manager.ts │ │ ├── index.ts │ │ ├── launchpad-monitor.ts │ │ ├── monitor-config.ts │ │ └── utils │ │ │ ├── __tests__ │ │ │ ├── debounce-results.test.ts │ │ │ └── sort-windows.test.ts │ │ │ ├── debounce-results.ts │ │ │ └── sort-windows.ts │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ └── vitest.config.ts ├── scaffold │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── config │ │ ├── .gitignore │ │ ├── defaults.ps1 │ │ └── presets │ │ │ └── exhibit_power_config.pow │ ├── package.json │ ├── scripts │ │ ├── functions.psm1 │ │ ├── install_choco.ps1 │ │ ├── install_cygwin.ps1 │ │ ├── install_node_dependencies.ps1 │ │ ├── load_config.ps1 │ │ ├── vendor │ │ │ ├── force-mkdir.psm1 │ │ │ ├── powerplan.psm1 │ │ │ └── take-own.psm1 │ │ └── windows │ │ │ ├── clear_desktop_background.ps1 │ │ │ ├── clear_desktop_shortcuts.ps1 │ │ │ ├── config_explorer.ps1 │ │ │ ├── disable_accessibility.ps1 │ │ │ ├── disable_app_installs.ps1 │ │ │ ├── disable_app_restore.ps1 │ │ │ ├── disable_cortana_search.ps1 │ │ │ ├── disable_edge_swipes.ps1 │ │ │ ├── disable_error_reporting.ps1 │ │ │ ├── disable_firewall.ps1 │ │ │ ├── disable_max_path_length.ps1 │ │ │ ├── disable_new_network_window.ps1 │ │ │ ├── disable_news_and_interests.ps1 │ │ │ ├── disable_notifications.ps1 │ │ │ ├── disable_screensaver.ps1 │ │ │ ├── disable_touch_feedback.ps1 │ │ │ ├── disable_touch_gestures.ps1 │ │ │ ├── disable_update_check.ps1 │ │ │ ├── disable_update_service.ps1 │ │ │ ├── disable_win_setup_prompts.ps1 │ │ │ ├── enable_auto_login.ps1 │ │ │ ├── enable_daily_reboot.ps1 │ │ │ ├── enable_firewall.ps1 │ │ │ ├── enable_script_execution.ps1 │ │ │ ├── enable_startup_task.ps1 │ │ │ ├── enable_update_service.ps1 │ │ │ ├── reset_text_scale.ps1 │ │ │ ├── set_computer_name.ps1 │ │ │ ├── set_power_settings.ps1 │ │ │ ├── set_timezone.ps1 │ │ │ ├── uninstall_bloatware.ps1 │ │ │ ├── uninstall_one_drive.ps1 │ │ │ ├── unpin_start_menu_apps.ps1 │ │ │ └── win8_config_startpage.ps1 │ ├── setup.bat │ ├── setup.ps1 │ ├── src │ │ └── index.ts │ ├── start2.bin │ └── tsconfig.json ├── testing │ ├── package.json │ ├── src │ │ ├── setup.ts │ │ ├── test-utils.ts │ │ └── vitest.d.ts │ └── tsconfig.json ├── tsconfig │ ├── base.json │ ├── internal.json │ ├── package.json │ └── test.json └── utils │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── __tests__ │ │ ├── log-manager.test.ts │ │ ├── on-exit.test.ts │ │ ├── plugin-driver.test.ts │ │ └── test-utils.ts │ ├── console-transport.ts │ ├── index.ts │ ├── log-manager.ts │ ├── on-exit.ts │ └── plugin-driver.ts │ ├── tsconfig.json │ ├── tsconfig.src.json │ ├── tsconfig.test.json │ └── vitest.config.ts ├── patches └── @changesets+assemble-release-plan+6.0.5.patch ├── tsconfig.json └── vitest.workspace.js /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "bluecadet/launchpad" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "develop", 12 | "updateInternalDependencies": "patch", 13 | "privatePackages": { 14 | "version": false, 15 | "tag": false 16 | }, 17 | "___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": { 18 | "onlyUpdatePeerDependentsWhenOutOfRange": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Declare files that will always have LF line endings on checkout. 2 | *.sh text eol=lf 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/basic_pull_request.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ## Screenshots (if appropriate): 21 | 22 | ## Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | 28 | ## Checklist: 29 | 30 | 31 | - [ ] My code follows the code style of this project. 32 | - [ ] My change requires a change to the documentation. 33 | - [ ] I have updated the documentation accordingly. 34 | - [ ] My change requires a test to make sure it continually works. 35 | - [ ] I have added tests accordingly. 36 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Main 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - main 8 | - develop 9 | paths: 10 | - "packages/**" 11 | - "docs/**" 12 | merge_group: 13 | 14 | concurrency: 15 | group: ${{ github.workflow }}-${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | permissions: 19 | contents: read # to fetch code (actions/checkout) 20 | 21 | env: 22 | FORCE_COLOR: 3 23 | 24 | jobs: 25 | 26 | ci: 27 | name: CI 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v4 31 | - uses: actions/setup-node@v4 32 | with: 33 | node-version-file: 'package.json' 34 | cache: 'npm' 35 | 36 | - name: Install dependencies 37 | run: npm ci 38 | 39 | - name: lint 40 | run: npm run lint 41 | 42 | - name: lint dependency versions 43 | # ignore internal launchpad dependency mismatches. These versions 44 | # are handled by changesets. 45 | run: npx sherif -i @bluecadet/launchpad* 46 | 47 | - name: Validate types 48 | run: npm run build 49 | 50 | - name: Run tests 51 | run: npm run test -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs site to Pages 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | paths: 9 | - "docs/**" 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: pages 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | with: 32 | fetch-depth: 0 33 | - name: Setup Node 34 | uses: actions/setup-node@v4 35 | with: 36 | node-version-file: 'package.json' 37 | cache: npm 38 | - name: Setup Pages 39 | uses: actions/configure-pages@v4 40 | - name: Install dependencies 41 | run: npm ci 42 | - name: Build with VitePress 43 | run: npm run docs:build 44 | - name: Upload artifact 45 | uses: actions/upload-pages-artifact@v3 46 | with: 47 | path: docs/.vitepress/dist 48 | 49 | # Deployment job 50 | deploy: 51 | environment: 52 | name: github-pages 53 | url: ${{ steps.deployment.outputs.page_url }} 54 | needs: build 55 | runs-on: ubuntu-latest 56 | name: Deploy 57 | steps: 58 | - name: Deploy to GitHub Pages 59 | id: deployment 60 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - develop 7 | paths: 8 | - "packages/**" 9 | - ".changeset/**" 10 | 11 | permissions: {} 12 | jobs: 13 | release: 14 | # prevents this action from running on forks 15 | if: github.repository == 'bluecadet/launchpad' 16 | 17 | permissions: 18 | contents: write # to create release (changesets/action) 19 | id-token: write # OpenID Connect token needed for provenance 20 | pull-requests: write # to create pull request (changesets/action) 21 | 22 | name: Release 23 | runs-on: ubuntu-latest 24 | 25 | steps: 26 | - name: Checkout Repo 27 | uses: actions/checkout@v4 28 | with: 29 | # This makes Actions fetch all Git history so that Changesets can generate changelogs with the correct commits 30 | fetch-depth: 0 31 | # custom token so that the changesets stuff still works with our branch protections 32 | token: ${{ secrets.BC_GITHUB_TOKEN }} 33 | 34 | - uses: actions/setup-node@v4 35 | with: 36 | node-version-file: 'package.json' 37 | cache: 'npm' 38 | 39 | - name: Install Dependencies 40 | run: npm ci 41 | 42 | - name: Create Release Pull Request or Publish to npm 43 | id: changesets 44 | uses: changesets/action@v1 45 | with: 46 | publish: npm run release 47 | env: 48 | GITHUB_TOKEN: ${{ secrets.BC_GITHUB_TOKEN }} 49 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Launchpad 3 | config.json 4 | user-config.json 5 | user-config.js 6 | .logs 7 | .tmp 8 | .content 9 | .vscode 10 | .downloads 11 | downloads 12 | .credentials.json 13 | credentials.json 14 | 15 | # MacOS 16 | .DS_Store 17 | 18 | # Logs 19 | logs 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | lerna-debug.log* 25 | .pnpm-debug.log* 26 | 27 | # Diagnostic reports (https://nodejs.org/api/report.html) 28 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 29 | 30 | # Runtime data 31 | pids 32 | *.pid 33 | *.seed 34 | *.pid.lock 35 | 36 | # Directory for instrumented libs generated by jscoverage/JSCover 37 | lib-cov 38 | 39 | # Coverage directory used by tools like istanbul 40 | coverage 41 | *.lcov 42 | 43 | # nyc test coverage 44 | .nyc_output 45 | 46 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 47 | .grunt 48 | 49 | # Bower dependency directory (https://bower.io/) 50 | bower_components 51 | 52 | # node-waf configuration 53 | .lock-wscript 54 | 55 | # Compiled binary addons (https://nodejs.org/api/addons.html) 56 | build/Release 57 | 58 | # Dependency directories 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | web_modules/ 64 | 65 | # TypeScript cache 66 | *.tsbuildinfo 67 | 68 | # Optional npm cache directory 69 | .npm 70 | 71 | # Optional eslint cache 72 | .eslintcache 73 | 74 | # Optional stylelint cache 75 | .stylelintcache 76 | 77 | # Microbundle cache 78 | .rpt2_cache/ 79 | .rts2_cache_cjs/ 80 | .rts2_cache_es/ 81 | .rts2_cache_umd/ 82 | 83 | # Optional REPL history 84 | .node_repl_history 85 | 86 | # Output of 'npm pack' 87 | *.tgz 88 | 89 | # Yarn Integrity file 90 | .yarn-integrity 91 | 92 | # dotenv environment variables file 93 | .env.development.local 94 | .env.test.local 95 | .env.production.local 96 | .env.local 97 | 98 | # parcel-bundler cache (https://parceljs.org/) 99 | .cache 100 | .parcel-cache 101 | 102 | # Next.js build output 103 | .next 104 | out 105 | 106 | # Nuxt.js build / generate output 107 | .nuxt 108 | dist 109 | 110 | # Gatsby files 111 | .cache/ 112 | # Comment in the public line in if your project uses Gatsby and not Next.js 113 | # https://nextjs.org/blog/next-9-1#public-directory-support 114 | # public 115 | 116 | # vuepress build output 117 | .vuepress/dist 118 | 119 | # vuepress v2.x temp and cache directory 120 | .temp 121 | .cache 122 | 123 | # Serverless directories 124 | .serverless/ 125 | 126 | # FuseBox cache 127 | .fusebox/ 128 | 129 | # DynamoDB Local files 130 | .dynamodb/ 131 | 132 | # TernJS port file 133 | .tern-port 134 | 135 | # Stores VSCode versions used for testing VSCode extensions 136 | .vscode-test 137 | 138 | # yarn v2 139 | .yarn/cache 140 | .yarn/unplugged 141 | .yarn/build-state.yml 142 | .yarn/install-state.gz 143 | .pnp.* 144 | 145 | # changesets 146 | !.changeset/config.json 147 | 148 | # generated .d.ts files 149 | types 150 | 151 | # vitepress 152 | docs/**/cache 153 | docs/**/dist -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Launchpad 2 | 3 | ## Generating Changelogs 4 | 5 | If you are contributing a user-facing or noteworthy change to Launchpad that should be added to the changelog, you should include a changeset with your PR. 6 | 7 | To add a changeset, run this script locally: 8 | 9 | ``` 10 | npm run changeset 11 | ``` 12 | 13 | Follow the prompts to select which package(s) are affected by your change, and whether the change is a major, minor or patch change. This will create a file in the `.changesets` directory of the repo. This change should be committed and included with your PR. 14 | 15 | Considerations: 16 | 17 | - A changeset is required to trigger the versioning/publishing workflow. 18 | - Non-packages, like examples and tests, do not need changesets. 19 | - You can use markdown in your changeset to include code examples, headings, and more. However, please use plain text for the first line of your changeset. The formatting of the GitHub release notes does not support headings as the first line of the changeset. 20 | 21 | ## Releases 22 | 23 | The [Changesets GitHub action](https://github.com/changesets/action#with-publishing) will create and update a PR that applies changesets and publishes new versions of changed packages to npm. 24 | 25 | To release a new version of Launchpad, find the `Version Packages` PR, read it over, and merge it. 26 | 27 | The `main` branch is kept up to date with the latest releases. 28 | 29 | ## Testing Launchpad 30 | 31 | If you want to test Launchpad as a local dependency and frequently make changes, then the best way to do that is to clone launchpad and link `npm @bluecadet/launchpad` in your local project. 32 | 33 | For example: 34 | 35 | ```bat 36 | git clone git@github.com:bluecadet/launchpad.git 37 | cd launchpad 38 | npm i 39 | cd packages/launchpad 40 | npm link 41 | 42 | cd ../../my-test-project 43 | @REM If needed: npm init 44 | npm link @bluecadet/launchpad --save 45 | ``` 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bluecadet 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🚀 Launchpad 2 | 3 | Launchpad is a suite of tools for managing interactive installations. It provides content management, process monitoring, and system configuration capabilities. 4 | 5 | ## Documentation 6 | 7 | For complete documentation, configuration options, and guides, visit: 8 | [Launchpad Documentation](https://bluecadet.github.io/launchpad/) 9 | 10 | ## Core Packages 11 | 12 | - [@bluecadet/launchpad-cli](./packages/cli): Command line interface and configuration management 13 | - [@bluecadet/launchpad-content](./packages/content): Content pipeline and transformation tools 14 | - [@bluecadet/launchpad-monitor](./packages/monitor): Process monitoring and management 15 | - [@bluecadet/launchpad-scaffold](./packages/scaffold): System configuration 16 | - [@bluecadet/launchpad-utils](./packages/utils): Shared utilities 17 | - [@bluecadet/launchpad](./packages/launchpad): Meta-package that installs all core packages 18 | 19 | ## Contributing 20 | 21 | See [CONTRIBUTING.md](./CONTRIBUTING.md) for development guidelines. 22 | 23 | ## License 24 | 25 | MIT © Bluecadet 26 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [], 11 | "include": ["**/src/**/*.ts", "docs/**/*.ts", "docs/**/*.mts", "docs/**/*.vue"] 12 | }, 13 | "formatter": { 14 | "enabled": true, 15 | "indentStyle": "tab", 16 | "lineWidth": 100 17 | }, 18 | "organizeImports": { 19 | "enabled": true 20 | }, 21 | "linter": { 22 | "enabled": true, 23 | "rules": { 24 | "recommended": true 25 | } 26 | }, 27 | "javascript": { 28 | "formatter": { 29 | "quoteStyle": "double" 30 | } 31 | }, 32 | "overrides": [ 33 | { 34 | "include": ["**/*.test.ts"], 35 | "linter": { 36 | "rules": { 37 | "style": { 38 | "noNonNullAssertion": "off" 39 | }, 40 | "suspicious": { 41 | "noExplicitAny": "off" 42 | } 43 | } 44 | } 45 | } 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import type { Theme } from "vitepress"; 2 | import DefaultTheme from "vitepress/theme"; 3 | // https://vitepress.dev/guide/custom-theme 4 | import { h } from "vue"; 5 | import "./style.css"; 6 | 7 | export default { 8 | extends: DefaultTheme, 9 | Layout: () => { 10 | return h(DefaultTheme.Layout, null, { 11 | // https://vitepress.dev/guide/extending-default-theme#layout-slots 12 | }); 13 | }, 14 | enhanceApp({ app, router, siteData }) {}, 15 | } satisfies Theme; 16 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-docs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "type": "module", 6 | "devDependencies": { 7 | "@bluecadet/launchpad": "2.0.12", 8 | "@bluecadet/launchpad-cli": "2.1.1", 9 | "@bluecadet/launchpad-content": "2.1.3", 10 | "@bluecadet/launchpad-monitor": "2.0.5", 11 | "@bluecadet/launchpad-scaffold": "2.0.0" 12 | }, 13 | "dependencies": { 14 | "lucide-vue-next": "^0.461.0", 15 | "vitepress": "^1.5.0", 16 | "vue": "^3.5.12" 17 | }, 18 | "scripts": { 19 | "dev": "vitepress dev", 20 | "build": "vitepress build", 21 | "preview": "vitepress preview" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/components/PackageCard.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | -------------------------------------------------------------------------------- /docs/src/components/PackageHeader.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 34 | 35 | -------------------------------------------------------------------------------- /docs/src/components/package-versions.data.ts: -------------------------------------------------------------------------------- 1 | import cliPackage from "@bluecadet/launchpad-cli/package.json" with { type: "json" }; 2 | import contentPackage from "@bluecadet/launchpad-content/package.json" with { type: "json" }; 3 | import monitorPackage from "@bluecadet/launchpad-monitor/package.json" with { type: "json" }; 4 | import scaffoldPackage from "@bluecadet/launchpad-scaffold/package.json" with { type: "json" }; 5 | import launchpadPackage from "@bluecadet/launchpad/package.json" with { type: "json" }; 6 | import { defineLoader } from "vitepress"; 7 | 8 | export interface Data { 9 | launchpad: string; 10 | content: string; 11 | monitor: string; 12 | cli: string; 13 | scaffold: string; 14 | } 15 | 16 | declare const data: Data; 17 | 18 | export { data }; 19 | 20 | export default defineLoader({ 21 | async load(): Promise { 22 | return { 23 | launchpad: launchpadPackage.version, 24 | content: contentPackage.version, 25 | monitor: monitorPackage.version, 26 | cli: cliPackage.version, 27 | scaffold: scaffoldPackage.version, 28 | }; 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /docs/src/guides/fetching-content.md: -------------------------------------------------------------------------------- 1 | # Fetching Content 2 | 3 | Launchpad's content fetching process is designed to be flexible and robust, allowing you to fetch, transform, and manage content from various sources. This guide provides an overview of how content flows through Launchpad, from configuration to storage. 4 | 5 | ## Basic Configuration 6 | 7 | Content fetching requires a `ContentConfig` object in your Launchpad configuration: 8 | 9 | ```js 10 | // launchpad.config.js 11 | import { defineConfig } from '@bluecadet/launchpad-cli'; 12 | import { jsonSource } from '@bluecadet/launchpad-content'; 13 | 14 | export default defineConfig({ 15 | content: { 16 | sources: [ 17 | jsonSource({ 18 | id: "example-source", 19 | files: { 20 | "example.json": "https://example.com/api/data", 21 | }, 22 | }), 23 | ], 24 | plugins: [], // Add plugins here 25 | downloadPath: './content', 26 | backupAndRestore: true, 27 | }, 28 | }); 29 | ``` 30 | 31 | See the [Content Configuration Reference](/reference/content/content-config.md) for all options. 32 | 33 | ## Running Content Updates 34 | 35 | You can update content in two ways: 36 | 37 | ```bash 38 | # As part of the full Launchpad startup 39 | npx launchpad start 40 | 41 | # Content updates only 42 | npx launchpad content 43 | ``` 44 | 45 | See the [CLI Commands Reference](../reference/cli/commands.md) for more details. 46 | 47 | ## Content Sources 48 | 49 | Sources define where your content comes from. Launchpad includes several built-in sources: 50 | 51 | - JSON/REST APIs 52 | - CMS platforms (Contentful, Sanity, etc.) 53 | 54 | Check the [Content Sources Reference](../reference/content/sources/index.md) to learn more about available sources and how to create custom ones. 55 | 56 | ## Transform with Plugins 57 | 58 | Plugins process your content after it's downloaded. Common use cases include: 59 | 60 | - Converting Markdown to HTML 61 | - Resizing images 62 | - Validating data 63 | - Custom transformations 64 | 65 | Learn more in the [Plugins Reference](../reference/content/plugins/index.md). 66 | 67 | ## Best Practices 68 | 69 | - **Use Backup and Restore**: Enable `backupAndRestore` to automatically recover from failures 70 | - **Implement Error Handling**: Use the [error handling system](../reference/content/index.md#error-handling) to gracefully handle failures 71 | - **Monitor Progress**: Use the [logging system](../reference/content/index.md#logging) to track content updates 72 | - **Organize Sources**: Group related content into separate sources for better management 73 | - **Cache Effectively**: Configure appropriate cache settings for your content type 74 | 75 | ## Next Steps 76 | 77 | - Read about [Content Configuration](../reference/content/content-config.md) 78 | - Learn about [Content Sources](../reference/content/sources/index.md) 79 | - Explore [Plugin Development](../reference/content/plugins/index.md) 80 | - Understanding [Error Handling](../reference/content/index.md#error-handling) 81 | 82 | For complete API documentation, visit the [Content Reference Documentation](../reference/content/index.md). 83 | -------------------------------------------------------------------------------- /docs/src/guides/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | ## Installation 4 | 5 | Launchpad is modular - you can install just the packages you need: 6 | 7 | ```bash 8 | # Install the CLI (required) 9 | npm install @bluecadet/launchpad-cli 10 | 11 | # Install content management (optional) 12 | npm install @bluecadet/launchpad-content 13 | 14 | # Install process monitoring (optional) 15 | npm install @bluecadet/launchpad-monitor 16 | 17 | # Install system configuration (optional) 18 | npm install @bluecadet/launchpad-scaffold 19 | ``` 20 | 21 | Alternatively, install everything at once: 22 | 23 | ```bash 24 | npm install @bluecadet/launchpad 25 | ``` 26 | 27 | ## Basic Setup 28 | 29 | 1. Create a configuration file: 30 | 31 | ```js 32 | // launchpad.config.js (or launchpad.config.ts, launchpad.config.mjs, etc.) 33 | import { defineConfig } from '@bluecadet/launchpad-cli'; 34 | import { jsonSource } from '@bluecadet/launchpad-content'; 35 | 36 | export default defineConfig({ 37 | content: { 38 | // Content management configuration 39 | sources: [ 40 | jsonSource({ 41 | id: "flickr-images", 42 | files: { 43 | "spaceships.json": 44 | "https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=spaceship", 45 | "rockets.json": 46 | "https://api.flickr.com/services/feeds/photos_public.gne?format=json&nojsoncallback=1&tags=rocket", 47 | }, 48 | }), 49 | ] 50 | }, 51 | monitor: { 52 | // Process management configuration 53 | apps: [ 54 | { 55 | pm2: { 56 | name: "my-app", 57 | script: "my-app.exe", 58 | cwd: "./builds/", 59 | } 60 | } 61 | ] 62 | } 63 | }); 64 | ``` 65 | 66 | 2. Run launchpad: 67 | 68 | ```bash 69 | # Download content and start apps 70 | npx launchpad start 71 | 72 | # Only download fresh content 73 | npx launchpad content 74 | 75 | # Only manage apps 76 | npx launchpad monitor 77 | 78 | # Stop any running launchpad processes 79 | npx launchpad stop 80 | ``` 81 | -------------------------------------------------------------------------------- /docs/src/guides/running-applications.md: -------------------------------------------------------------------------------- 1 | # Running Applications 2 | 3 | Launchpad's monitoring system helps you manage and maintain long-running applications reliably. This guide covers how to configure, launch, and monitor your applications using Launchpad. 4 | 5 | ## Overview 6 | 7 | The monitoring system is built on top of [PM2](https://pm2.keymetrics.io/), a robust process manager, providing: 8 | 9 | - Process management and auto-restart 10 | - Log collection and rotation 11 | - Application status monitoring 12 | - Graceful shutdown handling 13 | 14 | ## Basic Setup 15 | 16 | 1. Install the required packages: 17 | 18 | ```bash 19 | npm install @bluecadet/launchpad-cli @bluecadet/launchpad-monitor 20 | ``` 21 | 22 | 2. Configure your applications in `launchpad.config.js`: 23 | 24 | ```js 25 | import { defineConfig } from '@bluecadet/launchpad-cli'; 26 | 27 | export default defineConfig({ 28 | monitor: { 29 | apps: [ 30 | { 31 | pm2: { 32 | name: "my-app", 33 | script: "./app.exe", 34 | cwd: "./builds/", 35 | // Optional: environment variables 36 | env: { 37 | PORT: "3000" 38 | } 39 | } 40 | } 41 | ] 42 | } 43 | }); 44 | ``` 45 | 46 | >[!WARNING] 47 | >Always test your configuration in a development environment first 48 | 49 | 3. Start your application 50 | 51 | ```bash 52 | npx launchpad monitor start 53 | ``` 54 | 55 | ## Configuration Options 56 | 57 | ### Basic Settings 58 | 59 | - `name`: Unique identifier for your application 60 | - `script`: Path to your executable or script 61 | - `cwd`: Working directory for your application 62 | 63 | ### Advanced Settings 64 | 65 | ```js 66 | { 67 | pm2: { 68 | // Process settings 69 | autorestart: true, 70 | 71 | // Resource limits 72 | max_memory_restart: '1G', 73 | 74 | // Environment 75 | env: { 76 | NODE_ENV: 'production' 77 | } 78 | } 79 | } 80 | ``` 81 | 82 | ## Best Practices 83 | 84 | 1. **Unique Names**: Give each application a unique, descriptive name 85 | 2. **Error Handling**: Configure proper restart policies 86 | 3. **Resource Limits**: Set memory limits to prevent system overload 87 | 4. **Logging**: Use appropriate log levels for debugging 88 | 89 | ## Next Steps 90 | 91 | - Learn about [content management](./fetching-content.md) 92 | - Read the [monitor reference](../reference/monitor/index.md) for detailed API documentation 93 | -------------------------------------------------------------------------------- /docs/src/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 🚀 Launchpad 3 | titleTemplate: ':title' 4 | --- 5 | 9 | 10 | 11 | 12 | Launchpad provides a collection of tools designed to streamline the development, deployment, and maintenance of media installations. It handles content management, process monitoring, system configuration, and more. 13 | 14 | ## Key Features 15 | 16 | - 📂 **Content Management**: Fetch and transform content from any source 17 | - 🔍 **Process Monitoring**: Keep your applications running reliably 18 | - 🛠️ **System Configuration**: Automate Windows kiosk setup 19 | - 💻 **Command Line Interface**: Easy-to-use commands for common operations 20 | - 🔌 **Plugin Architecture**: Extend functionality with custom plugins 21 | 22 | ## Why Launchpad? 23 | 24 | - **Reliable**: Built for 24/7 operation in museum environments 25 | - **Flexible**: Modular design lets you use only what you need 26 | - **Extensible**: Plugin system for custom functionality 27 | - **Type-Safe**: Written in TypeScript with full type coverage 28 | 29 | ## Core Packages 30 | 31 | 37 | 38 | 47 | 48 | ## Quick Start 49 | 50 | 1. Install the packages you need: 51 | 52 | ```bash 53 | npm install @bluecadet/launchpad-cli @bluecadet/launchpad-content @bluecadet/launchpad-monitor 54 | ``` 55 | 56 | 2. Create a configuration file: 57 | 58 | ```js 59 | // launchpad.config.js 60 | import { defineConfig } from '@bluecadet/launchpad-cli'; 61 | 62 | export default defineConfig({ 63 | content: { 64 | sources: [ 65 | // Content source configurations 66 | ] 67 | }, 68 | monitor: { 69 | apps: [ 70 | // Application configurations 71 | ] 72 | } 73 | }); 74 | ``` 75 | 76 | 3. Run launchpad: 77 | 78 | ```bash 79 | npx launchpad start 80 | ``` 81 | 82 | >[!NOTE] Tip 83 | > See the [Getting Started](/guides/getting-started) guide for detailed setup instructions. 84 | 85 | --- 86 | 87 | Developed with ❤️ by [Bluecadet](https://bluecadet.com), available free and open-source under the MIT license. 88 | -------------------------------------------------------------------------------- /docs/src/public/google39b48f38931b597a.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google39b48f38931b597a.html -------------------------------------------------------------------------------- /docs/src/reference/cli/commands.md: -------------------------------------------------------------------------------- 1 | # CLI Commands 2 | 3 | Launchpad provides several commands to manage your media installations. Each command can be run using `npx launchpad ` or just `launchpad ` if installed globally. 4 | 5 | ## Start Command 6 | 7 | ```bash 8 | launchpad start [options] 9 | ``` 10 | 11 | The `start` command is the primary way to launch your application. It: 12 | 13 | 1. Downloads fresh content from configured sources 14 | 2. Starts and monitors configured applications 15 | 3. Initializes health monitoring if configured 16 | 17 | ## Stop Command 18 | 19 | ```bash 20 | launchpad stop [options] 21 | ``` 22 | 23 | The `stop` command gracefully shuts down all Launchpad processes: 24 | 25 | - Stops all monitored applications 26 | - Kills any existing PM2 instances 27 | - Cleans up temporary files 28 | 29 | ## Content Command 30 | 31 | ```bash 32 | launchpad content [options] 33 | ``` 34 | 35 | Use this command to manage content independently: 36 | 37 | - Downloads fresh content from all configured sources 38 | - Runs content transformations 39 | - Updates local content cache 40 | - Does not start or affect running applications 41 | 42 | This is useful for updating content without restarting applications. 43 | 44 | ## Monitor Command 45 | 46 | ```bash 47 | launchpad monitor [options] 48 | ``` 49 | 50 | The `monitor` command focuses on application management: 51 | 52 | - Starts configured applications 53 | - Monitors for crashes and restarts as needed 54 | - Collects process statistics 55 | - Does not download or update content 56 | 57 | Use this when you want to restart applications without refreshing content. 58 | 59 | ## Scaffold Command 60 | 61 | ```bash 62 | launchpad scaffold [options] 63 | ``` 64 | 65 | This command configures Windows PCs for exhibit environments: 66 | 67 | - Requires administrative privileges 68 | - Configures Windows kiosk mode 69 | - Optimizes power settings 70 | - Sets up common exhibit configurations 71 | - Applies system-level settings 72 | 73 | ## Global Options 74 | 75 | All commands support these options: 76 | 77 | | Option | Description | Type | 78 | |--------|-------------|------| 79 | | `--config, -c` | Path to your JS config file | string | 80 | | `--env, -e` | Path(s) to your .env file(s) | array | 81 | | `--env-cascade, -E` | Cascade env variables from multiple .env files | string | 82 | | `--help` | Show help information | flag | 83 | -------------------------------------------------------------------------------- /docs/src/reference/cli/config-loading.md: -------------------------------------------------------------------------------- 1 | # Config Loading 2 | 3 | The Launchpad CLI uses a flexible configuration system that automatically searches for and loads your project configuration. The CLI supports Javascript config files. 4 | 5 | ## Config File Search 6 | 7 | The CLI searches for config files with the following names: 8 | 9 | 1. `launchpad.config.js` 10 | 2. `launchpad.config.mjs` 11 | 3. `launchpad.config.ts` 12 | 4. `launchpad.config.cjs` 13 | 5. `launchpad.config.mts` 14 | 6. `launchpad.config.cts` 15 | 16 | The search starts in the current working directory and recursively searches up parent directories (up to 64 levels) until a config file is found. 17 | 18 | ## Config File Format 19 | 20 | ### JavaScript/TypeScript Config (Recommended) 21 | 22 | ```js 23 | import { defineConfig } from '@bluecadet/launchpad-cli'; 24 | 25 | export default defineConfig({ 26 | content: { 27 | // Content management configuration 28 | }, 29 | monitor: { 30 | // Process monitoring configuration 31 | }, 32 | }); 33 | ``` 34 | 35 | ## Configuration Structure 36 | 37 | Your config file can include settings for any of Launchpad's main modules: 38 | 39 | - `content` - Content management settings ([Content Config Reference](../content/content-config)) 40 | - `monitor` - Process monitoring settings ([Monitor Config Reference](../monitor/monitor-config)) 41 | 42 | ## Environment Variables 43 | 44 | Config files can reference environment variables using the `process.env` object in JavaScript configs. For managing environment variables, see the [Environment Variables](./env) documentation. 45 | 46 | ## Type Safety 47 | 48 | When using TypeScript or an editor with TypeScript support (like VS Code), the `defineConfig` helper provides: 49 | 50 | - Full IntelliSense for all configuration options 51 | - Type checking for configuration values 52 | - Auto-completion suggestions 53 | - Documentation hints 54 | 55 | ## Example 56 | 57 | ```js 58 | import { defineConfig } from '@bluecadet/launchpad-cli'; 59 | import { jsonSource } from '@bluecadet/launchpad-content'; 60 | 61 | export default defineConfig({ 62 | content: { 63 | sources: [ 64 | jsonSource({ 65 | id: "api-data", 66 | files: { 67 | "data.json": process.env.API_ENDPOINT 68 | } 69 | }) 70 | ], 71 | downloadPath: "./content" 72 | }, 73 | monitor: { 74 | apps: [ 75 | { 76 | pm2: { 77 | name: "exhibit-app", 78 | script: "./app.exe" 79 | } 80 | } 81 | ] 82 | } 83 | }); 84 | ``` 85 | -------------------------------------------------------------------------------- /docs/src/reference/cli/env.md: -------------------------------------------------------------------------------- 1 | # Environment Variables 2 | 3 | The `launchpad-cli` package supports loading environment variables from `.env` files using the `--env` and `--env-cascade` flags. This allows you to manage different configurations for various environments (development, staging, production, etc.). 4 | 5 | ## Usage 6 | 7 | To load environment variables from a `.env` file: 8 | 9 | ```bash 10 | npx launchpad --env .env 11 | ``` 12 | 13 | You can also load multiple `.env` files in sequence: 14 | 15 | ```bash 16 | npx launchpad --env .env.local --env .env 17 | ``` 18 | 19 | When loading multiple files, variables from later files will override those from earlier files if they share the same name. 20 | 21 | ### Environment Cascading 22 | 23 | The `--env-cascade` flag provides an automated way to load multiple environment files in a specific order. For example: 24 | 25 | ```bash 26 | npx launchpad --env-cascade production 27 | ``` 28 | 29 | This will load files in the following order: 30 | 31 | 1. `.env` 32 | 2. `.env.local` 33 | 3. `.env.production` 34 | 4. `.env.production.local` 35 | 36 | ## Best Practices 37 | 38 | - **Environment-Specific Files**: Create separate `.env` files for different environments: 39 | - `.env.development` for development settings 40 | - `.env.staging` for staging settings 41 | - `.env.production` for production settings 42 | 43 | - **Security**: 44 | - Keep sensitive data (API keys, passwords) in local `.env` files that aren't committed 45 | - Use `.env.local` for machine-specific overrides 46 | 47 | - **Documentation**: 48 | - Create a `.env.example` file 49 | - List all required variables with example values 50 | - Include this file in version control 51 | 52 | Example `.env.example`: 53 | 54 | ```sh 55 | # API Configuration 56 | API_KEY=your_api_key_here 57 | API_URL=https://api.example.com 58 | 59 | # Database Configuration 60 | DATABASE_URL=postgresql://user:password@localhost:5432/dbname 61 | ``` 62 | -------------------------------------------------------------------------------- /docs/src/reference/cli/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@bluecadet/launchpad-cli" 3 | --- 4 | 5 | 8 | 9 | 10 | 11 | The CLI package provides a command-line interface for managing Launchpad installations. It serves as the primary entry point for running content management, process monitoring, and system configuration tasks. 12 | 13 | ## Features 14 | 15 | - **Command Line Interface**: Easy-to-use commands for common operations: 16 | - Start/stop content downloads and app monitoring 17 | - Run content updates independently 18 | - Configure system settings 19 | - Access help documentation 20 | 21 | - **Configuration Management**: 22 | - Load and parse config files 23 | - Environment variable handling with dotenv 24 | - Cascading configuration support 25 | - Type-safe config validation 26 | 27 | - **Flexible Commands**: 28 | - `start`: Full launchpad startup (content + apps) 29 | - `stop`: Graceful shutdown of all processes 30 | - `content`: Content-only operations 31 | - `monitor`: Process monitoring operations 32 | - `scaffold`: System configuration (Windows) 33 | 34 | ## Installation 35 | 36 | The CLI can be installed globally via npm: 37 | 38 | ```bash 39 | npm install @bluecadet/launchpad-cli 40 | 41 | # install any other modules you need, depeneding on your use-case 42 | npm install @bluecadet/launchpad-content @bluecadet/launchpad-monitor 43 | ``` 44 | 45 | ## Usage 46 | 47 | Once installed, you can run commands using the `launchpad` executable: 48 | 49 | ```bash 50 | npx launchpad [options] 51 | ``` 52 | 53 | For help: 54 | 55 | ```bash 56 | npx launchpad help 57 | ``` 58 | 59 | > [!NOTE] Note: 60 | > if installed globally (`npm install -g @bluecadet/launchpad-cli`) you don't need the `npx` prefix when running commands. 61 | 62 | See [Commands](./commands.md) for more information on the available commands. 63 | -------------------------------------------------------------------------------- /docs/src/reference/content/content-config.md: -------------------------------------------------------------------------------- 1 | # Content Config 2 | 3 | Configuration for managing content sources, plugins, and file handling settings. 4 | 5 | ## Options 6 | 7 | ### `sources` 8 | 9 | - **Type:** `Array>` 10 | - **Default:** `[]` 11 | 12 | A list of content source options that defines where content is downloaded from. You can configure multiple sources and use different source types simultaneously. Each source can be either a direct ContentSource object or a Promise that resolves to a ContentSource. 13 | 14 | For detailed source configuration options, see [Sources Reference](./sources/index.md). 15 | 16 | ### `plugins` 17 | 18 | - **Type:** `Array` 19 | - **Default:** `[]` 20 | 21 | A list of content transformation plugins that process content after download. Plugins can modify, analyze, or enhance content before final output. 22 | 23 | See [Content Plugin Reference](./plugins/index.md) for available plugins and usage. 24 | 25 | ### `downloadPath` 26 | 27 | - **Type:** `string` 28 | - **Default:** `.downloads/` 29 | 30 | Base directory path where downloaded files are stored. Can be absolute or relative path. 31 | 32 | ### `tempPath` 33 | 34 | - **Type:** `string` 35 | - **Default:** `.tmp/%TIMESTAMP%/` 36 | 37 | Temporary directory path used during content processing. The `%TIMESTAMP%` token is replaced with current timestamp. 38 | 39 | ### `backupPath` 40 | 41 | - **Type:** `string` 42 | - **Default:** `.backup/%TIMESTAMP%/` 43 | 44 | Directory path where existing content is backed up before processing new downloads. Critical for recovery if downloads fail. 45 | 46 | ### `keep` 47 | 48 | - **Type:** `string[]` 49 | - **Default:** `[]` 50 | 51 | Glob patterns for files to preserve when clearing directories. 52 | 53 | Example: 54 | 55 | - `["*.json"]` - Keep all JSON files 56 | - `["**/*.csv", "*.git*"]` - Keep all CSV files in any subdirectory, and any git related files. 57 | 58 | ### `backupAndRestore` 59 | 60 | - **Type:** `boolean` 61 | - **Default:** `true` 62 | 63 | When enabled: 64 | 65 | - Creates backup of existing files before download 66 | - Restores backup if any source download fails 67 | - Ensures atomic success/failure of multi-source downloads 68 | 69 | ### `maxTimeout` 70 | 71 | - **Type:** `number` 72 | - **Default:** `30000` 73 | 74 | Maximum time in milliseconds to wait for network requests before timing out. 75 | 76 | ### `encodeCharacters` 77 | 78 | - **Type:** `string` 79 | - **Default:** `<>:"|?*` 80 | 81 | Special characters to encode in file paths for both content and media downloads. Ensures valid filenames across systems. 82 | -------------------------------------------------------------------------------- /docs/src/reference/content/data-store.md: -------------------------------------------------------------------------------- 1 | # DataStore 2 | 3 | The DataStore is a file system-based storage system used to manage content during the fetch and transform process. It provides a simple API for storing and retrieving content, with support for namespaces and documents. 4 | 5 | ## Core Concepts 6 | 7 | ### Namespaces 8 | 9 | Namespaces represent collections of documents from a single source. Each content source gets its own namespace, identified by the source's ID. 10 | 11 | ### Documents 12 | 13 | Documents are individual files containing content data. They can be either single JSON files or batched files (for paginated content). 14 | 15 | ## API Reference 16 | 17 | ### DataStore 18 | 19 | #### `createNamespace(namespaceId: string)` 20 | 21 | Creates a new namespace in the data store. Returns a Result containing the namespace. 22 | 23 | #### `namespace(namespaceId: string)` 24 | 25 | Gets an existing namespace. Returns a Result containing the namespace. 26 | 27 | #### `getDocument(namespaceId: string, documentId: string)` 28 | 29 | Gets a specific document from a namespace. Returns a Result containing the document. 30 | 31 | #### `filter(ids?: DataKeys)` 32 | 33 | Filters documents based on namespace and document IDs. Returns grouped results by namespace. 34 | 35 | ### Namespace 36 | 37 | #### `insert(id: string, data: Promise | AsyncIterable)` 38 | 39 | Inserts a new document into the namespace. Data can be a Promise for single documents or an AsyncIterable for batched documents. 40 | 41 | #### `document(id: string)` 42 | 43 | Gets a document by ID from the namespace. 44 | 45 | #### `documents()` 46 | 47 | Gets all documents in the namespace. 48 | 49 | #### `waitFor(id: string)` 50 | 51 | Returns a promise that resolves when the document with the passed ID has finished being written. 52 | 53 | ### Document 54 | 55 | #### `update(cb: (data: T) => T | Promise)` 56 | 57 | Updates document content using a callback function. 58 | 59 | #### `apply(pathExpression: string, fn: (x: unknown) => unknown)` 60 | 61 | Applies a transformation to specific paths in the document using JSONPath. 62 | 63 | #### `query(pathExpression: string)` 64 | 65 | Queries document content using JSONPath expressions. 66 | 67 | ## Error Handling 68 | 69 | The DataStore uses the `neverthrow` library for error handling. Most methods return a `Result` or `ResultAsync` type: 70 | 71 | ```typescript 72 | const namespaceResult = dataStore.namespace('my-source'); 73 | if (namespaceResult.isErr()) { 74 | console.error('Error:', namespaceResult.error); 75 | } else { 76 | const namespace = namespaceResult.value; 77 | // Use namespace... 78 | } 79 | ``` 80 | 81 | For async operations, you can use the `andThen` method: 82 | 83 | ```typescript 84 | await dataStore 85 | .createNamespace('my-source') 86 | .andThen(namespace => namespace.insert('doc1', data)); 87 | ``` 88 | -------------------------------------------------------------------------------- /docs/src/reference/content/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@bluecadet/launchpad-content" 3 | --- 4 | 5 | 8 | 9 | 10 | 11 | The content package is a powerful tool for downloading, transforming, and managing content from various sources. It provides a flexible, plugin-based architecture for handling content pipelines. 12 | 13 | ## Features 14 | 15 | - **Extensible Source System**: Easily connect to any content source: 16 | - Build custom source adapters with a simple interface 17 | - Includes ready-to-use adapters for popular CMSs (Contentful, Airtable, Sanity, etc.) 18 | - Type-safe data fetching and validation 19 | 20 | - **Flexible Plugin Architecture**: Transform and process content your way: 21 | - Create custom plugins with straightforward APIs 22 | - Chain multiple transformations 23 | - Built-in plugins for common tasks (Markdown, image processing, etc.) 24 | - Full control over the content pipeline 25 | 26 | - **Robust Content Management**: 27 | - Intelligent diffing for efficient updates 28 | - Automatic backup and recovery 29 | - Configurable file organization 30 | - Temporary file cleanup 31 | - Progress tracking and detailed logging 32 | 33 | ## Installation 34 | 35 | ```bash 36 | npm install @bluecadet/launchpad-content 37 | ``` 38 | 39 | ## Basic Usage 40 | 41 | ```typescript 42 | import LaunchpadContent from '@bluecadet/launchpad-content'; 43 | 44 | const content = new LaunchpadContent({ 45 | sources: [ 46 | // Content source configurations 47 | ], 48 | plugins: [ 49 | // Plugin configurations 50 | ], 51 | downloadPath: './content' 52 | }); 53 | 54 | // Start content download and processing 55 | await content.start(); 56 | ``` 57 | 58 | ## Configuration 59 | 60 | Content operations are configured through a `ContentConfig` object that specifies: 61 | 62 | - **Sources**: Array of content sources to fetch from 63 | - **Plugins**: Array of plugins for content processing 64 | - **Paths**: Various path configurations for content storage 65 | - **Backup Options**: Settings for content backup and restoration 66 | 67 | See the [Content Config](./content-config) section for detailed configuration options. 68 | 69 | ## Plugins 70 | 71 | The plugin system is core to the content package's functionality. Plugins can: 72 | 73 | - Transform content formats 74 | - Process media files 75 | - Add custom processing steps 76 | - Handle errors and logging 77 | 78 | Learn more about available plugins and creating custom ones in the [Plugins](./plugins/index.md) section. 79 | 80 | ## Error Handling 81 | 82 | The package uses the `neverthrow` library for robust error handling: 83 | 84 | - Type-safe error handling 85 | - Clear error boundaries 86 | - Graceful failure recovery 87 | - Automatic backup restoration on errors when configured 88 | -------------------------------------------------------------------------------- /docs/src/reference/content/plugins/md-to-html.md: -------------------------------------------------------------------------------- 1 | # mdToHtml Content Plugin 2 | 3 | The `mdToHtml` plugin is used to transform Markdown content into HTML. It supports both block and inline rendering, with optional sanitization and custom Markdown syntax extensions. 4 | 5 | ## Usage 6 | 7 | To use the `mdToHtml` plugin, include it in the list of content plugins in your configuration: 8 | 9 | ```typescript{1,6-8} 10 | import { mdToHtml } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | plugins: [ 15 | mdToHtml({ 16 | path: '$.item.description' 17 | }) 18 | ] 19 | } 20 | }); 21 | ``` 22 | 23 | ## Options 24 | 25 | ### `path` 26 | 27 | - **Type:** `string` 28 | - **Required** 29 | 30 | Specifies the JSONPath to the content that needs to be transformed from Markdown to HTML. 31 | 32 | ### `simplified` 33 | 34 | - **Type:** `boolean` 35 | - **Default:** `false` 36 | 37 | When set to `true`, the plugin will render the Markdown content as inline HTML, suitable for single paragraph content. 38 | 39 | ### `keys` 40 | 41 | - **Type:** `DataKeys` 42 | - **Default:** `undefined` 43 | 44 | Specifies the data keys to which the transformation should be applied. If not provided, the transformation will be applied to all keys. 45 | -------------------------------------------------------------------------------- /docs/src/reference/content/plugins/media-downloader.md: -------------------------------------------------------------------------------- 1 | # mediaDownloader Content Plugin 2 | 3 | The `mediaDownloader` plugin downloads media assets (like images and videos) referenced in your content and stores them locally. This is useful for ensuring media availability and optimizing load times. 4 | 5 | Downloaded media files are colocated with the sources that reference them. 6 | 7 | ## Usage 8 | 9 | To use the `mediaDownloader` plugin, include it in the list of content plugins in your configuration: 10 | 11 | ```typescript{1,6-8} 12 | import { mediaDownloader } from '@bluecadet/launchpad-content'; 13 | 14 | export default defineConfig({ 15 | content: { 16 | plugins: [ 17 | mediaDownloader({ 18 | maxConcurrent: 4 19 | }) 20 | ] 21 | } 22 | }); 23 | ``` 24 | 25 | ## Options 26 | 27 | ### `keys` 28 | 29 | - **Type:** `string[]` 30 | - **Default:** `undefined` 31 | 32 | Specifies which data keys to search for media URLs. If not provided, all keys will be searched. 33 | 34 | ### `mediaPattern` 35 | 36 | - **Type:** `RegExp` 37 | - **Default:** `/https?.*\.(jpe?g|png|webp|avi|mov|mp4|mpg|mpeg|webm)(\?.*)$/i` 38 | 39 | Regex pattern to match URLs for downloading. 40 | 41 | ### `matchPath` 42 | 43 | - **Type:** `string` 44 | - **Default:** `undefined` 45 | 46 | JSONPath-Plus compatible path to match URLs. Overrides `mediaPattern` if provided. 47 | 48 | ### `maxConcurrent` 49 | 50 | - **Type:** `number` 51 | - **Default:** `4` 52 | 53 | Number of concurrent downloads allowed. 54 | 55 | ### `ignoreCache` 56 | 57 | - **Type:** `boolean` 58 | - **Default:** `false` 59 | 60 | If true, always downloads files regardless of cache status. 61 | 62 | ### `enableIfModifiedSinceCheck` 63 | 64 | - **Type:** `boolean` 65 | - **Default:** `true` 66 | 67 | Enables HTTP if-modified-since check for cached files. 68 | 69 | ### `maxTimeout` 70 | 71 | - **Type:** `number` 72 | - **Default:** `10000` 73 | 74 | Maximum timeout (in milliseconds) for HTTP requests. 75 | 76 | ### `updatePaths` 77 | 78 | - **Type:** `boolean` 79 | - **Default:** `true` 80 | 81 | Updates downloaded media URLs in content to point to local paths. Required for using the 'sharp' plugin. 82 | -------------------------------------------------------------------------------- /docs/src/reference/content/plugins/sanity-image-url-transform.md: -------------------------------------------------------------------------------- 1 | # sanityImageUrlTransform Content Plugin 2 | 3 | The `sanityImageUrlTransform` plugin transforms Sanity image references into usable URLs. It can apply image transformations like resizing, cropping, and format conversion using Sanity's image URL builder. 4 | 5 | ## Usage 6 | 7 | To use the `sanityImageUrlTransform` plugin, include it in your configuration before your `mediaDownloader` plugin: 8 | 9 | ```typescript{1,6-12} 10 | import { sanityImageUrlTransform } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | plugins: [ 15 | sanityImageUrlTransform({ 16 | projectId: 'your-project-id', 17 | dataset: 'production', 18 | buildUrl: (builder) => builder 19 | .width(800) 20 | .format('webp') 21 | }), 22 | mediaDownloader({ 23 | //... 24 | }) 25 | ] 26 | } 27 | }); 28 | ``` 29 | 30 | ## Options 31 | 32 | ### `projectId` 33 | 34 | - **Type:** `string` 35 | - **Required** 36 | 37 | Your Sanity project ID. 38 | 39 | ### `dataset` 40 | 41 | - **Type:** `string` 42 | - **Default:** `"production"` 43 | 44 | The Sanity dataset to use. 45 | 46 | ### `apiToken` 47 | 48 | - **Type:** `string` 49 | - **Optional** 50 | 51 | Sanity API token, required if accessing a private dataset. 52 | 53 | ### `path` 54 | 55 | - **Type:** `string` 56 | - **Default:** `'$..*[?(@._type=="image")]'` 57 | 58 | JSONPath to the content to transform. By default, matches all nodes with `_type` of "image". 59 | 60 | ### `buildUrl` 61 | 62 | - **Type:** `(builder: ImageUrlBuilder) => ImageUrlBuilder` 63 | - **Default:** `builder => builder` 64 | 65 | Function to configure image transformations using Sanity's image URL builder. 66 | 67 | ### `newProperty` 68 | 69 | - **Type:** `string` 70 | - **Default:** `"transformedUrl"` 71 | 72 | The property name where the transformed URL will be stored. 73 | 74 | ### `keys` 75 | 76 | - **Type:** `string[]` 77 | - **Optional** 78 | 79 | Specific data keys to transform. If not provided, transforms all keys. 80 | -------------------------------------------------------------------------------- /docs/src/reference/content/plugins/sanity-to-html.md: -------------------------------------------------------------------------------- 1 | # sanityToHtml Content Plugin 2 | 3 | The `sanityToHtml` plugin is used to transform Sanity.io Portable Text content into HTML. It converts block content from Sanity's structured format into standard HTML markup. 4 | 5 | ## Usage 6 | 7 | To use the `sanityToHtml` plugin, include it in the list of content plugins in your configuration: 8 | 9 | ```typescript{1,6-8} 10 | import { sanityToHtml } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | plugins: [ 15 | sanityToHtml({ 16 | path: '$.item.content' 17 | }) 18 | ] 19 | } 20 | }); 21 | ``` 22 | 23 | ## Options 24 | 25 | ### `path` 26 | 27 | - **Type:** `string` 28 | - **Required** 29 | 30 | Specifies the JSONPath to the Sanity Portable Text content that needs to be transformed to HTML. 31 | 32 | ### `keys` 33 | 34 | - **Type:** `DataKeys` 35 | - **Default:** `undefined` 36 | 37 | Specifies the data keys to which the transformation should be applied. If not provided, the transformation will be applied to all keys. 38 | -------------------------------------------------------------------------------- /docs/src/reference/content/plugins/sanity-to-md.md: -------------------------------------------------------------------------------- 1 | # sanityToMd Content Plugin 2 | 3 | The `sanityToMd` plugin is used to transform Sanity.io Portable Text content into Markdown. It converts block content from Sanity's structured format into standard Markdown syntax. 4 | 5 | ## Usage 6 | 7 | To use the `sanityToMd` plugin, include it in the list of content plugins in your configuration: 8 | 9 | ```typescript{1,6-8} 10 | import { sanityToMd } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | plugins: [ 15 | sanityToMd({ 16 | path: '$.item.content' 17 | }) 18 | ] 19 | } 20 | }); 21 | ``` 22 | 23 | ## Options 24 | 25 | ### `path` 26 | 27 | - **Type:** `string` 28 | - **Required** 29 | 30 | Specifies the JSONPath to the Sanity Portable Text content that needs to be transformed to Markdown. 31 | 32 | ### `keys` 33 | 34 | - **Type:** `DataKeys` 35 | - **Default:** `undefined` 36 | 37 | Specifies the data keys to which the transformation should be applied. If not provided, the transformation will be applied to all keys. 38 | -------------------------------------------------------------------------------- /docs/src/reference/content/plugins/sanity-to-plain.md: -------------------------------------------------------------------------------- 1 | # sanityToPlain Content Plugin 2 | 3 | The `sanityToPlain` plugin is used to transform Sanity.io Portable Text content into plain text. It extracts text content from Sanity's structured format, removing any markup or formatting. 4 | 5 | ## Usage 6 | 7 | To use the `sanityToPlain` plugin, include it in the list of content plugins in your configuration: 8 | 9 | ```typescript{1,6-8} 10 | import { sanityToPlain } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | plugins: [ 15 | sanityToPlain({ 16 | path: '$.item.content' 17 | }) 18 | ] 19 | } 20 | }); 21 | ``` 22 | 23 | ## Options 24 | 25 | ### `path` 26 | 27 | - **Type:** `string` 28 | - **Required** 29 | 30 | Specifies the JSONPath to the Sanity Portable Text content that needs to be transformed to plain text. 31 | 32 | ### `keys` 33 | 34 | - **Type:** `DataKeys` 35 | - **Default:** `undefined` 36 | 37 | Specifies the data keys to which the transformation should be applied. If not provided, the transformation will be applied to all keys. 38 | -------------------------------------------------------------------------------- /docs/src/reference/content/plugins/sharp.md: -------------------------------------------------------------------------------- 1 | # Sharp Content Plugin 2 | 3 | The `sharp` plugin is used to transform downloaded images using the [Sharp](https://sharp.pixelplumbing.com/) image processing library. It can resize, format convert, and apply various transformations to your images. 4 | 5 | ## Usage 6 | 7 | To use the `sharp` plugin, include it in the list of content plugins after the mediaDownloader in your configuration: 8 | 9 | ```typescript{1,7-12} 10 | import { sharp } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | plugins: [ 15 | mediaDownloader({}), 16 | sharp({ 17 | buildTransform: (transform) => transform 18 | .resize(800, 600) 19 | .jpeg({ quality: 80 }), 20 | updateURLs: true 21 | }) 22 | ] 23 | } 24 | }); 25 | ``` 26 | 27 | ## Options 28 | 29 | ### `buildTransform` 30 | 31 | - **Type:** `(transform: Sharp) => Sharp` 32 | - **Required** 33 | 34 | A function that takes a Sharp instance and returns a transformed Sharp instance. This is where you define the image transformations to apply. 35 | 36 | ### `mediaPattern` 37 | 38 | - **Type:** `RegExp` 39 | - **Default:** `/\.(jpe?g|png|webp|tiff|gif|svg)$/i` 40 | 41 | Regex pattern to match image files that should be transformed. 42 | 43 | ### `matchPath` 44 | 45 | - **Type:** `string` 46 | - **Optional** 47 | 48 | JSONPath-Plus compatible path to match images to transform. Overrides `mediaPattern` if provided. 49 | 50 | ### `updateURLs` 51 | 52 | - **Type:** `boolean` 53 | - **Default:** `false` 54 | 55 | When true, updates URLs in the content to point to the transformed images. Note: if you have multiple transforms targeting the same image, you should keep this as false. 56 | 57 | ### `keys` 58 | 59 | - **Type:** `string[]` 60 | - **Optional** 61 | 62 | Specifies which data keys to transform. If not provided, all keys will be searched for images. 63 | 64 | ### `concurrency` 65 | 66 | - **Type:** `number` 67 | - **Default:** `4` 68 | 69 | The number of images to transform concurrently. 70 | -------------------------------------------------------------------------------- /docs/src/reference/content/sources/airtable-source.md: -------------------------------------------------------------------------------- 1 | # Airtable Content Source 2 | 3 | The `airtableSource` content source is used to fetch data from Airtable. It supports fetching data from specified tables and views, and can transform the data into a simplified format. 4 | 5 | ## Usage 6 | 7 | To use the `airtableSource` content source, include it in the list of content sources in your configuration: 8 | 9 | ```typescript{1,6-12} 10 | import { airtableSource } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | sources: [ 15 | airtableSource({ 16 | id: 'myAirtableSource', 17 | baseId: 'appXXXXXXXXXXXXXX', 18 | apiKey: 'keyXXXXXXXXXXXXXX', 19 | tables: ['Table1', 'Table2'], 20 | keyValueTables: ['Settings'] 21 | }) 22 | ] 23 | } 24 | }); 25 | ``` 26 | 27 | ## Options 28 | 29 | ### `id` 30 | 31 | - **Type:** `string` 32 | - **Required** 33 | 34 | Specifies the unique identifier for this source. This will be used as the download path. 35 | 36 | ### `baseId` 37 | 38 | - **Type:** `string` 39 | - **Required** 40 | 41 | Specifies the Airtable base ID. See [Airtable documentation](https://help.appsheet.com/en/articles/1785063-using-data-from-airtable#:~:text=To%20obtain%20the%20ID%20of,API%20page%20of%20the%20base) for more details on how to obtain this ID. 42 | 43 | ### `defaultView` 44 | 45 | - **Type:** `string` 46 | - **Default:** `'Grid view'` 47 | 48 | Specifies the table view to select for syncing by default. 49 | 50 | ### `tables` 51 | 52 | - **Type:** `string[]` 53 | - **Default:** `[]` 54 | 55 | Specifies the tables you want to fetch from. 56 | 57 | ### `keyValueTables` 58 | 59 | - **Type:** `string[]` 60 | - **Default:** `[]` 61 | 62 | As a convenience feature, you can store tables listed here as key/value pairs. Field names should be `key` and `value`. 63 | 64 | ### `endpointUrl` 65 | 66 | - **Type:** `string` 67 | - **Default:** `'https://api.airtable.com'` 68 | 69 | Specifies the API endpoint to use for Airtable. 70 | 71 | ### `appendLocalAttachmentPaths` 72 | 73 | - **Type:** `boolean` 74 | - **Default:** `true` 75 | 76 | Appends the local path of attachments to the saved JSON. 77 | 78 | ### `apiKey` 79 | 80 | - **Type:** `string` 81 | - **Required** 82 | 83 | Specifies the Airtable API Key. 84 | -------------------------------------------------------------------------------- /docs/src/reference/content/sources/contentful-source.md: -------------------------------------------------------------------------------- 1 | # Contentful Content Source 2 | 3 | The `contentfulSource` content source is used to fetch entries and assets from Contentful. It supports both published content (using the Content Delivery API) and draft content (using the Preview API), with built-in pagination handling. 4 | 5 | ## Usage 6 | 7 | To use the `contentfulSource` content source, include it in the list of content sources in your configuration: 8 | 9 | ```typescript{1,6-13} 10 | import { contentfulSource } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | sources: [ 15 | contentfulSource({ 16 | id: 'myContentfulSource', 17 | space: 'spaceXXXXXXXXXXXX', 18 | deliveryToken: 'your-delivery-token', 19 | previewToken: 'your-preview-token', // Optional 20 | contentTypes: ['article', 'page'], // Optional 21 | usePreviewApi: false // Optional 22 | }) 23 | ] 24 | } 25 | }); 26 | ``` 27 | 28 | ## Options 29 | 30 | ### `id` 31 | 32 | - **Type:** `string` 33 | - **Required** 34 | 35 | Specifies the unique identifier for this source. This will be used as the download path. 36 | 37 | ### `space` 38 | 39 | - **Type:** `string` 40 | - **Required** 41 | 42 | Your Contentful space ID. 43 | 44 | ### `deliveryToken` 45 | 46 | - **Type:** `string` 47 | - **Required** (unless using Preview API exclusively) 48 | 49 | Content delivery token used to access published content. 50 | 51 | ### `previewToken` 52 | 53 | - **Type:** `string` 54 | - **Required** if `usePreviewApi` is true 55 | 56 | Content preview token used to access draft/unpublished content. 57 | 58 | ### `usePreviewApi` 59 | 60 | - **Type:** `boolean` 61 | - **Default:** `false` 62 | 63 | Set to true to use the Preview API instead of the Content Delivery API. Requires `previewToken` to be set. 64 | 65 | ### `contentTypes` 66 | 67 | - **Type:** `string[]` 68 | - **Default:** `[]` 69 | 70 | Optionally limit queries to specific content types. This will also apply to linked assets. Types that link to other types will include up to 10 levels of child content. 71 | 72 | ### `filename` 73 | 74 | - **Type:** `string` 75 | - **Default:** `'content.json'` 76 | 77 | The filename where content (entries and assets metadata) will be stored. 78 | 79 | ### `protocol` 80 | 81 | - **Type:** `string` 82 | - **Default:** `'https'` 83 | 84 | This updates the asset urls for better compatibility with the mediaDownloader plugin. By default, asset urls have no protocol. 85 | 86 | ### `host` 87 | 88 | - **Type:** `string` 89 | - **Default:** `'cdn.contentful.com'` or `'preview.contentful.com'` if `usePreviewApi` is true 90 | 91 | The API host to use for requests. 92 | 93 | ### `searchParams` 94 | 95 | - **Type:** `Record` 96 | - **Default:** 97 | 98 | ```typescript 99 | { 100 | limit: 1000, 101 | include: 10 102 | } 103 | ``` 104 | 105 | Additional search parameters to pass to the Contentful API. Supports all parameters from the [Contentful Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters). 106 | -------------------------------------------------------------------------------- /docs/src/reference/content/sources/index.md: -------------------------------------------------------------------------------- 1 | # Content Sources 2 | 3 | Content sources are used to fetch content from various external systems or APIs. These sources define how and where to retrieve content, which is then processed and transformed by content plugins. 4 | 5 | ## Type Reference 6 | 7 | ```typescript 8 | type ContentSource = { 9 | id: string; 10 | fetch: (context: FetchContext) => SourceFetchResult | SourceFetchResult[]; 11 | }; 12 | ``` 13 | 14 | ## Properties 15 | 16 | ### `id` 17 | 18 | The unique identifier for this source. The documents for this source will be written to a subdirectory with this name in the configured download directory. 19 | 20 | ### `fetch` 21 | 22 | A sync callback for fetching the source data. Returns either a single document instance, or an array of documents. 23 | 24 | ## Fetch Context 25 | 26 | ### `logger` 27 | 28 | A logger instance for logging messages and errors during the fetch process. 29 | 30 | ### `dataStore` 31 | 32 | A data store instance for storing and retrieving fetched content. This is useful if one source needs to reference the data from a prior source. Ex: fetching shopify data based on the products referenced in a sanity api call. 33 | 34 | ## Source Fetch Result 35 | 36 | ### `id` 37 | 38 | - **Type:** `string` 39 | 40 | The unique identifier for the fetched document. 41 | 42 | ### `data` 43 | 44 | - **Type:** `Promise | AsyncIterable` 45 | 46 | The fetched data, either as a promise returning a single document or an async iterable returning multiple documents. If it's a promise, it will be written to a single json file. If it's an async iterable, each yield will be written to a separate json file with a index suffix (ie `data-001.json`). 47 | 48 | ## Example 49 | 50 | To define a custom content source, use the `defineSource` function: 51 | 52 | ```typescript 53 | import { defineSource } from '@bluecadet/launchpad-content'; 54 | 55 | export default defineSource({ 56 | id: 'myCustomSource', 57 | fetch: (context) => { 58 | return { 59 | id: 'documentId', 60 | data: fetchDataFromAPI(), 61 | }; 62 | }, 63 | }); 64 | ``` 65 | 66 | For detailed source configuration options, see the specific source documentation. 67 | -------------------------------------------------------------------------------- /docs/src/reference/content/sources/json-source.md: -------------------------------------------------------------------------------- 1 | # JSON Content Source 2 | 3 | The `jsonSource` content source is used to fetch data from JSON endpoints via HTTP(S). It supports fetching multiple JSON files from different URLs and saving them with custom identifiers. 4 | 5 | ## Usage 6 | 7 | To use the `jsonSource` content source, include it in the list of content sources in your configuration: 8 | 9 | ```typescript{1,6-13} 10 | import { jsonSource } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | sources: [ 15 | jsonSource({ 16 | id: 'myJsonSource', 17 | files: { 18 | 'data1': 'https://api.example.com/data1.json', 19 | 'data2': 'https://api.example.com/data2.json' 20 | }, 21 | maxTimeout: 60000 22 | }) 23 | ] 24 | } 25 | }); 26 | ``` 27 | 28 | ## Options 29 | 30 | ### `id` 31 | 32 | - **Type:** `string` 33 | - **Required** 34 | 35 | Specifies the unique identifier for this source. This will be used as the download path. 36 | 37 | ### `files` 38 | 39 | - **Type:** `Record` 40 | - **Required** 41 | 42 | A mapping of JSON keys to URLs. Each key will be used as the identifier for the downloaded JSON file, while the corresponding URL specifies where to fetch the JSON data from. 43 | 44 | For example: 45 | 46 | ```typescript 47 | { 48 | 'settings': 'https://api.example.com/settings.json', 49 | 'users': 'https://api.example.com/users.json' 50 | } 51 | ``` 52 | 53 | This will create files named `settings.json` and `users.json` in the output directory. 54 | 55 | ### `maxTimeout` 56 | 57 | - **Type:** `number` 58 | - **Default:** `30000` 59 | 60 | Specifies the maximum time (in milliseconds) to wait for a response from each JSON endpoint before timing out. The default is 30 seconds. 61 | -------------------------------------------------------------------------------- /docs/src/reference/content/sources/sanity-source.md: -------------------------------------------------------------------------------- 1 | # Sanity Content Source 2 | 3 | The `sanitySource` content source is used to fetch data from Sanity.io. It supports fetching data using GROQ queries and can handle pagination of large datasets. 4 | 5 | ## Usage 6 | 7 | To use the `sanitySource` content source, include it in the list of content sources in your configuration: 8 | 9 | ```typescript{1,6-15} 10 | import { sanitySource } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | sources: [ 15 | sanitySource({ 16 | id: 'mySanitySource', 17 | projectId: 'your-project-id', 18 | dataset: 'production', 19 | apiToken: 'your-api-token', // Required for private datasets 20 | queries: [ 21 | 'project', // Fetches all documents of type 'project' 22 | { id: 'featured', query: '*[_type == "article" && featured == true]' } 23 | ] 24 | }) 25 | ] 26 | } 27 | }); 28 | ``` 29 | 30 | ## Options 31 | 32 | ### `id` 33 | 34 | - **Type:** `string` 35 | - **Required** 36 | 37 | Specifies the unique identifier for this source. This will be used as the download path. 38 | 39 | ### `projectId` 40 | 41 | - **Type:** `string` 42 | - **Required** 43 | 44 | Your Sanity project ID. 45 | 46 | ### `dataset` 47 | 48 | - **Type:** `string` 49 | - **Default:** `'production'` 50 | 51 | The name of the dataset you want to fetch from. 52 | 53 | ### `apiToken` 54 | 55 | - **Type:** `string` 56 | - **Optional** 57 | 58 | Sanity API token. Required if you're accessing a private dataset. 59 | 60 | ### `apiVersion` 61 | 62 | - **Type:** `string` 63 | - **Default:** `'v2021-10-21'` 64 | 65 | The Sanity API version to use. 66 | 67 | ### `queries` 68 | 69 | - **Type:** `Array` 70 | - **Required** 71 | 72 | An array of queries to fetch. Each query can be either: 73 | 74 | - A string representing a document type (e.g., `'article'` will fetch all documents of type 'article') 75 | - An object with a custom GROQ query and an ID for the result set 76 | 77 | > [!NOTE] Note: 78 | > if the query includes a range (e.g., `*[_type == "article"][0...100]` or `*[_type == "article"][0]`), the `limit` and `maxNumPages` options will be ignored, and the query will be executed as is. 79 | 80 | ### `useCdn` 81 | 82 | - **Type:** `boolean` 83 | - **Default:** `true` 84 | 85 | Set to `false` if you want to ensure fresh data instead of potentially cached responses. 86 | 87 | ### `limit` 88 | 89 | - **Type:** `number` 90 | - **Default:** `100` 91 | 92 | Maximum number of documents to fetch per page. 93 | 94 | ### `maxNumPages` 95 | 96 | - **Type:** `number` 97 | - **Default:** `1000` 98 | 99 | Maximum number of pages to fetch. 100 | 101 | ### `mergePages` 102 | 103 | - **Type:** `boolean` 104 | - **Default:** `false` 105 | 106 | When `true`, combines all paginated results into a single file. When `false`, each page is stored separately. 107 | -------------------------------------------------------------------------------- /docs/src/reference/content/sources/strapi-source.md: -------------------------------------------------------------------------------- 1 | # Strapi Content Source 2 | 3 | The `strapiSource` content source is used to fetch data from Strapi CMS. It supports both Strapi v3 and v4, with automatic pagination and customizable query parameters. 4 | 5 | ## Usage 6 | 7 | To use the `strapiSource` content source, include it in the list of content sources in your configuration: 8 | 9 | ```typescript{1,6-13} 10 | import { strapiSource } from '@bluecadet/launchpad-content'; 11 | 12 | export default defineConfig({ 13 | content: { 14 | sources: [ 15 | strapiSource({ 16 | id: 'myStrapiSource', 17 | version: '4', 18 | baseUrl: 'http://localhost:1337', 19 | identifier: 'admin@example.com', 20 | password: 'your-password', 21 | queries: ['api/articles', 'api/categories'] 22 | }) 23 | ] 24 | } 25 | }); 26 | ``` 27 | 28 | ## Options 29 | 30 | ### `id` 31 | 32 | - **Type:** `string` 33 | - **Required** 34 | 35 | Specifies the unique identifier for this source. This will be used as the download path. 36 | 37 | ### `version` 38 | 39 | - **Type:** `"3" | "4"` 40 | - **Default:** `"3"` 41 | 42 | Specifies the Strapi version. Supports either version 3 or 4. 43 | 44 | ### `baseUrl` 45 | 46 | - **Type:** `string` 47 | - **Required** 48 | 49 | The base URL of your Strapi CMS (with or without trailing slash). 50 | 51 | ### `queries` 52 | 53 | - **Type:** `(string | { contentType: string, params: Record })[]` 54 | - **Required** 55 | 56 | Queries for each type of content you want to fetch. You can specify either: 57 | - A string URL path (e.g. `"api/articles"`) 58 | - An object with `contentType` and `params` for more control over the query parameters 59 | 60 | ### `identifier` 61 | 62 | - **Type:** `string` 63 | - **Required if token not provided** 64 | 65 | Username or email for authentication. Should be configured via environment variables. 66 | 67 | ### `password` 68 | 69 | - **Type:** `string` 70 | - **Required if token not provided** 71 | 72 | Password for authentication. Should be configured via environment variables. 73 | 74 | ### `token` 75 | 76 | - **Type:** `string` 77 | - **Required if identifier/password not provided** 78 | 79 | A previously generated JWT token for authentication. 80 | 81 | ### `limit` 82 | 83 | - **Type:** `number` 84 | - **Default:** `100` 85 | 86 | Maximum number of entries to fetch per page. 87 | 88 | ### `maxNumPages` 89 | 90 | - **Type:** `number` 91 | - **Default:** `1000` 92 | 93 | Maximum number of pages to fetch. 94 | 95 | ### `pageNumZeroPad` 96 | 97 | - **Type:** `number` 98 | - **Default:** `2` 99 | 100 | Number of zeros to pad each JSON filename index with. 101 | 102 | -------------------------------------------------------------------------------- /docs/src/reference/monitor/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@bluecadet/launchpad-monitor" 3 | --- 4 | 5 | 8 | 9 | 10 | 11 | The monitor package is a robust process management and monitoring tool designed for media installations. It provides comprehensive control over application lifecycles, window management, and system monitoring. 12 | 13 | ## Features 14 | 15 | - **Process Management**: Complete control over application lifecycles: 16 | - Launch and monitor multiple applications 17 | - Automatic crash recovery 18 | - Graceful shutdowns and restarts 19 | - PM2 integration for advanced process control 20 | 21 | - **Window Management**: Powerful window control capabilities: 22 | - Move windows to foreground/background 23 | - Minimize/maximize operations 24 | - Window visibility control 25 | 26 | - **Logging System**: Comprehensive logging features: 27 | - Centralized log collection 28 | - Configurable log routing 29 | - stdout/stderr capture 30 | - Log file management 31 | - Debug and error tracking 32 | 33 | - **Event System**: Flexible event handling: 34 | - Process lifecycle hooks 35 | - Plugin event integration 36 | 37 | ## Installation 38 | 39 | ```bash 40 | npm install @bluecadet/launchpad-monitor 41 | ``` 42 | 43 | ## Basic Usage 44 | 45 | ```typescript 46 | import LaunchpadMonitor from '@bluecadet/launchpad-monitor'; 47 | 48 | const monitor = new LaunchpadMonitor({ 49 | apps: [ 50 | { 51 | name: "my-app", 52 | pm2: { 53 | script: "./app.js" 54 | }, 55 | windows: { 56 | foreground: true 57 | } 58 | } 59 | ] 60 | }); 61 | 62 | // Start monitoring 63 | await monitor.start(); 64 | ``` 65 | 66 | ## Configuration 67 | 68 | Monitor operations are configured through a `MonitorConfig` object that specifies: 69 | 70 | - **Apps**: Array of applications to manage 71 | - **Process Settings**: PM2 configuration options 72 | - **Window Management**: Window behavior settings 73 | - **Logging Options**: Log handling preferences 74 | 75 | See the [Monitor Config](./monitor-config) section for detailed configuration options. 76 | 77 | ## Error Handling 78 | 79 | The package uses `neverthrow` for reliable error handling: 80 | 81 | - Type-safe error management 82 | - Graceful failure recovery 83 | - Clear error reporting 84 | - Process recovery strategies 85 | 86 | ## Plugin Support 87 | 88 | The monitor package supports plugins for extending functionality: 89 | 90 | - Custom process management 91 | - Enhanced window control 92 | - Additional monitoring capabilities 93 | - Custom event handling 94 | - Integration with other systems 95 | -------------------------------------------------------------------------------- /docs/src/reference/monitor/monitor-config.md: -------------------------------------------------------------------------------- 1 | # Monitor Config 2 | 3 | Configuration for managing process monitoring, window management, and logging settings. 4 | 5 | ## Options 6 | 7 | ### `apps` 8 | 9 | - **Type:** `Array` 10 | - **Default:** `[]` 11 | 12 | A list of apps to launch and monitor. Each app can be configured with PM2 settings, window management options, and logging preferences. 13 | 14 | For detailed app configuration options, see [App Config](#app-config). 15 | 16 | ### `deleteExistingBeforeConnect` 17 | 18 | - **Type:** `boolean` 19 | - **Default:** `false` 20 | 21 | When enabled, deletes existing PM2 processes before connecting. Useful for volatile apps or when node processes might quit unexpectedly, ensuring a clean slate on startup. 22 | 23 | ### `windowsApi` 24 | 25 | - **Type:** `WindowsApiConfig` 26 | - **Default:** `{}` 27 | 28 | Advanced configuration for the Windows API, used for managing foreground/minimized/hidden windows. 29 | 30 | #### `debounceDelay` 31 | 32 | - **Type:** `number` 33 | - **Default:** `3000` 34 | 35 | The delay (in milliseconds) until windows are ordered after launch. If your app takes a long time to open all of its windows, set this to a higher value to ensure it can be on top of the launchpad terminal window. Higher values also reduce CPU load if apps relaunch frequently. 36 | 37 | ### `plugins` 38 | 39 | - **Type:** `Array` 40 | - **Default:** `[]` 41 | 42 | A list of monitor plugins for extending functionality. 43 | 44 | ### `shutdownOnExit` 45 | 46 | - **Type:** `boolean` 47 | - **Default:** `true` 48 | 49 | When enabled, listens for exit events to handle graceful shutdown. 50 | 51 | ## App Config 52 | 53 | Each app in the `apps` array can have the following configuration: 54 | 55 | ### `pm2` 56 | 57 | - **Type:** `pm2.StartOptions` 58 | 59 | PM2 configuration for the app. See [PM2 documentation](https://pm2.keymetrics.io/docs/usage/application-declaration/#attributes-available) for available options. 60 | 61 | ### `windows` 62 | 63 | - **Type:** `WindowConfig` 64 | - **Default:** `{}` 65 | 66 | Settings for window management: 67 | 68 | - `foreground`: Move to foreground after launch 69 | - `minimize`: Minimize windows after launch 70 | - `hide`: Hide windows after launch 71 | 72 | ### `logging` 73 | 74 | - **Type:** `AppLogConfig` 75 | - **Default:** `{}` 76 | 77 | Settings for log management: 78 | 79 | - `logToLaunchpadDir`: Route logs to launchpad's directory 80 | - `mode`: Log capture method ('bus' or 'file') 81 | - `showStdout`: Include stdout output 82 | - `showStderr`: Include stderr output 83 | -------------------------------------------------------------------------------- /docs/src/reference/scaffold/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "@bluecadet/launchpad-scaffold" 3 | --- 4 | 5 | 8 | 9 | 10 | 11 | The scaffold package is a specialized toolset for configuring Windows systems in exhibit and kiosk environments. It provides automated setup and optimization features to prepare Windows machines for reliable, long-term operation. 12 | 13 | ## Features 14 | 15 | - **Windows Kiosk Setup**: 16 | - Automate Windows configuration for kiosk mode 17 | - Configure auto-login and startup applications 18 | - Disable unnecessary Windows features 19 | - Optimize interface for touch and kiosk usage 20 | 21 | - **System Optimization**: 22 | - Power settings configuration 23 | - Windows Update management 24 | - Service optimization 25 | - Performance tuning 26 | - User interface customization 27 | 28 | - **Security Controls**: 29 | - System policy management 30 | - Interface lockdown options 31 | 32 | ## Installation 33 | 34 | ```bash 35 | npm install @bluecadet/launchpad-scaffold 36 | ``` 37 | 38 | ## Basic Usage 39 | 40 | ```typescript 41 | import { launchScaffold } from '@bluecadet/launchpad-scaffold'; 42 | import { LogManager } from '@bluecadet/launchpad-utils'; 43 | 44 | // instantiate the logger before starting the scaffold process 45 | const logger = LogManager.getLogger('my-app'); 46 | 47 | // Launch the scaffold setup process 48 | await launchScaffold(logger); 49 | ``` 50 | 51 | ## System Requirements 52 | 53 | - Windows 10 or Windows 11 54 | - Administrative privileges 55 | - PowerShell execution enabled 56 | - Node.js 18 or higher 57 | 58 | ## Configuration 59 | 60 | The scaffold package uses a combination of: 61 | 62 | - PowerShell scripts for system configuration 63 | - Batch files for process execution 64 | - Node.js for orchestration 65 | - Windows Registry modifications 66 | - System policy updates 67 | 68 | ## Security Considerations 69 | 70 | - Requires elevated privileges 71 | - Modifies system settings 72 | - Changes Windows configurations 73 | - Alters user permissions 74 | 75 | ## Limitations 76 | 77 | - Windows-only support 78 | - Some features require specific Windows versions 79 | - Certain settings may require additional manual configuration 80 | - Windows 11 has limited support for some kiosk features 81 | 82 | ## Error Handling 83 | 84 | - Provides detailed logs of all operations 85 | - Fails gracefully with clear error messages 86 | - Supports rollback of critical changes 87 | - Includes diagnostic information for troubleshooting 88 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | commands: 3 | check: 4 | glob: "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}" 5 | run: npx @biomejs/biome check --write --no-errors-on-unmatched --files-ignore-unknown=true --colors=off {staged_files} 6 | stage_fixed: true -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "launchpad", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "monorepo for @bluecadet/launchpad and friends", 6 | "scripts": { 7 | "changeset": "changeset", 8 | "lint": "npx @biomejs/biome check", 9 | "lint:fix": "npx @biomejs/biome check --fix", 10 | "build": "tsc --build", 11 | "dev": "tsc --build --watch", 12 | "docs:build": "npm run build -w @bluecadet/launchpad-docs", 13 | "release": "npm run build && changeset publish", 14 | "test": "vitest", 15 | "postinstall": "patch-package" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/bluecadet/launchpad.git" 20 | }, 21 | "contributors": [], 22 | "license": "ISC", 23 | "bugs": { 24 | "url": "https://github.com/bluecadet/launchpad/issues" 25 | }, 26 | "homepage": "https://github.com/bluecadet/launchpad#readme", 27 | "workspaces": [ 28 | "./packages/*", 29 | "./docs" 30 | ], 31 | "devDependencies": { 32 | "@biomejs/biome": "1.9.4", 33 | "@changesets/changelog-github": "^0.4.6", 34 | "@changesets/cli": "^2.23.0", 35 | "@types/node": "^22.9.3", 36 | "lefthook": "^1.11.12", 37 | "patch-package": "^8.0.0", 38 | "sherif": "^1.0.1", 39 | "typescript": "^5.7.2", 40 | "vitest": "^3.0.7" 41 | }, 42 | "engines": { 43 | "node": ">=18" 44 | }, 45 | "packageManager": "npm@10.9.0" 46 | } 47 | -------------------------------------------------------------------------------- /packages/cli/README.md: -------------------------------------------------------------------------------- 1 | # @bluecadet/launchpad-cli 2 | 3 | Command line interface for managing media installations with Launchpad. Provides commands for content management, process monitoring, and system configuration. 4 | 5 | ## Documentation 6 | 7 | For complete documentation, configuration options, and guides, visit: 8 | [Launchpad Documentation](https://bluecadet.github.io/launchpad/) 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install @bluecadet/launchpad-cli 14 | 15 | # Install additional modules as needed 16 | npm install @bluecadet/launchpad-content @bluecadet/launchpad-monitor 17 | ``` 18 | 19 | ## Basic Usage 20 | 21 | ```bash 22 | # Download content and start apps 23 | npx launchpad start 24 | 25 | # Only download fresh content 26 | npx launchpad content 27 | 28 | # Only manage apps 29 | npx launchpad monitor 30 | 31 | # Stop all processes 32 | npx launchpad stop 33 | ``` 34 | 35 | ## License 36 | 37 | MIT © Bluecadet 38 | -------------------------------------------------------------------------------- /packages/cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-cli", 3 | "version": "2.1.1", 4 | "description": "CLI for @bluecadet/launchpad utilities", 5 | "engines": { 6 | "npm": ">=8.5.1", 7 | "node": ">=17.5.0" 8 | }, 9 | "type": "module", 10 | "bin": { 11 | "launchpad": "./dist/cli.js" 12 | }, 13 | "exports": { 14 | ".": { 15 | "types": "./dist/index.d.ts", 16 | "default": "./dist/index.js" 17 | }, 18 | "./package.json": "./package.json" 19 | }, 20 | "files": [ 21 | "dist/**/*.js", 22 | "dist/**/*.d.ts" 23 | ], 24 | "scripts": { 25 | "build": "tsc --build" 26 | }, 27 | "repository": { 28 | "type": "git", 29 | "url": "git+https://github.com/bluecadet/launchpad.git" 30 | }, 31 | "author": { 32 | "name": "Bluecadet", 33 | "url": "https://bluecadet.com" 34 | }, 35 | "maintainers": [ 36 | { 37 | "name": "Benjamin Bojko", 38 | "url": "https://github.com/benjaminbojko" 39 | }, 40 | { 41 | "name": "Pete Inge", 42 | "url": "https://github.com/pingevt" 43 | }, 44 | { 45 | "name": "Clay Tercek", 46 | "url": "https://github.com/claytercek" 47 | } 48 | ], 49 | "license": "ISC", 50 | "bugs": { 51 | "url": "https://github.com/bluecadet/launchpad/issues" 52 | }, 53 | "homepage": "https://github.com/bluecadet/launchpad/packages/cli", 54 | "dependencies": { 55 | "@bluecadet/launchpad-utils": "~2.0.1", 56 | "chalk": "^5.0.0", 57 | "dotenv": "^16.4.5", 58 | "jiti": "^2.4.2", 59 | "neverthrow": "^8.1.1", 60 | "yargs": "^17.7.2", 61 | "zod": "^3.23.8" 62 | }, 63 | "peerDependencies": { 64 | "@bluecadet/launchpad-content": "~2.1.0", 65 | "@bluecadet/launchpad-monitor": "~2.0.0", 66 | "@bluecadet/launchpad-scaffold": "~2.0.0" 67 | }, 68 | "peerDependenciesMeta": { 69 | "@bluecadet/launchpad-content": { 70 | "optional": true 71 | }, 72 | "@bluecadet/launchpad-monitor": { 73 | "optional": true 74 | }, 75 | "@bluecadet/launchpad-scaffold": { 76 | "optional": true 77 | } 78 | }, 79 | "devDependencies": { 80 | "@bluecadet/launchpad-tsconfig": "0.1.0", 81 | "@types/yargs": "^17.0.33" 82 | }, 83 | "keywords": [ 84 | "bluecadet", 85 | "cli", 86 | "configuration-management", 87 | "daemon", 88 | "deploy", 89 | "deployment", 90 | "dev ops", 91 | "devops", 92 | "download-manager", 93 | "exhibits", 94 | "forever-monitor", 95 | "forever", 96 | "graceful", 97 | "installations", 98 | "keep process alive", 99 | "log", 100 | "logs", 101 | "monitoring", 102 | "node.js monitoring", 103 | "nodemon", 104 | "pm2", 105 | "process configuration", 106 | "process manager", 107 | "production", 108 | "profiling", 109 | "runtime", 110 | "sysadmin", 111 | "tools", 112 | "windows-desktop" 113 | ] 114 | } 115 | -------------------------------------------------------------------------------- /packages/cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { hideBin } from "yargs/helpers"; 4 | import yargs from "yargs/yargs"; 5 | export { defineConfig } from "./launchpad-config.js"; 6 | 7 | export type LaunchpadArgv = { 8 | config?: string; 9 | env?: (string | number)[]; 10 | envCascade?: string; 11 | }; 12 | 13 | yargs(hideBin(process.argv)) 14 | .parserConfiguration({ 15 | // See https://github.com/yargs/yargs-parser#camel-case-expansion 16 | "camel-case-expansion": false, 17 | }) 18 | .option("config", { alias: "c", describe: "Path to your JS config file", type: "string" }) 19 | .option("env", { alias: "e", describe: "Path(s) to your .env file(s)", type: "array" }) 20 | .option("env-cascade", { 21 | alias: "E", 22 | describe: 23 | "cascade env variables from `.env`, `.env.`, `.env.local`, `.env..local` in launchpad root dir", 24 | type: "string", 25 | }) 26 | .command("start", "Starts launchpad by updating content and starting apps.", async ({ argv }) => { 27 | const resolvedArgv = await argv; 28 | const { start } = await import("./commands/start.js"); 29 | await start(resolvedArgv); 30 | }) 31 | .command( 32 | "stop", 33 | "Stops launchpad by stopping apps and killing any existing PM2 instance.", 34 | async ({ argv }) => { 35 | const resolvedArgv = await argv; 36 | const { stop } = await import("./commands/stop.js"); 37 | await stop(resolvedArgv); 38 | }, 39 | ) 40 | .command("content", "Only download content.", async ({ argv }) => { 41 | const resolvedArgv = await argv; 42 | const { content } = await import("./commands/content.js"); 43 | await content(resolvedArgv); 44 | }) 45 | .command("monitor", "Only start apps.", async ({ argv }) => { 46 | const resolvedArgv = await argv; 47 | const { monitor } = await import("./commands/monitor.js"); 48 | await monitor(resolvedArgv); 49 | }) 50 | .command( 51 | "scaffold", 52 | "Configures the current PC for exhibit environments (with admin prompt).", 53 | async ({ argv }) => { 54 | const resolvedArgv = await argv; 55 | const { scaffold } = await import("./commands/scaffold.js"); 56 | await scaffold(resolvedArgv); 57 | }, 58 | ) 59 | .help() 60 | .parse(); 61 | -------------------------------------------------------------------------------- /packages/cli/src/commands/content.ts: -------------------------------------------------------------------------------- 1 | import { ResultAsync, err, ok } from "neverthrow"; 2 | import type { LaunchpadArgv } from "../cli.js"; 3 | import { ConfigError, ImportError } from "../errors.js"; 4 | import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/command-utils.js"; 5 | 6 | export function content(argv: LaunchpadArgv) { 7 | return loadConfigAndEnv(argv) 8 | .mapErr((error) => handleFatalError(error, console)) 9 | .andThen(initializeLogger) 10 | .andThen(({ config, rootLogger }) => { 11 | return importLaunchpadContent() 12 | .andThen(({ default: LaunchpadContent }) => { 13 | if (!config.content) { 14 | return err(new ConfigError("No content config found in your config file.")); 15 | } 16 | 17 | const contentInstance = new LaunchpadContent(config.content, rootLogger); 18 | return contentInstance.download(); 19 | }) 20 | .orElse((error) => handleFatalError(error, rootLogger)); 21 | }); 22 | } 23 | 24 | export function importLaunchpadContent() { 25 | return ResultAsync.fromPromise( 26 | import("@bluecadet/launchpad-content"), 27 | (e) => 28 | new ImportError( 29 | 'Could not find module "@bluecadet/launchpad-content". Make sure you have installed it.', 30 | { cause: e }, 31 | ), 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/cli/src/commands/monitor.ts: -------------------------------------------------------------------------------- 1 | import { ResultAsync, err, ok } from "neverthrow"; 2 | import type { LaunchpadArgv } from "../cli.js"; 3 | import { ConfigError, ImportError, MonitorError } from "../errors.js"; 4 | import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/command-utils.js"; 5 | 6 | export function monitor(argv: LaunchpadArgv) { 7 | return loadConfigAndEnv(argv) 8 | .mapErr((error) => handleFatalError(error, console)) 9 | .andThen(initializeLogger) 10 | .andThen(({ config, rootLogger }) => { 11 | return importLaunchpadMonitor() 12 | .andThen(({ default: LaunchpadMonitor }) => { 13 | if (!config.monitor) { 14 | return err(new ConfigError("No monitor config found in your config file.")); 15 | } 16 | 17 | const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger); 18 | return ok(monitorInstance); 19 | }) 20 | .andThrough((monitorInstance) => { 21 | return ResultAsync.fromPromise( 22 | monitorInstance.connect(), 23 | (e) => new MonitorError("Failed to connect to monitor", { cause: e }), 24 | ); 25 | }) 26 | .andThrough((monitorInstance) => { 27 | return ResultAsync.fromPromise( 28 | monitorInstance.start(), 29 | (e) => new MonitorError("Failed to start monitor", { cause: e }), 30 | ); 31 | }) 32 | .orElse((error) => handleFatalError(error, rootLogger)); 33 | }); 34 | } 35 | 36 | export function importLaunchpadMonitor() { 37 | return ResultAsync.fromPromise( 38 | import("@bluecadet/launchpad-monitor"), 39 | () => 40 | new ImportError( 41 | 'Could not find module "@bluecadet/launchpad-monitor". Make sure you have installed it.', 42 | ), 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /packages/cli/src/commands/scaffold.ts: -------------------------------------------------------------------------------- 1 | import { launchScaffold } from "@bluecadet/launchpad-scaffold"; 2 | import { LogManager } from "@bluecadet/launchpad-utils"; 3 | import type { LaunchpadArgv } from "../cli.js"; 4 | 5 | export async function scaffold(argv: LaunchpadArgv) { 6 | const rootLogger = LogManager.configureRootLogger(); 7 | await launchScaffold(rootLogger); 8 | } 9 | -------------------------------------------------------------------------------- /packages/cli/src/commands/start.ts: -------------------------------------------------------------------------------- 1 | import { ResultAsync, err, ok } from "neverthrow"; 2 | import type { LaunchpadArgv } from "../cli.js"; 3 | import { ConfigError, MonitorError } from "../errors.js"; 4 | import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/command-utils.js"; 5 | import { importLaunchpadContent } from "./content.js"; 6 | import { importLaunchpadMonitor } from "./monitor.js"; 7 | 8 | export async function start(argv: LaunchpadArgv) { 9 | return loadConfigAndEnv(argv) 10 | .mapErr((error) => handleFatalError(error, console)) 11 | .andThen(initializeLogger) 12 | .andThen(({ config, rootLogger }) => { 13 | return importLaunchpadContent() 14 | .andThen(({ default: LaunchpadContent }) => { 15 | if (!config.content) { 16 | return err(new ConfigError("No content config found in your config file.")); 17 | } 18 | 19 | const contentInstance = new LaunchpadContent(config.content, rootLogger); 20 | return contentInstance.start(); 21 | }) 22 | .andThen(() => importLaunchpadMonitor()) 23 | .andThen(({ default: LaunchpadMonitor }) => { 24 | if (!config.monitor) { 25 | return err(new ConfigError("No monitor config found in your config file.")); 26 | } 27 | 28 | const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger); 29 | return ok(monitorInstance); 30 | }) 31 | .andThrough((monitorInstance) => { 32 | return ResultAsync.fromPromise( 33 | monitorInstance.connect(), 34 | (e) => new MonitorError("Failed to connect to monitor", { cause: e }), 35 | ); 36 | }) 37 | .andThrough((monitorInstance) => { 38 | return ResultAsync.fromPromise( 39 | monitorInstance.start(), 40 | (e) => new MonitorError("Failed to start monitor", { cause: e }), 41 | ); 42 | }) 43 | .orElse((error) => handleFatalError(error, rootLogger)); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /packages/cli/src/commands/stop.ts: -------------------------------------------------------------------------------- 1 | import LaunchpadMonitor from "@bluecadet/launchpad-monitor"; 2 | import { LogManager } from "@bluecadet/launchpad-utils"; 3 | import type { LaunchpadArgv } from "../cli.js"; 4 | 5 | export async function stop(argv: LaunchpadArgv) { 6 | const logger = LogManager.configureRootLogger(); 7 | await LaunchpadMonitor.kill(logger); 8 | } 9 | -------------------------------------------------------------------------------- /packages/cli/src/errors.ts: -------------------------------------------------------------------------------- 1 | class LaunchpadCLIError extends Error { 2 | constructor(...args: ConstructorParameters) { 3 | super(...args); 4 | this.name = "LaunchpadCLIError"; 5 | } 6 | } 7 | 8 | export class ImportError extends LaunchpadCLIError { 9 | constructor(...args: ConstructorParameters) { 10 | super(...args); 11 | this.name = "ImportError"; 12 | } 13 | } 14 | 15 | export class ConfigError extends LaunchpadCLIError { 16 | constructor(...args: ConstructorParameters) { 17 | super(...args); 18 | this.name = "ConfigError"; 19 | } 20 | } 21 | 22 | export class MonitorError extends LaunchpadCLIError { 23 | constructor(...args: ConstructorParameters) { 24 | super(...args); 25 | this.name = "MonitorError"; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/index.ts: -------------------------------------------------------------------------------- 1 | export { defineConfig } from "./launchpad-config.js"; 2 | -------------------------------------------------------------------------------- /packages/cli/src/launchpad-config.ts: -------------------------------------------------------------------------------- 1 | import type { ContentConfig } from "@bluecadet/launchpad-content"; 2 | import type { MonitorConfig } from "@bluecadet/launchpad-monitor"; 3 | import type { LogConfig } from "@bluecadet/launchpad-utils"; 4 | 5 | export type LaunchpadConfig = { 6 | content?: ContentConfig; 7 | monitor?: MonitorConfig; 8 | logging?: LogConfig; 9 | }; 10 | 11 | /** 12 | * Applies defaults to the provided launchpad config. 13 | */ 14 | export function resolveLaunchpadConfig(config: LaunchpadConfig) { 15 | // NOTE: at the moment, there are no defaults to apply 16 | // so this function is just a passthrough 17 | return config; 18 | } 19 | 20 | export type ResolvedLaunchpadOptions = ReturnType; 21 | 22 | /** 23 | * Type definition for the config object. 24 | */ 25 | export function defineConfig(config: LaunchpadConfig) { 26 | return config; 27 | } 28 | -------------------------------------------------------------------------------- /packages/cli/src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import path from "node:path"; 3 | import url from "node:url"; 4 | import chalk from "chalk"; 5 | import { createJiti } from "jiti"; 6 | 7 | const DEFAULT_CONFIG_PATHS = [ 8 | "launchpad.config.js", 9 | "launchpad.config.mjs", 10 | "launchpad.config.ts", 11 | "launchpad.config.cjs", 12 | "launchpad.config.mts", 13 | "launchpad.config.cts", 14 | ]; 15 | 16 | /** 17 | * Searches for all config files in the current and parent directories, up to a max depth of 64. 18 | * @returns {string[]} Array of absolute paths to the config files found. 19 | */ 20 | export function findConfig() { 21 | const configs = findAllConfigsRecursive(); 22 | if (configs.length > 0) { 23 | console.log(`Found configs: ${configs.map((c) => chalk.white(c)).join(", ")}`); 24 | } 25 | return configs.length > 0 ? configs[0] : null; 26 | } 27 | 28 | /** 29 | * Searches for all config files in the current and parent directories, up to a max depth of 64. 30 | * @returns {string[]} Array of absolute paths to the config files found. 31 | */ 32 | function findAllConfigsRecursive() { 33 | const maxDepth = 64; 34 | const foundConfigs = []; 35 | let currentDir = process.env.INIT_CWD ? path.resolve(process.env.INIT_CWD) : process.cwd(); 36 | 37 | for (let i = 0; i < maxDepth; i++) { 38 | for (const defaultPath of DEFAULT_CONFIG_PATHS) { 39 | const candidatePath = path.join(currentDir, defaultPath); 40 | if (fs.existsSync(candidatePath)) { 41 | foundConfigs.push(candidatePath); 42 | } 43 | } 44 | 45 | const parentDir = path.resolve(currentDir, ".."); 46 | if (currentDir === parentDir) { 47 | // Can't navigate any more levels up 48 | break; 49 | } 50 | 51 | currentDir = parentDir; 52 | } 53 | 54 | return foundConfigs; 55 | } 56 | 57 | export async function loadConfigFromFile(configPath: string): Promise> { 58 | if (!configPath) { 59 | return {}; 60 | } 61 | 62 | const jiti = createJiti(import.meta.url); 63 | 64 | try { 65 | // need to use fileURLToPath here for windows support (prefixes with file://) 66 | const fileUrl = url.pathToFileURL(configPath); 67 | return await jiti.import(fileUrl.toString(), { default: true }); 68 | } catch (err) { 69 | throw new Error(`Unable to load config file '${chalk.white(configPath)}'`, { cause: err }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/cli/src/utils/env.ts: -------------------------------------------------------------------------------- 1 | import fs from "node:fs"; 2 | import chalk from "chalk"; 3 | import dotenv from "dotenv"; 4 | 5 | /** 6 | * Load env files from paths into process.env 7 | * @param {string[]} paths 8 | */ 9 | export function resolveEnv(paths: string[]) { 10 | for (const envFilePath of paths) { 11 | // check if file exists at path 12 | if (!fs.existsSync(envFilePath)) { 13 | continue; 14 | } 15 | 16 | // load env file 17 | dotenv.config({ 18 | path: envFilePath, 19 | }); 20 | 21 | console.log(`Loaded env file '${chalk.white(envFilePath)}'`); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "**/*.test.ts", 12 | "**/__tests__/*" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../utils/tsconfig.src.json" 17 | }, 18 | { 19 | "path": "../monitor/tsconfig.src.json" 20 | }, 21 | { 22 | "path": "../scaffold" 23 | }, 24 | { 25 | "path": "../content/tsconfig.src.json" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /packages/content/README.md: -------------------------------------------------------------------------------- 1 | # @bluecadet/launchpad-content 2 | 3 | Content management tools for Launchpad interactive installations. Fetch, transform, and manage content from any source with a flexible plugin system. 4 | 5 | ## Documentation 6 | 7 | For complete documentation, configuration options, and guides, visit: 8 | [Launchpad Documentation](https://bluecadet.github.io/launchpad/) 9 | 10 | ## Features 11 | 12 | - Fetch content from multiple sources (APIs, CMSs, etc.) 13 | - Transform content with plugins 14 | - Automatic backup and restore 15 | - Error handling and logging 16 | - Media file downloads and processing 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install @bluecadet/launchpad-content @bluecadet/launchpad-cli 22 | ``` 23 | 24 | ## Basic Usage 25 | 26 | ```js 27 | // launchpad.config.js 28 | import { defineConfig } from '@bluecadet/launchpad-cli'; 29 | import { jsonSource } from '@bluecadet/launchpad-content'; 30 | 31 | export default defineConfig({ 32 | content: { 33 | sources: [ 34 | jsonSource({ 35 | id: "api-data", 36 | files: { 37 | "data.json": "https://api.example.com/data" 38 | } 39 | }) 40 | ] 41 | } 42 | }); 43 | ``` 44 | 45 | ## License 46 | 47 | MIT © Bluecadet 48 | -------------------------------------------------------------------------------- /packages/content/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-content", 3 | "version": "2.1.3", 4 | "description": "Content syncing pipeline for various sources", 5 | "type": "module", 6 | "files": [ 7 | "dist/**/*.js", 8 | "dist/**/*.d.ts" 9 | ], 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "default": "./dist/index.js" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "scripts": { 18 | "test": "vitest" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/bluecadet/launchpad.git" 23 | }, 24 | "author": "Bluecadet", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/bluecadet/launchpad/issues" 28 | }, 29 | "homepage": "https://github.com/bluecadet/launchpad/packages/content", 30 | "dependencies": { 31 | "@bluecadet/launchpad-utils": "~2.0.1", 32 | "chalk": "^5.0.0", 33 | "glob": "^11.0.0", 34 | "jsonpath-plus": "^10.3.0", 35 | "ky": "^1.7.2", 36 | "markdown-it": "^14.1.0", 37 | "neverthrow": "^8.1.1", 38 | "p-queue": "^7.1.0", 39 | "qs": "^6.11.1", 40 | "sanitize-html": "^2.5.1", 41 | "zod": "^3.23.8" 42 | }, 43 | "peerDependencies": { 44 | "@portabletext/to-html": "2.0.0", 45 | "@sanity/block-content-to-markdown": "^0.0.5", 46 | "@sanity/client": "^6.4.9", 47 | "@sanity/image-url": "^1.1.0", 48 | "airtable": "^0.11.1", 49 | "contentful": "^9.0.0", 50 | "sharp": "^0.33.5" 51 | }, 52 | "peerDependenciesMeta": { 53 | "@portabletext/to-html": { 54 | "optional": true 55 | }, 56 | "@sanity/block-content-to-markdown": { 57 | "optional": true 58 | }, 59 | "@sanity/client": { 60 | "optional": true 61 | }, 62 | "@sanity/image-url": { 63 | "optional": true 64 | }, 65 | "airtable": { 66 | "optional": true 67 | }, 68 | "contentful": { 69 | "optional": true 70 | }, 71 | "sharp": { 72 | "optional": true 73 | } 74 | }, 75 | "devDependencies": { 76 | "@bluecadet/launchpad-testing": "0.1.0", 77 | "@bluecadet/launchpad-tsconfig": "0.1.0", 78 | "@portabletext/to-html": "2.0.0", 79 | "@sanity/block-content-to-markdown": "^0.0.5", 80 | "@sanity/client": "^6.4.9", 81 | "@sanity/image-url": "^1.1.0", 82 | "@types/markdown-it": "^14.1.2", 83 | "@types/qs": "^6.9.7", 84 | "@types/sanitize-html": "^2.9.0", 85 | "airtable": "^0.11.1", 86 | "contentful": "^9.0.0", 87 | "memfs": "^4.14.0", 88 | "msw": "^2.6.6", 89 | "sharp": "^0.33.5", 90 | "vitest": "^3.0.7" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/content/src/index.ts: -------------------------------------------------------------------------------- 1 | import LaunchpadContent from "./launchpad-content.js"; 2 | 3 | export * from "./content-config.js"; 4 | export * from "./launchpad-content.js"; 5 | export * from "./utils/file-utils.js"; 6 | export * from "./sources/index.js"; 7 | export * from "./plugins/index.js"; 8 | export * from "./content-plugin-driver.js"; 9 | 10 | export default LaunchpadContent; 11 | -------------------------------------------------------------------------------- /packages/content/src/plugins/__tests__/plugins.test-utils.ts: -------------------------------------------------------------------------------- 1 | import path from "node:path"; 2 | import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; 3 | import type { Logger } from "@bluecadet/launchpad-utils"; 4 | import { vol } from "memfs"; 5 | import { vi } from "vitest"; 6 | import { afterEach } from "vitest"; 7 | import { type ContentConfig, contentConfigSchema } from "../../content-config.js"; 8 | import { DataStore } from "../../utils/data-store.js"; 9 | 10 | afterEach(() => { 11 | vol.reset(); 12 | }); 13 | 14 | /** 15 | * Creates a test context with a DataStore and logger 16 | */ 17 | export async function createTestPluginContext({ 18 | baseOptions = {}, 19 | logger = createMockLogger(), 20 | }: { namespaces?: string[]; baseOptions?: ContentConfig; logger?: Logger } = {}) { 21 | const data = new DataStore("/"); 22 | 23 | return { 24 | data, 25 | logger, 26 | abortSignal: new AbortController().signal, 27 | paths: { 28 | getDownloadPath: vi 29 | .fn() 30 | .mockImplementation((sourceId?: string) => path.join("/download", sourceId || "")), 31 | getTempPath: vi 32 | .fn() 33 | .mockImplementation((sourceId?: string) => path.join("/temp", sourceId || "")), 34 | getBackupPath: vi 35 | .fn() 36 | .mockImplementation((sourceId?: string) => path.join("/backup", sourceId || "")), 37 | }, 38 | contentOptions: contentConfigSchema.parse(baseOptions), 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /packages/content/src/plugins/__tests__/sanity-to-html.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import sanityToHtml from "../sanity-to-html.js"; 3 | import { createTestPluginContext } from "./plugins.test-utils.js"; 4 | 5 | describe("sanityToHtml plugin", () => { 6 | const validBlock = { 7 | _type: "block", 8 | children: [ 9 | { 10 | _type: "span", 11 | text: "Hello world", 12 | }, 13 | ], 14 | }; 15 | 16 | it("should convert Sanity block to html", async () => { 17 | const ctx = await createTestPluginContext(); 18 | const namespace = (await ctx.data.createNamespace("test"))._unsafeUnwrap(); 19 | await namespace.insert("doc1", Promise.resolve({ content: validBlock })); 20 | 21 | const plugin = sanityToHtml({ path: "$.content" }); 22 | await plugin.hooks.onContentFetchDone(ctx); 23 | 24 | const result = await ctx.data.getDocument("test", "doc1")._unsafeUnwrap()._read(); 25 | expect((result as any).content).toBe("

Hello world

"); 26 | }); 27 | 28 | it("should only transform specified keys", async () => { 29 | const ctx = await createTestPluginContext(); 30 | const testNamespace = (await ctx.data.createNamespace("test"))._unsafeUnwrap(); 31 | const skipNamespace = (await ctx.data.createNamespace("skip"))._unsafeUnwrap(); 32 | await testNamespace.insert("doc1", Promise.resolve({ content: validBlock })); 33 | await skipNamespace.insert("doc2", Promise.resolve({ content: validBlock })); 34 | 35 | const plugin = sanityToHtml({ path: "$.content", keys: ["test"] }); 36 | await plugin.hooks.onContentFetchDone(ctx); 37 | 38 | const transformed = await ctx.data.getDocument("test", "doc1")._unsafeUnwrap()._read(); 39 | const skipped = await ctx.data.getDocument("skip", "doc2")._unsafeUnwrap()._read(); 40 | 41 | expect((transformed as any).content).toBe("

Hello world

"); 42 | expect((skipped as any).content).toEqual(validBlock); 43 | }); 44 | 45 | it("should throw error for invalid block content", async () => { 46 | const ctx = await createTestPluginContext(); 47 | const namespace = (await ctx.data.createNamespace("test"))._unsafeUnwrap(); 48 | await namespace.insert("doc1", Promise.resolve({ content: "not a block" })); 49 | 50 | const plugin = sanityToHtml({ path: "$.content" }); 51 | await expect(plugin.hooks.onContentFetchDone(ctx)).rejects.toThrow( 52 | "Error applying content transform", 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/content/src/plugins/__tests__/sanity-to-markdown.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import sanityToMd from "../sanity-to-markdown.js"; 3 | import { createTestPluginContext } from "./plugins.test-utils.js"; 4 | 5 | describe("sanityToMd plugin", () => { 6 | const validBlock = { 7 | _type: "block", 8 | children: [ 9 | { 10 | _type: "span", 11 | text: "Hello world", 12 | }, 13 | ], 14 | }; 15 | 16 | it("should convert Sanity block to markdown", async () => { 17 | const ctx = await createTestPluginContext(); 18 | const namespace = (await ctx.data.createNamespace("test"))._unsafeUnwrap(); 19 | await namespace.insert("doc1", Promise.resolve({ content: validBlock })); 20 | 21 | const plugin = sanityToMd({ path: "$.content" }); 22 | await plugin.hooks.onContentFetchDone(ctx); 23 | 24 | const result = await ctx.data.getDocument("test", "doc1")._unsafeUnwrap()._read(); 25 | expect((result as any).content).toBe("Hello world"); 26 | }); 27 | 28 | it("should only transform specified keys", async () => { 29 | const ctx = await createTestPluginContext(); 30 | const testNamespace = (await ctx.data.createNamespace("test"))._unsafeUnwrap(); 31 | const skipNamespace = (await ctx.data.createNamespace("skip"))._unsafeUnwrap(); 32 | await testNamespace.insert("doc1", Promise.resolve({ content: validBlock })); 33 | await skipNamespace.insert("doc2", Promise.resolve({ content: validBlock })); 34 | 35 | const plugin = sanityToMd({ path: "$.content", keys: ["test"] }); 36 | await plugin.hooks.onContentFetchDone(ctx); 37 | 38 | const transformed = await ctx.data.getDocument("test", "doc1")._unsafeUnwrap()._read(); 39 | const skipped = await ctx.data.getDocument("skip", "doc2")._unsafeUnwrap()._read(); 40 | 41 | expect((transformed as any).content).toBe("Hello world"); 42 | expect((skipped as any).content).toEqual(validBlock); 43 | }); 44 | 45 | it("should throw error for invalid block content", async () => { 46 | const ctx = await createTestPluginContext(); 47 | const namespace = (await ctx.data.createNamespace("test"))._unsafeUnwrap(); 48 | await namespace.insert("doc1", Promise.resolve({ content: "not a block" })); 49 | 50 | const plugin = sanityToMd({ path: "$.content" }); 51 | await expect(plugin.hooks.onContentFetchDone(ctx)).rejects.toThrow( 52 | "Error applying content transform", 53 | ); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/content/src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | export { default as mdToHtml } from "./md-to-html.js"; 2 | export { default as sanityToHtml } from "./sanity-to-html.js"; 3 | export { default as sanityToPlain } from "./sanity-to-plain.js"; 4 | export { default as sanityToMd } from "./sanity-to-markdown.js"; 5 | export { default as mediaDownloader } from "./media-downloader.js"; 6 | export { default as sharp } from "./sharp.js"; 7 | export { default as sanityImageUrlTransform } from "./sanity-image-url-transform.js"; 8 | -------------------------------------------------------------------------------- /packages/content/src/plugins/md-to-html.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from "markdown-it"; 2 | import sanitizeHtml from "sanitize-html"; 3 | 4 | import { z } from "zod"; 5 | import { defineContentPlugin } from "../content-plugin-driver.js"; 6 | import { applyTransformToFiles } from "../utils/content-transform-utils.js"; 7 | import { type DataKeys, dataKeysSchema } from "../utils/data-store.js"; 8 | import markdownItItalicBold from "../utils/markdown-it-italic-bold.js"; 9 | import { parsePluginConfig } from "./contentPluginHelpers.js"; 10 | 11 | const mdToHtmlSchema = z.object({ 12 | /** JSONPath to the content to transform */ 13 | path: z.string().describe("JSONPath to the content to transform"), 14 | /** Enable for single paragraph content, will render inline */ 15 | simplified: z 16 | .boolean() 17 | .describe("Enable for single paragraph content, will render inline") 18 | .default(false), 19 | /** Data keys to apply the transform to. If not provided, all keys will be transformed */ 20 | keys: dataKeysSchema.optional(), 21 | }); 22 | 23 | export default function mdToHtml(options: z.input) { 24 | const { path, keys, simplified } = parsePluginConfig("mdToHtml", mdToHtmlSchema, options); 25 | 26 | return defineContentPlugin({ 27 | name: "md-to-html", 28 | hooks: { 29 | async onContentFetchDone(ctx) { 30 | let transformCount = 0; 31 | ctx.logger.info("Transforming Markdown strings to HTML..."); 32 | 33 | await applyTransformToFiles({ 34 | dataStore: ctx.data, 35 | path, 36 | keys, 37 | logger: ctx.logger, 38 | transformFn: (content) => { 39 | if (typeof content !== "string") { 40 | throw new Error("Can't convert non-string content to html."); 41 | } 42 | 43 | const sanitizedStr = sanitizeHtml(content); 44 | const md = new MarkdownIt(); 45 | 46 | if (simplified) { 47 | md.use(markdownItItalicBold); 48 | return md.renderInline(sanitizedStr); 49 | } 50 | 51 | transformCount++; 52 | 53 | return md.render(sanitizedStr); 54 | }, 55 | }); 56 | 57 | ctx.logger.info(`Transformed ${transformCount} Markdown strings.`); 58 | }, 59 | }, 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /packages/content/src/plugins/sanity-to-html.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { defineContentPlugin } from "../content-plugin-driver.js"; 3 | import { applyTransformToFiles, isBlockContent } from "../utils/content-transform-utils.js"; 4 | import { dataKeysSchema } from "../utils/data-store.js"; 5 | import { parsePluginConfig } from "./contentPluginHelpers.js"; 6 | 7 | const sanityToHtmlSchema = z.object({ 8 | /** JSONPath to the content to transform */ 9 | path: z.string().describe("JSONPath to the content to transform"), 10 | /** Data keys to apply the transform to. If not provided, all keys will be transformed. */ 11 | keys: dataKeysSchema.optional(), 12 | }); 13 | 14 | function tryImportPortableText() { 15 | try { 16 | return import("@portabletext/to-html"); 17 | } catch (e) { 18 | throw new Error( 19 | 'Could not find peer dependency "@portabletext/to-html". Make sure you have installed it.', 20 | { cause: e }, 21 | ); 22 | } 23 | } 24 | 25 | export default function sanityToHtml(options: z.input) { 26 | const { path, keys } = parsePluginConfig("sanityToHtml", sanityToHtmlSchema, options); 27 | 28 | return defineContentPlugin({ 29 | name: "sanity-to-html", 30 | hooks: { 31 | async onContentFetchDone(ctx) { 32 | const { toHTML } = await tryImportPortableText(); 33 | 34 | let transformCount = 0; 35 | ctx.logger.info("Transforming sanity blocks to HTML..."); 36 | 37 | await applyTransformToFiles({ 38 | dataStore: ctx.data, 39 | path, 40 | keys, 41 | logger: ctx.logger, 42 | transformFn: (content) => { 43 | if (!isBlockContent(content)) { 44 | throw new Error(`Content is not a valid Sanity text block: ${content}`); 45 | } 46 | 47 | transformCount++; 48 | 49 | return toHTML(content); 50 | }, 51 | }); 52 | 53 | ctx.logger.info(`Transformed ${transformCount} Sanity blocks to HTML.`); 54 | }, 55 | }, 56 | }); 57 | } 58 | -------------------------------------------------------------------------------- /packages/content/src/plugins/sanity-to-markdown.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { defineContentPlugin } from "../content-plugin-driver.js"; 3 | import { applyTransformToFiles, isBlockContent } from "../utils/content-transform-utils.js"; 4 | import { dataKeysSchema } from "../utils/data-store.js"; 5 | import { parsePluginConfig } from "./contentPluginHelpers.js"; 6 | 7 | const sanityToMdSchema = z.object({ 8 | /** JSONPath to the content to transform */ 9 | path: z.string().describe("JSONPath to the content to transform"), 10 | /** Data keys to apply the transform to. If not provided, all keys will be transformed. */ 11 | keys: dataKeysSchema.optional(), 12 | }); 13 | 14 | function tryImportBlockToMd() { 15 | try { 16 | // @ts-expect-error - no types from this lib 17 | return import("@sanity/block-content-to-markdown"); 18 | } catch (e) { 19 | throw new Error( 20 | 'Could not find peer dependency "@sanity/block-content-to-markdown". Make sure you have installed it.', 21 | { cause: e }, 22 | ); 23 | } 24 | } 25 | 26 | export default function sanityToMd(options: z.input) { 27 | const { path, keys } = parsePluginConfig("sanityToMd", sanityToMdSchema, options); 28 | 29 | return defineContentPlugin({ 30 | name: "sanity-to-markdown", 31 | hooks: { 32 | async onContentFetchDone(ctx) { 33 | const { default: toMarkdown } = await tryImportBlockToMd(); 34 | 35 | let transformCount = 0; 36 | ctx.logger.info("Transforming sanity blocks to markdown..."); 37 | 38 | await applyTransformToFiles({ 39 | dataStore: ctx.data, 40 | path, 41 | keys, 42 | logger: ctx.logger, 43 | transformFn: (content) => { 44 | if (!isBlockContent(content)) { 45 | throw new Error(`Content is not a valid Sanity text block: ${content}`); 46 | } 47 | 48 | transformCount++; 49 | 50 | return toMarkdown(content); 51 | }, 52 | }); 53 | 54 | ctx.logger.info(`Transformed ${transformCount} Sanity blocks to markdown.`); 55 | }, 56 | }, 57 | }); 58 | } 59 | -------------------------------------------------------------------------------- /packages/content/src/plugins/sanity-to-plain.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import { defineContentPlugin } from "../content-plugin-driver.js"; 3 | import { applyTransformToFiles, isBlockContent } from "../utils/content-transform-utils.js"; 4 | import { dataKeysSchema } from "../utils/data-store.js"; 5 | import { parsePluginConfig } from "./contentPluginHelpers.js"; 6 | 7 | const sanityToPlainSchema = z.object({ 8 | /** JSONPath to the content to transform */ 9 | path: z.string().describe("JSONPath to the content to transform"), 10 | /** Data keys to apply the transform to. If not provided, all keys will be transformed. */ 11 | keys: dataKeysSchema.optional(), 12 | }); 13 | 14 | export default function sanityToPlain(options: z.input) { 15 | const { path, keys } = parsePluginConfig("sanityToPlain", sanityToPlainSchema, options); 16 | 17 | return defineContentPlugin({ 18 | name: "sanity-to-plain", 19 | hooks: { 20 | async onContentFetchDone(ctx) { 21 | let transformCount = 0; 22 | ctx.logger.info("Transforming sanity blocks to plain text..."); 23 | 24 | await applyTransformToFiles({ 25 | dataStore: ctx.data, 26 | path, 27 | keys, 28 | logger: ctx.logger, 29 | transformFn: (content) => { 30 | if (!isBlockWithChildren(content)) { 31 | throw new Error(`Content is not a valid Sanity text block: ${content}`); 32 | } 33 | 34 | transformCount++; 35 | 36 | return content.children.map((child) => child.text).join(""); 37 | }, 38 | }); 39 | 40 | ctx.logger.info(`Transformed ${transformCount} Sanity blocks to plain text.`); 41 | }, 42 | }, 43 | }); 44 | } 45 | 46 | function isBlockWithChildren( 47 | content: unknown, 48 | ): content is { _type: "block"; children: { text: string }[] } { 49 | // check if object 50 | if (!isBlockContent(content)) { 51 | return false; 52 | } 53 | 54 | // check if children 55 | if (!("children" in content) || !Array.isArray(content.children)) { 56 | return false; 57 | } 58 | 59 | // check if children are objects with 'text' property 60 | if (!content.children.every((child) => typeof child === "object" && "text" in child)) { 61 | return false; 62 | } 63 | 64 | return true; 65 | } 66 | -------------------------------------------------------------------------------- /packages/content/src/sources/index.ts: -------------------------------------------------------------------------------- 1 | export { default as jsonSource } from "./json-source.js"; 2 | export { default as airtableSource } from "./airtable-source.js"; 3 | export { default as contentfulSource } from "./contentful-source.js"; 4 | export { default as sanitySource } from "./sanity-source.js"; 5 | export { default as strapiSource } from "./strapi-source.js"; 6 | export * from "./source.js"; 7 | -------------------------------------------------------------------------------- /packages/content/src/sources/json-source.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk"; 2 | import ky from "ky"; 3 | import { okAsync } from "neverthrow"; 4 | import { z } from "zod"; 5 | import { defineSource } from "./source.js"; 6 | 7 | const jsonSourceSchema = z.object({ 8 | /** required field to identify this source. Will be used as download path. */ 9 | id: z.string().describe("Required field to identify this source. Will be used as download path."), 10 | /** A mapping of json key -> url */ 11 | files: z.record(z.string(), z.string()).describe("A mapping of json key -> url"), 12 | /** Max request timeout in ms. Defaults to 30 seconds. */ 13 | maxTimeout: z.number().describe("Max request timeout in ms.").default(30_000), 14 | }); 15 | 16 | export default function jsonSource(options: z.input) { 17 | const parsedOptions = jsonSourceSchema.parse(options); 18 | 19 | return defineSource({ 20 | id: parsedOptions.id, 21 | fetch: (ctx) => { 22 | return Object.entries(parsedOptions.files).map(([key, url]) => { 23 | ctx.logger.debug(`Downloading json ${chalk.blue(url)}`); 24 | 25 | return { 26 | id: key, 27 | data: ky.get(url, { timeout: parsedOptions.maxTimeout }).json(), 28 | }; 29 | }); 30 | }, 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /packages/content/src/sources/source.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from "@bluecadet/launchpad-utils"; 2 | import { z } from "zod"; 3 | import type { DataStore } from "../utils/data-store.js"; 4 | 5 | /** 6 | * Context object passed to the `fetch` method of a source. 7 | */ 8 | export type FetchContext = { 9 | /** 10 | * Logger instance 11 | */ 12 | logger: Logger; 13 | /** 14 | * Data store instance 15 | */ 16 | dataStore: DataStore; 17 | }; 18 | 19 | function asyncIterableSchema(): z.ZodType> { 20 | return z.custom>( 21 | (data) => { 22 | if (typeof data[Symbol.asyncIterator] === "function") { 23 | return true; 24 | } 25 | return false; 26 | }, 27 | { 28 | message: "Expected an AsyncIterable", 29 | }, 30 | ); 31 | } 32 | 33 | const sourceFetchResultDocumentSchema = z.object({ 34 | /** Id of the document, which is how it will be referenced in the data store */ 35 | id: z 36 | .string() 37 | .describe("Id of the document, which is how it will be referenced in the data store"), 38 | /** Either a promise returning a single document, or an async iterable returning multiple documents. */ 39 | data: z 40 | .union([z.promise(z.unknown()), asyncIterableSchema()]) 41 | .describe( 42 | "Either a promise returning a single document, or an async iterable returning multiple documents.", 43 | ), 44 | }); 45 | 46 | export const contentSourceSchema = z.object({ 47 | /** Id of the source. This will be the 'namespace' for the documents fetched from this source. */ 48 | id: z 49 | .string() 50 | .describe( 51 | "Id of the source. This will be the 'namespace' for the documents fetched from this source.", 52 | ), 53 | /** Fetches the documents from the source. Returns either an array of documents or a single document. */ 54 | fetch: z 55 | .function(z.tuple([z.custom().describe("Fetch context object.")])) 56 | .returns(z.union([z.array(sourceFetchResultDocumentSchema), sourceFetchResultDocumentSchema])) 57 | .describe( 58 | "Fetches the documents from the source. Returns either an array of documents or a single document.", 59 | ), 60 | }); 61 | 62 | export type ContentSource = z.infer; 63 | 64 | /** 65 | * This function doesn't do anything, just returns the source parameter. It's just to make it easier to define/type sources. 66 | */ 67 | export function defineSource(src: T): T { 68 | return src; 69 | } 70 | -------------------------------------------------------------------------------- /packages/content/src/utils/__tests__/markdown-it-italic-bold.test.ts: -------------------------------------------------------------------------------- 1 | import MarkdownIt from "markdown-it"; 2 | import { describe, expect, it } from "vitest"; 3 | import italicBoldPlugin from "../markdown-it-italic-bold.js"; 4 | 5 | describe("markdown-it-italic-bold plugin", () => { 6 | it("should convert * to for italics", () => { 7 | const md = new MarkdownIt(); 8 | md.use(italicBoldPlugin); 9 | const result = md.render("*italic*"); 10 | expect(result.trim()).toBe("

italic

"); 11 | }); 12 | 13 | it("should convert _ to for italics", () => { 14 | const md = new MarkdownIt(); 15 | md.use(italicBoldPlugin); 16 | const result = md.render("_italic_"); 17 | expect(result.trim()).toBe("

italic

"); 18 | }); 19 | 20 | it("should convert ** to for bold", () => { 21 | const md = new MarkdownIt(); 22 | md.use(italicBoldPlugin); 23 | const result = md.render("**bold**"); 24 | expect(result.trim()).toBe("

bold

"); 25 | }); 26 | 27 | it("should convert __ to for bold", () => { 28 | const md = new MarkdownIt(); 29 | md.use(italicBoldPlugin); 30 | const result = md.render("__bold__"); 31 | expect(result.trim()).toBe("

bold

"); 32 | }); 33 | 34 | it("should handle mixed italic and bold", () => { 35 | const md = new MarkdownIt(); 36 | md.use(italicBoldPlugin); 37 | const result = md.render("*italic* and **bold**"); 38 | expect(result.trim()).toBe("

italic and bold

"); 39 | }); 40 | 41 | it("should handle nested italic and bold", () => { 42 | const md = new MarkdownIt(); 43 | md.use(italicBoldPlugin); 44 | const result = md.render("**bold *italic* text**"); 45 | expect(result.trim()).toBe("

bold italic text

"); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/content/src/utils/__tests__/safe-ky.test.ts: -------------------------------------------------------------------------------- 1 | import { http, HttpResponse } from "msw"; 2 | import { setupServer } from "msw/node"; 3 | import { ok } from "neverthrow"; 4 | import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; 5 | import { SafeKyFetchError, SafeKyParseError, safeKy } from "../safe-ky.js"; 6 | 7 | const server = setupServer(); 8 | 9 | beforeAll(() => server.listen({ onUnhandledRequest: "error" })); 10 | afterAll(() => server.close()); 11 | afterEach(() => server.resetHandlers()); 12 | 13 | describe("safeKy", () => { 14 | it("should handle successful requests", async () => { 15 | server.use( 16 | http.get("https://api.example.com/json", () => { 17 | return HttpResponse.json({ data: "test" }); 18 | }), 19 | http.get("https://api.example.com/text", () => { 20 | return HttpResponse.text("test"); 21 | }), 22 | http.get("https://api.example.com/arrayBuffer", () => { 23 | return HttpResponse.arrayBuffer(new Uint8Array([1, 2, 3]).buffer); 24 | }), 25 | http.get("https://api.example.com/blob", () => { 26 | return new HttpResponse(new Blob(["test"])); 27 | }), 28 | ); 29 | 30 | const jsonResult = await safeKy("https://api.example.com/json").json(); 31 | expect(jsonResult).toEqual(ok({ data: "test" })); 32 | 33 | const textResult = await safeKy("https://api.example.com/text").text(); 34 | expect(textResult).toEqual(ok("test")); 35 | 36 | const arrayBufferResult = await safeKy("https://api.example.com/arrayBuffer").arrayBuffer(); 37 | expect(arrayBufferResult).toEqual(ok(new Uint8Array([1, 2, 3]).buffer)); 38 | 39 | const blobResult = await safeKy("https://api.example.com/blob").blob(); 40 | expect(blobResult).toEqual(ok(new Blob(["test"]))); 41 | }); 42 | 43 | it("should handle network errors", async () => { 44 | server.use( 45 | http.get("https://api.example.com", () => { 46 | return HttpResponse.error(); 47 | }), 48 | ); 49 | 50 | const result = safeKy("https://api.example.com", { 51 | retry: { limit: 0 }, 52 | }); 53 | 54 | const jsonResult = await result.json(); 55 | expect(jsonResult).toBeErr(); 56 | expect(jsonResult._unsafeUnwrapErr()).toBeInstanceOf(SafeKyFetchError); 57 | expect(jsonResult._unsafeUnwrapErr().message).toContain("Error during request"); 58 | }); 59 | 60 | it("should handle parsing errors", async () => { 61 | server.use( 62 | http.get("https://api.example.com", () => { 63 | return new HttpResponse("Invalid JSON", { 64 | headers: { "Content-Type": "application/json" }, 65 | }); 66 | }), 67 | ); 68 | 69 | const result = safeKy("https://api.example.com"); 70 | 71 | const jsonResult = await result.json(); 72 | expect(jsonResult).toBeErr(); 73 | expect(jsonResult._unsafeUnwrapErr()).toBeInstanceOf(SafeKyParseError); 74 | expect(jsonResult._unsafeUnwrapErr().message).toContain("Error parsing JSON"); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /packages/content/src/utils/content-transform-utils.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from "@bluecadet/launchpad-utils"; 2 | import chalk from "chalk"; 3 | import { type Result, ok } from "neverthrow"; 4 | import type { DataKeys, DataStore, Document } from "./data-store.js"; 5 | 6 | /** 7 | * @param ids A list containing a combination of namespace ids, and namespace/document id tuples. If not provided, all documents will be matched. 8 | */ 9 | export function getMatchingDocuments( 10 | dataStore: DataStore, 11 | ids?: DataKeys, 12 | ): Result, Error> { 13 | if (!ids) { 14 | return ok(dataStore.allDocuments()); 15 | } 16 | 17 | return dataStore.filter(ids).map((results) => results.flatMap((result) => result.documents)); 18 | } 19 | 20 | export function regexToJSONPathQuery(regex: RegExp): string { 21 | return `$..[?(@.match(${regex}))]`; 22 | } 23 | 24 | type ApplyTransformToFilesParams = { 25 | dataStore: DataStore; 26 | path: string; 27 | transformFn: (content: unknown) => unknown; 28 | logger: Logger; 29 | keys?: DataKeys; 30 | }; 31 | 32 | /** 33 | * Shared logic for content transforms 34 | */ 35 | export async function applyTransformToFiles({ 36 | dataStore, 37 | path, 38 | transformFn, 39 | logger, 40 | keys, 41 | }: ApplyTransformToFilesParams) { 42 | const pathStr = chalk.yellow(path); 43 | 44 | const matchingDocuments = getMatchingDocuments(dataStore, keys); 45 | 46 | if (matchingDocuments.isErr()) { 47 | throw matchingDocuments.error; 48 | } 49 | 50 | for (const document of matchingDocuments.value) { 51 | logger.debug(chalk.gray(`Applying content transform to '${pathStr}' for key '${document.id}'`)); 52 | 53 | await document.apply(path, transformFn); 54 | } 55 | } 56 | 57 | export function isBlockContent(content: unknown): content is { _type: "block" } { 58 | // check if object 59 | if (typeof content !== "object" || content === null) { 60 | return false; 61 | } 62 | 63 | // check if block 64 | if (!("_type" in content) || content._type !== "block") { 65 | return false; 66 | } 67 | 68 | return true; 69 | } 70 | -------------------------------------------------------------------------------- /packages/content/src/utils/fetch-logger.ts: -------------------------------------------------------------------------------- 1 | import { FixedConsoleLogger, NO_TTY } from "@bluecadet/launchpad-utils"; 2 | import chalk from "chalk"; 3 | import type { ResultAsync } from "neverthrow"; 4 | 5 | const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; 6 | 7 | type FetchState = 8 | | { 9 | state: "pending"; 10 | } 11 | | { 12 | state: "resolved"; 13 | duration: number; 14 | } 15 | | { 16 | state: "rejected"; 17 | duration: number; 18 | }; 19 | 20 | export class FetchLogger extends FixedConsoleLogger { 21 | #fetches: Map> = new Map(); 22 | #spinnerFrameIndex = 0; 23 | 24 | async addFetch( 25 | sourceId: string, 26 | documentId: string, 27 | fetchPromise: ResultAsync, 28 | ): Promise { 29 | if (!this.#fetches.has(sourceId)) { 30 | this.#fetches.set(sourceId, new Map()); 31 | } 32 | 33 | this.#fetches.get(sourceId)?.set(documentId, { 34 | state: "pending", 35 | }); 36 | 37 | const startTime = Date.now(); 38 | 39 | try { 40 | const result = await fetchPromise; 41 | 42 | if (!result.isOk()) { 43 | throw result.error; 44 | } 45 | 46 | const endTime = Date.now(); 47 | const duration = endTime - startTime; 48 | this.#fetches.get(sourceId)?.set(documentId, { state: "resolved", duration }); 49 | this.logger.info( 50 | `${this.getIcon("resolved")} Fetched ${documentId} from ${sourceId} in ${duration}ms`, 51 | { 52 | [NO_TTY]: true, 53 | }, 54 | ); 55 | } catch (e) { 56 | const endTime = Date.now(); 57 | const duration = endTime - startTime; 58 | this.#fetches.get(sourceId)?.set(documentId, { state: "rejected", duration }); 59 | this.logger.error( 60 | `${this.getIcon("rejected")} Fetched ${documentId} from ${sourceId} in ${duration}ms`, 61 | { 62 | [NO_TTY]: true, 63 | }, 64 | ); 65 | } 66 | } 67 | 68 | getIcon(state: FetchState["state"]) { 69 | switch (state) { 70 | case "pending": 71 | return chalk.cyan(spinnerFrames[this.#spinnerFrameIndex]); 72 | case "resolved": 73 | return chalk.green`✓`; 74 | case "rejected": 75 | return chalk.red`✗`; 76 | } 77 | } 78 | 79 | override getFixedConsoleMessage(): string { 80 | this.#spinnerFrameIndex = (this.#spinnerFrameIndex + 1) % spinnerFrames.length; 81 | 82 | let message = ""; 83 | for (const [sourceId, documentFetches] of this.#fetches) { 84 | for (const [documentId, fetchState] of documentFetches) { 85 | message += ` ${this.getIcon(fetchState.state)} ${chalk.gray(`${sourceId}/`)}${documentId}`; 86 | 87 | if (fetchState.state !== "pending") { 88 | message += chalk.gray(` (${fetchState.duration}ms)`); 89 | } 90 | 91 | message += "\n"; 92 | } 93 | } 94 | 95 | return message; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /packages/content/src/utils/fetch-paginated.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from "@bluecadet/launchpad-utils"; 2 | 3 | export type FetchPaginatedOptions = { 4 | /** 5 | * The number of items to fetch per page 6 | */ 7 | limit: number; 8 | /** 9 | * The maximum number of pages to fetch. If this is reached, the fetch will be terminated early. 10 | */ 11 | maxFetchCount?: number; 12 | /** 13 | * A function that takes a params object and returns a ResultAsync of an array of T. To indicate the end of pagination, return an empty array, or null. 14 | */ 15 | fetchPageFn: (params: { limit: number; offset: number }) => Promise; 16 | /** 17 | * A logger instance 18 | */ 19 | logger: Logger; 20 | /** 21 | * Whether to merge pages into a single array. Defaults to false. 22 | */ 23 | mergePages?: Merge; 24 | }; 25 | 26 | /** 27 | * Handles paginated fetching. Returns an async iterable, unless mergePages is true in which case it returns a flattened array. 28 | */ 29 | export function fetchPaginated({ 30 | fetchPageFn, 31 | limit, 32 | logger, 33 | maxFetchCount = 1000, 34 | mergePages, 35 | }: FetchPaginatedOptions): Merge extends true ? Promise : AsyncGenerator { 36 | async function* generator() { 37 | for (let i = 0; i < maxFetchCount; i++) { 38 | logger.debug(`Fetching page ${i}`); 39 | const data = await fetchPageFn({ limit, offset: i * limit }); 40 | 41 | if (data === null || (Array.isArray(data) && data.length === 0)) { 42 | return; 43 | } 44 | 45 | yield data as T; 46 | } 47 | } 48 | 49 | return (mergePages ? getFlattened(generator()) : generator()) as Merge extends true 50 | ? Promise 51 | : AsyncGenerator; 52 | } 53 | 54 | async function getFlattened(generator: AsyncGenerator) { 55 | const pages: T[] = []; 56 | for await (const page of generator) { 57 | pages.push(page); 58 | } 59 | return pages.flat(1); 60 | } 61 | -------------------------------------------------------------------------------- /packages/content/src/utils/markdown-it-italic-bold.ts: -------------------------------------------------------------------------------- 1 | import type { PluginSimple } from "markdown-it"; 2 | import type { RenderRule } from "markdown-it/lib/renderer.mjs"; 3 | 4 | const render: RenderRule = (tokens, idx, options, _env, self) => { 5 | const token = tokens[idx]; 6 | 7 | if (!token) { 8 | return ""; 9 | } 10 | 11 | if (token.markup === "*" || token.markup === "_") { 12 | token.tag = "i"; 13 | } else if (token.markup === "**" || token.markup === "__") { 14 | token.tag = "b"; 15 | } 16 | return self.renderToken(tokens, idx, options); 17 | }; 18 | 19 | const plugin: PluginSimple = (md) => { 20 | md.renderer.rules.em_open = render; 21 | md.renderer.rules.em_close = render; 22 | md.renderer.rules.strong_open = render; 23 | md.renderer.rules.strong_close = render; 24 | }; 25 | 26 | export default plugin; 27 | -------------------------------------------------------------------------------- /packages/content/src/utils/result-async-queue.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from "@bluecadet/launchpad-utils"; 2 | import chalk from "chalk"; 3 | import { Result, ResultAsync, err, ok } from "neverthrow"; 4 | import PQueue from "p-queue"; 5 | 6 | type ResultAsyncTaskOptions = { 7 | signal?: AbortSignal; 8 | }; 9 | 10 | export type ResultAsyncTask = ( 11 | options: ResultAsyncTaskOptions, 12 | ) => ResultAsync; 13 | 14 | /** 15 | * Wraps a PQueue instance to provide a ResultAsync interface. 16 | */ 17 | export default class ResultAsyncQueue { 18 | queue: PQueue; 19 | 20 | constructor(options?: ConstructorParameters[0]) { 21 | this.queue = new PQueue(options); 22 | } 23 | 24 | /** 25 | * Add a ResultAsync to the queue, returning a ResultAsync that resolves to the task's result, or void if the task is aborted. 26 | */ 27 | add( 28 | task: ResultAsyncTask, 29 | ): ResultAsync { 30 | return ResultAsync.fromSafePromise(this.queue.add(task)).andThen((result) => { 31 | if (!result) { 32 | return ok(undefined); 33 | } 34 | return result; 35 | }); 36 | } 37 | 38 | addAll( 39 | tasks: Array>, 40 | options: { logger: Logger; abortOnError?: boolean }, 41 | ): ResultAsync, Array> { 42 | let wrappedTasks = tasks; 43 | 44 | if (options.abortOnError) { 45 | wrappedTasks = tasks.map((task) => { 46 | return (...args) => 47 | task(...args).mapErr((e) => { 48 | this.queue.clear(); 49 | options.logger.error( 50 | `Cancelled ${chalk.red(`${this.queue.size} remaining sync tasks`)} due to ${chalk.red("error")}:`, 51 | ); 52 | return e; 53 | }); 54 | }); 55 | } 56 | 57 | return ResultAsync.fromSafePromise(this.queue.addAll(tasks)).andThen((val) => { 58 | return Result.combineWithAllErrors(val); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/content/src/utils/safe-ky.ts: -------------------------------------------------------------------------------- 1 | import ky, { type Input, type Options, type KyResponse, type ResponsePromise } from "ky"; 2 | import { Err, Ok, ResultAsync, err, ok } from "neverthrow"; 3 | 4 | export class SafeKyFetchError extends Error { 5 | constructor(...args: ConstructorParameters) { 6 | super(...args); 7 | this.name = "SafeKyFetchError"; 8 | } 9 | } 10 | 11 | export class SafeKyParseError extends Error { 12 | constructor(...args: ConstructorParameters) { 13 | super(...args); 14 | this.name = "SafeKyParseError"; 15 | } 16 | } 17 | 18 | export function safeKy(input: Input, options?: Options): SafeKyResultAsync { 19 | const req = ky(input, options); 20 | return SafeKyResultAsync.fromRequest(req); 21 | } 22 | 23 | type SafeKyResponseResult = Omit< 24 | KyResponse, 25 | "json" | "text" | "arrayBuffer" | "blob" 26 | > & { 27 | // biome-ignore lint/suspicious/noExplicitAny: TODO 28 | json: () => ResultAsync; 29 | text: () => ResultAsync; 30 | arrayBuffer: () => ResultAsync; 31 | blob: () => ResultAsync; 32 | }; 33 | 34 | class SafeKyResultAsync extends ResultAsync< 35 | SafeKyResponseResult, 36 | SafeKyFetchError | SafeKyParseError 37 | > { 38 | static fromRequest(promise: ResponsePromise): SafeKyResultAsync { 39 | const newPromise = promise 40 | .then((res) => { 41 | const remapped = { 42 | headers: res.headers, 43 | ok: res.ok, 44 | status: res.status, 45 | statusText: res.statusText, 46 | type: res.type, 47 | url: res.url, 48 | redirected: res.redirected, 49 | body: res.body, 50 | json: () => 51 | ResultAsync.fromPromise( 52 | res.json(), 53 | (error) => new SafeKyParseError("Error parsing JSON", { cause: error }), 54 | ), 55 | text: () => 56 | ResultAsync.fromPromise( 57 | res.text(), 58 | (error) => new SafeKyParseError("Error parsing text", { cause: error }), 59 | ), 60 | arrayBuffer: () => 61 | ResultAsync.fromPromise( 62 | res.arrayBuffer(), 63 | (error) => new SafeKyParseError("Error parsing array buffer", { cause: error }), 64 | ), 65 | blob: () => 66 | ResultAsync.fromPromise( 67 | res.blob(), 68 | (error) => new SafeKyParseError("Error parsing blob", { cause: error }), 69 | ), 70 | }; 71 | 72 | return new Ok(remapped) as Ok, SafeKyFetchError | SafeKyParseError>; 73 | }) 74 | .catch((error) => { 75 | return new Err(new SafeKyFetchError("Error during request", { cause: error })) as Err< 76 | SafeKyResponseResult, 77 | SafeKyFetchError | SafeKyParseError 78 | >; 79 | }); 80 | 81 | return new SafeKyResultAsync(newPromise); 82 | } 83 | 84 | json() { 85 | return this.andThen((res) => res.json()); 86 | } 87 | 88 | text() { 89 | return this.andThen((res) => res.text()); 90 | } 91 | 92 | arrayBuffer() { 93 | return this.andThen((res) => res.arrayBuffer()); 94 | } 95 | 96 | blob() { 97 | return this.andThen((res) => res.blob()); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/content/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.src.json" 5 | }, 6 | { 7 | "path": "./tsconfig.test.json" 8 | } 9 | ], 10 | "files": [] 11 | } -------------------------------------------------------------------------------- /packages/content/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "**/*.test.ts", 12 | "**/__tests__/*" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../utils/tsconfig.src.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/content/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/test.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "@bluecadet/launchpad-testing/vitest.d.ts" 6 | ] 7 | }, 8 | "include": [ 9 | "src/**/*.test.ts", 10 | "src/**/__tests__", 11 | "vitest.config.ts" 12 | ], 13 | "references": [ 14 | { 15 | "path": "./tsconfig.src.json" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/content/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineProject } from 'vitest/config'; 2 | 3 | export default defineProject({ 4 | test: { 5 | environment: 'node', 6 | setupFiles: '@bluecadet/launchpad-testing/setup.ts', 7 | server: { 8 | deps: { 9 | // inline these deps so that they use the fs mocks 10 | inline: ['glob', 'path-scurry'] 11 | } 12 | } 13 | } 14 | }); 15 | -------------------------------------------------------------------------------- /packages/dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Launchpad Dashboard 2 | 3 | This package is WIP. More to come. -------------------------------------------------------------------------------- /packages/dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-dashboard", 3 | "version": "2.0.0", 4 | "description": "", 5 | "type": "module", 6 | "files": [ 7 | "dist/**/*.js", 8 | "dist/**/*.d.ts" 9 | ], 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "default": "./dist/index.js" 14 | } 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git+https://github.com/bluecadet/launchpad.git" 22 | }, 23 | "author": "Bluecadet", 24 | "license": "ISC", 25 | "bugs": { 26 | "url": "https://github.com/bluecadet/launchpad/issues" 27 | }, 28 | "homepage": "https://github.com/bluecadet/launchpad/packages/dashboard", 29 | "devDependencies": { 30 | "@bluecadet/launchpad-tsconfig": "0.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/dashboard/src/index.ts: -------------------------------------------------------------------------------- 1 | // WIP 2 | 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/dashboard/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "**/*.test.ts", 12 | "**/__tests__/*" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../utils/tsconfig.src.json" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /packages/launchpad/README.md: -------------------------------------------------------------------------------- 1 | # @bluecadet/launchpad 2 | 3 | All-in-one package for building and managing interactive media installations. This package is a convenient way to install all core Launchpad packages at once. 4 | 5 | ## Documentation 6 | 7 | For complete documentation, configuration options, and guides, visit: 8 | [Launchpad Documentation](https://bluecadet.github.io/launchpad/) 9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install @bluecadet/launchpad 14 | ``` 15 | 16 | This will install all core packages: 17 | 18 | - `@bluecadet/launchpad-cli`: Command line interface 19 | - `@bluecadet/launchpad-content`: Content management 20 | - `@bluecadet/launchpad-monitor`: Process monitoring 21 | - `@bluecadet/launchpad-scaffold`: System configuration 22 | 23 | ## Basic Usage 24 | 25 | ```bash 26 | # Download content and start apps 27 | npx launchpad start 28 | 29 | # Only download fresh content 30 | npx launchpad content 31 | 32 | # Only manage apps 33 | npx launchpad monitor 34 | 35 | # Stop all processes 36 | npx launchpad stop 37 | ``` 38 | 39 | ## Note 40 | 41 | This is a meta-package that includes no code of its own. It simply installs all core Launchpad packages. For more targeted installations, you can install individual packages directly. 42 | 43 | ## License 44 | 45 | MIT © Bluecadet 46 | -------------------------------------------------------------------------------- /packages/launchpad/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad", 3 | "version": "2.0.12", 4 | "description": "Suite of tools to manage media installations", 5 | "engines": { 6 | "npm": ">=8.5.1", 7 | "node": ">=17.5.0" 8 | }, 9 | "type": "module", 10 | "scripts": { 11 | "start": "node index.js", 12 | "test": "cd test && node test.js" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/bluecadet/launchpad.git" 17 | }, 18 | "author": { 19 | "name": "Bluecadet", 20 | "url": "https://bluecadet.com" 21 | }, 22 | "files": [ 23 | "dist/**/*.js", 24 | "dist/**/*.d.ts" 25 | ], 26 | "exports": { 27 | ".": { 28 | "types": "./dist/index.d.ts", 29 | "default": "./dist/index.js" 30 | }, 31 | "./package.json": "./package.json" 32 | }, 33 | "maintainers": [ 34 | { 35 | "name": "Benjamin Bojko", 36 | "url": "https://github.com/benjaminbojko" 37 | }, 38 | { 39 | "name": "Pete Inge", 40 | "url": "https://github.com/pingevt" 41 | }, 42 | { 43 | "name": "Clay Tercek", 44 | "url": "https://github.com/claytercek" 45 | } 46 | ], 47 | "license": "ISC", 48 | "bugs": { 49 | "url": "https://github.com/bluecadet/launchpad/issues" 50 | }, 51 | "homepage": "https://github.com/bluecadet/launchpad#readme", 52 | "dependencies": { 53 | "@bluecadet/launchpad-cli": "2.1.1", 54 | "@bluecadet/launchpad-content": "2.1.3", 55 | "@bluecadet/launchpad-dashboard": "2.0.0", 56 | "@bluecadet/launchpad-monitor": "2.0.5", 57 | "@bluecadet/launchpad-scaffold": "2.0.0" 58 | }, 59 | "devDependencies": { 60 | "@bluecadet/launchpad-tsconfig": "0.1.0" 61 | }, 62 | "keywords": [ 63 | "bluecadet", 64 | "cli", 65 | "configuration-management", 66 | "daemon", 67 | "deploy", 68 | "deployment", 69 | "dev ops", 70 | "devops", 71 | "download-manager", 72 | "exhibits", 73 | "forever-monitor", 74 | "forever", 75 | "graceful", 76 | "installations", 77 | "keep process alive", 78 | "log", 79 | "logs", 80 | "monitoring", 81 | "node.js monitoring", 82 | "nodemon", 83 | "pm2", 84 | "process configuration", 85 | "process manager", 86 | "production", 87 | "profiling", 88 | "runtime", 89 | "sysadmin", 90 | "tools", 91 | "windows-desktop" 92 | ] 93 | } 94 | -------------------------------------------------------------------------------- /packages/launchpad/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "@bluecadet/launchpad-cli"; 2 | export * from "@bluecadet/launchpad-content"; 3 | export * from "@bluecadet/launchpad-monitor"; 4 | -------------------------------------------------------------------------------- /packages/launchpad/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "**/*.test.ts", 12 | "**/__tests__/*" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../utils/tsconfig.src.json" 17 | }, 18 | { 19 | "path": "../dashboard" 20 | }, 21 | { 22 | "path": "../monitor/tsconfig.src.json" 23 | }, 24 | { 25 | "path": "../scaffold" 26 | }, 27 | { 28 | "path": "../content/tsconfig.src.json" 29 | }, 30 | { 31 | "path": "../cli" 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /packages/monitor/README.md: -------------------------------------------------------------------------------- 1 | # @bluecadet/launchpad-monitor 2 | 3 | Process monitoring and management for interactive installations. Part of the Launchpad suite of tools. 4 | 5 | ## Documentation 6 | 7 | For complete documentation, examples, and API reference, visit: 8 | 9 | 10 | ## Features 11 | 12 | - Process management via PM2 13 | - Plugin system for custom monitoring behavior 14 | - Process lifecycle hooks 15 | - Built-in logging and error handling 16 | - Window management capabilities 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install @bluecadet/launchpad-monitor 22 | ``` 23 | 24 | ## Basic Usage 25 | 26 | ```typescript 27 | import { Monitor } from '@bluecadet/launchpad-monitor'; 28 | 29 | const monitor = new Monitor({ 30 | apps: [{ 31 | name: 'my-app', 32 | script: 'app.js' 33 | }] 34 | }); 35 | 36 | await monitor.start(); 37 | ``` 38 | 39 | ## License 40 | 41 | MIT © Bluecadet 42 | -------------------------------------------------------------------------------- /packages/monitor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-monitor", 3 | "version": "2.0.5", 4 | "description": "", 5 | "type": "module", 6 | "files": [ 7 | "dist/**/*.js", 8 | "dist/**/*.d.ts" 9 | ], 10 | "exports": { 11 | ".": { 12 | "types": "./dist/index.d.ts", 13 | "default": "./dist/index.js" 14 | }, 15 | "./package.json": "./package.json" 16 | }, 17 | "scripts": { 18 | "test": "vitest" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/bluecadet/launchpad.git" 23 | }, 24 | "author": "Bluecadet", 25 | "license": "ISC", 26 | "bugs": { 27 | "url": "https://github.com/bluecadet/launchpad/issues" 28 | }, 29 | "homepage": "https://github.com/bluecadet/launchpad/packages/monitor", 30 | "dependencies": { 31 | "@bluecadet/launchpad-utils": "~2.0.0", 32 | "auto-bind": "^5.0.1", 33 | "chalk": "^5.0.0", 34 | "cross-spawn": "^7.0.3", 35 | "neverthrow": "^8.1.1", 36 | "node-window-manager": "^2.2.4", 37 | "pm2": "^5.4.3", 38 | "semver": "^7.3.5", 39 | "tail": "^2.2.4", 40 | "zod": "^3.23.8" 41 | }, 42 | "devDependencies": { 43 | "@bluecadet/launchpad-testing": "0.1.0", 44 | "@bluecadet/launchpad-tsconfig": "0.1.0", 45 | "@types/axon": "2.0", 46 | "@types/cross-spawn": "^6.0.2", 47 | "@types/semver": "^7.5.8", 48 | "@types/tail": "2.2", 49 | "axon": "^2.0.3", 50 | "vitest": "^3.0.7" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/monitor/src/core/__tests__/bus-manager.test.ts: -------------------------------------------------------------------------------- 1 | import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; 2 | import pm2 from "pm2"; 3 | import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; 4 | import { BusManager } from "../bus-manager.js"; 5 | import { createMockSubEmitterSocket } from "./core.test-utils.js"; 6 | 7 | function buildTestBusManager() { 8 | const mockLogger = createMockLogger(); 9 | 10 | const { mockSubEmitterSocket, emit } = createMockSubEmitterSocket(); 11 | 12 | const busManager = new BusManager(mockLogger); 13 | 14 | // @ts-ignore 15 | vi.spyOn(pm2, "launchBus").mockImplementation((cb) => cb(null, mockSubEmitterSocket)); 16 | 17 | return { busManager, mockLogger, mockSubEmitterSocket, emit }; 18 | } 19 | 20 | describe("BusManager", () => { 21 | afterEach(() => { 22 | vi.clearAllMocks(); 23 | }); 24 | 25 | describe("connect", () => { 26 | it("should connect to PM2 bus successfully", async () => { 27 | const { busManager, mockSubEmitterSocket, emit } = buildTestBusManager(); 28 | 29 | const result = await busManager.connect(); 30 | 31 | expect(pm2.launchBus).toHaveBeenCalled(); 32 | expect(mockSubEmitterSocket.on).toHaveBeenCalledWith("*", expect.any(Function)); 33 | expect(result.isOk()).toBe(true); 34 | }); 35 | 36 | it("should handle connection errors", async () => { 37 | const { busManager } = buildTestBusManager(); 38 | 39 | const testError = new Error("Bus connection failed"); 40 | vi.spyOn(pm2, "launchBus").mockImplementation((cb) => cb(testError, null)); 41 | 42 | const result = await busManager.connect(); 43 | 44 | expect(result).toBeErr(); 45 | expect(result._unsafeUnwrapErr().message).toContain("Bus connection failed"); 46 | }); 47 | }); 48 | 49 | describe("disconnect", () => { 50 | it("should disconnect from PM2 bus successfully", async () => { 51 | const { busManager, mockSubEmitterSocket, emit } = buildTestBusManager(); 52 | 53 | await busManager.connect(); 54 | const result = await busManager.disconnect(); 55 | 56 | expect(mockSubEmitterSocket.off).toHaveBeenCalledWith("*"); 57 | expect(result.isOk()).toBe(true); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/monitor/src/core/__tests__/core.test-utils.ts: -------------------------------------------------------------------------------- 1 | import type { SubEmitterSocket } from "axon"; 2 | import { vi } from "vitest"; 3 | 4 | export function createMockSubEmitterSocket() { 5 | // biome-ignore lint/suspicious/noExplicitAny: 6 | const listeners: Map void)[]> = new Map(); 7 | 8 | const mockSubEmitterSocket: SubEmitterSocket = { 9 | on: vi.fn((event, listener) => { 10 | listeners.set(event, [...(listeners.get(event) ?? []), listener]); 11 | return mockSubEmitterSocket; 12 | }), 13 | off: vi.fn((event) => { 14 | listeners.set(event, []); 15 | return mockSubEmitterSocket; 16 | }), 17 | onmessage: vi.fn(), 18 | bind: vi.fn(), 19 | connect: vi.fn(), 20 | close: vi.fn(), 21 | }; 22 | 23 | function emit(event: string, data: unknown) { 24 | for (const listener of listeners.get(event) ?? []) { 25 | listener(data); 26 | } 27 | for (const listener of listeners.get("*") ?? []) { 28 | listener(event, data); 29 | } 30 | } 31 | 32 | return { mockSubEmitterSocket, emit }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/monitor/src/index.ts: -------------------------------------------------------------------------------- 1 | import LaunchpadMonitor from "./launchpad-monitor.js"; 2 | 3 | // export * from './windows-api.js'; // Includes optional dependencies, so not exported here 4 | export * from "./launchpad-monitor.js"; 5 | export * from "./monitor-config.js"; 6 | export * from "./core/monitor-plugin-driver.js"; 7 | 8 | export default LaunchpadMonitor; 9 | -------------------------------------------------------------------------------- /packages/monitor/src/utils/debounce-results.ts: -------------------------------------------------------------------------------- 1 | import { ResultAsync } from "neverthrow"; 2 | 3 | /** 4 | * Creates a debounced version of a function that returns ResultAsync. 5 | * 6 | * @param fn The function to debounce which returns ResultAsync 7 | * @param wait The number of milliseconds to delay 8 | * @returns A debounced function that returns ResultAsync with the same types as the input function 9 | */ 10 | 11 | // biome-ignore lint/suspicious/noExplicitAny: any required for generic type handling 12 | export function debounceResultAsync( 13 | fn: (...args: T) => ResultAsync, 14 | wait: number, 15 | ): (...args: T) => ResultAsync { 16 | let timeout: NodeJS.Timeout | null = null; 17 | let pendingPromise: ResultAsync | null = null; 18 | let latestArgs: T | null = null; 19 | 20 | return (...args: T): ResultAsync => { 21 | // Always update the latest args 22 | latestArgs = args; 23 | 24 | // If there's already a pending promise, return it 25 | if (pendingPromise) { 26 | return pendingPromise; 27 | } 28 | 29 | // Create a new ResultAsync that will resolve when the debounced function is called 30 | pendingPromise = ResultAsync.fromPromise( 31 | new Promise((resolve, reject) => { 32 | // Clear any existing timeout 33 | if (timeout) { 34 | clearTimeout(timeout); 35 | } 36 | 37 | // Set a new timeout 38 | timeout = setTimeout(() => { 39 | // Safely capture the latest args, falling back to the original args if null 40 | const currentArgs = latestArgs || args; 41 | 42 | // Reset state 43 | timeout = null; 44 | pendingPromise = null; 45 | latestArgs = null; 46 | 47 | // Call the original function with the captured args 48 | const result = fn(...currentArgs); 49 | 50 | // Ensure result is defined and has a match method 51 | if (result && typeof result.match === "function") { 52 | result.match( 53 | (value) => resolve(value), 54 | (error) => reject(error), 55 | ); 56 | } else { 57 | // Handle the edge case where result is not as expected 58 | reject(new Error("Invalid ResultAsync returned from debounced function")); 59 | } 60 | }, wait); 61 | }), 62 | (error) => error as E, 63 | ); 64 | 65 | return pendingPromise; 66 | }; 67 | } 68 | -------------------------------------------------------------------------------- /packages/monitor/src/utils/sort-windows.ts: -------------------------------------------------------------------------------- 1 | import type { Logger } from "@bluecadet/launchpad-utils"; 2 | import chalk from "chalk"; 3 | import { windowManager } from "node-window-manager"; 4 | import semver from "semver"; 5 | import type { ResolvedAppConfig } from "../monitor-config.js"; 6 | 7 | type SortApp = { 8 | options: ResolvedAppConfig; 9 | pid?: number; 10 | }; 11 | 12 | /** 13 | * The minimum major node version to support window ordering. Node versions < 17 seem to have a fatal 14 | * bug with the native API, which will intermittently cause V8 to crash hard. Defaults to '>=17.4.0'. 15 | */ 16 | export const MIN_NODE_VERSION = ">=17.4.0"; 17 | 18 | const sortWindows = async (apps: SortApp[], logger: Logger): Promise => { 19 | const currNodeVersion = process.version; 20 | if (!semver.satisfies(currNodeVersion, MIN_NODE_VERSION)) { 21 | return Promise.reject( 22 | new Error( 23 | `Can't sort windows because the current node version '${currNodeVersion}' doesn't satisfy the required version '${MIN_NODE_VERSION}'. Please upgrade node to apply window settings like foreground/minimize/hide.`, 24 | ), 25 | ); 26 | } 27 | 28 | logger.debug(`Applying window settings to ${apps.length} ${apps.length === 1 ? "app" : "apps"}`); 29 | 30 | const fgPids = new Set(); 31 | const minPids = new Set(); 32 | const hidePids = new Set(); 33 | 34 | windowManager.requestAccessibility(); 35 | const visibleWindows = windowManager.getWindows().filter((win) => win.isVisible()); 36 | const visiblePids = new Set(visibleWindows.map((win) => win.processId)); 37 | 38 | for (const app of apps) { 39 | if (!app.pid) { 40 | logger.warn( 41 | `Can't sort windows for ${chalk.blue(app.options.pm2.name)} because it has no pid.`, 42 | ); 43 | continue; 44 | } 45 | 46 | if (!visiblePids.has(app.pid)) { 47 | logger.warn( 48 | `No window found for ${chalk.blue(app.options.pm2.name)} with pid ${chalk.blue(app.pid)}.`, 49 | ); 50 | continue; 51 | } 52 | 53 | if (app.options.windows.hide) { 54 | hidePids.add(app.pid); 55 | } 56 | if (app.options.windows.minimize) { 57 | minPids.add(app.pid); 58 | } 59 | if (app.options.windows.foreground) { 60 | fgPids.add(app.pid); 61 | } 62 | } 63 | 64 | for (const win of visibleWindows) { 65 | if (hidePids.has(win.processId)) { 66 | logger.info(`Hiding ${chalk.blue(win.getTitle())} (pid: ${chalk.blue(win.processId)})`); 67 | win.hide(); 68 | } 69 | 70 | if (minPids.has(win.processId)) { 71 | logger.info(`Minimizing ${chalk.blue(win.getTitle())} (pid: ${chalk.blue(win.processId)})`); 72 | win.minimize(); 73 | } 74 | if (fgPids.has(win.processId)) { 75 | logger.info( 76 | `Foregrounding ${chalk.blue(win.getTitle())} (pid: ${chalk.blue(win.processId)})`, 77 | ); 78 | win.bringToTop(); 79 | } 80 | } 81 | 82 | logger.debug("Done applying window settings."); 83 | }; 84 | 85 | export default sortWindows; 86 | -------------------------------------------------------------------------------- /packages/monitor/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.src.json" 5 | }, 6 | { 7 | "path": "./tsconfig.test.json" 8 | } 9 | ], 10 | "files": [] 11 | } -------------------------------------------------------------------------------- /packages/monitor/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "**/*.test.ts", 12 | "**/__tests__/*" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../utils/tsconfig.src.json" 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/monitor/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/test.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "@bluecadet/launchpad-testing/vitest.d.ts" 6 | ] 7 | }, 8 | "include": [ 9 | "src/**/*.test.ts", 10 | "src/**/__tests__", 11 | "vitest.config.ts" 12 | ], 13 | "references": [ 14 | { 15 | "path": "./tsconfig.src.json" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/monitor/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineProject } from 'vitest/config'; 2 | 3 | export default defineProject({ 4 | test: { 5 | environment: 'node', 6 | setupFiles: '@bluecadet/launchpad-testing/setup.ts' 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /packages/scaffold/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | config/user.ps1 -------------------------------------------------------------------------------- /packages/scaffold/README.md: -------------------------------------------------------------------------------- 1 | # @bluecadet/launchpad-scaffold 2 | 3 | Windows system configuration tools for interactive media installations. This package helps automate the setup of Windows machines for kiosk and exhibit environments. 4 | 5 | ## Documentation 6 | 7 | For complete documentation and guides, visit: 8 | [Launchpad Documentation](https://bluecadet.github.io/launchpad/) 9 | 10 | ## Features 11 | 12 | - Automated Windows kiosk setup 13 | - System optimization and lockdown 14 | - Power settings management 15 | - Startup task configuration 16 | - User interface customization 17 | 18 | ## Installation 19 | 20 | ```bash 21 | npm install @bluecadet/launchpad-scaffold @bluecadet/launchpad-cli 22 | ``` 23 | 24 | ## Basic Usage 25 | 26 | ```bash 27 | npx launchpad scaffold 28 | ``` 29 | 30 | ## License 31 | 32 | MIT © Bluecadet 33 | -------------------------------------------------------------------------------- /packages/scaffold/config/.gitignore: -------------------------------------------------------------------------------- 1 | user.ps1 -------------------------------------------------------------------------------- /packages/scaffold/config/presets/exhibit_power_config.pow: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluecadet/launchpad/4aee3ca04c6d4dc266227772f6d9e49e5d6391c3/packages/scaffold/config/presets/exhibit_power_config.pow -------------------------------------------------------------------------------- /packages/scaffold/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-scaffold", 3 | "version": "2.0.0", 4 | "description": "Suite of PS1 scripts to configure Windows 8/10 PCs for permanent exhibits.", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./dist/index.js" 10 | }, 11 | "./package.json": "./package.json" 12 | }, 13 | "files": [ 14 | "dist/**/*.js", 15 | "dist/**/*.d.ts", 16 | "config", 17 | "scripts", 18 | "setup.bat", 19 | "setup.ps1" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/bluecadet/launchpad.git" 24 | }, 25 | "author": "Bluecadet", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/bluecadet/launchpad/issues" 29 | }, 30 | "dependencies": { 31 | "@bluecadet/launchpad-utils": "~2.0.0", 32 | "sudo-prompt": "^9.2.1" 33 | }, 34 | "devDependencies": { 35 | "@bluecadet/launchpad-tsconfig": "0.1.0" 36 | }, 37 | "homepage": "https://github.com/bluecadet/launchpad/packages/scaffold" 38 | } 39 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/install_choco.ps1: -------------------------------------------------------------------------------- 1 | Set-ExecutionPolicy Bypass -Scope Process -Force; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1')) 2 | 3 | # See https://stackoverflow.com/a/46760714/782899 4 | # Make `refreshenv` available right away, by defining the $env:ChocolateyInstall 5 | # variable and importing the Chocolatey profile module. 6 | # Note: Using `. $PROFILE` instead *may* work, but isn't guaranteed to. 7 | $env:ChocolateyInstall = Convert-Path "$((Get-Command choco).Path)\..\.." 8 | Import-Module "$env:ChocolateyInstall\helpers\chocolateyProfile.psm1" 9 | 10 | # refreshenv is now an alias for Update-SessionEnvironment 11 | # (rather than invoking refreshenv.cmd, the *batch file* for use with cmd.exe) 12 | # This should make git.exe accessible via the refreshed $env:PATH, so that it 13 | # can be called by name only. 14 | refreshenv -------------------------------------------------------------------------------- /packages/scaffold/scripts/install_cygwin.ps1: -------------------------------------------------------------------------------- 1 | # Download and install cygwin 2 | 3 | # See https://blog.jourdant.me/post/3-ways-to-download-files-with-powershell 4 | Import-Module BitsTransfer 5 | Start-BitsTransfer -Source http://cygwin.com/setup-x86_64.exe -Destination cygwin_setup.exe 6 | 7 | # .\cygwin_setup.exe -nq -P librsync2,rsync,openssh,curl,wget,unzip | Out-Null # Out-Null causes PS to wait for exe to exit 8 | 9 | Start-Process -FilePath ".\cygwin_setup.exe" -ArgumentList "--no-admin -nq -P librsync2,rsync,openssh,curl,wget,unzip" -Wait 10 | 11 | Remove-Item cygwin_setup.exe 12 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/install_node_dependencies.ps1: -------------------------------------------------------------------------------- 1 | # Install runtime dependencies 2 | 3 | # Choco's refreshenv command can't seem to load NPM into the path on the fly 4 | # so we need to temporarily add NodeJS to the path here. 5 | # $env:Path += ";C:\ProgramData\nvm\;" 6 | 7 | choco uninstall nodejs 8 | choco install nvm -y 9 | nvm install latest 10 | nvm use latest 11 | 12 | $env:Path += ";$env:ProgramFiles\nodejs\;" 13 | 14 | # new-item -path alias:npm -value "C:\Program Files\nodejs\npm" 15 | 16 | # cmd /c npm install -g npm@8.5.1 17 | npm i -g npm@latest 18 | 19 | Push-Location '../../'; 20 | npm install 21 | Pop-Location; 22 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/load_config.ps1: -------------------------------------------------------------------------------- 1 | Param ( 2 | [Parameter(HelpMessage='Path to your config file. Defaults to .\config\user.ps1')] [string]$config="" 3 | ) 4 | 5 | if ([string]::IsNullOrEmpty($config)) { 6 | $config = ".\config\user.ps1" 7 | } 8 | 9 | $DefaultConfigPath = '.\config\defaults.ps1'; 10 | $UserConfigPath = $config; 11 | 12 | Write-Host "Loading default config from $DefaultConfigPath" -ForegroundColor Cyan; 13 | & $DefaultConfigPath; 14 | 15 | if (!(FileExists $UserConfigPath)) { 16 | Write-Host "No user config found. Creating one for you."; 17 | 18 | Write-Host "Edit " -ForegroundColor Yellow -NoNewline; 19 | Write-Host $UserConfigPath -ForegroundColor White -NoNewline; 20 | Write-Host " to customize your settings." -ForegroundColor Yellow; 21 | 22 | Copy-Item $DefaultConfigPath $UserConfigPath; 23 | 24 | Write-Host "Close Notepad to continue..." -ForegroundColor Yellow; 25 | Start-Process Notepad.exe -ArgumentList $UserConfigPath -NoNewWindow -Wait; 26 | } 27 | 28 | if (FileExists $UserConfigPath) { 29 | Write-Host "Loading user config from $UserConfigPath" -ForegroundColor Cyan; 30 | & $UserConfigPath; 31 | } 32 | 33 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/vendor/force-mkdir.psm1: -------------------------------------------------------------------------------- 1 | # Thanks to raydric, this function should be used instead of `mkdir -force`. 2 | # 3 | # While `mkdir -force` works fine when dealing with regular folders, it behaves 4 | # strange when using it at registry level. If the target registry key is 5 | # already present, all values within that key are purged. 6 | function force-mkdir($path) { 7 | if (!(Test-Path -Path $path)) { 8 | # Write-Host "-- Creating full path to: " $path -ForegroundColor White -BackgroundColor DarkGreen 9 | New-Item -ItemType Directory -Force -Path $path | Out-Null; 10 | } 11 | } -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/clear_desktop_background.ps1: -------------------------------------------------------------------------------- 1 | Set-ItemProperty 'HKCU:\Control Panel\Colors' -Name Background -Value "0 0 0" -Force 2 | Set-ItemProperty 'HKCU:\Control Panel\Desktop' -Name Wallpaper -value "" -Force 3 | 4 | # rundll32.exe user32.dll, UpdatePerUserSystemParameters 5 | 6 | Write-Host "Cleared desktop background. Please restart or sign out/in for changes to apply." -ForegroundColor Yellow 7 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/clear_desktop_shortcuts.ps1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluecadet/launchpad/4aee3ca04c6d4dc266227772f6d9e49e5d6391c3/packages/scaffold/scripts/windows/clear_desktop_shortcuts.ps1 -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_accessibility.ps1: -------------------------------------------------------------------------------- 1 | # See https://github.com/W4RH4WK/Debloat-Windows-10/blob/master/scripts/optimize-user-interface.ps1 2 | Write-Output "Disabling keyboard accessibility keys" 3 | Set-ItemProperty "HKCU:\Control Panel\Accessibility\StickyKeys" "Flags" "506" 4 | Set-ItemProperty "HKCU:\Control Panel\Accessibility\Keyboard Response" "Flags" "122" 5 | Set-ItemProperty "HKCU:\Control Panel\Accessibility\ToggleKeys" "Flags" "58" -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_app_installs.ps1: -------------------------------------------------------------------------------- 1 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 2 | 3 | # Prevents Apps from re-installing 4 | force-mkdir "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" 5 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "FeatureManagementEnabled" 0 6 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "OemPreInstalledAppsEnabled" 0 7 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "PreInstalledAppsEnabled" 0 8 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SilentInstalledAppsEnabled" 0 9 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "ContentDeliveryAllowed" 0 10 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "PreInstalledAppsEverEnabled" 0 11 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SubscribedContentEnabled" 0 12 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SubscribedContent-338388Enabled" 0 13 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SubscribedContent-338389Enabled" 0 14 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SubscribedContent-314559Enabled" 0 15 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SubscribedContent-338387Enabled" 0 16 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SubscribedContent-338393Enabled" 0 17 | Set-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SubscribedContent-310093Enabled" 0 18 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SystemPaneSuggestionsEnabled" 0 19 | Set-ItemProperty "HKCU:\Software\Microsoft\Windows\CurrentVersion\ContentDeliveryManager" "SoftLandingEnabled" 0 20 | 21 | force-mkdir "HKLM:\SOFTWARE\Policies\Microsoft\WindowsStore" 22 | Set-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\WindowsStore" "AutoDownload" 2 23 | 24 | # Prevents "Suggested Applications" returning 25 | force-mkdir "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" 26 | Set-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\CloudContent" "DisableWindowsConsumerFeatures" 1 27 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_app_restore.ps1: -------------------------------------------------------------------------------- 1 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 2 | 3 | # Prevents Apps from re-installing 4 | force-mkdir "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" 5 | Set-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" "DisableAutomaticRestartSignOn" 1 6 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_cortana_search.ps1: -------------------------------------------------------------------------------- 1 | # Disable cortana in search 2 | $path = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search" 3 | IF(!(Test-Path -Path $path)) { 4 | New-Item -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows" -Name "Windows Search" 5 | } 6 | Set-ItemProperty -Path $path -Name "AllowCortana" -Value 0 7 | 8 | # Hide search in task bar 9 | Set-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Search" "SearchboxTaskbarMode" 0 10 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_edge_swipes.ps1: -------------------------------------------------------------------------------- 1 | # See https://www.tenforums.com/tutorials/48507-enable-disable-edge-swipe-screen-windows-10-a.html 2 | Write-Output "Disabling edge swipes" 3 | 4 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 5 | force-mkdir "HKLM:\SOFTWARE\Policies\Microsoft\Windows\EdgeUI" 6 | Set-ItemProperty "HKLM:\SOFTWARE\Policies\Microsoft\Windows\EdgeUI" "AllowEdgeSwipe" 0 -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_error_reporting.ps1: -------------------------------------------------------------------------------- 1 | # See https://pureinfotech.com/disable-taskbar-news-widget-windows-10/ 2 | 3 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 4 | 5 | force-mkdir "HKLM:\Software\Microsoft\Windows\Windows Error Reporting" 6 | 7 | Set-ItemProperty -Path "HKLM:\Software\Microsoft\Windows\Windows Error Reporting" -Name "Disabled " -Type DWord -Value 1 8 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_firewall.ps1: -------------------------------------------------------------------------------- 1 | # See https://github.com/morphogencc/ofxWindowsSetup/blob/master/scripts/disable_firewall.ps1 2 | 3 | # disable_firewall.ps1 4 | # ------------------------- 5 | # Disables the Windows Firewall. 6 | 7 | Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled False -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_max_path_length.ps1: -------------------------------------------------------------------------------- 1 | # See https://www.howtogeek.com/266621/how-to-make-windows-10-accept-file-paths-over-260-characters/ 2 | 3 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 4 | 5 | force-mkdir "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" 6 | 7 | Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\FileSystem" -Name "LongPathsEnabled" -Type DWord -Value 1 -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_new_network_window.ps1: -------------------------------------------------------------------------------- 1 | Write-Output "Disabling new network window" 2 | 3 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 4 | force-mkdir "HKLM:\SYSTEM\currentControlSet\Control\Network\NewNetworkWindowOff" 5 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_news_and_interests.ps1: -------------------------------------------------------------------------------- 1 | # See https://pureinfotech.com/disable-taskbar-news-widget-windows-10/ 2 | 3 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 4 | 5 | force-mkdir "HKCU:\Software\Microsoft\Windows\CurrentVersion\Feeds" 6 | 7 | Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Feeds" -Name "IsFeedsAvailable" -Type DWord -Value 0 -Force 8 | Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Feeds" -Name "ShellFeedsTaskbarViewMode" -Type DWord -Value 2 -Force 9 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_notifications.ps1: -------------------------------------------------------------------------------- 1 | # See https://community.spiceworks.com/topic/2079430-any-powershell-script-to-disable-all-windows-10-notifications?page=1#entry-7336255 2 | 3 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 4 | 5 | force-mkdir "HKCU:\Software\Policies\Microsoft\Windows\Explorer" 6 | 7 | Set-ItemProperty -Path "HKCU:\Software\Policies\Microsoft\Windows\Explorer" -Name "DisableNotificationCenter" -Type DWord -Value 1 8 | Set-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\PushNotifications" -Name "ToastEnabled" -Type DWord -Value 0 -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_screensaver.ps1: -------------------------------------------------------------------------------- 1 | # See http://www.teqlog.com/disable-screensaver-group-policy.html 2 | 3 | Write-Output "Disabling screen saver" 4 | 5 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 6 | 7 | $folder = "Software\Policies\Microsoft\Windows\Control Panel\Desktop"; 8 | 9 | force-mkdir "HKLM:\$folder" 10 | force-mkdir "HKCU:\$folder" 11 | 12 | Set-ItemProperty -Path "HKLM:\$folder" -Name "ScreenSaveTimeOut" -Value 0 13 | Set-ItemProperty -Path "HKLM:\$folder" -Name "ScreenSaveActive" -Value 0 14 | Set-ItemProperty -Path "HKLM:\$folder" -Name "ScreenSaverIsSecure" -Value 0 15 | Set-ItemProperty -Path "HKLM:\$folder" -Name "SCRNSAVE.EXE" -Value "" 16 | 17 | set-ItemProperty -path "HKCU:\$folder" -name "ScreenSaveTimeOut" -value 0 18 | set-ItemProperty -path "HKCU:\$folder" -name "ScreenSaveActive" -value 0 19 | set-ItemProperty -path "HKCU:\$folder" -name "ScreenSaverIsSecure" -value 0 20 | set-ItemProperty -path "HKCU:\$folder" -name "SCRNSAVE.EXE" -value "" 21 | 22 | set-ItemProperty -path "HKCU:\Control Panel\Desktop" -name "ScreenSaveTimeOut" -value 0 23 | set-ItemProperty -path "HKCU:\Control Panel\Desktop" -name "ScreenSaveActive" -value 0 24 | set-ItemProperty -path "HKCU:\Control Panel\Desktop" -name "ScreenSaverIsSecure" -value 0 25 | set-ItemProperty -path "HKCU:\Control Panel\Desktop" -name "SCRNSAVE.EXE" -value "" -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_touch_feedback.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 3 | 4 | force-mkdir "HKCU:\Control Panel\Cursors" 5 | 6 | # See https://www.tenforums.com/tutorials/98415-turn-off-touch-visual-feedback-windows-10-a.html 7 | Set-ItemProperty -Path "HKCU:\Control Panel\Cursors" -Name "ContactVisualization" -Type DWord -Value 0 8 | Set-ItemProperty -Path "HKCU:\Control Panel\Cursors" -Name "GestureVisualization" -Type DWord -Value 0 9 | 10 | # See https://www.top-password.com/blog/turn-on-off-press-and-hold-for-right-clicking-in-windows-10/ 11 | Set-ItemProperty -Path "HKCU:\Software\Microsoft\Wisp\Touch" -Name "TouchMode_hold" -Type DWord -Value 0 12 | Set-ItemProperty -Path "HKCU:\Software\Microsoft\Wisp\Pen\SysEventParameters" -Name "HoldMode" -Type DWord -Value 3 13 | 14 | # See https://getadmx.com/?Category=Windows_10_2016&Policy=Microsoft.Policies.TabletPCInputPanel::EdgeTarget_2 15 | force-mkdir "HKLM:\software\policies\microsoft\TabletTip\1.7" 16 | Set-ItemProperty -Path "HKLM:\software\policies\microsoft\TabletTip\1.7" -Name "DisableEdgeTarget" -Type DWord -Value 0 17 | 18 | Write-Host "Disabled pen and touch feedback. Please restart or sign out/in for changes to apply." -ForegroundColor Yellow 19 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_touch_gestures.ps1: -------------------------------------------------------------------------------- 1 | 2 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 3 | 4 | force-mkdir "HKCU:\Control Panel\Desktop" 5 | 6 | # See https://answers.microsoft.com/en-us/windows/forum/all/windows-1011-touch-gesture/af9a6d19-8aa6-4d26-9693-55aa591110b3 7 | Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "TouchGestureSetting" -Type DWord -Value 0 8 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_update_check.ps1: -------------------------------------------------------------------------------- 1 | if (!(Test-Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate")) {New-Item -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate" -Type Folder | Out-Null} 2 | if (!(Test-Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU")) {New-Item -Path "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" -Type Folder | Out-Null} 3 | 4 | # Enable Automatic Updates 5 | Set-ItemProperty "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" "NoAutoUpdate" 1 6 | 7 | # Configure to Auto-Download but not Install: NotConfigured: 0, Disabled: 1, NotifyBeforeDownload: 2, NotifyBeforeInstall: 3, ScheduledInstall: 4 8 | Set-ItemProperty "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" "AUOptions" 1 9 | 10 | # Disable automatic reboot after install 11 | Set-ItemProperty "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate" "NoAutoRebootWithLoggedOnUsers" 1 12 | Set-ItemProperty "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" "NoAutoRebootWithLoggedOnUsers" 1 -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_update_service.ps1: -------------------------------------------------------------------------------- 1 | try { 2 | Get-Variable $PSScriptRoot -Scope Global -ErrorAction 'Stop' 3 | Import-Module -DisableNameChecking $PSScriptRoot/scripts/functions.psm1; 4 | } catch [System.Management.Automation.ItemNotFoundException] { 5 | Import-Module -DisableNameChecking $PSScriptRoot/../functions.psm1; 6 | } 7 | 8 | Write-Output "Disabling Windows Update Medic Service" 9 | # Windows Update Medic Service can only be disabled via the registry 10 | Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -Name 'Start' -value 4 11 | Write-Host "Successfully disabled the 'WaaSMedicSVC' service on $ComputerName." -ForegroundColor Green; 12 | $WaaSMedicSVC = get-ciminstance win32_service -Filter "Name='WaaSMedicSVC'" -Ea 0; 13 | if($WaaSMedicSVC.State -eq "Running"){ 14 | $ComputerName = "$(hostname)" 15 | $result = (Invoke-CimMethod -InputObject $WaaSMedicSVC -methodname StopService).ReturnValue 16 | if ($result) { 17 | Write-Host "Failed to stop the 'WaaSMedicSVC' service on $ComputerName. The return value was $result." -ForegroundColor Red; 18 | } else { 19 | Write-Host "Successfully stopped the 'WaaSMedicSVC' service on $ComputerName." -ForegroundColor Green; 20 | } 21 | } 22 | 23 | Write-Output "Disabling Update Orchestrator Service" 24 | Disable-Service("UsoSvc"); 25 | 26 | Write-Output "Disabling Windows Update Service" 27 | Disable-Service("wuauserv"); 28 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/disable_win_setup_prompts.ps1: -------------------------------------------------------------------------------- 1 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 2 | 3 | # Prevents "Let's finish setting up this device" prompt in Windows 11 4 | force-mkdir "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\UserProfileEngagement" 5 | Set-ItemProperty "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\UserProfileEngagement" "ScoobeSystemSettingEnabled" 0 -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/enable_auto_login.ps1: -------------------------------------------------------------------------------- 1 | # See https://github.com/morphogencc/ofxWindowsSetup/blob/master/scripts/enable_autologin.ps1 2 | 3 | # enable_autologin.ps1 4 | # ------------------------- 5 | # Enables automatic login without a username and password; either place the username and password in the configuration file or run this 6 | # in the powershell and enter them manually. 7 | 8 | param ( 9 | [string]$computername = "", 10 | [string]$username = "", 11 | [string]$password = "" 12 | ) 13 | 14 | if (!$LaunchpadConfig) { 15 | & $PSScriptRoot\..\load_config.ps1 16 | } 17 | 18 | if ($computername -eq "") { 19 | $computername = $global:LaunchpadConfig.Computer.ComputerName; 20 | } 21 | 22 | if($username -eq "") { 23 | $username = $global:LaunchpadConfig.Computer.WindowsUsername; 24 | } 25 | 26 | if ($password -eq "") { 27 | # TODO: Use $c = Get-Credential -credential $username; $c.password # This will prompt the user for the current password 28 | $password = $global:LaunchpadConfig.Computer.WindowsPassword; 29 | } 30 | 31 | Write-Host "Enabling auto login for user '$username'" -ForegroundColor Magenta 32 | 33 | # Autologin 34 | Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name AutoAdminLogon -Value 1 35 | Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name DefaultDomainName -Value $computername 36 | Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name DefaultUserName -Value $username 37 | Set-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon' -Name DefaultPassword -Value $password -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/enable_daily_reboot.ps1: -------------------------------------------------------------------------------- 1 | # See https://github.com/morphogencc/ofxWindowsSetup/blob/master/scripts/set_scheduled_reboot.ps1 2 | 3 | param ( 4 | [string]$rebootTime = "" 5 | ) 6 | 7 | $taskName = "Daily System Reboot" 8 | $taskPath = "\Exhibit" 9 | 10 | if ($Global:LaunchpadConfig) { 11 | $taskPath = $Global:LaunchpadConfig.Computer.TaskSchedulerPath; 12 | } 13 | 14 | if($rebootTime -eq "") { 15 | if (!$Global:LaunchpadConfig) { 16 | & $PSScriptRoot\..\load_config.ps1 17 | } 18 | $rebootTime = $Global:LaunchpadConfig.Computer.RebootTime 19 | } 20 | 21 | $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue 22 | 23 | if (!$task) { 24 | $action = New-ScheduledTaskAction -Execute "C:\WINDOWS\System32\shutdown.exe" -Argument "-r -f" 25 | $trigger = New-ScheduledTaskTrigger -Daily -AT $rebootTime 26 | $settings = New-ScheduledTaskSettingsSet 27 | $inputObject = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings 28 | Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -InputObject $inputObject 29 | New-ScheduledTaskAction -Execute "PowerShell.exe" 30 | } -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/enable_firewall.ps1: -------------------------------------------------------------------------------- 1 | # See https://github.com/morphogencc/ofxWindowsSetup/blob/master/scripts/disable_firewall.ps1 2 | 3 | # disable_firewall.ps1 4 | # ------------------------- 5 | # Disables the Windows Firewall. 6 | 7 | Set-NetFirewallProfile -Profile Domain,Public,Private -Enabled True -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/enable_script_execution.ps1: -------------------------------------------------------------------------------- 1 | # See https://www.howtogeek.com/266621/how-to-make-windows-10-accept-file-paths-over-260-characters/ 2 | 3 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 4 | 5 | force-mkdir "HKLM:\SOFTWARE\Policies\Microsoft\Windows" 6 | 7 | Set-ItemProperty -Path "HKLM:\SOFTWARE\Policies\Microsoft\Windows" -Name "EnableScripts" -Type DWord -Value 1 8 | Set-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\PowerShell\1\ShellIds\Microsoft.PowerShell" -Name "ExecutionPolicy" -Type String -Value "RemoteSigned" 9 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/enable_startup_task.ps1: -------------------------------------------------------------------------------- 1 | # See https://stackoverflow.com/questions/41235618/powershell-command-to-create-a-schedule-task-to-execute-a-batch-file-during-boot 2 | 3 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/force-mkdir.psm1 4 | 5 | if (!$Global:LaunchpadConfig) { 6 | & $PSScriptRoot\..\load_config.ps1 7 | } 8 | 9 | $taskName = "Launch exhibit app on boot" 10 | $taskPath = $Global:LaunchpadConfig.Computer.TaskSchedulerPath; 11 | $taskAction = $Global:LaunchpadConfig.Computer.StartupAction; 12 | $taskDir = (Resolve-Path $Global:LaunchpadConfig.Computer.StartupWorkingDir).Path; 13 | $taskDelay = $Global:LaunchpadConfig.Computer.StartupDelay; 14 | 15 | if ($global:LaunchpadConfig.Computer.StartupCreateBat) { 16 | $filename = $global:LaunchpadConfig.Computer.StartupBat; 17 | $batPath = "${taskDir}${filename}"; 18 | if (!(Test-Path $batPath)) { 19 | force-mkdir $taskDir; 20 | Write-Host "Creating a startup .bat at ${batPath}"; 21 | New-Item $batPath -Force | Out-Null; 22 | Set-Content $batPath $global:LaunchpadConfig.Computer.StartupBatContent; 23 | } else { 24 | Write-Host "Startup .bat already exists at ${batPath}"; 25 | } 26 | } 27 | 28 | $task = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue 29 | 30 | if (!$task) { 31 | $action = New-ScheduledTaskAction -Execute $taskAction -WorkingDirectory $taskDir 32 | $trigger = New-ScheduledTaskTrigger -AtStartup 33 | $trigger.Delay = $taskDelay 34 | $settings = New-ScheduledTaskSettingsSet 35 | $inputObject = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings 36 | Register-ScheduledTask -TaskName $taskName -TaskPath $taskPath -InputObject $inputObject 37 | } -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/enable_update_service.ps1: -------------------------------------------------------------------------------- 1 | try { 2 | Get-Variable $PSScriptRoot -Scope Global -ErrorAction 'Stop' 3 | Import-Module -DisableNameChecking $PSScriptRoot/functions.psm1; 4 | } catch [System.Management.Automation.ItemNotFoundException] { 5 | Import-Module -DisableNameChecking ../functions.psm1; 6 | } 7 | 8 | Write-Output "Enabling Windows Update Service" 9 | Enable-Service("wuauserv"); 10 | 11 | Write-Output "Enabling Update Orchestrator Service" 12 | Enable-Service("UsoSvc"); 13 | 14 | Write-Output "Enabling Windows Update Medic Service" 15 | Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\WaaSMedicSvc' -Name 'Start' -value 2 16 | Enable-Service("WaaSMedicSVC"); 17 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/reset_text_scale.ps1: -------------------------------------------------------------------------------- 1 | $scaling = -1 2 | $dpi = -1 3 | 4 | if ((Get-ItemProperty "HKCU:\Control Panel\Desktop" -Name "Win8DpiScaling" -EA 0).Win8DpiScaling -ne $null) { 5 | $scaling = Get-ItemProperty -Path "HKCU:\Control Panel\Desktop"| Select-Object -ExpandProperty Win8DpiScaling 6 | } 7 | if ((Get-ItemProperty "HKCU:\Control Panel\Desktop" -Name "LogPixels" -EA 0).LogPixels -ne $null) { 8 | $dpi = Get-ItemProperty -Path "HKCU:\Control Panel\Desktop"| Select-Object -ExpandProperty LogPixels 9 | } 10 | 11 | if (($dpi -NE 96) -OR ($scaling -NE 1)) { 12 | Write-Host "Setting Windows display text scaling to 100%" 13 | Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "Win8DpiScaling" -Value 1 14 | Set-ItemProperty -Path "HKCU:\Control Panel\Desktop" -Name "LogPixels" -Value 96 15 | Write-Host "You will have to log out and back in for text scaling changes to take effect." -ForegroundColor Yellow 16 | } 17 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/set_computer_name.ps1: -------------------------------------------------------------------------------- 1 | # See https://github.com/morphogencc/ofxWindowsSetup/blob/master/scripts/set_computer_name.ps1 2 | 3 | param ( 4 | [string]$computername = "" 5 | ) 6 | 7 | if($computername -eq "") { 8 | if (!$LaunchpadConfig) { 9 | & $PSScriptRoot\..\load_config.ps1 10 | } 11 | $computername = $global:LaunchpadConfig.Computer.ComputerName 12 | } 13 | 14 | $currentHostname = hostname; 15 | 16 | if ($currentHostname -eq $computername) { 17 | Write-Host "Computer name already set to '$computername'" -ForegroundColor Green; 18 | } else { 19 | Write-Host "Setting computer name to '$computername'" -ForegroundColor Magenta 20 | Rename-Computer $computername 21 | } 22 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/set_power_settings.ps1: -------------------------------------------------------------------------------- 1 | # Power Settings 2 | 3 | param ( 4 | [string]$planPath = $null 5 | ) 6 | if (($planPath -eq $null) -OR ($planPath -eq "") -OR !(Test-Path $planPath)) { 7 | $planPath = $Global:LaunchpadConfig.Computer.PowerConfig 8 | } 9 | 10 | Import-Module -DisableNameChecking $PSScriptRoot/../vendor/powerplan.psm1 11 | 12 | $planName = 'Exhibit' 13 | 14 | Write-Host "Importing power plan '$planName' from $planPath" 15 | powercfg /IMPORT "$planPath" 16 | 17 | Write-Host "Selecting power plan '$planName'" 18 | 19 | # Using https://github.com/torgro/PowerPlan 20 | Set-Powerplan -Planname "$planName" 21 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/set_timezone.ps1: -------------------------------------------------------------------------------- 1 | # See https://github.com/morphogencc/ofxWindowsSetup/blob/master/scripts/set_timezone.ps1 2 | 3 | param ( 4 | [string]$timezone = "" 5 | ) 6 | 7 | if($timezone -eq "") { 8 | if (!$LaunchpadConfig) { 9 | & $PSScriptRoot\..\load_config.ps1 10 | } 11 | $timezone = $global:LaunchpadConfig.Computer.Timezone 12 | } 13 | 14 | & "$env:windir\system32\tzutil.exe" /s $timezone 15 | 16 | $taskName1 = "Resync Time 1 of 2" 17 | $taskName2 = "Resync Time 2 of 2" 18 | $taskPath = "\Exhibit" 19 | 20 | if ($Global:LaunchpadConfig) { 21 | $taskPath = $Global:LaunchpadConfig.Computer.TaskSchedulerPath; 22 | } 23 | 24 | $task1 = Get-ScheduledTask -TaskName $taskName1 -ErrorAction SilentlyContinue 25 | $task2 = Get-ScheduledTask -TaskName $taskName2 -ErrorAction SilentlyContinue 26 | 27 | if (!$task1) { 28 | $action = New-ScheduledTaskAction -Execute "C:\Windows\System32\sc.exe" -Argument "start w32time task_started" 29 | $trigger = New-ScheduledTaskTrigger -AtLogOn 30 | $settings = New-ScheduledTaskSettingsSet 31 | $principal = New-ScheduledTaskPrincipal -UserId "$($env:USERDOMAIN)\$($env:USERNAME)" -LogonType ServiceAccount -RunLevel Highest 32 | Register-ScheduledTask -TaskName $taskName1 -TaskPath $taskPath -Description "Synchronize time at startup." -Action $action -Trigger $trigger -Settings $settings -Principal $principal 33 | } 34 | 35 | 36 | if (!$task2) { 37 | $action = New-ScheduledTaskAction -Execute "C:\Windows\System32\w32tm.exe" -Argument "/resync" 38 | $trigger = New-ScheduledTaskTrigger -AtLogOn 39 | $settings = New-ScheduledTaskSettingsSet 40 | $principal = New-ScheduledTaskPrincipal -UserId "$($env:USERDOMAIN)\$($env:USERNAME)" -LogonType ServiceAccount -RunLevel Highest 41 | Register-ScheduledTask -TaskName $taskName2 -TaskPath $taskPath -Description "Synchronize time at startup." -Action $action -Trigger $trigger -Settings $settings -Principal $principal 42 | } -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/uninstall_one_drive.ps1: -------------------------------------------------------------------------------- 1 | cmd.exe /c "taskkill /f /im OneDrive.exe" 2 | cmd.exe /c "%SystemRoot%\System32\OneDriveSetup.exe /uninstall" 3 | cmd.exe /c "%SystemRoot%\SysWOW64\OneDriveSetup.exe /uninstall" -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/unpin_start_menu_apps.ps1: -------------------------------------------------------------------------------- 1 | if ((Get-ComputerInfo | Select-Object -expand OsName) -match 11) { 2 | # Windows 11 3 | Write-Host "Unpinning start menu apps is not supported on Windows 11" 4 | 5 | } else { 6 | # Windows 10 7 | 8 | # See https://appuals.com/pin-unpin-application-windows-10/ 9 | function Pin-App { param( 10 | [string]$appname, 11 | [switch]$unpin 12 | ) 13 | try{ 14 | if ($unpin.IsPresent){ 15 | ((New-Object -Com Shell.Application).NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items() | ?{$_.Name -like $appname}).Verbs() | ?{$_.Name.replace('&','') -match 'From "Start" UnPin|Unpin from Start'} | %{$_.DoIt()} 16 | return "App '$appname' unpinned from Start" 17 | } else { 18 | ((New-Object -Com Shell.Application).NameSpace('shell:::{4234d49b-0245-4df3-b780-3893943456e1}').Items() | ?{$_.Name -like $appname}).Verbs() | ?{$_.Name.replace('&','') -match 'To "Start" Pin|Pin to Start'} | %{$_.DoIt()} 19 | return "App '$appname' pinned to Start" 20 | } 21 | }catch{ 22 | Write-Error "Error Pinning/Unpinning App! (App-Name correct?)" 23 | } 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /packages/scaffold/scripts/windows/win8_config_startpage.ps1: -------------------------------------------------------------------------------- 1 | # Configure Windows 8 Start Page 2 | Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartPage' -Name OpenAtLogon -Value 0 3 | Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartPage' -Name MonitorOverride -Value 1 4 | Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartPage' -Name MakeAllAppsDefault -Value 1 5 | Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartPage' -Name DesktopFirst -Value 1 6 | Set-ItemProperty -Path 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartPage' -Name GlobalSearchInApps -Value 1 -------------------------------------------------------------------------------- /packages/scaffold/setup.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | cd /D "%~dp0" 4 | 5 | set configPath=%~1 6 | 7 | call PowerShell.exe -ExecutionPolicy ByPass -Command "./setup.ps1 '%configPath%'" 8 | -------------------------------------------------------------------------------- /packages/scaffold/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "node:path"; 2 | import { LogManager, type Logger } from "@bluecadet/launchpad-utils"; 3 | import * as sudo from "sudo-prompt"; 4 | 5 | export function launchScaffold(parentLogger: Logger) { 6 | const logger = LogManager.getLogger("scaffold", parentLogger); 7 | 8 | if (process.platform !== "win32") { 9 | logger.error("Launchpad Scaffold currently only supports Windows"); 10 | logger.error("Exiting..."); 11 | process.exit(1); 12 | } 13 | 14 | logger.info("Starting Launchpad Scaffold script..."); 15 | 16 | return sudo.exec( 17 | `start ${path.resolve(import.meta.dirname, "../setup.bat")}`, 18 | { 19 | name: "Launchpad Scaffold", 20 | }, 21 | (error, stdout, stderr) => { 22 | if (error) throw error; 23 | console.log(stdout); 24 | }, 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /packages/scaffold/start2.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluecadet/launchpad/4aee3ca04c6d4dc266227772f6d9e49e5d6391c3/packages/scaffold/start2.bin -------------------------------------------------------------------------------- /packages/scaffold/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "**/*.test.ts", 12 | "**/__tests__/*" 13 | ], 14 | "references": [ 15 | { 16 | "path": "../utils/tsconfig.src.json" 17 | } 18 | ] 19 | } -------------------------------------------------------------------------------- /packages/testing/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-testing", 3 | "private": true, 4 | "version": "0.1.0", 5 | "type": "module", 6 | "devDependencies": { 7 | "@bluecadet/launchpad-tsconfig": "0.1.0", 8 | "ky": "^1.7.2", 9 | "memfs": "^4.14.0", 10 | "msw": "^2.6.6", 11 | "neverthrow": "^8.1.1", 12 | "winston": "^3.17.0", 13 | "winston-transport": "^4.9.0" 14 | }, 15 | "exports": { 16 | "./setup.ts": "./src/setup.ts", 17 | "./test-utils.ts": "./src/test-utils.ts", 18 | "./vitest.d.ts": "./src/vitest.d.ts" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/testing/src/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | type MockLogger = { 4 | children: Map; 5 | // biome-ignore lint/suspicious/noExplicitAny: not actually relevant, just a mock 6 | child: (options: any) => MockLogger; 7 | once: () => void; 8 | debug: () => void; 9 | info: () => void; 10 | warn: () => void; 11 | error: () => void; 12 | close: () => void; 13 | log: () => void; 14 | }; 15 | 16 | export function createMockLogger() { 17 | const children = new Map(); 18 | return { 19 | child: (options: Parameters[0]) => { 20 | const child = createMockLogger(); 21 | children.set(options.module, child); 22 | return child; 23 | }, 24 | once: vi.fn(), 25 | debug: vi.fn(), 26 | info: vi.fn(), 27 | warn: vi.fn(), 28 | error: vi.fn(), 29 | close: vi.fn(), 30 | log: vi.fn(), 31 | children, 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/testing/src/vitest.d.ts: -------------------------------------------------------------------------------- 1 | import "vitest"; 2 | 3 | interface CustomMatchers { 4 | toBeOk: () => R; 5 | toBeErr: () => R; 6 | } 7 | 8 | declare module "vitest" { 9 | // biome-ignore lint/suspicious/noExplicitAny: following vitest recommended pattern 10 | interface Assertion extends CustomMatchers {} 11 | interface AsymmetricMatchersContaining extends CustomMatchers {} 12 | } 13 | -------------------------------------------------------------------------------- /packages/testing/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/internal.json", 3 | "include": ["src"], 4 | } 5 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "skipLibCheck": true, 5 | "target": "es2022", 6 | "allowJs": true, 7 | "resolveJsonModule": true, 8 | "moduleDetection": "force", 9 | "isolatedModules": true, 10 | "verbatimModuleSyntax": true, 11 | "strict": true, 12 | "noUncheckedIndexedAccess": true, 13 | "noImplicitOverride": true, 14 | "module": "NodeNext", 15 | "outDir": "dist", 16 | "sourceMap": true, 17 | "declaration": true, 18 | "composite": true, 19 | "declarationMap": true, 20 | "lib": ["es2022"], 21 | "stripInternal": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/tsconfig/internal.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-tsconfig", 3 | "private": true, 4 | "version": "0.1.0", 5 | "files": [ 6 | "*.json" 7 | ] 8 | } -------------------------------------------------------------------------------- /packages/tsconfig/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "noEmit": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/utils/README.md: -------------------------------------------------------------------------------- 1 | # Launchpad Utils 2 | 3 | Collection of utils used across [@bluecadet/launchpad](https://www.npmjs.com/package/@bluecadet/launchpad) packages. 4 | -------------------------------------------------------------------------------- /packages/utils/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@bluecadet/launchpad-utils", 3 | "version": "2.0.1", 4 | "description": "Common utilities used by multiple launchpad modules", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.ts", 9 | "default": "./dist/index.js" 10 | } 11 | }, 12 | "files": [ 13 | "dist/**/*.js", 14 | "dist/**/*.d.ts" 15 | ], 16 | "scripts": { 17 | "build": "tsc", 18 | "test": "vitest", 19 | "typecheck": "tsc --noEmit" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/bluecadet/launchpad.git" 24 | }, 25 | "author": "Bluecadet", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/bluecadet/launchpad/issues" 29 | }, 30 | "dependencies": { 31 | "@sindresorhus/slugify": "^2.1.0", 32 | "ansi-escapes": "^7.0.0", 33 | "chalk": "^5.0.0", 34 | "moment": "^2.29.1", 35 | "neverthrow": "^8.1.1", 36 | "winston": "^3.17.0", 37 | "winston-daily-rotate-file": "^4.5.5", 38 | "zod": "^3.23.8" 39 | }, 40 | "homepage": "https://github.com/bluecadet/launchpad/packages/utils", 41 | "devDependencies": { 42 | "@bluecadet/launchpad-testing": "0.1.0", 43 | "@bluecadet/launchpad-tsconfig": "0.1.0", 44 | "vitest": "^3.0.7" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/utils/src/__tests__/on-exit.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, it, vi } from "vitest"; 2 | import { _reset, onExit } from "../on-exit.js"; 3 | 4 | describe("onExit", () => { 5 | beforeEach(() => { 6 | _reset(); 7 | }); 8 | 9 | it("should register callbacks for exit events", async () => { 10 | const callback = vi.fn(); 11 | onExit(callback, false); 12 | 13 | // Simulate exit events 14 | await Promise.all([ 15 | process.emit("beforeExit", 0), 16 | process.emit("SIGTERM", "SIGTERM"), 17 | process.emit("SIGINT", "SIGINT"), 18 | ]); 19 | 20 | // Should be called once per event 21 | expect(callback).toHaveBeenCalledTimes(3); 22 | }); 23 | 24 | it("should only call once when once=true", async () => { 25 | const callback = vi.fn(); 26 | onExit(callback, true); 27 | 28 | // Simulate multiple exit events 29 | await Promise.all([ 30 | process.emit("SIGTERM", "SIGTERM"), 31 | process.emit("SIGTERM", "SIGTERM"), 32 | process.emit("SIGTERM", "SIGTERM"), 33 | ]); 34 | 35 | expect(callback).toHaveBeenCalledTimes(1); 36 | }); 37 | 38 | it("should call multiple times when once=false", async () => { 39 | const callback = vi.fn(); 40 | onExit(callback, false); 41 | 42 | // Simulate multiple exit events 43 | await Promise.all([ 44 | process.emit("SIGTERM", "SIGTERM"), 45 | process.emit("SIGTERM", "SIGTERM"), 46 | process.emit("SIGTERM", "SIGTERM"), 47 | ]); 48 | 49 | expect(callback).toHaveBeenCalledTimes(3); 50 | }); 51 | 52 | it("should handle uncaught exceptions when includeUncaught=true", async () => { 53 | const callback = vi.fn(); 54 | onExit(callback, false, true); 55 | 56 | // Simulate uncaught exception 57 | // @ts-ignore 58 | await process.emit("uncaughtException", "uncaughtException"); 59 | // @ts-ignore 60 | await process.emit("unhandledRejection", "unhandledRejection"); 61 | 62 | expect(callback).toHaveBeenCalledTimes(2); 63 | }); 64 | 65 | it("should not handle uncaught exceptions when includeUncaught=false", async () => { 66 | const callback = vi.fn(); 67 | onExit(callback, false, false); 68 | 69 | // Simulate uncaught exception 70 | // @ts-ignore 71 | await process.emit("uncaughtException", "uncaughtException"); 72 | // @ts-ignore 73 | await process.emit("unhandledRejection", "unhandledRejection"); 74 | 75 | expect(callback).not.toHaveBeenCalled(); 76 | }); 77 | 78 | it("should handle async callbacks", async () => { 79 | let flag = false; 80 | const asyncCallback = async () => { 81 | await new Promise((resolve) => setTimeout(resolve, 10)); 82 | flag = true; 83 | }; 84 | 85 | onExit(asyncCallback); 86 | 87 | await process.emit("SIGTERM", "SIGTERM"); 88 | 89 | await vi.waitFor(() => expect(flag).toBe(true)); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /packages/utils/src/__tests__/test-utils.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | import type { Logger } from "../log-manager.ts"; 3 | 4 | export function createMockLogger(): Logger { 5 | return { 6 | info: vi.fn(), 7 | warn: vi.fn(), 8 | error: vi.fn(), 9 | debug: vi.fn(), 10 | once: vi.fn(), 11 | close: vi.fn(), 12 | child: () => createMockLogger(), 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /packages/utils/src/index.ts: -------------------------------------------------------------------------------- 1 | export { onExit } from "./on-exit.js"; 2 | export { LogManager, logConfigSchema } from "./log-manager.js"; 3 | export { 4 | default as PluginDriver, 5 | HookContextProvider, 6 | createPluginValidator, 7 | } from "./plugin-driver.js"; 8 | export type { Logger, LogConfig } from "./log-manager.js"; 9 | export type { Plugin, HookSet, BaseHookContext } from "./plugin-driver.js"; 10 | export { 11 | FixedConsoleLogger, 12 | TTY_ONLY, 13 | NO_TTY, 14 | TTY_FIXED, 15 | TTY_FIXED_END, 16 | } from "./console-transport.js"; 17 | -------------------------------------------------------------------------------- /packages/utils/src/on-exit.ts: -------------------------------------------------------------------------------- 1 | interface Callback { 2 | callback: () => Promise | void; 3 | once: boolean; 4 | includeUncaught: boolean; 5 | } 6 | 7 | let didTrigger = false; 8 | const callbacks: Callback[] = []; 9 | 10 | const events = [ 11 | "beforeExit", 12 | "SIGHUP", 13 | "SIGINT", 14 | "SIGQUIT", 15 | "SIGILL", 16 | "SIGTRAP", 17 | "SIGABRT", 18 | "SIGBUS", 19 | "SIGFPE", 20 | "SIGUSR1", 21 | "SIGSEGV", 22 | "SIGUSR2", 23 | "SIGTERM", 24 | "uncaughtException", 25 | "unhandledRejection", 26 | ] as const; 27 | 28 | for (const event of events) { 29 | process.on(event, async (event) => { 30 | const thisIsFirstTrigger = !didTrigger; 31 | didTrigger = true; 32 | 33 | for (const callback of callbacks) { 34 | if (callback.once && !thisIsFirstTrigger) { 35 | continue; 36 | } 37 | 38 | if ( 39 | !callback.includeUncaught && 40 | (event === "uncaughtException" || event === "unhandledRejection") 41 | ) { 42 | continue; 43 | } 44 | 45 | await callback.callback(); 46 | } 47 | }); 48 | } 49 | 50 | export const onExit = ( 51 | callback: () => Promise | void = async () => {}, 52 | once = true, 53 | includeUncaught = false, 54 | ): void => { 55 | callbacks.push({ callback, once, includeUncaught }); 56 | }; 57 | 58 | /** @internal */ 59 | export function _reset(): void { 60 | didTrigger = false; 61 | callbacks.length = 0; 62 | } 63 | -------------------------------------------------------------------------------- /packages/utils/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "references": [ 3 | { 4 | "path": "./tsconfig.src.json" 5 | }, 6 | { 7 | "path": "./tsconfig.test.json" 8 | } 9 | ], 10 | "files": [] 11 | } -------------------------------------------------------------------------------- /packages/utils/tsconfig.src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "rootDir": "src" 6 | }, 7 | "include": [ 8 | "src/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "**/*.test.ts", 12 | "**/__tests__/*" 13 | ] 14 | } -------------------------------------------------------------------------------- /packages/utils/tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/test.json", 3 | "compilerOptions": { 4 | "types": [ 5 | "@bluecadet/launchpad-testing/vitest.d.ts" 6 | ] 7 | }, 8 | "include": [ 9 | "src/**/*.test.ts", 10 | "src/**/__tests__", 11 | "vitest.config.ts" 12 | ], 13 | "references": [ 14 | { 15 | "path": "./tsconfig.src.json" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/utils/vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineProject } from 'vitest/config'; 2 | 3 | export default defineProject({ 4 | test: { 5 | environment: 'node', 6 | setupFiles: '@bluecadet/launchpad-testing/setup.ts' 7 | } 8 | }); 9 | -------------------------------------------------------------------------------- /patches/@changesets+assemble-release-plan+6.0.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@changesets/assemble-release-plan/dist/changesets-assemble-release-plan.cjs.js b/node_modules/@changesets/assemble-release-plan/dist/changesets-assemble-release-plan.cjs.js 2 | index e32a5e5..08b9a10 100644 3 | --- a/node_modules/@changesets/assemble-release-plan/dist/changesets-assemble-release-plan.cjs.js 4 | +++ b/node_modules/@changesets/assemble-release-plan/dist/changesets-assemble-release-plan.cjs.js 5 | @@ -347,7 +347,7 @@ function shouldBumpMajor({ 6 | // we check if it is a peerDependency because if it is, our dependent bump type might need to be major. 7 | return depType === "peerDependencies" && nextRelease.type !== "none" && nextRelease.type !== "patch" && ( // 1. If onlyUpdatePeerDependentsWhenOutOfRange set to true, bump major if the version is leaving the range. 8 | // 2. If onlyUpdatePeerDependentsWhenOutOfRange set to false, bump major regardless whether or not the version is leaving the range. 9 | - !onlyUpdatePeerDependentsWhenOutOfRange || !semverSatisfies__default["default"](incrementVersion(nextRelease, preInfo), versionRange)) && ( // bump major only if the dependent doesn't already has a major release. 10 | + !onlyUpdatePeerDependentsWhenOutOfRange) && ( // bump major only if the dependent doesn't already has a major release. 11 | !releases.has(dependent) || releases.has(dependent) && releases.get(dependent).type !== "major"); 12 | } 13 | 14 | @@ -444,7 +444,7 @@ function matchFixedConstraint(releases, packagesByName, config) { 15 | 16 | function getPreVersion(version) { 17 | let parsed = semverParse__default["default"](version); 18 | - let preVersion = parsed.prerelease[1] === undefined ? -1 : parsed.prerelease[1]; 19 | + let preVersion = parsed?.prerelease[1] === undefined ? -1 : parsed.prerelease[1]; 20 | 21 | if (typeof preVersion !== "number") { 22 | throw new errors.InternalError("preVersion is not a number"); 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@bluecadet/launchpad-tsconfig/base.json", 3 | "references": [ 4 | { 5 | "path": "packages/utils/tsconfig.src.json" 6 | }, 7 | { 8 | "path": "packages/dashboard" 9 | }, 10 | { 11 | "path": "packages/monitor/tsconfig.src.json" 12 | }, 13 | { 14 | "path": "packages/scaffold" 15 | }, 16 | { 17 | "path": "packages/content/tsconfig.src.json" 18 | }, 19 | { 20 | "path": "packages/cli" 21 | }, 22 | { 23 | "path": "packages/launchpad" 24 | } 25 | ], 26 | "include": [] 27 | } -------------------------------------------------------------------------------- /vitest.workspace.js: -------------------------------------------------------------------------------- 1 | import { defineWorkspace } from 'vitest/config'; 2 | 3 | export default defineWorkspace([ 4 | 'packages/*' 5 | ]); 6 | --------------------------------------------------------------------------------