├── .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 |
12 |
13 |
14 | @bluecadet/launchpad-{{ package }}
15 | {{ description }}
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/docs/src/components/PackageHeader.vue:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | @bluecadet/launchpad
13 | @bluecadet/launchpad-{{ package }}
14 |
15 |
33 |
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 |
--------------------------------------------------------------------------------