project-runner 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +172 -0
- package/package.json +47 -0
- package/src/analyzer/dependencies.ts +118 -0
- package/src/analyzer/index.ts +56 -0
- package/src/analyzer/package-manager.ts +119 -0
- package/src/analyzer/scripts.ts +98 -0
- package/src/cli/info.ts +87 -0
- package/src/cli/run.ts +117 -0
- package/src/cli/script.ts +59 -0
- package/src/index.ts +141 -0
- package/src/runner/executor.ts +93 -0
- package/src/utils/log.ts +86 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 liangzhenqi
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
# project-runner (pr)
|
|
2
|
+
|
|
3
|
+
零配置智能项目运行器 - 一键运行任意 Node.js 项目
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/project-runner)
|
|
6
|
+
[](https://github.com/liangzhenqi/project-runner/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
## 特性
|
|
9
|
+
|
|
10
|
+
- **零配置** - 自动检测项目类型和包管理器
|
|
11
|
+
- **智能识别** - 支持 npm / yarn / pnpm / bun
|
|
12
|
+
- **一键运行** - 自动安装依赖并启动项目
|
|
13
|
+
- **跨平台** - 支持 Windows / macOS / Linux
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# 使用 npm
|
|
19
|
+
npm install -g project-runner
|
|
20
|
+
|
|
21
|
+
# 使用 bun
|
|
22
|
+
bun install -g project-runner
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 使用
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# 一键运行项目(自动 install + 启动开发服务器)
|
|
29
|
+
pr run
|
|
30
|
+
|
|
31
|
+
# 显示详细检测过程
|
|
32
|
+
pr run -v
|
|
33
|
+
|
|
34
|
+
# 跳过依赖安装
|
|
35
|
+
pr run --no-install
|
|
36
|
+
|
|
37
|
+
# 运行测试
|
|
38
|
+
pr test
|
|
39
|
+
|
|
40
|
+
# 构建项目
|
|
41
|
+
pr build
|
|
42
|
+
|
|
43
|
+
# 生产模式启动
|
|
44
|
+
pr start
|
|
45
|
+
|
|
46
|
+
# 运行任意 package.json 脚本
|
|
47
|
+
pr lint
|
|
48
|
+
pr custom-script
|
|
49
|
+
|
|
50
|
+
# 查看项目信息
|
|
51
|
+
pr info
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 工作原理
|
|
55
|
+
|
|
56
|
+
`pr run` 执行以下完整流程:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
1. 检测项目类型 (Node.js / Python)
|
|
60
|
+
2. 检测包管理器 (npm / yarn / pnpm / bun)
|
|
61
|
+
- 优先级: packageManager 字段 > volta 字段 > lockfile
|
|
62
|
+
3. 检测依赖状态
|
|
63
|
+
- node_modules 是否存在
|
|
64
|
+
- lockfile 是否更新
|
|
65
|
+
4. 安装依赖(如需要)
|
|
66
|
+
5. 读取 scripts,确定启动命令
|
|
67
|
+
6. 启动项目
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## 包管理器检测
|
|
71
|
+
|
|
72
|
+
`pr` 通过以下方式检测包管理器(按优先级排序):
|
|
73
|
+
|
|
74
|
+
1. **packageManager 字段** (corepack)
|
|
75
|
+
```json
|
|
76
|
+
{ "packageManager": "pnpm@9.1.0" }
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
2. **volta 字段**
|
|
80
|
+
```json
|
|
81
|
+
{ "volta": { "pnpm": "9.1.0" } }
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
3. **Lockfile 检测**
|
|
85
|
+
- `bun.lockb` / `bun.lock` → bun
|
|
86
|
+
- `pnpm-lock.yaml` → pnpm
|
|
87
|
+
- `yarn.lock` → yarn
|
|
88
|
+
- `package-lock.json` → npm
|
|
89
|
+
|
|
90
|
+
## 命令
|
|
91
|
+
|
|
92
|
+
| 命令 | 说明 |
|
|
93
|
+
|------|------|
|
|
94
|
+
| `pr run` | 完整流程:检测 → install → 启动开发服务器 |
|
|
95
|
+
| `pr test` | 运行测试 |
|
|
96
|
+
| `pr build` | 构建项目 |
|
|
97
|
+
| `pr start` | 生产模式启动 |
|
|
98
|
+
| `pr info` | 显示项目分析结果 |
|
|
99
|
+
| `pr <script>` | 运行 package.json 中的任意脚本 |
|
|
100
|
+
|
|
101
|
+
## 选项
|
|
102
|
+
|
|
103
|
+
| 选项 | 说明 |
|
|
104
|
+
|------|------|
|
|
105
|
+
| `-v, --verbose` | 显示详细检测过程 |
|
|
106
|
+
| `-d, --dir <path>` | 指定项目目录(默认:当前目录)|
|
|
107
|
+
| `--no-install` | 跳过依赖安装步骤 |
|
|
108
|
+
| `-h, --help` | 显示帮助信息 |
|
|
109
|
+
| `-V, --version` | 显示版本号 |
|
|
110
|
+
|
|
111
|
+
## 示例输出
|
|
112
|
+
|
|
113
|
+
### `pr info`
|
|
114
|
+
|
|
115
|
+
```
|
|
116
|
+
pr - 项目分析结果
|
|
117
|
+
────────────────────────────────────────
|
|
118
|
+
项目名称: my-app
|
|
119
|
+
版本: 1.0.0
|
|
120
|
+
项目类型: nodejs
|
|
121
|
+
包管理器: pnpm (lockfile)
|
|
122
|
+
依赖状态: 已就绪
|
|
123
|
+
|
|
124
|
+
识别的命令:
|
|
125
|
+
pr run → pnpm dev
|
|
126
|
+
pr test → pnpm test
|
|
127
|
+
pr build → pnpm build
|
|
128
|
+
|
|
129
|
+
所有脚本:
|
|
130
|
+
dev → vite
|
|
131
|
+
test → vitest
|
|
132
|
+
build → tsc && vite build
|
|
133
|
+
lint → eslint .
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### `pr run -v`
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
[pr] 正在分析项目...
|
|
140
|
+
[pr] 项目类型: nodejs
|
|
141
|
+
[pr] 包管理器: pnpm (from lockfile)
|
|
142
|
+
[pr] 将执行脚本: dev
|
|
143
|
+
[pr] 依赖状态: 已是最新
|
|
144
|
+
> pnpm dev
|
|
145
|
+
|
|
146
|
+
> my-app@1.0.0 dev
|
|
147
|
+
> vite
|
|
148
|
+
|
|
149
|
+
VITE v5.0.0 ready in 300 ms
|
|
150
|
+
➜ Local: http://localhost:5173/
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 开发
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# 克隆项目
|
|
157
|
+
git clone https://github.com/liangzhenqi/project-runner.git
|
|
158
|
+
cd project-runner
|
|
159
|
+
|
|
160
|
+
# 安装依赖
|
|
161
|
+
bun install
|
|
162
|
+
|
|
163
|
+
# 本地运行
|
|
164
|
+
bun run dev
|
|
165
|
+
|
|
166
|
+
# 链接到全局
|
|
167
|
+
bun link
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## License
|
|
171
|
+
|
|
172
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "project-runner",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "零配置智能项目运行器 - 一键运行任意 Node.js 项目",
|
|
5
|
+
"author": "liangzhenqi",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/liangzhenqi/project-runner.git"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"cli",
|
|
13
|
+
"runner",
|
|
14
|
+
"project",
|
|
15
|
+
"npm",
|
|
16
|
+
"yarn",
|
|
17
|
+
"pnpm",
|
|
18
|
+
"bun",
|
|
19
|
+
"dev",
|
|
20
|
+
"build",
|
|
21
|
+
"test"
|
|
22
|
+
],
|
|
23
|
+
"module": "src/index.ts",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"bin": {
|
|
26
|
+
"pr": "src/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src",
|
|
30
|
+
"README.md"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"dev": "bun run src/index.ts",
|
|
34
|
+
"build": "bun build src/index.ts --outdir dist --target node",
|
|
35
|
+
"link": "bun link",
|
|
36
|
+
"prepublishOnly": "echo 'Ready to publish'"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/bun": "latest"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"typescript": "^5"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { stat } from 'fs/promises'
|
|
2
|
+
import { join } from 'path'
|
|
3
|
+
|
|
4
|
+
export interface DependencyStatus {
|
|
5
|
+
hasNodeModules: boolean
|
|
6
|
+
needsInstall: boolean
|
|
7
|
+
reason?: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// Lockfile 列表
|
|
11
|
+
const LOCKFILES = [
|
|
12
|
+
'bun.lockb',
|
|
13
|
+
'bun.lock',
|
|
14
|
+
'pnpm-lock.yaml',
|
|
15
|
+
'yarn.lock',
|
|
16
|
+
'package-lock.json',
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 检测项目的依赖状态
|
|
21
|
+
* 判断是否需要执行 install
|
|
22
|
+
*/
|
|
23
|
+
export async function checkDependencyStatus(projectDir: string): Promise<DependencyStatus> {
|
|
24
|
+
const nodeModulesPath = join(projectDir, 'node_modules')
|
|
25
|
+
|
|
26
|
+
// 1. 检查 node_modules 是否存在
|
|
27
|
+
const nodeModulesExists = await directoryExists(nodeModulesPath)
|
|
28
|
+
if (!nodeModulesExists) {
|
|
29
|
+
return {
|
|
30
|
+
hasNodeModules: false,
|
|
31
|
+
needsInstall: true,
|
|
32
|
+
reason: 'node_modules 不存在',
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// 2. 检查 lockfile 是否比 node_modules 更新
|
|
37
|
+
const lockfilePath = await findLockfile(projectDir)
|
|
38
|
+
if (lockfilePath) {
|
|
39
|
+
const lockfileMtime = await getModifiedTime(lockfilePath)
|
|
40
|
+
const nodeModulesMtime = await getModifiedTime(nodeModulesPath)
|
|
41
|
+
|
|
42
|
+
if (lockfileMtime && nodeModulesMtime && lockfileMtime > nodeModulesMtime) {
|
|
43
|
+
return {
|
|
44
|
+
hasNodeModules: true,
|
|
45
|
+
needsInstall: true,
|
|
46
|
+
reason: 'lockfile 已更新',
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// 3. 检查 package.json 是否比 node_modules 更新
|
|
52
|
+
const packageJsonPath = join(projectDir, 'package.json')
|
|
53
|
+
const packageJsonMtime = await getModifiedTime(packageJsonPath)
|
|
54
|
+
const nodeModulesMtime = await getModifiedTime(nodeModulesPath)
|
|
55
|
+
|
|
56
|
+
if (packageJsonMtime && nodeModulesMtime && packageJsonMtime > nodeModulesMtime) {
|
|
57
|
+
return {
|
|
58
|
+
hasNodeModules: true,
|
|
59
|
+
needsInstall: true,
|
|
60
|
+
reason: 'package.json 已更新',
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 依赖状态正常
|
|
65
|
+
return {
|
|
66
|
+
hasNodeModules: true,
|
|
67
|
+
needsInstall: false,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* 查找项目中的 lockfile
|
|
73
|
+
*/
|
|
74
|
+
async function findLockfile(projectDir: string): Promise<string | null> {
|
|
75
|
+
for (const lockfile of LOCKFILES) {
|
|
76
|
+
const lockfilePath = join(projectDir, lockfile)
|
|
77
|
+
if (await fileExists(lockfilePath)) {
|
|
78
|
+
return lockfilePath
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取文件/目录的修改时间
|
|
86
|
+
*/
|
|
87
|
+
async function getModifiedTime(path: string): Promise<number | null> {
|
|
88
|
+
try {
|
|
89
|
+
const stats = await stat(path)
|
|
90
|
+
return stats.mtimeMs
|
|
91
|
+
} catch {
|
|
92
|
+
return null
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* 检查目录是否存在
|
|
98
|
+
*/
|
|
99
|
+
async function directoryExists(path: string): Promise<boolean> {
|
|
100
|
+
try {
|
|
101
|
+
const stats = await stat(path)
|
|
102
|
+
return stats.isDirectory()
|
|
103
|
+
} catch {
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 检查文件是否存在
|
|
110
|
+
*/
|
|
111
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
112
|
+
try {
|
|
113
|
+
const stats = await stat(path)
|
|
114
|
+
return stats.isFile()
|
|
115
|
+
} catch {
|
|
116
|
+
return false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { detectPackageManager, type PackageManagerInfo } from './package-manager'
|
|
2
|
+
import { analyzeScripts, type ScriptsInfo } from './scripts'
|
|
3
|
+
import { checkDependencyStatus, type DependencyStatus } from './dependencies'
|
|
4
|
+
|
|
5
|
+
export interface ProjectInfo {
|
|
6
|
+
type: 'nodejs' | 'python' | 'unknown'
|
|
7
|
+
packageManager: PackageManagerInfo
|
|
8
|
+
scripts: ScriptsInfo | null
|
|
9
|
+
dependencies: DependencyStatus
|
|
10
|
+
name?: string
|
|
11
|
+
version?: string
|
|
12
|
+
description?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 分析项目,获取完整的项目信息
|
|
17
|
+
*/
|
|
18
|
+
export async function analyzeProject(projectDir: string): Promise<ProjectInfo> {
|
|
19
|
+
// 检测项目类型(目前仅支持 Node.js)
|
|
20
|
+
const packageJsonPath = `${projectDir}/package.json`
|
|
21
|
+
const hasPackageJson = await Bun.file(packageJsonPath).exists()
|
|
22
|
+
|
|
23
|
+
if (!hasPackageJson) {
|
|
24
|
+
return {
|
|
25
|
+
type: 'unknown',
|
|
26
|
+
packageManager: { name: 'npm', source: 'default' },
|
|
27
|
+
scripts: null,
|
|
28
|
+
dependencies: { hasNodeModules: false, needsInstall: false },
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 读取 package.json 基本信息
|
|
33
|
+
const packageJson = await Bun.file(packageJsonPath).json().catch(() => ({}))
|
|
34
|
+
|
|
35
|
+
// 并行执行检测
|
|
36
|
+
const [packageManager, scripts, dependencies] = await Promise.all([
|
|
37
|
+
detectPackageManager(projectDir),
|
|
38
|
+
analyzeScripts(projectDir),
|
|
39
|
+
checkDependencyStatus(projectDir),
|
|
40
|
+
])
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
type: 'nodejs',
|
|
44
|
+
packageManager,
|
|
45
|
+
scripts,
|
|
46
|
+
dependencies,
|
|
47
|
+
name: packageJson.name,
|
|
48
|
+
version: packageJson.version,
|
|
49
|
+
description: packageJson.description,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 导出子模块
|
|
54
|
+
export { detectPackageManager, type PackageManagerInfo } from './package-manager'
|
|
55
|
+
export { analyzeScripts, type ScriptsInfo, getAvailableScripts, hasScript } from './scripts'
|
|
56
|
+
export { checkDependencyStatus, type DependencyStatus } from './dependencies'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// 包管理器类型
|
|
2
|
+
export type PackageManager = 'npm' | 'yarn' | 'pnpm' | 'bun'
|
|
3
|
+
|
|
4
|
+
// 包管理器检测来源
|
|
5
|
+
export type DetectionSource = 'packageManager' | 'volta' | 'lockfile' | 'default'
|
|
6
|
+
|
|
7
|
+
export interface PackageManagerInfo {
|
|
8
|
+
name: PackageManager
|
|
9
|
+
version?: string
|
|
10
|
+
source: DetectionSource
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Lockfile 检测映射
|
|
14
|
+
const LOCKFILE_MAP: Record<string, PackageManager> = {
|
|
15
|
+
'bun.lockb': 'bun',
|
|
16
|
+
'bun.lock': 'bun',
|
|
17
|
+
'pnpm-lock.yaml': 'pnpm',
|
|
18
|
+
'yarn.lock': 'yarn',
|
|
19
|
+
'package-lock.json': 'npm',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 检测项目使用的包管理器
|
|
24
|
+
* 优先级: packageManager 字段 > volta 字段 > lockfile
|
|
25
|
+
*/
|
|
26
|
+
export async function detectPackageManager(projectDir: string): Promise<PackageManagerInfo> {
|
|
27
|
+
const packageJsonPath = `${projectDir}/package.json`
|
|
28
|
+
|
|
29
|
+
// 读取 package.json
|
|
30
|
+
const packageJson = await readPackageJson(packageJsonPath)
|
|
31
|
+
if (!packageJson) {
|
|
32
|
+
return { name: 'npm', source: 'default' }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 1. 检查 packageManager 字段 (corepack)
|
|
36
|
+
if (packageJson.packageManager) {
|
|
37
|
+
const match = packageJson.packageManager.match(/^(npm|yarn|pnpm|bun)@(.+)$/)
|
|
38
|
+
if (match) {
|
|
39
|
+
return {
|
|
40
|
+
name: match[1] as PackageManager,
|
|
41
|
+
version: match[2],
|
|
42
|
+
source: 'packageManager',
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. 检查 volta 字段
|
|
48
|
+
if (packageJson.volta) {
|
|
49
|
+
for (const pm of ['pnpm', 'yarn', 'npm'] as PackageManager[]) {
|
|
50
|
+
if (packageJson.volta[pm]) {
|
|
51
|
+
return {
|
|
52
|
+
name: pm,
|
|
53
|
+
version: packageJson.volta[pm],
|
|
54
|
+
source: 'volta',
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 3. 检测 lockfile
|
|
61
|
+
for (const [lockfile, pm] of Object.entries(LOCKFILE_MAP)) {
|
|
62
|
+
const exists = await Bun.file(`${projectDir}/${lockfile}`).exists()
|
|
63
|
+
if (exists) {
|
|
64
|
+
return { name: pm, source: 'lockfile' }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 默认使用 npm
|
|
69
|
+
return { name: 'npm', source: 'default' }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 获取包管理器的运行命令
|
|
74
|
+
*/
|
|
75
|
+
export function getRunCommand(pm: PackageManager, script: string): string[] {
|
|
76
|
+
switch (pm) {
|
|
77
|
+
case 'bun':
|
|
78
|
+
return ['bun', 'run', script]
|
|
79
|
+
case 'pnpm':
|
|
80
|
+
return ['pnpm', script]
|
|
81
|
+
case 'yarn':
|
|
82
|
+
return ['yarn', script]
|
|
83
|
+
case 'npm':
|
|
84
|
+
default:
|
|
85
|
+
return ['npm', 'run', script]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取包管理器的安装命令
|
|
91
|
+
*/
|
|
92
|
+
export function getInstallCommand(pm: PackageManager): string[] {
|
|
93
|
+
switch (pm) {
|
|
94
|
+
case 'bun':
|
|
95
|
+
return ['bun', 'install']
|
|
96
|
+
case 'pnpm':
|
|
97
|
+
return ['pnpm', 'install']
|
|
98
|
+
case 'yarn':
|
|
99
|
+
return ['yarn', 'install']
|
|
100
|
+
case 'npm':
|
|
101
|
+
default:
|
|
102
|
+
return ['npm', 'install']
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 读取 package.json
|
|
108
|
+
*/
|
|
109
|
+
async function readPackageJson(path: string): Promise<any | null> {
|
|
110
|
+
try {
|
|
111
|
+
const file = Bun.file(path)
|
|
112
|
+
if (await file.exists()) {
|
|
113
|
+
return await file.json()
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// 忽略错误
|
|
117
|
+
}
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
export interface ScriptsInfo {
|
|
2
|
+
scripts: Record<string, string>
|
|
3
|
+
// 识别出的主要命令(dev, test, build, start)
|
|
4
|
+
detected: {
|
|
5
|
+
dev?: string // 开发启动脚本名
|
|
6
|
+
test?: string // 测试脚本名
|
|
7
|
+
build?: string // 构建脚本名
|
|
8
|
+
start?: string // 生产启动脚本名
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// 开发命令的常见名称(按优先级排序)
|
|
13
|
+
const DEV_PATTERNS = ['dev', 'serve', 'start:dev', 'develop', 'watch']
|
|
14
|
+
|
|
15
|
+
// 测试命令的常见名称
|
|
16
|
+
const TEST_PATTERNS = ['test', 'test:unit', 'test:all', 'spec']
|
|
17
|
+
|
|
18
|
+
// 构建命令的常见名称
|
|
19
|
+
const BUILD_PATTERNS = ['build', 'compile', 'bundle', 'dist']
|
|
20
|
+
|
|
21
|
+
// 生产启动命令的常见名称
|
|
22
|
+
const START_PATTERNS = ['start', 'serve', 'preview', 'production']
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* 读取并分析 package.json 的 scripts 字段
|
|
26
|
+
*/
|
|
27
|
+
export async function analyzeScripts(projectDir: string): Promise<ScriptsInfo | null> {
|
|
28
|
+
const packageJsonPath = `${projectDir}/package.json`
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const file = Bun.file(packageJsonPath)
|
|
32
|
+
if (!await file.exists()) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const packageJson = await file.json()
|
|
37
|
+
const scripts = packageJson.scripts || {}
|
|
38
|
+
|
|
39
|
+
// 智能识别主要命令
|
|
40
|
+
const detected = {
|
|
41
|
+
dev: findMatchingScript(scripts, DEV_PATTERNS),
|
|
42
|
+
test: findMatchingScript(scripts, TEST_PATTERNS),
|
|
43
|
+
build: findMatchingScript(scripts, BUILD_PATTERNS),
|
|
44
|
+
start: findMatchingScript(scripts, START_PATTERNS),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return { scripts, detected }
|
|
48
|
+
} catch {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 从 scripts 中找到匹配的脚本名
|
|
55
|
+
* @param scripts package.json 的 scripts 对象
|
|
56
|
+
* @param patterns 要匹配的模式列表(按优先级排序)
|
|
57
|
+
*/
|
|
58
|
+
function findMatchingScript(scripts: Record<string, string>, patterns: string[]): string | undefined {
|
|
59
|
+
const scriptNames = Object.keys(scripts)
|
|
60
|
+
|
|
61
|
+
// 按优先级检查每个模式
|
|
62
|
+
for (const pattern of patterns) {
|
|
63
|
+
// 精确匹配
|
|
64
|
+
if (scripts[pattern]) {
|
|
65
|
+
return pattern
|
|
66
|
+
}
|
|
67
|
+
// 模糊匹配(包含模式的脚本名)
|
|
68
|
+
const fuzzyMatch = scriptNames.find(name =>
|
|
69
|
+
name.toLowerCase().includes(pattern.toLowerCase())
|
|
70
|
+
)
|
|
71
|
+
if (fuzzyMatch) {
|
|
72
|
+
return fuzzyMatch
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return undefined
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 获取所有可用的脚本列表
|
|
81
|
+
*/
|
|
82
|
+
export function getAvailableScripts(scriptsInfo: ScriptsInfo): string[] {
|
|
83
|
+
return Object.keys(scriptsInfo.scripts)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 检查脚本是否存在
|
|
88
|
+
*/
|
|
89
|
+
export function hasScript(scriptsInfo: ScriptsInfo, scriptName: string): boolean {
|
|
90
|
+
return scriptName in scriptsInfo.scripts
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 获取脚本的实际命令内容
|
|
95
|
+
*/
|
|
96
|
+
export function getScriptCommand(scriptsInfo: ScriptsInfo, scriptName: string): string | undefined {
|
|
97
|
+
return scriptsInfo.scripts[scriptName]
|
|
98
|
+
}
|
package/src/cli/info.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { analyzeProject } from '../analyzer'
|
|
2
|
+
import { colors } from '../utils/log'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* pr info 命令
|
|
6
|
+
* 显示项目分析结果
|
|
7
|
+
*/
|
|
8
|
+
export async function infoCommand(projectDir: string) {
|
|
9
|
+
const project = await analyzeProject(projectDir)
|
|
10
|
+
|
|
11
|
+
if (project.type === 'unknown') {
|
|
12
|
+
console.log(`${colors.red}✗${colors.reset} 未检测到项目类型`)
|
|
13
|
+
console.log(' 请确保当前目录包含 package.json 或其他项目配置文件')
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 项目基本信息
|
|
18
|
+
console.log()
|
|
19
|
+
console.log(`${colors.cyan}${colors.bold}pr - 项目分析结果${colors.reset}`)
|
|
20
|
+
console.log('─'.repeat(40))
|
|
21
|
+
|
|
22
|
+
if (project.name) {
|
|
23
|
+
console.log(`${colors.bold}项目名称:${colors.reset} ${project.name}`)
|
|
24
|
+
}
|
|
25
|
+
if (project.version) {
|
|
26
|
+
console.log(`${colors.bold}版本:${colors.reset} ${project.version}`)
|
|
27
|
+
}
|
|
28
|
+
if (project.description) {
|
|
29
|
+
console.log(`${colors.bold}描述:${colors.reset} ${project.description}`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.log(`${colors.bold}项目类型:${colors.reset} ${project.type}`)
|
|
33
|
+
|
|
34
|
+
// 包管理器信息
|
|
35
|
+
const pm = project.packageManager
|
|
36
|
+
let pmInfo = pm.name
|
|
37
|
+
if (pm.version) {
|
|
38
|
+
pmInfo += `@${pm.version}`
|
|
39
|
+
}
|
|
40
|
+
pmInfo += ` ${colors.dim}(${pm.source})${colors.reset}`
|
|
41
|
+
console.log(`${colors.bold}包管理器:${colors.reset} ${pmInfo}`)
|
|
42
|
+
|
|
43
|
+
// 依赖状态
|
|
44
|
+
const deps = project.dependencies
|
|
45
|
+
const depsStatus = deps.needsInstall
|
|
46
|
+
? `${colors.yellow}需要安装${colors.reset} (${deps.reason})`
|
|
47
|
+
: `${colors.green}已就绪${colors.reset}`
|
|
48
|
+
console.log(`${colors.bold}依赖状态:${colors.reset} ${depsStatus}`)
|
|
49
|
+
|
|
50
|
+
console.log()
|
|
51
|
+
|
|
52
|
+
// Scripts 信息
|
|
53
|
+
if (project.scripts) {
|
|
54
|
+
const { scripts, detected } = project.scripts
|
|
55
|
+
|
|
56
|
+
// 显示识别的主要命令
|
|
57
|
+
console.log(`${colors.bold}识别的命令:${colors.reset}`)
|
|
58
|
+
if (detected.dev) {
|
|
59
|
+
console.log(` ${colors.green}pr run${colors.reset} → ${pm.name} ${detected.dev}`)
|
|
60
|
+
}
|
|
61
|
+
if (detected.test) {
|
|
62
|
+
console.log(` ${colors.green}pr test${colors.reset} → ${pm.name} ${detected.test}`)
|
|
63
|
+
}
|
|
64
|
+
if (detected.build) {
|
|
65
|
+
console.log(` ${colors.green}pr build${colors.reset} → ${pm.name} ${detected.build}`)
|
|
66
|
+
}
|
|
67
|
+
if (detected.start) {
|
|
68
|
+
console.log(` ${colors.green}pr start${colors.reset} → ${pm.name} ${detected.start}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
console.log()
|
|
72
|
+
|
|
73
|
+
// 显示所有可用脚本
|
|
74
|
+
const allScripts = Object.keys(scripts)
|
|
75
|
+
if (allScripts.length > 0) {
|
|
76
|
+
console.log(`${colors.bold}所有脚本:${colors.reset}`)
|
|
77
|
+
for (const name of allScripts) {
|
|
78
|
+
const cmd = scripts[name]
|
|
79
|
+
// 截断过长的命令
|
|
80
|
+
const displayCmd = cmd.length > 40 ? cmd.slice(0, 40) + '...' : cmd
|
|
81
|
+
console.log(` ${colors.cyan}${name}${colors.reset} ${colors.dim}→ ${displayCmd}${colors.reset}`)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
console.log()
|
|
87
|
+
}
|
package/src/cli/run.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { analyzeProject, type ProjectInfo } from '../analyzer'
|
|
2
|
+
import { getRunCommand, getInstallCommand } from '../analyzer/package-manager'
|
|
3
|
+
import { execute } from '../runner/executor'
|
|
4
|
+
import { log, error, warn, success, info, newline } from '../utils/log'
|
|
5
|
+
|
|
6
|
+
type ScriptType = 'dev' | 'test' | 'build' | 'start'
|
|
7
|
+
|
|
8
|
+
interface RunOptions {
|
|
9
|
+
noInstall?: boolean
|
|
10
|
+
scriptType?: ScriptType
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* qy run 命令
|
|
15
|
+
* 完整流程:检测 → install → 启动
|
|
16
|
+
*/
|
|
17
|
+
export async function runCommand(projectDir: string, options: RunOptions = {}) {
|
|
18
|
+
const { noInstall = false, scriptType = 'dev' } = options
|
|
19
|
+
|
|
20
|
+
// 1. 分析项目
|
|
21
|
+
log('正在分析项目...')
|
|
22
|
+
const project = await analyzeProject(projectDir)
|
|
23
|
+
|
|
24
|
+
if (project.type === 'unknown') {
|
|
25
|
+
error('未检测到项目类型。请确保当前目录包含 package.json')
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// 2. 输出检测结果
|
|
30
|
+
log(`项目类型: ${project.type}`)
|
|
31
|
+
log(`包管理器: ${project.packageManager.name} (from ${project.packageManager.source})`)
|
|
32
|
+
|
|
33
|
+
if (!project.scripts) {
|
|
34
|
+
error('无法读取 package.json 的 scripts')
|
|
35
|
+
process.exit(1)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 3. 确定要执行的脚本
|
|
39
|
+
const scriptName = findScript(project, scriptType)
|
|
40
|
+
|
|
41
|
+
if (!scriptName) {
|
|
42
|
+
error(`未找到 ${scriptType} 相关的脚本`)
|
|
43
|
+
showAvailableScripts(project)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log(`将执行脚本: ${scriptName}`)
|
|
48
|
+
|
|
49
|
+
// 4. 检查依赖状态,决定是否需要 install
|
|
50
|
+
if (!noInstall && project.dependencies.needsInstall) {
|
|
51
|
+
log(`依赖状态: ${project.dependencies.reason || '需要安装'}`)
|
|
52
|
+
newline()
|
|
53
|
+
|
|
54
|
+
// 执行 install
|
|
55
|
+
const installCmd = getInstallCommand(project.packageManager.name)
|
|
56
|
+
const installExitCode = await execute(installCmd, { cwd: projectDir })
|
|
57
|
+
|
|
58
|
+
if (installExitCode !== 0) {
|
|
59
|
+
error('依赖安装失败')
|
|
60
|
+
process.exit(installExitCode)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
success('依赖安装完成')
|
|
64
|
+
newline()
|
|
65
|
+
} else if (!noInstall) {
|
|
66
|
+
log('依赖状态: 已是最新')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 5. 执行脚本
|
|
70
|
+
const runCmd = getRunCommand(project.packageManager.name, scriptName)
|
|
71
|
+
const exitCode = await execute(runCmd, { cwd: projectDir })
|
|
72
|
+
|
|
73
|
+
if (exitCode !== 0) {
|
|
74
|
+
process.exit(exitCode)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 根据脚本类型找到对应的脚本名
|
|
80
|
+
*/
|
|
81
|
+
function findScript(project: ProjectInfo, scriptType: ScriptType): string | undefined {
|
|
82
|
+
const scripts = project.scripts
|
|
83
|
+
if (!scripts) return undefined
|
|
84
|
+
|
|
85
|
+
// 使用智能检测的结果
|
|
86
|
+
const detected = scripts.detected[scriptType]
|
|
87
|
+
if (detected) {
|
|
88
|
+
return detected
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 如果没有检测到,尝试精确匹配
|
|
92
|
+
if (scripts.scripts[scriptType]) {
|
|
93
|
+
return scriptType
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 显示可用的脚本列表
|
|
101
|
+
*/
|
|
102
|
+
function showAvailableScripts(project: ProjectInfo) {
|
|
103
|
+
if (!project.scripts) return
|
|
104
|
+
|
|
105
|
+
const scriptNames = Object.keys(project.scripts.scripts)
|
|
106
|
+
if (scriptNames.length === 0) {
|
|
107
|
+
warn('package.json 中没有定义任何 scripts')
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
info('可用的脚本:')
|
|
112
|
+
for (const name of scriptNames) {
|
|
113
|
+
console.log(` - ${name}`)
|
|
114
|
+
}
|
|
115
|
+
console.log()
|
|
116
|
+
info('使用 qy <script> 运行任意脚本')
|
|
117
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { analyzeProject } from '../analyzer'
|
|
2
|
+
import { getRunCommand } from '../analyzer/package-manager'
|
|
3
|
+
import { execute } from '../runner/executor'
|
|
4
|
+
import { error, log, info } from '../utils/log'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 运行任意 package.json 脚本
|
|
8
|
+
*/
|
|
9
|
+
export async function scriptCommand(projectDir: string, scriptName: string) {
|
|
10
|
+
// 分析项目
|
|
11
|
+
const project = await analyzeProject(projectDir)
|
|
12
|
+
|
|
13
|
+
if (project.type === 'unknown') {
|
|
14
|
+
error('未检测到项目类型。请确保当前目录包含 package.json')
|
|
15
|
+
process.exit(1)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (!project.scripts) {
|
|
19
|
+
error('无法读取 package.json 的 scripts')
|
|
20
|
+
process.exit(1)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// 检查脚本是否存在
|
|
24
|
+
const scripts = project.scripts.scripts
|
|
25
|
+
if (!(scriptName in scripts)) {
|
|
26
|
+
error(`脚本 "${scriptName}" 不存在`)
|
|
27
|
+
console.log()
|
|
28
|
+
showAvailableScripts(scripts)
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
log(`包管理器: ${project.packageManager.name}`)
|
|
33
|
+
log(`执行脚本: ${scriptName}`)
|
|
34
|
+
|
|
35
|
+
// 执行脚本
|
|
36
|
+
const runCmd = getRunCommand(project.packageManager.name, scriptName)
|
|
37
|
+
const exitCode = await execute(runCmd, { cwd: projectDir })
|
|
38
|
+
|
|
39
|
+
if (exitCode !== 0) {
|
|
40
|
+
process.exit(exitCode)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 显示可用脚本列表
|
|
46
|
+
*/
|
|
47
|
+
function showAvailableScripts(scripts: Record<string, string>) {
|
|
48
|
+
const scriptNames = Object.keys(scripts)
|
|
49
|
+
|
|
50
|
+
if (scriptNames.length === 0) {
|
|
51
|
+
info('package.json 中没有定义任何脚本')
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
info('可用的脚本:')
|
|
56
|
+
for (const name of scriptNames) {
|
|
57
|
+
console.log(` - ${name}`)
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { resolve } from 'path'
|
|
4
|
+
import { setVerbose, error, info } from './utils/log'
|
|
5
|
+
import { setupSignalHandlers } from './runner/executor'
|
|
6
|
+
import { runCommand } from './cli/run'
|
|
7
|
+
import { infoCommand } from './cli/info'
|
|
8
|
+
import { scriptCommand } from './cli/script'
|
|
9
|
+
|
|
10
|
+
const VERSION = '0.1.0'
|
|
11
|
+
|
|
12
|
+
interface CliOptions {
|
|
13
|
+
verbose: boolean
|
|
14
|
+
dir: string
|
|
15
|
+
noInstall: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseArgs(args: string[]): { command: string; options: CliOptions; args: string[] } {
|
|
19
|
+
const options: CliOptions = {
|
|
20
|
+
verbose: false,
|
|
21
|
+
dir: process.cwd(),
|
|
22
|
+
noInstall: false,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let command = ''
|
|
26
|
+
const remainingArgs: string[] = []
|
|
27
|
+
let i = 0
|
|
28
|
+
|
|
29
|
+
while (i < args.length) {
|
|
30
|
+
const arg = args[i]
|
|
31
|
+
|
|
32
|
+
if (arg === '-v' || arg === '--verbose') {
|
|
33
|
+
options.verbose = true
|
|
34
|
+
} else if (arg === '-d' || arg === '--dir') {
|
|
35
|
+
options.dir = resolve(args[++i] || '.')
|
|
36
|
+
} else if (arg === '--no-install') {
|
|
37
|
+
options.noInstall = true
|
|
38
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
39
|
+
command = 'help'
|
|
40
|
+
} else if (arg === '-V' || arg === '--version') {
|
|
41
|
+
command = 'version'
|
|
42
|
+
} else if (!arg.startsWith('-')) {
|
|
43
|
+
if (!command) {
|
|
44
|
+
command = arg
|
|
45
|
+
} else {
|
|
46
|
+
remainingArgs.push(arg)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
i++
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return { command, options, args: remainingArgs }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function showHelp() {
|
|
57
|
+
console.log(`
|
|
58
|
+
${'\x1b[36m'}pr${'\x1b[0m'} v${VERSION} - 零配置智能项目运行器 (project-runner)
|
|
59
|
+
|
|
60
|
+
${'\x1b[1m'}用法:${'\x1b[0m'} pr <command> [options]
|
|
61
|
+
|
|
62
|
+
${'\x1b[1m'}命令:${'\x1b[0m'}
|
|
63
|
+
run 完整流程:检测 → install → 启动开发服务器
|
|
64
|
+
test 运行测试
|
|
65
|
+
build 构建项目
|
|
66
|
+
start 生产模式启动
|
|
67
|
+
info 显示项目分析结果
|
|
68
|
+
<script> 运行 package.json 中的任意脚本
|
|
69
|
+
|
|
70
|
+
${'\x1b[1m'}选项:${'\x1b[0m'}
|
|
71
|
+
-v, --verbose 显示详细检测过程
|
|
72
|
+
-d, --dir <path> 指定项目目录 (默认: 当前目录)
|
|
73
|
+
--no-install 跳过依赖安装步骤
|
|
74
|
+
-h, --help 显示帮助信息
|
|
75
|
+
-V, --version 显示版本号
|
|
76
|
+
|
|
77
|
+
${'\x1b[1m'}示例:${'\x1b[0m'}
|
|
78
|
+
pr run 一键启动项目
|
|
79
|
+
pr run -v 显示详细检测过程
|
|
80
|
+
pr test 运行测试
|
|
81
|
+
pr lint 运行 lint 脚本
|
|
82
|
+
pr info 查看项目信息
|
|
83
|
+
`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function showVersion() {
|
|
87
|
+
console.log(`pr v${VERSION}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function main() {
|
|
91
|
+
// 设置信号处理
|
|
92
|
+
setupSignalHandlers()
|
|
93
|
+
|
|
94
|
+
// 解析命令行参数
|
|
95
|
+
const { command, options, args } = parseArgs(process.argv.slice(2))
|
|
96
|
+
|
|
97
|
+
// 设置 verbose 模式
|
|
98
|
+
setVerbose(options.verbose)
|
|
99
|
+
|
|
100
|
+
// 处理命令
|
|
101
|
+
switch (command) {
|
|
102
|
+
case '':
|
|
103
|
+
case 'help':
|
|
104
|
+
showHelp()
|
|
105
|
+
break
|
|
106
|
+
|
|
107
|
+
case 'version':
|
|
108
|
+
showVersion()
|
|
109
|
+
break
|
|
110
|
+
|
|
111
|
+
case 'run':
|
|
112
|
+
await runCommand(options.dir, { noInstall: options.noInstall, scriptType: 'dev' })
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
case 'test':
|
|
116
|
+
await runCommand(options.dir, { noInstall: true, scriptType: 'test' })
|
|
117
|
+
break
|
|
118
|
+
|
|
119
|
+
case 'build':
|
|
120
|
+
await runCommand(options.dir, { noInstall: true, scriptType: 'build' })
|
|
121
|
+
break
|
|
122
|
+
|
|
123
|
+
case 'start':
|
|
124
|
+
await runCommand(options.dir, { noInstall: true, scriptType: 'start' })
|
|
125
|
+
break
|
|
126
|
+
|
|
127
|
+
case 'info':
|
|
128
|
+
await infoCommand(options.dir)
|
|
129
|
+
break
|
|
130
|
+
|
|
131
|
+
default:
|
|
132
|
+
// 尝试运行自定义脚本
|
|
133
|
+
await scriptCommand(options.dir, command)
|
|
134
|
+
break
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main().catch((err) => {
|
|
139
|
+
error(err.message)
|
|
140
|
+
process.exit(1)
|
|
141
|
+
})
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { spawn, type Subprocess } from 'bun'
|
|
2
|
+
import { execLog } from '../utils/log'
|
|
3
|
+
|
|
4
|
+
let currentProcess: Subprocess | null = null
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 执行命令
|
|
8
|
+
* @param cmd 命令数组 ['npm', 'run', 'dev']
|
|
9
|
+
* @param options 选项
|
|
10
|
+
*/
|
|
11
|
+
export async function execute(
|
|
12
|
+
cmd: string[],
|
|
13
|
+
options: {
|
|
14
|
+
cwd?: string
|
|
15
|
+
env?: Record<string, string>
|
|
16
|
+
silent?: boolean
|
|
17
|
+
} = {}
|
|
18
|
+
): Promise<number> {
|
|
19
|
+
const { cwd = process.cwd(), env, silent = false } = options
|
|
20
|
+
|
|
21
|
+
if (!silent) {
|
|
22
|
+
execLog(cmd.join(' '))
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// 启动子进程
|
|
26
|
+
currentProcess = spawn({
|
|
27
|
+
cmd,
|
|
28
|
+
cwd,
|
|
29
|
+
env: { ...process.env, ...env },
|
|
30
|
+
stdio: ['inherit', 'inherit', 'inherit'],
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// 等待进程结束
|
|
34
|
+
const exitCode = await currentProcess.exited
|
|
35
|
+
currentProcess = null
|
|
36
|
+
|
|
37
|
+
return exitCode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 执行命令并返回输出
|
|
42
|
+
*/
|
|
43
|
+
export async function executeCapture(
|
|
44
|
+
cmd: string[],
|
|
45
|
+
options: {
|
|
46
|
+
cwd?: string
|
|
47
|
+
env?: Record<string, string>
|
|
48
|
+
} = {}
|
|
49
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
50
|
+
const { cwd = process.cwd(), env } = options
|
|
51
|
+
|
|
52
|
+
const proc = spawn({
|
|
53
|
+
cmd,
|
|
54
|
+
cwd,
|
|
55
|
+
env: { ...process.env, ...env },
|
|
56
|
+
stdout: 'pipe',
|
|
57
|
+
stderr: 'pipe',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const stdout = await new Response(proc.stdout).text()
|
|
61
|
+
const stderr = await new Response(proc.stderr).text()
|
|
62
|
+
const exitCode = await proc.exited
|
|
63
|
+
|
|
64
|
+
return { stdout, stderr, exitCode }
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 设置信号处理器
|
|
69
|
+
*/
|
|
70
|
+
export function setupSignalHandlers() {
|
|
71
|
+
// Ctrl+C 处理
|
|
72
|
+
process.on('SIGINT', () => {
|
|
73
|
+
if (currentProcess) {
|
|
74
|
+
currentProcess.kill()
|
|
75
|
+
}
|
|
76
|
+
process.exit(0)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
// SIGTERM 处理
|
|
80
|
+
process.on('SIGTERM', () => {
|
|
81
|
+
if (currentProcess) {
|
|
82
|
+
currentProcess.kill()
|
|
83
|
+
}
|
|
84
|
+
process.exit(0)
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* 获取当前运行的进程
|
|
90
|
+
*/
|
|
91
|
+
export function getCurrentProcess(): Subprocess | null {
|
|
92
|
+
return currentProcess
|
|
93
|
+
}
|
package/src/utils/log.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// ANSI 颜色代码
|
|
2
|
+
const colors = {
|
|
3
|
+
reset: '\x1b[0m',
|
|
4
|
+
bold: '\x1b[1m',
|
|
5
|
+
dim: '\x1b[2m',
|
|
6
|
+
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
green: '\x1b[32m',
|
|
9
|
+
yellow: '\x1b[33m',
|
|
10
|
+
blue: '\x1b[34m',
|
|
11
|
+
magenta: '\x1b[35m',
|
|
12
|
+
cyan: '\x1b[36m',
|
|
13
|
+
white: '\x1b[37m',
|
|
14
|
+
gray: '\x1b[90m',
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 全局 verbose 状态
|
|
18
|
+
let isVerbose = false
|
|
19
|
+
|
|
20
|
+
export function setVerbose(verbose: boolean) {
|
|
21
|
+
isVerbose = verbose
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getVerbose(): boolean {
|
|
25
|
+
return isVerbose
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 日志函数(仅在 verbose 模式下输出)
|
|
30
|
+
*/
|
|
31
|
+
export function log(message: string) {
|
|
32
|
+
if (isVerbose) {
|
|
33
|
+
console.log(`${colors.cyan}[pr]${colors.reset} ${message}`)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 信息日志(始终输出)
|
|
39
|
+
*/
|
|
40
|
+
export function info(message: string) {
|
|
41
|
+
console.log(`${colors.cyan}[pr]${colors.reset} ${message}`)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* 成功日志
|
|
46
|
+
*/
|
|
47
|
+
export function success(message: string) {
|
|
48
|
+
console.log(`${colors.green}✓${colors.reset} ${message}`)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* 警告日志
|
|
53
|
+
*/
|
|
54
|
+
export function warn(message: string) {
|
|
55
|
+
console.log(`${colors.yellow}⚠${colors.reset} ${message}`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 错误日志
|
|
60
|
+
*/
|
|
61
|
+
export function error(message: string) {
|
|
62
|
+
console.error(`${colors.red}✗${colors.reset} ${message}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 执行命令的日志
|
|
67
|
+
*/
|
|
68
|
+
export function execLog(command: string) {
|
|
69
|
+
console.log(`${colors.dim}>${colors.reset} ${colors.bold}${command}${colors.reset}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 输出空行
|
|
74
|
+
*/
|
|
75
|
+
export function newline() {
|
|
76
|
+
console.log()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 输出带颜色的文本
|
|
81
|
+
*/
|
|
82
|
+
export function colorize(text: string, color: keyof typeof colors): string {
|
|
83
|
+
return `${colors[color]}${text}${colors.reset}`
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { colors }
|