Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b8973559ca | |||
| f6485830b8 | |||
| b9a06880b4 | |||
| e0d5abce65 | |||
| fedb05fee5 | |||
| 1dc11d3b58 | |||
| 39067c8780 | |||
| c0dfe84115 | |||
| 87c87d6c03 | |||
| e0db706d55 | |||
| 503a9cbdc6 | |||
| e3ac72906d | |||
| ab0da30e78 | |||
| de6fc197cd | |||
| 9d2d5d27fa | |||
| 52ce8bc622 |
48
CHANGELOG.md
48
CHANGELOG.md
@@ -1,5 +1,53 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.0.7
|
||||||
|
|
||||||
|
[compare changes](https://gitea.websimple.com/pascalmartineau/wpop/compare/v0.0.6...v0.0.7)
|
||||||
|
|
||||||
|
### 🩹 Fixes
|
||||||
|
|
||||||
|
- Local site url from env + cwd (f648583)
|
||||||
|
|
||||||
|
## v0.0.6
|
||||||
|
|
||||||
|
[compare changes](https://gitea.websimple.com/pascalmartineau/wpop/compare/v0.0.5...v0.0.6)
|
||||||
|
|
||||||
|
## v0.0.5
|
||||||
|
|
||||||
|
[compare changes](https://gitea.websimple.com/pascalmartineau/wpop/compare/v0.0.4...v0.0.5)
|
||||||
|
|
||||||
|
### 🚀 Enhancements
|
||||||
|
|
||||||
|
- Wpop sync (fedb05f)
|
||||||
|
|
||||||
|
## v0.0.4
|
||||||
|
|
||||||
|
[compare changes](https://gitea.websimple.com/pascalmartineau/wpop/compare/v0.0.3...v0.0.4)
|
||||||
|
|
||||||
|
### 🚀 Enhancements
|
||||||
|
|
||||||
|
- Enhance deployment process with Gitea Actions integration and improved environment variable handling (39067c8)
|
||||||
|
|
||||||
|
## v0.0.3
|
||||||
|
|
||||||
|
[compare changes](https://gitea.websimple.com/pascalmartineau/wpop/compare/v0.0.2...v0.0.3)
|
||||||
|
|
||||||
|
### 🏡 Chore
|
||||||
|
|
||||||
|
- Check and publish on release (87c87d6)
|
||||||
|
|
||||||
|
## v0.0.2
|
||||||
|
|
||||||
|
[compare changes](https://gitea.websimple.com/pascalmartineau/wpop/compare/v0.0.1...v0.0.2)
|
||||||
|
|
||||||
|
### 🚀 Enhancements
|
||||||
|
|
||||||
|
- Implement deploy command with SSH support and context management (52ce8bc)
|
||||||
|
- Bundler with tsdown (9d2d5d2)
|
||||||
|
- Add deploy command options for component inclusion and enhance deployment logic (de6fc19)
|
||||||
|
- Add CLAUDE.md for project guidance and command documentation (ab0da30)
|
||||||
|
- Enhance deployment process with improved error handling and logging (e3ac729)
|
||||||
|
|
||||||
## v0.0.1
|
## v0.0.1
|
||||||
|
|
||||||
### 🚀 Enhancements
|
### 🚀 Enhancements
|
||||||
|
|||||||
47
CLAUDE.md
Normal file
47
CLAUDE.md
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project
|
||||||
|
|
||||||
|
`@lewebsimple/wpop` (`wpop`) — a Node CLI that deploys WordPress projects from a developer's working tree to a remote host over SSH/rsync. Authored ESM TypeScript, bundled with `tsdown` to `dist/cli.mjs` (the `bin` entry).
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `pnpm dev -- <args>` — run the CLI from source via `tsx` (e.g. `pnpm dev -- deploy --dry-run`)
|
||||||
|
- `pnpm build` — bundle to `dist/` with tsdown (ESM, node24 target, emits .d.ts)
|
||||||
|
- `pnpm typecheck` — `tsc --noEmit` against `tsconfig.json`
|
||||||
|
- `pnpm lint` / `pnpm lint:fix` — oxlint
|
||||||
|
- `pnpm format` / `pnpm format:check` — oxfmt
|
||||||
|
- `pnpm check` — runs format:check + lint + typecheck (use this before declaring work done; there is no test suite)
|
||||||
|
- `pnpm release` — `changelogen --release --push --noAuthors`
|
||||||
|
|
||||||
|
There are no unit tests. Validate changes by running the CLI with `--dry-run` (every shell-out is gated by `run()` and prints `[dry-run] <cmd>` instead of executing).
|
||||||
|
|
||||||
|
Package manager is pnpm@10 (declared via `packageManager`); husky + lint-staged run oxfmt/oxlint on staged files at commit time.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Single command today (`deploy`), but the layout assumes more will be added.
|
||||||
|
|
||||||
|
- `src/cli.ts` — commander entry. Defines global flags (`--cwd`, `--dry-run`, `--json`, `--yes`, `--verbose`) on the root program and registers subcommands. Each subcommand action calls `createContext(program.opts())` and passes the context as the first argument to its handler. New commands should follow this pattern: register in `cli.ts`, implement in `src/commands/<name>.ts`, take `(context, options)`.
|
||||||
|
- `src/lib/context.ts` — `WPopContext` carries the resolved cwd and the global flags. Every side-effecting helper takes a context; nothing reads `process.cwd()` or the flags directly.
|
||||||
|
- `src/lib/run.ts` — `run(context, cmd, args, opts)` is the single chokepoint for spawning processes via `execa`. **In dry-run mode it logs and returns without executing.** Any new shell-out must go through `run` (or use `execa` directly only when capturing stdout, and in that case branch on `context.dryRun` like `sshOutput` in `deploy.ts`).
|
||||||
|
- `src/lib/env.ts` — Zod schema and resolution for deploy-time env vars (`REMOTE_HOST`, `REMOTE_USER`, `REMOTE_PATH`, `REMOTE_PORT`, `SSH_PRIVATE_KEY`, `WPOP_CACHE_DIR`, `WP_VERSION`, `WP_LOCALE`). Schema is parsed lazily inside the command, not at import. If any `REMOTE_*` value is missing, it can read Gitea Actions variables from the inferred repo/org using `WEBSIMPLE_GITEA_API_TOKEN`, `WPOP_GITEA_TOKEN`, or `GITEA_TOKEN`; repo inference comes from `git remote get-url origin` and can be overridden with `WPOP_GITEA_REPO` or `WPOP_GITEA_OWNER` + `WPOP_GITEA_REPO_NAME`.
|
||||||
|
- `src/lib/ssh.ts` — builds `PreparedSsh` from env: optionally writes `SSH_PRIVATE_KEY` to a 0600 tempfile, runs `ssh-keyscan` to populate `~/.ssh/known_hosts`, then verifies access with `ssh -o BatchMode=yes`. `sshArgs`/`sshTarget`/`rsyncSshShell` are used to construct ssh and rsync invocations consistently.
|
||||||
|
- `src/commands/deploy.ts` — orchestrates the deploy. Order matters:
|
||||||
|
1. Parse `--include` (default `vendor,plugins,themes,mu-plugins`; `all` adds `core`).
|
||||||
|
2. Build local artifacts: `composer install --no-dev` (skippable), then per-theme `pnpm/yarn/npm install + build` under `wp-content/themes/*/` (lockfile detection picks the package manager). `node_modules` is removed after each theme build.
|
||||||
|
3. **Drift check** — for every content component being deployed, list remote top-level dirs under `wp-content/<component>` and abort if any are absent locally. This guards against `rsync --delete` wiping plugins/themes installed out-of-band on the server.
|
||||||
|
4. Rsync, in this order: core (excludes `wp-config.php`, `wp-content/`, `.htaccess`, `.user.ini`, `php.ini`, `robots.txt`, `.well-known/`), `vendor/` + `composer.json/lock`, then each content component. After vendor sync, asserts `vendor/autoload.php` is referenced from remote `wp-config.php`.
|
||||||
|
5. Remote DB updates: `wp core update-db`, `wp wc update` if WooCommerce is present, `wp acf json sync` if ACF ≥ 6.8 is installed.
|
||||||
|
- Caches (`composer/`, `npm/`, `pnpm/`, `yarn/`) live under `WPOP_CACHE_DIR` (default `/tmp/wpop`) and are wired into the child env so repeated runs reuse downloads.
|
||||||
|
|
||||||
|
Logging uses `consola`; structured output is opt-in via `--json` (currently only `deploy` emits a one-shot JSON header).
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- ESM only (`"type": "module"`); imports use the `node:` prefix for built-ins.
|
||||||
|
- JSON imports use the import attribute syntax (`with { type: "json" }`) — required by the node24 target.
|
||||||
|
- Prefer adding to `src/lib/` for shared helpers; commands should stay thin orchestrators that call into lib.
|
||||||
|
- `oxlint` runs the `correctness` category as errors with `typescript`/`unicorn`/`oxc` plugins enabled — fix lint issues rather than disabling rules.
|
||||||
24
README.md
24
README.md
@@ -1,3 +1,25 @@
|
|||||||
# @lewebsimple/wpop
|
# WPop
|
||||||
|
|
||||||
WordPress operations CLI for Websimple projects.
|
WordPress operations CLI for Websimple projects.
|
||||||
|
|
||||||
|
## Deploy environment
|
||||||
|
|
||||||
|
`wpop deploy` needs:
|
||||||
|
|
||||||
|
- `REMOTE_HOST`
|
||||||
|
- `REMOTE_PORT`
|
||||||
|
- `REMOTE_USER`
|
||||||
|
- `REMOTE_PATH`
|
||||||
|
|
||||||
|
If any `REMOTE_*` value is missing, `wpop` tries to read Websimple Gitea Actions variables from the
|
||||||
|
current repository and its organization. Provide a read-only Gitea token with one of:
|
||||||
|
|
||||||
|
- `WEBSIMPLE_GITEA_API_TOKEN`
|
||||||
|
- `WPOP_GITEA_TOKEN`
|
||||||
|
- `GITEA_TOKEN`
|
||||||
|
|
||||||
|
By default, the repository is inferred from `git remote get-url origin`. You can override this with:
|
||||||
|
|
||||||
|
- `WPOP_GITEA_REPO=wp-sites/example`
|
||||||
|
- `WPOP_GITEA_OWNER=wp-sites` and `WPOP_GITEA_REPO_NAME=example`
|
||||||
|
- `WPOP_GITEA_BASE_URL=https://gitea.websimple.com`
|
||||||
|
|||||||
16
package.json
16
package.json
@@ -1,27 +1,30 @@
|
|||||||
{
|
{
|
||||||
"name": "@lewebsimple/wpop",
|
"name": "@lewebsimple/wpop",
|
||||||
"version": "0.0.1",
|
"version": "0.0.7",
|
||||||
"private": true,
|
|
||||||
"description": "WordPress operations CLI for Websimple projects.",
|
"description": "WordPress operations CLI for Websimple projects.",
|
||||||
|
"license": "MIT",
|
||||||
"author": "Pascal Martineau <pascal@lewebsimple.ca>",
|
"author": "Pascal Martineau <pascal@lewebsimple.ca>",
|
||||||
"bin": {
|
"bin": {
|
||||||
"wpop": "./dist/cli.js"
|
"wpop": "./dist/cli.mjs"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc -p tsconfig.json",
|
"build": "tsdown",
|
||||||
"changelog": "changelogen",
|
"changelog": "changelogen",
|
||||||
"check": "npm run format:check && npm run lint && npm run typecheck",
|
"check": "pnpm run format:check && pnpm run lint && pnpm run typecheck",
|
||||||
"dev": "tsx src/cli.ts",
|
"dev": "tsx src/cli.ts",
|
||||||
"format:check": "oxfmt . --check",
|
"format:check": "oxfmt . --check",
|
||||||
"format": "oxfmt . --write",
|
"format": "oxfmt . --write",
|
||||||
"lint:fix": "oxlint . --fix",
|
"lint:fix": "oxlint . --fix",
|
||||||
"lint": "oxlint .",
|
"lint": "oxlint .",
|
||||||
"prepare": "husky",
|
"prepare": "husky",
|
||||||
"release": "changelogen --release --push --noAuthors",
|
"release": "pnpm check && pnpm build && changelogen --release --push --noAuthors && pnpm publish",
|
||||||
"typecheck": "tsc -p tsconfig.json --noEmit"
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -39,6 +42,7 @@
|
|||||||
"lint-staged": "^17.0.2",
|
"lint-staged": "^17.0.2",
|
||||||
"oxfmt": "^0.48.0",
|
"oxfmt": "^0.48.0",
|
||||||
"oxlint": "^1.63.0",
|
"oxlint": "^1.63.0",
|
||||||
|
"tsdown": "^0.22.0",
|
||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "^6.0.3"
|
"typescript": "^6.0.3"
|
||||||
},
|
},
|
||||||
|
|||||||
540
pnpm-lock.yaml
generated
540
pnpm-lock.yaml
generated
@@ -45,6 +45,9 @@ importers:
|
|||||||
oxlint:
|
oxlint:
|
||||||
specifier: ^1.63.0
|
specifier: ^1.63.0
|
||||||
version: 1.63.0
|
version: 1.63.0
|
||||||
|
tsdown:
|
||||||
|
specifier: ^0.22.0
|
||||||
|
version: 0.22.0(tsx@4.21.0)(typescript@6.0.3)
|
||||||
tsx:
|
tsx:
|
||||||
specifier: ^4.21.0
|
specifier: ^4.21.0
|
||||||
version: 4.21.0
|
version: 4.21.0
|
||||||
@@ -54,6 +57,36 @@ importers:
|
|||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
|
'@babel/generator@8.0.0-rc.4':
|
||||||
|
resolution: {integrity: sha512-YZ+FuIgkj7KrIb2a2X1XiY0QYgDxAbVbYP64SjwJzOK3euCsUerzenh2oqdsmKuPSlhzmFOOklnxzHAzXagvpw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@8.0.0-rc.4':
|
||||||
|
resolution: {integrity: sha512-dluR3v287dp6YPF57kyKKrHPKffUeuxH1zQcF1WD30TeFzWXhDiVi1U6PkqaDB0++H1PeCwRhmYl4DvoerlPIw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@8.0.0-rc.4':
|
||||||
|
resolution: {integrity: sha512-HTD3bskipk5MSm08twTW6832jzIXUhxMddy4NPPzIMuyMEsrs0ZgwAaMj5ubB5+6hMlUjDu17vNconEmwsmpYg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
|
'@babel/parser@8.0.0-rc.4':
|
||||||
|
resolution: {integrity: sha512-0S/1yefMa15N4i2v3t8Fw9pgMHhf2gF6Lc1UEXI96Ls6FNAjqvHHZouZ2ZS/deqLhbMFtmfVeFac6iTsvFbLwA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
'@babel/types@8.0.0-rc.4':
|
||||||
|
resolution: {integrity: sha512-bw30DV880P/VYtsjWWdoWmJpb9S2Vn1/PqayyccTELzRQ/HslIO7+BD9rNoZ4AAFOAjC1vrNeBCkAsyh6Ibfww==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
|
||||||
|
'@emnapi/core@1.10.0':
|
||||||
|
resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==}
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.10.0':
|
||||||
|
resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==}
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.2.1':
|
||||||
|
resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==}
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.7':
|
'@esbuild/aix-ppc64@0.27.7':
|
||||||
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
|
resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -210,6 +243,28 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2':
|
||||||
|
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||||
|
engines: {node: '>=6.0.0'}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5':
|
||||||
|
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.1.4':
|
||||||
|
resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==}
|
||||||
|
peerDependencies:
|
||||||
|
'@emnapi/core': ^1.7.1
|
||||||
|
'@emnapi/runtime': ^1.7.1
|
||||||
|
|
||||||
|
'@oxc-project/types@0.129.0':
|
||||||
|
resolution: {integrity: sha512-3oz8m3FGdr2nDXVqmFUw7jolKliC4MoyXYIG2c7gpjBnzUWQpUGIYcXYKxTdTi+N2jusvt610ckTMkxdwHkYEg==}
|
||||||
|
|
||||||
'@oxfmt/binding-android-arm-eabi@0.48.0':
|
'@oxfmt/binding-android-arm-eabi@0.48.0':
|
||||||
resolution: {integrity: sha512-uwqk+/KhQvBIpULD8SMM/zAafMRC/+DV/xsEQjkkIsJ/kLmEI/2bxonVowcYTiXqqZ/a0FEW8DPkZY3VvwELDA==}
|
resolution: {integrity: sha512-uwqk+/KhQvBIpULD8SMM/zAafMRC/+DV/xsEQjkkIsJ/kLmEI/2bxonVowcYTiXqqZ/a0FEW8DPkZY3VvwELDA==}
|
||||||
engines: {node: ^20.19.0 || >=22.12.0}
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
@@ -454,6 +509,107 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
|
'@quansync/fs@1.0.0':
|
||||||
|
resolution: {integrity: sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==}
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.0.0':
|
||||||
|
resolution: {integrity: sha512-TWMZnRLMe63C2Lhyicviu7ZHaU4kxa6PS3rofvc9GmcvptzNN11BcfQ4Sl7MwTOsisQoa2keB/EBdNCAnUo8vA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.0.0':
|
||||||
|
resolution: {integrity: sha512-6XcD+8k0gPVItNagEw78/qqcBDwKcwDYS8V2hRmVsfUSIrd8cWe/CBvRDI5toqFyPfj+FJr6t8U6Xj2P2prEew==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.0.0':
|
||||||
|
resolution: {integrity: sha512-iN/tWVXRQDWvmZlKdceP1Dwug9GDpEymhb9p4xnEe6zvCg5lFmzVljl+1qR1NVx3yfGpr2Na+CuLmv5IU8uzfQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.0.0':
|
||||||
|
resolution: {integrity: sha512-jjQMDvvwSOuhOwMszD/klSOjyWMM3zI64hWTj9KT5x4MxRbZAf+7vLQ6qouRhtsLVFHr3f0ILaJAfgENPiQdAQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0':
|
||||||
|
resolution: {integrity: sha512-d//Dtg2x6/m3mbV64yUGNnDGNZaDGRpDLLNGerHQUVObuNaIQaaDp25yUiqGXtHEXX+NP2d0wAlmKgpYgIAJ2A==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.0.0':
|
||||||
|
resolution: {integrity: sha512-n7Ofp0mx+aB2cC+Sdy5YtMnXtY9lchnHbY+3Yt0uq9JsWQExf4f5Whu0tK0R8Jdc9S6RchTHjIFY7uc92puOVQ==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.0.0':
|
||||||
|
resolution: {integrity: sha512-EIVjy2cgd7uuMMo94FVkBp7F6DhcZAUwNURkSG3RwUmvAXR6s0ISxM81U+IydcZByPG0pZIHsf1b6kTxoFDgJA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0':
|
||||||
|
resolution: {integrity: sha512-JEwwOPcwTLAcpDQlqSmjEmfs63xJnSiUNIGvLcDLUHCWK4XowpS/7c7tUsUH6uT/ct6bMUTdXKfI8967FYj6mg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.0.0':
|
||||||
|
resolution: {integrity: sha512-0wjCFhLrihtAubnT9iA0N++0pSV0z5Hg7tNGdNJ4RFaINceHadoF+kiFGyY1qSSNVIAZtLotG8Ju1bgDPkjnFA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0':
|
||||||
|
resolution: {integrity: sha512-Dfn7iak9BcMMePxcoJfpSbWqnEyrp/dRF63/8qW/eHBdOZov6x5aShLLEYGYdIeSJ6vMLK/XCVB+lGIxm41bQA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [glibc]
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.0.0':
|
||||||
|
resolution: {integrity: sha512-5/utzzDmD/pD/bmuaUcbTf/sZYy0aztwIVlfpoW1fTjCZ0BaPOMVWGZL1zvgxyi7ZIVYWlxKONHmSbHuiOh8Jw==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
libc: [musl]
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0':
|
||||||
|
resolution: {integrity: sha512-ouJs8VcUomfLfpbUECqFMRqdV4x6aeAK3MA4m6vTrJJjKyWTV5KnxZx7Jd9G+GlDaQQxubcba00x16OyJ1meig==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0':
|
||||||
|
resolution: {integrity: sha512-E+oHKGiDA+lsKMmFtffDDw91EryDT7uJocrIuCHqhm6bCTM6xFK+3gaCkYOHfPwQr0cCNarSM2xaELoQDz9jJg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [wasm32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.0.0':
|
||||||
|
resolution: {integrity: sha512-yYK02n8Rngo+gbm1y6G0+7jk1sJ/2Wt7K0me0Y7k/ErBpyf+LJ2gFpqWVTcRV1rUepBlQRmpgWkTQCiiwrK0Ow==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.0.0':
|
||||||
|
resolution: {integrity: sha512-14bpChMahXRRXiTwahSl+zzHPW6qQTXtkMuJBFlbo+pqSAews2d4BdCSHfrJ/MBsCZtpmTafsY+1QhBzitcmdg==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0':
|
||||||
|
resolution: {integrity: sha512-aKs/3GSWyV0mrhNmt/96/Z3yczC3yvrzYATCiCXQebBsGyYzjNdUphRVLeJQ67ySKVXRfMxt2lm12pmXvbPFQQ==}
|
||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1':
|
'@sec-ant/readable-stream@0.4.1':
|
||||||
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==}
|
||||||
|
|
||||||
@@ -461,6 +617,15 @@ packages:
|
|||||||
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.2':
|
||||||
|
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||||
|
|
||||||
|
'@types/estree@1.0.9':
|
||||||
|
resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==}
|
||||||
|
|
||||||
|
'@types/jsesc@2.5.1':
|
||||||
|
resolution: {integrity: sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==}
|
||||||
|
|
||||||
'@types/node@25.6.0':
|
'@types/node@25.6.0':
|
||||||
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==}
|
||||||
|
|
||||||
@@ -479,6 +644,17 @@ packages:
|
|||||||
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
ansis@4.2.0:
|
||||||
|
resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
ast-kit@3.0.0-beta.1:
|
||||||
|
resolution: {integrity: sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
|
birpc@4.0.0:
|
||||||
|
resolution: {integrity: sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==}
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -491,6 +667,10 @@ packages:
|
|||||||
magicast:
|
magicast:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
cac@7.0.0:
|
||||||
|
resolution: {integrity: sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==}
|
||||||
|
engines: {node: '>=20.19.0'}
|
||||||
|
|
||||||
changelogen@0.6.2:
|
changelogen@0.6.2:
|
||||||
resolution: {integrity: sha512-QtC7+r9BxoUm+XDAwhLbz3CgU134J1ytfE3iCpLpA4KFzX2P1e6s21RrWDwUBzfx66b1Rv+6lOA2nS2btprd+A==}
|
resolution: {integrity: sha512-QtC7+r9BxoUm+XDAwhLbz3CgU134J1ytfE3iCpLpA4KFzX2P1e6s21RrWDwUBzfx66b1Rv+6lOA2nS2btprd+A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -547,9 +727,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
|
resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
dts-resolver@3.0.0:
|
||||||
|
resolution: {integrity: sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
oxc-resolver: '>=11.0.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
oxc-resolver:
|
||||||
|
optional: true
|
||||||
|
|
||||||
emoji-regex@10.6.0:
|
emoji-regex@10.6.0:
|
||||||
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==}
|
||||||
|
|
||||||
|
empathic@2.0.0:
|
||||||
|
resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==}
|
||||||
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
environment@1.1.0:
|
environment@1.1.0:
|
||||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -559,6 +752,9 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==}
|
||||||
|
|
||||||
eventemitter3@5.0.4:
|
eventemitter3@5.0.4:
|
||||||
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==}
|
||||||
|
|
||||||
@@ -569,6 +765,15 @@ packages:
|
|||||||
exsolve@1.0.8:
|
exsolve@1.0.8:
|
||||||
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==}
|
||||||
|
|
||||||
|
fdir@6.5.0:
|
||||||
|
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
peerDependencies:
|
||||||
|
picomatch: ^3 || ^4
|
||||||
|
peerDependenciesMeta:
|
||||||
|
picomatch:
|
||||||
|
optional: true
|
||||||
|
|
||||||
figures@6.1.0:
|
figures@6.1.0:
|
||||||
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -589,10 +794,17 @@ packages:
|
|||||||
get-tsconfig@4.14.0:
|
get-tsconfig@4.14.0:
|
||||||
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
|
resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==}
|
||||||
|
|
||||||
|
get-tsconfig@5.0.0-beta.5:
|
||||||
|
resolution: {integrity: sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==}
|
||||||
|
engines: {node: '>=20.20.0'}
|
||||||
|
|
||||||
giget@3.2.0:
|
giget@3.2.0:
|
||||||
resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==}
|
resolution: {integrity: sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
hookable@6.1.1:
|
||||||
|
resolution: {integrity: sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==}
|
||||||
|
|
||||||
human-signals@8.0.1:
|
human-signals@8.0.1:
|
||||||
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
|
resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==}
|
||||||
engines: {node: '>=18.18.0'}
|
engines: {node: '>=18.18.0'}
|
||||||
@@ -602,6 +814,10 @@ packages:
|
|||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
import-without-cache@0.4.0:
|
||||||
|
resolution: {integrity: sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.0.0}
|
||||||
|
|
||||||
is-docker@3.0.0:
|
is-docker@3.0.0:
|
||||||
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
@@ -639,6 +855,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
jsesc@3.1.0:
|
||||||
|
resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
kleur@3.0.3:
|
kleur@3.0.3:
|
||||||
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
resolution: {integrity: sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -671,6 +892,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
obug@2.1.1:
|
||||||
|
resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==}
|
||||||
|
|
||||||
ofetch@1.5.1:
|
ofetch@1.5.1:
|
||||||
resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
|
resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
|
||||||
|
|
||||||
@@ -733,6 +957,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
|
|
||||||
|
quansync@1.0.0:
|
||||||
|
resolution: {integrity: sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==}
|
||||||
|
|
||||||
rc9@3.0.1:
|
rc9@3.0.1:
|
||||||
resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
|
resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==}
|
||||||
|
|
||||||
@@ -750,6 +977,30 @@ packages:
|
|||||||
rfdc@1.4.1:
|
rfdc@1.4.1:
|
||||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||||
|
|
||||||
|
rolldown-plugin-dts@0.25.0:
|
||||||
|
resolution: {integrity: sha512-GE3uDZgUuA9l6g+1u928TRmadd5IVhaWiwpWast2kCyLv9tYJJCC6E5HHkV0HGmwC5ZL73xh12/PRZI+KZ2vdQ==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.0.0}
|
||||||
|
peerDependencies:
|
||||||
|
'@ts-macro/tsc': ^0.3.6
|
||||||
|
'@typescript/native-preview': '>=7.0.0-dev.20260325.1'
|
||||||
|
rolldown: ^1.0.0
|
||||||
|
typescript: ^6.0.0
|
||||||
|
vue-tsc: ~3.2.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@ts-macro/tsc':
|
||||||
|
optional: true
|
||||||
|
'@typescript/native-preview':
|
||||||
|
optional: true
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
vue-tsc:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
rolldown@1.0.0:
|
||||||
|
resolution: {integrity: sha512-yD986aXDESFGS95spT1LAv0jssywP4npMEjmMHyN2/5+eE8qQJUype2AaKkRiLgBgyD0LFlubwAht7VmY8rGoA==}
|
||||||
|
engines: {node: ^20.19.0 || >=22.12.0}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
run-applescript@7.1.0:
|
run-applescript@7.1.0:
|
||||||
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
|
resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -812,10 +1063,55 @@ packages:
|
|||||||
resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
|
resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
tinyglobby@0.2.16:
|
||||||
|
resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==}
|
||||||
|
engines: {node: '>=12.0.0'}
|
||||||
|
|
||||||
tinypool@2.1.0:
|
tinypool@2.1.0:
|
||||||
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
|
resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==}
|
||||||
engines: {node: ^20.0.0 || >=22.0.0}
|
engines: {node: ^20.0.0 || >=22.0.0}
|
||||||
|
|
||||||
|
tree-kill@1.2.2:
|
||||||
|
resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
tsdown@0.22.0:
|
||||||
|
resolution: {integrity: sha512-FgW0hHb27nGQA/+F3d5+U9wKXkfilk9DVkc5+7x/ZqF03g+Hoz/eeApT32jqxATt9eRoR+1jxk7MUMON+O4CXw==}
|
||||||
|
engines: {node: ^22.18.0 || >=24.0.0}
|
||||||
|
hasBin: true
|
||||||
|
peerDependencies:
|
||||||
|
'@arethetypeswrong/core': ^0.18.1
|
||||||
|
'@tsdown/css': 0.22.0
|
||||||
|
'@tsdown/exe': 0.22.0
|
||||||
|
'@vitejs/devtools': '*'
|
||||||
|
publint: ^0.3.8
|
||||||
|
tsx: '*'
|
||||||
|
typescript: ^5.0.0 || ^6.0.0
|
||||||
|
unplugin-unused: ^0.5.0
|
||||||
|
unrun: '*'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@arethetypeswrong/core':
|
||||||
|
optional: true
|
||||||
|
'@tsdown/css':
|
||||||
|
optional: true
|
||||||
|
'@tsdown/exe':
|
||||||
|
optional: true
|
||||||
|
'@vitejs/devtools':
|
||||||
|
optional: true
|
||||||
|
publint:
|
||||||
|
optional: true
|
||||||
|
tsx:
|
||||||
|
optional: true
|
||||||
|
typescript:
|
||||||
|
optional: true
|
||||||
|
unplugin-unused:
|
||||||
|
optional: true
|
||||||
|
unrun:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
tsx@4.21.0:
|
tsx@4.21.0:
|
||||||
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==}
|
||||||
engines: {node: '>=18.0.0'}
|
engines: {node: '>=18.0.0'}
|
||||||
@@ -829,6 +1125,9 @@ packages:
|
|||||||
ufo@1.6.4:
|
ufo@1.6.4:
|
||||||
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
resolution: {integrity: sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==}
|
||||||
|
|
||||||
|
unconfig-core@7.5.0:
|
||||||
|
resolution: {integrity: sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==}
|
||||||
|
|
||||||
undici-types@7.19.2:
|
undici-types@7.19.2:
|
||||||
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==}
|
||||||
|
|
||||||
@@ -867,6 +1166,44 @@ packages:
|
|||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
|
'@babel/generator@8.0.0-rc.4':
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 8.0.0-rc.4
|
||||||
|
'@babel/types': 8.0.0-rc.4
|
||||||
|
'@jridgewell/gen-mapping': 0.3.13
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
'@types/jsesc': 2.5.1
|
||||||
|
jsesc: 3.1.0
|
||||||
|
|
||||||
|
'@babel/helper-string-parser@8.0.0-rc.4': {}
|
||||||
|
|
||||||
|
'@babel/helper-validator-identifier@8.0.0-rc.4': {}
|
||||||
|
|
||||||
|
'@babel/parser@8.0.0-rc.4':
|
||||||
|
dependencies:
|
||||||
|
'@babel/types': 8.0.0-rc.4
|
||||||
|
|
||||||
|
'@babel/types@8.0.0-rc.4':
|
||||||
|
dependencies:
|
||||||
|
'@babel/helper-string-parser': 8.0.0-rc.4
|
||||||
|
'@babel/helper-validator-identifier': 8.0.0-rc.4
|
||||||
|
|
||||||
|
'@emnapi/core@1.10.0':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/wasi-threads': 1.2.1
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/runtime@1.10.0':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@emnapi/wasi-threads@1.2.1':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
'@esbuild/aix-ppc64@0.27.7':
|
'@esbuild/aix-ppc64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -945,6 +1282,29 @@ snapshots:
|
|||||||
'@esbuild/win32-x64@0.27.7':
|
'@esbuild/win32-x64@0.27.7':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@jridgewell/gen-mapping@0.3.13':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
'@jridgewell/trace-mapping': 0.3.31
|
||||||
|
|
||||||
|
'@jridgewell/resolve-uri@3.1.2': {}
|
||||||
|
|
||||||
|
'@jridgewell/sourcemap-codec@1.5.5': {}
|
||||||
|
|
||||||
|
'@jridgewell/trace-mapping@0.3.31':
|
||||||
|
dependencies:
|
||||||
|
'@jridgewell/resolve-uri': 3.1.2
|
||||||
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|
||||||
|
'@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/core': 1.10.0
|
||||||
|
'@emnapi/runtime': 1.10.0
|
||||||
|
'@tybys/wasm-util': 0.10.2
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@oxc-project/types@0.129.0': {}
|
||||||
|
|
||||||
'@oxfmt/binding-android-arm-eabi@0.48.0':
|
'@oxfmt/binding-android-arm-eabi@0.48.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
@@ -1059,10 +1419,74 @@ snapshots:
|
|||||||
'@oxlint/binding-win32-x64-msvc@1.63.0':
|
'@oxlint/binding-win32-x64-msvc@1.63.0':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
'@quansync/fs@1.0.0':
|
||||||
|
dependencies:
|
||||||
|
quansync: 1.0.0
|
||||||
|
|
||||||
|
'@rolldown/binding-android-arm64@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-arm64@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-darwin-x64@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-freebsd-x64@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-gnu@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-arm64-musl@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-s390x-gnu@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-gnu@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-linux-x64-musl@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-openharmony-arm64@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-wasm32-wasi@1.0.0':
|
||||||
|
dependencies:
|
||||||
|
'@emnapi/core': 1.10.0
|
||||||
|
'@emnapi/runtime': 1.10.0
|
||||||
|
'@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-arm64-msvc@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/binding-win32-x64-msvc@1.0.0':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@rolldown/pluginutils@1.0.0': {}
|
||||||
|
|
||||||
'@sec-ant/readable-stream@0.4.1': {}
|
'@sec-ant/readable-stream@0.4.1': {}
|
||||||
|
|
||||||
'@sindresorhus/merge-streams@4.0.0': {}
|
'@sindresorhus/merge-streams@4.0.0': {}
|
||||||
|
|
||||||
|
'@tybys/wasm-util@0.10.2':
|
||||||
|
dependencies:
|
||||||
|
tslib: 2.8.1
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@types/estree@1.0.9': {}
|
||||||
|
|
||||||
|
'@types/jsesc@2.5.1': {}
|
||||||
|
|
||||||
'@types/node@25.6.0':
|
'@types/node@25.6.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
undici-types: 7.19.2
|
undici-types: 7.19.2
|
||||||
@@ -1080,6 +1504,16 @@ snapshots:
|
|||||||
|
|
||||||
ansi-styles@6.2.3: {}
|
ansi-styles@6.2.3: {}
|
||||||
|
|
||||||
|
ansis@4.2.0: {}
|
||||||
|
|
||||||
|
ast-kit@3.0.0-beta.1:
|
||||||
|
dependencies:
|
||||||
|
'@babel/parser': 8.0.0-rc.4
|
||||||
|
estree-walker: 3.0.3
|
||||||
|
pathe: 2.0.3
|
||||||
|
|
||||||
|
birpc@4.0.0: {}
|
||||||
|
|
||||||
bundle-name@4.1.0:
|
bundle-name@4.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
run-applescript: 7.1.0
|
run-applescript: 7.1.0
|
||||||
@@ -1099,6 +1533,8 @@ snapshots:
|
|||||||
pkg-types: 2.3.1
|
pkg-types: 2.3.1
|
||||||
rc9: 3.0.1
|
rc9: 3.0.1
|
||||||
|
|
||||||
|
cac@7.0.0: {}
|
||||||
|
|
||||||
changelogen@0.6.2:
|
changelogen@0.6.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
c12: 3.3.4
|
c12: 3.3.4
|
||||||
@@ -1159,8 +1595,12 @@ snapshots:
|
|||||||
|
|
||||||
dotenv@17.4.2: {}
|
dotenv@17.4.2: {}
|
||||||
|
|
||||||
|
dts-resolver@3.0.0: {}
|
||||||
|
|
||||||
emoji-regex@10.6.0: {}
|
emoji-regex@10.6.0: {}
|
||||||
|
|
||||||
|
empathic@2.0.0: {}
|
||||||
|
|
||||||
environment@1.1.0: {}
|
environment@1.1.0: {}
|
||||||
|
|
||||||
esbuild@0.27.7:
|
esbuild@0.27.7:
|
||||||
@@ -1192,6 +1632,10 @@ snapshots:
|
|||||||
'@esbuild/win32-ia32': 0.27.7
|
'@esbuild/win32-ia32': 0.27.7
|
||||||
'@esbuild/win32-x64': 0.27.7
|
'@esbuild/win32-x64': 0.27.7
|
||||||
|
|
||||||
|
estree-walker@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
'@types/estree': 1.0.9
|
||||||
|
|
||||||
eventemitter3@5.0.4: {}
|
eventemitter3@5.0.4: {}
|
||||||
|
|
||||||
execa@9.6.1:
|
execa@9.6.1:
|
||||||
@@ -1211,6 +1655,10 @@ snapshots:
|
|||||||
|
|
||||||
exsolve@1.0.8: {}
|
exsolve@1.0.8: {}
|
||||||
|
|
||||||
|
fdir@6.5.0(picomatch@4.0.4):
|
||||||
|
optionalDependencies:
|
||||||
|
picomatch: 4.0.4
|
||||||
|
|
||||||
figures@6.1.0:
|
figures@6.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-unicode-supported: 2.1.0
|
is-unicode-supported: 2.1.0
|
||||||
@@ -1229,12 +1677,20 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
resolve-pkg-maps: 1.0.0
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
|
get-tsconfig@5.0.0-beta.5:
|
||||||
|
dependencies:
|
||||||
|
resolve-pkg-maps: 1.0.0
|
||||||
|
|
||||||
giget@3.2.0: {}
|
giget@3.2.0: {}
|
||||||
|
|
||||||
|
hookable@6.1.1: {}
|
||||||
|
|
||||||
human-signals@8.0.1: {}
|
human-signals@8.0.1: {}
|
||||||
|
|
||||||
husky@9.1.7: {}
|
husky@9.1.7: {}
|
||||||
|
|
||||||
|
import-without-cache@0.4.0: {}
|
||||||
|
|
||||||
is-docker@3.0.0: {}
|
is-docker@3.0.0: {}
|
||||||
|
|
||||||
is-fullwidth-code-point@5.1.0:
|
is-fullwidth-code-point@5.1.0:
|
||||||
@@ -1259,6 +1715,8 @@ snapshots:
|
|||||||
|
|
||||||
jiti@2.7.0: {}
|
jiti@2.7.0: {}
|
||||||
|
|
||||||
|
jsesc@3.1.0: {}
|
||||||
|
|
||||||
kleur@3.0.3: {}
|
kleur@3.0.3: {}
|
||||||
|
|
||||||
lint-staged@17.0.2:
|
lint-staged@17.0.2:
|
||||||
@@ -1297,6 +1755,8 @@ snapshots:
|
|||||||
path-key: 4.0.0
|
path-key: 4.0.0
|
||||||
unicorn-magic: 0.3.0
|
unicorn-magic: 0.3.0
|
||||||
|
|
||||||
|
obug@2.1.1: {}
|
||||||
|
|
||||||
ofetch@1.5.1:
|
ofetch@1.5.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
destr: 2.0.5
|
destr: 2.0.5
|
||||||
@@ -1389,6 +1849,8 @@ snapshots:
|
|||||||
kleur: 3.0.3
|
kleur: 3.0.3
|
||||||
sisteransi: 1.0.5
|
sisteransi: 1.0.5
|
||||||
|
|
||||||
|
quansync@1.0.0: {}
|
||||||
|
|
||||||
rc9@3.0.1:
|
rc9@3.0.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
defu: 6.1.7
|
defu: 6.1.7
|
||||||
@@ -1405,6 +1867,43 @@ snapshots:
|
|||||||
|
|
||||||
rfdc@1.4.1: {}
|
rfdc@1.4.1: {}
|
||||||
|
|
||||||
|
rolldown-plugin-dts@0.25.0(rolldown@1.0.0)(typescript@6.0.3):
|
||||||
|
dependencies:
|
||||||
|
'@babel/generator': 8.0.0-rc.4
|
||||||
|
'@babel/helper-validator-identifier': 8.0.0-rc.4
|
||||||
|
'@babel/parser': 8.0.0-rc.4
|
||||||
|
ast-kit: 3.0.0-beta.1
|
||||||
|
birpc: 4.0.0
|
||||||
|
dts-resolver: 3.0.0
|
||||||
|
get-tsconfig: 5.0.0-beta.5
|
||||||
|
obug: 2.1.1
|
||||||
|
rolldown: 1.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
typescript: 6.0.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- oxc-resolver
|
||||||
|
|
||||||
|
rolldown@1.0.0:
|
||||||
|
dependencies:
|
||||||
|
'@oxc-project/types': 0.129.0
|
||||||
|
'@rolldown/pluginutils': 1.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
'@rolldown/binding-android-arm64': 1.0.0
|
||||||
|
'@rolldown/binding-darwin-arm64': 1.0.0
|
||||||
|
'@rolldown/binding-darwin-x64': 1.0.0
|
||||||
|
'@rolldown/binding-freebsd-x64': 1.0.0
|
||||||
|
'@rolldown/binding-linux-arm-gnueabihf': 1.0.0
|
||||||
|
'@rolldown/binding-linux-arm64-gnu': 1.0.0
|
||||||
|
'@rolldown/binding-linux-arm64-musl': 1.0.0
|
||||||
|
'@rolldown/binding-linux-ppc64-gnu': 1.0.0
|
||||||
|
'@rolldown/binding-linux-s390x-gnu': 1.0.0
|
||||||
|
'@rolldown/binding-linux-x64-gnu': 1.0.0
|
||||||
|
'@rolldown/binding-linux-x64-musl': 1.0.0
|
||||||
|
'@rolldown/binding-openharmony-arm64': 1.0.0
|
||||||
|
'@rolldown/binding-wasm32-wasi': 1.0.0
|
||||||
|
'@rolldown/binding-win32-arm64-msvc': 1.0.0
|
||||||
|
'@rolldown/binding-win32-x64-msvc': 1.0.0
|
||||||
|
|
||||||
run-applescript@7.1.0: {}
|
run-applescript@7.1.0: {}
|
||||||
|
|
||||||
scule@1.3.0: {}
|
scule@1.3.0: {}
|
||||||
@@ -1454,8 +1953,44 @@ snapshots:
|
|||||||
|
|
||||||
tinyexec@1.1.2: {}
|
tinyexec@1.1.2: {}
|
||||||
|
|
||||||
|
tinyglobby@0.2.16:
|
||||||
|
dependencies:
|
||||||
|
fdir: 6.5.0(picomatch@4.0.4)
|
||||||
|
picomatch: 4.0.4
|
||||||
|
|
||||||
tinypool@2.1.0: {}
|
tinypool@2.1.0: {}
|
||||||
|
|
||||||
|
tree-kill@1.2.2: {}
|
||||||
|
|
||||||
|
tsdown@0.22.0(tsx@4.21.0)(typescript@6.0.3):
|
||||||
|
dependencies:
|
||||||
|
ansis: 4.2.0
|
||||||
|
cac: 7.0.0
|
||||||
|
defu: 6.1.7
|
||||||
|
empathic: 2.0.0
|
||||||
|
hookable: 6.1.1
|
||||||
|
import-without-cache: 0.4.0
|
||||||
|
obug: 2.1.1
|
||||||
|
picomatch: 4.0.4
|
||||||
|
rolldown: 1.0.0
|
||||||
|
rolldown-plugin-dts: 0.25.0(rolldown@1.0.0)(typescript@6.0.3)
|
||||||
|
semver: 7.7.4
|
||||||
|
tinyexec: 1.1.2
|
||||||
|
tinyglobby: 0.2.16
|
||||||
|
tree-kill: 1.2.2
|
||||||
|
unconfig-core: 7.5.0
|
||||||
|
optionalDependencies:
|
||||||
|
tsx: 4.21.0
|
||||||
|
typescript: 6.0.3
|
||||||
|
transitivePeerDependencies:
|
||||||
|
- '@ts-macro/tsc'
|
||||||
|
- '@typescript/native-preview'
|
||||||
|
- oxc-resolver
|
||||||
|
- vue-tsc
|
||||||
|
|
||||||
|
tslib@2.8.1:
|
||||||
|
optional: true
|
||||||
|
|
||||||
tsx@4.21.0:
|
tsx@4.21.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
esbuild: 0.27.7
|
esbuild: 0.27.7
|
||||||
@@ -1467,6 +2002,11 @@ snapshots:
|
|||||||
|
|
||||||
ufo@1.6.4: {}
|
ufo@1.6.4: {}
|
||||||
|
|
||||||
|
unconfig-core@7.5.0:
|
||||||
|
dependencies:
|
||||||
|
'@quansync/fs': 1.0.0
|
||||||
|
quansync: 1.0.0
|
||||||
|
|
||||||
undici-types@7.19.2: {}
|
undici-types@7.19.2: {}
|
||||||
|
|
||||||
unicorn-magic@0.3.0: {}
|
unicorn-magic@0.3.0: {}
|
||||||
|
|||||||
50
src/cli.ts
50
src/cli.ts
@@ -1,5 +1,9 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { Command } from "commander";
|
import { Command } from "commander";
|
||||||
|
import { deploy } from "./commands/deploy";
|
||||||
|
import { sync } from "./commands/sync";
|
||||||
|
import { createContext } from "./lib/context";
|
||||||
|
import { configureLogger, emitJson } from "./lib/output";
|
||||||
import pkg from "../package.json" with { type: "json" };
|
import pkg from "../package.json" with { type: "json" };
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
@@ -8,6 +12,7 @@ program
|
|||||||
.name("wpop")
|
.name("wpop")
|
||||||
.description("WordPress operations CLI for Websimple projects.")
|
.description("WordPress operations CLI for Websimple projects.")
|
||||||
.version(pkg.version)
|
.version(pkg.version)
|
||||||
|
.option("--cwd <path>", "project working directory")
|
||||||
.option("--dry-run", "show what would happen without making changes")
|
.option("--dry-run", "show what would happen without making changes")
|
||||||
.option("--json", "output machine-readable JSON")
|
.option("--json", "output machine-readable JSON")
|
||||||
.option("--yes", "skip confirmation prompts")
|
.option("--yes", "skip confirmation prompts")
|
||||||
@@ -16,8 +21,49 @@ program
|
|||||||
program
|
program
|
||||||
.command("deploy")
|
.command("deploy")
|
||||||
.description("Deploy a WordPress project")
|
.description("Deploy a WordPress project")
|
||||||
.action(() => {
|
.option(
|
||||||
throw new Error("Not implemented yet.");
|
"--include <components>",
|
||||||
|
"comma-separated components to deploy: core,vendor,plugins,themes,mu-plugins or all",
|
||||||
|
)
|
||||||
|
.option("--skip-composer", "skip Composer dependency installation")
|
||||||
|
.option("--skip-node", "skip theme asset builds")
|
||||||
|
.action(async (options: { include?: string; skipComposer?: boolean; skipNode?: boolean }) => {
|
||||||
|
const context = createContext(program.opts());
|
||||||
|
configureLogger(context);
|
||||||
|
try {
|
||||||
|
await deploy(context, options);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(context, error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
program
|
||||||
|
.command("sync")
|
||||||
|
.description("Sync a WordPress project from the remote into the local working copy")
|
||||||
|
.option(
|
||||||
|
"--include <components>",
|
||||||
|
"comma-separated components to sync: database,uploads,plugins,themes,mu-plugins or all",
|
||||||
|
)
|
||||||
|
.option("--skip-search-replace", "skip URL search-replace after database import")
|
||||||
|
.action(async (options: { include?: string; skipSearchReplace?: boolean }) => {
|
||||||
|
const context = createContext(program.opts());
|
||||||
|
configureLogger(context);
|
||||||
|
try {
|
||||||
|
await sync(context, options);
|
||||||
|
} catch (error) {
|
||||||
|
handleError(context, error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleError(context: { json: boolean }, error: unknown): void {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
if (context.json) {
|
||||||
|
emitJson({ ok: false, error: message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
process.stderr.write(`Error: ${message}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
program.parse();
|
program.parse();
|
||||||
|
|||||||
665
src/commands/deploy.ts
Normal file
665
src/commands/deploy.ts
Normal file
@@ -0,0 +1,665 @@
|
|||||||
|
import { existsSync, mkdirSync, readdirSync, rmSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { consola } from "consola";
|
||||||
|
import prompts from "prompts";
|
||||||
|
import type { WPopContext } from "../lib/context";
|
||||||
|
import { readDeployEnv, type DeployEnv } from "../lib/env";
|
||||||
|
import { emitJson } from "../lib/output";
|
||||||
|
import { parseMarkedOutput, rsync, sshOutput, sshRun } from "../lib/remote";
|
||||||
|
import { run } from "../lib/run";
|
||||||
|
import { createSshConfig, prepareSsh, sshTarget, type PreparedSsh } from "../lib/ssh";
|
||||||
|
import { createTempDir } from "../lib/tempdir";
|
||||||
|
|
||||||
|
const DEFAULT_COMPONENTS = ["vendor", "plugins", "themes", "mu-plugins"] as const;
|
||||||
|
const ALL_COMPONENTS = ["core", ...DEFAULT_COMPONENTS] as const;
|
||||||
|
const CONTENT_COMPONENTS = ["plugins", "themes", "mu-plugins"] as const;
|
||||||
|
|
||||||
|
const CACHE_BUCKETS = {
|
||||||
|
composer: "COMPOSER_CACHE_DIR",
|
||||||
|
npm: "npm_config_cache",
|
||||||
|
pnpm: "PNPM_STORE_DIR",
|
||||||
|
yarn: "YARN_CACHE_FOLDER",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const CORE_RSYNC_EXCLUDES = [
|
||||||
|
"wp-config.php",
|
||||||
|
"wp-content/",
|
||||||
|
".htaccess",
|
||||||
|
".user.ini",
|
||||||
|
"php.ini",
|
||||||
|
"robots.txt",
|
||||||
|
".well-known/",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const REMOTE_LIST_BEGIN = "__WPOP_REMOTE_LIST_BEGIN__";
|
||||||
|
const REMOTE_LIST_END = "__WPOP_REMOTE_LIST_END__";
|
||||||
|
const COMPOSER_AUTOLOAD_BEGIN = "__WPOP_COMPOSER_AUTOLOAD_BEGIN__";
|
||||||
|
const COMPOSER_AUTOLOAD_END = "__WPOP_COMPOSER_AUTOLOAD_END__";
|
||||||
|
|
||||||
|
type PackageManager = {
|
||||||
|
lockfile: string;
|
||||||
|
install: readonly string[];
|
||||||
|
build: readonly string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const PACKAGE_MANAGERS: readonly PackageManager[] = [
|
||||||
|
{
|
||||||
|
lockfile: "pnpm-lock.yaml",
|
||||||
|
install: ["pnpm", "install", "--frozen-lockfile", "--silent"],
|
||||||
|
build: ["pnpm", "build"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lockfile: "yarn.lock",
|
||||||
|
install: ["yarn", "install", "--frozen-lockfile", "--silent"],
|
||||||
|
build: ["yarn", "build"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lockfile: "package-lock.json",
|
||||||
|
install: ["npm", "ci", "--no-audit", "--loglevel=error"],
|
||||||
|
build: ["npm", "run", "build"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export type DeployComponent = (typeof ALL_COMPONENTS)[number];
|
||||||
|
|
||||||
|
export type DeployOptions = {
|
||||||
|
include?: string;
|
||||||
|
skipComposer?: boolean;
|
||||||
|
skipNode?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DeployReport = {
|
||||||
|
command: "deploy";
|
||||||
|
cwd: string;
|
||||||
|
include: DeployComponent[];
|
||||||
|
remote: { host: string; port: number; user: string; path: string };
|
||||||
|
cacheDir: string;
|
||||||
|
dryRun: boolean;
|
||||||
|
steps: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function deploy(context: WPopContext, options: DeployOptions): Promise<void> {
|
||||||
|
const env = await readDeployEnv(process.env, { cwd: context.cwd });
|
||||||
|
const promptedForComponents = shouldPromptForComponents(context, options.include);
|
||||||
|
const components = await resolveComponents(context, options.include);
|
||||||
|
if (!components) {
|
||||||
|
consola.warn("Aborted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commandEnv = createCommandEnv(env.WPOP_CACHE_DIR);
|
||||||
|
const report = buildReport(context, env, components);
|
||||||
|
|
||||||
|
if (context.json) {
|
||||||
|
if (!context.yes && !context.dryRun) {
|
||||||
|
throw new Error("--json requires --yes (cannot prompt for confirmation in JSON mode)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
consola.info(
|
||||||
|
`Planning deploy of ${[...components].join(", ")} to ${sshTargetSummary(env)}:${env.REMOTE_PATH}`,
|
||||||
|
);
|
||||||
|
if (context.dryRun) {
|
||||||
|
consola.info("Dry-run: no remote changes will be made");
|
||||||
|
}
|
||||||
|
if (!promptedForComponents && !(await confirm(context, components, env))) {
|
||||||
|
consola.warn("Aborted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureCacheDirs(env.WPOP_CACHE_DIR);
|
||||||
|
const ssh = await prepareSsh(context, createSshConfig(env), env.WPOP_CACHE_DIR);
|
||||||
|
|
||||||
|
await installComposerDependencies(context, options, commandEnv, report);
|
||||||
|
await buildThemes(context, options, commandEnv, report);
|
||||||
|
await checkRemoteContentDrift(context, env, ssh, components);
|
||||||
|
|
||||||
|
if (components.has("core")) {
|
||||||
|
await syncCore(context, env, ssh, commandEnv);
|
||||||
|
report.steps.push("sync:core");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (components.has("vendor")) {
|
||||||
|
await syncVendor(context, env, ssh);
|
||||||
|
report.steps.push("sync:vendor");
|
||||||
|
if (existsSync(join(context.cwd, "vendor"))) {
|
||||||
|
await ensureRemoteComposerAutoload(context, env, ssh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const component of CONTENT_COMPONENTS) {
|
||||||
|
if (components.has(component)) {
|
||||||
|
await syncContentComponent(context, env, ssh, component);
|
||||||
|
report.steps.push(`sync:${component}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateRemoteDatabase(context, env, ssh);
|
||||||
|
report.steps.push("db:update");
|
||||||
|
|
||||||
|
if (context.json) {
|
||||||
|
emitJson(report);
|
||||||
|
} else {
|
||||||
|
consola.success("Deploy complete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReport(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
components: Set<DeployComponent>,
|
||||||
|
): DeployReport {
|
||||||
|
return {
|
||||||
|
command: "deploy",
|
||||||
|
cwd: context.cwd,
|
||||||
|
include: [...components],
|
||||||
|
remote: {
|
||||||
|
host: env.REMOTE_HOST,
|
||||||
|
port: env.REMOTE_PORT,
|
||||||
|
user: env.REMOTE_USER,
|
||||||
|
path: env.REMOTE_PATH,
|
||||||
|
},
|
||||||
|
cacheDir: env.WPOP_CACHE_DIR,
|
||||||
|
dryRun: context.dryRun,
|
||||||
|
steps: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sshTargetSummary(env: DeployEnv): string {
|
||||||
|
return `${env.REMOTE_USER}@${env.REMOTE_HOST}:${env.REMOTE_PORT}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPromptForComponents(context: WPopContext, include?: string): boolean {
|
||||||
|
return !include && !context.yes && !context.dryRun && !context.json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveComponents(
|
||||||
|
context: WPopContext,
|
||||||
|
include?: string,
|
||||||
|
): Promise<Set<DeployComponent> | undefined> {
|
||||||
|
if (!shouldPromptForComponents(context, include)) {
|
||||||
|
return parseComponents(include);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await prompts({
|
||||||
|
type: "multiselect",
|
||||||
|
name: "components",
|
||||||
|
message: "Select deploy components",
|
||||||
|
choices: ALL_COMPONENTS.map((component) => ({
|
||||||
|
title: component,
|
||||||
|
value: component,
|
||||||
|
selected: DEFAULT_COMPONENTS.includes(component as (typeof DEFAULT_COMPONENTS)[number]),
|
||||||
|
})),
|
||||||
|
instructions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Array.isArray(response.components)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.components.length === 0) {
|
||||||
|
throw new Error("Select at least one deploy component");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(response.components as DeployComponent[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirm(
|
||||||
|
context: WPopContext,
|
||||||
|
components: Set<DeployComponent>,
|
||||||
|
env: DeployEnv,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (context.yes || context.dryRun) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "confirmed",
|
||||||
|
message: `Deploy ${[...components].join(",")} to ${sshTargetSummary(env)}:${env.REMOTE_PATH}?`,
|
||||||
|
initial: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(response.confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseComponents(include?: string): Set<DeployComponent> {
|
||||||
|
if (!include) {
|
||||||
|
return new Set(DEFAULT_COMPONENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include === "all") {
|
||||||
|
return new Set(ALL_COMPONENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = include
|
||||||
|
.split(",")
|
||||||
|
.map((component) => component.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const invalid = components.filter(
|
||||||
|
(component) => !ALL_COMPONENTS.includes(component as DeployComponent),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
throw new Error(`Invalid deploy component(s): ${invalid.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(components as DeployComponent[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createCommandEnv(cacheDir: string): NodeJS.ProcessEnv {
|
||||||
|
const overrides: NodeJS.ProcessEnv = { WP_CLI_ALLOW_ROOT: "1" };
|
||||||
|
for (const [bucket, envVar] of Object.entries(CACHE_BUCKETS)) {
|
||||||
|
overrides[envVar] = join(cacheDir, bucket);
|
||||||
|
}
|
||||||
|
return { ...process.env, ...overrides };
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureCacheDirs(cacheDir: string): void {
|
||||||
|
for (const bucket of Object.keys(CACHE_BUCKETS)) {
|
||||||
|
mkdirSync(join(cacheDir, bucket), { recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function installComposerDependencies(
|
||||||
|
context: WPopContext,
|
||||||
|
options: DeployOptions,
|
||||||
|
commandEnv: NodeJS.ProcessEnv,
|
||||||
|
report: DeployReport,
|
||||||
|
): Promise<void> {
|
||||||
|
if (options.skipComposer) {
|
||||||
|
consola.info("Skipping Composer installation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(join(context.cwd, "composer.json"))) {
|
||||||
|
consola.warn("composer.json not found, skipping Composer installation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.info("Installing Composer dependencies");
|
||||||
|
await run(
|
||||||
|
context,
|
||||||
|
"composer",
|
||||||
|
["install", "--no-dev", "--no-interaction", "--optimize-autoloader", "--prefer-dist"],
|
||||||
|
{ env: commandEnv },
|
||||||
|
);
|
||||||
|
report.steps.push("composer:install");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function buildThemes(
|
||||||
|
context: WPopContext,
|
||||||
|
options: DeployOptions,
|
||||||
|
commandEnv: NodeJS.ProcessEnv,
|
||||||
|
report: DeployReport,
|
||||||
|
): Promise<void> {
|
||||||
|
if (options.skipNode) {
|
||||||
|
consola.info("Skipping theme builds");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themesDir = join(context.cwd, "wp-content", "themes");
|
||||||
|
if (!existsSync(themesDir)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const entry of readdirSync(themesDir, { withFileTypes: true })) {
|
||||||
|
if (!entry.isDirectory()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeDir = join(themesDir, entry.name);
|
||||||
|
if (!existsSync(join(themeDir, "package.json"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const manager = PACKAGE_MANAGERS.find((pm) => existsSync(join(themeDir, pm.lockfile)));
|
||||||
|
const install = manager?.install ?? PACKAGE_MANAGERS[2].install;
|
||||||
|
const build = manager?.build ?? PACKAGE_MANAGERS[2].build;
|
||||||
|
|
||||||
|
consola.info(`Building theme ${entry.name} (${install[0]})`);
|
||||||
|
|
||||||
|
const installArgs = install.slice(1);
|
||||||
|
if (install[0] === "pnpm" && commandEnv.PNPM_STORE_DIR) {
|
||||||
|
installArgs.push("--store-dir", commandEnv.PNPM_STORE_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
await run(context, install[0], installArgs, { cwd: themeDir, env: commandEnv });
|
||||||
|
await run(context, build[0], build.slice(1), { cwd: themeDir, env: commandEnv });
|
||||||
|
report.steps.push(`theme:${entry.name}`);
|
||||||
|
|
||||||
|
if (!context.dryRun) {
|
||||||
|
rmSync(join(themeDir, "node_modules"), { force: true, recursive: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkRemoteContentDrift(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
components: Set<DeployComponent>,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const component of CONTENT_COMPONENTS) {
|
||||||
|
if (!components.has(component)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const local = listLocalTopLevelDirs(join(context.cwd, remoteContentPath(component)));
|
||||||
|
const remote = await listRemoteTopLevelDirs(context, env, ssh, remoteContentPath(component));
|
||||||
|
const unmanaged = remote.filter((name) => !local.includes(name));
|
||||||
|
|
||||||
|
if (unmanaged.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
`Remote ${component} contains entries that are not present in the local build:`,
|
||||||
|
...unmanaged.map((name) => `- ${remoteContentPath(component)}/${name}`),
|
||||||
|
"Refusing to deploy because rsync --delete would remove them.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listLocalTopLevelDirs(path: string): string[] {
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return readdirSync(path, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listRemoteTopLevelDirs(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
path: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
// Paths flow through env vars and are quoted with JSON.stringify, which is
|
||||||
|
// close enough to POSIX double-quote escaping for the values we handle here.
|
||||||
|
const script = `set -e
|
||||||
|
printf '%s\\n' ${JSON.stringify(REMOTE_LIST_BEGIN)}
|
||||||
|
cd ${JSON.stringify(env.REMOTE_PATH)}
|
||||||
|
if [ -d ${JSON.stringify(path)} ]; then
|
||||||
|
find ${JSON.stringify(path)} -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort
|
||||||
|
fi
|
||||||
|
printf '%s\\n' ${JSON.stringify(REMOTE_LIST_END)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { stdout } = await sshOutput(context, ssh, script);
|
||||||
|
if (context.dryRun) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseMarkedOutput(stdout, REMOTE_LIST_BEGIN, REMOTE_LIST_END, "remote directory listing");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncCore(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
commandEnv: NodeJS.ProcessEnv,
|
||||||
|
): Promise<void> {
|
||||||
|
const { path: coreDir, cleanup } = createTempDir(context, "wpop-core");
|
||||||
|
|
||||||
|
consola.info("Preparing WordPress core deploy payload");
|
||||||
|
await run(
|
||||||
|
context,
|
||||||
|
"wp",
|
||||||
|
[
|
||||||
|
"core",
|
||||||
|
"download",
|
||||||
|
"--skip-content",
|
||||||
|
`--path=${coreDir}`,
|
||||||
|
`--version=${env.WP_VERSION}`,
|
||||||
|
`--locale=${env.WP_LOCALE}`,
|
||||||
|
],
|
||||||
|
{ env: commandEnv },
|
||||||
|
);
|
||||||
|
|
||||||
|
consola.info("Synchronizing WordPress core");
|
||||||
|
await rsync(context, ssh, [
|
||||||
|
"--delete",
|
||||||
|
...CORE_RSYNC_EXCLUDES.map((entry) => `--exclude=${entry}`),
|
||||||
|
`${coreDir}/`,
|
||||||
|
`${sshTarget(ssh)}:${env.REMOTE_PATH}/`,
|
||||||
|
]);
|
||||||
|
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncVendor(context: WPopContext, env: DeployEnv, ssh: PreparedSsh): Promise<void> {
|
||||||
|
if (existsSync(join(context.cwd, "vendor"))) {
|
||||||
|
await ensureRemoteDirectory(context, env, ssh, "vendor");
|
||||||
|
consola.info("Synchronizing vendor");
|
||||||
|
await rsync(context, ssh, [
|
||||||
|
"--delete",
|
||||||
|
`${join(context.cwd, "vendor")}/`,
|
||||||
|
`${sshTarget(ssh)}:${env.REMOTE_PATH}/vendor/`,
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
consola.warn("vendor directory not found, skipping vendor sync");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of ["composer.json", "composer.lock"]) {
|
||||||
|
const localPath = join(context.cwd, file);
|
||||||
|
if (existsSync(localPath)) {
|
||||||
|
consola.info(`Synchronizing ${file}`);
|
||||||
|
await rsync(context, ssh, [localPath, `${sshTarget(ssh)}:${env.REMOTE_PATH}/${file}`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureRemoteComposerAutoload(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
): Promise<void> {
|
||||||
|
consola.info("Checking remote Composer autoload inclusion");
|
||||||
|
const script = `printf '%s\\n' ${JSON.stringify(COMPOSER_AUTOLOAD_BEGIN)}
|
||||||
|
if ! cd ${JSON.stringify(env.REMOTE_PATH)}; then
|
||||||
|
echo missing_remote_path
|
||||||
|
elif [ ! -f wp-config.php ]; then
|
||||||
|
echo missing_wp_config
|
||||||
|
elif grep -q 'vendor/autoload.php' wp-config.php; then
|
||||||
|
echo ok
|
||||||
|
else
|
||||||
|
echo missing_autoload
|
||||||
|
fi
|
||||||
|
printf '%s\\n' ${JSON.stringify(COMPOSER_AUTOLOAD_END)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { stdout } = await sshOutput(context, ssh, script);
|
||||||
|
if (context.dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [status] = parseMarkedOutput(
|
||||||
|
stdout,
|
||||||
|
COMPOSER_AUTOLOAD_BEGIN,
|
||||||
|
COMPOSER_AUTOLOAD_END,
|
||||||
|
"remote Composer autoload check",
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status === "ok") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "missing_wp_config") {
|
||||||
|
throw new Error(`Remote wp-config.php not found in ${env.REMOTE_PATH}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "missing_autoload") {
|
||||||
|
await repairRemoteComposerAutoload(context, env, ssh);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status === "missing_remote_path") {
|
||||||
|
throw new Error(`Remote path does not exist or is not accessible: ${env.REMOTE_PATH}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Unexpected remote Composer autoload check result: ${status ?? "empty output"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repairRemoteComposerAutoload(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
): Promise<void> {
|
||||||
|
if (context.dryRun) {
|
||||||
|
consola.info("[dry-run] Would add Composer autoload include to remote wp-config.php");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.json && !context.yes) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
"Remote wp-config.php does not include vendor/autoload.php.",
|
||||||
|
"Re-run with --yes to allow wpop to add the Composer autoload include automatically.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.yes && !(await confirmRemoteComposerAutoloadRepair(env))) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
"Remote wp-config.php does not include vendor/autoload.php.",
|
||||||
|
"Deploy cannot continue safely without the Composer autoload include.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.info("Adding Composer autoload include to remote wp-config.php");
|
||||||
|
const script = `set -e
|
||||||
|
cd ${JSON.stringify(env.REMOTE_PATH)}
|
||||||
|
php <<'PHP'
|
||||||
|
<?php
|
||||||
|
$path = 'wp-config.php';
|
||||||
|
$contents = file_get_contents($path);
|
||||||
|
|
||||||
|
if ($contents === false) {
|
||||||
|
fwrite(STDERR, "Could not read wp-config.php\\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strpos($contents, 'vendor/autoload.php') !== false) {
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preg_match('/^<\\?php\\s*/', $contents)) {
|
||||||
|
fwrite(STDERR, "wp-config.php does not start with <?php\\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$backup = $path . '.wpop-' . gmdate('YmdHis') . '.bak';
|
||||||
|
if (!copy($path, $backup)) {
|
||||||
|
fwrite(STDERR, "Could not create backup $backup\\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
$autoload = "if ( file_exists( __DIR__ . '/vendor/autoload.php' ) ) {\\n\\trequire_once __DIR__ . '/vendor/autoload.php';\\n}\\n\\n";
|
||||||
|
$updated = preg_replace('/^<\\?php\\s*/', "<?php\\n" . $autoload, $contents, 1);
|
||||||
|
|
||||||
|
if ($updated === null || file_put_contents($path, $updated) === false) {
|
||||||
|
fwrite(STDERR, "Could not update wp-config.php\\n");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "Created backup $backup\\n";
|
||||||
|
PHP
|
||||||
|
`;
|
||||||
|
|
||||||
|
await sshRun(context, ssh, script);
|
||||||
|
await ensureRemoteComposerAutoload(context, env, ssh);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmRemoteComposerAutoloadRepair(env: DeployEnv): Promise<boolean> {
|
||||||
|
const response = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "confirmed",
|
||||||
|
message: [
|
||||||
|
"Remote wp-config.php does not include vendor/autoload.php.",
|
||||||
|
`Add the Composer autoload include on ${env.REMOTE_HOST}:${env.REMOTE_PATH}/wp-config.php?`,
|
||||||
|
].join(" "),
|
||||||
|
initial: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(response.confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncContentComponent(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
component: (typeof CONTENT_COMPONENTS)[number],
|
||||||
|
): Promise<void> {
|
||||||
|
const path = remoteContentPath(component);
|
||||||
|
const localPath = join(context.cwd, path);
|
||||||
|
|
||||||
|
if (!existsSync(localPath)) {
|
||||||
|
consola.warn(`${path} not found, skipping ${component} sync`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureRemoteDirectory(context, env, ssh, path);
|
||||||
|
consola.info(`Synchronizing ${component}`);
|
||||||
|
await rsync(context, ssh, [
|
||||||
|
"--delete",
|
||||||
|
`${localPath}/`,
|
||||||
|
`${sshTarget(ssh)}:${env.REMOTE_PATH}/${path}/`,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function remoteContentPath(component: (typeof CONTENT_COMPONENTS)[number]): string {
|
||||||
|
return `wp-content/${component}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureRemoteDirectory(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
path: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await sshRun(context, ssh, `mkdir -p ${JSON.stringify(join(env.REMOTE_PATH, path))}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateRemoteDatabase(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
): Promise<void> {
|
||||||
|
consola.info("Updating remote database");
|
||||||
|
const script = `set -e
|
||||||
|
cd ${JSON.stringify(env.REMOTE_PATH)}
|
||||||
|
if [ -f wp-config.php ]; then
|
||||||
|
wp core update-db
|
||||||
|
if [ -d wp-content/plugins/woocommerce ]; then
|
||||||
|
echo "Updating WooCommerce database..."
|
||||||
|
wp wc update
|
||||||
|
fi
|
||||||
|
|
||||||
|
ACF_VERSION=""
|
||||||
|
if wp plugin is-installed advanced-custom-fields-pro >/dev/null 2>&1; then
|
||||||
|
ACF_VERSION="$(wp plugin get advanced-custom-fields-pro --field=version)"
|
||||||
|
elif wp plugin is-installed advanced-custom-fields >/dev/null 2>&1; then
|
||||||
|
ACF_VERSION="$(wp plugin get advanced-custom-fields --field=version)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$ACF_VERSION" ] && php -r 'exit(version_compare($argv[1], "6.8", ">=") ? 0 : 1);' "$ACF_VERSION"; then
|
||||||
|
if wp acf json status >/dev/null 2>&1; then
|
||||||
|
echo "Synchronizing ACF JSON..."
|
||||||
|
wp acf json sync
|
||||||
|
else
|
||||||
|
echo "ACF $ACF_VERSION detected, but ACF JSON CLI is unavailable. Skipping."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "wp-config.php not found. Skipping database update."
|
||||||
|
fi
|
||||||
|
`;
|
||||||
|
|
||||||
|
await sshRun(context, ssh, script);
|
||||||
|
}
|
||||||
439
src/commands/sync.ts
Normal file
439
src/commands/sync.ts
Normal file
@@ -0,0 +1,439 @@
|
|||||||
|
import { existsSync, mkdirSync, readdirSync } from "node:fs";
|
||||||
|
import { basename, join } from "node:path";
|
||||||
|
import { consola } from "consola";
|
||||||
|
import prompts from "prompts";
|
||||||
|
import type { WPopContext } from "../lib/context";
|
||||||
|
import { readDeployEnv, type DeployEnv } from "../lib/env";
|
||||||
|
import { emitJson } from "../lib/output";
|
||||||
|
import { parseMarkedOutput, rsync, sshOutput } from "../lib/remote";
|
||||||
|
import { run } from "../lib/run";
|
||||||
|
import { createSshConfig, prepareSsh, sshArgs, sshTarget, type PreparedSsh } from "../lib/ssh";
|
||||||
|
|
||||||
|
const SYNC_COMPONENTS = ["database", "uploads", "plugins", "themes", "mu-plugins"] as const;
|
||||||
|
const DEFAULT_SYNC_COMPONENTS = ["database", "uploads"] as const;
|
||||||
|
const CONTENT_SYNC_COMPONENTS = ["uploads", "plugins", "themes", "mu-plugins"] as const;
|
||||||
|
const CODE_SYNC_COMPONENTS = ["plugins", "themes", "mu-plugins"] as const;
|
||||||
|
|
||||||
|
const REMOTE_LIST_BEGIN = "__WPOP_REMOTE_LIST_BEGIN__";
|
||||||
|
const REMOTE_LIST_END = "__WPOP_REMOTE_LIST_END__";
|
||||||
|
const REMOTE_OPTION_BEGIN = "__WPOP_REMOTE_OPTION_BEGIN__";
|
||||||
|
const REMOTE_OPTION_END = "__WPOP_REMOTE_OPTION_END__";
|
||||||
|
const REMOTE_DB_BEGIN = "__WPOP_REMOTE_DB_BEGIN__";
|
||||||
|
|
||||||
|
export type SyncComponent = (typeof SYNC_COMPONENTS)[number];
|
||||||
|
|
||||||
|
export type SyncOptions = {
|
||||||
|
include?: string;
|
||||||
|
skipSearchReplace?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SyncReport = {
|
||||||
|
command: "sync";
|
||||||
|
cwd: string;
|
||||||
|
include: SyncComponent[];
|
||||||
|
remote: { host: string; port: number; user: string; path: string };
|
||||||
|
cacheDir: string;
|
||||||
|
dryRun: boolean;
|
||||||
|
steps: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function sync(context: WPopContext, options: SyncOptions): Promise<void> {
|
||||||
|
const env = await readDeployEnv(process.env, { cwd: context.cwd });
|
||||||
|
const promptedForComponents = shouldPromptForComponents(context, options.include);
|
||||||
|
const components = await resolveComponents(context, options.include);
|
||||||
|
if (!components) {
|
||||||
|
consola.warn("Aborted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const report = buildReport(context, env, components);
|
||||||
|
|
||||||
|
if (context.json) {
|
||||||
|
if (!context.yes && !context.dryRun) {
|
||||||
|
throw new Error("--json requires --yes (cannot prompt for confirmation in JSON mode)");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
consola.info(
|
||||||
|
`Planning sync of ${[...components].join(", ")} from ${sshTargetSummary(env)}:${env.REMOTE_PATH}`,
|
||||||
|
);
|
||||||
|
if (context.dryRun) {
|
||||||
|
consola.info("Dry-run: no local changes will be made");
|
||||||
|
}
|
||||||
|
if (components.has("database")) {
|
||||||
|
consola.warn("This will OVERWRITE the local database.");
|
||||||
|
}
|
||||||
|
if (!promptedForComponents && !(await confirm(context, components, env))) {
|
||||||
|
consola.warn("Aborted.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ssh = await prepareSsh(context, createSshConfig(env), env.WPOP_CACHE_DIR);
|
||||||
|
|
||||||
|
await warnContentDrift(context, env, ssh, components);
|
||||||
|
|
||||||
|
let localSiteurl: string | undefined;
|
||||||
|
if (components.has("database") && !options.skipSearchReplace) {
|
||||||
|
localSiteurl = resolveLocalSiteurl(context) ?? (await readLocalOption(context, "siteurl"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const component of CONTENT_SYNC_COMPONENTS) {
|
||||||
|
if (components.has(component)) {
|
||||||
|
await syncContentComponent(context, env, ssh, component);
|
||||||
|
report.steps.push(`sync:${component}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (components.has("database")) {
|
||||||
|
await syncDatabase(context, env, ssh);
|
||||||
|
report.steps.push("db:import");
|
||||||
|
|
||||||
|
if (!options.skipSearchReplace) {
|
||||||
|
await runSearchReplace(context, ssh, env, localSiteurl);
|
||||||
|
report.steps.push("db:search-replace");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.json) {
|
||||||
|
emitJson(report);
|
||||||
|
} else {
|
||||||
|
consola.success("Sync complete.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReport(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
components: Set<SyncComponent>,
|
||||||
|
): SyncReport {
|
||||||
|
return {
|
||||||
|
command: "sync",
|
||||||
|
cwd: context.cwd,
|
||||||
|
include: [...components],
|
||||||
|
remote: {
|
||||||
|
host: env.REMOTE_HOST,
|
||||||
|
port: env.REMOTE_PORT,
|
||||||
|
user: env.REMOTE_USER,
|
||||||
|
path: env.REMOTE_PATH,
|
||||||
|
},
|
||||||
|
cacheDir: env.WPOP_CACHE_DIR,
|
||||||
|
dryRun: context.dryRun,
|
||||||
|
steps: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function sshTargetSummary(env: DeployEnv): string {
|
||||||
|
return `${env.REMOTE_USER}@${env.REMOTE_HOST}:${env.REMOTE_PORT}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldPromptForComponents(context: WPopContext, include?: string): boolean {
|
||||||
|
return !include && !context.yes && !context.dryRun && !context.json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveComponents(
|
||||||
|
context: WPopContext,
|
||||||
|
include?: string,
|
||||||
|
): Promise<Set<SyncComponent> | undefined> {
|
||||||
|
if (!shouldPromptForComponents(context, include)) {
|
||||||
|
return parseComponents(include);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await prompts({
|
||||||
|
type: "multiselect",
|
||||||
|
name: "components",
|
||||||
|
message: "Select sync components",
|
||||||
|
choices: SYNC_COMPONENTS.map((component) => ({
|
||||||
|
title: component,
|
||||||
|
value: component,
|
||||||
|
selected: DEFAULT_SYNC_COMPONENTS.includes(
|
||||||
|
component as (typeof DEFAULT_SYNC_COMPONENTS)[number],
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
instructions: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!Array.isArray(response.components)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (response.components.length === 0) {
|
||||||
|
throw new Error("Select at least one sync component");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(response.components as SyncComponent[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirm(
|
||||||
|
context: WPopContext,
|
||||||
|
components: Set<SyncComponent>,
|
||||||
|
env: DeployEnv,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (context.yes || context.dryRun) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await prompts({
|
||||||
|
type: "confirm",
|
||||||
|
name: "confirmed",
|
||||||
|
message: `Sync ${[...components].join(",")} from ${sshTargetSummary(env)}:${env.REMOTE_PATH} into ${context.cwd}?`,
|
||||||
|
initial: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(response.confirmed);
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseComponents(include?: string): Set<SyncComponent> {
|
||||||
|
if (!include) {
|
||||||
|
return new Set(DEFAULT_SYNC_COMPONENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (include === "all") {
|
||||||
|
return new Set(SYNC_COMPONENTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
const components = include
|
||||||
|
.split(",")
|
||||||
|
.map((component) => component.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const invalid = components.filter(
|
||||||
|
(component) => !SYNC_COMPONENTS.includes(component as SyncComponent),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (invalid.length > 0) {
|
||||||
|
throw new Error(`Invalid sync component(s): ${invalid.join(", ")}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Set(components as SyncComponent[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contentPath(component: (typeof CONTENT_SYNC_COMPONENTS)[number]): string {
|
||||||
|
return component === "uploads" ? "wp-content/uploads" : `wp-content/${component}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncContentComponent(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
component: (typeof CONTENT_SYNC_COMPONENTS)[number],
|
||||||
|
): Promise<void> {
|
||||||
|
const path = contentPath(component);
|
||||||
|
const localPath = join(context.cwd, path);
|
||||||
|
|
||||||
|
if (!context.dryRun) {
|
||||||
|
mkdirSync(localPath, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.info(`Synchronizing ${component} from remote`);
|
||||||
|
|
||||||
|
const args: string[] = [];
|
||||||
|
if (component !== "uploads") {
|
||||||
|
args.push("--delete");
|
||||||
|
}
|
||||||
|
args.push(`${sshTarget(ssh)}:${env.REMOTE_PATH}/${path}/`, `${localPath}/`);
|
||||||
|
|
||||||
|
await rsync(context, ssh, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function warnContentDrift(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
components: Set<SyncComponent>,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const component of CODE_SYNC_COMPONENTS) {
|
||||||
|
if (!components.has(component)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const localPath = join(context.cwd, contentPath(component));
|
||||||
|
const remote = await listRemoteTopLevelDirs(context, env, ssh, contentPath(component));
|
||||||
|
const local = listLocalTopLevelDirs(localPath);
|
||||||
|
const onlyLocal = local.filter((name) => !remote.includes(name));
|
||||||
|
|
||||||
|
if (onlyLocal.length > 0) {
|
||||||
|
consola.warn(
|
||||||
|
[
|
||||||
|
`Local ${component} contains entries that are not present on the remote:`,
|
||||||
|
...onlyLocal.map((name) => `- ${contentPath(component)}/${name}`),
|
||||||
|
"rsync --delete will remove them on sync.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listLocalTopLevelDirs(path: string): string[] {
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return readdirSync(path, { withFileTypes: true })
|
||||||
|
.filter((entry) => entry.isDirectory())
|
||||||
|
.map((entry) => entry.name)
|
||||||
|
.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listRemoteTopLevelDirs(
|
||||||
|
context: WPopContext,
|
||||||
|
env: DeployEnv,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
path: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const script = `set -e
|
||||||
|
printf '%s\\n' ${JSON.stringify(REMOTE_LIST_BEGIN)}
|
||||||
|
cd ${JSON.stringify(env.REMOTE_PATH)}
|
||||||
|
if [ -d ${JSON.stringify(path)} ]; then
|
||||||
|
find ${JSON.stringify(path)} -mindepth 1 -maxdepth 1 -type d -printf '%f\n' | sort
|
||||||
|
fi
|
||||||
|
printf '%s\\n' ${JSON.stringify(REMOTE_LIST_END)}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const { stdout } = await sshOutput(context, ssh, script);
|
||||||
|
if (context.dryRun) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseMarkedOutput(stdout, REMOTE_LIST_BEGIN, REMOTE_LIST_END, "remote directory listing");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function syncDatabase(context: WPopContext, env: DeployEnv, ssh: PreparedSsh): Promise<void> {
|
||||||
|
consola.info("Importing remote database into local");
|
||||||
|
|
||||||
|
const remoteDump = [
|
||||||
|
`cd ${JSON.stringify(env.REMOTE_PATH)}`,
|
||||||
|
`printf '%s\\n' ${JSON.stringify(REMOTE_DB_BEGIN)}`,
|
||||||
|
"wp db export --skip-plugins --skip-themes --single-transaction --quick --default-character-set=utf8mb4 -",
|
||||||
|
].join(" && ");
|
||||||
|
|
||||||
|
const pipeline = [
|
||||||
|
`ssh ${sshArgs(ssh).join(" ")} ${sshTarget(ssh)} ${JSON.stringify(remoteDump)}`,
|
||||||
|
`sed -n '/^${REMOTE_DB_BEGIN}$/,$p' | sed '1d'`,
|
||||||
|
`wp --path=${JSON.stringify(context.cwd)} db import --skip-plugins --skip-themes -`,
|
||||||
|
].join(" | ");
|
||||||
|
|
||||||
|
await run(context, "bash", ["-o", "pipefail", "-c", pipeline]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runSearchReplace(
|
||||||
|
context: WPopContext,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
env: DeployEnv,
|
||||||
|
localSiteurl: string | undefined,
|
||||||
|
): Promise<void> {
|
||||||
|
const remoteSiteurl = await readRemoteOption(context, ssh, env, "siteurl");
|
||||||
|
const remoteHome = await readRemoteOption(context, ssh, env, "home");
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
consola.info("[dry-run] Would run wp search-replace for siteurl and home");
|
||||||
|
await run(context, "wp", ["search-replace", "<REMOTE_URL>", "<LOCAL_URL>", "--all-tables"], {
|
||||||
|
cwd: context.cwd,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!localSiteurl) {
|
||||||
|
consola.warn("Could not determine local siteurl; skipping search-replace");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!remoteSiteurl) {
|
||||||
|
consola.warn("Could not determine remote siteurl; skipping search-replace");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (remoteSiteurl === localSiteurl) {
|
||||||
|
consola.info(`Local and remote siteurl match (${localSiteurl}); skipping search-replace`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.info(`Rewriting URLs: ${remoteSiteurl} -> ${localSiteurl}`);
|
||||||
|
await run(
|
||||||
|
context,
|
||||||
|
"wp",
|
||||||
|
[
|
||||||
|
"search-replace",
|
||||||
|
remoteSiteurl,
|
||||||
|
localSiteurl,
|
||||||
|
"--all-tables",
|
||||||
|
"--report-changed-only",
|
||||||
|
"--skip-columns=guid",
|
||||||
|
],
|
||||||
|
{ cwd: context.cwd },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (remoteHome && remoteHome !== remoteSiteurl) {
|
||||||
|
const localHome = localSiteurl;
|
||||||
|
consola.info(`Rewriting home URLs: ${remoteHome} -> ${localHome}`);
|
||||||
|
await run(
|
||||||
|
context,
|
||||||
|
"wp",
|
||||||
|
[
|
||||||
|
"search-replace",
|
||||||
|
remoteHome,
|
||||||
|
localHome,
|
||||||
|
"--all-tables",
|
||||||
|
"--report-changed-only",
|
||||||
|
"--skip-columns=guid",
|
||||||
|
],
|
||||||
|
{ cwd: context.cwd },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await run(context, "wp", ["cache", "flush"], { cwd: context.cwd });
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveLocalSiteurl(context: WPopContext): string | undefined {
|
||||||
|
const protocol = process.env.WEBSIMPLE_STACK_PROTOCOL?.trim();
|
||||||
|
const domain = process.env.WEBSIMPLE_STACK_DOMAIN?.trim();
|
||||||
|
if (!protocol || !domain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const subdomain = basename(context.cwd);
|
||||||
|
if (!subdomain) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return `${protocol}://${subdomain}.${domain}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readLocalOption(context: WPopContext, option: string): Promise<string | undefined> {
|
||||||
|
const result = await run(context, "wp", ["option", "get", option], {
|
||||||
|
capture: true,
|
||||||
|
cwd: context.cwd,
|
||||||
|
reject: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.stdout.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readRemoteOption(
|
||||||
|
context: WPopContext,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
env: DeployEnv,
|
||||||
|
option: string,
|
||||||
|
): Promise<string | undefined> {
|
||||||
|
const script = `printf '%s\\n' ${JSON.stringify(REMOTE_OPTION_BEGIN)}
|
||||||
|
cd ${JSON.stringify(env.REMOTE_PATH)} && wp option get ${JSON.stringify(option)} || true
|
||||||
|
printf '%s\\n' ${JSON.stringify(REMOTE_OPTION_END)}
|
||||||
|
`;
|
||||||
|
const { stdout } = await sshOutput(context, ssh, script);
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = parseMarkedOutput(
|
||||||
|
stdout,
|
||||||
|
REMOTE_OPTION_BEGIN,
|
||||||
|
REMOTE_OPTION_END,
|
||||||
|
`remote option ${option}`,
|
||||||
|
);
|
||||||
|
return lines.join("\n").trim() || undefined;
|
||||||
|
}
|
||||||
19
src/lib/context.ts
Normal file
19
src/lib/context.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
export type WPopContext = {
|
||||||
|
cwd: string;
|
||||||
|
dryRun: boolean;
|
||||||
|
json: boolean;
|
||||||
|
yes: boolean;
|
||||||
|
verbose: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createContext(options: Partial<WPopContext>): WPopContext {
|
||||||
|
return {
|
||||||
|
cwd: resolve(options.cwd ?? process.cwd()),
|
||||||
|
dryRun: Boolean(options.dryRun),
|
||||||
|
json: Boolean(options.json),
|
||||||
|
yes: Boolean(options.yes),
|
||||||
|
verbose: Boolean(options.verbose),
|
||||||
|
};
|
||||||
|
}
|
||||||
278
src/lib/env.ts
Normal file
278
src/lib/env.ts
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { consola } from "consola";
|
||||||
|
import { execa } from "execa";
|
||||||
|
|
||||||
|
const deployEnvSchema = z.object({
|
||||||
|
REMOTE_HOST: z.string().min(1),
|
||||||
|
REMOTE_USER: z.string().min(1),
|
||||||
|
REMOTE_PATH: z.string().min(1),
|
||||||
|
REMOTE_PORT: z.coerce.number().int().positive().default(22),
|
||||||
|
SSH_PRIVATE_KEY: z.string().optional(),
|
||||||
|
WPOP_CACHE_DIR: z.string().default("/tmp/wpop"),
|
||||||
|
WP_VERSION: z.string().default("latest"),
|
||||||
|
WP_LOCALE: z.string().default("fr_CA"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const remoteEnvKeys = ["REMOTE_HOST", "REMOTE_PORT", "REMOTE_USER", "REMOTE_PATH"] as const;
|
||||||
|
const requiredRemoteEnvKeys = ["REMOTE_HOST", "REMOTE_USER", "REMOTE_PATH"] as const;
|
||||||
|
const giteaVariableKeys = [
|
||||||
|
"REMOTE_HOST",
|
||||||
|
"REMOTE_PORT",
|
||||||
|
"REMOTE_USER",
|
||||||
|
"REMOTE_PATH",
|
||||||
|
"WP_SITE_URL",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type DeployEnv = z.infer<typeof deployEnvSchema>;
|
||||||
|
|
||||||
|
type GiteaVariableKey = (typeof giteaVariableKeys)[number];
|
||||||
|
|
||||||
|
type GiteaRepo = {
|
||||||
|
owner: string;
|
||||||
|
repo: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GiteaVariable = {
|
||||||
|
name?: string;
|
||||||
|
data?: string;
|
||||||
|
value?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function readDeployEnv(
|
||||||
|
env: NodeJS.ProcessEnv = process.env,
|
||||||
|
options: { cwd?: string } = {},
|
||||||
|
): Promise<DeployEnv> {
|
||||||
|
const normalizedEnv = normalizeEnv(env);
|
||||||
|
const missingRemoteKeys = remoteEnvKeys.filter((key) => !hasValue(normalizedEnv[key]));
|
||||||
|
|
||||||
|
if (missingRemoteKeys.length > 0) {
|
||||||
|
const giteaEnv = await readGiteaDeployEnv(normalizedEnv, options.cwd);
|
||||||
|
for (const key of giteaVariableKeys) {
|
||||||
|
if (!hasValue(normalizedEnv[key]) && hasValue(giteaEnv[key])) {
|
||||||
|
normalizedEnv[key] = giteaEnv[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const missingRequiredKeys = requiredRemoteEnvKeys.filter((key) => !hasValue(normalizedEnv[key]));
|
||||||
|
if (missingRequiredKeys.length > 0) {
|
||||||
|
throw new Error(
|
||||||
|
[
|
||||||
|
`Missing deploy environment variable(s): ${missingRequiredKeys.join(", ")}`,
|
||||||
|
"Set them directly or provide WEBSIMPLE_GITEA_API_TOKEN so wpop can read Gitea Actions variables.",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return deployEnvSchema.parse(normalizedEnv);
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeEnv(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
|
||||||
|
const normalized = { ...env };
|
||||||
|
for (const key of [...remoteEnvKeys, "WP_SITE_URL"] as const) {
|
||||||
|
if (typeof normalized[key] === "string" && normalized[key].trim() === "") {
|
||||||
|
delete normalized[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readGiteaDeployEnv(
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
cwd = process.cwd(),
|
||||||
|
): Promise<Partial<Record<GiteaVariableKey, string>>> {
|
||||||
|
const token = getGiteaToken(env);
|
||||||
|
if (!token) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const repo = await resolveGiteaRepo(env, cwd);
|
||||||
|
if (!repo) {
|
||||||
|
consola.warn("Could not infer Gitea repository from git origin; skipping Gitea env lookup");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.info(
|
||||||
|
`Reading deploy environment from Gitea Actions variables (${repo.owner}/${repo.repo})`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseUrl = getGiteaBaseUrl(env);
|
||||||
|
const [orgVariables, repoVariables] = await Promise.all([
|
||||||
|
fetchGiteaVariables(
|
||||||
|
baseUrl,
|
||||||
|
token,
|
||||||
|
`/api/v1/orgs/${encodeURIComponent(repo.owner)}/actions/variables`,
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
fetchGiteaVariables(
|
||||||
|
baseUrl,
|
||||||
|
token,
|
||||||
|
`/api/v1/repos/${encodeURIComponent(repo.owner)}/${encodeURIComponent(repo.repo)}/actions/variables`,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
...variablesToEnv(orgVariables),
|
||||||
|
...variablesToEnv(repoVariables),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGiteaToken(env: NodeJS.ProcessEnv): string | undefined {
|
||||||
|
return firstValue(env.WPOP_GITEA_TOKEN, env.WEBSIMPLE_GITEA_API_TOKEN, env.GITEA_TOKEN);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getGiteaBaseUrl(env: NodeJS.ProcessEnv): string {
|
||||||
|
return firstValue(env.WPOP_GITEA_BASE_URL, env.GITEA_BASE_URL) ?? "https://gitea.websimple.com";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveGiteaRepo(
|
||||||
|
env: NodeJS.ProcessEnv,
|
||||||
|
cwd: string,
|
||||||
|
): Promise<GiteaRepo | undefined> {
|
||||||
|
const explicit = parseRepoSpec(env.WPOP_GITEA_REPO);
|
||||||
|
if (explicit) {
|
||||||
|
return explicit;
|
||||||
|
}
|
||||||
|
|
||||||
|
const explicitRepo = firstValue(env.WPOP_GITEA_REPO_NAME, env.GITEA_REPOSITORY);
|
||||||
|
if (explicitRepo) {
|
||||||
|
const owner = firstValue(env.WPOP_GITEA_OWNER, env.GITEA_REPOSITORY_OWNER) ?? "wp-sites";
|
||||||
|
return parseRepoSpec(explicitRepo) ?? { owner, repo: trimGitSuffix(explicitRepo) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const originUrl = await readGitOriginUrl(cwd);
|
||||||
|
if (!originUrl) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseGiteaRemoteUrl(originUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readGitOriginUrl(cwd: string): Promise<string | undefined> {
|
||||||
|
const result = await execa("git", ["remote", "get-url", "origin"], {
|
||||||
|
cwd,
|
||||||
|
reject: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.stdout.trim() || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRepoSpec(spec: string | undefined): GiteaRepo | undefined {
|
||||||
|
const value = firstValue(spec);
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [owner, repo] = value.split("/", 2);
|
||||||
|
if (!owner || !repo) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { owner, repo: trimGitSuffix(repo) };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGiteaRemoteUrl(remoteUrl: string): GiteaRepo | undefined {
|
||||||
|
try {
|
||||||
|
const url = new URL(remoteUrl);
|
||||||
|
if (url.hostname === "gitea.websimple.com") {
|
||||||
|
return parseRepoSpec(url.pathname.replace(/^\/+/, ""));
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Not a URL-form remote; try scp-style below.
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlMatch = remoteUrl.match(
|
||||||
|
/gitea\.websimple\.com[:/](?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/,
|
||||||
|
);
|
||||||
|
if (urlMatch?.groups) {
|
||||||
|
return {
|
||||||
|
owner: urlMatch.groups.owner,
|
||||||
|
repo: trimGitSuffix(urlMatch.groups.repo),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchGiteaVariables(
|
||||||
|
baseUrl: string,
|
||||||
|
token: string,
|
||||||
|
path: string,
|
||||||
|
optional: boolean,
|
||||||
|
): Promise<GiteaVariable[]> {
|
||||||
|
const variables: GiteaVariable[] = [];
|
||||||
|
|
||||||
|
for (let page = 1; page <= 100; page += 1) {
|
||||||
|
const url = new URL(path, baseUrl);
|
||||||
|
url.searchParams.set("page", String(page));
|
||||||
|
url.searchParams.set("limit", "100");
|
||||||
|
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
Authorization: `token ${token}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.status === 404 && optional) {
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Gitea API request failed (${response.status}) while reading ${path}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pageVariables = (await response.json()) as GiteaVariable[];
|
||||||
|
variables.push(...pageVariables);
|
||||||
|
|
||||||
|
if (pageVariables.length < 100) {
|
||||||
|
return variables;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Gitea Actions variables pagination exceeded safety limit");
|
||||||
|
}
|
||||||
|
|
||||||
|
function variablesToEnv(variables: GiteaVariable[]): Partial<Record<GiteaVariableKey, string>> {
|
||||||
|
const env: Partial<Record<GiteaVariableKey, string>> = {};
|
||||||
|
for (const variable of variables) {
|
||||||
|
if (!isGiteaVariableKey(variable.name)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = firstValue(variable.data, variable.value);
|
||||||
|
if (value) {
|
||||||
|
env[variable.name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGiteaVariableKey(value: string | undefined): value is GiteaVariableKey {
|
||||||
|
return giteaVariableKeys.includes(value as GiteaVariableKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
function firstValue(...values: (string | undefined)[]): string | undefined {
|
||||||
|
for (const value of values) {
|
||||||
|
const trimmed = value?.trim();
|
||||||
|
if (trimmed) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasValue(value: string | undefined): boolean {
|
||||||
|
return Boolean(firstValue(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimGitSuffix(value: string): string {
|
||||||
|
return value.endsWith(".git") ? value.slice(0, -4) : value;
|
||||||
|
}
|
||||||
14
src/lib/output.ts
Normal file
14
src/lib/output.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { consola } from "consola";
|
||||||
|
import type { WPopContext } from "./context";
|
||||||
|
|
||||||
|
export function configureLogger(context: WPopContext): void {
|
||||||
|
if (context.json) {
|
||||||
|
consola.level = -999;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
consola.level = context.verbose ? 4 : 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emitJson(payload: unknown): void {
|
||||||
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
|
||||||
|
}
|
||||||
129
src/lib/remote.ts
Normal file
129
src/lib/remote.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import type { WPopContext } from "./context";
|
||||||
|
import { run } from "./run";
|
||||||
|
import { type PreparedSsh, rsyncSshShell, sshArgs, sshTarget } from "./ssh";
|
||||||
|
|
||||||
|
const SSH_RUN_BEGIN = "__WPOP_SSH_RUN_BEGIN__";
|
||||||
|
const SSH_RUN_END = "__WPOP_SSH_RUN_END__";
|
||||||
|
|
||||||
|
export async function rsync(context: WPopContext, ssh: PreparedSsh, args: string[]): Promise<void> {
|
||||||
|
const result = await run(context, "rsync", ["-az", "-e", rsyncSshShell(ssh), ...args], {
|
||||||
|
capture: true,
|
||||||
|
reject: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = filterRemoteBanner([result.stdout, result.stderr].filter(Boolean).join("\n"));
|
||||||
|
if (context.verbose && output) {
|
||||||
|
process.stdout.write(`${output}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
if (!context.verbose && output) {
|
||||||
|
process.stderr.write(`${output}\n`);
|
||||||
|
}
|
||||||
|
throw new Error(`rsync failed with exit code ${result.exitCode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sshRun(
|
||||||
|
context: WPopContext,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
script: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await run(context, "ssh", [...sshArgs(ssh), sshTarget(ssh)], {
|
||||||
|
stdin: wrapRemoteScriptOutput(script),
|
||||||
|
capture: true,
|
||||||
|
reject: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = parseMarkedOutput(
|
||||||
|
result.stdout,
|
||||||
|
SSH_RUN_BEGIN,
|
||||||
|
SSH_RUN_END,
|
||||||
|
"remote command output",
|
||||||
|
);
|
||||||
|
if (context.verbose && output.length > 0) {
|
||||||
|
process.stdout.write(`${output.join("\n")}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.exitCode !== 0) {
|
||||||
|
if (!context.verbose && output.length > 0) {
|
||||||
|
process.stderr.write(`${output.join("\n")}\n`);
|
||||||
|
}
|
||||||
|
throw new Error(`Remote SSH command failed with exit code ${result.exitCode}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sshOutput(
|
||||||
|
context: WPopContext,
|
||||||
|
ssh: PreparedSsh,
|
||||||
|
script: string,
|
||||||
|
): Promise<{ stdout: string }> {
|
||||||
|
return run(context, "ssh", [...sshArgs(ssh), sshTarget(ssh)], {
|
||||||
|
stdin: script,
|
||||||
|
capture: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseMarkedOutput(
|
||||||
|
stdout: string,
|
||||||
|
beginMarker: string,
|
||||||
|
endMarker: string,
|
||||||
|
label: string,
|
||||||
|
): string[] {
|
||||||
|
const output = stdout.split("\n").map((line) => line.trim());
|
||||||
|
const begin = output.indexOf(beginMarker);
|
||||||
|
const end = output.indexOf(endMarker);
|
||||||
|
|
||||||
|
if (begin === -1 || end === -1 || end < begin) {
|
||||||
|
throw new Error(`Could not parse ${label} from SSH output`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return output.slice(begin + 1, end).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapRemoteScriptOutput(script: string): string {
|
||||||
|
return `printf '%s\\n' ${JSON.stringify(SSH_RUN_BEGIN)}
|
||||||
|
(
|
||||||
|
${script}
|
||||||
|
) 2>&1
|
||||||
|
status=$?
|
||||||
|
printf '%s\\n' ${JSON.stringify(SSH_RUN_END)}
|
||||||
|
exit "$status"
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRemoteBanner(output: string): string {
|
||||||
|
const lines = output.split("\n");
|
||||||
|
const filtered: string[] = [];
|
||||||
|
let skippingBanner = false;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.includes("This server is managed by Ansible and Cloud-init.")) {
|
||||||
|
skippingBanner = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skippingBanner) {
|
||||||
|
if (line.trim().startsWith("Last deployment:")) {
|
||||||
|
skippingBanner = false;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
filtered.push(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
.map((line) => line.trimEnd())
|
||||||
|
.filter((line, index, all) => line.trim() || (index > 0 && index < all.length - 1))
|
||||||
|
.join("\n")
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
85
src/lib/run.ts
Normal file
85
src/lib/run.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { consola } from "consola";
|
||||||
|
import { execa, type ExecaError } from "execa";
|
||||||
|
import type { WPopContext } from "./context";
|
||||||
|
|
||||||
|
export type RunOptions = {
|
||||||
|
cwd?: string;
|
||||||
|
env?: NodeJS.ProcessEnv;
|
||||||
|
reject?: boolean;
|
||||||
|
stdin?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CaptureOptions = RunOptions & { capture: true };
|
||||||
|
|
||||||
|
export type CaptureResult = { exitCode: number; stderr: string; stdout: string };
|
||||||
|
|
||||||
|
export async function run(
|
||||||
|
context: WPopContext,
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options?: RunOptions,
|
||||||
|
): Promise<void>;
|
||||||
|
export async function run(
|
||||||
|
context: WPopContext,
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options: CaptureOptions,
|
||||||
|
): Promise<CaptureResult>;
|
||||||
|
export async function run(
|
||||||
|
context: WPopContext,
|
||||||
|
command: string,
|
||||||
|
args: string[],
|
||||||
|
options: RunOptions | CaptureOptions = {},
|
||||||
|
): Promise<void | CaptureResult> {
|
||||||
|
const printable = [command, ...args].join(" ");
|
||||||
|
const capture = "capture" in options && options.capture === true;
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
consola.info(`[dry-run] ${printable}`);
|
||||||
|
return capture ? { exitCode: 0, stderr: "", stdout: "" } : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.debug(printable);
|
||||||
|
|
||||||
|
if (capture) {
|
||||||
|
const result = await execa(command, args, {
|
||||||
|
cwd: options.cwd ?? context.cwd,
|
||||||
|
env: options.env,
|
||||||
|
input: options.stdin,
|
||||||
|
reject: options.reject,
|
||||||
|
});
|
||||||
|
return { exitCode: result.exitCode ?? 0, stderr: result.stderr, stdout: result.stdout };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.verbose) {
|
||||||
|
await execa(command, args, {
|
||||||
|
cwd: options.cwd ?? context.cwd,
|
||||||
|
env: options.env,
|
||||||
|
input: options.stdin,
|
||||||
|
stdio: options.stdin ? ["pipe", "inherit", "inherit"] : "inherit",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await execa(command, args, {
|
||||||
|
cwd: options.cwd ?? context.cwd,
|
||||||
|
env: options.env,
|
||||||
|
input: options.stdin,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
printProcessFailureOutput(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printProcessFailureOutput(error: unknown): void {
|
||||||
|
const processError = error as Partial<ExecaError>;
|
||||||
|
const output = [processError.stdout, processError.stderr]
|
||||||
|
.filter((value): value is string => typeof value === "string" && value.trim().length > 0)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
process.stderr.write(`${output}\n`);
|
||||||
|
}
|
||||||
|
}
|
||||||
152
src/lib/ssh.ts
Normal file
152
src/lib/ssh.ts
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { consola } from "consola";
|
||||||
|
import { execa } from "execa";
|
||||||
|
import type { WPopContext } from "./context";
|
||||||
|
import type { DeployEnv } from "./env";
|
||||||
|
import { run } from "./run";
|
||||||
|
import { createTempDir } from "./tempdir";
|
||||||
|
|
||||||
|
export type SshConfig = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
privateKey?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PreparedSsh = {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
user: string;
|
||||||
|
identityFile?: string;
|
||||||
|
knownHostsFile: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createSshConfig(env: DeployEnv): SshConfig {
|
||||||
|
return {
|
||||||
|
host: env.REMOTE_HOST,
|
||||||
|
port: env.REMOTE_PORT,
|
||||||
|
user: env.REMOTE_USER,
|
||||||
|
privateKey: env.SSH_PRIVATE_KEY,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function prepareSsh(
|
||||||
|
context: WPopContext,
|
||||||
|
config: SshConfig,
|
||||||
|
cacheDir: string,
|
||||||
|
): Promise<PreparedSsh> {
|
||||||
|
const knownHostsFile = ensureKnownHostsFile(context, cacheDir);
|
||||||
|
|
||||||
|
const prepared: PreparedSsh = {
|
||||||
|
host: config.host,
|
||||||
|
port: config.port,
|
||||||
|
user: config.user,
|
||||||
|
knownHostsFile,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (config.privateKey) {
|
||||||
|
prepared.identityFile = writePrivateKey(context, config.privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
await ensureKnownHost(context, prepared);
|
||||||
|
await verifySshAuth(context, prepared);
|
||||||
|
|
||||||
|
return prepared;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sshTarget(config: PreparedSsh): string {
|
||||||
|
return `${config.user}@${config.host}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function sshArgs(config: PreparedSsh): string[] {
|
||||||
|
const args = [
|
||||||
|
"-T",
|
||||||
|
"-p",
|
||||||
|
String(config.port),
|
||||||
|
"-o",
|
||||||
|
`UserKnownHostsFile=${config.knownHostsFile}`,
|
||||||
|
"-o",
|
||||||
|
"GlobalKnownHostsFile=/dev/null",
|
||||||
|
"-o",
|
||||||
|
"LogLevel=ERROR",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (config.identityFile) {
|
||||||
|
args.push("-i", config.identityFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function rsyncSshShell(config: PreparedSsh): string {
|
||||||
|
return ["ssh", ...sshArgs(config)].join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureKnownHostsFile(context: WPopContext, cacheDir: string): string {
|
||||||
|
const path = join(cacheDir, "known_hosts");
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdirSync(cacheDir, { recursive: true });
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
writeFileSync(path, "", { mode: 0o600 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
function writePrivateKey(context: WPopContext, privateKey: string): string {
|
||||||
|
consola.info("Using SSH private key from SSH_PRIVATE_KEY");
|
||||||
|
|
||||||
|
const { path: keyDir, cleanup } = createTempDir(context, "wpop-ssh");
|
||||||
|
|
||||||
|
if (context.dryRun) {
|
||||||
|
return join(keyDir, "id_ed25519");
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyPath = join(keyDir, "id_ed25519");
|
||||||
|
writeFileSync(keyPath, privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`, {
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
|
||||||
|
process.once("exit", cleanup);
|
||||||
|
|
||||||
|
return keyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureKnownHost(context: WPopContext, config: PreparedSsh): Promise<void> {
|
||||||
|
if (context.dryRun) {
|
||||||
|
consola.info(`Ensuring SSH host key for ${config.host} is known`);
|
||||||
|
await run(context, "ssh-keyscan", ["-p", String(config.port), "-H", config.host]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await hostInKnownHosts(config)) {
|
||||||
|
consola.debug(`Host ${config.host}:${config.port} already in known_hosts`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
consola.info(`Scanning SSH host key for ${config.host}`);
|
||||||
|
const { stdout } = await execa("ssh-keyscan", ["-p", String(config.port), "-H", config.host]);
|
||||||
|
appendFileSync(config.knownHostsFile, `${stdout}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function hostInKnownHosts(config: PreparedSsh): Promise<boolean> {
|
||||||
|
if (!existsSync(config.knownHostsFile)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const lookup = config.port === 22 ? config.host : `[${config.host}]:${config.port}`;
|
||||||
|
const result = await execa("ssh-keygen", ["-F", lookup, "-f", config.knownHostsFile], {
|
||||||
|
reject: false,
|
||||||
|
});
|
||||||
|
return result.exitCode === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifySshAuth(context: WPopContext, config: PreparedSsh): Promise<void> {
|
||||||
|
consola.info(`Verifying SSH access to ${sshTarget(config)}`);
|
||||||
|
|
||||||
|
await run(context, "ssh", [...sshArgs(config), "-o", "BatchMode=yes", sshTarget(config), "true"]);
|
||||||
|
}
|
||||||
24
src/lib/tempdir.ts
Normal file
24
src/lib/tempdir.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import { mkdtempSync, rmSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import type { WPopContext } from "./context";
|
||||||
|
|
||||||
|
export type TempDir = {
|
||||||
|
path: string;
|
||||||
|
cleanup: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createTempDir(context: WPopContext, prefix: string): TempDir {
|
||||||
|
if (context.dryRun) {
|
||||||
|
return {
|
||||||
|
path: join(tmpdir(), prefix),
|
||||||
|
cleanup: () => {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = mkdtempSync(join(tmpdir(), `${prefix}-`));
|
||||||
|
return {
|
||||||
|
path,
|
||||||
|
cleanup: () => rmSync(path, { force: true, recursive: true }),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2023",
|
"target": "ES2023",
|
||||||
"lib": ["ES2023"],
|
"lib": ["ES2023"],
|
||||||
"module": "NodeNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "NodeNext",
|
"moduleResolution": "Bundler",
|
||||||
"types": ["node"],
|
"types": ["node"],
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
@@ -12,8 +12,9 @@
|
|||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"sourceMap": true,
|
"sourceMap": true,
|
||||||
"rootDir": "src",
|
"rootDir": ".",
|
||||||
"outDir": "dist"
|
"outDir": "dist",
|
||||||
|
"noEmit": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts"]
|
"include": ["src/**/*.ts", "tsdown.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
10
tsdown.config.ts
Normal file
10
tsdown.config.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { defineConfig } from "tsdown";
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
entry: ["src/cli.ts"],
|
||||||
|
format: "esm",
|
||||||
|
target: "node24",
|
||||||
|
dts: true,
|
||||||
|
clean: true,
|
||||||
|
platform: "node",
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user