1
0
mirror of https://gitcode.com/github-mirrors/react-native-update-cli.git synced 2025-09-16 09:41:38 +08:00
Code Issues Packages Projects Releases Wiki Activity GitHub Gitee

Compare commits

...

163 Commits

Author SHA1 Message Date
sunnylqm
fd46bafb98 Update version to 2.1.3 in package.json; set baseUrl in .swcrc; refactor API query logic to use dynamic base URL and update defaultEndpoints in constants.ts 2025-09-14 17:25:35 +08:00
sunny.ll
917f99f2ab Fix package selection logic to match by package name instead of version; improve error handling for package retrieval. 2025-09-10 17:11:12 +08:00
sunny.ll
ac13464d4d Enhance package command options to include packageId and packageVersion; improve package selection logic with error handling for missing packages. 2025-09-10 16:18:34 +08:00
sunny.ll
f82bab887a Remove console log for module registration in module-manager.ts 2025-09-10 15:38:56 +08:00
sunny.ll
07ff19fbe5 Update version to 2.1.1 in package.json and improve error handling for package loading in dep-versions.ts 2025-09-10 11:05:24 +08:00
sunny.ll
a4467717bc Add deletePackage command with confirmation and error handling; update version to 2.1.0 and enhance localization for delete operations 2025-09-09 12:34:06 +08:00
Sunny Luo
cd890347d1 Update package.json 2025-08-26 09:50:25 +08:00
sunnylqm
e6de3eeef3 Enhance upload commands to support custom versioning and update documentation. Added version option to uploadIpa, uploadApk, and uploadApp commands. Updated README and localization files to reflect changes. 2025-08-22 10:09:37 +08:00
sunnylqm
7e7e555450 Update package.json for version 2.0.0, change repository and issue URLs, and format files and keywords for better readability. 2025-07-30 14:34:43 +08:00
波仔糕
e98bcf504f cli modular refactor (#16)
* add logic to support SENTRY_PROPERTIES parameter

* remove update.json and meta.json files in ppk

* udpapte

* refactor modles

* update

* add package-module file

* update

* update readme file

* modifu cli.json file

* fix command issues

* improve version workflow logic

* udpate

* update

* update

* update

* udpate

* udpate

* add example

* update readme file

* udpate version

* change logic to use pushy command uniformly
2025-07-24 11:46:20 +08:00
Sunny Luo
4cb5f7fa4e Update claude.yml 2025-07-22 14:58:58 +08:00
Sunny Luo
13ab10f53f Create claude.yml 2025-07-22 14:44:44 +08:00
Sunny Luo
6db237fc8c bump 1.46.2 2025-06-28 17:13:49 +08:00
波仔糕
bd7d78e7ac remove update.json and meta.json files in ppk (#15)
* add logic to support SENTRY_PROPERTIES parameter

* remove update.json and meta.json files in ppk

* udpapte
2025-06-28 17:13:13 +08:00
sunnylqm
91a602979d Refactor command exports to use specific command groups for app, bundle, package, user, and version modules 2025-06-23 11:30:36 +08:00
sunnylqm
2cfe451db5 one in all command 2025-06-20 20:32:06 +08:00
Sunny Luo
c7d0aadbf2 Update package.json 2025-06-15 18:19:17 +08:00
波仔糕
2e37d6f1c7 add logic to support SENTRY_PROPERTIES parameter (#14) 2025-06-15 10:10:04 +08:00
sunnylqm
6e7d5de82e Update version to 1.45.5, clean up imports in package.ts, and enhance localization for rollout and version messages in English and Chinese locales 2025-05-16 10:36:59 +08:00
sunnylqm
3266f09644 Update version to 1.45.4, add dryRun option in cli.json and versions.ts, and enhance localization for dryRun messages 2025-05-16 09:32:46 +08:00
sunnylqm
848f528625 Rename semverRange to packageVersionRange in cli.json and versions.ts for consistency 2025-05-15 22:48:15 +08:00
sunnylqm
18d772b1bc fix version compare 2025-05-15 22:45:08 +08:00
sunnylqm
bf0cea66b9 Update version to 1.45.1 and add semverRange property in cli.json 2025-05-15 22:29:57 +08:00
sunnylqm
8c31ee5762 semver range 2025-05-15 22:13:42 +08:00
sunnylqm
002e8662d6 Update version to 1.44.7 and modify import in zip.js to use readEntry instead of readEntire 2025-05-15 11:58:36 +08:00
sunnylqm
ebfb9dd4b9 Update version to 1.44.6, add Node.js import protocol setting, and clean up import statements across multiple files 2025-05-14 16:08:27 +08:00
sunnylqm
59c5be2719 inline latest version code 2025-05-07 14:51:32 +08:00
sunnylqm
2e8e242e75 fix latestverion timeout 2025-05-07 13:20:56 +08:00
sunnylqm
0cfb1a9beb Update i18n strings to include whitespace in file size exceeded messages for English and Chinese locales 2025-04-23 20:55:33 +08:00
波仔糕
b22c598ca4 add paramaters for bundle command to auto publish (#12)
* add paramaters for bundle command to auto publish

* Update bundle.ts

---------

Co-authored-by: Sunny Luo <sunnylqm@gmail.com>
2025-04-23 11:19:26 +08:00
sunnylqm
573ab60e44 fix i18n 2025-04-20 00:04:18 +08:00
sunnylqm
4613da1fbe Update version to 1.44.1 and adjust i18n strings for lock file detection in English and Chinese locales 2025-04-16 14:37:54 +08:00
sunnylqm
f2f33a190d cleanup 2025-04-16 07:51:27 +08:00
sunnylqm
cc6c1aae79 check lockfile in workspaces 2025-04-15 22:39:01 +08:00
sunnylqm
91b26ebc87 fix some output 2025-04-15 17:40:31 +08:00
波仔糕
9cb8d458ad add logic to support multi language (#11)
* add logic to support multi language

* Update en.ts

* Update en.ts

---------

Co-authored-by: Sunny Luo <sunnylqm@gmail.com>
2025-04-13 17:02:52 +08:00
sunnylqm
d853918d8d Update i18n strings to include whitespace in total apps count for English and Chinese locales 2025-04-12 00:38:04 +08:00
sunnylqm
c4a34490e3 fix versions command 2025-04-11 16:09:33 +08:00
sunnylqm
e2737a29eb Fix comparison operator in package version retrieval in versions.ts 2025-04-10 21:53:29 +08:00
sunnylqm
9b55610675 Update version to 1.43.4 and add 'txt.map' to ignored packing extensions 2025-04-10 21:43:04 +08:00
sunnylqm
5d1466ddec Update i18n strings to include whitespace control in ppkPackageGenerated messages for English and Chinese locales 2025-04-10 21:22:59 +08:00
sunnylqm
081d54ba63 update deps 2025-04-09 11:42:30 +08:00
sunnylqm
4326c08d79 downgrade commander 2025-04-09 10:52:38 +08:00
sunnylqm
20010a9ea6 Update version to 1.43.1 and handle optional chaining for origin URL in getCommitInfo function 2025-04-07 19:56:17 +08:00
sunnylqm
36220a48aa Update package description for clarity and specificity 2025-04-02 08:39:26 +08:00
sunnylqm
d281d72737 check lockfiles 2025-04-02 08:27:05 +08:00
sunnylqm
d351243ab7 check lockfile 2025-04-01 23:24:02 +08:00
sunnylqm
fbdacfffd2 add gitignore check 2025-04-01 23:13:20 +08:00
sunnylqm
b725d2b08e i18n 2025-03-30 23:40:28 +08:00
sunnylqm
3f1b43e38e update i18n 2025-03-25 09:36:58 +08:00
sunnylqm
328b1f5447 update i18n 2025-03-24 23:16:09 +08:00
sunnylqm
ef7f6a2087 Swap React Native CLI paths based on version compatibility in runReactNativeBundleCommand function 2025-03-17 11:25:11 +08:00
sunnylqm
d2798d3177 Remove cache usage in getLatestVersion function for improved reliability 2025-03-16 11:01:10 +08:00
sunnylqm
b0a8fc928a fix format 2025-03-15 19:08:34 +08:00
sunnylqm
a559140b72 fix number format 2025-03-15 19:03:19 +08:00
sunnylqm
8bd19bc0f7 add commit info 2025-03-15 18:50:39 +08:00
sunnylqm
d974be6706 add i18n 2025-03-15 18:33:05 +08:00
Sunny Luo
d0495fb271 Feat/deps (#10)
* init

* add deps

* sort key
2025-03-14 13:44:47 +08:00
Sunny Luo
613f39a59e Delete yarn.lock 2025-03-14 13:37:56 +08:00
sunnylqm
94c06e9908 add timeout for version query 2025-02-26 16:11:40 +08:00
sunnylqm
ac1a000a3b v1.41.0-beta.1 2025-02-21 11:43:24 +08:00
sunnylqm
4e51f291ca Refactor project configuration and dependencies
- Move IS_CRESC logic to constants module
- Update package.json with new type definitions
- Simplify global variable usage
- Update bun.lock with latest package versions
- Improve type safety and configuration management
2025-02-19 22:53:24 +08:00
sunnylqm
04b5d12daa deps 2025-02-19 22:24:54 +08:00
sunnylqm
67644b5c11 v1.41.0-beta.0 2025-02-19 22:06:24 +08:00
sunnylqm
546faef83f more cresc config 2025-02-18 22:37:09 +08:00
sunnylqm
e713f4bbd1 Bump package version to 1.40.1 2025-02-15 23:14:41 +08:00
sunnylqm
5a6463de33 Remove debug console.log in zip utility 2025-02-15 23:14:11 +08:00
sunnylqm
1fb308af94 Refactor i18n locales from JSON to TypeScript modules 2025-02-15 22:51:26 +08:00
sunnylqm
f10d4d3004 Add option to disable Hermes for React Native bundle 2025-02-15 21:40:28 +08:00
sunnylqm
0f44de772f init i18n 2025-02-15 00:38:55 +08:00
Sunny Luo
814a9d10fb Update package.json 2025-02-13 17:17:31 +08:00
sunnylqm
c08c5c0b07 fix taro cli path 2025-02-13 16:13:48 +08:00
sunnylqm
dc8c134ff0 v1.40.0-beta.0 2025-02-13 16:07:25 +08:00
sunnylqm
1d1e6cde0f support taro 2025-02-13 16:02:04 +08:00
sunny.luo
f16aff5674 Improve file filtering during bundle packing
# Conflicts:
#	package.json
#	src/bundle.js
2025-02-10 17:22:00 +08:00
Sunny Luo
d7da311c5e Update package.json 2025-02-09 16:15:04 +08:00
波仔糕
abef760f43 add logic to remove soucemap and merge sourcemap params (#9)
* add logic to remove soucemap and merge sourcemap params

* Update bundle.js

---------

Co-authored-by: Sunny Luo <sunnylqm@gmail.com>
2025-02-09 16:14:45 +08:00
Sunny Luo
467ef0c60d Update package.json 2025-01-23 23:15:22 +08:00
波仔糕
0b08c7760d Support sentry (#8)
* add logic to support sentry

* udpate

* change reference path

* support git commits and version info

* udate

* add try catch for require.resolve

* update upload sourcemap workflow
2025-01-23 22:02:13 +08:00
sunnylqm
b24b27d100 ts 2025-01-11 20:23:25 +08:00
sunnylqm
4f0784172f fix expo cli 2025-01-11 20:20:16 +08:00
sunnylqm
26725200b7 bump 2025-01-11 15:47:29 +08:00
sunnylqm
8bd31b8dc1 fix get 2025-01-11 15:45:28 +08:00
Sunny Luo
59f2421067 Update package.json 2025-01-09 21:00:29 +08:00
sunnylqm
8f0ca60d24 fix error message 2025-01-09 20:58:06 +08:00
Sunny Luo
6fcdb988c8 Merge pull request #6 from bozaigao/master
implement getAppInfo and uploadApp methods
2025-01-07 14:19:42 +08:00
HeYanbo
040f723305 implement getAppInfo and uploadApp methods 2025-01-07 13:52:28 +08:00
sunnylqm
12bddc446d Enhance package.json: add lint script and update devDependencies 2025-01-05 21:22:14 +08:00
sunnylqm
ba3d88627a support harmony 2025-01-05 21:16:52 +08:00
Sunny Luo
3987a5191f Update publish.yml 2024-12-26 15:41:57 +08:00
Sunny Luo
9903fb97e4 bump 2024-12-26 15:40:05 +08:00
Sunny Luo
834c0bb351 Create lint.yml 2024-12-26 14:57:21 +08:00
Sunny Luo
fa8ba44bfa Create publish.yml 2024-12-26 14:56:49 +08:00
Sunny Luo
6e4e89428c Merge pull request #5 from bozaigao/master
适配鸿蒙
2024-12-26 12:14:14 +08:00
HeYanbo
e018761fc8 init 2024-12-25 23:38:29 +08:00
sunny.luo
6534379d81 use expo cli if available 2024-12-11 17:23:28 +08:00
sunny.luo
bb777bf973 update 2024-12-02 16:32:34 +08:00
sunny.luo
ea05d4ab66 fix chalk 2024-11-25 10:51:35 +08:00
sunny.luo
a259ff671d check latest version 2024-11-24 11:51:55 +08:00
sunny.luo
3f68fa0f11 compose sourcemap 2024-11-19 23:28:54 +08:00
sunnylqm
fa750ba157 update 2024-11-16 18:54:34 +08:00
sunnylqm
91fc0caa07 v1.32.3 2024-11-16 18:33:09 +08:00
sunnylqm
36e1be084d remove request 2024-11-16 08:24:18 +08:00
sunnylqm
e099002629 v1.32.0 2024-11-16 07:51:18 +08:00
sunnylqm
5dd3b64720 ts 2024-11-16 07:48:27 +08:00
sunnylqm
31ec3edc5a v1.31.2 2024-11-16 07:41:08 +08:00
Sunny Luo
4b772194d1 Update versions.js 2024-10-23 10:41:46 +08:00
sunnylqm
792418c964 add dep 2024-10-20 16:52:06 +08:00
sunnylqm
d5f468aa44 fix gradle check 2024-10-19 11:44:32 +08:00
sunnylqm
04f75cd51b add rollout 2024-10-19 11:31:50 +08:00
sunnylqm
5a05679e2b add version id null 2024-10-16 23:20:58 +08:00
sunny.luo
69f4372b63 note for apk permission 2024-10-15 14:56:41 +08:00
sunnylqm
dbfdce6412 v1.30.3 2024-10-09 20:31:48 +08:00
sunnylqm
1372a30ea3 show error 2024-10-08 19:32:52 +08:00
Sunny Luo
d51ef37a62 Update index.js 2024-10-08 13:16:23 +08:00
sunnylqm
90a59cfe7b support rn75 2024-09-24 21:20:42 +08:00
sunnylqm
ffcc6c9c96 v1.30.0 2024-09-24 21:20:18 +08:00
sunnylqm
b00b3144a3 v1.29.0 2024-09-22 17:45:10 +08:00
sunnylqm
d3ee908227 warn version 2024-09-22 17:44:31 +08:00
sunnylqm
5302c8968f fix expo cli on windows 2024-08-22 17:15:17 +08:00
sunnylqm
5740a3315c v1.28.0 2024-08-22 17:15:04 +08:00
sunnylqm
4a11a4118f add min/max packageverion 2024-07-21 23:12:15 +08:00
sunnylqm
feb1128748 v1.26.0 2024-07-10 09:48:06 +08:00
sunnylqm
8fd74d76b1 v1.25.1 2024-07-05 10:34:18 +08:00
sunnylqm
209a989a48 v1.25.0 2024-07-04 21:34:34 +08:00
sunnylqm
aab9ea6b98 v1.24.0 2024-07-04 18:01:51 +08:00
sunnylqm
5f45a66aad v1.23.1 2024-06-11 20:27:04 +08:00
sunnylqm
67f6aeef02 v1.23.0 2024-06-11 20:22:12 +08:00
sunnylqm
5a28847e46 v1.22.0 2024-03-21 21:04:16 +08:00
sunnylqm
54f7358357 chore: ignore sourcemap 2024-03-21 21:03:34 +08:00
sunnylqm
d2ec856756 v1.21.0 2024-03-07 21:08:30 +08:00
sunnylqm
84e5e34820 v1.20.6 2024-03-07 11:16:19 +08:00
sunnylqm
006951ae88 v1.20.5 2024-03-06 20:45:59 +08:00
sunnylqm
b1fda5ab9c v1.20.3 2024-03-06 20:36:25 +08:00
sunnylqm
385b974aff v1.20.2 2024-03-06 10:20:00 +08:00
sunnylqm
2a02deb015 v1.20.1 2024-03-06 09:55:48 +08:00
sunnylqm
8b7cb809f6 v1.20.0 2024-03-05 23:29:31 +08:00
sunnylqm
070991d08b fix: hermes path 2024-03-05 23:29:05 +08:00
sunnylqm
0d03e18c58 fix: version name 2024-03-05 23:03:10 +08:00
sunnylqm
a0dfcb5c4b v1.19.0 2024-02-18 20:04:05 +08:00
sunnylqm
587da8aaf9 Fix android crunchPngs option and add support for expo-router 2024-02-18 20:03:43 +08:00
sunnylqm
724088a810 v1.18.0 2024-02-03 21:28:38 +08:00
sunnylqm
7c20b30c85 feat: support expo 2024-02-03 21:27:47 +08:00
sunnylqm
6a053c6428 v1.17.0 2024-01-30 19:15:15 +08:00
sunnylqm
f202fc590d fix: rndir path 2024-01-30 19:15:00 +08:00
sunnylqm
0f8cf3c399 v1.16.0 2024-01-30 17:50:32 +08:00
sunnylqm
6a0d82c7a5 fix: rndir path 2024-01-30 17:50:09 +08:00
sunnylqm
eed19992d8 v1.15.0 2024-01-30 12:43:48 +08:00
sunnylqm
1b5078831c fix: require path 2024-01-30 12:43:31 +08:00
sunnylqm
bc9aff343a v1.14.0 2024-01-29 19:11:06 +08:00
sunnylqm
7aaa32a5a2 v1.14.0-beta.0 2024-01-29 19:02:01 +08:00
sunnylqm
ab7920fe38 feat: support symlink 2024-01-29 19:01:11 +08:00
sunnylqm
d912ace4a7 v1.13.0 2023-09-19 18:40:12 +08:00
sunnylqm
8af4d314ce fix appid equal 2023-09-19 18:36:26 +08:00
sunnylqm
f2d5269512 feat: support rn0.71 android hermes 2023-07-20 18:27:14 +08:00
sunnylqm
fe24c4ca36 v1.12.0 2023-07-20 18:25:47 +08:00
sunnylqm
cf61c297a6 update endpoint 2023-01-29 10:35:44 +08:00
sunnylqm
f9adc700ed v1.11.0 2023-01-11 23:36:13 +08:00
sunnylqm
dcff16cbb5 support source-map 2023-01-11 23:35:39 +08:00
sunnylqm
2bb8e83010 v1.10.0 2022-07-01 23:08:16 +08:00
sunnylqm
0cfc6e4f0d support rn 69 hermes 2022-07-01 23:02:20 +08:00
sunnylqm
da7bdbfdd2 v1.9.0 2022-06-29 09:42:05 +08:00
sunnylqm
07ee28ba3b add updateVersionInfo 2022-06-29 09:41:44 +08:00
79 changed files with 17390 additions and 4576 deletions

49
.github/workflows/claude.yml vendored Normal file
View File

@@ -0,0 +1,49 @@
name: Claude PR Assistant
on:
issue_comment:
types: [created]
pull_request_review_comment:
types: [created]
issues:
types: [opened, assigned]
pull_request_review:
types: [submitted]
jobs:
claude-code-action:
if: |
(github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) ||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) ||
(github.event_name == 'issues' && contains(github.event.issue.body, '@claude'))
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: read
issues: read
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 1
- name: Run Claude PR Action
uses: anthropics/claude-code-action@beta
with:
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
# Or use OAuth token instead:
# claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
timeout_minutes: "60"
# Optional: Restrict network access to specific domains only
# experimental_allowed_domains: |
# .anthropic.com
# .github.com
# api.github.com
# .githubusercontent.com
# bun.sh
# registry.npmjs.org
# .blob.core.windows.net
env:
ANTHROPIC_BASE_URL: https://anyrouter.top

47
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: lint
on:
pull_request:
branches:
- master
push:
branches:
- master
# Cancel a currently running workflow from the same PR/branch/tag
# when a new workflow is triggered
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
lint:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20.x]
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
- name: Install Dependency
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: '--max_old_space_size=4096'
run: bun install --frozen-lockfile
- name: Run lint
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_OPTIONS: '--max_old_space_size=4096'
run: bun lint

22
.github/workflows/publish.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: Publish Package to npmjs
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- run: bun install --frozen-lockfile
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

3
.gitignore vendored
View File

@@ -105,3 +105,6 @@ dist
lib/
.DS_Store
# react-native-update
.update
.pushy

1
.npmrc Normal file
View File

@@ -0,0 +1 @@
registry = "https://registry.npmjs.org"

16
.swcrc Normal file
View File

@@ -0,0 +1,16 @@
{
"jsc": {
"baseUrl": "./src",
"loose": true,
"target": "es2018",
"parser": {
"syntax": "typescript",
"tsx": false,
"decorators": false,
"dynamicImport": false
}
},
"module": {
"type": "commonjs"
}
}

604
README.md
View File

@@ -1 +1,603 @@
# react-native-update-cli
# React Native Update CLI
[中文文档](./README.zh-CN.md) | [Chinese Documentation](./README.zh-CN.md)
A unified React Native Update CLI that supports both traditional commands and modular architecture with custom publishing workflows.
## 🚀 Features
- **Unified CLI**: Single `pushy` command for all functionality
- **Backward Compatibility**: All existing commands work as before
- **Modular Architecture**: Split CLI functionality into independent modules
- **Custom Workflows**: Support for creating custom publishing workflows
- **Extensibility**: Users can import and register custom modules
- **Type Safety**: Complete TypeScript type support
## 📦 Installation
```bash
npm install react-native-update-cli
```
## 🎯 Quick Start
### Basic Usage
```bash
# Use unified CLI
npx pushy help
# List all available commands and workflows
npx pushy list
# Execute built-in workflow
npx pushy workflow setup-app
# Execute custom workflow
npx pushy workflow custom-publish
```
### Programmatic Usage
```typescript
import { moduleManager, CLIProviderImpl } from 'react-native-update-cli';
// Get CLI provider
const provider = moduleManager.getProvider();
// Execute bundling
const bundleResult = await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true,
});
// Publish version
const publishResult = await provider.publish({
name: 'v1.2.3',
description: 'Bug fixes and improvements',
rollout: 100,
});
```
## 🔧 Creating Custom Modules
### 1. Define Module
```typescript
import type {
CLIModule,
CommandDefinition,
CustomWorkflow,
} from 'react-native-update-cli';
export const myCustomModule: CLIModule = {
name: 'my-custom',
version: '1.0.0',
commands: [
{
name: 'custom-command',
description: 'My custom command',
handler: async (context) => {
console.log('Executing custom command...');
return {
success: true,
data: { message: 'Custom command executed' },
};
},
options: {
param: { hasValue: true, description: 'Custom parameter' },
},
},
],
workflows: [
{
name: 'my-workflow',
description: 'My custom workflow',
steps: [
{
name: 'step1',
description: 'First step',
execute: async (context, previousResult) => {
console.log('Executing step 1...');
return { step1Completed: true };
},
},
{
name: 'step2',
description: 'Second step',
execute: async (context, previousResult) => {
console.log('Executing step 2...');
return { ...previousResult, step2Completed: true };
},
},
],
},
],
init: (provider) => {
console.log('Custom module initialized');
},
cleanup: () => {
console.log('Custom module cleanup');
},
};
```
### 2. Register Module
```typescript
import { moduleManager } from 'react-native-update-cli';
import { myCustomModule } from './my-custom-module';
// Register custom module
moduleManager.registerModule(myCustomModule);
// Execute custom command
const result = await moduleManager.executeCommand('custom-command', {
args: [],
options: { param: 'value' },
});
// Execute custom workflow
const workflowResult = await moduleManager.executeWorkflow('my-workflow', {
args: [],
options: {},
});
```
## 🔄 Workflow System
### Workflow Steps
Each workflow step contains:
- `name`: Step name
- `description`: Step description
- `execute`: Execution function
- `condition`: Optional condition function
### Conditional Execution
```typescript
{
name: 'conditional-step',
description: 'Only execute in production',
execute: async (context, previousResult) => {
// Execution logic
},
condition: (context) => {
return context.options.environment === 'production';
}
}
```
### Workflow Validation
```typescript
{
name: 'validated-workflow',
description: 'Workflow with validation',
steps: [...],
validate: (context) => {
if (!context.options.requiredParam) {
console.error('Required parameter missing');
return false;
}
return true;
}
}
```
## 📋 Built-in Modules
### Bundle Module (`bundle`)
- `bundle`: Bundle JavaScript code and optionally publish
- `diff`: Generate differences between two PPK files
- `hdiff`: Generate hdiff between two PPK files
- `diffFromApk`: Generate differences from APK files
- `hdiffFromApk`: Generate hdiff from APK files
- `hdiffFromApp`: Generate hdiff from APP files
- `diffFromIpa`: Generate differences from IPA files
- `hdiffFromIpa`: Generate hdiff from IPA files
### Version Module (`version`)
- `publish`: Publish new version
- `versions`: List all versions
- `update`: Update version information
- `updateVersionInfo`: Update version metadata
### App Module (`app`)
- `createApp`: Create new application
- `apps`: List all applications
- `selectApp`: Select application
- `deleteApp`: Delete application
### Package Module (`package`)
- `uploadIpa`: Upload IPA files (supports `--version` to override extracted version)
- `uploadApk`: Upload APK files (supports `--version` to override extracted version)
- `uploadApp`: Upload APP files (supports `--version` to override extracted version)
- `parseApp`: Parse APP file information
- `parseIpa`: Parse IPA file information
- `parseApk`: Parse APK file information
- `packages`: List packages
### User Module (`user`)
- `login`: Login
- `logout`: Logout
- `me`: Show user information
## 🛠️ CLI Provider API
### Core Functionality
```typescript
interface CLIProvider {
// Bundle
bundle(options: BundleOptions): Promise<CommandResult>;
// Publish
publish(options: PublishOptions): Promise<CommandResult>;
// Upload
upload(options: UploadOptions): Promise<CommandResult>;
// Application management
getSelectedApp(
platform?: Platform,
): Promise<{ appId: string; platform: Platform }>;
listApps(platform?: Platform): Promise<CommandResult>;
createApp(name: string, platform: Platform): Promise<CommandResult>;
// Version management
listVersions(appId: string): Promise<CommandResult>;
getVersion(appId: string, versionId: string): Promise<CommandResult>;
updateVersion(
appId: string,
versionId: string,
updates: Partial<Version>,
): Promise<CommandResult>;
// Package management
listPackages(appId: string, platform?: Platform): Promise<CommandResult>;
getPackage(appId: string, packageId: string): Promise<CommandResult>;
// Utility functions
getPlatform(platform?: Platform): Promise<Platform>;
loadSession(): Promise<Session>;
saveToLocal(key: string, value: string): void;
question(prompt: string): Promise<string>;
// Workflows
registerWorkflow(workflow: CustomWorkflow): void;
executeWorkflow(
workflowName: string,
context: CommandContext,
): Promise<CommandResult>;
}
```
### Custom Commands
```typescript
// Execute custom bundle command
const bundleResult = await moduleManager.executeCommand('custom-bundle', {
args: [],
options: {
platform: 'android',
validate: true,
optimize: true,
},
});
// Generate diff file
const diffResult = await moduleManager.executeCommand('diff', {
args: [],
options: {
origin: './build/v1.0.0.ppk',
next: './build/v1.1.0.ppk',
output: './build/diff.patch',
},
});
// Generate diff from APK files
const apkDiffResult = await moduleManager.executeCommand('diffFromApk', {
args: [],
options: {
origin: './build/app-v1.0.0.apk',
next: './build/app-v1.1.0.apk',
output: './build/apk-diff.patch',
},
});
```
## 🔧 Configuration
### Environment Variables
```bash
# Set API endpoint
export PUSHY_REGISTRY=https://your-api-endpoint.com
# Set non-interactive mode
export NO_INTERACTIVE=true
```
### Configuration File
Create `update.json` file:
```json
{
"ios": {
"appId": "your-ios-app-id",
"appKey": "your-ios-app-key"
},
"android": {
"appId": "your-android-app-id",
"appKey": "your-android-app-key"
}
}
```
## 🚨 Important Notes
1. **Backward Compatibility**: The new modular CLI maintains compatibility with existing CLI
2. **Type Safety**: All APIs have complete TypeScript type definitions
3. **Error Handling**: All operations return standardized result formats
4. **Resource Cleanup**: Modules support cleanup functions to release resources
5. **Module Separation**: Functionality is logically separated into different modules for easy maintenance and extension
## 🤝 Contributing
Welcome to submit Issues and Pull Requests to improve this project!
## 🚀 Provider API Usage Guide
Provider provides a concise programming interface suitable for integrating React Native Update CLI functionality in applications.
### 📋 Core API Methods
#### Core Business Functions
```typescript
// Bundle application
await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true,
});
// Publish version
await provider.publish({
name: 'v1.0.0',
description: 'Bug fixes',
rollout: 100,
});
// Upload file
await provider.upload({
filePath: 'app.ipa',
platform: 'ios',
});
```
#### Application Management
```typescript
// Create application
await provider.createApp('MyApp', 'ios');
// List applications
await provider.listApps('ios');
// Get current application
const { appId, platform } = await provider.getSelectedApp('ios');
```
#### Version Management
```typescript
// List versions
await provider.listVersions('app123');
// Update version
await provider.updateVersion('app123', 'version456', {
name: 'v1.1.0',
description: 'New features',
});
```
#### Utility Functions
```typescript
// Get platform
const platform = await provider.getPlatform('ios');
// Load session
const session = await provider.loadSession();
```
### 🎯 Use Cases
#### 1. Automated Build Scripts
```typescript
import { moduleManager } from 'react-native-update-cli';
async function buildAndPublish() {
const provider = moduleManager.getProvider();
// 1. Bundle
const bundleResult = await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true,
});
if (!bundleResult.success) {
throw new Error(`Bundle failed: ${bundleResult.error}`);
}
// 2. Publish
const publishResult = await provider.publish({
name: 'v1.2.3',
description: 'Bug fixes and performance improvements',
rollout: 100,
});
if (!publishResult.success) {
throw new Error(`Publish failed: ${publishResult.error}`);
}
console.log('Build and publish completed!');
}
```
#### 2. CI/CD Integration
```typescript
async function ciBuild() {
const provider = moduleManager.getProvider();
const result = await provider.bundle({
platform: process.env.PLATFORM as 'ios' | 'android',
dev: process.env.NODE_ENV !== 'production',
sourcemap: process.env.NODE_ENV === 'production',
});
return result;
}
```
#### 3. Application Management Service
```typescript
class AppManagementService {
private provider = moduleManager.getProvider();
async setupNewApp(name: string, platform: Platform) {
// Create application
const createResult = await this.provider.createApp(name, platform);
if (createResult.success) {
// Get application information
const { appId } = await this.provider.getSelectedApp(platform);
// List versions
await this.provider.listVersions(appId);
return { appId, success: true };
}
return { success: false, error: createResult.error };
}
}
```
### ⚠️ Important Notes
1. **Error Handling**: All Provider methods return `CommandResult`, need to check the `success` field
2. **Type Safety**: Provider provides complete TypeScript type support
3. **Session Management**: Ensure login before use, can check via `loadSession()`
4. **Platform Support**: Supports `'ios' | 'android' | 'harmony'` three platforms
### 🔧 Advanced Features
#### Custom Workflows
```typescript
// Register custom workflow
provider.registerWorkflow({
name: 'quick-release',
description: 'Quick release process',
steps: [
{
name: 'bundle',
execute: async () => {
return await provider.bundle({ platform: 'ios', dev: false });
},
},
{
name: 'publish',
execute: async (context, bundleResult) => {
if (!bundleResult.success) {
throw new Error('Bundle failed, cannot publish');
}
return await provider.publish({ name: 'auto-release', rollout: 50 });
},
},
],
});
// Execute workflow
await provider.executeWorkflow('quick-release', { args: [], options: {} });
```
### 📚 Complete Example
```typescript
import { moduleManager } from 'react-native-update-cli';
class ReactNativeUpdateService {
private provider = moduleManager.getProvider();
async initialize() {
// Load session
await this.provider.loadSession();
}
async buildAndDeploy(platform: Platform, version: string) {
try {
// 1. Bundle
const bundleResult = await this.provider.bundle({
platform,
dev: false,
sourcemap: true,
});
if (!bundleResult.success) {
throw new Error(`Bundle failed: ${bundleResult.error}`);
}
// 2. Publish
const publishResult = await this.provider.publish({
name: version,
description: `Release ${version}`,
rollout: 100,
});
if (!publishResult.success) {
throw new Error(`Publish failed: ${publishResult.error}`);
}
return { success: true, data: publishResult.data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async getAppInfo(platform: Platform) {
const { appId } = await this.provider.getSelectedApp(platform);
const versions = await this.provider.listVersions(appId);
return { appId, versions };
}
}
// Usage example
const service = new ReactNativeUpdateService();
await service.initialize();
await service.buildAndDeploy('ios', 'v1.0.0');
```

601
README.zh-CN.md Normal file
View File

@@ -0,0 +1,601 @@
# React Native Update CLI
这是一个统一的 React Native Update CLI同时支持传统命令和模块化架构以及自定义发布流程。
## 🚀 特性
- **统一 CLI**: 使用单个`pushy`命令提供所有功能
- **向后兼容**: 所有现有命令都能正常工作
- **模块化架构**: 将 CLI 功能拆分为独立的模块
- **自定义工作流**: 支持创建自定义的发布流程
- **可扩展性**: 用户可以导入和注册自定义模块
- **类型安全**: 完整的 TypeScript 类型支持
## 📦 安装
```bash
npm install react-native-update-cli
```
## 🎯 快速开始
### 基本使用
```bash
# 使用统一CLI
npx pushy help
# 列出所有可用命令和工作流
npx pushy list
# 执行内置的工作流
npx pushy workflow setup-app
# 执行自定义工作流
npx pushy workflow custom-publish
```
### 编程方式使用
```typescript
import { moduleManager, CLIProviderImpl } from 'react-native-update-cli';
// 获取CLI提供者
const provider = moduleManager.getProvider();
// 执行打包
const bundleResult = await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true,
});
// 发布版本
const publishResult = await provider.publish({
name: 'v1.2.3',
description: 'Bug fixes and improvements',
rollout: 100,
});
```
## 🔧 创建自定义模块
### 1. 定义模块
```typescript
import type {
CLIModule,
CommandDefinition,
CustomWorkflow,
} from 'react-native-update-cli';
export const myCustomModule: CLIModule = {
name: 'my-custom',
version: '1.0.0',
commands: [
{
name: 'custom-command',
description: 'My custom command',
handler: async (context) => {
console.log('Executing custom command...');
return {
success: true,
data: { message: 'Custom command executed' },
};
},
options: {
param: { hasValue: true, description: 'Custom parameter' },
},
},
],
workflows: [
{
name: 'my-workflow',
description: 'My custom workflow',
steps: [
{
name: 'step1',
description: 'First step',
execute: async (context, previousResult) => {
console.log('Executing step 1...');
return { step1Completed: true };
},
},
{
name: 'step2',
description: 'Second step',
execute: async (context, previousResult) => {
console.log('Executing step 2...');
return { ...previousResult, step2Completed: true };
},
},
],
},
],
init: (provider) => {
console.log('Custom module initialized');
},
cleanup: () => {
console.log('Custom module cleanup');
},
};
```
### 2. 注册模块
```typescript
import { moduleManager } from 'react-native-update-cli';
import { myCustomModule } from './my-custom-module';
// 注册自定义模块
moduleManager.registerModule(myCustomModule);
// 执行自定义命令
const result = await moduleManager.executeCommand('custom-command', {
args: [],
options: { param: 'value' },
});
// 执行自定义工作流
const workflowResult = await moduleManager.executeWorkflow('my-workflow', {
args: [],
options: {},
});
```
## 🔄 工作流系统
### 工作流步骤
每个工作流步骤包含:
- `name`: 步骤名称
- `description`: 步骤描述
- `execute`: 执行函数
- `condition`: 可选的条件函数
### 条件执行
```typescript
{
name: 'conditional-step',
description: 'Only execute in production',
execute: async (context, previousResult) => {
// 执行逻辑
},
condition: (context) => {
return context.options.environment === 'production';
}
}
```
### 工作流验证
```typescript
{
name: 'validated-workflow',
description: 'Workflow with validation',
steps: [...],
validate: (context) => {
if (!context.options.requiredParam) {
console.error('Required parameter missing');
return false;
}
return true;
}
}
```
## 📋 内置模块
### Bundle 模块 (`bundle`)
- `bundle`: 打包 JavaScript 代码并可选发布
- `diff`: 生成两个 PPK 文件之间的差异
- `hdiff`: 生成两个 PPK 文件之间的 hdiff
- `diffFromApk`: 从 APK 文件生成差异
- `hdiffFromApk`: 从 APK 文件生成 hdiff
- `hdiffFromApp`: 从 APP 文件生成 hdiff
- `diffFromIpa`: 从 IPA 文件生成差异
- `hdiffFromIpa`: 从 IPA 文件生成 hdiff
### Version 模块 (`version`)
- `publish`: 发布新版本
- `versions`: 列出所有版本
- `update`: 更新版本信息
- `updateVersionInfo`: 更新版本元数据
### App 模块 (`app`)
- `createApp`: 创建新应用
- `apps`: 列出所有应用
- `selectApp`: 选择应用
- `deleteApp`: 删除应用
### Package 模块 (`package`)
- `uploadIpa`: 上传 IPA 文件(支持 `--version` 参数覆盖提取的版本)
- `uploadApk`: 上传 APK 文件(支持 `--version` 参数覆盖提取的版本)
- `uploadApp`: 上传 APP 文件(支持 `--version` 参数覆盖提取的版本)
- `parseApp`: 解析 APP 文件信息
- `parseIpa`: 解析 IPA 文件信息
- `parseApk`: 解析 APK 文件信息
- `packages`: 列出包
### User 模块 (`user`)
- `login`: 登录
- `logout`: 登出
- `me`: 显示用户信息
## 🛠️ CLI 提供者 API
### 核心功能
```typescript
interface CLIProvider {
// 打包
bundle(options: BundleOptions): Promise<CommandResult>;
// 发布
publish(options: PublishOptions): Promise<CommandResult>;
// 上传
upload(options: UploadOptions): Promise<CommandResult>;
// 应用管理
getSelectedApp(
platform?: Platform,
): Promise<{ appId: string; platform: Platform }>;
listApps(platform?: Platform): Promise<CommandResult>;
createApp(name: string, platform: Platform): Promise<CommandResult>;
// 版本管理
listVersions(appId: string): Promise<CommandResult>;
getVersion(appId: string, versionId: string): Promise<CommandResult>;
updateVersion(
appId: string,
versionId: string,
updates: Partial<Version>,
): Promise<CommandResult>;
// 包管理
listPackages(appId: string, platform?: Platform): Promise<CommandResult>;
getPackage(appId: string, packageId: string): Promise<CommandResult>;
// 工具函数
getPlatform(platform?: Platform): Promise<Platform>;
loadSession(): Promise<Session>;
saveToLocal(key: string, value: string): void;
question(prompt: string): Promise<string>;
// 工作流
registerWorkflow(workflow: CustomWorkflow): void;
executeWorkflow(
workflowName: string,
context: CommandContext,
): Promise<CommandResult>;
}
```
### 自定义命令
```typescript
// 执行自定义打包命令
const bundleResult = await moduleManager.executeCommand('custom-bundle', {
args: [],
options: {
platform: 'android',
validate: true,
optimize: true,
},
});
// 生成差异文件
const diffResult = await moduleManager.executeCommand('diff', {
args: [],
options: {
origin: './build/v1.0.0.ppk',
next: './build/v1.1.0.ppk',
output: './build/diff.patch',
},
});
// 从APK文件生成差异
const apkDiffResult = await moduleManager.executeCommand('diffFromApk', {
args: [],
options: {
origin: './build/app-v1.0.0.apk',
next: './build/app-v1.1.0.apk',
output: './build/apk-diff.patch',
},
});
```
## 🔧 配置
### 环境变量
```bash
# 设置API端点
export PUSHY_REGISTRY=https://your-api-endpoint.com
# 设置非交互模式
export NO_INTERACTIVE=true
```
### 配置文件
创建 `update.json` 文件:
```json
{
"ios": {
"appId": "your-ios-app-id",
"appKey": "your-ios-app-key"
},
"android": {
"appId": "your-android-app-id",
"appKey": "your-android-app-key"
}
}
```
## 🚨 注意事项
1. **向后兼容**: 新的模块化 CLI 保持与现有 CLI 的兼容性
2. **类型安全**: 所有 API 都有完整的 TypeScript 类型定义
3. **错误处理**: 所有操作都返回标准化的结果格式
4. **资源清理**: 模块支持清理函数来释放资源
5. **模块分离**: 功能按逻辑分离到不同模块中,便于维护和扩展
## 🤝 贡献
欢迎提交 Issue 和 Pull Request 来改进这个项目!
## 🚀 Provider API 使用指南
Provider 提供了简洁的编程接口,适合在应用程序中集成 React Native Update CLI 功能。
### 📋 核心 API 方法
#### 核心业务功能
```typescript
// 打包应用
await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true,
});
// 发布版本
await provider.publish({
name: 'v1.0.0',
description: 'Bug fixes',
rollout: 100,
});
// 上传文件
await provider.upload({
filePath: 'app.ipa',
platform: 'ios',
});
```
#### 应用管理
```typescript
// 创建应用
await provider.createApp('MyApp', 'ios');
// 列出应用
await provider.listApps('ios');
// 获取当前应用
const { appId, platform } = await provider.getSelectedApp('ios');
```
#### 版本管理
```typescript
// 列出版本
await provider.listVersions('app123');
// 更新版本
await provider.updateVersion('app123', 'version456', {
name: 'v1.1.0',
description: 'New features',
});
```
#### 工具函数
```typescript
// 获取平台
const platform = await provider.getPlatform('ios');
// 加载会话
const session = await provider.loadSession();
```
### 🎯 使用场景
#### 1. 自动化构建脚本
```typescript
import { moduleManager } from 'react-native-update-cli';
async function buildAndPublish() {
const provider = moduleManager.getProvider();
// 1. 打包
const bundleResult = await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true,
});
if (!bundleResult.success) {
throw new Error(`打包失败: ${bundleResult.error}`);
}
// 2. 发布
const publishResult = await provider.publish({
name: 'v1.2.3',
description: 'Bug fixes and performance improvements',
rollout: 100,
});
if (!publishResult.success) {
throw new Error(`发布失败: ${publishResult.error}`);
}
console.log('构建和发布完成!');
}
```
#### 2. CI/CD 集成
```typescript
async function ciBuild() {
const provider = moduleManager.getProvider();
const result = await provider.bundle({
platform: process.env.PLATFORM as 'ios' | 'android',
dev: process.env.NODE_ENV !== 'production',
sourcemap: process.env.NODE_ENV === 'production',
});
return result;
}
```
#### 3. 应用管理服务
```typescript
class AppManagementService {
private provider = moduleManager.getProvider();
async setupNewApp(name: string, platform: Platform) {
// 创建应用
const createResult = await this.provider.createApp(name, platform);
if (createResult.success) {
// 获取应用信息
const { appId } = await this.provider.getSelectedApp(platform);
// 列出版本
await this.provider.listVersions(appId);
return { appId, success: true };
}
return { success: false, error: createResult.error };
}
}
```
### ⚠️ 注意事项
1. **错误处理**: 所有 Provider 方法都返回`CommandResult`,需要检查`success`字段
2. **类型安全**: Provider 提供完整的 TypeScript 类型支持
3. **会话管理**: 使用前确保已登录,可通过`loadSession()`检查
4. **平台支持**: 支持`'ios' | 'android' | 'harmony'`三个平台
### 🔧 高级功能
#### 自定义工作流
```typescript
// 注册自定义工作流
provider.registerWorkflow({
name: 'quick-release',
description: '快速发布流程',
steps: [
{
name: 'bundle',
execute: async () => {
return await provider.bundle({ platform: 'ios', dev: false });
},
},
{
name: 'publish',
execute: async (context, bundleResult) => {
if (!bundleResult.success) {
throw new Error('打包失败,无法发布');
}
return await provider.publish({ name: 'auto-release', rollout: 50 });
},
},
],
});
// 执行工作流
await provider.executeWorkflow('quick-release', { args: [], options: {} });
```
### 📚 完整示例
```typescript
import { moduleManager } from 'react-native-update-cli';
class ReactNativeUpdateService {
private provider = moduleManager.getProvider();
async initialize() {
// 加载会话
await this.provider.loadSession();
}
async buildAndDeploy(platform: Platform, version: string) {
try {
// 1. 打包
const bundleResult = await this.provider.bundle({
platform,
dev: false,
sourcemap: true,
});
if (!bundleResult.success) {
throw new Error(`打包失败: ${bundleResult.error}`);
}
// 2. 发布
const publishResult = await this.provider.publish({
name: version,
description: `Release ${version}`,
rollout: 100,
});
if (!publishResult.success) {
throw new Error(`发布失败: ${publishResult.error}`);
}
return { success: true, data: publishResult.data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
async getAppInfo(platform: Platform) {
const { appId } = await this.provider.getSelectedApp(platform);
const versions = await this.provider.listVersions(appId);
return { appId, versions };
}
}
// 使用示例
const service = new ReactNativeUpdateService();
await service.initialize();
await service.buildAndDeploy('ios', 'v1.0.0');
```

40
biome.json Normal file
View File

@@ -0,0 +1,40 @@
{
"files": {
"ignore": ["lib"]
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noExplicitAny": "off",
"noAssignInExpressions": "off",
"noDoubleEquals": "off"
},
"style": {
"noNonNullAssertion": "off",
"useNodejsImportProtocol": "off"
},
"a11y": {
"useValidAnchor": "off"
},
"performance": {
"noDelete": "off"
}
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 80,
"attributePosition": "auto"
},
"javascript": {
"formatter": {
"quoteStyle": "single"
}
}
}

930
bun.lock Normal file
View File

@@ -0,0 +1,930 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"dependencies": {
"@colors/colors": "^1.6.0",
"bplist-parser": "^0.3.2",
"bytebuffer": "^5.0.1",
"cgbi-to-png": "^1.0.7",
"chalk": "4",
"cli-arguments": "^0.2.1",
"commander": "^13",
"compare-versions": "^6.1.1",
"filesize-parser": "^1.5.1",
"form-data": "^4.0.2",
"fs-extra": "8",
"global-dirs": "^4.0.0",
"gradle-to-js": "^2.0.1",
"i18next": "^24.2.3",
"isomorphic-git": "^1.30.1",
"isomorphic-unzip": "^1.1.5",
"node-fetch": "^2.6.1",
"plist": "^3.1.0",
"progress": "^2.0.3",
"properties": "^1.2.1",
"read": "^4.1.0",
"registry-auth-token": "^5.1.0",
"semver": "^7.7.2",
"tcp-ping": "^0.1.1",
"tty-table": "4.2",
"yauzl": "^3.2.0",
"yazl": "3.3.1",
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@swc/cli": "0.7.7",
"@swc/core": "^1.11.24",
"@types/filesize-parser": "^1.5.3",
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.15.18",
"@types/node-fetch": "^2.6.12",
"@types/progress": "^2.0.7",
"@types/semver": "^7.7.0",
"@types/tcp-ping": "^0.1.6",
"@types/yauzl": "^2.10.3",
"@types/yazl": "^2.4.6",
"typescript": "^5.8.3",
},
},
},
"trustedDependencies": [
"@biomejs/biome",
"@swc/core",
],
"packages": {
"@babel/runtime": ["@babel/runtime@7.26.10", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="],
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@napi-rs/nice": ["@napi-rs/nice@1.0.1", "", { "optionalDependencies": { "@napi-rs/nice-android-arm-eabi": "1.0.1", "@napi-rs/nice-android-arm64": "1.0.1", "@napi-rs/nice-darwin-arm64": "1.0.1", "@napi-rs/nice-darwin-x64": "1.0.1", "@napi-rs/nice-freebsd-x64": "1.0.1", "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", "@napi-rs/nice-linux-arm64-gnu": "1.0.1", "@napi-rs/nice-linux-arm64-musl": "1.0.1", "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", "@napi-rs/nice-linux-s390x-gnu": "1.0.1", "@napi-rs/nice-linux-x64-gnu": "1.0.1", "@napi-rs/nice-linux-x64-musl": "1.0.1", "@napi-rs/nice-win32-arm64-msvc": "1.0.1", "@napi-rs/nice-win32-ia32-msvc": "1.0.1", "@napi-rs/nice-win32-x64-msvc": "1.0.1" } }, "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ=="],
"@napi-rs/nice-android-arm-eabi": ["@napi-rs/nice-android-arm-eabi@1.0.1", "", { "os": "android", "cpu": "arm" }, "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w=="],
"@napi-rs/nice-android-arm64": ["@napi-rs/nice-android-arm64@1.0.1", "", { "os": "android", "cpu": "arm64" }, "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA=="],
"@napi-rs/nice-darwin-arm64": ["@napi-rs/nice-darwin-arm64@1.0.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA=="],
"@napi-rs/nice-darwin-x64": ["@napi-rs/nice-darwin-x64@1.0.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ=="],
"@napi-rs/nice-freebsd-x64": ["@napi-rs/nice-freebsd-x64@1.0.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw=="],
"@napi-rs/nice-linux-arm-gnueabihf": ["@napi-rs/nice-linux-arm-gnueabihf@1.0.1", "", { "os": "linux", "cpu": "arm" }, "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q=="],
"@napi-rs/nice-linux-arm64-gnu": ["@napi-rs/nice-linux-arm64-gnu@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA=="],
"@napi-rs/nice-linux-arm64-musl": ["@napi-rs/nice-linux-arm64-musl@1.0.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw=="],
"@napi-rs/nice-linux-ppc64-gnu": ["@napi-rs/nice-linux-ppc64-gnu@1.0.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q=="],
"@napi-rs/nice-linux-riscv64-gnu": ["@napi-rs/nice-linux-riscv64-gnu@1.0.1", "", { "os": "linux", "cpu": "none" }, "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig=="],
"@napi-rs/nice-linux-s390x-gnu": ["@napi-rs/nice-linux-s390x-gnu@1.0.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg=="],
"@napi-rs/nice-linux-x64-gnu": ["@napi-rs/nice-linux-x64-gnu@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA=="],
"@napi-rs/nice-linux-x64-musl": ["@napi-rs/nice-linux-x64-musl@1.0.1", "", { "os": "linux", "cpu": "x64" }, "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ=="],
"@napi-rs/nice-win32-arm64-msvc": ["@napi-rs/nice-win32-arm64-msvc@1.0.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg=="],
"@napi-rs/nice-win32-ia32-msvc": ["@napi-rs/nice-win32-ia32-msvc@1.0.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw=="],
"@napi-rs/nice-win32-x64-msvc": ["@napi-rs/nice-win32-x64-msvc@1.0.1", "", { "os": "win32", "cpu": "x64" }, "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg=="],
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
"@pnpm/config.env-replace": ["@pnpm/config.env-replace@1.1.0", "", {}, "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w=="],
"@pnpm/network.ca-file": ["@pnpm/network.ca-file@1.0.2", "", { "dependencies": { "graceful-fs": "4.2.10" } }, "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA=="],
"@pnpm/npm-conf": ["@pnpm/npm-conf@2.3.1", "", { "dependencies": { "@pnpm/config.env-replace": "^1.1.0", "@pnpm/network.ca-file": "^1.0.1", "config-chain": "^1.1.11" } }, "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw=="],
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@sindresorhus/is": ["@sindresorhus/is@5.6.0", "", {}, "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g=="],
"@swc/cli": ["@swc/cli@0.7.7", "", { "dependencies": { "@swc/counter": "^0.1.3", "@xhmikosr/bin-wrapper": "^13.0.5", "commander": "^8.3.0", "fast-glob": "^3.2.5", "minimatch": "^9.0.3", "piscina": "^4.3.1", "semver": "^7.3.8", "slash": "3.0.0", "source-map": "^0.7.3" }, "peerDependencies": { "@swc/core": "^1.2.66", "chokidar": "^4.0.1" }, "optionalPeers": ["chokidar"], "bin": { "swc": "bin/swc.js", "swcx": "bin/swcx.js", "spack": "bin/spack.js" } }, "sha512-j4yYm9bx3pxWofaJKX1BFwj/3ngUDynN4UIQ2Xd2h0h/7Gt7zkReBTpDN7g5S13mgAYxacaTHTOUsz18097E8w=="],
"@swc/core": ["@swc/core@1.11.24", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.24", "@swc/core-darwin-x64": "1.11.24", "@swc/core-linux-arm-gnueabihf": "1.11.24", "@swc/core-linux-arm64-gnu": "1.11.24", "@swc/core-linux-arm64-musl": "1.11.24", "@swc/core-linux-x64-gnu": "1.11.24", "@swc/core-linux-x64-musl": "1.11.24", "@swc/core-win32-arm64-msvc": "1.11.24", "@swc/core-win32-ia32-msvc": "1.11.24", "@swc/core-win32-x64-msvc": "1.11.24" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-MaQEIpfcEMzx3VWWopbofKJvaraqmL6HbLlw2bFZ7qYqYw3rkhM0cQVEgyzbHtTWwCwPMFZSC2DUbhlZgrMfLg=="],
"@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.24", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dhtVj0PC1APOF4fl5qT2neGjRLgHAAYfiVP8poJelhzhB/318bO+QCFWAiimcDoyMgpCXOhTp757gnoJJrheWA=="],
"@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.24", "", { "os": "darwin", "cpu": "x64" }, "sha512-H/3cPs8uxcj2Fe3SoLlofN5JG6Ny5bl8DuZ6Yc2wr7gQFBmyBkbZEz+sPVgsID7IXuz7vTP95kMm1VL74SO5AQ=="],
"@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.24", "", { "os": "linux", "cpu": "arm" }, "sha512-PHJgWEpCsLo/NGj+A2lXZ2mgGjsr96ULNW3+T3Bj2KTc8XtMUkE8tmY2Da20ItZOvPNC/69KroU7edyo1Flfbw=="],
"@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-C2FJb08+n5SD4CYWCTZx1uR88BN41ZieoHvI8A55hfVf2woT8+6ZiBzt74qW2g+ntZ535Jts5VwXAKdu41HpBg=="],
"@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.24", "", { "os": "linux", "cpu": "arm64" }, "sha512-ypXLIdszRo0re7PNNaXN0+2lD454G8l9LPK/rbfRXnhLWDBPURxzKlLlU/YGd2zP98wPcVooMmegRSNOKfvErw=="],
"@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.24", "", { "os": "linux", "cpu": "x64" }, "sha512-IM7d+STVZD48zxcgo69L0yYptfhaaE9cMZ+9OoMxirNafhKKXwoZuufol1+alEFKc+Wbwp+aUPe/DeWC/Lh3dg=="],
"@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.24", "", { "os": "linux", "cpu": "x64" }, "sha512-DZByJaMVzSfjQKKQn3cqSeqwy6lpMaQDQQ4HPlch9FWtDx/dLcpdIhxssqZXcR2rhaQVIaRQsCqwV6orSDGAGw=="],
"@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.24", "", { "os": "win32", "cpu": "arm64" }, "sha512-Q64Ytn23y9aVDKN5iryFi8mRgyHw3/kyjTjT4qFCa8AEb5sGUuSj//AUZ6c0J7hQKMHlg9do5Etvoe61V98/JQ=="],
"@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.24", "", { "os": "win32", "cpu": "ia32" }, "sha512-9pKLIisE/Hh2vJhGIPvSoTK4uBSPxNVyXHmOrtdDot4E1FUUI74Vi8tFdlwNbaj8/vusVnb8xPXsxF1uB0VgiQ=="],
"@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.24", "", { "os": "win32", "cpu": "x64" }, "sha512-sybnXtOsdB+XvzVFlBVGgRHLqp3yRpHK7CrmpuDKszhj/QhmsaZzY/GHSeALlMtLup13M0gqbcQvsTNlAHTg3w=="],
"@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="],
"@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="],
"@szmarczak/http-timer": ["@szmarczak/http-timer@5.0.1", "", { "dependencies": { "defer-to-connect": "^2.0.1" } }, "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw=="],
"@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
"@types/filesize-parser": ["@types/filesize-parser@1.5.3", "", {}, "sha512-D8veS92mQAfcKz5kbV9WINfKvvaxVfQHqnO3OWW6FxdYTZozDDb8mTLWCsvEqe8M/M7skeULTZzYeMf3Lsss+A=="],
"@types/fs-extra": ["@types/fs-extra@11.0.4", "", { "dependencies": { "@types/jsonfile": "*", "@types/node": "*" } }, "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ=="],
"@types/http-cache-semantics": ["@types/http-cache-semantics@4.0.4", "", {}, "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA=="],
"@types/jsonfile": ["@types/jsonfile@6.1.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ=="],
"@types/node": ["@types/node@22.15.18", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-v1DKRfUdyW+jJhZNEI1PYy29S2YRxMV5AOO/x/SjKmW0acCIOqmbj6Haf9eHAhsPmrhlHSxEhv/1WszcLWV4cg=="],
"@types/node-fetch": ["@types/node-fetch@2.6.12", "", { "dependencies": { "@types/node": "*", "form-data": "^4.0.0" } }, "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA=="],
"@types/progress": ["@types/progress@2.0.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-iadjw02vte8qWx7U0YM++EybBha2CQLPGu9iJ97whVgJUT5Zq9MjAPYUnbfRI2Kpehimf1QjFJYxD0t8nqzu5w=="],
"@types/semver": ["@types/semver@7.7.0", "", {}, "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA=="],
"@types/tcp-ping": ["@types/tcp-ping@0.1.6", "", {}, "sha512-1TTOm3cK+0NrZ6uTlY51YERzo5tEW3RybX1594vy9oiZOc1vsVL4LmdiIklDJXYLlzmve5lBkM5GjvwBG1Bc4A=="],
"@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="],
"@types/yazl": ["@types/yazl@2.4.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-/ifFjQtcKaoZOjl5NNCQRR0fAKafB3Foxd7J/WvFPTMea46zekapcR30uzkwIkKAAuq5T6d0dkwz754RFH27hg=="],
"@xhmikosr/archive-type": ["@xhmikosr/archive-type@7.0.0", "", { "dependencies": { "file-type": "^19.0.0" } }, "sha512-sIm84ZneCOJuiy3PpWR5bxkx3HaNt1pqaN+vncUBZIlPZCq8ASZH+hBVdu5H8znR7qYC6sKwx+ie2Q7qztJTxA=="],
"@xhmikosr/bin-check": ["@xhmikosr/bin-check@7.0.3", "", { "dependencies": { "execa": "^5.1.1", "isexe": "^2.0.0" } }, "sha512-4UnCLCs8DB+itHJVkqFp9Zjg+w/205/J2j2wNBsCEAm/BuBmtua2hhUOdAMQE47b1c7P9Xmddj0p+X1XVsfHsA=="],
"@xhmikosr/bin-wrapper": ["@xhmikosr/bin-wrapper@13.0.5", "", { "dependencies": { "@xhmikosr/bin-check": "^7.0.3", "@xhmikosr/downloader": "^15.0.1", "@xhmikosr/os-filter-obj": "^3.0.0", "bin-version-check": "^5.1.0" } }, "sha512-DT2SAuHDeOw0G5bs7wZbQTbf4hd8pJ14tO0i4cWhRkIJfgRdKmMfkDilpaJ8uZyPA0NVRwasCNAmMJcWA67osw=="],
"@xhmikosr/decompress": ["@xhmikosr/decompress@10.0.1", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.0.1", "@xhmikosr/decompress-tarbz2": "^8.0.1", "@xhmikosr/decompress-targz": "^8.0.1", "@xhmikosr/decompress-unzip": "^7.0.0", "graceful-fs": "^4.2.11", "make-dir": "^4.0.0", "strip-dirs": "^3.0.0" } }, "sha512-6uHnEEt5jv9ro0CDzqWlFgPycdE+H+kbJnwyxgZregIMLQ7unQSCNVsYG255FoqU8cP46DyggI7F7LohzEl8Ag=="],
"@xhmikosr/decompress-tar": ["@xhmikosr/decompress-tar@8.0.1", "", { "dependencies": { "file-type": "^19.0.0", "is-stream": "^2.0.1", "tar-stream": "^3.1.7" } }, "sha512-dpEgs0cQKJ2xpIaGSO0hrzz3Kt8TQHYdizHsgDtLorWajuHJqxzot9Hbi0huRxJuAGG2qiHSQkwyvHHQtlE+fg=="],
"@xhmikosr/decompress-tarbz2": ["@xhmikosr/decompress-tarbz2@8.0.1", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.0.1", "file-type": "^19.0.0", "is-stream": "^2.0.1", "seek-bzip": "^2.0.0", "unbzip2-stream": "^1.4.3" } }, "sha512-OF+6DysDZP5YTDO8uHuGG6fMGZjc+HszFPBkVltjoje2Cf60hjBg/YP5OQndW1hfwVWOdP7f3CnJiPZHJUTtEg=="],
"@xhmikosr/decompress-targz": ["@xhmikosr/decompress-targz@8.0.1", "", { "dependencies": { "@xhmikosr/decompress-tar": "^8.0.1", "file-type": "^19.0.0", "is-stream": "^2.0.1" } }, "sha512-mvy5AIDIZjQ2IagMI/wvauEiSNHhu/g65qpdM4EVoYHUJBAmkQWqcPJa8Xzi1aKVTmOA5xLJeDk7dqSjlHq8Mg=="],
"@xhmikosr/decompress-unzip": ["@xhmikosr/decompress-unzip@7.0.0", "", { "dependencies": { "file-type": "^19.0.0", "get-stream": "^6.0.1", "yauzl": "^3.1.2" } }, "sha512-GQMpzIpWTsNr6UZbISawsGI0hJ4KA/mz5nFq+cEoPs12UybAqZWKbyIaZZyLbJebKl5FkLpsGBkrplJdjvUoSQ=="],
"@xhmikosr/downloader": ["@xhmikosr/downloader@15.0.1", "", { "dependencies": { "@xhmikosr/archive-type": "^7.0.0", "@xhmikosr/decompress": "^10.0.1", "content-disposition": "^0.5.4", "defaults": "^3.0.0", "ext-name": "^5.0.0", "file-type": "^19.0.0", "filenamify": "^6.0.0", "get-stream": "^6.0.1", "got": "^13.0.0" } }, "sha512-fiuFHf3Dt6pkX8HQrVBsK0uXtkgkVlhrZEh8b7VgoDqFf+zrgFBPyrwCqE/3nDwn3hLeNz+BsrS7q3mu13Lp1g=="],
"@xhmikosr/os-filter-obj": ["@xhmikosr/os-filter-obj@3.0.0", "", { "dependencies": { "arch": "^3.0.0" } }, "sha512-siPY6BD5dQ2SZPl3I0OZBHL27ZqZvLEosObsZRQ1NUB8qcxegwt0T9eKtV96JMFQpIz1elhkzqOg4c/Ri6Dp9A=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.8.10", "", {}, "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"arch": ["arch@3.0.0", "", {}, "sha512-AmIAC+Wtm2AU8lGfTtHsw0Y9Qtftx2YXEEtiBP10xFUtMOA+sHHx6OAddyL52mUKh1vsXQ6/w1mVDptZCyUt4Q=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.1", "", { "dependencies": { "call-bind": "^1.0.5", "is-array-buffer": "^3.0.4" } }, "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg=="],
"array.prototype.flat": ["array.prototype.flat@1.3.2", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "es-shim-unscopables": "^1.0.0" } }, "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA=="],
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.5", "define-properties": "^1.2.1", "es-abstract": "^1.22.3", "es-errors": "^1.2.1", "get-intrinsic": "^1.2.3", "is-array-buffer": "^3.0.4", "is-shared-array-buffer": "^1.0.2" } }, "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A=="],
"async-lock": ["async-lock@1.4.1", "", {}, "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
"b4a": ["b4a@1.6.7", "", {}, "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"bare-events": ["bare-events@2.5.0", "", {}, "sha512-/E8dDe9dsbLyh2qrZ64PEPadOQ0F4gbl1sUJOrmph7xOiIxfY8vwab/4bFLh4Y88/Hk/ujKcrQKc+ps0mv873A=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"big-integer": ["big-integer@1.6.52", "", {}, "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg=="],
"bin-version": ["bin-version@6.0.0", "", { "dependencies": { "execa": "^5.0.0", "find-versions": "^5.0.0" } }, "sha512-nk5wEsP4RiKjG+vF+uG8lFsEn4d7Y6FVDamzzftSunXOoOcOOkzcWdKVlGgFFwlUQCj63SgnUkLLGF8v7lufhw=="],
"bin-version-check": ["bin-version-check@5.1.0", "", { "dependencies": { "bin-version": "^6.0.0", "semver": "^7.5.3", "semver-truncate": "^3.0.0" } }, "sha512-bYsvMqJ8yNGILLz1KP9zKLzQ6YpljV3ln1gqhuLkUtyfGi3qXKGuK2p+U4NAvjVFzDFiBBtOpCOSFNuYYEGZ5g=="],
"bplist-parser": ["bplist-parser@0.3.2", "", { "dependencies": { "big-integer": "1.6.x" } }, "sha512-apC2+fspHGI3mMKj+dGevkGo/tCqVB8jMb6i+OX+E29p0Iposz07fABkRIfVUPNd5A5VbuOz1bZbnmkKLYF+wQ=="],
"brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"breakword": ["breakword@1.0.6", "", { "dependencies": { "wcwidth": "^1.0.1" } }, "sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="],
"bufferpack": ["bufferpack@0.0.6", "", {}, "sha512-MTWvLHElqczrIVhge9qHtqgNigJFyh0+tCDId5yCbFAfuekHWIG+uAgvoHVflwrDPuY/e47JE1ki5qcM7w4uLg=="],
"bytebuffer": ["bytebuffer@5.0.1", "", { "dependencies": { "long": "~3" } }, "sha512-IuzSdmADppkZ6DlpycMkm8l9zeEq16fWtLvunEwFiYciR/BHo4E8/xs5piFquG+Za8OWmMqHF8zuRviz2LHvRQ=="],
"cacheable-lookup": ["cacheable-lookup@7.0.0", "", {}, "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w=="],
"cacheable-request": ["cacheable-request@10.2.14", "", { "dependencies": { "@types/http-cache-semantics": "^4.0.2", "get-stream": "^6.0.1", "http-cache-semantics": "^4.1.1", "keyv": "^4.5.3", "mimic-response": "^4.0.0", "normalize-url": "^8.0.0", "responselike": "^3.0.0" } }, "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ=="],
"call-bind": ["call-bind@1.0.7", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.1" } }, "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="],
"cgbi-to-png": ["cgbi-to-png@1.0.7", "", { "dependencies": { "bufferpack": "0.0.6", "crc": "^3.3.0", "stream-to-buffer": "^0.1.0", "streamifier": "^0.1.1" } }, "sha512-YR80kxTPuq9oRpZUdQmNEQWrmTKLINk1cfLVfyrV7Rfr9KLtLJdcockPKbreIr4JYAq+DhHBR7w+WA/tF5VDaQ=="],
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
"clean-git-ref": ["clean-git-ref@2.0.1", "", {}, "sha512-bLSptAy2P0s6hU4PzuIMKmMJJSE6gLXGH1cntDu7bWJUksvuM+7ReOK61mozULErYvP6a15rnYl0zFDef+pyPw=="],
"cli-arguments": ["cli-arguments@0.2.1", "", {}, "sha512-vaoTjiREjxKlpTNMiaJUkQnYRhgui8r+huhB6mMHcGQyz5F7Hd1o1jsW9C/wRKjlNYQ6fTvODLtZe7DxfEIz8g=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
"compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="],
"config-chain": ["config-chain@1.1.13", "", { "dependencies": { "ini": "^1.3.4", "proto-list": "~1.2.1" } }, "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ=="],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"crc": ["crc@3.8.0", "", { "dependencies": { "buffer": "^5.1.0" } }, "sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ=="],
"crc-32": ["crc-32@1.2.2", "", { "bin": { "crc32": "bin/crc32.njs" } }, "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ=="],
"cross-spawn": ["cross-spawn@7.0.3", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w=="],
"csv": ["csv@5.5.3", "", { "dependencies": { "csv-generate": "^3.4.3", "csv-parse": "^4.16.3", "csv-stringify": "^5.6.5", "stream-transform": "^2.1.3" } }, "sha512-QTaY0XjjhTQOdguARF0lGKm5/mEq9PD9/VhZZegHDIBq2tQwgNpHc3dneD4mGo2iJs+fTKv5Bp0fZ+BRuY3Z0g=="],
"csv-generate": ["csv-generate@3.4.3", "", {}, "sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw=="],
"csv-parse": ["csv-parse@4.16.3", "", {}, "sha512-cO1I/zmz4w2dcKHVvpCr7JVRu8/FymG5OEpmvsZYlccYolPBLoVGKUHgNoc4ZGkFeFlWGEDmMyBM+TTqRdW/wg=="],
"csv-stringify": ["csv-stringify@5.6.5", "", {}, "sha512-PjiQ659aQ+fUTQqSrd1XEDnOr52jh30RBurfzkscaE2tPaFsDH5wOAHJiw8XAHphRknCwMUE9KRayc4K/NbO8A=="],
"data-view-buffer": ["data-view-buffer@1.0.1", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA=="],
"data-view-byte-length": ["data-view-byte-length@1.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ=="],
"data-view-byte-offset": ["data-view-byte-offset@1.0.0", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA=="],
"decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="],
"defer-to-connect": ["defer-to-connect@2.0.1", "", {}, "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg=="],
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="],
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"diff3": ["diff3@0.0.3", "", {}, "sha512-iSq8ngPOt0K53A6eVr4d5Kn6GNrM2nQZtC740pzIriHtn4pOQ2lyzEXQMBeVcWERN0ye7fhBsk9PbLLQOnUx/g=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"es-abstract": ["es-abstract@1.23.3", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "arraybuffer.prototype.slice": "^1.0.3", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", "data-view-buffer": "^1.0.1", "data-view-byte-length": "^1.0.1", "data-view-byte-offset": "^1.0.0", "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.0.3", "es-to-primitive": "^1.2.1", "function.prototype.name": "^1.1.6", "get-intrinsic": "^1.2.4", "get-symbol-description": "^1.0.2", "globalthis": "^1.0.3", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2", "has-proto": "^1.0.3", "has-symbols": "^1.0.3", "hasown": "^2.0.2", "internal-slot": "^1.0.7", "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", "is-data-view": "^1.0.1", "is-negative-zero": "^2.0.3", "is-regex": "^1.1.4", "is-shared-array-buffer": "^1.0.3", "is-string": "^1.0.7", "is-typed-array": "^1.1.13", "is-weakref": "^1.0.2", "object-inspect": "^1.13.1", "object-keys": "^1.1.1", "object.assign": "^4.1.5", "regexp.prototype.flags": "^1.5.2", "safe-array-concat": "^1.1.2", "safe-regex-test": "^1.0.3", "string.prototype.trim": "^1.2.9", "string.prototype.trimend": "^1.0.8", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.2", "typed-array-byte-length": "^1.0.1", "typed-array-byte-offset": "^1.0.2", "typed-array-length": "^1.0.6", "unbox-primitive": "^1.0.2", "which-typed-array": "^1.1.15" } }, "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"es-shim-unscopables": ["es-shim-unscopables@1.0.2", "", { "dependencies": { "hasown": "^2.0.0" } }, "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw=="],
"es-to-primitive": ["es-to-primitive@1.2.1", "", { "dependencies": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", "is-symbol": "^1.0.2" } }, "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="],
"ext-list": ["ext-list@2.2.2", "", { "dependencies": { "mime-db": "^1.28.0" } }, "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA=="],
"ext-name": ["ext-name@5.0.0", "", { "dependencies": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" } }, "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ=="],
"fast-fifo": ["fast-fifo@1.3.2", "", {}, "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ=="],
"fast-glob": ["fast-glob@3.3.2", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow=="],
"fastq": ["fastq@1.17.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w=="],
"fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="],
"file-type": ["file-type@19.6.0", "", { "dependencies": { "get-stream": "^9.0.1", "strtok3": "^9.0.1", "token-types": "^6.0.0", "uint8array-extras": "^1.3.0" } }, "sha512-VZR5I7k5wkD0HgFnMsq5hOsSc710MJMu5Nc5QYsbe38NN5iPV/XTObYLc/cpttRTf6lX538+5uO1ZQRhYibiZQ=="],
"filename-reserved-regex": ["filename-reserved-regex@3.0.0", "", {}, "sha512-hn4cQfU6GOT/7cFHXBqeBg2TbrMBgdD0kcjLhvSQYYwm3s4B6cjvBfb7nBALJLAXqmU5xajSa7X2NnUud/VCdw=="],
"filenamify": ["filenamify@6.0.0", "", { "dependencies": { "filename-reserved-regex": "^3.0.0" } }, "sha512-vqIlNogKeyD3yzrm0yhRMQg8hOVwYcYRfjEoODd49iCprMn4HL85gK3HcykQE53EPIpX3HcAbGA5ELQv216dAQ=="],
"filesize-parser": ["filesize-parser@1.5.1", "", {}, "sha512-wRjdlQ5JM3WHZp6xpakIHQbkcGig8ANglYQDPcQSgZUN5kcDGOgmAwB0396BxzHxcl+kr+GLuusxBnsjdO6x9A=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="],
"find-versions": ["find-versions@5.1.0", "", { "dependencies": { "semver-regex": "^4.0.5" } }, "sha512-+iwzCJ7C5v5KgcBuueqVoNiHVoQpwiUK5XFLjf0affFTep+Wcw93tPvmb8tqujDNmzhBDPddnWV/qgWSXgq+Hg=="],
"for-each": ["for-each@0.3.3", "", { "dependencies": { "is-callable": "^1.1.3" } }, "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw=="],
"form-data": ["form-data@4.0.2", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "mime-types": "^2.1.12" } }, "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w=="],
"form-data-encoder": ["form-data-encoder@2.1.4", "", {}, "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw=="],
"fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"function.prototype.name": ["function.prototype.name@1.1.6", "", { "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.2.0", "es-abstract": "^1.22.1", "functions-have-names": "^1.2.3" } }, "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg=="],
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
"get-symbol-description": ["get-symbol-description@1.0.2", "", { "dependencies": { "call-bind": "^1.0.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4" } }, "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"global-dirs": ["global-dirs@4.0.0", "", { "dependencies": { "ini": "2.0.0" } }, "sha512-PJ0OjGf/kVuu9gh5IPgAyssfJne5PsU9+ICxfWiRYDUnYq8ob+Y2nSWAEUNEHRj+gowyzI+wg5/nWkvcjcyLwg=="],
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"got": ["got@13.0.0", "", { "dependencies": { "@sindresorhus/is": "^5.2.0", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", "cacheable-request": "^10.2.8", "decompress-response": "^6.0.0", "form-data-encoder": "^2.1.2", "get-stream": "^6.0.1", "http2-wrapper": "^2.1.10", "lowercase-keys": "^3.0.0", "p-cancelable": "^3.0.0", "responselike": "^3.0.0" } }, "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gradle-to-js": ["gradle-to-js@2.0.1", "", { "dependencies": { "lodash.merge": "^4.6.2" }, "bin": { "gradle-to-js": "cli.js" } }, "sha512-is3hDn9zb8XXnjbEeAEIqxTpLHUiGBqjegLmXPuyMBfKAggpadWFku4/AP8iYAGBX6qR9/5UIUIp47V0XI3aMw=="],
"grapheme-splitter": ["grapheme-splitter@1.0.4", "", {}, "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ=="],
"has-bigints": ["has-bigints@1.0.2", "", {}, "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ=="],
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
"has-proto": ["has-proto@1.0.3", "", {}, "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-cache-semantics": ["http-cache-semantics@4.1.1", "", {}, "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ=="],
"http2-wrapper": ["http2-wrapper@2.2.1", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.2.0" } }, "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ=="],
"human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="],
"i18next": ["i18next@24.2.3", "", { "dependencies": { "@babel/runtime": "^7.26.10" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@2.0.0", "", {}, "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA=="],
"inspect-with-kind": ["inspect-with-kind@1.0.5", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-MAQUJuIo7Xqk8EVNP+6d3CKq9c80hi4tjIbIAT6lmGW9W6WzlHiu9PS8uSuUYU+Do+j1baiFp3H25XEVxDIG2g=="],
"internal-slot": ["internal-slot@1.0.7", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.0", "side-channel": "^1.0.4" } }, "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g=="],
"is-array-buffer": ["is-array-buffer@3.0.4", "", { "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.2.1" } }, "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw=="],
"is-bigint": ["is-bigint@1.0.4", "", { "dependencies": { "has-bigints": "^1.0.1" } }, "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg=="],
"is-boolean-object": ["is-boolean-object@1.1.2", "", { "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA=="],
"is-callable": ["is-callable@1.2.7", "", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
"is-data-view": ["is-data-view@1.0.1", "", { "dependencies": { "is-typed-array": "^1.1.13" } }, "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w=="],
"is-date-object": ["is-date-object@1.0.5", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-number-object": ["is-number-object@1.0.7", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ=="],
"is-plain-obj": ["is-plain-obj@1.1.0", "", {}, "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg=="],
"is-regex": ["is-regex@1.1.4", "", { "dependencies": { "call-bind": "^1.0.2", "has-tostringtag": "^1.0.0" } }, "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg=="],
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7" } }, "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"is-string": ["is-string@1.0.7", "", { "dependencies": { "has-tostringtag": "^1.0.0" } }, "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg=="],
"is-symbol": ["is-symbol@1.0.4", "", { "dependencies": { "has-symbols": "^1.0.2" } }, "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg=="],
"is-typed-array": ["is-typed-array@1.1.13", "", { "dependencies": { "which-typed-array": "^1.1.14" } }, "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw=="],
"is-weakref": ["is-weakref@1.0.2", "", { "dependencies": { "call-bind": "^1.0.2" } }, "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ=="],
"isarray": ["isarray@2.0.5", "", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"isomorphic-git": ["isomorphic-git@1.30.1", "", { "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", "crc-32": "^1.2.0", "diff3": "0.0.3", "ignore": "^5.1.4", "minimisted": "^2.0.0", "pako": "^1.0.10", "path-browserify": "^1.0.1", "pify": "^4.0.1", "readable-stream": "^3.4.0", "sha.js": "^2.4.9", "simple-get": "^4.0.1" }, "bin": { "isogit": "cli.cjs" } }, "sha512-eWBlPIPDOctGY/bTUc/whs6EZ8YvnG1H2kOjTCJ/AkvBWUzODXcfulhpiA8Y4Px9e+bRYYkifE5fSE8FcRk8Ew=="],
"isomorphic-unzip": ["isomorphic-unzip@1.1.5", "", { "dependencies": { "buffer": "^5.0.7", "yauzl": "^2.8.0" } }, "sha512-2McA51lWhmO3Kk438jxVcYeh6L+AOqVnl9XdX1yI7GlLA9RwEyTBgGem1rNuRIU2abAmOiv+IagThdUxASY4IA=="],
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
"jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="],
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
"locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"long": ["long@3.2.0", "", {}, "sha512-ZYvPPOMqUwPoDsbJaR10iQJYnMuZhRTvHYl62ErLIEX7RgFlziSBUUvrt3OVfc47QlHHpzPZYP17g3Fv7oeJkg=="],
"lowercase-keys": ["lowercase-keys@3.0.0", "", {}, "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ=="],
"make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
"merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minimisted": ["minimisted@2.0.1", "", { "dependencies": { "minimist": "^1.2.5" } }, "sha512-1oPjfuLQa2caorJUM8HV8lGgWCc0qqAO1MNv/k05G4qslmsndV/5WdNZrqCiyqiz3wohia2Ij2B7w2Dr7/IyrA=="],
"mixme": ["mixme@0.5.10", "", {}, "sha512-5H76ANWinB1H3twpJ6JY8uvAtpmFvHNArpilJAjXRKXSDDLPIMoZArw5SH0q9z+lLs8IrMw7Q2VWpWimFKFT1Q=="],
"mute-stream": ["mute-stream@2.0.0", "", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
"normalize-url": ["normalize-url@8.0.1", "", {}, "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w=="],
"npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="],
"object-inspect": ["object-inspect@1.13.2", "", {}, "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g=="],
"object-keys": ["object-keys@1.1.1", "", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
"object.assign": ["object.assign@4.1.5", "", { "dependencies": { "call-bind": "^1.0.5", "define-properties": "^1.2.1", "has-symbols": "^1.0.3", "object-keys": "^1.1.1" } }, "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="],
"p-cancelable": ["p-cancelable@3.0.0", "", {}, "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="],
"path-browserify": ["path-browserify@1.0.1", "", {}, "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g=="],
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"peek-readable": ["peek-readable@5.3.1", "", {}, "sha512-GVlENSDW6KHaXcd9zkZltB7tCLosKB/4Hg0fqBJkAoBgYG2Tn1xtMgXtSUuMU9AK/gCm/tTdT8mgAeF4YNeeqw=="],
"pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="],
"picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="],
"piscina": ["piscina@4.7.0", "", { "optionalDependencies": { "@napi-rs/nice": "^1.0.1" } }, "sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw=="],
"plist": ["plist@3.1.0", "", { "dependencies": { "@xmldom/xmldom": "^0.8.8", "base64-js": "^1.5.1", "xmlbuilder": "^15.1.1" } }, "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ=="],
"possible-typed-array-names": ["possible-typed-array-names@1.0.0", "", {}, "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q=="],
"progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="],
"properties": ["properties@1.2.1", "", {}, "sha512-qYNxyMj1JeW54i/EWEFsM1cVwxJbtgPp8+0Wg9XjNaK6VE/c4oRi6PNu5p7w1mNXEIQIjV5Wwn8v8Gz82/QzdQ=="],
"proto-list": ["proto-list@1.2.4", "", {}, "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA=="],
"queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
"queue-tick": ["queue-tick@1.0.1", "", {}, "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag=="],
"quick-lru": ["quick-lru@5.1.1", "", {}, "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA=="],
"read": ["read@4.1.0", "", { "dependencies": { "mute-stream": "^2.0.0" } }, "sha512-uRfX6K+f+R8OOrYScaM3ixPY4erg69f8DN6pgTvMcA9iRc8iDhwrA4m3Yu8YYKsXJgVvum+m8PkRboZwwuLzYA=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "set-function-name": "^2.0.2" } }, "sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ=="],
"registry-auth-token": ["registry-auth-token@5.1.0", "", { "dependencies": { "@pnpm/npm-conf": "^2.1.0" } }, "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="],
"resolve-alpn": ["resolve-alpn@1.2.1", "", {}, "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g=="],
"responselike": ["responselike@3.0.0", "", { "dependencies": { "lowercase-keys": "^3.0.0" } }, "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg=="],
"reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
"safe-array-concat": ["safe-array-concat@1.1.2", "", { "dependencies": { "call-bind": "^1.0.7", "get-intrinsic": "^1.2.4", "has-symbols": "^1.0.3", "isarray": "^2.0.5" } }, "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-regex-test": ["safe-regex-test@1.0.3", "", { "dependencies": { "call-bind": "^1.0.6", "es-errors": "^1.3.0", "is-regex": "^1.1.4" } }, "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw=="],
"seek-bzip": ["seek-bzip@2.0.0", "", { "dependencies": { "commander": "^6.0.0" }, "bin": { "seek-bunzip": "bin/seek-bunzip", "seek-table": "bin/seek-bzip-table" } }, "sha512-SMguiTnYrhpLdk3PwfzHeotrcwi8bNV4iemL9tx9poR/yeaMYwB9VzR1w7b57DuWpuqR8n6oZboi0hj3AxZxQg=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"semver-regex": ["semver-regex@4.0.5", "", {}, "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw=="],
"semver-truncate": ["semver-truncate@3.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-LJWA9kSvMolR51oDE6PN3kALBNaUdkxzAGcexw8gjMA8xr5zUqK0JiR3CgARSqanYF3Z1YHvsErb1KDgh+v7Rg=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
"sha.js": ["sha.js@2.4.11", "", { "dependencies": { "inherits": "^2.0.1", "safe-buffer": "^5.0.1" }, "bin": { "sha.js": "./bin.js" } }, "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"side-channel": ["side-channel@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.4", "object-inspect": "^1.13.1" } }, "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="],
"smartwrap": ["smartwrap@2.0.2", "", { "dependencies": { "array.prototype.flat": "^1.2.3", "breakword": "^1.0.5", "grapheme-splitter": "^1.0.4", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1", "yargs": "^15.1.0" }, "bin": { "smartwrap": "src/terminal-adapter.js" } }, "sha512-vCsKNQxb7PnCNd2wY1WClWifAc2lwqsG8OaswpJkVJsvMGcnEntdTCDajZCkk93Ay1U3t/9puJmb525Rg5MZBA=="],
"sort-keys": ["sort-keys@1.1.2", "", { "dependencies": { "is-plain-obj": "^1.0.0" } }, "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg=="],
"sort-keys-length": ["sort-keys-length@1.0.1", "", { "dependencies": { "sort-keys": "^1.0.0" } }, "sha512-GRbEOUqCxemTAk/b32F2xa8wDTs+Z1QHOkbhJDQTvv/6G3ZkbJ+frYWsTcc7cBB3Fu4wy4XlLCuNtJuMn7Gsvw=="],
"source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="],
"stream-to": ["stream-to@0.2.2", "", {}, "sha512-Kg1BSDTwgGiVMtTCJNlo7kk/xzL33ZuZveEBRt6rXw+f1WLK/8kmz2NVCT/Qnv0JkV85JOHcLhD82mnXsR3kPw=="],
"stream-to-buffer": ["stream-to-buffer@0.1.0", "", { "dependencies": { "stream-to": "~0.2.0" } }, "sha512-Da4WoKaZyu3nf+bIdIifh7IPkFjARBnBK+pYqn0EUJqksjV9afojjaCCHUemH30Jmu7T2qcKvlZm2ykN38uzaw=="],
"stream-transform": ["stream-transform@2.1.3", "", { "dependencies": { "mixme": "^0.5.1" } }, "sha512-9GHUiM5hMiCi6Y03jD2ARC1ettBXkQBoQAe7nJsPknnI0ow10aXjTnew8QtYQmLjzn974BnmWEAJgCY6ZP1DeQ=="],
"streamifier": ["streamifier@0.1.1", "", {}, "sha512-zDgl+muIlWzXNsXeyUfOk9dChMjlpkq0DRsxujtYPgyJ676yQ8jEm6zzaaWHFDg5BNcLuif0eD2MTyJdZqXpdg=="],
"streamx": ["streamx@2.20.2", "", { "dependencies": { "fast-fifo": "^1.3.2", "queue-tick": "^1.0.1", "text-decoder": "^1.1.0" }, "optionalDependencies": { "bare-events": "^2.2.0" } }, "sha512-aDGDLU+j9tJcUdPGOaHmVF1u/hhI+CsGkT02V3OKlHDV7IukOI+nTWAGkiZEKCO35rWN1wIr4tS7YFr1f4qSvA=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string.prototype.trim": ["string.prototype.trim@1.2.9", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.0", "es-object-atoms": "^1.0.0" } }, "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw=="],
"string.prototype.trimend": ["string.prototype.trimend@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ=="],
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-dirs": ["strip-dirs@3.0.0", "", { "dependencies": { "inspect-with-kind": "^1.0.5", "is-plain-obj": "^1.1.0" } }, "sha512-I0sdgcFTfKQlUPZyAqPJmSG3HLO9rWDFnxonnIbskYNM3DwFOeTNB5KzVq3dA1GdRAc/25b5Y7UO2TQfKWw4aQ=="],
"strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="],
"strtok3": ["strtok3@9.0.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^5.3.1" } }, "sha512-ERPW+XkvX9W2A+ov07iy+ZFJpVdik04GhDA4eVogiG9hpC97Kem2iucyzhFxbFRvQ5o2UckFtKZdp1hkGvnrEw=="],
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
"tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
"tcp-ping": ["tcp-ping@0.1.1", "", {}, "sha512-7Ed10Ds0hYnF+O1lfiZ2iSZ1bCAj+96Madctebmq7Y1ALPWlBY4YI8C6pCL+UTlshFY5YogixKLpgDP/4BlHrw=="],
"text-decoder": ["text-decoder@1.2.1", "", {}, "sha512-x9v3H/lTKIJKQQe7RPQkLfKAnc9lUTkWDypIQgTzPJAq+5/GCDHonmshfvlsNSj58yyshbIJJDLmU15qNERrXQ=="],
"through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"token-types": ["token-types@6.0.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-lbDrTLVsHhOMljPscd0yitpozq7Ga2M5Cvez5AjGg8GASBjtt6iERCAJ93yommPmz62fb45oFIXHEZ3u9bfJEA=="],
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
"tty-table": ["tty-table@4.2.3", "", { "dependencies": { "chalk": "^4.1.2", "csv": "^5.5.3", "kleur": "^4.1.5", "smartwrap": "^2.0.2", "strip-ansi": "^6.0.1", "wcwidth": "^1.0.1", "yargs": "^17.7.1" }, "bin": { "tty-table": "adapters/terminal-adapter.js" } }, "sha512-Fs15mu0vGzCrj8fmJNP7Ynxt5J7praPXqFN0leZeZBXJwkMxv9cb2D454k1ltrtUSJbZ4yH4e0CynsHLxmUfFA=="],
"typed-array-buffer": ["typed-array-buffer@1.0.2", "", { "dependencies": { "call-bind": "^1.0.7", "es-errors": "^1.3.0", "is-typed-array": "^1.1.13" } }, "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ=="],
"typed-array-byte-length": ["typed-array-byte-length@1.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-proto": "^1.0.3", "is-typed-array": "^1.1.13" } }, "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw=="],
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.2", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-proto": "^1.0.3", "is-typed-array": "^1.1.13" } }, "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA=="],
"typed-array-length": ["typed-array-length@1.0.6", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0" } }, "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="],
"unbox-primitive": ["unbox-primitive@1.0.2", "", { "dependencies": { "call-bind": "^1.0.2", "has-bigints": "^1.0.2", "has-symbols": "^1.0.3", "which-boxed-primitive": "^1.0.2" } }, "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw=="],
"unbzip2-stream": ["unbzip2-stream@1.4.3", "", { "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="],
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"which-boxed-primitive": ["which-boxed-primitive@1.0.2", "", { "dependencies": { "is-bigint": "^1.0.1", "is-boolean-object": "^1.1.0", "is-number-object": "^1.0.4", "is-string": "^1.0.5", "is-symbol": "^1.0.3" } }, "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg=="],
"which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="],
"which-typed-array": ["which-typed-array@1.1.15", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "has-tostringtag": "^1.0.2" } }, "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"xmlbuilder": ["xmlbuilder@15.1.1", "", {}, "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"yauzl": ["yauzl@3.2.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "pend": "~1.2.0" } }, "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w=="],
"yazl": ["yazl@3.3.1", "", { "dependencies": { "buffer-crc32": "^1.0.0" } }, "sha512-BbETDVWG+VcMUle37k5Fqp//7SDOK2/1+T7X8TD96M3D9G8jK5VLUdQVdVjGi8im7FGkazX7kk5hkU8X4L5Bng=="],
"@pnpm/network.ca-file/graceful-fs": ["graceful-fs@4.2.10", "", {}, "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="],
"@swc/cli/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
"@types/fs-extra/@types/node": ["@types/node@22.9.3", "", { "dependencies": { "undici-types": "~6.19.8" } }, "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw=="],
"@types/jsonfile/@types/node": ["@types/node@22.9.3", "", { "dependencies": { "undici-types": "~6.19.8" } }, "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw=="],
"@types/node-fetch/@types/node": ["@types/node@22.9.3", "", { "dependencies": { "undici-types": "~6.19.8" } }, "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw=="],
"@types/node-fetch/form-data": ["form-data@4.0.1", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "mime-types": "^2.1.12" } }, "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw=="],
"@types/progress/@types/node": ["@types/node@22.9.3", "", { "dependencies": { "undici-types": "~6.19.8" } }, "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw=="],
"@types/yauzl/@types/node": ["@types/node@22.9.3", "", { "dependencies": { "undici-types": "~6.19.8" } }, "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw=="],
"@types/yazl/@types/node": ["@types/node@22.9.3", "", { "dependencies": { "undici-types": "~6.19.8" } }, "sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw=="],
"@xhmikosr/downloader/defaults": ["defaults@3.0.0", "", {}, "sha512-RsqXDEAALjfRTro+IFNKpcPCt0/Cy2FqHSIlnomiJp9YGadpQnrtbRpSgN2+np21qHcIKiva4fiOQGjS9/qR/A=="],
"arraybuffer.prototype.slice/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"bin-version-check/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"cacheable-request/mimic-response": ["mimic-response@4.0.0", "", {}, "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg=="],
"call-bind/es-define-property": ["es-define-property@1.0.0", "", { "dependencies": { "get-intrinsic": "^1.2.4" } }, "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ=="],
"call-bind/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"config-chain/ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"define-data-property/es-define-property": ["es-define-property@1.0.0", "", { "dependencies": { "get-intrinsic": "^1.2.4" } }, "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ=="],
"define-data-property/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"es-abstract/es-define-property": ["es-define-property@1.0.0", "", { "dependencies": { "get-intrinsic": "^1.2.4" } }, "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ=="],
"es-abstract/es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="],
"es-abstract/es-set-tostringtag": ["es-set-tostringtag@2.0.3", "", { "dependencies": { "get-intrinsic": "^1.2.4", "has-tostringtag": "^1.0.2", "hasown": "^2.0.1" } }, "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ=="],
"es-abstract/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"es-abstract/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"es-abstract/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"file-type/get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="],
"get-symbol-description/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"globalthis/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"has-property-descriptors/es-define-property": ["es-define-property@1.0.0", "", { "dependencies": { "get-intrinsic": "^1.2.4" } }, "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ=="],
"has-tostringtag/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"is-array-buffer/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"is-symbol/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"isomorphic-unzip/yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="],
"make-dir/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"object.assign/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"safe-array-concat/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"safe-array-concat/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"seek-bzip/commander": ["commander@6.2.1", "", {}, "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA=="],
"semver-truncate/semver": ["semver@7.6.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A=="],
"set-function-length/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"set-function-length/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"side-channel/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"smartwrap/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="],
"string.prototype.trim/es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="],
"string.prototype.trimend/es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="],
"string.prototype.trimstart/es-object-atoms": ["es-object-atoms@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw=="],
"typed-array-byte-length/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"typed-array-byte-offset/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"typed-array-length/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"unbox-primitive/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"which-typed-array/gopd": ["gopd@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.1.3" } }, "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA=="],
"yazl/buffer-crc32": ["buffer-crc32@1.0.0", "", {}, "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w=="],
"@types/fs-extra/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"@types/jsonfile/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"@types/node-fetch/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"@types/progress/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"@types/yauzl/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"@types/yazl/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="],
"arraybuffer.prototype.slice/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"call-bind/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"define-data-property/es-define-property/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"define-data-property/gopd/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"file-type/get-stream/is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="],
"get-symbol-description/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"globalthis/gopd/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"has-property-descriptors/es-define-property/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"is-array-buffer/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"set-function-length/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"side-channel/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"smartwrap/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="],
"smartwrap/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="],
"smartwrap/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="],
"typed-array-byte-length/gopd/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"typed-array-byte-offset/gopd/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"typed-array-length/gopd/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"which-typed-array/gopd/get-intrinsic": ["get-intrinsic@1.2.4", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2", "has-proto": "^1.0.1", "has-symbols": "^1.0.3", "hasown": "^2.0.0" } }, "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ=="],
"define-data-property/es-define-property/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"define-data-property/gopd/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"globalthis/gopd/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"has-property-descriptors/es-define-property/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"smartwrap/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="],
"typed-array-byte-length/gopd/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"typed-array-byte-offset/gopd/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"typed-array-length/gopd/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
"which-typed-array/gopd/get-intrinsic/has-symbols": ["has-symbols@1.0.3", "", {}, "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A=="],
}
}

205
cli.json
View File

@@ -3,11 +3,9 @@
"defaultCommand": "help",
"commands": {
"help": {},
"login": {},
"logout": {},
"me": {},
"createApp": {
"options": {
"name": {
@@ -33,9 +31,28 @@
}
}
},
"uploadIpa": {},
"uploadApk": {},
"uploadIpa": {
"options": {
"version": {
"hasValue": true
}
}
},
"uploadApk": {
"options": {
"version": {
"hasValue": true
}
}
},
"uploadApp": {
"options": {
"version": {
"hasValue": true
}
}
},
"parseApp": {},
"parseIpa": {},
"parseApk": {},
"packages": {
@@ -45,7 +62,19 @@
}
}
},
"deletePackage": {
"options": {
"appId": {
"hasValue": true
},
"packageVersion": {
"hasValue": true
},
"packageId": {
"hasValue": true
}
}
},
"publish": {
"options": {
"platform": {
@@ -59,6 +88,27 @@
},
"metaInfo": {
"hasValue": true
},
"packageId": {
"hasValue": true
},
"packageVersion": {
"hasValue": true
},
"minPackageVersion": {
"hasValue": true
},
"maxPackageVersion": {
"hasValue": true
},
"packageVersionRange": {
"hasValue": true
},
"rollout": {
"hasValue": true
},
"dryRun": {
"default": false
}
}
},
@@ -69,7 +119,6 @@
}
}
},
"update": {
"options": {
"platform": {
@@ -81,18 +130,53 @@
"packageId": {
"hasValue": true
},
"packageVersion": {
"hasValue": true
},
"minPackageVersion": {
"hasValue": true
},
"maxPackageVersion": {
"hasValue": true
},
"packageVersionRange": {
"hasValue": true
},
"rollout": {
"hasValue": true
},
"dryRun": {
"default": false
}
}
},
"updateVersionInfo": {
"options": {
"platform": {
"hasValue": true
},
"versionId": {
"hasValue": true
},
"name": {
"default": false,
"hasValue": true
},
"description": {
"default": false,
"hasValue": true
},
"metaInfo": {
"default": false,
"hasValue": true
}
}
},
"build": {
"description": "Bundle javascript and copy assets."
},
"bundle": {
"description": "Bundle javascript code only.",
"description": "Bundle javascript code only and optionally publish.",
"options": {
"dev": {
"default": "false",
@@ -110,14 +194,61 @@
"hasValue": true
},
"intermediaDir": {
"default": ".pushy/intermedia/${platform}",
"default": "${tempDir}/intermedia/${platform}",
"hasValue": true
},
"output": {
"default": ".pushy/output/${platform}.${time}.ppk",
"default": "${tempDir}/output/${platform}.${time}.ppk",
"hasValue": true
},
"verbose": {}
"sourcemap": {
"default": false
},
"taro": {
"default": false
},
"expo": {
"default": false
},
"rncli": {
"default": false
},
"disableHermes": {
"default": false
},
"name": {
"hasValue": true,
"description": "Version name for publishing"
},
"description": {
"hasValue": true,
"description": "Version description for publishing"
},
"metaInfo": {
"hasValue": true,
"description": "Meta information for publishing"
},
"packageId": {
"hasValue": true
},
"packageVersion": {
"hasValue": true
},
"minPackageVersion": {
"hasValue": true
},
"maxPackageVersion": {
"hasValue": true
},
"packageVersionRange": {
"hasValue": true
},
"rollout": {
"hasValue": true
},
"dryRun": {
"default": false
}
}
},
"release": {
@@ -127,7 +258,7 @@
"description": "Create diff patch",
"options": {
"output": {
"default": ".pushy/output/diff",
"default": "${tempDir}/output/diff",
"hasValue": true
}
}
@@ -136,7 +267,7 @@
"description": "Create diff patch from a Android package(.apk)",
"options": {
"output": {
"default": ".pushy/output/diff-${time}.apk-patch",
"default": "${tempDir}/output/diff-${time}.apk-patch",
"hasValue": true
}
}
@@ -145,7 +276,7 @@
"description": "Create diff patch from a iOS package(.ipa)",
"options": {
"output": {
"default": ".pushy/output/diff-${time}.ipa-patch",
"default": "${tempDir}/output/diff-${time}.ipa-patch",
"hasValue": true
}
}
@@ -154,7 +285,7 @@
"description": "Create hdiff patch",
"options": {
"output": {
"default": ".pushy/output/hdiff",
"default": "${tempDir}/output/hdiff",
"hasValue": true
}
}
@@ -163,7 +294,25 @@
"description": "Create hdiff patch from a Android package(.apk)",
"options": {
"output": {
"default": ".pushy/output/hdiff-${time}.apk-patch",
"default": "${tempDir}/output/hdiff-${time}.apk-patch",
"hasValue": true
}
}
},
"hdiffFromPPK": {
"description": "Create hdiff patch from a Prepare package(.ppk)",
"options": {
"output": {
"default": "${tempDir}/output/hdiff-${time}.ppk-patch",
"hasValue": true
}
}
},
"hdiffFromApp": {
"description": "Create hdiff patch from a Harmony package(.app)",
"options": {
"output": {
"default": "${tempDir}/output/hdiff-${time}.app-patch",
"hasValue": true
}
}
@@ -172,7 +321,25 @@
"description": "Create hdiff patch from a iOS package(.ipa)",
"options": {
"output": {
"default": ".pushy/output/hdiff-${time}.ipa-patch",
"default": "${tempDir}/output/hdiff-${time}.ipa-patch",
"hasValue": true
}
}
},
"list": {
"description": "List all bundles",
"options": {
"output": {
"default": "${tempDir}/output/list",
"hasValue": true
}
}
},
"workflow": {
"description": "List all workflows",
"options": {
"output": {
"default": "${tempDir}/output/workflow",
"hasValue": true
}
}

View File

@@ -0,0 +1,471 @@
# 核心模块增强工作流
这个文档详细介绍了为React Native Update CLI核心模块设计的增强工作流包括`app-module``bundle-module``package-module``user-module``version-module`的高级工作流功能。
## 📋 目录
- [App模块工作流](#app模块工作流)
- [Bundle模块工作流](#bundle模块工作流)
- [Package模块工作流](#package模块工作流)
- [User模块工作流](#user模块工作流)
- [Version模块工作流](#version模块工作流)
- [工作流使用示例](#工作流使用示例)
- [最佳实践](#最佳实践)
## 🚀 快速开始
```bash
# 运行所有增强工作流演示
npx ts-node example/scripts/enhanced-workflow-demo.ts
# 交互式执行特定工作流
npx ts-node example/scripts/enhanced-workflow-demo.ts interactive [工作流名称] [参数...]
# 示例:应用初始化
npx ts-node example/scripts/enhanced-workflow-demo.ts interactive app-initialization --name MyApp --platform ios
# 示例:智能打包
npx ts-node example/scripts/enhanced-workflow-demo.ts interactive intelligent-bundle --platform android --optimize true
# 示例:版本发布
npx ts-node example/scripts/enhanced-workflow-demo.ts interactive version-release-management --name v1.0.0 --platform ios --dryRun true
```
---
## 📱 App模块工作流
### 1. 应用初始化工作流 (`app-initialization`)
**用途**: 完整的应用创建和初始化流程
**功能特性**:
- ✅ 参数验证和格式检查
- ✅ 应用存在性检查和冲突处理
- ✅ 应用创建和配置
- ✅ 自动选择新创建的应用
- ✅ 完整性验证和健康检查
**工作流步骤**:
1. **参数验证**: 检查应用名称、平台、下载URL格式
2. **存在性检查**: 验证应用是否已存在,支持强制覆盖
3. **应用创建**: 执行应用创建操作
4. **基本配置**: 设置更新策略、安全参数、版本控制
5. **应用选择**: 自动选择新创建的应用
6. **设置验证**: 验证应用配置的完整性和可用性
**使用示例**:
```typescript
await moduleManager.executeWorkflow('app-initialization', {
args: [],
options: {
name: 'MyAwesomeApp', // 应用名称 (必需)
platform: 'ios', // 平台 (必需)
downloadUrl: 'https://...', // 下载URL (可选)
force: false // 强制覆盖 (可选)
}
});
```
**适用场景**:
- 新项目应用创建
- 多环境应用设置
- 自动化部署脚本
### 2. 多平台应用管理工作流 (`multi-platform-app-management`)
**用途**: 跨平台应用统一管理和优化
**功能特性**:
- 🔍 全平台应用扫描
- 📊 应用状态分析和统计
- ⚡ 自动优化建议和执行
- 📈 应用健康度评估
**工作流步骤**:
1. **平台扫描**: 扫描iOS、Android、Harmony平台的所有应用
2. **状态分析**: 分析应用活跃度、版本分布、平台分布
3. **问题识别**: 识别非活跃应用、配置问题
4. **自动优化**: 执行应用配置优化和清理
**使用示例**:
```typescript
await moduleManager.executeWorkflow('multi-platform-app-management', {
args: [],
options: {
includeInactive: true, // 包含非活跃应用
autoOptimize: true // 自动优化配置
}
});
```
**适用场景**:
- 应用生态管理
- 定期健康检查
- 批量优化操作
---
## 📦 Bundle模块工作流
### 1. 智能打包工作流 (`intelligent-bundle`)
**用途**: 自动优化的多平台智能构建
**功能特性**:
- 🖥️ 构建环境自动检测
- 📂 项目结构智能分析
- ⚙️ 自动优化配置
- 🏗️ 多平台并行构建
- 🔍 构建质量检查
**工作流步骤**:
1. **环境检测**: 检查Node.js版本、内存、平台兼容性
2. **项目分析**: 分析项目类型、依赖、预估大小
3. **优化设置**: 根据项目特征自动配置优化选项
4. **多平台构建**: 并行构建指定平台或所有平台
5. **质量检查**: 检查构建质量、包大小、构建时间
**使用示例**:
```typescript
await moduleManager.executeWorkflow('intelligent-bundle', {
args: [],
options: {
platform: 'ios', // 目标平台 (可选,不指定则构建所有)
dev: false, // 开发模式
sourcemap: true, // 生成源码映射
optimize: true // 启用自动优化
}
});
```
**适用场景**:
- 自动化CI/CD构建
- 多平台发布准备
- 性能优化构建
### 2. 增量构建工作流 (`incremental-build`)
**用途**: 高效的增量更新包生成
**功能特性**:
- 🔍 自动基准版本检测
- 🏗️ 当前版本构建
- 📥 基准版本下载
- 🔄 智能差异计算
- ✅ 差异包验证
**工作流步骤**:
1. **基准检测**: 自动检测或使用指定的基准版本
2. **当前构建**: 构建当前版本的Bundle
3. **基准下载**: 下载基准版本的Bundle文件
4. **差异生成**: 计算并生成差异包
5. **验证测试**: 验证差异包的完整性和可用性
**使用示例**:
```typescript
await moduleManager.executeWorkflow('incremental-build', {
args: [],
options: {
platform: 'android', // 目标平台 (必需)
baseVersion: 'v1.0.0', // 基准版本 (可选,自动检测)
skipValidation: false // 跳过验证
}
});
```
**适用场景**:
- 热更新包生成
- 减少更新下载大小
- 快速增量发布
---
## 📄 Package模块工作流
### 1. 批量包处理工作流 (`batch-package-processing`)
**用途**: 批量处理多个应用包文件
**功能特性**:
- 🔍 智能文件扫描
- 📊 包信息分析统计
- 🔍 批量内容解析
- 📤 自动上传处理
- 📋 详细处理报告
**工作流步骤**:
1. **文件扫描**: 扫描指定目录的包文件IPA、APK、APP
2. **信息分析**: 分析包大小、平台分布、版本信息
3. **内容解析**: 批量解析包的元信息、权限、资源
4. **批量上传**: 自动上传解析成功的包文件
5. **报告生成**: 生成详细的处理报告和统计信息
**使用示例**:
```typescript
await moduleManager.executeWorkflow('batch-package-processing', {
args: [],
options: {
directory: './packages', // 包文件目录
pattern: '*.{ipa,apk,app}', // 文件匹配模式
skipUpload: false // 跳过上传步骤
}
});
```
**适用场景**:
- 批量包文件处理
- 包文件质量检查
- 自动化包管理
---
## 👤 User模块工作流
> User模块已经在现有代码中包含了完善的工作流
### 1. 认证状态检查工作流 (`auth-check`)
**功能特性**:
- 🔐 会话状态检查
- ✅ 服务端验证
- 👤 用户信息获取
- 🔄 自动登录支持
### 2. 完整登录流程工作流 (`login-flow`)
**功能特性**:
- 🔍 现有会话检查
- 🔐 用户登录执行
- ✅ 登录状态验证
- 📋 流程状态汇总
---
## 🏷️ Version模块工作流
### 1. 版本发布管理工作流 (`version-release-management`)
**用途**: 完整的版本发布生命周期管理
**功能特性**:
- 🔍 发布前全面检查
- ✅ 版本信息验证
- ⚙️ 发布参数配置
- 🚀 发布执行和监控
- 📊 发布后监控分析
- 📋 完整发布报告
**工作流步骤**:
1. **发布前检查**: 验证版本格式、平台支持、构建环境、依赖完整性
2. **版本验证**: 检查版本冲突、规范性、发布类型
3. **发布准备**: 生成发布说明、配置分发参数、设置回滚策略
4. **执行发布**: 上传版本包、更新信息、配置分发、激活版本
5. **发布监控**: 监控下载成功率、安装成功率、崩溃率等关键指标
6. **发布总结**: 生成完整的发布报告和统计信息
**使用示例**:
```typescript
await moduleManager.executeWorkflow('version-release-management', {
args: [],
options: {
name: 'v2.1.0', // 版本名称 (必需)
description: 'Major update', // 版本描述
platform: 'ios', // 目标平台 (必需)
rollout: 50, // 发布覆盖率
packageVersion: '2.1.0', // 包版本号
dryRun: false, // 模拟发布
force: false // 强制发布
}
});
```
**适用场景**:
- 正式版本发布
- 灰度发布管理
- 发布质量控制
---
## 🔗 工作流使用示例
### 1. 完整发布流程组合
```typescript
// 完整的应用发布流程
async function completeReleaseFlow() {
// 1. 应用初始化
await moduleManager.executeWorkflow('app-initialization', {
args: [],
options: {
name: 'ProductionApp',
platform: 'ios',
force: true
}
});
// 2. 智能打包
await moduleManager.executeWorkflow('intelligent-bundle', {
args: [],
options: {
platform: 'ios',
dev: false,
optimize: true
}
});
// 3. 版本发布
await moduleManager.executeWorkflow('version-release-management', {
args: [],
options: {
name: 'v1.0.0',
platform: 'ios',
rollout: 100
}
});
}
```
### 2. 多平台批量构建
```typescript
async function multiPlatformBuild() {
const platforms = ['ios', 'android', 'harmony'];
for (const platform of platforms) {
await moduleManager.executeWorkflow('intelligent-bundle', {
args: [],
options: {
platform,
dev: false,
optimize: true
}
});
}
}
```
### 3. 增量更新流程
```typescript
async function incrementalUpdateFlow() {
// 1. 生成增量包
const buildResult = await moduleManager.executeWorkflow('incremental-build', {
args: [],
options: {
platform: 'android',
baseVersion: 'v1.0.0'
}
});
// 2. 发布增量更新
if (buildResult.success) {
await moduleManager.executeWorkflow('version-release-management', {
args: [],
options: {
name: 'v1.0.1',
platform: 'android',
rollout: 20 // 小范围发布
}
});
}
}
```
---
## 📋 最佳实践
### 1. 工作流选择指南
| 场景 | 推荐工作流 | 配置建议 |
|------|------------|----------|
| 新应用创建 | `app-initialization` | 启用force参数避免冲突 |
| 生产发布 | `intelligent-bundle` + `version-release-management` | 关闭dev模式启用优化 |
| 热更新 | `incremental-build` | 指定合适的基准版本 |
| 批量管理 | `batch-package-processing` | 定期执行包文件清理 |
| 灰度发布 | `version-release-management` | 设置合适的rollout比例 |
### 2. 错误处理策略
```typescript
async function robustWorkflowExecution() {
try {
const result = await moduleManager.executeWorkflow('app-initialization', {
args: [],
options: { name: 'MyApp', platform: 'ios' }
});
if (!result.success) {
console.error('工作流执行失败:', result.error);
// 执行回滚或重试逻辑
}
} catch (error) {
console.error('工作流异常:', error);
// 异常处理逻辑
}
}
```
### 3. 工作流监控
```typescript
// 工作流执行监控
const workflowMonitor = {
async executeWithMonitoring(workflowName: string, context: any) {
const startTime = Date.now();
console.log(`开始执行工作流: ${workflowName}`);
try {
const result = await moduleManager.executeWorkflow(workflowName, context);
const duration = Date.now() - startTime;
console.log(`工作流执行完成: ${workflowName}, 耗时: ${duration}ms`);
return result;
} catch (error) {
const duration = Date.now() - startTime;
console.error(`工作流执行失败: ${workflowName}, 耗时: ${duration}ms`, error);
throw error;
}
}
};
```
### 4. 配置管理
```typescript
// 工作流配置管理
const workflowConfigs = {
development: {
'intelligent-bundle': { dev: true, optimize: false },
'version-release-management': { dryRun: true, rollout: 10 }
},
production: {
'intelligent-bundle': { dev: false, optimize: true },
'version-release-management': { dryRun: false, rollout: 100 }
}
};
async function executeWithConfig(workflowName: string, environment: string) {
const config = workflowConfigs[environment]?.[workflowName] || {};
return await moduleManager.executeWorkflow(workflowName, {
args: [],
options: config
});
}
```
---
## 🎯 总结
这些增强的核心工作流为React Native Update CLI提供了
1. **完整的应用生命周期管理** - 从创建到发布的全流程覆盖
2. **智能化构建和优化** - 自动环境检测和性能优化
3. **高效的增量更新** - 减少更新包大小,提升用户体验
4. **批量处理能力** - 提高大规模应用管理效率
5. **规范化发布流程** - 确保发布质量和一致性
每个工作流都经过精心设计,包含详细的步骤、错误处理、进度反馈和结果验证,为开发者提供了强大而可靠的自动化工具。

376
example/README.md Normal file
View File

@@ -0,0 +1,376 @@
# Custom Modules and Workflows Examples
[中文文档](./README.zh-CN.md) | [Chinese Documentation](./README.zh-CN.md)
This directory contains complete examples of React Native Update CLI custom modules and workflows, demonstrating how to extend the CLI functionality.
## 📁 Directory Structure
```
example/
├── modules/ # Custom module examples
│ ├── custom-deploy-module.ts # Custom deployment module
│ └── analytics-module.ts # Analytics module
├── workflows/ # Custom workflow examples
│ └── custom-workflows.ts # Complex workflow collection
├── scripts/ # Execution script examples
│ ├── register-modules.ts # Module registration and execution
│ ├── provider-api-example.ts # Provider API usage examples
│ └── workflow-demo.ts # Workflow demonstration script
└── README.md # This documentation
```
## 🚀 Quick Start
### 1. Run Module Registration and Execution Examples
```bash
# Compile TypeScript (if needed)
npm run build
# Run module examples
npx ts-node example/scripts/register-modules.ts
```
### 2. Run Provider API Examples
```bash
npx ts-node example/scripts/provider-api-example.ts
```
### 3. Run Workflow Demonstrations
```bash
# Run all workflow demonstrations
npx ts-node example/scripts/workflow-demo.ts
# Interactive execution of specific workflows
npx ts-node example/scripts/workflow-demo.ts interactive canary-deployment --version 1.0.0 --initialRollout 5
# Multi-environment deployment workflow
npx ts-node example/scripts/workflow-demo.ts interactive multi-env-deploy --version 1.0.0
# Rollback workflow
npx ts-node example/scripts/workflow-demo.ts interactive rollback-workflow --targetVersion 0.9.5
```
## 📦 Custom Module Examples
### 1. Custom Deployment Module (`custom-deploy-module.ts`)
This module demonstrates how to create a complete deployment management module, including:
#### Commands:
- `deploy-dev`: Deploy to development environment
- `deploy-prod`: Deploy to production environment
- `rollback`: Rollback to specified version
#### Workflows:
- `full-deploy`: Complete deployment process (development → testing → production)
- `hotfix-deploy`: Quick hotfix deployment process
#### Usage Example:
```typescript
import { moduleManager } from 'react-native-update-cli';
import { customDeployModule } from './modules/custom-deploy-module';
// Register module
moduleManager.registerModule(customDeployModule);
// Execute development deployment
await moduleManager.executeCommand('deploy-dev', {
args: [],
options: { platform: 'ios', force: true }
});
// Execute complete deployment workflow
await moduleManager.executeWorkflow('full-deploy', {
args: [],
options: { version: '1.2.3' }
});
```
### 2. Analytics Module (`analytics-module.ts`)
Demonstrates how to create analytics and statistics functionality:
#### Commands:
- `track-deployment`: Record deployment statistics
- `deployment-report`: Generate deployment reports
#### Workflows:
- `deploy-with-analytics`: Deployment process with analytics
## 🔄 Custom Workflow Examples
### 1. Canary Deployment Workflow (`canary-deployment`)
Implements a complete canary deployment process:
- ✅ Prepare canary deployment environment
- ✅ Initial small-scale deployment
- ✅ Monitor key metrics
- ✅ Automatically expand deployment based on metrics
- ✅ Final validation
```typescript
await moduleManager.executeWorkflow('canary-deployment', {
args: [],
options: {
version: '2.1.0',
initialRollout: 10, // 初始10%用户
autoExpand: true // 自动扩大范围
}
});
```
### 2. Multi-Environment Deployment Workflow (`multi-env-deploy`)
Implements a standard multi-environment deployment process:
- ✅ Deploy to development environment
- ✅ Run integration tests
- ✅ Deploy to staging environment
- ✅ Run end-to-end tests
- ✅ Deploy to production environment
- ✅ Post-deployment validation
```typescript
await moduleManager.executeWorkflow('multi-env-deploy', {
args: [],
options: {
version: '2.1.0',
skipProduction: false, // 不跳过生产部署
forceProduction: false // 测试失败时不强制部署
}
});
```
### 3. Rollback Workflow (`rollback-workflow`)
Safe application rollback process:
- ✅ Validate target version
- ✅ Backup current state
- ✅ Execute rollback operation
- ✅ Verify rollback results
- ✅ Notify relevant personnel
```typescript
await moduleManager.executeWorkflow('rollback-workflow', {
args: [],
options: {
targetVersion: '2.0.5',
skipVerification: false
}
});
```
## 🛠️ Provider API Usage Examples
Provider API provides programmatic interfaces suitable for integration in applications:
### Basic Usage
```typescript
import { moduleManager } from 'react-native-update-cli';
const provider = moduleManager.getProvider();
// Bundle application
const bundleResult = await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true
});
// Publish version
const publishResult = await provider.publish({
name: 'v1.0.0',
description: 'Bug fixes',
rollout: 100
});
// Upload file
const uploadResult = await provider.upload({
filePath: 'app.ipa',
platform: 'ios'
});
```
### Application Management
```typescript
// Create application
await provider.createApp('MyApp', 'ios');
// Get current application
const { appId, platform } = await provider.getSelectedApp('ios');
// List versions
const versions = await provider.listVersions(appId);
// Update version
await provider.updateVersion(appId, versionId, {
name: 'v1.1.0',
description: 'New features'
});
```
### Automation Service Class
```typescript
class DeploymentService {
private provider = moduleManager.getProvider();
async buildAndPublish(platform: Platform, version: string) {
// 1. Bundle
const bundleResult = await this.provider.bundle({
platform, dev: false, sourcemap: true
});
// 2. Publish
const publishResult = await this.provider.publish({
name: version, rollout: 100
});
return { bundleResult, publishResult };
}
}
```
## 🎯 Advanced Features
### 1. Workflow Validation
```typescript
const workflow: CustomWorkflow = {
name: 'my-workflow',
steps: [...],
validate: (context) => {
if (!context.options.version) {
console.error('Version number must be specified');
return false;
}
return true;
}
};
```
### 2. Conditional Execution
```typescript
const step: WorkflowStep = {
name: 'conditional-step',
execute: async (context) => { /* ... */ },
condition: (context) => {
return context.options.environment === 'production';
}
};
```
### 3. Error Handling
```typescript
try {
const result = await moduleManager.executeCommand('deploy-prod', {
args: [],
options: {} // Missing required parameters
});
} catch (error) {
console.error('Execution failed:', error.message);
}
```
### 4. Custom Workflow Registration
```typescript
const provider = moduleManager.getProvider();
provider.registerWorkflow({
name: 'custom-workflow',
description: 'Custom workflow',
steps: [
{
name: 'step1',
execute: async (context, previousResult) => {
// Execution logic
return { step1: 'completed' };
}
}
]
});
// Execute workflow
await provider.executeWorkflow('custom-workflow', {
args: [],
options: {}
});
```
## 📝 Best Practices
### 1. Module Design
- **Single Responsibility**: Each module focuses on specific functional domains
- **Clear Naming**: Use descriptive command and option names
- **Complete Documentation**: Provide descriptions for all commands and options
- **Error Handling**: Provide clear error messages and recovery suggestions
### 2. Workflow Design
- **Atomic Operations**: Each step should be atomic and independently executable
- **State Passing**: Properly use previousResult to pass state
- **Error Recovery**: Consider cleanup and recovery mechanisms for failures
- **Progress Feedback**: Provide clear progress information to users
### 3. Development Recommendations
- **Type Safety**: Make full use of the TypeScript type system
- **Test Coverage**: Write tests for custom modules
- **Documentation Maintenance**: Keep examples and documentation synchronized
- **Version Management**: Set appropriate version numbers for modules
## 🐛 故障排除
### 常见问题
1. **模块注册失败**
```typescript
// 确保模块符合 CLIModule 接口
const module: CLIModule = {
name: 'my-module',
version: '1.0.0',
commands: [...],
workflows: [...]
};
```
2. **Command Execution Failed**
```typescript
// Check command name and parameters
await moduleManager.executeCommand('correct-command-name', {
args: [],
options: { requiredParam: 'value' }
});
```
3. **Workflow Validation Failed**
```typescript
// Ensure all required options are provided
await moduleManager.executeWorkflow('workflow-name', {
args: [],
options: { version: '1.0.0' } // Required parameter
});
```
## 📖 Related Documentation
- [Main Project README](../README.md)
- [Modular Architecture Documentation](../docs/architecture.md)
- [API Reference Documentation](../docs/api-reference.md)
- [Contributing Guide](../CONTRIBUTING.md)
## 🤝 Contributing
Welcome to submit more examples and improvement suggestions! Please check the main project's contributing guide.

374
example/README.zh-CN.md Normal file
View File

@@ -0,0 +1,374 @@
# 自定义模块和工作流示例
这个目录包含了 React Native Update CLI 自定义模块和工作流的完整示例,演示如何扩展 CLI 的功能。
## 📁 目录结构
```
example/
├── modules/ # 自定义模块示例
│ ├── custom-deploy-module.ts # 自定义部署模块
│ └── analytics-module.ts # 分析统计模块
├── workflows/ # 自定义工作流示例
│ └── custom-workflows.ts # 复杂工作流集合
├── scripts/ # 执行脚本示例
│ ├── register-modules.ts # 模块注册和执行
│ ├── provider-api-example.ts # Provider API 使用示例
│ └── workflow-demo.ts # 工作流演示脚本
└── README.md # 本文档
```
## 🚀 快速开始
### 1. 运行模块注册和执行示例
```bash
# 编译TypeScript (如果需要)
npm run build
# 运行模块示例
npx ts-node example/scripts/register-modules.ts
```
### 2. 运行Provider API示例
```bash
npx ts-node example/scripts/provider-api-example.ts
```
### 3. 运行工作流演示
```bash
# 运行所有工作流演示
npx ts-node example/scripts/workflow-demo.ts
# 交互式执行特定工作流
npx ts-node example/scripts/workflow-demo.ts interactive canary-deployment --version 1.0.0 --initialRollout 5
# 多环境部署工作流
npx ts-node example/scripts/workflow-demo.ts interactive multi-env-deploy --version 1.0.0
# 回滚工作流
npx ts-node example/scripts/workflow-demo.ts interactive rollback-workflow --targetVersion 0.9.5
```
## 📦 自定义模块示例
### 1. 自定义部署模块 (`custom-deploy-module.ts`)
这个模块演示了如何创建一个完整的部署管理模块,包含:
#### 命令:
- `deploy-dev`: 部署到开发环境
- `deploy-prod`: 部署到生产环境
- `rollback`: 回滚到指定版本
#### 工作流:
- `full-deploy`: 完整部署流程(开发 → 测试 → 生产)
- `hotfix-deploy`: 热修复快速部署流程
#### 使用示例:
```typescript
import { moduleManager } from 'react-native-update-cli';
import { customDeployModule } from './modules/custom-deploy-module';
// 注册模块
moduleManager.registerModule(customDeployModule);
// 执行开发部署
await moduleManager.executeCommand('deploy-dev', {
args: [],
options: { platform: 'ios', force: true }
});
// 执行完整部署工作流
await moduleManager.executeWorkflow('full-deploy', {
args: [],
options: { version: '1.2.3' }
});
```
### 2. 分析统计模块 (`analytics-module.ts`)
演示如何创建分析和统计功能:
#### 命令:
- `track-deployment`: 记录部署统计信息
- `deployment-report`: 生成部署报告
#### 工作流:
- `deploy-with-analytics`: 带统计的部署流程
## 🔄 自定义工作流示例
### 1. 灰度发布工作流 (`canary-deployment`)
实现完整的灰度发布流程:
- ✅ 准备灰度发布环境
- ✅ 初始小范围部署
- ✅ 监控关键指标
- ✅ 基于指标自动扩大发布范围
- ✅ 最终验证
```typescript
await moduleManager.executeWorkflow('canary-deployment', {
args: [],
options: {
version: '2.1.0',
initialRollout: 10, // 初始10%用户
autoExpand: true // 自动扩大范围
}
});
```
### 2. 多环境发布工作流 (`multi-env-deploy`)
实现标准的多环境发布流程:
- ✅ 部署到开发环境
- ✅ 运行集成测试
- ✅ 部署到预发布环境
- ✅ 运行端到端测试
- ✅ 部署到生产环境
- ✅ 部署后验证
```typescript
await moduleManager.executeWorkflow('multi-env-deploy', {
args: [],
options: {
version: '2.1.0',
skipProduction: false, // 不跳过生产部署
forceProduction: false // 测试失败时不强制部署
}
});
```
### 3. 回滚工作流 (`rollback-workflow`)
安全的应用回滚流程:
- ✅ 验证目标版本
- ✅ 备份当前状态
- ✅ 执行回滚操作
- ✅ 验证回滚结果
- ✅ 通知相关人员
```typescript
await moduleManager.executeWorkflow('rollback-workflow', {
args: [],
options: {
targetVersion: '2.0.5',
skipVerification: false
}
});
```
## 🛠️ Provider API 使用示例
Provider API 提供了编程式接口,适合在应用程序中集成:
### 基本使用
```typescript
import { moduleManager } from 'react-native-update-cli';
const provider = moduleManager.getProvider();
// 打包应用
const bundleResult = await provider.bundle({
platform: 'ios',
dev: false,
sourcemap: true
});
// 发布版本
const publishResult = await provider.publish({
name: 'v1.0.0',
description: 'Bug fixes',
rollout: 100
});
// 上传文件
const uploadResult = await provider.upload({
filePath: 'app.ipa',
platform: 'ios'
});
```
### 应用管理
```typescript
// 创建应用
await provider.createApp('MyApp', 'ios');
// 获取当前应用
const { appId, platform } = await provider.getSelectedApp('ios');
// 列出版本
const versions = await provider.listVersions(appId);
// 更新版本
await provider.updateVersion(appId, versionId, {
name: 'v1.1.0',
description: 'New features'
});
```
### 自动化服务类
```typescript
class DeploymentService {
private provider = moduleManager.getProvider();
async buildAndPublish(platform: Platform, version: string) {
// 1. 打包
const bundleResult = await this.provider.bundle({
platform, dev: false, sourcemap: true
});
// 2. 发布
const publishResult = await this.provider.publish({
name: version, rollout: 100
});
return { bundleResult, publishResult };
}
}
```
## 🎯 高级特性
### 1. 工作流验证
```typescript
const workflow: CustomWorkflow = {
name: 'my-workflow',
steps: [...],
validate: (context) => {
if (!context.options.version) {
console.error('必须指定版本号');
return false;
}
return true;
}
};
```
### 2. 条件执行
```typescript
const step: WorkflowStep = {
name: 'conditional-step',
execute: async (context) => { /* ... */ },
condition: (context) => {
return context.options.environment === 'production';
}
};
```
### 3. 错误处理
```typescript
try {
const result = await moduleManager.executeCommand('deploy-prod', {
args: [],
options: {} // 缺少必需参数
});
} catch (error) {
console.error('执行失败:', error.message);
}
```
### 4. 自定义工作流注册
```typescript
const provider = moduleManager.getProvider();
provider.registerWorkflow({
name: 'custom-workflow',
description: '自定义工作流',
steps: [
{
name: 'step1',
execute: async (context, previousResult) => {
// 执行逻辑
return { step1: 'completed' };
}
}
]
});
// 执行工作流
await provider.executeWorkflow('custom-workflow', {
args: [],
options: {}
});
```
## 📝 最佳实践
### 1. 模块设计
- **单一职责**: 每个模块专注于特定功能领域
- **清晰命名**: 使用描述性的命令和选项名称
- **完整文档**: 为所有命令和选项提供描述
- **错误处理**: 提供清晰的错误信息和恢复建议
### 2. 工作流设计
- **原子操作**: 每个步骤应该是原子的,可独立执行
- **状态传递**: 合理使用 previousResult 传递状态
- **错误恢复**: 考虑失败时的清理和恢复机制
- **进度反馈**: 提供清晰的进度信息给用户
### 3. 开发建议
- **类型安全**: 充分利用 TypeScript 类型系统
- **测试覆盖**: 为自定义模块编写测试
- **文档维护**: 保持示例和文档的同步更新
- **版本管理**: 为模块设置合适的版本号
## 🐛 故障排除
### 常见问题
1. **模块注册失败**
```typescript
// 确保模块符合 CLIModule 接口
const module: CLIModule = {
name: 'my-module',
version: '1.0.0',
commands: [...],
workflows: [...]
};
```
2. **命令执行失败**
```typescript
// 检查命令名称和参数
await moduleManager.executeCommand('correct-command-name', {
args: [],
options: { requiredParam: 'value' }
});
```
3. **工作流验证失败**
```typescript
// 确保提供所有必需的选项
await moduleManager.executeWorkflow('workflow-name', {
args: [],
options: { version: '1.0.0' } // 必需参数
});
```
## 📖 相关文档
- [主项目 README](../README.md)
- [模块化架构文档](../docs/architecture.md)
- [API 参考文档](../docs/api-reference.md)
- [贡献指南](../CONTRIBUTING.md)
## 🤝 贡献
欢迎提交更多示例和改进建议!请查看主项目的贡献指南。

View File

@@ -0,0 +1,53 @@
# Custom Version Parameter Usage
This document demonstrates how to use the `--version` parameter with upload commands to override the version extracted from APK/IPA/APP files.
## Commands Supporting Custom Version
- `uploadApk --version <version>`
- `uploadIpa --version <version>`
- `uploadApp --version <version>`
## Usage Examples
### Upload APK with Custom Version
```bash
# Upload APK and override version to "1.2.3-custom"
cresc uploadApk app-release.apk --version "1.2.3-custom"
```
### Upload IPA with Custom Version
```bash
# Upload IPA and override version to "2.0.0-beta"
cresc uploadIpa MyApp.ipa --version "2.0.0-beta"
```
### Upload APP with Custom Version
```bash
# Upload APP and override version to "3.1.0-harmony"
cresc uploadApp MyApp.app --version "3.1.0-harmony"
```
## Behavior
1. **Without `--version`**: The command uses the version extracted from the package file (APK/IPA/APP)
2. **With `--version`**: The command uses the provided custom version instead of the extracted version
3. **Console Output**: When using a custom version, the CLI will display "Using custom version: <version>" message
## Use Cases
- **Testing**: Upload test builds with specific version identifiers
- **Hotfixes**: Override version numbers for emergency releases
- **Version Alignment**: Ensure consistent versioning across different platforms
- **Manual Override**: When the extracted version is incorrect or inappropriate
## Example Output
```
$ cresc uploadApk app-release.apk --version "1.2.3-hotfix"
Using custom version: 1.2.3-hotfix
Successfully uploaded APK native package (id: 12345, version: 1.2.3-hotfix, buildTime: 2024-01-15T10:30:00Z)
```

View File

@@ -0,0 +1,149 @@
import type {
CLIModule,
CLIProvider,
CommandContext,
CommandResult,
} from '../../src/types';
/**
* 分析统计模块示例
* 演示一个简单的分析统计功能模块
*/
export const analyticsModule: CLIModule = {
name: 'analytics',
version: '1.0.0',
commands: [
{
name: 'track-deployment',
description: '记录部署统计信息',
handler: async (context: CommandContext): Promise<CommandResult> => {
console.log('📊 记录部署统计信息...');
const { platform, environment, version } = context.options;
const deploymentData = {
timestamp: new Date().toISOString(),
platform: platform || 'unknown',
environment: environment || 'unknown',
version: version || 'unknown',
success: true,
duration: Math.floor(Math.random() * 1000) + 500, // 模拟部署时长
};
console.log('✅ 部署统计已记录:');
console.log(JSON.stringify(deploymentData, null, 2));
return {
success: true,
data: deploymentData,
};
},
options: {
platform: {
hasValue: true,
description: '平台',
},
environment: {
hasValue: true,
description: '环境',
},
version: {
hasValue: true,
description: '版本',
},
},
},
{
name: 'deployment-report',
description: '生成部署报告',
handler: async (context: CommandContext): Promise<CommandResult> => {
console.log('📈 生成部署报告...');
const { days = 7 } = context.options;
// 模拟生成报告数据
const report = {
period: `最近 ${days}`,
totalDeployments: Math.floor(Math.random() * 50) + 10,
successRate: 95.5,
averageDuration: '2.3分钟',
platformBreakdown: {
ios: 45,
android: 38,
harmony: 12,
},
environmentBreakdown: {
development: 60,
staging: 25,
production: 15,
},
};
console.log('📊 部署报告:');
console.log(JSON.stringify(report, null, 2));
return {
success: true,
data: report,
};
},
options: {
days: {
hasValue: true,
default: 7,
description: '报告天数',
},
},
},
],
workflows: [
{
name: 'deploy-with-analytics',
description: '带统计的部署流程',
steps: [
{
name: 'pre-deployment',
description: '部署前准备',
execute: async (context: CommandContext) => {
console.log('📋 部署前准备...');
return { startTime: Date.now() };
},
},
{
name: 'deployment',
description: '执行部署',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🚀 执行部署...');
await new Promise((resolve) => setTimeout(resolve, 2000));
return { ...previousResult, deploymentCompleted: true };
},
},
{
name: 'record-analytics',
description: '记录统计信息',
execute: async (context: CommandContext, previousResult: any) => {
console.log('📊 记录统计信息...');
const duration = Date.now() - previousResult.startTime;
const analytics = {
duration,
timestamp: new Date().toISOString(),
success: true,
};
console.log(`✅ 部署完成,耗时 ${duration}ms`);
return { ...previousResult, analytics };
},
},
],
},
],
init: (provider: CLIProvider) => {
console.log('📊 分析统计模块已初始化');
},
cleanup: () => {
console.log('🧹 分析统计模块清理完成');
},
};

View File

@@ -0,0 +1,315 @@
import type {
CLIModule,
CLIProvider,
CommandContext,
CommandDefinition,
CommandResult,
CustomWorkflow,
} from '../../src/types';
/**
* 自定义部署模块示例
* 演示如何创建一个包含多个命令和工作流的自定义模块
*/
export const customDeployModule: CLIModule = {
name: 'custom-deploy',
version: '1.0.0',
commands: [
{
name: 'deploy-dev',
description: '部署到开发环境',
handler: async (context: CommandContext): Promise<CommandResult> => {
console.log('🚀 开始部署到开发环境...');
const { platform = 'ios', force = false } = context.options;
try {
// 模拟部署逻辑
console.log(`📱 平台: ${platform}`);
console.log(`🔧 强制部署: ${force ? '是' : '否'}`);
// 模拟一些部署步骤
console.log('1. 检查环境配置...');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('2. 构建应用包...');
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log('3. 上传到开发服务器...');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('✅ 部署到开发环境完成!');
return {
success: true,
data: {
environment: 'development',
platform,
deployTime: new Date().toISOString(),
buildId: `dev-${Date.now()}`,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '部署失败',
};
}
},
options: {
platform: {
hasValue: true,
default: 'ios',
description: '目标平台 (ios/android/harmony)',
},
force: {
hasValue: false,
default: false,
description: '强制部署,跳过确认',
},
},
},
{
name: 'deploy-prod',
description: '部署到生产环境',
handler: async (context: CommandContext): Promise<CommandResult> => {
console.log('🔥 开始部署到生产环境...');
const { version, rollout = 100 } = context.options;
if (!version) {
return {
success: false,
error: '生产部署必须指定版本号',
};
}
try {
console.log(`📦 版本: ${version}`);
console.log(`📊 发布比例: ${rollout}%`);
console.log('1. 安全检查...');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('2. 生产构建...');
await new Promise((resolve) => setTimeout(resolve, 2000));
console.log('3. 部署到生产环境...');
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log('4. 健康检查...');
await new Promise((resolve) => setTimeout(resolve, 800));
console.log('🎉 生产部署完成!');
return {
success: true,
data: {
environment: 'production',
version,
rollout,
deployTime: new Date().toISOString(),
buildId: `prod-${Date.now()}`,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '生产部署失败',
};
}
},
options: {
version: {
hasValue: true,
description: '版本号 (必需)',
},
rollout: {
hasValue: true,
default: 100,
description: '发布比例 (0-100)',
},
},
},
{
name: 'rollback',
description: '回滚到指定版本',
handler: async (context: CommandContext): Promise<CommandResult> => {
console.log('🔄 开始回滚操作...');
const { version, immediate = false } = context.options;
if (!version) {
return {
success: false,
error: '回滚操作必须指定目标版本',
};
}
try {
console.log(`🎯 目标版本: ${version}`);
console.log(`⚡ 立即回滚: ${immediate ? '是' : '否'}`);
console.log('1. 验证目标版本...');
await new Promise((resolve) => setTimeout(resolve, 800));
console.log('2. 准备回滚...');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log('3. 执行回滚...');
await new Promise((resolve) => setTimeout(resolve, 1200));
console.log('✅ 回滚完成!');
return {
success: true,
data: {
targetVersion: version,
rollbackTime: new Date().toISOString(),
immediate,
},
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '回滚失败',
};
}
},
options: {
version: {
hasValue: true,
description: '目标版本号 (必需)',
},
immediate: {
hasValue: false,
default: false,
description: '立即回滚,不等待确认',
},
},
},
],
workflows: [
{
name: 'full-deploy',
description: '完整部署流程:开发 -> 测试 -> 生产',
steps: [
{
name: 'deploy-to-dev',
description: '部署到开发环境',
execute: async (context: CommandContext) => {
console.log('🔧 步骤 1: 部署到开发环境');
// 这里可以调用其他命令或执行自定义逻辑
return { environment: 'dev', status: 'completed' };
},
},
{
name: 'run-tests',
description: '运行自动化测试',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🧪 步骤 2: 运行自动化测试');
console.log(' - 单元测试...');
await new Promise((resolve) => setTimeout(resolve, 1000));
console.log(' - 集成测试...');
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log(' - E2E测试...');
await new Promise((resolve) => setTimeout(resolve, 2000));
return { ...previousResult, tests: 'passed' };
},
},
{
name: 'deploy-to-prod',
description: '部署到生产环境',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🚀 步骤 3: 部署到生产环境');
if (previousResult.tests !== 'passed') {
throw new Error('测试未通过,无法部署到生产环境');
}
return {
...previousResult,
environment: 'production',
status: 'deployed',
};
},
condition: (context: CommandContext) => {
// 只有在非跳过生产部署的情况下才执行
return !context.options.skipProd;
},
},
],
validate: (context: CommandContext) => {
if (!context.options.version) {
console.error('❌ 完整部署流程需要指定版本号');
return false;
}
return true;
},
options: {
version: {
hasValue: true,
description: '版本号 (必需)',
},
skipProd: {
hasValue: false,
default: false,
description: '跳过生产部署',
},
},
},
{
name: 'hotfix-deploy',
description: '热修复快速部署流程',
steps: [
{
name: 'validate-hotfix',
description: '验证热修复',
execute: async (context: CommandContext) => {
console.log('🔍 验证热修复内容...');
const { hotfixId } = context.options;
if (!hotfixId) {
throw new Error('缺少热修复ID');
}
return { hotfixId, validated: true };
},
},
{
name: 'emergency-deploy',
description: '紧急部署',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🚨 执行紧急部署...');
console.log('⚡ 快速构建...');
await new Promise((resolve) => setTimeout(resolve, 800));
console.log('🚀 立即发布...');
await new Promise((resolve) => setTimeout(resolve, 600));
return {
...previousResult,
deployed: true,
deployTime: new Date().toISOString(),
};
},
},
],
options: {
hotfixId: {
hasValue: true,
description: '热修复ID (必需)',
},
},
},
],
init: (provider: CLIProvider) => {
console.log('🎯 自定义部署模块已初始化');
console.log(' 可用命令: deploy-dev, deploy-prod, rollback');
console.log(' 可用工作流: full-deploy, hotfix-deploy');
},
cleanup: () => {
console.log('🧹 自定义部署模块清理完成');
},
};

41
example/package.json Normal file
View File

@@ -0,0 +1,41 @@
{
"name": "react-native-update-cli-examples",
"version": "1.0.0",
"description": "React Native Update CLI 自定义模块和工作流示例",
"private": true,
"scripts": {
"build": "tsc --noEmit --skipLibCheck",
"register-modules": "ts-node scripts/register-modules.ts",
"provider-demo": "ts-node scripts/provider-api-example.ts",
"workflow-demo": "ts-node scripts/workflow-demo.ts",
"workflow-interactive": "ts-node scripts/workflow-demo.ts interactive",
"enhanced-workflow-demo": "ts-node scripts/enhanced-workflow-demo.ts",
"enhanced-workflow-interactive": "ts-node scripts/enhanced-workflow-demo.ts interactive",
"demo:all": "npm run register-modules && npm run provider-demo && npm run workflow-demo && npm run enhanced-workflow-demo",
"demo:canary": "npm run workflow-interactive canary-deployment -- --version 1.0.0 --initialRollout 5",
"demo:multi-env": "npm run workflow-interactive multi-env-deploy -- --version 1.0.0",
"demo:rollback": "npm run workflow-interactive rollback-workflow -- --targetVersion 0.9.5",
"demo:app-init": "npm run enhanced-workflow-interactive app-initialization -- --name MyApp --platform ios",
"demo:smart-bundle": "npm run enhanced-workflow-interactive intelligent-bundle -- --platform android --optimize true",
"demo:version-release": "npm run enhanced-workflow-interactive version-release-management -- --name v1.0.0 --platform ios --dryRun true"
},
"dependencies": {
"react-native-update-cli": "file:../"
},
"devDependencies": {
"@types/node": "^20.0.0",
"ts-node": "^10.9.0",
"typescript": "^5.0.0"
},
"keywords": [
"react-native",
"update",
"cli",
"module",
"workflow",
"example",
"custom"
],
"author": "reactnativecn",
"license": "BSD-3-Clause"
}

View File

@@ -0,0 +1,552 @@
#!/usr/bin/env ts-node
import { moduleManager } from '../../src/module-manager';
import { enhancedCoreWorkflows } from '../workflows/enhanced-core-workflows';
/**
* 增强核心工作流演示脚本
* 展示如何使用为核心模块设计的高级工作流
*/
async function registerEnhancedWorkflows() {
console.log('📦 注册增强核心工作流...\n');
const provider = moduleManager.getProvider();
// 注册所有增强核心工作流
enhancedCoreWorkflows.forEach((workflow) => {
provider.registerWorkflow(workflow);
console.log(`✅ 注册工作流: ${workflow.name}`);
console.log(` 描述: ${workflow.description}`);
console.log(` 步骤数: ${workflow.steps.length}`);
console.log();
});
console.log('📋 所有增强核心工作流注册完成\n');
}
/**
* 演示App模块工作流
*/
async function demonstrateAppWorkflows() {
console.log('📱 演示App模块增强工作流\n');
console.log('='.repeat(70));
// 1. 应用初始化工作流
console.log('🚀 应用初始化工作流演示');
console.log('-'.repeat(40));
try {
const initResult = await moduleManager.executeWorkflow(
'app-initialization',
{
args: [],
options: {
name: 'MyAwesomeApp',
platform: 'ios',
downloadUrl: 'https://example.com/download',
force: false,
},
},
);
console.log('\\n📊 应用初始化结果:');
console.log(
`创建状态: ${initResult.data?.created ? '✅ 成功' : '❌ 失败'}`,
);
console.log(
`配置状态: ${initResult.data?.configured ? '✅ 成功' : '❌ 失败'}`,
);
console.log(
`验证状态: ${initResult.data?.verified ? '✅ 成功' : '❌ 失败'}`,
);
} catch (error) {
console.error(
'❌ 应用初始化工作流失败:',
error instanceof Error ? error.message : error,
);
}
console.log('\\n' + '-'.repeat(40));
// 2. 多平台应用管理工作流
console.log('\\n🌍 多平台应用管理工作流演示');
console.log('-'.repeat(40));
try {
const managementResult = await moduleManager.executeWorkflow(
'multi-platform-app-management',
{
args: [],
options: {
includeInactive: true,
autoOptimize: true,
},
},
);
console.log('\\n📊 多平台管理结果:');
if (managementResult.data?.analysis) {
const analysis = managementResult.data.analysis;
console.log(`总应用数: ${analysis.totalApps}`);
console.log(`活跃应用: ${analysis.activeApps}`);
console.log(`平台分布: ${JSON.stringify(analysis.platformDistribution)}`);
}
} catch (error) {
console.error(
'❌ 多平台应用管理工作流失败:',
error instanceof Error ? error.message : error,
);
}
console.log('\\n' + '='.repeat(70) + '\\n');
}
/**
* 演示Bundle模块工作流
*/
async function demonstrateBundleWorkflows() {
console.log('📦 演示Bundle模块增强工作流\n');
console.log('='.repeat(70));
// 1. 智能打包工作流
console.log('🧠 智能打包工作流演示');
console.log('-'.repeat(40));
try {
const bundleResult = await moduleManager.executeWorkflow(
'intelligent-bundle',
{
args: [],
options: {
platform: 'ios',
dev: false,
sourcemap: true,
optimize: true,
},
},
);
console.log('\\n📊 智能打包结果:');
if (bundleResult.data?.buildResults) {
const builds = bundleResult.data.buildResults;
builds.forEach((build: any) => {
console.log(
`${build.platform}: ${build.success ? '✅ 成功' : '❌ 失败'} (${build.buildTime}s, ${build.bundleSize}MB)`,
);
});
}
if (bundleResult.data?.averageScore) {
console.log(`平均质量评分: ${bundleResult.data.averageScore}%`);
}
} catch (error) {
console.error(
'❌ 智能打包工作流失败:',
error instanceof Error ? error.message : error,
);
}
console.log('\\n' + '-'.repeat(40));
// 2. 增量构建工作流
console.log('\\n🔄 增量构建工作流演示');
console.log('-'.repeat(40));
try {
const incrementalResult = await moduleManager.executeWorkflow(
'incremental-build',
{
args: [],
options: {
platform: 'android',
baseVersion: 'v1.0.0',
skipValidation: false,
},
},
);
console.log('\\n📊 增量构建结果:');
if (incrementalResult.data?.diffPackage) {
const diff = incrementalResult.data.diffPackage;
console.log(`基准版本: ${diff.fromVersion}`);
console.log(`目标版本: ${diff.toVersion}`);
console.log(`原始大小: ${diff.originalSize}MB`);
console.log(`差异包大小: ${diff.diffSize}MB`);
console.log(`压缩比: ${diff.compressionRatio}%`);
}
console.log(
`验证状态: ${incrementalResult.data?.allValid ? '✅ 通过' : '❌ 失败'}`,
);
} catch (error) {
console.error(
'❌ 增量构建工作流失败:',
error instanceof Error ? error.message : error,
);
}
console.log('\\n' + '='.repeat(70) + '\\n');
}
/**
* 演示Package模块工作流
*/
async function demonstratePackageWorkflows() {
console.log('📄 演示Package模块增强工作流\n');
console.log('='.repeat(70));
// 批量包处理工作流
console.log('📦 批量包处理工作流演示');
console.log('-'.repeat(40));
try {
const packageResult = await moduleManager.executeWorkflow(
'batch-package-processing',
{
args: [],
options: {
directory: './packages',
pattern: '*.{ipa,apk,app}',
skipUpload: false,
},
},
);
console.log('\\n📊 批量包处理结果:');
if (packageResult.data?.report) {
const report = packageResult.data.report;
console.log(`总包数: ${report.summary.totalPackages}`);
console.log(`解析成功: ${report.summary.parsedSuccessfully}`);
console.log(`上传成功: ${report.summary.uploadedSuccessfully}`);
console.log(`总大小: ${report.summary.totalSize.toFixed(1)}MB`);
if (report.failedOperations.length > 0) {
console.log('\\n❌ 失败操作:');
report.failedOperations.forEach((op: any) => {
console.log(` ${op.operation}: ${op.file}`);
});
}
}
} catch (error) {
console.error(
'❌ 批量包处理工作流失败:',
error instanceof Error ? error.message : error,
);
}
console.log('\\n' + '='.repeat(70) + '\\n');
}
/**
* 演示Version模块工作流
*/
async function demonstrateVersionWorkflows() {
console.log('🏷️ 演示Version模块增强工作流\n');
console.log('='.repeat(70));
// 版本发布管理工作流
console.log('🚀 版本发布管理工作流演示');
console.log('-'.repeat(40));
try {
const versionResult = await moduleManager.executeWorkflow(
'version-release-management',
{
args: [],
options: {
name: 'v2.1.0',
description: 'Major feature update with bug fixes',
platform: 'ios',
rollout: 50,
dryRun: true, // 使用模拟发布
force: false,
},
},
);
console.log('\\n📊 版本发布结果:');
if (versionResult.data?.summary) {
const summary = versionResult.data.summary;
console.log(`版本: ${summary.version}`);
console.log(`平台: ${summary.platform}`);
console.log(`发布状态: ${summary.success ? '✅ 成功' : '❌ 失败'}`);
console.log(
`监控状态: ${summary.monitoringHealthy ? '✅ 正常' : '⚠️ 有警告'}`,
);
if (summary.releaseId) {
console.log(`发布ID: ${summary.releaseId}`);
}
}
if (versionResult.data?.dryRun) {
console.log('\\n🔍 这是一次模拟发布,未实际执行');
}
} catch (error) {
console.error(
'❌ 版本发布管理工作流失败:',
error instanceof Error ? error.message : error,
);
}
console.log('\\n' + '='.repeat(70) + '\\n');
}
/**
* 演示工作流组合使用
*/
async function demonstrateWorkflowComposition() {
console.log('🔗 演示工作流组合使用\n');
console.log('='.repeat(70));
console.log('📋 完整发布流程演示 (应用初始化 → 智能打包 → 版本发布)');
console.log('-'.repeat(60));
try {
// 1. 应用初始化
console.log('\\n步骤 1: 应用初始化');
const appResult = await moduleManager.executeWorkflow(
'app-initialization',
{
args: [],
options: {
name: 'CompositeApp',
platform: 'android',
force: true,
},
},
);
if (!appResult.success) {
throw new Error('应用初始化失败');
}
// 2. 智能打包
console.log('\\n步骤 2: 智能打包');
const bundleResult = await moduleManager.executeWorkflow(
'intelligent-bundle',
{
args: [],
options: {
platform: 'android',
dev: false,
optimize: true,
},
},
);
if (!bundleResult.success) {
throw new Error('智能打包失败');
}
// 3. 版本发布
console.log('\\n步骤 3: 版本发布');
const releaseResult = await moduleManager.executeWorkflow(
'version-release-management',
{
args: [],
options: {
name: 'v1.0.0',
description: 'Initial release via composition workflow',
platform: 'android',
rollout: 10,
dryRun: true,
},
},
);
if (!releaseResult.success) {
throw new Error('版本发布失败');
}
console.log('\\n🎉 完整发布流程执行成功!');
console.log('📊 流程总结:');
console.log(
` ✅ 应用初始化: ${appResult.data?.created ? '成功' : '失败'}`,
);
console.log(
` ✅ 智能打包: ${bundleResult.data?.allSuccess ? '成功' : '失败'}`,
);
console.log(
` ✅ 版本发布: ${releaseResult.data?.summary?.success ? '成功' : '失败'}`,
);
} catch (error) {
console.error(
'❌ 工作流组合执行失败:',
error instanceof Error ? error.message : error,
);
}
console.log('\\n' + '='.repeat(70) + '\\n');
}
/**
* 列出所有增强工作流及其用途
*/
async function listEnhancedWorkflows() {
console.log('📋 增强核心工作流列表\n');
console.log('='.repeat(70));
const workflowCategories = {
App模块工作流: [
{
name: 'app-initialization',
description: '完整应用初始化流程 - 创建、配置、验证',
useCase: '新应用创建和设置',
},
{
name: 'multi-platform-app-management',
description: '多平台应用统一管理工作流',
useCase: '跨平台应用管理和优化',
},
],
Bundle模块工作流: [
{
name: 'intelligent-bundle',
description: '智能打包工作流 - 自动优化和多平台构建',
useCase: '高效的自动化构建',
},
{
name: 'incremental-build',
description: '增量构建工作流 - 生成差异包',
useCase: '减少更新包大小',
},
],
Package模块工作流: [
{
name: 'batch-package-processing',
description: '批量包处理工作流 - 上传、解析、验证',
useCase: '批量处理应用包文件',
},
],
Version模块工作流: [
{
name: 'version-release-management',
description: '版本发布管理工作流 - 完整的版本发布生命周期',
useCase: '规范化版本发布流程',
},
],
};
Object.entries(workflowCategories).forEach(([category, workflows]) => {
console.log(`\\n📂 ${category}:`);
console.log('-'.repeat(50));
workflows.forEach((workflow, index) => {
console.log(`${index + 1}. ${workflow.name}`);
console.log(` 描述: ${workflow.description}`);
console.log(` 用途: ${workflow.useCase}`);
console.log();
});
});
console.log('='.repeat(70) + '\\n');
}
/**
* 主函数
*/
async function main() {
console.log('🎯 增强核心工作流演示脚本\\n');
try {
// 1. 注册增强工作流
await registerEnhancedWorkflows();
// 2. 列出所有工作流
await listEnhancedWorkflows();
// 3. 演示各模块工作流
await demonstrateAppWorkflows();
await demonstrateBundleWorkflows();
await demonstratePackageWorkflows();
await demonstrateVersionWorkflows();
// 4. 演示工作流组合
await demonstrateWorkflowComposition();
console.log('🎉 所有增强核心工作流演示完成!');
} catch (error) {
console.error('❌ 演示过程中发生错误:', error);
process.exit(1);
}
}
/**
* 交互式工作流执行
*/
async function interactiveEnhancedWorkflowExecution() {
console.log('\\n🎮 交互式增强工作流执行\\n');
const workflowName = process.argv[3];
if (!workflowName) {
console.log('使用方法:');
console.log(' npm run enhanced-workflow-demo [工作流名称]');
console.log('\\n可用的增强工作流:');
console.log(' App模块:');
console.log(' - app-initialization');
console.log(' - multi-platform-app-management');
console.log(' Bundle模块:');
console.log(' - intelligent-bundle');
console.log(' - incremental-build');
console.log(' Package模块:');
console.log(' - batch-package-processing');
console.log(' Version模块:');
console.log(' - version-release-management');
return;
}
// 解析命令行参数
const options: Record<string, any> = {};
for (let i = 4; i < process.argv.length; i += 2) {
const key = process.argv[i]?.replace(/^--/, '');
const value = process.argv[i + 1];
if (key && value) {
// 尝试解析布尔值和数字
if (value === 'true') options[key] = true;
else if (value === 'false') options[key] = false;
else if (/^\d+$/.test(value)) options[key] = Number.parseInt(value);
else options[key] = value;
}
}
console.log(`执行增强工作流: ${workflowName}`);
console.log('参数:', options);
console.log();
try {
await registerEnhancedWorkflows();
const result = await moduleManager.executeWorkflow(workflowName, {
args: [],
options,
});
console.log('\\n📊 工作流执行结果:');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error('❌ 工作流执行失败:', error);
process.exit(1);
}
}
// 执行脚本
if (require.main === module) {
if (process.argv.length > 2 && process.argv[2] === 'interactive') {
interactiveEnhancedWorkflowExecution()
.then(() => process.exit(0))
.catch((error) => {
console.error('❌ 交互式执行失败:', error);
process.exit(1);
});
} else {
main()
.then(() => process.exit(0))
.catch((error) => {
console.error('❌ 演示脚本执行失败:', error);
process.exit(1);
});
}
}

View File

@@ -0,0 +1,342 @@
#!/usr/bin/env ts-node
import { moduleManager } from '../../src/module-manager';
import type { CLIProvider, Platform } from '../../src/types';
/**
* Provider API 使用示例
* 演示如何使用 CLIProvider 进行编程式操作
*/
class DeploymentService {
private provider: CLIProvider;
constructor() {
this.provider = moduleManager.getProvider();
}
/**
* 自动化构建和发布流程
*/
async buildAndPublish(platform: Platform, version: string) {
console.log(
`🚀 开始 ${platform} 平台的构建和发布流程 (版本: ${version})\n`,
);
try {
// 1. 打包应用
console.log('📦 正在打包应用...');
const bundleResult = await this.provider.bundle({
platform,
dev: false,
sourcemap: true,
bundleName: `app-${version}.bundle`,
});
if (!bundleResult.success) {
throw new Error(`打包失败: ${bundleResult.error}`);
}
console.log('✅ 打包完成');
// 2. 发布版本
console.log('\n📡 正在发布版本...');
const publishResult = await this.provider.publish({
name: version,
description: `自动发布版本 ${version}`,
rollout: 100,
});
if (!publishResult.success) {
throw new Error(`发布失败: ${publishResult.error}`);
}
console.log('✅ 发布完成');
return {
success: true,
bundleData: bundleResult.data,
publishData: publishResult.data,
};
} catch (error) {
console.error('❌ 构建发布失败:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* 应用管理示例
*/
async manageApp(platform: Platform) {
console.log(`📱 管理 ${platform} 应用\n`);
try {
// 获取当前选中的应用
const { appId } = await this.provider.getSelectedApp(platform);
console.log(`当前应用ID: ${appId}`);
// 列出应用版本
const versionsResult = await this.provider.listVersions(appId);
if (versionsResult.success && versionsResult.data) {
console.log('📋 应用版本列表:');
versionsResult.data.forEach((version: any, index: number) => {
console.log(` ${index + 1}. ${version.name} (${version.id})`);
});
}
// 列出应用包
const packagesResult = await (this.provider as any).listPackages(appId);
if (packagesResult.success && packagesResult.data) {
console.log('\n📦 应用包列表:');
packagesResult.data.forEach((pkg: any, index: number) => {
console.log(` ${index + 1}. ${pkg.name} (${pkg.id})`);
});
}
return { appId, platform };
} catch (error) {
console.error('❌ 应用管理失败:', error);
throw error;
}
}
/**
* 批量操作示例
*/
async batchOperations() {
console.log('🔄 批量操作示例\n');
const platforms: Platform[] = ['ios', 'android'];
const results = [];
for (const platform of platforms) {
try {
console.log(`--- 处理 ${platform} 平台 ---`);
// 获取平台信息
const platformInfo = await this.provider.getPlatform(platform);
console.log(`平台: ${platformInfo}`);
// 模拟打包操作
const bundleResult = await this.provider.bundle({
platform,
dev: true,
sourcemap: false,
});
results.push({
platform,
success: bundleResult.success,
data: bundleResult.data,
});
console.log(
`${platform} 处理完成: ${bundleResult.success ? '✅' : '❌'}\n`,
);
} catch (error) {
console.error(`${platform} 处理失败:`, error);
results.push({
platform,
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
});
}
}
return results;
}
/**
* 文件上传示例
*/
async uploadExample() {
console.log('📤 文件上传示例\n');
// 模拟上传文件路径
const mockFilePaths = {
ios: '/path/to/app.ipa',
android: '/path/to/app.apk',
};
try {
for (const [platform, filePath] of Object.entries(mockFilePaths)) {
console.log(`上传 ${platform} 文件: ${filePath}`);
// 注意:这里是模拟,实际使用时需要真实的文件路径
const uploadResult = await this.provider.upload({
platform: platform as Platform,
filePath,
appId: 'mock-app-id',
});
console.log(
`${platform} 上传结果:`,
uploadResult.success ? '✅' : '❌',
);
if (!uploadResult.success) {
console.log(`错误: ${uploadResult.error}`);
}
}
} catch (error) {
console.error('❌ 上传过程中发生错误:', error);
}
}
}
/**
* 高级工作流示例
*/
async function demonstrateAdvancedWorkflows() {
console.log('\n🔧 高级工作流示例\n');
const provider = moduleManager.getProvider();
// 注册自定义工作流
provider.registerWorkflow({
name: 'advanced-ci-cd',
description: '高级CI/CD流程',
steps: [
{
name: 'environment-check',
description: '环境检查',
execute: async (context) => {
console.log('🔍 检查环境配置...');
await new Promise((resolve) => setTimeout(resolve, 1000));
return { environmentValid: true };
},
},
{
name: 'quality-gate',
description: '质量门禁',
execute: async (context, previousResult) => {
console.log('🛡️ 执行质量门禁检查...');
await new Promise((resolve) => setTimeout(resolve, 1500));
// 模拟质量检查
const qualityScore = Math.random() * 100;
const passed = qualityScore > 80;
console.log(
`质量分数: ${qualityScore.toFixed(1)}/100 ${passed ? '✅' : '❌'}`,
);
if (!passed) {
throw new Error('质量门禁检查未通过');
}
return { ...previousResult, qualityScore };
},
},
{
name: 'multi-platform-build',
description: '多平台构建',
execute: async (context, previousResult) => {
console.log('🏗️ 多平台并行构建...');
const platforms: Platform[] = ['ios', 'android'];
const buildResults = [];
// 模拟并行构建
for (const platform of platforms) {
console.log(` 构建 ${platform}...`);
await new Promise((resolve) => setTimeout(resolve, 800));
buildResults.push({ platform, success: true });
}
return { ...previousResult, builds: buildResults };
},
},
{
name: 'deployment-notification',
description: '部署通知',
execute: async (context, previousResult) => {
console.log('📢 发送部署通知...');
const notification = {
message: '部署完成',
platforms: previousResult.builds?.map((b: any) => b.platform) || [],
timestamp: new Date().toISOString(),
};
console.log('通知内容:', JSON.stringify(notification, null, 2));
return { ...previousResult, notification };
},
},
],
validate: (context) => {
if (!context.options.environment) {
console.error('❌ 必须指定环境参数');
return false;
}
return true;
},
options: {
environment: {
hasValue: true,
description: '部署环境 (必需)',
},
},
});
// 执行高级工作流
try {
const result = await provider.executeWorkflow('advanced-ci-cd', {
args: [],
options: {
environment: 'production',
},
});
console.log('\n🎉 高级工作流执行完成:', result);
} catch (error) {
console.error('❌ 高级工作流执行失败:', error);
}
}
/**
* 主函数
*/
async function main() {
console.log('🎯 Provider API 使用示例\n');
const service = new DeploymentService();
try {
// 1. 构建和发布示例
await service.buildAndPublish('ios', '1.2.3');
console.log('\n' + '='.repeat(50) + '\n');
// 2. 应用管理示例
await service.manageApp('ios');
console.log('\n' + '='.repeat(50) + '\n');
// 3. 批量操作示例
const batchResults = await service.batchOperations();
console.log('批量操作结果:', batchResults);
console.log('\n' + '='.repeat(50) + '\n');
// 4. 文件上传示例
await service.uploadExample();
console.log('\n' + '='.repeat(50) + '\n');
// 5. 高级工作流示例
await demonstrateAdvancedWorkflows();
} catch (error) {
console.error('❌ 示例执行失败:', error);
process.exit(1);
}
}
// 执行示例
if (require.main === module) {
main()
.then(() => {
console.log('\n✨ Provider API 示例执行完成');
process.exit(0);
})
.catch((error) => {
console.error('❌ 示例执行失败:', error);
process.exit(1);
});
}

View File

@@ -0,0 +1,155 @@
#!/usr/bin/env ts-node
import { moduleManager } from '../../src/module-manager';
import { analyticsModule } from '../modules/analytics-module';
import { customDeployModule } from '../modules/custom-deploy-module';
/**
* 模块注册和执行示例脚本
* 演示如何注册自定义模块并执行命令和工作流
*/
async function main() {
console.log('🚀 开始模块注册和执行示例\n');
try {
// 1. 注册自定义模块
console.log('📦 注册自定义模块...');
moduleManager.registerModule(customDeployModule);
moduleManager.registerModule(analyticsModule);
console.log('✅ 模块注册完成\n');
// 2. 列出所有可用的命令
console.log('📋 可用命令列表:');
const commands = moduleManager.listCommands();
commands.forEach((cmd) => {
console.log(` - ${cmd.name}: ${cmd.description || '无描述'}`);
});
console.log();
// 3. 列出所有可用的工作流
console.log('🔄 可用工作流列表:');
const workflows = moduleManager.listWorkflows();
workflows.forEach((workflow) => {
console.log(` - ${workflow.name}: ${workflow.description || '无描述'}`);
});
console.log();
// 4. 执行自定义命令示例
console.log('🎯 执行命令示例:\n');
// 执行开发部署命令
console.log('--- 执行 deploy-dev 命令 ---');
const devDeployResult = await moduleManager.executeCommand('deploy-dev', {
args: [],
options: {
platform: 'ios',
force: true,
},
});
console.log('结果:', devDeployResult);
console.log();
// 执行分析统计命令
console.log('--- 执行 track-deployment 命令 ---');
const trackResult = await moduleManager.executeCommand('track-deployment', {
args: [],
options: {
platform: 'android',
environment: 'production',
version: '1.2.3',
},
});
console.log('结果:', trackResult);
console.log();
// 生成部署报告
console.log('--- 执行 deployment-report 命令 ---');
const reportResult = await moduleManager.executeCommand(
'deployment-report',
{
args: [],
options: {
days: 30,
},
},
);
console.log('结果:', reportResult);
console.log();
// 5. 执行工作流示例
console.log('🔄 执行工作流示例:\n');
// 执行带统计的部署工作流
console.log('--- 执行 deploy-with-analytics 工作流 ---');
const analyticsWorkflowResult = await moduleManager.executeWorkflow(
'deploy-with-analytics',
{
args: [],
options: {},
},
);
console.log('工作流结果:', analyticsWorkflowResult);
console.log();
// 执行热修复工作流
console.log('--- 执行 hotfix-deploy 工作流 ---');
const hotfixWorkflowResult = await moduleManager.executeWorkflow(
'hotfix-deploy',
{
args: [],
options: {
hotfixId: 'HF-2024-001',
},
},
);
console.log('工作流结果:', hotfixWorkflowResult);
console.log();
console.log('🎉 所有示例执行完成!');
} catch (error) {
console.error('❌ 执行过程中发生错误:', error);
process.exit(1);
}
}
// 错误处理函数
async function demonstrateErrorHandling() {
console.log('\n🚨 错误处理示例:\n');
try {
// 尝试执行不存在的命令
console.log('--- 尝试执行不存在的命令 ---');
await moduleManager.executeCommand('non-existent-command', {
args: [],
options: {},
});
} catch (error) {
console.log('捕获错误:', error instanceof Error ? error.message : error);
}
try {
// 尝试执行缺少必需参数的命令
console.log('\n--- 尝试执行缺少必需参数的命令 ---');
await moduleManager.executeCommand('deploy-prod', {
args: [],
options: {}, // 缺少必需的 version 参数
});
} catch (error) {
console.log('捕获错误:', error instanceof Error ? error.message : error);
}
}
// 主函数执行
if (require.main === module) {
main()
.then(() => demonstrateErrorHandling())
.then(() => {
console.log('\n✨ 示例脚本执行完成');
process.exit(0);
})
.catch((error) => {
console.error('❌ 脚本执行失败:', error);
process.exit(1);
});
}

View File

@@ -0,0 +1,290 @@
#!/usr/bin/env ts-node
import { moduleManager } from '../../src/module-manager';
import { customWorkflows } from '../workflows/custom-workflows';
/**
* 工作流演示脚本
* 展示如何注册和执行复杂的工作流
*/
async function registerCustomWorkflows() {
console.log('📋 注册自定义工作流...\n');
const provider = moduleManager.getProvider();
// 注册所有自定义工作流
customWorkflows.forEach((workflow) => {
provider.registerWorkflow(workflow);
console.log(`✅ 注册工作流: ${workflow.name} - ${workflow.description}`);
});
console.log('\n📋 所有工作流注册完成\n');
}
/**
* 演示灰度发布工作流
*/
async function demonstrateCanaryDeployment() {
console.log('🔥 演示灰度发布工作流\n');
console.log('='.repeat(60));
try {
const result = await moduleManager.executeWorkflow('canary-deployment', {
args: [],
options: {
version: '2.1.0',
initialRollout: 10,
autoExpand: true,
},
});
console.log('\n📊 灰度发布工作流结果:');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error('❌ 灰度发布工作流执行失败:', error);
}
console.log('\n' + '='.repeat(60) + '\n');
}
/**
* 演示多环境发布工作流
*/
async function demonstrateMultiEnvironmentDeploy() {
console.log('🌍 演示多环境发布工作流\n');
console.log('='.repeat(60));
try {
const result = await moduleManager.executeWorkflow('multi-env-deploy', {
args: [],
options: {
version: '2.1.0',
skipProduction: false,
forceProduction: false,
},
});
console.log('\n📊 多环境发布工作流结果:');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error('❌ 多环境发布工作流执行失败:', error);
}
console.log('\n' + '='.repeat(60) + '\n');
}
/**
* 演示回滚工作流
*/
async function demonstrateRollbackWorkflow() {
console.log('🔄 演示回滚工作流\n');
console.log('='.repeat(60));
try {
const result = await moduleManager.executeWorkflow('rollback-workflow', {
args: [],
options: {
targetVersion: '2.0.5',
skipVerification: false,
},
});
console.log('\n📊 回滚工作流结果:');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error('❌ 回滚工作流执行失败:', error);
}
console.log('\n' + '='.repeat(60) + '\n');
}
/**
* 演示工作流验证失败的情况
*/
async function demonstrateWorkflowValidation() {
console.log('⚠️ 演示工作流验证\n');
console.log('='.repeat(60));
// 1. 演示缺少必需参数的情况
console.log('--- 测试缺少必需参数 ---');
try {
await moduleManager.executeWorkflow('canary-deployment', {
args: [],
options: {}, // 缺少 version 参数
});
} catch (error) {
console.log(
'✅ 正确捕获验证错误:',
error instanceof Error ? error.message : error,
);
}
console.log('\n--- 测试回滚工作流验证 ---');
try {
await moduleManager.executeWorkflow('rollback-workflow', {
args: [],
options: {}, // 缺少 targetVersion 参数
});
} catch (error) {
console.log(
'✅ 正确捕获验证错误:',
error instanceof Error ? error.message : error,
);
}
console.log('\n' + '='.repeat(60) + '\n');
}
/**
* 演示工作流的条件执行
*/
async function demonstrateConditionalExecution() {
console.log('🔀 演示条件执行\n');
console.log('='.repeat(60));
// 演示跳过生产部署
console.log('--- 跳过生产环境部署 ---');
try {
const result = await moduleManager.executeWorkflow('multi-env-deploy', {
args: [],
options: {
version: '2.1.1',
skipProduction: true, // 跳过生产部署
},
});
console.log('📊 跳过生产部署的结果:');
console.log(`包含生产部署步骤: ${result.data?.production ? '是' : '否'}`);
} catch (error) {
console.error('❌ 条件执行演示失败:', error);
}
console.log('\n' + '='.repeat(60) + '\n');
}
/**
* 列出所有可用的工作流
*/
async function listAvailableWorkflows() {
console.log('📋 可用工作流列表\n');
console.log('='.repeat(60));
const workflows = moduleManager.listWorkflows();
workflows.forEach((workflow, index) => {
console.log(`${index + 1}. ${workflow.name}`);
console.log(` 描述: ${workflow.description || '无描述'}`);
console.log(` 步骤数: ${workflow.steps.length}`);
if (workflow.options) {
console.log(' 选项:');
Object.entries(workflow.options).forEach(([key, option]) => {
const opt = option as any;
const required = opt.hasValue && !opt.default;
console.log(
` --${key}: ${opt.description || '无描述'} ${required ? '(必需)' : ''}`,
);
});
}
console.log();
});
console.log('='.repeat(60) + '\n');
}
/**
* 主函数
*/
async function main() {
console.log('🎯 工作流演示脚本\n');
try {
// 1. 注册自定义工作流
await registerCustomWorkflows();
// 2. 列出所有可用工作流
await listAvailableWorkflows();
// 3. 演示各种工作流
await demonstrateCanaryDeployment();
await demonstrateMultiEnvironmentDeploy();
await demonstrateRollbackWorkflow();
// 4. 演示验证和条件执行
await demonstrateWorkflowValidation();
await demonstrateConditionalExecution();
console.log('🎉 所有工作流演示完成!');
} catch (error) {
console.error('❌ 演示过程中发生错误:', error);
process.exit(1);
}
}
/**
* 交互式工作流执行
*/
async function interactiveWorkflowExecution() {
console.log('\n🎮 交互式工作流执行\n');
const workflowName = process.argv[3];
if (!workflowName) {
console.log('使用方法:');
console.log(' npm run workflow-demo [工作流名称]');
console.log('\n可用的工作流:');
console.log(' - canary-deployment');
console.log(' - multi-env-deploy');
console.log(' - rollback-workflow');
return;
}
// 解析命令行参数
const options: Record<string, any> = {};
for (let i = 4; i < process.argv.length; i += 2) {
const key = process.argv[i]?.replace(/^--/, '');
const value = process.argv[i + 1];
if (key && value) {
options[key] = value;
}
}
console.log(`执行工作流: ${workflowName}`);
console.log('参数:', options);
console.log();
try {
await registerCustomWorkflows();
const result = await moduleManager.executeWorkflow(workflowName, {
args: [],
options,
});
console.log('\n📊 工作流执行结果:');
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error('❌ 工作流执行失败:', error);
process.exit(1);
}
}
// 执行脚本
if (require.main === module) {
if (process.argv.length > 2 && process.argv[2] === 'interactive') {
interactiveWorkflowExecution()
.then(() => process.exit(0))
.catch((error) => {
console.error('❌ 交互式执行失败:', error);
process.exit(1);
});
} else {
main()
.then(() => process.exit(0))
.catch((error) => {
console.error('❌ 演示脚本执行失败:', error);
process.exit(1);
});
}
}

23
example/tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"moduleResolution": "node",
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": false,
"resolveJsonModule": true,
"noEmit": true,
"types": ["node"]
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"],
"ts-node": {
"transpileOnly": true,
"compilerOptions": {
"module": "commonjs"
}
}
}

View File

@@ -0,0 +1,582 @@
import type {
CLIProvider,
CommandContext,
CustomWorkflow,
} from '../../src/types';
/**
* 自定义工作流集合
* 演示各种复杂的工作流场景
*/
/**
* 灰度发布工作流
*/
export const canaryDeploymentWorkflow: CustomWorkflow = {
name: 'canary-deployment',
description: '灰度发布工作流 - 逐步增加用户覆盖率',
steps: [
{
name: 'prepare-canary',
description: '准备灰度发布',
execute: async (context: CommandContext) => {
console.log('🔧 准备灰度发布环境...');
const { version, initialRollout = 5 } = context.options;
console.log(`📦 版本: ${version}`);
console.log(`📊 初始覆盖率: ${initialRollout}%`);
await new Promise((resolve) => setTimeout(resolve, 1000));
return {
version,
currentRollout: initialRollout,
stage: 'prepared',
};
},
},
{
name: 'initial-deployment',
description: '初始小范围部署',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🚀 执行初始小范围部署...');
const { currentRollout } = previousResult;
console.log(`部署到 ${currentRollout}% 用户...`);
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log('✅ 初始部署完成');
return {
...previousResult,
deploymentTime: new Date().toISOString(),
stage: 'initial-deployed',
};
},
},
{
name: 'monitor-metrics',
description: '监控关键指标',
execute: async (context: CommandContext, previousResult: any) => {
console.log('📊 监控关键指标...');
// 模拟监控数据
const metrics = {
crashRate: Math.random() * 0.01, // 0-1%
responseTime: 150 + Math.random() * 100, // 150-250ms
userSatisfaction: 85 + Math.random() * 10, // 85-95%
errorRate: Math.random() * 0.005, // 0-0.5%
};
console.log('📈 监控结果:');
console.log(` 崩溃率: ${(metrics.crashRate * 100).toFixed(3)}%`);
console.log(` 响应时间: ${metrics.responseTime.toFixed(1)}ms`);
console.log(` 用户满意度: ${metrics.userSatisfaction.toFixed(1)}%`);
console.log(` 错误率: ${(metrics.errorRate * 100).toFixed(3)}%`);
// 判断是否可以继续扩大范围
const canProceed =
metrics.crashRate < 0.005 &&
metrics.errorRate < 0.003 &&
metrics.userSatisfaction > 80;
console.log(`🔍 健康检查: ${canProceed ? '✅ 通过' : '❌ 未通过'}`);
return {
...previousResult,
metrics,
canProceed,
stage: 'monitored',
};
},
},
{
name: 'expand-rollout',
description: '扩大发布范围',
execute: async (context: CommandContext, previousResult: any) => {
const { canProceed, currentRollout } = previousResult;
if (!canProceed) {
console.log('⚠️ 指标不达标,停止扩大发布范围');
return {
...previousResult,
stage: 'rollout-stopped',
};
}
console.log('📈 扩大发布范围...');
const newRollout = Math.min(currentRollout * 2, 100);
console.log(`覆盖率从 ${currentRollout}% 扩大到 ${newRollout}%`);
await new Promise((resolve) => setTimeout(resolve, 1200));
return {
...previousResult,
currentRollout: newRollout,
stage: newRollout >= 100 ? 'fully-deployed' : 'expanded',
};
},
condition: (context: CommandContext) => {
// 只有在启用自动扩大的情况下才执行
return context.options.autoExpand !== false;
},
},
{
name: 'final-verification',
description: '最终验证',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🔍 执行最终验证...');
const { stage, currentRollout } = previousResult;
if (stage === 'rollout-stopped') {
console.log('❌ 灰度发布因指标不达标而停止');
return {
...previousResult,
finalStatus: 'failed',
reason: 'metrics-failed',
};
}
console.log('✅ 灰度发布验证完成');
console.log(`📊 最终覆盖率: ${currentRollout}%`);
return {
...previousResult,
finalStatus: 'success',
completedAt: new Date().toISOString(),
};
},
},
],
validate: (context: CommandContext) => {
if (!context.options.version) {
console.error('❌ 灰度发布必须指定版本号');
return false;
}
return true;
},
options: {
version: {
hasValue: true,
description: '发布版本号 (必需)',
},
initialRollout: {
hasValue: true,
default: 5,
description: '初始覆盖率百分比',
},
autoExpand: {
hasValue: false,
default: true,
description: '自动扩大发布范围',
},
},
};
/**
* 多环境发布工作流
*/
export const multiEnvironmentDeployWorkflow: CustomWorkflow = {
name: 'multi-env-deploy',
description: '多环境依次发布工作流',
steps: [
{
name: 'deploy-to-dev',
description: '部署到开发环境',
execute: async (context: CommandContext) => {
console.log('🔧 部署到开发环境...');
await new Promise((resolve) => setTimeout(resolve, 1000));
const devResult = {
environment: 'development',
deployTime: new Date().toISOString(),
success: true,
};
console.log('✅ 开发环境部署完成');
return { dev: devResult };
},
},
{
name: 'run-integration-tests',
description: '运行集成测试',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🧪 运行集成测试...');
const testSuites = ['API测试', '数据库测试', '第三方服务测试'];
const results = [];
for (const suite of testSuites) {
console.log(` 运行 ${suite}...`);
await new Promise((resolve) => setTimeout(resolve, 500));
const passed = Math.random() > 0.1; // 90% 通过率
results.push({ suite, passed });
console.log(` ${passed ? '✅' : '❌'} ${suite}`);
}
const allPassed = results.every((r) => r.passed);
console.log(
`🧪 集成测试结果: ${allPassed ? '✅ 全部通过' : '❌ 有失败项'}`,
);
return {
...previousResult,
integrationTests: { results, allPassed },
};
},
},
{
name: 'deploy-to-staging',
description: '部署到预发布环境',
execute: async (context: CommandContext, previousResult: any) => {
const { integrationTests } = previousResult;
if (!integrationTests.allPassed) {
throw new Error('集成测试未通过,无法部署到预发布环境');
}
console.log('🎭 部署到预发布环境...');
await new Promise((resolve) => setTimeout(resolve, 1500));
const stagingResult = {
environment: 'staging',
deployTime: new Date().toISOString(),
success: true,
};
console.log('✅ 预发布环境部署完成');
return {
...previousResult,
staging: stagingResult,
};
},
},
{
name: 'run-e2e-tests',
description: '运行端到端测试',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🎯 运行端到端测试...');
const e2eTests = [
'用户登录流程',
'核心业务流程',
'支付流程',
'数据同步',
];
const results = [];
for (const test of e2eTests) {
console.log(` 测试 ${test}...`);
await new Promise((resolve) => setTimeout(resolve, 800));
const passed = Math.random() > 0.05; // 95% 通过率
results.push({ test, passed });
console.log(` ${passed ? '✅' : '❌'} ${test}`);
}
const allPassed = results.every((r) => r.passed);
console.log(
`🎯 E2E测试结果: ${allPassed ? '✅ 全部通过' : '❌ 有失败项'}`,
);
return {
...previousResult,
e2eTests: { results, allPassed },
};
},
},
{
name: 'deploy-to-production',
description: '部署到生产环境',
execute: async (context: CommandContext, previousResult: any) => {
const { e2eTests } = previousResult;
if (!e2eTests.allPassed) {
console.log('⚠️ E2E测试未全部通过需要手动确认是否继续部署');
if (!context.options.forceProduction) {
throw new Error('E2E测试未通过使用 --force-production 强制部署');
}
}
console.log('🚀 部署到生产环境...');
// 生产部署需要更长时间
await new Promise((resolve) => setTimeout(resolve, 2000));
const productionResult = {
environment: 'production',
deployTime: new Date().toISOString(),
success: true,
version: context.options.version,
};
console.log('🎉 生产环境部署完成');
return {
...previousResult,
production: productionResult,
};
},
condition: (context: CommandContext) => {
// 只有在非跳过生产部署的情况下才执行
return !context.options.skipProduction;
},
},
{
name: 'post-deployment-verification',
description: '部署后验证',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🔍 执行部署后验证...');
const verifications = [
'健康检查',
'关键接口测试',
'监控数据验证',
'用户访问验证',
];
for (const verification of verifications) {
console.log(` ${verification}...`);
await new Promise((resolve) => setTimeout(resolve, 300));
console.log(`${verification} 通过`);
}
console.log('✅ 部署后验证完成');
return {
...previousResult,
postDeploymentVerification: {
completed: true,
verifiedAt: new Date().toISOString(),
},
};
},
},
],
validate: (context: CommandContext) => {
if (!context.options.version) {
console.error('❌ 多环境部署必须指定版本号');
return false;
}
return true;
},
options: {
version: {
hasValue: true,
description: '发布版本号 (必需)',
},
skipProduction: {
hasValue: false,
default: false,
description: '跳过生产环境部署',
},
forceProduction: {
hasValue: false,
default: false,
description: '强制部署到生产环境(即使测试未全部通过)',
},
},
};
/**
* 回滚工作流
*/
export const rollbackWorkflow: CustomWorkflow = {
name: 'rollback-workflow',
description: '应用回滚工作流',
steps: [
{
name: 'validate-target-version',
description: '验证目标版本',
execute: async (context: CommandContext) => {
console.log('🔍 验证目标回滚版本...');
const { targetVersion } = context.options;
if (!targetVersion) {
throw new Error('必须指定目标回滚版本');
}
// 模拟版本验证
console.log(`验证版本 ${targetVersion} 是否存在...`);
await new Promise((resolve) => setTimeout(resolve, 800));
const versionExists = true; // 模拟版本存在
if (!versionExists) {
throw new Error(`版本 ${targetVersion} 不存在`);
}
console.log(`✅ 版本 ${targetVersion} 验证通过`);
return {
targetVersion,
validated: true,
};
},
},
{
name: 'backup-current-state',
description: '备份当前状态',
execute: async (context: CommandContext, previousResult: any) => {
console.log('💾 备份当前应用状态...');
const backup = {
backupId: `backup-${Date.now()}`,
timestamp: new Date().toISOString(),
currentVersion: 'current-version', // 在实际应用中获取当前版本
configSnapshot: 'config-data', // 在实际应用中获取配置快照
};
await new Promise((resolve) => setTimeout(resolve, 1500));
console.log(`✅ 状态备份完成备份ID: ${backup.backupId}`);
return {
...previousResult,
backup,
};
},
},
{
name: 'execute-rollback',
description: '执行回滚',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🔄 执行回滚操作...');
const { targetVersion } = previousResult;
console.log(`回滚到版本: ${targetVersion}`);
// 模拟回滚过程
const rollbackSteps = [
'停止当前服务',
'切换到目标版本',
'更新配置',
'重启服务',
];
for (const step of rollbackSteps) {
console.log(` ${step}...`);
await new Promise((resolve) => setTimeout(resolve, 600));
console.log(`${step} 完成`);
}
console.log('🎉 回滚执行完成');
return {
...previousResult,
rollbackCompleted: true,
rollbackTime: new Date().toISOString(),
};
},
},
{
name: 'verify-rollback',
description: '验证回滚结果',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🔍 验证回滚结果...');
const verificationChecks = [
'服务可用性检查',
'功能完整性检查',
'性能基线检查',
'数据一致性检查',
];
const results = [];
for (const check of verificationChecks) {
console.log(` ${check}...`);
await new Promise((resolve) => setTimeout(resolve, 400));
const passed = Math.random() > 0.05; // 95% 通过率
results.push({ check, passed });
console.log(` ${passed ? '✅' : '❌'} ${check}`);
}
const allPassed = results.every((r) => r.passed);
if (!allPassed) {
console.log('⚠️ 部分验证未通过,可能需要进一步检查');
} else {
console.log('✅ 回滚验证全部通过');
}
return {
...previousResult,
verification: { results, allPassed },
};
},
},
{
name: 'notify-stakeholders',
description: '通知相关人员',
execute: async (context: CommandContext, previousResult: any) => {
console.log('📧 通知相关人员...');
const { targetVersion, verification } = previousResult;
const notification = {
type: 'rollback-completed',
targetVersion,
success: verification.allPassed,
timestamp: new Date().toISOString(),
notifiedStakeholders: [
'开发团队',
'运维团队',
'产品团队',
'测试团队',
],
};
console.log('📬 发送通知给:');
notification.notifiedStakeholders.forEach((stakeholder) => {
console.log(` - ${stakeholder}`);
});
await new Promise((resolve) => setTimeout(resolve, 500));
console.log('✅ 通知发送完成');
return {
...previousResult,
notification,
};
},
},
],
validate: (context: CommandContext) => {
if (!context.options.targetVersion) {
console.error('❌ 回滚操作必须指定目标版本');
return false;
}
return true;
},
options: {
targetVersion: {
hasValue: true,
description: '目标回滚版本 (必需)',
},
skipVerification: {
hasValue: false,
default: false,
description: '跳过回滚后验证',
},
},
};
/**
* 导出所有工作流
*/
export const customWorkflows = [
canaryDeploymentWorkflow,
multiEnvironmentDeployWorkflow,
rollbackWorkflow,
];

File diff suppressed because it is too large Load Diff

1863
example/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1 @@
/**
* Created by tdzl2003 on 2/22/16.
*/
module.exports = require('./lib');
module.exports = require('./lib');

View File

@@ -1,10 +1,11 @@
{
"name": "react-native-update-cli",
"version": "1.8.1",
"description": "Command tools for javaScript updater with `pushy` service for react native apps.",
"version": "2.1.3",
"description": "command line tool for react-native-update (remote updates for react native)",
"main": "index.js",
"bin": {
"pushy": "lib/index.js"
"pushy": "lib/index.js",
"cresc": "lib/index.js"
},
"files": [
"lib",
@@ -12,49 +13,76 @@
"cli.json"
],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"prepublish": "babel src --out-dir lib"
"build": "swc src -d lib --strip-leading-paths",
"prepublishOnly": "npm run build && chmod +x lib/index.js",
"lint": "tsc --noEmit & biome check --write ."
},
"repository": {
"type": "git",
"url": "git+https://github.com/reactnativecn/react-native-pushy-cli.git"
"url": "git+https://github.com/reactnativecn/react-native-update-cli.git"
},
"keywords": [
"react-native",
"ios",
"android",
"harmony",
"update"
],
"author": "reactnativecn",
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/reactnativecn/react-native-pushy/issues"
"url": "https://github.com/reactnativecn/react-native-update-cli/issues"
},
"homepage": "https://github.com/reactnativecn/react-native-pushy/tree/master/react-native-pushy-cli",
"homepage": "https://github.com/reactnativecn/react-native-update-cli",
"dependencies": {
"app-info-parser": "^1.0.0",
"@colors/colors": "^1.6.0",
"bplist-parser": "^0.3.2",
"bytebuffer": "^5.0.1",
"cgbi-to-png": "^1.0.7",
"chalk": "4",
"cli-arguments": "^0.2.1",
"filesize-parser": "^1.5.0",
"commander": "^13",
"compare-versions": "^6.1.1",
"filesize-parser": "^1.5.1",
"form-data": "^4.0.2",
"fs-extra": "8",
"gradle-to-js": "^2.0.0",
"global-dirs": "^4.0.0",
"gradle-to-js": "^2.0.1",
"i18next": "^24.2.3",
"isomorphic-git": "^1.30.1",
"isomorphic-unzip": "^1.1.5",
"node-fetch": "^2.6.1",
"plist": "^3.1.0",
"progress": "^2.0.3",
"read": "^1.0.7",
"request": "^2.88.2",
"properties": "^1.2.1",
"read": "^4.1.0",
"registry-auth-token": "^5.1.0",
"semver": "^7.7.2",
"tcp-ping": "^0.1.1",
"tty-table": "4.1",
"update-notifier": "^4.1.1",
"yauzl": "^2.10.0",
"yazl": "2.5.1"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-plugin-syntax-object-rest-spread": "^6.13.0",
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
"babel-plugin-transform-es2015-spread": "^6.22.0",
"babel-plugin-transform-object-rest-spread": "^6.26.0"
"tty-table": "4.2",
"yauzl": "^3.2.0",
"yazl": "3.3.1"
},
"engines": {
"node": ">= 8"
}
"node": ">= 14"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@swc/cli": "0.7.7",
"@swc/core": "^1.11.24",
"@types/filesize-parser": "^1.5.3",
"@types/fs-extra": "^11.0.4",
"@types/node": "^22.15.18",
"@types/node-fetch": "^2.6.12",
"@types/progress": "^2.0.7",
"@types/semver": "^7.7.0",
"@types/tcp-ping": "^0.1.6",
"@types/yauzl": "^2.10.3",
"@types/yazl": "^2.4.6",
"typescript": "^5.8.3"
},
"trustedDependencies": [
"@biomejs/biome",
"@swc/core"
]
}

3035
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,179 +0,0 @@
const fetch = require('node-fetch');
const defaultEndpoint = 'http://u.reactnative.cn/api';
let host = process.env.PUSHY_REGISTRY || defaultEndpoint;
const fs = require('fs');
import request from 'request';
import ProgressBar from 'progress';
const packageJson = require('../package.json');
const tcpp = require('tcp-ping');
const util = require('util');
const path = require('path');
import filesizeParser from 'filesize-parser';
import { pricingPageUrl } from './utils';
const tcpPing = util.promisify(tcpp.ping);
let session = undefined;
let savedSession = undefined;
const userAgent = `react-native-update-cli/${packageJson.version}`;
exports.loadSession = async function () {
if (fs.existsSync('.update')) {
try {
exports.replaceSession(JSON.parse(fs.readFileSync('.update', 'utf8')));
savedSession = session;
} catch (e) {
console.error(
'Failed to parse file `.update`. Try to remove it manually.',
);
throw e;
}
}
};
exports.getSession = function () {
return session;
};
exports.replaceSession = function (newSession) {
session = newSession;
};
exports.saveSession = function () {
// Only save on change.
if (session !== savedSession) {
const current = session;
const data = JSON.stringify(current, null, 4);
fs.writeFileSync('.update', data, 'utf8');
savedSession = current;
}
};
exports.closeSession = function () {
if (fs.existsSync('.update')) {
fs.unlinkSync('.update');
savedSession = undefined;
}
session = undefined;
host = process.env.PUSHY_REGISTRY || defaultEndpoint;
};
async function query(url, options) {
const resp = await fetch(url, options);
const text = await resp.text();
let json;
try {
json = JSON.parse(text);
} catch (e) {
throw new Error(`Server error: ${text}`);
}
if (resp.status !== 200) {
throw Object.assign(new Error(json.message || json.error), {
status: resp.status,
});
}
return json;
}
function queryWithoutBody(method) {
return function (api) {
return query(host + api, {
method,
headers: {
'User-Agent': userAgent,
'X-AccessToken': session ? session.token : '',
},
});
};
}
function queryWithBody(method) {
return function (api, body) {
return query(host + api, {
method,
headers: {
'User-Agent': userAgent,
'Content-Type': 'application/json',
'X-AccessToken': session ? session.token : '',
},
body: JSON.stringify(body),
});
};
}
exports.get = queryWithoutBody('GET');
exports.post = queryWithBody('POST');
exports.put = queryWithBody('PUT');
exports.doDelete = queryWithBody('DELETE');
async function uploadFile(fn, key) {
const { url, backupUrl, formData, maxSize } = await exports.post('/upload', {
ext: path.extname(fn),
});
let realUrl = url;
if (backupUrl) {
if (global.USE_ACC_OSS) {
realUrl = backupUrl;
} else {
const pingResult = await tcpPing({
address: url.replace('https://', ''),
attempts: 4,
timeout: 1000,
});
// console.log({pingResult});
if (isNaN(pingResult.avg) || pingResult.avg > 150) {
realUrl = backupUrl;
}
}
// console.log({realUrl});
}
const fileSize = fs.statSync(fn).size;
if (maxSize && fileSize > filesizeParser(maxSize)) {
throw new Error(
`此文件大小${(fileSize / 1048576).toFixed(
1,
)}m, 超出当前额度${maxSize}。您可以考虑升级付费业务以提升此额度。详情请访问: ${pricingPageUrl}`,
);
}
const bar = new ProgressBar(' 上传中 [:bar] :percent :etas', {
complete: '=',
incomplete: ' ',
total: fileSize,
});
const info = await new Promise((resolve, reject) => {
if (key) {
formData.key = key;
}
formData.file = fs.createReadStream(fn);
formData.file.on('data', function (data) {
bar.tick(data.length);
});
request.post(
realUrl,
{
formData,
},
(err, resp, body) => {
if (err) {
return reject(err);
}
if (resp.statusCode > 299) {
return reject(
Object.assign(new Error(body), { status: resp.statusCode }),
);
}
resolve({ hash: formData.key });
},
);
});
return info;
}
exports.uploadFile = uploadFile;

185
src/api.ts Normal file
View File

@@ -0,0 +1,185 @@
import fs from 'fs';
import path from 'path';
import util from 'util';
import filesizeParser from 'filesize-parser';
import FormData from 'form-data';
import fetch from 'node-fetch';
import ProgressBar from 'progress';
import tcpp from 'tcp-ping';
import packageJson from '../package.json';
import type { Package, Session } from './types';
import {
credentialFile,
pricingPageUrl,
} from './utils/constants';
import { t } from './utils/i18n';
import { getBaseUrl } from 'utils/http-helper';
const tcpPing = util.promisify(tcpp.ping);
let session: Session | undefined;
let savedSession: Session | undefined;
const userAgent = `react-native-update-cli/${packageJson.version}`;
export const getSession = () => session;
export const replaceSession = (newSession: { token: string }) => {
session = newSession;
};
export const loadSession = async () => {
if (fs.existsSync(credentialFile)) {
try {
replaceSession(JSON.parse(fs.readFileSync(credentialFile, 'utf8')));
savedSession = session;
} catch (e) {
console.error(
`Failed to parse file ${credentialFile}. Try to remove it manually.`,
);
throw e;
}
}
};
export const saveSession = () => {
// Only save on change.
if (session !== savedSession) {
const current = session;
const data = JSON.stringify(current, null, 4);
fs.writeFileSync(credentialFile, data, 'utf8');
savedSession = current;
}
};
export const closeSession = () => {
if (fs.existsSync(credentialFile)) {
fs.unlinkSync(credentialFile);
savedSession = undefined;
}
session = undefined;
};
async function query(url: string, options: fetch.RequestInit) {
const baseUrl = await getBaseUrl;
const fullUrl = `${baseUrl}${url}`;
const resp = await fetch(fullUrl, options);
const text = await resp.text();
let json: any;
try {
json = JSON.parse(text);
} catch (e) {}
if (resp.status !== 200) {
const message = json?.message || resp.statusText;
if (resp.status === 401) {
throw new Error(t('loginExpired'));
}
throw new Error(message);
}
return json;
}
function queryWithoutBody(method: string) {
return (api: string) =>
query(api, {
method,
headers: {
'User-Agent': userAgent,
'X-AccessToken': session ? session.token : '',
},
});
}
function queryWithBody(method: string) {
return (api: string, body?: Record<string, any>) =>
query(api, {
method,
headers: {
'User-Agent': userAgent,
'Content-Type': 'application/json',
'X-AccessToken': session ? session.token : '',
},
body: JSON.stringify(body),
});
}
export const get = queryWithoutBody('GET');
export const post = queryWithBody('POST');
export const put = queryWithBody('PUT');
export const doDelete = queryWithBody('DELETE');
export async function uploadFile(fn: string, key?: string) {
const { url, backupUrl, formData, maxSize } = await post('/upload', {
ext: path.extname(fn),
});
let realUrl = url;
if (backupUrl) {
// @ts-ignore
if (global.USE_ACC_OSS) {
realUrl = backupUrl;
} else {
const pingResult = await tcpPing({
address: url.replace('https://', ''),
attempts: 4,
timeout: 1000,
});
// console.log({pingResult});
if (Number.isNaN(pingResult.avg) || pingResult.avg > 150) {
realUrl = backupUrl;
}
}
// console.log({realUrl});
}
const fileSize = fs.statSync(fn).size;
if (maxSize && fileSize > filesizeParser(maxSize)) {
const readableFileSize = `${(fileSize / 1048576).toFixed(1)}m`;
throw new Error(
t('fileSizeExceeded', {
fileSize: readableFileSize,
maxSize,
pricingPageUrl,
}),
);
}
const bar = new ProgressBar(' Uploading [:bar] :percent :etas', {
complete: '=',
incomplete: ' ',
total: fileSize,
});
const form = new FormData();
for (const [k, v] of Object.entries(formData)) {
form.append(k, v);
}
const fileStream = fs.createReadStream(fn);
fileStream.on('data', (data) => {
bar.tick(data.length);
});
if (key) {
form.append('key', key);
}
form.append('file', fileStream);
const res = await fetch(realUrl, {
method: 'POST',
body: form,
});
if (res.status > 299) {
throw new Error(`${res.status}: ${res.statusText}`);
}
// const body = await response.json();
return { hash: key || formData.key };
}
export const getAllPackages = async (appId: string) => {
const { data } = await get(`/app/${appId}/package/list?limit=1000`);
return data as Package[] | undefined | null;
};

View File

@@ -1,127 +0,0 @@
import { question } from './utils';
import fs from 'fs';
const Table = require('tty-table');
const { post, get, doDelete } = require('./api');
const validPlatforms = {
ios: 1,
android: 1,
};
export function checkPlatform(platform) {
if (!validPlatforms[platform]) {
throw new Error(`无法识别的平台 '${platform}'`);
}
return platform;
}
export function getSelectedApp(platform) {
checkPlatform(platform);
if (!fs.existsSync('update.json')) {
throw new Error(
`App not selected. run 'pushy selectApp --platform ${platform}' first!`,
);
}
const updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
if (!updateInfo[platform]) {
throw new Error(
`App not selected. run 'pushy selectApp --platform ${platform}' first!`,
);
}
return updateInfo[platform];
}
export async function listApp(platform) {
const { data } = await get('/app/list');
const list = platform ? data.filter((v) => v.platform === platform) : data;
const header = [
{ value: '应用 id' },
{ value: '应用名称' },
{ value: '平台' },
];
const rows = [];
for (const app of list) {
rows.push([app.id, app.name, app.platform]);
}
console.log(Table(header, rows).render());
if (platform) {
console.log(`\共 ${list.length} ${platform} 个应用`);
} else {
console.log(`\共 ${list.length} 个应用`);
}
return list;
}
export async function chooseApp(platform) {
const list = await listApp(platform);
while (true) {
const id = await question('输入应用 id:');
const app = list.find((v) => v.id === (id | 0));
if (app) {
return app;
}
}
}
export const commands = {
createApp: async function ({ options }) {
const name = options.name || (await question('应用名称:'));
const { downloadUrl } = options;
const platform = checkPlatform(
options.platform || (await question('平台(ios/android):')),
);
const { id } = await post('/app/create', { name, platform });
console.log(`已成功创建应用id: ${id}`);
await this.selectApp({
args: [id],
options: { platform, downloadUrl },
});
},
deleteApp: async function ({ args, options }) {
const { platform } = options;
const id = args[0] || chooseApp(platform);
if (!id) {
console.log('已取消');
}
await doDelete(`/app/${id}`);
console.log('操作成功');
},
apps: async function ({ options }) {
const { platform } = options;
listApp(platform);
},
selectApp: async function ({ args, options }) {
const platform = checkPlatform(
options.platform || (await question('平台(ios/android):')),
);
const id = args[0] ? parseInt(args[0]) : (await chooseApp(platform)).id;
let updateInfo = {};
if (fs.existsSync('update.json')) {
try {
updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
} catch (e) {
console.error(
'Failed to parse file `update.json`. Try to remove it manually.',
);
throw e;
}
}
const { appKey } = await get(`/app/${id}`);
updateInfo[platform] = {
appId: id,
appKey,
};
fs.writeFileSync(
'update.json',
JSON.stringify(updateInfo, null, 4),
'utf8',
);
},
};

138
src/app.ts Normal file
View File

@@ -0,0 +1,138 @@
import fs from 'fs';
import Table from 'tty-table';
import { question } from './utils';
import { doDelete, get, post } from './api';
import type { Platform } from './types';
import { t } from './utils/i18n';
const validPlatforms = ['ios', 'android', 'harmony'];
export async function getPlatform(platform?: string) {
return assertPlatform(
platform || (await question(t('platformQuestion'))),
) as Platform;
}
export function assertPlatform(platform: string) {
if (!validPlatforms.includes(platform)) {
throw new Error(t('unsupportedPlatform', { platform }));
}
return platform;
}
export function getSelectedApp(platform: Platform) {
assertPlatform(platform);
if (!fs.existsSync('update.json')) {
throw new Error(t('appNotSelected', { platform }));
}
const updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
if (!updateInfo[platform]) {
throw new Error(t('appNotSelected', { platform }));
}
return updateInfo[platform];
}
export async function listApp(platform: Platform | '' = '') {
const { data } = await get('/app/list');
const list = platform ? data.filter((v) => v.platform === platform) : data;
const header = [
{ value: t('appId') },
{ value: t('appName') },
{ value: t('platform') },
];
const rows = [];
for (const app of list) {
rows.push([app.id, app.name, app.platform]);
}
console.log(Table(header, rows).render());
console.log(`\n${t('totalApps', { count: list.length, platform })}`);
return list;
}
export async function chooseApp(platform: Platform) {
const list = await listApp(platform);
while (true) {
const id = await question(t('enterAppIdQuestion'));
const app = list.find((v) => v.id === Number(id));
if (app) {
return app;
}
}
}
export const appCommands = {
createApp: async function ({
options,
}: {
options: { name: string; downloadUrl: string; platform: Platform };
}) {
const name = options.name || (await question(t('appNameQuestion')));
const { downloadUrl } = options;
const platform = await getPlatform(options.platform);
const { id } = await post('/app/create', { name, platform, downloadUrl });
console.log(t('createAppSuccess', { id }));
await this.selectApp({
args: [id],
options: { platform },
});
},
deleteApp: async ({
args,
options,
}: {
args: string[];
options: { platform: Platform };
}) => {
const { platform } = options;
const id = args[0] || chooseApp(platform);
if (!id) {
console.log(t('cancelled'));
}
await doDelete(`/app/${id}`);
console.log(t('operationSuccess'));
},
apps: async ({ options }: { options: { platform: Platform } }) => {
const { platform } = options;
listApp(platform);
},
selectApp: async ({
args,
options,
}: {
args: string[];
options: { platform: Platform };
}) => {
const platform = await getPlatform(options.platform);
const id = args[0]
? Number.parseInt(args[0])
: (await chooseApp(platform)).id;
let updateInfo: Partial<
Record<Platform, { appId: number; appKey: string }>
> = {};
if (fs.existsSync('update.json')) {
try {
updateInfo = JSON.parse(fs.readFileSync('update.json', 'utf8'));
} catch (e) {
console.error(t('failedToParseUpdateJson'));
throw e;
}
}
const { appKey } = await get(`/app/${id}`);
updateInfo[platform] = {
appId: id,
appKey,
};
fs.writeFileSync(
'update.json',
JSON.stringify(updateInfo, null, 4),
'utf8',
);
},
};

View File

@@ -1,638 +0,0 @@
const path = require('path');
import { getRNVersion, translateOptions } from './utils';
import * as fs from 'fs-extra';
import { ZipFile } from 'yazl';
import { open as openZipFile } from 'yauzl';
import { question, printVersionCommand } from './utils';
import { checkPlatform } from './app';
const { spawn, spawnSync } = require('child_process');
const g2js = require('gradle-to-js/lib/parser');
const os = require('os');
var bsdiff, hdiff, diff;
try {
bsdiff = require('node-bsdiff').diff;
} catch (e) {}
try {
hdiff = require('node-hdiffpatch').diff;
} catch (e) {}
async function runReactNativeBundleCommand(
bundleName,
development,
entryFile,
outputFolder,
platform,
sourcemapOutput,
config,
) {
let gradleConfig = {};
if (platform === 'android') {
gradleConfig = await checkGradleConfig();
// if (gradleConfig.crunchPngs !== false) {
// throw new Error('请先禁用android的crunchPngs优化具体请参考 https://pushy.reactnative.cn/docs/getting-started.html#%E7%A6%81%E7%94%A8android%E7%9A%84crunch%E4%BC%98%E5%8C%96')
// }
}
let reactNativeBundleArgs = [];
let envArgs = process.env.PUSHY_ENV_ARGS;
if (envArgs) {
Array.prototype.push.apply(
reactNativeBundleArgs,
envArgs.trim().split(/\s+/),
);
}
fs.emptyDirSync(outputFolder);
Array.prototype.push.apply(reactNativeBundleArgs, [
path.join('node_modules', 'react-native', 'local-cli', 'cli.js'),
'bundle',
'--assets-dest',
outputFolder,
'--bundle-output',
path.join(outputFolder, bundleName),
'--dev',
development,
'--entry-file',
entryFile,
'--platform',
platform,
'--reset-cache',
]);
if (sourcemapOutput) {
reactNativeBundleArgs.push('--sourcemap-output', sourcemapOutput);
}
if (config) {
reactNativeBundleArgs.push('--config', config);
}
const reactNativeBundleProcess = spawn('node', reactNativeBundleArgs);
console.log(
`Running bundle command: node ${reactNativeBundleArgs.join(' ')}`,
);
return new Promise((resolve, reject) => {
reactNativeBundleProcess.stdout.on('data', (data) => {
console.log(data.toString().trim());
});
reactNativeBundleProcess.stderr.on('data', (data) => {
console.error(data.toString().trim());
});
reactNativeBundleProcess.on('close', async (exitCode) => {
if (exitCode) {
reject(
new Error(
`"react-native bundle" command exited with code ${exitCode}.`,
),
);
} else {
if (gradleConfig.enableHermes) {
await compileHermesByteCode(bundleName, outputFolder);
}
resolve(null);
}
});
});
}
function getHermesOSBin() {
if (os.platform() === 'win32') return 'win64-bin';
if (os.platform() === 'darwin') return 'osx-bin';
if (os.platform() === 'linux') return 'linux64-bin';
}
async function checkGradleConfig() {
let enableHermes = false;
let crunchPngs;
try {
const gradleConfig = await g2js.parseFile('android/app/build.gradle');
const projectConfig = gradleConfig['project.ext.react'];
for (const packagerConfig of projectConfig) {
if (
packagerConfig.includes('enableHermes') &&
packagerConfig.includes('true')
) {
enableHermes = true;
break;
}
}
crunchPngs = gradleConfig.android.buildTypes.release.crunchPngs;
} catch (e) {}
return {
enableHermes,
crunchPngs,
};
}
async function compileHermesByteCode(bundleName, outputFolder) {
console.log(`Hermes enabled, now compiling to hermes bytecode:\n`);
const hermesPackage = fs.existsSync('node_modules/hermes-engine')
? 'node_modules/hermes-engine' // 0.2+
: 'node_modules/hermesvm'; // < 0.2
const hermesPath = `${hermesPackage}/${getHermesOSBin()}`;
const hermesCommand = fs.existsSync(`${hermesPath}/hermesc`)
? `${hermesPath}/hermesc` // 0.5+
: `${hermesPath}/hermes`; // < 0.5
spawnSync(
path.join.apply(null, hermesCommand.split('/')),
[
'-emit-binary',
'-out',
path.join(outputFolder, bundleName),
path.join(outputFolder, bundleName),
'-O',
],
{ stdio: 'ignore' },
);
}
async function pack(dir, output) {
console.log('Packing');
fs.ensureDirSync(path.dirname(output));
await new Promise((resolve, reject) => {
var zipfile = new ZipFile();
function addDirectory(root, rel) {
if (rel) {
zipfile.addEmptyDirectory(rel);
}
const childs = fs.readdirSync(root);
for (const name of childs) {
if (name === '.' || name === '..') {
continue;
}
const fullPath = path.join(root, name);
const stat = fs.statSync(fullPath);
if (stat.isFile()) {
//console.log('adding: ' + rel+name);
zipfile.addFile(fullPath, rel + name);
} else if (stat.isDirectory()) {
//console.log('adding: ' + rel+name+'/');
addDirectory(fullPath, rel + name + '/');
}
}
}
addDirectory(dir, '');
zipfile.outputStream.on('error', (err) => reject(err));
zipfile.outputStream
.pipe(fs.createWriteStream(output))
.on('close', function () {
resolve();
});
zipfile.end();
});
console.log('ppk热更包已生成并保存到: ' + output);
}
function readEntire(entry, zipFile) {
const buffers = [];
return new Promise((resolve, reject) => {
zipFile.openReadStream(entry, (err, stream) => {
stream.pipe({
write(chunk) {
buffers.push(chunk);
},
end() {
resolve(Buffer.concat(buffers));
},
prependListener() {},
on() {},
once() {},
emit() {},
});
});
});
}
function basename(fn) {
const m = /^(.+\/)[^\/]+\/?$/.exec(fn);
return m && m[1];
}
async function diffFromPPK(origin, next, output) {
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
const originMap = {};
let originSource;
await enumZipEntries(origin, (entry, zipFile) => {
originEntries[entry.fileName] = entry;
if (!/\/$/.test(entry.fileName)) {
// isFile
originMap[entry.crc32] = entry.fileName;
if (entry.fileName === 'index.bundlejs') {
// This is source.
return readEntire(entry, zipFile).then((v) => (originSource = v));
}
}
});
if (!originSource) {
throw new Error(
`Bundle file not found! Please use default bundle file name and path.`,
);
}
const copies = {};
var zipfile = new ZipFile();
const writePromise = new Promise((resolve, reject) => {
zipfile.outputStream.on('error', (err) => {
throw err;
});
zipfile.outputStream
.pipe(fs.createWriteStream(output))
.on('close', function () {
resolve();
});
});
const addedEntry = {};
function addEntry(fn) {
//console.log(fn);
if (!fn || addedEntry[fn]) {
return;
}
const base = basename(fn);
if (base) {
addEntry(base);
}
zipfile.addEmptyDirectory(fn);
}
const newEntries = {};
await enumZipEntries(next, (entry, nextZipfile) => {
newEntries[entry.fileName] = entry;
if (/\/$/.test(entry.fileName)) {
// Directory
if (!originEntries[entry.fileName]) {
addEntry(entry.fileName);
}
} else if (entry.fileName === 'index.bundlejs') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then((newSource) => {
//console.log('Begin diff');
zipfile.addBuffer(
diff(originSource, newSource),
'index.bundlejs.patch',
);
//console.log('End diff');
});
} else {
// If same file.
const originEntry = originEntries[entry.fileName];
if (originEntry && originEntry.crc32 === entry.crc32) {
// ignore
return;
}
// If moved from other place
if (originMap[entry.crc32]) {
const base = basename(entry.fileName);
if (!originEntries[base]) {
addEntry(base);
}
copies[entry.fileName] = originMap[entry.crc32];
return;
}
// New file.
addEntry(basename(entry.fileName));
return new Promise((resolve, reject) => {
nextZipfile.openReadStream(entry, function (err, readStream) {
if (err) {
return reject(err);
}
zipfile.addReadStream(readStream, entry.fileName);
readStream.on('end', () => {
//console.log('add finished');
resolve();
});
});
});
}
});
const deletes = {};
for (var k in originEntries) {
if (!newEntries[k]) {
console.log('Delete ' + k);
deletes[k] = 1;
}
}
//console.log({copies, deletes});
zipfile.addBuffer(
Buffer.from(JSON.stringify({ copies, deletes })),
'__diff.json',
);
zipfile.end();
await writePromise;
}
async function diffFromPackage(
origin,
next,
output,
originBundleName,
transformPackagePath = (v) => v,
) {
fs.ensureDirSync(path.dirname(output));
const originEntries = {};
const originMap = {};
let originSource;
await enumZipEntries(origin, (entry, zipFile) => {
if (!/\/$/.test(entry.fileName)) {
const fn = transformPackagePath(entry.fileName);
if (!fn) {
return;
}
//console.log(fn);
// isFile
originEntries[fn] = entry.crc32;
originMap[entry.crc32] = fn;
if (fn === originBundleName) {
// This is source.
return readEntire(entry, zipFile).then((v) => (originSource = v));
}
}
});
if (!originSource) {
throw new Error(
`Bundle file not found! Please use default bundle file name and path.`,
);
}
const copies = {};
var zipfile = new ZipFile();
const writePromise = new Promise((resolve, reject) => {
zipfile.outputStream.on('error', (err) => {
throw err;
});
zipfile.outputStream
.pipe(fs.createWriteStream(output))
.on('close', function () {
resolve();
});
});
await enumZipEntries(next, (entry, nextZipfile) => {
if (/\/$/.test(entry.fileName)) {
// Directory
zipfile.addEmptyDirectory(entry.fileName);
} else if (entry.fileName === 'index.bundlejs') {
//console.log('Found bundle');
return readEntire(entry, nextZipfile).then((newSource) => {
//console.log('Begin diff');
zipfile.addBuffer(
diff(originSource, newSource),
'index.bundlejs.patch',
);
//console.log('End diff');
});
} else {
// If same file.
if (originEntries[entry.fileName] === entry.crc32) {
copies[entry.fileName] = '';
return;
}
// If moved from other place
if (originMap[entry.crc32]) {
copies[entry.fileName] = originMap[entry.crc32];
return;
}
return new Promise((resolve, reject) => {
nextZipfile.openReadStream(entry, function (err, readStream) {
if (err) {
return reject(err);
}
zipfile.addReadStream(readStream, entry.fileName);
readStream.on('end', () => {
//console.log('add finished');
resolve();
});
});
});
}
});
zipfile.addBuffer(Buffer.from(JSON.stringify({ copies })), '__diff.json');
zipfile.end();
await writePromise;
}
function enumZipEntries(zipFn, callback) {
return new Promise((resolve, reject) => {
openZipFile(zipFn, { lazyEntries: true }, (err, zipfile) => {
if (err) {
return reject(err);
}
zipfile.on('end', resolve);
zipfile.on('error', reject);
zipfile.on('entry', (entry) => {
const result = callback(entry, zipfile);
if (result && typeof result.then === 'function') {
result.then(() => zipfile.readEntry());
} else {
zipfile.readEntry();
}
});
zipfile.readEntry();
});
});
}
function diffArgsCheck(args, options, diffFn) {
const [origin, next] = args;
if (!origin || !next) {
console.error(`Usage: pushy ${diffFn} <origin> <next>`);
process.exit(1);
}
if (diffFn.startsWith('hdiff')) {
if (!hdiff) {
console.error(
`This function needs "node-hdiffpatch".
Please run "npm i node-hdiffpatch" to install`,
);
process.exit(1);
}
diff = hdiff;
} else {
if (!bsdiff) {
console.error(
`This function needs "node-bsdiff".
Please run "npm i node-bsdiff" to install`,
);
process.exit(1);
}
diff = bsdiff;
}
const { output } = options;
return {
origin,
next,
realOutput: output.replace(/\$\{time\}/g, '' + Date.now()),
};
}
export const commands = {
bundle: async function ({ options }) {
const platform = checkPlatform(
options.platform || (await question('平台(ios/android):')),
);
let {
bundleName,
entryFile,
intermediaDir,
output,
dev,
verbose,
} = translateOptions({
...options,
platform,
});
// const sourcemapOutput = path.join(intermediaDir, bundleName + ".map");
const realOutput = output.replace(/\$\{time\}/g, '' + Date.now());
if (!platform) {
throw new Error('Platform must be specified.');
}
const { version, major, minor } = getRNVersion();
console.log('Bundling with react-native: ', version);
printVersionCommand();
await runReactNativeBundleCommand(
bundleName,
dev,
entryFile,
intermediaDir,
platform,
);
await pack(path.resolve(intermediaDir), realOutput);
const v = await question('是否现在上传此热更包?(Y/N)');
if (v.toLowerCase() === 'y') {
await this.publish({
args: [realOutput],
options: {
platform,
},
});
}
},
async diff({ args, options }) {
const { origin, next, realOutput } = diffArgsCheck(args, options, 'diff');
await diffFromPPK(origin, next, realOutput, 'index.bundlejs');
console.log(`${realOutput} generated.`);
},
async hdiff({ args, options }) {
const { origin, next, realOutput } = diffArgsCheck(args, options, 'hdiff');
await diffFromPPK(origin, next, realOutput, 'index.bundlejs');
console.log(`${realOutput} generated.`);
},
async diffFromApk({ args, options }) {
const { origin, next, realOutput } = diffArgsCheck(
args,
options,
'diffFromApk',
);
await diffFromPackage(
origin,
next,
realOutput,
'assets/index.android.bundle',
);
console.log(`${realOutput} generated.`);
},
async hdiffFromApk({ args, options }) {
const { origin, next, realOutput } = diffArgsCheck(
args,
options,
'hdiffFromApk',
);
await diffFromPackage(
origin,
next,
realOutput,
'assets/index.android.bundle',
);
console.log(`${realOutput} generated.`);
},
async diffFromIpa({ args, options }) {
const { origin, next, realOutput } = diffArgsCheck(
args,
options,
'diffFromIpa',
);
await diffFromPackage(origin, next, realOutput, 'main.jsbundle', (v) => {
const m = /^Payload\/[^/]+\/(.+)$/.exec(v);
return m && m[1];
});
console.log(`${realOutput} generated.`);
},
async hdiffFromIpa({ args, options }) {
const { origin, next, realOutput } = diffArgsCheck(
args,
options,
'hdiffFromIpa',
);
await diffFromPackage(origin, next, realOutput, 'main.jsbundle', (v) => {
const m = /^Payload\/[^/]+\/(.+)$/.exec(v);
return m && m[1];
});
console.log(`${realOutput} generated.`);
},
};

1144
src/bundle.ts Normal file

File diff suppressed because it is too large Load Diff

30
src/exports.ts Normal file
View File

@@ -0,0 +1,30 @@
export { moduleManager } from './module-manager';
export { CLIProviderImpl } from './provider';
export type {
CLIProvider,
CLIModule,
CommandDefinition,
CustomWorkflow,
WorkflowStep,
CommandContext,
CommandResult,
BundleOptions,
PublishOptions,
UploadOptions,
Platform,
Session,
Version,
Package,
} from './types';
export { builtinModules } from './modules';
export { bundleModule } from './modules/bundle-module';
export { versionModule } from './modules/version-module';
export { appModule } from './modules/app-module';
export { userModule } from './modules/user-module';
export { packageModule } from './modules/package-module';
export { loadSession, getSession } from './api';
export { getPlatform, getSelectedApp } from './app';
export { question, saveToLocal } from './utils';

View File

@@ -1,52 +0,0 @@
#!/usr/bin/env node
const { loadSession } = require('./api');
const updateNotifier = require('update-notifier');
import { printVersionCommand } from './utils/index.js';
const pkg = require('../package.json');
updateNotifier({ pkg }).notify({ isGlobal: true });
function printUsage({ args }) {
// const commandName = args[0];
// TODO: print usage of commandName, or print global usage.
console.log('Usage is under development now.');
console.log(
'Visit `https://github.com/reactnativecn/react-native-pushy` for early document.',
);
process.exit(1);
}
const commands = {
...require('./user').commands,
...require('./bundle').commands,
...require('./app').commands,
...require('./package').commands,
...require('./versions').commands,
help: printUsage,
};
function run() {
if (process.argv.indexOf('-v') >= 0 || process.argv[2] === 'version') {
printVersionCommand();
process.exit();
}
const argv = require('cli-arguments').parse(require('../cli.json'));
global.NO_INTERACTIVE = argv.options['no-interactive'];
global.USE_ACC_OSS = argv.options['acc'];
loadSession()
.then(() => commands[argv.command](argv))
.catch((err) => {
if (err.status === 401) {
console.log('尚未登录。\n请在项目目录中运行`pushy login`命令来登录');
return;
}
console.error(err.stack);
process.exit(-1);
});
}
run();

155
src/index.ts Normal file
View File

@@ -0,0 +1,155 @@
#!/usr/bin/env node
import { loadSession } from './api';
import { appCommands } from './app';
import { bundleCommands } from './bundle';
import { moduleManager } from './module-manager';
import { builtinModules } from './modules';
import { packageCommands } from './package';
import type { CommandContext } from './types';
import { userCommands } from './user';
import { printVersionCommand } from './utils';
import { t } from './utils/i18n';
import { versionCommands } from './versions';
function registerBuiltinModules() {
for (const module of builtinModules) {
try {
moduleManager.registerModule(module);
} catch (error) {
console.error(`Failed to register module ${module.name}:`, error);
}
}
}
function printUsage() {
console.log('React Native Update CLI');
console.log('');
console.log('Traditional commands:');
const legacyCommands = {
...userCommands,
...bundleCommands,
...appCommands,
...packageCommands,
...versionCommands,
};
for (const [name, handler] of Object.entries(legacyCommands)) {
console.log(` ${name}: Legacy command`);
}
console.log('');
console.log('Modular commands:');
const commands = moduleManager.getRegisteredCommands();
for (const command of commands) {
console.log(
` ${command.name}: ${command.description || 'No description'}`,
);
}
console.log('');
console.log('Available workflows:');
const workflows = moduleManager.getRegisteredWorkflows();
for (const workflow of workflows) {
console.log(
` ${workflow.name}: ${workflow.description || 'No description'}`,
);
}
console.log('');
console.log('Special commands:');
console.log(' list: List all available commands and workflows');
console.log(' workflow <name>: Execute a specific workflow');
console.log(' help: Show this help message');
console.log('');
console.log(
'Visit `https://github.com/reactnativecn/react-native-update` for document.',
);
process.exit(1);
}
const legacyCommands = {
...userCommands,
...bundleCommands,
...appCommands,
...packageCommands,
...versionCommands,
help: printUsage,
};
async function run() {
await printVersionCommand();
if (process.argv.indexOf('-v') >= 0 || process.argv[2] === 'version') {
process.exit();
}
// Register builtin modules for modular functionality
registerBuiltinModules();
const argv = require('cli-arguments').parse(require('../cli.json'));
global.NO_INTERACTIVE = argv.options['no-interactive'];
global.USE_ACC_OSS = argv.options.acc;
const context: CommandContext = {
args: argv.args || [],
options: argv.options || {},
};
try {
await loadSession();
context.session = require('./api').getSession();
// Handle special modular commands first
if (argv.command === 'help') {
printUsage();
} else if (argv.command === 'list') {
moduleManager.listAll();
} else if (argv.command === 'workflow') {
const workflowName = argv.args[0];
if (!workflowName) {
console.error('Workflow name is required');
process.exit(1);
}
const result = await moduleManager.executeWorkflow(workflowName, context);
if (!result.success) {
console.error('Workflow execution failed:', result.error);
process.exit(1);
}
console.log('Workflow completed successfully:', result.data);
}
// Try legacy commands first for backward compatibility
else if (legacyCommands[argv.command]) {
await legacyCommands[argv.command](argv);
}
// Fall back to modular commands
else {
const result = await moduleManager.executeCommand(argv.command, context);
if (!result.success) {
console.error('Command execution failed:', result.error);
process.exit(1);
}
console.log('Command completed successfully:', result.data);
}
} catch (err: any) {
if (err.status === 401) {
console.log(t('loginFirst'));
return;
}
console.error(err.stack);
process.exit(-1);
}
}
export { moduleManager };
export { CLIProviderImpl } from './provider';
export type {
CLIProvider,
CLIModule,
CommandDefinition,
CustomWorkflow,
WorkflowStep,
} from './types';
run();

140
src/locales/en.ts Normal file
View File

@@ -0,0 +1,140 @@
export default {
addedToGitignore: 'Added {{line}} to .gitignore',
androidCrunchPngsWarning:
'The crunchPngs option of android seems not disabled (Please ignore this warning if already disabled), which may cause abnormal consumption of mobile network traffic. Please refer to https://cresc.dev/docs/getting-started#disable-crunchpngs-on-android \n',
appId: 'App ID',
appIdMismatchApk:
'App ID mismatch! Current APK: {{appIdInPkg}}, current update.json: {{appId}}',
appIdMismatchApp:
'App ID mismatch! Current APP: {{appIdInPkg}}, current update.json: {{appId}}',
appIdMismatchIpa:
'App ID mismatch! Current IPA: {{appIdInPkg}}, current update.json: {{appId}}',
appKeyMismatchApk:
'App Key mismatch! Current APK: {{appKeyInPkg}}, current update.json: {{appKey}}',
appKeyMismatchApp:
'App Key mismatch! Current APP: {{appKeyInPkg}}, current update.json: {{appKey}}',
appKeyMismatchIpa:
'App Key mismatch! Current IPA: {{appKeyInPkg}}, current update.json: {{appKey}}',
appName: 'App Name',
appNameQuestion: 'App Name:',
appNotSelected:
'App not selected. run `cresc selectApp --platform {{platform}}` first!',
appUploadSuccess:
'Successfully uploaded APP native package (id: {{id}}, version: {{version}}, buildTime: {{buildTime}})',
apkUploadSuccess:
'Successfully uploaded APK native package (id: {{id}}, version: {{version}}, buildTime: {{buildTime}})',
boundTo: ', bound to: {{name}} ({{id}})',
buildTimeNotFound:
'Cannot get the build timestamp of this package. Please update `react-native-update` to the latest version and re-package and upload.',
bundleCommandError:
'"react-native bundle" command exited with code {{code}}.',
bundleNotFound:
'Bundle file not found. Please ensure that this {{packageType}} is a release version and the bundle file name is the default `{{entryFile}}`',
bundlingWithRN: 'Bundling with react-native: {{version}}',
cancelled: 'Cancelled',
composingSourceMap: 'Composing source map',
copyFileFailed: 'Failed to copy file: {{error}}',
copyHarmonyBundleError: 'Error copying Harmony bundle: {{error}}',
copyingDebugId: 'Copying debugid',
createAppSuccess: 'App created successfully (id: {{id}})',
deleteFile: 'Delete {{- file}}',
deletingFile: 'Delete {{- file}}',
enterAppIdQuestion: 'Enter AppId:',
enterNativePackageId: 'Enter native package ID:',
errorInHarmonyApp: 'Error in getEntryFromHarmonyApp: {{error}}',
expiredStatus: '(Expired)',
failedToParseIcon: '[Warning] failed to parse icon: {{error}}',
failedToParseUpdateJson:
'Failed to parse file `update.json`. Try to remove it manually.',
fileGenerated: '{{- file}} generated.',
fileSizeExceeded:
'This file size is {{fileSize}} , exceeding the current quota {{maxSize}} . You may consider upgrading to a higher plan to increase this quota. Details can be found at: {{- pricingPageUrl}}',
hermesDisabled: 'Hermes disabled',
hermesEnabledCompiling: 'Hermes enabled, now compiling to hermes bytecode:\n',
ipaUploadSuccess:
'Successfully uploaded IPA native package (id: {{id}}, version: {{version}}, buildTime: {{buildTime}})',
keyStrings: 'Key strings:',
latestVersionTag: '(latest: {{version}})',
lockBestPractice: `
Best practices for lock files:
1. All members of the development team should use the same package manager to maintain a single lock file.
2. Add the lock file to version control (but do not commit multiple lock files of different formats).
3. Pay attention to changes in the lock file during code review.
This can reduce the risk of inconsistent dependencies and supply chain attacks.
`,
lockNotFound:
'No lock file detected, which may cause inconsistent dependencies and hot-updating issues.',
loggedOut: 'Logged out',
loginExpired:
'Login information has expired. Please use `cresc login` command to re-login',
loginFirst:
'Not logged in.\nPlease run `cresc login` in the project directory to login.',
multipleLocksFound:
'Multiple lock files detected ({{- lockFiles}}), which may cause inconsistent dependencies and hot-updating issues.',
nativePackageId: 'Native Package ID',
nativeVersion: 'Native Version',
nativeVersionNotFoundGte: 'No native version found >= {{version}}',
nativeVersionNotFoundLte: 'No native version found <= {{version}}',
nativeVersionNotFoundMatch: 'No matching native version found: {{version}}',
nativePackageIdNotFound: 'No native package id found: {{id}}',
noPackagesFound: 'No packages found. (appId: {{appId}})',
offset: 'Offset {{offset}}',
operationComplete: 'Operation complete, bound to {{count}} native versions',
operationSuccess: 'Operation successful',
packageIdRequired: 'Please provide packageId or packageVersion parameter',
packageUploadSuccess:
'Successfully uploaded new hot update package (id: {{id}})',
packing: 'Packing',
pausedStatus: '(Paused)',
platform: 'Platform',
platformPrompt: 'Platform (ios/android/harmony):',
platformQuestion: 'Platform(ios/android/harmony):',
platformRequired: 'Platform must be specified.',
pluginDetectionError: 'error while detecting {{name}} plugin: {{error}}',
pluginDetected: 'detected {{name}} plugin',
ppkPackageGenerated: 'ppk package generated and saved to: {{- output}}',
processingError: 'Error processing file: {{error}}',
processingPackage: 'Processing the package {{count}} ...',
processingStringPool: 'Processing the string pool ...',
publishUsage:
'Usage: pushy publish <ppk file> --platform ios|android|harmony',
rnuVersionNotFound:
'react-native-update: Cannot get the version number. Please run the command in the project directory',
rolloutConfigSet:
'Set {{rollout}}% rollout for OTA update {{version}} on native version(s) {{versions}}',
rolloutRangeError: 'rollout must be an integer between 1-100',
runningHermesc: 'Running hermesc: {{- command}} {{- args}}',
sentryCliNotFound:
'Cannot find Sentry CLI tool, please make sure @sentry/cli is properly installed',
sentryReleaseCreated: 'Sentry release created for version: {{version}}',
totalApps: 'Total {{count}} {{platform}} apps',
totalPackages: 'Total {{count}} packages',
typeStrings: 'Type strings:',
unsupportedPlatform: 'Unsupported platform `{{platform}}`',
uploadBundlePrompt: 'Upload this bundle now?(Y/N)',
uploadingSourcemap: 'Uploading sourcemap',
usageDiff: 'Usage: cresc {{command}} <origin> <next>',
usageParseApk: 'Usage: cresc parseApk <apk file>',
usageParseApp: 'Usage: cresc parseApp <app file>',
usageParseIpa: 'Usage: cresc parseIpa <ipa file>',
usageUnderDevelopment: 'Usage is under development now.',
usageUploadApk: 'Usage: cresc uploadApk <apk file>',
usageUploadApp: 'Usage: cresc uploadApp <app file>',
usageUploadIpa: 'Usage: cresc uploadIpa <ipa file>',
versionBind:
'Bound hot update {{version}} to native version {{nativeVersion}} (id: {{id}})',
welcomeMessage: 'Welcome to Cresc hot update service, {{name}}.',
versionNameQuestion: 'Enter OTA update name:',
versionDescriptionQuestion: 'Enter OTA update description:',
versionMetaInfoQuestion: 'Enter custom meta info:',
updateNativePackageQuestion: 'Bind to native package now?(Y/N)',
unnamed: '(Unnamed)',
dryRun: 'Below is the dry-run result, no actual operation will be performed:',
usingCustomVersion: 'Using custom version: {{version}}',
confirmDeletePackage:
'Confirm delete native package {{packageId}}? This operation cannot be undone (Y/N):',
deletePackageSuccess: 'Native package {{packageId}} deleted successfully',
deletePackageError:
'Failed to delete native package {{packageId}}: {{error}}',
usageDeletePackage: 'Usage: cresc deletePackage [packageId] --appId [appId]',
};

132
src/locales/zh.ts Normal file
View File

@@ -0,0 +1,132 @@
export default {
addedToGitignore: '已将 {{line}} 添加到 .gitignore',
androidCrunchPngsWarning:
'android 的 crunchPngs 选项似乎尚未禁用(如已禁用则请忽略此提示),这可能导致热更包体积异常增大,具体请参考 https://pushy.reactnative.cn/docs/getting-started.html#%E7%A6%81%E7%94%A8-android-%E7%9A%84-crunch-%E4%BC%98%E5%8C%96 \n',
appId: '应用 id',
appIdMismatchApk:
'appId不匹配当前apk: {{appIdInPkg}}, 当前update.json: {{appId}}',
appIdMismatchApp:
'appId不匹配当前app: {{appIdInPkg}}, 当前update.json: {{appId}}',
appIdMismatchIpa:
'appId不匹配当前ipa: {{appIdInPkg}}, 当前update.json: {{appId}}',
appKeyMismatchApk:
'appKey不匹配当前apk: {{appKeyInPkg}}, 当前update.json: {{appKey}}',
appKeyMismatchApp:
'appKey不匹配当前app: {{appKeyInPkg}}, 当前update.json: {{appKey}}',
appKeyMismatchIpa:
'appKey不匹配当前ipa: {{appKeyInPkg}}, 当前update.json: {{appKey}}',
appName: '应用名称',
appNameQuestion: '应用名称:',
appNotSelected:
'尚未选择应用。请先运行 `pushy selectApp --platform {{platform}}` 来选择应用',
appUploadSuccess:
'已成功上传app原生包id: {{id}}, version: {{version}}, buildTime: {{buildTime}}',
apkUploadSuccess:
'已成功上传apk原生包id: {{id}}, version: {{version}}, buildTime: {{buildTime}}',
boundTo: ', 已绑定:{{name}} ({{id}})',
buildTimeNotFound:
'无法获取此包的编译时间戳。请更新 `react-native-update` 到最新版本后重新打包上传。',
bundleCommandError: '"react-native bundle" 命令退出,代码为 {{code}}。',
bundleNotFound:
'找不到 bundle 文件。请确保此 {{packageType}} 为 release 版本,且 bundle 文件名为默认的 `{{entryFile}}`',
bundlingWithRN: '正在使用 react-native {{version}} 打包',
cancelled: '已取消',
composingSourceMap: '正在生成 source map',
copyFileFailed: '复制文件失败:{{error}}',
copyHarmonyBundleError: '复制 Harmony bundle 错误:{{error}}',
copyingDebugId: '正在复制 debugid',
createAppSuccess: '已成功创建应用id: {{id}}',
deleteFile: '删除 {{- file}}',
deletingFile: '删除 {{- file}}',
enterAppIdQuestion: '输入应用 id:',
enterNativePackageId: '输入原生包 id:',
errorInHarmonyApp: '获取 Harmony 应用入口时出错:{{error}}',
expiredStatus: '(已过期)',
failedToParseIcon: '[警告] 解析图标失败:{{error}}',
failedToParseUpdateJson: '无法解析文件 `update.json`。请手动删除它。',
fileGenerated: '已生成 {{- file}}',
fileSizeExceeded:
'此文件大小 {{fileSize}} , 超出当前额度 {{maxSize}} 。您可以考虑升级付费业务以提升此额度。详情请访问: {{- pricingPageUrl}}',
hermesDisabled: 'Hermes 已禁用',
hermesEnabledCompiling: 'Hermes 已启用,正在编译为 hermes 字节码:\n',
ipaUploadSuccess:
'已成功上传ipa原生包id: {{id}}, version: {{version}}, buildTime: {{buildTime}}',
keyStrings: '键字符串:',
latestVersionTag: '(最新:{{version}}',
lockBestPractice: `
关于 lock 文件的最佳实践:
1. 开发团队中的所有成员应该使用相同的包管理器,维护同一份 lock 文件。
2. 将 lock 文件添加到版本控制中(但不要同时提交多种不同格式的 lock 文件)。
3. 代码审核时应关注 lock 文件的变化。
这样可以最大限度避免因依赖关系不一致而导致的热更异常,也降低供应链攻击等安全隐患。
`,
lockNotFound:
'没有检测到任何 lock 文件,这可能导致依赖关系不一致而使热更异常。',
loggedOut: '已退出登录',
loginExpired: '登录信息已过期,请使用 `pushy login` 命令重新登录',
loginFirst: '尚未登录。\n请在项目目录中运行`pushy login`命令来登录',
multipleLocksFound:
'检测到多种不同格式的锁文件({{- lockFiles}}),这可能导致依赖关系不一致而使热更异常。',
nativePackageId: '原生包 Id',
nativeVersion: '原生版本',
nativeVersionNotFoundGte: '未查询到 >= {{version}} 的原生版本',
nativeVersionNotFoundLte: '未查询到 <= {{version}} 的原生版本',
nativeVersionNotFoundMatch: '未查询到匹配原生版本:{{version}}',
nativePackageIdNotFound: '未查询到原生包 id: {{id}}',
noPackagesFound: '未查询到任何原生包appId: {{appId}}',
offset: '偏移量 {{offset}}',
operationComplete: '操作完成,共已绑定 {{count}} 个原生版本',
operationSuccess: '操作成功',
packageIdRequired: '请提供 packageId 或 packageVersion 参数',
packageUploadSuccess: '已成功上传新热更包id: {{id}}',
packing: '正在打包',
pausedStatus: '(已暂停)',
platform: '平台',
platformPrompt: '平台(ios/android/harmony):',
platformQuestion: '平台(ios/android/harmony):',
platformRequired: '必须指定平台。',
pluginDetectionError: '检测 {{name}} 插件时出错:{{error}}',
pluginDetected: '检测到 {{name}} 插件',
ppkPackageGenerated: 'ppk 热更包已生成并保存到: {{- output}}',
processingError: '处理文件时出错:{{error}}',
processingPackage: '正在处理包 {{count}}...',
processingStringPool: '正在处理字符串池...',
publishUsage:
'使用方法: pushy publish ppk后缀文件 --platform ios|android|harmony',
rnuVersionNotFound:
'react-native-update: 无法获取版本号。请在项目目录中运行命令',
rolloutConfigSet:
'已在原生版本 {{versions}} 上设置灰度发布 {{rollout}}% 热更包 {{version}}',
rolloutRangeError: 'rollout 必须是 1-100 的整数',
runningHermesc: '运行 hermesc{{- command}} {{- args}}',
sentryCliNotFound: '无法找到 Sentry CLI 工具,请确保已正确安装 @sentry/cli',
sentryReleaseCreated: '已为版本 {{version}} 创建 Sentry release',
totalApps: '共 {{count}} 个 {{platform}} 应用',
totalPackages: '共 {{count}} 个包',
typeStrings: '类型字符串:',
unsupportedPlatform: '无法识别的平台 `{{platform}}`',
uploadBundlePrompt: '是否现在上传此热更包?(Y/N)',
uploadingSourcemap: '正在上传 sourcemap',
usageDiff: '用法pushy {{command}} <origin> <next>',
usageParseApk: '使用方法: pushy parseApk apk后缀文件',
usageParseApp: '使用方法: pushy parseApp app后缀文件',
usageParseIpa: '使用方法: pushy parseIpa ipa后缀文件',
usageUploadApk: '使用方法: pushy uploadApk apk后缀文件',
usageUploadApp: '使用方法: pushy uploadApp app后缀文件',
usageUploadIpa: '使用方法: pushy uploadIpa ipa后缀文件',
versionBind:
'已将热更包 {{version}} 绑定到原生版本 {{nativeVersion}} (id: {{id}})',
welcomeMessage: '欢迎使用 pushy 热更新服务,{{name}}。',
versionNameQuestion: '输入版本名称:',
versionDescriptionQuestion: '输入版本描述:',
versionMetaInfoQuestion: '输入自定义的 meta info:',
updateNativePackageQuestion: '是否现在将此热更应用到原生包上?(Y/N)',
unnamed: '(未命名)',
dryRun: '以下是 dry-run 模拟运行结果,不会实际执行任何操作:',
usingCustomVersion: '使用自定义版本:{{version}}',
confirmDeletePackage: '确认删除原生包 {{packageId}}? 此操作不可撤销 (Y/N):',
deletePackageSuccess: '原生包 {{packageId}} 删除成功',
deletePackageError: '删除原生包 {{packageId}} 失败: {{error}}',
usageDeletePackage:
'使用方法: pushy deletePackage [packageId] --appId [appId]',
};

149
src/module-manager.ts Normal file
View File

@@ -0,0 +1,149 @@
import { CLIProviderImpl } from './provider';
import type {
CLIModule,
CLIProvider,
CommandDefinition,
CustomWorkflow,
} from './types';
export class ModuleManager {
private modules: Map<string, CLIModule> = new Map();
private provider: CLIProvider;
private commands: Map<string, CommandDefinition> = new Map();
private workflows: Map<string, CustomWorkflow> = new Map();
constructor() {
this.provider = new CLIProviderImpl();
}
registerModule(module: CLIModule): void {
if (this.modules.has(module.name)) {
throw new Error(`Module '${module.name}' is already registered`);
}
this.modules.set(module.name, module);
if (module.commands) {
for (const command of module.commands) {
this.registerCommand(command);
}
}
if (module.workflows) {
for (const workflow of module.workflows) {
this.registerWorkflow(workflow);
}
}
if (module.init) {
module.init(this.provider);
}
// console.log(
// `Module '${module.name}' (v${module.version}) registered successfully`,
// );
}
unregisterModule(moduleName: string): void {
const module = this.modules.get(moduleName);
if (!module) {
throw new Error(`Module '${moduleName}' is not registered`);
}
if (module.commands) {
for (const command of module.commands) {
this.commands.delete(command.name);
}
}
if (module.workflows) {
for (const workflow of module.workflows) {
this.workflows.delete(workflow.name);
}
}
if (module.cleanup) {
module.cleanup();
}
this.modules.delete(moduleName);
console.log(`Module '${moduleName}' unregistered successfully`);
}
registerCommand(command: CommandDefinition): void {
if (this.commands.has(command.name)) {
throw new Error(`Command '${command.name}' is already registered`);
}
this.commands.set(command.name, command);
}
registerWorkflow(workflow: CustomWorkflow): void {
if (this.workflows.has(workflow.name)) {
throw new Error(`Workflow '${workflow.name}' is already registered`);
}
this.workflows.set(workflow.name, workflow);
this.provider.registerWorkflow(workflow);
}
getRegisteredCommands(): CommandDefinition[] {
return Array.from(this.commands.values());
}
getRegisteredWorkflows(): CustomWorkflow[] {
return Array.from(this.workflows.values());
}
getRegisteredModules(): CLIModule[] {
return Array.from(this.modules.values());
}
async executeCommand(commandName: string, context: any): Promise<any> {
const command = this.commands.get(commandName);
if (!command) {
throw new Error(`Command '${commandName}' not found`);
}
return await command.handler(context);
}
async executeWorkflow(workflowName: string, context: any): Promise<any> {
return await this.provider.executeWorkflow(workflowName, context);
}
getProvider(): CLIProvider {
return this.provider;
}
listCommands(): any[] {
return Array.from(this.commands.values());
}
listWorkflows(): CustomWorkflow[] {
return Array.from(this.workflows.values());
}
listAll(): void {
console.log('\n=== Registered Commands ===');
for (const command of this.commands.values()) {
console.log(
` ${command.name}: ${command.description || 'No description'}`,
);
}
console.log('\n=== Registered Workflows ===');
for (const workflow of this.workflows.values()) {
console.log(
` ${workflow.name}: ${workflow.description || 'No description'}`,
);
}
console.log('\n=== Registered Modules ===');
for (const module of this.modules.values()) {
console.log(
` ${module.name} (v${module.version}): ${module.commands?.length || 0} commands, ${module.workflows?.length || 0} workflows`,
);
}
}
}
export const moduleManager = new ModuleManager();

205
src/modules/app-module.ts Normal file
View File

@@ -0,0 +1,205 @@
import { appCommands } from '../app';
import type { CLIModule, CommandContext } from '../types';
export const appModule: CLIModule = {
name: 'app',
version: '1.0.0',
commands: [],
workflows: [
{
name: 'setup-app',
description: 'Setup a new app with initial configuration',
steps: [
{
name: 'create',
description: 'Create the app',
execute: async (context: CommandContext) => {
console.log('Creating app in workflow');
const { name, downloadUrl, platform } = context.options;
await appCommands.createApp({
options: {
name: name || '',
downloadUrl: downloadUrl || '',
platform: platform || '',
},
});
return { appCreated: true };
},
},
{
name: 'select',
description: 'Select the created app',
execute: async (context: CommandContext, previousResult: any) => {
console.log('Selecting app in workflow');
const { platform } = context.options;
await appCommands.selectApp({
args: [],
options: { platform: platform || '' },
});
return { ...previousResult, appSelected: true };
},
},
],
},
{
name: 'manage-apps',
description: 'Manage multiple apps',
steps: [
{
name: 'list-apps',
description: 'List all apps',
execute: async (context: CommandContext) => {
console.log('Listing all apps');
const { platform } = context.options;
await appCommands.apps({
options: { platform: platform || '' },
});
return { appsListed: true };
},
},
{
name: 'select-target-app',
description: 'Select target app for operations',
execute: async (context: CommandContext, previousResult: any) => {
console.log('Selecting target app');
const { platform } = context.options;
await appCommands.selectApp({
args: [],
options: { platform: platform || '' },
});
return { ...previousResult, targetAppSelected: true };
},
},
],
},
{
name: 'multi-platform-app-management',
description: 'Multi-platform app unified management workflow',
steps: [
{
name: 'scan-platforms',
description: 'Scan apps on all platforms',
execute: async (context: CommandContext) => {
console.log('🔍 Scanning apps on all platforms...');
const platforms = ['ios', 'android', 'harmony'];
const appsData = {};
for (const platform of platforms) {
console.log(` Scanning ${platform} platform...`);
// Simulate getting app list
await new Promise((resolve) => setTimeout(resolve, 500));
const appCount = Math.floor(Math.random() * 5) + 1;
const apps = Array.from({ length: appCount }, (_, i) => ({
id: `${platform}_app_${i + 1}`,
name: `App ${i + 1}`,
platform,
version: `1.${i}.0`,
status: Math.random() > 0.2 ? 'active' : 'inactive',
}));
appsData[platform] = apps;
console.log(` ✅ Found ${appCount} apps`);
}
console.log('✅ Platform scanning completed');
return { platforms, appsData, scanned: true };
},
},
{
name: 'analyze-apps',
description: 'Analyze app status',
execute: async (context: CommandContext, previousResult: any) => {
console.log('📊 Analyzing app status...');
const { appsData } = previousResult;
const analysis = {
totalApps: 0,
activeApps: 0,
inactiveApps: 0,
platformDistribution: {},
issues: [],
};
for (const [platform, apps] of Object.entries(appsData)) {
const platformApps = apps as any[];
analysis.totalApps += platformApps.length;
analysis.platformDistribution[platform] = platformApps.length;
for (const app of platformApps) {
if (app.status === 'active') {
analysis.activeApps++;
} else {
analysis.inactiveApps++;
analysis.issues.push(
`${platform}/${app.name}: App is inactive`,
);
}
}
}
console.log('📈 Analysis results:');
console.log(` Total apps: ${analysis.totalApps}`);
console.log(` Active apps: ${analysis.activeApps}`);
console.log(` Inactive apps: ${analysis.inactiveApps}`);
if (analysis.issues.length > 0) {
console.log('⚠️ Issues found:');
analysis.issues.forEach((issue) => console.log(` - ${issue}`));
}
return { ...previousResult, analysis };
},
},
{
name: 'optimize-apps',
description: 'Optimize app configuration',
execute: async (context: CommandContext, previousResult: any) => {
console.log('⚡ Optimizing app configuration...');
const { analysis } = previousResult;
const optimizations = [];
if (analysis.inactiveApps > 0) {
console.log(' Handling inactive apps...');
optimizations.push('Reactivate inactive apps');
}
if (analysis.totalApps > 10) {
console.log(' Many apps detected, suggest grouping...');
optimizations.push('Create app groups');
}
// Simulate optimization process
for (const optimization of optimizations) {
console.log(` Executing: ${optimization}...`);
await new Promise((resolve) => setTimeout(resolve, 800));
console.log(`${optimization} completed`);
}
console.log('✅ App optimization completed');
return { ...previousResult, optimizations, optimized: true };
},
},
],
options: {
includeInactive: {
hasValue: false,
default: true,
description: 'Include inactive apps',
},
autoOptimize: {
hasValue: false,
default: true,
description: 'Auto optimize configuration',
},
},
},
],
};

View File

@@ -0,0 +1,202 @@
import { bundleCommands } from '../bundle';
import type { CLIModule, CommandContext } from '../types';
export const bundleModule: CLIModule = {
name: 'bundle',
version: '1.0.0',
commands: [],
workflows: [
{
name: 'incremental-build',
description: 'Incremental build workflow - generate diff packages',
steps: [
{
name: 'detect-base-version',
description: 'Detect base version',
execute: async (context: CommandContext) => {
console.log('🔍 Detecting base version...');
const { baseVersion, platform } = context.options;
if (baseVersion) {
console.log(`✅ Using specified base version: ${baseVersion}`);
return { baseVersion, specified: true };
}
console.log('Auto detecting latest version...');
await new Promise((resolve) => setTimeout(resolve, 800));
const autoDetectedVersion = `v${Math.floor(Math.random() * 3) + 1}.${Math.floor(Math.random() * 10)}.${Math.floor(Math.random() * 10)}`;
console.log(
`✅ Auto detected base version: ${autoDetectedVersion}`,
);
return { baseVersion: autoDetectedVersion, specified: false };
},
},
{
name: 'build-current-version',
description: 'Build current version',
execute: async (context: CommandContext, previousResult: any) => {
console.log('🏗️ Building current version...');
const {
platform,
dev = false,
sourcemap = false,
bundleName = 'index.bundlejs',
entryFile = 'index.js',
intermediaDir,
taro = false,
expo = false,
rncli = false,
disableHermes = false,
output,
} = context.options;
console.log(`Building ${platform} platform...`);
console.log(` Entry file: ${entryFile}`);
console.log(` Bundle name: ${bundleName}`);
console.log(` Development mode: ${dev}`);
console.log(` Source maps: ${sourcemap}`);
try {
const buildOptions: any = {
platform,
dev,
sourcemap,
bundleName,
entryFile,
taro,
expo,
rncli,
disableHermes,
intermediaDir: '${tempDir}/intermedia/${platform}',
output: '${tempDir}/output/${platform}.${time}.ppk',
};
if (intermediaDir) {
buildOptions.intermediaDir = intermediaDir;
}
await bundleCommands.bundle({
args: [],
options: buildOptions,
});
const currentBuild = {
version: `v${Math.floor(Math.random() * 3) + 2}.0.0`,
platform,
bundlePath: `./build/current_${platform}.ppk`,
size: Math.floor(Math.random() * 15) + 10,
buildTime: Date.now(),
};
console.log(
`✅ Current version build completed: ${currentBuild.version}`,
);
return { ...previousResult, currentBuild };
} catch (error) {
console.error('❌ Current version build failed:', error);
throw error;
}
},
},
],
validate: (context: CommandContext) => {
if (!context.options.platform) {
console.error('❌ Incremental build requires platform specification');
return false;
}
return true;
},
options: {
platform: {
hasValue: true,
description: 'Target platform (required)',
},
baseVersion: {
hasValue: true,
description: 'Base version (auto detect if not specified)',
},
skipValidation: {
hasValue: false,
default: false,
description: 'Skip diff package validation',
},
dev: {
hasValue: false,
default: false,
description: 'Development mode build',
},
bundleName: {
hasValue: true,
default: 'index.bundlejs',
description: 'Bundle file name',
},
entryFile: {
hasValue: true,
default: 'index.js',
description: 'Entry file',
},
sourcemap: {
hasValue: false,
default: false,
description: 'Generate source maps',
},
output: {
hasValue: true,
description: 'Custom output path for diff package',
},
intermediaDir: {
hasValue: true,
description: 'Intermediate directory',
},
taro: {
hasValue: false,
default: false,
description: 'Use Taro CLI',
},
expo: {
hasValue: false,
default: false,
description: 'Use Expo CLI',
},
rncli: {
hasValue: false,
default: false,
description: 'Use React Native CLI',
},
disableHermes: {
hasValue: false,
default: false,
description: 'Disable Hermes',
},
name: {
hasValue: true,
description: 'Version name for publishing',
},
description: {
hasValue: true,
description: 'Version description for publishing',
},
metaInfo: {
hasValue: true,
description: 'Meta information for publishing',
},
rollout: {
hasValue: true,
description: 'Rollout percentage',
},
dryRun: {
hasValue: false,
default: false,
description: 'Dry run mode',
},
},
},
],
};

19
src/modules/index.ts Normal file
View File

@@ -0,0 +1,19 @@
import { appModule } from './app-module';
import { bundleModule } from './bundle-module';
import { packageModule } from './package-module';
import { userModule } from './user-module';
import { versionModule } from './version-module';
export { bundleModule } from './bundle-module';
export { versionModule } from './version-module';
export { appModule } from './app-module';
export { userModule } from './user-module';
export { packageModule } from './package-module';
export const builtinModules = [
bundleModule,
versionModule,
appModule,
userModule,
packageModule,
];

View File

@@ -0,0 +1,11 @@
import { packageCommands } from '../package';
import type { CLIModule } from '../types';
export const packageModule: CLIModule = {
name: 'package',
version: '1.0.0',
commands: [],
workflows: [],
};

406
src/modules/user-module.ts Normal file
View File

@@ -0,0 +1,406 @@
import { getSession, loadSession } from '../api';
import type { CLIModule, CommandContext } from '../types';
import { userCommands } from '../user';
export const userModule: CLIModule = {
name: 'user',
version: '1.0.0',
commands: [],
workflows: [
{
name: 'auth-check',
description: 'Check authentication status and user information',
options: {
autoLogin: {
default: false,
description: 'Automatically login if not authenticated',
},
showDetails: {
default: true,
description: 'Show detailed user information',
},
},
steps: [
{
name: 'load-session',
description: 'Load existing session from local storage',
execute: async (context: CommandContext) => {
console.log('Loading session from local storage...');
try {
await loadSession();
const session = getSession();
if (session && session.token) {
console.log('✓ Session found in local storage');
return {
sessionLoaded: true,
hasToken: true,
session,
};
} else {
console.log('✗ No valid session found in local storage');
return {
sessionLoaded: true,
hasToken: false,
session: null,
};
}
} catch (error) {
console.log(
'✗ Failed to load session:',
error instanceof Error ? error.message : 'Unknown error',
);
return {
sessionLoaded: false,
hasToken: false,
session: null,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
{
name: 'validate-session',
description: 'Validate session by calling API',
execute: async (context: CommandContext, previousResult: any) => {
if (!previousResult.hasToken) {
console.log('No token available, skipping validation');
return {
...previousResult,
validated: false,
reason: 'No token available',
};
}
console.log('Validating session with server...');
try {
await userCommands.me();
console.log('✓ Session is valid');
return {
...previousResult,
validated: true,
reason: 'Session validated successfully',
};
} catch (error) {
console.log(
'✗ Session validation failed:',
error instanceof Error ? error.message : 'Unknown error',
);
return {
...previousResult,
validated: false,
reason:
error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
{
name: 'get-user-info',
description: 'Get current user information',
execute: async (context: CommandContext, previousResult: any) => {
if (!previousResult.validated) {
console.log('Session not valid, cannot get user info');
return {
...previousResult,
userInfo: null,
reason: 'Session not valid',
};
}
console.log('Getting user information...');
try {
const { get } = await import('../api');
const userInfo = await get('/user/me');
console.log('✓ User information retrieved successfully');
if (context.options.showDetails !== false) {
console.log('\n=== User Information ===');
for (const [key, value] of Object.entries(userInfo)) {
if (key !== 'ok') {
console.log(`${key}: ${value}`);
}
}
console.log('========================\n');
}
return {
...previousResult,
userInfo,
reason: 'User info retrieved successfully',
};
} catch (error) {
console.log(
'✗ Failed to get user info:',
error instanceof Error ? error.message : 'Unknown error',
);
return {
...previousResult,
userInfo: null,
reason:
error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
{
name: 'handle-auth-failure',
description: 'Handle authentication failure',
execute: async (context: CommandContext, previousResult: any) => {
if (previousResult.validated) {
console.log('✓ Authentication check completed successfully');
return {
...previousResult,
authCheckComplete: true,
status: 'authenticated',
};
}
console.log('✗ Authentication check failed');
if (context.options.autoLogin) {
console.log('Attempting automatic login...');
try {
await userCommands.login({ args: [] });
console.log('✓ Automatic login successful');
return {
...previousResult,
authCheckComplete: true,
status: 'auto-logged-in',
autoLoginSuccess: true,
};
} catch (error) {
console.log(
'✗ Automatic login failed:',
error instanceof Error ? error.message : 'Unknown error',
);
return {
...previousResult,
authCheckComplete: true,
status: 'failed',
autoLoginSuccess: false,
autoLoginError:
error instanceof Error ? error.message : 'Unknown error',
};
}
} else {
console.log('Please run login command to authenticate');
return {
...previousResult,
authCheckComplete: true,
status: 'unauthenticated',
suggestion: 'Run login command to authenticate',
};
}
},
},
],
},
{
name: 'login-flow',
description: 'Complete login flow with validation',
options: {
email: { hasValue: true, description: 'User email' },
password: { hasValue: true, description: 'User password' },
validateAfterLogin: {
default: true,
description: 'Validate session after login',
},
},
steps: [
{
name: 'check-existing-session',
description: 'Check if user is already logged in',
execute: async (context: CommandContext) => {
console.log('Checking existing session...');
try {
await loadSession();
const session = getSession();
if (session && session.token) {
try {
await userCommands.me();
console.log('✓ User is already logged in');
return {
alreadyLoggedIn: true,
session: session,
status: 'authenticated',
};
} catch (error) {
console.log(
'✗ Existing session is invalid, proceeding with login',
);
return {
alreadyLoggedIn: false,
session: null,
status: 'session-expired',
};
}
} else {
console.log('No existing session found');
return {
alreadyLoggedIn: false,
session: null,
status: 'no-session',
};
}
} catch (error) {
console.log(
'Error checking existing session:',
error instanceof Error ? error.message : 'Unknown error',
);
return {
alreadyLoggedIn: false,
session: null,
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
{
name: 'perform-login',
description: 'Perform user login',
execute: async (context: CommandContext, previousResult: any) => {
if (previousResult.alreadyLoggedIn) {
console.log('Skipping login - user already authenticated');
return {
...previousResult,
loginPerformed: false,
loginSuccess: true,
};
}
console.log('Performing login...');
try {
const loginArgs = [];
if (context.options.email) {
loginArgs.push(context.options.email);
}
if (context.options.password) {
loginArgs.push(context.options.password);
}
await userCommands.login({ args: loginArgs });
console.log('✓ Login successful');
return {
...previousResult,
loginPerformed: true,
loginSuccess: true,
};
} catch (error) {
console.log(
'✗ Login failed:',
error instanceof Error ? error.message : 'Unknown error',
);
return {
...previousResult,
loginPerformed: true,
loginSuccess: false,
loginError:
error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
{
name: 'validate-login',
description: 'Validate login by getting user info',
execute: async (context: CommandContext, previousResult: any) => {
if (
!previousResult.loginSuccess &&
!previousResult.alreadyLoggedIn
) {
console.log('Login failed, skipping validation');
return {
...previousResult,
validationPerformed: false,
validationSuccess: false,
};
}
if (context.options.validateAfterLogin === false) {
console.log('Skipping validation as requested');
return {
...previousResult,
validationPerformed: false,
validationSuccess: true,
};
}
console.log('Validating login by getting user information...');
try {
const userInfo = await userCommands.me();
console.log('✓ Login validation successful');
return {
...previousResult,
validationPerformed: true,
validationSuccess: true,
userInfo,
};
} catch (error) {
console.log(
'✗ Login validation failed:',
error instanceof Error ? error.message : 'Unknown error',
);
return {
...previousResult,
validationPerformed: true,
validationSuccess: false,
validationError:
error instanceof Error ? error.message : 'Unknown error',
};
}
},
},
{
name: 'login-summary',
description: 'Provide login flow summary',
execute: async (context: CommandContext, previousResult: any) => {
console.log('\n=== Login Flow Summary ===');
if (previousResult.alreadyLoggedIn) {
console.log('Status: Already logged in');
console.log('Session: Valid');
} else if (previousResult.loginSuccess) {
console.log('Status: Login successful');
if (previousResult.validationSuccess) {
console.log('Validation: Passed');
} else {
console.log('Validation: Failed');
}
} else {
console.log('Status: Login failed');
console.log(
'Error:',
previousResult.loginError || 'Unknown error',
);
}
console.log('==========================\n');
return {
...previousResult,
flowComplete: true,
finalStatus:
previousResult.alreadyLoggedIn || previousResult.loginSuccess
? 'success'
: 'failed',
};
},
},
],
},
],
};

View File

@@ -0,0 +1,8 @@
import type { CLIModule} from '../types';
export const versionModule: CLIModule = {
name: 'version',
version: '1.0.0',
commands: [],
workflows: [],
};

View File

@@ -1,146 +0,0 @@
/**
* Created by tdzl2003 on 4/2/16.
*/
const { get, post, uploadFile } = require('./api');
import { question, saveToLocal } from './utils';
import { checkPlatform, getSelectedApp } from './app';
import { getApkInfo, getIpaInfo } from './utils';
const Table = require('tty-table');
export async function listPackage(appId) {
const { data } = await get(`/app/${appId}/package/list?limit=1000`);
const header = [{ value: '原生包 Id' }, { value: '原生版本' }];
const rows = [];
for (const pkg of data) {
const { version } = pkg;
let versionInfo = '';
if (version) {
versionInfo = `, 已绑定:${version.name} (${version.id})`;
} else {
// versionInfo = ' (newest)';
}
let output = pkg.name;
if (pkg.status === 'paused') {
output += '(已暂停)';
}
if (pkg.status === 'expired') {
output += '(已过期)';
}
output += versionInfo;
rows.push([pkg.id, output]);
}
console.log(Table(header, rows).render());
console.log(`\n${data.length} 个包`);
return data;
}
export async function choosePackage(appId) {
const list = await listPackage(appId);
while (true) {
const id = await question('输入原生包 id:');
const app = list.find((v) => v.id === (id | 0));
if (app) {
return app;
}
}
}
export const commands = {
uploadIpa: async function ({ args }) {
const fn = args[0];
if (!fn || !fn.endsWith('.ipa')) {
throw new Error('使用方法: pushy uploadIpa ipa后缀文件');
}
const {
versionName,
buildTime,
appId: appIdInPkg,
appKey: appKeyInPkg,
} = await getIpaInfo(fn);
const { appId, appKey } = await getSelectedApp('ios');
if (appIdInPkg && appIdInPkg !== appId) {
throw new Error(
`appId不匹配当前ipa: ${appIdInPkg}, 当前update.json: ${appId}`,
);
}
if (appKeyInPkg && appKeyInPkg !== appKey) {
throw new Error(
`appKey不匹配当前ipa: ${appKeyInPkg}, 当前update.json: ${appKey}`,
);
}
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime,
});
saveToLocal(fn, `${appId}/package/${id}.ipa`);
console.log(`已成功上传ipa原生包id: ${id}`);
},
uploadApk: async function ({ args }) {
const fn = args[0];
if (!fn || !fn.endsWith('.apk')) {
throw new Error('使用方法: pushy uploadApk apk后缀文件');
}
const {
versionName,
buildTime,
appId: appIdInPkg,
appKey: appKeyInPkg,
} = await getApkInfo(fn);
const { appId, appKey } = await getSelectedApp('android');
if (appIdInPkg && appIdInPkg !== appId) {
throw new Error(
`appId不匹配当前apk: ${appIdInPkg}, 当前update.json: ${appId}`,
);
}
if (appKeyInPkg && appKeyInPkg !== appKey) {
throw new Error(
`appKey不匹配当前apk: ${appKeyInPkg}, 当前update.json: ${appKey}`,
);
}
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime,
});
saveToLocal(fn, `${appId}/package/${id}.apk`);
console.log(`已成功上传apk原生包id: ${id}`);
},
parseIpa: async function ({ args }) {
const fn = args[0];
if (!fn || !fn.endsWith('.ipa')) {
throw new Error('使用方法: pushy parseIpa ipa后缀文件');
}
console.log(await getIpaInfo(fn));
},
parseApk: async function ({ args }) {
const fn = args[0];
if (!fn || !fn.endsWith('.apk')) {
throw new Error('使用方法: pushy parseApk apk后缀文件');
}
console.log(await getApkInfo(fn));
},
packages: async function ({ options }) {
const platform = checkPlatform(
options.platform || (await question('平台(ios/android):')),
);
const { appId } = await getSelectedApp(platform);
await listPackage(appId);
},
};

264
src/package.ts Normal file
View File

@@ -0,0 +1,264 @@
import { get, getAllPackages, post, uploadFile, doDelete } from './api';
import { question, saveToLocal } from './utils';
import { t } from './utils/i18n';
import { getPlatform, getSelectedApp } from './app';
import Table from 'tty-table';
import type { Platform } from './types';
import { getApkInfo, getAppInfo, getIpaInfo } from './utils';
import { depVersions } from './utils/dep-versions';
import { getCommitInfo } from './utils/git';
export async function listPackage(appId: string) {
const allPkgs = await getAllPackages(appId);
const header = [
{ value: t('nativePackageId') },
{ value: t('nativeVersion') },
];
const rows = [];
for (const pkg of allPkgs) {
const { version } = pkg;
let versionInfo = '';
if (version) {
const versionObj = version as any;
versionInfo = t('boundTo', {
name: versionObj.name || version,
id: versionObj.id || version,
});
}
let output = pkg.name;
if (pkg.status === 'paused') {
output += t('pausedStatus');
}
if (pkg.status === 'expired') {
output += t('expiredStatus');
}
output += versionInfo;
rows.push([pkg.id, output]);
}
console.log(Table(header, rows).render());
console.log(t('totalPackages', { count: allPkgs.length }));
return allPkgs;
}
export async function choosePackage(appId: string) {
const list = await listPackage(appId);
while (true) {
const id = await question(t('enterNativePackageId'));
const app = list.find((v) => v.id.toString() === id);
if (app) {
return app;
}
}
}
export const packageCommands = {
uploadIpa: async ({
args,
options,
}: {
args: string[];
options: Record<string, any>;
}) => {
const fn = args[0];
if (!fn || !fn.endsWith('.ipa')) {
throw new Error(t('usageUploadIpa'));
}
const ipaInfo = await getIpaInfo(fn);
const { versionName: extractedVersionName, buildTime } = ipaInfo;
const appIdInPkg = (ipaInfo as any).appId;
const appKeyInPkg = (ipaInfo as any).appKey;
const { appId, appKey } = await getSelectedApp('ios');
if (appIdInPkg && appIdInPkg != appId) {
throw new Error(t('appIdMismatchIpa', { appIdInPkg, appId }));
}
if (appKeyInPkg && appKeyInPkg !== appKey) {
throw new Error(t('appKeyMismatchIpa', { appKeyInPkg, appKey }));
}
// Use custom version if provided, otherwise use extracted version
const versionName = options.version || extractedVersionName;
if (options.version) {
console.log(t('usingCustomVersion', { version: versionName }));
}
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime,
deps: depVersions,
commit: await getCommitInfo(),
});
saveToLocal(fn, `${appId}/package/${id}.ipa`);
console.log(t('ipaUploadSuccess', { id, version: versionName, buildTime }));
},
uploadApk: async ({
args,
options,
}: {
args: string[];
options: Record<string, any>;
}) => {
const fn = args[0];
if (!fn || !fn.endsWith('.apk')) {
throw new Error(t('usageUploadApk'));
}
const apkInfo = await getApkInfo(fn);
const { versionName: extractedVersionName, buildTime } = apkInfo;
const appIdInPkg = (apkInfo as any).appId;
const appKeyInPkg = (apkInfo as any).appKey;
const { appId, appKey } = await getSelectedApp('android');
if (appIdInPkg && appIdInPkg != appId) {
throw new Error(t('appIdMismatchApk', { appIdInPkg, appId }));
}
if (appKeyInPkg && appKeyInPkg !== appKey) {
throw new Error(t('appKeyMismatchApk', { appKeyInPkg, appKey }));
}
// Use custom version if provided, otherwise use extracted version
const versionName = options.version || extractedVersionName;
if (options.version) {
console.log(t('usingCustomVersion', { version: versionName }));
}
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime,
deps: depVersions,
commit: await getCommitInfo(),
});
saveToLocal(fn, `${appId}/package/${id}.apk`);
console.log(t('apkUploadSuccess', { id, version: versionName, buildTime }));
},
uploadApp: async ({
args,
options,
}: {
args: string[];
options: Record<string, any>;
}) => {
const fn = args[0];
if (!fn || !fn.endsWith('.app')) {
throw new Error(t('usageUploadApp'));
}
const appInfo = await getAppInfo(fn);
const { versionName: extractedVersionName, buildTime } = appInfo;
const appIdInPkg = (appInfo as any).appId;
const appKeyInPkg = (appInfo as any).appKey;
const { appId, appKey } = await getSelectedApp('harmony');
if (appIdInPkg && appIdInPkg != appId) {
throw new Error(t('appIdMismatchApp', { appIdInPkg, appId }));
}
if (appKeyInPkg && appKeyInPkg !== appKey) {
throw new Error(t('appKeyMismatchApp', { appKeyInPkg, appKey }));
}
// Use custom version if provided, otherwise use extracted version
const versionName = options.version || extractedVersionName;
if (options.version) {
console.log(t('usingCustomVersion', { version: versionName }));
}
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/package/create`, {
name: versionName,
hash,
buildTime,
deps: depVersions,
commit: await getCommitInfo(),
});
saveToLocal(fn, `${appId}/package/${id}.app`);
console.log(t('appUploadSuccess', { id, version: versionName, buildTime }));
},
parseApp: async ({ args }: { args: string[] }) => {
const fn = args[0];
if (!fn || !fn.endsWith('.app')) {
throw new Error(t('usageParseApp'));
}
console.log(await getAppInfo(fn));
},
parseIpa: async ({ args }: { args: string[] }) => {
const fn = args[0];
if (!fn || !fn.endsWith('.ipa')) {
throw new Error(t('usageParseIpa'));
}
console.log(await getIpaInfo(fn));
},
parseApk: async ({ args }: { args: string[] }) => {
const fn = args[0];
if (!fn || !fn.endsWith('.apk')) {
throw new Error(t('usageParseApk'));
}
console.log(await getApkInfo(fn));
},
packages: async ({ options }: { options: { platform: Platform } }) => {
const platform = await getPlatform(options.platform);
const { appId } = await getSelectedApp(platform);
await listPackage(appId);
},
deletePackage: async ({
args,
options,
}: {
args: string[];
options: { appId?: string, packageId?: string, packageVersion?: string };
}) => {
let { appId, packageId, packageVersion } = options;
if (!appId) {
const platform = await getPlatform();
appId = (await getSelectedApp(platform)).appId as string;
}
// If no packageId provided as argument, let user choose from list
if (!packageId) {
const allPkgs = await getAllPackages(appId);
if (!allPkgs) {
throw new Error(t('noPackagesFound', { appId }));
}
const selectedPackage = allPkgs.find((pkg) => pkg.name === packageVersion);
if (!selectedPackage) {
throw new Error(t('packageNotFound', { packageVersion }));
}
packageId = selectedPackage.id;
}
// Confirm deletion
// const confirmDelete = await question(
// t('confirmDeletePackage', { packageId }),
// );
// if (
// confirmDelete.toLowerCase() !== 'y' &&
// confirmDelete.toLowerCase() !== 'yes'
// ) {
// console.log(t('cancelled'));
// return;
// }
try {
await doDelete(`/app/${appId}/package/${packageId}`);
console.log(t('deletePackageSuccess', { packageId }));
} catch (error: any) {
throw new Error(
t('deletePackageError', { packageId, error: error.message }),
);
}
},
};

341
src/provider.ts Normal file
View File

@@ -0,0 +1,341 @@
import { getSession, loadSession } from './api';
import { getPlatform, getSelectedApp } from './app';
import type {
BundleOptions,
CLIProvider,
CommandContext,
CommandResult,
CustomWorkflow,
Platform,
PublishOptions,
Session,
UploadOptions,
Version,
} from './types';
export class CLIProviderImpl implements CLIProvider {
private workflows: Map<string, CustomWorkflow> = new Map();
private session?: Session;
constructor() {
this.init();
}
private async init() {
try {
await loadSession();
this.session = getSession();
} catch (error) {}
}
async bundle(options: BundleOptions): Promise<CommandResult> {
try {
const context: CommandContext = {
args: [],
options: {
dev: options.dev || false,
platform: options.platform,
bundleName: options.bundleName || 'index.bundlejs',
entryFile: options.entryFile || 'index.js',
output: options.output || '${tempDir}/output/${platform}.${time}.ppk',
sourcemap: options.sourcemap || false,
taro: options.taro || false,
expo: options.expo || false,
rncli: options.rncli || false,
disableHermes: options.disableHermes || false,
},
};
const { bundleCommands } = await import('./bundle');
await bundleCommands.bundle(context);
return {
success: true,
data: { message: 'Bundle created successfully' },
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error during bundling',
};
}
}
async publish(options: PublishOptions): Promise<CommandResult> {
try {
const context: CommandContext = {
args: [],
options: {
name: options.name,
description: options.description,
metaInfo: options.metaInfo,
packageId: options.packageId,
packageVersion: options.packageVersion,
minPackageVersion: options.minPackageVersion,
maxPackageVersion: options.maxPackageVersion,
packageVersionRange: options.packageVersionRange,
rollout: options.rollout,
dryRun: options.dryRun || false,
},
};
const { versionCommands } = await import('./versions');
await versionCommands.publish(context);
return {
success: true,
data: { message: 'Version published successfully' },
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error during publishing',
};
}
}
async upload(options: UploadOptions): Promise<CommandResult> {
try {
const platform = await this.getPlatform(options.platform);
const { appId } = await this.getSelectedApp(platform);
const filePath = options.filePath;
const fileType = filePath.split('.').pop()?.toLowerCase();
const context: CommandContext = {
args: [filePath],
options: { platform, appId, version: options.version },
};
const { packageCommands } = await import('./package');
switch (fileType) {
case 'ipa':
await packageCommands.uploadIpa(context);
break;
case 'apk':
await packageCommands.uploadApk(context);
break;
case 'app':
await packageCommands.uploadApp(context);
break;
default:
throw new Error(`Unsupported file type: ${fileType}`);
}
return {
success: true,
data: { message: 'File uploaded successfully' },
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error during upload',
};
}
}
async getSelectedApp(
platform?: Platform,
): Promise<{ appId: string; platform: Platform }> {
const resolvedPlatform = await this.getPlatform(platform);
return getSelectedApp(resolvedPlatform);
}
async listApps(platform?: Platform): Promise<CommandResult> {
try {
const resolvedPlatform = await this.getPlatform(platform);
const { appCommands } = await import('./app');
await appCommands.apps({ options: { platform: resolvedPlatform } });
return {
success: true,
data: { message: 'Apps listed successfully' },
};
} catch (error) {
return {
success: false,
error:
error instanceof Error ? error.message : 'Unknown error listing apps',
};
}
}
async createApp(name: string, platform: Platform): Promise<CommandResult> {
try {
const { appCommands } = await import('./app');
await appCommands.createApp({
options: {
name,
platform,
downloadUrl: '',
},
});
return {
success: true,
data: { message: 'App created successfully' },
};
} catch (error) {
return {
success: false,
error:
error instanceof Error ? error.message : 'Unknown error creating app',
};
}
}
async listVersions(appId: string): Promise<CommandResult> {
try {
const context: CommandContext = {
args: [],
options: { appId },
};
const { versionCommands } = await import('./versions');
await versionCommands.versions(context);
return {
success: true,
data: { message: 'Versions listed successfully' },
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error listing versions',
};
}
}
async updateVersion(
appId: string,
versionId: string,
updates: Partial<Version>,
): Promise<CommandResult> {
try {
const context: CommandContext = {
args: [versionId],
options: {
appId,
...updates,
},
};
const { versionCommands } = await import('./versions');
await versionCommands.update(context);
return {
success: true,
data: { message: 'Version updated successfully' },
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error updating version',
};
}
}
async getPlatform(platform?: Platform): Promise<Platform> {
return getPlatform(platform);
}
async loadSession(): Promise<Session> {
await loadSession();
this.session = getSession();
if (!this.session) {
throw new Error('Failed to load session');
}
return this.session;
}
registerWorkflow(workflow: CustomWorkflow): void {
this.workflows.set(workflow.name, workflow);
}
async executeWorkflow(
workflowName: string,
context: CommandContext,
): Promise<CommandResult> {
const workflow = this.workflows.get(workflowName);
if (!workflow) {
return {
success: false,
error: `Workflow '${workflowName}' not found`,
};
}
try {
let previousResult: any = null;
for (const step of workflow.steps) {
if (step.condition && !step.condition(context)) {
console.log(`Skipping step '${step.name}' due to condition`);
continue;
}
console.log(`Executing step '${step.name}'`);
previousResult = await step.execute(context, previousResult);
}
return {
success: true,
data: {
message: `Workflow '${workflowName}' completed successfully`,
result: previousResult,
},
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: `Workflow '${workflowName}' failed`,
};
}
}
getRegisteredWorkflows(): string[] {
return Array.from(this.workflows.keys());
}
async listPackages(appId?: string): Promise<CommandResult> {
try {
const context: CommandContext = {
args: [],
options: appId ? { appId } : {},
};
const { listPackage } = await import('./package');
const result = await listPackage(appId || '');
return {
success: true,
data: result,
};
} catch (error) {
return {
success: false,
error:
error instanceof Error
? error.message
: 'Unknown error listing packages',
};
}
}
}

148
src/types.ts Normal file
View File

@@ -0,0 +1,148 @@
declare global {
var NO_INTERACTIVE: boolean;
var USE_ACC_OSS: boolean;
}
export interface Session {
token: string;
}
export type Platform = 'ios' | 'android' | 'harmony';
export interface Package {
id: string;
name: string;
version?: string;
status?: string;
appId?: string;
appKey?: string;
versionName?: any;
buildTime?: any;
}
export interface Version {
id: string;
hash: string;
name: string;
packages?: Package[];
}
export interface CommandContext {
args: string[];
options: Record<string, any>;
platform?: Platform;
appId?: string;
session?: Session;
}
export interface CommandResult {
success: boolean;
data?: any;
error?: string;
}
export interface CommandDefinition {
name: string;
description?: string;
handler: (context: CommandContext) => Promise<CommandResult>;
options?: Record<
string,
{
hasValue?: boolean;
default?: any;
description?: string;
}
>;
}
export interface BundleOptions {
dev?: boolean;
platform?: Platform;
bundleName?: string;
entryFile?: string;
output?: string;
sourcemap?: boolean;
taro?: boolean;
expo?: boolean;
rncli?: boolean;
disableHermes?: boolean;
}
export interface PublishOptions {
name?: string;
description?: string;
metaInfo?: string;
packageId?: string;
packageVersion?: string;
minPackageVersion?: string;
maxPackageVersion?: string;
packageVersionRange?: string;
rollout?: number;
dryRun?: boolean;
}
export interface UploadOptions {
platform?: Platform;
filePath: string;
appId?: string;
version?: string;
}
export interface WorkflowStep {
name: string;
description?: string;
execute: (context: CommandContext, previousResult?: any) => Promise<any>;
condition?: (context: CommandContext) => boolean;
}
export interface CustomWorkflow {
name: string;
description?: string;
steps: WorkflowStep[];
validate?: (context: CommandContext) => boolean;
options?: Record<
string,
{
hasValue?: boolean;
default?: any;
description?: string;
}
>;
}
export interface CLIProvider {
bundle: (options: BundleOptions) => Promise<CommandResult>;
publish: (options: PublishOptions) => Promise<CommandResult>;
upload: (options: UploadOptions) => Promise<CommandResult>;
createApp: (name: string, platform: Platform) => Promise<CommandResult>;
listApps: (platform?: Platform) => Promise<CommandResult>;
getSelectedApp: (
platform?: Platform,
) => Promise<{ appId: string; platform: Platform }>;
listVersions: (appId: string) => Promise<CommandResult>;
updateVersion: (
appId: string,
versionId: string,
updates: Partial<Version>,
) => Promise<CommandResult>;
getPlatform: (platform?: Platform) => Promise<Platform>;
loadSession: () => Promise<Session>;
registerWorkflow: (workflow: CustomWorkflow) => void;
executeWorkflow: (
workflowName: string,
context: CommandContext,
) => Promise<CommandResult>;
}
export interface CLIModule {
name: string;
version: string;
commands?: CommandDefinition[];
workflows?: CustomWorkflow[];
init?: (provider: CLIProvider) => void;
cleanup?: () => void;
}

View File

@@ -1,43 +0,0 @@
/**
* Created by tdzl2003 on 2/13/16.
*/
import {question} from './utils';
const {
post,
get,
replaceSession,
saveSession,
closeSession,
} = require('./api');
const crypto = require('crypto');
function md5(str) {
return crypto.createHash('md5').update(str).digest('hex');
}
exports.commands = {
login: async function ({args}){
const email = args[0] || await question('email:');
const pwd = args[1] || await question('password:', true);
const {token, info} = await post('/user/login', {
email,
pwd: md5(pwd),
});
replaceSession({token});
await saveSession();
console.log(`欢迎使用 pushy 热更新服务, ${info.name}.`);
},
logout: async function (){
await closeSession();
console.log('已退出登录');
},
me: async function (){
const me = await get('/user/me');
for (const k in me) {
if (k !== 'ok') {
console.log(`${k}: ${me[k]}`);
}
}
},
}

35
src/user.ts Normal file
View File

@@ -0,0 +1,35 @@
import crypto from 'crypto';
import type { CommandContext } from 'types';
import { closeSession, get, post, replaceSession, saveSession } from './api';
import { question } from './utils';
import { t } from './utils/i18n';
function md5(str: string) {
return crypto.createHash('md5').update(str).digest('hex');
}
export const userCommands = {
login: async ({ args }: { args: string[] }) => {
const email = args[0] || (await question('email:'));
const pwd = args[1] || (await question('password:', true));
const { token, info } = await post('/user/login', {
email,
pwd: md5(pwd),
});
replaceSession({ token });
await saveSession();
console.log(t('welcomeMessage', { name: info.name }));
},
logout: async (context: CommandContext) => {
await closeSession();
console.log(t('loggedOut'));
},
me: async () => {
const me = await get('/user/me');
for (const k in me) {
if (k !== 'ok') {
console.log(`${k}: ${me[k]}`);
}
}
},
};

View File

@@ -0,0 +1,35 @@
import fs from 'fs';
// import path from 'path';
import { credentialFile, tempDir } from './constants';
import { t } from './i18n';
export function addGitIgnore() {
const shouldIgnore = [credentialFile, tempDir];
const gitignorePath = '.gitignore';
if (!fs.existsSync(gitignorePath)) {
return;
}
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf-8');
const gitignoreLines = gitignoreContent.split('\n');
for (const line of gitignoreLines) {
const index = shouldIgnore.indexOf(line.trim());
if (index !== -1) {
shouldIgnore.splice(index, 1);
}
}
if (shouldIgnore.length > 0) {
gitignoreLines.push('# react-native-update');
for (const line of shouldIgnore) {
gitignoreLines.push(line);
console.log(t('addedToGitignore', { line }));
}
fs.writeFileSync(gitignorePath, gitignoreLines.join('\n'));
}
}

View File

@@ -0,0 +1,100 @@
const Zip = require('./zip');
const {
mapInfoResource,
findApkIconPath,
getBase64FromBuffer,
} = require('./utils');
const ManifestName = /^androidmanifest\.xml$/;
const ResourceName = /^resources\.arsc$/;
const ManifestXmlParser = require('./xml-parser/manifest');
const ResourceFinder = require('./resource-finder');
class ApkParser extends Zip {
/**
* parser for parsing .apk file
* @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
*/
constructor(file) {
super(file);
if (!(this instanceof ApkParser)) {
return new ApkParser(file);
}
}
parse() {
return new Promise((resolve, reject) => {
this.getEntries([ManifestName, ResourceName])
.then((buffers) => {
if (!buffers[ManifestName]) {
throw new Error("AndroidManifest.xml can't be found.");
}
let apkInfo = this._parseManifest(buffers[ManifestName]);
let resourceMap;
if (!buffers[ResourceName]) {
resolve(apkInfo);
} else {
// parse resourceMap
resourceMap = this._parseResourceMap(buffers[ResourceName]);
// update apkInfo with resourceMap
apkInfo = mapInfoResource(apkInfo, resourceMap);
// find icon path and parse icon
const iconPath = findApkIconPath(apkInfo);
if (iconPath) {
this.getEntry(iconPath)
.then((iconBuffer) => {
apkInfo.icon = iconBuffer
? getBase64FromBuffer(iconBuffer)
: null;
resolve(apkInfo);
})
.catch((e) => {
apkInfo.icon = null;
resolve(apkInfo);
console.warn('[Warning] failed to parse icon: ', e);
});
} else {
apkInfo.icon = null;
resolve(apkInfo);
}
}
})
.catch((e) => {
reject(e);
});
});
}
/**
* Parse manifest
* @param {Buffer} buffer // manifest file's buffer
*/
_parseManifest(buffer) {
try {
const parser = new ManifestXmlParser(buffer, {
ignore: [
'application.activity',
'application.service',
'application.receiver',
'application.provider',
'permission-group',
],
});
return parser.parse();
} catch (e) {
throw new Error('Parse AndroidManifest.xml error: ', e);
}
}
/**
* Parse resourceMap
* @param {Buffer} buffer // resourceMap file's buffer
*/
_parseResourceMap(buffer) {
try {
return new ResourceFinder().processResourceTable(buffer);
} catch (e) {
throw new Error('Parser resources.arsc error: ' + e);
}
}
}
module.exports = ApkParser;

View File

@@ -0,0 +1,16 @@
const Zip = require('./zip');
class AppParser extends Zip {
/**
* parser for parsing .apk file
* @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
*/
constructor(file) {
super(file);
if (!(this instanceof AppParser)) {
return new AppParser(file);
}
}
}
module.exports = AppParser;

View File

@@ -0,0 +1,45 @@
const ApkParser = require('./apk');
const IpaParser = require('./ipa');
const AppParser = require('./app');
const supportFileTypes = ['ipa', 'apk', 'app'];
class AppInfoParser {
file: string | File;
parser: any;
/**
* parser for parsing .ipa or .apk file
* @param {String | File} file // file's path in Node, instance of File in Browser
*/
constructor(file: string | File) {
if (!file) {
throw new Error(
"Param miss: file(file's path in Node, instance of File in browser).",
);
}
const splits = (typeof file === 'string' ? file : file.name).split('.');
const fileType = splits[splits.length - 1].toLowerCase();
if (!supportFileTypes.includes(fileType)) {
throw new Error(
'Unsupported file type, only support .ipa or .apk or .app file.',
);
}
this.file = file;
switch (fileType) {
case 'ipa':
this.parser = new IpaParser(this.file);
break;
case 'apk':
this.parser = new ApkParser(this.file);
break;
case 'app':
this.parser = new AppParser(this.file);
break;
}
}
parse() {
return this.parser.parse();
}
}
export default AppInfoParser;

View File

@@ -0,0 +1,104 @@
const parsePlist = require('plist').parse;
const parseBplist = require('bplist-parser').parseBuffer;
const cgbiToPng = require('cgbi-to-png');
const Zip = require('./zip');
const { findIpaIconPath, getBase64FromBuffer, isBrowser } = require('./utils');
const PlistName = /payload\/[^\/]+?.app\/info.plist$/i;
const ProvisionName = /payload\/.+?\.app\/embedded.mobileprovision/;
class IpaParser extends Zip {
/**
* parser for parsing .ipa file
* @param {String | File | Blob} file // file's path in Node, instance of File or Blob in Browser
*/
constructor(file) {
super(file);
if (!(this instanceof IpaParser)) {
return new IpaParser(file);
}
}
parse() {
return new Promise((resolve, reject) => {
this.getEntries([PlistName, ProvisionName])
.then((buffers) => {
if (!buffers[PlistName]) {
throw new Error("Info.plist can't be found.");
}
const plistInfo = this._parsePlist(buffers[PlistName]);
// parse mobile provision
const provisionInfo = this._parseProvision(buffers[ProvisionName]);
plistInfo.mobileProvision = provisionInfo;
// find icon path and parse icon
const iconRegex = new RegExp(
findIpaIconPath(plistInfo).toLowerCase(),
);
this.getEntry(iconRegex)
.then((iconBuffer) => {
try {
// In general, the ipa file's icon has been specially processed, should be converted
plistInfo.icon = iconBuffer
? getBase64FromBuffer(cgbiToPng.revert(iconBuffer))
: null;
} catch (err) {
if (isBrowser()) {
// Normal conversion in other cases
plistInfo.icon = iconBuffer
? getBase64FromBuffer(
window.btoa(String.fromCharCode(...iconBuffer)),
)
: null;
} else {
plistInfo.icon = null;
console.warn('[Warning] failed to parse icon: ', err);
}
}
resolve(plistInfo);
})
.catch((e) => {
reject(e);
});
})
.catch((e) => {
reject(e);
});
});
}
/**
* Parse plist
* @param {Buffer} buffer // plist file's buffer
*/
_parsePlist(buffer) {
let result;
const bufferType = buffer[0];
if (bufferType === 60 || bufferType === '<' || bufferType === 239) {
result = parsePlist(buffer.toString());
} else if (bufferType === 98) {
result = parseBplist(buffer)[0];
} else {
throw new Error('Unknown plist buffer type.');
}
return result;
}
/**
* parse provision
* @param {Buffer} buffer // provision file's buffer
*/
_parseProvision(buffer) {
let info = {};
if (buffer) {
let content = buffer.toString('utf-8');
const firstIndex = content.indexOf('<?xml');
const endIndex = content.indexOf('</plist>');
content = content.slice(firstIndex, endIndex + 8);
if (content) {
info = parsePlist(content);
}
}
return info;
}
}
module.exports = IpaParser;

View File

@@ -0,0 +1,495 @@
/**
* Code translated from a C# project https://github.com/hylander0/Iteedee.ApkReader/blob/master/Iteedee.ApkReader/ApkResourceFinder.cs
*
* Decode binary file `resources.arsc` from a .apk file to a JavaScript Object.
*/
var ByteBuffer = require('bytebuffer');
var DEBUG = false;
var RES_STRING_POOL_TYPE = 0x0001;
var RES_TABLE_TYPE = 0x0002;
var RES_TABLE_PACKAGE_TYPE = 0x0200;
var RES_TABLE_TYPE_TYPE = 0x0201;
var RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
// The 'data' holds a ResTable_ref, a reference to another resource
// table entry.
var TYPE_REFERENCE = 0x01;
// The 'data' holds an index into the containing resource table's
// global value string pool.
var TYPE_STRING = 0x03;
function ResourceFinder() {
this.valueStringPool = null;
this.typeStringPool = null;
this.keyStringPool = null;
this.package_id = 0;
this.responseMap = {};
this.entryMap = {};
}
/**
* Same to C# BinaryReader.readBytes
*
* @param bb ByteBuffer
* @param len length
* @returns {Buffer}
*/
ResourceFinder.readBytes = (bb, len) => {
var uint8Array = new Uint8Array(len);
for (var i = 0; i < len; i++) {
uint8Array[i] = bb.readUint8();
}
return ByteBuffer.wrap(uint8Array, 'binary', true);
};
//
/**
*
* @param {ByteBuffer} bb
* @return {Map<String, Set<String>>}
*/
ResourceFinder.prototype.processResourceTable = function (resourceBuffer) {
const bb = ByteBuffer.wrap(resourceBuffer, 'binary', true);
// Resource table structure
var type = bb.readShort(),
headerSize = bb.readShort(),
size = bb.readInt(),
packageCount = bb.readInt(),
buffer,
bb2;
if (type != RES_TABLE_TYPE) {
throw new Error('No RES_TABLE_TYPE found!');
}
if (size != bb.limit) {
throw new Error('The buffer size not matches to the resource table size.');
}
bb.offset = headerSize;
var realStringPoolCount = 0,
realPackageCount = 0;
while (true) {
var pos, t, hs, s;
try {
pos = bb.offset;
t = bb.readShort();
hs = bb.readShort();
s = bb.readInt();
} catch (e) {
break;
}
if (t == RES_STRING_POOL_TYPE) {
// Process the string pool
if (realStringPoolCount == 0) {
// Only the first string pool is processed.
if (DEBUG) {
console.log('Processing the string pool ...');
}
buffer = new ByteBuffer(s);
bb.offset = pos;
bb.prependTo(buffer);
bb2 = ByteBuffer.wrap(buffer, 'binary', true);
bb2.LE();
this.valueStringPool = this.processStringPool(bb2);
}
realStringPoolCount++;
} else if (t == RES_TABLE_PACKAGE_TYPE) {
// Process the package
if (DEBUG) {
console.log('Processing the package ' + realPackageCount + ' ...');
}
buffer = new ByteBuffer(s);
bb.offset = pos;
bb.prependTo(buffer);
bb2 = ByteBuffer.wrap(buffer, 'binary', true);
bb2.LE();
this.processPackage(bb2);
realPackageCount++;
} else {
throw new Error('Unsupported type');
}
bb.offset = pos + s;
if (!bb.remaining()) break;
}
if (realStringPoolCount != 1) {
throw new Error('More than 1 string pool found!');
}
if (realPackageCount != packageCount) {
throw new Error('Real package count not equals the declared count.');
}
return this.responseMap;
};
/**
*
* @param {ByteBuffer} bb
*/
ResourceFinder.prototype.processPackage = function (bb) {
// Package structure
var type = bb.readShort(),
headerSize = bb.readShort(),
size = bb.readInt(),
id = bb.readInt();
this.package_id = id;
for (var i = 0; i < 256; ++i) {
bb.readUint8();
}
var typeStrings = bb.readInt(),
lastPublicType = bb.readInt(),
keyStrings = bb.readInt(),
lastPublicKey = bb.readInt();
if (typeStrings != headerSize) {
throw new Error(
'TypeStrings must immediately following the package structure header.',
);
}
if (DEBUG) {
console.log('Type strings:');
}
var lastPosition = bb.offset;
bb.offset = typeStrings;
var bbTypeStrings = ResourceFinder.readBytes(bb, bb.limit - bb.offset);
bb.offset = lastPosition;
this.typeStringPool = this.processStringPool(bbTypeStrings);
// Key strings
if (DEBUG) {
console.log('Key strings:');
}
bb.offset = keyStrings;
var key_type = bb.readShort(),
key_headerSize = bb.readShort(),
key_size = bb.readInt();
lastPosition = bb.offset;
bb.offset = keyStrings;
var bbKeyStrings = ResourceFinder.readBytes(bb, bb.limit - bb.offset);
bb.offset = lastPosition;
this.keyStringPool = this.processStringPool(bbKeyStrings);
// Iterate through all chunks
var typeSpecCount = 0;
var typeCount = 0;
bb.offset = keyStrings + key_size;
var bb2;
while (true) {
var pos = bb.offset;
try {
var t = bb.readShort();
var hs = bb.readShort();
var s = bb.readInt();
} catch (e) {
break;
}
if (t == RES_TABLE_TYPE_SPEC_TYPE) {
bb.offset = pos;
bb2 = ResourceFinder.readBytes(bb, s);
this.processTypeSpec(bb2);
typeSpecCount++;
} else if (t == RES_TABLE_TYPE_TYPE) {
bb.offset = pos;
bb2 = ResourceFinder.readBytes(bb, s);
this.processType(bb2);
typeCount++;
}
if (s == 0) {
break;
}
bb.offset = pos + s;
if (!bb.remaining()) {
break;
}
}
};
/**
*
* @param {ByteBuffer} bb
*/
ResourceFinder.prototype.processType = function (bb) {
var type = bb.readShort(),
headerSize = bb.readShort(),
size = bb.readInt(),
id = bb.readByte(),
res0 = bb.readByte(),
res1 = bb.readShort(),
entryCount = bb.readInt(),
entriesStart = bb.readInt();
var refKeys = {};
var config_size = bb.readInt();
// Skip the config data
bb.offset = headerSize;
if (headerSize + entryCount * 4 != entriesStart) {
throw new Error('HeaderSize, entryCount and entriesStart are not valid.');
}
// Start to get entry indices
var entryIndices = new Array(entryCount);
for (var i = 0; i < entryCount; ++i) {
entryIndices[i] = bb.readInt();
}
// Get entries
for (var i = 0; i < entryCount; ++i) {
if (entryIndices[i] == -1) continue;
var resource_id = (this.package_id << 24) | (id << 16) | i;
var pos = bb.offset,
entry_size,
entry_flag,
entry_key,
value_size,
value_res0,
value_dataType,
value_data;
try {
entry_size = bb.readShort();
entry_flag = bb.readShort();
entry_key = bb.readInt();
} catch (e) {
break;
}
// Get the value (simple) or map (complex)
var FLAG_COMPLEX = 0x0001;
if ((entry_flag & FLAG_COMPLEX) == 0) {
// Simple case
value_size = bb.readShort();
value_res0 = bb.readByte();
value_dataType = bb.readByte();
value_data = bb.readInt();
var idStr = Number(resource_id).toString(16);
var keyStr = this.keyStringPool[entry_key];
var data = null;
if (DEBUG) {
console.log(
'Entry 0x' + idStr + ', key: ' + keyStr + ', simple value type: ',
);
}
var key = Number.parseInt(idStr, 16);
var entryArr = this.entryMap[key];
if (entryArr == null) {
entryArr = [];
}
entryArr.push(keyStr);
this.entryMap[key] = entryArr;
if (value_dataType == TYPE_STRING) {
data = this.valueStringPool[value_data];
if (DEBUG) {
console.log(', data: ' + this.valueStringPool[value_data] + '');
}
} else if (value_dataType == TYPE_REFERENCE) {
var hexIndex = Number(value_data).toString(16);
refKeys[idStr] = value_data;
} else {
data = '' + value_data;
if (DEBUG) {
console.log(', data: ' + value_data + '');
}
}
this.putIntoMap('@' + idStr, data);
} else {
// Complex case
var entry_parent = bb.readInt();
var entry_count = bb.readInt();
for (var j = 0; j < entry_count; ++j) {
var ref_name = bb.readInt();
value_size = bb.readShort();
value_res0 = bb.readByte();
value_dataType = bb.readByte();
value_data = bb.readInt();
}
if (DEBUG) {
console.log(
'Entry 0x' +
Number(resource_id).toString(16) +
', key: ' +
this.keyStringPool[entry_key] +
', complex value, not printed.',
);
}
}
}
for (var refK in refKeys) {
var values =
this.responseMap['@' + Number(refKeys[refK]).toString(16).toUpperCase()];
if (values != null && Object.keys(values).length < 1000) {
for (var value in values) {
this.putIntoMap('@' + refK, values[value]);
}
}
}
};
/**
*
* @param {ByteBuffer} bb
* @return {Array}
*/
ResourceFinder.prototype.processStringPool = (bb) => {
// String pool structure
//
var type = bb.readShort(),
headerSize = bb.readShort(),
size = bb.readInt(),
stringCount = bb.readInt(),
styleCount = bb.readInt(),
flags = bb.readInt(),
stringsStart = bb.readInt(),
stylesStart = bb.readInt(),
u16len,
buffer;
var isUTF_8 = (flags & 256) != 0;
var offsets = new Array(stringCount);
for (var i = 0; i < stringCount; ++i) {
offsets[i] = bb.readInt();
}
var strings = new Array(stringCount);
for (var i = 0; i < stringCount; ++i) {
var pos = stringsStart + offsets[i];
bb.offset = pos;
strings[i] = '';
if (isUTF_8) {
u16len = bb.readUint8();
if ((u16len & 0x80) != 0) {
u16len = ((u16len & 0x7f) << 8) + bb.readUint8();
}
var u8len = bb.readUint8();
if ((u8len & 0x80) != 0) {
u8len = ((u8len & 0x7f) << 8) + bb.readUint8();
}
if (u8len > 0) {
buffer = ResourceFinder.readBytes(bb, u8len);
try {
strings[i] = ByteBuffer.wrap(buffer, 'utf8', true).toString('utf8');
} catch (e) {
if (DEBUG) {
console.error(e);
console.log('Error when turning buffer to utf-8 string.');
}
}
} else {
strings[i] = '';
}
} else {
u16len = bb.readUint16();
if ((u16len & 0x8000) != 0) {
// larger than 32768
u16len = ((u16len & 0x7fff) << 16) + bb.readUint16();
}
if (u16len > 0) {
var len = u16len * 2;
buffer = ResourceFinder.readBytes(bb, len);
try {
strings[i] = ByteBuffer.wrap(buffer, 'utf8', true).toString('utf8');
} catch (e) {
if (DEBUG) {
console.error(e);
console.log('Error when turning buffer to utf-8 string.');
}
}
}
}
if (DEBUG) {
console.log('Parsed value: {0}', strings[i]);
}
}
return strings;
};
/**
*
* @param {ByteBuffer} bb
*/
ResourceFinder.prototype.processTypeSpec = function (bb) {
var type = bb.readShort(),
headerSize = bb.readShort(),
size = bb.readInt(),
id = bb.readByte(),
res0 = bb.readByte(),
res1 = bb.readShort(),
entryCount = bb.readInt();
if (DEBUG) {
console.log('Processing type spec ' + this.typeStringPool[id - 1] + '...');
}
var flags = new Array(entryCount);
for (var i = 0; i < entryCount; ++i) {
flags[i] = bb.readInt();
}
};
ResourceFinder.prototype.putIntoMap = function (resId, value) {
if (this.responseMap[resId.toUpperCase()] == null) {
this.responseMap[resId.toUpperCase()] = [];
}
if (value) {
this.responseMap[resId.toUpperCase()].push(value);
}
};
module.exports = ResourceFinder;

View File

@@ -0,0 +1,172 @@
function objectType(o) {
return Object.prototype.toString.call(o).slice(8, -1).toLowerCase();
}
function isArray(o) {
return objectType(o) === 'array';
}
function isObject(o) {
return objectType(o) === 'object';
}
function isPrimitive(o) {
return (
o === null ||
['boolean', 'number', 'string', 'undefined'].includes(objectType(o))
);
}
function isBrowser() {
return (
typeof process === 'undefined' ||
Object.prototype.toString.call(process) !== '[object process]'
);
}
/**
* map file place with resourceMap
* @param {Object} apkInfo // json info parsed from .apk file
* @param {Object} resourceMap // resourceMap
*/
function mapInfoResource(apkInfo, resourceMap) {
iteratorObj(apkInfo);
return apkInfo;
function iteratorObj(obj) {
for (const i in obj) {
if (isArray(obj[i])) {
iteratorArray(obj[i]);
} else if (isObject(obj[i])) {
iteratorObj(obj[i]);
} else if (isPrimitive(obj[i])) {
if (isResources(obj[i])) {
obj[i] = resourceMap[transKeyToMatchResourceMap(obj[i])];
}
}
}
}
function iteratorArray(array) {
const l = array.length;
for (let i = 0; i < l; i++) {
if (isArray(array[i])) {
iteratorArray(array[i]);
} else if (isObject(array[i])) {
iteratorObj(array[i]);
} else if (isPrimitive(array[i])) {
if (isResources(array[i])) {
array[i] = resourceMap[transKeyToMatchResourceMap(array[i])];
}
}
}
}
function isResources(attrValue) {
if (!attrValue) return false;
if (typeof attrValue !== 'string') {
attrValue = attrValue.toString();
}
return attrValue.indexOf('resourceId:') === 0;
}
function transKeyToMatchResourceMap(resourceId) {
return '@' + resourceId.replace('resourceId:0x', '').toUpperCase();
}
}
/**
* find .apk file's icon path from json info
* @param info // json info parsed from .apk file
*/
function findApkIconPath(info) {
if (!info.application.icon || !info.application.icon.splice) {
return '';
}
const rulesMap = {
mdpi: 48,
hdpi: 72,
xhdpi: 96,
xxdpi: 144,
xxxhdpi: 192,
};
const resultMap = {};
const maxDpiIcon = { dpi: 120, icon: '' };
for (const i in rulesMap) {
info.application.icon.some((icon) => {
if (icon && icon.indexOf(i) !== -1) {
resultMap['application-icon-' + rulesMap[i]] = icon;
return true;
}
});
// get the maximal size icon
if (
resultMap['application-icon-' + rulesMap[i]] &&
rulesMap[i] >= maxDpiIcon.dpi
) {
maxDpiIcon.dpi = rulesMap[i];
maxDpiIcon.icon = resultMap['application-icon-' + rulesMap[i]];
}
}
if (Object.keys(resultMap).length === 0 || !maxDpiIcon.icon) {
maxDpiIcon.dpi = 120;
maxDpiIcon.icon = info.application.icon[0] || '';
resultMap['applicataion-icon-120'] = maxDpiIcon.icon;
}
return maxDpiIcon.icon;
}
/**
* find .ipa file's icon path from json info
* @param info // json info parsed from .ipa file
*/
function findIpaIconPath(info) {
if (
info.CFBundleIcons &&
info.CFBundleIcons.CFBundlePrimaryIcon &&
info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles &&
info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles.length
) {
return info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles[
info.CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles.length - 1
];
} else if (info.CFBundleIconFiles && info.CFBundleIconFiles.length) {
return info.CFBundleIconFiles[info.CFBundleIconFiles.length - 1];
} else {
return '.app/Icon.png';
}
}
/**
* transform buffer to base64
* @param {Buffer} buffer
*/
function getBase64FromBuffer(buffer) {
return 'data:image/png;base64,' + buffer.toString('base64');
}
/**
* 去除unicode空字符
* @param {String} str
*/
function decodeNullUnicode(str) {
if (typeof str === 'string') {
// eslint-disable-next-line
str = str.replace(/\u0000/g, '');
}
return str;
}
module.exports = {
isArray,
isObject,
isPrimitive,
isBrowser,
mapInfoResource,
findApkIconPath,
findIpaIconPath,
getBase64FromBuffer,
decodeNullUnicode,
};

View File

@@ -0,0 +1,686 @@
// From https://github.com/openstf/adbkit-apkreader
const NodeType = {
ELEMENT_NODE: 1,
ATTRIBUTE_NODE: 2,
CDATA_SECTION_NODE: 4,
};
const ChunkType = {
NULL: 0x0000,
STRING_POOL: 0x0001,
TABLE: 0x0002,
XML: 0x0003,
XML_FIRST_CHUNK: 0x0100,
XML_START_NAMESPACE: 0x0100,
XML_END_NAMESPACE: 0x0101,
XML_START_ELEMENT: 0x0102,
XML_END_ELEMENT: 0x0103,
XML_CDATA: 0x0104,
XML_LAST_CHUNK: 0x017f,
XML_RESOURCE_MAP: 0x0180,
TABLE_PACKAGE: 0x0200,
TABLE_TYPE: 0x0201,
TABLE_TYPE_SPEC: 0x0202,
};
const StringFlags = {
SORTED: 1 << 0,
UTF8: 1 << 8,
};
// Taken from android.util.TypedValue
const TypedValue = {
COMPLEX_MANTISSA_MASK: 0x00ffffff,
COMPLEX_MANTISSA_SHIFT: 0x00000008,
COMPLEX_RADIX_0p23: 0x00000003,
COMPLEX_RADIX_16p7: 0x00000001,
COMPLEX_RADIX_23p0: 0x00000000,
COMPLEX_RADIX_8p15: 0x00000002,
COMPLEX_RADIX_MASK: 0x00000003,
COMPLEX_RADIX_SHIFT: 0x00000004,
COMPLEX_UNIT_DIP: 0x00000001,
COMPLEX_UNIT_FRACTION: 0x00000000,
COMPLEX_UNIT_FRACTION_PARENT: 0x00000001,
COMPLEX_UNIT_IN: 0x00000004,
COMPLEX_UNIT_MASK: 0x0000000f,
COMPLEX_UNIT_MM: 0x00000005,
COMPLEX_UNIT_PT: 0x00000003,
COMPLEX_UNIT_PX: 0x00000000,
COMPLEX_UNIT_SHIFT: 0x00000000,
COMPLEX_UNIT_SP: 0x00000002,
DENSITY_DEFAULT: 0x00000000,
DENSITY_NONE: 0x0000ffff,
TYPE_ATTRIBUTE: 0x00000002,
TYPE_DIMENSION: 0x00000005,
TYPE_FIRST_COLOR_INT: 0x0000001c,
TYPE_FIRST_INT: 0x00000010,
TYPE_FLOAT: 0x00000004,
TYPE_FRACTION: 0x00000006,
TYPE_INT_BOOLEAN: 0x00000012,
TYPE_INT_COLOR_ARGB4: 0x0000001e,
TYPE_INT_COLOR_ARGB8: 0x0000001c,
TYPE_INT_COLOR_RGB4: 0x0000001f,
TYPE_INT_COLOR_RGB8: 0x0000001d,
TYPE_INT_DEC: 0x00000010,
TYPE_INT_HEX: 0x00000011,
TYPE_LAST_COLOR_INT: 0x0000001f,
TYPE_LAST_INT: 0x0000001f,
TYPE_NULL: 0x00000000,
TYPE_REFERENCE: 0x00000001,
TYPE_STRING: 0x00000003,
};
class BinaryXmlParser {
constructor(buffer, options = {}) {
this.buffer = buffer;
this.cursor = 0;
this.strings = [];
this.resources = [];
this.document = null;
this.parent = null;
this.stack = [];
this.debug = options.debug || false;
}
readU8() {
this.debug && console.group('readU8');
this.debug && console.debug('cursor:', this.cursor);
const val = this.buffer[this.cursor];
this.debug && console.debug('value:', val);
this.cursor += 1;
this.debug && console.groupEnd();
return val;
}
readU16() {
this.debug && console.group('readU16');
this.debug && console.debug('cursor:', this.cursor);
const val = this.buffer.readUInt16LE(this.cursor);
this.debug && console.debug('value:', val);
this.cursor += 2;
this.debug && console.groupEnd();
return val;
}
readS32() {
this.debug && console.group('readS32');
this.debug && console.debug('cursor:', this.cursor);
const val = this.buffer.readInt32LE(this.cursor);
this.debug && console.debug('value:', val);
this.cursor += 4;
this.debug && console.groupEnd();
return val;
}
readU32() {
this.debug && console.group('readU32');
this.debug && console.debug('cursor:', this.cursor);
const val = this.buffer.readUInt32LE(this.cursor);
this.debug && console.debug('value:', val);
this.cursor += 4;
this.debug && console.groupEnd();
return val;
}
readLength8() {
this.debug && console.group('readLength8');
let len = this.readU8();
if (len & 0x80) {
len = (len & 0x7f) << 8;
len += this.readU8();
}
this.debug && console.debug('length:', len);
this.debug && console.groupEnd();
return len;
}
readLength16() {
this.debug && console.group('readLength16');
let len = this.readU16();
if (len & 0x8000) {
len = (len & 0x7fff) << 16;
len += this.readU16();
}
this.debug && console.debug('length:', len);
this.debug && console.groupEnd();
return len;
}
readDimension() {
this.debug && console.group('readDimension');
const dimension = {
value: null,
unit: null,
rawUnit: null,
};
const value = this.readU32();
const unit = dimension.value & 0xff;
dimension.value = value >> 8;
dimension.rawUnit = unit;
switch (unit) {
case TypedValue.COMPLEX_UNIT_MM:
dimension.unit = 'mm';
break;
case TypedValue.COMPLEX_UNIT_PX:
dimension.unit = 'px';
break;
case TypedValue.COMPLEX_UNIT_DIP:
dimension.unit = 'dp';
break;
case TypedValue.COMPLEX_UNIT_SP:
dimension.unit = 'sp';
break;
case TypedValue.COMPLEX_UNIT_PT:
dimension.unit = 'pt';
break;
case TypedValue.COMPLEX_UNIT_IN:
dimension.unit = 'in';
break;
}
this.debug && console.groupEnd();
return dimension;
}
readFraction() {
this.debug && console.group('readFraction');
const fraction = {
value: null,
type: null,
rawType: null,
};
const value = this.readU32();
const type = value & 0xf;
fraction.value = this.convertIntToFloat(value >> 4);
fraction.rawType = type;
switch (type) {
case TypedValue.COMPLEX_UNIT_FRACTION:
fraction.type = '%';
break;
case TypedValue.COMPLEX_UNIT_FRACTION_PARENT:
fraction.type = '%p';
break;
}
this.debug && console.groupEnd();
return fraction;
}
readHex24() {
this.debug && console.group('readHex24');
var val = (this.readU32() & 0xffffff).toString(16);
this.debug && console.groupEnd();
return val;
}
readHex32() {
this.debug && console.group('readHex32');
var val = this.readU32().toString(16);
this.debug && console.groupEnd();
return val;
}
readTypedValue() {
this.debug && console.group('readTypedValue');
const typedValue = {
value: null,
type: null,
rawType: null,
};
const start = this.cursor;
let size = this.readU16();
/* const zero = */ this.readU8();
const dataType = this.readU8();
// Yes, there has been a real world APK where the size is malformed.
if (size === 0) {
size = 8;
}
typedValue.rawType = dataType;
switch (dataType) {
case TypedValue.TYPE_INT_DEC:
typedValue.value = this.readS32();
typedValue.type = 'int_dec';
break;
case TypedValue.TYPE_INT_HEX:
typedValue.value = this.readS32();
typedValue.type = 'int_hex';
break;
case TypedValue.TYPE_STRING:
var ref = this.readS32();
typedValue.value = ref > 0 ? this.strings[ref] : '';
typedValue.type = 'string';
break;
case TypedValue.TYPE_REFERENCE:
var id = this.readU32();
typedValue.value = `resourceId:0x${id.toString(16)}`;
typedValue.type = 'reference';
break;
case TypedValue.TYPE_INT_BOOLEAN:
typedValue.value = this.readS32() !== 0;
typedValue.type = 'boolean';
break;
case TypedValue.TYPE_NULL:
this.readU32();
typedValue.value = null;
typedValue.type = 'null';
break;
case TypedValue.TYPE_INT_COLOR_RGB8:
typedValue.value = this.readHex24();
typedValue.type = 'rgb8';
break;
case TypedValue.TYPE_INT_COLOR_RGB4:
typedValue.value = this.readHex24();
typedValue.type = 'rgb4';
break;
case TypedValue.TYPE_INT_COLOR_ARGB8:
typedValue.value = this.readHex32();
typedValue.type = 'argb8';
break;
case TypedValue.TYPE_INT_COLOR_ARGB4:
typedValue.value = this.readHex32();
typedValue.type = 'argb4';
break;
case TypedValue.TYPE_DIMENSION:
typedValue.value = this.readDimension();
typedValue.type = 'dimension';
break;
case TypedValue.TYPE_FRACTION:
typedValue.value = this.readFraction();
typedValue.type = 'fraction';
break;
default: {
const type = dataType.toString(16);
console.debug(
`Not sure what to do with typed value of type 0x${type}, falling back to reading an uint32.`,
);
typedValue.value = this.readU32();
typedValue.type = 'unknown';
}
}
// Ensure we consume the whole value
const end = start + size;
if (this.cursor !== end) {
const type = dataType.toString(16);
const diff = end - this.cursor;
console.debug(`Cursor is off by ${diff} bytes at ${this.cursor} at supposed end \
of typed value of type 0x${type}. The typed value started at offset ${start} \
and is supposed to end at offset ${end}. Ignoring the rest of the value.`);
this.cursor = end;
}
this.debug && console.groupEnd();
return typedValue;
}
// https://twitter.com/kawasima/status/427730289201139712
convertIntToFloat(int) {
const buf = new ArrayBuffer(4);
new Int32Array(buf)[0] = int;
return new Float32Array(buf)[0];
}
readString(encoding) {
this.debug && console.group('readString', encoding);
switch (encoding) {
case 'utf-8':
var stringLength = this.readLength8(encoding);
this.debug && console.debug('stringLength:', stringLength);
var byteLength = this.readLength8(encoding);
this.debug && console.debug('byteLength:', byteLength);
var value = this.buffer.toString(
encoding,
this.cursor,
(this.cursor += byteLength),
);
this.debug && console.debug('value:', value);
this.debug && console.groupEnd();
return value;
case 'ucs2':
stringLength = this.readLength16(encoding);
this.debug && console.debug('stringLength:', stringLength);
byteLength = stringLength * 2;
this.debug && console.debug('byteLength:', byteLength);
value = this.buffer.toString(
encoding,
this.cursor,
(this.cursor += byteLength),
);
this.debug && console.debug('value:', value);
this.debug && console.groupEnd();
return value;
default:
throw new Error(`Unsupported encoding '${encoding}'`);
}
}
readChunkHeader() {
this.debug && console.group('readChunkHeader');
var header = {
startOffset: this.cursor,
chunkType: this.readU16(),
headerSize: this.readU16(),
chunkSize: this.readU32(),
};
this.debug && console.debug('startOffset:', header.startOffset);
this.debug && console.debug('chunkType:', header.chunkType);
this.debug && console.debug('headerSize:', header.headerSize);
this.debug && console.debug('chunkSize:', header.chunkSize);
this.debug && console.groupEnd();
return header;
}
readStringPool(header) {
this.debug && console.group('readStringPool');
header.stringCount = this.readU32();
this.debug && console.debug('stringCount:', header.stringCount);
header.styleCount = this.readU32();
this.debug && console.debug('styleCount:', header.styleCount);
header.flags = this.readU32();
this.debug && console.debug('flags:', header.flags);
header.stringsStart = this.readU32();
this.debug && console.debug('stringsStart:', header.stringsStart);
header.stylesStart = this.readU32();
this.debug && console.debug('stylesStart:', header.stylesStart);
if (header.chunkType !== ChunkType.STRING_POOL) {
throw new Error('Invalid string pool header');
}
const offsets = [];
for (let i = 0, l = header.stringCount; i < l; ++i) {
this.debug && console.debug('offset:', i);
offsets.push(this.readU32());
}
const sorted = (header.flags & StringFlags.SORTED) === StringFlags.SORTED;
this.debug && console.debug('sorted:', sorted);
const encoding =
(header.flags & StringFlags.UTF8) === StringFlags.UTF8 ? 'utf-8' : 'ucs2';
this.debug && console.debug('encoding:', encoding);
const stringsStart = header.startOffset + header.stringsStart;
this.cursor = stringsStart;
for (let i = 0, l = header.stringCount; i < l; ++i) {
this.debug && console.debug('string:', i);
this.debug && console.debug('offset:', offsets[i]);
this.cursor = stringsStart + offsets[i];
this.strings.push(this.readString(encoding));
}
// Skip styles
this.cursor = header.startOffset + header.chunkSize;
this.debug && console.groupEnd();
return null;
}
readResourceMap(header) {
this.debug && console.group('readResourceMap');
const count = Math.floor((header.chunkSize - header.headerSize) / 4);
for (let i = 0; i < count; ++i) {
this.resources.push(this.readU32());
}
this.debug && console.groupEnd();
return null;
}
readXmlNamespaceStart(/* header */) {
this.debug && console.group('readXmlNamespaceStart');
/* const line = */ this.readU32();
/* const commentRef = */ this.readU32();
/* const prefixRef = */ this.readS32();
/* const uriRef = */ this.readS32();
// We don't currently care about the values, but they could
// be accessed like so:
//
// namespaceURI.prefix = this.strings[prefixRef] // if prefixRef > 0
// namespaceURI.uri = this.strings[uriRef] // if uriRef > 0
this.debug && console.groupEnd();
return null;
}
readXmlNamespaceEnd(/* header */) {
this.debug && console.group('readXmlNamespaceEnd');
/* const line = */ this.readU32();
/* const commentRef = */ this.readU32();
/* const prefixRef = */ this.readS32();
/* const uriRef = */ this.readS32();
// We don't currently care about the values, but they could
// be accessed like so:
//
// namespaceURI.prefix = this.strings[prefixRef] // if prefixRef > 0
// namespaceURI.uri = this.strings[uriRef] // if uriRef > 0
this.debug && console.groupEnd();
return null;
}
readXmlElementStart(/* header */) {
this.debug && console.group('readXmlElementStart');
const node = {
namespaceURI: null,
nodeType: NodeType.ELEMENT_NODE,
nodeName: null,
attributes: [],
childNodes: [],
};
/* const line = */ this.readU32();
/* const commentRef = */ this.readU32();
const nsRef = this.readS32();
const nameRef = this.readS32();
if (nsRef > 0) {
node.namespaceURI = this.strings[nsRef];
}
node.nodeName = this.strings[nameRef];
/* const attrStart = */ this.readU16();
/* const attrSize = */ this.readU16();
const attrCount = this.readU16();
/* const idIndex = */ this.readU16();
/* const classIndex = */ this.readU16();
/* const styleIndex = */ this.readU16();
for (let i = 0; i < attrCount; ++i) {
node.attributes.push(this.readXmlAttribute());
}
if (this.document) {
this.parent.childNodes.push(node);
this.parent = node;
} else {
this.document = this.parent = node;
}
this.stack.push(node);
this.debug && console.groupEnd();
return node;
}
readXmlAttribute() {
this.debug && console.group('readXmlAttribute');
const attr = {
namespaceURI: null,
nodeType: NodeType.ATTRIBUTE_NODE,
nodeName: null,
name: null,
value: null,
typedValue: null,
};
const nsRef = this.readS32();
const nameRef = this.readS32();
const valueRef = this.readS32();
if (nsRef > 0) {
attr.namespaceURI = this.strings[nsRef];
}
attr.nodeName = attr.name = this.strings[nameRef];
if (valueRef > 0) {
// some apk have versionName with special characters
if (attr.name === 'versionName') {
// only keep printable characters
// https://www.ascii-code.com/characters/printable-characters
this.strings[valueRef] = this.strings[valueRef].replace(
/[^\x21-\x7E]/g,
'',
);
}
attr.value = this.strings[valueRef];
}
attr.typedValue = this.readTypedValue();
this.debug && console.groupEnd();
return attr;
}
readXmlElementEnd(/* header */) {
this.debug && console.group('readXmlCData');
/* const line = */ this.readU32();
/* const commentRef = */ this.readU32();
/* const nsRef = */ this.readS32();
/* const nameRef = */ this.readS32();
this.stack.pop();
this.parent = this.stack[this.stack.length - 1];
this.debug && console.groupEnd();
return null;
}
readXmlCData(/* header */) {
this.debug && console.group('readXmlCData');
const cdata = {
namespaceURI: null,
nodeType: NodeType.CDATA_SECTION_NODE,
nodeName: '#cdata',
data: null,
typedValue: null,
};
/* const line = */ this.readU32();
/* const commentRef = */ this.readU32();
const dataRef = this.readS32();
if (dataRef > 0) {
cdata.data = this.strings[dataRef];
}
cdata.typedValue = this.readTypedValue();
this.parent.childNodes.push(cdata);
this.debug && console.groupEnd();
return cdata;
}
readNull(header) {
this.debug && console.group('readNull');
this.cursor += header.chunkSize - header.headerSize;
this.debug && console.groupEnd();
return null;
}
parse() {
this.debug && console.group('BinaryXmlParser.parse');
const xmlHeader = this.readChunkHeader();
if (xmlHeader.chunkType !== ChunkType.XML) {
throw new Error('Invalid XML header');
}
while (this.cursor < this.buffer.length) {
this.debug && console.group('chunk');
const start = this.cursor;
const header = this.readChunkHeader();
switch (header.chunkType) {
case ChunkType.STRING_POOL:
this.readStringPool(header);
break;
case ChunkType.XML_RESOURCE_MAP:
this.readResourceMap(header);
break;
case ChunkType.XML_START_NAMESPACE:
this.readXmlNamespaceStart(header);
break;
case ChunkType.XML_END_NAMESPACE:
this.readXmlNamespaceEnd(header);
break;
case ChunkType.XML_START_ELEMENT:
this.readXmlElementStart(header);
break;
case ChunkType.XML_END_ELEMENT:
this.readXmlElementEnd(header);
break;
case ChunkType.XML_CDATA:
this.readXmlCData(header);
break;
case ChunkType.NULL:
this.readNull(header);
break;
default:
throw new Error(`Unsupported chunk type '${header.chunkType}'`);
}
// Ensure we consume the whole chunk
const end = start + header.chunkSize;
if (this.cursor !== end) {
const diff = end - this.cursor;
const type = header.chunkType.toString(16);
console.debug(`Cursor is off by ${diff} bytes at ${this.cursor} at supposed \
end of chunk of type 0x${type}. The chunk started at offset ${start} and is \
supposed to end at offset ${end}. Ignoring the rest of the chunk.`);
this.cursor = end;
}
this.debug && console.groupEnd();
}
this.debug && console.groupEnd();
return this.document;
}
}
module.exports = BinaryXmlParser;

View File

@@ -0,0 +1,224 @@
// From https://github.com/openstf/adbkit-apkreader
const BinaryXmlParser = require('./binary');
const INTENT_MAIN = 'android.intent.action.MAIN';
const CATEGORY_LAUNCHER = 'android.intent.category.LAUNCHER';
class ManifestParser {
constructor(buffer, options = {}) {
this.buffer = buffer;
this.xmlParser = new BinaryXmlParser(this.buffer, options);
}
collapseAttributes(element) {
const collapsed = Object.create(null);
for (const attr of Array.from(element.attributes)) {
collapsed[attr.name] = attr.typedValue.value;
}
return collapsed;
}
parseIntents(element, target) {
target.intentFilters = [];
target.metaData = [];
return element.childNodes.forEach((element) => {
switch (element.nodeName) {
case 'intent-filter': {
const intentFilter = this.collapseAttributes(element);
intentFilter.actions = [];
intentFilter.categories = [];
intentFilter.data = [];
element.childNodes.forEach((element) => {
switch (element.nodeName) {
case 'action':
intentFilter.actions.push(this.collapseAttributes(element));
break;
case 'category':
intentFilter.categories.push(this.collapseAttributes(element));
break;
case 'data':
intentFilter.data.push(this.collapseAttributes(element));
break;
}
});
target.intentFilters.push(intentFilter);
break;
}
case 'meta-data':
target.metaData.push(this.collapseAttributes(element));
break;
}
});
}
parseApplication(element) {
const app = this.collapseAttributes(element);
app.activities = [];
app.activityAliases = [];
app.launcherActivities = [];
app.services = [];
app.receivers = [];
app.providers = [];
app.usesLibraries = [];
app.metaData = [];
element.childNodes.forEach((element) => {
switch (element.nodeName) {
case 'activity': {
const activity = this.collapseAttributes(element);
this.parseIntents(element, activity);
app.activities.push(activity);
if (this.isLauncherActivity(activity)) {
app.launcherActivities.push(activity);
}
break;
}
case 'activity-alias': {
const activityAlias = this.collapseAttributes(element);
this.parseIntents(element, activityAlias);
app.activityAliases.push(activityAlias);
if (this.isLauncherActivity(activityAlias)) {
app.launcherActivities.push(activityAlias);
}
break;
}
case 'service': {
const service = this.collapseAttributes(element);
this.parseIntents(element, service);
app.services.push(service);
break;
}
case 'receiver': {
const receiver = this.collapseAttributes(element);
this.parseIntents(element, receiver);
app.receivers.push(receiver);
break;
}
case 'provider': {
const provider = this.collapseAttributes(element);
provider.grantUriPermissions = [];
provider.metaData = [];
provider.pathPermissions = [];
element.childNodes.forEach((element) => {
switch (element.nodeName) {
case 'grant-uri-permission':
provider.grantUriPermissions.push(
this.collapseAttributes(element),
);
break;
case 'meta-data':
provider.metaData.push(this.collapseAttributes(element));
break;
case 'path-permission':
provider.pathPermissions.push(this.collapseAttributes(element));
break;
}
});
app.providers.push(provider);
break;
}
case 'uses-library':
app.usesLibraries.push(this.collapseAttributes(element));
break;
case 'meta-data':
app.metaData.push(this.collapseAttributes(element));
break;
}
});
return app;
}
isLauncherActivity(activity) {
return activity.intentFilters.some((filter) => {
const hasMain = filter.actions.some(
(action) => action.name === INTENT_MAIN,
);
if (!hasMain) {
return false;
}
return filter.categories.some(
(category) => category.name === CATEGORY_LAUNCHER,
);
});
}
parse() {
const document = this.xmlParser.parse();
const manifest = this.collapseAttributes(document);
manifest.usesPermissions = [];
manifest.usesPermissionsSDK23 = [];
manifest.permissions = [];
manifest.permissionTrees = [];
manifest.permissionGroups = [];
manifest.instrumentation = null;
manifest.usesSdk = null;
manifest.usesConfiguration = null;
manifest.usesFeatures = [];
manifest.supportsScreens = null;
manifest.compatibleScreens = [];
manifest.supportsGlTextures = [];
manifest.application = Object.create(null);
document.childNodes.forEach((element) => {
switch (element.nodeName) {
case 'uses-permission':
manifest.usesPermissions.push(this.collapseAttributes(element));
break;
case 'uses-permission-sdk-23':
manifest.usesPermissionsSDK23.push(this.collapseAttributes(element));
break;
case 'permission':
manifest.permissions.push(this.collapseAttributes(element));
break;
case 'permission-tree':
manifest.permissionTrees.push(this.collapseAttributes(element));
break;
case 'permission-group':
manifest.permissionGroups.push(this.collapseAttributes(element));
break;
case 'instrumentation':
manifest.instrumentation = this.collapseAttributes(element);
break;
case 'uses-sdk':
manifest.usesSdk = this.collapseAttributes(element);
break;
case 'uses-configuration':
manifest.usesConfiguration = this.collapseAttributes(element);
break;
case 'uses-feature':
manifest.usesFeatures.push(this.collapseAttributes(element));
break;
case 'supports-screens':
manifest.supportsScreens = this.collapseAttributes(element);
break;
case 'compatible-screens':
element.childNodes.forEach((screen) => {
return manifest.compatibleScreens.push(
this.collapseAttributes(screen),
);
});
break;
case 'supports-gl-texture':
manifest.supportsGlTextures.push(this.collapseAttributes(element));
break;
case 'application':
manifest.application = this.parseApplication(element);
break;
}
});
return manifest;
}
}
module.exports = ManifestParser;

View File

@@ -0,0 +1,66 @@
const Unzip = require('isomorphic-unzip');
const { isBrowser, decodeNullUnicode } = require('./utils');
const { enumZipEntries, readEntry } = require('../../bundle');
class Zip {
constructor(file) {
if (isBrowser()) {
if (!(file instanceof window.Blob || typeof file.size !== 'undefined')) {
throw new Error(
'Param error: [file] must be an instance of Blob or File in browser.',
);
}
this.file = file;
} else {
if (typeof file !== 'string') {
throw new Error('Param error: [file] must be file path in Node.');
}
this.file = require('path').resolve(file);
}
this.unzip = new Unzip(this.file);
}
/**
* get entries by regexps, the return format is: { <filename>: <Buffer|Blob> }
* @param {Array} regexps // regexps for matching files
* @param {String} type // return type, can be buffer or blob, default buffer
*/
getEntries(regexps, type = 'buffer') {
regexps = regexps.map((regex) => decodeNullUnicode(regex));
return new Promise((resolve, reject) => {
this.unzip.getBuffer(regexps, { type }, (err, buffers) => {
err ? reject(err) : resolve(buffers);
});
});
}
/**
* get entry by regex, return an instance of Buffer or Blob
* @param {Regex} regex // regex for matching file
* @param {String} type // return type, can be buffer or blob, default buffer
*/
getEntry(regex, type = 'buffer') {
regex = decodeNullUnicode(regex);
return new Promise((resolve, reject) => {
this.unzip.getBuffer([regex], { type }, (err, buffers) => {
// console.log(buffers);
err ? reject(err) : resolve(buffers[regex]);
});
});
}
async getEntryFromHarmonyApp(regex) {
try {
let originSource;
await enumZipEntries(this.file, (entry, zipFile) => {
if (regex.test(entry.fileName)) {
return readEntry(entry, zipFile).then((v) => (originSource = v));
}
});
return originSource;
} catch (error) {
console.error('Error in getEntryFromHarmonyApp:', error);
}
}
}
module.exports = Zip;

View File

@@ -0,0 +1,89 @@
import fs from 'fs';
import path from 'path';
import { t } from './i18n';
const lockFiles = [
'package-lock.json',
'yarn.lock',
'pnpm-lock.yaml',
'bun.lockb',
'bun.lock',
];
// Function to check if a package.json has a workspaces field
function hasWorkspaces(dir: string): boolean {
const pkgPath = path.join(dir, 'package.json');
if (fs.existsSync(pkgPath)) {
try {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
return !!pkg.workspaces;
} catch (e) {
// Ignore parsing errors
}
}
return false;
}
// Helper function to find lock files in a specific directory
function findLockFilesInDir(directory: string): string[] {
const found: string[] = [];
for (const file of lockFiles) {
const filePath = path.join(directory, file);
if (fs.existsSync(filePath)) {
found.push(filePath);
}
}
return found;
}
export function checkLockFiles() {
const cwd = process.cwd();
let searchDir = cwd;
let foundLockFiles = findLockFilesInDir(searchDir);
// If no lock file in cwd, try to find monorepo root and check there
if (foundLockFiles.length === 0) {
// Search upwards for package.json with workspaces
let currentDir = path.dirname(cwd); // Start searching from parent
let projectRootDir: string | null = null;
while (true) {
if (hasWorkspaces(currentDir)) {
projectRootDir = currentDir;
break;
}
const parentDir = path.dirname(currentDir);
if (parentDir === currentDir) {
// Reached the filesystem root
break;
}
currentDir = parentDir;
}
// If a potential root was found, switch search directory and re-check
if (projectRootDir) {
searchDir = projectRootDir;
foundLockFiles = findLockFilesInDir(searchDir);
}
// If no projectRootDir found, foundLockFiles remains empty and searchDir remains cwd
}
// Handle results based on findings in the final searchDir
if (foundLockFiles.length === 1) {
// Successfully found one lock file in the determined searchDir
return;
}
if (foundLockFiles.length > 1) {
// Found multiple lock files in the determined searchDir
console.warn(t('lockBestPractice'));
throw new Error(
t('multipleLocksFound', { lockFiles: foundLockFiles.join(', ') }),
);
}
// If we reach here, foundLockFiles.length === 0
console.warn(t('lockBestPractice'));
// Warn instead of throwing an error if no lock file is found
console.warn(t('lockNotFound'));
}

31
src/utils/check-plugin.ts Normal file
View File

@@ -0,0 +1,31 @@
import { t } from './i18n';
import { plugins } from './plugin-config';
interface BundleParams {
sentry: boolean;
sourcemap: boolean;
[key: string]: any;
}
export async function checkPlugins(): Promise<BundleParams> {
const params: BundleParams = {
sentry: false,
sourcemap: false,
};
for (const plugin of plugins) {
try {
const isEnabled = await plugin.detect();
if (isEnabled && plugin.bundleParams) {
Object.assign(params, plugin.bundleParams);
console.log(t('pluginDetected', { name: plugin.name }));
}
} catch (err) {
console.warn(
t('pluginDetectionError', { name: plugin.name, error: err }),
);
}
}
return params;
}

15
src/utils/constants.ts Normal file
View File

@@ -0,0 +1,15 @@
import path from 'path';
const scriptName = path.basename(process.argv[1]) as 'cresc' | 'pushy';
export const IS_CRESC = scriptName === 'cresc';
export const credentialFile = IS_CRESC ? '.cresc.token' : '.update';
export const updateJson = IS_CRESC ? 'cresc.config.json' : 'update.json';
export const tempDir = IS_CRESC ? '.cresc.temp' : '.pushy';
export const pricingPageUrl = IS_CRESC
? 'https://cresc.dev/pricing'
: 'https://pushy.reactnative.cn/pricing.html';
export const defaultEndpoints = IS_CRESC
? ['https://api.cresc.dev', 'https://api.cresc.app']
: ['https://update.reactnative.cn/api', 'https://update.react-native.cn/api'];

37
src/utils/dep-versions.ts Normal file
View File

@@ -0,0 +1,37 @@
let currentPackage = null;
try {
currentPackage = require(`${process.cwd()}/package.json`);
} catch (e) {
// console.warn('No package.json file were found');
}
const _depVersions: Record<string, string> = {};
if (currentPackage) {
const depKeys = currentPackage.dependencies
? Object.keys(currentPackage.dependencies)
: [];
const devDepKeys = currentPackage.devDependencies
? Object.keys(currentPackage.devDependencies)
: [];
const dedupedDeps = [...new Set([...depKeys, ...devDepKeys])];
for (const dep of dedupedDeps) {
try {
const packageJsonPath = require.resolve(`${dep}/package.json`, {
paths: [process.cwd()],
});
const version = require(packageJsonPath).version;
_depVersions[dep] = version;
} catch (e) {}
}
}
export const depVersions = Object.keys(_depVersions)
.sort() // Sort the keys alphabetically
.reduce((obj, key) => {
obj[key] = _depVersions[key]; // Rebuild the object with sorted keys
return obj;
}, {} as Record<string, string>);
// console.log({ depVersions });

50
src/utils/git.ts Normal file
View File

@@ -0,0 +1,50 @@
import fs from 'fs';
import path from 'path';
import git from 'isomorphic-git';
export interface CommitInfo {
hash: string;
message: string;
author: string;
timestamp: string;
origin: string;
}
function findGitRoot(dir = process.cwd()) {
const gitRoot = fs.readdirSync(dir).find((dir) => dir === '.git');
if (gitRoot) {
// console.log({ gitRoot });
return path.join(dir, gitRoot);
}
const parentDir = path.dirname(dir);
if (parentDir === dir) {
return null;
}
return findGitRoot(parentDir);
}
const gitRoot = findGitRoot();
export async function getCommitInfo(): Promise<CommitInfo | undefined> {
if (!gitRoot) {
return;
}
try {
const remotes = await git.listRemotes({ fs, gitdir: gitRoot });
const origin =
remotes.find((remote) => remote.remote === 'origin') || remotes[0];
const { commit, oid } = (
await git.log({ fs, gitdir: gitRoot, depth: 1 })
)[0];
return {
hash: oid,
message: commit.message,
author: commit.author.name || commit.committer.name,
timestamp: String(commit.committer.timestamp),
origin: origin?.url,
};
} catch (error) {
console.error(error);
return;
}
}

83
src/utils/http-helper.ts Normal file
View File

@@ -0,0 +1,83 @@
import { defaultEndpoints } from './constants';
// const baseUrl = `http://localhost:9000`;
// let baseUrl = SERVER.main[0];
// const baseUrl = `https://p.reactnative.cn/api`;
export function promiseAny<T>(promises: Promise<T>[]) {
return new Promise<T>((resolve, reject) => {
let count = 0;
for (const promise of promises) {
Promise.resolve(promise)
.then(resolve)
.catch(() => {
count++;
if (count === promises.length) {
reject(new Error('All promises were rejected'));
}
});
}
});
}
export const ping = async (url: string) => {
let pingFinished = false;
return Promise.race([
fetch(url, {
method: 'HEAD',
})
.then(({ status, statusText }) => {
pingFinished = true;
if (status === 200) {
// console.log('ping success', url);
return url;
}
// console.log('ping failed', url, status, statusText);
throw new Error('ping failed');
})
.catch((e) => {
pingFinished = true;
// console.log('ping error', url, e);
throw new Error('ping error');
}),
new Promise((_, reject) =>
setTimeout(() => {
reject(new Error('ping timeout'));
if (!pingFinished) {
// console.log('ping timeout', url);
}
}, 2000),
),
]) as Promise<string | null>;
};
export const testUrls = async (urls?: string[]) => {
if (!urls?.length) {
return null;
}
const ret = await promiseAny(urls.map(ping));
if (ret) {
return ret;
}
// console.log('all ping failed, use first url:', urls[0]);
return urls[0];
};
export const getBaseUrl = (async () => {
const testEndpoint = process.env.PUSHY_REGISTRY || process.env.RNU_API;
if (testEndpoint) {
return testEndpoint;
}
return testUrls(defaultEndpoints.map((url) => `${url}/status`)).then(
(ret) => {
let baseUrl = defaultEndpoints[0];
if (ret) {
// remove /status
baseUrl = ret.replace('/status', '');
}
// console.log('baseUrl', baseUrl);
return baseUrl;
},
);
})();

39
src/utils/i18n.ts Normal file
View File

@@ -0,0 +1,39 @@
import i18next from 'i18next';
import en from '../locales/en';
import zh from '../locales/zh';
import { IS_CRESC } from './constants';
i18next.init({
lng: IS_CRESC ? 'en' : 'zh',
// debug: process.env.NODE_ENV !== 'production',
// debug: true,
resources: {
en: {
translation: en,
},
zh: {
translation: zh,
},
},
interpolation: {
escapeValue: false,
},
});
declare module 'i18next' {
// Extend CustomTypeOptions
interface CustomTypeOptions {
// custom namespace type, if you changed it
defaultNS: 'en';
// custom resources type
resources: {
en: typeof en;
zh: typeof zh;
};
// other
}
}
export function t(key: string, options?: any): string {
return i18next.t(key as any, options);
}

View File

@@ -1,154 +0,0 @@
/**
* Created by tdzl2003 on 2/13/16.
*/
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
const pkg = require('../../package.json');
const AppInfoParser = require('app-info-parser');
var read = require('read');
export function question(query, password) {
if (NO_INTERACTIVE) {
return Promise.resolve('');
}
return new Promise((resolve, reject) =>
read(
{
prompt: query,
silent: password,
replace: password ? '*' : undefined,
},
(err, result) => (err ? reject(err) : resolve(result)),
),
);
}
export function translateOptions(options) {
const ret = {};
for (let key in options) {
const v = options[key];
if (typeof v === 'string') {
ret[key] = v.replace(/\$\{(\w+)\}/g, function (v, n) {
return options[n] || process.env[n] || v;
});
} else {
ret[key] = v;
}
}
return ret;
}
export function getRNVersion() {
const version = JSON.parse(
fs.readFileSync(path.resolve('node_modules/react-native/package.json')),
).version;
// We only care about major and minor version.
const match = /^(\d+)\.(\d+)\./.exec(version);
return {
version,
major: match[1] | 0,
minor: match[2] | 0,
};
}
export async function getApkInfo(fn) {
const appInfoParser = new AppInfoParser(fn);
const bundleFile = await appInfoParser.parser.getEntry(
/assets\/index.android.bundle/,
);
if (!bundleFile) {
throw new Error(
'找不到bundle文件。请确保此apk为release版本且bundle文件名为默认的index.android.bundle',
);
}
const updateJsonFile = await appInfoParser.parser.getEntry(
/res\/raw\/update.json/,
);
let appCredential = {};
if (updateJsonFile) {
appCredential = JSON.parse(updateJsonFile.toString()).android;
}
const { versionName, application } = await appInfoParser.parse();
let buildTime = 0;
if (Array.isArray(application.metaData)) {
for (const meta of application.metaData) {
if (meta.name === 'pushy_build_time') {
buildTime = meta.value[0];
}
}
}
if (buildTime == 0) {
throw new Error(
'无法获取此包的编译时间戳。请更新react-native-update到最新版本后重新打包上传。',
);
}
return { versionName, buildTime, ...appCredential };
}
export async function getIpaInfo(fn) {
const appInfoParser = new AppInfoParser(fn);
const bundleFile = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/main.jsbundle/,
);
if (!bundleFile) {
throw new Error(
'找不到bundle文件。请确保此ipa为release版本且bundle文件名为默认的main.jsbundle',
);
}
const updateJsonFile = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/assets\/update.json/,
);
let appCredential = {};
if (updateJsonFile) {
appCredential = JSON.parse(updateJsonFile.toString()).ios;
}
const {
CFBundleShortVersionString: versionName,
} = await appInfoParser.parse();
let buildTimeTxtBuffer = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/pushy_build_time.txt/,
);
if (!buildTimeTxtBuffer) {
// Not in root bundle when use `use_frameworks`
buildTimeTxtBuffer = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/frameworks\/react_native_update.framework\/pushy_build_time.txt/,
);
}
if (!buildTimeTxtBuffer) {
throw new Error(
'无法获取此包的编译时间戳。请更新react-native-update到最新版本后重新打包上传。',
);
}
const buildTime = buildTimeTxtBuffer.toString().replace('\n', '');
return { versionName, buildTime, ...appCredential };
}
const localDir = path.resolve(os.homedir(), '.pushy');
fs.ensureDirSync(localDir);
export function saveToLocal(originPath, destName) {
// TODO
// const destPath = path.join(localDir, destName);
// fs.ensureDirSync(path.dirname(destPath));
// fs.copyFileSync(originPath, destPath);
}
export function printVersionCommand() {
console.log('react-native-update-cli: ' + pkg.version);
try {
const PACKAGE_JSON_PATH = path.resolve(
process.cwd(),
'node_modules',
'react-native-update',
'package.json',
);
console.log('react-native-update: ' + require(PACKAGE_JSON_PATH).version);
} catch (e) {
console.log('react-native-update: 无法获取版本号,请在项目目录中运行命令');
}
}
export const pricingPageUrl = 'https://pushy.reactnative.cn/pricing.html';

218
src/utils/index.ts Normal file
View File

@@ -0,0 +1,218 @@
import os from 'os';
import path from 'path';
import chalk from 'chalk';
import { satisfies } from 'compare-versions';
import fs from 'fs-extra';
import pkg from '../../package.json';
import latestVersion from '../utils/latest-version';
import AppInfoParser from './app-info-parser';
import { checkPlugins } from './check-plugin';
import { read } from 'read';
import { IS_CRESC, tempDir } from './constants';
import { depVersions } from './dep-versions';
import { t } from './i18n';
export async function question(query: string, password?: boolean) {
if (NO_INTERACTIVE) {
return '';
}
return read({
prompt: query,
silent: password,
replace: password ? '*' : undefined,
});
}
export function translateOptions(options: Record<string, string>) {
const ret: Record<string, string> = {};
for (const key in options) {
const v = options[key];
if (typeof v === 'string') {
ret[key] = v.replace(
/\$\{(\w+)\}/g,
(v, n) => options[n] || process.env[n] || v,
);
} else {
ret[key] = v;
}
}
return ret;
}
export async function getApkInfo(fn: string) {
const appInfoParser = new AppInfoParser(fn);
const bundleFile = await appInfoParser.parser.getEntry(
/assets\/index.android.bundle/,
);
if (!bundleFile) {
throw new Error(
t('bundleNotFound', {
packageType: 'apk',
entryFile: 'index.android.bundle',
}),
);
}
const updateJsonFile = await appInfoParser.parser.getEntry(
/res\/raw\/update.json/,
);
let appCredential = {};
if (updateJsonFile) {
appCredential = JSON.parse(updateJsonFile.toString()).android;
}
const { versionName, application } = await appInfoParser.parse();
let buildTime = 0;
if (Array.isArray(application.metaData)) {
for (const meta of application.metaData) {
if (meta.name === 'pushy_build_time') {
buildTime = meta.value[0];
}
}
}
if (buildTime == 0) {
throw new Error(t('buildTimeNotFound'));
}
return { versionName, buildTime, ...appCredential };
}
export async function getAppInfo(fn: string) {
const appInfoParser = new AppInfoParser(fn);
const bundleFile = await appInfoParser.parser.getEntryFromHarmonyApp(
/rawfile\/bundle.harmony.js/,
);
if (!bundleFile) {
throw new Error(
t('bundleNotFound', {
packageType: 'app',
entryFile: 'bundle.harmony.js',
}),
);
}
const updateJsonFile =
await appInfoParser.parser.getEntryFromHarmonyApp(/rawfile\/update.json/);
let appCredential = {};
if (updateJsonFile) {
appCredential = JSON.parse(updateJsonFile.toString()).harmony;
}
const metaJsonFile =
await appInfoParser.parser.getEntryFromHarmonyApp(/rawfile\/meta.json/);
let metaData: Record<string, any> = {};
if (metaJsonFile) {
metaData = JSON.parse(metaJsonFile.toString());
}
const { versionName, pushy_build_time } = metaData;
let buildTime = 0;
if (pushy_build_time) {
buildTime = pushy_build_time;
}
if (buildTime == 0) {
throw new Error(t('buildTimeNotFound'));
}
return { versionName, buildTime, ...appCredential };
}
export async function getIpaInfo(fn: string) {
const appInfoParser = new AppInfoParser(fn);
const bundleFile = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/main.jsbundle/,
);
if (!bundleFile) {
throw new Error(
t('bundleNotFound', {
packageType: 'ipa',
entryFile: 'main.jsbundle',
}),
);
}
const updateJsonFile = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/assets\/update.json/,
);
let appCredential = {};
if (updateJsonFile) {
appCredential = JSON.parse(updateJsonFile.toString()).ios;
}
const { CFBundleShortVersionString: versionName } =
await appInfoParser.parse();
let buildTimeTxtBuffer = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/pushy_build_time.txt/,
);
if (!buildTimeTxtBuffer) {
// Not in root bundle when use `use_frameworks`
buildTimeTxtBuffer = await appInfoParser.parser.getEntry(
/payload\/.+?\.app\/frameworks\/react_native_update.framework\/pushy_build_time.txt/,
);
}
if (!buildTimeTxtBuffer) {
throw new Error(t('buildTimeNotFound'));
}
const buildTime = buildTimeTxtBuffer.toString().replace('\n', '');
return { versionName, buildTime, ...appCredential };
}
const localDir = path.resolve(os.homedir(), tempDir);
fs.ensureDirSync(localDir);
export function saveToLocal(originPath: string, destName: string) {
// TODO
// const destPath = path.join(localDir, destName);
// fs.ensureDirSync(path.dirname(destPath));
// fs.copyFileSync(originPath, destPath);
}
async function getLatestVersion(pkgNames: string[]) {
return latestVersion(pkgNames, {
// useCache: true,
requestOptions: {
timeout: 2000,
},
})
.then((pkgs) => pkgs.map((pkg) => pkg.latest))
.catch(() => []);
}
export async function printVersionCommand() {
let [latestRnuCliVersion, latestRnuVersion] = await getLatestVersion([
'react-native-update-cli',
'react-native-update',
]);
latestRnuCliVersion = latestRnuCliVersion
? ` ${t('latestVersionTag', {
version: chalk.green(latestRnuCliVersion),
})}`
: '';
console.log(`react-native-update-cli: ${pkg.version}${latestRnuCliVersion}`);
const rnuVersion = depVersions['react-native-update'];
if (rnuVersion) {
latestRnuVersion = latestRnuVersion
? ` ${t('latestVersionTag', { version: chalk.green(latestRnuVersion) })}`
: '';
console.log(`react-native-update: ${rnuVersion}${latestRnuVersion}`);
if (IS_CRESC) {
if (satisfies(rnuVersion, '<10.27.0')) {
console.error(
'Unsupported version, please update to the latest version: npm i react-native-update@latest',
);
process.exit(1);
}
} else {
if (satisfies(rnuVersion, '<8.5.2')) {
console.warn(
`当前版本已不再支持,请至少升级到 v8 的最新小版本后重新打包(代码无需改动): npm i react-native-update@8 .
如有使用安装 apk 的功能,请注意添加所需权限 https://pushy.reactnative.cn/docs/api#async-function-downloadandinstallapkurl`,
);
} else if (satisfies(rnuVersion, '9.0.0 - 9.2.1')) {
console.warn(
`当前版本已不再支持,请至少升级到 v9 的最新小版本后重新打包(代码无需改动,可直接热更): npm i react-native-update@9 .
如有使用安装 apk 的功能,请注意添加所需权限 https://pushy.reactnative.cn/docs/api#async-function-downloadandinstallapkurl`,
);
} else if (satisfies(rnuVersion, '10.0.0 - 10.17.0')) {
console.warn(
'当前版本已不再支持,请升级到 v10 的最新小版本(代码无需改动,可直接热更): npm i react-native-update@10',
);
}
}
} else {
console.log(t('rnuVersionNotFound'));
}
}
export { checkPlugins };

View File

@@ -0,0 +1,443 @@
import { existsSync, readFileSync } from 'fs';
import { dirname } from 'path';
import {
blue,
bold,
cyan,
gray,
green,
italic,
magenta,
red,
reset,
strip,
underline,
yellow,
} from '@colors/colors/safe';
import semverDiff from 'semver/functions/diff';
import semverMajor from 'semver/functions/major';
import latestVersion, {
type Package,
type PackageJson,
type LatestVersionPackage,
type LatestVersionOptions,
} from '.';
interface TableColumn {
label: string;
attrName: keyof TableRow;
align: 'left' | 'center' | 'right';
maxLength: number;
items: string[];
}
type TableRowGroup =
| 'patch'
| 'minor'
| 'major'
| 'majorVersionZero'
| 'unknown';
interface TableRow {
name: string;
location: string;
installed: string;
tagOrRange: string;
separator: string;
wanted: string;
latest: string;
group: TableRowGroup;
}
const colorizeDiff = (from: string, to: string): string => {
const toParts = to.split('.');
const diffIndex = from.split('.').findIndex((part, i) => part !== toParts[i]);
if (diffIndex !== -1) {
let color = magenta;
if (toParts[0] !== '0') {
color = diffIndex === 0 ? red : diffIndex === 1 ? cyan : green;
}
const start = toParts.slice(0, diffIndex).join('.');
const mid = diffIndex === 0 ? '' : '.';
const end = color(toParts.slice(diffIndex).join('.'));
return `${start}${mid}${end}`;
}
return to;
};
const columnCellRenderer = (column: TableColumn, row: TableRow): string => {
let text = row[column.attrName];
const gap =
text.length < column.maxLength
? ' '.repeat(column.maxLength - text.length)
: '';
switch (column.attrName) {
case 'name':
text = yellow(text);
break;
case 'installed':
case 'separator':
text = blue(text);
break;
case 'location':
case 'tagOrRange':
text = gray(text);
break;
case 'wanted':
text = colorizeDiff(row.installed, text);
break;
case 'latest':
if (text !== row.wanted) {
text = colorizeDiff(row.installed, text);
}
break;
default:
break;
}
return column.align === 'right' ? `${gap}${text}` : `${text}${gap}`;
};
const columnHeaderRenderer = (column: TableColumn): string => {
const text = column.label;
const gap =
text.length < column.maxLength
? ' '.repeat(column.maxLength - text.length)
: '';
return column.align === 'right'
? `${gap}${underline(text)}`
: `${underline(text)}${gap}`;
};
const drawBox = (
lines: string[],
color = yellow,
horizontalPadding = 3,
): void => {
const maxLineWidth = lines.reduce(
(max, row) => Math.max(max, strip(row).length),
0,
);
console.log(color(`${'─'.repeat(maxLineWidth + horizontalPadding * 2)}`));
lines.forEach((row) => {
const padding = ' '.repeat(horizontalPadding);
const fullRow = `${row}${' '.repeat(maxLineWidth - strip(row).length)}`;
console.log(
`${color('│')}${padding}${reset(fullRow)}${padding}${color('│')}`,
);
});
console.log(color(`${'─'.repeat(maxLineWidth + horizontalPadding * 2)}`));
};
const getTableColumns = (rows: TableRow[]): TableColumn[] => {
const columns: TableColumn[] = [
{
label: 'Package',
attrName: 'name',
align: 'left',
maxLength: 0,
items: [],
},
{
label: 'Location',
attrName: 'location',
align: 'left',
maxLength: 0,
items: [],
},
{
label: 'Installed',
attrName: 'installed',
align: 'right',
maxLength: 0,
items: [],
},
{
label: '',
attrName: 'separator',
align: 'center',
maxLength: 0,
items: [],
},
{
label: 'Range',
attrName: 'tagOrRange',
align: 'right',
maxLength: 0,
items: [],
},
{
label: '',
attrName: 'separator',
align: 'center',
maxLength: 0,
items: [],
},
{
label: 'Wanted',
attrName: 'wanted',
align: 'right',
maxLength: 0,
items: [],
},
{
label: 'Latest',
attrName: 'latest',
align: 'right',
maxLength: 0,
items: [],
},
];
rows.forEach((row) => {
columns.forEach((column) => {
column.maxLength = Math.max(
column.label.length,
column.maxLength,
row[column.attrName].length || 0,
);
});
});
return columns;
};
const getTableRows = (updates: LatestVersionPackage[]): TableRow[] => {
return updates.reduce<TableRow[]>((all, pkg) => {
const {
name,
latest,
local,
globalNpm,
globalYarn,
wantedTagOrRange,
updatesAvailable,
} = pkg;
const getGroup = (a?: string, b?: string): TableRowGroup => {
if (b && semverMajor(b) === 0) {
return 'majorVersionZero';
} else if (a && b) {
const releaseType = semverDiff(a, b) ?? '';
if (['major', 'premajor', 'prerelease'].includes(releaseType)) {
return 'major';
} else if (['minor', 'preminor'].includes(releaseType)) {
return 'minor';
} else if (['patch', 'prepatch'].includes(releaseType)) {
return 'patch';
}
}
return 'unknown';
};
const add = (
group: TableRowGroup,
location: string,
installed?: string,
wanted?: string,
) =>
all.push({
name: ' ' + name,
location,
installed: installed ?? 'unknown',
latest: latest ?? 'unknown',
tagOrRange: wantedTagOrRange ?? 'unknown',
separator: '→',
wanted: wanted ?? 'unknown',
group,
});
if (updatesAvailable) {
if (updatesAvailable.local) {
add(
getGroup(local, updatesAvailable.local),
'local',
local,
updatesAvailable.local,
);
}
if (updatesAvailable.globalNpm) {
add(
getGroup(globalNpm, updatesAvailable.globalNpm),
'NPM global',
globalNpm,
updatesAvailable.globalNpm,
);
}
if (updatesAvailable.globalYarn) {
add(
getGroup(globalYarn, updatesAvailable.globalYarn),
'YARN global',
globalYarn,
updatesAvailable.globalYarn,
);
}
} else {
if (local && local !== latest) {
add(getGroup(local, latest), 'local', local, pkg.wanted);
}
if (globalNpm && globalNpm !== latest) {
add(getGroup(globalNpm, latest), 'NPM global', globalNpm, pkg.wanted);
}
if (globalYarn && globalYarn !== latest) {
add(
getGroup(globalYarn, latest),
'YARN global',
globalYarn,
pkg.wanted,
);
}
if (!local && !globalNpm && !globalYarn) {
add('unknown', 'unknown', 'unknown', pkg.wanted);
}
}
return all;
}, []);
};
const displayTable = (latestVersionPackages: LatestVersionPackage[]): void => {
const updates = latestVersionPackages.filter((pkg) => pkg.updatesAvailable);
if (updates.length) {
const rows = getTableRows(updates);
const hasUpdates = rows.some((row) => row.installed !== 'unknown');
const columns = getTableColumns(rows);
const columnGap = 2;
const getGroupLines = (
groupType: TableRowGroup,
color: (str: string) => string,
title: string,
description?: string,
): string[] => {
const items = rows
.filter((row) => row.group === groupType)
.sort((a, b) => (a.name > b.name ? 1 : -1));
return !items.length
? []
: [
'',
color(`${bold(title)} ${italic(`(${description})`)}`),
...items.map((row) =>
columns
.map((column) => columnCellRenderer(column, row))
.join(' '.repeat(columnGap)),
),
];
};
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
drawBox(
[
'',
hasUpdates ? yellow('Important updates are available.') : undefined,
hasUpdates ? '' : undefined,
columns.map(columnHeaderRenderer).join(' '.repeat(columnGap)),
...getGroupLines(
'patch',
green,
'Patch',
'backwards-compatible bug fixes',
),
...getGroupLines(
'minor',
cyan,
'Minor',
'backwards-compatible features',
),
...getGroupLines(
'major',
red,
'Major',
'potentially breaking API changes',
),
...getGroupLines(
'majorVersionZero',
magenta,
'Major version zero',
'not stable, anything may change',
),
...getGroupLines('unknown', blue, 'Missing', 'not installed'),
'',
].filter((line) => line !== undefined) as string[],
);
} else {
console.log(green('🎉 Packages are up-to-date'));
}
};
const checkVersions = async (
packages: Package | Package[] | PackageJson,
skipMissing: boolean,
options: LatestVersionOptions = { useCache: true },
): Promise<void> => {
const ora = (await import('ora')).default;
const spinner = ora({ text: cyan('Checking versions...') });
spinner.start();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
let latestVersionPackages: LatestVersionPackage[] = await latestVersion(
packages,
options,
);
if (skipMissing) {
latestVersionPackages = latestVersionPackages.filter(
(pkg) => pkg.local ?? pkg.globalNpm ?? pkg.globalYarn,
);
}
spinner.stop();
displayTable(latestVersionPackages);
};
void (async () => {
let args = process.argv.slice(2);
const skipMissing = args.includes('--skip-missing');
// Remove any options from the arguments
args = args.filter((arg) => !arg.startsWith('-'));
// If argument is a package.json file
if (args.length === 1 && args[0].endsWith('package.json')) {
if (existsSync(args[0])) {
process.chdir(dirname(args[0]));
await checkVersions(
JSON.parse(readFileSync(args[0]).toString()) as PackageJson,
skipMissing,
);
} else {
console.log(cyan('No package.json file were found'));
}
}
// else..
else {
// Check if a local package.json file exists
let localPkgJson: PackageJson | undefined;
if (existsSync('package.json')) {
localPkgJson = JSON.parse(readFileSync('package.json').toString());
}
// Check given arguments
if (args.length) {
// Map arguments with any range that could be found in local package.json
args = args.map((arg) => {
if (localPkgJson?.dependencies?.[arg]) {
return `${arg}@${localPkgJson.dependencies?.[arg]}`;
}
if (localPkgJson?.devDependencies?.[arg]) {
return `${arg}@${localPkgJson.devDependencies?.[arg]}`;
}
if (localPkgJson?.peerDependencies?.[arg]) {
return `${arg}@${localPkgJson.peerDependencies?.[arg]}`;
}
return arg;
});
await checkVersions(args, skipMissing);
}
// ...else check the local package.json if any
else if (localPkgJson) {
await checkVersions(localPkgJson, skipMissing);
}
// ...else do nothing
else {
console.log(cyan('No packages were found'));
}
}
})();

View File

@@ -0,0 +1,497 @@
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import type {
Agent,
RequestOptions as HttpRequestOptions,
IncomingMessage,
} from 'http';
import type { RequestOptions as HttpsRequestOptions } from 'https';
import { homedir } from 'os';
import { dirname, join, parse, resolve as pathResolve } from 'path';
import { URL } from 'url';
import { npm, yarn } from 'global-dirs';
import registryAuthToken from 'registry-auth-token';
import getRegistryUrl from 'registry-auth-token/registry-url';
import gt from 'semver/functions/gt';
import maxSatisfying from 'semver/ranges/max-satisfying';
interface RegistryVersions {
/**
* The latest version of the package found on the registry (if found).
*/
latest?: string;
/**
* The next version of the package found on the registry (if found).
*/
next?: string;
/**
* The latest version of the package found on the registry and satisfied by the wanted tag or version range.
*/
wanted?: string;
}
interface InstalledVersions {
/**
* The current local installed version of the package (if installed).
*/
local?: string;
/**
* The current npm global installed version of the package (if installed).
*/
globalNpm?: string;
/**
* The current yarn global installed version of the package (if installed).
*/
globalYarn?: string;
}
interface LatestVersionPackage extends InstalledVersions, RegistryVersions {
/**
* The name of the package.
*/
name: string;
/**
* The tag or version range that was provided (if provided).
* @default "latest"
*/
wantedTagOrRange?: string;
/**
* Whether the local or global installed versions (if any) could be upgraded or not, based on the wanted version.
*/
updatesAvailable:
| {
local: string | false;
globalNpm: string | false;
globalYarn: string | false;
}
| false;
/**
* Any error that might have occurred during the process.
*/
error?: Error;
}
interface RequestOptions {
readonly ca?: string | Buffer | Array<string | Buffer>;
readonly rejectUnauthorized?: boolean;
readonly agent?: Agent | boolean;
readonly timeout?: number;
}
interface LatestVersionOptions {
/**
* Awaiting the api to return might take time, depending on the network, and might impact your package loading performance.
* You can use the cache mechanism to improve load performance and reduce unnecessary network requests.
* If `useCache` is not supplied, the api will always check for updates and wait for every requests to return before returning itself.
* If `useCache` is used, the api will always returned immediately, with either (for each provided packages):
* 1) a latest/next version available if a cache was found
* 2) no latest/next version available if no cache was found - in such case updates will be fetched in the background and a cache will
* be created for each provided packages and made available for the next call to the api.
* @default false
*/
readonly useCache?: boolean;
/**
* How long the cache for the provided packages should be used before being refreshed (in milliseconds).
* If `useCache` is not supplied, this option has no effect.
* If `0` is used, this will force the cache to refresh immediately:
* 1) The api will returned immediately (without any latest nor next version available for the provided packages)
* 2) New updates will be fetched in the background
* 3) The cache for each provided packages will be refreshed and made available for the next call to the api
* @default ONE_DAY
*/
readonly cacheMaxAge?: number;
/**
* A JavaScript package registry url that implements the CommonJS Package Registry specification.
* @default "Looks at any registry urls in the .npmrc file or fallback to the default npm registry instead"
* @example <caption>.npmrc</caption>
* registry = 'https://custom-registry.com/'
* @pkgscope:registry = 'https://custom-registry.com/'
*/
readonly registryUrl?: string;
/**
* Set of options to be passed down to Node.js http/https request.
* @example <caption>Behind a proxy with self-signed certificate</caption>
* { ca: [ fs.readFileSync('proxy-cert.pem') ] }
* @example <caption>Bypassing certificate validation</caption>
* { rejectUnauthorized: false }
*/
readonly requestOptions?: RequestOptions;
}
interface LatestVersion {
/**
* Get latest versions of packages from of a package json like object.
* @param {PackageJson} item - A package json like object (with dependencies, devDependencies and peerDependencies attributes).
* @example { dependencies: { 'npm': 'latest' }, devDependencies: { 'npm': '1.3.2' }, peerDependencies: { '@scope/name': '^5.0.2' } }
* @param {LatestVersionOptions} [options] - An object optionally specifying the use of the cache, the max age of the cache, the registry url and the http or https options.
* If `useCache` is not supplied, the default of `false` is used.
* If `cacheMaxAge` is not supplied, the default of `one day` is used.
* If `registryUrl` is not supplied, the default from `.npmrc` is used or a fallback to the `npm registry url` instead.
* @returns {Promise<LatestVersionPackage[]>}
*/
(
item: PackageJson,
options?: LatestVersionOptions,
): Promise<LatestVersionPackage[]>;
/**
* Get latest version of a single package.
* @param {Package} item - A single package object (represented by a string that should match the following format: `${'@' | ''}${string}@${string}`)
* @example 'npm', 'npm@1.3.2', '@scope/name@^5.0.2'
* @param {LatestVersionOptions} [options] - An object optionally specifying the use of the cache, the max age of the cache, the registry url and the http or https options.
* If `useCache` is not supplied, the default of `false` is used.
* If `cacheMaxAge` is not supplied, the default of `one day` is used.
* If `registryUrl` is not supplied, the default from `.npmrc` is used or a fallback to the npm registry url instead.
* @returns {Promise<LatestVersionPackage>}
*/
(
item: Package,
options?: LatestVersionOptions,
): Promise<LatestVersionPackage>;
/**
* Get latest versions of a collection of packages.
* @param {Package[]} items - A collection of package object (represented by a string that should match the following format: `${'@' | ''}${string}@${string}`)
* @example ['npm', 'npm@1.3.2', '@scope/name@^5.0.2']
* @param {LatestVersionOptions} [options] - An object optionally specifying the use of the cache, the max age of the cache, the registry url and the http or https options.
* If `useCache` is not supplied, the default of `false` is used.
* If `cacheMaxAge` is not supplied, the default of `one day` is used.
* If `registryUrl` is not supplied, the default from `.npmrc` is used or a fallback to the npm registry url instead.
* @returns {Promise<LatestVersionPackage[]>}
*/
(
items: Package[],
options?: LatestVersionOptions,
): Promise<LatestVersionPackage[]>; // eslint-disable-line @typescript-eslint/unified-signatures
}
type PackageRange = `${'@' | ''}${string}@${string}`;
type Package = PackageRange | string; // eslint-disable-line @typescript-eslint/no-redundant-type-constituents
type PackageJsonDependencies = Record<string, string>;
type PackageJson = Record<string, any> &
(
| {
dependencies: PackageJsonDependencies;
}
| {
devDependencies: PackageJsonDependencies;
}
| {
peerDependencies: PackageJsonDependencies;
}
);
/**
* @internal
*/
interface PackageMetadata {
name: string;
lastUpdateDate: number;
versions: string[];
distTags: Record<string, string>;
}
export const ONE_DAY = 1000 * 60 * 60 * 24; // eslint-disable-line @typescript-eslint/naming-convention
const isPackageJson = (obj: any): obj is PackageJson => {
return (
(obj as PackageJson).dependencies !== undefined ||
(obj as PackageJson).devDependencies !== undefined ||
(obj as PackageJson).peerDependencies !== undefined
);
};
const downloadMetadata = (
pkgName: string,
options?: LatestVersionOptions,
): Promise<PackageMetadata> => {
return new Promise((resolve, reject) => {
const i = pkgName.indexOf('/');
const pkgScope = i !== -1 ? pkgName.slice(0, i) : '';
const registryUrl = options?.registryUrl ?? getRegistryUrl(pkgScope);
const pkgUrl = new URL(
encodeURIComponent(pkgName).replace(/^%40/, '@'),
registryUrl,
);
let requestOptions: HttpRequestOptions | HttpsRequestOptions = {
headers: {
accept:
'application/vnd.npm.install-v1+json; q=1.0, application/json; q=0.8, */*',
},
host: pkgUrl.hostname,
path: pkgUrl.pathname,
port: pkgUrl.port,
};
const authInfo = registryAuthToken(pkgUrl.toString(), { recursive: true });
if (authInfo && requestOptions.headers) {
(requestOptions.headers as any).authorization = `${authInfo.type} ${authInfo.token}`;
}
if (options?.requestOptions) {
requestOptions = { ...requestOptions, ...options.requestOptions };
}
const { get } = require(pkgUrl.protocol === 'https:' ? 'https' : 'http');
const request = get(requestOptions, (res: IncomingMessage) => {
if (res.statusCode === 200) {
let rawData = '';
res.setEncoding('utf8');
res.on('data', (chunk: string) => (rawData += chunk));
res.once('error', (err) => {
res.removeAllListeners();
reject(`Request error (${err.message}): ${pkgUrl}`);
});
res.once('end', () => {
res.removeAllListeners();
try {
const pkgMetadata = JSON.parse(rawData);
resolve({
name: pkgName,
lastUpdateDate: Date.now(),
versions: Object.keys(pkgMetadata.versions as string[]),
distTags: pkgMetadata['dist-tags'],
});
return;
} catch (err) {
reject(err);
return;
}
});
} else {
res.removeAllListeners();
res.resume(); // consume response data to free up memory
reject(`Request error (${res.statusCode}): ${pkgUrl}`);
return;
}
});
const abort = (error: Error | string): void => {
request.destroy();
reject(error);
};
request.once('timeout', () => {
abort(`Request timed out: ${pkgUrl}`);
});
request.once('error', (err: Error) => {
abort(err);
});
request.on('close', () => {
request.removeAllListeners();
});
});
};
const getCacheDir = (name = '@badisi/latest-version'): string => {
const homeDir = homedir();
switch (process.platform) {
case 'darwin':
return join(homeDir, 'Library', 'Caches', name);
case 'win32':
return join(
process.env.LOCALAPPDATA ?? join(homeDir, 'AppData', 'Local'),
name,
'Cache',
);
default:
return join(process.env.XDG_CACHE_HOME ?? join(homeDir, '.cache'), name);
}
};
const saveMetadataToCache = (pkg: PackageMetadata): void => {
const filePath = join(getCacheDir(), `${pkg.name}.json`);
if (!existsSync(dirname(filePath))) {
mkdirSync(dirname(filePath), { recursive: true });
}
writeFileSync(filePath, JSON.stringify(pkg));
};
const getMetadataFromCache = (
pkgName: string,
options?: LatestVersionOptions,
): PackageMetadata | undefined => {
const maxAge = options?.cacheMaxAge ?? ONE_DAY;
if (maxAge !== 0) {
const pkgCacheFilePath = join(getCacheDir(), `${pkgName}.json`);
if (existsSync(pkgCacheFilePath)) {
const pkg = JSON.parse(
readFileSync(pkgCacheFilePath).toString(),
) as PackageMetadata;
if (Date.now() - pkg.lastUpdateDate < maxAge) {
return pkg;
}
}
}
return undefined; // invalidates cache
};
const getRegistryVersions = async (
pkgName: string,
tagOrRange?: string,
options?: LatestVersionOptions,
): Promise<RegistryVersions> => {
let pkgMetadata: PackageMetadata | undefined;
if (pkgName.length && options?.useCache) {
pkgMetadata = getMetadataFromCache(pkgName, options);
if (!pkgMetadata) {
pkgMetadata = await downloadMetadata(pkgName, options);
saveMetadataToCache(pkgMetadata);
}
} else if (pkgName.length) {
pkgMetadata = await downloadMetadata(pkgName, options);
}
const versions: RegistryVersions = {
latest: pkgMetadata?.distTags.latest,
next: pkgMetadata?.distTags.next,
};
if (tagOrRange && pkgMetadata?.distTags[tagOrRange]) {
versions.wanted = pkgMetadata.distTags[tagOrRange];
} else if (tagOrRange && pkgMetadata?.versions.length) {
versions.wanted =
maxSatisfying(pkgMetadata.versions, tagOrRange) ?? undefined;
}
return versions;
};
const getInstalledVersion = (
pkgName: string,
location: keyof InstalledVersions = 'local',
): string | undefined => {
try {
if (location === 'globalNpm') {
return require(join(npm.packages, pkgName, 'package.json'))
?.version as string;
} else if (location === 'globalYarn') {
// Make sure package is globally installed by Yarn
const yarnGlobalPkg = require(
pathResolve(yarn.packages, '..', 'package.json'),
);
if (!yarnGlobalPkg?.dependencies?.[pkgName]) {
return undefined;
}
return require(join(yarn.packages, pkgName, 'package.json'))
?.version as string;
} else {
/**
* Compute the local paths manually as require.resolve() and require.resolve.paths()
* cannot be trusted anymore.
* @see https://github.com/nodejs/node/issues/33460
* @see https://github.com/nodejs/loaders/issues/26
*/
const { root } = parse(process.cwd());
let path = process.cwd();
const localPaths = [join(path, 'node_modules')];
while (path !== root) {
path = dirname(path);
localPaths.push(join(path, 'node_modules'));
}
for (const localPath of localPaths) {
const pkgPath = join(localPath, pkgName, 'package.json');
if (existsSync(pkgPath)) {
return require(pkgPath)?.version as string;
}
}
}
return undefined;
} catch {
return undefined;
}
};
const getInfo = async (
pkg: Package,
options?: LatestVersionOptions,
): Promise<LatestVersionPackage> => {
const i = pkg.lastIndexOf('@');
let pkgInfo: LatestVersionPackage = {
name: i > 1 ? pkg.slice(0, i) : pkg,
wantedTagOrRange: i > 1 ? pkg.slice(i + 1) : 'latest',
updatesAvailable: false,
};
try {
pkgInfo = {
...pkgInfo,
local: getInstalledVersion(pkgInfo.name, 'local'),
globalNpm: getInstalledVersion(pkgInfo.name, 'globalNpm'),
globalYarn: getInstalledVersion(pkgInfo.name, 'globalYarn'),
...(await getRegistryVersions(
pkgInfo.name,
pkgInfo.wantedTagOrRange,
options,
)),
};
const local =
pkgInfo.local && pkgInfo.wanted
? gt(pkgInfo.wanted, pkgInfo.local)
? pkgInfo.wanted
: false
: false;
const globalNpm =
pkgInfo.globalNpm && pkgInfo.wanted
? gt(pkgInfo.wanted, pkgInfo.globalNpm)
? pkgInfo.wanted
: false
: false;
const globalYarn =
pkgInfo.globalYarn && pkgInfo.wanted
? gt(pkgInfo.wanted, pkgInfo.globalYarn)
? pkgInfo.wanted
: false
: false;
pkgInfo.updatesAvailable =
local || globalNpm || globalYarn
? { local, globalNpm, globalYarn }
: false;
} catch (err: any) {
pkgInfo.error = err?.message ?? err;
}
return pkgInfo;
};
const latestVersion: LatestVersion = async (
arg: Package | Package[] | PackageJson,
options?: LatestVersionOptions,
): Promise<any> => {
const pkgs: Package[] = [];
if (typeof arg === 'string') {
pkgs.push(arg);
} else if (Array.isArray(arg)) {
pkgs.push(...arg);
} else if (isPackageJson(arg)) {
const addDeps = (deps?: PackageJsonDependencies): void => {
if (deps) {
pkgs.push(
...Object.keys(deps).map((key: string) => `${key}@${deps[key]}`),
);
}
};
addDeps(arg.dependencies as PackageJsonDependencies | undefined);
addDeps(arg.devDependencies as PackageJsonDependencies | undefined);
addDeps(arg.peerDependencies as PackageJsonDependencies | undefined);
}
const jobs = await Promise.allSettled(
pkgs.map((pkg) => getInfo(pkg, options)),
);
const results = jobs.map(
(jobResult: PromiseSettledResult<LatestVersionPackage>) =>
(jobResult as PromiseFulfilledResult<LatestVersionPackage>).value,
);
return typeof arg === 'string' ? results[0] : results;
};
export type {
LatestVersion,
Package,
PackageRange,
PackageJson,
PackageJsonDependencies,
RegistryVersions,
LatestVersionPackage,
RequestOptions,
LatestVersionOptions,
};
export default latestVersion;

View File

@@ -0,0 +1,32 @@
import fs from 'fs-extra';
interface PluginConfig {
name: string;
bundleParams?: {
[key: string]: any;
};
detect: () => Promise<boolean>;
}
export const plugins: PluginConfig[] = [
{
name: 'sentry',
bundleParams: {
sentry: true,
sourcemap: true,
},
detect: async () => {
try {
await fs.access('ios/sentry.properties');
return true;
} catch {
try {
await fs.access('android/sentry.properties');
return true;
} catch {
return false;
}
}
},
},
];

View File

@@ -1,152 +0,0 @@
const { get, post, put, uploadFile } = require('./api');
import { question, saveToLocal } from './utils';
import { checkPlatform, getSelectedApp } from './app';
import { choosePackage } from './package';
async function showVersion(appId, offset) {
const { data, count } = await get(`/app/${appId}/version/list`);
console.log(`Offset ${offset}`);
for (const version of data) {
let packageInfo = version.packages
.slice(0, 3)
.map((v) => v.name)
.join(', ');
const count = version.packages.length;
if (count > 3) {
packageInfo += `...and ${count - 3} more`;
}
if (count === 0) {
packageInfo = `(no package)`;
} else {
packageInfo = `[${packageInfo}]`;
}
console.log(
`${version.id}) ${version.hash.slice(0, 8)} ${
version.name
} ${packageInfo}`,
);
}
return data;
}
async function listVersions(appId) {
let offset = 0;
while (true) {
await showVersion(appId, offset);
const cmd = await question('page Up/page Down/Begin/Quit(U/D/B/Q)');
switch (cmd.toLowerCase()) {
case 'u':
offset = Math.max(0, offset - 10);
break;
case 'd':
offset += 10;
break;
case 'b':
offset = 0;
break;
case 'q':
return;
}
}
}
async function chooseVersion(appId) {
let offset = 0;
while (true) {
const data = await showVersion(appId, offset);
const cmd = await question(
'Enter versionId or page Up/page Down/Begin(U/D/B)',
);
switch (cmd.toLowerCase()) {
case 'U':
offset = Math.max(0, offset - 10);
break;
case 'D':
offset += 10;
break;
case 'B':
offset = 0;
break;
default: {
const v = data.find((v) => v.id === (cmd | 0));
if (v) {
return v;
}
}
}
}
}
export const commands = {
publish: async function ({ args, options }) {
const fn = args[0];
const { name, description, metaInfo } = options;
if (!fn || !fn.endsWith('.ppk')) {
throw new Error(
'使用方法: pushy publish ppk后缀文件 --platform ios|android',
);
}
const platform = checkPlatform(
options.platform || (await question('平台(ios/android):')),
);
const { appId } = await getSelectedApp(platform);
const { hash } = await uploadFile(fn);
const { id } = await post(`/app/${appId}/version/create`, {
name: name || (await question('输入版本名称: ')) || '(未命名)',
hash,
description: description || (await question('输入版本描述:')),
metaInfo: metaInfo || (await question('输入自定义的 meta info:')),
});
// TODO local diff
saveToLocal(fn, `${appId}/ppk/${id}.ppk`);
console.log(`已成功上传新热更包id: ${id}`);
const v = await question('是否现在将此热更应用到原生包上?(Y/N)');
if (v.toLowerCase() === 'y') {
await this.update({ args: [], options: { versionId: id, platform } });
}
},
versions: async function ({ options }) {
const platform = checkPlatform(
options.platform || (await question('平台(ios/android):')),
);
const { appId } = await getSelectedApp(platform);
await listVersions(appId);
},
update: async function ({ args, options }) {
const platform = checkPlatform(
options.platform || (await question('平台(ios/android):')),
);
const { appId } = await getSelectedApp(platform);
const versionId = options.versionId || (await chooseVersion(appId)).id;
let pkgId;
let pkgVersion = options.packageVersion;
if (pkgVersion) {
pkgVersion = pkgVersion.trim();
const { data } = await get(`/app/${appId}/package/list?limit=1000`);
const pkg = data.find((d) => d.name === pkgVersion);
if (pkg) {
pkgId = pkg.id;
} else {
throw new Error(`未查询到匹配原生版本:${pkgVersion}`);
}
}
if (!pkgId) {
pkgId = options.packageId || (await choosePackage(appId)).id;
}
if (!pkgId) {
throw new Error('请提供 packageId 或 packageVersion 参数');
}
await put(`/app/${appId}/package/${pkgId}`, {
versionId,
});
console.log('操作成功');
},
};

355
src/versions.ts Normal file
View File

@@ -0,0 +1,355 @@
import { get, getAllPackages, post, put, uploadFile } from './api';
import { question, saveToLocal } from './utils';
import { t } from './utils/i18n';
import chalk from 'chalk';
import { satisfies } from 'compare-versions';
import type { Package, Platform, Version } from './types';
import { getPlatform, getSelectedApp } from './app';
import { choosePackage } from './package';
import { depVersions } from './utils/dep-versions';
import { getCommitInfo } from './utils/git';
interface VersionCommandOptions {
appId?: string;
name?: string;
description?: string;
metaInfo?: string;
platform?: Platform;
versionId?: string;
packageId?: string;
packageVersion?: string;
minPackageVersion?: string;
maxPackageVersion?: string;
packageVersionRange?: string;
rollout?: string;
dryRun?: boolean;
}
async function showVersion(appId: string, offset: number) {
const { data, count } = await get(`/app/${appId}/version/list`);
console.log(t('offset', { offset }));
for (const version of data) {
const pkgCount = version.packages?.length || 0;
let packageInfo = '';
if (pkgCount === 0) {
packageInfo = 'no package';
} else {
packageInfo = version.packages
?.slice(0, 3)
.map((pkg: Package) => pkg.name)
.join(', ');
if (pkgCount > 3) {
packageInfo += `...and ${pkgCount - 3} more`;
} else {
packageInfo = `[${packageInfo}]`;
}
}
console.log(
`${version.id}) ${version.hash.slice(0, 8)} ${
version.name
} ${packageInfo}`,
);
}
return data;
}
async function listVersions(appId: string) {
let offset = 0;
while (true) {
await showVersion(appId, offset);
const cmd = await question('page Up/page Down/Begin/Quit(U/D/B/Q)');
switch (cmd.toLowerCase()) {
case 'u':
offset = Math.max(0, offset - 10);
break;
case 'd':
offset += 10;
break;
case 'b':
offset = 0;
break;
case 'q':
return;
}
}
}
async function chooseVersion(appId: string) {
let offset = 0;
while (true) {
const data = await showVersion(appId, offset);
const cmd = await question(
'Enter versionId or page Up/page Down/Begin(U/D/B)',
);
switch (cmd.toUpperCase()) {
case 'U':
offset = Math.max(0, offset - 10);
break;
case 'D':
offset += 10;
break;
case 'B':
offset = 0;
break;
default: {
const versionId = Number.parseInt(cmd, 10);
const v = data.find(
(version: Version) => version.id === String(versionId),
);
if (v) {
return v;
}
}
}
}
}
export const bindVersionToPackages = async ({
appId,
versionId,
pkgs,
rollout,
dryRun,
}: {
appId: string;
versionId: string;
pkgs: Package[];
rollout?: number;
dryRun?: boolean;
}) => {
if (dryRun) {
console.log(chalk.yellow(t('dryRun')));
}
if (rollout !== undefined) {
const rolloutConfig: Record<string, number> = {};
for (const pkg of pkgs) {
rolloutConfig[pkg.name] = rollout;
}
if (!dryRun) {
await put(`/app/${appId}/version/${versionId}`, {
config: {
rollout: rolloutConfig,
},
});
}
console.log(
`${t('rolloutConfigSet', {
versions: pkgs.map((pkg: Package) => pkg.name).join(', '),
rollout: rollout,
})}`,
);
}
for (const pkg of pkgs) {
if (!dryRun) {
await put(`/app/${appId}/package/${pkg.id}`, {
versionId,
});
}
console.log(
`${t('versionBind', {
version: versionId,
nativeVersion: pkg.name,
id: pkg.id,
})}`,
);
}
console.log(t('operationComplete', { count: pkgs.length }));
};
export const versionCommands = {
publish: async function ({
args,
options,
}: {
args: string[];
options: VersionCommandOptions;
}) {
const fn = args[0];
const { name, description, metaInfo } = options;
if (!fn || !fn.endsWith('.ppk')) {
throw new Error(t('publishUsage'));
}
const platform = await getPlatform(options.platform);
const { appId } = await getSelectedApp(platform);
const { hash } = await uploadFile(fn);
const versionName =
name || (await question(t('versionNameQuestion'))) || t('unnamed');
const { id } = await post(`/app/${appId}/version/create`, {
name: versionName,
hash,
description:
description || (await question(t('versionDescriptionQuestion'))),
metaInfo: metaInfo || (await question(t('versionMetaInfoQuestion'))),
deps: depVersions,
commit: await getCommitInfo(),
});
// TODO local diff
saveToLocal(fn, `${appId}/ppk/${id}.ppk`);
console.log(t('packageUploadSuccess', { id }));
const {
packageId,
packageVersion,
packageVersionRange,
minPackageVersion,
maxPackageVersion,
rollout,
dryRun,
} = options;
if (
packageId ||
packageVersion ||
packageVersionRange ||
minPackageVersion ||
maxPackageVersion
) {
await this.update({
options: {
versionId: id,
platform,
packageId,
packageVersion,
packageVersionRange,
minPackageVersion,
maxPackageVersion,
rollout,
dryRun,
},
});
} else {
const q = await question(t('updateNativePackageQuestion'));
if (q.toLowerCase() === 'y') {
await this.update({ options: { versionId: id, platform } });
}
}
return versionName;
},
versions: async ({ options }: { options: VersionCommandOptions }) => {
const platform = await getPlatform(options.platform);
const { appId } = await getSelectedApp(platform);
await listVersions(appId);
},
update: async ({ options }: { options: VersionCommandOptions }) => {
const platform = await getPlatform(options.platform);
const appId = options.appId || (await getSelectedApp(platform)).appId;
let versionId = options.versionId || (await chooseVersion(appId)).id;
if (versionId === 'null') {
versionId = undefined;
}
let pkgId = options.packageId;
let pkgVersion = options.packageVersion;
let minPkgVersion = options.minPackageVersion;
let maxPkgVersion = options.maxPackageVersion;
let packageVersionRange = options.packageVersionRange;
let rollout: number | undefined = undefined;
if (options.rollout !== undefined) {
try {
rollout = Number.parseInt(options.rollout);
} catch (e) {
throw new Error(t('rolloutRangeError'));
}
if (rollout < 1 || rollout > 100) {
throw new Error(t('rolloutRangeError'));
}
}
const allPkgs = await getAllPackages(appId);
if (!allPkgs) {
throw new Error(t('noPackagesFound', { appId }));
}
let pkgsToBind: Package[] = [];
if (minPkgVersion) {
minPkgVersion = String(minPkgVersion).trim();
pkgsToBind = allPkgs.filter((pkg: Package) =>
satisfies(pkg.name, `>=${minPkgVersion}`),
);
if (pkgsToBind.length === 0) {
throw new Error(
t('nativeVersionNotFoundGte', { version: minPkgVersion }),
);
}
} else if (maxPkgVersion) {
maxPkgVersion = String(maxPkgVersion).trim();
pkgsToBind = allPkgs.filter((pkg: Package) =>
satisfies(pkg.name, `<=${maxPkgVersion}`),
);
if (pkgsToBind.length === 0) {
throw new Error(
t('nativeVersionNotFoundLte', { version: maxPkgVersion }),
);
}
} else if (pkgVersion) {
pkgVersion = pkgVersion.trim();
const pkg = allPkgs.find((pkg: Package) => pkg.name === pkgVersion);
if (pkg) {
pkgsToBind = [pkg];
} else {
throw new Error(
t('nativeVersionNotFoundMatch', { version: pkgVersion }),
);
}
} else if (packageVersionRange) {
packageVersionRange = packageVersionRange.trim();
pkgsToBind = allPkgs.filter((pkg: Package) =>
satisfies(pkg.name, packageVersionRange!),
);
if (pkgsToBind.length === 0) {
throw new Error(
t('nativeVersionNotFoundMatch', { version: packageVersionRange }),
);
}
} else {
if (!pkgId) {
pkgId = (await choosePackage(appId)).id;
}
if (!pkgId) {
throw new Error(t('packageIdRequired'));
}
const pkg = allPkgs.find(
(pkg: Package) => String(pkg.id) === String(pkgId),
);
if (pkg) {
pkgsToBind = [pkg];
} else {
throw new Error(t('nativePackageIdNotFound', { id: pkgId }));
}
}
await bindVersionToPackages({
appId,
versionId,
pkgs: pkgsToBind,
rollout,
dryRun: options.dryRun,
});
},
updateVersionInfo: async ({
options,
}: {
options: VersionCommandOptions;
}) => {
const platform = await getPlatform(options.platform);
const { appId } = await getSelectedApp(platform);
const versionId = options.versionId || (await chooseVersion(appId)).id;
const updateParams: Record<string, string> = {};
if (options.name) updateParams.name = options.name;
if (options.description) updateParams.description = options.description;
if (options.metaInfo) updateParams.metaInfo = options.metaInfo;
await put(`/app/${appId}/version/${versionId}`, updateParams);
console.log(t('operationSuccess'));
},
};

79
test-modules.js Normal file
View File

@@ -0,0 +1,79 @@
#!/usr/bin/env node
// Simple test script to verify module loading and workflows
console.log('🔍 Testing module workflows...\n');
try {
// Test app module
console.log('=== App Module ===');
const { appModule } = require('./lib/modules/app-module');
console.log(`✅ Commands: ${appModule.commands.length}`);
console.log(`✅ Workflows: ${appModule.workflows.length}`);
appModule.workflows.forEach((w) => {
console.log(` - ${w.name}: ${w.description}`);
console.log(` Steps: ${w.steps.length}`);
});
console.log();
// Test bundle module
console.log('=== Bundle Module ===');
const { bundleModule } = require('./lib/modules/bundle-module');
console.log(`✅ Commands: ${bundleModule.commands.length}`);
console.log(`✅ Workflows: ${bundleModule.workflows.length}`);
bundleModule.workflows.forEach((w) => {
console.log(` - ${w.name}: ${w.description}`);
console.log(` Steps: ${w.steps.length}`);
});
console.log();
// Test package module
console.log('=== Package Module ===');
const { packageModule } = require('./lib/modules/package-module');
console.log(`✅ Commands: ${packageModule.commands.length}`);
console.log(`✅ Workflows: ${packageModule.workflows.length}`);
packageModule.workflows.forEach((w) => {
console.log(` - ${w.name}: ${w.description}`);
console.log(` Steps: ${w.steps.length}`);
});
console.log();
// Test version module
console.log('=== Version Module ===');
const { versionModule } = require('./lib/modules/version-module');
console.log(`✅ Commands: ${versionModule.commands.length}`);
console.log(`✅ Workflows: ${versionModule.workflows.length}`);
versionModule.workflows.forEach((w) => {
console.log(` - ${w.name}: ${w.description}`);
console.log(` Steps: ${w.steps.length}`);
});
console.log();
// Test user module
console.log('=== User Module ===');
const { userModule } = require('./lib/modules/user-module');
console.log(`✅ Commands: ${userModule.commands.length}`);
console.log(`✅ Workflows: ${userModule.workflows.length}`);
userModule.workflows.forEach((w) => {
console.log(` - ${w.name}: ${w.description}`);
console.log(` Steps: ${w.steps.length}`);
});
console.log();
console.log('🎉 All modules loaded successfully with enhanced workflows!');
// Summary
const totalWorkflows = [
appModule,
bundleModule,
packageModule,
versionModule,
userModule,
].reduce((sum, module) => sum + module.workflows.length, 0);
console.log(`\n📊 Summary:`);
console.log(` Total workflows: ${totalWorkflows}`);
console.log(` Enhanced modules: 5/5`);
} catch (error) {
console.error('❌ Error testing modules:', error.message);
process.exit(1);
}

76
tsconfig.json Normal file
View File

@@ -0,0 +1,76 @@
{
"compilerOptions": {
/* Basic Options */
"target": "ESNext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
"lib": [
"ESNext",
"DOM"
] /* Specify library files to be included in the compilation. */,
"allowJs": true /* Allow javascript files to be compiled. */,
// "checkJs": true /* Report errors in .js files. */,
"jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */,
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
// "outDir": "lib" /* Redirect output structure to the directory. */,
// "rootDir": "src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
// "composite": true, /* Enable project compilation */
// "removeComments": true, /* Do not emit comments to output. */
"noEmit": true /* Do not emit outputs. */,
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
"isolatedModules": true /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */,
/* Strict Type-Checking Options */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
"strictNullChecks": true /* Enable strict null checks. */,
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
// "noUnusedLocals": true /* Report errors on unused locals. */,
// "noUnusedParameters": true /* Report errors on unused parameters. */,
"noImplicitReturns": false /* Report error when not all code paths in function return a value. */,
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
"baseUrl": "./src" /* Base directory to resolve non-absolute module names. */,
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [
// "src",
// "db",
// "config",
// "models"
// ] /* List of root folders whose combined content represents the structure of the project at runtime. */,
"typeRoots": [
"src/types"
] /* List of folders to include type definitions from. */,
// "types": [], /* Type declaration files to be included in compilation. */
"allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */,
"paths": {
"@/*": ["src/*"]
},
"resolveJsonModule": true,
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */,
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
"skipLibCheck": true,
"incremental": true
},
"exclude": ["node_modules", "lib"]
}