diff --git a/.changeset/social-pianos-rest.md b/.changeset/social-pianos-rest.md new file mode 100644 index 00000000..c98cf03f --- /dev/null +++ b/.changeset/social-pianos-rest.md @@ -0,0 +1,8 @@ +--- +"@bluecadet/launchpad-content": minor +"@bluecadet/launchpad-monitor": minor +"@bluecadet/launchpad-utils": minor +"@bluecadet/launchpad-cli": minor +--- + +Support absolute or relative paths in launchpad config. diff --git a/.changeset/solid-webs-slide.md b/.changeset/solid-webs-slide.md new file mode 100644 index 00000000..66ce617a --- /dev/null +++ b/.changeset/solid-webs-slide.md @@ -0,0 +1,8 @@ +--- +"@bluecadet/launchpad-content": minor +"@bluecadet/launchpad-monitor": minor +"@bluecadet/launchpad-utils": minor +"@bluecadet/launchpad-cli": minor +--- + +Resolve all paths relative to the launchpad config directory when run from the CLI diff --git a/.gitattributes b/.gitattributes index a3002918..7f80b853 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,2 @@ -# Declare files that will always have LF line endings on checkout. -*.sh text eol=lf +* text=auto +* text eol=lf \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fafb067c..b871b419 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,51 +1,57 @@ -name: Main - -on: - pull_request: - push: - branches: - - main - - develop - paths: - - "packages/**" - - "docs/**" - merge_group: - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -permissions: - contents: read # to fetch code (actions/checkout) - -env: - FORCE_COLOR: 3 - -jobs: - - ci: - name: CI - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version-file: 'package.json' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: lint - run: npm run lint - - - name: lint dependency versions - # ignore internal launchpad dependency mismatches. These versions - # are handled by changesets. - run: npx sherif -i @bluecadet/launchpad* - - - name: Validate types - run: npm run build - - - name: Run tests +name: Main + +on: + pull_request: + push: + branches: + - main + - develop + paths: + - "packages/**" + - "docs/**" + merge_group: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read # to fetch code (actions/checkout) + +env: + FORCE_COLOR: 3 + +jobs: + + ci: + name: CI + strategy: + matrix: + os: [ubuntu-latest, windows-latest] + node: [18, 24] + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: lint + run: npm run lint + + - name: lint dependency versions + # ignore internal launchpad dependency mismatches. These versions + # are handled by changesets. + run: npx sherif -i @bluecadet/launchpad* + + - name: Validate types + run: npm run build + + - name: Run tests run: npm run test \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6628a357..54387d24 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,45 +1,45 @@ -# Contributing to Launchpad - -## Generating Changelogs - -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. - -To add a changeset, run this script locally: - -``` -npm run changeset -``` - -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. - -Considerations: - -- A changeset is required to trigger the versioning/publishing workflow. -- Non-packages, like examples and tests, do not need changesets. -- 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. - -## Releases - -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. - -To release a new version of Launchpad, find the `Version Packages` PR, read it over, and merge it. - -The `main` branch is kept up to date with the latest releases. - -## Testing Launchpad - -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. - -For example: - -```bat -git clone git@github.com:bluecadet/launchpad.git -cd launchpad -npm i -cd packages/launchpad -npm link - -cd ../../my-test-project -@REM If needed: npm init -npm link @bluecadet/launchpad --save -``` +# Contributing to Launchpad + +## Generating Changelogs + +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. + +To add a changeset, run this script locally: + +``` +npm run changeset +``` + +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. + +Considerations: + +- A changeset is required to trigger the versioning/publishing workflow. +- Non-packages, like examples and tests, do not need changesets. +- 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. + +## Releases + +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. + +To release a new version of Launchpad, find the `Version Packages` PR, read it over, and merge it. + +The `main` branch is kept up to date with the latest releases. + +## Testing Launchpad + +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. + +For example: + +```bat +git clone git@github.com:bluecadet/launchpad.git +cd launchpad +npm i +cd packages/launchpad +npm link + +cd ../../my-test-project +@REM If needed: npm init +npm link @bluecadet/launchpad --save +``` diff --git a/biome.json b/biome.json index 847188d9..68240948 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", + "$schema": "https://biomejs.dev/schemas/2.0.6/schema.json", "vcs": { "enabled": false, "clientKind": "git", @@ -7,21 +7,31 @@ }, "files": { "ignoreUnknown": false, - "ignore": [], - "include": ["**/src/**/*.ts", "docs/**/*.ts", "docs/**/*.mts", "docs/**/*.vue"] + "includes": ["**/src/**/*.ts", "**/docs/**/*.ts", "**/docs/**/*.mts", "**/docs/**/*.vue"] }, "formatter": { "enabled": true, "indentStyle": "tab", - "lineWidth": 100 - }, - "organizeImports": { - "enabled": true + "lineWidth": 100, + "lineEnding": "lf" }, + "assist": { "actions": { "source": { "organizeImports": "on" } } }, "linter": { "enabled": true, "rules": { - "recommended": true + "recommended": true, + "style": { + "noParameterAssign": "error", + "useAsConstAssertion": "error", + "useDefaultParameterLast": "error", + "useEnumInitializers": "error", + "useSelfClosingElements": "error", + "useSingleVarDeclarator": "error", + "noUnusedTemplateLiteral": "error", + "useNumberNamespace": "error", + "noInferrableTypes": "error", + "noUselessElse": "error" + } } }, "javascript": { @@ -31,7 +41,7 @@ }, "overrides": [ { - "include": ["**/*.test.ts"], + "includes": ["**/*.test.ts"], "linter": { "rules": { "style": { diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 34202c6c..21782cc2 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -55,7 +55,7 @@ export default defineConfig({ }, // add canonical link to head - transformPageData(pageData, ctx) { + transformPageData(pageData, _ctx) { const canonicalUrl = `https://bluecadet.github.io/launchpad/${pageData.relativePath}` .replace(/index\.md$/, "") .replace(/\.md$/, ".html"); diff --git a/docs/.vitepress/theme/index.ts b/docs/.vitepress/theme/index.ts index f5debb24..e79fa52e 100644 --- a/docs/.vitepress/theme/index.ts +++ b/docs/.vitepress/theme/index.ts @@ -11,5 +11,5 @@ export default { // https://vitepress.dev/guide/extending-default-theme#layout-slots }); }, - enhanceApp({ app, router, siteData }) {}, + enhanceApp() {}, } satisfies Theme; diff --git a/docs/src/components/PackageCard.vue b/docs/src/components/PackageCard.vue index bd694421..9e7ef248 100644 --- a/docs/src/components/PackageCard.vue +++ b/docs/src/components/PackageCard.vue @@ -1,7 +1,5 @@ diff --git a/docs/src/components/package-versions.data.ts b/docs/src/components/package-versions.data.ts index 581e89d3..d2348a1d 100644 --- a/docs/src/components/package-versions.data.ts +++ b/docs/src/components/package-versions.data.ts @@ -1,8 +1,8 @@ +import launchpadPackage from "@bluecadet/launchpad/package.json" with { type: "json" }; import cliPackage from "@bluecadet/launchpad-cli/package.json" with { type: "json" }; import contentPackage from "@bluecadet/launchpad-content/package.json" with { type: "json" }; import monitorPackage from "@bluecadet/launchpad-monitor/package.json" with { type: "json" }; import scaffoldPackage from "@bluecadet/launchpad-scaffold/package.json" with { type: "json" }; -import launchpadPackage from "@bluecadet/launchpad/package.json" with { type: "json" }; import { defineLoader } from "vitepress"; export interface Data { diff --git a/docs/src/guides/running-applications.md b/docs/src/guides/running-applications.md index ef0ab567..8526139f 100644 --- a/docs/src/guides/running-applications.md +++ b/docs/src/guides/running-applications.md @@ -58,7 +58,7 @@ npx launchpad monitor start - `name`: Unique identifier for your application - `script`: Path to your executable or script -- `cwd`: Working directory for your application +- `cwd`: Working directory for your application. Relative paths are resolved against the current working directory of the launchpad configuration. ### Advanced Settings diff --git a/docs/src/recipes/custom-content-plugin.md b/docs/src/recipes/custom-content-plugin.md index 3e82a419..1d4a07a8 100644 --- a/docs/src/recipes/custom-content-plugin.md +++ b/docs/src/recipes/custom-content-plugin.md @@ -92,6 +92,7 @@ const myPlugin = { logger, // Plugin-specific logging paths, // Helper functions for paths abortSignal // Check if process is stopping + cwd, // The launchpad configuration directory } = ctx; // Example: Log number of documents diff --git a/docs/src/recipes/static-web-monitor.md b/docs/src/recipes/static-web-monitor.md index 342ee425..83c28bbc 100644 --- a/docs/src/recipes/static-web-monitor.md +++ b/docs/src/recipes/static-web-monitor.md @@ -32,7 +32,7 @@ export default defineConfig({ pm2: { name: "webapp-browser", // Path to Chromium (adjust for your environment) - cwd: path.join(homedir(), "AppData/Local/Chromium/Application"), + cwd: path.resolve(homedir(), "AppData/Local/Chromium/Application"), script: "chrome.exe", args: "--kiosk --incognito --disable-pinch --overscroll-history-navgation=0 --enable-auto-reload --autoplay-policy=no-user-gesture-required http://localhost:8080" }, diff --git a/docs/src/reference/content/content-config.md b/docs/src/reference/content/content-config.md index ef25e233..aa725f97 100644 --- a/docs/src/reference/content/content-config.md +++ b/docs/src/reference/content/content-config.md @@ -29,6 +29,8 @@ See [Content Plugin Reference](./plugins/index.md) for available plugins and usa Base directory path where downloaded files are stored. Can be absolute or relative path. +Relative paths are resolved against the directory of the launchpad configuration. + ### `tempPath` - **Type:** `string` @@ -36,6 +38,8 @@ Base directory path where downloaded files are stored. Can be absolute or relati Temporary directory path used during content processing. The `%TIMESTAMP%` token is replaced with current timestamp. +Relative paths are resolved against the directory of the launchpad configuration. + ### `backupPath` - **Type:** `string` @@ -43,6 +47,8 @@ Temporary directory path used during content processing. The `%TIMESTAMP%` token Directory path where existing content is backed up before processing new downloads. Critical for recovery if downloads fail. +Relative paths are resolved against the directory of the launchpad configuration. + ### `keep` - **Type:** `string[]` diff --git a/docs/src/reference/content/plugins/index.md b/docs/src/reference/content/plugins/index.md index ea6978d6..6a16b11f 100644 --- a/docs/src/reference/content/plugins/index.md +++ b/docs/src/reference/content/plugins/index.md @@ -67,6 +67,7 @@ type CombinedContentHookContext = { contentOptions: ResolvedContentConfig; logger: Logger; abortSignal: AbortSignal; + cwd: string; paths: { getDownloadPath: (source?: string) => string; getTempPath: (source?: string) => string; @@ -97,3 +98,7 @@ A plugin-specific logger. ### `abortSignal` Signals the launchpad process is aborting. Triggered on exception or manual quit. + +### `cwd` + +The current working directory of the launchpad configuration. This is useful for resolving paths relative to the configuration files. \ No newline at end of file diff --git a/docs/src/reference/monitor/plugins.md b/docs/src/reference/monitor/plugins.md index b4ff1aac..4869a85c 100644 --- a/docs/src/reference/monitor/plugins.md +++ b/docs/src/reference/monitor/plugins.md @@ -85,6 +85,7 @@ type CombinedMonitorHookContext = { monitor: LaunchpadMonitor; logger: Logger; abortSignal: AbortSignal; + cwd: string; }; ``` @@ -99,3 +100,7 @@ A plugin-specific logger for recording events and errors. ### `abortSignal` Signals when the monitor process is shutting down, allowing plugins to handle cleanup. + +### `cwd` + +The current working directory of the monitor configuration. This is useful for resolving paths relative to the configuration files. \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index f7169301..fc1a6409 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "./docs" ], "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.6", "@changesets/changelog-github": "^0.4.6", "@changesets/cli": "^2.23.0", "@types/node": "^22.9.3", @@ -37,9 +37,9 @@ "vue": "^3.5.12" }, "devDependencies": { - "@bluecadet/launchpad": "2.0.11", + "@bluecadet/launchpad": "2.0.12", "@bluecadet/launchpad-cli": "2.1.1", - "@bluecadet/launchpad-content": "2.1.2", + "@bluecadet/launchpad-content": "2.1.3", "@bluecadet/launchpad-monitor": "2.0.5", "@bluecadet/launchpad-scaffold": "2.0.0" } @@ -329,11 +329,10 @@ } }, "node_modules/@biomejs/biome": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-1.9.4.tgz", - "integrity": "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.0.6.tgz", + "integrity": "sha512-RRP+9cdh5qwe2t0gORwXaa27oTOiQRQvrFf49x2PA1tnpsyU7FIHX4ZOFMtBC4QNtyWsN7Dqkf5EDbg4X+9iqA==", "dev": true, - "hasInstallScript": true, "license": "MIT OR Apache-2.0", "bin": { "biome": "bin/biome" @@ -346,20 +345,20 @@ "url": "https://opencollective.com/biome" }, "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "1.9.4", - "@biomejs/cli-darwin-x64": "1.9.4", - "@biomejs/cli-linux-arm64": "1.9.4", - "@biomejs/cli-linux-arm64-musl": "1.9.4", - "@biomejs/cli-linux-x64": "1.9.4", - "@biomejs/cli-linux-x64-musl": "1.9.4", - "@biomejs/cli-win32-arm64": "1.9.4", - "@biomejs/cli-win32-x64": "1.9.4" + "@biomejs/cli-darwin-arm64": "2.0.6", + "@biomejs/cli-darwin-x64": "2.0.6", + "@biomejs/cli-linux-arm64": "2.0.6", + "@biomejs/cli-linux-arm64-musl": "2.0.6", + "@biomejs/cli-linux-x64": "2.0.6", + "@biomejs/cli-linux-x64-musl": "2.0.6", + "@biomejs/cli-win32-arm64": "2.0.6", + "@biomejs/cli-win32-x64": "2.0.6" } }, "node_modules/@biomejs/cli-darwin-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-1.9.4.tgz", - "integrity": "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.0.6.tgz", + "integrity": "sha512-AzdiNNjNzsE6LfqWyBvcL29uWoIuZUkndu+wwlXW13EKcBHbbKjNQEZIJKYDc6IL+p7bmWGx3v9ZtcRyIoIz5A==", "cpu": [ "arm64" ], @@ -374,9 +373,9 @@ } }, "node_modules/@biomejs/cli-darwin-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-1.9.4.tgz", - "integrity": "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.0.6.tgz", + "integrity": "sha512-wJjjP4E7bO4WJmiQaLnsdXMa516dbtC6542qeRkyJg0MqMXP0fvs4gdsHhZ7p9XWTAmGIjZHFKXdsjBvKGIJJQ==", "cpu": [ "x64" ], @@ -391,9 +390,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-1.9.4.tgz", - "integrity": "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.0.6.tgz", + "integrity": "sha512-ZSVf6TYo5rNMUHIW1tww+rs/krol7U5A1Is/yzWyHVZguuB0lBnIodqyFuwCNqG9aJGyk7xIMS8HG0qGUPz0SA==", "cpu": [ "arm64" ], @@ -408,9 +407,9 @@ } }, "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-1.9.4.tgz", - "integrity": "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.6.tgz", + "integrity": "sha512-CVPEMlin3bW49sBqLBg2x016Pws7eUXA27XYDFlEtponD0luYjg2zQaMJ2nOqlkKG9fqzzkamdYxHdMDc2gZFw==", "cpu": [ "arm64" ], @@ -425,9 +424,9 @@ } }, "node_modules/@biomejs/cli-linux-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz", - "integrity": "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.0.6.tgz", + "integrity": "sha512-geM1MkHTV1Kh2Cs/Xzot9BOF3WBacihw6bkEmxkz4nSga8B9/hWy5BDiOG3gHDGIBa8WxT0nzsJs2f/hPqQIQw==", "cpu": [ "x64" ], @@ -442,9 +441,9 @@ } }, "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-1.9.4.tgz", - "integrity": "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.6.tgz", + "integrity": "sha512-mKHE/e954hR/hSnAcJSjkf4xGqZc/53Kh39HVW1EgO5iFi0JutTN07TSjEMg616julRtfSNJi0KNyxvc30Y4rQ==", "cpu": [ "x64" ], @@ -459,9 +458,9 @@ } }, "node_modules/@biomejs/cli-win32-arm64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-1.9.4.tgz", - "integrity": "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.0.6.tgz", + "integrity": "sha512-290V4oSFoKaprKE1zkYVsDfAdn0An5DowZ+GIABgjoq1ndhvNxkJcpxPsiYtT7slbVe3xmlT0ncdfOsN7KruzA==", "cpu": [ "arm64" ], @@ -476,9 +475,9 @@ } }, "node_modules/@biomejs/cli-win32-x64": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-1.9.4.tgz", - "integrity": "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.0.6.tgz", + "integrity": "sha512-bfM1Bce0d69Ao7pjTjUS+AWSZ02+5UHdiAP85Th8e9yV5xzw6JrHXbL5YWlcEKQ84FIZMdDc7ncuti1wd2sdbw==", "cpu": [ "x64" ], @@ -10399,7 +10398,7 @@ }, "packages/content": { "name": "@bluecadet/launchpad-content", - "version": "2.1.2", + "version": "2.1.3", "license": "ISC", "dependencies": { "@bluecadet/launchpad-utils": "~2.0.1", @@ -10431,6 +10430,9 @@ "sharp": "^0.33.5", "vitest": "^3.0.7" }, + "engines": { + "node": ">=18" + }, "peerDependencies": { "@portabletext/to-html": "2.0.0", "@sanity/block-content-to-markdown": "^0.0.5", @@ -10486,11 +10488,11 @@ }, "packages/launchpad": { "name": "@bluecadet/launchpad", - "version": "2.0.11", + "version": "2.0.12", "license": "ISC", "dependencies": { "@bluecadet/launchpad-cli": "2.1.1", - "@bluecadet/launchpad-content": "2.1.2", + "@bluecadet/launchpad-content": "2.1.3", "@bluecadet/launchpad-dashboard": "2.0.0", "@bluecadet/launchpad-monitor": "2.0.5", "@bluecadet/launchpad-scaffold": "2.0.0" @@ -10499,8 +10501,7 @@ "@bluecadet/launchpad-tsconfig": "0.1.0" }, "engines": { - "node": ">=17.5.0", - "npm": ">=8.5.1" + "node": ">=18" } }, "packages/monitor": { @@ -10528,6 +10529,9 @@ "@types/tail": "2.2", "axon": "^2.0.3", "vitest": "^3.0.7" + }, + "engines": { + "node": ">=18" } }, "packages/monitor/node_modules/chalk": { diff --git a/package.json b/package.json index f8a696b6..0b72f9ff 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "./docs" ], "devDependencies": { - "@biomejs/biome": "1.9.4", + "@biomejs/biome": "2.0.6", "@changesets/changelog-github": "^0.4.6", "@changesets/cli": "^2.23.0", "@types/node": "^22.9.3", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index f5f8df2d..594c0681 100755 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -2,6 +2,7 @@ import { hideBin } from "yargs/helpers"; import yargs from "yargs/yargs"; + export { defineConfig } from "./launchpad-config.js"; export type LaunchpadArgv = { diff --git a/packages/cli/src/commands/content.ts b/packages/cli/src/commands/content.ts index 8e110f0a..0a8d5909 100644 --- a/packages/cli/src/commands/content.ts +++ b/packages/cli/src/commands/content.ts @@ -1,4 +1,4 @@ -import { ResultAsync, err, ok } from "neverthrow"; +import { err, ResultAsync } from "neverthrow"; import type { LaunchpadArgv } from "../cli.js"; import { ConfigError, ImportError } from "../errors.js"; import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/command-utils.js"; @@ -6,18 +6,19 @@ import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/c export function content(argv: LaunchpadArgv) { return loadConfigAndEnv(argv) .mapErr((error) => handleFatalError(error, console)) - .andThen(initializeLogger) - .andThen(({ config, rootLogger }) => { - return importLaunchpadContent() - .andThen(({ default: LaunchpadContent }) => { - if (!config.content) { - return err(new ConfigError("No content config found in your config file.")); - } + .andThen(({ dir, config }) => { + return initializeLogger(config, dir).asyncAndThen((rootLogger) => { + return importLaunchpadContent() + .andThen(({ default: LaunchpadContent }) => { + if (!config.content) { + return err(new ConfigError("No content config found in your config file.")); + } - const contentInstance = new LaunchpadContent(config.content, rootLogger); - return contentInstance.download(); - }) - .orElse((error) => handleFatalError(error, rootLogger)); + const contentInstance = new LaunchpadContent(config.content, rootLogger, dir); + return contentInstance.download(); + }) + .orElse((error) => handleFatalError(error, rootLogger)); + }); }); } diff --git a/packages/cli/src/commands/monitor.ts b/packages/cli/src/commands/monitor.ts index ba4f56cf..eeca1cbc 100644 --- a/packages/cli/src/commands/monitor.ts +++ b/packages/cli/src/commands/monitor.ts @@ -1,4 +1,4 @@ -import { ResultAsync, err, ok } from "neverthrow"; +import { err, ok, ResultAsync } from "neverthrow"; import type { LaunchpadArgv } from "../cli.js"; import { ConfigError, ImportError, MonitorError } from "../errors.js"; import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/command-utils.js"; @@ -6,30 +6,31 @@ import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/c export function monitor(argv: LaunchpadArgv) { return loadConfigAndEnv(argv) .mapErr((error) => handleFatalError(error, console)) - .andThen(initializeLogger) - .andThen(({ config, rootLogger }) => { - return importLaunchpadMonitor() - .andThen(({ default: LaunchpadMonitor }) => { - if (!config.monitor) { - return err(new ConfigError("No monitor config found in your config file.")); - } + .andThen(({ dir, config }) => { + return initializeLogger(config, dir).asyncAndThen((rootLogger) => { + return importLaunchpadMonitor() + .andThen(({ default: LaunchpadMonitor }) => { + if (!config.monitor) { + return err(new ConfigError("No monitor config found in your config file.")); + } - const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger); - return ok(monitorInstance); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise( - monitorInstance.connect(), - (e) => new MonitorError("Failed to connect to monitor", { cause: e }), - ); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise( - monitorInstance.start(), - (e) => new MonitorError("Failed to start monitor", { cause: e }), - ); - }) - .orElse((error) => handleFatalError(error, rootLogger)); + const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger, dir); + return ok(monitorInstance); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise( + monitorInstance.connect(), + (e) => new MonitorError("Failed to connect to monitor", { cause: e }), + ); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise( + monitorInstance.start(), + (e) => new MonitorError("Failed to start monitor", { cause: e }), + ); + }) + .orElse((error) => handleFatalError(error, rootLogger)); + }); }); } diff --git a/packages/cli/src/commands/scaffold.ts b/packages/cli/src/commands/scaffold.ts index e12f8d82..35703108 100644 --- a/packages/cli/src/commands/scaffold.ts +++ b/packages/cli/src/commands/scaffold.ts @@ -2,7 +2,7 @@ import { launchScaffold } from "@bluecadet/launchpad-scaffold"; import { LogManager } from "@bluecadet/launchpad-utils"; import type { LaunchpadArgv } from "../cli.js"; -export async function scaffold(argv: LaunchpadArgv) { +export async function scaffold(_argv: LaunchpadArgv) { const rootLogger = LogManager.configureRootLogger(); await launchScaffold(rootLogger); } diff --git a/packages/cli/src/commands/start.ts b/packages/cli/src/commands/start.ts index 94ed3080..b8ddf189 100644 --- a/packages/cli/src/commands/start.ts +++ b/packages/cli/src/commands/start.ts @@ -1,4 +1,4 @@ -import { ResultAsync, err, ok } from "neverthrow"; +import { err, ok, ResultAsync } from "neverthrow"; import type { LaunchpadArgv } from "../cli.js"; import { ConfigError, MonitorError } from "../errors.js"; import { handleFatalError, initializeLogger, loadConfigAndEnv } from "../utils/command-utils.js"; @@ -8,38 +8,39 @@ import { importLaunchpadMonitor } from "./monitor.js"; export async function start(argv: LaunchpadArgv) { return loadConfigAndEnv(argv) .mapErr((error) => handleFatalError(error, console)) - .andThen(initializeLogger) - .andThen(({ config, rootLogger }) => { - return importLaunchpadContent() - .andThen(({ default: LaunchpadContent }) => { - if (!config.content) { - return err(new ConfigError("No content config found in your config file.")); - } + .andThen(({ dir, config }) => { + return initializeLogger(config, dir).asyncAndThen((rootLogger) => { + return importLaunchpadContent() + .andThen(({ default: LaunchpadContent }) => { + if (!config.content) { + return err(new ConfigError("No content config found in your config file.")); + } - const contentInstance = new LaunchpadContent(config.content, rootLogger); - return contentInstance.start(); - }) - .andThen(() => importLaunchpadMonitor()) - .andThen(({ default: LaunchpadMonitor }) => { - if (!config.monitor) { - return err(new ConfigError("No monitor config found in your config file.")); - } + const contentInstance = new LaunchpadContent(config.content, rootLogger); + return contentInstance.start(); + }) + .andThen(() => importLaunchpadMonitor()) + .andThen(({ default: LaunchpadMonitor }) => { + if (!config.monitor) { + return err(new ConfigError("No monitor config found in your config file.")); + } - const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger); - return ok(monitorInstance); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise( - monitorInstance.connect(), - (e) => new MonitorError("Failed to connect to monitor", { cause: e }), - ); - }) - .andThrough((monitorInstance) => { - return ResultAsync.fromPromise( - monitorInstance.start(), - (e) => new MonitorError("Failed to start monitor", { cause: e }), - ); - }) - .orElse((error) => handleFatalError(error, rootLogger)); + const monitorInstance = new LaunchpadMonitor(config.monitor, rootLogger); + return ok(monitorInstance); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise( + monitorInstance.connect(), + (e) => new MonitorError("Failed to connect to monitor", { cause: e }), + ); + }) + .andThrough((monitorInstance) => { + return ResultAsync.fromPromise( + monitorInstance.start(), + (e) => new MonitorError("Failed to start monitor", { cause: e }), + ); + }) + .orElse((error) => handleFatalError(error, rootLogger)); + }); }); } diff --git a/packages/cli/src/commands/stop.ts b/packages/cli/src/commands/stop.ts index 05164f39..3ecd1a05 100644 --- a/packages/cli/src/commands/stop.ts +++ b/packages/cli/src/commands/stop.ts @@ -2,7 +2,7 @@ import LaunchpadMonitor from "@bluecadet/launchpad-monitor"; import { LogManager } from "@bluecadet/launchpad-utils"; import type { LaunchpadArgv } from "../cli.js"; -export async function stop(argv: LaunchpadArgv) { +export async function stop(_argv: LaunchpadArgv) { const logger = LogManager.configureRootLogger(); await LaunchpadMonitor.kill(logger); } diff --git a/packages/cli/src/utils/command-utils.ts b/packages/cli/src/utils/command-utils.ts index 5ce8cd9e..0e04af30 100644 --- a/packages/cli/src/utils/command-utils.ts +++ b/packages/cli/src/utils/command-utils.ts @@ -1,7 +1,7 @@ import path from "node:path"; -import { LogManager, type Logger, NO_TTY, TTY_FIXED_END } from "@bluecadet/launchpad-utils"; +import { type Logger, LogManager, TTY_FIXED_END } from "@bluecadet/launchpad-utils"; import chalk from "chalk"; -import { ResultAsync, err, errAsync, ok } from "neverthrow"; +import { errAsync, ok, ResultAsync } from "neverthrow"; import { ZodError } from "zod"; import type { LaunchpadArgv } from "../cli.js"; import { ConfigError } from "../errors.js"; @@ -11,7 +11,7 @@ import { resolveEnv } from "./env.js"; export function loadConfigAndEnv( argv: LaunchpadArgv, -): ResultAsync { +): ResultAsync<{ dir: string; config: ResolvedLaunchpadOptions }, ConfigError> { const configPath = argv.config ?? findConfig(); if (!configPath) { @@ -45,13 +45,13 @@ export function loadConfigAndEnv( new ConfigError(`Failed to load config file at path: ${chalk.white(configPath)}`, { cause: e, }), - ).map((config) => resolveLaunchpadConfig(config)); + ).map((config) => ({ dir: configDir, config: resolveLaunchpadConfig(config) })); } -export function initializeLogger(config: ResolvedLaunchpadOptions) { - const rootLogger = LogManager.configureRootLogger(config.logging); +export function initializeLogger(config: ResolvedLaunchpadOptions, cwd?: string) { + const rootLogger = LogManager.configureRootLogger(config.logging, cwd); - return ok({ config, rootLogger }); + return ok(rootLogger); } export function handleFatalError(error: Error, rootLogger: Logger | Console): never { diff --git a/packages/cli/src/utils/config.ts b/packages/cli/src/utils/config.ts index faad0a2e..3b660adf 100644 --- a/packages/cli/src/utils/config.ts +++ b/packages/cli/src/utils/config.ts @@ -36,7 +36,7 @@ function findAllConfigsRecursive() { for (let i = 0; i < maxDepth; i++) { for (const defaultPath of DEFAULT_CONFIG_PATHS) { - const candidatePath = path.join(currentDir, defaultPath); + const candidatePath = path.resolve(currentDir, defaultPath); if (fs.existsSync(candidatePath)) { foundConfigs.push(candidatePath); } diff --git a/packages/content/package.json b/packages/content/package.json index 1a7716cd..908dcdde 100644 --- a/packages/content/package.json +++ b/packages/content/package.json @@ -21,6 +21,9 @@ "type": "git", "url": "git+https://github.com/bluecadet/launchpad.git" }, + "engines": { + "node": ">=18" + }, "author": "Bluecadet", "license": "ISC", "bugs": { diff --git a/packages/content/src/__tests__/content-integration.test.ts b/packages/content/src/__tests__/content-integration.test.ts index d977659c..7723deec 100644 --- a/packages/content/src/__tests__/content-integration.test.ts +++ b/packages/content/src/__tests__/content-integration.test.ts @@ -1,7 +1,7 @@ -import path from "node:path/posix"; +import path from "node:path"; import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; import { vol } from "memfs"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import LaunchpadContent from "../launchpad-content.js"; @@ -66,7 +66,7 @@ describe("Content Integration", () => { expect(result).toBeOk(); // Check if markdown was converted to HTML - const articlePath = path.join("/downloads", "blog", "article.json"); + const articlePath = path.resolve("/downloads", "blog", "article.json"); expect(vol.existsSync(articlePath)).toBe(true); const article = JSON.parse(vol.readFileSync(articlePath, "utf8").toString()); expect(article.content).toContain("

Hello World

"); @@ -75,7 +75,7 @@ describe("Content Integration", () => { // Check if media was downloaded const mediaFiles = ["gallery1.jpg", "gallery2.jpg"]; for (const file of mediaFiles) { - const mediaPath = path.join("/downloads", "blog", file); + const mediaPath = path.resolve("/downloads", "blog", file); expect(vol.existsSync(mediaPath)).toBe(true); expect(vol.readFileSync(mediaPath, "utf8")).toBe("fake image data"); } @@ -146,7 +146,7 @@ describe("Content Integration", () => { expect(result).toBeOk(); - const articlePath = path.join("/downloads", "cms", "article.json"); + const articlePath = path.resolve("/downloads", "cms", "article.json"); expect(vol.existsSync(articlePath)).toBe(true); const article = JSON.parse(vol.readFileSync(articlePath, "utf8").toString()); @@ -213,8 +213,8 @@ describe("Content Integration", () => { expect(result).toBeOk(); // Check if shared media was downloaded only once and is accessible from both sources - const mediaPath1 = path.join("/downloads", "source1", "shared.jpg"); - const mediaPath2 = path.join("/downloads", "source2", "shared.jpg"); + const mediaPath1 = path.resolve("/downloads", "source1", "shared.jpg"); + const mediaPath2 = path.resolve("/downloads", "source2", "shared.jpg"); expect(vol.existsSync(mediaPath1)).toBe(true); expect(vol.existsSync(mediaPath2)).toBe(true); diff --git a/packages/content/src/__tests__/content-plugin-driver.test.ts b/packages/content/src/__tests__/content-plugin-driver.test.ts index a67b24d2..26858542 100644 --- a/packages/content/src/__tests__/content-plugin-driver.test.ts +++ b/packages/content/src/__tests__/content-plugin-driver.test.ts @@ -33,7 +33,7 @@ describe("ContentPluginDriver", () => { it("should provide correct context to plugins", async () => { const { dataStore, options, paths } = createMockContext(); const baseLogger = createMockLogger(); - const driver = new PluginDriver(baseLogger); + const driver = new PluginDriver({ logger: baseLogger }); const contentDriver = new ContentPluginDriver(driver, { dataStore, options, @@ -62,7 +62,7 @@ describe("ContentPluginDriver", () => { it("should handle plugin-specific temp paths correctly", async () => { const { dataStore, options, paths } = createMockContext(); const baseLogger = createMockLogger(); - const driver = new PluginDriver(baseLogger); + const driver = new PluginDriver({ logger: baseLogger }); const contentDriver = new ContentPluginDriver(driver, { dataStore, options, @@ -98,7 +98,7 @@ describe("ContentPluginDriver", () => { it("should handle setup errors with ContentError", async () => { const { dataStore, options, paths } = createMockContext(); const baseLogger = createMockLogger(); - const driver = new PluginDriver(baseLogger); + const driver = new PluginDriver({ logger: baseLogger }); const contentDriver = new ContentPluginDriver(driver, { dataStore, options, @@ -125,7 +125,7 @@ describe("ContentPluginDriver", () => { it("should handle fetch errors with ContentError", async () => { const { dataStore, options, paths } = createMockContext(); const baseLogger = createMockLogger(); - const driver = new PluginDriver(baseLogger); + const driver = new PluginDriver({ logger: baseLogger }); const contentDriver = new ContentPluginDriver(driver, { dataStore, options, @@ -154,7 +154,7 @@ describe("ContentPluginDriver", () => { it("should call hooks in correct order", async () => { const { dataStore, options, paths } = createMockContext(); const baseLogger = createMockLogger(); - const driver = new PluginDriver(baseLogger); + const driver = new PluginDriver({ logger: baseLogger }); const contentDriver = new ContentPluginDriver(driver, { dataStore, options, diff --git a/packages/content/src/__tests__/launchpad-content.test.ts b/packages/content/src/__tests__/launchpad-content.test.ts index 8ab8d40b..a5f2a073 100644 --- a/packages/content/src/__tests__/launchpad-content.test.ts +++ b/packages/content/src/__tests__/launchpad-content.test.ts @@ -1,8 +1,7 @@ -import path from "node:path/posix"; +import path from "node:path"; import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; import { vol } from "memfs"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { contentConfigSchema } from "../content-config.js"; import { ContentError, type ContentPlugin } from "../content-plugin-driver.js"; import LaunchpadContent from "../launchpad-content.js"; import { defineSource } from "../sources/source.js"; @@ -15,9 +14,9 @@ describe("LaunchpadContent", () => { const createBasicConfig = (plugins: ContentPlugin[] = []) => { return { - downloadPath: "/downloads", - tempPath: "/temp", - backupPath: "/backups", + downloadPath: "downloads", + tempPath: "temp", + backupPath: "backups", sources: [ defineSource({ id: "test", @@ -44,7 +43,7 @@ describe("LaunchpadContent", () => { expect(result).toBeOk(); - const filePath = path.join("/downloads", "test", "doc1.json"); + const filePath = path.resolve("/downloads", "test", "doc1.json"); expect(vol.existsSync(filePath)).toBe(true); expect(vol.readFileSync(filePath, "utf8")).toBe(JSON.stringify({ hello: "world" })); }); @@ -146,7 +145,7 @@ describe("LaunchpadContent", () => { it("should handle download path token replacement", () => { const content = new LaunchpadContent(createBasicConfig(), createMockLogger()); const path = content._getDetokenizedPath("/path/to/%DOWNLOAD_PATH%/file", "/downloads"); - expect(path).toBe("/path/to/downloads/file"); + expect(path).toMatchPath("/path/to/downloads/file"); }); it("should handle timestamp token replacement", () => { @@ -156,8 +155,59 @@ describe("LaunchpadContent", () => { const content = new LaunchpadContent(createBasicConfig(), createMockLogger()); const path = content._getDetokenizedPath("/path/to/%TIMESTAMP%/file", "/downloads"); - expect(path).toMatch("/path/to/2024-01-02_00-00-00/file"); + expect(path).toMatchPath("/path/to/2024-01-02_00-00-00/file"); vi.useRealTimers(); }); + + it("should use the provided cwd for path resolution", () => { + const content = new LaunchpadContent(createBasicConfig(), createMockLogger(), "/some/cwd"); + expect(content.getDownloadPath()).toMatchPath("/some/cwd/downloads"); + expect(content.getDownloadPath("source-id")).toMatchPath("/some/cwd/downloads/source-id"); + expect(content.getTempPath()).toMatchPath("/some/cwd/temp"); + expect(content.getTempPath("source-id")).toMatchPath("/some/cwd/temp/source-id"); + expect(content.getTempPath("source-id", "plugin-name")).toMatchPath( + "/some/cwd/temp/plugin-name/source-id", + ); + expect(content.getBackupPath("source-id")).toMatchPath("/some/cwd/backups/source-id"); + expect(content.getBackupPath()).toMatchPath("/some/cwd/backups"); + }); + + it("should default to process.cwd() if no cwd is provided", () => { + const content = new LaunchpadContent(createBasicConfig(), createMockLogger()); + + expect(content.getDownloadPath()).toMatchPath("downloads"); + expect(content.getDownloadPath("source-id")).toMatchPath("downloads/source-id"); + expect(content.getTempPath()).toMatchPath("temp"); + expect(content.getTempPath("source-id")).toMatchPath("temp/source-id"); + expect(content.getTempPath("source-id", "plugin-name")).toMatchPath( + "temp/plugin-name/source-id", + ); + expect(content.getBackupPath("source-id")).toMatchPath("backups/source-id"); + expect(content.getBackupPath()).toMatchPath("backups"); + }); + + it("should support absolute path parameters", () => { + // even though cwd is set, absolute paths should still work + const content = new LaunchpadContent( + { + downloadPath: "/absolute/downloads", + tempPath: "/absolute/temp", + backupPath: "/absolute/backups", + sources: [], + }, + createMockLogger(), + "/some/cwd", + ); + + expect(content.getDownloadPath()).toMatchPath("/absolute/downloads"); + expect(content.getDownloadPath("source-id")).toMatchPath("/absolute/downloads/source-id"); + expect(content.getTempPath()).toMatchPath("/absolute/temp"); + expect(content.getTempPath("source-id")).toMatchPath("/absolute/temp/source-id"); + expect(content.getTempPath("source-id", "plugin-name")).toMatchPath( + "/absolute/temp/plugin-name/source-id", + ); + expect(content.getBackupPath("source-id")).toMatchPath("/absolute/backups/source-id"); + expect(content.getBackupPath()).toMatchPath("/absolute/backups"); + }); }); }); diff --git a/packages/content/src/content-config.ts b/packages/content/src/content-config.ts index 2f04a5b4..fb143322 100644 --- a/packages/content/src/content-config.ts +++ b/packages/content/src/content-config.ts @@ -1,7 +1,6 @@ import { z } from "zod"; -import { type ContentPlugin, contentPluginSchema } from "./content-plugin-driver.js"; +import { contentPluginSchema } from "./content-plugin-driver.js"; import { type ContentSource, contentSourceSchema } from "./sources/source.js"; -import plugin from "./utils/markdown-it-italic-bold.js"; export const DOWNLOAD_PATH_TOKEN = "%DOWNLOAD_PATH%"; export const TIMESTAMP_TOKEN = "%TIMESTAMP%"; diff --git a/packages/content/src/content-plugin-driver.ts b/packages/content/src/content-plugin-driver.ts index b6722bb4..7633b836 100644 --- a/packages/content/src/content-plugin-driver.ts +++ b/packages/content/src/content-plugin-driver.ts @@ -1,9 +1,9 @@ import { type BaseHookContext, + createPluginValidator, HookContextProvider, type Plugin, type PluginDriver, - createPluginValidator, } from "@bluecadet/launchpad-utils"; import type { ResolvedContentConfig } from "./content-config.js"; import type { DataStore } from "./utils/data-store.js"; diff --git a/packages/content/src/index.ts b/packages/content/src/index.ts index 3d1b18f5..e5360906 100755 --- a/packages/content/src/index.ts +++ b/packages/content/src/index.ts @@ -1,10 +1,10 @@ import LaunchpadContent from "./launchpad-content.js"; export * from "./content-config.js"; +export * from "./content-plugin-driver.js"; export * from "./launchpad-content.js"; -export * from "./utils/file-utils.js"; -export * from "./sources/index.js"; export * from "./plugins/index.js"; -export * from "./content-plugin-driver.js"; +export * from "./sources/index.js"; +export * from "./utils/file-utils.js"; export default LaunchpadContent; diff --git a/packages/content/src/launchpad-content.ts b/packages/content/src/launchpad-content.ts index 81b8deaf..1f5c03b4 100644 --- a/packages/content/src/launchpad-content.ts +++ b/packages/content/src/launchpad-content.ts @@ -1,15 +1,14 @@ import path from "node:path"; -import { LogManager, type Logger } from "@bluecadet/launchpad-utils"; -import { PluginDriver } from "@bluecadet/launchpad-utils"; +import { type Logger, LogManager, PluginDriver } from "@bluecadet/launchpad-utils"; import chalk from "chalk"; -import { Result, ResultAsync, err, ok, okAsync } from "neverthrow"; +import { err, ok, okAsync, Result, ResultAsync } from "neverthrow"; import { type ConfigContentSource, type ContentConfig, + contentConfigSchema, DOWNLOAD_PATH_TOKEN, type ResolvedContentConfig, TIMESTAMP_TOKEN, - contentConfigSchema, } from "./content-config.js"; import { ContentError, ContentPluginDriver } from "./content-plugin-driver.js"; import type { ContentSource } from "./sources/source.js"; @@ -24,10 +23,13 @@ class LaunchpadContent { _rawSources: ConfigContentSource[]; _startDatetime = new Date(); _dataStore: DataStore; + _cwd: string; - constructor(config: ContentConfig, parentLogger: Logger) { + constructor(config: ContentConfig, parentLogger: Logger, cwd = process.cwd()) { this._config = contentConfigSchema.parse(config); + this._cwd = cwd; + this._logger = LogManager.getLogger("content", parentLogger); this._dataStore = new DataStore(this._config.downloadPath); @@ -35,7 +37,10 @@ class LaunchpadContent { // create all sources this._rawSources = this._config.sources; - const basePluginDriver = new PluginDriver(this._logger, this._config.plugins); + const basePluginDriver = new PluginDriver( + { logger: this._logger, cwd: this._cwd }, + this._config.plugins, + ); this._pluginDriver = new ContentPluginDriver(basePluginDriver, { dataStore: this._dataStore, @@ -221,9 +226,9 @@ class LaunchpadContent { getDownloadPath(sourceId?: string): string { if (sourceId) { - return path.resolve(path.join(this._config.downloadPath, sourceId)); + return path.resolve(this._cwd, this._config.downloadPath, sourceId); } - return path.resolve(this._config.downloadPath); + return path.resolve(this._cwd, this._config.downloadPath); } getTempPath(sourceId?: string, pluginName?: string): string { @@ -232,14 +237,14 @@ class LaunchpadContent { let detokenizedPath = this._getDetokenizedPath(tokenizedPath, downloadPath); if (pluginName) { - detokenizedPath = path.join(detokenizedPath, pluginName); + detokenizedPath = path.resolve(this._cwd, detokenizedPath, pluginName); } if (sourceId) { - detokenizedPath = path.join(detokenizedPath, sourceId); + detokenizedPath = path.resolve(this._cwd, detokenizedPath, sourceId); } - return detokenizedPath; + return path.resolve(this._cwd, detokenizedPath); } getBackupPath(sourceId?: string): string { @@ -247,9 +252,9 @@ class LaunchpadContent { const tokenizedPath = this._config.backupPath; const detokenizedPath = this._getDetokenizedPath(tokenizedPath, downloadPath); if (sourceId) { - return path.join(detokenizedPath, sourceId); + return path.resolve(path.resolve(this._cwd, detokenizedPath, sourceId)); } - return detokenizedPath; + return path.resolve(path.resolve(this._cwd, detokenizedPath)); } _createSourcesFromConfig( @@ -353,7 +358,7 @@ class LaunchpadContent { if (innerTokenizedPath.includes(DOWNLOAD_PATH_TOKEN)) { innerTokenizedPath = innerTokenizedPath.replace(DOWNLOAD_PATH_TOKEN, downloadPath); } - return path.resolve(innerTokenizedPath); + return path.join(innerTokenizedPath); } } diff --git a/packages/content/src/plugins/__tests__/media-downloader.test.ts b/packages/content/src/plugins/__tests__/media-downloader.test.ts index c30276ea..34388b27 100644 --- a/packages/content/src/plugins/__tests__/media-downloader.test.ts +++ b/packages/content/src/plugins/__tests__/media-downloader.test.ts @@ -1,14 +1,14 @@ -import path from "node:path/posix"; +import path from "node:path"; import { vol } from "memfs"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import type { z } from "zod"; import mediaDownloader, { - localFilePathFromUrl, checkCacheStatus, downloadFile, findMediaUrls, + localFilePathFromUrl, mediaDownloaderConfigSchema, } from "../media-downloader.js"; import { createTestPluginContext } from "./plugins.test-utils.js"; @@ -316,7 +316,7 @@ describe("mediaDownloader", () => { // Check all files were downloaded const expectedFiles = ["1.jpg", "2.jpg", "3.jpg"]; for (const file of expectedFiles) { - const filePath = path.join(ctx.paths.getDownloadPath(), "test", file); + const filePath = path.resolve(ctx.paths.getDownloadPath(), "test", file); expect(vol.existsSync(filePath)).toBe(true); expect(vol.readFileSync(filePath, "utf8")).toBe("media content"); } @@ -349,11 +349,11 @@ describe("mediaDownloader", () => { await plugin.hooks.onContentFetchDone!(ctx); // Success file should exist - const successPath = path.join(ctx.paths.getDownloadPath(), "test", "success.jpg"); + const successPath = path.resolve(ctx.paths.getDownloadPath(), "test", "success.jpg"); expect(vol.existsSync(successPath)).toBe(true); // Error file should not exist - const errorPath = path.join(ctx.paths.getDownloadPath(), "test", "error.jpg"); + const errorPath = path.resolve(ctx.paths.getDownloadPath(), "test", "error.jpg"); expect(vol.existsSync(errorPath)).toBe(false); }); }); diff --git a/packages/content/src/plugins/__tests__/plugins.test-utils.ts b/packages/content/src/plugins/__tests__/plugins.test-utils.ts index da550545..d63b9da7 100644 --- a/packages/content/src/plugins/__tests__/plugins.test-utils.ts +++ b/packages/content/src/plugins/__tests__/plugins.test-utils.ts @@ -2,8 +2,7 @@ import path from "node:path"; import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; import type { Logger } from "@bluecadet/launchpad-utils"; import { vol } from "memfs"; -import { vi } from "vitest"; -import { afterEach } from "vitest"; +import { afterEach, vi } from "vitest"; import { type ContentConfig, contentConfigSchema } from "../../content-config.js"; import { DataStore } from "../../utils/data-store.js"; @@ -17,23 +16,28 @@ afterEach(() => { export async function createTestPluginContext({ baseOptions = {}, logger = createMockLogger(), -}: { namespaces?: string[]; baseOptions?: ContentConfig; logger?: Logger } = {}) { +}: { + namespaces?: string[]; + baseOptions?: ContentConfig; + logger?: Logger; +} = {}) { const data = new DataStore("/"); return { data, logger, abortSignal: new AbortController().signal, + cwd: "/", paths: { getDownloadPath: vi .fn() - .mockImplementation((sourceId?: string) => path.join("/download", sourceId || "")), + .mockImplementation((sourceId?: string) => path.resolve("download", sourceId || "")), getTempPath: vi .fn() - .mockImplementation((sourceId?: string) => path.join("/temp", sourceId || "")), + .mockImplementation((sourceId?: string) => path.resolve("temp", sourceId || "")), getBackupPath: vi .fn() - .mockImplementation((sourceId?: string) => path.join("/backup", sourceId || "")), + .mockImplementation((sourceId?: string) => path.resolve("backup", sourceId || "")), }, contentOptions: contentConfigSchema.parse(baseOptions), }; diff --git a/packages/content/src/plugins/__tests__/sharp.test.ts b/packages/content/src/plugins/__tests__/sharp.test.ts index 1f9e508b..ba5e1b4c 100644 --- a/packages/content/src/plugins/__tests__/sharp.test.ts +++ b/packages/content/src/plugins/__tests__/sharp.test.ts @@ -1,3 +1,4 @@ +import path from "node:path"; import { vol } from "memfs"; import type Sharp from "sharp"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; @@ -89,7 +90,7 @@ describe("sharp", () => { }); await expect(plugin.hooks.onContentFetchDone!(ctx)).rejects.toThrow( - "Input file '/download/test/nonexistent.jpg' does not exist", + `Input file '${path.resolve("/download/test/nonexistent.jpg")}' does not exist`, ); }); diff --git a/packages/content/src/plugins/index.ts b/packages/content/src/plugins/index.ts index e1a5467b..1ddb4e21 100644 --- a/packages/content/src/plugins/index.ts +++ b/packages/content/src/plugins/index.ts @@ -1,7 +1,7 @@ export { default as mdToHtml } from "./md-to-html.js"; +export { default as mediaDownloader } from "./media-downloader.js"; +export { default as sanityImageUrlTransform } from "./sanity-image-url-transform.js"; export { default as sanityToHtml } from "./sanity-to-html.js"; -export { default as sanityToPlain } from "./sanity-to-plain.js"; export { default as sanityToMd } from "./sanity-to-markdown.js"; -export { default as mediaDownloader } from "./media-downloader.js"; +export { default as sanityToPlain } from "./sanity-to-plain.js"; export { default as sharp } from "./sharp.js"; -export { default as sanityImageUrlTransform } from "./sanity-image-url-transform.js"; diff --git a/packages/content/src/plugins/md-to-html.ts b/packages/content/src/plugins/md-to-html.ts index 72597cdf..d5bd3dd8 100644 --- a/packages/content/src/plugins/md-to-html.ts +++ b/packages/content/src/plugins/md-to-html.ts @@ -4,7 +4,7 @@ import sanitizeHtml from "sanitize-html"; import { z } from "zod"; import { defineContentPlugin } from "../content-plugin-driver.js"; import { applyTransformToFiles } from "../utils/content-transform-utils.js"; -import { type DataKeys, dataKeysSchema } from "../utils/data-store.js"; +import { dataKeysSchema } from "../utils/data-store.js"; import markdownItItalicBold from "../utils/markdown-it-italic-bold.js"; import { parsePluginConfig } from "./contentPluginHelpers.js"; diff --git a/packages/content/src/plugins/media-downloader.ts b/packages/content/src/plugins/media-downloader.ts index 6ee22856..d9da09b8 100644 --- a/packages/content/src/plugins/media-downloader.ts +++ b/packages/content/src/plugins/media-downloader.ts @@ -3,7 +3,7 @@ import fs from "node:fs"; import path from "node:path"; import { pipeline } from "node:stream/promises"; import chalk from "chalk"; -import { ResultAsync, errAsync, ok, okAsync } from "neverthrow"; +import { errAsync, ok, okAsync, ResultAsync } from "neverthrow"; import { z } from "zod"; import { type ContentHookContext, defineContentPlugin } from "../content-plugin-driver.js"; import type { DataStore } from "../utils/data-store.js"; @@ -295,7 +295,7 @@ function setupDownloadDirectories( function cleanupAfterDownload( ctx: ContentHookContext, - config: MediaDownloaderConfigWithDefaults, + _config: MediaDownloaderConfigWithDefaults, ): ResultAsync { return FileUtils.copy(ctx.paths.getTempPath(), ctx.paths.getDownloadPath()) .andThen(() => FileUtils.remove(ctx.paths.getTempPath())) @@ -354,8 +354,8 @@ export default function mediaDownloader(config: z.input { const destDir = ctx.paths.getTempPath(mediaItem.sourceId); const backupDir = ctx.paths.getBackupPath(mediaItem.sourceId); - const destPath = path.join(destDir, mediaItem.localPath); - const backupPath = path.join(backupDir, mediaItem.localPath); + const destPath = path.resolve(destDir, mediaItem.localPath); + const backupPath = path.resolve(backupDir, mediaItem.localPath); const task = ({ signal }: { signal?: AbortSignal }) => downloadMedia( diff --git a/packages/content/src/plugins/sharp.ts b/packages/content/src/plugins/sharp.ts index ad1dc19f..557643f2 100644 --- a/packages/content/src/plugins/sharp.ts +++ b/packages/content/src/plugins/sharp.ts @@ -158,12 +158,15 @@ export default function sharp(options: z.input) { if (!sourceUrls.has(val)) { sourceUrls.add(val); - const fullInputPath = path.join(ctx.paths.getDownloadPath(source.namespaceId), val); - const fullOutputPath = path.join( + const fullInputPath = path.resolve( + ctx.paths.getDownloadPath(source.namespaceId), + val, + ); + const fullOutputPath = path.resolve( ctx.paths.getTempPath(source.namespaceId), newLocalPath, ); - const fullBackupPath = path.join( + const fullBackupPath = path.resolve( ctx.paths.getBackupPath(source.namespaceId), newLocalPath, ); diff --git a/packages/content/src/sources/__tests__/airtable-source.test.ts b/packages/content/src/sources/__tests__/airtable-source.test.ts index fbd38e3e..568c7f70 100644 --- a/packages/content/src/sources/__tests__/airtable-source.test.ts +++ b/packages/content/src/sources/__tests__/airtable-source.test.ts @@ -1,5 +1,5 @@ import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { DataStore } from "../../utils/data-store.js"; diff --git a/packages/content/src/sources/__tests__/contentful-source.test.ts b/packages/content/src/sources/__tests__/contentful-source.test.ts index 611977f7..6edba409 100644 --- a/packages/content/src/sources/__tests__/contentful-source.test.ts +++ b/packages/content/src/sources/__tests__/contentful-source.test.ts @@ -1,5 +1,5 @@ import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { DataStore } from "../../utils/data-store.js"; diff --git a/packages/content/src/sources/__tests__/json-source.test.ts b/packages/content/src/sources/__tests__/json-source.test.ts index c64bfc48..78577bb4 100644 --- a/packages/content/src/sources/__tests__/json-source.test.ts +++ b/packages/content/src/sources/__tests__/json-source.test.ts @@ -1,5 +1,5 @@ import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { DataStore } from "../../utils/data-store.js"; diff --git a/packages/content/src/sources/__tests__/sanity-source.test.ts b/packages/content/src/sources/__tests__/sanity-source.test.ts index f10032f8..72934ccc 100644 --- a/packages/content/src/sources/__tests__/sanity-source.test.ts +++ b/packages/content/src/sources/__tests__/sanity-source.test.ts @@ -1,5 +1,5 @@ import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { DataStore } from "../../utils/data-store.js"; @@ -230,15 +230,12 @@ describe("sanitySource", () => { it("should support single item responses", async () => { server.use( - http.get( - "https://test-project.api.sanity.io/v2021-10-21/data/query/production", - ({ request }) => { - return HttpResponse.json({ - result: { _type: "test", title: "Test Document" }, - ms: 15, - }); - }, - ), + http.get("https://test-project.api.sanity.io/v2021-10-21/data/query/production", () => { + return HttpResponse.json({ + result: { _type: "test", title: "Test Document" }, + ms: 15, + }); + }), ); const source = await sanitySource({ diff --git a/packages/content/src/sources/__tests__/strapi-source.test.ts b/packages/content/src/sources/__tests__/strapi-source.test.ts index f18f2ca7..36b10c36 100644 --- a/packages/content/src/sources/__tests__/strapi-source.test.ts +++ b/packages/content/src/sources/__tests__/strapi-source.test.ts @@ -1,5 +1,5 @@ import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { DataStore } from "../../utils/data-store.js"; @@ -26,8 +26,27 @@ function createFetchContext() { }; } -describe("strapiSource", () => { - it("should fail with unsupported version", async () => { +const majorNodeVersion = Number.parseInt(process.versions.node.split(".")[0]!); + +describe.runIf(majorNodeVersion < 20)("strapiSource - unsupported", () => { + it("should fail with unsupported node version", async () => { + await expect(() => + strapiSource({ + id: "test-strapi", + baseUrl: "http://localhost:1337", + identifier: "test@example.com", + password: "password", + version: "4", + queries: ["test-content"], + }), + ).rejects.toThrowError( + `Unsupported node version ${process.versions.node}. Strapi source requires node >= 20.`, + ); + }); +}); + +describe.runIf(majorNodeVersion >= 20)("strapiSource", () => { + it("should fail with unsupported strapi API version", async () => { await expect(() => strapiSource({ id: "test-strapi", @@ -38,7 +57,22 @@ describe("strapiSource", () => { version: "5", queries: ["test-content"], }), - ).rejects.toThrow(); + ).rejects.toThrowErrorMatchingInlineSnapshot(` + [ZodError: [ + { + "received": "5", + "code": "invalid_enum_value", + "options": [ + "3", + "4" + ], + "path": [ + "version" + ], + "message": "Invalid enum value. Expected '3' | '4', received '5'" + } + ]] + `); }); describe("Strapi v4", () => { diff --git a/packages/content/src/sources/index.ts b/packages/content/src/sources/index.ts index c879c0fc..c8f568ea 100644 --- a/packages/content/src/sources/index.ts +++ b/packages/content/src/sources/index.ts @@ -1,6 +1,6 @@ -export { default as jsonSource } from "./json-source.js"; export { default as airtableSource } from "./airtable-source.js"; export { default as contentfulSource } from "./contentful-source.js"; +export { default as jsonSource } from "./json-source.js"; export { default as sanitySource } from "./sanity-source.js"; -export { default as strapiSource } from "./strapi-source.js"; export * from "./source.js"; +export { default as strapiSource } from "./strapi-source.js"; diff --git a/packages/content/src/sources/json-source.ts b/packages/content/src/sources/json-source.ts index 4c3aece5..ef7ae2ca 100644 --- a/packages/content/src/sources/json-source.ts +++ b/packages/content/src/sources/json-source.ts @@ -1,6 +1,5 @@ import chalk from "chalk"; import ky from "ky"; -import { okAsync } from "neverthrow"; import { z } from "zod"; import { defineSource } from "./source.js"; diff --git a/packages/content/src/sources/strapi-source.ts b/packages/content/src/sources/strapi-source.ts index 97ec493a..6a44b85b 100644 --- a/packages/content/src/sources/strapi-source.ts +++ b/packages/content/src/sources/strapi-source.ts @@ -101,11 +101,11 @@ class StrapiVersionUtils { this.logger = logger; } - buildUrl(query: StrapiObjectQuery, pagination?: StrapiPagination): string { + buildUrl(_query: StrapiObjectQuery, _pagination?: StrapiPagination): string { throw new Error("Not implemented"); } - hasPaginationParams(query: StrapiObjectQuery): boolean { + hasPaginationParams(_query: StrapiObjectQuery): boolean { throw new Error("Not implemented"); } @@ -117,7 +117,7 @@ class StrapiVersionUtils { return result; } - canFetchMore(result: unknown): boolean { + canFetchMore(_result: unknown): boolean { throw new Error("Not implemented"); } @@ -235,6 +235,14 @@ async function getToken(assembledOptions: StrapiSourceSchemaOutput) { } export default async function strapiSource(options: z.input) { + const majorNodeVersion = Number.parseInt(process.versions.node.split(".")?.[0] ?? "0"); + + if (majorNodeVersion < 20) { + throw new Error( + `Unsupported node version ${process.versions.node}. Strapi source requires node >= 20.`, + ); + } + const assembledOptions = strapiSourceSchema.parse(options); if (assembledOptions.version !== "4" && assembledOptions.version !== "3") { diff --git a/packages/content/src/utils/__tests__/data-store.test.ts b/packages/content/src/utils/__tests__/data-store.test.ts index 5df9c0e7..785a9aff 100644 --- a/packages/content/src/utils/__tests__/data-store.test.ts +++ b/packages/content/src/utils/__tests__/data-store.test.ts @@ -1,4 +1,4 @@ -import path from "node:path/posix"; +import path from "node:path"; import { vol } from "memfs"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { DataStore } from "../data-store.js"; @@ -27,7 +27,7 @@ describe("SingleDocument", () => { expect(docResult).toBeOk(); const fileContent = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", "test-doc.json"), + path.resolve(TEST_DIR, "test-namespace", "test-doc.json"), "utf-8", ); expect(JSON.parse(fileContent.toString())).toEqual({ content: "test content" }); @@ -51,13 +51,13 @@ describe("SingleDocument", () => { })); const originalContent = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", "test-doc.original.json"), + path.resolve(TEST_DIR, "test-namespace", "test-doc.original.json"), "utf-8", ); expect(JSON.parse(originalContent.toString())).toEqual({ content: "original content" }); const modifiedContent = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", "test-doc.json"), + path.resolve(TEST_DIR, "test-namespace", "test-doc.json"), "utf-8", ); expect(JSON.parse(modifiedContent.toString())).toEqual({ content: "modified content" }); @@ -80,7 +80,7 @@ describe("SingleDocument", () => { ); const fileContent = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", "test-doc.json"), + path.resolve(TEST_DIR, "test-namespace", "test-doc.json"), "utf-8", ); expect(JSON.parse(fileContent.toString())).toEqual({ @@ -101,13 +101,13 @@ describe("SingleDocument", () => { ); const fileContent = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", "test-doc.json"), + path.resolve(TEST_DIR, "test-namespace", "test-doc.json"), "utf-8", ); expect(JSON.parse(fileContent.toString())).toMatchObject({ content: "test content A" }); const extensionFileContent = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", "test-doc.extension"), + path.resolve(TEST_DIR, "test-namespace", "test-doc.extension"), "utf-8", ); expect(JSON.parse(extensionFileContent.toString())).toMatchObject({ @@ -115,7 +115,7 @@ describe("SingleDocument", () => { }); const extensionExtensionFileContent = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", "test-doc.extension.extension"), + path.resolve(TEST_DIR, "test-namespace", "test-doc.extension.extension"), "utf-8", ); expect(JSON.parse(extensionExtensionFileContent.toString())).toMatchObject({ @@ -148,7 +148,7 @@ describe("BatchDocument", () => { { id: 3, content: "third" }, ]; - const doc = await namespace.insert( + const _doc = await namespace.insert( "test-doc", (async function* () { for (const item of items) { @@ -161,7 +161,7 @@ describe("BatchDocument", () => { for (let i = 0; i < items.length; i++) { const filename = `test-doc-${i.toString().padStart(2, "0")}.json`; const content = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", filename), + path.resolve(TEST_DIR, "test-namespace", filename), "utf-8", ); expect(JSON.parse(content.toString())).toEqual(items[i]); @@ -192,7 +192,7 @@ describe("BatchDocument", () => { for (let i = 0; i < items.length; i++) { const filename = `test-doc-${i.toString().padStart(2, "0")}.json`; const content = await vol.readFileSync( - path.join(TEST_DIR, "test-namespace", filename), + path.resolve(TEST_DIR, "test-namespace", filename), "utf-8", ); expect(JSON.parse(content.toString())).toEqual({ @@ -219,7 +219,7 @@ describe("DataStore", () => { const result = await store.createNamespace("test-namespace"); expect(result).toBeOk(); - const exists = await vol.existsSync(path.join(TEST_DIR, "test-namespace")); + const exists = await vol.existsSync(path.resolve(TEST_DIR, "test-namespace")); expect(exists).toBe(true); }); diff --git a/packages/content/src/utils/__tests__/fetch-paginated.test.ts b/packages/content/src/utils/__tests__/fetch-paginated.test.ts index cd2cfb9c..aa62039c 100644 --- a/packages/content/src/utils/__tests__/fetch-paginated.test.ts +++ b/packages/content/src/utils/__tests__/fetch-paginated.test.ts @@ -1,5 +1,5 @@ import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { fetchPaginated } from "../fetch-paginated.js"; diff --git a/packages/content/src/utils/__tests__/safe-ky.test.ts b/packages/content/src/utils/__tests__/safe-ky.test.ts index 11745aab..6d4df4b6 100644 --- a/packages/content/src/utils/__tests__/safe-ky.test.ts +++ b/packages/content/src/utils/__tests__/safe-ky.test.ts @@ -1,7 +1,7 @@ -import { http, HttpResponse } from "msw"; +import { HttpResponse, http } from "msw"; import { setupServer } from "msw/node"; import { ok } from "neverthrow"; -import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { SafeKyFetchError, SafeKyParseError, safeKy } from "../safe-ky.js"; const server = setupServer(); diff --git a/packages/content/src/utils/content-transform-utils.ts b/packages/content/src/utils/content-transform-utils.ts index ff0e219d..2cd695b2 100644 --- a/packages/content/src/utils/content-transform-utils.ts +++ b/packages/content/src/utils/content-transform-utils.ts @@ -1,6 +1,6 @@ import type { Logger } from "@bluecadet/launchpad-utils"; import chalk from "chalk"; -import { type Result, ok } from "neverthrow"; +import { ok, type Result } from "neverthrow"; import type { DataKeys, DataStore, Document } from "./data-store.js"; /** diff --git a/packages/content/src/utils/data-store.ts b/packages/content/src/utils/data-store.ts index d399cb37..6c68b6a0 100644 --- a/packages/content/src/utils/data-store.ts +++ b/packages/content/src/utils/data-store.ts @@ -1,7 +1,7 @@ import fs from "node:fs/promises"; -import path, { resolve } from "node:path"; +import path from "node:path"; import { JSONPath } from "jsonpath-plus"; -import { Result, ResultAsync, err, errAsync, ok, okAsync } from "neverthrow"; +import { err, errAsync, ok, okAsync, Result, ResultAsync } from "neverthrow"; import { z } from "zod"; import { ensureDir } from "./file-utils.js"; @@ -135,7 +135,7 @@ class SingleDocument extends Document { constructor(directory: string, id: string) { super(id); const filename = id.includes(".") ? id : `${id}.json`; - this.#path = path.join(directory, filename); + this.#path = path.resolve(directory, filename); } async initialize(data: T | Promise) { @@ -340,7 +340,7 @@ class Namespace { constructor(parentDirectory: string, id: string) { this.#id = id; - this.#directory = path.join(parentDirectory, id); + this.#directory = path.resolve(parentDirectory, id); } get id() { diff --git a/packages/content/src/utils/fetch-logger.ts b/packages/content/src/utils/fetch-logger.ts index 151a41a4..f51064c2 100644 --- a/packages/content/src/utils/fetch-logger.ts +++ b/packages/content/src/utils/fetch-logger.ts @@ -52,7 +52,7 @@ export class FetchLogger extends FixedConsoleLogger { [NO_TTY]: true, }, ); - } catch (e) { + } catch (_e) { const endTime = Date.now(); const duration = endTime - startTime; this.#fetches.get(sourceId)?.set(documentId, { state: "rejected", duration }); diff --git a/packages/content/src/utils/file-utils.ts b/packages/content/src/utils/file-utils.ts index 64ae4374..7ab85019 100644 --- a/packages/content/src/utils/file-utils.ts +++ b/packages/content/src/utils/file-utils.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import path from "node:path"; import { glob } from "glob"; -import { ResultAsync, okAsync } from "neverthrow"; +import { okAsync, ResultAsync } from "neverthrow"; export class FileUtilsError extends Error { constructor(...args: ConstructorParameters) { @@ -43,9 +43,9 @@ export function removeFilesFromDir( exclude: string[] = [], ): ResultAsync { // Glob expects posix paths - const globPath = path.join(dirPath, "**/*").replaceAll(path.sep, path.posix.sep); + const globPath = path.resolve(dirPath, "**/*").replaceAll(path.sep, path.posix.sep); const excludePaths = exclude.map((pattern) => - path.join(dirPath, pattern).replaceAll(path.sep, path.posix.sep), + path.resolve(dirPath, pattern).replaceAll(path.sep, path.posix.sep), ); return ResultAsync.fromPromise( @@ -200,7 +200,7 @@ export function copyDir( ) .andThen((entries) => ResultAsync.combine( - entries.map((entry) => copy(path.join(src, entry), path.join(dest, entry), options)), + entries.map((entry) => copy(path.resolve(src, entry), path.resolve(dest, entry), options)), ), ) .map(() => undefined); diff --git a/packages/content/src/utils/result-async-queue.ts b/packages/content/src/utils/result-async-queue.ts index bc58fc5a..477e8217 100644 --- a/packages/content/src/utils/result-async-queue.ts +++ b/packages/content/src/utils/result-async-queue.ts @@ -1,6 +1,6 @@ import type { Logger } from "@bluecadet/launchpad-utils"; import chalk from "chalk"; -import { Result, ResultAsync, err, ok } from "neverthrow"; +import { ok, Result, ResultAsync } from "neverthrow"; import PQueue from "p-queue"; type ResultAsyncTaskOptions = { @@ -39,10 +39,10 @@ export default class ResultAsyncQueue { tasks: Array>, options: { logger: Logger; abortOnError?: boolean }, ): ResultAsync, Array> { - let wrappedTasks = tasks; + let _wrappedTasks = tasks; if (options.abortOnError) { - wrappedTasks = tasks.map((task) => { + _wrappedTasks = tasks.map((task) => { return (...args) => task(...args).mapErr((e) => { this.queue.clear(); diff --git a/packages/content/src/utils/safe-ky.ts b/packages/content/src/utils/safe-ky.ts index 7b454625..304d7905 100644 --- a/packages/content/src/utils/safe-ky.ts +++ b/packages/content/src/utils/safe-ky.ts @@ -1,5 +1,5 @@ -import ky, { type Input, type Options, type KyResponse, type ResponsePromise } from "ky"; -import { Err, Ok, ResultAsync, err, ok } from "neverthrow"; +import ky, { type Input, type KyResponse, type Options, type ResponsePromise } from "ky"; +import { Err, Ok, ResultAsync } from "neverthrow"; export class SafeKyFetchError extends Error { constructor(...args: ConstructorParameters) { diff --git a/packages/launchpad/package.json b/packages/launchpad/package.json index 7ee08c65..df9f39f3 100644 --- a/packages/launchpad/package.json +++ b/packages/launchpad/package.json @@ -3,8 +3,7 @@ "version": "2.0.12", "description": "Suite of tools to manage media installations", "engines": { - "npm": ">=8.5.1", - "node": ">=17.5.0" + "node": ">=18" }, "type": "module", "scripts": { diff --git a/packages/monitor/README.md b/packages/monitor/README.md index dbfad4d4..7135fa83 100644 --- a/packages/monitor/README.md +++ b/packages/monitor/README.md @@ -1,41 +1,41 @@ -# @bluecadet/launchpad-monitor - -Process monitoring and management for interactive installations. Part of the Launchpad suite of tools. - -## Documentation - -For complete documentation, examples, and API reference, visit: - - -## Features - -- Process management via PM2 -- Plugin system for custom monitoring behavior -- Process lifecycle hooks -- Built-in logging and error handling -- Window management capabilities - -## Installation - -```bash -npm install @bluecadet/launchpad-monitor -``` - -## Basic Usage - -```typescript -import { Monitor } from '@bluecadet/launchpad-monitor'; - -const monitor = new Monitor({ - apps: [{ - name: 'my-app', - script: 'app.js' - }] -}); - -await monitor.start(); -``` - -## License - -MIT © Bluecadet +# @bluecadet/launchpad-monitor + +Process monitoring and management for interactive installations. Part of the Launchpad suite of tools. + +## Documentation + +For complete documentation, examples, and API reference, visit: + + +## Features + +- Process management via PM2 +- Plugin system for custom monitoring behavior +- Process lifecycle hooks +- Built-in logging and error handling +- Window management capabilities + +## Installation + +```bash +npm install @bluecadet/launchpad-monitor +``` + +## Basic Usage + +```typescript +import { Monitor } from '@bluecadet/launchpad-monitor'; + +const monitor = new Monitor({ + apps: [{ + name: 'my-app', + script: 'app.js' + }] +}); + +await monitor.start(); +``` + +## License + +MIT © Bluecadet diff --git a/packages/monitor/package.json b/packages/monitor/package.json index d581970f..778cca8a 100644 --- a/packages/monitor/package.json +++ b/packages/monitor/package.json @@ -17,6 +17,9 @@ "scripts": { "test": "vitest" }, + "engines": { + "node": ">=18" + }, "repository": { "type": "git", "url": "git+https://github.com/bluecadet/launchpad.git" diff --git a/packages/monitor/src/__tests__/launchpad-monitor.test.ts b/packages/monitor/src/__tests__/launchpad-monitor.test.ts index 2ea79e93..68e465bd 100644 --- a/packages/monitor/src/__tests__/launchpad-monitor.test.ts +++ b/packages/monitor/src/__tests__/launchpad-monitor.test.ts @@ -4,6 +4,7 @@ import { describe, expect, it, vi } from "vitest"; import { AppManager } from "../core/app-manager.js"; import type { MonitorPlugin } from "../core/monitor-plugin-driver.js"; import LaunchpadMonitor from "../launchpad-monitor.js"; +import type { MonitorConfig } from "../monitor-config.js"; // Mock process.exit to prevent tests from actually exiting // @ts-expect-error - mockImplementation returns undefined @@ -30,7 +31,7 @@ const mockPlugin = { } as MonitorPlugin; function createTestMonitor( - config = { + config: MonitorConfig = { apps: [ { pm2: { @@ -41,9 +42,10 @@ function createTestMonitor( ], plugins: [mockPlugin], }, + cwd?: string, ) { const rootLogger = createMockLogger(); - const monitor = new LaunchpadMonitor(config, rootLogger); + const monitor = new LaunchpadMonitor(config, rootLogger, cwd); const monitorLogger = rootLogger.children.get("monitor"); if (!monitorLogger) { @@ -54,7 +56,7 @@ function createTestMonitor( monitor, rootLogger, monitorLogger, - plugin: config.plugins[0] as MonitorPlugin, + plugin: config.plugins![0] as MonitorPlugin, }; } @@ -247,4 +249,34 @@ describe("LaunchpadMonitor", () => { }); }); }); + + describe("cwd handling", () => { + it("should use provided cwd for app paths", () => { + const cwd = "/test/cwd"; + const { monitor } = createTestMonitor( + { + apps: [{ pm2: { name: "test-app", script: "test.js", cwd: "app/cwd" } }], + plugins: [mockPlugin], + }, + cwd, + ); + + expect(monitor._cwd).toBe(cwd); + expect(monitor._appManager.getAppOptions("test-app")._unsafeUnwrap().pm2.cwd).toMatchPath( + "/test/cwd/app/cwd", + ); + }); + + it("should default to process.cwd() if no cwd is provided", () => { + const { monitor } = createTestMonitor({ + apps: [{ pm2: { name: "test-app", script: "test.js", cwd: "app/cwd" } }], + plugins: [mockPlugin], + }); + + expect(monitor._cwd).toBe(process.cwd()); + expect(monitor._appManager.getAppOptions("test-app")._unsafeUnwrap().pm2.cwd).toMatchPath( + "/app/cwd", + ); + }); + }); }); diff --git a/packages/monitor/src/core/__tests__/app-manager.test.ts b/packages/monitor/src/core/__tests__/app-manager.test.ts index 362fb909..be1839b9 100644 --- a/packages/monitor/src/core/__tests__/app-manager.test.ts +++ b/packages/monitor/src/core/__tests__/app-manager.test.ts @@ -1,11 +1,34 @@ +import path from "node:path"; import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; -import { ok, okAsync } from "neverthrow"; +import { okAsync } from "neverthrow"; import { describe, expect, it, vi } from "vitest"; import type { ResolvedMonitorConfig } from "../../monitor-config.js"; import { AppManager } from "../app-manager.js"; import { ProcessManager } from "../process-manager.js"; -function setupTestAppManager() { +function setupTestAppManager( + apps: ResolvedMonitorConfig["apps"] = [ + { + pm2: { + name: "test-app", + script: "test.js", + cwd: "app/cwd", + }, + windows: { + foreground: false, + minimize: false, + hide: false, + }, + logging: { + logToLaunchpadDir: true, + mode: "bus", + showStdout: true, + showStderr: true, + }, + }, + ], + cwd = process.cwd(), +) { const mockLogger = createMockLogger(); const processManager = new ProcessManager(mockLogger); @@ -26,25 +49,7 @@ function setupTestAppManager() { vi.spyOn(processManager, "deleteAllProcesses"); const mockConfig = { - apps: [ - { - pm2: { - name: "test-app", - script: "test.js", - }, - windows: { - foreground: false, - minimize: false, - hide: false, - }, - logging: { - logToLaunchpadDir: true, - mode: "bus", - showStdout: true, - showStderr: true, - }, - }, - ], + apps, windowsApi: { debounceDelay: 3000, }, @@ -53,7 +58,7 @@ function setupTestAppManager() { shutdownOnExit: true, } as ResolvedMonitorConfig; - const appManager = new AppManager(mockLogger, processManager, mockConfig); + const appManager = new AppManager(mockLogger, processManager, mockConfig, cwd); return { appManager, @@ -148,7 +153,18 @@ describe("AppManager", () => { const { appManager, mockConfig } = setupTestAppManager(); const result = await appManager.getAppOptions("test-app"); expect(result.isOk()).toBe(true); - expect(result._unsafeUnwrap()).toEqual(mockConfig.apps[0]); + + const resolvedCWD = path.resolve(process.cwd(), mockConfig.apps[0]?.pm2.cwd!); + + const expectedOptions = { + ...mockConfig.apps[0], + pm2: { + ...mockConfig.apps[0]!.pm2, + cwd: resolvedCWD, + }, + }; + + expect(result._unsafeUnwrap()).toEqual(expectedOptions); }); it("should throw for invalid app name", async () => { @@ -160,4 +176,20 @@ describe("AppManager", () => { ); }); }); + + describe("cwd handling", () => { + it("should resolve pm2 cwd relative to the manager's cwd", () => { + const { appManager } = setupTestAppManager(undefined, "/passed/cwd"); + const appOptions = appManager.getAppOptions("test-app")._unsafeUnwrap(); + expect(appOptions.pm2.cwd).toBeDefined(); + expect(appOptions.pm2.cwd).toEqual(path.resolve("/passed/cwd", "app/cwd")); + }); + + it("should use default cwd if not specified in config", () => { + const { appManager } = setupTestAppManager(); + const appOptions = appManager.getAppOptions("test-app")._unsafeUnwrap(); + expect(appOptions.pm2.cwd).toBeDefined(); + expect(appOptions.pm2.cwd).toEqual(path.resolve(process.cwd(), "app/cwd")); + }); + }); }); diff --git a/packages/monitor/src/core/__tests__/bus-manager.test.ts b/packages/monitor/src/core/__tests__/bus-manager.test.ts index e4185444..e04f8e89 100644 --- a/packages/monitor/src/core/__tests__/bus-manager.test.ts +++ b/packages/monitor/src/core/__tests__/bus-manager.test.ts @@ -1,6 +1,6 @@ import { createMockLogger } from "@bluecadet/launchpad-testing/test-utils.ts"; import pm2 from "pm2"; -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { BusManager } from "../bus-manager.js"; import { createMockSubEmitterSocket } from "./core.test-utils.js"; @@ -24,7 +24,7 @@ describe("BusManager", () => { describe("connect", () => { it("should connect to PM2 bus successfully", async () => { - const { busManager, mockSubEmitterSocket, emit } = buildTestBusManager(); + const { busManager, mockSubEmitterSocket } = buildTestBusManager(); const result = await busManager.connect(); @@ -48,7 +48,7 @@ describe("BusManager", () => { describe("disconnect", () => { it("should disconnect from PM2 bus successfully", async () => { - const { busManager, mockSubEmitterSocket, emit } = buildTestBusManager(); + const { busManager, mockSubEmitterSocket } = buildTestBusManager(); await busManager.connect(); const result = await busManager.disconnect(); diff --git a/packages/monitor/src/core/__tests__/core.test-utils.ts b/packages/monitor/src/core/__tests__/core.test-utils.ts index ba7de892..b86dcc06 100644 --- a/packages/monitor/src/core/__tests__/core.test-utils.ts +++ b/packages/monitor/src/core/__tests__/core.test-utils.ts @@ -2,7 +2,7 @@ import type { SubEmitterSocket } from "axon"; import { vi } from "vitest"; export function createMockSubEmitterSocket() { - // biome-ignore lint/suspicious/noExplicitAny: + // biome-ignore lint/suspicious/noExplicitAny: test utility const listeners: Map void)[]> = new Map(); const mockSubEmitterSocket: SubEmitterSocket = { diff --git a/packages/monitor/src/core/__tests__/monitor-plugin-driver.test.ts b/packages/monitor/src/core/__tests__/monitor-plugin-driver.test.ts index 8097ac2d..ecf28e04 100644 --- a/packages/monitor/src/core/__tests__/monitor-plugin-driver.test.ts +++ b/packages/monitor/src/core/__tests__/monitor-plugin-driver.test.ts @@ -38,7 +38,7 @@ describe("MonitorPluginDriver", () => { }, }; - const basePluginDriver = new PluginDriver(mockLogger, [mockPlugin]); + const basePluginDriver = new PluginDriver({ logger: mockLogger }, [mockPlugin]); monitorPluginDriver = new MonitorPluginDriver(basePluginDriver, { monitor: mockMonitor }); }); diff --git a/packages/monitor/src/core/__tests__/process-manager.test.ts b/packages/monitor/src/core/__tests__/process-manager.test.ts index a61f536b..98a48985 100644 --- a/packages/monitor/src/core/__tests__/process-manager.test.ts +++ b/packages/monitor/src/core/__tests__/process-manager.test.ts @@ -15,12 +15,12 @@ describe("ProcessManager", () => { processManager = new ProcessManager(mockLogger); // Setup PM2 mock implementations - pm2.connect = vi.fn().mockImplementation((force, cb) => cb(null)); + pm2.connect = vi.fn().mockImplementation((_force, cb) => cb(null)); pm2.disconnect = vi.fn(); pm2.list = vi.fn().mockImplementation((cb) => cb(null, [])); - pm2.start = vi.fn().mockImplementation((options, cb) => cb(null, {})); - pm2.stop = vi.fn().mockImplementation((name, cb) => cb(null, {})); - pm2.delete = vi.fn().mockImplementation((name, cb) => cb(null, {})); + pm2.start = vi.fn().mockImplementation((_options, cb) => cb(null, {})); + pm2.stop = vi.fn().mockImplementation((_name, cb) => cb(null, {})); + pm2.delete = vi.fn().mockImplementation((_name, cb) => cb(null, {})); // @ts-ignore - this is a private api, so throws a type error pm2.Client = { // eslint-disable-next-line n/no-callback-literal diff --git a/packages/monitor/src/core/app-manager.ts b/packages/monitor/src/core/app-manager.ts index ee44fad7..ec14ae2c 100644 --- a/packages/monitor/src/core/app-manager.ts +++ b/packages/monitor/src/core/app-manager.ts @@ -1,5 +1,6 @@ +import path from "node:path"; import type { Logger } from "@bluecadet/launchpad-utils"; -import { type Result, ResultAsync, err, errAsync, ok, okAsync } from "neverthrow"; +import { err, errAsync, ok, okAsync, type Result, ResultAsync } from "neverthrow"; import type pm2 from "pm2"; import type { ResolvedAppConfig, ResolvedMonitorConfig } from "../monitor-config.js"; import { debounceResultAsync } from "../utils/debounce-results.js"; @@ -10,11 +11,18 @@ export class AppManager { #logger: Logger; #processManager: ProcessManager; #config: ResolvedMonitorConfig; - - constructor(logger: Logger, processManager: ProcessManager, config: ResolvedMonitorConfig) { + #cwd: string; + + constructor( + logger: Logger, + processManager: ProcessManager, + config: ResolvedMonitorConfig, + cwd: string = process.cwd(), + ) { this.#logger = logger; this.#processManager = processManager; this.#config = config; + this.#cwd = cwd; this.applyWindowSettings = debounceResultAsync( this.applyWindowSettings.bind(this), @@ -80,7 +88,18 @@ export class AppManager { if (!options) { return err(new Error(`No app found with the name '${appName}'`)); } - return ok(options); + return ok(this.#updateConfigCWD(options)); + } + + #updateConfigCWD(appConfig: ResolvedAppConfig): ResolvedAppConfig { + return { + ...appConfig, + pm2: { + ...appConfig.pm2, + // Ensure the cwd is resolved relative to the manager's cwd + cwd: appConfig.pm2.cwd ? path.resolve(this.#cwd, appConfig.pm2.cwd) : undefined, + }, + }; } applyWindowSettings(appNames: string[] = []): ResultAsync { diff --git a/packages/monitor/src/core/bus-manager.ts b/packages/monitor/src/core/bus-manager.ts index 07823b91..4241ce65 100644 --- a/packages/monitor/src/core/bus-manager.ts +++ b/packages/monitor/src/core/bus-manager.ts @@ -1,6 +1,6 @@ -import { LogManager, type Logger } from "@bluecadet/launchpad-utils"; +import { type Logger, LogManager } from "@bluecadet/launchpad-utils"; import type { SubEmitterSocket } from "axon"; -import { type Result, ResultAsync, err, ok } from "neverthrow"; +import { err, ok, type Result, ResultAsync } from "neverthrow"; import pm2 from "pm2"; import { Tail } from "tail"; import { LogModes, type ResolvedAppConfig } from "../monitor-config.js"; @@ -202,7 +202,7 @@ export class BusManager { appLogger.info(data); } - #handleTailError(appName: string, data: string, isTailError = false) { + #handleTailError(appName: string, data: string, _isTailError = false) { const appLogger = LogManager.getLogger(appName, this.#logger); appLogger.error(data); } diff --git a/packages/monitor/src/core/monitor-plugin-driver.ts b/packages/monitor/src/core/monitor-plugin-driver.ts index ebabc2f2..e5b6e258 100644 --- a/packages/monitor/src/core/monitor-plugin-driver.ts +++ b/packages/monitor/src/core/monitor-plugin-driver.ts @@ -1,10 +1,10 @@ import { type BaseHookContext, + createPluginValidator, HookContextProvider, type Plugin, type PluginDriver, } from "@bluecadet/launchpad-utils"; -import { createPluginValidator } from "@bluecadet/launchpad-utils"; import type pm2 from "pm2"; import type LaunchpadMonitor from "../launchpad-monitor.js"; diff --git a/packages/monitor/src/core/process-manager.ts b/packages/monitor/src/core/process-manager.ts index 2d2bc538..87dfb16a 100644 --- a/packages/monitor/src/core/process-manager.ts +++ b/packages/monitor/src/core/process-manager.ts @@ -1,5 +1,5 @@ import type { Logger } from "@bluecadet/launchpad-utils"; -import { Result, ResultAsync, err, ok, okAsync } from "neverthrow"; +import { err, ok, okAsync, Result, ResultAsync } from "neverthrow"; import pm2 from "pm2"; export class ProcessManager { diff --git a/packages/monitor/src/index.ts b/packages/monitor/src/index.ts index b605a733..f87d895c 100755 --- a/packages/monitor/src/index.ts +++ b/packages/monitor/src/index.ts @@ -1,8 +1,8 @@ import LaunchpadMonitor from "./launchpad-monitor.js"; +export * from "./core/monitor-plugin-driver.js"; // export * from './windows-api.js'; // Includes optional dependencies, so not exported here export * from "./launchpad-monitor.js"; export * from "./monitor-config.js"; -export * from "./core/monitor-plugin-driver.js"; export default LaunchpadMonitor; diff --git a/packages/monitor/src/launchpad-monitor.ts b/packages/monitor/src/launchpad-monitor.ts index 7f4d3141..1e0cfe8f 100644 --- a/packages/monitor/src/launchpad-monitor.ts +++ b/packages/monitor/src/launchpad-monitor.ts @@ -1,8 +1,7 @@ -import { LogManager, type Logger, onExit } from "@bluecadet/launchpad-utils"; -import { PluginDriver } from "@bluecadet/launchpad-utils"; +import { type Logger, LogManager, onExit, PluginDriver } from "@bluecadet/launchpad-utils"; import autoBind from "auto-bind"; import { spawn } from "cross-spawn"; -import { ResultAsync, okAsync } from "neverthrow"; +import { okAsync, ResultAsync } from "neverthrow"; import type pm2 from "pm2"; import { AppManager } from "./core/app-manager.js"; import { BusManager } from "./core/bus-manager.js"; @@ -10,8 +9,8 @@ import { MonitorPluginDriver } from "./core/monitor-plugin-driver.js"; import { ProcessManager } from "./core/process-manager.js"; import { type MonitorConfig, - type ResolvedMonitorConfig, monitorConfigSchema, + type ResolvedMonitorConfig, } from "./monitor-config.js"; class LaunchpadMonitor { @@ -22,15 +21,17 @@ class LaunchpadMonitor { _appManager: AppManager; _pluginDriver: MonitorPluginDriver; _isShuttingDown = false; + _cwd: string; - constructor(config: MonitorConfig, parentLogger: Logger) { + constructor(config: MonitorConfig, parentLogger: Logger, cwd = process.cwd()) { autoBind(this); this._logger = LogManager.getLogger("monitor", parentLogger); this._config = monitorConfigSchema.parse(config); + this._cwd = cwd; this._processManager = new ProcessManager(this._logger); this._busManager = new BusManager(this._logger); - this._appManager = new AppManager(this._logger, this._processManager, this._config); + this._appManager = new AppManager(this._logger, this._processManager, this._config, cwd); for (const appConf of this._config.apps) { this._busManager.initAppLogging(appConf); @@ -42,7 +43,10 @@ class LaunchpadMonitor { }); } - const basePluginDriver = new PluginDriver(this._logger, this._config.plugins); + const basePluginDriver = new PluginDriver( + { logger: this._logger, cwd: this._cwd }, + this._config.plugins, + ); this._pluginDriver = new MonitorPluginDriver(basePluginDriver, { monitor: this }); } diff --git a/packages/monitor/src/utils/__tests__/debounce-results.test.ts b/packages/monitor/src/utils/__tests__/debounce-results.test.ts index b85e2977..6c16eded 100644 --- a/packages/monitor/src/utils/__tests__/debounce-results.test.ts +++ b/packages/monitor/src/utils/__tests__/debounce-results.test.ts @@ -1,4 +1,4 @@ -import { ResultAsync, err, ok } from "neverthrow"; +import { ResultAsync } from "neverthrow"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { debounceResultAsync } from "../debounce-results.js"; diff --git a/packages/scaffold/scripts/vendor/powerplan.psm1 b/packages/scaffold/scripts/vendor/powerplan.psm1 index e91d9741..686c9dd3 100644 --- a/packages/scaffold/scripts/vendor/powerplan.psm1 +++ b/packages/scaffold/scripts/vendor/powerplan.psm1 @@ -1,261 +1,261 @@ -# See https://github.com/torgro/PowerPlan - -function Get-Powerplan -{ -<# -.Synopsis - Get a Powerplan by name or all of them -.DESCRIPTION - This cmdlet queries the CIM class Win32_PowerPlan. See also Set-PowerPlan cmdlet -.EXAMPLE - Get-Powerplan - This command will output all powerplans: -Caption : -Description : Automatically balances performance with energy consumption on capable hardware. -ElementName : Balanced -InstanceID : Microsoft:PowerPlan\{381b4222-f694-41f0-9685-ff5bb260df2e} -IsActive : False -PSComputerName : -Caption : -Description : Favors performance, but may use more energy. -ElementName : High performance -InstanceID : Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c} -IsActive : True -PSComputerName : -Caption : -Description : Saves energy by reducing your computer’s performance where possible. -ElementName : Power saver -InstanceID : Microsoft:PowerPlan\{a1841308-3541-4fab-bc81-f71556f20b4a} -IsActive : False -PSComputerName : -.EXAMPLE - Get-Powerplan -PlanName high* - This command will output all plans that begins with high -Caption : -Description : Favors performance, but may use more energy. -ElementName : High performance -InstanceID : Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c} -IsActive : True -PSComputerName : -.EXAMPLE - Get-PowerPlan -PlanName high* -ComputerName "Server1","Server2" - Will output the powerplan with name like high for server1 and server2 - -.EXAMPLE - Get-PowerPlan -Active - Will output the active powerplan -.OUTPUTS - CimInstance -.NOTES - Powerplan and performance -.COMPONENT - Powerplan -.ROLE - Powerplan -.FUNCTIONALITY - This cmdlet queries the CIM class Win32_PowerPlan -#> -[cmdletbinding()] -[OutputType([CimInstance[]])] -Param( - [Parameter( - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true, - ValueFromRemainingArguments=$false - )] - [Alias("ElementName")] - [string]$PlanName = "*" - , - [Parameter( - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true, - ValueFromRemainingArguments=$false - )] - [string[]]$ComputerName, - [switch]$Active -) - - Begin - { - $f = $MyInvocation.InvocationName - Write-Verbose -Message "$f - START" - - $GetCimInstance = @{ - Namespace = "root\cimv2\power" - ClassName = "Win32_PowerPlan" - } - - if ($ComputerName) - { - $GetCimInstance.Add("ComputerName",$ComputerName) - } - - if ($Active) - { - $GetCimInstance.Add("Filter",'IsActive="True"') - } - } - - Process - { - if ($PlanName) - { - Get-CimInstance @GetCimInstance | Where-Object ElementName -Like "$PlanName" - } - else - { - Get-CimInstance @GetCimInstance - } - } - - End - { - Write-Verbose -Message "$f - END" - } -} - -function Set-PowerPlan -{ -<# -.Synopsis - Sets a Powerplan by name or by value provided from the pipeline -.DESCRIPTION - This cmdlet invokes the CIM-method Activate in class Win32_PowerPlan. See also Get-PowerPlan cmdlet -.EXAMPLE - Set-PowerPlan -PlanName high* - This will set the current powerplan to High for the current computer -.EXAMPLE - Get-Powerplan -PlanName "Power Saver" | Set-PowerPlan - Will set the powerplan to "Power Saver" for current computer -.EXAMPLE - Get-Powerplan -PlanName "Power Saver" -ComputerName "Server1","Server2" | Set-PowerPlan - This will set the current powerpla to "Power Saver" for the computers Server1 and Server2 -.EXAMPLE - Set-PowerPlan -PlanName "Power Saver" -ComputerName "Server1","Server2" - This will set the current powerpla to "Power Saver" for the computers Server1 and Server2 -.NOTES - Powerplan and performance -.COMPONENT - Powerplan -.ROLE - Powerplan -.FUNCTIONALITY - This cmdlet invokes CIM-methods in the class Win32_PowerPlan -#> -[cmdletbinding( - SupportsShouldProcess=$true, - ConfirmImpact='Medium' -)] -Param( - [Parameter( - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true, - ValueFromRemainingArguments=$false - )] - [Alias("ElementName")] - [string]$PlanName = "*" - , - [Parameter( - ValueFromPipeline=$true, - ValueFromPipelineByPropertyName=$true, - ValueFromRemainingArguments=$false - )] - [Alias("PSComputerName")] - [string[]]$ComputerName -) - - Begin - { - $f = $MyInvocation.InvocationName - Write-Verbose -Message "$f - START" - $GetCimInstance = @{ - Namespace = "root\cimv2\power" - ClassName = "Win32_PowerPlan" - } - - if ($ComputerName) - { - $GetCimInstance.Add("ComputerName",$ComputerName) - } - - $InvokeCimMethod = @{ - MethodName = "Activate" - } - - if ($WhatIfPreference) - { - $InvokeCimMethod.Add("WhatIf",$true) - } - } - - Process - { - Write-Verbose -Message "$f - ElementName=$PlanName" - $CimObjectPowerPlan = Get-CimInstance @GetCimInstance | Where-Object ElementName -like "$PlanName" - - foreach ($Instance in $CimObjectPowerPlan) - { - if ($pscmdlet.ShouldProcess($Instance)) - { - $null = Invoke-CimMethod -InputObject $Instance @InvokeCimMethod - } - } - if (-not $CimObjectPowerPlan) - { - Write-Warning -Message "Unable to find powerplan $PlanName" - } - } - - End - { - Write-Verbose -Message "$f - END" - } - -} - -<# - DSC Resource - Manages the power plan selection for a computer. -#> -[DscResource()] -class PowerPlan -{ - - <# - This property is the name of an available power plan. - #> - [DscProperty(Key)] - [string]$Name - - <# - Sets the specified power plan as active. - #> - [void] Set() - { - Set-PowerPlan $this.Name - } - - <# - Tests if the machine is using the specified power plan. - #> - [bool] Test() - { - if ((Get-PowerPlan -Active).ElementName -eq $this.Name) - { - return $true - } - else - { - return $false - } - } - - <# - Returns an instance of this class to identify the active plan. - #> - [PowerPlan] Get() - { - $this.Name = (Get-PowerPlan -Active).ElementName - return $this - } +# See https://github.com/torgro/PowerPlan + +function Get-Powerplan +{ +<# +.Synopsis + Get a Powerplan by name or all of them +.DESCRIPTION + This cmdlet queries the CIM class Win32_PowerPlan. See also Set-PowerPlan cmdlet +.EXAMPLE + Get-Powerplan + This command will output all powerplans: +Caption : +Description : Automatically balances performance with energy consumption on capable hardware. +ElementName : Balanced +InstanceID : Microsoft:PowerPlan\{381b4222-f694-41f0-9685-ff5bb260df2e} +IsActive : False +PSComputerName : +Caption : +Description : Favors performance, but may use more energy. +ElementName : High performance +InstanceID : Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c} +IsActive : True +PSComputerName : +Caption : +Description : Saves energy by reducing your computer’s performance where possible. +ElementName : Power saver +InstanceID : Microsoft:PowerPlan\{a1841308-3541-4fab-bc81-f71556f20b4a} +IsActive : False +PSComputerName : +.EXAMPLE + Get-Powerplan -PlanName high* + This command will output all plans that begins with high +Caption : +Description : Favors performance, but may use more energy. +ElementName : High performance +InstanceID : Microsoft:PowerPlan\{8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c} +IsActive : True +PSComputerName : +.EXAMPLE + Get-PowerPlan -PlanName high* -ComputerName "Server1","Server2" + Will output the powerplan with name like high for server1 and server2 + +.EXAMPLE + Get-PowerPlan -Active + Will output the active powerplan +.OUTPUTS + CimInstance +.NOTES + Powerplan and performance +.COMPONENT + Powerplan +.ROLE + Powerplan +.FUNCTIONALITY + This cmdlet queries the CIM class Win32_PowerPlan +#> +[cmdletbinding()] +[OutputType([CimInstance[]])] +Param( + [Parameter( + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true, + ValueFromRemainingArguments=$false + )] + [Alias("ElementName")] + [string]$PlanName = "*" + , + [Parameter( + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true, + ValueFromRemainingArguments=$false + )] + [string[]]$ComputerName, + [switch]$Active +) + + Begin + { + $f = $MyInvocation.InvocationName + Write-Verbose -Message "$f - START" + + $GetCimInstance = @{ + Namespace = "root\cimv2\power" + ClassName = "Win32_PowerPlan" + } + + if ($ComputerName) + { + $GetCimInstance.Add("ComputerName",$ComputerName) + } + + if ($Active) + { + $GetCimInstance.Add("Filter",'IsActive="True"') + } + } + + Process + { + if ($PlanName) + { + Get-CimInstance @GetCimInstance | Where-Object ElementName -Like "$PlanName" + } + else + { + Get-CimInstance @GetCimInstance + } + } + + End + { + Write-Verbose -Message "$f - END" + } +} + +function Set-PowerPlan +{ +<# +.Synopsis + Sets a Powerplan by name or by value provided from the pipeline +.DESCRIPTION + This cmdlet invokes the CIM-method Activate in class Win32_PowerPlan. See also Get-PowerPlan cmdlet +.EXAMPLE + Set-PowerPlan -PlanName high* + This will set the current powerplan to High for the current computer +.EXAMPLE + Get-Powerplan -PlanName "Power Saver" | Set-PowerPlan + Will set the powerplan to "Power Saver" for current computer +.EXAMPLE + Get-Powerplan -PlanName "Power Saver" -ComputerName "Server1","Server2" | Set-PowerPlan + This will set the current powerpla to "Power Saver" for the computers Server1 and Server2 +.EXAMPLE + Set-PowerPlan -PlanName "Power Saver" -ComputerName "Server1","Server2" + This will set the current powerpla to "Power Saver" for the computers Server1 and Server2 +.NOTES + Powerplan and performance +.COMPONENT + Powerplan +.ROLE + Powerplan +.FUNCTIONALITY + This cmdlet invokes CIM-methods in the class Win32_PowerPlan +#> +[cmdletbinding( + SupportsShouldProcess=$true, + ConfirmImpact='Medium' +)] +Param( + [Parameter( + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true, + ValueFromRemainingArguments=$false + )] + [Alias("ElementName")] + [string]$PlanName = "*" + , + [Parameter( + ValueFromPipeline=$true, + ValueFromPipelineByPropertyName=$true, + ValueFromRemainingArguments=$false + )] + [Alias("PSComputerName")] + [string[]]$ComputerName +) + + Begin + { + $f = $MyInvocation.InvocationName + Write-Verbose -Message "$f - START" + $GetCimInstance = @{ + Namespace = "root\cimv2\power" + ClassName = "Win32_PowerPlan" + } + + if ($ComputerName) + { + $GetCimInstance.Add("ComputerName",$ComputerName) + } + + $InvokeCimMethod = @{ + MethodName = "Activate" + } + + if ($WhatIfPreference) + { + $InvokeCimMethod.Add("WhatIf",$true) + } + } + + Process + { + Write-Verbose -Message "$f - ElementName=$PlanName" + $CimObjectPowerPlan = Get-CimInstance @GetCimInstance | Where-Object ElementName -like "$PlanName" + + foreach ($Instance in $CimObjectPowerPlan) + { + if ($pscmdlet.ShouldProcess($Instance)) + { + $null = Invoke-CimMethod -InputObject $Instance @InvokeCimMethod + } + } + if (-not $CimObjectPowerPlan) + { + Write-Warning -Message "Unable to find powerplan $PlanName" + } + } + + End + { + Write-Verbose -Message "$f - END" + } + +} + +<# + DSC Resource + Manages the power plan selection for a computer. +#> +[DscResource()] +class PowerPlan +{ + + <# + This property is the name of an available power plan. + #> + [DscProperty(Key)] + [string]$Name + + <# + Sets the specified power plan as active. + #> + [void] Set() + { + Set-PowerPlan $this.Name + } + + <# + Tests if the machine is using the specified power plan. + #> + [bool] Test() + { + if ((Get-PowerPlan -Active).ElementName -eq $this.Name) + { + return $true + } + else + { + return $false + } + } + + <# + Returns an instance of this class to identify the active plan. + #> + [PowerPlan] Get() + { + $this.Name = (Get-PowerPlan -Active).ElementName + return $this + } } \ No newline at end of file diff --git a/packages/scaffold/src/index.ts b/packages/scaffold/src/index.ts index c5ad1d9a..04aa85d4 100755 --- a/packages/scaffold/src/index.ts +++ b/packages/scaffold/src/index.ts @@ -1,5 +1,5 @@ import * as path from "node:path"; -import { LogManager, type Logger } from "@bluecadet/launchpad-utils"; +import { type Logger, LogManager } from "@bluecadet/launchpad-utils"; import * as sudo from "sudo-prompt"; export function launchScaffold(parentLogger: Logger) { @@ -18,7 +18,7 @@ export function launchScaffold(parentLogger: Logger) { { name: "Launchpad Scaffold", }, - (error, stdout, stderr) => { + (error, stdout, _stderr) => { if (error) throw error; console.log(stdout); }, diff --git a/packages/testing/src/setup.ts b/packages/testing/src/setup.ts index dfb07a3d..ad1966ab 100644 --- a/packages/testing/src/setup.ts +++ b/packages/testing/src/setup.ts @@ -1,15 +1,9 @@ -import * as posixPath from "node:path/posix"; -import { fs, vol } from "memfs"; -import { type Result, err, ok } from "neverthrow"; -import { afterEach, expect, vi } from "vitest"; +import path from "node:path"; +import { fs } from "memfs"; +import { err, ok, type Result } from "neverthrow"; +import { expect, vi } from "vitest"; import type { LogEntry } from "winston"; -// Mocking the `path` module to use posix paths for consistency across platforms -vi.mock("node:path", () => ({ - ...posixPath, - default: posixPath, -})); - vi.mock("fs", () => ({ ...fs, default: fs, @@ -45,11 +39,11 @@ vi.mock("pm2", () => { return { default: { list: vi.fn().mockImplementation((cb) => cb(null, [])), - start: vi.fn().mockImplementation((options, cb) => cb(null, {})), - stop: vi.fn().mockImplementation((name, cb) => cb(null, {})), - connect: vi.fn().mockImplementation((force, cb) => cb(null)), + start: vi.fn().mockImplementation((_options, cb) => cb(null, {})), + stop: vi.fn().mockImplementation((_name, cb) => cb(null, {})), + connect: vi.fn().mockImplementation((_force, cb) => cb(null)), disconnect: vi.fn().mockImplementation(() => undefined), - delete: vi.fn().mockImplementation((name, cb) => { + delete: vi.fn().mockImplementation((_name, cb) => { cb(null, {}); }), launchBus: vi.fn().mockImplementation((cb) => @@ -95,7 +89,7 @@ vi.mock("winston-daily-rotate-file", async () => { const { default: Transport } = await import("winston-transport"); class DummyTransport extends Transport { - override log(info: LogEntry) { + override log(_info: LogEntry) { // do nothing } } @@ -108,6 +102,8 @@ vi.mock("winston-daily-rotate-file", async () => { }; }); +process.chdir("/"); + // neverthrow expect helpers expect.extend({ toBeOk: (result: Result) => { @@ -141,4 +137,26 @@ expect.extend({ actual: result, }; }, + + toMatchPath: (received: string, expected: string) => { + // resolve both paths to ensure they are normalized for the current platform + const normalizedReceived = path.resolve(received); + const normalizedExpected = path.resolve(expected); + + if (normalizedReceived === normalizedExpected) { + return { + pass: true, + message: () => + `Expected paths not to match:\n Received: ${normalizedReceived}\n Expected: ${normalizedExpected}`, + }; + } + + return { + pass: false, + message: () => + `Expected paths to match:\n Received: ${normalizedReceived}\n Expected: ${normalizedExpected}\n Original received: ${received}\n Original expected: ${expected}`, + expected: normalizedExpected, + actual: normalizedReceived, + }; + }, }); diff --git a/packages/testing/src/vitest.d.ts b/packages/testing/src/vitest.d.ts index 7e31ff1b..df862110 100644 --- a/packages/testing/src/vitest.d.ts +++ b/packages/testing/src/vitest.d.ts @@ -3,6 +3,7 @@ import "vitest"; interface CustomMatchers { toBeOk: () => R; toBeErr: () => R; + toMatchPath: (expected: string) => R; } declare module "vitest" { diff --git a/packages/utils/README.md b/packages/utils/README.md index 6b5cdfdf..219cbdf0 100644 --- a/packages/utils/README.md +++ b/packages/utils/README.md @@ -1,3 +1,3 @@ -# Launchpad Utils - -Collection of utils used across [@bluecadet/launchpad](https://www.npmjs.com/package/@bluecadet/launchpad) packages. +# Launchpad Utils + +Collection of utils used across [@bluecadet/launchpad](https://www.npmjs.com/package/@bluecadet/launchpad) packages. diff --git a/packages/utils/src/__tests__/log-manager.test.ts b/packages/utils/src/__tests__/log-manager.test.ts index d61568ff..4294ad0e 100644 --- a/packages/utils/src/__tests__/log-manager.test.ts +++ b/packages/utils/src/__tests__/log-manager.test.ts @@ -1,4 +1,4 @@ -import path from "node:path/posix"; +import path from "node:path"; import moment from "moment"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import winston from "winston"; @@ -7,7 +7,7 @@ import { LogManager } from "../log-manager.js"; // we don't want to actually log anything to the console during tests const consoleLogSpy = vi .spyOn(winston.transports.Console.prototype, "log") - .mockImplementation((info, cb) => { + .mockImplementation((_info, cb) => { if (cb && typeof cb === "function") cb(); }); @@ -54,7 +54,7 @@ describe("LogManager", () => { }); it("should create child loggers with module names", () => { - const logger = LogManager.configureRootLogger(); + const _logger = LogManager.configureRootLogger(); const childLogger = LogManager.getLogger("test-module"); expect(childLogger).toBeDefined(); @@ -117,17 +117,20 @@ describe("LogManager", () => { describe("getFilePath", () => { it("should generate correct file paths", () => { - const manager = new LogManager({ - fileOptions: { - dirname: "test-logs", - extension: ".log", + const manager = new LogManager( + { + fileOptions: { + dirname: "test-logs", + extension: ".log", + }, }, - }); + "/some/cwd", + ); const dateStr = moment().format("YYYY-MM-DD"); const filePath = manager.getFilePath("test-type"); - expect(filePath).toBe(path.join("test-logs", `${dateStr}-test-type.log`)); + expect(filePath).toBe(path.resolve("/some/cwd/test-logs", `${dateStr}-test-type.log`)); }); it("should return templated paths when requested", () => { diff --git a/packages/utils/src/__tests__/plugin-driver.test.ts b/packages/utils/src/__tests__/plugin-driver.test.ts index 20ba5fe3..0a5a9555 100644 --- a/packages/utils/src/__tests__/plugin-driver.test.ts +++ b/packages/utils/src/__tests__/plugin-driver.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, vi } from "vitest"; -import PluginDriver, { HookContextProvider, PluginError } from "../plugin-driver.js"; import type { HookSet, Plugin } from "../plugin-driver.js"; +import PluginDriver, { HookContextProvider, PluginError } from "../plugin-driver.js"; import { createMockLogger } from "./test-utils.js"; describe("PluginDriver", () => { @@ -14,13 +14,13 @@ describe("PluginDriver", () => { }; const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger, [plugin]); + const driver = new PluginDriver({ logger: mockLogger }, [plugin]); expect(driver.plugins).toContain(plugin); }); it("should add plugins after initialization", () => { const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger); + const driver = new PluginDriver({ logger: mockLogger }); const plugin = { name: "test-plugin", hooks: { @@ -57,7 +57,7 @@ describe("PluginDriver", () => { const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger, [plugin1, plugin2]); + const driver = new PluginDriver({ logger: mockLogger }, [plugin1, plugin2]); await driver.runHookSequential("testHook"); expect(order).toEqual([1, 2]); @@ -84,7 +84,7 @@ describe("PluginDriver", () => { }; const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger, [plugin1, plugin2]); + const driver = new PluginDriver({ logger: mockLogger }, [plugin1, plugin2]); const result = await driver._runHookSequentialWithCtx("testHook", () => ({}), []); expect(result.isErr()).toBe(true); @@ -117,7 +117,7 @@ describe("PluginDriver", () => { }; const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger, [plugin1, plugin2]); + const driver = new PluginDriver({ logger: mockLogger }, [plugin1, plugin2]); const result = await driver._runHookParallelWithCtx("testHook", () => ({}), []); expect(result.isOk()).toBe(true); @@ -147,7 +147,7 @@ describe("PluginDriver", () => { }; const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger, [plugin1, plugin2]); + const driver = new PluginDriver({ logger: mockLogger }, [plugin1, plugin2]); const result = await driver._runHookParallelWithCtx("testHook", () => ({}), []); expect(result.isErr()).toBe(true); @@ -179,7 +179,7 @@ describe("PluginDriver", () => { }; const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger, [plugin1, plugin2]); + const driver = new PluginDriver({ logger: mockLogger }, [plugin1, plugin2]); const result = await driver._runHookParallelWithCtx("testHook", () => ({}), []); expect(result.isErr()).toBe(true); @@ -200,7 +200,7 @@ describe("PluginDriver", () => { }; const mockLogger = createMockLogger(); - const driver = new PluginDriver(mockLogger, [plugin]); + const driver = new PluginDriver({ logger: mockLogger }, [plugin]); await driver.runHookSequential("testHook"); }); }); @@ -224,7 +224,7 @@ describe("HookContextProvider", () => { }; const mockLogger = createMockLogger(); - const baseDriver = new PluginDriver(mockLogger, [plugin]); + const baseDriver = new PluginDriver({ logger: mockLogger }, [plugin]); const provider = new TestContextProvider(baseDriver); const result = await provider.runHookSequential("testHook"); @@ -258,7 +258,7 @@ describe("HookContextProvider", () => { }; const mockLogger = createMockLogger(); - const baseDriver = new PluginDriver(mockLogger, [plugin1, plugin2]); + const baseDriver = new PluginDriver({ logger: mockLogger }, [plugin1, plugin2]); const provider = new TestContextProvider(baseDriver); // Test sequential diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index d46e3f69..9892b068 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,16 +1,16 @@ -export { onExit } from "./on-exit.js"; -export { LogManager, logConfigSchema } from "./log-manager.js"; -export { - default as PluginDriver, - HookContextProvider, - createPluginValidator, -} from "./plugin-driver.js"; -export type { Logger, LogConfig } from "./log-manager.js"; -export type { Plugin, HookSet, BaseHookContext } from "./plugin-driver.js"; export { FixedConsoleLogger, - TTY_ONLY, NO_TTY, TTY_FIXED, TTY_FIXED_END, + TTY_ONLY, } from "./console-transport.js"; +export type { LogConfig, Logger } from "./log-manager.js"; +export { LogManager, logConfigSchema } from "./log-manager.js"; +export { onExit } from "./on-exit.js"; +export type { BaseHookContext, HookSet, Plugin } from "./plugin-driver.js"; +export { + createPluginValidator, + default as PluginDriver, + HookContextProvider, +} from "./plugin-driver.js"; diff --git a/packages/utils/src/log-manager.ts b/packages/utils/src/log-manager.ts index ad820be3..9be1eba8 100644 --- a/packages/utils/src/log-manager.ts +++ b/packages/utils/src/log-manager.ts @@ -89,9 +89,11 @@ export class LogManager { static _instance: LogManager | null = null; private _config: ResolvedLogConfig; private _rootLogger: WinstonLogger; + private _cwd: string; - constructor(config: LogConfig = {}) { + constructor(config: LogConfig = {}, cwd: string = process.cwd()) { this._config = logConfigSchema.parse(config); + this._cwd = cwd; const { format: consoleFormat, ...rest } = this._config; @@ -144,9 +146,9 @@ export class LogManager { return LogManager._instance; } - static configureRootLogger(config?: LogConfig): WinstonLogger { + static configureRootLogger(config?: LogConfig, cwd?: string): WinstonLogger { if (LogManager._instance === null) { - LogManager._instance = new LogManager(config); + LogManager._instance = new LogManager(config, cwd); } else { LogManager._instance._rootLogger.warn("Root logger already configured. Ignoring."); } @@ -177,7 +179,7 @@ export class LogManager { output = output.replace(DATE_KEY, dateStr); output = slugify(output); output = output + this._config.fileOptions.extension; - output = path.join(this._config.fileOptions.dirname, output); + output = path.resolve(this._cwd, this._config.fileOptions.dirname, output); } return output; } diff --git a/packages/utils/src/plugin-driver.ts b/packages/utils/src/plugin-driver.ts index 09130f14..d9201731 100644 --- a/packages/utils/src/plugin-driver.ts +++ b/packages/utils/src/plugin-driver.ts @@ -1,5 +1,5 @@ import chalk from "chalk"; -import { ResultAsync, okAsync } from "neverthrow"; +import { okAsync, ResultAsync } from "neverthrow"; import { z } from "zod"; import type { Logger } from "./log-manager.js"; import { onExit } from "./on-exit.js"; @@ -24,6 +24,7 @@ export class PluginError extends Error { export interface BaseHookContext { logger: Logger; abortSignal: AbortSignal; + cwd: string; } export type HookSet = Record< @@ -65,13 +66,15 @@ export default class PluginDriver { readonly #baseHookContexts = new Map, BaseHookContext>(); readonly #baseLogger: Logger; readonly #abortController = new AbortController(); + readonly #cwd: string; get plugins(): ReadonlyArray> { return this.#plugins; } - constructor(baseLogger: Logger, plugins?: Plugin[]) { - this.#baseLogger = baseLogger; + constructor({ logger, cwd }: { logger: Logger; cwd?: string }, plugins?: Plugin[]) { + this.#baseLogger = logger; + this.#cwd = cwd || process.cwd(); if (plugins) { this.add(plugins); @@ -90,6 +93,7 @@ export default class PluginDriver { this.#baseHookContexts.set(plugin, { logger: this.#baseLogger.child({ module: `plugin:${plugin.name}` }), abortSignal: this.#abortController.signal, + cwd: this.#cwd, }); } } @@ -186,11 +190,11 @@ export class HookContextProvider { return this.#innerDriver.plugins; } - protected _initialize(plugins: ReadonlyArray>): void { + protected _initialize(_plugins: ReadonlyArray>): void { // implement in subclass } - protected _getPluginContext(plugin: Plugin): C { + protected _getPluginContext(_plugin: Plugin): C { throw new Error("_getPluginContext Not implemented"); }